Skip to content
Draft
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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ require (
github.com/sdcio/cache v0.0.38
github.com/sdcio/logger v0.0.3
github.com/sdcio/schema-server v0.0.34
github.com/sdcio/sdc-protos v0.0.51
github.com/sdcio/sdc-protos v0.0.52-0.20260504101706-192d1e8e12bf
github.com/sdcio/yang-parser v0.0.12
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -193,8 +193,8 @@ github.com/sdcio/logger v0.0.3 h1:IFUbObObGry+S8lHGwOQKKRxJSuOphgRU/hxVhOdMOM=
github.com/sdcio/logger v0.0.3/go.mod h1:yWaOxK/G6vszjg8tKZiMqiEjlZouHsjFME4zSk+SAEA=
github.com/sdcio/schema-server v0.0.34 h1:NNDOkvtUMONtBA7cVvN96F+FWGD/Do6HNqfchy9B8eI=
github.com/sdcio/schema-server v0.0.34/go.mod h1:6t8HLXpqUqEJmE5yNZh29u/KZw0jlOICdNWns7zE4GE=
github.com/sdcio/sdc-protos v0.0.51 h1:sFc2ct8v4D7rBgFdg/fmXgJffcFhupfx4QJKsSAoolA=
github.com/sdcio/sdc-protos v0.0.51/go.mod h1:FkJMZWtp7Rcc/EedbX2mt1tET/j8KdavNl2BsHf03+o=
github.com/sdcio/sdc-protos v0.0.52-0.20260504101706-192d1e8e12bf h1:O+6B6u6J9ul6uGdG5vyeK5B2eu9ujgubVCtERl0lG8o=
github.com/sdcio/sdc-protos v0.0.52-0.20260504101706-192d1e8e12bf/go.mod h1:FkJMZWtp7Rcc/EedbX2mt1tET/j8KdavNl2BsHf03+o=
github.com/sdcio/yang-parser v0.0.12 h1:RSSeqfAOIsJx5Lno5u4/ezyOmQYUduQ22rBfU/mtpJ4=
github.com/sdcio/yang-parser v0.0.12/go.mod h1:CBqn3Miq85qmFVGHxHXHLluXkaIOsTzV06IM4DW6+D4=
github.com/sirikothe/gotextfsm v1.0.1-0.20200816110946-6aa2cfd355e4 h1:FHUL2HofYJuslFOQdy/JjjP36zxqIpd/dcoiwLMIs7k=
Expand Down
5 changes: 3 additions & 2 deletions pkg/tree/importer/import_config_adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@ type ImportConfigAdapterElement interface {
// When and were to expect a Leafs or LeafList is defined by the yang schema.
// The String value is typically used for the keys.
GetKeyValue(ctx context.Context, slt *sdcpb.SchemaLeafType) (string, error)
// GetTVValue returns the TypedValue based value defined via the SchemaLeafType. Can also only be called on Leafs or LeafLists
GetTVValue(ctx context.Context, slt *sdcpb.SchemaLeafType) (*sdcpb.TypedValue, error)
// GetTVValue returns the TypedValue based value defined via the SchemaLeafType. Can also only be called on Leafs or LeafLists.
// For union-typed leaves the second return value is the matched branch SchemaLeafType; for non-union leaves it is nil. Proto/XML import set it via InferUnionMemberFromTypedValue (RFC 7950 §9.12 first match).
GetTVValue(ctx context.Context, slt *sdcpb.SchemaLeafType) (*sdcpb.TypedValue, *sdcpb.SchemaLeafType, error)
// returns the name of the actual Level.
GetName() string
}
4 changes: 2 additions & 2 deletions pkg/tree/importer/json/json_tree_importer.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,8 @@ func (j *JsonTreeImporterElement) GetKeyValue(ctx context.Context, slt *sdcpb.Sc
return fmt.Sprintf("%v", j.data), nil
}

func (j *JsonTreeImporterElement) GetTVValue(ctx context.Context, slt *sdcpb.SchemaLeafType) (*sdcpb.TypedValue, error) {
return sdcpb.ConvertJsonValueToTv(j.data, slt)
func (j *JsonTreeImporterElement) GetTVValue(ctx context.Context, slt *sdcpb.SchemaLeafType) (*sdcpb.TypedValue, *sdcpb.SchemaLeafType, error) {
return sdcpb.ConvertJsonValueToTvWithType(j.data, slt)
}

func (j *JsonTreeImporterElement) GetName() string {
Expand Down
45 changes: 35 additions & 10 deletions pkg/tree/importer/json/json_tree_importer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,12 @@ func TestJsonTreeImporter_GetTVValue(t *testing.T) {
slt *sdcpb.SchemaLeafType
}
tests := []struct {
name string
fields fields
args args
want *sdcpb.TypedValue
wantErr bool
name string
fields fields
args args
want *sdcpb.TypedValue
wantMatchedType string // expected matched type name ("" means same as input type)
wantErr bool
}{
{
name: "string",
Expand All @@ -144,8 +145,9 @@ func TestJsonTreeImporter_GetTVValue(t *testing.T) {
Type: "string",
},
},
wantErr: false,
want: &sdcpb.TypedValue{Value: &sdcpb.TypedValue_StringVal{StringVal: "foobar"}},
wantErr: false,
wantMatchedType: "string",
want: &sdcpb.TypedValue{Value: &sdcpb.TypedValue_StringVal{StringVal: "foobar"}},
},
{
name: "int",
Expand All @@ -158,8 +160,28 @@ func TestJsonTreeImporter_GetTVValue(t *testing.T) {
Type: "int32",
},
},
wantErr: false,
want: &sdcpb.TypedValue{Value: &sdcpb.TypedValue_IntVal{IntVal: 5}},
wantErr: false,
wantMatchedType: "int32",
want: &sdcpb.TypedValue{Value: &sdcpb.TypedValue_IntVal{IntVal: 5}},
},
{
name: "union matched string branch",
fields: fields{
name: "foo",
data: "hello",
},
args: args{
&sdcpb.SchemaLeafType{
Type: "union",
UnionTypes: []*sdcpb.SchemaLeafType{
{Type: "uint32"},
{Type: "string"},
},
},
},
wantErr: false,
wantMatchedType: "string",
want: &sdcpb.TypedValue{Value: &sdcpb.TypedValue_StringVal{StringVal: "hello"}},
},
}
for _, tt := range tests {
Expand All @@ -168,14 +190,17 @@ func TestJsonTreeImporter_GetTVValue(t *testing.T) {
data: tt.fields.data,
name: tt.fields.name,
}
got, err := j.GetTVValue(ctx, tt.args.slt)
got, matchedType, err := j.GetTVValue(ctx, tt.args.slt)
if (err != nil) != tt.wantErr {
t.Errorf("JsonTreeImporter.GetTVValue() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("JsonTreeImporter.GetTVValue() = %v, want %v", got, tt.want)
}
if tt.wantMatchedType != "" && (matchedType == nil || matchedType.Type != tt.wantMatchedType) {
t.Errorf("JsonTreeImporter.GetTVValue() matchedType = %v, want type %q", matchedType, tt.wantMatchedType)
}
})
}
}
13 changes: 9 additions & 4 deletions pkg/tree/importer/proto/proto_tree_importer.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,21 +80,26 @@ func (p *ProtoTreeImporterElement) GetElement(key string) importer.ImportConfigA
}

func (p *ProtoTreeImporterElement) GetKeyValue(ctx context.Context, slt *sdcpb.SchemaLeafType) (string, error) {
tv, err := p.GetTVValue(ctx, slt)
tv, _, err := p.GetTVValue(ctx, slt)
if err != nil {
return "", fmt.Errorf("failed GetTVValue for %s", p.data.Name)
}
return tv.ToString(), nil
}

func (p *ProtoTreeImporterElement) GetTVValue(ctx context.Context, slt *sdcpb.SchemaLeafType) (*sdcpb.TypedValue, error) {
// GetTVValue unmarshals the proto-serialized TypedValue. For union-typed leaves,
// InferUnionMemberFromTypedValue attaches the matched branch per RFC 7950 §9.12
// (first matching member in schema order; same narrowing as TVFromStringWithType).
func (p *ProtoTreeImporterElement) GetTVValue(ctx context.Context, slt *sdcpb.SchemaLeafType) (*sdcpb.TypedValue, *sdcpb.SchemaLeafType, error) {
result := &sdcpb.TypedValue{}
err := proto.Unmarshal(p.data.LeafVariant, result)
if err != nil {
return nil, err
return nil, nil, err
}
return result, nil
matched := importer.InferUnionMemberFromTypedValue(result, slt)
return result, matched, nil
}

func (p *ProtoTreeImporterElement) GetName() string {
return p.data.Name
}
Expand Down
34 changes: 34 additions & 0 deletions pkg/tree/importer/proto/proto_tree_importer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ import (
"github.com/sdcio/data-server/pkg/tree/ops"
"github.com/sdcio/data-server/pkg/tree/types"
"github.com/sdcio/data-server/pkg/utils/testhelper"
sdcpb "github.com/sdcio/sdc-protos/sdcpb"
"github.com/sdcio/sdc-protos/tree_persist"
"go.uber.org/mock/gomock"
"google.golang.org/protobuf/proto"
)

func TestProtoTreeImporter(t *testing.T) {
Expand Down Expand Up @@ -175,3 +178,34 @@ func TestProtoTreeImporter(t *testing.T) {
})
}
}

func TestProtoTreeImporterElement_GetTVValue_UnionInfersUniqueBranch(t *testing.T) {
ctx := context.Background()

tv := &sdcpb.TypedValue{Value: &sdcpb.TypedValue_StringVal{StringVal: "hello"}}
raw, err := proto.Marshal(tv)
if err != nil {
t.Fatal(err)
}

elem := NewProtoTreeImporterElement(&tree_persist.TreeElement{
Name: "leaf",
LeafVariant: raw,
})

unionSlt := &sdcpb.SchemaLeafType{
Type: "union",
UnionTypes: []*sdcpb.SchemaLeafType{
{Type: "uint32"},
{Type: "string"},
},
}

_, matchedType, err := elem.GetTVValue(ctx, unionSlt)
if err != nil {
t.Fatalf("GetTVValue() unexpected error: %v", err)
}
if matchedType == nil || matchedType.Type != "string" {
t.Fatalf("GetTVValue() matchedType = %v, want non-nil branch type string", matchedType)
}
}
132 changes: 132 additions & 0 deletions pkg/tree/importer/union_member.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package importer

import sdcpb "github.com/sdcio/sdc-protos/sdcpb"

// InferUnionMemberFromTypedValue returns the effective union branch type for a
// unmarshaled TypedValue and declared union schema.
//
// Resolution follows RFC 7950 §9.12: union member types are considered in schema
// declaration order (UnionTypes order). Lexically, the first branch for which
// TVFromStringWithType(tv.ToString()) succeeds and the result is TypedValue.Equal
// to tv wins (same narrowing as validating XML text against each member in order).
//
// If no branch matches lexically (for example pattern/length checks fail during
// lexical parse even though the wire TypedValue was produced from a structured
// union member), a structural fallback runs: among branches whose declared type
// matches tv's protobuf oneof shape (see tvShapeCompatibleWithBranch), the first
// such member in declaration order wins.
func InferUnionMemberFromTypedValue(tv *sdcpb.TypedValue, declared *sdcpb.SchemaLeafType) *sdcpb.SchemaLeafType {
if tv == nil || declared == nil || declared.Type != "union" {
return nil
}
if m := inferUnionLexicalEqual(tv, declared); m != nil {
return m
}
return inferUnionStructuralUnique(tv, declared)
}

// inferUnionLexicalEqual narrows the union by re-parsing tv's canonical string
// through each branch type in declaration order. The first branch for which
// TVFromStringWithType succeeds and the result is Equal to tv wins (RFC 7950 §9.12).
// If none match, callers may fall back to structural inference.
func inferUnionLexicalEqual(tv *sdcpb.TypedValue, declared *sdcpb.SchemaLeafType) *sdcpb.SchemaLeafType {
lexical := tv.ToString()
ts := tv.GetTimestamp() // forwarded for branches whose lexical parse depends on time context
for _, branch := range declared.UnionTypes {
if branch == nil {
continue
}
refTV, matched, err := sdcpb.TVFromStringWithType(branch, lexical, ts)
if err != nil {
continue
}
if !refTV.Equal(tv) {
continue
}
return matched
}
return nil
}

// inferUnionStructuralUnique picks a branch when lexical equality cannot decide:
// it considers branches whose declared type is compatible with tv's protobuf oneof
// "shape" (see tvShapeCompatibleWithBranch). Nested union branches recurse until
// a concrete leaf type matches or inner inference returns nil. The first
// compatible member in union declaration order wins (RFC 7950 §9.12).
func inferUnionStructuralUnique(tv *sdcpb.TypedValue, declared *sdcpb.SchemaLeafType) *sdcpb.SchemaLeafType {
var matches []*sdcpb.SchemaLeafType
for _, branch := range declared.UnionTypes {
if branch == nil {
continue
}
if !tvShapeCompatibleWithBranch(tv, branch) {
continue
}
if branch.Type == "union" {
// Record the resolved inner leaf (or sub-union member), not the nested union node itself.
if inner := InferUnionMemberFromTypedValue(tv, branch); inner != nil {
matches = append(matches, inner)
}
continue
}
matches = append(matches, branch)
}
if len(matches) == 0 {
return nil
}
return matches[0]
}

// tvShapeCompatibleWithBranch is a coarse wire-type filter: it checks whether the
// TypedValue's protobuf variant could plausibly hold a value of branch's YANG type.
// It is not full validation (ranges, patterns, identities). Unknown or unmapped
// combinations return false; leafref recurses to the target type.
func tvShapeCompatibleWithBranch(tv *sdcpb.TypedValue, branch *sdcpb.SchemaLeafType) bool {
if tv == nil || branch == nil {
return false
}
if branch.Type == "union" {
for _, sub := range branch.UnionTypes {
if tvShapeCompatibleWithBranch(tv, sub) {
return true
}
}
return false
}
switch tv.Value.(type) {
case *sdcpb.TypedValue_StringVal:
switch branch.Type {
case "string", "enumeration", "bits", "binary", "identityref", "instance-identifier":
return true
case "leafref":
return branch.LeafrefTargetType != nil && tvShapeCompatibleWithBranch(tv, branch.LeafrefTargetType)
default:
return false
}
case *sdcpb.TypedValue_UintVal:
switch branch.Type {
case "uint8", "uint16", "uint32", "uint64":
return true
default:
return false
}
case *sdcpb.TypedValue_IntVal:
switch branch.Type {
case "int8", "int16", "int32", "int64":
return true
default:
return false
}
case *sdcpb.TypedValue_BoolVal:
return branch.Type == "boolean"
case *sdcpb.TypedValue_DecimalVal:
return branch.Type == "decimal64"
case *sdcpb.TypedValue_EmptyVal:
return branch.Type == "empty"
case *sdcpb.TypedValue_IdentityrefVal:
return branch.Type == "identityref"
default:
// e.g. JsonVal, JsonIetfVal, LeaflistVal — not handled by this wire-shape union filter.
return false
}
}
30 changes: 30 additions & 0 deletions pkg/tree/importer/union_member_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package importer_test

import (
"testing"

"github.com/sdcio/data-server/pkg/tree/importer"
sdcpb "github.com/sdcio/sdc-protos/sdcpb"
)

func TestInferUnionMemberFromTypedValue_TwoStringBranchesRFCFirstMember(t *testing.T) {
tv := &sdcpb.TypedValue{Value: &sdcpb.TypedValue_StringVal{StringVal: "hello"}}
first := &sdcpb.SchemaLeafType{Type: "string"}
second := &sdcpb.SchemaLeafType{Type: "string"}
decl := &sdcpb.SchemaLeafType{
Type: "union",
UnionTypes: []*sdcpb.SchemaLeafType{first, second},
}
m := importer.InferUnionMemberFromTypedValue(tv, decl)
if m != first {
t.Fatalf("RFC 7950 §9.12 first matching member: want first branch pointer, got %v (first=%p second=%p)", m, first, second)
}
}

func TestInferUnionMemberFromTypedValue_NonUnion(t *testing.T) {
tv := &sdcpb.TypedValue{Value: &sdcpb.TypedValue_StringVal{StringVal: "x"}}
decl := &sdcpb.SchemaLeafType{Type: "string"}
if m := importer.InferUnionMemberFromTypedValue(tv, decl); m != nil {
t.Fatalf("non-union declared type: want nil, got %v", m)
}
}
15 changes: 13 additions & 2 deletions pkg/tree/importer/xml/xml_tree_importer.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"

"github.com/beevik/etree"

"github.com/sdcio/data-server/pkg/tree/importer"
sdcpb "github.com/sdcio/sdc-protos/sdcpb"
)
Expand Down Expand Up @@ -81,8 +82,18 @@ func (x *XmlTreeImporterElement) GetKeyValue(ctx context.Context, slt *sdcpb.Sch
return tv.ToString(), nil
}

func (x *XmlTreeImporterElement) GetTVValue(ctx context.Context, slt *sdcpb.SchemaLeafType) (*sdcpb.TypedValue, error) {
return sdcpb.TVFromString(slt, x.elem.Text(), 0)
// GetTVValue parses the element text with TVFromStringWithType. For union-typed
// leaves, InferUnionMemberFromTypedValue selects the matched branch using RFC 7950
// §9.12 member order (same rules as proto tree import).
func (x *XmlTreeImporterElement) GetTVValue(ctx context.Context, slt *sdcpb.SchemaLeafType) (*sdcpb.TypedValue, *sdcpb.SchemaLeafType, error) {
tv, matched, err := sdcpb.TVFromStringWithType(slt, x.elem.Text(), 0)
if err != nil {
return nil, nil, err
}
if slt != nil && slt.Type == "union" {
return tv, importer.InferUnionMemberFromTypedValue(tv, slt), nil
}
return tv, matched, nil
}

func (x *XmlTreeImporterElement) GetName() string {
Expand Down
Loading
Loading