Skip to content

Commit dd45617

Browse files
committed
Add YAML model structs for manifest YAML files
Part of #191. These structs will represent the contents of the new "manifest" file, which contains all the information we'll need in the future to cleanly upgrade a template output.
1 parent 1793fff commit dd45617

File tree

11 files changed

+314
-22
lines changed

11 files changed

+314
-22
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ require (
1111
github.com/mattn/go-isatty v0.0.19
1212
github.com/posener/complete/v2 v2.1.0
1313
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63
14+
golang.org/x/mod v0.12.0
1415
gopkg.in/yaml.v3 v3.0.1
1516
)
1617

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 h1:m64FZMko/V45gv0bNmrNYoDEq
9797
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8=
9898
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
9999
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
100+
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
101+
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
100102
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
101103
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
102104
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=

templates/model/manifest/manifest.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
// Copyright 2023 The Authors (see AUTHORS file)
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package manifest
16+
17+
import (
18+
"errors"
19+
"io"
20+
21+
"github.com/abcxyz/abc/templates/model"
22+
"gopkg.in/yaml.v3"
23+
)
24+
25+
// TODO "... do not modify" comment in the encoded YAML
26+
// TODO validate before marshaling just to be safe
27+
28+
// Decode unmarshals the YAML Spec from r. This function exists so we can
29+
// validate the Spec model before providing it to the caller; we don't want the
30+
// caller to forget, and thereby introduce bugs.
31+
//
32+
// If the Spec parses successfully but then fails validation, the spec will be
33+
// returned along with the validation error.
34+
func Decode(r io.Reader) (*Manifest, error) {
35+
out := &Manifest{}
36+
if err := model.DecodeAndValidate(r, out); err != nil {
37+
return nil, err
38+
}
39+
return out, nil
40+
}
41+
42+
func Encode(w io.Writer, m *Manifest) error {
43+
enc := yaml.NewEncoder(w)
44+
enc.Encode()
45+
}
46+
47+
// TODO differences vs design doc:
48+
// - kv pair rather than map for inputs
49+
// - temlpate_dirhash name
50+
// - template_location name
51+
// - file_hashes
52+
53+
// Manifest TODO
54+
type Manifest struct {
55+
Pos model.ConfigPos `yaml:"-"`
56+
57+
APIVersion model.String `yaml:"api_version"`
58+
TemplateLocation model.String `yaml:"template_location"`
59+
TemplateDirhash model.String `yaml:"template_dirhash"`
60+
Inputs []*Input `yaml:"inputs"`
61+
OutputHashes []*FileHash `yaml:"output_hashes"`
62+
}
63+
64+
// UnmarshalYAML implements yaml.Unmarshaler.
65+
func (m *Manifest) UnmarshalYAML(n *yaml.Node) error {
66+
return model.UnmarshalPlain(n, m, &m.Pos)
67+
}
68+
69+
// Validate() implements model.Validator.
70+
func (m *Manifest) Validate() error {
71+
// We don't validate the TemplateDirhash here beyond just verifying that
72+
// it's present; it's validated more later when it's used.
73+
//
74+
// Inputs and OutputHashes can legally be empty.
75+
return errors.Join(
76+
model.IsKnownSchemaVersion(&m.Pos, m.APIVersion, "api_version"),
77+
model.NotZeroModel(&m.Pos, m.TemplateLocation, "template_location"),
78+
model.NotZeroModel(&m.Pos, m.TemplateDirhash, "template_dirhash"),
79+
model.ValidateEach(m.Inputs),
80+
model.ValidateEach(m.OutputHashes),
81+
)
82+
}
83+
84+
// Input TODO
85+
type Input struct {
86+
Pos model.ConfigPos
87+
88+
Name model.String `yaml:"name"`
89+
Value model.String `yaml:"value"`
90+
}
91+
92+
// UnmarshalYAML implements yaml.Unmarshaler.
93+
func (i *Input) UnmarshalYAML(n *yaml.Node) error {
94+
return model.UnmarshalPlain(n, i, &i.Pos)
95+
}
96+
97+
// Validate() implements model.Validator.
98+
func (i *Input) Validate() error {
99+
return errors.Join(
100+
model.NotZeroModel(&i.Pos, i.Name, "name"),
101+
model.NotZeroModel(&i.Pos, i.Value, "value"),
102+
)
103+
}
104+
105+
// FileHash TODO
106+
type FileHash struct {
107+
Pos model.ConfigPos
108+
109+
File model.String `yaml:"file"`
110+
Hash model.String `yaml:"hash"`
111+
}
112+
113+
// UnmarshalYAML implements yaml.Unmarshaler.
114+
func (f *FileHash) UnmarshalYAML(n *yaml.Node) error {
115+
return model.UnmarshalPlain(n, f, &f.Pos)
116+
}
117+
118+
// Validate() implements model.Validator.
119+
func (f *FileHash) Validate() error {
120+
return errors.Join(
121+
model.NotZeroModel(&f.Pos, f.File, "file"),
122+
model.NotZeroModel(&f.Pos, f.Hash, "hash"),
123+
)
124+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
// Copyright 2023 The Authors (see AUTHORS file)
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package manifest
16+
17+
import (
18+
"io"
19+
"strings"
20+
"testing"
21+
22+
"github.com/abcxyz/abc/templates/model"
23+
"github.com/abcxyz/pkg/testutil"
24+
"github.com/google/go-cmp/cmp"
25+
"github.com/google/go-cmp/cmp/cmpopts"
26+
"gopkg.in/yaml.v3"
27+
)
28+
29+
func TestDecode(t *testing.T) {
30+
t.Parallel()
31+
32+
cases := []struct {
33+
name string
34+
in string
35+
want *Manifest
36+
wantUnmarshalErr string
37+
wantValidateErr string
38+
}{
39+
{
40+
name: "simple-success",
41+
in: `
42+
api_version: 'cli.abcxyz.dev/v1alpha1'
43+
template_location: 'github.com/abcxyz/abc.git//t/rest_server'
44+
template_dirhash: 'h1:5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03'
45+
inputs:
46+
- name: 'my_input_1'
47+
value: 'my_value_1'
48+
- name: 'my_input_2'
49+
value: 'my_value_2'
50+
output_hashes:
51+
- file: 'a/b/c.txt'
52+
hash: 'h1:b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c'
53+
- file: 'd/e/f.txt'
54+
hash: 'h1:7d865e959b2466918c9863afca942d0fb89d7c9ac0c99bafc3749504ded97730'
55+
`,
56+
want: &Manifest{
57+
APIVersion: model.String{Val: "cli.abcxyz.dev/v1alpha1"},
58+
TemplateLocation: model.String{Val: "github.com/abcxyz/abc.git//t/rest_server"},
59+
TemplateDirhash: model.String{Val: "h1:5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03"},
60+
Inputs: []*Input{
61+
{
62+
Name: model.String{Val: "my_input_1"},
63+
Value: model.String{Val: "my_value_1"},
64+
},
65+
{
66+
Name: model.String{Val: "my_input_2"},
67+
Value: model.String{Val: "my_value_2"},
68+
},
69+
},
70+
OutputHashes: []*FileHash{
71+
{
72+
File: model.String{Val: "a/b/c.txt"},
73+
Hash: model.String{Val: "h1:b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c"},
74+
},
75+
{
76+
File: model.String{Val: "d/e/f.txt"},
77+
Hash: model.String{Val: "h1:7d865e959b2466918c9863afca942d0fb89d7c9ac0c99bafc3749504ded97730"},
78+
},
79+
},
80+
},
81+
},
82+
}
83+
84+
for _, tc := range cases {
85+
tc := tc
86+
87+
t.Run(tc.name, func(t *testing.T) {
88+
t.Parallel()
89+
90+
got := &Manifest{}
91+
dec := newDecoder(strings.NewReader(tc.in))
92+
err := dec.Decode(got)
93+
94+
if diff := testutil.DiffErrString(err, tc.wantUnmarshalErr); err != nil {
95+
t.Fatal(diff)
96+
}
97+
if err != nil {
98+
return
99+
}
100+
101+
err = got.Validate()
102+
if diff := testutil.DiffErrString(err, tc.wantValidateErr); diff != "" {
103+
t.Fatal(diff)
104+
}
105+
if err != nil {
106+
return
107+
}
108+
109+
opt := cmpopts.IgnoreTypes(&model.ConfigPos{}, model.ConfigPos{}) // don't force test authors to assert the line and column numbers
110+
if diff := cmp.Diff(got, tc.want, opt); diff != "" {
111+
t.Errorf("unmarshaling didn't yield expected struct. Diff (-got +want): %s", diff)
112+
}
113+
})
114+
}
115+
}
116+
117+
// newDecoder returns a yaml Decoder with the desired options.
118+
func newDecoder(r io.Reader) *yaml.Decoder {
119+
dec := yaml.NewDecoder(r)
120+
dec.KnownFields(true) // Fail if any unexpected fields are seen. Often doesn't work: https://github.com/go-yaml/yaml/issues/460
121+
return dec
122+
}

templates/model/pos.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ type ConfigPos struct {
3333
Column int
3434
}
3535

36+
func (c ConfigPos) IsZero() bool {
37+
return c == ConfigPos{}
38+
}
39+
3640
// YAMLPos constructs a position struct based on a YAML parse cursor.
3741
func YAMLPos(n *yaml.Node) *ConfigPos {
3842
return &ConfigPos{
@@ -51,7 +55,7 @@ func YAMLPos(n *yaml.Node) *ConfigPos {
5155
// Creating an error: c.Errorf("something went wrong doing action %s", action)
5256
func (c *ConfigPos) Errorf(fmtStr string, args ...any) error {
5357
err := fmt.Errorf(fmtStr, args...)
54-
if c == nil || *c == (ConfigPos{}) {
58+
if c == nil || c.IsZero() {
5559
return err
5660
}
5761

templates/model/spec/spec.go

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ package spec
1919

2020
import (
2121
"errors"
22-
"fmt"
2322
"io"
2423
"strings"
2524

@@ -35,19 +34,17 @@ import (
3534
// If the Spec parses successfully but then fails validation, the spec will be
3635
// returned along with the validation error.
3736
func Decode(r io.Reader) (*Spec, error) {
38-
dec := newDecoder(r)
39-
var spec Spec
40-
if err := dec.Decode(&spec); err != nil {
41-
return nil, fmt.Errorf("error parsing YAML spec file: %w", err)
37+
// dec := yaml.NewDecoder(r)
38+
// var spec Spec
39+
// if err := dec.Decode(&spec); err != nil {
40+
// return nil, fmt.Errorf("error parsing YAML spec file: %w", err)
41+
// }
42+
// return &spec, spec.Validate()
43+
out := &Spec{}
44+
if err := model.DecodeAndValidate(r, out); err != nil {
45+
return nil, err
4246
}
43-
return &spec, spec.Validate()
44-
}
45-
46-
// newDecoder returns a yaml Decoder with the desired options.
47-
func newDecoder(r io.Reader) *yaml.Decoder {
48-
dec := yaml.NewDecoder(r)
49-
dec.KnownFields(true) // Fail if any unexpected fields are seen. Often doesn't work: https://github.com/go-yaml/yaml/issues/460
50-
return dec
47+
return out, nil
5148
}
5249

5350
// Spec represents a parsed spec.yaml file describing a template.

templates/model/spec/spec_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@
1515
package spec
1616

1717
import (
18+
"io"
1819
"strings"
1920
"testing"
2021

2122
"github.com/abcxyz/abc/templates/model"
2223
"github.com/abcxyz/pkg/testutil"
2324
"github.com/google/go-cmp/cmp"
2425
"github.com/google/go-cmp/cmp/cmpopts"
26+
"gopkg.in/yaml.v3"
2527
)
2628

2729
func TestSpecUnmarshal(t *testing.T) {
@@ -1095,3 +1097,10 @@ params:
10951097
})
10961098
}
10971099
}
1100+
1101+
// newDecoder returns a yaml Decoder with the desired options.
1102+
func newDecoder(r io.Reader) *yaml.Decoder {
1103+
dec := yaml.NewDecoder(r)
1104+
dec.KnownFields(true) // Fail if any unexpected fields are seen. Often doesn't work: https://github.com/go-yaml/yaml/issues/460
1105+
return dec
1106+
}

templates/model/test/test.go

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,15 @@ type Test struct {
5757
}
5858

5959
// UnmarshalYAML implements yaml.Unmarshaler.
60-
func (i *Test) UnmarshalYAML(n *yaml.Node) error {
61-
return model.UnmarshalPlain(n, i, &i.Pos) //nolint:wrapcheck
60+
func (t *Test) UnmarshalYAML(n *yaml.Node) error {
61+
return model.UnmarshalPlain(n, t, &t.Pos) //nolint:wrapcheck
62+
}
63+
64+
func (t *Test) Validate() error {
65+
return errors.Join(
66+
model.IsKnownSchemaVersion(&t.Pos, t.APIVersion, "api_version"),
67+
model.ValidateEach(t.Inputs),
68+
)
6269
}
6370

6471
// DecodeTest unmarshals the YAML Spec from r.
@@ -71,8 +78,8 @@ func DecodeTest(r io.Reader) (*Test, error) {
7178
return nil, fmt.Errorf("error parsing test YAML file: %w", err)
7279
}
7380

74-
return &test, errors.Join(
75-
model.OneOf(&test.Pos, test.APIVersion, []string{"cli.abcxyz.dev/v1alpha1"}, "apiVersion"),
76-
model.ValidateEach(test.Inputs),
77-
)
81+
if err := test.Validate(); err != nil {
82+
return nil, err
83+
}
84+
return &test, nil
7885
}

templates/model/test/test_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ inputs:
7676
in: `inputs:
7777
- name: 'person_name'
7878
value: 'iron_man'`,
79-
wantErr: `at line 1 column 1: field "apiVersion" value must be one of [cli.abcxyz.dev/v1alpha1]`,
79+
wantErr: `at line 1 column 1: field "api_version" value must be one of [cli.abcxyz.dev/v1alpha1]`,
8080
},
8181
}
8282

0 commit comments

Comments
 (0)