diff --git a/decode.go b/decode.go index 59e59d65..d039d54c 100644 --- a/decode.go +++ b/decode.go @@ -38,7 +38,7 @@ type parser struct { textless bool hook DecoderHook - hookPath []string + hookPath []*Node } func newParser(b []byte) *parser { @@ -72,7 +72,7 @@ func (p *parser) init() { } p.anchors = make(map[string]*Node) p.expect(yaml_STREAM_START_EVENT) - p.hookPath = make([]string, 0) + p.hookPath = make([]*Node, 0) p.doneInit = true } @@ -157,13 +157,10 @@ func (p *parser) parse(triggerHook bool) *Node { case yaml_ALIAS_EVENT: n = p.alias() case yaml_MAPPING_START_EVENT: - triggerHook = false // maps are not leaf nodes, skip hook n = p.mapping() case yaml_SEQUENCE_START_EVENT: - triggerHook = false // sequences are not leaf nodes, skip hook n = p.sequence() case yaml_DOCUMENT_START_EVENT: - triggerHook = false // documents are not leaf nodes, skip hook n = p.document() case yaml_STREAM_END_EVENT: // Happens when attempting to decode an empty buffer. @@ -175,11 +172,49 @@ func (p *parser) parse(triggerHook bool) *Node { } if triggerHook && p.hook != nil { - p.hook(p.hookPath, n) + p.triggerHook(n) } return n } +func (p *parser) triggerHook(node *Node) { + path := make([]string, 0, len(p.hookPath)) + for _, n := range p.hookPath { + if n.Kind == SequenceNode { + // sequence nodes have no value, we use the index of the element instead + path = append(path, strconv.Itoa(len(n.Content))) + } else { + path = append(path, n.Value) + } + } + + // we want to give the hook its own pointer + hookNode := *node + if len(p.hookPath) > 0 { + if parent := p.hookPath[len(p.hookPath)-1]; parent.Kind != SequenceNode { + // Go-yaml represents keys in a map as scalar nodes, which are not + // related to the map values in any way. In the decoder hook we + // simplify this by exposing the path to the key node and supplying + // the corresponding value node that contains the data for that key. + // The line and column in the value node point to where the value + // starts, which can cause some confusion, as it does not match the + // line and column of the key referenced in the path. That's why we + // adjust the line and column to match the key node's line and + // column. After the hook returns we copy the node back into the + // original node so we need to adjust the line and column again to + // preserve the original values. + oldLine, oldColumn := node.Line, node.Column // store original line and column + hookNode.Line, hookNode.Column = parent.Line, parent.Column // adjust line and column + defer func() { + node.Line, node.Column = oldLine, oldColumn // restore original line and column + }() + } + } + + p.hook(path, &hookNode) + *node = hookNode // overwrite node in case the hook changed it +} + func (p *parser) node(kind Kind, defaultTag, tag, value string) *Node { var style Style if tag != "" && tag != "!" { @@ -271,14 +306,14 @@ func (p *parser) sequence() *Node { } p.anchor(n, p.event.anchor) p.expect(yaml_SEQUENCE_START_EVENT) + p.hookPath = append(p.hookPath, n) for p.peek() != yaml_SEQUENCE_END_EVENT { - p.hookPath = append(p.hookPath, strconv.Itoa(len(n.Content))) p.parseChild(n, true) - p.hookPath = p.hookPath[:len(p.hookPath)-1] } n.LineComment = string(p.event.line_comment) n.FootComment = string(p.event.foot_comment) p.expect(yaml_SEQUENCE_END_EVENT) + p.hookPath = p.hookPath[:len(p.hookPath)-1] return n } @@ -293,7 +328,7 @@ func (p *parser) mapping() *Node { p.expect(yaml_MAPPING_START_EVENT) for p.peek() != yaml_MAPPING_END_EVENT { k := p.parseChild(n, false) - p.hookPath = append(p.hookPath, k.Value) + p.hookPath = append(p.hookPath, k) if block && k.FootComment != "" { // Must be a foot comment for the prior value when being dedented. if len(n.Content) > 2 { diff --git a/decode_test.go b/decode_test.go index 680837ac..cae4b806 100644 --- a/decode_test.go +++ b/decode_test.go @@ -1725,13 +1725,12 @@ a: f: g: h: - i: - - 1 - - 2 + i: [1,2] j: {"k": true, "l": false} x: - - z: yes + - z: + zz: yes y: no - z: up w: down @@ -1749,24 +1748,39 @@ x: err := dec.Decode(map[string]interface{}{}) c.Assert(err, IsNil) - want := map[string]string{ - "a.b": "foo", - "a.c.d.e": "bar", - "f.g.h.i.0": "1", - "f.g.h.i.1": "2", - "j.k": "true", - "j.l": "false", - "x.0.z": "yes", - "x.0.y": "no", - "x.1.z": "up", - "x.1.w": "down", + // values represent a slice that contains the expected line, column and value + want := map[string][]interface{}{ + "a": {2, 1, ""}, + "a.b": {3, 3, "foo"}, + "a.c": {4, 3, ""}, + "a.c.d": {5, 5, ""}, + "a.c.d.e": {6, 7, "bar"}, + "f": {7, 1, ""}, + "f.g": {8, 3, ""}, + "f.g.h": {9, 5, ""}, + "f.g.h.i": {10, 7, ""}, + "f.g.h.i.0": {10, 11, "1"}, + "f.g.h.i.1": {10, 13, "2"}, + "j": {11, 1, ""}, + "j.k": {11, 5, "true"}, + "j.l": {11, 16, "false"}, + "x": {13, 1, ""}, + "x.0": {14, 5, ""}, + "x.0.z": {14, 5, ""}, + "x.0.z.zz": {15, 7, "yes"}, + "x.0.y": {16, 5, "no"}, + "x.1": {17, 5, ""}, + "x.1.z": {17, 5, "up"}, + "x.1.w": {18, 5, "down"}, } c.Assert(len(got), Equals, len(want)) - for k,v := range want { + for k, v := range want { comment := Commentf("key: %s", k) node, ok := got[k] - c.Assert(ok, Equals, true, comment) - c.Assert(node.Value, Equals, v, comment) + c.Check(ok, Equals, true, comment) + c.Check(node.Line, Equals, v[0], comment) + c.Check(node.Column, Equals, v[1], comment) + c.Check(node.Value, Equals, v[2], comment) } } diff --git a/yaml.go b/yaml.go index 68bccc7f..d7fe6b93 100644 --- a/yaml.go +++ b/yaml.go @@ -127,11 +127,11 @@ func (dec *Decoder) KnownFields(enable bool) { dec.knownFields = enable } -// DecoderHook is called for every leaf node and receives the node itself and -// the path leading up to the node. +// DecoderHook is called for every node and receives the node itself and the +// path leading up to the node. type DecoderHook func(path []string, node *Node) -// WithHook adds a DecoderHook that gets called for every leaf node. +// WithHook adds a DecoderHook that gets called for every node. func (dec *Decoder) WithHook(h DecoderHook) { dec.parser.withHook(h) } @@ -495,6 +495,17 @@ func (e *TypeError) Error() string { return b.String() } +func (e *TypeError) Unwrap() []error { + if len(e.Errors) == 0 { + return nil + } + errs := make([]error, len(e.Errors)) + for i,err := range e.Errors { + errs[i] = err + } + return errs +} + type Kind uint32 const (