From 2cd4d7acf6f301827b23dd4f97dada141717675a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Char=C4=99za?= Date: Thu, 1 Aug 2024 08:54:08 +0200 Subject: [PATCH] Add validation against schema for JSON --- nodeutil/json_rdr.go | 100 +++++++++++++++++++++++++++ nodeutil/json_rdr_test.go | 138 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 238 insertions(+) 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) + } + } +}