mirror of https://github.com/databricks/cli.git
Add configuration normalization code (#915)
## Changes This is similar to #904 but instead of converting the dynamic configuration to Go structs, this normalizes a `config.Value` according to the type of a Go struct and returns the new, normalized `config.Value`. This will be used to ensure that two `config.Value` trees are type-compatible before we can merge them (i.e. instances from different files). Warnings and errors during normalization are accumulated and returned as a `diag.Diagnostics` structure. We can use this to surface warnings about unknown fields, or errors about invalid types, in aggregate instead of one-by-one. This approach is inspired by the pattern to accumulate diagnostics in Terraform provider code. ## Tests New unit tests.
This commit is contained in:
parent
486bf59627
commit
a60c40e71e
|
@ -0,0 +1,235 @@
|
||||||
|
package convert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/databricks/cli/libs/config"
|
||||||
|
"github.com/databricks/cli/libs/diag"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Normalize(dst any, src config.Value) (config.Value, diag.Diagnostics) {
|
||||||
|
return normalizeType(reflect.TypeOf(dst), src)
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeType(typ reflect.Type, src config.Value) (config.Value, diag.Diagnostics) {
|
||||||
|
for typ.Kind() == reflect.Pointer {
|
||||||
|
typ = typ.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
switch typ.Kind() {
|
||||||
|
case reflect.Struct:
|
||||||
|
return normalizeStruct(typ, src)
|
||||||
|
case reflect.Map:
|
||||||
|
return normalizeMap(typ, src)
|
||||||
|
case reflect.Slice:
|
||||||
|
return normalizeSlice(typ, src)
|
||||||
|
case reflect.String:
|
||||||
|
return normalizeString(typ, src)
|
||||||
|
case reflect.Bool:
|
||||||
|
return normalizeBool(typ, src)
|
||||||
|
case reflect.Int, reflect.Int32, reflect.Int64:
|
||||||
|
return normalizeInt(typ, src)
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
return normalizeFloat(typ, src)
|
||||||
|
}
|
||||||
|
|
||||||
|
return config.NilValue, diag.Errorf("unsupported type: %s", typ.Kind())
|
||||||
|
}
|
||||||
|
|
||||||
|
func typeMismatch(expected config.Kind, src config.Value) diag.Diagnostic {
|
||||||
|
return diag.Diagnostic{
|
||||||
|
Severity: diag.Error,
|
||||||
|
Summary: fmt.Sprintf("expected %s, found %s", expected, src.Kind()),
|
||||||
|
Location: src.Location(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeStruct(typ reflect.Type, src config.Value) (config.Value, diag.Diagnostics) {
|
||||||
|
var diags diag.Diagnostics
|
||||||
|
|
||||||
|
switch src.Kind() {
|
||||||
|
case config.KindMap:
|
||||||
|
out := make(map[string]config.Value)
|
||||||
|
info := getStructInfo(typ)
|
||||||
|
for k, v := range src.MustMap() {
|
||||||
|
index, ok := info.Fields[k]
|
||||||
|
if !ok {
|
||||||
|
diags = diags.Append(diag.Diagnostic{
|
||||||
|
Severity: diag.Warning,
|
||||||
|
Summary: fmt.Sprintf("unknown field: %s", k),
|
||||||
|
Location: src.Location(),
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize the value according to the field type.
|
||||||
|
v, err := normalizeType(typ.FieldByIndex(index).Type, v)
|
||||||
|
if err != nil {
|
||||||
|
diags = diags.Extend(err)
|
||||||
|
// Skip the element if it cannot be normalized.
|
||||||
|
if err.HasError() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return config.NewValue(out, src.Location()), diags
|
||||||
|
case config.KindNil:
|
||||||
|
return src, diags
|
||||||
|
}
|
||||||
|
|
||||||
|
return config.NilValue, diags.Append(typeMismatch(config.KindMap, src))
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeMap(typ reflect.Type, src config.Value) (config.Value, diag.Diagnostics) {
|
||||||
|
var diags diag.Diagnostics
|
||||||
|
|
||||||
|
switch src.Kind() {
|
||||||
|
case config.KindMap:
|
||||||
|
out := make(map[string]config.Value)
|
||||||
|
for k, v := range src.MustMap() {
|
||||||
|
// Normalize the value according to the map element type.
|
||||||
|
v, err := normalizeType(typ.Elem(), v)
|
||||||
|
if err != nil {
|
||||||
|
diags = diags.Extend(err)
|
||||||
|
// Skip the element if it cannot be normalized.
|
||||||
|
if err.HasError() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return config.NewValue(out, src.Location()), diags
|
||||||
|
case config.KindNil:
|
||||||
|
return src, diags
|
||||||
|
}
|
||||||
|
|
||||||
|
return config.NilValue, diags.Append(typeMismatch(config.KindMap, src))
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeSlice(typ reflect.Type, src config.Value) (config.Value, diag.Diagnostics) {
|
||||||
|
var diags diag.Diagnostics
|
||||||
|
|
||||||
|
switch src.Kind() {
|
||||||
|
case config.KindSequence:
|
||||||
|
out := make([]config.Value, 0, len(src.MustSequence()))
|
||||||
|
for _, v := range src.MustSequence() {
|
||||||
|
// Normalize the value according to the slice element type.
|
||||||
|
v, err := normalizeType(typ.Elem(), v)
|
||||||
|
if err != nil {
|
||||||
|
diags = diags.Extend(err)
|
||||||
|
// Skip the element if it cannot be normalized.
|
||||||
|
if err.HasError() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out = append(out, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return config.NewValue(out, src.Location()), diags
|
||||||
|
case config.KindNil:
|
||||||
|
return src, diags
|
||||||
|
}
|
||||||
|
|
||||||
|
return config.NilValue, diags.Append(typeMismatch(config.KindSequence, src))
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeString(typ reflect.Type, src config.Value) (config.Value, diag.Diagnostics) {
|
||||||
|
var diags diag.Diagnostics
|
||||||
|
var out string
|
||||||
|
|
||||||
|
switch src.Kind() {
|
||||||
|
case config.KindString:
|
||||||
|
out = src.MustString()
|
||||||
|
case config.KindBool:
|
||||||
|
out = strconv.FormatBool(src.MustBool())
|
||||||
|
case config.KindInt:
|
||||||
|
out = strconv.FormatInt(src.MustInt(), 10)
|
||||||
|
case config.KindFloat:
|
||||||
|
out = strconv.FormatFloat(src.MustFloat(), 'f', -1, 64)
|
||||||
|
default:
|
||||||
|
return config.NilValue, diags.Append(typeMismatch(config.KindString, src))
|
||||||
|
}
|
||||||
|
|
||||||
|
return config.NewValue(out, src.Location()), diags
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeBool(typ reflect.Type, src config.Value) (config.Value, diag.Diagnostics) {
|
||||||
|
var diags diag.Diagnostics
|
||||||
|
var out bool
|
||||||
|
|
||||||
|
switch src.Kind() {
|
||||||
|
case config.KindBool:
|
||||||
|
out = src.MustBool()
|
||||||
|
case config.KindString:
|
||||||
|
// See https://github.com/go-yaml/yaml/blob/f6f7691b1fdeb513f56608cd2c32c51f8194bf51/decode.go#L684-L693.
|
||||||
|
switch src.MustString() {
|
||||||
|
case "true", "y", "Y", "yes", "Yes", "YES", "on", "On", "ON":
|
||||||
|
out = true
|
||||||
|
case "false", "n", "N", "no", "No", "NO", "off", "Off", "OFF":
|
||||||
|
out = false
|
||||||
|
default:
|
||||||
|
// Cannot interpret as a boolean.
|
||||||
|
return config.NilValue, diags.Append(typeMismatch(config.KindBool, src))
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return config.NilValue, diags.Append(typeMismatch(config.KindBool, src))
|
||||||
|
}
|
||||||
|
|
||||||
|
return config.NewValue(out, src.Location()), diags
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeInt(typ reflect.Type, src config.Value) (config.Value, diag.Diagnostics) {
|
||||||
|
var diags diag.Diagnostics
|
||||||
|
var out int64
|
||||||
|
|
||||||
|
switch src.Kind() {
|
||||||
|
case config.KindInt:
|
||||||
|
out = src.MustInt()
|
||||||
|
case config.KindString:
|
||||||
|
var err error
|
||||||
|
out, err = strconv.ParseInt(src.MustString(), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return config.NilValue, diags.Append(diag.Diagnostic{
|
||||||
|
Severity: diag.Error,
|
||||||
|
Summary: fmt.Sprintf("cannot parse %q as an integer", src.MustString()),
|
||||||
|
Location: src.Location(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return config.NilValue, diags.Append(typeMismatch(config.KindInt, src))
|
||||||
|
}
|
||||||
|
|
||||||
|
return config.NewValue(out, src.Location()), diags
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeFloat(typ reflect.Type, src config.Value) (config.Value, diag.Diagnostics) {
|
||||||
|
var diags diag.Diagnostics
|
||||||
|
var out float64
|
||||||
|
|
||||||
|
switch src.Kind() {
|
||||||
|
case config.KindFloat:
|
||||||
|
out = src.MustFloat()
|
||||||
|
case config.KindString:
|
||||||
|
var err error
|
||||||
|
out, err = strconv.ParseFloat(src.MustString(), 64)
|
||||||
|
if err != nil {
|
||||||
|
return config.NilValue, diags.Append(diag.Diagnostic{
|
||||||
|
Severity: diag.Error,
|
||||||
|
Summary: fmt.Sprintf("cannot parse %q as a floating point number", src.MustString()),
|
||||||
|
Location: src.Location(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return config.NilValue, diags.Append(typeMismatch(config.KindFloat, src))
|
||||||
|
}
|
||||||
|
|
||||||
|
return config.NewValue(out, src.Location()), diags
|
||||||
|
}
|
|
@ -0,0 +1,435 @@
|
||||||
|
package convert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/databricks/cli/libs/config"
|
||||||
|
"github.com/databricks/cli/libs/diag"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNormalizeStruct(t *testing.T) {
|
||||||
|
type Tmp struct {
|
||||||
|
Foo string `json:"foo"`
|
||||||
|
Bar string `json:"bar"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var typ Tmp
|
||||||
|
vin := config.V(map[string]config.Value{
|
||||||
|
"foo": config.V("bar"),
|
||||||
|
"bar": config.V("baz"),
|
||||||
|
})
|
||||||
|
|
||||||
|
vout, err := Normalize(typ, vin)
|
||||||
|
assert.Empty(t, err)
|
||||||
|
assert.Equal(t, vin, vout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeStructElementDiagnostic(t *testing.T) {
|
||||||
|
type Tmp struct {
|
||||||
|
Foo string `json:"foo"`
|
||||||
|
Bar string `json:"bar"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var typ Tmp
|
||||||
|
vin := config.V(map[string]config.Value{
|
||||||
|
"foo": config.V("bar"),
|
||||||
|
"bar": config.V(map[string]config.Value{"an": config.V("error")}),
|
||||||
|
})
|
||||||
|
|
||||||
|
vout, err := Normalize(typ, vin)
|
||||||
|
assert.Len(t, err, 1)
|
||||||
|
assert.Equal(t, diag.Diagnostic{
|
||||||
|
Severity: diag.Error,
|
||||||
|
Summary: `expected string, found map`,
|
||||||
|
Location: config.Location{},
|
||||||
|
}, err[0])
|
||||||
|
|
||||||
|
// Elements that encounter an error during normalization are dropped.
|
||||||
|
assert.Equal(t, map[string]any{
|
||||||
|
"foo": "bar",
|
||||||
|
}, vout.AsAny())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeStructUnknownField(t *testing.T) {
|
||||||
|
type Tmp struct {
|
||||||
|
Foo string `json:"foo"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var typ Tmp
|
||||||
|
vin := config.V(map[string]config.Value{
|
||||||
|
"foo": config.V("bar"),
|
||||||
|
"bar": config.V("baz"),
|
||||||
|
})
|
||||||
|
|
||||||
|
vout, err := Normalize(typ, vin)
|
||||||
|
assert.Len(t, err, 1)
|
||||||
|
assert.Equal(t, diag.Diagnostic{
|
||||||
|
Severity: diag.Warning,
|
||||||
|
Summary: `unknown field: bar`,
|
||||||
|
Location: vin.Get("foo").Location(),
|
||||||
|
}, err[0])
|
||||||
|
|
||||||
|
// The field that can be mapped to the struct field is retained.
|
||||||
|
assert.Equal(t, map[string]any{
|
||||||
|
"foo": "bar",
|
||||||
|
}, vout.AsAny())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeStructNil(t *testing.T) {
|
||||||
|
type Tmp struct {
|
||||||
|
Foo string `json:"foo"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var typ Tmp
|
||||||
|
vin := config.NilValue
|
||||||
|
vout, err := Normalize(typ, vin)
|
||||||
|
assert.Empty(t, err)
|
||||||
|
assert.Equal(t, vin, vout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeStructError(t *testing.T) {
|
||||||
|
type Tmp struct {
|
||||||
|
Foo string `json:"foo"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var typ Tmp
|
||||||
|
vin := config.V("string")
|
||||||
|
_, err := Normalize(typ, vin)
|
||||||
|
assert.Len(t, err, 1)
|
||||||
|
assert.Equal(t, diag.Diagnostic{
|
||||||
|
Severity: diag.Error,
|
||||||
|
Summary: `expected map, found string`,
|
||||||
|
Location: vin.Get("foo").Location(),
|
||||||
|
}, err[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeMap(t *testing.T) {
|
||||||
|
var typ map[string]string
|
||||||
|
vin := config.V(map[string]config.Value{
|
||||||
|
"foo": config.V("bar"),
|
||||||
|
"bar": config.V("baz"),
|
||||||
|
})
|
||||||
|
|
||||||
|
vout, err := Normalize(typ, vin)
|
||||||
|
assert.Empty(t, err)
|
||||||
|
assert.Equal(t, vin, vout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeMapElementDiagnostic(t *testing.T) {
|
||||||
|
var typ map[string]string
|
||||||
|
vin := config.V(map[string]config.Value{
|
||||||
|
"foo": config.V("bar"),
|
||||||
|
"bar": config.V(map[string]config.Value{"an": config.V("error")}),
|
||||||
|
})
|
||||||
|
|
||||||
|
vout, err := Normalize(typ, vin)
|
||||||
|
assert.Len(t, err, 1)
|
||||||
|
assert.Equal(t, diag.Diagnostic{
|
||||||
|
Severity: diag.Error,
|
||||||
|
Summary: `expected string, found map`,
|
||||||
|
Location: config.Location{},
|
||||||
|
}, err[0])
|
||||||
|
|
||||||
|
// Elements that encounter an error during normalization are dropped.
|
||||||
|
assert.Equal(t, map[string]any{
|
||||||
|
"foo": "bar",
|
||||||
|
}, vout.AsAny())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeMapNil(t *testing.T) {
|
||||||
|
var typ map[string]string
|
||||||
|
vin := config.NilValue
|
||||||
|
vout, err := Normalize(typ, vin)
|
||||||
|
assert.Empty(t, err)
|
||||||
|
assert.Equal(t, vin, vout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeMapError(t *testing.T) {
|
||||||
|
var typ map[string]string
|
||||||
|
vin := config.V("string")
|
||||||
|
_, err := Normalize(typ, vin)
|
||||||
|
assert.Len(t, err, 1)
|
||||||
|
assert.Equal(t, diag.Diagnostic{
|
||||||
|
Severity: diag.Error,
|
||||||
|
Summary: `expected map, found string`,
|
||||||
|
Location: vin.Location(),
|
||||||
|
}, err[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeSlice(t *testing.T) {
|
||||||
|
var typ []string
|
||||||
|
vin := config.V([]config.Value{
|
||||||
|
config.V("foo"),
|
||||||
|
config.V("bar"),
|
||||||
|
})
|
||||||
|
|
||||||
|
vout, err := Normalize(typ, vin)
|
||||||
|
assert.Empty(t, err)
|
||||||
|
assert.Equal(t, vin, vout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeSliceElementDiagnostic(t *testing.T) {
|
||||||
|
var typ []string
|
||||||
|
vin := config.V([]config.Value{
|
||||||
|
config.V("foo"),
|
||||||
|
config.V("bar"),
|
||||||
|
config.V(map[string]config.Value{"an": config.V("error")}),
|
||||||
|
})
|
||||||
|
|
||||||
|
vout, err := Normalize(typ, vin)
|
||||||
|
assert.Len(t, err, 1)
|
||||||
|
assert.Equal(t, diag.Diagnostic{
|
||||||
|
Severity: diag.Error,
|
||||||
|
Summary: `expected string, found map`,
|
||||||
|
Location: config.Location{},
|
||||||
|
}, err[0])
|
||||||
|
|
||||||
|
// Elements that encounter an error during normalization are dropped.
|
||||||
|
assert.Equal(t, []any{"foo", "bar"}, vout.AsAny())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeSliceNil(t *testing.T) {
|
||||||
|
var typ []string
|
||||||
|
vin := config.NilValue
|
||||||
|
vout, err := Normalize(typ, vin)
|
||||||
|
assert.Empty(t, err)
|
||||||
|
assert.Equal(t, vin, vout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeSliceError(t *testing.T) {
|
||||||
|
var typ []string
|
||||||
|
vin := config.V("string")
|
||||||
|
_, err := Normalize(typ, vin)
|
||||||
|
assert.Len(t, err, 1)
|
||||||
|
assert.Equal(t, diag.Diagnostic{
|
||||||
|
Severity: diag.Error,
|
||||||
|
Summary: `expected sequence, found string`,
|
||||||
|
Location: vin.Location(),
|
||||||
|
}, err[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeString(t *testing.T) {
|
||||||
|
var typ string
|
||||||
|
vin := config.V("string")
|
||||||
|
vout, err := Normalize(&typ, vin)
|
||||||
|
assert.Empty(t, err)
|
||||||
|
assert.Equal(t, vin, vout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeStringNil(t *testing.T) {
|
||||||
|
var typ string
|
||||||
|
vin := config.NewValue(nil, config.Location{File: "file", Line: 1, Column: 1})
|
||||||
|
_, err := Normalize(&typ, vin)
|
||||||
|
assert.Len(t, err, 1)
|
||||||
|
assert.Equal(t, diag.Diagnostic{
|
||||||
|
Severity: diag.Error,
|
||||||
|
Summary: `expected string, found nil`,
|
||||||
|
Location: vin.Location(),
|
||||||
|
}, err[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeStringFromBool(t *testing.T) {
|
||||||
|
var typ string
|
||||||
|
vin := config.NewValue(true, config.Location{File: "file", Line: 1, Column: 1})
|
||||||
|
vout, err := Normalize(&typ, vin)
|
||||||
|
assert.Empty(t, err)
|
||||||
|
assert.Equal(t, config.NewValue("true", vin.Location()), vout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeStringFromInt(t *testing.T) {
|
||||||
|
var typ string
|
||||||
|
vin := config.NewValue(123, config.Location{File: "file", Line: 1, Column: 1})
|
||||||
|
vout, err := Normalize(&typ, vin)
|
||||||
|
assert.Empty(t, err)
|
||||||
|
assert.Equal(t, config.NewValue("123", vin.Location()), vout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeStringFromFloat(t *testing.T) {
|
||||||
|
var typ string
|
||||||
|
vin := config.NewValue(1.20, config.Location{File: "file", Line: 1, Column: 1})
|
||||||
|
vout, err := Normalize(&typ, vin)
|
||||||
|
assert.Empty(t, err)
|
||||||
|
assert.Equal(t, config.NewValue("1.2", vin.Location()), vout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeStringError(t *testing.T) {
|
||||||
|
var typ string
|
||||||
|
vin := config.V(map[string]config.Value{"an": config.V("error")})
|
||||||
|
_, err := Normalize(&typ, vin)
|
||||||
|
assert.Len(t, err, 1)
|
||||||
|
assert.Equal(t, diag.Diagnostic{
|
||||||
|
Severity: diag.Error,
|
||||||
|
Summary: `expected string, found map`,
|
||||||
|
Location: config.Location{},
|
||||||
|
}, err[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeBool(t *testing.T) {
|
||||||
|
var typ bool
|
||||||
|
vin := config.V(true)
|
||||||
|
vout, err := Normalize(&typ, vin)
|
||||||
|
assert.Empty(t, err)
|
||||||
|
assert.Equal(t, config.V(true), vout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeBoolNil(t *testing.T) {
|
||||||
|
var typ bool
|
||||||
|
vin := config.NewValue(nil, config.Location{File: "file", Line: 1, Column: 1})
|
||||||
|
_, err := Normalize(&typ, vin)
|
||||||
|
assert.Len(t, err, 1)
|
||||||
|
assert.Equal(t, diag.Diagnostic{
|
||||||
|
Severity: diag.Error,
|
||||||
|
Summary: `expected bool, found nil`,
|
||||||
|
Location: vin.Location(),
|
||||||
|
}, err[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeBoolFromString(t *testing.T) {
|
||||||
|
var typ bool
|
||||||
|
|
||||||
|
for _, c := range []struct {
|
||||||
|
Input string
|
||||||
|
Output bool
|
||||||
|
}{
|
||||||
|
{"true", true},
|
||||||
|
{"false", false},
|
||||||
|
{"Y", true},
|
||||||
|
{"N", false},
|
||||||
|
{"on", true},
|
||||||
|
{"off", false},
|
||||||
|
} {
|
||||||
|
vin := config.V(c.Input)
|
||||||
|
vout, err := Normalize(&typ, vin)
|
||||||
|
assert.Empty(t, err)
|
||||||
|
assert.Equal(t, config.V(c.Output), vout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeBoolFromStringError(t *testing.T) {
|
||||||
|
var typ bool
|
||||||
|
vin := config.V("abc")
|
||||||
|
_, err := Normalize(&typ, vin)
|
||||||
|
assert.Len(t, err, 1)
|
||||||
|
assert.Equal(t, diag.Diagnostic{
|
||||||
|
Severity: diag.Error,
|
||||||
|
Summary: `expected bool, found string`,
|
||||||
|
Location: vin.Location(),
|
||||||
|
}, err[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeBoolError(t *testing.T) {
|
||||||
|
var typ bool
|
||||||
|
vin := config.V(map[string]config.Value{"an": config.V("error")})
|
||||||
|
_, err := Normalize(&typ, vin)
|
||||||
|
assert.Len(t, err, 1)
|
||||||
|
assert.Equal(t, diag.Diagnostic{
|
||||||
|
Severity: diag.Error,
|
||||||
|
Summary: `expected bool, found map`,
|
||||||
|
Location: config.Location{},
|
||||||
|
}, err[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeInt(t *testing.T) {
|
||||||
|
var typ int
|
||||||
|
vin := config.V(123)
|
||||||
|
vout, err := Normalize(&typ, vin)
|
||||||
|
assert.Empty(t, err)
|
||||||
|
assert.Equal(t, config.V(int64(123)), vout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeIntNil(t *testing.T) {
|
||||||
|
var typ int
|
||||||
|
vin := config.NewValue(nil, config.Location{File: "file", Line: 1, Column: 1})
|
||||||
|
_, err := Normalize(&typ, vin)
|
||||||
|
assert.Len(t, err, 1)
|
||||||
|
assert.Equal(t, diag.Diagnostic{
|
||||||
|
Severity: diag.Error,
|
||||||
|
Summary: `expected int, found nil`,
|
||||||
|
Location: vin.Location(),
|
||||||
|
}, err[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeIntFromString(t *testing.T) {
|
||||||
|
var typ int
|
||||||
|
vin := config.V("123")
|
||||||
|
vout, err := Normalize(&typ, vin)
|
||||||
|
assert.Empty(t, err)
|
||||||
|
assert.Equal(t, config.V(int64(123)), vout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeIntFromStringError(t *testing.T) {
|
||||||
|
var typ int
|
||||||
|
vin := config.V("abc")
|
||||||
|
_, err := Normalize(&typ, vin)
|
||||||
|
assert.Len(t, err, 1)
|
||||||
|
assert.Equal(t, diag.Diagnostic{
|
||||||
|
Severity: diag.Error,
|
||||||
|
Summary: `cannot parse "abc" as an integer`,
|
||||||
|
Location: vin.Location(),
|
||||||
|
}, err[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeIntError(t *testing.T) {
|
||||||
|
var typ int
|
||||||
|
vin := config.V(map[string]config.Value{"an": config.V("error")})
|
||||||
|
_, err := Normalize(&typ, vin)
|
||||||
|
assert.Len(t, err, 1)
|
||||||
|
assert.Equal(t, diag.Diagnostic{
|
||||||
|
Severity: diag.Error,
|
||||||
|
Summary: `expected int, found map`,
|
||||||
|
Location: config.Location{},
|
||||||
|
}, err[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeFloat(t *testing.T) {
|
||||||
|
var typ float64
|
||||||
|
vin := config.V(1.2)
|
||||||
|
vout, err := Normalize(&typ, vin)
|
||||||
|
assert.Empty(t, err)
|
||||||
|
assert.Equal(t, config.V(1.2), vout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeFloatNil(t *testing.T) {
|
||||||
|
var typ float64
|
||||||
|
vin := config.NewValue(nil, config.Location{File: "file", Line: 1, Column: 1})
|
||||||
|
_, err := Normalize(&typ, vin)
|
||||||
|
assert.Len(t, err, 1)
|
||||||
|
assert.Equal(t, diag.Diagnostic{
|
||||||
|
Severity: diag.Error,
|
||||||
|
Summary: `expected float, found nil`,
|
||||||
|
Location: vin.Location(),
|
||||||
|
}, err[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeFloatFromString(t *testing.T) {
|
||||||
|
var typ float64
|
||||||
|
vin := config.V("1.2")
|
||||||
|
vout, err := Normalize(&typ, vin)
|
||||||
|
assert.Empty(t, err)
|
||||||
|
assert.Equal(t, config.V(1.2), vout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeFloatFromStringError(t *testing.T) {
|
||||||
|
var typ float64
|
||||||
|
vin := config.V("abc")
|
||||||
|
_, err := Normalize(&typ, vin)
|
||||||
|
assert.Len(t, err, 1)
|
||||||
|
assert.Equal(t, diag.Diagnostic{
|
||||||
|
Severity: diag.Error,
|
||||||
|
Summary: `cannot parse "abc" as a floating point number`,
|
||||||
|
Location: vin.Location(),
|
||||||
|
}, err[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeFloatError(t *testing.T) {
|
||||||
|
var typ float64
|
||||||
|
vin := config.V(map[string]config.Value{"an": config.V("error")})
|
||||||
|
_, err := Normalize(&typ, vin)
|
||||||
|
assert.Len(t, err, 1)
|
||||||
|
assert.Equal(t, diag.Diagnostic{
|
||||||
|
Severity: diag.Error,
|
||||||
|
Summary: `expected float, found map`,
|
||||||
|
Location: config.Location{},
|
||||||
|
}, err[0])
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
package diag
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/databricks/cli/libs/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Diagnostic struct {
|
||||||
|
Severity Severity
|
||||||
|
|
||||||
|
// Summary is a short description of the diagnostic.
|
||||||
|
// This is expected to be a single line and always present.
|
||||||
|
Summary string
|
||||||
|
|
||||||
|
// Detail is a longer description of the diagnostic.
|
||||||
|
// This may be multiple lines and may be nil.
|
||||||
|
Detail string
|
||||||
|
|
||||||
|
// Location is a source code location associated with the diagnostic message.
|
||||||
|
// It may be zero if there is no associated location.
|
||||||
|
Location config.Location
|
||||||
|
}
|
||||||
|
|
||||||
|
// Errorf creates a new error diagnostic.
|
||||||
|
func Errorf(format string, args ...any) Diagnostics {
|
||||||
|
return []Diagnostic{
|
||||||
|
{
|
||||||
|
Severity: Error,
|
||||||
|
Summary: fmt.Sprintf(format, args...),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warningf creates a new warning diagnostic.
|
||||||
|
func Warningf(format string, args ...any) Diagnostics {
|
||||||
|
return []Diagnostic{
|
||||||
|
{
|
||||||
|
Severity: Warning,
|
||||||
|
Summary: fmt.Sprintf(format, args...),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Infof creates a new info diagnostic.
|
||||||
|
func Infof(format string, args ...any) Diagnostics {
|
||||||
|
return []Diagnostic{
|
||||||
|
{
|
||||||
|
Severity: Info,
|
||||||
|
Summary: fmt.Sprintf(format, args...),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Diagsnostics holds zero or more instances of [Diagnostic].
|
||||||
|
type Diagnostics []Diagnostic
|
||||||
|
|
||||||
|
// Append adds a new diagnostic to the end of the list.
|
||||||
|
func (ds Diagnostics) Append(d Diagnostic) Diagnostics {
|
||||||
|
return append(ds, d)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extend adds all diagnostics from another list to the end of the list.
|
||||||
|
func (ds Diagnostics) Extend(other Diagnostics) Diagnostics {
|
||||||
|
return append(ds, other...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasError returns true if any of the diagnostics are errors.
|
||||||
|
func (ds Diagnostics) HasError() bool {
|
||||||
|
for _, d := range ds {
|
||||||
|
if d.Severity == Error {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package diag
|
||||||
|
|
||||||
|
type Severity int
|
||||||
|
|
||||||
|
const (
|
||||||
|
Error Severity = iota
|
||||||
|
Warning
|
||||||
|
Info
|
||||||
|
)
|
Loading…
Reference in New Issue