package configkit_test import ( "errors" "os" "path/filepath" "strings" "testing" configkit "gitea.maximumdirect.net/eric/ConfigKit" ) type sourceVersionModeReader struct { *strings.Reader reads int } func (reader *sourceVersionModeReader) Read(data []byte) (int, error) { reader.reads++ return reader.Reader.Read(data) } func TestFacadeSourceVersionModesAcrossOrdinaryAndLayeredLoads(t *testing.T) { t.Parallel() var checked []configkit.Version definition := configkit.Definition[map[string]any]{Version: &configkit.VersionRule{ Path: configkit.Keys("meta", "version"), Check: func(version configkit.Version, _ *configkit.Validation) error { checked = append(checked, version) return nil }, }} ordinary, err := configkit.Load(configkit.Bytes("ordinary", []byte("meta: {version: ordinary-v}\n")), configkit.Options[map[string]any]{ Definition: definition, SourceVersionMode: configkit.SourceVersionModeIfPresent, }) if err != nil || ordinary == nil { t.Fatalf("ordinary enabled load = %#v, %v", ordinary, err) } if got := ordinary.Config["meta"].(map[string]any)["version"]; got != "ordinary-v" { t.Fatalf("main declaration was not retained: %#v", ordinary.Config) } directory := t.TempDir() mainPath := filepath.Join(directory, "main.yml") if err := os.WriteFile(mainPath, []byte("meta: {version: main-v}\n"), 0o600); err != nil { t.Fatal(err) } layered, err := configkit.LoadLayered(configkit.Input{Source: configkit.File(mainPath)}, configkit.LayeredOptions[map[string]any]{ Definition: definition, SourceVersionMode: configkit.SourceVersionModeRequired, Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{ Source: configkit.Mapping("underlay", map[string]any{ "meta": map[string]any{"version": "underlay-v"}, "fallback": "present", }), Destination: configkit.Keys("defaults"), }}}}, }) if err != nil || layered == nil { t.Fatalf("layered enabled load = %#v, %v", layered, err) } defaults, ok := layered.Config["defaults"].(map[string]any) if !ok || defaults["fallback"] != "present" { t.Fatalf("layered underlay payload = %#v", layered.Config["defaults"]) } if meta, ok := defaults["meta"].(map[string]any); !ok || len(meta) != 0 { t.Fatalf("accepted underlay declaration was not consumed: %#v", defaults["meta"]) } if len(checked) != 3 { t.Fatalf("checker calls across separate mode uses = %#v", checked) } for _, want := range []string{"ordinary-v", "main-v", "underlay-v"} { found := false for _, version := range checked { found = found || version.Present && version.Text == want } if !found { t.Errorf("checked versions %#v do not include %q", checked, want) } } } func TestFacadeSourceVersionModeZeroPreservesDefaultBehavior(t *testing.T) { t.Parallel() checks := 0 result, err := configkit.Load(configkit.Bytes("default", []byte("version: v1\n")), configkit.Options[map[string]any]{ Definition: configkit.Definition[map[string]any]{Version: &configkit.VersionRule{ Path: configkit.Keys("version"), Check: func(version configkit.Version, _ *configkit.Validation) error { checks++ if version != (configkit.Version{Present: true, Text: "v1"}) { t.Errorf("default version = %#v", version) } return nil }, }}, }) if err != nil || result == nil || result.Config["version"] != "v1" || checks != 1 { t.Fatalf("zero-mode load = %#v, %v; checks = %d", result, err, checks) } } func TestFacadeSourceVersionModeValidationPrecedesSourceReads(t *testing.T) { t.Parallel() validDefinition := configkit.Definition[map[string]any]{Version: &configkit.VersionRule{ Path: configkit.Keys("version"), Check: func(configkit.Version, *configkit.Validation) error { return nil }, }} for _, test := range []struct { name string mode configkit.SourceVersionMode def configkit.Definition[map[string]any] layered bool }{ {name: "unknown ordinary mode", mode: configkit.SourceVersionMode(255), def: validDefinition}, {name: "ordinary mode requires checker", mode: configkit.SourceVersionModeIfPresent}, {name: "unknown layered mode", mode: configkit.SourceVersionMode(255), def: validDefinition, layered: true}, {name: "layered mode requires checker", mode: configkit.SourceVersionModeRequired, layered: true}, } { t.Run(test.name, func(t *testing.T) { reader := &sourceVersionModeReader{Reader: strings.NewReader("version: v1\n")} source := configkit.Reader("version.yml", reader) var err error if test.layered { _, err = configkit.LoadLayered(configkit.Input{Source: source}, configkit.LayeredOptions[map[string]any]{ Definition: test.def, SourceVersionMode: test.mode, }) } else { _, err = configkit.Load(source, configkit.Options[map[string]any]{ Definition: test.def, SourceVersionMode: test.mode, }) } var loadError *configkit.LoadError if !errors.As(err, &loadError) || len(loadError.Diagnostics()) != 1 || loadError.Diagnostics()[0].Code != configkit.CodeOptionsInvalid { t.Fatalf("invalid mode error = %#v", err) } if reader.reads != 0 { t.Fatalf("invalid options read source %d times", reader.reads) } }) } } func TestFacadeSourceVersionDefinitionCanBeReusedWithDifferentModes(t *testing.T) { t.Parallel() checks := 0 definition := configkit.Definition[map[string]any]{Version: &configkit.VersionRule{ Path: configkit.Keys("version"), Check: func(version configkit.Version, _ *configkit.Validation) error { checks++ if version.Present { t.Errorf("missing declaration reached checker as %#v", version) } return nil }, }} source := configkit.Bytes("reused", []byte("value: retained\n")) ifPresent, err := configkit.Load(source, configkit.Options[map[string]any]{ Definition: definition, SourceVersionMode: configkit.SourceVersionModeIfPresent, }) if err != nil || ifPresent == nil || checks != 1 { t.Fatalf("if-present load = %#v, %v; checks = %d", ifPresent, err, checks) } required, err := configkit.Load(source, configkit.Options[map[string]any]{ Definition: definition, SourceVersionMode: configkit.SourceVersionModeRequired, }) var loadError *configkit.LoadError if required != nil || !errors.As(err, &loadError) || len(loadError.Diagnostics()) != 1 || loadError.Diagnostics()[0].Code != configkit.CodeVersionError { t.Fatalf("required load = %#v, %v", required, err) } if checks != 1 { t.Fatalf("required absence reached checker: calls = %d", checks) } }