Apply CLI overrides to decoded configuration
This commit is contained in:
@@ -547,3 +547,139 @@ func fmtLoadError(loadError *LoadError) string {
|
||||
_ = WriteDiagnostics(&output, loadError.Diagnostics())
|
||||
return loadError.Error() + output.String()
|
||||
}
|
||||
|
||||
func TestApplyDecodedOverridesUsesSchemaAndPreservesTypedOwnership(t *testing.T) {
|
||||
t.Parallel()
|
||||
type child struct {
|
||||
Name string `yaml:"name"`
|
||||
Enabled bool `yaml:"enabled"`
|
||||
}
|
||||
type config struct {
|
||||
Nested child `yaml:"nested"`
|
||||
Labels map[string]child `yaml:"labels"`
|
||||
Items []child `yaml:"items"`
|
||||
Empty []string `yaml:"empty"`
|
||||
Count int `yaml:"count"`
|
||||
Enabled bool `yaml:"enabled"`
|
||||
Label string `yaml:"label"`
|
||||
Small namedSmall `yaml:"small"`
|
||||
Text validatedText `yaml:"text"`
|
||||
Delay time.Duration `yaml:"delay"`
|
||||
APIKey Secret `yaml:"api_key"`
|
||||
Maybe *string `yaml:"maybe"`
|
||||
}
|
||||
defaultMaybe := "default"
|
||||
defaults := config{
|
||||
Nested: child{Name: "default", Enabled: true},
|
||||
Labels: map[string]child{"default": {Name: "default", Enabled: true}},
|
||||
Items: []child{{Name: "default", Enabled: true}},
|
||||
Empty: []string{"default"},
|
||||
Count: 8, Enabled: true, Label: "default", Small: 7, Text: "default", Delay: time.Second,
|
||||
Maybe: &defaultMaybe,
|
||||
}
|
||||
input := parseDocumentForDecode(t, "nested: {name: source, enabled: true}\nlabels: {old: {name: source, enabled: true}}\nitems: [{name: discarded, enabled: true}]\nempty: [source]\ncount: 7\nenabled: true\nlabel: source\nsmall: 3\ntext: source\ndelay: 1s\napi_key: SOURCE_SECRET\nmaybe: source\n")
|
||||
schema, schemaIssue := buildSchema(reflect.TypeFor[config](), testSecretBinding())
|
||||
if schemaIssue != nil {
|
||||
t.Fatalf("buildSchema() issue = %#v", schemaIssue)
|
||||
}
|
||||
decoded, decodeIssues := decodeConfiguration(schema, reflect.ValueOf(defaults), input)
|
||||
if len(decodeIssues) != 0 {
|
||||
t.Fatalf("decodeConfiguration() issues = %#v", decodeIssues)
|
||||
}
|
||||
batch, diagnostics := preparePostStructuralOverrides(overridePreparationInput{root: input}, nil, []Override{
|
||||
{Path: Keys("nested"), Value: map[string]any{"enabled": false}, Flag: "--nested"},
|
||||
{Path: Keys("labels"), Value: map[string]any{
|
||||
"old": map[string]any{"enabled": false}, "new": map[string]any{"name": "cli"},
|
||||
}, Flag: "--labels"},
|
||||
{Path: Keys("items"), Value: []any{map[string]any{"name": "replacement"}}, Flag: "--items"},
|
||||
{Path: Keys("empty"), Value: []any{}, Flag: "--empty"},
|
||||
{Path: Keys("count"), Value: 0, Flag: "--count"},
|
||||
{Path: Keys("enabled"), Value: false, Flag: "--enabled"},
|
||||
{Path: Keys("label"), Value: "", Flag: "--label"},
|
||||
{Path: Keys("small"), Value: 0, Flag: "--small"},
|
||||
{Path: Keys("text"), Value: "cli text", Flag: "--text"},
|
||||
{Path: Keys("delay"), Value: "250ms", Flag: "--delay"},
|
||||
{Path: Keys("api_key"), Value: "CLI_SECRET", Flag: "--api-key"},
|
||||
{Path: Keys("maybe"), Value: nil, Flag: "--maybe"},
|
||||
}, nil, defaultImportLimits())
|
||||
if len(diagnostics) != 0 {
|
||||
t.Fatalf("preparePostStructuralOverrides() diagnostics = %#v", diagnostics)
|
||||
}
|
||||
result, issues := applyDecodedOverrides(schema, decoded, batch, decodeContext{})
|
||||
if len(issues) != 0 {
|
||||
t.Fatalf("applyDecodedOverrides() issues = %#v", issues)
|
||||
}
|
||||
got := result.value.Interface().(config)
|
||||
if got.Nested != (child{Name: "source", Enabled: false}) || !reflect.DeepEqual(got.Labels, map[string]child{
|
||||
"default": {Name: "default", Enabled: true},
|
||||
"old": {Name: "source", Enabled: false},
|
||||
"new": {Name: "cli"},
|
||||
}) || !reflect.DeepEqual(got.Items, []child{{Name: "replacement"}}) || got.Count != 0 || got.Enabled ||
|
||||
got.Empty == nil || len(got.Empty) != 0 || got.Label != "" || got.Small != 0 ||
|
||||
got.Text != "cli text" || got.Delay != 250*time.Millisecond || got.APIKey.name != "CLI_SECRET" || got.Maybe != nil {
|
||||
t.Fatalf("post-decode configuration = %#v", got)
|
||||
}
|
||||
if result.preCLI == result.final || !reflect.DeepEqual(result.preCLI, decoded.final) || len(result.overrides) != len(batch.overrides) {
|
||||
t.Fatalf("result snapshots/contributions = pre:%p final:%p overrides:%d", result.preCLI, result.final, len(result.overrides))
|
||||
}
|
||||
if _, explicit := result.presence["/items/0/enabled"]; explicit {
|
||||
t.Fatalf("replacement list inherited presence from its discarded item: %#v", result.presence)
|
||||
}
|
||||
if result.presence["/items/0/name"].location.SourceID != batch.overrides[2].source.ID ||
|
||||
result.presence["/nested"].location.SourceID != "source-1" ||
|
||||
result.origins["/items/0/name"].Layer != LayerCLI || result.origins["/nested/name"].Layer != LayerBase ||
|
||||
result.origins["/nested/enabled"].Layer != LayerCLI || result.origins["/labels/default/name"].Layer != LayerDefault {
|
||||
t.Fatalf("presence/origins = %#v / %#v", result.presence, result.origins)
|
||||
}
|
||||
if node, ok := documentAtPath(result.final, Keys("items").Index(0).Key("enabled")); !ok || node.Kind != document.Boolean || node.Text != "false" {
|
||||
t.Fatalf("replacement list default projection = %#v", node)
|
||||
}
|
||||
|
||||
got.Labels["new"] = child{Name: "mutated"}
|
||||
result.overrides[0].value.Mapping[0].Value.Text = "changed"
|
||||
if decoded.value.Interface().(config).Labels["old"].Name != "source" || batch.overrides[0].value.Mapping[0].Value.Text != "false" {
|
||||
t.Fatal("post-decode result mutation reached the pre-CLI value or prepared batch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyDecodedOverridesRejectsSchemaErrorsWithoutPartialResult(t *testing.T) {
|
||||
t.Parallel()
|
||||
type config struct {
|
||||
Nested struct {
|
||||
Kept int `yaml:"kept"`
|
||||
} `yaml:"nested"`
|
||||
Count int `yaml:"count"`
|
||||
}
|
||||
input := parseDocumentForDecode(t, "nested: {kept: 1}\ncount: 2\n")
|
||||
schema := mustSchemaFor[config](t)
|
||||
decoded, issues := decodeConfiguration(schema, reflect.ValueOf(config{}), input)
|
||||
if len(issues) != 0 {
|
||||
t.Fatalf("decodeConfiguration() issues = %#v", issues)
|
||||
}
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
override Override
|
||||
code Code
|
||||
}{
|
||||
{name: "unknown nested field", override: Override{Path: Keys("nested"), Value: map[string]any{"unknown": 1}, Flag: "--nested"}, code: CodeDecodeUnknownField},
|
||||
{name: "invalid type", override: Override{Path: Keys("count"), Value: "not an integer", Flag: "--count"}, code: CodeDecodeType},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
batch, diagnostics := preparePostStructuralOverrides(overridePreparationInput{root: input}, nil, []Override{test.override}, nil, defaultImportLimits())
|
||||
if len(diagnostics) != 0 {
|
||||
t.Fatalf("preparePostStructuralOverrides() diagnostics = %#v", diagnostics)
|
||||
}
|
||||
result, issues := applyDecodedOverrides(schema, decoded, batch, decodeContext{})
|
||||
if len(issues) == 0 || issues[0].code != test.code {
|
||||
t.Fatalf("decode issues = %#v, want code %q", issues, test.code)
|
||||
}
|
||||
if result.value.IsValid() || result.final != nil {
|
||||
t.Fatalf("failed assignment returned a partial result: %#v", result)
|
||||
}
|
||||
original := decoded.value.Interface().(config)
|
||||
if original.Count != 2 || original.Nested.Kept != 1 {
|
||||
t.Fatal("failed assignment mutated the pre-CLI decoded value")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user