Add public facade value wrappers
This commit is contained in:
@@ -323,6 +323,26 @@ Acceptance: candidate wrapper tests and engine tests pass. Run
|
||||
`go test ./internal/facade ./internal/engine` and the everyday check. Lifecycle
|
||||
entry points are added in Stage 6; do not add nonfunctional stubs here.
|
||||
|
||||
### Completion
|
||||
|
||||
Status: complete (2026-09-12).
|
||||
|
||||
Created the temporary `internal/facade` package (named `configkit`) with
|
||||
root-owned `Path`, `Source`, and `Secret` wrappers; root-owned scalar and
|
||||
provenance records; explicit private conversions; `LoadError`; and
|
||||
`WriteDiagnostics`. Public fields and signatures contain only facade types.
|
||||
`Path` carries its engine value privately without text round-tripping, `Source`
|
||||
retains the engine's source value so copied reader sources preserve their shared
|
||||
claim, and the facade builds the exact-type secret binding locally. No lifecycle
|
||||
entry point or placeholder was added.
|
||||
|
||||
Candidate tests cover semantic path distinctions, source byte snapshotting and
|
||||
copied-reader ownership, external-package type ownership and interfaces, and
|
||||
safe secret formatting/JSON. Linux validation with Go 1.26.7 passed
|
||||
`go test ./internal/facade ./internal/engine` and `./scripts/check.sh`. There
|
||||
were no deviations. The next handoff separates engine publication from its
|
||||
generic result before lifecycle delegation begins.
|
||||
|
||||
## Stage 5 — Separate engine publication and inspection
|
||||
|
||||
Status: not started.
|
||||
|
||||
91
internal/facade/conversion.go
Normal file
91
internal/facade/conversion.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package configkit
|
||||
|
||||
import "gitea.maximumdirect.net/eric/ConfigKit/internal/engine"
|
||||
|
||||
func fromEngineLocation(value engine.Location) Location {
|
||||
return Location{SourceID: value.SourceID, Line: value.Line, Column: value.Column}
|
||||
}
|
||||
func toEngineLocation(value Location) engine.Location {
|
||||
return engine.Location{SourceID: value.SourceID, Line: value.Line, Column: value.Column}
|
||||
}
|
||||
func fromEngineLocationPointer(value *engine.Location) *Location {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
result := fromEngineLocation(*value)
|
||||
return &result
|
||||
}
|
||||
func toEngineLocationPointer(value *Location) *engine.Location {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
result := toEngineLocation(*value)
|
||||
return &result
|
||||
}
|
||||
func fromEngineLocations(values []engine.Location) []Location {
|
||||
if values == nil {
|
||||
return nil
|
||||
}
|
||||
result := make([]Location, len(values))
|
||||
for i := range values {
|
||||
result[i] = fromEngineLocation(values[i])
|
||||
}
|
||||
return result
|
||||
}
|
||||
func toEngineLocations(values []Location) []engine.Location {
|
||||
if values == nil {
|
||||
return nil
|
||||
}
|
||||
result := make([]engine.Location, len(values))
|
||||
for i := range values {
|
||||
result[i] = toEngineLocation(values[i])
|
||||
}
|
||||
return result
|
||||
}
|
||||
func fromEngineSourceInfo(value engine.SourceInfo) SourceInfo {
|
||||
return SourceInfo{ID: value.ID, Label: value.Label, Path: value.Path, Layer: Layer(value.Layer), ImportedFrom: fromEngineLocationPointer(value.ImportedFrom)}
|
||||
}
|
||||
func toEngineSourceInfo(value SourceInfo) engine.SourceInfo {
|
||||
return engine.SourceInfo{ID: value.ID, Label: value.Label, Path: value.Path, Layer: engine.Layer(value.Layer), ImportedFrom: toEngineLocationPointer(value.ImportedFrom)}
|
||||
}
|
||||
func fromEngineOrigin(value engine.Origin) Origin {
|
||||
return Origin{Layer: Layer(value.Layer), Locations: fromEngineLocations(value.Locations), Operation: ChangeKind(value.Operation)}
|
||||
}
|
||||
func toEngineOrigin(value Origin) engine.Origin {
|
||||
return engine.Origin{Layer: engine.Layer(value.Layer), Locations: toEngineLocations(value.Locations), Operation: engine.ChangeKind(value.Operation)}
|
||||
}
|
||||
func fromEngineDiagnostic(value engine.Diagnostic) Diagnostic {
|
||||
return Diagnostic{Code: Code(value.Code), Severity: Severity(value.Severity), Path: fromEnginePath(value.Path), Message: value.Message, Primary: fromEngineLocationPointer(value.Primary), Related: fromEngineLocations(value.Related)}
|
||||
}
|
||||
func toEngineDiagnostic(value Diagnostic) engine.Diagnostic {
|
||||
return engine.Diagnostic{Code: engine.Code(value.Code), Severity: engine.Severity(value.Severity), Path: toEnginePath(value.Path), Message: value.Message, Primary: toEngineLocationPointer(value.Primary), Related: toEngineLocations(value.Related)}
|
||||
}
|
||||
func toEngineDiagnostics(values []Diagnostic) []engine.Diagnostic {
|
||||
if values == nil {
|
||||
return nil
|
||||
}
|
||||
result := make([]engine.Diagnostic, len(values))
|
||||
for i := range values {
|
||||
result[i] = toEngineDiagnostic(values[i])
|
||||
}
|
||||
return result
|
||||
}
|
||||
func fromEngineFieldInfo(value engine.FieldInfo) FieldInfo {
|
||||
return FieldInfo{Explicit: value.Explicit, Null: value.Null, Origin: fromEngineOrigin(value.Origin)}
|
||||
}
|
||||
func fromEngineChange(value engine.Change) Change {
|
||||
result := Change{Path: fromEnginePath(value.Path), Origin: fromEngineOrigin(value.Origin), Reasons: append([]string(nil), value.Reasons...)}
|
||||
if value.Previous != nil {
|
||||
result.Previous = make([]Origin, len(value.Previous))
|
||||
for i := range value.Previous {
|
||||
result.Previous[i] = fromEngineOrigin(value.Previous[i])
|
||||
}
|
||||
}
|
||||
if value.Dependencies != nil {
|
||||
result.Dependencies = make([]Path, len(value.Dependencies))
|
||||
for i := range value.Dependencies {
|
||||
result.Dependencies[i] = fromEnginePath(value.Dependencies[i])
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
44
internal/facade/diagnostic.go
Normal file
44
internal/facade/diagnostic.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package configkit
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/engine"
|
||||
)
|
||||
|
||||
// LoadError reports safe structured diagnostics for a failed configuration load.
|
||||
type LoadError struct{ value *engine.LoadError }
|
||||
|
||||
func (e *LoadError) Error() string {
|
||||
if e == nil || e.value == nil {
|
||||
return "configkit: configuration load failed"
|
||||
}
|
||||
return e.value.Error()
|
||||
}
|
||||
func (e *LoadError) Diagnostics() []Diagnostic {
|
||||
if e == nil || e.value == nil {
|
||||
return nil
|
||||
}
|
||||
values := e.value.Diagnostics()
|
||||
result := make([]Diagnostic, len(values))
|
||||
for i := range values {
|
||||
result[i] = fromEngineDiagnostic(values[i])
|
||||
}
|
||||
return result
|
||||
}
|
||||
func (e *LoadError) Sources() []SourceInfo {
|
||||
if e == nil || e.value == nil {
|
||||
return nil
|
||||
}
|
||||
values := e.value.Sources()
|
||||
result := make([]SourceInfo, len(values))
|
||||
for i := range values {
|
||||
result[i] = fromEngineSourceInfo(values[i])
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// WriteDiagnostics writes diagnostics in canonical order.
|
||||
func WriteDiagnostics(output io.Writer, diagnostics []Diagnostic) error {
|
||||
return engine.WriteDiagnostics(output, toEngineDiagnostics(diagnostics))
|
||||
}
|
||||
43
internal/facade/facade_external_test.go
Normal file
43
internal/facade/facade_external_test.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package configkit_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit/internal/facade"
|
||||
)
|
||||
|
||||
var (
|
||||
_ error = (*configkit.LoadError)(nil)
|
||||
_ fmt.Formatter = configkit.Secret{}
|
||||
_ fmt.Stringer = configkit.Path{}
|
||||
_ json.Marshaler = configkit.Secret{}
|
||||
)
|
||||
|
||||
func TestFacadeValuesRemainRootOwned(t *testing.T) {
|
||||
t.Parallel()
|
||||
const packagePath = "gitea.maximumdirect.net/eric/ConfigKit/internal/facade"
|
||||
for _, candidate := range []reflect.Type{
|
||||
reflect.TypeFor[configkit.Path](), reflect.TypeFor[configkit.Source](),
|
||||
reflect.TypeFor[configkit.Secret](), reflect.TypeFor[configkit.Diagnostic](),
|
||||
reflect.TypeFor[configkit.LoadError](),
|
||||
} {
|
||||
if candidate.PkgPath() != packagePath {
|
||||
t.Fatalf("type %s belongs to %q", candidate, candidate.PkgPath())
|
||||
}
|
||||
}
|
||||
secret := configkit.SecretEnv("SAFE_NAME")
|
||||
formatted := fmt.Sprintf("%#v", secret)
|
||||
encoded, err := json.Marshal(secret)
|
||||
if formatted == "" || err != nil || string(encoded) != `"SAFE_NAME"` {
|
||||
t.Fatalf("secret rendering = %q, %s, %v", formatted, encoded, err)
|
||||
}
|
||||
if got := configkit.Keys("a/b~").Index(0).String(); got != "/a~1b~0/0" {
|
||||
t.Fatalf("path = %q", got)
|
||||
}
|
||||
if got := (&configkit.LoadError{}).Error(); got != "configkit: configuration load failed" {
|
||||
t.Fatalf("zero LoadError = %q", got)
|
||||
}
|
||||
}
|
||||
52
internal/facade/facade_test.go
Normal file
52
internal/facade/facade_test.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package configkit
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/engine"
|
||||
)
|
||||
|
||||
func TestWrappedSourcesPreserveEngineOwnership(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type config struct {
|
||||
Value string `yaml:"value"`
|
||||
}
|
||||
data := []byte("value: retained\n")
|
||||
source := Bytes("snapshot.yml", data)
|
||||
data[7] = 'X'
|
||||
result, err := engine.Load[config](source.toEngine(), engine.SecretBinding{})
|
||||
if err != nil || result.Config.Value != "retained" {
|
||||
t.Fatalf("snapshotted source = %#v, %v", result, err)
|
||||
}
|
||||
|
||||
readerSource := Reader("reader.yml", strings.NewReader("value: once\n"))
|
||||
first := readerSource
|
||||
if _, err := engine.Load[config](first.toEngine(), engine.SecretBinding{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = engine.Load[config](readerSource.toEngine(), engine.SecretBinding{})
|
||||
var loadError *engine.LoadError
|
||||
if !errors.As(err, &loadError) || len(loadError.Diagnostics()) != 1 || loadError.Diagnostics()[0].Code != engine.CodeSourceReaderUsed {
|
||||
t.Fatalf("copied reader source error = %#v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrappedPathsRetainEngineSegmentIntent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
parsed, err := ParsePath("/0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
keyed, indexed := Keys("0"), Path{}.Index(0)
|
||||
if parsed.String() != "/0" || keyed.String() != "/0" || indexed.String() != "/0" {
|
||||
t.Fatalf("path spellings = %q, %q, %q", parsed, keyed, indexed)
|
||||
}
|
||||
if reflect.DeepEqual(parsed.value, keyed.value) || reflect.DeepEqual(parsed.value, indexed.value) || reflect.DeepEqual(keyed.value, indexed.value) {
|
||||
t.Fatal("path wrappers collapsed parsed, mapping-key, and index segments")
|
||||
}
|
||||
}
|
||||
33
internal/facade/path.go
Normal file
33
internal/facade/path.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package configkit
|
||||
|
||||
import (
|
||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/engine"
|
||||
)
|
||||
|
||||
// Path identifies a configuration value using YAML field names. Its textual form
|
||||
// is a JSON Pointer. Paths are immutable; the zero value is the root path.
|
||||
type Path struct {
|
||||
value engine.Path
|
||||
}
|
||||
|
||||
// Keys constructs a path of mapping keys. Keys with no arguments returns the root path.
|
||||
func Keys(keys ...string) Path { return Path{value: engine.Keys(keys...)} }
|
||||
|
||||
// ParsePath parses a JSON Pointer while preserving context-dependent numeric tokens.
|
||||
func ParsePath(pointer string) (Path, error) {
|
||||
value, err := engine.ParsePath(pointer)
|
||||
return Path{value: value}, err
|
||||
}
|
||||
|
||||
// Key returns a new path with key appended as a mapping-key segment.
|
||||
func (p Path) Key(key string) Path { return Path{value: p.value.Key(key)} }
|
||||
|
||||
// Index returns a new path with index appended as a sequence-index segment.
|
||||
func (p Path) Index(index int) Path { return Path{value: p.value.Index(index)} }
|
||||
|
||||
// String returns the path's canonical JSON Pointer spelling.
|
||||
func (p Path) String() string { return p.value.String() }
|
||||
|
||||
func (p Path) clone() Path { return Path{value: p.value} }
|
||||
func toEnginePath(path Path) engine.Path { return path.value }
|
||||
func fromEnginePath(path engine.Path) Path { return Path{value: path} }
|
||||
286
internal/facade/records.go
Normal file
286
internal/facade/records.go
Normal file
@@ -0,0 +1,286 @@
|
||||
package configkit
|
||||
|
||||
// Layer identifies the precedence layer responsible for a configuration value.
|
||||
type Layer string
|
||||
|
||||
const (
|
||||
LayerDefault Layer = "default"
|
||||
LayerBase Layer = "base"
|
||||
LayerProfile Layer = "profile"
|
||||
LayerCLI Layer = "cli"
|
||||
LayerResolution Layer = "resolution"
|
||||
)
|
||||
|
||||
// ChangeKind identifies a recorded operation. The zero value is
|
||||
// used for input and default origins.
|
||||
type ChangeKind string
|
||||
|
||||
const (
|
||||
// ChangeDecoding records the built-in transition to canonical typed values.
|
||||
// It is inspection metadata, not an operation accepted by Resolution.Record.
|
||||
ChangeDecoding ChangeKind = "decoding"
|
||||
ChangeDerivedDefault ChangeKind = "derived_default"
|
||||
ChangeNormalization ChangeKind = "normalization"
|
||||
)
|
||||
|
||||
// Severity classifies a diagnostic.
|
||||
type Severity string
|
||||
|
||||
const (
|
||||
SeverityError Severity = "error"
|
||||
SeverityWarning Severity = "warning"
|
||||
)
|
||||
|
||||
// Code is a stable, machine-readable diagnostic classification.
|
||||
type Code string
|
||||
|
||||
const (
|
||||
CodeSourceInvalid Code = "source.invalid"
|
||||
CodeSourceMissing Code = "source.missing"
|
||||
CodeSourcePermission Code = "source.permission"
|
||||
CodeSourceRead Code = "source.read"
|
||||
CodeSourceNotRegular Code = "source.not_regular"
|
||||
CodeSourceLimitExceeded Code = "source.limit_exceeded"
|
||||
CodeSourceReaderUsed Code = "source.reader_used"
|
||||
|
||||
CodeYAMLInvalid Code = "yaml.invalid"
|
||||
CodeYAMLEmpty Code = "yaml.empty"
|
||||
CodeYAMLMultipleDocuments Code = "yaml.multiple_documents"
|
||||
CodeYAMLRootType Code = "yaml.root_type"
|
||||
CodeYAMLDuplicateKey Code = "yaml.duplicate_key"
|
||||
CodeYAMLTag Code = "yaml.tag"
|
||||
CodeYAMLAnchor Code = "yaml.anchor"
|
||||
CodeYAMLAlias Code = "yaml.alias"
|
||||
CodeYAMLMergeKey Code = "yaml.merge_key"
|
||||
CodeYAMLScalar Code = "yaml.scalar"
|
||||
CodeYAMLDepthLimit Code = "yaml.depth_limit"
|
||||
CodeYAMLNodeLimit Code = "yaml.node_limit"
|
||||
|
||||
CodeSchemaRoot Code = "schema.root"
|
||||
CodeSchemaUnsupportedType Code = "schema.unsupported_type"
|
||||
CodeSchemaAmbiguousField Code = "schema.ambiguous_field"
|
||||
CodeSchemaTag Code = "schema.tag"
|
||||
CodeSchemaTextCodec Code = "schema.text_codec"
|
||||
CodeSchemaInline Code = "schema.inline"
|
||||
CodeSchemaCycle Code = "schema.cycle"
|
||||
CodeSchemaProjectionLimit Code = "schema.projection_limit"
|
||||
CodeSchemaCodec Code = "schema.codec"
|
||||
CodeSchemaNonFinite Code = "schema.non_finite"
|
||||
CodeSchemaDynamicType Code = "schema.dynamic_type"
|
||||
|
||||
CodeDecodeType Code = "decode.type"
|
||||
CodeDecodeNull Code = "decode.null"
|
||||
CodeDecodeRange Code = "decode.range"
|
||||
CodeDecodeDuration Code = "decode.duration"
|
||||
CodeDecodeText Code = "decode.text"
|
||||
CodeDecodeUnknownField Code = "decode.unknown_field"
|
||||
CodeDecodeArrayLength Code = "decode.array_length"
|
||||
|
||||
CodeOptionsInvalid Code = "options.invalid"
|
||||
CodeOptionsUnavailable Code = "options.unavailable"
|
||||
|
||||
CodeImportDisabled Code = "import.disabled"
|
||||
CodeImportInvalid Code = "import.invalid"
|
||||
CodeImportMissing Code = "import.missing"
|
||||
CodeImportPermission Code = "import.permission"
|
||||
CodeImportRead Code = "import.read"
|
||||
CodeImportOutsideRoot Code = "import.outside_root"
|
||||
CodeImportCycle Code = "import.cycle"
|
||||
CodeImportRepeated Code = "import.repeated"
|
||||
CodeImportLimitExceeded Code = "import.limit_exceeded"
|
||||
CodeImportVersion Code = "import.version"
|
||||
|
||||
CodeProfileInvalid Code = "profile.invalid"
|
||||
CodeProfileMissing Code = "profile.missing"
|
||||
CodeProfileOutsideRoot Code = "profile.outside_root"
|
||||
CodeProfileRepeated Code = "profile.repeated"
|
||||
CodeProfileDirective Code = "profile.directive"
|
||||
CodeProfileVersion Code = "profile.version"
|
||||
CodeProfileLimitExceeded Code = "profile.limit_exceeded"
|
||||
|
||||
CodeComposeInvalid Code = "compose.invalid"
|
||||
CodeComposeConflict Code = "compose.conflict"
|
||||
|
||||
CodeVersionType Code = "version.type"
|
||||
CodeVersionError Code = "version.error"
|
||||
CodeVersionWarning Code = "version.warning"
|
||||
CodeVersionHookFailed Code = "version.hook_failed"
|
||||
|
||||
CodeResolveError Code = "resolve.error"
|
||||
CodeResolveWarning Code = "resolve.warning"
|
||||
CodeResolveHookFailed Code = "resolve.hook_failed"
|
||||
CodeResolveInvalidPath Code = "resolve.invalid_path"
|
||||
CodeResolveInvalidKind Code = "resolve.invalid_kind"
|
||||
CodeResolveInvalidRecord Code = "resolve.invalid_record"
|
||||
CodeResolveInvalidTarget Code = "resolve.invalid_target"
|
||||
CodeResolveInvalidValue Code = "resolve.invalid_value"
|
||||
CodeResolveUnrecordedChange Code = "resolve.unrecorded_change"
|
||||
|
||||
CodeValidateError Code = "validate.error"
|
||||
CodeValidateWarning Code = "validate.warning"
|
||||
CodeValidateInvalidPath Code = "validate.invalid_path"
|
||||
CodeValidateMutation Code = "validate.mutation"
|
||||
|
||||
CodeSecretsInvalidPath Code = "secrets.invalid_path"
|
||||
CodeSecretsMissing Code = "secrets.missing"
|
||||
CodeSecretsPermission Code = "secrets.permission"
|
||||
CodeSecretsRead Code = "secrets.read"
|
||||
CodeSecretsNotRegular Code = "secrets.not_regular"
|
||||
CodeSecretsInvalidName Code = "secrets.invalid_name"
|
||||
CodeSecretsLimitExceeded Code = "secrets.limit_exceeded"
|
||||
CodeSecretsInvalidValue Code = "secrets.invalid_value"
|
||||
CodeSecretsApplyFailed Code = "secrets.apply_failed"
|
||||
|
||||
CodeSecretRequired Code = "secret.required"
|
||||
CodeSecretInvalidName Code = "secret.invalid_name"
|
||||
CodeSecretUnset Code = "secret.unset"
|
||||
CodeSecretEmpty Code = "secret.empty"
|
||||
|
||||
CodeOverrideInvalidPath Code = "override.invalid_path"
|
||||
CodeOverrideInvalidLabel Code = "override.invalid_label"
|
||||
CodeOverrideInvalidValue Code = "override.invalid_value"
|
||||
CodeOverrideDuplicate Code = "override.duplicate"
|
||||
CodeOverrideOverlap Code = "override.overlap"
|
||||
CodeOverrideDirective Code = "override.directive"
|
||||
CodeOverrideVersion Code = "override.version"
|
||||
CodeOverrideLimitExceeded Code = "override.limit_exceeded"
|
||||
|
||||
CodeExplainLimitExceeded Code = "explain.limit_exceeded"
|
||||
)
|
||||
|
||||
// Location identifies a position in a configuration source. Zero line and column
|
||||
// values mean that coordinates are unavailable or synthetic.
|
||||
type Location struct {
|
||||
SourceID string
|
||||
Line int
|
||||
Column int
|
||||
}
|
||||
|
||||
// SourceInfo describes a source discovered during a load. Path is a canonical
|
||||
// local path when one exists. ImportedFrom identifies the declaration that
|
||||
// included this source.
|
||||
type SourceInfo struct {
|
||||
ID string
|
||||
Label string
|
||||
Path string
|
||||
Layer Layer
|
||||
ImportedFrom *Location
|
||||
}
|
||||
|
||||
// Diagnostic is a safe structured problem report. Message and source labels must
|
||||
// never contain configuration values or other secrets.
|
||||
type Diagnostic struct {
|
||||
Code Code
|
||||
Severity Severity
|
||||
Path Path
|
||||
Message string
|
||||
Primary *Location
|
||||
Related []Location
|
||||
}
|
||||
|
||||
// Origin describes where an effective definition came from. Multiple locations
|
||||
// represent a composite origin; resolution origins are synthetic and identify their
|
||||
// operation instead of a file location.
|
||||
type Origin struct {
|
||||
Layer Layer
|
||||
Locations []Location
|
||||
Operation ChangeKind
|
||||
}
|
||||
|
||||
// FieldInfo describes an effective configuration path.
|
||||
type FieldInfo struct {
|
||||
Explicit bool
|
||||
Null bool
|
||||
Origin Origin
|
||||
}
|
||||
|
||||
// Change describes a definition or application-resolution operation. Path can name
|
||||
// an ancestor operation affecting the queried descendant. Historical values are
|
||||
// intentionally not exposed by this record.
|
||||
type Change struct {
|
||||
Path Path
|
||||
Origin Origin
|
||||
Previous []Origin
|
||||
Reasons []string
|
||||
Dependencies []Path
|
||||
}
|
||||
|
||||
func cloneLocation(location *Location) *Location {
|
||||
if location == nil {
|
||||
return nil
|
||||
}
|
||||
cloned := *location
|
||||
return &cloned
|
||||
}
|
||||
|
||||
func cloneLocations(locations []Location) []Location {
|
||||
if locations == nil {
|
||||
return nil
|
||||
}
|
||||
return append([]Location(nil), locations...)
|
||||
}
|
||||
|
||||
func cloneSourceInfo(source SourceInfo) SourceInfo {
|
||||
source.ImportedFrom = cloneLocation(source.ImportedFrom)
|
||||
return source
|
||||
}
|
||||
|
||||
func cloneSourceInfos(sources []SourceInfo) []SourceInfo {
|
||||
if sources == nil {
|
||||
return nil
|
||||
}
|
||||
cloned := make([]SourceInfo, len(sources))
|
||||
for i, source := range sources {
|
||||
cloned[i] = cloneSourceInfo(source)
|
||||
}
|
||||
return cloned
|
||||
}
|
||||
|
||||
func cloneOrigin(origin Origin) Origin {
|
||||
origin.Locations = cloneLocations(origin.Locations)
|
||||
return origin
|
||||
}
|
||||
|
||||
func cloneFieldInfo(info FieldInfo) FieldInfo {
|
||||
info.Origin = cloneOrigin(info.Origin)
|
||||
return info
|
||||
}
|
||||
|
||||
func cloneChange(change Change) Change {
|
||||
change.Path = change.Path.clone()
|
||||
change.Origin = cloneOrigin(change.Origin)
|
||||
if change.Previous != nil {
|
||||
previous := make([]Origin, len(change.Previous))
|
||||
for i, origin := range change.Previous {
|
||||
previous[i] = cloneOrigin(origin)
|
||||
}
|
||||
change.Previous = previous
|
||||
}
|
||||
change.Reasons = append([]string(nil), change.Reasons...)
|
||||
if change.Dependencies != nil {
|
||||
dependencies := make([]Path, len(change.Dependencies))
|
||||
for i, path := range change.Dependencies {
|
||||
dependencies[i] = path.clone()
|
||||
}
|
||||
change.Dependencies = dependencies
|
||||
}
|
||||
return change
|
||||
}
|
||||
|
||||
func cloneDiagnostic(diagnostic Diagnostic) Diagnostic {
|
||||
diagnostic.Path = diagnostic.Path.clone()
|
||||
diagnostic.Primary = cloneLocation(diagnostic.Primary)
|
||||
diagnostic.Related = cloneLocations(diagnostic.Related)
|
||||
return diagnostic
|
||||
}
|
||||
|
||||
func cloneDiagnostics(diagnostics []Diagnostic) []Diagnostic {
|
||||
if diagnostics == nil {
|
||||
return nil
|
||||
}
|
||||
cloned := make([]Diagnostic, len(diagnostics))
|
||||
for i, diagnostic := range diagnostics {
|
||||
cloned[i] = cloneDiagnostic(diagnostic)
|
||||
}
|
||||
return cloned
|
||||
}
|
||||
52
internal/facade/secret.go
Normal file
52
internal/facade/secret.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package configkit
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"strconv"
|
||||
|
||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/engine"
|
||||
)
|
||||
|
||||
// Secret is an opaque reference to an environment variable. Its name is public
|
||||
// configuration; its resolved value is available only through Value.
|
||||
type Secret struct {
|
||||
name, value string
|
||||
specified, resolved bool
|
||||
}
|
||||
|
||||
// SecretEnv constructs an unresolved environment-backed secret reference.
|
||||
func SecretEnv(name string) Secret { return Secret{name: name, specified: true} }
|
||||
|
||||
// EnvName returns the public environment-variable name.
|
||||
func (s Secret) EnvName() string { return s.name }
|
||||
|
||||
// Value returns the resolved credential, or an empty string when unresolved.
|
||||
func (s Secret) Value() string {
|
||||
if !s.resolved {
|
||||
return ""
|
||||
}
|
||||
return s.value
|
||||
}
|
||||
|
||||
// IsResolved reports whether this reference resolved to a nonempty value during its configuration load.
|
||||
func (s Secret) IsResolved() bool { return s.resolved }
|
||||
|
||||
// Format renders only the public reference name for every formatting verb.
|
||||
func (s Secret) Format(state fmt.State, _ rune) {
|
||||
_, _ = io.WriteString(state, "configkit.SecretEnv("+strconv.Quote(s.name)+")")
|
||||
}
|
||||
|
||||
// MarshalJSON emits only the public environment-variable name.
|
||||
func (s Secret) MarshalJSON() ([]byte, error) { return json.Marshal(s.name) }
|
||||
|
||||
func secretBinding() engine.SecretBinding {
|
||||
return engine.SecretBinding{Type: reflect.TypeFor[Secret](), Read: func(value reflect.Value) engine.SecretState {
|
||||
secret := value.Interface().(Secret)
|
||||
return engine.SecretState{Name: secret.name, Value: secret.value, Specified: secret.specified, Resolved: secret.resolved}
|
||||
}, Make: func(state engine.SecretState) reflect.Value {
|
||||
return reflect.ValueOf(Secret{name: state.Name, value: state.Value, specified: state.Specified, resolved: state.Resolved})
|
||||
}}
|
||||
}
|
||||
25
internal/facade/source.go
Normal file
25
internal/facade/source.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package configkit
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/engine"
|
||||
)
|
||||
|
||||
// Source is an opaque configuration input. Construct sources with File, Bytes,
|
||||
// Reader, or NoSource; the zero value is invalid.
|
||||
type Source struct{ value engine.Source }
|
||||
|
||||
// File returns a source that reopens path for every load attempt.
|
||||
func File(path string) Source { return Source{value: engine.File(path)} }
|
||||
|
||||
// Bytes snapshots data immediately.
|
||||
func Bytes(label string, data []byte) Source { return Source{value: engine.Bytes(label, data)} }
|
||||
|
||||
// Reader returns a single-use source. Copies share the same single-use claim.
|
||||
func Reader(label string, input io.Reader) Source { return Source{value: engine.Reader(label, input)} }
|
||||
|
||||
// NoSource explicitly selects defaults-only input.
|
||||
func NoSource() Source { return Source{value: engine.NoSource()} }
|
||||
|
||||
func (s Source) toEngine() engine.Source { return s.value }
|
||||
Reference in New Issue
Block a user