Skip to content

Commit

Permalink
Preserve extension fields when marshalling/unmarshalling
Browse files Browse the repository at this point in the history
This makes it so external callers don't need to handle this themselves.

Signed-off-by: Brian Goff <cpuguy83@gmail.com>
  • Loading branch information
cpuguy83 committed Jan 31, 2025
1 parent e6a09b2 commit 82a5293
Show file tree
Hide file tree
Showing 3 changed files with 96 additions and 14 deletions.
72 changes: 58 additions & 14 deletions load.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,36 +210,80 @@ func (s *Spec) SubstituteArgs(env map[string]string, opts ...SubstituteOpt) erro
func LoadSpec(dt []byte) (*Spec, error) {
var spec Spec

dt, err := stripXFields(dt)
if err != nil {
return nil, fmt.Errorf("error stripping x-fields: %w", err)
}

if err := yaml.UnmarshalWithOptions(dt, &spec, yaml.Strict()); err != nil {
return nil, fmt.Errorf("error unmarshalling spec: %w", err)
return nil, errors.Wrap(err, "error unmarshalling spec")
}

if err := spec.Validate(); err != nil {
return nil, err
}
spec.FillDefaults()

spec.FillDefaults()
return &spec, nil
}

func stripXFields(dt []byte) ([]byte, error) {
func (s *Spec) UnmarshalYAML(f func(interface{}) error) error {
var obj map[string]interface{}
if err := yaml.Unmarshal(dt, &obj); err != nil {
return nil, fmt.Errorf("error unmarshalling spec: %w", err)

if err := f(&obj); err != nil {
return err
}

var ext map[string]interface{}

addExt := func(k string, v interface{}) {
if ext == nil {
ext = make(map[string]interface{})
}
ext[k] = v
}
for k, v := range obj {
if !strings.HasPrefix(k, "x-") && !strings.HasPrefix(k, "X-") {
continue
}
addExt(strings.TrimPrefix(k, "x-"), v)
delete(obj, k)
}

type internalSpec Spec
var s2 internalSpec

dt, err := yaml.Marshal(obj)
if err != nil {
return err
}

if err := yaml.UnmarshalWithOptions(dt, &s2, yaml.Strict()); err != nil {
return err
}

*s = Spec(s2)
s.Ext = ext
return nil
}

func (s Spec) MarshalYAML() ([]byte, error) {
// We need to define a new type to avoid infinite recursion of MarshalYAML.
type internalSpec Spec

type specExt struct {
internalSpec `yaml:",inline"`
Ext map[string]interface{} `yaml:",omitempty,inline"`
}

for k := range obj {
if strings.HasPrefix(k, "x-") || strings.HasPrefix(k, "X-") {
delete(obj, k)
var ext map[string]interface{}
if len(s.Ext) > 0 {
ext = maps.Clone(s.Ext)
for k, v := range ext {
delete(ext, k)
ext["x-"+k] = v
}
}

return yaml.Marshal(obj)
return yaml.Marshal(specExt{
internalSpec: internalSpec(s),
Ext: ext,
})
}

func (s *BuildStep) processBuildArgs(lex *shell.Lex, args map[string]string, allowArg func(string) bool) error {
Expand Down
36 changes: 36 additions & 0 deletions load_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"reflect"
"testing"

"github.com/goccy/go-yaml"
"github.com/moby/buildkit/frontend/dockerui"
"gotest.tools/v3/assert"
"gotest.tools/v3/assert/cmp"
Expand Down Expand Up @@ -1160,3 +1161,38 @@ func TestImage_validate(t *testing.T) {
})
}
}

func TestExtensionFieldMarshalUnmarshal(t *testing.T) {
dt := []byte(`
name: test
x-hello: world
x-foo:
- bar
- baz
`)

var spec Spec
err := yaml.Unmarshal(dt, &spec)
assert.NilError(t, err)

assert.Check(t, cmp.Equal(spec.Name, "test"), spec)
assert.Assert(t, spec.Ext != nil)
assert.Check(t, spec.Ext["hello"] != nil, "%+v", spec)
assert.Check(t, cmp.Equal(spec.Ext["hello"].(string), "world"), "%+v", spec.Ext)
assert.Check(t, cmp.DeepEqual(spec.Ext["foo"].([]interface{}), []interface{}{"bar", "baz"}), "%+v", spec.Ext)

// marshal and unmarshal to ensure the extension fields are preserved

dt, err = yaml.Marshal(spec)
assert.NilError(t, err)

var spec2 Spec
err = yaml.Unmarshal(dt, &spec2)
assert.NilError(t, err)

assert.Check(t, cmp.Equal(spec2.Name, "test"), spec2)
assert.Assert(t, spec2.Ext != nil, string(dt))
assert.Check(t, spec2.Ext["hello"] != nil, "%+v", spec2.Ext)
assert.Check(t, cmp.Equal(spec2.Ext["hello"].(string), "world"), "%+v", spec2.Ext)
assert.Check(t, cmp.DeepEqual(spec2.Ext["foo"].([]interface{}), []interface{}{"bar", "baz"}), "%+v", spec2.Ext)
}
2 changes: 2 additions & 0 deletions spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ type Spec struct {
// Each item in this list is run with a separate rootfs and cannot interact with other tests.
// Each [TestSpec] is run with a separate rootfs, asynchronously from other [TestSpec].
Tests []*TestSpec `yaml:"tests,omitempty" json:"tests,omitempty"`

Ext map[string]interface{} `yaml:"-" json:"-"`
}

// PatchSpec is used to apply a patch to a source with a given set of options.
Expand Down

0 comments on commit 82a5293

Please sign in to comment.