Documentation
¶
Overview ¶
Package config loads, merges, validates, and decodes Diene service configuration.
The loader layers three sources in a fixed precedence: a base YAML document carrying full defaults, a sparse per-landscape YAML overlay, and the process environment applied LAST. The YAML layers are read with two independent github.com/spf13/viper instances combined through MergeConfigMap; the environment layer is produced by github.com/AtomiCloud/diene.go-core-utils/lib/coreutils.EnvironmentToNestedMap so lists arrive as contiguous indexed keys (FOO__0, FOO__1) rather than a JSON or comma-separated encoding, and it is folded on with github.com/AtomiCloud/diene.go-core-utils/lib/coreutils.DeepMerge.
Validation happens exactly once, against the fully merged tree, using a generated JSON Schema (draft 2020-12). A service composes the root schema from engine-owned Block fragments plus its own keys: config never defines the otel, auth-engine, or api-engine block schemas, it only merges and validates them. A validation failure is reported as a problem-typed error (github.com/AtomiCloud/diene.go-errors-problems), recoverable with the validation-error catalog id and HTTP 400, and carries the offending field paths and messages as readable issue data.
The environment prefix is a required per-application option with no baked default; ATOMI_ is only an example. Configuration keys match across snake, kebab, camel, and Pascal spellings, blank environment values are treated as unset, and every committed configuration YAML declares its schema on its first line.
Index ¶
- Constants
- func FragmentFromType(model any) (map[string]any, error)
- func GenerateSchema(model any) ([]byte, error)
- type AppBlock
- type Block
- type BytesYAMLSource
- type Config
- type EnvSource
- type FileYAMLSource
- type Issue
- type Loader
- type MapEnvSource
- type OSEnvSource
- type Option
- func WithBaseDir(dir string) Option
- func WithBaseSource(source YAMLSource) Option
- func WithEnvPrefix(prefix string) Option
- func WithEnvSource(source EnvSource) Option
- func WithErrorPortal(portal problem.ErrorPortal) Option
- func WithLandscape(landscape string) Option
- func WithOverlaySource(landscape string, source YAMLSource) Option
- func WithSchema(schema Schema) Option
- type Schema
- type YAMLSource
Examples ¶
- AppBlock
- AppBlock (Landscape)
- AppBlock (Module)
- AppBlock (Platform)
- AppBlock (Service)
- AppBlock (Version)
- AppBlockSchema
- Block
- Block (Key)
- Block (Required)
- Block (Schema)
- BytesYAMLSource
- BytesYAMLSource.Name
- BytesYAMLSource.Read
- ComposeSchema
- Config
- Config.App
- Config.Decode
- Config.Raw
- EnvSource
- FileYAMLSource
- FileYAMLSource.Name
- FileYAMLSource.Path
- FileYAMLSource.Read
- FragmentFromType
- GenerateSchema
- Issue
- Issue (Message)
- Issue (Path)
- Issue.String
- Loader
- Loader (Overlay)
- Loader.Load
- MapEnvSource
- MapEnvSource.Environ
- MapEnvSource.Name
- NewBlock
- NewBytesYAMLSource
- NewConfig
- NewFileYAMLSource
- NewLoader
- NewMapEnvSource
- NewOSEnvSource
- NewOptionalFileYAMLSource
- OSEnvSource
- OSEnvSource.Environ
- OSEnvSource.Name
- Option
- Schema
- Schema.Marshal
- Schema.Root
- Schema.Validate
- Schema.WithPortal
- SchemaFromJSON
- ValidationIssues
- WithBaseDir
- WithBaseSource
- WithEnvPrefix
- WithEnvSource
- WithErrorPortal
- WithLandscape
- WithOverlaySource
- WithSchema
- YAMLSource
Constants ¶
const AppKey = "app"
AppKey is the root property that carries the service-tree AppBlock.
const BaseLandscape = "base"
BaseLandscape is the sentinel landscape that applies no overlay: the base defaults are used as-is. An empty resolved landscape behaves the same way.
const Draft2020 = "https://json-schema.org/draft/2020-12/schema"
Draft2020 is the JSON Schema draft this package generates and validates against.
Variables ¶
This section is empty.
Functions ¶
func FragmentFromType ¶
FragmentFromType reflects a Go type into a JSON Schema fragment map suitable for NewBlock. It threads the reflection and decode through a single error path so a caller composes typed blocks without hand-authoring JSON.
The reflector emits root resource markers ("$schema" and "$id") that describe a standalone document; a mountable fragment must not carry them, because ComposeSchema owns the composed root's dialect and generates each block's resource identity. Only those two reflector-generated root keys are removed — a hand-authored "$schema" or "$id" in a fragment remains an authoring fault.
Example ¶
ExampleFragmentFromType shows deriving a composable fragment from a Go type.
package main
import (
"fmt"
"github.com/AtomiCloud/diene.go-config/lib/config"
)
func main() {
fragment, _ := config.FragmentFromType(config.AppBlock{})
fmt.Println(fragment["type"])
}
Output: object
func GenerateSchema ¶
GenerateSchema reflects a Go type into a draft-2020-12 JSON Schema fragment using invopop/jsonschema. It is the generator engines and services use to derive a Block fragment from a typed model, and the artifact generator uses to emit the committed schema.
Example ¶
ExampleGenerateSchema shows reflecting a Go type into a JSON Schema fragment.
package main
import (
"fmt"
"strings"
"github.com/AtomiCloud/diene.go-config/lib/config"
)
func main() {
schema, _ := config.GenerateSchema(config.AppBlock{})
fmt.Println(strings.Contains(string(schema), "landscape"))
}
Output: true
Types ¶
type AppBlock ¶
type AppBlock struct {
// Landscape is the deployment landscape, e.g. lapras or pichu.
Landscape string `json:"landscape" yaml:"landscape" jsonschema:"required"`
// Platform is the owning platform segment.
Platform string `json:"platform" yaml:"platform" jsonschema:"required"`
// Service is the service segment.
Service string `json:"service" yaml:"service" jsonschema:"required"`
// Module is the module segment within the service.
Module string `json:"module" yaml:"module" jsonschema:"required"`
// Version is the released service version.
Version string `json:"version" yaml:"version" jsonschema:"required"`
}
AppBlock is the service-tree identity block every Diene service declares under the "app" key. Its landscape segment is the natural source of the runtime landscape when one is not passed explicitly to the loader, and the full LPSM tuple mirrors the service-tree identity used across the platform.
Example ¶
ExampleAppBlock shows the service-tree identity block.
package main
import (
"fmt"
"github.com/AtomiCloud/diene.go-config/lib/config"
)
func main() {
app := config.AppBlock{Landscape: "lapras", Platform: "sulfoxide", Service: "config", Module: "lib", Version: "1.0.0"}
fmt.Println(app.Landscape, app.Platform, app.Service, app.Module, app.Version)
}
Output: lapras sulfoxide config lib 1.0.0
Example (Landscape) ¶
ExampleAppBlock_landscape shows the service-tree landscape field.
package main
import (
"fmt"
"github.com/AtomiCloud/diene.go-config/lib/config"
)
func main() {
fmt.Println(config.AppBlock{Landscape: "lapras"}.Landscape)
}
Output: lapras
Example (Module) ¶
ExampleAppBlock_module shows the service-tree module field.
package main
import (
"fmt"
"github.com/AtomiCloud/diene.go-config/lib/config"
)
func main() {
fmt.Println(config.AppBlock{Module: "lib"}.Module)
}
Output: lib
Example (Platform) ¶
ExampleAppBlock_platform shows the service-tree platform field.
package main
import (
"fmt"
"github.com/AtomiCloud/diene.go-config/lib/config"
)
func main() {
fmt.Println(config.AppBlock{Platform: "sulfoxide"}.Platform)
}
Output: sulfoxide
Example (Service) ¶
ExampleAppBlock_service shows the service-tree service field.
package main
import (
"fmt"
"github.com/AtomiCloud/diene.go-config/lib/config"
)
func main() {
fmt.Println(config.AppBlock{Service: "config"}.Service)
}
Output: config
Example (Version) ¶
ExampleAppBlock_version shows the service-tree version field.
package main
import (
"fmt"
"github.com/AtomiCloud/diene.go-config/lib/config"
)
func main() {
fmt.Println(config.AppBlock{Version: "1.0.0"}.Version)
}
Output: 1.0.0
type Block ¶
type Block struct {
// Key is the root property the fragment is mounted under, e.g. "otel".
Key string
// Required marks the block as mandatory in the composed root schema.
Required bool
// Schema is the draft-2020-12 JSON Schema fragment describing the block.
//
// The fragment is mounted as its own schema RESOURCE, so an ordinary
// fragment-local pointer such as {"$ref": "#/$defs/body"} resolves inside the
// block and stays portable; it cannot reach into another block. Do not author
// "$id" or "$schema" here — [ComposeSchema] owns the composed root's dialect
// and generates each block's resource identity.
//
// A supported SUBSET of the dialect is accepted, because validation matches
// keys canonically (see [Schema.Validate]). Rejected as authoring faults:
// patternProperties and propertyNames, which constrain the authored spelling
// of a key; $anchor, $dynamicAnchor, $dynamicRef, $recursiveAnchor,
// $recursiveRef, and any non-local, percent-encoded, or anchor-form $ref;
// $vocabulary; dependencies and additionalItems, which the 2020-12 dialect
// ignores; and contentSchema, contentEncoding, and contentMediaType, which
// this validator does not assert. Those words appearing as DATA under const,
// enum, default, examples, or an unknown annotation are ordinary content.
Schema map[string]any
}
Block is one composable section of the root configuration schema. Engines export the Block for the section they own — otel, auth-engine, api-engine, standard-config — and a service composes them with ComposeSchema alongside its own keys. config never defines an engine's block schema; it only merges and validates the fragments it is handed.
Example ¶
ExampleBlock shows an engine-owned composable block value.
package main
import (
"fmt"
"github.com/AtomiCloud/diene.go-config/lib/config"
)
func main() {
block := config.Block{Key: "otel", Required: true, Schema: map[string]any{"type": "object"}}
fmt.Println(block.Key, block.Required, block.Schema["type"])
}
Output: otel true object
Example (Key) ¶
ExampleBlock_key shows the root property a block mounts under.
package main
import (
"fmt"
"github.com/AtomiCloud/diene.go-config/lib/config"
)
func main() {
fmt.Println(config.NewBlock("otel", true, map[string]any{"type": "object"}).Key)
}
Output: otel
Example (Required) ¶
ExampleBlock_required shows whether a block is mandatory in the composed root.
package main
import (
"fmt"
"github.com/AtomiCloud/diene.go-config/lib/config"
)
func main() {
fmt.Println(config.NewBlock("otel", true, map[string]any{"type": "object"}).Required)
}
Output: true
Example (Schema) ¶
ExampleBlock_schema shows the fragment a block carries.
package main
import (
"fmt"
"github.com/AtomiCloud/diene.go-config/lib/config"
)
func main() {
fmt.Println(config.NewBlock("otel", true, map[string]any{"type": "object"}).Schema["type"])
}
Output: object
func AppBlockSchema ¶
func AppBlockSchema() Block
AppBlockSchema returns the config-owned JSON Schema fragment for the "app" block. Unlike engine blocks, the service-tree identity block belongs to config itself, so it ships a stable literal fragment (draft 2020-12) rather than accepting one. GenerateSchema reflects the same AppBlock type and the oracle test proves the two agree.
Example ¶
ExampleAppBlockSchema shows the config-owned app block fragment.
package main
import (
"fmt"
"github.com/AtomiCloud/diene.go-config/lib/config"
)
func main() {
fmt.Println(config.AppBlockSchema().Key)
}
Output: app
func NewBlock ¶
NewBlock builds a Block mounting fragment under key. It clones fragment over the JSON-like schema domain (string-keyed maps, slices, and scalars) so the block owns an independent copy and later caller mutation cannot alter the composed schema. It is the constructor engines use to export their owned section for composition.
Example ¶
ExampleNewBlock shows an engine exporting its owned section for composition. config never defines the block; it only mounts and validates the fragment.
package main
import (
"fmt"
"github.com/AtomiCloud/diene.go-config/lib/config"
)
func main() {
block := config.NewBlock("otel", true, map[string]any{"type": "object"})
fmt.Println(block.Key, block.Required)
}
Output: otel true
type BytesYAMLSource ¶
type BytesYAMLSource struct {
// contains filtered or unexported fields
}
BytesYAMLSource is a YAMLSource backed by an in-memory document. It is the source the testhelper and inline callers use to supply a base or overlay without touching the filesystem.
Example ¶
ExampleBytesYAMLSource shows an in-memory YAML layer.
package main
import (
"fmt"
"github.com/AtomiCloud/diene.go-config/lib/config"
)
func main() {
source := config.NewBytesYAMLSource("inline", []byte("a: 1"))
fmt.Println(source.Name())
}
Output: inline
func NewBytesYAMLSource ¶
func NewBytesYAMLSource(name string, content []byte) BytesYAMLSource
NewBytesYAMLSource creates an in-memory YAML layer named name. It clones content so later caller mutation cannot alter the layer; a nil document stays nil (an absent layer).
Example ¶
ExampleNewBytesYAMLSource shows constructing an in-memory YAML layer.
package main
import (
"context"
"fmt"
"github.com/AtomiCloud/diene.go-config/lib/config"
)
func main() {
source := config.NewBytesYAMLSource("inline", []byte("a: 1"))
content, _ := source.Read(context.Background())
fmt.Println(string(content))
}
Output: a: 1
func (BytesYAMLSource) Name ¶
func (s BytesYAMLSource) Name() string
Name returns the layer name.
Example ¶
ExampleBytesYAMLSource_Name shows the layer name.
package main
import (
"fmt"
"github.com/AtomiCloud/diene.go-config/lib/config"
)
func main() {
fmt.Println(config.NewBytesYAMLSource("inline", nil).Name())
}
Output: inline
func (BytesYAMLSource) Read ¶
func (s BytesYAMLSource) Read(_ context.Context) ([]byte, error)
Read returns a clone of the in-memory document, so a caller cannot mutate the layer through the returned slice. A nil content yields an absent layer.
Example ¶
ExampleBytesYAMLSource_Read shows reading the in-memory document.
package main
import (
"context"
"fmt"
"github.com/AtomiCloud/diene.go-config/lib/config"
)
func main() {
content, _ := config.NewBytesYAMLSource("inline", []byte("a: 1")).Read(context.Background())
fmt.Println(string(content))
}
Output: a: 1
type Config ¶
type Config struct {
// contains filtered or unexported fields
}
Config is a fully merged and validated configuration tree. It is produced by Loader.Load and, for tests, by NewConfig over an already-merged map.
Example ¶
ExampleConfig shows a merged configuration tree serving a decoded value.
package main
import (
"fmt"
"github.com/AtomiCloud/diene.go-config/lib/config"
)
func main() {
cfg := config.NewConfig(map[string]any{"demo": map[string]any{"region": "local"}})
var region string
_ = cfg.Decode("demo.region", ®ion)
fmt.Println(region)
}
Output: local
func NewConfig ¶
NewConfig wraps an already-merged configuration tree. It deep-clones raw over the supported configuration domain — string-keyed maps, slices, arrays, pointers, and value-semantic structs (such as time.Time) — so the config owns an independent copy and later caller mutation cannot alter it or race a concurrent Config.Decode. A mutable value reachable only through a struct's unexported field is the one documented exception: reflection cannot copy it, so it stays shared (the JSON-like configuration domain has no such value). Loader.Load uses it after validation.
Example ¶
ExampleNewConfig shows wrapping an already-merged tree.
package main
import (
"fmt"
"github.com/AtomiCloud/diene.go-config/lib/config"
)
func main() {
cfg := config.NewConfig(map[string]any{"demo": map[string]any{"region": "local"}})
fmt.Println(cfg.Raw()["demo"])
}
Output: map[region:local]
func (*Config) App ¶
App decodes the service-tree AppBlock from the "app" key.
Example ¶
ExampleConfig_App shows recovering the service-tree identity block.
package main
import (
"fmt"
"github.com/AtomiCloud/diene.go-config/lib/config"
)
func main() {
cfg := config.NewConfig(map[string]any{"app": map[string]any{
"landscape": "lapras", "platform": "sulfoxide",
"service": "config", "module": "lib", "version": "1.0.0",
}})
app, _ := cfg.App()
fmt.Println(app.Service)
}
Output: config
func (*Config) Decode ¶
Decode decodes the subtree at a dotted key into target, matching keys across snake, kebab, camel, and Pascal spellings. It is the typed-slice serving surface: pass a pointer to a slice or struct and the validated values decode into it. A missing key, or a key whose canonical form is ambiguous among siblings, is an error, so resolution never depends on map iteration order.
Example ¶
ExampleConfig_Decode shows the typed-slice serving surface decoding a validated subtree into a Go slice.
package main
import (
"fmt"
"github.com/AtomiCloud/diene.go-config/lib/config"
)
func main() {
cfg := config.NewConfig(map[string]any{"demo": map[string]any{"replicas": []any{1, 2, 3}}})
var replicas []int
_ = cfg.Decode("demo.replicas", &replicas)
fmt.Println(replicas)
}
Output: [1 2 3]
func (*Config) Raw ¶
Raw returns an independent deep clone of the merged configuration tree over the supported configuration domain (see NewConfig), so callers cannot mutate the config through the returned map.
Example ¶
ExampleConfig_Raw shows reading the merged tree as an independent clone.
package main
import (
"fmt"
"github.com/AtomiCloud/diene.go-config/lib/config"
)
func main() {
cfg := config.NewConfig(map[string]any{"app": map[string]any{"service": "config"}})
fmt.Println(cfg.Raw()["app"])
}
Output: map[service:config]
type EnvSource ¶
type EnvSource interface {
// Name identifies the layer for diagnostics.
Name() string
// Environ returns the environment as a flat key to value map.
Environ(ctx context.Context) (map[string]string, error)
}
EnvSource yields the process environment folded on as the final layer.
Example ¶
ExampleEnvSource shows the environment layer seam.
package main
import (
"fmt"
"github.com/AtomiCloud/diene.go-config/lib/config"
)
func main() {
var source config.EnvSource = config.NewMapEnvSource("fake", nil)
fmt.Println(source.Name())
}
Output: fake
type FileYAMLSource ¶
type FileYAMLSource struct {
// contains filtered or unexported fields
}
FileYAMLSource is a YAMLSource backed by a filesystem path. A missing required file is an error; a missing optional file is an absent layer.
Example ¶
ExampleFileYAMLSource shows a filesystem-backed YAML layer.
package main
import (
"fmt"
"github.com/AtomiCloud/diene.go-config/lib/config"
)
func main() {
source := config.NewFileYAMLSource("base", "/etc/config/settings.yaml")
fmt.Println(source.Path())
}
Output: /etc/config/settings.yaml
func NewFileYAMLSource ¶
func NewFileYAMLSource(name, path string) FileYAMLSource
NewFileYAMLSource creates a required YAML layer read from path.
Example ¶
ExampleNewFileYAMLSource shows a filesystem-backed base layer.
package main
import (
"context"
"fmt"
"os"
"path/filepath"
"github.com/AtomiCloud/diene.go-config/lib/config"
)
const exampleBase = `
app:
landscape: base
platform: sulfoxide
service: config
module: lib
version: 1.0.0
`
func main() {
dir, _ := os.MkdirTemp("", "config-example")
defer func() { _ = os.RemoveAll(dir) }()
path := filepath.Join(dir, "settings.yaml")
_ = os.WriteFile(path, []byte(exampleBase), 0o600)
loader := config.NewLoader(
config.WithEnvPrefix("ATOMI_"),
config.WithBaseSource(config.NewFileYAMLSource("base", path)),
config.WithEnvSource(config.NewOSEnvSource()),
config.WithSchema(config.ComposeSchema(config.AppBlockSchema())),
)
cfg, _ := loader.Load(context.Background())
app, _ := cfg.App()
fmt.Println(app.Platform)
}
Output: sulfoxide
func NewOptionalFileYAMLSource ¶
func NewOptionalFileYAMLSource(name, path string) FileYAMLSource
NewOptionalFileYAMLSource creates an optional YAML layer; a missing file yields an absent layer instead of an error, which is how a landscape with no overlay resolves.
Example ¶
ExampleNewOptionalFileYAMLSource shows an absent optional layer resolving to no document rather than an error.
package main
import (
"context"
"fmt"
"os"
"path/filepath"
"github.com/AtomiCloud/diene.go-config/lib/config"
)
func main() {
source := config.NewOptionalFileYAMLSource("overlay", filepath.Join(os.TempDir(), "config-example-absent.yaml"))
content, err := source.Read(context.Background())
fmt.Println(content == nil, err)
}
Output: true <nil>
func (FileYAMLSource) Name ¶
func (s FileYAMLSource) Name() string
Name returns the layer name.
Example ¶
ExampleFileYAMLSource_Name shows the layer name.
package main
import (
"fmt"
"github.com/AtomiCloud/diene.go-config/lib/config"
)
func main() {
fmt.Println(config.NewFileYAMLSource("base", "/etc/config/settings.yaml").Name())
}
Output: base
func (FileYAMLSource) Path ¶
func (s FileYAMLSource) Path() string
Path returns the filesystem path the layer reads from.
Example ¶
ExampleFileYAMLSource_Path shows the configured path.
package main
import (
"fmt"
"github.com/AtomiCloud/diene.go-config/lib/config"
)
func main() {
fmt.Println(config.NewFileYAMLSource("base", "/etc/config/settings.yaml").Path())
}
Output: /etc/config/settings.yaml
func (FileYAMLSource) Read ¶
func (s FileYAMLSource) Read(_ context.Context) ([]byte, error)
Read returns the file contents, or (nil, nil) when an optional file is absent.
Example ¶
ExampleFileYAMLSource_Read shows reading a file layer.
package main
import (
"context"
"fmt"
"os"
"path/filepath"
"github.com/AtomiCloud/diene.go-config/lib/config"
)
func main() {
dir, _ := os.MkdirTemp("", "config-file")
defer func() { _ = os.RemoveAll(dir) }()
path := filepath.Join(dir, "settings.yaml")
_ = os.WriteFile(path, []byte("a: 1"), 0o600)
content, _ := config.NewFileYAMLSource("base", path).Read(context.Background())
fmt.Println(string(content))
}
Output: a: 1
type Issue ¶
type Issue struct {
// Path is the dotted instance location, e.g. "app.landscape", or "(root)".
Path string
// Message explains why the value at Path is invalid.
Message string
}
Issue is a single, human-readable schema-validation failure: the dotted path of the offending value and the reason it was rejected.
Example ¶
ExampleIssue shows a readable validation issue.
package main
import (
"fmt"
"github.com/AtomiCloud/diene.go-config/lib/config"
)
func main() {
fmt.Println(config.Issue{Path: "app.version", Message: "required"})
}
Output: app.version: required
Example (Message) ¶
ExampleIssue_message shows the reason a value was rejected.
package main
import (
"fmt"
"github.com/AtomiCloud/diene.go-config/lib/config"
)
func main() {
fmt.Println(config.Issue{Path: "app.landscape", Message: "required"}.Message)
}
Output: required
Example (Path) ¶
ExampleIssue_path shows the dotted location of a validation issue.
package main
import (
"fmt"
"github.com/AtomiCloud/diene.go-config/lib/config"
)
func main() {
fmt.Println(config.Issue{Path: "app.landscape", Message: "required"}.Path)
}
Output: app.landscape
func ValidationIssues ¶
ValidationIssues recovers the readable Issue list from a problem-typed validation error produced by Schema.Validate or Loader.Load. The second result reports whether err carried a validation problem with a fields payload.
Example ¶
ExampleValidationIssues shows recovering readable issues from a load failure.
package main
import (
"fmt"
"github.com/AtomiCloud/diene.go-config/lib/config"
)
func main() {
err := config.ComposeSchema(config.AppBlockSchema()).Validate(map[string]any{})
issues, ok := config.ValidationIssues(err)
fmt.Println(ok, issues[0].String() != "")
}
Output: true true
func (Issue) String ¶
String renders the issue as "path: message".
Example ¶
ExampleIssue_String shows the "path: message" rendering.
package main
import (
"fmt"
"github.com/AtomiCloud/diene.go-config/lib/config"
)
func main() {
fmt.Println(config.Issue{Path: "app.version", Message: "required"}.String())
}
Output: app.version: required
type Loader ¶
type Loader struct {
// contains filtered or unexported fields
}
Loader assembles a validated Config from a base YAML layer, an optional per-landscape overlay, and the process environment applied last. Construct it with NewLoader and one or more Option values.
Example ¶
ExampleLoader shows the base-then-environment layering with a required schema: the base document sets full defaults and the environment layer, applied last, wins.
package main
import (
"context"
"fmt"
"github.com/AtomiCloud/diene.go-config/lib/config"
)
const exampleBase = `
app:
landscape: base
platform: sulfoxide
service: config
module: lib
version: 1.0.0
`
func main() {
loader := config.NewLoader(
config.WithEnvPrefix("ATOMI_"),
config.WithBaseSource(config.NewBytesYAMLSource("base", []byte(exampleBase))),
config.WithEnvSource(config.NewMapEnvSource("env", map[string]string{
"ATOMI_APP__VERSION": "2.0.0",
})),
config.WithSchema(config.ComposeSchema(config.AppBlockSchema())),
)
cfg, err := loader.Load(context.Background())
if err != nil {
fmt.Println(err)
return
}
app, _ := cfg.App()
fmt.Println(app.Landscape, app.Version)
}
Output: base 2.0.0
Example (Overlay) ¶
ExampleLoader_overlay shows a sparse per-landscape overlay overriding the base.
package main
import (
"context"
"fmt"
"github.com/AtomiCloud/diene.go-config/lib/config"
problemtest "github.com/AtomiCloud/diene.go-errors-problems/testhelper"
)
const exampleBase = `
app:
landscape: base
platform: sulfoxide
service: config
module: lib
version: 1.0.0
`
func main() {
loader := config.NewLoader(
config.WithEnvPrefix("ATOMI_"),
config.WithBaseSource(config.NewBytesYAMLSource("base", []byte(exampleBase))),
config.WithLandscape("lapras"),
config.WithOverlaySource("lapras", config.NewBytesYAMLSource("overlay", []byte("app:\n version: 3.0.0\n"))),
config.WithEnvSource(config.NewMapEnvSource("env", nil)),
config.WithSchema(config.ComposeSchema(config.AppBlockSchema())),
config.WithErrorPortal(problemtest.SampleErrorPortal()),
)
cfg, _ := loader.Load(context.Background())
app, _ := cfg.App()
fmt.Println(app.Version)
}
Output: 3.0.0
func NewLoader ¶
NewLoader creates a loader with the process-environment source as the env layer. Supply the required WithEnvPrefix, a base via WithBaseSource or WithBaseDir, and a schema via WithSchema; Loader.Load fails fast when any of the three is missing. A nil Option is ignored.
Example ¶
ExampleNewLoader shows constructing a loader from options.
package main
import (
"context"
"fmt"
"github.com/AtomiCloud/diene.go-config/lib/config"
)
func main() {
loader := config.NewLoader(config.WithEnvPrefix("ATOMI_"))
cfg, err := loader.Load(context.Background())
fmt.Println(cfg, err != nil)
}
Output: <nil> true
func (*Loader) Load ¶
Load reads, merges, validates, and returns the configuration. The base YAML full defaults are overlaid by the resolved landscape's sparse overlay, and the environment is folded on LAST. Layers merge with the core-utils canonical rule, so a value spelled in any of snake, kebab, camel, or Pascal in one layer overrides the same logical key in another. Every layer is rejected if two sibling keys share a canonical form, so resolution never depends on map iteration order. The fully merged tree is validated exactly once against the required schema; problems mint their type URI from the schema's portal, or from WithErrorPortal when one is set. An invalid tree fails fast.
Example ¶
ExampleLoader_Load shows loading a validated configuration.
loader := config.NewLoader(
config.WithEnvPrefix("ATOMI_"),
config.WithBaseSource(config.NewBytesYAMLSource("base", []byte(exampleBase))),
config.WithEnvSource(config.NewMapEnvSource("env", nil)),
config.WithSchema(config.ComposeSchema(config.AppBlockSchema())),
)
cfg, _ := loader.Load(context.Background())
app, _ := cfg.App()
fmt.Println(app.Platform)
Output: sulfoxide
type MapEnvSource ¶
type MapEnvSource struct {
// contains filtered or unexported fields
}
MapEnvSource is an EnvSource backed by an in-memory map, used to drive the env layer deterministically in tests.
Example ¶
ExampleMapEnvSource shows an in-memory env layer.
package main
import (
"fmt"
"github.com/AtomiCloud/diene.go-config/lib/config"
)
func main() {
source := config.NewMapEnvSource("fake", map[string]string{"K": "V"})
fmt.Println(source.Name())
}
Output: fake
func NewMapEnvSource ¶
func NewMapEnvSource(name string, vars map[string]string) MapEnvSource
NewMapEnvSource creates an in-memory env layer named name. It clones vars so later caller mutation cannot alter the layer.
Example ¶
ExampleNewMapEnvSource shows constructing an in-memory env layer.
package main
import (
"context"
"fmt"
"github.com/AtomiCloud/diene.go-config/lib/config"
)
func main() {
environment, _ := config.NewMapEnvSource("fake", map[string]string{"K": "V"}).Environ(context.Background())
fmt.Println(environment["K"])
}
Output: V
func (MapEnvSource) Environ ¶
Environ returns a clone of the in-memory environment, so a caller cannot mutate the layer through the returned map.
Example ¶
ExampleMapEnvSource_Environ shows reading the in-memory environment.
package main
import (
"context"
"fmt"
"github.com/AtomiCloud/diene.go-config/lib/config"
)
func main() {
environment, _ := config.NewMapEnvSource("fake", map[string]string{"K": "V"}).Environ(context.Background())
fmt.Println(environment["K"])
}
Output: V
func (MapEnvSource) Name ¶
func (s MapEnvSource) Name() string
Name returns the layer name.
Example ¶
ExampleMapEnvSource_Name shows the layer name.
package main
import (
"fmt"
"github.com/AtomiCloud/diene.go-config/lib/config"
)
func main() {
fmt.Println(config.NewMapEnvSource("fake", nil).Name())
}
Output: fake
type OSEnvSource ¶
type OSEnvSource struct{}
OSEnvSource reads the live process environment.
Example ¶
ExampleOSEnvSource shows the live process-environment layer.
package main
import (
"fmt"
"github.com/AtomiCloud/diene.go-config/lib/config"
)
func main() {
fmt.Println(config.NewOSEnvSource().Name())
}
Output: process-environment
func NewOSEnvSource ¶
func NewOSEnvSource() OSEnvSource
NewOSEnvSource creates a source over the live process environment.
Example ¶
ExampleNewOSEnvSource shows constructing the process-environment layer.
package main
import (
"fmt"
"github.com/AtomiCloud/diene.go-config/lib/config"
)
func main() {
fmt.Println(config.NewOSEnvSource().Name())
}
Output: process-environment
func (OSEnvSource) Environ ¶
Environ returns the process environment as a flat map.
Example ¶
ExampleOSEnvSource_Environ shows reading the process environment.
package main
import (
"context"
"fmt"
"github.com/AtomiCloud/diene.go-config/lib/config"
)
func main() {
environment, err := config.NewOSEnvSource().Environ(context.Background())
fmt.Println(environment != nil, err)
}
Output: true <nil>
func (OSEnvSource) Name ¶
func (OSEnvSource) Name() string
Name returns the layer name.
Example ¶
ExampleOSEnvSource_Name shows the layer name.
package main
import (
"fmt"
"github.com/AtomiCloud/diene.go-config/lib/config"
)
func main() {
fmt.Println(config.NewOSEnvSource().Name())
}
Output: process-environment
type Option ¶
type Option func(*Loader)
Option configures a Loader. Options are applied in order by NewLoader.
Example ¶
ExampleOption shows an option value configuring a loader.
package main
import (
"context"
"fmt"
"github.com/AtomiCloud/diene.go-config/lib/config"
)
func main() {
option := config.WithEnvPrefix("ATOMI_")
loader := config.NewLoader(option)
_, err := loader.Load(context.Background())
fmt.Println(err != nil)
}
Output: true
func WithBaseDir ¶
WithBaseDir configures file-mode layering rooted at dir: the base reads dir/settings.yaml and each landscape overlay reads dir/settings.<landscape>.yaml when present.
Example ¶
ExampleWithBaseDir shows file-mode layering rooted at a directory.
dir, _ := os.MkdirTemp("", "config-basedir")
defer func() { _ = os.RemoveAll(dir) }()
_ = os.WriteFile(filepath.Join(dir, "settings.yaml"), []byte(exampleBase), 0o600)
loader := config.NewLoader(
config.WithEnvPrefix("ATOMI_"),
config.WithBaseDir(dir),
config.WithEnvSource(config.NewMapEnvSource("env", nil)),
config.WithSchema(config.ComposeSchema(config.AppBlockSchema())),
)
cfg, _ := loader.Load(context.Background())
app, _ := cfg.App()
fmt.Println(app.Landscape)
Output: base
func WithBaseSource ¶
func WithBaseSource(source YAMLSource) Option
WithBaseSource sets the base YAML layer carrying full defaults.
Example ¶
ExampleWithBaseSource shows supplying an in-memory base layer.
option := config.WithBaseSource(config.NewBytesYAMLSource("base", []byte(exampleBase)))
loader := config.NewLoader(config.WithEnvPrefix("ATOMI_"), option, config.WithSchema(config.ComposeSchema(config.AppBlockSchema())))
cfg, _ := loader.Load(context.Background())
app, _ := cfg.App()
fmt.Println(app.Service)
Output: config
func WithEnvPrefix ¶
WithEnvPrefix sets the required environment prefix for the env layer, e.g. "ATOMI_". There is no default: a loader with no prefix fails fast, so ATOMI_ is only an example and never baked in.
Example ¶
ExampleWithEnvPrefix shows the required env prefix option.
package main
import (
"context"
"fmt"
"github.com/AtomiCloud/diene.go-config/lib/config"
)
func main() {
_, err := config.NewLoader(config.WithEnvPrefix("ATOMI_")).Load(context.Background())
fmt.Println(err != nil)
}
Output: true
func WithEnvSource ¶
WithEnvSource overrides the env layer source. The default reads the live process environment.
Example ¶
ExampleWithEnvSource shows overriding the env layer with a fake.
loader := config.NewLoader(
config.WithEnvPrefix("ATOMI_"),
config.WithBaseSource(config.NewBytesYAMLSource("base", []byte(exampleBase))),
config.WithEnvSource(config.NewMapEnvSource("env", map[string]string{"ATOMI_APP__VERSION": "6.0.0"})),
config.WithSchema(config.ComposeSchema(config.AppBlockSchema())),
)
cfg, _ := loader.Load(context.Background())
app, _ := cfg.App()
fmt.Println(app.Version)
Output: 6.0.0
func WithErrorPortal ¶
func WithErrorPortal(portal problem.ErrorPortal) Option
WithErrorPortal sets the service-tree portal load-path problems mint their type URI from, overriding the schema's own portal. When it is not set, Load uses the schema's Schema.WithPortal portal, falling back to the client-local portal.
Example ¶
ExampleWithErrorPortal shows overriding the load-path error portal.
package main
import (
"context"
"fmt"
"github.com/AtomiCloud/diene.go-config/lib/config"
"github.com/AtomiCloud/diene.go-errors-problems/lib/problem"
)
func main() {
loader := config.NewLoader(
config.WithEnvPrefix("ATOMI_"),
config.WithBaseSource(config.NewBytesYAMLSource("base", []byte("app:\n landscape: base\n"))),
config.WithEnvSource(config.NewMapEnvSource("env", nil)),
config.WithSchema(config.ComposeSchema(config.AppBlockSchema())),
config.WithErrorPortal(problem.LocalErrorPortal()),
)
_, err := loader.Load(context.Background())
issues, _ := config.ValidationIssues(err)
fmt.Println(len(issues) > 0)
}
Output: true
func WithLandscape ¶
WithLandscape sets the landscape explicitly, overriding the value resolved from the base document's app.landscape.
Example ¶
ExampleWithLandscape shows overriding the resolved landscape.
loader := config.NewLoader(
config.WithEnvPrefix("ATOMI_"),
config.WithBaseSource(config.NewBytesYAMLSource("base", []byte(exampleBase))),
config.WithLandscape("lapras"),
config.WithOverlaySource("lapras", config.NewBytesYAMLSource("overlay", []byte("app:\n version: 4.0.0\n"))),
config.WithEnvSource(config.NewMapEnvSource("env", nil)),
config.WithSchema(config.ComposeSchema(config.AppBlockSchema())),
)
cfg, _ := loader.Load(context.Background())
app, _ := cfg.App()
fmt.Println(app.Version)
Output: 4.0.0
func WithOverlaySource ¶
func WithOverlaySource(landscape string, source YAMLSource) Option
WithOverlaySource registers an explicit overlay layer for landscape, taking precedence over file-mode resolution. It is how in-memory and test overlays are supplied.
Example ¶
ExampleWithOverlaySource shows registering an explicit overlay.
option := config.WithOverlaySource("lapras", config.NewBytesYAMLSource("overlay", []byte("app:\n version: 5.0.0\n")))
loader := config.NewLoader(
config.WithEnvPrefix("ATOMI_"),
config.WithBaseSource(config.NewBytesYAMLSource("base", []byte(exampleBase))),
config.WithLandscape("lapras"),
option,
config.WithEnvSource(config.NewMapEnvSource("env", nil)),
config.WithSchema(config.ComposeSchema(config.AppBlockSchema())),
)
cfg, _ := loader.Load(context.Background())
app, _ := cfg.App()
fmt.Println(app.Version)
Output: 5.0.0
func WithSchema ¶
WithSchema sets the required schema the fully merged tree is validated against. It is mandatory: Loader.Load fails fast when no schema is configured, so startup validation can never be silently skipped.
Example ¶
ExampleWithSchema shows supplying the required validation schema.
option := config.WithSchema(config.ComposeSchema(config.AppBlockSchema()))
loader := config.NewLoader(config.WithEnvPrefix("ATOMI_"), config.WithBaseSource(config.NewBytesYAMLSource("base", []byte(exampleBase))), option)
cfg, _ := loader.Load(context.Background())
app, _ := cfg.App()
fmt.Println(app.Module)
Output: lib
type Schema ¶
type Schema struct {
// contains filtered or unexported fields
}
Schema is a composed, validatable root configuration schema.
Example ¶
ExampleSchema shows a composed root schema.
package main
import (
"fmt"
"github.com/AtomiCloud/diene.go-config/lib/config"
)
func main() {
schema := config.ComposeSchema(config.AppBlockSchema())
fmt.Println(schema.Root()["type"])
}
Output: object
func ComposeSchema ¶
ComposeSchema assembles a draft-2020-12 root object schema from block fragments. Each block becomes a property under its key; required blocks join the root "required" list. Additional top-level keys are permitted so a service can carry its own configuration alongside the composed engine blocks.
Blocks are composed on the JSON-VISIBLE spelling of Block.Key — the spelling the key has once serialized, in which any byte that is not valid UTF-8 becomes U+FFFD. Two keys that a JSON document cannot tell apart are therefore ONE block, and the later block wins its fragment, its requiredness, and its generated resource identity, mirroring layer-merge precedence. Block.Key itself keeps whatever the caller supplied.
Every block is mounted with a generated "$id", making it its own schema resource: a fragment-local "#/$defs/..." pointer resolves against the block rather than the composed root, which is what lets independently authored fragments use ordinary local pointers. A fragment that authors its own "$id" is left intact and rejected by Schema.Validate, so the mistake is reported rather than silently overwritten.
Example ¶
ExampleComposeSchema shows a service composing the config-owned app block into a root schema.
package main
import (
"fmt"
"github.com/AtomiCloud/diene.go-config/lib/config"
)
func main() {
schema := config.ComposeSchema(config.AppBlockSchema())
fmt.Println(schema.Root()["type"], schema.Root()["$schema"])
}
Output: object https://json-schema.org/draft/2020-12/schema
func SchemaFromJSON ¶
SchemaFromJSON loads a composed root schema from its committed JSON artifact, so a service can validate against the exact schema it ships rather than recomposing it at runtime.
Example ¶
ExampleSchemaFromJSON shows loading a committed schema artifact and validating against the exact schema a service ships.
package main
import (
"fmt"
"github.com/AtomiCloud/diene.go-config/lib/config"
)
func main() {
artifact, _ := config.ComposeSchema(config.AppBlockSchema()).Marshal()
schema, _ := config.SchemaFromJSON(artifact)
fmt.Println(schema.Root()["type"])
}
Output: object
func (Schema) Marshal ¶
Marshal renders the composed root schema as indented draft-2020-12 JSON — the form committed as the generated schema artifact.
Example ¶
ExampleSchema_Marshal shows serializing the composed schema for the committed artifact.
package main
import (
"fmt"
"strings"
"github.com/AtomiCloud/diene.go-config/lib/config"
)
func main() {
artifact, _ := config.ComposeSchema(config.AppBlockSchema()).Marshal()
fmt.Println(strings.HasPrefix(string(artifact), "{"))
}
Output: true
func (Schema) Root ¶
Root returns an independent clone of the composed root schema as a JSON-like map, so callers cannot mutate the schema this Schema validates against. It is the shape Schema.Marshal serializes for the committed artifact.
Example ¶
ExampleSchema_Root shows reading the composed root as an independent clone.
package main
import (
"fmt"
"github.com/AtomiCloud/diene.go-config/lib/config"
)
func main() {
schema := config.ComposeSchema(config.AppBlockSchema())
fmt.Println(schema.Root()["additionalProperties"])
}
Output: true
func (Schema) Validate ¶
Validate checks instance against the composed root schema exactly once.
Keys match CANONICALLY: separators and case are ignored, exactly as Config.Decode resolves a dotted key. Both sides are put in canonical form before the compiler runs, so every key comparison it makes — properties, required, dependentRequired, additionalProperties fall-through, unevaluated accounting, the inner checks of not and contains, and object const and enum equality — is spelling-insensitive. A spelling Decode can resolve is therefore always a spelling this constrains. Two branches may spell one logical key differently; they name one key and each branch's constraints apply natively. Sibling keys that share a canonical form are rejected, since canonicalizing them would collapse two declarations into one.
Reported paths carry the AUTHORED spellings of the instance; message text may name the canonical form of a property, so the path is the authoritative locator.
A schema-validation failure is returned as a problem-typed *problem.Error (validation-error, HTTP 400, recoverable) carrying the offending field paths and messages under data.fields. An unsupported schema construct (see Block), a canonical collision, a malformed schema, or an instance that cannot be normalized is returned as a plain wrapped error, since those are authoring faults rather than configuration-validation failures.
Example ¶
ExampleSchema_Validate shows a validation failure surfacing as a problem-typed error whose readable issues name the offending field.
package main
import (
"fmt"
"github.com/AtomiCloud/diene.go-config/lib/config"
)
func main() {
schema := config.ComposeSchema(config.AppBlockSchema())
instance := map[string]any{"app": map[string]any{
"landscape": "lapras", "platform": "sulfoxide",
"service": "config", "module": "lib", "version": "",
}}
err := schema.Validate(instance)
issues, _ := config.ValidationIssues(err)
fmt.Println(issues[0].Path)
}
Output: app.version
func (Schema) WithPortal ¶
func (s Schema) WithPortal(portal problem.ErrorPortal) Schema
WithPortal returns a copy of the schema whose validation failures mint their type URI from portal. A service passes its build-time service-tree portal so the problem type URI carries its own LPSM identity.
Example ¶
ExampleSchema_WithPortal shows binding a service-tree portal so validation problems mint their type URI from that identity.
package main
import (
"fmt"
"github.com/AtomiCloud/diene.go-config/lib/config"
problemtest "github.com/AtomiCloud/diene.go-errors-problems/testhelper"
)
func main() {
schema := config.ComposeSchema(config.AppBlockSchema()).WithPortal(problemtest.SampleErrorPortal())
err := schema.Validate(map[string]any{})
issues, ok := config.ValidationIssues(err)
fmt.Println(ok, len(issues) > 0)
}
Output: true true
type YAMLSource ¶
type YAMLSource interface {
// Name identifies the layer for diagnostics.
Name() string
// Read returns the layer's YAML bytes. An absent optional layer returns
// (nil, nil) rather than an error.
Read(ctx context.Context) ([]byte, error)
}
YAMLSource yields the raw YAML document of one configuration layer. The base layer and each landscape overlay are read through this seam so an in-memory fake can stand in for the filesystem in tests.
Example ¶
ExampleYAMLSource shows the YAML layer seam.
package main
import (
"fmt"
"github.com/AtomiCloud/diene.go-config/lib/config"
)
func main() {
var source config.YAMLSource = config.NewBytesYAMLSource("inline", []byte("a: 1"))
fmt.Println(source.Name())
}
Output: inline
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
internal
|
|
|
clone
Package clone deep-clones the string-keyed configuration domain so a caller cannot mutate a value it handed across a public ownership boundary.
|
Package clone deep-clones the string-keyed configuration domain so a caller cannot mutate a value it handed across a public ownership boundary. |
|
collision
Package collision detects sibling configuration keys whose canonical (separator- and case-insensitive) forms collide, within a single tree and recursively through nested objects and arrays of objects.
|
Package collision detects sibling configuration keys whose canonical (separator- and case-insensitive) forms collide, within a single tree and recursively through nested objects and arrays of objects. |
|
layers
Package layers builds and resolves the YAML configuration layers: it parses documents with two Viper instances, folds the overlay onto the base with MergeConfigMap after aligning cross-spelled keys canonically, resolves and validates the landscape token, and safely resolves a file-mode overlay path.
|
Package layers builds and resolves the YAML configuration layers: it parses documents with two Viper instances, folds the overlay onto the base with MergeConfigMap after aligning cross-spelled keys canonically, resolves and validates the landscape token, and safely resolves a file-mode overlay path. |
|
nilguard
Package nilguard reports whether an interface value is nil or a typed nil (an interface wrapping a nil pointer, map, slice, func, or channel), so a public API can reject a missing dependency with a deterministic error instead of panicking on it.
|
Package nilguard reports whether an interface value is nil or a typed nil (an interface wrapping a nil pointer, map, slice, func, or channel), so a public API can reject a missing dependency with a deterministic error instead of panicking on it. |
|
resource
Package resource derives the schema resource identifier a composed configuration block is mounted under.
|
Package resource derives the schema resource identifier a composed configuration block is mounted under. |
|
schemaview
Package schemaview turns an authored draft-2020-12 configuration schema into the form the validator actually evaluates, so that one equivalence relation governs both sides of validation.
|
Package schemaview turns an authored draft-2020-12 configuration schema into the form the validator actually evaluates, so that one equivalence relation governs both sides of validation. |
|
tree
Package tree resolves dotted configuration keys against a merged map using the family's canonical, casing-insensitive key matching.
|
Package tree resolves dotted configuration keys against a merged map using the family's canonical, casing-insensitive key matching. |
|
valid
Package valid compiles a composed JSON Schema, normalizes a configuration instance, validates it, and renders failures as problem-typed errors.
|
Package valid compiles a composed JSON Schema, normalizes a configuration instance, validates it, and renders failures as problem-typed errors. |