config

package
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 26, 2026 License: MIT Imports: 19 Imported by: 0

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

Examples

Constants

View Source
const AppKey = "app"

AppKey is the root property that carries the service-tree AppBlock.

View Source
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.

View Source
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

func FragmentFromType(model any) (map[string]any, error)

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

func GenerateSchema(model any) ([]byte, error)

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

func NewBlock(key string, required bool, fragment map[string]any) Block

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", &region)
	fmt.Println(region)
}
Output:
local

func NewConfig

func NewConfig(raw map[string]any) *Config

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

func (c *Config) App() (AppBlock, error)

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

func (c *Config) Decode(key string, target any) error

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

func (c *Config) Raw() map[string]any

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

func ValidationIssues(err error) ([]Issue, bool)

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

func (i Issue) String() 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

func NewLoader(options ...Option) *Loader

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

func (l *Loader) Load(ctx context.Context) (*Config, error)

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

func (s MapEnvSource) Environ(_ context.Context) (map[string]string, error)

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

func (OSEnvSource) Environ(_ context.Context) (map[string]string, error)

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

func WithBaseDir(dir string) Option

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

func WithEnvPrefix(prefix string) Option

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

func WithEnvSource(source EnvSource) Option

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

func WithLandscape(landscape string) Option

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

func WithSchema(schema Schema) Option

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

func ComposeSchema(blocks ...Block) Schema

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

func SchemaFromJSON(raw []byte) (Schema, error)

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

func (s Schema) Marshal() ([]byte, error)

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

func (s Schema) Root() map[string]any

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

func (s Schema) Validate(instance map[string]any) error

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

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL