diff --git a/go.mod b/go.mod index a732b9c1..c94adbb9 100644 --- a/go.mod +++ b/go.mod @@ -31,6 +31,8 @@ require ( gopkg.in/ini.v1 v1.67.0 // Apache 2.0 ) +require gopkg.in/yaml.v3 v3.0.1 + require ( cloud.google.com/go/compute v1.23.0 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect @@ -60,5 +62,4 @@ require ( google.golang.org/grpc v1.58.2 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/libs/config/location.go b/libs/config/location.go new file mode 100644 index 00000000..534b21c2 --- /dev/null +++ b/libs/config/location.go @@ -0,0 +1,13 @@ +package config + +import "fmt" + +type Location struct { + File string + Line int + Column int +} + +func (l Location) String() string { + return fmt.Sprintf("%s:%d:%d", l.File, l.Line, l.Column) +} diff --git a/libs/config/location_test.go b/libs/config/location_test.go new file mode 100644 index 00000000..31013193 --- /dev/null +++ b/libs/config/location_test.go @@ -0,0 +1,13 @@ +package config_test + +import ( + "testing" + + "github.com/databricks/cli/libs/config" + "github.com/stretchr/testify/assert" +) + +func TestLocation(t *testing.T) { + loc := config.Location{File: "file", Line: 1, Column: 2} + assert.Equal(t, "file:1:2", loc.String()) +} diff --git a/libs/config/value.go b/libs/config/value.go new file mode 100644 index 00000000..994aec38 --- /dev/null +++ b/libs/config/value.go @@ -0,0 +1,110 @@ +package config + +import "time" + +type Value struct { + v any + l Location + + // Whether or not this value is an anchor. + // If this node doesn't map to a type, we don't need to warn about it. + anchor bool +} + +// NilValue is equal to the zero-value of Value. +var NilValue = Value{} + +// NewValue constructs a new Value with the given value and location. +func NewValue(v any, loc Location) Value { + return Value{ + v: v, + l: loc, + } +} + +func (v Value) AsMap() (map[string]Value, bool) { + m, ok := v.v.(map[string]Value) + return m, ok +} + +func (v Value) Location() Location { + return v.l +} + +func (v Value) AsAny() any { + switch vv := v.v.(type) { + case map[string]Value: + m := make(map[string]any) + for k, v := range vv { + m[k] = v.AsAny() + } + return m + case []Value: + a := make([]any, len(vv)) + for i, v := range vv { + a[i] = v.AsAny() + } + return a + case string: + return vv + case bool: + return vv + case int: + return vv + case int32: + return vv + case int64: + return vv + case float32: + return vv + case float64: + return vv + case time.Time: + return vv + case nil: + return nil + default: + // Panic because we only want to deal with known types. + panic("not handled") + } +} + +func (v Value) Get(key string) Value { + m, ok := v.AsMap() + if !ok { + return NilValue + } + + vv, ok := m[key] + if !ok { + return NilValue + } + + return vv +} + +func (v Value) Index(i int) Value { + s, ok := v.v.([]Value) + if !ok { + return NilValue + } + + if i < 0 || i >= len(s) { + return NilValue + } + + return s[i] +} + +func (v Value) MarkAnchor() Value { + return Value{ + v: v.v, + l: v.l, + + anchor: true, + } +} + +func (v Value) IsAnchor() bool { + return v.anchor +} diff --git a/libs/config/value_test.go b/libs/config/value_test.go new file mode 100644 index 00000000..cb8ef16a --- /dev/null +++ b/libs/config/value_test.go @@ -0,0 +1,37 @@ +package config_test + +import ( + "testing" + + "github.com/databricks/cli/libs/config" + "github.com/stretchr/testify/assert" +) + +func TestValueIsAnchor(t *testing.T) { + var zero config.Value + assert.False(t, zero.IsAnchor()) + mark := zero.MarkAnchor() + assert.True(t, mark.IsAnchor()) +} + +func TestValueAsMap(t *testing.T) { + var zeroValue config.Value + m, ok := zeroValue.AsMap() + assert.False(t, ok) + assert.Nil(t, m) + + var intValue = config.NewValue(1, config.Location{}) + m, ok = intValue.AsMap() + assert.False(t, ok) + assert.Nil(t, m) + + var mapValue = config.NewValue( + map[string]config.Value{ + "key": config.NewValue("value", config.Location{File: "file", Line: 1, Column: 2}), + }, + config.Location{File: "file", Line: 1, Column: 2}, + ) + m, ok = mapValue.AsMap() + assert.True(t, ok) + assert.Len(t, m, 1) +} diff --git a/libs/config/yamlloader/loader.go b/libs/config/yamlloader/loader.go new file mode 100644 index 00000000..6472c137 --- /dev/null +++ b/libs/config/yamlloader/loader.go @@ -0,0 +1,227 @@ +package yamlloader + +import ( + "fmt" + "math" + "strconv" + "strings" + "time" + + "github.com/databricks/cli/libs/config" + "gopkg.in/yaml.v3" +) + +type loader struct { + path string +} + +func errorf(loc config.Location, format string, args ...interface{}) error { + return fmt.Errorf("yaml (%s): %s", loc, fmt.Sprintf(format, args...)) +} + +func newLoader(path string) *loader { + return &loader{ + path: path, + } +} + +func (d *loader) location(node *yaml.Node) config.Location { + return config.Location{ + File: d.path, + Line: node.Line, + Column: node.Column, + } +} + +func (d *loader) load(node *yaml.Node) (config.Value, error) { + loc := config.Location{ + File: d.path, + Line: node.Line, + Column: node.Column, + } + + var value config.Value + var err error + + switch node.Kind { + case yaml.DocumentNode: + value, err = d.loadDocument(node, loc) + case yaml.SequenceNode: + value, err = d.loadSequence(node, loc) + case yaml.MappingNode: + value, err = d.loadMapping(node, loc) + case yaml.ScalarNode: + value, err = d.loadScalar(node, loc) + case yaml.AliasNode: + value, err = d.loadAlias(node, loc) + default: + return config.NilValue, errorf(loc, "unknown node kind: %v", node.Kind) + } + + if err != nil { + return value, err + } + + // Mark value as anchor if needed. + // If this node doesn't map to a type, we don't need to warn about it. + if node.Anchor != "" { + value = value.MarkAnchor() + } + + return value, nil +} + +func (d *loader) loadDocument(node *yaml.Node, loc config.Location) (config.Value, error) { + return d.load(node.Content[0]) +} + +func (d *loader) loadSequence(node *yaml.Node, loc config.Location) (config.Value, error) { + acc := make([]config.Value, len(node.Content)) + for i, n := range node.Content { + v, err := d.load(n) + if err != nil { + return config.NilValue, err + } + + acc[i] = v + } + + return config.NewValue(acc, loc), nil +} + +func (d *loader) loadMapping(node *yaml.Node, loc config.Location) (config.Value, error) { + var merge *yaml.Node + + acc := make(map[string]config.Value) + for i := 0; i < len(node.Content); i += 2 { + key := node.Content[i] + val := node.Content[i+1] + + // Assert that keys are strings + if key.Kind != yaml.ScalarNode { + return config.NilValue, errorf(loc, "key is not a scalar") + } + + st := key.ShortTag() + switch st { + case "!!str": + // OK + case "!!merge": + if merge != nil { + panic("merge node already set") + } + merge = val + continue + default: + return config.NilValue, errorf(loc, "invalid key tag: %v", st) + } + + v, err := d.load(val) + if err != nil { + return config.NilValue, err + } + + acc[key.Value] = v + } + + if merge == nil { + return config.NewValue(acc, loc), nil + } + + // Build location for the merge node. + var mloc = d.location(merge) + var merr = errorf(mloc, "map merge requires map or sequence of maps as the value") + + // Flatten the merge node into a slice of nodes. + // It can be either a single node or a sequence of nodes. + var mnodes []*yaml.Node + switch merge.Kind { + case yaml.SequenceNode: + mnodes = merge.Content + case yaml.AliasNode: + mnodes = []*yaml.Node{merge} + default: + return config.NilValue, merr + } + + // Build a sequence of values to merge. + // The entries that we already accumulated have precedence. + var seq []map[string]config.Value + for _, n := range mnodes { + v, err := d.load(n) + if err != nil { + return config.NilValue, err + } + m, ok := v.AsMap() + if !ok { + return config.NilValue, merr + } + seq = append(seq, m) + } + + // Append the accumulated entries to the sequence. + seq = append(seq, acc) + out := make(map[string]config.Value) + for _, m := range seq { + for k, v := range m { + out[k] = v + } + } + + return config.NewValue(out, loc), nil +} + +func (d *loader) loadScalar(node *yaml.Node, loc config.Location) (config.Value, error) { + st := node.ShortTag() + switch st { + case "!!str": + return config.NewValue(node.Value, loc), nil + case "!!bool": + switch strings.ToLower(node.Value) { + case "true": + return config.NewValue(true, loc), nil + case "false": + return config.NewValue(false, loc), nil + default: + return config.NilValue, errorf(loc, "invalid bool value: %v", node.Value) + } + case "!!int": + i64, err := strconv.ParseInt(node.Value, 10, 64) + if err != nil { + return config.NilValue, errorf(loc, "invalid int value: %v", node.Value) + } + // Use regular int type instead of int64 if possible. + if i64 >= math.MinInt32 && i64 <= math.MaxInt32 { + return config.NewValue(int(i64), loc), nil + } + return config.NewValue(i64, loc), nil + case "!!float": + f64, err := strconv.ParseFloat(node.Value, 64) + if err != nil { + return config.NilValue, errorf(loc, "invalid float value: %v", node.Value) + } + return config.NewValue(f64, loc), nil + case "!!null": + return config.NewValue(nil, loc), nil + case "!!timestamp": + // Try a couple of layouts + for _, layout := range []string{ + "2006-1-2T15:4:5.999999999Z07:00", // RCF3339Nano with short date fields. + "2006-1-2t15:4:5.999999999Z07:00", // RFC3339Nano with short date fields and lower-case "t". + "2006-1-2 15:4:5.999999999", // space separated with no time zone + "2006-1-2", // date only + } { + t, terr := time.Parse(layout, node.Value) + if terr == nil { + return config.NewValue(t, loc), nil + } + } + return config.NilValue, errorf(loc, "invalid timestamp value: %v", node.Value) + default: + return config.NilValue, errorf(loc, "unknown tag: %v", st) + } +} + +func (d *loader) loadAlias(node *yaml.Node, loc config.Location) (config.Value, error) { + return d.load(node.Alias) +} diff --git a/libs/config/yamlloader/testdata/anchor_01.yml b/libs/config/yamlloader/testdata/anchor_01.yml new file mode 100644 index 00000000..b677d60b --- /dev/null +++ b/libs/config/yamlloader/testdata/anchor_01.yml @@ -0,0 +1,12 @@ +# 1. Basic Anchor and Alias +defaults: &DEFAULTS + color: red + size: large + +shirt1: + <<: *DEFAULTS + pattern: striped + +shirt2: + <<: *DEFAULTS + pattern: plain diff --git a/libs/config/yamlloader/testdata/anchor_02.yml b/libs/config/yamlloader/testdata/anchor_02.yml new file mode 100644 index 00000000..86650987 --- /dev/null +++ b/libs/config/yamlloader/testdata/anchor_02.yml @@ -0,0 +1,13 @@ +# 2. Merging Anchors +# Here, multiple anchors can be merged into a single item. +defaults: &DEFAULTS + color: red + size: large + +materials: &MATERIALS + primary: cotton + secondary: polyester + +shirt: + <<: [*DEFAULTS, *MATERIALS] + pattern: striped diff --git a/libs/config/yamlloader/testdata/anchor_03.yml b/libs/config/yamlloader/testdata/anchor_03.yml new file mode 100644 index 00000000..f06b7999 --- /dev/null +++ b/libs/config/yamlloader/testdata/anchor_03.yml @@ -0,0 +1,10 @@ +# 3. Overriding Merged Anchor Values +# You can override values when merging. +defaults: &DEFAULTS + color: red + size: large + pattern: plain + +shirt: + <<: *DEFAULTS + color: blue diff --git a/libs/config/yamlloader/testdata/anchor_04.yml b/libs/config/yamlloader/testdata/anchor_04.yml new file mode 100644 index 00000000..bbc4bc02 --- /dev/null +++ b/libs/config/yamlloader/testdata/anchor_04.yml @@ -0,0 +1,16 @@ +# 4. Nested Anchors +# This demonstrates the reuse of nested content. +address: &ADDRESS + city: San Francisco + state: CA + country: USA + +person1: + name: Alice + address: *ADDRESS + +person2: + name: Bob + address: + <<: *ADDRESS + city: Los Angeles diff --git a/libs/config/yamlloader/testdata/anchor_05.yml b/libs/config/yamlloader/testdata/anchor_05.yml new file mode 100644 index 00000000..9a4c961b --- /dev/null +++ b/libs/config/yamlloader/testdata/anchor_05.yml @@ -0,0 +1,15 @@ +# 5. Using Anchors for List Items +# You can use anchors for list items too. +features: &FEATURES + - wifi + - bluetooth + +phone1: + name: PhoneA + features: *FEATURES + +phone2: + name: PhoneB + features: + - camera + - *FEATURES diff --git a/libs/config/yamlloader/testdata/anchor_06.yml b/libs/config/yamlloader/testdata/anchor_06.yml new file mode 100644 index 00000000..f27650fa --- /dev/null +++ b/libs/config/yamlloader/testdata/anchor_06.yml @@ -0,0 +1,5 @@ +# 6. String Anchors +commonMessage: &msg "Hello, World!" + +greeting1: *msg +greeting2: *msg diff --git a/libs/config/yamlloader/testdata/anchor_07.yml b/libs/config/yamlloader/testdata/anchor_07.yml new file mode 100644 index 00000000..b8f32214 --- /dev/null +++ b/libs/config/yamlloader/testdata/anchor_07.yml @@ -0,0 +1,10 @@ +# 7. Number Anchors +defaultAge: &age 25 + +person1: + name: Alice + age: *age + +person2: + name: Bob + age: *age diff --git a/libs/config/yamlloader/testdata/anchor_08.yml b/libs/config/yamlloader/testdata/anchor_08.yml new file mode 100644 index 00000000..8b01e21f --- /dev/null +++ b/libs/config/yamlloader/testdata/anchor_08.yml @@ -0,0 +1,10 @@ +# 8. Boolean Anchors +isActive: &active true + +user1: + username: user1 + active: *active + +user2: + username: user2 + active: *active diff --git a/libs/config/yamlloader/testdata/error_01.yml b/libs/config/yamlloader/testdata/error_01.yml new file mode 100644 index 00000000..11b96486 --- /dev/null +++ b/libs/config/yamlloader/testdata/error_01.yml @@ -0,0 +1,6 @@ +# Use string anchor to extend a mapping. +str: &str "Hello world!" + +map: + <<: *str + key: value diff --git a/libs/config/yamlloader/testdata/error_02.yml b/libs/config/yamlloader/testdata/error_02.yml new file mode 100644 index 00000000..5e9c6276 --- /dev/null +++ b/libs/config/yamlloader/testdata/error_02.yml @@ -0,0 +1,6 @@ +# Use string anchor inside sequence to extend a mapping. +str: &str "Hello world!" + +map: + <<: [*str] + key: value diff --git a/libs/config/yamlloader/testdata/error_03.yml b/libs/config/yamlloader/testdata/error_03.yml new file mode 100644 index 00000000..c4d3c397 --- /dev/null +++ b/libs/config/yamlloader/testdata/error_03.yml @@ -0,0 +1,4 @@ +# Extend a mapping with a literal string. +map: + <<: value + key: value diff --git a/libs/config/yamlloader/testdata/mix_01.yml b/libs/config/yamlloader/testdata/mix_01.yml new file mode 100644 index 00000000..98706bf6 --- /dev/null +++ b/libs/config/yamlloader/testdata/mix_01.yml @@ -0,0 +1,90 @@ +# Scalars + +# Strings can be unquoted +name: John Doe + +# Strings can be single quoted +single_quoted_string: 'This is a single quoted string' + +# Strings can be double quoted (allows for escape sequences) +double_quoted_string: "This is a double quoted string with an escaped newline \n and tab \t." + +# Multiline string with folded style (newlines become spaces) +folded_style: > + This is a very + long string that + spans several lines. + +# Multiline string with literal block style (newlines are preserved) +literal_style: | + Line 1 + Line 2 + Line 3 + +# Integers +age: 30 + +# Floating point +price: 19.99 + +# Boolean values +is_student: true +is_employed: false + +# Null value +middle_name: ~ # null can also be represented with 'null' + +# Timestamp +timestamp: 2023-01-01T12:00:00Z + +# Sequences (lists) + +# Inline list +fruits: [apple, banana, cherry] + +# Block style list +books: + - 'Moby Dick' + - '1984' + - 'Pride and Prejudice' + +# Mappings (dictionaries) + +# Inline dictionary +address: {street: '123 Main St', city: 'Anytown', zip: '12345'} + +# Block style dictionary +employee: + first_name: Jane + last_name: Smith + age: 40 + +# Edge cases and advanced features + +# Strings that look like other types must be quoted +looks_like_number: '12345' +looks_like_boolean: 'yes' +looks_like_null: 'null' +looks_like_timestamp: '2023-01-01T12:00:00Z' + +# Using anchors and aliases to reuse properties +base_address: &base + street: '456 Elm St' + city: 'Sometown' + +office_address: + <<: *base # Merge the base address into this mapping + suite: 500 + +# Nested structures +users: + - name: Alice + age: 28 + interests: + - reading + - cycling + - name: Bob + age: 35 + interests: + - cooking + - hiking diff --git a/libs/config/yamlloader/testdata/mix_02.yml b/libs/config/yamlloader/testdata/mix_02.yml new file mode 100644 index 00000000..757d626d --- /dev/null +++ b/libs/config/yamlloader/testdata/mix_02.yml @@ -0,0 +1,49 @@ +# Scalars with special characters + +# Commas and square brackets in strings should be enclosed in quotes +special_chars: "[This, string] has, special chars." + +# Strings starting with reserved indicators must be quoted +reserved_string: "@not_a_directive" +colon_string: "this: looks like a mapping, but it's not" + +# Explicit data type declaration +explicit_string: !!str 12345 + +# Sequences with nested mappings and lists + +teams: + - + name: Team A + members: + - Alice + - Bob + tasks: + - task1: + status: incomplete + due_date: 2023-01-15 + - task2: + status: complete + +# Complex mapping keys +? | + Multi-line key + which is unusual but valid +: multi-line key's value + +"complex key: with colon": complex key's value + +# Set (a mapping with null values) +set_example: + item1: ~ + item2: ~ + +# Merge multiple mappings (with override) +base_colors: &colors + red: "#FF0000" + blue: "#0000FF" + green: "#00FF00" + +theme: + <<: *colors + blue: "#001122" # Overriding the blue color from base_colors diff --git a/libs/config/yamlloader/yaml.go b/libs/config/yamlloader/yaml.go new file mode 100644 index 00000000..0f0f4e60 --- /dev/null +++ b/libs/config/yamlloader/yaml.go @@ -0,0 +1,19 @@ +package yamlloader + +import ( + "io" + + "github.com/databricks/cli/libs/config" + "gopkg.in/yaml.v3" +) + +func LoadYAML(path string, r io.Reader) (config.Value, error) { + var node yaml.Node + dec := yaml.NewDecoder(r) + err := dec.Decode(&node) + if err != nil { + return config.NilValue, err + } + + return newLoader(path).load(&node) +} diff --git a/libs/config/yamlloader/yaml_anchor_test.go b/libs/config/yamlloader/yaml_anchor_test.go new file mode 100644 index 00000000..a8b66686 --- /dev/null +++ b/libs/config/yamlloader/yaml_anchor_test.go @@ -0,0 +1,117 @@ +package yamlloader_test + +import ( + "testing" + + "github.com/databricks/cli/libs/config" + "github.com/stretchr/testify/assert" +) + +func TestYAMLAnchor01(t *testing.T) { + file := "testdata/anchor_01.yml" + self := loadYAML(t, file) + assert.NotEqual(t, config.NilValue, self) + + assert.True(t, self.Get("defaults").IsAnchor()) + assert.False(t, self.Get("shirt1").IsAnchor()) + assert.False(t, self.Get("shirt2").IsAnchor()) + + pattern := self.Get("shirt1").Get("pattern") + assert.Equal(t, "striped", pattern.AsAny()) + assert.Equal(t, config.Location{File: file, Line: 8, Column: 12}, pattern.Location()) +} + +func TestYAMLAnchor02(t *testing.T) { + file := "testdata/anchor_02.yml" + self := loadYAML(t, file) + assert.NotEqual(t, config.NilValue, self) + + color := self.Get("shirt").Get("color") + assert.Equal(t, "red", color.AsAny()) + assert.Equal(t, config.Location{File: file, Line: 4, Column: 10}, color.Location()) + + primary := self.Get("shirt").Get("primary") + assert.Equal(t, "cotton", primary.AsAny()) + assert.Equal(t, config.Location{File: file, Line: 8, Column: 12}, primary.Location()) + + pattern := self.Get("shirt").Get("pattern") + assert.Equal(t, "striped", pattern.AsAny()) + assert.Equal(t, config.Location{File: file, Line: 13, Column: 12}, pattern.Location()) +} + +func TestYAMLAnchor03(t *testing.T) { + file := "testdata/anchor_03.yml" + self := loadYAML(t, file) + assert.NotEqual(t, config.NilValue, self) + + // Assert the override took place. + blue := self.Get("shirt").Get("color") + assert.Equal(t, "blue", blue.AsAny()) + assert.Equal(t, file, blue.Location().File) + assert.Equal(t, 10, blue.Location().Line) + assert.Equal(t, 10, blue.Location().Column) +} + +func TestYAMLAnchor04(t *testing.T) { + file := "testdata/anchor_04.yml" + self := loadYAML(t, file) + assert.NotEqual(t, config.NilValue, self) + + p1 := self.Get("person1").Get("address").Get("city") + assert.Equal(t, "San Francisco", p1.AsAny()) + assert.Equal(t, config.Location{File: file, Line: 4, Column: 9}, p1.Location()) + + p2 := self.Get("person2").Get("address").Get("city") + assert.Equal(t, "Los Angeles", p2.AsAny()) + assert.Equal(t, config.Location{File: file, Line: 16, Column: 11}, p2.Location()) +} + +func TestYAMLAnchor05(t *testing.T) { + file := "testdata/anchor_05.yml" + self := loadYAML(t, file) + assert.NotEqual(t, config.NilValue, self) + + features := self.Get("phone1").Get("features") + assert.Equal(t, "wifi", features.Index(0).AsAny()) + assert.Equal(t, config.Location{File: file, Line: 4, Column: 5}, features.Index(0).Location()) + assert.Equal(t, "bluetooth", features.Index(1).AsAny()) + assert.Equal(t, config.Location{File: file, Line: 5, Column: 5}, features.Index(1).Location()) +} + +func TestYAMLAnchor06(t *testing.T) { + file := "testdata/anchor_06.yml" + self := loadYAML(t, file) + assert.NotEqual(t, config.NilValue, self) + + greeting := self.Get("greeting1") + assert.Equal(t, "Hello, World!", greeting.AsAny()) + assert.Equal(t, config.Location{File: file, Line: 2, Column: 16}, greeting.Location()) +} + +func TestYAMLAnchor07(t *testing.T) { + file := "testdata/anchor_07.yml" + self := loadYAML(t, file) + assert.NotEqual(t, config.NilValue, self) + + name := self.Get("person1").Get("name") + assert.Equal(t, "Alice", name.AsAny()) + assert.Equal(t, config.Location{File: file, Line: 5, Column: 9}, name.Location()) + + age := self.Get("person1").Get("age") + assert.Equal(t, 25, age.AsAny()) + assert.Equal(t, config.Location{File: file, Line: 2, Column: 13}, age.Location()) +} + +func TestYAMLAnchor08(t *testing.T) { + file := "testdata/anchor_08.yml" + self := loadYAML(t, file) + assert.NotEqual(t, config.NilValue, self) + + username := self.Get("user1").Get("username") + assert.Equal(t, "user1", username.AsAny()) + assert.Equal(t, config.Location{File: file, Line: 5, Column: 13}, username.Location()) + + active := self.Get("user1").Get("active") + assert.Equal(t, true, active.AsAny()) + assert.Equal(t, config.Location{File: file, Line: 2, Column: 11}, active.Location()) +} diff --git a/libs/config/yamlloader/yaml_error_test.go b/libs/config/yamlloader/yaml_error_test.go new file mode 100644 index 00000000..2685042f --- /dev/null +++ b/libs/config/yamlloader/yaml_error_test.go @@ -0,0 +1,36 @@ +package yamlloader_test + +import ( + "bytes" + "os" + "testing" + + "github.com/databricks/cli/libs/config/yamlloader" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" +) + +func TestYAMLErrorMapMerge(t *testing.T) { + for _, file := range []string{ + "testdata/error_01.yml", + "testdata/error_02.yml", + "testdata/error_03.yml", + } { + input, err := os.ReadFile(file) + require.NoError(t, err) + + t.Run(file, func(t *testing.T) { + t.Run("reference", func(t *testing.T) { + var ref any + err = yaml.Unmarshal(input, &ref) + assert.ErrorContains(t, err, "map merge requires map or sequence of maps as the value") + }) + + t.Run("self", func(t *testing.T) { + _, err := yamlloader.LoadYAML(file, bytes.NewBuffer(input)) + assert.ErrorContains(t, err, "map merge requires map or sequence of maps as the value") + }) + }) + } +} diff --git a/libs/config/yamlloader/yaml_mix_test.go b/libs/config/yamlloader/yaml_mix_test.go new file mode 100644 index 00000000..9cd0753d --- /dev/null +++ b/libs/config/yamlloader/yaml_mix_test.go @@ -0,0 +1,26 @@ +package yamlloader_test + +import ( + "testing" + + "github.com/databricks/cli/libs/config" + "github.com/stretchr/testify/assert" +) + +func TestYAMLMix01(t *testing.T) { + file := "testdata/mix_01.yml" + self := loadYAML(t, file) + assert.NotEqual(t, config.NilValue, self) + + assert.True(t, self.Get("base_address").IsAnchor()) + assert.False(t, self.Get("office_address").IsAnchor()) +} + +func TestYAMLMix02(t *testing.T) { + file := "testdata/mix_02.yml" + self := loadYAML(t, file) + assert.NotEqual(t, config.NilValue, self) + + assert.True(t, self.Get("base_colors").IsAnchor()) + assert.False(t, self.Get("theme").IsAnchor()) +} diff --git a/libs/config/yamlloader/yaml_test.go b/libs/config/yamlloader/yaml_test.go new file mode 100644 index 00000000..017caccd --- /dev/null +++ b/libs/config/yamlloader/yaml_test.go @@ -0,0 +1,30 @@ +package yamlloader_test + +import ( + "bytes" + "os" + "testing" + + "github.com/databricks/cli/libs/config" + "github.com/databricks/cli/libs/config/yamlloader" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" +) + +func loadYAML(t *testing.T, path string) config.Value { + input, err := os.ReadFile(path) + require.NoError(t, err) + + var ref any + err = yaml.Unmarshal(input, &ref) + require.NoError(t, err) + + self, err := yamlloader.LoadYAML(path, bytes.NewBuffer(input)) + require.NoError(t, err) + assert.NotNil(t, self) + + // Deep-equal the two values to ensure that the loader is producing + assert.EqualValues(t, ref, self.AsAny()) + return self +}