Skip to content

Commit

Permalink
feat: graph-based reference resolving
Browse files Browse the repository at this point in the history
Signed-off-by: Lukas Jarosch <[email protected]>
  • Loading branch information
lukasjarosch committed Feb 15, 2024
1 parent 60c1a4a commit 7caee81
Show file tree
Hide file tree
Showing 5 changed files with 207 additions and 108 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ require (
)

require (
github.com/dominikbraun/graph v0.23.0 // indirect
github.com/kr/pretty v0.2.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo=
github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
Expand Down
154 changes: 134 additions & 20 deletions reference.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,24 @@ import (
"regexp"
"strings"

"github.com/dominikbraun/graph"

"github.com/lukasjarosch/skipper/data"
)

// TODO: handle reference-to-reference
// TODO: PathReferences / KeyReferences
// TODO: handle cyclic references
// TODO: handle multiple references per path
// TODO: handle reference-to-reference with multiple references on the same path

var (
// ReferenceRegex defines the strings which are valid references
// See: https://regex101.com/r/lIuuep/1
ReferenceRegex = regexp.MustCompile(`\${(?P<reference>[\w-]+(?:\:[\w-]+)*)}`)

ErrUndefinedReference = fmt.Errorf("undefined reference")
ErrReferenceSourceIsNil = fmt.Errorf("reference source is nil")
ErrUndefinedReferenceTarget = fmt.Errorf("undefined reference target path")
ErrReferenceSourceIsNil = fmt.Errorf("reference source is nil")
)

// Reference is a reference to a value with a different path.
Expand All @@ -39,9 +43,9 @@ type ResolvedReference struct {
// TargetValue is the value to which the TargetPath points to.
// If TargetReference is not nil, this value must be [data.NilValue].
TargetValue data.Value
// TargetReference is non-nil if the Reference points to another [Reference]
// TargetReference is non-nil if the Reference points to another [ResolvedReference]
// If the Reference just points to a scalar value, this will be nil.
TargetReference *Reference
TargetReference *ResolvedReference
}

type ReferenceSourceWalker interface {
Expand Down Expand Up @@ -72,36 +76,146 @@ func ParseReferences(source ReferenceSourceWalker) ([]Reference, error) {
return references, nil
}

// ReferencesInValue returns all references within the passed value.
// Note that the returned references do not have a 'Path' set!
func ReferencesInValue(value data.Value) []Reference {
var references []Reference

referenceMatches := ReferenceRegex.FindAllStringSubmatch(value.String(), -1)
if referenceMatches != nil {
for _, match := range referenceMatches {
references = append(references, Reference{
TargetPath: ReferencePathToPath(match[1]),
Path: data.Path{},
})
}
}

return references
}

type referenceVertex struct {
Reference
RawValue data.Value
}

type ReferenceSourceGetter interface {
GetPath(path data.Path) (data.Value, error)
}

func ResolveReferences(references []Reference, resolveSource ReferenceSourceGetter) ([]ResolvedReference, error) {
if resolveSource == nil {
return nil, ErrReferenceSourceIsNil
// 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 []Reference, resolveSource ReferenceSourceGetter) ([]Reference, error) {
referenceNodeHash := func(ref referenceVertex) string {
return ref.TargetPath.String()
}

var errs error
var resolvedReferences []ResolvedReference
for _, reference := range references {
val, err := resolveSource.GetPath(reference.TargetPath)
g := graph.New(referenceNodeHash, graph.Acyclic(), graph.Directed())

// Register all references as nodes
var nodes []referenceVertex
for _, ref := range references {
rawValue, err := resolveSource.GetPath(ref.TargetPath)
if err != nil {
errs = errors.Join(errs, fmt.Errorf("%w %s at %s: %w", ErrUndefinedReference, reference.Name(), reference.Path, err))
continue
return nil, fmt.Errorf("%w: %s at path %s", ErrUndefinedReferenceTarget, ref.Name(), ref.Path)
}
node := referenceVertex{Reference: ref, RawValue: rawValue}
err = g.AddVertex(node)
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
}
}
nodes = append(nodes, 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 _, refNode := range nodes {
referenceDependencies := ReferencesInValue(refNode.RawValue)

for _, refDep := range referenceDependencies {
n, err := g.Vertex(refDep.TargetPath.String())
if err != nil {
return nil, err
}

if refNode.TargetPath.Equals(n.TargetPath) {
return nil, fmt.Errorf("self-referencing reference %s at path %s", refNode.Name(), refNode.Path)
}

err = g.AddEdge(refNode.TargetPath.String(), n.TargetPath.String())
if err != nil {
return nil, err
}
}
}

resolvedReferences = append(resolvedReferences, ResolvedReference{
Reference: reference,
TargetValue: val,
})
// 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
}
if errs != nil {
return nil, errs

// 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]))
}

// Now that we have the order in which references must be replaced,
// lets finally re-order the passed (non-deduplicated) input references.
var orderedReferences []Reference
for _, refOrder := range reversedOrder {
for _, ref := range references {
if ref.TargetPath.Equals(refOrder) {
orderedReferences = append(orderedReferences, ref)
}
}
}

// sanity check
if len(orderedReferences) != len(references) {
return nil, fmt.Errorf("unexpected amount of resolved references, this is a bug")
}

return resolvedReferences, nil
return orderedReferences, nil
}

// func ResolveReferences(references []Reference, resolveSource ReferenceSourceGetter) ([]ResolvedReference, error) {
// if resolveSource == nil {
// return nil, ErrReferenceSourceIsNil
// }
//
// var errs error
// var resolvedReferences []ResolvedReference
// for _, reference := range references {
// val, err := resolveSource.GetPath(reference.TargetPath)
// if err != nil {
// errs = errors.Join(errs, fmt.Errorf("%w %s at %s: %w", ErrUndefinedReferenceTarget, reference.Name(), reference.Path, err))
// continue
// }
//
// resolvedReferences = append(resolvedReferences, ResolvedReference{
// Reference: reference,
// TargetValue: val,
// })
// }
// if errs != nil {
// return nil, errs
// }
//
// return resolvedReferences, nil
// }

// ReferencePathToPath converts the path used within references (colon-separated) to a proper [data.Path]
func ReferencePathToPath(referencePath string) data.Path {
referencePath = strings.ReplaceAll(referencePath, ":", ".")
Expand Down
Loading

0 comments on commit 7caee81

Please sign in to comment.