Skip to content

Commit

Permalink
Make dependency resolution a provider
Browse files Browse the repository at this point in the history
  • Loading branch information
jwillp committed Aug 19, 2024
1 parent 49b9038 commit 9c5c060
Show file tree
Hide file tree
Showing 10 changed files with 606 additions and 188 deletions.
148 changes: 94 additions & 54 deletions dependency_resolution.go → depresolve.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -43,62 +89,61 @@ 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{}{}
}

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.
Expand All @@ -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,
),
Expand All @@ -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, "\", \""),
),
)
Expand All @@ -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
190 changes: 190 additions & 0 deletions depresolve_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
}
Loading

0 comments on commit 9c5c060

Please sign in to comment.