diff --git a/step_command.go b/step_command.go index b776153..b49ed4c 100644 --- a/step_command.go +++ b/step_command.go @@ -36,6 +36,7 @@ type CommandStep struct { Env map[string]string `yaml:"env,omitempty"` Signature *Signature `yaml:"signature,omitempty"` Matrix *Matrix `yaml:"matrix,omitempty"` + Cache *Cache `yaml:"cache,omitempty"` // RemainingFields stores any other top-level mapping items so they at least // survive an unmarshal-marshal round-trip. diff --git a/step_command_cache.go b/step_command_cache.go new file mode 100644 index 0000000..3dc1111 --- /dev/null +++ b/step_command_cache.go @@ -0,0 +1,50 @@ +package pipeline + +import ( + "fmt" + + "github.com/buildkite/go-pipeline/ordered" +) + +var _ ordered.Unmarshaler = (*Cache)(nil) + +var ( + errUnsupportedCacheType = fmt.Errorf("unsupported type for cache") +) + +// Cache models the cache settings for a given step +type Cache struct { + Paths []string `json:"paths" yaml:"paths"` + + RemainingFields map[string]any `yaml:",inline"` +} + +// UnmarshalOrdered unmarshals from the following types: +// - string: a single path +// - []string: multiple paths +// - ordered.Map: a map containing paths, among potentially other things +func (c *Cache) UnmarshalOrdered(o any) error { + switch v := o.(type) { + case string: + c.Paths = []string{v} + + case []any: + s := make([]string, 0, len(v)) + if err := ordered.Unmarshal(v, &s); err != nil { + return err + } + + c.Paths = s + + case *ordered.MapSA: + type wrappedCache Cache + if err := ordered.Unmarshal(o, (*wrappedCache)(c)); err != nil { + return err + } + + default: + return fmt.Errorf("%w: %T", errUnsupportedCacheType, v) + } + + return nil +} diff --git a/step_command_cache_test.go b/step_command_cache_test.go new file mode 100644 index 0000000..9fe4222 --- /dev/null +++ b/step_command_cache_test.go @@ -0,0 +1,91 @@ +package pipeline + +import ( + "errors" + "testing" + + "github.com/buildkite/go-pipeline/ordered" + "github.com/google/go-cmp/cmp" +) + +func TestCacheUnmarshalOrdered(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + input any + want Cache + wantErr error + }{ + { + name: "single path", + input: "path/to/cache", + want: Cache{Paths: []string{"path/to/cache"}}, + }, + { + name: "array of paths", + input: []any{"path/to/cache", "another/path"}, + want: Cache{Paths: []string{"path/to/cache", "another/path"}}, + }, + { + name: "full cache settings block", + input: ordered.MapFromItems( + ordered.TupleSA{Key: "paths", Value: []any{"path/to/cache", "another/path"}}, + ), + want: Cache{Paths: []string{"path/to/cache", "another/path"}}, + }, + { + name: "full cache settings block with extra fields", + input: ordered.MapFromItems( + ordered.TupleSA{Key: "paths", Value: []any{"path/to/cache", "another/path"}}, + ordered.TupleSA{Key: "extra", Value: "field"}, + ), + want: Cache{ + Paths: []string{"path/to/cache", "another/path"}, + RemainingFields: map[string]any{"extra": "field"}, + }, + }, + { + name: "multi-type list of scalar paths get normalised to strings", + input: []any{"path/to/cache", 42, true}, // 42 and true are valid directory paths, so we should keep them as strings + want: Cache{Paths: []string{"path/to/cache", "42", "true"}}, + }, + { + name: "non-scalar elements in an array", + input: []any{"path/to/cache", []int{1, 2, 3}, map[string]any{"hi": "there"}}, + wantErr: ordered.ErrUnsupportedSrc, + }, + { + name: "invalid typed scalar", + input: 42, + wantErr: errUnsupportedCacheType, + }, + { + name: "invalid map", + input: ordered.MapFromItems( + ordered.TupleSA{ + Key: "paths", + Value: ordered.MapFromItems( // nested map, not allowed + ordered.TupleSA{Key: "path", Value: "path/to/cache"}, + ), + }, + ), + wantErr: ordered.ErrIncompatibleTypes, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + var c Cache + if err := c.UnmarshalOrdered(tc.input); !errors.Is(err, tc.wantErr) { + t.Fatalf("Cache.UnmarshalOrdered(%v) = %v, want: %v", tc.input, err, tc.wantErr) + } + + if diff := cmp.Diff(c, tc.want); diff != "" { + t.Errorf("Cache diff after UnmarshalOrdered (-got +want):\n%s", diff) + } + }) + } +}