Skip to content

Commit 8585af8

Browse files
committed
use generics to make graph reusable
Signed-off-by: Nicolas De Loof <[email protected]>
1 parent b949fc8 commit 8585af8

File tree

5 files changed

+144
-124
lines changed

5 files changed

+144
-124
lines changed

graph/graph.go

Lines changed: 19 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -20,74 +20,39 @@ import (
2020
"fmt"
2121
"strings"
2222

23-
"github.com/compose-spec/compose-go/v2/types"
2423
"github.com/compose-spec/compose-go/v2/utils"
2524
"golang.org/x/exp/slices"
2625
)
2726

2827
// graph represents project as service dependencies
29-
type graph struct {
30-
vertices map[string]*vertex
28+
type graph[T any] struct {
29+
vertices map[string]*vertex[T]
3130
}
3231

3332
// vertex represents a service in the dependencies structure
34-
type vertex struct {
33+
type vertex[T any] struct {
3534
key string
36-
service *types.ServiceConfig
37-
children map[string]*vertex
38-
parents map[string]*vertex
35+
service *T
36+
children map[string]*vertex[T]
37+
parents map[string]*vertex[T]
3938
}
4039

41-
// newGraph creates a service graph from project
42-
func newGraph(project *types.Project) (*graph, error) {
43-
g := &graph{
44-
vertices: map[string]*vertex{},
45-
}
46-
47-
for name, s := range project.Services {
48-
g.addVertex(name, s)
49-
}
50-
51-
for name, s := range project.Services {
52-
src := g.vertices[name]
53-
for dep, condition := range s.DependsOn {
54-
dest, ok := g.vertices[dep]
55-
if !ok {
56-
if condition.Required {
57-
if ds, exists := project.DisabledServices[dep]; exists {
58-
return nil, fmt.Errorf("service %q is required by %q but is disabled. Can be enabled by profiles %s", dep, name, ds.Profiles)
59-
}
60-
return nil, fmt.Errorf("service %q depends on unknown service %q", name, dep)
61-
}
62-
delete(s.DependsOn, name)
63-
project.Services[name] = s
64-
continue
65-
}
66-
src.children[dep] = dest
67-
dest.parents[name] = src
68-
}
69-
}
70-
71-
err := g.checkCycle()
72-
return g, err
73-
}
74-
75-
func (g *graph) addVertex(name string, service types.ServiceConfig) {
76-
g.vertices[name] = &vertex{
40+
func (g *graph[T]) addVertex(name string, service T) {
41+
g.vertices[name] = &vertex[T]{
7742
key: name,
7843
service: &service,
79-
parents: map[string]*vertex{},
80-
children: map[string]*vertex{},
44+
parents: map[string]*vertex[T]{},
45+
children: map[string]*vertex[T]{},
8146
}
8247
}
8348

84-
func (g *graph) addEdge(src, dest string) {
49+
func (g *graph[T]) addEdge(src, dest string) {
8550
g.vertices[src].children[dest] = g.vertices[dest]
8651
g.vertices[dest].parents[src] = g.vertices[src]
8752
}
8853

89-
func (g *graph) roots() []*vertex {
90-
var res []*vertex
54+
func (g *graph[T]) roots() []*vertex[T] {
55+
var res []*vertex[T]
9156
for _, v := range g.vertices {
9257
if len(v.parents) == 0 {
9358
res = append(res, v)
@@ -96,8 +61,8 @@ func (g *graph) roots() []*vertex {
9661
return res
9762
}
9863

99-
func (g *graph) leaves() []*vertex {
100-
var res []*vertex
64+
func (g *graph[T]) leaves() []*vertex[T] {
65+
var res []*vertex[T]
10166
for _, v := range g.vertices {
10267
if len(v.children) == 0 {
10368
res = append(res, v)
@@ -107,8 +72,8 @@ func (g *graph) leaves() []*vertex {
10772
return res
10873
}
10974

110-
func (g *graph) checkCycle() error {
111-
// iterate on verticles in a name-order to render a predicable error message
75+
func (g *graph[T]) checkCycle() error {
76+
// iterate on vertices in a name-order to render a predicable error message
11277
// this is required by tests and enforce command reproducibility by user, which otherwise could be confusing
11378
names := utils.MapKeys(g.vertices)
11479
for _, name := range names {
@@ -120,7 +85,7 @@ func (g *graph) checkCycle() error {
12085
return nil
12186
}
12287

123-
func searchCycle(path []string, v *vertex) error {
88+
func searchCycle[T any](path []string, v *vertex[T]) error {
12489
names := utils.MapKeys(v.children)
12590
for _, name := range names {
12691
if i := slices.Index(path, name); i > 0 {
@@ -136,7 +101,7 @@ func searchCycle(path []string, v *vertex) error {
136101
}
137102

138103
// descendents return all descendents for a vertex, might contain duplicates
139-
func (v *vertex) descendents() []string {
104+
func (v *vertex[T]) descendents() []string {
140105
var vx []string
141106
for _, n := range v.children {
142107
vx = append(vx, n.key)

graph/graph_test.go

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ func TestBuildGraph(t *testing.T) {
112112
desc string
113113
services types.Services
114114
disabled types.Services
115-
expectedVertices map[string]*vertex
115+
expectedVertices map[string]*vertex[types.ServiceConfig]
116116
expectedError string
117117
}{
118118
{
@@ -123,12 +123,12 @@ func TestBuildGraph(t *testing.T) {
123123
DependsOn: types.DependsOnConfig{},
124124
},
125125
},
126-
expectedVertices: map[string]*vertex{
126+
expectedVertices: map[string]*vertex[types.ServiceConfig]{
127127
"test": {
128128
key: "test",
129129
service: &types.ServiceConfig{Name: "test"},
130-
children: map[string]*vertex{},
131-
parents: map[string]*vertex{},
130+
children: map[string]*vertex[types.ServiceConfig]{},
131+
parents: map[string]*vertex[types.ServiceConfig]{},
132132
},
133133
},
134134
},
@@ -144,18 +144,18 @@ func TestBuildGraph(t *testing.T) {
144144
DependsOn: types.DependsOnConfig{},
145145
},
146146
},
147-
expectedVertices: map[string]*vertex{
147+
expectedVertices: map[string]*vertex[types.ServiceConfig]{
148148
"test": {
149149
key: "test",
150150
service: &types.ServiceConfig{Name: "test"},
151-
children: map[string]*vertex{},
152-
parents: map[string]*vertex{},
151+
children: map[string]*vertex[types.ServiceConfig]{},
152+
parents: map[string]*vertex[types.ServiceConfig]{},
153153
},
154154
"another": {
155155
key: "another",
156156
service: &types.ServiceConfig{Name: "another"},
157-
children: map[string]*vertex{},
158-
parents: map[string]*vertex{},
157+
children: map[string]*vertex[types.ServiceConfig]{},
158+
parents: map[string]*vertex[types.ServiceConfig]{},
159159
},
160160
},
161161
},
@@ -173,20 +173,20 @@ func TestBuildGraph(t *testing.T) {
173173
DependsOn: types.DependsOnConfig{},
174174
},
175175
},
176-
expectedVertices: map[string]*vertex{
176+
expectedVertices: map[string]*vertex[types.ServiceConfig]{
177177
"test": {
178178
key: "test",
179179
service: &types.ServiceConfig{Name: "test"},
180-
children: map[string]*vertex{
180+
children: map[string]*vertex[types.ServiceConfig]{
181181
"another": {},
182182
},
183-
parents: map[string]*vertex{},
183+
parents: map[string]*vertex[types.ServiceConfig]{},
184184
},
185185
"another": {
186186
key: "another",
187187
service: &types.ServiceConfig{Name: "another"},
188-
children: map[string]*vertex{},
189-
parents: map[string]*vertex{
188+
children: map[string]*vertex[types.ServiceConfig]{},
189+
parents: map[string]*vertex[types.ServiceConfig]{
190190
"test": {},
191191
},
192192
},
@@ -204,12 +204,12 @@ func TestBuildGraph(t *testing.T) {
204204
},
205205
},
206206
},
207-
expectedVertices: map[string]*vertex{
207+
expectedVertices: map[string]*vertex[types.ServiceConfig]{
208208
"test": {
209209
key: "test",
210210
service: &types.ServiceConfig{Name: "test"},
211-
children: map[string]*vertex{},
212-
parents: map[string]*vertex{},
211+
children: map[string]*vertex[types.ServiceConfig]{},
212+
parents: map[string]*vertex[types.ServiceConfig]{},
213213
},
214214
},
215215
},
@@ -268,30 +268,30 @@ func TestBuildGraph(t *testing.T) {
268268
DependsOn: types.DependsOnConfig{},
269269
},
270270
},
271-
expectedVertices: map[string]*vertex{
271+
expectedVertices: map[string]*vertex[types.ServiceConfig]{
272272
"test": {
273273
key: "test",
274274
service: &types.ServiceConfig{Name: "test"},
275-
children: map[string]*vertex{
275+
children: map[string]*vertex[types.ServiceConfig]{
276276
"another": {},
277277
},
278-
parents: map[string]*vertex{},
278+
parents: map[string]*vertex[types.ServiceConfig]{},
279279
},
280280
"another": {
281281
key: "another",
282282
service: &types.ServiceConfig{Name: "another"},
283-
children: map[string]*vertex{
283+
children: map[string]*vertex[types.ServiceConfig]{
284284
"another_dep": {},
285285
},
286-
parents: map[string]*vertex{
286+
parents: map[string]*vertex[types.ServiceConfig]{
287287
"test": {},
288288
},
289289
},
290290
"another_dep": {
291291
key: "another_dep",
292292
service: &types.ServiceConfig{Name: "another_dep"},
293-
children: map[string]*vertex{},
294-
parents: map[string]*vertex{
293+
children: map[string]*vertex[types.ServiceConfig]{},
294+
parents: map[string]*vertex[types.ServiceConfig]{
295295
"another": {},
296296
},
297297
},
@@ -384,7 +384,7 @@ func TestWith_RootNodesAndUp(t *testing.T) {
384384
}
385385
}
386386

387-
func assertVertexEqual(t *testing.T, a, b vertex) {
387+
func assertVertexEqual(t *testing.T, a, b vertex[types.ServiceConfig]) {
388388
assert.Equal(t, a.key, b.key)
389389
assert.Equal(t, a.service.Name, b.service.Name)
390390
for c := range a.children {
@@ -397,9 +397,9 @@ func assertVertexEqual(t *testing.T, a, b vertex) {
397397
}
398398
}
399399

400-
func exampleGraph() *graph {
401-
graph := &graph{
402-
vertices: map[string]*vertex{},
400+
func exampleGraph() *graph[types.ServiceConfig] {
401+
graph := &graph[types.ServiceConfig]{
402+
vertices: map[string]*vertex[types.ServiceConfig]{},
403403
}
404404

405405
/** graph topology:

graph/services.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
Copyright 2020 The Compose Specification Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package graph
18+
19+
import (
20+
"context"
21+
"fmt"
22+
23+
"github.com/compose-spec/compose-go/v2/types"
24+
)
25+
26+
// InDependencyOrder walk the service graph an invoke VisitorFn in respect to dependency order
27+
func InDependencyOrder(ctx context.Context, project *types.Project, fn VisitorFn[types.ServiceConfig], options ...func(*Options)) error {
28+
_, err := CollectInDependencyOrder[any](ctx, project, func(ctx context.Context, s string, config types.ServiceConfig) (any, error) {
29+
return nil, fn(ctx, s, config)
30+
}, options...)
31+
return err
32+
}
33+
34+
// CollectInDependencyOrder walk the service graph an invoke CollectorFn in respect to dependency order, then return result for each call
35+
func CollectInDependencyOrder[T any](ctx context.Context, project *types.Project, fn CollectorFn[types.ServiceConfig, T], options ...func(*Options)) (map[string]T, error) {
36+
graph, err := newGraph(project)
37+
if err != nil {
38+
return nil, err
39+
}
40+
t := newTraversal(fn)
41+
for _, option := range options {
42+
option(t.Options)
43+
}
44+
err = walk(ctx, graph, t)
45+
return t.results, err
46+
}
47+
48+
// newGraph creates a service graph from project
49+
func newGraph(project *types.Project) (*graph[types.ServiceConfig], error) {
50+
g := &graph[types.ServiceConfig]{
51+
vertices: map[string]*vertex[types.ServiceConfig]{},
52+
}
53+
54+
for name, s := range project.Services {
55+
g.addVertex(name, s)
56+
}
57+
58+
for name, s := range project.Services {
59+
src := g.vertices[name]
60+
for dep, condition := range s.DependsOn {
61+
dest, ok := g.vertices[dep]
62+
if !ok {
63+
if condition.Required {
64+
if ds, exists := project.DisabledServices[dep]; exists {
65+
return nil, fmt.Errorf("service %q is required by %q but is disabled. Can be enabled by profiles %s", dep, name, ds.Profiles)
66+
}
67+
return nil, fmt.Errorf("service %q depends on unknown service %q", name, dep)
68+
}
69+
delete(s.DependsOn, name)
70+
project.Services[name] = s
71+
continue
72+
}
73+
src.children[dep] = dest
74+
dest.parents[name] = src
75+
}
76+
}
77+
78+
err := g.checkCycle()
79+
return g, err
80+
}

0 commit comments

Comments
 (0)