Skip to content

Commit 0acd412

Browse files
committed
feat: OperationRegistry extensibility and ContainerSnippet constant
- Add NewWidgetRegistryWithOps(extraOps) for extending known operations - Move knownOperations from package-level var to WidgetRegistry field to eliminate global mutable state race - Convert validateDefinitionOperations and validateMappings to WidgetRegistry methods - Use backend.ContainerSnippet constant in SetLayout check
1 parent 48db8b3 commit 0acd412

3 files changed

Lines changed: 87 additions & 21 deletions

File tree

mdl/backend/mpr/page_mutator.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ func (m *mprPageMutator) DropVariable(name string) error {
254254
}
255255

256256
func (m *mprPageMutator) SetLayout(newLayout string, paramMappings map[string]string) error {
257-
if m.containerType == "snippet" {
257+
if m.containerType == backend.ContainerSnippet {
258258
return fmt.Errorf("SET Layout is not supported for snippets")
259259
}
260260

mdl/executor/widget_registry.go

Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,13 @@ import (
1616

1717
// WidgetRegistry holds loaded widget definitions keyed by uppercase MDL name.
1818
type WidgetRegistry struct {
19-
byMDLName map[string]*WidgetDefinition // keyed by uppercase MDLName
20-
byWidgetID map[string]*WidgetDefinition // keyed by widgetId
19+
byMDLName map[string]*WidgetDefinition // keyed by uppercase MDLName
20+
byWidgetID map[string]*WidgetDefinition // keyed by widgetId
21+
knownOperations map[string]bool // operations accepted during validation
2122
}
2223

23-
// knownOperations is the set of operation names supported by the widget engine.
24-
var knownOperations = map[string]bool{
24+
// defaultKnownOperations is the set of operation names supported by the widget engine.
25+
var defaultKnownOperations = map[string]bool{
2526
"attribute": true,
2627
"association": true,
2728
"primitive": true,
@@ -34,11 +35,37 @@ var knownOperations = map[string]bool{
3435
"attributeObjects": true,
3536
}
3637

38+
// knownOperations is the active set used for validation, initialized from
39+
// defaultKnownOperations and now stored per-registry to avoid global mutable state.
40+
41+
func copyOps(src map[string]bool) map[string]bool {
42+
dst := make(map[string]bool, len(src))
43+
for k, v := range src {
44+
dst[k] = v
45+
}
46+
return dst
47+
}
48+
3749
// NewWidgetRegistry creates a registry pre-loaded with embedded definitions.
50+
// Uses the default set of known operations for validation.
3851
func NewWidgetRegistry() (*WidgetRegistry, error) {
52+
return NewWidgetRegistryWithOps(nil)
53+
}
54+
55+
// NewWidgetRegistryWithOps creates a registry pre-loaded with embedded definitions,
56+
// extending the default known operations with extraOps for validation.
57+
// This allows user-defined widgets to declare custom operations that would otherwise
58+
// fail validation. Pass nil for the default set.
59+
func NewWidgetRegistryWithOps(extraOps map[string]bool) (*WidgetRegistry, error) {
60+
ops := copyOps(defaultKnownOperations)
61+
for op := range extraOps {
62+
ops[op] = true
63+
}
64+
3965
reg := &WidgetRegistry{
40-
byMDLName: make(map[string]*WidgetDefinition),
41-
byWidgetID: make(map[string]*WidgetDefinition),
66+
byMDLName: make(map[string]*WidgetDefinition),
67+
byWidgetID: make(map[string]*WidgetDefinition),
68+
knownOperations: ops,
4269
}
4370

4471
entries, err := definitions.EmbeddedFS.ReadDir(".")
@@ -61,7 +88,7 @@ func NewWidgetRegistry() (*WidgetRegistry, error) {
6188
return nil, mdlerrors.NewBackend(fmt.Sprintf("parse definition %s", entry.Name()), err)
6289
}
6390

64-
if err := validateDefinitionOperations(&def, entry.Name()); err != nil {
91+
if err := reg.validateDefinitionOperations(&def, entry.Name()); err != nil {
6592
return nil, err
6693
}
6794

@@ -156,7 +183,7 @@ func (r *WidgetRegistry) loadDefinitionsFromDir(dir string) error {
156183
return mdlerrors.NewValidationf("invalid definition %s: widgetId and mdlName are required", entry.Name())
157184
}
158185

159-
if err := validateDefinitionOperations(&def, entry.Name()); err != nil {
186+
if err := r.validateDefinitionOperations(&def, entry.Name()); err != nil {
160187
return err
161188
}
162189

@@ -180,22 +207,22 @@ func (r *WidgetRegistry) loadDefinitionsFromDir(dir string) error {
180207
// validateDefinitionOperations checks that all operation names in a definition
181208
// are recognized by the known operations set, and validates source/operation
182209
// compatibility and mapping order dependencies.
183-
func validateDefinitionOperations(def *WidgetDefinition, source string) error {
184-
if err := validateMappings(def.PropertyMappings, source, ""); err != nil {
210+
func (r *WidgetRegistry) validateDefinitionOperations(def *WidgetDefinition, source string) error {
211+
if err := r.validateMappings(def.PropertyMappings, source, ""); err != nil {
185212
return err
186213
}
187214
for _, s := range def.ChildSlots {
188-
if !knownOperations[s.Operation] {
215+
if !r.knownOperations[s.Operation] {
189216
return mdlerrors.NewValidationf("%s: unknown operation %q in childSlots for key %q", source, s.Operation, s.PropertyKey)
190217
}
191218
}
192219
for _, mode := range def.Modes {
193220
ctx := fmt.Sprintf("mode %q ", mode.Name)
194-
if err := validateMappings(mode.PropertyMappings, source, ctx); err != nil {
221+
if err := r.validateMappings(mode.PropertyMappings, source, ctx); err != nil {
195222
return err
196223
}
197224
for _, s := range mode.ChildSlots {
198-
if !knownOperations[s.Operation] {
225+
if !r.knownOperations[s.Operation] {
199226
return mdlerrors.NewValidationf("%s: unknown operation %q in %schildSlots for key %q", source, s.Operation, ctx, s.PropertyKey)
200227
}
201228
}
@@ -213,10 +240,10 @@ var incompatibleSourceOps = map[string]map[string]bool{
213240

214241
// validateMappings validates a slice of property mappings for operation existence,
215242
// source/operation compatibility, and mapping order (Association requires prior DataSource).
216-
func validateMappings(mappings []PropertyMapping, source, modeCtx string) error {
243+
func (r *WidgetRegistry) validateMappings(mappings []PropertyMapping, source, modeCtx string) error {
217244
hasDataSource := false
218245
for _, m := range mappings {
219-
if !knownOperations[m.Operation] {
246+
if !r.knownOperations[m.Operation] {
220247
return mdlerrors.NewValidationf("%s: unknown operation %q in %spropertyMappings for key %q", source, m.Operation, modeCtx, m.PropertyKey)
221248
}
222249
// Check source/operation compatibility

mdl/executor/widget_registry_test.go

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,41 @@ func TestRegistryLoadUserDefinitions(t *testing.T) {
189189
}
190190
}
191191

192+
func TestNewWidgetRegistryWithOps_ExtendsKnownOperations(t *testing.T) {
193+
// A definition with a custom operation should fail with default ops
194+
customDef := &WidgetDefinition{
195+
WidgetID: "com.example.Custom",
196+
MDLName: "CUSTOM",
197+
PropertyMappings: []PropertyMapping{
198+
{PropertyKey: "prop", Source: "Attribute", Operation: "customOp"},
199+
},
200+
}
201+
202+
// Default registry should reject custom operation
203+
defaultReg, err := NewWidgetRegistry()
204+
if err != nil {
205+
t.Fatalf("NewWidgetRegistry() error: %v", err)
206+
}
207+
if err := defaultReg.validateDefinitionOperations(customDef, "custom.def.json"); err == nil {
208+
t.Error("expected error for unknown operation 'customOp' with default ops, got nil")
209+
}
210+
211+
// Extended registry should accept custom operation
212+
extReg, err := NewWidgetRegistryWithOps(map[string]bool{"customOp": true})
213+
if err != nil {
214+
t.Fatalf("NewWidgetRegistryWithOps() error: %v", err)
215+
}
216+
if err := extReg.validateDefinitionOperations(customDef, "custom.def.json"); err != nil {
217+
t.Errorf("unexpected error with extended ops: %v", err)
218+
}
219+
}
220+
192221
func TestValidateDefinitionOperations_MappingOrderDependency(t *testing.T) {
222+
reg, err := NewWidgetRegistry()
223+
if err != nil {
224+
t.Fatalf("NewWidgetRegistry() error: %v", err)
225+
}
226+
193227
// Association before DataSource should fail validation
194228
badDef := &WidgetDefinition{
195229
WidgetID: "com.example.Bad",
@@ -199,7 +233,7 @@ func TestValidateDefinitionOperations_MappingOrderDependency(t *testing.T) {
199233
{PropertyKey: "dsProp", Source: "DataSource", Operation: "datasource"},
200234
},
201235
}
202-
if err := validateDefinitionOperations(badDef, "bad.def.json"); err == nil {
236+
if err := reg.validateDefinitionOperations(badDef, "bad.def.json"); err == nil {
203237
t.Error("expected error for Association before DataSource, got nil")
204238
}
205239

@@ -212,7 +246,7 @@ func TestValidateDefinitionOperations_MappingOrderDependency(t *testing.T) {
212246
{PropertyKey: "assocProp", Source: "Association", Operation: "association"},
213247
},
214248
}
215-
if err := validateDefinitionOperations(goodDef, "good.def.json"); err != nil {
249+
if err := reg.validateDefinitionOperations(goodDef, "good.def.json"); err != nil {
216250
t.Errorf("unexpected error for DataSource before Association: %v", err)
217251
}
218252

@@ -230,12 +264,17 @@ func TestValidateDefinitionOperations_MappingOrderDependency(t *testing.T) {
230264
},
231265
},
232266
}
233-
if err := validateDefinitionOperations(modeDef, "mode.def.json"); err == nil {
267+
if err := reg.validateDefinitionOperations(modeDef, "mode.def.json"); err == nil {
234268
t.Error("expected error for Association before DataSource in mode, got nil")
235269
}
236270
}
237271

238272
func TestValidateDefinitionOperations_SourceOperationCompatibility(t *testing.T) {
273+
reg, err := NewWidgetRegistry()
274+
if err != nil {
275+
t.Fatalf("NewWidgetRegistry() error: %v", err)
276+
}
277+
239278
// Source "Attribute" with Operation "association" should fail
240279
badDef := &WidgetDefinition{
241280
WidgetID: "com.example.Bad",
@@ -244,7 +283,7 @@ func TestValidateDefinitionOperations_SourceOperationCompatibility(t *testing.T)
244283
{PropertyKey: "prop", Source: "Attribute", Operation: "association"},
245284
},
246285
}
247-
if err := validateDefinitionOperations(badDef, "bad.def.json"); err == nil {
286+
if err := reg.validateDefinitionOperations(badDef, "bad.def.json"); err == nil {
248287
t.Error("expected error for Source='Attribute' with Operation='association', got nil")
249288
}
250289

@@ -256,7 +295,7 @@ func TestValidateDefinitionOperations_SourceOperationCompatibility(t *testing.T)
256295
{PropertyKey: "prop", Source: "Association", Operation: "attribute"},
257296
},
258297
}
259-
if err := validateDefinitionOperations(badDef2, "bad2.def.json"); err == nil {
298+
if err := reg.validateDefinitionOperations(badDef2, "bad2.def.json"); err == nil {
260299
t.Error("expected error for Source='Association' with Operation='attribute', got nil")
261300
}
262301
}

0 commit comments

Comments
 (0)