Skip to content

Commit 2770fbb

Browse files
mcfvsoligon-fvs
authored andcommitted
Add validation against schema for JSON
1 parent 81ee7f9 commit 2770fbb

File tree

2 files changed

+238
-0
lines changed

2 files changed

+238
-0
lines changed

nodeutil/json_rdr.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"io"
88
"strings"
99

10+
"github.com/freeconf/yang/fc"
1011
"github.com/freeconf/yang/node"
1112
"github.com/freeconf/yang/val"
1213

@@ -44,6 +45,105 @@ func (self *JSONRdr) Node() (node.Node, error) {
4445
return JsonContainerReader(self.values), nil
4546
}
4647

48+
// This function inspects the JSON payload against YANG schema,
49+
// looking for missing or unexpected keys.
50+
func (self *JSONRdr) Validate(selection *node.Selection) error {
51+
n, err := self.Node()
52+
if err != nil {
53+
return err
54+
}
55+
56+
err = validate(self.values, selection.Split(n))
57+
if err != nil {
58+
return fmt.Errorf("%w: %s", fc.BadRequestError, err)
59+
}
60+
61+
return nil
62+
}
63+
64+
func validate(value interface{}, selection *node.Selection) error {
65+
m := selection.Meta()
66+
if meta.IsContainer(m) {
67+
containerValue, ok := value.(map[string]interface{})
68+
if !ok {
69+
return fmt.Errorf("expected a container, got: %+v", value)
70+
}
71+
return validateContainer(containerValue, selection)
72+
} else if meta.IsList(m) {
73+
listValue, ok := value.([]interface{})
74+
if !ok {
75+
return fmt.Errorf("expected a list, got: %+v", value)
76+
}
77+
return validateList(listValue, selection)
78+
} else {
79+
// TODO: no validation for leaves, choices, and the rest
80+
}
81+
82+
return nil
83+
}
84+
85+
func validateList(elements []interface{}, selection *node.Selection) error {
86+
elementSelection, err := selection.First()
87+
88+
for i := range elements {
89+
for err != nil {
90+
return fmt.Errorf("error selecting list element %d: %s", i, err)
91+
}
92+
path := elementSelection.Selection.Path.String()
93+
94+
element, ok := elements[i].(map[string]interface{})
95+
if !ok {
96+
return fmt.Errorf("expected a map for path %s, got %+v", path, elements[i])
97+
}
98+
if err := validateChildNodes(element, elementSelection.Selection); err != nil {
99+
return err
100+
}
101+
elementSelection, err = elementSelection.Next()
102+
}
103+
104+
return nil
105+
}
106+
107+
func validateChildNodes(values map[string]interface{}, selection *node.Selection) error {
108+
m := selection.Meta()
109+
path := selection.Path.String()
110+
metaChildren := map[string]struct{}{}
111+
hd := m.(meta.HasDataDefinitions)
112+
113+
for _, child := range hd.DataDefinitions() {
114+
id := child.Ident()
115+
metaChildren[id] = struct{}{}
116+
details := child.(meta.HasDetails)
117+
118+
value, ok := values[id]
119+
if !ok {
120+
if details.Mandatory() {
121+
return fmt.Errorf("missing mandatory node: %s/%s", path, id)
122+
}
123+
} else {
124+
newSelection, err := selection.Find(id)
125+
if err != nil {
126+
return fmt.Errorf("error finding: %s/%s: %s", path, id, err)
127+
}
128+
if err := validate(value, newSelection); err != nil {
129+
return err
130+
}
131+
}
132+
}
133+
134+
for k := range values {
135+
if _, ok := metaChildren[k]; !ok {
136+
return fmt.Errorf("unexpected node: %s/%s", path, k)
137+
}
138+
}
139+
140+
return nil
141+
}
142+
143+
func validateContainer(values map[string]interface{}, selection *node.Selection) error {
144+
return validateChildNodes(values, selection)
145+
}
146+
47147
func (self *JSONRdr) decode() (map[string]interface{}, error) {
48148
if self.values == nil {
49149
d := json.NewDecoder(self.In)

nodeutil/json_rdr_test.go

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package nodeutil
22

33
import (
4+
"strings"
45
"testing"
56

67
"github.com/freeconf/yang/fc"
@@ -277,3 +278,140 @@ func TestReadQualifiedJsonIdentRef(t *testing.T) {
277278
fc.AssertEqual(t, "derived-type", actual["type"].(val.IdentRef).Label)
278279
fc.AssertEqual(t, "local-type", actual["type2"].(val.IdentRef).Label)
279280
}
281+
282+
func TestValidateHappyCase(t *testing.T) {
283+
mstring := `
284+
module x {
285+
revision 0;
286+
container c {
287+
leaf l1 {
288+
type int32;
289+
mandatory true;
290+
}
291+
leaf l2 {
292+
type int32;
293+
}
294+
}
295+
list l {
296+
leaf l1 {
297+
type int32;
298+
mandatory true;
299+
}
300+
leaf l2 {
301+
type int32;
302+
}
303+
}
304+
}`
305+
payload := `
306+
{
307+
"c": {
308+
"l1": 1
309+
},
310+
"l": [
311+
{"l1": 1, "l2": 2},
312+
{"l1": 1}
313+
]
314+
}`
315+
module, err := parser.LoadModuleFromString(nil, mstring)
316+
if err != nil {
317+
t.Fatal(err)
318+
}
319+
320+
t.Log(payload)
321+
322+
n, err := ReadJSON(payload)
323+
fc.AssertEqual(t, nil, err)
324+
selection := node.NewBrowser(module, n).Root()
325+
326+
reader := JSONRdr{In: strings.NewReader(payload)}
327+
if err := reader.Validate(selection); err != nil {
328+
t.Errorf("validation should pass, but got error: %s", err)
329+
}
330+
}
331+
332+
func TestValidateForInvalidPayloads(t *testing.T) {
333+
tests := []struct{
334+
mstring string
335+
payload string
336+
msg string
337+
expectedErr string
338+
}{
339+
{
340+
mstring: `
341+
module x {
342+
revision 0;
343+
leaf l {
344+
type int32;
345+
mandatory true;
346+
}
347+
}`,
348+
payload: `{}`,
349+
msg: "should fail when mandatory container child is missing",
350+
expectedErr: "missing mandatory node: x/l",
351+
},
352+
{
353+
mstring: `
354+
module x {
355+
revision 0;
356+
container c {
357+
leaf l1 {
358+
type string;
359+
}
360+
}
361+
}`,
362+
payload: `{"c": {"l1": 1, "extra": 3}}`,
363+
msg: "should fail on unexpected container child",
364+
expectedErr: "unexpected node: x/c/extra",
365+
},
366+
{
367+
mstring: `
368+
module x {
369+
revision 0;
370+
list l {
371+
leaf l1 {
372+
type string;
373+
mandatory true;
374+
}
375+
}
376+
}`,
377+
payload: `{"l": [{}]}`,
378+
msg: "should fail when mandatory list child is missing",
379+
expectedErr: "missing mandatory node: x/l/l1",
380+
},
381+
{
382+
mstring: `
383+
module x {
384+
revision 0;
385+
list l {
386+
leaf l1 {
387+
type string;
388+
mandatory true;
389+
}
390+
}
391+
}`,
392+
payload: `{"l": [{"l1": "foo", "extra": 1}]}`,
393+
msg: "should fail on unexpected list child",
394+
expectedErr: "unexpected node: x/l/extra",
395+
},
396+
}
397+
398+
for _, test := range tests {
399+
module, err := parser.LoadModuleFromString(nil, test.mstring)
400+
if err != nil {
401+
t.Fatal(err)
402+
}
403+
404+
t.Log(test.payload)
405+
406+
n, err := ReadJSON(test.payload)
407+
fc.AssertEqual(t, nil, err)
408+
selection := node.NewBrowser(module, n).Root()
409+
410+
reader := JSONRdr{In: strings.NewReader(test.payload)}
411+
if err := reader.Validate(selection); err != nil {
412+
fc.AssertEqual(t, strings.Contains(err.Error(), test.expectedErr), true, "unexpected error")
413+
} else {
414+
t.Errorf(test.msg)
415+
}
416+
}
417+
}

0 commit comments

Comments
 (0)