Use entry metadata for resolution presence

This commit is contained in:
2026-10-26 14:30:25 +00:00
parent 0dd16aa8a9
commit 28ef9b6200
4 changed files with 154 additions and 7 deletions

View File

@@ -13,7 +13,6 @@ const closedCallbackContextMessage = "configkit: callback context is no longer a
type resolutionState struct {
active bool
schema *schemaDescriptor
presence map[string]inputPresence
document *document.Node
input resolutionInput
records map[string]*resolutionRecord
@@ -78,8 +77,12 @@ func (r *Resolution) IsExplicit(path Path) bool {
if !state.validPath(path) {
return false
}
_, explicit := state.presence[path.String()]
return explicit
return state.inputExplicit(path)
}
func (state *resolutionState) inputExplicit(path Path) bool {
info, found := state.input.lookup(path)
return found && info.Explicit
}
// InputInfo returns detached entry-state metadata for path. The entry state is
@@ -134,7 +137,7 @@ func Default[V any](resolution *Resolution, path Path, target *V, value V, reaso
if !ok {
return
}
if _, explicit := state.presence[path.String()]; explicit {
if state.inputExplicit(path) {
return
}
*target = value
@@ -282,7 +285,7 @@ func (state *resolutionState) addDiagnostic(code Code, path Path, message string
func runResolution[T any](configuration *T, resolver func(*T, *Resolution) error, schema *schemaDescriptor, decoded decodedConfiguration, input *document.Node, entry resolutionInput) resolutionResult {
state := &resolutionState{
active: true, schema: schema, presence: decoded.presence, document: input,
active: true, schema: schema, document: input,
input: entry,
records: make(map[string]*resolutionRecord),
}

View File

@@ -311,6 +311,85 @@ func TestResolutionRejectsInvalidProjectedValuesAndSuppressesReturnedErrors(t *t
}
}
func TestResolutionPresenceUsesEntryShapeAcrossDynamicChanges(t *testing.T) {
t.Parallel()
type config struct {
Dynamic any `yaml:"dynamic"`
}
tests := []struct {
name string
input string
keyPresent bool
}{
{name: "mapping to sequence", input: "dynamic: {'0': input}\n", keyPresent: true},
{name: "sequence to mapping", input: "dynamic: [input]\n", keyPresent: false},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
parsed, err := ParsePath("/dynamic/0")
if err != nil {
t.Fatal(err)
}
var callbackErr error
result, err := testLoad(Bytes("dynamic.yml", []byte(test.input)), Options[config]{Definition: Definition[config]{
Resolve: func(configuration *config, resolution *Resolution) error {
check := func(path Path, want bool) error {
info, found := resolution.InputInfo(path)
explicit := resolution.IsExplicit(path)
if found != want || explicit != want || (found && info.Explicit != explicit) {
return errors.New("resolution presence disagrees with entry shape")
}
return nil
}
keyPath := Keys("dynamic", "0")
indexPath := Keys("dynamic").Index(0)
for _, checkPath := range []struct {
path Path
want bool
}{
{path: keyPath, want: test.keyPresent},
{path: indexPath, want: !test.keyPresent},
{path: parsed, want: true},
{path: Keys("dynamic", "missing"), want: false},
} {
if err := check(checkPath.path, checkPath.want); err != nil {
callbackErr = err
return err
}
}
resolution.Record(Keys("dynamic"), ChangeNormalization, "change dynamic shape")
child := ""
if test.keyPresent {
configuration.Dynamic = []any{}
Default(resolution, indexPath, &child, "derived", "default new sequence child")
configuration.Dynamic = []any{child}
} else {
configuration.Dynamic = map[string]any{}
Default(resolution, keyPath, &child, "derived", "default new mapping child")
configuration.Dynamic = map[string]any{"0": child}
}
if child != "derived" {
callbackErr = errors.New("default was suppressed by the old container shape")
return callbackErr
}
return nil
},
}})
if callbackErr != nil {
t.Fatal(callbackErr)
}
if err != nil {
t.Fatal(err)
}
if len(result.resolution) != 2 || result.resolution[1].change.Origin.Operation != ChangeDerivedDefault {
t.Fatalf("resolution records = %#v", result.resolution)
}
})
}
}
func TestResolutionEntryQueriesExposeDetachedLoadInput(t *testing.T) {
t.Parallel()