diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index c1cb48f..355f178 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -196,6 +196,8 @@ coverage, including existing namespace and repeated-source cases. ## Stage 6 — Exercise the combined workflow +**Status:** Complete. The executable consumer example and strict-peer regression pass. + Add a maintained executable example in `examples/consumers` for a single layered load using `SearchConfig`, a mapping companion with optional named files and recursive directory imports, an independently located embedded diff --git a/examples/consumers/layered.go b/examples/consumers/layered.go index 230ce3f..0d9270a 100644 --- a/examples/consumers/layered.go +++ b/examples/consumers/layered.go @@ -9,7 +9,7 @@ import ( configkit "go.maximumdirect.net/configkit" ) -//go:embed testdata/native-source testdata/layered-bundle testdata/collection-bundle testdata/ordinary-imports testdata/source-version +//go:embed testdata/native-source testdata/layered-bundle testdata/collection-bundle testdata/ordinary-imports testdata/source-version testdata/main-relative-bundle var bundledFiles embed.FS // BundledConfig is a small configuration loaded directly from an embedded file. diff --git a/examples/consumers/main_relative.go b/examples/consumers/main_relative.go new file mode 100644 index 0000000..a0b783a --- /dev/null +++ b/examples/consumers/main_relative.go @@ -0,0 +1,47 @@ +package consumers + +import ( + "io/fs" + "path/filepath" + + configkit "go.maximumdirect.net/configkit" +) + +// CompanionConfig demonstrates independently bundled and operator prompt tiers. +type CompanionConfig struct { + Mode string `yaml:"mode"` + Prompts []CompanionPrompt `yaml:"prompts"` +} + +type CompanionPrompt struct { + ID string `yaml:"id"` + Text string `yaml:"text"` + Note string `yaml:"note"` +} + +// LoadCompanionConfig discovers a main file and binds operator imports and the +// selected overlay to its directory while retaining the embedded catalog tier. +func LoadCompanionConfig(mainPath, overlay string) (*configkit.Result[CompanionConfig], error) { + files, err := fs.Sub(bundledFiles, "testdata/main-relative-bundle") + if err != nil { + return nil, err + } + bundle := configkit.NewFilesystem("bundled catalog", files) + absolute, err := filepath.Abs(mainPath) + if err != nil { + return nil, err + } + declarations := map[string]any{"config": map[string]any{"imports": []any{ + map[string]any{"path": "prompts.yml", "destination": []any{"prompts"}, "placement": "sequence-item"}, + map[string]any{"dir": "operator", "recursive": true, "pattern": []any{"*.yml", "*.yaml"}, "destination": []any{"prompts"}, "placement": "sequence-item"}, + }}} + main := configkit.SearchConfig("", configkit.Searches(configkit.SearchPaths(absolute)), configkit.SearchOptions{Required: true}) + return configkit.LoadLayered(configkit.Input{Source: main}, configkit.LayeredOptions[CompanionConfig]{ + Underlays: []configkit.Underlay{ + {Inputs: []configkit.Input{{Source: bundle.File("catalog.yml")}}}, + {Inputs: []configkit.Input{{Source: configkit.Mapping("operator imports", declarations), Imports: true, MainRelative: configkit.MainRelative()}}}, + }, + Overlay: overlay, + Definition: configkit.Definition[CompanionConfig]{SequenceIdentities: []configkit.SequenceIdentity{configkit.SequenceIdentityAt(configkit.Keys("prompts"), "id")}}, + }) +} diff --git a/examples/consumers/main_relative_test.go b/examples/consumers/main_relative_test.go new file mode 100644 index 0000000..12e0cc5 --- /dev/null +++ b/examples/consumers/main_relative_test.go @@ -0,0 +1,46 @@ +package consumers_test + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + configkit "go.maximumdirect.net/configkit" + "go.maximumdirect.net/configkit/examples/consumers" +) + +func Example_mainRelativeCompanions() { + result, err := consumers.LoadCompanionConfig(fixture("main-relative", "main.yml"), "live") + if err != nil { + fmt.Println(err) + return + } + fmt.Println(result.Config.Mode, result.Config.Prompts) + // Output: overlay [{shared operator replacement } {bundled bundled text } {local local text transitive detail}] +} + +func TestMainRelativeStrictPeers(t *testing.T) { + root := t.TempDir() + for name, contents := range map[string]string{ + "main.yml": "mode: main\n", "prompts.yml": "id: duplicate\ntext: first\n", "operator/second.yml": "id: duplicate\ntext: second\n", + } { + file := filepath.Join(root, name) + if err := os.MkdirAll(filepath.Dir(file), 0700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(file, []byte(contents), 0600); err != nil { + t.Fatal(err) + } + } + _, err := consumers.LoadCompanionConfig(filepath.Join(root, "main.yml"), "") + if err == nil { + t.Fatal("duplicate peer identities succeeded") + } + for _, d := range err.(*configkit.LoadError).Diagnostics() { + if d.Code == configkit.CodeComposeConflict { + return + } + } + t.Fatalf("diagnostics = %#v", err.(*configkit.LoadError).Diagnostics()) +} diff --git a/examples/consumers/testdata/main-relative-bundle/catalog.yml b/examples/consumers/testdata/main-relative-bundle/catalog.yml new file mode 100644 index 0000000..defa601 --- /dev/null +++ b/examples/consumers/testdata/main-relative-bundle/catalog.yml @@ -0,0 +1,7 @@ +mode: embedded +prompts: + - id: shared + text: embedded text + note: replaced + - id: bundled + text: bundled text diff --git a/examples/consumers/testdata/main-relative/main.yml b/examples/consumers/testdata/main-relative/main.yml new file mode 100644 index 0000000..e110ae7 --- /dev/null +++ b/examples/consumers/testdata/main-relative/main.yml @@ -0,0 +1 @@ +mode: main diff --git a/examples/consumers/testdata/main-relative/operator/nested/one.yaml b/examples/consumers/testdata/main-relative/operator/nested/one.yaml new file mode 100644 index 0000000..5066539 --- /dev/null +++ b/examples/consumers/testdata/main-relative/operator/nested/one.yaml @@ -0,0 +1,5 @@ +config: + imports: + - path: ../../shared/extra.yml +id: local +text: local text diff --git a/examples/consumers/testdata/main-relative/overlays/live.yml b/examples/consumers/testdata/main-relative/overlays/live.yml new file mode 100644 index 0000000..6dd5467 --- /dev/null +++ b/examples/consumers/testdata/main-relative/overlays/live.yml @@ -0,0 +1 @@ +mode: overlay diff --git a/examples/consumers/testdata/main-relative/prompts.yml b/examples/consumers/testdata/main-relative/prompts.yml new file mode 100644 index 0000000..799574c --- /dev/null +++ b/examples/consumers/testdata/main-relative/prompts.yml @@ -0,0 +1,2 @@ +id: shared +text: operator replacement diff --git a/examples/consumers/testdata/main-relative/shared/extra.yml b/examples/consumers/testdata/main-relative/shared/extra.yml new file mode 100644 index 0000000..659f14b --- /dev/null +++ b/examples/consumers/testdata/main-relative/shared/extra.yml @@ -0,0 +1 @@ +note: transitive detail