Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add validation against schema for JSON #117

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}
}
}