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

Support validating ordered maps #853

Merged
merged 1 commit into from
Jun 1, 2023
Merged
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
188 changes: 94 additions & 94 deletions integration_tests/schemaops/ctestschema/ctestschema.go

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions integration_tests/schemaops/yang/ctestschema.yang
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ module ctestschema {
list ordered-list {
key "key";
ordered-by user;
// These numbers are for testing the validation logic in ytypes/.
min-elements 0;
max-elements 5;

leaf key { type leafref { path "../config/key"; } }

Expand Down
6 changes: 4 additions & 2 deletions internal/yreflect/reflect_orderedmap.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ type goOrderedList interface {
// IsYANGOrderedList is a marker method that indicates that the struct
// implements the goOrderedList interface.
IsYANGOrderedList()
// Len returns the size of the ordered list.
Len() int
}

// AppendIntoOrderedMap appends a populated value into the ordered map.
Expand All @@ -50,8 +52,8 @@ func AppendIntoOrderedMap(orderedMap goOrderedList, value interface{}) error {

// RangeOrderedMap calls a visitor function over each key-value pair in order.
//
// If the visit function returns false, the for loop breaks.
// An erorr is returned if the ordered map is not well-formed.
// The for loop break when either the visit function returns false or an error
// is encountered due to the ordered map not being well-formed.
func RangeOrderedMap(orderedMap goOrderedList, visit func(k reflect.Value, v reflect.Value) bool) error {
omv := reflect.ValueOf(orderedMap)
// First get the ordered keys, and then index into each of the values associated with it.
Expand Down
8 changes: 8 additions & 0 deletions ygot/render_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2049,6 +2049,14 @@ func (o *MultiKeyOrderedMap) Values() []*OrderedMultikeyedList {
return values
}

// Len returns a size of OrderedMultikeyedList_OrderedMap
func (o *MultiKeyOrderedMap) Len() int {
if o == nil {
return 0
}
return len(o.keys)
}

// AppendNew creates and appends a new OrderedMultikeyedList, returning the
// newly-initialized v. It returns an error if the v already exists.
func (o *MultiKeyOrderedMap) AppendNew(Key1 string, Key2 uint64) (*OrderedMultikeyedList, error) {
Expand Down
8 changes: 8 additions & 0 deletions ygot/struct_validation_map_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,14 @@ func (o *OrderedMap) Values() []*OrderedList {
return values
}

// Len returns a size of OrderedList_OrderedMap
func (o *OrderedMap) Len() int {
if o == nil {
return 0
}
return len(o.keys)
}

// Get returns the value corresponding to the key. If the key is not found, nil
// is returned.
func (o *OrderedMap) Get(key string) *OrderedList {
Expand Down
2 changes: 2 additions & 0 deletions ygot/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ type GoOrderedList interface {
// IsYANGOrderedList is a marker method that indicates that the struct
// implements the GoOrderedList interface.
IsYANGOrderedList()
// Len returns the size of the ordered list.
Len() int
}

// KeyHelperGoStruct is an interface which can be implemented by Go structs
Expand Down
35 changes: 22 additions & 13 deletions ytypes/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,40 +44,49 @@ func validateList(schema *yang.Entry, value interface{}) util.Errors {
util.DbgPrint("validateList with value %v, type %T, schema name %s", value, value, schema.Name)

kind := reflect.TypeOf(value).Kind()
if kind == reflect.Slice || kind == reflect.Map {
orderedMap, isOrderedMap := value.(ygot.GoOrderedList)
if kind == reflect.Slice || kind == reflect.Map || isOrderedMap {
// Check list attributes: size constraints etc.
// Skip this check if not a list type - in this case value may be a list
// element which shares the list schema (excluding ListAttr).
errors = util.AppendErrs(errors, validateListAttr(schema, value))
}

switch kind {
case reflect.Slice:
checkMapElement := func(key, val reflect.Value) {
structElems := val.Elem()
// Check that keys are present and have correct values.
errors = util.AppendErrs(errors, checkKeys(schema, structElems, key))

// Verify each elements's fields.
errors = util.AppendErrs(errors, validateStructElems(schema, val.Interface()))
}

switch {
case isOrderedMap:
errors = util.AppendErr(errors, yreflect.RangeOrderedMap(orderedMap, func(k, v reflect.Value) bool {
checkMapElement(k, v)
return true
}))
case kind == reflect.Slice:
// List without key is a slice in the data tree.
sv := reflect.ValueOf(value)
for i := 0; i < sv.Len(); i++ {
errors = util.AppendErrs(errors, validateStructElems(schema, sv.Index(i).Interface()))
}
case reflect.Map:
case kind == reflect.Map:
// List with key is a map in the data tree, with the key being the value
// of the key field(s) in the elements.
for _, key := range reflect.ValueOf(value).MapKeys() {
cv := reflect.ValueOf(value).MapIndex(key).Interface()
structElems := reflect.ValueOf(cv).Elem()
// Check that keys are present and have correct values.
errors = util.AppendErrs(errors, checkKeys(schema, structElems, key))

// Verify each elements's fields.
errors = util.AppendErrs(errors, validateStructElems(schema, cv))
checkMapElement(key, reflect.ValueOf(value).MapIndex(key))
}
case reflect.Ptr:
case kind == reflect.Ptr:
// Validate was called on a list element rather than the whole list, or
// on a completely bogus struct. In either case, evaluate just the
// element against the list schema without considering list attributes.
errors = util.AppendErrs(errors, validateStructElems(schema, value))

default:
errors = util.AppendErr(errors, fmt.Errorf("validateList expected map/slice type for %s, got %T", schema.Name, value))
errors = util.AppendErr(errors, fmt.Errorf("validateList expected map/slice/GoOrderedList type for %s, got %T", schema.Name, value))
}

return errors
Expand Down
83 changes: 83 additions & 0 deletions ytypes/list_exported_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -354,3 +354,86 @@ func TestUnmarshalSingleListElement(t *testing.T) {
})
}
}

func TestValidatedOrderedMap(t *testing.T) {
tests := []struct {
desc string
inSchema *yang.Entry
inVal any
wantErr bool
}{{
desc: "single-keyed list",
inSchema: ctestschema.SchemaTree["OrderedList"],
inVal: ctestschema.GetOrderedMap(t),
}, {
desc: "multi-keyed list",
inSchema: ctestschema.SchemaTree["OrderedMultikeyedList"],
inVal: ctestschema.GetOrderedMapMultikeyed(t),
}, {
desc: "single-keyed list with missing key",
inSchema: ctestschema.SchemaTree["OrderedList"],
inVal: func() *ctestschema.OrderedList_OrderedMap {
om := ctestschema.GetOrderedMap(t)
om.Get("foo").Key = nil
return om
}(),
wantErr: true,
}, {
desc: "single-keyed list with mismatching key",
inSchema: ctestschema.SchemaTree["OrderedList"],
inVal: func() *ctestschema.OrderedList_OrderedMap {
om := ctestschema.GetOrderedMap(t)
om.Get("foo").Key = ygot.String("foosball")
return om
}(),
wantErr: true,
}, {
desc: "single-keyed list with too many elements",
inSchema: ctestschema.SchemaTree["OrderedList"],
inVal: func() *ctestschema.OrderedList_OrderedMap {
om := &ctestschema.OrderedList_OrderedMap{}
om.AppendNew("alpha")
om.AppendNew("bravo")
om.AppendNew("charlie")
om.AppendNew("delta")
om.AppendNew("echo")
// One too many
om.AppendNew("foxtrot")
return om
}(),
wantErr: true,
}, {
desc: "multi-keyed list with missing key",
inSchema: ctestschema.SchemaTree["OrderedMultikeyedList"],
inVal: func() *ctestschema.OrderedMultikeyedList_OrderedMap {
om := ctestschema.GetOrderedMapMultikeyed(t)
om.Get(ctestschema.OrderedMultikeyedList_Key{
Key1: "foo",
Key2: 42,
}).Key2 = nil
return om
},
wantErr: true,
}, {
desc: "multi-keyed list with mismatching key",
inSchema: ctestschema.SchemaTree["OrderedMultikeyedList"],
inVal: func() *ctestschema.OrderedMultikeyedList_OrderedMap {
om := ctestschema.GetOrderedMapMultikeyed(t)
om.Get(ctestschema.OrderedMultikeyedList_Key{
Key1: "foo",
Key2: 42,
}).Key2 = ygot.Uint64(43)
return om
},
wantErr: true,
}}

for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
errs := ytypes.Validate(tt.inSchema, tt.inVal)
if got, want := (errs != nil), tt.wantErr; got != want {
t.Errorf("%s: b.Validate(%v) got error: %v, want error? %v", tt.desc, tt.inVal, errs, tt.wantErr)
}
})
}
}
2 changes: 1 addition & 1 deletion ytypes/list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ func TestValidateList(t *testing.T) {

// bad value type
err = util.Errors(validateList(validListSchema, struct{}{})).Error()
wantErr = `validateList expected map/slice type for valid-list-schema, got struct {}`
wantErr = `validateList expected map/slice/GoOrderedList type for valid-list-schema, got struct {}`
if got, want := err, wantErr; got != want {
t.Errorf("nil schema: Unmarshal got error: %v, want error: %v", got, want)
}
Expand Down
9 changes: 7 additions & 2 deletions ytypes/util_schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (

"github.com/openconfig/goyang/pkg/yang"
"github.com/openconfig/ygot/util"
"github.com/openconfig/ygot/ygot"
)

//lint:file-ignore U1000 Ignore all unused code, it represents generated code.
Expand Down Expand Up @@ -91,8 +92,12 @@ func validateListAttr(schema *yang.Entry, value interface{}) util.Errors {

var size uint64
if value != nil {
switch reflect.TypeOf(value).Kind() {
case reflect.Slice, reflect.Map:
orderedMap, isOrderedMap := value.(ygot.GoOrderedList)
kind := reflect.TypeOf(value).Kind()
switch {
case isOrderedMap:
size = uint64(orderedMap.Len())
case kind == reflect.Slice, kind == reflect.Map:
size = uint64(reflect.ValueOf(value).Len())
default:
return util.NewErrs(fmt.Errorf("value %v type %T must be map or slice type for schema %s", value, value, schema.Name))
Expand Down
1 change: 1 addition & 0 deletions ytypes/validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ func customValidation(val ygot.GoStruct) error {
}
return nil
}

func TestValidate(t *testing.T) {
leafSchema := &yang.Entry{Name: "leaf-schema", Kind: yang.LeafEntry, Type: &yang.YangType{Kind: yang.Ystring}}

Expand Down