diff --git a/node/edit.go b/node/edit.go index a323071..6958a48 100644 --- a/node/edit.go +++ b/node/edit.go @@ -16,6 +16,7 @@ const ( editUpsert editStrategy = iota + 1 editInsert editUpdate + editReplace ) type editor struct { @@ -51,7 +52,6 @@ func (e editor) enter(from *Selection, to *Selection, new bool, strategy editStr } else { ml := newContainerMetaList(from) m := ml.nextMeta() - //fmt.Printf("Begin %s\n", meta.SchemaPath(from.Meta())) for m != nil { var err error if meta.IsLeaf(m) { @@ -64,7 +64,6 @@ func (e editor) enter(from *Selection, to *Selection, new bool, strategy editStr } m = ml.nextMeta() } - //fmt.Printf("Ended %s\n", meta.SchemaPath(from.Meta())) } return nil } @@ -84,19 +83,24 @@ func (e editor) leaf(from *Selection, to *Selection, m meta.Leafable, new bool, return err } + r.Selection = to + r.From = from if hnd.Val != nil { // If there is a different choice selected, need to clear it // first if in upsert mode - if strategy == editUpsert { + if strategy == editUpsert || strategy == editReplace { if err := e.clearOnDifferentChoiceCase(to, m); err != nil { return err } } - r.Selection = to - r.From = from if err := to.set(&r, &hnd); err != nil { return err } + } else if strategy == editReplace { + r.Clear = true + if err := to.set(&r, &ValueHandle{}); err != nil { + return err + } } return nil } @@ -147,6 +151,24 @@ func (e editor) clearChoiceCase(sel *Selection, c *meta.ChoiceCase) error { func (e editor) node(from *Selection, to *Selection, m meta.HasDataDefinitions, new bool, strategy editStrategy) error { var newChild bool var err error + // this ensures that even on panic we release any selections created in this func and it's loop + var fromChild *Selection + var toChild *Selection + releaseFromChild := func() { + if fromChild != nil { + fromChild.Release() + fromChild = nil + } + } + defer releaseFromChild() + releaseToChild := func() { + if toChild != nil { + toChild.Release() + toChild = nil + } + } + defer releaseToChild() + fromRequest := ChildRequest{ Request: Request{ Selection: from, @@ -155,11 +177,13 @@ func (e editor) node(from *Selection, to *Selection, m meta.HasDataDefinitions, }, Meta: m, } - fromChild, err := from.selekt(&fromRequest) - if fromChild == nil || err != nil { + fromChild, err = from.selekt(&fromRequest) + if err != nil { return err } - defer fromChild.Release() + if fromChild == nil && strategy != editReplace { + return nil + } toRequest := ChildRequest{ Request: Request{ Selection: to, @@ -171,23 +195,34 @@ func (e editor) node(from *Selection, to *Selection, m meta.HasDataDefinitions, toRequest.New = false toRequest.Selection = to - toChild, err := to.selekt(&toRequest) + toChild, err = to.selekt(&toRequest) if err != nil { return err } - if toChild != nil { - defer toChild.Release() - } - toRequest.New = true + switch strategy { case editInsert: if toChild != nil { return fmt.Errorf("%w. item '%s' found in '%s'. ", fc.ConflictError, m.Ident(), fromRequest.Path) } + toRequest.New = true + if toChild, err = to.selekt(&toRequest); err != nil { + return err + } + newChild = true + case editReplace: + if toChild != nil { + if err := toChild.delete(); err != nil { + return err + } + } + if fromChild == nil { + return nil + } + toRequest.New = true if toChild, err = to.selekt(&toRequest); err != nil { return err } - defer toChild.Release() newChild = true case editUpsert: @@ -198,12 +233,10 @@ func (e editor) node(from *Selection, to *Selection, m meta.HasDataDefinitions, } if toChild == nil { + toRequest.New = true if toChild, err = to.selekt(&toRequest); err != nil { return err } - if toChild != nil { - defer toChild.Release() - } newChild = true } case editUpdate: @@ -225,7 +258,6 @@ func (e editor) node(from *Selection, to *Selection, m meta.HasDataDefinitions, } func (e editor) list(from *Selection, to *Selection, m *meta.List, new bool, strategy editStrategy) error { - // this ensures that even on panic we release any selections created in this func and it's loop var fromChild *Selection var toChild *Selection @@ -257,7 +289,7 @@ func (e editor) list(from *Selection, to *Selection, m *meta.List, new bool, str var key []val.Value var err error fromChild, key, err = from.selectVisibleListItem(fromRequest) - if err != nil || fromChild == nil { + if err != nil { return err } p.Key = key @@ -310,6 +342,11 @@ func (e editor) list(from *Selection, to *Selection, m *meta.List, new bool, str return err } newItem = true + case editReplace: + if toChild, _, _, err = to.selectListItem(&toRequest); err != nil { + return err + } + newItem = true default: return strategyNotImplemented } @@ -330,5 +367,57 @@ func (e editor) list(from *Selection, to *Selection, m *meta.List, new bool, str return err } } + if strategy == editReplace { + // Iterate through "to" list, look if such element is in "from" list, delete otherwise + p = *to.Path + toRequest = ListRequest{ + Request: Request{ + Selection: to, + Path: &p, + Base: e.basePath, + }, + First: true, + Meta: m, + } + fromRequest = &ListRequest{ + Request: Request{ + Selection: from, + Path: &p, + Base: e.basePath, + }, + First: true, + Meta: m, + } + for { + if toChild, _, key, err = to.selectListItem(&toRequest); err != nil { + return err + } + if toChild == nil { + break + } + + fromRequest.First = true + fromRequest.SetRow(toRequest.Row64) + fromRequest.Selection = to + fromRequest.From = toChild + fromRequest.Key = key + p.Key = key + if fromChild, _, _, err = from.selectListItem(fromRequest); err != nil { + return err + } + if fromChild == nil { + fmt.Printf("|||||delete fromChild=%v\n", toChild.Path) + if err = toChild.delete(); err != nil { + return err + } + toChild = nil + } + + releaseToChild() + releaseFromChild() + + toRequest.IncrementRow() + } + } return nil } diff --git a/node/selection.go b/node/selection.go index df0d37d..b0da89a 100644 --- a/node/selection.go +++ b/node/selection.go @@ -385,6 +385,11 @@ func (sel *Selection) Delete() (err error) { } }() + err = sel.delete() + return +} + +func (sel *Selection) delete() (err error) { if sel.InsideList { r := ListRequest{ Request: Request{ @@ -468,12 +473,10 @@ func (sel *Selection) UpdateInto(toNode Node) error { return e.edit(sel, sel.Split(toNode), editUpdate) } +// Replace current tree with given one func (sel *Selection) ReplaceFrom(fromNode Node) error { - parent := sel.parent - if err := sel.Delete(); err != nil { - return err - } - return parent.InsertFrom(fromNode) + e := editor{basePath: sel.Path} + return e.edit(sel.Split(fromNode), sel, editReplace) } // Copy given node into current node. There must be matching containers of list diff --git a/node/selection_test.go b/node/selection_test.go index f993881..4d5c192 100644 --- a/node/selection_test.go +++ b/node/selection_test.go @@ -84,7 +84,7 @@ func TestReplaceFrom(t *testing.T) { sel, err := root.Find("bird=robin/species") fc.RequireEqual(t, nil, err) n, _ := nodeutil.ReadJSON(` - {"species":{"name":"dragon"}} + {"class":"dragon"} `) err = sel.ReplaceFrom(n) fc.AssertEqual(t, nil, err) @@ -92,17 +92,27 @@ func TestReplaceFrom(t *testing.T) { fc.RequireEqual(t, nil, err) actual, err := js.JSON(sel) fc.AssertEqual(t, nil, err) - fc.AssertEqual(t, `{"name":"robin","wingspan":10,"species":{"name":"dragon"}}`, actual) + fc.AssertEqual(t, `{"name":"robin","wingspan":10,"species":{"class":"dragon"}}`, actual) // list item sel, err = root.Find("bird=robin") fc.RequireEqual(t, nil, err) n, _ = nodeutil.ReadJSON(` - {"bird":[{"name": "robin", "wingspan":11}]} + {"name": "robin", "wingspan":11} `) sel.ReplaceFrom(n) fc.AssertEqual(t, nil, err) actual, err = js.JSON(root) fc.AssertEqual(t, nil, err) fc.AssertEqual(t, `{"bird":[{"name":"blue jay","wingspan":0},{"name":"robin","wingspan":11}]}`, actual) + + // whole list + n, _ = nodeutil.ReadJSON(` + {"bird":[{"name":"blue jay"},{"name":"malak","species":{"class":"jedi"}}]} + `) + root.ReplaceFrom(n) + fc.AssertEqual(t, nil, err) + actual, err = js.JSON(root) + fc.AssertEqual(t, nil, err) + fc.AssertEqual(t, `{"bird":[{"name":"blue jay","wingspan":0},{"name":"malak","wingspan":0,"species":{"class":"jedi"}}]}`, actual) } diff --git a/nodeutil/json_rdr.go b/nodeutil/json_rdr.go index d413f1f..41a8804 100644 --- a/nodeutil/json_rdr.go +++ b/nodeutil/json_rdr.go @@ -7,6 +7,7 @@ import ( "io" "strings" + "github.com/freeconf/yang/fc" "github.com/freeconf/yang/node" "github.com/freeconf/yang/val" @@ -44,6 +45,105 @@ func (self *JSONRdr) Node() (node.Node, error) { return JsonContainerReader(self.values), nil } +// This function inspects the JSON payload against YANG schema, +// looking for missing or unexpected keys. +func (self *JSONRdr) Validate(selection *node.Selection) error { + n, err := self.Node() + if err != nil { + return err + } + + err = validate(self.values, selection.Split(n)) + if err != nil { + return fmt.Errorf("%w: %s", fc.BadRequestError, err) + } + + return nil +} + +func validate(value interface{}, selection *node.Selection) error { + m := selection.Meta() + if meta.IsContainer(m) { + containerValue, ok := value.(map[string]interface{}) + if !ok { + return fmt.Errorf("expected a container, got: %+v", value) + } + return validateContainer(containerValue, selection) + } else if meta.IsList(m) { + listValue, ok := value.([]interface{}) + if !ok { + return fmt.Errorf("expected a list, got: %+v", value) + } + return validateList(listValue, selection) + } else { + // TODO: no validation for leaves, choices, and the rest + } + + return nil +} + +func validateList(elements []interface{}, selection *node.Selection) error { + elementSelection, err := selection.First() + + for i := range elements { + for err != nil { + return fmt.Errorf("error selecting list element %d: %s", i, err) + } + path := elementSelection.Selection.Path.String() + + element, ok := elements[i].(map[string]interface{}) + if !ok { + return fmt.Errorf("expected a map for path %s, got %+v", path, elements[i]) + } + if err := validateChildNodes(element, elementSelection.Selection); err != nil { + return err + } + elementSelection, err = elementSelection.Next() + } + + return nil +} + +func validateChildNodes(values map[string]interface{}, selection *node.Selection) error { + m := selection.Meta() + path := selection.Path.String() + metaChildren := map[string]struct{}{} + hd := m.(meta.HasDataDefinitions) + + for _, child := range hd.DataDefinitions() { + id := child.Ident() + metaChildren[id] = struct{}{} + details := child.(meta.HasDetails) + + value, ok := values[id] + if !ok { + if details.Mandatory() { + return fmt.Errorf("missing mandatory node: %s/%s", path, id) + } + } else { + newSelection, err := selection.Find(id) + if err != nil { + return fmt.Errorf("error finding: %s/%s: %s", path, id, err) + } + if err := validate(value, newSelection); err != nil { + return err + } + } + } + + for k := range values { + if _, ok := metaChildren[k]; !ok { + return fmt.Errorf("unexpected node: %s/%s", path, k) + } + } + + return nil +} + +func validateContainer(values map[string]interface{}, selection *node.Selection) error { + return validateChildNodes(values, selection) +} + func (self *JSONRdr) decode() (map[string]interface{}, error) { if self.values == nil { d := json.NewDecoder(self.In) diff --git a/nodeutil/json_rdr_test.go b/nodeutil/json_rdr_test.go index 3bf2e62..84eeb42 100644 --- a/nodeutil/json_rdr_test.go +++ b/nodeutil/json_rdr_test.go @@ -1,6 +1,7 @@ package nodeutil import ( + "strings" "testing" "github.com/freeconf/yang/fc" @@ -277,3 +278,140 @@ func TestReadQualifiedJsonIdentRef(t *testing.T) { fc.AssertEqual(t, "derived-type", actual["type"].(val.IdentRef).Label) fc.AssertEqual(t, "local-type", actual["type2"].(val.IdentRef).Label) } + +func TestValidateHappyCase(t *testing.T) { + mstring := ` + module x { + revision 0; + container c { + leaf l1 { + type int32; + mandatory true; + } + leaf l2 { + type int32; + } + } + list l { + leaf l1 { + type int32; + mandatory true; + } + leaf l2 { + type int32; + } + } + }` + payload := ` + { + "c": { + "l1": 1 + }, + "l": [ + {"l1": 1, "l2": 2}, + {"l1": 1} + ] + }` + module, err := parser.LoadModuleFromString(nil, mstring) + if err != nil { + t.Fatal(err) + } + + t.Log(payload) + + n, err := ReadJSON(payload) + fc.AssertEqual(t, nil, err) + selection := node.NewBrowser(module, n).Root() + + reader := JSONRdr{In: strings.NewReader(payload)} + if err := reader.Validate(selection); err != nil { + t.Errorf("validation should pass, but got error: %s", err) + } +} + +func TestValidateForInvalidPayloads(t *testing.T) { + tests := []struct{ + mstring string + payload string + msg string + expectedErr string + }{ + { + mstring: ` + module x { + revision 0; + leaf l { + type int32; + mandatory true; + } + }`, + payload: `{}`, + msg: "should fail when mandatory container child is missing", + expectedErr: "missing mandatory node: x/l", + }, + { + mstring: ` + module x { + revision 0; + container c { + leaf l1 { + type string; + } + } + }`, + payload: `{"c": {"l1": 1, "extra": 3}}`, + msg: "should fail on unexpected container child", + expectedErr: "unexpected node: x/c/extra", + }, + { + mstring: ` + module x { + revision 0; + list l { + leaf l1 { + type string; + mandatory true; + } + } + }`, + payload: `{"l": [{}]}`, + msg: "should fail when mandatory list child is missing", + expectedErr: "missing mandatory node: x/l/l1", + }, + { + mstring: ` + module x { + revision 0; + list l { + leaf l1 { + type string; + mandatory true; + } + } + }`, + payload: `{"l": [{"l1": "foo", "extra": 1}]}`, + msg: "should fail on unexpected list child", + expectedErr: "unexpected node: x/l/extra", + }, + } + + for _, test := range tests { + module, err := parser.LoadModuleFromString(nil, test.mstring) + if err != nil { + t.Fatal(err) + } + + t.Log(test.payload) + + n, err := ReadJSON(test.payload) + fc.AssertEqual(t, nil, err) + selection := node.NewBrowser(module, n).Root() + + reader := JSONRdr{In: strings.NewReader(test.payload)} + if err := reader.Validate(selection); err != nil { + fc.AssertEqual(t, strings.Contains(err.Error(), test.expectedErr), true, "unexpected error") + } else { + t.Errorf(test.msg) + } + } +} diff --git a/nodeutil/reflect.go b/nodeutil/reflect.go index c0ab22c..844c509 100644 --- a/nodeutil/reflect.go +++ b/nodeutil/reflect.go @@ -487,7 +487,11 @@ func (self Reflect) strukt(ptrVal reflect.Value) node.Node { }, OnField: func(r node.FieldRequest, hnd *node.ValueHandle) (err error) { if r.Write { - err = self.WriteField(r.Meta, ptrVal, hnd.Val) + if r.Clear { + elemVal.SetZero() + } else { + err = self.WriteField(r.Meta, ptrVal, hnd.Val) + } } else { hnd.Val, err = self.ReadField(r.Meta, ptrVal) }