Skip to content

Commit fa130fb

Browse files
authored
fix: reject unsupported model envelope versions (#79)
* fix: reject unsupported model envelope versions * fix: accept loaded schema versions
1 parent ec5d350 commit fa130fb

5 files changed

Lines changed: 123 additions & 9 deletions

File tree

internal/umodel/import_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,29 @@ func TestValidateAcceptsValidUModelElements(t *testing.T) {
3838
}
3939
}
4040

41+
func TestValidateReportsUnsupportedEnvelopeVersionOnVersionField(t *testing.T) {
42+
svc := NewService(graphstore.NewMemoryStore(), WithImportRoot("/"))
43+
result, err := svc.Validate(context.Background(), "demo", []model.UModelElement{{
44+
Kind: "entity_set",
45+
Domain: "devops",
46+
Name: "devops.service",
47+
Version: "v9.9.9",
48+
Spec: map[string]any{},
49+
}})
50+
if err != nil {
51+
t.Fatalf("validate: %v", err)
52+
}
53+
if result.Valid {
54+
t.Fatalf("expected invalid result")
55+
}
56+
if len(result.Errors) != 1 {
57+
t.Fatalf("expected one validation error, got %+v", result.Errors)
58+
}
59+
if result.Errors[0].Field != "elements[0].version" {
60+
t.Fatalf("expected unsupported envelope version on version field, got %+v", result.Errors[0])
61+
}
62+
}
63+
4164
func TestImportFileBuildsIndexAndRebuildsFromSnapshot(t *testing.T) {
4265
ctx := context.Background()
4366
store := graphstore.NewMemoryStore()

internal/umodel/schemaspec/registry.go

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,16 @@ import (
1212
)
1313

1414
type Registry struct {
15-
schemas map[string]*Schema
15+
schemas map[string]*Schema
16+
acceptedEnvelopeVersions map[string]struct{}
1617
}
1718

1819
func NewRegistry(efs embed.FS, dir string) (*Registry, error) {
1920
entries, err := efs.ReadDir(dir)
2021
if err != nil {
2122
return nil, fmt.Errorf("read embedded dir %q: %w", dir, err)
2223
}
23-
reg := &Registry{schemas: make(map[string]*Schema)}
24+
reg := newEmptyRegistry()
2425
for _, entry := range entries {
2526
if entry.IsDir() {
2627
continue
@@ -37,12 +38,10 @@ func NewRegistry(efs embed.FS, dir string) (*Registry, error) {
3738
if s == nil {
3839
continue
3940
}
40-
if existing, ok := reg.schemas[s.Name]; ok {
41+
if _, ok := reg.schemas[s.Name]; ok {
4142
return nil, fmt.Errorf("duplicate schema name %q (in %s and earlier source)", s.Name, path)
42-
} else {
43-
_ = existing
4443
}
45-
reg.schemas[s.Name] = s
44+
reg.registerSchema(s)
4645
}
4746
return reg, nil
4847
}
@@ -52,7 +51,7 @@ func newRegistryFromFS(efs fs.ReadDirFS, dir string) (*Registry, error) {
5251
if err != nil {
5352
return nil, fmt.Errorf("read dir %q: %w", dir, err)
5453
}
55-
reg := &Registry{schemas: make(map[string]*Schema)}
54+
reg := newEmptyRegistry()
5655
for _, entry := range entries {
5756
if entry.IsDir() {
5857
continue
@@ -66,19 +65,46 @@ func newRegistryFromFS(efs fs.ReadDirFS, dir string) (*Registry, error) {
6665
return nil, fmt.Errorf("parse %s: %w", entry.Name(), err)
6766
}
6867
if s != nil {
69-
reg.schemas[s.Name] = s
68+
reg.registerSchema(s)
7069
}
7170
}
7271
return reg, nil
7372
}
7473

74+
func newEmptyRegistry() *Registry {
75+
return &Registry{
76+
schemas: make(map[string]*Schema),
77+
acceptedEnvelopeVersions: map[string]struct{}{
78+
// v0.1.0 is the manifest-level envelope version. Kind schema
79+
// versions are added as schemas load.
80+
"v0.1.0": {},
81+
},
82+
}
83+
}
84+
85+
func (r *Registry) registerSchema(s *Schema) {
86+
r.schemas[s.Name] = s
87+
if s.Version == "" {
88+
return
89+
}
90+
r.acceptedEnvelopeVersions[s.Version] = struct{}{}
91+
}
92+
7593
func (r *Registry) Lookup(kind string) *Schema {
7694
if r == nil {
7795
return nil
7896
}
7997
return r.schemas[kind]
8098
}
8199

100+
func (r *Registry) AcceptsVersion(version string) bool {
101+
if r == nil {
102+
return false
103+
}
104+
_, ok := r.acceptedEnvelopeVersions[version]
105+
return ok
106+
}
107+
82108
func (r *Registry) Kinds() []string {
83109
if r == nil {
84110
return nil

internal/umodel/schemaspec/registry_test.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,19 @@ func TestRegistryLookupReturnsNilForUnknownKind(t *testing.T) {
3232
}
3333
}
3434

35+
func TestRegistryAcceptsManifestAndLoadedSchemaVersions(t *testing.T) {
36+
reg := Default()
37+
if !reg.AcceptsVersion("v0.1.0") {
38+
t.Fatal("manifest envelope version v0.1.0 should be accepted")
39+
}
40+
if !reg.AcceptsVersion("v1.0.0") {
41+
t.Fatal("loaded schema version v1.0.0 should be accepted")
42+
}
43+
if reg.AcceptsVersion("v9.9.9") {
44+
t.Fatal("unknown envelope version should not be accepted")
45+
}
46+
}
47+
3548
func TestEntitySetLinkSchemaCarriesRequiredEntityLinkType(t *testing.T) {
3649
s := Default().Lookup("entity_set_link")
3750
if s == nil {

internal/umodel/schemaspec/validator.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ func NewNoopValidator() *Validator {
2929
// element.Kind. The returned error is non-nil only when the kind itself is
3030
// unknown to the registry; field-level problems are reported in Result.
3131
//
32-
// TODO(v2): respect element.Version; today we always use versions[0].
3332
// TODO(v2): also validate the kind/schema/metadata envelope (currently
3433
// elementFromPayload validates the minimum imperatively before the element
3534
// reaches this layer).
@@ -41,6 +40,12 @@ func (v *Validator) Validate(element model.UModelElement) (Result, error) {
4140
if s == nil {
4241
return Result{}, fmt.Errorf("unknown kind %q", element.Kind)
4342
}
43+
if element.Version != "" && !v.registry.AcceptsVersion(element.Version) {
44+
return Result{Errors: []Issue{{
45+
Path: "version",
46+
Reason: fmt.Sprintf("unsupported model envelope version %q", element.Version),
47+
}}}, nil
48+
}
4449
var res Result
4550
walk(element.Spec, s.Spec, "spec", &res)
4651
return res, nil

internal/umodel/schemaspec/validator_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,53 @@ func TestValidateReturnsErrorForUnknownKind(t *testing.T) {
1919
}
2020
}
2121

22+
func TestValidateReportsUnsupportedEnvelopeVersionIssue(t *testing.T) {
23+
v := schemaspec.DefaultValidator()
24+
res, err := v.Validate(model.UModelElement{Kind: "entity_set", Domain: "d", Name: "n", Version: "v9.9.9"})
25+
if err != nil {
26+
t.Fatalf("unknown version should be reported as a validation issue, got error: %v", err)
27+
}
28+
if !hasErrorAt(res, "version", "unsupported model envelope version") {
29+
t.Fatalf("expected unsupported envelope version issue on version, got %+v", res.Errors)
30+
}
31+
}
32+
33+
func TestValidateAcceptsCompatibleManifestVersion(t *testing.T) {
34+
v := schemaspec.DefaultValidator()
35+
res, err := v.Validate(model.UModelElement{
36+
Kind: "entity_set",
37+
Domain: "demo",
38+
Name: "demo.service",
39+
Version: "v0.1.0",
40+
Spec: map[string]any{
41+
"fields": []any{map[string]any{"name": "service_id", "type": "string"}},
42+
},
43+
})
44+
if err != nil {
45+
t.Fatalf("validate compatible manifest version: %v", err)
46+
}
47+
if len(res.Errors) != 0 {
48+
t.Fatalf("compatible manifest version should validate against default kind schema, got %+v", res.Errors)
49+
}
50+
}
51+
52+
func TestValidateAcceptsLoadedSchemaVersion(t *testing.T) {
53+
v := schemaspec.DefaultValidator()
54+
res, err := v.Validate(model.UModelElement{
55+
Kind: "metric_set",
56+
Domain: "demo",
57+
Name: "demo.metrics",
58+
Version: "v1.0.0",
59+
Spec: map[string]any{},
60+
})
61+
if err != nil {
62+
t.Fatalf("validate loaded schema version: %v", err)
63+
}
64+
if hasErrorAt(res, "version", "unsupported model envelope version") {
65+
t.Fatalf("loaded schema version should not report unsupported version, got %+v", res.Errors)
66+
}
67+
}
68+
2269
func TestNoopValidatorAcceptsEverything(t *testing.T) {
2370
v := schemaspec.NewNoopValidator()
2471
res, err := v.Validate(model.UModelElement{Kind: "anything_goes", Spec: map[string]any{"garbage": true}})

0 commit comments

Comments
 (0)