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

Added UnmarshalOpt to allow a best effort unmarshal #863

Merged
merged 17 commits into from
Jun 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
67 changes: 58 additions & 9 deletions ytypes/gnmi.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,29 +65,48 @@ func UnmarshalNotifications(schema *Schema, ns []*gpb.Notification, opts ...Unma
func UnmarshalSetRequest(schema *Schema, req *gpb.SetRequest, opts ...UnmarshalOpt) error {
preferShadowPath := hasPreferShadowPath(opts)
ignoreExtraFields := hasIgnoreExtraFields(opts)
bestEffortUnmarshal := hasBestEffortUnmarshal(opts)
if req == nil {
return nil
}
root := schema.Root
rootName := reflect.TypeOf(root).Elem().Name()

var complianceErrs *ComplianceErrors

// Process deletes, then replace, then updates.
if err := deletePaths(schema.SchemaTree[rootName], root, req.Prefix, req.Delete, preferShadowPath); err != nil {
return err
if err := deletePaths(schema.SchemaTree[rootName], root, req.Prefix, req.Delete, preferShadowPath, bestEffortUnmarshal); err != nil {
if bestEffortUnmarshal {
complianceErrs = complianceErrs.append(err.(*ComplianceErrors).Errors...)
} else {
return err
}
}
if err := replacePaths(schema.SchemaTree[rootName], root, req.Prefix, req.Replace, preferShadowPath, ignoreExtraFields); err != nil {
return err
if err := replacePaths(schema.SchemaTree[rootName], root, req.Prefix, req.Replace, preferShadowPath, ignoreExtraFields, bestEffortUnmarshal); err != nil {
if bestEffortUnmarshal {
complianceErrs = complianceErrs.append(err.(*ComplianceErrors).Errors...)
} else {
return err
}
}
if err := updatePaths(schema.SchemaTree[rootName], root, req.Prefix, req.Update, preferShadowPath, ignoreExtraFields); err != nil {
return err
if err := updatePaths(schema.SchemaTree[rootName], root, req.Prefix, req.Update, preferShadowPath, ignoreExtraFields, bestEffortUnmarshal); err != nil {
if bestEffortUnmarshal {
complianceErrs = complianceErrs.append(err.(*ComplianceErrors).Errors...)
} else {
return err
}
}

if bestEffortUnmarshal && complianceErrs != nil {
return complianceErrs
}
return nil
}

// deletePaths deletes a slice of paths from the given GoStruct.
func deletePaths(schema *yang.Entry, goStruct ygot.GoStruct, prefix *gpb.Path, paths []*gpb.Path, preferShadowPath bool) error {
func deletePaths(schema *yang.Entry, goStruct ygot.GoStruct, prefix *gpb.Path, paths []*gpb.Path, preferShadowPath, bestEffortUnmarshal bool) error {
var dopts []DelNodeOpt
var ce *ComplianceErrors
if preferShadowPath {
dopts = append(dopts, &PreferShadowPath{})
}
Expand All @@ -100,9 +119,17 @@ func deletePaths(schema *yang.Entry, goStruct ygot.GoStruct, prefix *gpb.Path, p
}
}
if err := DeleteNode(schema, goStruct, path, dopts...); err != nil {
if bestEffortUnmarshal {
ce = ce.append(err)
continue
}
return err
}
}

if bestEffortUnmarshal && ce != nil {
return ce
}
return nil
}

Expand Down Expand Up @@ -130,8 +157,9 @@ func joinPrefixToUpdate(prefix *gpb.Path, update *gpb.Update) (*gpb.Update, erro
// replacePaths unmarshals a slice of updates into the given GoStruct. It
// deletes the values at these paths before unmarshalling them. These updates
// can either by JSON-encoded or gNMI-encoded values (scalars).
func replacePaths(schema *yang.Entry, goStruct ygot.GoStruct, prefix *gpb.Path, updates []*gpb.Update, preferShadowPath, ignoreExtraFields bool) error {
func replacePaths(schema *yang.Entry, goStruct ygot.GoStruct, prefix *gpb.Path, updates []*gpb.Update, preferShadowPath, ignoreExtraFields, bestEffortUnmarshal bool) error {
var dopts []DelNodeOpt
var ce *ComplianceErrors
if preferShadowPath {
dopts = append(dopts, &PreferShadowPath{})
}
Expand All @@ -142,27 +170,48 @@ func replacePaths(schema *yang.Entry, goStruct ygot.GoStruct, prefix *gpb.Path,
return err
}
if err := DeleteNode(schema, goStruct, update.Path, dopts...); err != nil {
if bestEffortUnmarshal {
ce = ce.append(err)
continue
}
return err
}
if err := setNode(schema, goStruct, update, preferShadowPath, ignoreExtraFields); err != nil {
if bestEffortUnmarshal {
ce = ce.append(err)
continue
}
return err
}
}
if bestEffortUnmarshal && ce != nil {
return ce
}
return nil
}

// updatePaths unmarshals a slice of updates into the given GoStruct. These
// updates can either by JSON-encoded or gNMI-encoded values (scalars).
func updatePaths(schema *yang.Entry, goStruct ygot.GoStruct, prefix *gpb.Path, updates []*gpb.Update, preferShadowPath, ignoreExtraFields bool) error {
func updatePaths(schema *yang.Entry, goStruct ygot.GoStruct, prefix *gpb.Path, updates []*gpb.Update, preferShadowPath, ignoreExtraFields, bestEffortUnmarshal bool) error {
var ce *ComplianceErrors

for _, update := range updates {
var err error
if update, err = joinPrefixToUpdate(prefix, update); err != nil {
return err
}
if err := setNode(schema, goStruct, update, preferShadowPath, ignoreExtraFields); err != nil {
if bestEffortUnmarshal {
ce = ce.append(err)
continue
}
return err
}
}

if bestEffortUnmarshal && ce != nil {
return ce
}
return nil
}

Expand Down
159 changes: 159 additions & 0 deletions ytypes/gnmi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
package ytypes

import (
"errors"
"strings"
"testing"

"github.com/google/go-cmp/cmp"
Expand All @@ -32,6 +34,7 @@ func TestUnmarshalSetRequest(t *testing.T) {
inUnmarshalOpts []UnmarshalOpt
want ygot.GoStruct
wantErr bool
numErrs int
}{{
desc: "nil input",
inSchema: &Schema{
Expand Down Expand Up @@ -496,6 +499,75 @@ func TestUnmarshalSetRequest(t *testing.T) {
}},
},
wantErr: true,
}, {
desc: "mix of errors and a non-error with best-effort flag",
inSchema: &Schema{
Root: &ListElemStruct1{
Key1: ygot.String("mixederrors"),
Outer: &OuterContainerType1{
Inner: &InnerContainerType1{
Int32LeafName: ygot.Int32(43),
Int32LeafListName: []int32{100},
StringLeafName: ygot.String("bear"),
},
},
},
SchemaTree: map[string]*yang.Entry{
"ListElemStruct1": simpleSchema(),
},
},
inReq: &gpb.SetRequest{
Prefix: &gpb.Path{},
Update: []*gpb.Update{{
Path: mustPath("/key1"),
Val: &gpb.TypedValue{Value: &gpb.TypedValue_StringVal{StringVal: "non-error"}},
}, {
Path: mustPath("/outer/error"),
Val: &gpb.TypedValue{Value: &gpb.TypedValue_JsonIetfVal{
JsonIetfVal: []byte(`
{
"int32-leaf-list": [42]
}
`),
}},
}, {
Path: mustPath("/outer/error2"),
Val: &gpb.TypedValue{Value: &gpb.TypedValue_JsonIetfVal{
JsonIetfVal: []byte(`
{
"int32-leaf-list": [42]
}
`),
}},
}},
Replace: []*gpb.Update{{
Path: mustPath("/key1"),
Val: &gpb.TypedValue{Value: &gpb.TypedValue_StringVal{StringVal: "success"}},
}, {
Path: mustPath("/key2"),
Val: &gpb.TypedValue{Value: &gpb.TypedValue_StringVal{StringVal: "failure"}},
}, {
Path: mustPath("/key3"),
Val: &gpb.TypedValue{Value: &gpb.TypedValue_StringVal{StringVal: "failure"}},
}},
Delete: []*gpb.Path{
mustPath("/outer/inner/config/int32-leaf-field"),
mustPath("/outer/error"),
mustPath("/outer/error2"),
},
},
inUnmarshalOpts: []UnmarshalOpt{&BestEffortUnmarshal{}},
want: &ListElemStruct1{
Key1: ygot.String("non-error"),
Outer: &OuterContainerType1{
Inner: &InnerContainerType1{
Int32LeafListName: []int32{100},
StringLeafName: ygot.String("bear"),
},
},
},
wantErr: true,
numErrs: 6,
}}

for _, tt := range tests {
Expand All @@ -504,6 +576,19 @@ func TestUnmarshalSetRequest(t *testing.T) {
if gotErr := err != nil; gotErr != tt.wantErr {
t.Fatalf("got error: %v, want: %v", err, tt.wantErr)
}
if err != nil && hasBestEffortUnmarshal(tt.inUnmarshalOpts) {
var ce *ComplianceErrors
if errors.As(err, &ce) {
if len(ce.Errors) != tt.numErrs {
t.Fatalf("Got the incorrect number of errors: want %v, got %v", tt.numErrs, len(ce.Errors))
}
if !strings.HasPrefix(ce.Error(), "Noncompliance errors") {
t.Fatalf("Incorrect error message, should begin with \"Noncompliance errors\": %v", err)
}
} else {
t.Fatalf("Error casting BestEffortUnmarshal result to compliance errors struct")
}
}
if !tt.wantErr {
if diff := cmp.Diff(tt.inSchema.Root, tt.want); diff != "" {
t.Errorf("(-got, +want):\n%s", diff)
Expand All @@ -521,6 +606,7 @@ func TestUnmarshalNotifications(t *testing.T) {
inUnmarshalOpts []UnmarshalOpt
want ygot.GoStruct
wantErr bool
numErrs int
}{{
desc: "updates to an empty struct",
inSchema: &Schema{
Expand Down Expand Up @@ -774,6 +860,65 @@ func TestUnmarshalNotifications(t *testing.T) {
},
},
},
}, {
desc: "mix of errors and a non-error with best-effort flag",
inSchema: &Schema{
Root: &ListElemStruct1{
Key1: ygot.String("mixederrors"),
Outer: &OuterContainerType1{
Inner: &InnerContainerType1{
Int32LeafName: ygot.Int32(43),
Int32LeafListName: []int32{100},
StringLeafName: ygot.String("bear"),
},
},
},
SchemaTree: map[string]*yang.Entry{
"ListElemStruct1": simpleSchema(),
},
},
inNotifications: []*gpb.Notification{{
Prefix: &gpb.Path{},
Delete: []*gpb.Path{
mustPath("/outer/inner/config/int32-leaf-field"),
mustPath("/outer/error"),
mustPath("/outer/error2"),
},
Update: []*gpb.Update{{
Path: mustPath("/key1"),
Val: &gpb.TypedValue{Value: &gpb.TypedValue_StringVal{StringVal: "non-error"}},
}, {
Path: mustPath("/outer/error"),
Val: &gpb.TypedValue{Value: &gpb.TypedValue_JsonIetfVal{
JsonIetfVal: []byte(`
{
"int32-leaf-list": [42]
}
`),
}},
}, {
Path: mustPath("/outer/error2"),
Val: &gpb.TypedValue{Value: &gpb.TypedValue_JsonIetfVal{
JsonIetfVal: []byte(`
{
"int32-leaf-list": [42]
}
`),
}},
}},
}},
inUnmarshalOpts: []UnmarshalOpt{&BestEffortUnmarshal{}},
want: &ListElemStruct1{
Key1: ygot.String("non-error"),
Outer: &OuterContainerType1{
Inner: &InnerContainerType1{
Int32LeafListName: []int32{100},
StringLeafName: ygot.String("bear"),
},
},
},
wantErr: true,
numErrs: 4,
}}

for _, tt := range tests {
Expand All @@ -782,6 +927,20 @@ func TestUnmarshalNotifications(t *testing.T) {
if gotErr := err != nil; gotErr != tt.wantErr {
t.Fatalf("got error: %v, want: %v", err, tt.wantErr)
}
if err != nil && hasBestEffortUnmarshal(tt.inUnmarshalOpts) {
var ce *ComplianceErrors
if errors.As(err, &ce) {
if len(ce.Errors) != tt.numErrs {
t.Log(err.Error())
t.Fatalf("Got the incorrect number of errors: want %v, got %v", tt.numErrs, len(ce.Errors))
}
if !strings.HasPrefix(ce.Error(), "Noncompliance errors") {
t.Fatalf("Incorrect error message, should begin with \"Noncompliance errors\": %v", err)
}
} else {
t.Fatalf("Error casting BestEffortUnmarshal result to compliance errors struct")
}
}
if !tt.wantErr {
if diff := cmp.Diff(tt.inSchema.Root, tt.want); diff != "" {
t.Errorf("(-got, +want):\n%s", diff)
Expand Down
Loading
Loading