Skip to content

Commit

Permalink
feat: reference registry handling working
Browse files Browse the repository at this point in the history
  • Loading branch information
lukasjarosch committed Mar 6, 2024
1 parent 7bdbf38 commit 8a0c475
Show file tree
Hide file tree
Showing 5 changed files with 162 additions and 16 deletions.
38 changes: 25 additions & 13 deletions reference.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,19 +177,12 @@ func (manager *ValueReferenceManager) ReplaceReferences() error {
// If the source implements the [HookableRegisterScope] interface,
// the 'postRegisterScopeHook' is registered
func (manager *ValueReferenceManager) registerHooks() {
// We need to be aware of every 'write' (Set) operation
// within the source.
manager.source.RegisterPostSetHook(manager.postSetHook())

// In case we're dealing with a Registry, we also need to
// track any new classes being added to it.
if regSource, ok := manager.source.(HookableRegisterClass); ok {
if regSource, ok := manager.source.(HookablePostRegisterClass); ok {
regSource.RegisterPostRegisterClassHook(manager.postRegisterClassHook())
}

// In case we're dealing with an Inventory, we also need to
// track any new scopes being added.
if invSource, ok := manager.source.(HookableRegisterScope); ok {
if invSource, ok := manager.source.(HookablePostRegisterScope); ok {
invSource.RegisterPostRegisterScopeHook(manager.postRegisterScopeHook())
}
}
Expand Down Expand Up @@ -331,14 +324,33 @@ func (manager *ValueReferenceManager) postSetHook() SetHookFunc {

// TODO: dont forget to register the class hooks on the new class(es)!

func (manager *ValueReferenceManager) postRegisterScopeHook() RegisterScopeHookFunc {
return func(scope Scope, registry *Registry) error {
func (manager *ValueReferenceManager) postRegisterClassHook() RegisterClassHookFunc {
return func(class *Class) error {
// We cannot just use the class as source for the 'FindAllValueReferences' call.
// This is because it will then make all reference target paths absolute to the class.
// That's not what we want, the paths need to be relative to the managers source.
// Hence we need to re-discover all references within the managers source.
references, err := reference.FindAllValueReferences(manager.source)
if err != nil {
return err
}

// Add all references which originate from the newly added class.
for _, ref := range references {
if ref.Path.HasPrefix(class.Identifier) {
err = manager.addReference(ref)
if err != nil {
return err
}
}
}

return nil
}
}

func (manager *ValueReferenceManager) postRegisterClassHook() RegisterClassHookFunc {
return func(class *Class) error {
func (manager *ValueReferenceManager) postRegisterScopeHook() RegisterScopeHookFunc {
return func(scope Scope, registry *Registry) error {
return nil
}
}
Expand Down
89 changes: 89 additions & 0 deletions reference_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ func TestValueManager_SetHooks_Registry(t *testing.T) {
manager, err := NewValueReferenceManager(registry)
assert.NoError(t, err)

// These are the same tests as above, just to be sure that also works.
t.Run("expect no change if no reference is added/removed", func(t *testing.T) {
preReferenceCount := len(manager.AllReferences())
err = person.Set("test1", data.NewValue("hello there"))
Expand All @@ -114,6 +115,26 @@ func TestValueManager_SetHooks_Registry(t *testing.T) {
err = person.Set("valid.new.test5", data.NewValue("${valid:this:is:invalid}"))
assert.NoError(t, err)
})

// These tests are registry-specific as they use the postRegisterClass hook
t.Run("register a new class with only valid references", func(t *testing.T) {
common, err := NewClass("testdata/references/registry/common.yaml", codec.NewYamlCodec(), data.NewPath("common"))
assert.NoError(t, err)

err = registry.RegisterClass(common)
assert.NoError(t, err)
})
t.Run("registering a new class with invalid references must fail", func(t *testing.T) {
registry := NewRegistry()
NewValueReferenceManager(registry)

// the common class is the only class, hence some references are invalid
common, err := NewClass("testdata/references/registry/common.yaml", codec.NewYamlCodec(), data.NewPath("common"))
assert.NoError(t, err)

err = registry.RegisterClass(common)
assert.ErrorIs(t, err, ErrInvalidReferenceTargetPath)
})
}

func TestValueManager_ReplaceReferences(t *testing.T) {
Expand Down Expand Up @@ -192,3 +213,71 @@ func TestValueManager_ReplaceReferences(t *testing.T) {
}
})
}

func TestValueManager_ReplaceReferences_Registry(t *testing.T) {
person, err := NewClass("testdata/references/registry/person.yaml", codec.NewYamlCodec(), data.NewPath("person"))
assert.NoError(t, err)
greeting, err := NewClass("testdata/references/registry/greeting.yaml", codec.NewYamlCodec(), data.NewPath("greeting"))
assert.NoError(t, err)

registry := NewRegistry()
err = registry.RegisterClass(person)
assert.NoError(t, err)
err = registry.RegisterClass(greeting)
assert.NoError(t, err)

manager, err := NewValueReferenceManager(registry)
assert.NoError(t, err)

expected := map[string]data.Value{
"person.n": data.NewValue(35),
"greeting.casual": data.NewValue("Hey, John"),
}

t.Run("replace valid registry", func(t *testing.T) {
err = manager.ReplaceReferences()
assert.NoError(t, err)
for path, expectedValue := range expected {
val, err := registry.Get(path)
assert.NoError(t, err)
assert.Equal(t, expectedValue.Raw, val.Raw)
}
})

t.Run("replacing should be idempotent", func(t *testing.T) {
err := manager.ReplaceReferences()
assert.NoError(t, err)
for path, expectedValue := range expected {
val, err := registry.Get(path)
assert.NoError(t, err)
assert.Equal(t, expectedValue.Raw, val.Raw)
}

err = manager.ReplaceReferences()
assert.NoError(t, err)
for path, expectedValue := range expected {
val, err := registry.Get(path)
assert.NoError(t, err)
assert.Equal(t, expectedValue.Raw, val.Raw)
}
})

t.Run("replace again after adding a new class", func(t *testing.T) {
common, err := NewClass("testdata/references/registry/common.yaml", codec.NewYamlCodec(), data.NewPath("common"))
assert.NoError(t, err)
err = registry.RegisterClass(common)
assert.NoError(t, err)

// extend expected map
expected["common.bar"] = data.NewValue("bar")
expected["common.greeting"] = data.NewValue("Hey,")

err = manager.ReplaceReferences()
assert.NoError(t, err)
for path, expectedValue := range expected {
val, err := registry.Get(path)
assert.NoError(t, err)
assert.Equal(t, expectedValue.Raw, val.Raw)
}
})
}
44 changes: 42 additions & 2 deletions registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ type Registry struct {
// classes map a classIdentifier string to the actual classes
classes map[string]*Class
// paths map absolute data paths to a classIdentifier string (key of the classes map)
paths map[string]string
paths map[string]string
preRegisterClassHooks []RegisterClassHookFunc
postRegisterClassHooks []RegisterClassHookFunc
}

// NewRegistry returns a new, empty, registry.
Expand Down Expand Up @@ -105,12 +107,22 @@ func (reg *Registry) RegisterClass(class *Class) error {
class.RegisterPreSetHook(reg.classPreSetHook(class))
class.RegisterPostSetHook(reg.classPostSetHook(class))

// register class and all its paths
// register class and all its paths and call the hooks
err := reg.callPreRegisterClassHooks(class)
if err != nil {
return err
}

for _, classPath := range classPaths {
reg.paths[classPath] = class.Identifier.String()
}
reg.classes[class.Identifier.String()] = class

err = reg.callPostRegisterClassHooks(class)
if err != nil {
return err
}

return nil
}

Expand Down Expand Up @@ -320,3 +332,31 @@ func (reg *Registry) AllPaths() map[string]data.Path {
}
return paths
}

func (reg *Registry) RegisterPreRegisterClassHook(hook RegisterClassHookFunc) {
reg.preRegisterClassHooks = append(reg.preRegisterClassHooks, hook)
}

func (reg *Registry) RegisterPostRegisterClassHook(hook RegisterClassHookFunc) {
reg.postRegisterClassHooks = append(reg.postRegisterClassHooks, hook)
}

func (reg *Registry) callPreRegisterClassHooks(class *Class) error {
for _, hook := range reg.preRegisterClassHooks {
err := hook(class)
if err != nil {
return err
}
}
return nil
}

func (reg *Registry) callPostRegisterClassHooks(class *Class) error {
for _, hook := range reg.postRegisterClassHooks {
err := hook(class)
if err != nil {
return err
}
}
return nil
}
4 changes: 4 additions & 0 deletions testdata/references/registry/common.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
common:
foo: bar
bar: ${foo}
greeting: ${greeting:prefix}
3 changes: 2 additions & 1 deletion testdata/references/registry/greeting.yaml
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
greeting:
casual: "Hey, ${person:first_name}"
casual: "${prefix} ${person:first_name}"
prefix: "Hey,"

0 comments on commit 8a0c475

Please sign in to comment.