Skip to content

Commit

Permalink
Trigger hook for inner nodes (#4)
Browse files Browse the repository at this point in the history
* trigger hook for inner nodes

* ensure correct line and column numbers in hooks

* add support for Go 1.20 Unwrap() []error

* update documentation
  • Loading branch information
lovromazgon authored Mar 15, 2023
1 parent db0a37c commit 676c6c2
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 30 deletions.
53 changes: 44 additions & 9 deletions decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ type parser struct {
textless bool

hook DecoderHook
hookPath []string
hookPath []*Node
}

func newParser(b []byte) *parser {
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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.
Expand All @@ -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 != "!" {
Expand Down Expand Up @@ -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
}

Expand All @@ -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 {
Expand Down
50 changes: 32 additions & 18 deletions decode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}
}

Expand Down
17 changes: 14 additions & 3 deletions yaml.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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 (
Expand Down

0 comments on commit 676c6c2

Please sign in to comment.