diff --git a/.mockery.yaml b/.mockery.yaml index dc32868..c13bcf8 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -3,8 +3,9 @@ dir: "mocks/" packages: github.com/lukasjarosch/skipper: interfaces: - Codec: - DataContainer: - ReferenceValueSource: - ReferenceSourceGetter: - ReferenceSourceRelativeGetter: + ValueReferenceSource: + # Codec: + # DataContainer: + # ReferenceValueSource: + # ReferenceSourceGetter: + # ReferenceSourceRelativeGetter: diff --git a/mocks/mock_ValueReferenceSource.go b/mocks/mock_ValueReferenceSource.go new file mode 100644 index 0000000..d8dfff8 --- /dev/null +++ b/mocks/mock_ValueReferenceSource.go @@ -0,0 +1,133 @@ +// Code generated by mockery v2.34.2. DO NOT EDIT. + +package skipper + +import ( + data "github.com/lukasjarosch/skipper/data" + mock "github.com/stretchr/testify/mock" +) + +// MockValueReferenceSource is an autogenerated mock type for the ValueReferenceSource type +type MockValueReferenceSource struct { + mock.Mock +} + +type MockValueReferenceSource_Expecter struct { + mock *mock.Mock +} + +func (_m *MockValueReferenceSource) EXPECT() *MockValueReferenceSource_Expecter { + return &MockValueReferenceSource_Expecter{mock: &_m.Mock} +} + +// AbsolutePath provides a mock function with given fields: path, context +func (_m *MockValueReferenceSource) AbsolutePath(path data.Path, context data.Path) (data.Path, error) { + ret := _m.Called(path, context) + + var r0 data.Path + var r1 error + if rf, ok := ret.Get(0).(func(data.Path, data.Path) (data.Path, error)); ok { + return rf(path, context) + } + if rf, ok := ret.Get(0).(func(data.Path, data.Path) data.Path); ok { + r0 = rf(path, context) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(data.Path) + } + } + + if rf, ok := ret.Get(1).(func(data.Path, data.Path) error); ok { + r1 = rf(path, context) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockValueReferenceSource_AbsolutePath_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AbsolutePath' +type MockValueReferenceSource_AbsolutePath_Call struct { + *mock.Call +} + +// AbsolutePath is a helper method to define mock.On call +// - path data.Path +// - context data.Path +func (_e *MockValueReferenceSource_Expecter) AbsolutePath(path interface{}, context interface{}) *MockValueReferenceSource_AbsolutePath_Call { + return &MockValueReferenceSource_AbsolutePath_Call{Call: _e.mock.On("AbsolutePath", path, context)} +} + +func (_c *MockValueReferenceSource_AbsolutePath_Call) Run(run func(path data.Path, context data.Path)) *MockValueReferenceSource_AbsolutePath_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(data.Path), args[1].(data.Path)) + }) + return _c +} + +func (_c *MockValueReferenceSource_AbsolutePath_Call) Return(_a0 data.Path, _a1 error) *MockValueReferenceSource_AbsolutePath_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockValueReferenceSource_AbsolutePath_Call) RunAndReturn(run func(data.Path, data.Path) (data.Path, error)) *MockValueReferenceSource_AbsolutePath_Call { + _c.Call.Return(run) + return _c +} + +// Values provides a mock function with given fields: +func (_m *MockValueReferenceSource) Values() map[string]data.Value { + ret := _m.Called() + + var r0 map[string]data.Value + if rf, ok := ret.Get(0).(func() map[string]data.Value); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]data.Value) + } + } + + return r0 +} + +// MockValueReferenceSource_Values_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Values' +type MockValueReferenceSource_Values_Call struct { + *mock.Call +} + +// Values is a helper method to define mock.On call +func (_e *MockValueReferenceSource_Expecter) Values() *MockValueReferenceSource_Values_Call { + return &MockValueReferenceSource_Values_Call{Call: _e.mock.On("Values")} +} + +func (_c *MockValueReferenceSource_Values_Call) Run(run func()) *MockValueReferenceSource_Values_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockValueReferenceSource_Values_Call) Return(_a0 map[string]data.Value) *MockValueReferenceSource_Values_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockValueReferenceSource_Values_Call) RunAndReturn(run func() map[string]data.Value) *MockValueReferenceSource_Values_Call { + _c.Call.Return(run) + return _c +} + +// NewMockValueReferenceSource creates a new instance of MockValueReferenceSource. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockValueReferenceSource(t interface { + mock.TestingT + Cleanup(func()) +}) *MockValueReferenceSource { + mock := &MockValueReferenceSource{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/reference.go b/reference.go index cfc14c4..3499037 100644 --- a/reference.go +++ b/reference.go @@ -3,11 +3,12 @@ package skipper import ( "errors" "fmt" + "os" "regexp" "strings" - "github.com/davecgh/go-spew/spew" "github.com/dominikbraun/graph" + "github.com/dominikbraun/graph/draw" "github.com/lukasjarosch/skipper/data" ) @@ -17,9 +18,9 @@ import ( // TODO: test with Inventory var ( - // ReferenceRegex defines the strings which are valid references + // ValueReferenceRegex defines the strings which are valid references // See: https://regex101.com/r/lIuuep/1 - ReferenceRegex = regexp.MustCompile(`\${(?P[\w-]+(?:\:[\w-]+)*)}`) + ValueReferenceRegex = regexp.MustCompile(`\${(?P[\w-]+(?:\:[\w-]+)*)}`) ErrUndefinedReferenceTarget = fmt.Errorf("undefined reference target path") ErrReferenceSourceIsNil = fmt.Errorf("reference source is nil") @@ -27,6 +28,12 @@ var ( ErrCyclicReference = fmt.Errorf("cyclic reference") ) +type ValueReferenceSource interface { + AbsolutePathMaker + // Values must return a map of absolute (!) paths to their respective [data.Value]s. + Values() map[string]data.Value +} + // ValueReference is a reference to a value with a different path. type ValueReference struct { // Path is the path where the reference is defined @@ -35,344 +42,211 @@ type ValueReference struct { TargetPath data.Path // AbsoluteTargetPath is the TargetPath, but absolute to the source AbsoluteTargetPath data.Path - // If the value to which the reference (AbsoluteTargetPath) points to - // also contains references, then these are added as ChildReferences. - ChildReferences []ValueReference } func (ref ValueReference) Name() string { return fmt.Sprintf("${%s}", strings.ReplaceAll(ref.TargetPath.String(), ".", ":")) } -type ReferenceValueSource interface { - DataGetter - AbsolutePathMaker - // Values must return a map of absolute paths to their respective values. - // If the paths are not absolute then working with references will cause - // all sort of problems. - Values() map[string]data.Value +func (ref ValueReference) Hash() string { + return fmt.Sprintf("%s|%s", ref.Path, ref.AbsoluteTargetPath) +} + +// ValueReferenceManager is responsible for handling [ValueReference]s. +// It can parse, resolve and replace any value references. +type ValueReferenceManager struct { + source ValueReferenceSource + // allReferences contains all found references, even duplicates + allReferences []ValueReference + // references maps reference hashes to references, hence being + // the deduplicated version of allReferences + references map[string]ValueReference + // stores all references and their dependencies + dependencyGraph graph.Graph[string, ValueReference] } -// ParseValueReferences will, given the [ReferenceValueSource] extract a list -// of [ValueReference]s which can then be resolved and ultimately replaced. -func ParseValueReferences(source ReferenceValueSource) ([]ValueReference, error) { +// TODO: needs to provide function which is hooked into PostSetPath in each class +// This is because references need to be re-evaluated if anything changes +func NewValueReferenceManager(source ValueReferenceSource) (*ValueReferenceManager, error) { if source == nil { return nil, ErrReferenceSourceIsNil } - // Discover all references within the source. - var references []ValueReference - for path, value := range source.Values() { - refs, err := FindValueReferences(source, data.NewPath(path), value) - if err != nil { - return nil, err - } - references = append(references, refs...) + m := &ValueReferenceManager{ + source: source, } - // Now, for each reference we need to determine whether their targetValue contains - // references again. If it does, add them as ChildReferences. - for _, ref := range references { - val, err := source.GetPath(ref.AbsoluteTargetPath) - if err != nil { - return nil, err - } - - childReferences, err := FindValueReferences(source, ref.AbsoluteTargetPath, val) - if err != nil { - return nil, err - } - - ref.ChildReferences = append(ref.ChildReferences, childReferences...) - spew.Dump(val) + references, err := FindValueReferences(m.source) + if err != nil { + return nil, err } + m.allReferences = references - return references, nil -} - -func FindValueReferences(source AbsolutePathMaker, path data.Path, value data.Value) ([]ValueReference, error) { - var references []ValueReference - referenceMatches := ReferenceRegex.FindAllStringSubmatch(value.String(), -1) - if referenceMatches != nil { - for _, match := range referenceMatches { - // References can be relative to a Class. But that is hard to work with within a Registry or Inventory - // as a class-relative path can be valid within multiple classes / scopes. - // Hence an absolute path is resolved which we will be working with from now on. - // This works because the path returned by the 'Values()' call is expected to be - // absolute already. This then defines the context in which the reference is defined - // which in turn is used to resolve the absolute path to which the reference points to. - absPath, err := source.AbsolutePath(ReferencePathToPath(match[1]), path) - if err != nil { - return nil, fmt.Errorf("unable to resolve absolute path of '%s': %w", match[1], err) - } - - references = append(references, ValueReference{ - Path: path, - TargetPath: ReferencePathToPath(match[1]), - AbsoluteTargetPath: absPath, - }) - } + // deduplicate references in map + for _, ref := range m.allReferences { + m.references[ref.Hash()] = ref } - return references, nil -} -type OldParseSource interface { - DataWalker - AbsolutePathMaker + return m, nil } -// ParseReferences will use the [ReferenceSourceWalker] to traverse all values -// and search for References within those values. -// The returned slice of references contains all found references, even -// duplicates if a reference is used multiple times. -func ParseReferences(source OldParseSource) ([]ValueReference, error) { - if source == nil { - return nil, ErrReferenceSourceIsNil - } - - var references []ValueReference - source.Walk(func(path data.Path, value data.Value, isLeaf bool) error { - if !isLeaf { - return nil - } - referenceMatches := ReferenceRegex.FindAllStringSubmatch(value.String(), -1) - - if referenceMatches != nil { - for _, match := range referenceMatches { - absPath, err := source.AbsolutePath(ReferencePathToPath(match[1]), path) - if err != nil { - return fmt.Errorf("unable to resolve absolute path of '%s': %w", match[1], err) - } - ref := ValueReference{ - Path: path, - TargetPath: ReferencePathToPath(match[1]), - AbsoluteTargetPath: absPath, - } - - references = append(references, ref) - } +func CalculateReplacementOrder(dependencyGraph graph.Graph[string, ValueReference]) ([]ValueReference, error) { + // Perform a stable topological sort of the dependency graph. + // The returned orderedHashes is stable in that the references are sorted + // by their name length or alphabetically if they are the same length. + // This eliminates the issue that the actual topological sorting algorithm usually + // has multiple valid solutions. + orderedHashes, err := graph.StableTopologicalSort[string, ValueReference](dependencyGraph, func(s1, s2 string) bool { + // Strings are of different length, sort by length + if len(s1) != len(s2) { + return len(s1) < len(s2) } - - return nil + // Otherwise, sort alphabetically + return s1 > s2 }) + if err != nil { + return nil, fmt.Errorf("failed to sort reference graph: %w", err) + } - return references, nil -} - -// ReferencesInValue returns all references within the passed value. -// Note that the returned references do not have a 'Path' set! -// TODO: replace -func ReferencesInValue(value data.Value) []ValueReference { - var references []ValueReference - - referenceMatches := ReferenceRegex.FindAllStringSubmatch(value.String(), -1) - if referenceMatches != nil { - for _, match := range referenceMatches { - references = append(references, ValueReference{ - TargetPath: ReferencePathToPath(match[1]), - Path: data.Path{}, - }) + // The result of the topological sorting is reverse to what we want. + // We expect the ValueReference without any dependencies to be at the top. + var reversedOrder []ValueReference + for i := len(orderedHashes) - 1; i >= 0; i-- { + ref, err := dependencyGraph.Vertex(orderedHashes[i]) + if err != nil { + return nil, err } + reversedOrder = append(reversedOrder, ref) } - return references + return reversedOrder, nil } -type referenceVertex struct { - ValueReference - RawValue data.Value -} - -type ReferenceSource interface { - DataSetterGetter - DataWalker -} - -type ReferenceSourceRelativeGetter interface { - DataGetter - GetClassRelativePath(data.Path, data.Path) (data.Value, error) -} - -// ResolveReferences will resolve dependencies between all given references and return -// a sorted slice of the same references. This represents the order in which references should -// be replaced without causing dependency issues. -// -// Note that the list of references passed must contain all existing references, even duplicates. -func ResolveReferences(references []ValueReference, resolveSource DataGetter) ([]ValueReference, error) { - if resolveSource == nil { - return nil, ErrReferenceSourceIsNil - } - - // used by the graph to tell vertecies apart - referenceVertexHash := func(ref referenceVertex) string { - return ref.TargetPath.String() +func BuildDependencyGraph(references map[string]ValueReference) (graph.Graph[string, ValueReference], error) { + vertexHashFunc := func(ref ValueReference) string { + return ref.Hash() } - g := graph.New(referenceVertexHash, graph.Acyclic(), graph.Directed(), graph.PreventCycles()) + dependencyGraph := graph.New(vertexHashFunc, graph.Acyclic(), graph.Directed(), graph.PreventCycles()) - // Register all references as vertecies - var vertecies []referenceVertex - for _, ref := range references { - rawValue, err := resolveSource.GetPath(ref.TargetPath) - if err != nil { - // In case the path cannot be resolved it might be a class-local reference. - // We can attempt to resolve the Class since we know where the reference is located. - if errors.Is(err, ErrPathNotFound) { - if relativeSource, ok := resolveSource.(ReferenceSourceRelativeGetter); ok { - rawValue, err = relativeSource.GetClassRelativePath(ref.Path, ref.TargetPath) - if err != nil { - return nil, fmt.Errorf("%w: %s at path '%s': %w", ErrUndefinedReferenceTarget, ref.Name(), ref.Path, err) - } - } - return nil, fmt.Errorf("%w: %s at path '%s': %w", ErrUndefinedReferenceTarget, ref.Name(), ref.Path, err) - } else { - return nil, fmt.Errorf("%w: %s at path '%s': %w", ErrUndefinedReferenceTarget, ref.Name(), ref.Path, err) - } - } - node := referenceVertex{ValueReference: ref, RawValue: rawValue} - err = g.AddVertex(node) + // Tegister all references as graph vertecies + for _, reference := range references { + err := dependencyGraph.AddVertex(reference) if err != nil { - // References can occur multiple times, but we only need to resolve them once. - // So we can ignore the ErrVertexAlreadyExists error. - if !errors.Is(err, graph.ErrVertexAlreadyExists) { - return nil, err - } + return nil, fmt.Errorf("cannot add reference %s: %w", reference.Name(), err) } - vertecies = append(vertecies, node) } - // Create edges between dependent references by looking at the actual value the 'TargetPath' points to. - // If that value contains more references, then these are dependencies of the currently examined node (reference). - for _, referenceVertex := range vertecies { - referenceDependencies := ReferencesInValue(referenceVertex.RawValue) + // Create edges between dependent references + for _, reference := range references { + dependencies := ResolveDependantValueReferences(reference, references) - for _, referenceDependency := range referenceDependencies { - // The dependency of the current vertex must already be a vertex in the graph; fetch it. - n, err := g.Vertex(referenceDependency.TargetPath.String()) + for _, dependency := range dependencies { + dependencyVertex, err := dependencyGraph.Vertex(dependency.Hash()) if err != nil { - return nil, fmt.Errorf("%s: %w", referenceDependency.Name(), err) + return nil, fmt.Errorf("unexpectedly could not fetch reference vertex %s: %w", dependency.Hash(), err) } - if referenceVertex.TargetPath.Equals(n.TargetPath) { - return nil, fmt.Errorf("%s: %w", referenceVertex.Name(), ErrSelfReferencingReference) + // prevent self-referencing references + if dependencyVertex.AbsoluteTargetPath.Equals(reference.AbsoluteTargetPath) { + return nil, fmt.Errorf("%s: %w", reference.Name(), ErrSelfReferencingReference) } - err = g.AddEdge(referenceVertex.TargetPath.String(), n.TargetPath.String()) + err = dependencyGraph.AddEdge(reference.Hash(), dependency.Hash()) if err != nil { if errors.Is(err, graph.ErrEdgeCreatesCycle) { - return nil, fmt.Errorf("%s -> %s: %w", referenceVertex.Name(), referenceDependency.Name(), ErrCyclicReference) - } - // If the edge already exists, we do not need to add it again. Hence we ignore that error. - if !errors.Is(err, graph.ErrEdgeAlreadyExists) { - return nil, err + return nil, fmt.Errorf("%s -> %s: %w", reference.Name(), dependency.Name(), ErrCyclicReference) } + return nil, fmt.Errorf("failed to register dependency %s: %w", dependency.Name(), err) } } } - // Perform a stable topological sort of the dependency graph. - // The returned order is stable in that the references are sorted - // by their name length or alphabetically if they are the same length. - // This eliminates the issue that the actual topological sorting algorithm usually - // has multiple valid solutions. - order, err := graph.StableTopologicalSort[string, referenceVertex](g, func(s1, s2 string) bool { - // Strings are of different length, sort by length - if len(s1) != len(s2) { - return len(s1) < len(s2) - } - // Otherwise, sort alphabetically - return s1 > s2 - }) + return dependencyGraph, nil +} + +func VisualizeDependencyGraph(graph graph.Graph[string, ValueReference], filePath string, label string) error { + file, err := os.Create(filePath) if err != nil { - return nil, fmt.Errorf("failed to sort reference graph: %w", err) + return err } - - // Perform TopologicalSort which returns a list of strings (TargetPaths) - // starting with the one which has the most dependencies. - // order, err := graph.TopologicalSort[string, referenceVertex](g) - // if err != nil { - // return nil, err - // } - - // We need to reverse the result from the TopologicalSort. - // This is because the reference without dependencies will be sorted 'at the end'. - // But we want to resolve them first. - var reversedOrder []data.Path - for i := len(order) - 1; i >= 0; i-- { - reversedOrder = append(reversedOrder, data.NewPath(order[i])) + err = draw.DOT(graph, file, + draw.GraphAttribute("label", label), + draw.GraphAttribute("overlap", "prism")) + if err != nil { + return err } - // Now that we have the order in which references must be replaced, - // lets finally re-order the passed (non-deduplicated) input references. - var orderedReferences []ValueReference - for _, refOrder := range reversedOrder { - for _, ref := range references { - if ref.TargetPath.Equals(refOrder) { - orderedReferences = append(orderedReferences, ref) - } - } - } + return nil +} - // sanity check - if len(orderedReferences) != len(references) { - return nil, fmt.Errorf("unexpected amount of resolved references, this is a bug") +func ResolveDependantValueReferences(reference ValueReference, allReferences map[string]ValueReference) []ValueReference { + var dependencies []ValueReference + + // If the reference's AbsoluteTargetPath is a Path of any existing reference, + // the references depend on each other. + for _, ref := range allReferences { + if reference.AbsoluteTargetPath.Equals(ref.Path) { + dependencies = append(dependencies, ref) + } } - return orderedReferences, nil + return dependencies } -type ReferenceSourceSetter interface { - SetPath(data.Path, interface{}) error -} +// FindValueReferences searches for all ValueReferences within the given ValueReferenceSource. +func FindValueReferences(source ValueReferenceSource) ([]ValueReference, error) { + var references []ValueReference + var errs error -func ReplaceReferences(references []ValueReference, source DataSetterGetter) error { - if source == nil { - return ErrReferenceSourceIsNil - } + for path, value := range source.Values() { + referenceTargetPaths := FindReferenceTargetPaths(ValueReferenceRegex, value) - for _, reference := range references { - targetValue, err := source.GetPath(reference.TargetPath) - if err != nil { - // In case the path cannot be resolved it might be a class-local reference. - // We can attempt to resolve the Class since we know where the reference is located. - if errors.Is(err, ErrPathNotFound) { - if relativeSource, ok := source.(ReferenceSourceRelativeGetter); ok { - targetValue, err = relativeSource.GetClassRelativePath(reference.Path, reference.TargetPath) - if err != nil { - return fmt.Errorf("cannot resolve reference target path: %w", err) - } - } - } else { - return fmt.Errorf("cannot resolve reference target path: %w", err) + // Create ValueReference structs for every found referenceTargetPaths + for _, refTargetPath := range referenceTargetPaths { + absTargetPath, err := source.AbsolutePath(refTargetPath, data.NewPath(path)) + if err != nil { + return nil, errors.Join(errs, err) } - } - sourceValue, err := source.GetPath(reference.Path) - if err != nil { - return fmt.Errorf("cannot resolve reference path: %w", err) - } - - // If the sourceValue only contains the reference, then - // we just use the 'SetPath' function in order to preserve the type of targetValue. - // This is required to allow replacing maps and arrays. - if len(sourceValue.String()) == len(reference.Name()) { - err = source.SetPath(reference.Path, targetValue.Raw) - if err != nil { - return err + ref := ValueReference{ + Path: data.NewPath(path), + TargetPath: refTargetPath, + AbsoluteTargetPath: absTargetPath, } - continue + references = append(references, ref) } + } - // In this case the reference is 'embedded', e.g. "Hello there ${name}", - // therefore we can only perform a string replacement to not erase the surrounding context. - replacedValue := strings.Replace(sourceValue.String(), reference.Name(), targetValue.String(), 1) - err = source.SetPath(reference.Path, replacedValue) - if err != nil { - return err + if errs != nil { + return nil, errs + } + + return references, nil +} + +// FindReferenceTargetPaths yields all the targetPaths of any references contained within the value. +// If the value is 'foo ${bar:baz}', then the returned path would be `bar.baz`. +// The returned paths should be considered relative! +func FindReferenceTargetPaths(regex *regexp.Regexp, value data.Value) []data.Path { + var targetPaths []data.Path + + referenceMatches := regex.FindAllStringSubmatch(value.String(), -1) + if referenceMatches == nil { + return nil + } + + for _, match := range referenceMatches { + // If the regex itself is malformed, we can do nothing but panic. + // The first element (match[0]) will be the full string (aka the input). + // And the second element is expected to contain the part between the brackets. + if len(match) < 2 { + panic("regex match has not enough elements; this is a bug in the regex itself!") } + targetPaths = append(targetPaths, ReferencePathToPath(match[1])) } - return nil + return targetPaths } // ReferencePathToPath converts the path used within references (colon-separated) to a proper [data.Path] diff --git a/reference_test.go b/reference_test.go index c6bfdb6..fd16f85 100644 --- a/reference_test.go +++ b/reference_test.go @@ -1,1155 +1,499 @@ package skipper_test -// func TestParseValueReferencesNilReference(t *testing.T) { -// _, err := ParseValueReferences(nil) -// assert.ErrorIs(t, err, ErrReferenceSourceIsNil) -// } -// -// func TestParseValueReferencesNew(t *testing.T) { -// type input struct { -// path data.Path -// targetPath data.Path -// absoluteTargetPath data.Path -// targetValue data.Value -// children []input // only one layer deep! -// } -// tests := []struct { -// name string -// input []input -// expected []ValueReference -// }{ -// { -// name: "asdf", -// input: []input{ -// { -// path: data.NewPath("abs.ref.path"), -// targetPath: data.NewPath("foo"), -// absoluteTargetPath: data.NewPath("data.foo"), -// targetValue: data.NewValue("${bar}"), -// children: []input{ -// { -// path: data.NewPath("abs.ref.path.test"), -// targetPath: data.NewPath("bar"), -// absoluteTargetPath: data.NewPath("data.bar"), -// targetValue: data.NewValue("ohai"), -// children: nil, -// }, -// }, -// }, -// }, -// expected: []ValueReference{ -// { -// Path: data.NewPath("abs.ref.path"), -// TargetPath: data.NewPath("foo"), -// AbsoluteTargetPath: data.NewPath("data.foo"), -// }, -// { -// Path: data.NewPath("abs.ref.path.test"), -// TargetPath: data.NewPath("bar"), -// AbsoluteTargetPath: data.NewPath("data.bar"), -// }, -// }, -// }, -// } -// -// for _, tt := range tests { -// t.Run(tt.name, func(t *testing.T) { -// source := mocks.NewMockReferenceValueSource(t) -// -// valueMap := make(map[string]data.Value, len(tt.input)) -// for _, in := range tt.input { -// valueMap[in.path.String()] = in.targetValue -// } -// spew.Dump(valueMap) -// source.EXPECT().Values().Return(valueMap) -// -// for _, input := range tt.input { -// source.EXPECT().AbsolutePath(input.targetPath, input.path).Return(input.absoluteTargetPath, nil) -// source.EXPECT().GetPath(input.absoluteTargetPath).Return(input.targetValue, nil) -// -// for _, child := range input.children { -// source.EXPECT().AbsolutePath(child.targetPath, child.path).Return(child.absoluteTargetPath, nil) -// source.EXPECT().GetPath(child.absoluteTargetPath).Return(child.targetValue, nil) -// } -// } -// -// references, err := ParseValueReferences(source) -// -// assert.NoError(t, err) -// assert.NotNil(t, references) -// }) -// } -// } -// -// func TestParseValueReferences(t *testing.T) { -// tests := []struct { -// name string -// values map[string]data.Value -// expected []ValueReference -// }{ -// { -// name: "nil value", -// values: map[string]data.Value{ -// "foo": data.NewValue(nil), -// }, -// expected: nil, -// }, -// { -// name: "no reference", -// values: map[string]data.Value{ -// "foo": data.NewValue("test"), -// "bar": data.NewValue("some text"), -// }, -// expected: nil, -// }, -// { -// // nested references are not really supported -// // in this case only the inner references are parsed correctly -// name: "nested references", -// values: map[string]data.Value{ -// "foo": data.NewValue("${foo:${bar}}"), -// "bar": data.NewValue("${${foo}}"), -// "baz": data.NewValue("${foo${bar}}"), -// }, -// expected: []ValueReference{ -// { -// Path: data.NewPath("foo"), -// TargetPath: data.NewPath("bar"), -// }, -// { -// Path: data.NewPath("bar"), -// TargetPath: data.NewPath("foo"), -// }, -// { -// Path: data.NewPath("baz"), -// TargetPath: data.NewPath("bar"), -// }, -// }, -// }, -// { -// name: "malformed references", -// values: map[string]data.Value{ -// "empty": data.NewValue("${}"), -// "dot_separator": data.NewValue("${a.b}"), -// "unclosed": data.NewValue("${a"), -// "illegal_chars": data.NewValue("${a*b%}"), -// "missing_dollar": data.NewValue("{a:b}"), -// }, -// expected: nil, -// }, -// { -// name: "simple references", -// values: map[string]data.Value{ -// "foo": data.NewValue("${foo}"), -// "bar": data.NewValue("${bar}"), -// }, -// expected: []ValueReference{ -// { -// Path: data.NewPath("foo"), -// TargetPath: data.NewPath("foo"), -// }, -// { -// Path: data.NewPath("bar"), -// TargetPath: data.NewPath("bar"), -// }, -// }, -// }, -// { -// name: "long references", -// values: map[string]data.Value{ -// "foo": data.NewValue("${this:is:a:path}"), -// "bar": data.NewValue("${an:even:longer:path:why:not}"), -// }, -// expected: []ValueReference{ -// { -// Path: data.NewPath("foo"), -// TargetPath: data.NewPath("this.is.a.path"), -// }, -// { -// Path: data.NewPath("bar"), -// TargetPath: data.NewPath("an.even.longer.path.why.not"), -// }, -// }, -// }, -// { -// name: "embedded references", -// values: map[string]data.Value{ -// "foo": data.NewValue("Hello there, ${name}"), -// "bar": data.NewValue("Ohai, ${name:first} ${name:last}"), -// }, -// expected: []ValueReference{ -// { -// Path: data.NewPath("foo"), -// TargetPath: data.NewPath("name"), -// }, -// { -// Path: data.NewPath("bar"), -// TargetPath: data.NewPath("name.first"), -// }, -// { -// Path: data.NewPath("bar"), -// TargetPath: data.NewPath("name.last"), -// }, -// }, -// }, -// } -// -// for _, tt := range tests { -// t.Run(tt.name, func(t *testing.T) { -// source := mocks.NewMockReferenceValueSource(t) -// source.EXPECT().Values().Return(tt.values) -// -// for _, expect := range tt.expected { -// source.EXPECT().AbsolutePath(expect.TargetPath, expect.Path).Return(expect.AbsoluteTargetPath, nil) -// } -// -// references, err := ParseValueReferences(source) -// assert.NoError(t, err) -// if tt.expected == nil { -// assert.Nil(t, references) -// } else { -// assert.ElementsMatch(t, references, tt.expected) -// } -// source.AssertExpectations(t) -// }) -// } -// } -// -// func TestReferencesInValue(t *testing.T) { -// tests := []struct { -// name string -// value data.Value -// expected []ValueReference -// }{ -// { -// name: "no reference", -// value: data.NewValue("hello there"), -// expected: nil, -// }, -// { -// name: "one reference", -// value: data.NewValue("hello there, ${name}"), -// expected: []ValueReference{ -// { -// Path: data.NewPath(""), -// TargetPath: data.NewPath("name"), -// }, -// }, -// }, -// { -// name: "multiple references", -// value: data.NewValue("${greeting}, ${name:first} ${name:last}"), -// expected: []ValueReference{ -// { -// Path: data.NewPath(""), -// TargetPath: data.NewPath("greeting"), -// }, -// { -// Path: data.NewPath(""), -// TargetPath: data.NewPath("name.first"), -// }, -// { -// Path: data.NewPath(""), -// TargetPath: data.NewPath("name.last"), -// }, -// }, -// }, -// } -// -// for _, tt := range tests { -// t.Run(tt.name, func(t *testing.T) { -// references := ReferencesInValue(tt.value) -// -// if tt.expected == nil { -// assert.Nil(t, references) -// } else { -// assert.ElementsMatch(t, references, tt.expected) -// } -// }) -// } -// } -// -// func TestResolveReferences(t *testing.T) { -// type input struct { -// refTargetPath string -// getPathErr error -// value data.Value -// } -// -// tests := []struct { -// name string -// input []input -// expected []ValueReference -// err error -// }{ -// { -// name: "simple references", -// input: []input{ -// { -// refTargetPath: "foo", -// getPathErr: nil, -// value: data.NewValue("bar"), -// }, -// { -// refTargetPath: "bar", -// getPathErr: nil, -// value: data.NewValue("baz"), -// }, -// }, -// expected: []ValueReference{ -// { -// TargetPath: data.NewPath("bar"), -// Path: data.Path{}, // path is not relevant for ResolveReferences -// }, -// { -// TargetPath: data.NewPath("foo"), -// Path: data.Path{}, -// }, -// }, -// }, -// { -// name: "invalid references", -// input: []input{ -// { -// refTargetPath: "foo", -// getPathErr: ErrPathNotFound, -// value: data.NilValue, -// }, -// }, -// expected: nil, -// err: ErrUndefinedReferenceTarget, -// }, -// { -// name: "duplicate references", -// input: []input{ -// { -// refTargetPath: "foo", -// getPathErr: nil, -// value: data.NewValue("bar"), -// }, -// { -// refTargetPath: "foo", -// getPathErr: nil, -// value: data.NewValue("bar"), -// }, -// }, -// expected: []ValueReference{ -// { -// TargetPath: data.NewPath("foo"), -// Path: data.Path{}, -// }, -// { -// TargetPath: data.NewPath("foo"), -// Path: data.Path{}, -// }, -// }, -// err: nil, -// }, -// { -// name: "self-referencing", -// input: []input{ -// { -// refTargetPath: "foo", -// getPathErr: nil, -// value: data.NewValue("${foo}"), -// }, -// }, -// expected: nil, -// err: ErrSelfReferencingReference, -// }, -// { -// name: "cyclic references", -// input: []input{ -// { -// refTargetPath: "foo", -// getPathErr: nil, -// value: data.NewValue("${bar}"), -// }, -// { -// refTargetPath: "bar", -// getPathErr: nil, -// value: data.NewValue("${baz}"), -// }, -// { -// refTargetPath: "baz", -// getPathErr: nil, -// value: data.NewValue("${foo}"), -// }, -// }, -// expected: nil, -// err: ErrCyclicReference, -// }, -// { -// name: "simple chained references", -// input: []input{ -// { -// refTargetPath: "foo", -// getPathErr: nil, -// value: data.NewValue("${bar}"), -// }, -// { -// refTargetPath: "bar", -// getPathErr: nil, -// value: data.NewValue("${baz}"), -// }, -// { -// refTargetPath: "baz", -// getPathErr: nil, -// value: data.NewValue("ohai"), -// }, -// }, -// expected: []ValueReference{ -// { -// TargetPath: data.NewPath("baz"), -// Path: data.Path{}, -// }, -// { -// TargetPath: data.NewPath("bar"), -// Path: data.Path{}, -// }, -// { -// TargetPath: data.NewPath("foo"), -// Path: data.Path{}, -// }, -// }, -// err: nil, -// }, -// { -// name: "complex chained references", -// input: []input{ -// { -// refTargetPath: "foo", -// getPathErr: nil, -// value: data.NewValue("${bar} ${baz}"), -// }, -// { -// refTargetPath: "bar", -// getPathErr: nil, -// value: data.NewValue("${baz}"), -// }, -// { -// refTargetPath: "baz", -// getPathErr: nil, -// value: data.NewValue("ohai ${name}"), -// }, -// { -// refTargetPath: "name", -// getPathErr: nil, -// value: data.NewValue("${peter}"), -// }, -// { -// refTargetPath: "peter", -// getPathErr: nil, -// value: data.NewValue("${first} ${last}"), -// }, -// { -// refTargetPath: "first", -// getPathErr: nil, -// value: data.NewValue("Peter"), -// }, -// { -// refTargetPath: "last", -// getPathErr: nil, -// value: data.NewValue("Parker"), -// }, -// }, -// expected: []ValueReference{ -// { -// TargetPath: data.NewPath("first"), -// Path: data.Path{}, -// }, -// { -// TargetPath: data.NewPath("last"), -// Path: data.Path{}, -// }, -// { -// TargetPath: data.NewPath("peter"), -// Path: data.Path{}, -// }, -// { -// TargetPath: data.NewPath("name"), -// Path: data.Path{}, -// }, -// { -// TargetPath: data.NewPath("baz"), -// Path: data.Path{}, -// }, -// { -// TargetPath: data.NewPath("bar"), -// Path: data.Path{}, -// }, -// { -// TargetPath: data.NewPath("foo"), -// Path: data.Path{}, -// }, -// }, -// err: nil, -// }, -// } -// -// for _, tt := range tests { -// t.Run(tt.name, func(t *testing.T) { -// source := mocks.NewMockReferenceSourceGetter(t) -// -// for _, ref := range tt.input { -// source.EXPECT().GetPath(data.NewPath(ref.refTargetPath)).Return(ref.value, ref.getPathErr) -// } -// -// // convert input to slice of references -// var references []ValueReference -// for _, in := range tt.input { -// references = append(references, ValueReference{ -// TargetPath: data.NewPath(in.refTargetPath), -// Path: data.Path{}, // unused in ResolveReferences -// }) -// } -// -// result, err := ResolveReferences(references, source) -// -// if tt.err != nil { -// assert.ErrorIs(t, err, tt.err) -// assert.Nil(t, result) -// } else { -// assert.NoError(t, err) -// assert.NotNil(t, result) -// assert.Equal(t, tt.expected, result) -// assert.Len(t, result, len(references)) -// -// // TODO: finish -// } -// -// source.AssertExpectations(t) -// }) -// } -// } -// -// // TODO: test with ReferenceSourceRelativeGetter -// -// type ReferenceTestSuite struct { -// suite.Suite -// } -// -// // testCreateRegistry quickly creates a registry with files from the given rootPath -// // and ensures that the registry is properly created. -// func (suite *ReferenceTestSuite) createRegistry(rootPath string) *Registry { -// classFiles, err := DiscoverFiles(rootPath, codec.YamlPathSelector) -// assert.NoError(suite.T(), err) -// -// registry, err := NewRegistryFromFiles(classFiles, func(filePaths []string) ([]*Class, error) { -// return ClassLoader(rootPath, classFiles, codec.NewYamlCodec()) -// }) -// assert.NoError(suite.T(), err) -// assert.NotNil(suite.T(), registry) -// -// return registry -// } -// -// func (suite *ReferenceTestSuite) createInventory(rootPath string) *Inventory { -// dataPath := filepath.Join(rootPath, "data") -// targetsPath := filepath.Join(rootPath, "targets") -// -// dataRegistry := suite.createRegistry(dataPath) -// targetsRegistry := suite.createRegistry(targetsPath) -// -// inventory, err := NewInventory() -// assert.NoError(suite.T(), err) -// -// err = inventory.RegisterScope(DataScope, dataRegistry) -// assert.NoError(suite.T(), err) -// err = inventory.RegisterScope(TargetsScope, targetsRegistry) -// assert.NoError(suite.T(), err) -// -// return inventory -// } -// -// // testParse is a helper to quickly test the 'ParseReferences' function -// // which assumes that the function does not return an error. -// // It checks that all passed references are returned. -// func (suite *ReferenceTestSuite) testParse(source OldParseSource, expected []ValueReference) []ValueReference { -// // TEST: ensure source cannot be nil -// _, err := ParseReferences(nil) -// assert.ErrorIs(suite.T(), err, ErrReferenceSourceIsNil) -// -// // TEST: ensure that parsing succeeds and returns the expected references -// references, err := ParseReferences(source) -// assert.NoError(suite.T(), err) -// assert.NotNil(suite.T(), references) -// assert.Len(suite.T(), references, len(expected)) -// for _, reference := range references { -// assert.Contains(suite.T(), expected, reference) -// } -// -// return references -// } -// -// // testResolve performs the default set of tests for ResolveReferences. -// // The given expected references must be in the order in which the ResolveReferences returns them. -// func (suite *ReferenceTestSuite) testResolve(source DataSetterGetter, expected []ValueReference) []ValueReference { -// // TEST: ResolveReferences must not accept a nil source -// _, err := ResolveReferences(expected, nil) -// assert.ErrorIs(suite.T(), err, ErrReferenceSourceIsNil) -// -// // TEST: references with invalid TargetPath -// // We're reusing the path of an expected reference because that needs to be valid still. -// invalidReferences := []ValueReference{ -// { -// Path: expected[0].Path, -// TargetPath: data.NewPath("invalid.target.path"), -// }, -// { -// Path: expected[0].Path, -// TargetPath: data.NewPath("another.invalid.target.path"), -// }, -// } -// resolved, err := ResolveReferences(invalidReferences, source) -// assert.ErrorIs(suite.T(), err, ErrUndefinedReferenceTarget) -// assert.Nil(suite.T(), resolved) -// -// // TEST: ensure that the resolving works and check whether all expected references are still within the resolved ones. -// resolved, err = ResolveReferences(expected, source) -// assert.NoError(suite.T(), err) -// assert.Len(suite.T(), resolved, len(expected)) -// -// // for _, res := range resolved { -// // spew.Println(res.Name()) -// // } -// -// // The expected references are sorted and since we use a stable topological sort, -// // the result must be exactly the same. -// assert.Equal(suite.T(), expected, resolved, "The resolved references must match the content and order of the expected references") -// -// return resolved -// } -// -// func (suite *ReferenceTestSuite) testReplace(source ReferenceSource, resolved []ValueReference, expected map[string]data.Value) { -// // TEST: source cannot be nil -// err := ReplaceReferences(resolved, nil) -// assert.ErrorIs(suite.T(), err, ErrReferenceSourceIsNil) -// -// // TEST: replacing must result in the expected values -// err = ReplaceReferences(resolved, source) -// assert.NoError(suite.T(), err) -// for targetPath, expectedValue := range expected { -// value, err := source.GetPath(data.NewPath(targetPath)) -// assert.NoError(suite.T(), err) -// assert.Equal(suite.T(), expectedValue, value) -// } -// -// // // TEST: there must be no more references after replacing -// // parsedReferences, err := ParseReferences(source) -// // assert.NoError(suite.T(), err) -// // assert.Len(suite.T(), parsedReferences, 0) -// } -// -// // -------------------------------------------------- -// // Class Reference Tests -// // -------------------------------------------------- -// -// // ClassReferenceTestSuite is used to test references on a single Class (aka local references) -// type ClassReferenceTestSuite struct { -// ReferenceTestSuite -// } -// -// func (suite *ClassReferenceTestSuite) TestSimple() { -// expected := []ValueReference{ -// { -// Path: data.NewPath("simple.departments.engineering.manager"), -// TargetPath: data.NewPath("employees.0.name"), -// }, -// { -// Path: data.NewPath("simple.departments.analytics.manager"), -// TargetPath: data.NewPath("simple.employees.1.name"), -// }, -// { -// Path: data.NewPath("simple.departments.marketing.manager"), -// TargetPath: data.NewPath("simple.employees.2.name"), -// }, -// { -// Path: data.NewPath("simple.projects.Project_X.department"), -// TargetPath: data.NewPath("simple.departments.engineering.name"), -// }, -// } -// -// class, err := NewClass("testdata/references/local/simple.yaml", codec.NewYamlCodec(), data.NewPathFromOsPath("simple")) -// assert.NoError(suite.T(), err) -// -// // TEST: ParseReferences -// suite.testParse(class, expected) -// -// // TEST: ResolveReferences -// expectedOrder := []ValueReference{ -// expected[3], -// expected[1], -// expected[2], -// expected[0], -// } -// resolved := suite.testResolve(class, expectedOrder) -// -// // TEST: ReplaceReferences -// expectedReplaced := map[string]data.Value{ -// "simple.departments.engineering.manager": data.NewValue("John Doe"), -// "simple.departments.analytics.manager": data.NewValue("Jane Smith"), -// "simple.departments.marketing.manager": data.NewValue("Michael Johnson"), -// "simple.projects.Project_X.department": data.NewValue("Engineering"), -// } -// suite.testReplace(class, resolved, expectedReplaced) -// } -// -// func (suite *ClassReferenceTestSuite) TestNested() { -// expected := []ValueReference{ -// { -// Path: data.NewPath("nested.target"), -// TargetPath: data.NewPath("source"), -// }, -// { -// Path: data.NewPath("nested.target_array"), -// TargetPath: data.NewPath("source_array"), -// }, -// { -// Path: data.NewPath("nested.target_nested_map"), -// TargetPath: data.NewPath("nested_map"), -// }, -// { -// Path: data.NewPath("nested.target_nested_mixed"), -// TargetPath: data.NewPath("nested_mixed"), -// }, -// } -// -// class, err := NewClass("testdata/references/local/nested.yaml", codec.NewYamlCodec(), data.NewPathFromOsPath("nested")) -// assert.NoError(suite.T(), err) -// -// // TEST: ParseReferences -// suite.testParse(class, expected) -// -// // TEST: ResolveReferences -// expectedOrder := []ValueReference{ -// expected[3], -// expected[1], -// expected[2], -// expected[0], -// } -// resolved := suite.testResolve(class, expectedOrder) -// -// // TEST: ReplaceReferences -// expectedReplaced := map[string]data.Value{ -// "nested.target": data.NewValue(map[string]interface{}{ -// "foo": "bar", -// "bar": "baz", -// }, -// ), -// "nested.target_array": data.NewValue([]interface{}{ -// "foo", "bar", "baz", -// }), -// "nested.target_nested_map": data.NewValue(map[string]interface{}{ -// "foo": map[string]interface{}{ -// "bar": map[string]interface{}{ -// "baz": "qux", -// }, -// }, -// }), -// "nested.target_nested_mixed": data.NewValue(map[string]interface{}{ -// "foo": []interface{}{ -// map[string]interface{}{ -// "bar": "baz", -// }, -// "test", -// map[string]interface{}{ -// "foo": map[string]interface{}{ -// "bar": "baz", -// }, -// }, -// map[string]interface{}{ -// "array": []interface{}{ -// "one", "two", "three", -// }, -// }, -// }, -// }), -// } -// -// suite.testReplace(class, resolved, expectedReplaced) -// } -// -// func (suite *ClassReferenceTestSuite) TestChained() { -// expected := []ValueReference{ -// { -// Path: data.NewPath("chained.gotcha"), -// TargetPath: data.NewPath("chained.john.first_name"), -// }, -// { -// Path: data.NewPath("chained.name_placeholder"), -// TargetPath: data.NewPath("gotcha"), -// }, -// { -// Path: data.NewPath("chained.first_name"), -// TargetPath: data.NewPath("name_placeholder"), -// }, -// { -// Path: data.NewPath("chained.greeting"), -// TargetPath: data.NewPath("first_name"), -// }, -// { -// Path: data.NewPath("chained.ohai"), -// TargetPath: data.NewPath("test"), -// }, -// { -// Path: data.NewPath("chained.whoop"), -// TargetPath: data.NewPath("tesa"), -// }, -// { -// Path: data.NewPath("chained.longer"), -// TargetPath: data.NewPath("longer_ref"), -// }, -// } -// -// class, err := NewClass("testdata/references/local/chained.yaml", codec.NewYamlCodec(), data.NewPathFromOsPath("chained")) -// assert.NoError(suite.T(), err) -// -// // TEST: ParseReferences -// suite.testParse(class, expected) -// -// // TEST: ResolveReferences -// -// expectedOrder := []ValueReference{ -// expected[0], -// expected[1], -// expected[2], -// expected[3], -// expected[6], -// expected[5], -// expected[4], -// } -// resolved := suite.testResolve(class, expectedOrder) -// -// // TEST: ReplaceReferences -// expectedReplaced := map[string]data.Value{ -// "chained.greeting": data.NewValue("Hello, John"), -// "chained.gotcha": data.NewValue("John"), -// "chained.first_name": data.NewValue("John"), -// "chained.name_placeholder": data.NewValue("John"), -// } -// suite.testReplace(class, resolved, expectedReplaced) -// } -// -// func (suite *ClassReferenceTestSuite) TestCycle() { -// expected := []ValueReference{ -// { -// Path: data.NewPath("cycle.john"), -// TargetPath: data.NewPath("middle"), -// }, -// { -// Path: data.NewPath("cycle.name"), -// TargetPath: data.NewPath("john"), -// }, -// { -// Path: data.NewPath("cycle.middle"), -// TargetPath: data.NewPath("name"), -// }, -// } -// -// class, err := NewClass("testdata/references/local/cycle.yaml", codec.NewYamlCodec(), data.NewPathFromOsPath("cycle")) -// assert.NoError(suite.T(), err) -// -// // TEST: ResolveReferences cycle detection -// resolved, err := ResolveReferences(expected, class) -// assert.ErrorIs(suite.T(), err, ErrCyclicReference) -// assert.Len(suite.T(), resolved, 0) -// } -// -// func (suite *ClassReferenceTestSuite) TestMulti() { -// expected := []ValueReference{ -// { -// Path: data.NewPath("multi.project.description"), -// TargetPath: data.NewPath("project.name"), -// }, -// { -// Path: data.NewPath("multi.project.description"), -// TargetPath: data.NewPath("multi.project.name"), -// }, -// { -// Path: data.NewPath("multi.project.description"), -// TargetPath: data.NewPath("project.name"), -// }, -// { -// Path: data.NewPath("multi.project.description"), -// TargetPath: data.NewPath("multi.project.repo"), -// }, -// { -// Path: data.NewPath("multi.project.repo"), -// TargetPath: data.NewPath("multi.common.repo_url"), -// }, -// } -// -// class, err := NewClass("testdata/references/local/multi.yaml", codec.NewYamlCodec(), data.NewPathFromOsPath("multi")) -// assert.NoError(suite.T(), err) -// -// // TEST: ParseReferences -// suite.testParse(class, expected) -// -// // TEST: ResolveReferences -// expectedOrder := []ValueReference{ -// expected[4], -// expected[1], -// expected[3], -// expected[0], -// expected[2], -// } -// resolved := suite.testResolve(class, expectedOrder) -// -// // TEST: ReplaceReferences -// expectedReplaced := map[string]data.Value{ -// "multi.project.repo": data.NewValue("github.com/lukasjarosch/skipper"), -// "multi.project.description": data.NewValue("The skipper project is very cool. Because skipper helps in working with the Infrastructure as Data concept. The project 'skipper' is hosted on github.com/lukasjarosch/skipper.\n"), -// } -// suite.testReplace(class, resolved, expectedReplaced) -// } -// -// func TestClassReferences(t *testing.T) { -// suite.Run(t, new(ClassReferenceTestSuite)) -// } -// -// // -------------------------------------------------- -// // Registry Reference Tests -// // -------------------------------------------------- -// -// // RegistryReferenceTestSuite is used to test references on a Registry -// type RegistryReferenceTestSuite struct { -// ReferenceTestSuite -// } -// -// func (suite *RegistryReferenceTestSuite) TestSimple() { -// expected := []ValueReference{ -// { -// Path: data.NewPath("test.name"), -// TargetPath: data.NewPath("person.first_name"), -// }, -// { -// Path: data.NewPath("test.name"), -// TargetPath: data.NewPath("person.last_name"), -// }, -// { -// Path: data.NewPath("person.age"), -// TargetPath: data.NewPath("test.age"), -// }, -// } -// -// rootPath := "testdata/references/registry/simple" -// registry := suite.createRegistry(rootPath) -// -// // TEST: ParseReferences -// suite.testParse(registry, expected) -// -// // TEST: ResolveReferences -// expectedOrder := expected -// resolved := suite.testResolve(registry, expectedOrder) -// -// // TEST: ReplaceReferences -// expectedReplaced := map[string]data.Value{ -// "person.age": data.NewValue(30), -// "test.name": data.NewValue("John Doe"), -// } -// suite.testReplace(registry, resolved, expectedReplaced) -// } -// -// func (suite *RegistryReferenceTestSuite) TestNested() { -// expected := []ValueReference{ -// { -// Path: data.NewPath("test.target"), -// TargetPath: data.NewPath("nested.source"), -// }, -// { -// Path: data.NewPath("test.target_array"), -// TargetPath: data.NewPath("nested.source_array"), -// }, -// { -// Path: data.NewPath("test.target_nested_map"), -// TargetPath: data.NewPath("nested.nested_map"), -// }, -// { -// Path: data.NewPath("test.target_nested_mixed"), -// TargetPath: data.NewPath("nested.nested_mixed"), -// }, -// } -// -// rootPath := "testdata/references/registry/nested" -// registry := suite.createRegistry(rootPath) -// -// // TEST: ParseReferences -// suite.testParse(registry, expected) -// -// // TEST: ResolveReferences -// expectedOrder := []ValueReference{ -// expected[3], -// expected[1], -// expected[2], -// expected[0], -// } -// resolved := suite.testResolve(registry, expectedOrder) -// -// // TEST: ReplaceReferences -// expectedReplaced := map[string]data.Value{ -// "test.target": data.NewValue(map[string]interface{}{ -// "foo": "bar", -// "bar": "baz", -// }, -// ), -// "test.target_array": data.NewValue([]interface{}{ -// "foo", "bar", "baz", -// }), -// "test.target_nested_map": data.NewValue(map[string]interface{}{ -// "foo": map[string]interface{}{ -// "bar": map[string]interface{}{ -// "baz": "qux", -// }, -// }, -// }), -// "test.target_nested_mixed": data.NewValue(map[string]interface{}{ -// "foo": []interface{}{ -// map[string]interface{}{ -// "bar": "baz", -// }, -// "test", -// map[string]interface{}{ -// "foo": map[string]interface{}{ -// "bar": "baz", -// }, -// }, -// map[string]interface{}{ -// "array": []interface{}{ -// "one", "two", "three", -// }, -// }, -// }, -// }), -// } -// suite.testReplace(registry, resolved, expectedReplaced) -// } -// -// func (suite *RegistryReferenceTestSuite) TestChained() { -// expected := []ValueReference{ -// { -// Path: data.NewPath("test.full_name"), -// TargetPath: data.NewPath("person.first_name"), -// }, -// { -// Path: data.NewPath("test.full_name"), -// TargetPath: data.NewPath("person.last_name"), -// }, -// { -// Path: data.NewPath("test.greet_person"), -// TargetPath: data.NewPath("greeting.text"), -// }, -// { -// Path: data.NewPath("test.greet_person"), -// TargetPath: data.NewPath("full_name"), -// }, -// } -// -// rootPath := "testdata/references/registry/chained" -// registry := suite.createRegistry(rootPath) -// -// // TEST: ParseReferences -// suite.testParse(registry, expected) -// -// // TEST: ResolveReferences -// expectedOrder := expected -// resolved := suite.testResolve(registry, expectedOrder) -// -// // TEST: ReplaceReferences -// expectedReplaced := map[string]data.Value{ -// "test.full_name": data.NewValue("John Doe"), -// "test.greet_person": data.NewValue("Hello, John Doe"), -// } -// suite.testReplace(registry, resolved, expectedReplaced) -// } -// -// func (suite *RegistryReferenceTestSuite) TestCycle() { -// expected := []ValueReference{ -// { -// Path: data.NewPath("test.foo"), -// TargetPath: data.NewPath("cycle.foo"), -// }, -// { -// Path: data.NewPath("test.baz"), -// TargetPath: data.NewPath("foo"), -// }, -// { -// Path: data.NewPath("cycle.foo"), -// TargetPath: data.NewPath("bar"), -// }, -// { -// Path: data.NewPath("cycle.bar"), -// TargetPath: data.NewPath("test.baz"), -// }, -// } -// -// rootPath := "testdata/references/registry/cycle" -// registry := suite.createRegistry(rootPath) -// -// // TEST: ResolveReferences cycle detection -// resolved, err := ResolveReferences(expected, registry) -// assert.ErrorIs(suite.T(), err, ErrCyclicReference) -// assert.Len(suite.T(), resolved, 0) -// } -// -// func TestRegistryReferences(t *testing.T) { -// suite.Run(t, new(RegistryReferenceTestSuite)) -// } -// -// // -------------------------------------------------- -// // Inventory Reference Tests -// // -------------------------------------------------- -// -// // ClassReferenceTestSuite is used to test references on a single Class (aka local references) -// type InventoryReferenceTestSuite struct { -// ReferenceTestSuite -// } -// -// func (suite *InventoryReferenceTestSuite) TestSimple() { -// expected := []ValueReference{ -// { -// Path: data.NewPath("data.test.state"), -// TargetPath: data.NewPath("targets.skipper.default_state"), -// }, -// { -// Path: data.NewPath("data.test.foo"), -// TargetPath: data.NewPath("bar"), -// }, -// { -// Path: data.NewPath("targets.skipper.greeting"), -// TargetPath: data.NewPath("data.test.name"), -// }, -// { -// Path: data.NewPath("targets.skipper.hello"), -// TargetPath: data.NewPath("planet"), -// }, -// } -// -// rootPath := "testdata/references/inventory/simple" -// inventory := suite.createInventory(rootPath) -// -// // TEST: ParseReferences -// suite.testParse(inventory, expected) -// -// // TEST: ResolveReferences -// expectedOrder := []ValueReference{ -// expected[0], -// expected[2], -// expected[3], -// expected[1], -// } -// resolved := suite.testResolve(inventory, expectedOrder) -// -// // TEST: ReplaceReferences -// expectedReplaced := map[string]data.Value{ -// "data.test.state": data.NewValue("hungry"), -// "data.test.foo": data.NewValue("bar"), -// "targets.skipper.greeting": data.NewValue("Hello there, John Doe"), -// "targets.skipper.hello": data.NewValue("world"), -// } -// suite.testReplace(inventory, resolved, expectedReplaced) -// } -// -// func TestInventoryReferences(t *testing.T) { -// suite.Run(t, new(InventoryReferenceTestSuite)) -// } +import ( + "regexp" + "testing" + + "github.com/dominikbraun/graph" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + . "github.com/lukasjarosch/skipper" + "github.com/lukasjarosch/skipper/data" + mocks "github.com/lukasjarosch/skipper/mocks" +) + +func TestNewValueReferenceManager(t *testing.T) { + // TEST: source is nil + manager, err := NewValueReferenceManager(nil) + assert.ErrorIs(t, err, ErrReferenceSourceIsNil) + assert.Nil(t, manager) +} + +func TestCalculateReplacementOrder(t *testing.T) { + tests := []struct { + name string + references map[string]ValueReference + errExpected error + expectedOrder []ValueReference + }{ + { + name: "non dependent references", + references: map[string]ValueReference{ + "ref1": { + Path: data.NewPath("foo.bar"), + TargetPath: data.NewPath("name"), + AbsoluteTargetPath: data.NewPath("person.name"), + }, + "ref2": { + Path: data.NewPath("foo.baz"), + TargetPath: data.NewPath("age"), + AbsoluteTargetPath: data.NewPath("person.age"), + }, + }, + expectedOrder: []ValueReference{ + { + Path: data.NewPath("foo.bar"), + TargetPath: data.NewPath("name"), + AbsoluteTargetPath: data.NewPath("person.name"), + }, + { + Path: data.NewPath("foo.baz"), + TargetPath: data.NewPath("age"), + AbsoluteTargetPath: data.NewPath("person.age"), + }, + }, + }, + { + name: "long dependency chain", + references: map[string]ValueReference{ + "ref1": { + Path: data.NewPath("foo.bar"), + TargetPath: data.NewPath("bar"), + AbsoluteTargetPath: data.NewPath("common.bar"), + }, + "ref2": { + Path: data.NewPath("common.bar"), + TargetPath: data.NewPath("name"), + AbsoluteTargetPath: data.NewPath("common.name"), + }, + "ref3": { + Path: data.NewPath("common.name"), + TargetPath: data.NewPath("peter"), + AbsoluteTargetPath: data.NewPath("common.peter"), + }, + "ref4": { + Path: data.NewPath("common.peter"), + TargetPath: data.NewPath("hans"), + AbsoluteTargetPath: data.NewPath("common.hans"), + }, + }, + expectedOrder: []ValueReference{ + { + Path: data.NewPath("common.peter"), + TargetPath: data.NewPath("hans"), + AbsoluteTargetPath: data.NewPath("common.hans"), + }, + { + Path: data.NewPath("common.name"), + TargetPath: data.NewPath("peter"), + AbsoluteTargetPath: data.NewPath("common.peter"), + }, + { + Path: data.NewPath("common.bar"), + TargetPath: data.NewPath("name"), + AbsoluteTargetPath: data.NewPath("common.name"), + }, + { + Path: data.NewPath("foo.bar"), + TargetPath: data.NewPath("bar"), + AbsoluteTargetPath: data.NewPath("common.bar"), + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + graph, err := BuildDependencyGraph(tt.references) + assert.NoError(t, err) + assert.NotNil(t, graph) + + order, err := CalculateReplacementOrder(graph) + + if tt.errExpected != nil { + assert.ErrorIs(t, err, tt.errExpected) + assert.Nil(t, order) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedOrder, order) + } + }) + } +} + +func TestBuildDependencyGraph(t *testing.T) { + tests := []struct { + name string + references map[string]ValueReference + errExpected error + }{ + { + name: "non dependent references", + references: map[string]ValueReference{ + "ref1": { + Path: data.NewPath("foo.bar"), + TargetPath: data.NewPath("name"), + AbsoluteTargetPath: data.NewPath("person.name"), + }, + "ref2": { + Path: data.NewPath("foo.baz"), + TargetPath: data.NewPath("age"), + AbsoluteTargetPath: data.NewPath("person.age"), + }, + }, + }, + { + name: "references must be deduplicated", + references: map[string]ValueReference{ + "ref1": { + Path: data.NewPath("foo.bar"), + TargetPath: data.NewPath("name"), + AbsoluteTargetPath: data.NewPath("person.name"), + }, + "ref2": { + Path: data.NewPath("foo.bar"), + TargetPath: data.NewPath("name"), + AbsoluteTargetPath: data.NewPath("person.name"), + }, + }, + errExpected: graph.ErrVertexAlreadyExists, + }, + { + name: "self-referencing references are not allowed", + references: map[string]ValueReference{ + "ref1": { + Path: data.NewPath("foo.bar"), + TargetPath: data.NewPath("bar"), + AbsoluteTargetPath: data.NewPath("foo.bar"), + }, + "ref2": { + Path: data.NewPath("foo.baz"), + TargetPath: data.NewPath("baz"), + AbsoluteTargetPath: data.NewPath("foo.baz"), + }, + }, + errExpected: ErrSelfReferencingReference, + }, + { + name: "dependency cycles are not allowed", + references: map[string]ValueReference{ + "ref1": { + Path: data.NewPath("foo.bar"), + TargetPath: data.NewPath("name"), + AbsoluteTargetPath: data.NewPath("person.name"), + }, + "ref2": { + Path: data.NewPath("person.name"), + TargetPath: data.NewPath("some_name"), + AbsoluteTargetPath: data.NewPath("person.some_name"), + }, + "ref3": { + Path: data.NewPath("person.some_name"), + TargetPath: data.NewPath("foo.bar"), + AbsoluteTargetPath: data.NewPath("foo.bar"), + }, + }, + errExpected: ErrCyclicReference, + }, + { + name: "multiple dependencies", + references: map[string]ValueReference{ + "ref1": { + Path: data.NewPath("foo.bar"), + TargetPath: data.NewPath("bar"), + AbsoluteTargetPath: data.NewPath("common.bar"), + }, + "ref2": { + Path: data.NewPath("common.bar"), + TargetPath: data.NewPath("name"), + AbsoluteTargetPath: data.NewPath("common.name"), + }, + "ref3": { + Path: data.NewPath("foo.bar"), + TargetPath: data.NewPath("baz"), + AbsoluteTargetPath: data.NewPath("common.baz"), + }, + "ref4": { + Path: data.NewPath("common.baz"), + TargetPath: data.NewPath("ohai"), + AbsoluteTargetPath: data.NewPath("common.ohai"), + }, + }, + }, + { + name: "long dependency chain", + references: map[string]ValueReference{ + "ref1": { + Path: data.NewPath("foo.bar"), + TargetPath: data.NewPath("bar"), + AbsoluteTargetPath: data.NewPath("common.bar"), + }, + "ref2": { + Path: data.NewPath("common.bar"), + TargetPath: data.NewPath("name"), + AbsoluteTargetPath: data.NewPath("common.name"), + }, + "ref3": { + Path: data.NewPath("common.name"), + TargetPath: data.NewPath("peter"), + AbsoluteTargetPath: data.NewPath("common.peter"), + }, + "ref4": { + Path: data.NewPath("common.peter"), + TargetPath: data.NewPath("hans"), + AbsoluteTargetPath: data.NewPath("common.hans"), + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dependencyGraph, err := BuildDependencyGraph(tt.references) + + if tt.errExpected != nil { + assert.ErrorIs(t, err, tt.errExpected) + assert.Nil(t, dependencyGraph) + } else { + assert.NotNil(t, dependencyGraph) + assert.Nil(t, err) + } + }) + } +} + +func TestResolveDependantValueReferences(t *testing.T) { + tests := []struct { + name string + reference ValueReference + allReferences []ValueReference + expected []ValueReference + }{ + { + name: "no dependencies", + reference: ValueReference{ + Path: data.NewPath("foo.bar"), + TargetPath: data.NewPath("name"), + AbsoluteTargetPath: data.NewPath("person.name"), + }, + allReferences: []ValueReference{ + { + Path: data.NewPath("foo.bar"), + TargetPath: data.NewPath("name"), + AbsoluteTargetPath: data.NewPath("person.name"), + }, + }, + expected: nil, + }, + { + name: "empty dependency list will always return nil", + reference: ValueReference{ + Path: data.NewPath("foo.bar"), + TargetPath: data.NewPath("name"), + AbsoluteTargetPath: data.NewPath("person.name"), + }, + allReferences: []ValueReference{}, + expected: nil, + }, + { + name: "one dependency", + reference: ValueReference{ + Path: data.NewPath("foo.bar"), + TargetPath: data.NewPath("name"), + AbsoluteTargetPath: data.NewPath("person.name"), + }, + allReferences: []ValueReference{ + { + Path: data.NewPath("foo.bar"), + TargetPath: data.NewPath("name"), + AbsoluteTargetPath: data.NewPath("person.name"), + }, + { + Path: data.NewPath("person.name"), + TargetPath: data.NewPath("another.name"), + AbsoluteTargetPath: data.NewPath("person..another.name"), + }, + }, + expected: []ValueReference{ + { + Path: data.NewPath("person.name"), + TargetPath: data.NewPath("another.name"), + AbsoluteTargetPath: data.NewPath("person..another.name"), + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // allReferences needs to be a map + allReferenceMap := make(map[string]ValueReference, len(tt.allReferences)) + for _, ref := range tt.allReferences { + allReferenceMap[ref.Hash()] = ref + } + + result := ResolveDependantValueReferences(tt.reference, allReferenceMap) + + if tt.expected == nil { + assert.Nil(t, result) + } else { + assert.ElementsMatch(t, tt.expected, result) + } + }) + } +} + +func TestFindValueReferences(t *testing.T) { + type sourceData struct { + // maps paths to values of said paths + values map[string]data.Value + // maps the relative path to the expected absolute path + absPaths map[string]string + } + + tests := []struct { + name string + input sourceData + expected []ValueReference + }{ + { + name: "simple references", + input: sourceData{ + values: map[string]data.Value{ + "foo": data.NewValue("${bar}"), + "bar": data.NewValue("bar"), + "name": data.NewValue("${peter:name}"), + "peter.name": data.NewValue("Bob, lol"), + }, + absPaths: map[string]string{ + "bar": "bar", + "peter.name": "persons.peter.name", + }, + }, + expected: []ValueReference{ + { + Path: data.NewPath("foo"), + TargetPath: data.NewPath("bar"), + AbsoluteTargetPath: data.NewPath("bar"), + }, + { + Path: data.NewPath("name"), + TargetPath: data.NewPath("peter.name"), + AbsoluteTargetPath: data.NewPath("persons.peter.name"), + }, + }, + }, + { + name: "multiple references in value", + input: sourceData{ + values: map[string]data.Value{ + "foo": data.NewValue("${bar} ${baz} ${qux}"), + "bar": data.NewValue("bar"), + "baz": data.NewValue("baz"), + "qux": data.NewValue("qux"), + }, + absPaths: map[string]string{ + "bar": "foo.bar", + "baz": "foo.baz", + "qux": "foo.qux", + }, + }, + + expected: []ValueReference{ + { + Path: data.NewPath("foo"), + TargetPath: data.NewPath("bar"), + AbsoluteTargetPath: data.NewPath("foo.bar"), + }, + { + Path: data.NewPath("foo"), + TargetPath: data.NewPath("baz"), + AbsoluteTargetPath: data.NewPath("foo.baz"), + }, + { + Path: data.NewPath("foo"), + TargetPath: data.NewPath("qux"), + AbsoluteTargetPath: data.NewPath("foo.qux"), + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + source := mocks.NewMockReferenceValueSource(t) + source.EXPECT().Values().Return(tt.input.values) + + for rel, abs := range tt.input.absPaths { + source.EXPECT().AbsolutePath(data.NewPath(rel), mock.AnythingOfType("data.Path")).Return(data.NewPath(abs), nil) + } + + references, err := FindValueReferences(source) + assert.NoError(t, err) + assert.ElementsMatch(t, tt.expected, references) + }) + } +} + +func TestFindReferenceTargetPaths(t *testing.T) { + tests := []struct { + name string + value data.Value + expected []data.Path + panicExpected bool + regex *regexp.Regexp + }{ + { + name: "nil value must not return any references", + value: data.NilValue, + regex: ValueReferenceRegex, + expected: nil, + }, + { + name: "empty value must not return any references", + value: data.NewValue(""), + regex: ValueReferenceRegex, + expected: nil, + }, + { + name: "empty regex must result in panic", + value: data.NewValue("${foo}"), + regex: regexp.MustCompile(``), + expected: nil, + panicExpected: true, + }, + { + name: "single reference in value", + value: data.NewValue("${foo:bar}"), + regex: ValueReferenceRegex, + expected: []data.Path{ + data.NewPath("foo.bar"), + }, + }, + { + name: "multiple references in value", + value: data.NewValue("${foo} and ${foo:bar}, well but ${bar:baz:another:deep:path}"), + regex: ValueReferenceRegex, + expected: []data.Path{ + data.NewPath("foo"), + data.NewPath("foo.bar"), + data.NewPath("bar.baz.another.deep.path"), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.panicExpected { + assert.Panics(t, func() { + FindReferenceTargetPaths(tt.regex, tt.value) + }) + return + } + + targetPaths := FindReferenceTargetPaths(tt.regex, tt.value) + assert.ElementsMatch(t, tt.expected, targetPaths) + }) + } +} diff --git a/registry.go b/registry.go index 9ae40be..e5c665c 100644 --- a/registry.go +++ b/registry.go @@ -178,8 +178,7 @@ func (reg *Registry) HasPath(path data.Path) bool { return true } -// GetClassRelativePath attempts to resolve the target-class using the given classPath. -// Then it attempts to resolve the path (which might be class-local only). +// GetClassRelativePath attempts to resolve the target-class using the given classPath. Then it attempts to resolve the path (which might be class-local only). // The given classPath can be any path which is known to the registry which is enough to resolve the Class. func (reg *Registry) GetClassRelativePath(classPath data.Path, path data.Path) (data.Value, error) { classIdentifier, exists := reg.paths[classPath.String()] @@ -207,7 +206,7 @@ func (reg *Registry) Set(path string, value interface{}) error { if !exists { return fmt.Errorf("cannot set unknown path: %s: %w", path, ErrPathNotFound) } - return reg.classes[classIdentifier].Set(path, value) + return reg.classes[classIdentifier].Set(data.NewPath(path).StripPrefix(data.NewPath(classIdentifier)).String(), value) } // SetPath is just a wrapper for Set