Skip to content

Commit dcfe4c7

Browse files
authored
Add support for compressed schemas/fix basePath trimming. (#540)
* Handle compressed schemas, fix base path trimming, test coverage. * (M) protomap/proto.go * (M) protomap/proto_test.go - handle the case where there is a schemapath of the form {config,state}/field - i.e., a compressed schema. - fix trimming of base path and add coverage for it within tets. * (A) testdata/ - Update example protobufs for testing.
1 parent 7889ea7 commit dcfe4c7

File tree

4 files changed

+465
-190
lines changed

4 files changed

+465
-190
lines changed

protomap/proto.go

Lines changed: 145 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"google.golang.org/protobuf/types/descriptorpb"
2828

2929
"github.com/openconfig/gnmi/value"
30+
"github.com/openconfig/ygot/util"
3031
"github.com/openconfig/ygot/ygot"
3132

3233
gpb "github.com/openconfig/gnmi/proto/gnmi"
@@ -361,20 +362,106 @@ func resolvedPath(basePath, annotatedPath *gpb.Path) *gpb.Path {
361362
return np
362363
}
363364

364-
// ProtoFromPaths takes an input ygot-generated protobuf and unmarshals the values that are specified in the map
365-
// vals into it, using prefix as the prefix to any paths within the vals map. The message, p, is modified in place.
366-
// The map, vals, must be keyed by the gNMI path to the field which is annotated in the ygot generated protobuf,
367-
// the complete path is taken to be prefix + the key found in the map.
368-
func ProtoFromPaths(p proto.Message, vals map[*gpb.Path]interface{}, prefix *gpb.Path) error {
365+
// UnmapOpt marks that a particular option can be supplied as an argument
366+
// to the ProtoFromPaths function.
367+
type UnmapOpt interface {
368+
isUnmapOpt()
369+
}
370+
371+
// IgnoreExtraPaths indicates that unmapping should ignore any additional
372+
// paths that are found in the gNMI Notifications that do not have corresponding
373+
// fields in the protobuf.
374+
//
375+
// This option is typically used in conjunction with path compression where there
376+
// are some leaves that do not have corresponding fields.
377+
func IgnoreExtraPaths() *ignoreExtraPaths { return &ignoreExtraPaths{} }
378+
379+
type ignoreExtraPaths struct{}
380+
381+
// isUnmapOpt marks ignoreExtraPaths as an unmap option.
382+
func (*ignoreExtraPaths) isUnmapOpt() {}
383+
384+
// ValuePathPrefix indicates that the values in the supplied map have a prefix which
385+
// is equal to the supplied path. The prefix plus each path in the vals map must be
386+
// equal to the absolute path for the supplied values.
387+
func ValuePathPrefix(path *gpb.Path) *valuePathPrefix {
388+
return &valuePathPrefix{p: path}
389+
}
390+
391+
type valuePathPrefix struct{ p *gpb.Path }
392+
393+
// isUnmapOpt marks valuePathPrefix as an unmap option.
394+
func (*valuePathPrefix) isUnmapOpt() {}
395+
396+
// ProtobufMessagePrefix specifies the path that the protobuf message supplied to ProtoFromPaths
397+
// makes up. This is used in cases where the message itself is not the root - and hence unmarshalling
398+
// should look for paths relative to the specified path in the vals map.
399+
func ProtobufMessagePrefix(path *gpb.Path) *protoMsgPrefix {
400+
return &protoMsgPrefix{p: path}
401+
}
402+
403+
type protoMsgPrefix struct{ p *gpb.Path }
404+
405+
// isUnmapOpt marks protoMsgPrefix as an unmap option.
406+
func (*protoMsgPrefix) isUnmapOpt() {}
407+
408+
// ProtoFromPaths takes an input ygot-generated protobuf and unmarshals the values provided in vals into the map.
409+
// The vals map must be keyed by the gNMI path to the leaf, with the interface{} value being the value that the
410+
// leaf at the field should be set to.
411+
//
412+
// The protobuf p is modified in place to add the values.
413+
//
414+
// The set of UnmapOpts that are provided (opt) are used to control the behaviour of unmarshalling the specified data.
415+
//
416+
// ProtoFromPaths returns an error if the data cannot be unmarshalled.
417+
func ProtoFromPaths(p proto.Message, vals map[*gpb.Path]interface{}, opt ...UnmapOpt) error {
369418
if p == nil {
370419
return errors.New("nil protobuf supplied")
371420
}
372421

422+
valPrefix, err := hasValuePathPrefix(opt)
423+
if err != nil {
424+
return fmt.Errorf("invalid value prefix supplied, %v", err)
425+
}
426+
427+
protoPrefix, err := hasProtoMsgPrefix(opt)
428+
if err != nil {
429+
return fmt.Errorf("invalid protobuf message prefix supplied in options, %v", err)
430+
}
431+
432+
schemaPath := func(p *gpb.Path) *gpb.Path {
433+
np := proto.Clone(p).(*gpb.Path)
434+
for _, e := range np.Elem {
435+
e.Key = nil
436+
}
437+
return np
438+
}
439+
440+
// directCh is a map between the absolute schema path for a particular value, and
441+
// the value specified.
373442
directCh := map[*gpb.Path]interface{}{}
374443
for p, v := range vals {
375-
// TODO(robjs): needs fixing for compressed schemas.
376-
if len(p.GetElem()) == len(prefix.GetElem())+1 {
377-
directCh[p] = v
444+
absPath := &gpb.Path{
445+
Elem: append(append([]*gpb.PathElem{}, schemaPath(valPrefix).Elem...), p.Elem...),
446+
}
447+
448+
if !util.PathMatchesPathElemPrefix(absPath, protoPrefix) {
449+
return fmt.Errorf("invalid path provided, absolute paths must be used, %s does not have prefix %s", absPath, protoPrefix)
450+
}
451+
452+
// make the path absolute, and a schema path.
453+
pp := util.TrimGNMIPathElemPrefix(absPath, protoPrefix)
454+
455+
if len(pp.GetElem()) == 1 {
456+
directCh[pp] = v
457+
}
458+
// TODO(robjs): it'd be good to have something here that tells us whether we are in
459+
// a compressed schema. Potentially we should add something to the generated protobuf
460+
// as a fileoption that would give us this indication.
461+
if len(pp.Elem) == 2 {
462+
if pp.Elem[len(pp.Elem)-2].Name == "config" || pp.Elem[len(pp.Elem)-2].Name == "state" {
463+
directCh[pp] = v
464+
}
378465
}
379466
}
380467

@@ -389,8 +476,13 @@ func ProtoFromPaths(p proto.Message, vals map[*gpb.Path]interface{}, prefix *gpb
389476
}
390477

391478
for _, ap := range annotatedPath {
479+
if !util.PathMatchesPathElemPrefix(ap, protoPrefix) {
480+
rangeErr = fmt.Errorf("annotation %s does not match the supplied prefix %s", ap, protoPrefix)
481+
return false
482+
}
483+
trimmedAP := util.TrimGNMIPathElemPrefix(ap, protoPrefix)
392484
for chp, chv := range directCh {
393-
if proto.Equal(ap, chp) {
485+
if proto.Equal(trimmedAP, chp) {
394486
switch fd.Kind() {
395487
case protoreflect.MessageKind:
396488
v, isWrap, err := makeWrapper(m, fd, chv)
@@ -428,15 +520,56 @@ func ProtoFromPaths(p proto.Message, vals map[*gpb.Path]interface{}, prefix *gpb
428520
return rangeErr
429521
}
430522

431-
for chp := range directCh {
432-
if !mapped[chp] {
433-
return fmt.Errorf("did not map path %s to a proto field", chp)
523+
if !hasIgnoreExtraPaths(opt) {
524+
for chp := range directCh {
525+
if !mapped[chp] {
526+
return fmt.Errorf("did not map path %s to a proto field", chp)
527+
}
434528
}
435529
}
436530

437531
return nil
438532
}
439533

534+
// hasIgnoreExtraPaths checks whether the supplied opts slice contains the
535+
// ignoreExtraPaths option.
536+
func hasIgnoreExtraPaths(opts []UnmapOpt) bool {
537+
for _, o := range opts {
538+
if _, ok := o.(*ignoreExtraPaths); ok {
539+
return true
540+
}
541+
}
542+
return false
543+
}
544+
545+
// hasProtoMsgPrefix checks whether the supplied opts slice contains the
546+
// protoMsgPrefix option, and validates and returns the path it contains.
547+
func hasProtoMsgPrefix(opts []UnmapOpt) (*gpb.Path, error) {
548+
for _, o := range opts {
549+
if v, ok := o.(*protoMsgPrefix); ok {
550+
if v.p == nil {
551+
return nil, fmt.Errorf("invalid protobuf prefix supplied, %+v", v)
552+
}
553+
return v.p, nil
554+
}
555+
}
556+
return &gpb.Path{}, nil
557+
}
558+
559+
// hasValuePathPrefix checks whether the supplied opts slice contains
560+
// the valuePathPrefix option, and validates and returns the apth it contains.
561+
func hasValuePathPrefix(opts []UnmapOpt) (*gpb.Path, error) {
562+
for _, o := range opts {
563+
if v, ok := o.(*valuePathPrefix); ok {
564+
if v.p == nil {
565+
return nil, fmt.Errorf("invalid protobuf prefix supplied, %+v", v)
566+
}
567+
return v.p, nil
568+
}
569+
}
570+
return &gpb.Path{}, nil
571+
}
572+
440573
// makeWrapper generates a new message for field fd of the proto message msg with the value set to val.
441574
// The field fd must describe a field that has a message type. An error is returned if the wrong
442575
// type of payload is provided for the message. The second, boolean, return argument specifies whether

protomap/proto_test.go

Lines changed: 129 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@ func TestProtoFromPaths(t *testing.T) {
295295
desc string
296296
inProto proto.Message
297297
inVals map[*gpb.Path]interface{}
298-
inPrefix *gpb.Path
298+
inOpt []UnmapOpt
299299
wantProto proto.Message
300300
wantErrSubstring string
301301
}{{
@@ -441,18 +441,145 @@ func TestProtoFromPaths(t *testing.T) {
441441
mustPath("/bytes"): 42,
442442
},
443443
wantErrSubstring: "got non-byte slice value for bytes field",
444+
}, {
445+
desc: "compressed schema",
446+
inProto: &epb.ExampleMessage{},
447+
inVals: map[*gpb.Path]interface{}{
448+
mustPath("/state/compress"): "hello-world",
449+
},
450+
wantProto: &epb.ExampleMessage{
451+
Compress: &wpb.StringValue{Value: "hello-world"},
452+
},
453+
}, {
454+
desc: "trim prefix",
455+
inProto: &epb.Interface{},
456+
inVals: map[*gpb.Path]interface{}{
457+
mustPath("/interfaces/interface/config/description"): "interface-42",
458+
},
459+
inOpt: []UnmapOpt{
460+
ProtobufMessagePrefix(mustPath("/interfaces/interface")),
461+
},
462+
wantProto: &epb.Interface{
463+
Description: &wpb.StringValue{Value: "interface-42"},
464+
},
465+
}, {
466+
desc: "trim prefix with valPrefix",
467+
inProto: &epb.Interface{},
468+
inVals: map[*gpb.Path]interface{}{
469+
mustPath("description"): "interface-42",
470+
},
471+
inOpt: []UnmapOpt{
472+
ProtobufMessagePrefix(mustPath("/interfaces/interface")),
473+
ValuePathPrefix(mustPath("/interfaces/interface/config")),
474+
},
475+
wantProto: &epb.Interface{
476+
Description: &wpb.StringValue{Value: "interface-42"},
477+
},
478+
}, {
479+
desc: "invalid message with no annotation on one of its other fields",
480+
inProto: &epb.InvalidMessage{},
481+
inVals: map[*gpb.Path]interface{}{
482+
mustPath("three"): "str",
483+
},
484+
wantErrSubstring: "received field with invalid annotation",
485+
}, {
486+
desc: "invalid message with bad field type",
487+
inProto: &epb.BadMessageKeyTwo{},
488+
inVals: map[*gpb.Path]interface{}{
489+
mustPath("one"): "42",
490+
},
491+
wantErrSubstring: "unknown field kind",
492+
}, {
493+
desc: "extra paths, not ignored",
494+
inProto: &epb.Interface{},
495+
inVals: map[*gpb.Path]interface{}{
496+
mustPath("config/name"): "interface-42",
497+
mustPath("config/description"): "portal-to-wonderland",
498+
},
499+
inOpt: []UnmapOpt{
500+
ProtobufMessagePrefix(mustPath("/interfaces/interface")),
501+
ValuePathPrefix(mustPath("/interfaces/interface")),
502+
},
503+
wantProto: &epb.Interface{
504+
Description: &wpb.StringValue{Value: "interface-42"},
505+
},
506+
wantErrSubstring: `did not map path elem`,
507+
}, {
508+
desc: "extra paths, ignored",
509+
inProto: &epb.Interface{},
510+
inVals: map[*gpb.Path]interface{}{
511+
mustPath("config/name"): "interface-42",
512+
mustPath("config/description"): "portal-to-wonderland",
513+
},
514+
inOpt: []UnmapOpt{
515+
ProtobufMessagePrefix(mustPath("/interfaces/interface")),
516+
IgnoreExtraPaths(),
517+
ValuePathPrefix(mustPath("/interfaces/interface")),
518+
},
519+
wantProto: &epb.Interface{
520+
Description: &wpb.StringValue{Value: "portal-to-wonderland"},
521+
},
444522
}, {
445523
desc: "field that is not directly a child",
446524
inProto: &epb.ExampleMessage{},
447525
inVals: map[*gpb.Path]interface{}{
448526
mustPath("/one/two/three"): "ignored",
449527
},
450528
wantProto: &epb.ExampleMessage{},
529+
}, {
530+
desc: "value prefix specified - schema path",
531+
inProto: &epb.Interface{},
532+
inVals: map[*gpb.Path]interface{}{
533+
mustPath("description"): "interface-42",
534+
},
535+
inOpt: []UnmapOpt{
536+
ProtobufMessagePrefix(mustPath("/interfaces/interface")),
537+
ValuePathPrefix(mustPath("/interfaces/interface/config")),
538+
},
539+
wantProto: &epb.Interface{
540+
Description: &wpb.StringValue{Value: "interface-42"},
541+
},
542+
}, {
543+
desc: "value prefix specified - data tree path",
544+
inProto: &epb.Interface{},
545+
inVals: map[*gpb.Path]interface{}{
546+
mustPath("description"): "interface-42",
547+
},
548+
inOpt: []UnmapOpt{
549+
ProtobufMessagePrefix(mustPath("/interfaces/interface")),
550+
ValuePathPrefix(mustPath("/interfaces/interface[name=ethernet42]/config")),
551+
},
552+
wantProto: &epb.Interface{
553+
Description: &wpb.StringValue{Value: "interface-42"},
554+
},
555+
}, {
556+
desc: "bad trimmed value",
557+
inProto: &epb.Interface{},
558+
inVals: map[*gpb.Path]interface{}{
559+
mustPath("config/description"): "interface-84",
560+
},
561+
inOpt: []UnmapOpt{
562+
ProtobufMessagePrefix(mustPath("/interfaces/fish")),
563+
},
564+
wantErrSubstring: "invalid path provided, absolute paths must be used",
565+
}, {
566+
desc: "relative paths to protobuf prefix",
567+
inProto: &epb.Interface{},
568+
inVals: map[*gpb.Path]interface{}{
569+
mustPath("config/description"): "value",
570+
},
571+
inOpt: []UnmapOpt{
572+
ProtobufMessagePrefix(mustPath("/interfaces/interface")),
573+
ValuePathPrefix(mustPath("/interfaces/interface")),
574+
},
575+
wantProto: &epb.Interface{
576+
Description: &wpb.StringValue{Value: "value"},
577+
},
451578
}}
452579

453580
for _, tt := range tests {
454581
t.Run(tt.desc, func(t *testing.T) {
455-
err := ProtoFromPaths(tt.inProto, tt.inVals, tt.inPrefix)
582+
err := ProtoFromPaths(tt.inProto, tt.inVals, tt.inOpt...)
456583
if err != nil {
457584
if diff := errdiff.Substring(err, tt.wantErrSubstring); diff != "" {
458585
t.Fatalf("did not get expected error, %s", diff)

0 commit comments

Comments
 (0)