diff --git a/dependency_resolution.go b/depresolve.go similarity index 54% rename from dependency_resolution.go rename to depresolve.go index 13168f0..e6ab3a4 100644 --- a/dependency_resolution.go +++ b/depresolve.go @@ -20,19 +20,65 @@ import ( "strings" ) -type DependencySet map[SpecificationName]struct{} +const DependencyGraphContextName = "_dependency_graph" -func newDependencySetForSpecification(s Specification) DependencySet { - set := DependencySet{} - for _, d := range s.Dependencies() { - set[d] = struct{}{} +// ResolvedDependencies represents an ordered list of Specification that should be processed in that specific order to avoid +// unresolved types. +type ResolvedDependencies SpecificationGroup + +type DependencyProvider interface { + Supports(s Specification) bool + Provide(s Specification) []SpecificationName +} + +type DependencyResolutionProcessor struct { + providers []DependencyProvider +} + +func NewDependencyResolutionProcessor(providers ...DependencyProvider) *DependencyResolutionProcessor { + return &DependencyResolutionProcessor{providers: providers} +} + +func (p DependencyResolutionProcessor) Name() string { + return "dependency_resolution_processor" +} + +func (p DependencyResolutionProcessor) Process(ctx ProcessingContext) ([]ProcessingOutput, error) { + ctx.Logger.Info("\nResolving dependencies...") + + var nodes []dependencyNode + for _, s := range ctx.Specifications { + node := dependencyNode{Specification: s, Dependencies: nil} + for _, provider := range p.providers { + if !provider.Supports(s) { + continue + } + deps := provider.Provide(s) + node.Dependencies = newDependencySet(deps...) + break + } + nodes = append(nodes, node) } - return set + + deps, err := newDependencyGraph(nodes...).resolve() + if err != nil { + return nil, errors.WrapWithMessage(err, errors.InternalErrorCode, "failed resolving dependencies") + } + ctx.Logger.Success("Dependencies resolved successfully.") + + return []ProcessingOutput{ + { + Name: DependencyGraphContextName, + Value: deps, + }, + }, nil } +type dependencySet map[SpecificationName]struct{} + // diff Returns all elements that are in s and not in o. A / B -func (s DependencySet) diff(o DependencySet) DependencySet { - diff := DependencySet{} +func (s dependencySet) diff(o dependencySet) dependencySet { + diff := dependencySet{} for d := range s { if _, found := o[d]; !found { @@ -43,18 +89,8 @@ func (s DependencySet) diff(o DependencySet) DependencySet { return diff } -func (s DependencySet) Names() []SpecificationName { - var typeNames []SpecificationName - - for k := range s { - typeNames = append(typeNames, k) - } - - return typeNames -} - -func NewDependencySet(dependencies ...SpecificationName) DependencySet { - deps := DependencySet{} +func newDependencySet(dependencies ...SpecificationName) dependencySet { + deps := dependencySet{} for _, d := range dependencies { deps[d] = struct{}{} } @@ -62,43 +98,52 @@ func NewDependencySet(dependencies ...SpecificationName) DependencySet { return deps } -type DependencyGraph []Specification - -// Merge Allows merging this dependency graph with another one and returns the result. -func (g DependencyGraph) Merge(o DependencyGraph) DependencyGraph { - var lookup = make(map[SpecificationName]bool) - var merge []Specification +type dependencyNode struct { + Specification Specification + Dependencies dependencySet +} - for _, node := range g { - merge = append(merge, node) - lookup[node.Name()] = true - } +func (d dependencyNode) SpecificationName() SpecificationName { + return d.Specification.Name() +} - for _, node := range o { - if _, found := lookup[node.Name()]; found { - continue - } - merge = append(merge, node) - } +type dependencyGraph []dependencyNode - return NewDependencyGraph(merge...) -} +//// merge Allows merging this dependency graph with another one and returns the result. +//func (g dependencyGraph) merge(o dependencyGraph) dependencyGraph { +// var lookup = make(map[SpecificationName]bool) +// var merge []dependencyNode +// +// for _, node := range g { +// merge = append(merge, node) +// lookup[node.SpecificationName()] = true +// } +// +// for _, node := range o { +// if _, found := lookup[node.SpecificationName()]; found { +// continue +// } +// merge = append(merge, node) +// } +// +// return newDependencyGraph(merge...) +//} -func NewDependencyGraph(specifications ...Specification) DependencyGraph { - return append(DependencyGraph{}, specifications...) +func newDependencyGraph(specifications ...dependencyNode) dependencyGraph { + return append(dependencyGraph{}, specifications...) } -func (g DependencyGraph) Resolve() (ResolvedDependencies, error) { - var resolved []Specification +func (g dependencyGraph) resolve() (ResolvedDependencies, error) { + var resolved ResolvedDependencies // Look up of nodes to their typeName Names. specByTypeNames := map[SpecificationName]Specification{} // Map nodes to dependencies - dependenciesByTypeNames := map[SpecificationName]DependencySet{} + dependenciesByTypeNames := map[SpecificationName]dependencySet{} for _, n := range g { - specByTypeNames[n.Name()] = n - dependenciesByTypeNames[n.Name()] = newDependencySetForSpecification(n) + specByTypeNames[n.SpecificationName()] = n.Specification + dependenciesByTypeNames[n.SpecificationName()] = n.Dependencies } // The algorithm simply processes all nodes and tries to find the ones that have no dependencies. @@ -124,7 +169,7 @@ func (g DependencyGraph) Resolve() (ResolvedDependencies, error) { if _, found := specByTypeNames[dependency]; !found { return nil, errors.NewWithMessage( errors.InternalErrorCode, - fmt.Sprintf("specification with type FilePath \"%s\" depends on an unresolved type FilePath \"%s\"", + fmt.Sprintf("specification with type %q depends on an unresolved type %q", typeName, dependency, ), @@ -142,7 +187,7 @@ func (g DependencyGraph) Resolve() (ResolvedDependencies, error) { return nil, errors.NewWithMessage( errors.InternalErrorCode, fmt.Sprintf( - "circular dependencies found between nodes \"%s\"", + "circular dependencies found between nodes %q", strings.Join(circularDependencies, "\", \""), ), ) @@ -156,15 +201,10 @@ func (g DependencyGraph) Resolve() (ResolvedDependencies, error) { // Remove the resolved nodes from the remaining dependenciesByTypeNames. for typeName, dependencies := range dependenciesByTypeNames { - diff := dependencies.diff(NewDependencySet(typeNamesWithNoDependencies...)) + diff := dependencies.diff(newDependencySet(typeNamesWithNoDependencies...)) dependenciesByTypeNames[typeName] = diff } } - return append(ResolvedDependencies{}, resolved...), nil + return resolved, nil } - -// ResolvedDependencies represents an ordered list of Specification that should be processed in that specific order to avoid -// unresolved types. -// TODO Remove specification group and add its methods here. -type ResolvedDependencies SpecificationGroup diff --git a/depresolve_test.go b/depresolve_test.go new file mode 100644 index 0000000..107d059 --- /dev/null +++ b/depresolve_test.go @@ -0,0 +1,190 @@ +package specter + +import ( + "github.com/morebec/go-errors/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "os" + "testing" +) + +// MockDependencyProvider is a mock implementation of DependencyProvider for testing. +type MockDependencyProvider struct { + supportFunc func(s Specification) bool + provideFunc func(s Specification) []SpecificationName +} + +func (m *MockDependencyProvider) Supports(s Specification) bool { + return m.supportFunc(s) +} + +func (m *MockDependencyProvider) Provide(s Specification) []SpecificationName { + return m.provideFunc(s) +} + +func TestDependencyResolutionProcessor_Process(t *testing.T) { + type args struct { + specifications []Specification + providers []DependencyProvider + } + tests := []struct { + name string + given args + then ResolvedDependencies + expectedError error + }{ + { + name: "GIVEN no matching providers THEN returns resolved dependencies in the same order", + given: args{ + providers: []DependencyProvider{ + &MockDependencyProvider{ + supportFunc: func(s Specification) bool { + return false + }, + provideFunc: func(s Specification) []SpecificationName { + return nil + }, + }, + &MockDependencyProvider{ + supportFunc: func(s Specification) bool { + return false + }, + provideFunc: func(s Specification) []SpecificationName { + if s.Name() == "spec1" { + return []SpecificationName{"spec2"} + } + return nil + }, + }, + }, + specifications: SpecificationGroup{ + NewGenericSpecification("spec1", "type", Source{}), + NewGenericSpecification("spec2", "type", Source{}), + }, + }, + then: ResolvedDependencies{ + NewGenericSpecification("spec1", "type", Source{}), + NewGenericSpecification("spec2", "type", Source{}), + }, + expectedError: nil, + }, + { + name: "GIVEN no providers THEN returns nil", + given: args{ + providers: nil, + specifications: nil, + }, + then: nil, + expectedError: nil, + }, + { + name: "GIVEN a simple acyclic graph and multiple providers WHEN resolved THEN returns resolved dependencies", + given: args{ + providers: []DependencyProvider{ + &MockDependencyProvider{ + supportFunc: func(s Specification) bool { + return false + }, + provideFunc: func(s Specification) []SpecificationName { + return nil + }, + }, + &MockDependencyProvider{ + supportFunc: func(s Specification) bool { + return s.Type() == "type" + }, + provideFunc: func(s Specification) []SpecificationName { + if s.Name() == "spec1" { + return []SpecificationName{"spec2"} + } + return nil + }, + }, + }, + specifications: SpecificationGroup{ + NewGenericSpecification("spec1", "type", Source{}), + NewGenericSpecification("spec2", "type", Source{}), + }, + }, + then: ResolvedDependencies{ + NewGenericSpecification("spec2", "type", Source{}), // topological sort + NewGenericSpecification("spec1", "type", Source{}), + }, + expectedError: nil, + }, + { + name: "GIVEN circular dependencies WHEN resolved THEN returns an error", + given: args{ + providers: []DependencyProvider{ + &MockDependencyProvider{ + supportFunc: func(s Specification) bool { + return s.Type() == "type" + }, + provideFunc: func(s Specification) []SpecificationName { + if s.Name() == "spec1" { + return []SpecificationName{"spec2"} + } else if s.Name() == "spec2" { + return []SpecificationName{"spec1"} + } + return nil + }, + }, + }, + specifications: SpecificationGroup{ + NewGenericSpecification("spec1", "type", Source{}), + NewGenericSpecification("spec2", "type", Source{}), + }, + }, + then: nil, + expectedError: errors.NewWithMessage(errors.InternalErrorCode, "circular dependencies found"), + }, + { + name: "GIVEN unresolvable dependencies THEN returns an error", + given: args{ + providers: []DependencyProvider{ + &MockDependencyProvider{ + supportFunc: func(s Specification) bool { + return s.Type() == "type" + }, + provideFunc: func(s Specification) []SpecificationName { + return []SpecificationName{"spec3"} + }, + }, + }, + specifications: SpecificationGroup{ + NewGenericSpecification("spec1", "type", Source{}), + NewGenericSpecification("spec2", "type", Source{}), // spec2 is not provided + }, + }, + then: nil, + expectedError: errors.NewWithMessage(errors.InternalErrorCode, "specification with type \"spec1\" depends on an unresolved type \"spec3\""), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + processor := NewDependencyResolutionProcessor(tt.given.providers...) + + ctx := ProcessingContext{ + Specifications: tt.given.specifications, + Logger: NewColoredOutputLogger(ColoredOutputLoggerConfig{ + EnableColors: true, + Writer: os.Stdout, + }), + } + + var err error + ctx.Outputs, err = processor.Process(ctx) + if tt.expectedError != nil { + require.Error(t, err) + assert.ErrorContains(t, err, tt.expectedError.Error()) + return + } + + output := ctx.Output(DependencyGraphContextName).Value + graph := output.(ResolvedDependencies) + + require.NoError(t, err) + require.Equal(t, tt.then, graph) + }) + } +} diff --git a/go.mod b/go.mod index d521a93..687174e 100644 --- a/go.mod +++ b/go.mod @@ -5,14 +5,19 @@ go 1.18 require ( github.com/hashicorp/hcl/v2 v2.15.0 github.com/logrusorgru/aurora v2.0.3+incompatible + github.com/morebec/go-errors v0.0.0-20230111142457-1cf1ae0a90e1 + github.com/stretchr/testify v1.9.0 github.com/zclconf/go-cty v1.12.1 ) require ( github.com/agext/levenshtein v1.2.3 // indirect github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/google/go-cmp v0.5.9 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect - github.com/morebec/go-errors v0.0.0-20230111142457-1cf1ae0a90e1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect golang.org/x/text v0.6.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 98427f6..4f984c0 100644 --- a/go.sum +++ b/go.sum @@ -2,7 +2,9 @@ github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7l github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -15,15 +17,29 @@ github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczG github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= -github.com/morebec/go-errors v0.0.0-20230110041812-168878ec9ed1 h1:SU4Joq53EGwfrWIoWWGmQBowfu7NxCP7cPJK/7WdbSE= -github.com/morebec/go-errors v0.0.0-20230110041812-168878ec9ed1/go.mod h1:2kWkLazUclZ7piPieWawywbP06YUhsJor53Tn01dd9o= github.com/morebec/go-errors v0.0.0-20230111142457-1cf1ae0a90e1 h1:DO82P4i/4seRJc09t0v9QhtxVA7DMSTvK5XHxlPfVsg= github.com/morebec/go-errors v0.0.0-20230111142457-1cf1ae0a90e1/go.mod h1:MkiV/ScSJ7upu71su3IrfHDN2vec0cilxtOl/o5iTRw= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/zclconf/go-cty v1.12.1 h1:PcupnljUm9EIvbgSHQnHhUr3fO6oFmkOrvs2BAFNXXY= github.com/zclconf/go-cty v1.12.1/go.mod h1:s9IfD1LK5ccNMSWCVFCE2rJfHiZgi7JijgeWIMfhLvA= golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/hcl.go b/hcl.go index 7025060..84cce2a 100644 --- a/hcl.go +++ b/hcl.go @@ -89,8 +89,7 @@ func (l HCLGenericSpecLoader) Load(s Source) ([]Specification, error) { } // Create specification and add to list - //goland:noinspection GoRedundantConversion - specifications = append(specifications, GenericSpecification{ + specifications = append(specifications, &GenericSpecification{ name: SpecificationName(block.Labels[0]), typ: SpecificationType(block.Type), source: s, diff --git a/linting.go b/linting.go index 0ae918e..e041255 100644 --- a/linting.go +++ b/linting.go @@ -33,7 +33,7 @@ func (l LintingProcessor) Name() string { func (l LintingProcessor) Process(ctx ProcessingContext) ([]ProcessingOutput, error) { linter := CompositeSpecificationLinter(l.linters...) ctx.Logger.Info("\nLinting specifications ...") - lr := linter.Lint(SpecificationGroup(ctx.DependencyGraph)) + lr := linter.Lint(ctx.Specifications) if lr.HasWarnings() { for _, w := range lr.Warnings() { ctx.Logger.Warning(fmt.Sprintf("Warning: %s\n", w.Message)) diff --git a/processing.go b/processing.go index 1e8131f..b799630 100644 --- a/processing.go +++ b/processing.go @@ -1,9 +1,9 @@ package specter type ProcessingContext struct { - DependencyGraph ResolvedDependencies - Outputs []ProcessingOutput - Logger Logger + Specifications SpecificationGroup + Outputs []ProcessingOutput + Logger Logger } // Output returns the output associated with a given processor. @@ -35,9 +35,9 @@ type SpecificationProcessor interface { } type OutputProcessingContext struct { - DependencyGraph ResolvedDependencies - Outputs []ProcessingOutput - Logger Logger + Specifications SpecificationGroup + Outputs []ProcessingOutput + Logger Logger } // OutputProcessor are services responsible for processing outputs of SpecProcessors. diff --git a/spec.go b/spec.go index 4b72457..55f2b5f 100644 --- a/spec.go +++ b/spec.go @@ -1,6 +1,7 @@ package specter import ( + "fmt" "github.com/zclconf/go-cty/cty" ) @@ -28,59 +29,48 @@ type Specification interface { // SetSource sets the source of the specification. // This method should only be used by loaders. SetSource(s Source) - - // Dependencies returns a list of the Names of the specifications this one depends on. - Dependencies() []SpecificationName } // GenericSpecification is a generic implementation of a Specification that saves its attributes in a list of attributes for introspection. // these can be useful for loaders that are looser in what they allow. type GenericSpecification struct { - name SpecificationName - typ SpecificationType - source Source - dependencies []SpecificationName - Attributes []GenericSpecAttribute + name SpecificationName + typ SpecificationType + source Source + Attributes []GenericSpecAttribute } -func (s GenericSpecification) SetSource(src Source) { +func NewGenericSpecification(name SpecificationName, typ SpecificationType, source Source) *GenericSpecification { + return &GenericSpecification{name: name, typ: typ, source: source} +} + +func (s *GenericSpecification) SetSource(src Source) { s.source = src } -func (s GenericSpecification) Description() string { - if s.HasAttribute("description") { - attr := s.Attribute("description") - gAttr, ok := attr.Value.(GenericValue) - if ok { - return gAttr.AsString() - } +func (s *GenericSpecification) Description() string { + if !s.HasAttribute("description") { + return "" } - return "" + attr := s.Attribute("description") + return attr.Value.String() } -func NewGenericSpecification(name SpecificationName, typ SpecificationType, source Source, dependencies []SpecificationName) *GenericSpecification { - return &GenericSpecification{name: name, typ: typ, source: source, dependencies: dependencies} -} - -func (s GenericSpecification) Name() SpecificationName { +func (s *GenericSpecification) Name() SpecificationName { return s.name } -func (s GenericSpecification) Type() SpecificationType { +func (s *GenericSpecification) Type() SpecificationType { return s.typ } -func (s GenericSpecification) Source() Source { +func (s *GenericSpecification) Source() Source { return s.source } -func (s GenericSpecification) Dependencies() []SpecificationName { - return s.dependencies -} - // Attribute returns an attribute by its FilePath or nil if it was not found. -func (s GenericSpecification) Attribute(name string) *GenericSpecAttribute { +func (s *GenericSpecification) Attribute(name string) *GenericSpecAttribute { for _, a := range s.Attributes { if a.Name == name { return &a @@ -91,7 +81,7 @@ func (s GenericSpecification) Attribute(name string) *GenericSpecAttribute { } // HasAttribute indicates if a specification has a certain attribute or not. -func (s GenericSpecification) HasAttribute(name string) bool { +func (s *GenericSpecification) HasAttribute(name string) bool { for _, a := range s.Attributes { if a.Name == name { return true @@ -115,24 +105,27 @@ type GenericSpecAttribute struct { Value AttributeValue } -func (a GenericSpecAttribute) AsGenericValue() GenericValue { - return a.Value.(GenericValue) -} - -func (a GenericSpecAttribute) AsObjectValue() ObjectValue { - return a.Value.(ObjectValue) -} - type AttributeValue interface { - IsAttributeValue() + String() string } +var _ AttributeValue = GenericValue{} + // GenericValue represents a generic value that is mostly unknown in terms of type and intent. type GenericValue struct { cty.Value } -func (d GenericValue) IsAttributeValue() {} +func (d GenericValue) String() string { + switch d.Type() { + case cty.String: + return d.Value.AsString() + default: + return "" + } +} + +var _ AttributeValue = ObjectValue{} // ObjectValue represents a type of attribute value that is a nested data structure as opposed to a scalar value. type ObjectValue struct { @@ -140,7 +133,9 @@ type ObjectValue struct { Attributes []GenericSpecAttribute } -func (o ObjectValue) IsAttributeValue() {} +func (o ObjectValue) String() string { + return fmt.Sprintf("ObjectValue {Type: %s, Attributes: %v}", o.Type, o.Attributes) +} // SpecificationGroup Represents a list of Specification. type SpecificationGroup []Specification diff --git a/spec_test.go b/spec_test.go index 27455a9..547927f 100644 --- a/spec_test.go +++ b/spec_test.go @@ -3,9 +3,161 @@ package specter import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/zclconf/go-cty/cty" "testing" ) +func TestGenericSpecification_Description(t *testing.T) { + tests := []struct { + name string + given *GenericSpecification + then string + }{ + { + name: "GIVEN a specification with a description attribute THEN return the description", + given: &GenericSpecification{ + Attributes: []GenericSpecAttribute{ + { + Name: "description", + Value: GenericValue{cty.StringVal("This is a test specification")}, + }, + }, + }, + then: "This is a test specification", + }, + { + name: "GIVEN a specification without a description attribute THEN return an empty string", + given: &GenericSpecification{ + Attributes: []GenericSpecAttribute{}, + }, + then: "", + }, + { + name: "GIVEN a specification with a non-string description THEN return an empty string", + given: &GenericSpecification{ + Attributes: []GenericSpecAttribute{ + { + Name: "description", + Value: GenericValue{cty.NumberIntVal(42)}, // Not a string value + }, + }, + }, + then: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.given.Description() + assert.Equal(t, tt.then, got) + }) + } +} + +func TestGenericSpecification_Attribute(t *testing.T) { + tests := []struct { + name string + given *GenericSpecification + when string + then *GenericSpecAttribute + }{ + { + name: "GIVEN a specification with a specific attribute WHEN Attribute is called THEN return the attribute", + given: &GenericSpecification{ + Attributes: []GenericSpecAttribute{ + { + Name: "attr1", + Value: GenericValue{cty.StringVal("value1")}, + }, + }, + }, + when: "attr1", + then: &GenericSpecAttribute{ + Name: "attr1", + Value: GenericValue{cty.StringVal("value1")}, + }, + }, + { + name: "GIVEN a specification without the specified attribute WHEN Attribute is called THEN return nil", + given: &GenericSpecification{ + Attributes: []GenericSpecAttribute{}, + }, + when: "nonexistent", + then: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.given.Attribute(tt.when) + assert.Equal(t, tt.then, got) + }) + } +} + +func TestGenericSpecification_HasAttribute(t *testing.T) { + tests := []struct { + name string + given *GenericSpecification + when string + then bool + }{ + { + name: "GIVEN a specification with a specific attribute WHEN HasAttribute is called THEN return true", + given: &GenericSpecification{ + Attributes: []GenericSpecAttribute{ + { + Name: "attr1", + Value: GenericValue{cty.StringVal("value1")}, + }, + }, + }, + when: "attr1", + then: true, + }, + { + name: "GIVEN a specification without the specified attribute WHEN HasAttribute is called THEN return false", + given: &GenericSpecification{ + Attributes: []GenericSpecAttribute{}, + }, + when: "nonexistent", + then: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.given.HasAttribute(tt.when) + assert.Equal(t, tt.then, got) + }) + } +} + +func TestGenericSpecification_SetSource(t *testing.T) { + tests := []struct { + name string + given *GenericSpecification + when Source + then Source + }{ + { + name: "GIVEN a specification WHEN SetSource is called THEN updates the source", + given: &GenericSpecification{ + source: Source{Location: "initial/path"}, + }, + when: Source{Location: "new/path"}, + then: Source{Location: "new/path"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.given.SetSource(tt.when) + assert.Equal(t, tt.then, tt.given.Source()) + }) + } +} + // Test cases for NewSpecGroup func TestNewSpecGroup(t *testing.T) { tests := []struct { @@ -15,7 +167,7 @@ func TestNewSpecGroup(t *testing.T) { then func(SpecificationGroup) bool }{ { - name: "GIVEN no specifications WHEN calling NewSpecGroup THEN returns an empty group", + name: "GIVEN no specifications WHEN calling NewSpecGroup THEN return an empty group", given: []Specification{}, when: func() SpecificationGroup { return NewSpecGroup() @@ -25,15 +177,15 @@ func TestNewSpecGroup(t *testing.T) { }, }, { - name: "GIVEN multiple specifications WHEN calling NewSpecGroup THEN returns a group with those specifications", + name: "GIVEN multiple specifications WHEN calling NewSpecGroup THEN return a group with those specifications", given: []Specification{ - NewGenericSpecification("spec1", "type", Source{}, nil), - NewGenericSpecification("spec2", "type", Source{}, nil), + NewGenericSpecification("spec1", "type", Source{}), + NewGenericSpecification("spec2", "type", Source{}), }, when: func() SpecificationGroup { return NewSpecGroup( - NewGenericSpecification("spec1", "type", Source{}, nil), - NewGenericSpecification("spec2", "type", Source{}, nil), + NewGenericSpecification("spec1", "type", Source{}), + NewGenericSpecification("spec2", "type", Source{}), ) }, then: func(result SpecificationGroup) bool { @@ -54,7 +206,7 @@ func TestNewSpecGroup(t *testing.T) { } } -// Test cases for Merge +// Test cases for merge func TestSpecificationGroup_Merge(t *testing.T) { tests := []struct { name string @@ -63,30 +215,30 @@ func TestSpecificationGroup_Merge(t *testing.T) { then SpecificationGroup }{ { - name: "GIVEN two disjoint groups THEN returns a group with all specifications", + name: "GIVEN two disjoint groups THEN return a group with all specifications", given: NewSpecGroup( - NewGenericSpecification("spec1", "type", Source{}, nil), + NewGenericSpecification("spec1", "type", Source{}), ), when: NewSpecGroup( - NewGenericSpecification("spec2", "type", Source{}, nil), + NewGenericSpecification("spec2", "type", Source{}), ), then: NewSpecGroup( - NewGenericSpecification("spec1", "type", Source{}, nil), - NewGenericSpecification("spec2", "type", Source{}, nil), + NewGenericSpecification("spec1", "type", Source{}), + NewGenericSpecification("spec2", "type", Source{}), ), }, { - name: "GIVEN two groups with overlapping specifications THEN returns a group without duplicates", + name: "GIVEN two groups with overlapping specifications THEN return a group without duplicates", given: NewSpecGroup( - NewGenericSpecification("spec1", "type", Source{}, nil), + NewGenericSpecification("spec1", "type", Source{}), ), when: NewSpecGroup( - NewGenericSpecification("spec1", "type", Source{}, nil), - NewGenericSpecification("spec2", "type", Source{}, nil), + NewGenericSpecification("spec1", "type", Source{}), + NewGenericSpecification("spec2", "type", Source{}), ), then: NewSpecGroup( - NewGenericSpecification("spec1", "type", Source{}, nil), - NewGenericSpecification("spec2", "type", Source{}, nil), + NewGenericSpecification("spec1", "type", Source{}), + NewGenericSpecification("spec2", "type", Source{}), ), }, } @@ -109,7 +261,7 @@ func TestSpecificationGroup_Select(t *testing.T) { { name: "GIVEN no specifications matches, THEN return an empty group", given: SpecificationGroup{ - NewGenericSpecification("name", "type", Source{}, nil), + NewGenericSpecification("name", "type", Source{}), }, when: func(s Specification) bool { return false @@ -119,14 +271,14 @@ func TestSpecificationGroup_Select(t *testing.T) { { name: "GIVEN specifications matches, THEN return a group with only matching specifications", given: SpecificationGroup{ - NewGenericSpecification("spec1", "type", Source{}, nil), - NewGenericSpecification("spec2", "type", Source{}, nil), + NewGenericSpecification("spec1", "type", Source{}), + NewGenericSpecification("spec2", "type", Source{}), }, when: func(s Specification) bool { return s.Name() == "spec2" }, then: SpecificationGroup{ - NewGenericSpecification("spec2", "type", Source{}, nil), + NewGenericSpecification("spec2", "type", Source{}), }, }, } @@ -148,7 +300,7 @@ func TestSpecificationGroup_SelectType(t *testing.T) { { name: "GIVEN no specifications matches, THEN return an empty group", given: SpecificationGroup{ - NewGenericSpecification("name", "type", Source{}, nil), + NewGenericSpecification("name", "type", Source{}), }, when: "not_found", then: SpecificationGroup{}, @@ -156,12 +308,12 @@ func TestSpecificationGroup_SelectType(t *testing.T) { { name: "GIVEN a specification matches, THEN return a group with matching specification", given: SpecificationGroup{ - NewGenericSpecification("spec1", "type1", Source{}, nil), - NewGenericSpecification("spec2", "type2", Source{}, nil), + NewGenericSpecification("spec1", "type1", Source{}), + NewGenericSpecification("spec2", "type2", Source{}), }, when: "type1", then: SpecificationGroup{ - NewGenericSpecification("spec1", "type1", Source{}, nil), + NewGenericSpecification("spec1", "type1", Source{}), }, }, } @@ -173,6 +325,47 @@ func TestSpecificationGroup_SelectType(t *testing.T) { } } +func TestSpecificationGroup_SelectName(t *testing.T) { + tests := []struct { + name string + given SpecificationGroup + when SpecificationName + then Specification + }{ + { + name: "GIVEN a group with multiple specifications WHEN selecting an existing name THEN return the corresponding specification", + given: NewSpecGroup( + NewGenericSpecification("spec1", "type", Source{}), + NewGenericSpecification("spec2", "type", Source{}), + ), + when: "spec2", + then: NewGenericSpecification("spec2", "type", Source{}), + }, + { + name: "GIVEN a group with multiple specifications WHEN selecting a non-existent name THEN return nil", + given: NewSpecGroup( + NewGenericSpecification("spec1", "type", Source{}), + NewGenericSpecification("spec2", "type", Source{}), + ), + when: "spec3", + then: nil, + }, + { + name: "GIVEN an empty group WHEN selecting a name THEN return nil", + given: NewSpecGroup(), + when: "spec1", + then: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.given.SelectName(tt.when) + require.Equal(t, tt.then, got) + }) + } +} + func TestSpecificationGroup_SelectNames(t *testing.T) { tests := []struct { name string @@ -183,7 +376,7 @@ func TestSpecificationGroup_SelectNames(t *testing.T) { { name: "GIVEN no specifications matches, THEN return a group with no values", given: SpecificationGroup{ - NewGenericSpecification("name", "type", Source{}, nil), + NewGenericSpecification("name", "type", Source{}), }, when: []SpecificationName{"not_found"}, then: SpecificationGroup{}, @@ -191,12 +384,12 @@ func TestSpecificationGroup_SelectNames(t *testing.T) { { name: "GIVEN a specification matches, THEN return a group with matching specification", given: SpecificationGroup{ - NewGenericSpecification("spec1", "type", Source{}, nil), - NewGenericSpecification("spec2", "type", Source{}, nil), + NewGenericSpecification("spec1", "type", Source{}), + NewGenericSpecification("spec2", "type", Source{}), }, when: []SpecificationName{"spec1"}, then: SpecificationGroup{ - NewGenericSpecification("spec1", "type", Source{}, nil), + NewGenericSpecification("spec1", "type", Source{}), }, }, } @@ -218,20 +411,20 @@ func TestSpecificationGroup_Exclude(t *testing.T) { { name: "GIVEN no specifications matches, THEN return a group with the same values", given: SpecificationGroup{ - NewGenericSpecification("name", "type", Source{}, nil), + NewGenericSpecification("name", "type", Source{}), }, when: func(s Specification) bool { return false }, then: SpecificationGroup{ - NewGenericSpecification("name", "type", Source{}, nil), + NewGenericSpecification("name", "type", Source{}), }, }, { name: "GIVEN specifications matches, THEN return a group without matching specifications", given: SpecificationGroup{ - NewGenericSpecification("spec1", "type", Source{}, nil), - NewGenericSpecification("spec2", "type", Source{}, nil), + NewGenericSpecification("spec1", "type", Source{}), + NewGenericSpecification("spec2", "type", Source{}), }, when: func(s Specification) bool { return true @@ -257,22 +450,22 @@ func TestSpecificationGroup_ExcludeType(t *testing.T) { { name: "GIVEN no specifications matches, THEN return a group with the same values", given: SpecificationGroup{ - NewGenericSpecification("name", "type", Source{}, nil), + NewGenericSpecification("name", "type", Source{}), }, when: "not_found", then: SpecificationGroup{ - NewGenericSpecification("name", "type", Source{}, nil), + NewGenericSpecification("name", "type", Source{}), }, }, { name: "GIVEN a specification matches, THEN return a group without matching specification", given: SpecificationGroup{ - NewGenericSpecification("spec1", "type1", Source{}, nil), - NewGenericSpecification("spec2", "type2", Source{}, nil), + NewGenericSpecification("spec1", "type1", Source{}), + NewGenericSpecification("spec2", "type2", Source{}), }, when: "type1", then: SpecificationGroup{ - NewGenericSpecification("spec2", "type2", Source{}, nil), + NewGenericSpecification("spec2", "type2", Source{}), }, }, } @@ -294,22 +487,22 @@ func TestSpecificationGroup_ExcludeNames(t *testing.T) { { name: "GIVEN no specifications matches, THEN return a group with the same values", given: SpecificationGroup{ - NewGenericSpecification("name", "type", Source{}, nil), + NewGenericSpecification("name", "type", Source{}), }, when: []SpecificationName{"not_found"}, then: SpecificationGroup{ - NewGenericSpecification("name", "type", Source{}, nil), + NewGenericSpecification("name", "type", Source{}), }, }, { name: "GIVEN a specification matches, THEN return a group without matching specification", given: SpecificationGroup{ - NewGenericSpecification("spec1", "type", Source{}, nil), - NewGenericSpecification("spec2", "type", Source{}, nil), + NewGenericSpecification("spec1", "type", Source{}), + NewGenericSpecification("spec2", "type", Source{}), }, when: []SpecificationName{"spec1"}, then: SpecificationGroup{ - NewGenericSpecification("spec2", "type", Source{}, nil), + NewGenericSpecification("spec2", "type", Source{}), }, }, } @@ -329,10 +522,10 @@ func TestMapSpecGroup(t *testing.T) { then []string }{ { - name: "GIVEN a group with multiple specifications WHEN mapped to their names THEN returns a slice of specification names", + name: "GIVEN a group with multiple specifications WHEN mapped to their names THEN return a slice of specification names", given: NewSpecGroup( - NewGenericSpecification("spec1", "type", Source{}, nil), - NewGenericSpecification("spec2", "type", Source{}, nil), + NewGenericSpecification("spec1", "type", Source{}), + NewGenericSpecification("spec2", "type", Source{}), ), when: func(s Specification) string { return string(s.Name()) @@ -340,7 +533,7 @@ func TestMapSpecGroup(t *testing.T) { then: []string{"spec1", "spec2"}, }, { - name: "GIVEN an empty group WHEN mapped THEN returns a nil slice", + name: "GIVEN an empty group WHEN mapped THEN return a nil slice", given: NewSpecGroup(), when: func(s Specification) string { return string(s.Name()) @@ -348,10 +541,10 @@ func TestMapSpecGroup(t *testing.T) { then: nil, }, { - name: "GIVEN a group with multiple specifications WHEN mapped to a constant value THEN returns a slice of that value", + name: "GIVEN a group with multiple specifications WHEN mapped to a constant value THEN return a slice of that value", given: NewSpecGroup( - NewGenericSpecification("spec1", "type", Source{}, nil), - NewGenericSpecification("spec2", "type", Source{}, nil), + NewGenericSpecification("spec1", "type", Source{}), + NewGenericSpecification("spec2", "type", Source{}), ), when: func(s Specification) string { return "constant" diff --git a/specter.go b/specter.go index 5fdd428..ac10758 100644 --- a/specter.go +++ b/specter.go @@ -79,18 +79,9 @@ func (s Specter) Run(sourceLocations []string) error { return e } - // Resolve Dependencies - var deps ResolvedDependencies - deps, err = s.ResolveDependencies(specifications) - if err != nil { - e := errors.WrapWithMessage(err, errors.InternalErrorCode, "dependency resolution failed") - s.Logger.Error(e.Error()) - return e - } - // Process Specifications var outputs []ProcessingOutput - outputs, err = s.ProcessSpecifications(deps) + outputs, err = s.ProcessSpecifications(specifications) stats.NbOutputs = len(outputs) if err != nil { e := errors.WrapWithMessage(err, errors.InternalErrorCode, "failed processing specifications") @@ -102,7 +93,7 @@ func (s Specter) Run(sourceLocations []string) error { return nil } - if err = s.ProcessOutputs(deps, outputs); err != nil { + if err = s.ProcessOutputs(specifications, outputs); err != nil { e := errors.WrapWithMessage(err, errors.InternalErrorCode, "failed processing outputs") s.Logger.Error(e.Error()) return e @@ -189,23 +180,12 @@ func (s Specter) LoadSpecifications(sources []Source) ([]Specification, error) { return specifications, errors.GroupOrNil(errs) } -// ResolveDependencies resolves the dependencies between specifications. -func (s Specter) ResolveDependencies(specifications []Specification) (ResolvedDependencies, error) { - s.Logger.Info("\nResolving dependencies...") - deps, err := NewDependencyGraph(specifications...).Resolve() - if err != nil { - return nil, errors.WrapWithMessage(err, errors.InternalErrorCode, "failed resolving dependencies") - } - s.Logger.Success("Dependencies resolved successfully.") - return deps, nil -} - // ProcessSpecifications sends the specifications to processors. -func (s Specter) ProcessSpecifications(specifications ResolvedDependencies) ([]ProcessingOutput, error) { +func (s Specter) ProcessSpecifications(specs []Specification) ([]ProcessingOutput, error) { ctx := ProcessingContext{ - DependencyGraph: specifications, - Outputs: nil, - Logger: s.Logger, + Specifications: specs, + Outputs: nil, + Logger: s.Logger, } s.Logger.Info("\nProcessing specifications ...") @@ -227,11 +207,11 @@ func (s Specter) ProcessSpecifications(specifications ResolvedDependencies) ([]P } // ProcessOutputs sends a list of ProcessingOutputs to the registered OutputProcessors. -func (s Specter) ProcessOutputs(specifications ResolvedDependencies, outputs []ProcessingOutput) error { +func (s Specter) ProcessOutputs(specifications []Specification, outputs []ProcessingOutput) error { ctx := OutputProcessingContext{ - DependencyGraph: specifications, - Outputs: outputs, - Logger: s.Logger, + Specifications: specifications, + Outputs: outputs, + Logger: s.Logger, } s.Logger.Info("\nProcessing outputs ...")