Skip to content

Commit

Permalink
feat: property backtracking implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
Joel-Haeberli committed Sep 25, 2023
1 parent d5c0cee commit ec9a53c
Show file tree
Hide file tree
Showing 39 changed files with 603 additions and 1,806 deletions.
19 changes: 0 additions & 19 deletions momentum-core/artefacts/model.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package artefacts

import "momentum-core/tree"

type ArtefactType int

const (
Expand All @@ -22,20 +20,3 @@ type Artefact struct {
ParentId string `json:"parentId"` // id of parent artefacts
Parent *Artefact `json:"-"`
}

func toArtefact(n *tree.Node) *Artefact {

if n == nil {
return nil
}

artefact := new(Artefact)
artefact.Id = n.Id
artefact.Name = n.NormalizedPath()

if n.Kind == tree.File {
artefact.ArtefactType = FILE
}

return artefact
}
144 changes: 144 additions & 0 deletions momentum-core/backtracking/backtrack.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package backtracking

type Backtracker[T any, P any] interface {
// The function executes the backtrack algorithm as configured and returns the result
RunBacktrack() []*Match[T, P]
}

type Input[I any, T any, P any] interface {
// GetInput shall return the input of a configured source, such that the Parse function can parse the input into the Node structure
GetInput() I
// Parse shall map the input source into the node structure
Parse(I) *Node[T, P]
// GetSearch shall return the instance of the search
GetSearch() Search[T, P]
}

type Search[T any, P any] interface {
// The predicate is the fragment, the backtracking algorithm searches for
Predicate() *T
// The Comparable shall process a node and generate an object comparable to the predicate
Comparable(*Node[T, P]) *T
// IsMatch compares the predicate with a given compare value and returns true if they match and false if they do not
IsMatch(*T, *T) bool
// StopEarly shall return true if the algorithm must backtrack and reject the current branch.
// False if the algorithm shall process further even if no full match has been reached.
StopEarly(*T, *T) bool
}

// Node is the structure used inside the backtrack algorithm and describes a tree structure.
type Node[T any, P any] struct {
value *T
pointer *P // pointer to the actual element which belongs to the node
parent *Node[T, P]
children []*Node[T, P]
}

type Match[T any, P any] struct {
Input *T
Match *T
MatchNode *Node[T, P]
}

// Implementation of a backtracking algorithm to find matches on path as described on Wikipedia: https://en.wikipedia.org/wiki/Backtracking
func Backtrack[I any, T any, P any](inp Input[I, T, P]) []*Match[T, P] {

result := make([]*Match[T, P], 0)
return backtrack(inp.GetSearch(), root(inp.Parse(inp.GetInput())), result)
}

func backtrack[T any, P any](problem Search[T, P], n *Node[T, P], result []*Match[T, P]) []*Match[T, P] {

if n == nil {
return result
}

// Currently no rejection makes sense
// if reject(problem, n) {
// return backtrack(problem, next(problem, n), result)
// }

if accept(problem, n) {
result = appendOutput(problem, n, result)
}

return backtrack(problem, next(problem, n), result)
}

func root[T any, P any](n *Node[T, P]) *Node[T, P] {

current := n
for current.parent != nil {
current = current.parent
}

return current
}

func reject[T any, P any](search Search[T, P], n *Node[T, P]) bool {

// can be dangerous, because results are ommited possibly ommitted
return search.StopEarly(search.Predicate(), search.Comparable(n))
}

func accept[T any, P any](search Search[T, P], n *Node[T, P]) bool {

return search.IsMatch(search.Predicate(), search.Comparable(n))
}

func first[T any, P any](search Search[T, P], n *Node[T, P]) *Node[T, P] {

return n.children[0]
}

func next[T any, P any](search Search[T, P], n *Node[T, P]) *Node[T, P] {

if n == nil {
return nil
}

if len(n.children) == 0 {
// backtrack
return walkUp(n.parent, n)
}

return first(search, n)
}

func walkUp[T any, P any](parent *Node[T, P], current *Node[T, P]) *Node[T, P] {

found := false
if parent != nil && current != nil && current.parent == parent {
for _, chld := range parent.children {
if found {
return chld
} else if chld == current {
found = true
}
}
}

if found && parent.parent != nil {
// this indicates that the current node was the last of level, thus we need to walk up further
// if the parent is nil, this indicates we are at root and traversed the whole tree
if parent.parent == nil {
return nil
}

return walkUp(parent.parent, parent)
}

return nil
}

func appendOutput[T any, P any](search Search[T, P], n *Node[T, P], matches []*Match[T, P]) []*Match[T, P] {

match := new(Match[T, P])
match.Input = search.Predicate()
match.Match = n.value
match.MatchNode = n

matches = append(matches, match)

return matches
}
32 changes: 32 additions & 0 deletions momentum-core/backtracking/backtrack_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package backtracking_test

import (
"fmt"
"momentum-core/backtracking"
"os"
"path/filepath"
"testing"
)

func TestBacktracker(t *testing.T) {

wd, err := os.Getwd()
if err != nil {
fmt.Println(err)
t.FailNow()
}

testFilePath := filepath.Join(wd, "test.yaml")

backtracker := backtracking.NewPropertyBacktracker("test.for", testFilePath, backtracking.NewYamlPropertyParser())
result := backtracker.RunBacktrack()

if len(result) != 1 {
fmt.Println("expected one result")
t.FailNow()
}

for _, res := range result {
fmt.Println(res)
}
}
85 changes: 85 additions & 0 deletions momentum-core/backtracking/property-backtracker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package backtracking

import (
"momentum-core/yaml"
"strings"
)

const PROPERTY_SEPARATOR = "."

type PropertyParser interface {
ParseProperties(string) *Node[string, yaml.ViewNode]
}

// The property backtracker finds matches within a property structure.
// A property search is concepted as hierarchical string separated by dots,
// where each level of the
type PropertyBacktracker struct {
Backtracker[string, yaml.ViewNode]
Input[string, string, yaml.ViewNode]
Search[string, yaml.ViewNode]

inputParser PropertyParser
predicate string
path string // path to the yaml file which shall be processed
}

func NewPropertyBacktracker(predicate string, path string, parser PropertyParser) *PropertyBacktracker {

backtracker := new(PropertyBacktracker)
backtracker.predicate = predicate
backtracker.inputParser = parser
backtracker.path = path

return backtracker
}

func (backtracker *PropertyBacktracker) RunBacktrack() []*Match[string, yaml.ViewNode] {

return Backtrack[string, string, yaml.ViewNode](backtracker)
}

func (backtracker *PropertyBacktracker) GetInput() string {

return backtracker.path
}

func (backtracker *PropertyBacktracker) Parse(input string) *Node[string, yaml.ViewNode] {

return backtracker.inputParser.ParseProperties(backtracker.path)
}

func (backtracker *PropertyBacktracker) GetSearch() Search[string, yaml.ViewNode] {

return backtracker
}

func (backtracker *PropertyBacktracker) Predicate() *string {

return &backtracker.predicate
}

func (backtracker *PropertyBacktracker) Comparable(n *Node[string, yaml.ViewNode]) *string {

propertyString := ""
current := n
for current != nil {
propertyString = strings.Join([]string{*current.value, propertyString}, PROPERTY_SEPARATOR)
current = current.parent
}

cutted, _ := strings.CutPrefix(propertyString, PROPERTY_SEPARATOR)
cutted, _ = strings.CutSuffix(cutted, PROPERTY_SEPARATOR)
//fmt.Println("cutted propertyString:", propertyString)
return &cutted
}

func (backtracker *PropertyBacktracker) IsMatch(predicate *string, argument *string) bool {

return strings.EqualFold(*predicate, *argument)
}

func (backtracker *PropertyBacktracker) StopEarly(predicate *string, argument *string) bool {

return !strings.HasPrefix(*predicate, *argument)
}
9 changes: 9 additions & 0 deletions momentum-core/backtracking/test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
test:
for:
backtracker: test
not-for:
backtracker: test
go-never: to
trees:
are:
your: friends
54 changes: 54 additions & 0 deletions momentum-core/backtracking/yaml-parser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package backtracking

import (
"fmt"
"momentum-core/utils"
"momentum-core/yaml"
)

type YamlPropertyParser struct {
PropertyParser
}

func NewYamlPropertyParser() *YamlPropertyParser {

parser := new(YamlPropertyParser)

return parser
}

func (parser *YamlPropertyParser) ParseProperties(path string) *Node[string, yaml.ViewNode] {

if !utils.FileExists(path) {
return nil
}

root, err := yaml.ParseFile(path)
if err != nil {
fmt.Println(err)
return nil
}

if root.Children[0].Kind == yaml.Mapping {

return viewNodeToNode(root.Children[0], nil)
}

return nil
}

func viewNodeToNode(n *yaml.ViewNode, parent *Node[string, yaml.ViewNode]) *Node[string, yaml.ViewNode] {

r := new(Node[string, yaml.ViewNode])
r.parent = parent
r.value = &n.Path
r.pointer = n

children := make([]*Node[string, yaml.ViewNode], 0)
for _, child := range n.Children {
children = append(children, viewNodeToNode(child, r))
}
r.children = children

return r
}
15 changes: 7 additions & 8 deletions momentum-core/clients/kustomization-client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package clients
import (
"fmt"
"momentum-core/config"
"momentum-core/tree"
"momentum-core/utils"

"sigs.k8s.io/kustomize/api/krusty"
Expand Down Expand Up @@ -36,13 +35,14 @@ func (kustomizationService *KustomizationValidationClient) Validate(repoName str
return err
}

repoTree, err := tree.Parse(path)
if err != nil {
fmt.Println("failed parsing validation directory")
return err
}
// TODO -> fix kustomization validator
// repoTree, err := yaml.Parse(path)
// if err != nil {
// fmt.Println("failed parsing validation directory")
// return err
// }

err = kustomizationService.check(repoTree.MomentumRoot().FullPath())
// err = kustomizationService.check(repoTree.MomentumRoot().FullPath())
if err != nil {
fmt.Println("error while validating kustomize structure (check):", err.Error())
kustomizationService.validationCleanup(path)
Expand All @@ -68,7 +68,6 @@ func (kustomizationService *KustomizationValidationClient) check(path string) er

fs := filesys.MakeFsOnDisk()

// TODO -> OpenAI ApiSchemes for FluxCD -> Kubeconform
kustomizer := krusty.MakeKustomizer(krusty.MakeDefaultOptions())

_, err := kustomizer.Run(fs, path)
Expand Down
Loading

0 comments on commit ec9a53c

Please sign in to comment.