Skip to content

Commit e7c2f64

Browse files
committed
fix: remove Output.Path internally
Having both Output.Path and Output.Paths was a source of errors waiting to happen if the caller didn't call ResolvePaths first. This patch resolves the paths when deserialising so it reduces the mental load overall.
1 parent c619f5a commit e7c2f64

File tree

11 files changed

+82
-79
lines changed

11 files changed

+82
-79
lines changed

config/api.go

+23-15
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package config
22

33
import (
4+
"encoding/json"
45
"fmt"
56

67
"github.com/bmatcuk/doublestar/v4"
@@ -69,23 +70,36 @@ type Overlay struct {
6970
// Output defines where the aggregate versioned OpenAPI specs should be created
7071
// during compilation.
7172
type Output struct {
73+
Paths []string
74+
}
75+
76+
// outputJSON exists for historical purposes, we allowed both Path and Paths to
77+
// be specified in the api spec config. This makes handling internally more
78+
// complex as we have to deal with both, instead we can deal with it at
79+
// deserialisation time.
80+
type outputJSON struct {
7281
Path string `json:"path,omitempty"`
7382
Paths []string `json:"paths,omitempty"`
7483
}
7584

76-
// EffectivePaths returns a slice of effective configured output paths, whether
77-
// a single or multiple output paths have been configured.
78-
func (o *Output) ResolvePaths() []string {
79-
if o == nil {
80-
return []string{}
85+
func (o *Output) UnmarshalJSON(data []byte) error {
86+
oj := outputJSON{}
87+
err := json.Unmarshal(data, &oj)
88+
if err != nil {
89+
return err
8190
}
82-
if o.Path != "" {
83-
return []string{o.Path}
91+
if oj.Path != "" {
92+
if len(oj.Paths) > 0 {
93+
return fmt.Errorf("output should specify one of 'path' or 'paths', not both")
94+
}
95+
o.Paths = []string{oj.Path}
96+
return nil
8497
}
85-
return o.Paths
98+
o.Paths = oj.Paths
99+
return nil
86100
}
87101

88-
func (a APIs) init(p *Project) error {
102+
func (a APIs) init() error {
89103
if len(a) == 0 {
90104
return fmt.Errorf("no apis defined")
91105
}
@@ -100,12 +114,6 @@ func (a APIs) init(p *Project) error {
100114
return fmt.Errorf("%w (apis.%s.resources[%d])", err, api.Name, rcIndex)
101115
}
102116
}
103-
if api.Output != nil {
104-
if len(api.Output.Paths) > 0 && api.Output.Path != "" {
105-
return fmt.Errorf("output should specify one of 'path' or 'paths', not both (apis.%s.output)",
106-
api.Name)
107-
}
108-
}
109117
}
110118
return nil
111119
}

config/api_test.go

+34-22
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,56 @@
11
package config_test
22

33
import (
4-
"reflect"
4+
"encoding/json"
55
"testing"
66

7+
qt "github.com/frankban/quicktest"
8+
79
"github.com/snyk/vervet/v8/config"
810
)
911

10-
func TestOutput_ResolvePaths(t *testing.T) {
12+
func TestOutput_deserialise(t *testing.T) {
1113
tests := []struct {
12-
name string
13-
subject *config.Output
14-
want []string
14+
name string
15+
subject string
16+
wantPaths []string
17+
expectedErr string
1518
}{
1619
{
17-
name: "nil",
18-
subject: nil,
19-
want: []string{},
20+
name: "nil",
21+
subject: "{}",
22+
wantPaths: []string(nil),
23+
expectedErr: "",
24+
},
25+
{
26+
name: "returns path if exists",
27+
subject: `{"path": "path1"}`,
28+
wantPaths: []string{"path1"},
29+
expectedErr: "",
2030
},
2131
{
22-
name: "returns path if exists",
23-
subject: &config.Output{
24-
Path: "path",
25-
Paths: []string{"path1", "path2"},
26-
},
27-
want: []string{"path"},
32+
name: "return paths if exists",
33+
subject: `{"paths": ["path1", "path2"]}`,
34+
wantPaths: []string{"path1", "path2"},
35+
expectedErr: "",
2836
},
2937
{
30-
name: "return paths if path is empty",
31-
subject: &config.Output{
32-
Path: "",
33-
Paths: []string{"path1", "path2"},
34-
},
35-
want: []string{"path1", "path2"},
38+
name: "errors if both path and paths exist",
39+
subject: `{"path": "path1", "paths": ["path1", "path2"]}`,
40+
wantPaths: []string{},
41+
expectedErr: "output should specify one of 'path' or 'paths', not both",
3642
},
3743
}
3844
for _, tt := range tests {
3945
t.Run(tt.name, func(t *testing.T) {
40-
if got := tt.subject.ResolvePaths(); !reflect.DeepEqual(got, tt.want) {
41-
t.Errorf("ResolvePaths() = %v, want %v", got, tt.want)
46+
c := qt.New(t)
47+
out := config.Output{}
48+
err := json.Unmarshal([]byte(tt.subject), &out)
49+
if tt.expectedErr != "" {
50+
c.Assert(err.Error(), qt.Equals, tt.expectedErr)
51+
} else {
52+
c.Assert(err, qt.IsNil)
53+
c.Assert(out.Paths, qt.DeepEquals, tt.wantPaths)
4254
}
4355
})
4456
}

config/config_test.go

+4-2
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ servers:
5252
description: Test API`[1:],
5353
}},
5454
Output: &config.Output{
55-
Path: "testdata/output",
55+
Paths: []string{"testdata/output"},
5656
},
5757
},
5858
},
@@ -90,7 +90,9 @@ apis:
9090
- /another/place
9191
- /and/another
9292
`[1:],
93-
err: `output should specify one of 'path' or 'paths', not both \(apis\.testapi\.output\)`,
93+
err: "failed to unmarshal project configuration: " +
94+
"error unmarshaling JSON: " +
95+
"output should specify one of 'path' or 'paths', not both",
9496
}, {
9597
err: `no apis defined`,
9698
}}

config/project.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ func (p *Project) validate() error {
4545
if err != nil {
4646
return err
4747
}
48-
err = p.APIs.init(p)
48+
err = p.APIs.init()
4949
if err != nil {
5050
return err
5151
}

internal/cmd/backstage.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ func processCatalog(ctx *cli.Context, w io.Writer) error {
190190
sort.Strings(apiNames)
191191
for _, apiName := range apiNames {
192192
apiConf := proj.APIs[apiName]
193-
outputPaths := apiConf.Output.ResolvePaths()
193+
outputPaths := apiConf.Output.Paths
194194
if len(outputPaths) == 0 {
195195
log.Printf("API %q has no output paths, this command will have no effect", apiName)
196196
continue

internal/cmd/compiler.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ func projectFromContext(ctx *cli.Context) (*config.Project, error) {
151151
Path: ctx.Args().Get(0),
152152
}},
153153
Output: &config.Output{
154-
Path: ctx.Args().Get(1),
154+
Paths: []string{ctx.Args().Get(1)},
155155
},
156156
}
157157
if includePath := ctx.String("include"); includePath != "" {

internal/compiler/compiler.go

-3
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,6 @@ func New(ctx context.Context, proj *config.Project, options ...CompilerOption) (
102102
// Build output
103103
if apiConfig.Output != nil {
104104
paths := apiConfig.Output.Paths
105-
if len(paths) == 0 && apiConfig.Output.Path != "" {
106-
paths = []string{apiConfig.Output.Path}
107-
}
108105
if len(paths) > 0 {
109106
a.output = &output{
110107
paths: paths,

internal/simplebuild/build_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -857,7 +857,7 @@ func TestBuildSkipsVersionCheckWhenFetchFails(t *testing.T) {
857857
c.Assert(err, qt.IsNil)
858858

859859
dummyOutput := &config.Output{
860-
Path: tempDir,
860+
Paths: []string{tempDir},
861861
}
862862
dummyAPI := &config.API{
863863
Name: "dummy-api",

internal/simplebuild/output.go

+1-17
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ type DocWriter struct {
2727
// NewWriter initialises any output paths, removing existing files and
2828
// directories if they are present.
2929
func NewWriter(cfg config.Output, appendOutputFiles bool) (*DocWriter, error) {
30-
paths := getOutputPaths(cfg)
30+
paths := cfg.Paths
3131
toClear := paths
3232
if appendOutputFiles {
3333
// We treat the first path as the source of truth and copy the whole
@@ -128,22 +128,6 @@ func (out *DocWriter) Finalize() error {
128128
return nil
129129
}
130130

131-
// Some services have a need to write specs to multiple destinations. This
132-
// tends to happen in Typescript services in which we want to write specs to
133-
// two places:
134-
// - src/** for committing into git and ingesting into Backstage
135-
// - dist/** for runtime module access to compiled specs.
136-
//
137-
// To maintain backwards compatibility we still allow a single path in the
138-
// config file then normalise that here to an array.
139-
func getOutputPaths(cfg config.Output) []string {
140-
paths := cfg.Paths
141-
if len(paths) == 0 && cfg.Path != "" {
142-
paths = []string{cfg.Path}
143-
}
144-
return paths
145-
}
146-
147131
func getExisingSpecFiles(dir string) ([]string, error) {
148132
var outputFiles []string
149133
err := filepath.WalkDir(dir, func(filePath string, d fs.DirEntry, err error) error {

internal/simplebuild/output_test.go

+13-13
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ func TestDocSet_WriteOutputs(t *testing.T) {
3636
name: "write the doc sets to outputs",
3737
args: args{
3838
cfg: config.Output{
39-
Path: t.TempDir(),
39+
Paths: []string{t.TempDir()},
4040
},
4141
},
4242
docs: DocSet{
@@ -47,10 +47,10 @@ func TestDocSet_WriteOutputs(t *testing.T) {
4747
},
4848
assert: func(t *testing.T, args args) {
4949
t.Helper()
50-
files, err := filepath.Glob(filepath.Join(args.cfg.Path, "*"))
50+
files, err := filepath.Glob(filepath.Join(args.cfg.Paths[0], "*"))
5151
c.Assert(err, qt.IsNil)
5252
c.Assert(files, qt.HasLen, 2)
53-
goEmbedContents, err := os.ReadFile(path.Join(args.cfg.Path, "embed.go"))
53+
goEmbedContents, err := os.ReadFile(path.Join(args.cfg.Paths[0], "embed.go"))
5454
c.Assert(err, qt.IsNil)
5555
c.Assert(string(goEmbedContents), qt.Contains, "2024-01-01")
5656
},
@@ -59,7 +59,7 @@ func TestDocSet_WriteOutputs(t *testing.T) {
5959
name: "clears dir if appendOutputFiles is false",
6060
args: args{
6161
cfg: config.Output{
62-
Path: t.TempDir(),
62+
Paths: []string{t.TempDir()},
6363
},
6464
appendOutputFiles: false,
6565
},
@@ -71,15 +71,15 @@ func TestDocSet_WriteOutputs(t *testing.T) {
7171
},
7272
setup: func(t *testing.T, args args) {
7373
t.Helper()
74-
err = os.WriteFile(path.Join(args.cfg.Path, "existing-file"), []byte("existing"), 0644)
74+
err = os.WriteFile(path.Join(args.cfg.Paths[0], "existing-file"), []byte("existing"), 0644)
7575
c.Assert(err, qt.IsNil)
7676
},
7777
assert: func(t *testing.T, args args) {
7878
t.Helper()
79-
files, err := filepath.Glob(filepath.Join(args.cfg.Path, "*"))
79+
files, err := filepath.Glob(filepath.Join(args.cfg.Paths[0], "*"))
8080
c.Assert(err, qt.IsNil)
8181
c.Assert(files, qt.HasLen, 2)
82-
goEmbedContents, err := os.ReadFile(path.Join(args.cfg.Path, "embed.go"))
82+
goEmbedContents, err := os.ReadFile(path.Join(args.cfg.Paths[0], "embed.go"))
8383
c.Assert(err, qt.IsNil)
8484
c.Assert(string(goEmbedContents), qt.Contains, "2024-01-01")
8585
},
@@ -89,7 +89,7 @@ func TestDocSet_WriteOutputs(t *testing.T) {
8989
name: "merges files if appendOutputFiles is true, embeds existing files",
9090
args: args{
9191
cfg: config.Output{
92-
Path: t.TempDir(),
92+
Paths: []string{t.TempDir()},
9393
},
9494
appendOutputFiles: true,
9595
},
@@ -101,19 +101,19 @@ func TestDocSet_WriteOutputs(t *testing.T) {
101101
},
102102
setup: func(t *testing.T, args args) {
103103
t.Helper()
104-
err = os.WriteFile(path.Join(args.cfg.Path, "2024-02-01"), []byte("existing"), 0644)
104+
err = os.WriteFile(path.Join(args.cfg.Paths[0], "2024-02-01"), []byte("existing"), 0644)
105105
c.Assert(err, qt.IsNil)
106-
err = os.WriteFile(path.Join(args.cfg.Path, "2024-02-02"), []byte("existing"), 0644)
106+
err = os.WriteFile(path.Join(args.cfg.Paths[0], "2024-02-02"), []byte("existing"), 0644)
107107
c.Assert(err, qt.IsNil)
108-
err = os.WriteFile(path.Join(args.cfg.Path, "2024-02-03"), []byte("existing"), 0644)
108+
err = os.WriteFile(path.Join(args.cfg.Paths[0], "2024-02-03"), []byte("existing"), 0644)
109109
c.Assert(err, qt.IsNil)
110110
},
111111
assert: func(t *testing.T, args args) {
112112
t.Helper()
113-
files, err := filepath.Glob(filepath.Join(args.cfg.Path, "*"))
113+
files, err := filepath.Glob(filepath.Join(args.cfg.Paths[0], "*"))
114114
c.Assert(err, qt.IsNil)
115115
c.Assert(files, qt.HasLen, 2+3)
116-
goEmbedContents, err := os.ReadFile(path.Join(args.cfg.Path, "embed.go"))
116+
goEmbedContents, err := os.ReadFile(path.Join(args.cfg.Paths[0], "embed.go"))
117117
c.Assert(err, qt.IsNil)
118118
c.Assert(string(goEmbedContents), qt.Contains, "2024-01-01")
119119
c.Assert(string(goEmbedContents), qt.Contains, "2024-02-01")

specs/load.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,14 @@ func GetOutputSpecs(ctx context.Context, api *config.API) ([]*vervet.Document, e
5656
// lazy operations.
5757
func GetOutputSpecsItr(ctx context.Context, api *config.API) iter.Seq2[*vervet.Document, error] {
5858
return func(yield func(*vervet.Document, error) bool) {
59-
path := api.Output.ResolvePaths()
60-
if len(path) == 0 {
59+
outPaths := api.Output.Paths
60+
if len(outPaths) == 0 {
6161
// No output defined for this api
6262
return
6363
}
6464
resource := &config.ResourceSet{
6565
// All output paths should have the same contents
66-
Path: path[0],
66+
Path: outPaths[0],
6767
}
6868
paths, err := files.LocalFSSource{}.Match(resource)
6969
if err != nil {

0 commit comments

Comments
 (0)