Skip to content

Commit

Permalink
Add validation against schema for JSON
Browse files Browse the repository at this point in the history
  • Loading branch information
mcfvs committed Aug 1, 2024
1 parent 4939405 commit 2cd4d7a
Show file tree
Hide file tree
Showing 2 changed files with 238 additions and 0 deletions.
100 changes: 100 additions & 0 deletions nodeutil/json_rdr.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"io"
"strings"

"github.com/freeconf/yang/fc"
"github.com/freeconf/yang/node"
"github.com/freeconf/yang/val"

Expand Down Expand Up @@ -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)
Expand Down
138 changes: 138 additions & 0 deletions nodeutil/json_rdr_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package nodeutil

import (
"strings"
"testing"

"github.com/freeconf/yang/fc"
Expand Down Expand Up @@ -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)
}
}
}

0 comments on commit 2cd4d7a

Please sign in to comment.