Define main-relative input options
This commit is contained in:
@@ -16,6 +16,8 @@ release gate is part of this work.
|
|||||||
|
|
||||||
## Stage 1 — Define the Go API and preflight rules
|
## Stage 1 — Define the Go API and preflight rules
|
||||||
|
|
||||||
|
**Status:** Complete. Main-relative descriptions and layered preflight are in place.
|
||||||
|
|
||||||
In root and engine `layered.go`, add matching descriptions and carry them into
|
In root and engine `layered.go`, add matching descriptions and carry them into
|
||||||
`loadInput` and `loadRequest` in the engine. Use these public names:
|
`loadInput` and `loadRequest` in the engine. Use these public names:
|
||||||
|
|
||||||
|
|||||||
45
facade_main_relative_options_test.go
Normal file
45
facade_main_relative_options_test.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package configkit_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
configkit "go.maximumdirect.net/configkit"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMainRelativeOptions(t *testing.T) {
|
||||||
|
if configkit.MainRelative() == configkit.MainRelative() {
|
||||||
|
t.Fatal("binding descriptions must be distinct")
|
||||||
|
}
|
||||||
|
invalid := &configkit.FilesystemContext{BaseDir: "relative", Root: "relative"}
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
main configkit.Input
|
||||||
|
underlay configkit.Input
|
||||||
|
overlay string
|
||||||
|
overlayContext, fallback *configkit.FilesystemContext
|
||||||
|
wantError bool
|
||||||
|
}{
|
||||||
|
{name: "binding on main", main: configkit.Input{Source: configkit.NoSource(), MainRelative: configkit.MainRelative()}, wantError: true},
|
||||||
|
{name: "binding on file", underlay: configkit.Input{Source: configkit.File("missing"), MainRelative: configkit.MainRelative()}, wantError: true},
|
||||||
|
{name: "invalid fallback", underlay: configkit.Input{Source: configkit.NoSource(), MainRelative: &configkit.MainRelativeContext{Fallback: invalid}}, wantError: true},
|
||||||
|
{name: "shadowed binding", underlay: configkit.Input{Source: configkit.NoSource(), Filesystem: &configkit.FilesystemContext{Root: t.TempDir()}, MainRelative: &configkit.MainRelativeContext{Fallback: invalid}}},
|
||||||
|
{name: "inactive overlay fallback", fallback: invalid},
|
||||||
|
{name: "invalid explicit overlay", overlay: "test", overlayContext: invalid, fallback: &configkit.FilesystemContext{Root: t.TempDir(), BaseDir: t.TempDir()}, wantError: true},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
main := tc.main
|
||||||
|
if tc.name != "binding on main" {
|
||||||
|
main.Source = configkit.NoSource()
|
||||||
|
}
|
||||||
|
opts := configkit.LayeredOptions[layeredFacadeConfig]{Overlay: tc.overlay, OverlayFilesystem: tc.overlayContext, OverlayFallback: tc.fallback}
|
||||||
|
if tc.name == "binding on file" || tc.name == "invalid fallback" || tc.name == "shadowed binding" {
|
||||||
|
opts.Underlays = []configkit.Underlay{{Inputs: []configkit.Input{tc.underlay}}}
|
||||||
|
}
|
||||||
|
_, err := configkit.LoadLayered(main, opts)
|
||||||
|
if (err != nil) != tc.wantError {
|
||||||
|
t.Fatalf("error = %v, want error %v", err, tc.wantError)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,10 +7,16 @@ type Input struct {
|
|||||||
Source Source
|
Source Source
|
||||||
Imports bool
|
Imports bool
|
||||||
Filesystem *FilesystemContext
|
Filesystem *FilesystemContext
|
||||||
|
MainRelative *MainRelativeContext
|
||||||
Destination Path
|
Destination Path
|
||||||
PlacementMode PlacementMode
|
PlacementMode PlacementMode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MainRelativeContext struct {
|
||||||
|
Fallback *FilesystemContext
|
||||||
|
RequireBase bool
|
||||||
|
}
|
||||||
|
|
||||||
// Underlay contains strictly composed contributions at one precedence tier.
|
// Underlay contains strictly composed contributions at one precedence tier.
|
||||||
type Underlay struct{ Inputs []Input }
|
type Underlay struct{ Inputs []Input }
|
||||||
|
|
||||||
@@ -22,6 +28,7 @@ type LayeredOptions[T any] struct {
|
|||||||
Underlays []Underlay
|
Underlays []Underlay
|
||||||
Overlay string
|
Overlay string
|
||||||
OverlayFilesystem *FilesystemContext
|
OverlayFilesystem *FilesystemContext
|
||||||
|
OverlayFallback *FilesystemContext
|
||||||
Overrides []Override
|
Overrides []Override
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,13 +44,14 @@ func LoadLayered[T any](main Input, secrets SecretBinding, supplied ...LayeredOp
|
|||||||
}
|
}
|
||||||
request := loadRequest[T]{
|
request := loadRequest[T]{
|
||||||
main: loadInput{
|
main: loadInput{
|
||||||
source: main.Source, imports: main.Imports, filesystem: main.Filesystem,
|
source: main.Source, imports: main.Imports, filesystem: main.Filesystem, mainRelative: main.MainRelative,
|
||||||
destination: main.Destination.clone(), placementMode: main.PlacementMode,
|
destination: main.Destination.clone(), placementMode: main.PlacementMode,
|
||||||
},
|
},
|
||||||
definition: options.Definition, sourceVersionMode: options.SourceVersionMode,
|
definition: options.Definition, sourceVersionMode: options.SourceVersionMode,
|
||||||
importDefaults: options.ImportDefaults,
|
importDefaults: options.ImportDefaults,
|
||||||
overlay: options.Overlay,
|
overlay: options.Overlay,
|
||||||
overlayFilesystem: options.OverlayFilesystem,
|
overlayFilesystem: options.OverlayFilesystem,
|
||||||
|
overlayFallback: options.OverlayFallback,
|
||||||
overrides: append([]Override(nil), options.Overrides...),
|
overrides: append([]Override(nil), options.Overrides...),
|
||||||
underlays: make([]loadGroup, len(options.Underlays)),
|
underlays: make([]loadGroup, len(options.Underlays)),
|
||||||
}
|
}
|
||||||
@@ -51,7 +59,7 @@ func LoadLayered[T any](main Input, secrets SecretBinding, supplied ...LayeredOp
|
|||||||
request.underlays[index].inputs = make([]loadInput, len(underlay.Inputs))
|
request.underlays[index].inputs = make([]loadInput, len(underlay.Inputs))
|
||||||
for inputIndex, input := range underlay.Inputs {
|
for inputIndex, input := range underlay.Inputs {
|
||||||
request.underlays[index].inputs[inputIndex] = loadInput{
|
request.underlays[index].inputs[inputIndex] = loadInput{
|
||||||
source: input.Source, imports: input.Imports, filesystem: input.Filesystem,
|
source: input.Source, imports: input.Imports, filesystem: input.Filesystem, mainRelative: input.MainRelative,
|
||||||
destination: input.Destination.clone(), placementMode: input.PlacementMode,
|
destination: input.Destination.clone(), placementMode: input.PlacementMode,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ type loadRequest[T any] struct {
|
|||||||
effectiveVersion *VersionRule
|
effectiveVersion *VersionRule
|
||||||
overlay string
|
overlay string
|
||||||
overlayFilesystem *FilesystemContext
|
overlayFilesystem *FilesystemContext
|
||||||
|
overlayFallback *FilesystemContext
|
||||||
overrides []Override
|
overrides []Override
|
||||||
ordinary bool
|
ordinary bool
|
||||||
}
|
}
|
||||||
@@ -31,6 +32,7 @@ type loadInput struct {
|
|||||||
source Source
|
source Source
|
||||||
imports bool
|
imports bool
|
||||||
filesystem *FilesystemContext
|
filesystem *FilesystemContext
|
||||||
|
mainRelative *MainRelativeContext
|
||||||
destination Path
|
destination Path
|
||||||
placementMode PlacementMode
|
placementMode PlacementMode
|
||||||
}
|
}
|
||||||
@@ -57,6 +59,9 @@ func loadGroupedWithSecretLookupLimit[T any](request loadRequest[T], secrets Sec
|
|||||||
request.importDefaults = defaults
|
request.importDefaults = defaults
|
||||||
request.sequencePolicy = policy
|
request.sequencePolicy = policy
|
||||||
request.definition.sequenceIdentityRegistrations = nil
|
request.definition.sequenceIdentityRegistrations = nil
|
||||||
|
if request.main.mainRelative != nil && request.main.filesystem == nil {
|
||||||
|
diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "main input cannot use main-relative context"))
|
||||||
|
}
|
||||||
main, mainDiagnostics := normalizeInput(request.main, "")
|
main, mainDiagnostics := normalizeInput(request.main, "")
|
||||||
request.main = main
|
request.main = main
|
||||||
diagnostics = append(diagnostics, mainDiagnostics...)
|
diagnostics = append(diagnostics, mainDiagnostics...)
|
||||||
@@ -64,6 +69,13 @@ func loadGroupedWithSecretLookupLimit[T any](request loadRequest[T], secrets Sec
|
|||||||
for groupIndex := range request.underlays {
|
for groupIndex := range request.underlays {
|
||||||
for inputIndex := range request.underlays[groupIndex].inputs {
|
for inputIndex := range request.underlays[groupIndex].inputs {
|
||||||
input := &request.underlays[groupIndex].inputs[inputIndex]
|
input := &request.underlays[groupIndex].inputs[inputIndex]
|
||||||
|
if input.mainRelative != nil && input.filesystem == nil {
|
||||||
|
if input.source.kind == sourceFile || input.source.kind == sourceFilesystem || input.source.kind == sourceSearch {
|
||||||
|
diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "file and search underlays cannot use main-relative context"))
|
||||||
|
} else if input.mainRelative.Fallback != nil {
|
||||||
|
diagnostics = append(diagnostics, filesystemOptionDiagnostics(Source{kind: sourceBytes}, input.mainRelative.Fallback)...)
|
||||||
|
}
|
||||||
|
}
|
||||||
normalized, inputDiagnostics := normalizeInput(*input, "")
|
normalized, inputDiagnostics := normalizeInput(*input, "")
|
||||||
*input = normalized
|
*input = normalized
|
||||||
diagnostics = append(diagnostics, inputDiagnostics...)
|
diagnostics = append(diagnostics, inputDiagnostics...)
|
||||||
@@ -71,10 +83,10 @@ func loadGroupedWithSecretLookupLimit[T any](request loadRequest[T], secrets Sec
|
|||||||
}
|
}
|
||||||
diagnostics = append(diagnostics, overlayOptionDiagnostics(request.overlay)...)
|
diagnostics = append(diagnostics, overlayOptionDiagnostics(request.overlay)...)
|
||||||
if activeOverlay(request.overlay) {
|
if activeOverlay(request.overlay) {
|
||||||
if request.overlayFilesystem == nil {
|
if request.overlayFilesystem != nil {
|
||||||
diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "selected overlay requires explicit filesystem context"))
|
|
||||||
} else {
|
|
||||||
diagnostics = append(diagnostics, filesystemOptionDiagnostics(Source{kind: sourceBytes}, request.overlayFilesystem)...)
|
diagnostics = append(diagnostics, filesystemOptionDiagnostics(Source{kind: sourceBytes}, request.overlayFilesystem)...)
|
||||||
|
} else if request.overlayFallback != nil {
|
||||||
|
diagnostics = append(diagnostics, filesystemOptionDiagnostics(Source{kind: sourceBytes}, request.overlayFallback)...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(diagnostics) != 0 {
|
if len(diagnostics) != 0 {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ func normalizeInput(input loadInput, ordinaryOverlay string) (loadInput, []Diagn
|
|||||||
if input.source.kind == sourceNone && activeOverlay(ordinaryOverlay) && input.filesystem == nil {
|
if input.source.kind == sourceNone && activeOverlay(ordinaryOverlay) && input.filesystem == nil {
|
||||||
diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "NoSource overlays require explicit filesystem context"))
|
diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "NoSource overlays require explicit filesystem context"))
|
||||||
}
|
}
|
||||||
if input.filesystem == nil && (input.source.kind == sourceBytes || input.source.kind == sourceReader || input.source.kind == sourceMapping) && (input.imports || activeOverlay(ordinaryOverlay)) {
|
if input.filesystem == nil && input.mainRelative == nil && (input.source.kind == sourceBytes || input.source.kind == sourceReader || input.source.kind == sourceMapping) && (input.imports || activeOverlay(ordinaryOverlay)) {
|
||||||
diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "synthetic-source imports or overlays require filesystem context"))
|
diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "synthetic-source imports or overlays require filesystem context"))
|
||||||
}
|
}
|
||||||
return input, canonicalDiagnostics(diagnostics)
|
return input, canonicalDiagnostics(diagnostics)
|
||||||
|
|||||||
16
layered.go
16
layered.go
@@ -19,8 +19,19 @@ type Input struct {
|
|||||||
PlacementMode PlacementMode
|
PlacementMode PlacementMode
|
||||||
Imports bool
|
Imports bool
|
||||||
Filesystem *FilesystemContext
|
Filesystem *FilesystemContext
|
||||||
|
MainRelative *MainRelativeContext
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MainRelativeContext selects the main input's resolved context for an underlay.
|
||||||
|
// Fallback is used when the main has no usable context; RequireBase requires one.
|
||||||
|
type MainRelativeContext struct {
|
||||||
|
Fallback *FilesystemContext
|
||||||
|
RequireBase bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// MainRelative returns a fresh default main-relative binding description.
|
||||||
|
func MainRelative() *MainRelativeContext { return &MainRelativeContext{} }
|
||||||
|
|
||||||
// Underlay contains inputs that must compose strictly within one tier. Tiers
|
// Underlay contains inputs that must compose strictly within one tier. Tiers
|
||||||
// appear in lowest-to-highest precedence order; an empty tier is a no-op.
|
// appear in lowest-to-highest precedence order; an empty tier is a no-op.
|
||||||
type Underlay struct{ Inputs []Input }
|
type Underlay struct{ Inputs []Input }
|
||||||
@@ -42,6 +53,7 @@ type LayeredOptions[T any] struct {
|
|||||||
Underlays []Underlay
|
Underlays []Underlay
|
||||||
Overlay string
|
Overlay string
|
||||||
OverlayFilesystem *FilesystemContext
|
OverlayFilesystem *FilesystemContext
|
||||||
|
OverlayFallback *FilesystemContext
|
||||||
Overrides []Override
|
Overrides []Override
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,6 +80,9 @@ func toEngineInput(input Input) engine.Input {
|
|||||||
Filesystem: toEngineFilesystemContext(input.Filesystem), Destination: toEnginePath(input.Destination),
|
Filesystem: toEngineFilesystemContext(input.Filesystem), Destination: toEnginePath(input.Destination),
|
||||||
PlacementMode: engine.PlacementMode(input.PlacementMode),
|
PlacementMode: engine.PlacementMode(input.PlacementMode),
|
||||||
}
|
}
|
||||||
|
if input.MainRelative != nil {
|
||||||
|
result.MainRelative = &engine.MainRelativeContext{Fallback: toEngineFilesystemContext(input.MainRelative.Fallback), RequireBase: input.MainRelative.RequireBase}
|
||||||
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,6 +92,7 @@ func toEngineLayeredOptions[T any](options LayeredOptions[T]) engine.LayeredOpti
|
|||||||
SourceVersionMode: engine.SourceVersionModeFromFacade(uint8(options.SourceVersionMode)),
|
SourceVersionMode: engine.SourceVersionModeFromFacade(uint8(options.SourceVersionMode)),
|
||||||
Overlay: options.Overlay,
|
Overlay: options.Overlay,
|
||||||
OverlayFilesystem: toEngineFilesystemContext(options.OverlayFilesystem),
|
OverlayFilesystem: toEngineFilesystemContext(options.OverlayFilesystem),
|
||||||
|
OverlayFallback: toEngineFilesystemContext(options.OverlayFallback),
|
||||||
ImportDefaults: toEngineImportDefaults(options.ImportDefaults),
|
ImportDefaults: toEngineImportDefaults(options.ImportDefaults),
|
||||||
}
|
}
|
||||||
if options.Underlays != nil {
|
if options.Underlays != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user