config

package module
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Mar 10, 2026 License: BSD-2-Clause Imports: 15 Imported by: 0

README

Go Reference Actions Status Code Coverage Telegram EN Telegram RU

go-config: library to manage hierarchical configurations

About
go-config logo

go-config is a Go library that provides a uniform way to handle configurations with hierarchical inheritance, validation, and flexible merging strategies. It supports multiple data sources, key ordering preservation, and runtime modifications.

Overview

The library is designed for distributed systems where configuration often follows a hierarchical structure (e.g., global → group → replicaset → instance). It assembles configuration from multiple sources with priority-based merging and resolves effective configuration for any entity in the hierarchy.

Features
  • Hierarchical Configuration Inheritance: define multi-level hierarchies and resolve effective configuration for any leaf entity
  • Flexible Merge Strategies: choose how values are inherited — replace (default), append (for slices), or deep merge (for maps)
  • Fine-grained Exclusions: exclude specific keys from inheritance, either globally or from certain levels
  • Defaults: set default values that apply to every leaf entity unless overridden
  • JSON Schema Validation: validate configuration against JSON Schema or custom validators
  • Multiple Sources: load configuration from maps, files, directories, environment variables, or centralized key-value storages (etcd, TCS)
  • Order Preservation: maintain insertion order of keys when needed
  • Reactive Watch: monitor storage changes via the Watcher interface
  • Custom Mergers: full control over how collector values are merged into the configuration tree
Installation
go get github.com/tarantool/go-config
Quick Start
Basic Configuration from a Map
package main

import (
    "fmt"
    "log"

    "github.com/tarantool/go-config"
    "github.com/tarantool/go-config/collectors"
)

func main() {
    builder := config.NewBuilder()

    builder = builder.AddCollector(collectors.NewMap(map[string]any{
        "server": map[string]any{
            "host": "localhost",
            "port": 8080,
        },
        "database": map[string]any{
            "driver": "postgres",
            "port":   5432,
        },
    }).WithName("defaults"))

    cfg, errs := builder.Build()
    if len(errs) > 0 {
        log.Fatal(errs)
    }

    var host string
    _, _ = cfg.Get(config.NewKeyPath("server/host"), &host)
    fmt.Printf("Host: %s\n", host) // "localhost"

    var port int
    _, _ = cfg.Get(config.NewKeyPath("server/port"), &port)
    fmt.Printf("Port: %d\n", port) // 8080
}
Hierarchical Inheritance
package main

import (
    "fmt"
    "log"

    "github.com/tarantool/go-config"
    "github.com/tarantool/go-config/collectors"
)

func main() {
    builder := config.NewBuilder()

    builder = builder.AddCollector(collectors.NewMap(map[string]any{
        "replication": map[string]any{"failover": "manual"},
        "groups": map[string]any{
            "storages": map[string]any{
                "sharding": map[string]any{"roles": []any{"storage"}},
                "replicasets": map[string]any{
                    "s-001": map[string]any{
                        "leader": "s-001-a",
                        "instances": map[string]any{
                            "s-001-a": map[string]any{
                                "iproto": map[string]any{
                                    "listen": []any{map[string]any{"uri": "127.0.0.1:3301"}},
                                },
                            },
                        },
                    },
                },
            },
        },
    }).WithName("config"))

    // Register inheritance hierarchy.
    builder = builder.WithInheritance(
        config.Levels(config.Global, "groups", "replicasets", "instances"),
    )

    cfg, errs := builder.Build()
    if len(errs) > 0 {
        log.Fatal(errs)
    }

    // Resolve effective config for a specific instance.
    instanceCfg, err := cfg.Effective(
        config.NewKeyPath("groups/storages/replicasets/s-001/instances/s-001-a"),
    )
    if err != nil {
        log.Fatal(err)
    }

    var failover string
    _, _ = instanceCfg.Get(config.NewKeyPath("replication/failover"), &failover)
    fmt.Printf("Failover: %s\n", failover) // "manual" (inherited from global)

    var roles []string
    _, _ = instanceCfg.Get(config.NewKeyPath("sharding/roles"), &roles)
    fmt.Printf("Roles: %v\n", roles) // [storage] (inherited from group)
}
Tarantool Builder

The tarantool package provides a high-level builder with Tarantool defaults (env prefix, inheritance rules, schema validation):

package main

import (
    "context"
    "fmt"
    "log"

    "github.com/tarantool/go-config/tarantool"
)

func main() {
    cfg, err := tarantool.New().
        WithConfigFile("/etc/tarantool/config.yaml").
        WithoutSchema().
        Build(context.Background())
    if err != nil {
        log.Fatal(err)
    }

    var failover string
    instanceCfg, _ := cfg.Effective(
        config.NewKeyPath("groups/storages/replicasets/s-001/instances/s-001-a"),
    )
    _, _ = instanceCfg.Get(config.NewKeyPath("replication/failover"), &failover)
    fmt.Printf("Failover: %s\n", failover)
}
Collectors

Collectors are pluggable data sources. Each implements the Collector interface and streams configuration values via a channel.

Map Collector

Reads configuration from an in-memory map[string]any. Useful for defaults and tests.

File / Source Collector

Reads configuration from a single file (e.g., YAML) using the DataSource and Format interfaces.

Directory Collector

Reads all matching files from a directory (e.g., *.yaml). Each file is merged independently as a sub-collector. Supports recursive scanning.

Env Collector

Reads configuration from environment variables with a configurable prefix and key transformation.

Storage Collector

Reads multiple configuration documents from a centralized key-value storage (etcd, TCS) under a common prefix with integrity verification via go-storage.

Inheritance

Inheritance resolves effective configuration for leaf entities by merging values from all hierarchy levels (e.g., global → group → replicaset → instance). It supports:

  • Merge Strategies: MergeReplace (default), MergeAppend (for slices), MergeDeep (for maps)
  • Exclusions: WithNoInherit excludes keys entirely; WithNoInheritFrom excludes keys from specific levels
  • Defaults: WithDefaults applies default values with the lowest priority
builder = builder.WithInheritance(
    config.Levels(config.Global, "groups", "replicasets", "instances"),
    config.WithInheritMerge("roles", config.MergeAppend),
    config.WithInheritMerge("credentials", config.MergeDeep),
    config.WithNoInherit("leader"),
    config.WithDefaults(map[string]any{
        "replication": map[string]any{"failover": "manual"},
    }),
)
Validation

Configuration can be validated against a JSON Schema or a custom validator implementing the validator.Validator interface.

// JSON Schema validation.
builder, err := builder.WithJSONSchema(schemaReader)

// Custom validator.
builder = builder.WithValidator(myValidator)
Examples

Runnable examples are available in the root package as Example_* test functions. Run them all with go test -v -run Example ./....

Config API — example_config_test.go
Example Description
Example_basicGetAndLookup Core retrieval methods: Get, Lookup, and Stat
Example_walkConfig Iterating leaf values with Walk, depth control, and sub-paths
Example_sliceConfig Extracting a sub-configuration with Slice
Example_effectiveAll Resolving all leaf entities at once with EffectiveAll
Collectors — example_collectors_test.go
Example Description
Example_envCollector Environment variables with prefix, delimiter, and custom transform
Example_directoryCollector Reading YAML files from a directory, with recursive scanning
Example_fileSource Single-file reading via NewFile + NewSource
Example_storageCollector Key-value storage under a common prefix
Example_storageCollectorMultipleKeys Merging multiple storage keys
Example_storageCollectorWithMapOverride Combining storage and map collectors
Example_storageSource Using StorageSource as a DataSource
Example_storageSourceFetchStream Reading raw bytes from storage
Builder — example_builder_test.go
Example Description
Example_multipleCollectorPriority Priority-based merging across multiple collectors
Example_withJSONSchema WithJSONSchema and MustWithJSONSchema convenience APIs
Inheritance — example_inheritance_test.go
Example Description
Example_inheritanceBasic Hierarchical inheritance (global → group → replicaset → instance)
Example_inheritanceMergeStrategies MergeReplace, MergeAppend, and MergeDeep strategies
Example_inheritanceExclusions WithNoInherit and WithNoInheritFrom exclusions
Example_inheritanceDefaults Default values via WithDefaults
Custom Mergers — example_merger_test.go
Example Description
Example_validatingMerger Validating values before merging
Example_transformingMerger Transforming values based on path
Example_loggingMerger Logging all merge operations
Example_sourceBasedMerger Filtering by collector source
Validation — example_validation_test.go
Example Description
Example_validation JSON Schema validation
Example_customValidator Custom validator enforcing business rules
Storage — example_storage_test.go

See the Collectors table above for storage-related examples.

Contributing

Contributions are welcome! Please open an issue to discuss your ideas or submit a pull request.

License

This project is licensed under the BSD 2-Clause License – see the LICENSE file for details.

Documentation

Overview

Package config provides a uniform way to handle configurations with hierarchical inheritance, validation, and flexible merging strategies.

Key Features

  • Hierarchical Configuration Inheritance: define multi‑level hierarchies (global → group → replicaset → instance) and resolve effective configuration for any leaf entity.
  • Flexible Merge Strategies: choose how values are inherited: replace (default), append (for slices), or deep merge (for maps).
  • Fine‑grained Exclusions: exclude specific keys from inheritance, either globally or from certain levels.
  • Defaults: set default values that apply to every leaf entity unless overridden.
  • Validation: validate configuration with JSON Schema or custom validators.
  • Multiple Sources: load configuration from maps, files, environment variables, etc.
  • Order Preservation: maintain insertion order of keys when needed.

Quick Example

b := config.NewBuilder()
b = b.AddCollector(collectors.NewMap(map[string]any{
    "replication": map[string]any{"failover": "manual"},
    "groups": map[string]any{
        "storages": map[string]any{
            "sharding": map[string]any{"roles": []any{"storage"}},
            "replicasets": map[string]any{
                "s-001": map[string]any{
                    "leader": "s-001-a",
                    "instances": map[string]any{
                        "s-001-a": map[string]any{
                            "iproto": map[string]any{
                                "listen": []any{map[string]any{"uri": "127.0.0.1:3301"}},
                            },
                        },
                    },
                },
            },
        },
    },
}))

b = b.WithInheritance(
    config.Levels(config.Global, "groups", "replicasets", "instances"),
)

cfg, _ := b.Build(context.Background())
instanceCfg, _ := cfg.Effective(config.NewKeyPath("groups/storages/replicasets/s-001/instances/s-001-a"))

var failover string
instanceCfg.Get(config.NewKeyPath("replication/failover"), &failover)
fmt.Printf("Failover: %s\n", failover) // "manual" (inherited from global)

For detailed examples see the example_inheritance_test.go, example_merger_test.go, example_validation_test.go files.

The package supports configuration validation through the validator interface. See the validators subpackage for JSON Schema validation.

Example (BasicGetAndLookup)

Example_basicGetAndLookup demonstrates the core Config API methods: Get (extracts a typed value), Lookup (returns a Value without error on miss), and Stat (returns only metadata without touching the value).

data := map[string]any{
	"server": map[string]any{
		"host": "localhost",
		"port": 8080,
	},
}

builder := config.NewBuilder()

builder = builder.AddCollector(
	collectors.NewMap(data).
		WithName("app-config").
		WithSourceType(config.FileSource).
		WithRevision("v1.0"),
)

cfg, errs := builder.Build(context.Background())
if len(errs) > 0 {
	fmt.Printf("Build errors: %v\n", errs)
	return
}

// Get extracts a typed value and returns metadata.
var host string

meta, err := cfg.Get(config.NewKeyPath("server/host"), &host)
if err != nil {
	fmt.Printf("Get error: %v\n", err)
	return
}

fmt.Printf("Host: %s\n", host)
fmt.Printf("Source: %s\n", meta.Source.Name)
fmt.Printf("Revision: %s\n", meta.Revision)

// Get returns an error for missing keys.
var missing string

_, err = cfg.Get(config.NewKeyPath("server/timeout"), &missing)
fmt.Printf("Missing key error: %v\n", err)

// Lookup returns (Value, bool) — no error on miss.
val, ok := cfg.Lookup(config.NewKeyPath("server/port"))
fmt.Printf("Port found: %v\n", ok)

if ok {
	var port int

	_ = val.Get(&port)
	fmt.Printf("Port: %d\n", port)
}

_, ok = cfg.Lookup(config.NewKeyPath("server/missing"))
fmt.Printf("Missing found: %v\n", ok)

// Stat returns metadata without extracting the value.
statMeta, ok := cfg.Stat(config.NewKeyPath("server/host"))
fmt.Printf("Stat found: %v\n", ok)
fmt.Printf("Stat source: %s\n", statMeta.Source.Name)
Output:
Host: localhost
Source: app-config
Revision: v1.0
Missing key error: key not found: server/timeout
Port found: true
Port: 8080
Missing found: false
Stat found: true
Stat source: app-config
Example (CustomValidator)

Example_customValidator demonstrates a custom validator that enforces business rules.

val := &requiredFieldValidator{}

// Configuration missing required field.
data := map[string]any{
	"port": 8080,
}

builder := config.NewBuilder()

builder = builder.WithValidator(val)
builder = builder.AddCollector(collectors.NewMap(data).WithName("config"))

_, errs := builder.Build(context.Background())
if len(errs) > 0 {
	fmt.Printf("Missing service: %v\n", strings.Contains(errs[0].Error(), "service"))
}

// Configuration with required field.
dataWithService := map[string]any{
	"service": map[string]any{
		"name": "api",
	},
	"port": 8080,
}

builder = config.NewBuilder()
builder = builder.WithValidator(val)
builder = builder.AddCollector(collectors.NewMap(dataWithService).WithName("config"))

cfg, errs := builder.Build(context.Background())
if len(errs) > 0 {
	fmt.Printf("Unexpected errors: %v\n", errs)
} else {
	var serviceName string

	_, _ = cfg.Get(config.NewKeyPath("service/name"), &serviceName)
	fmt.Printf("Service name: %s\n", serviceName)
}
Output:
Missing service: true
Service name: api
Example (DirectoryCollector)

Example_directoryCollector demonstrates the Directory collector which reads all configuration files with a given extension from a directory and merges them into a unified configuration tree.

// Create a temporary directory with configuration files.
dir, err := os.MkdirTemp("", "go-config-example-*")
if err != nil {
	fmt.Printf("MkdirTemp error: %v\n", err)
	return
}

defer func() { _ = os.RemoveAll(dir) }()

// Write YAML configuration files.
err = os.WriteFile(filepath.Join(dir, "app.yaml"),
	[]byte("app:\n  name: myservice\n  port: 8080\n"), 0o600)
if err != nil {
	fmt.Printf("WriteFile error: %v\n", err)
	return
}

err = os.WriteFile(filepath.Join(dir, "db.yaml"),
	[]byte("database:\n  host: postgres\n  port: 5432\n"), 0o600)
if err != nil {
	fmt.Printf("WriteFile error: %v\n", err)
	return
}

// Create a subdirectory with another config file.
subdir := filepath.Join(dir, "extra")

err = os.MkdirAll(subdir, 0o750)
if err != nil {
	fmt.Printf("MkdirAll error: %v\n", err)
	return
}

err = os.WriteFile(filepath.Join(subdir, "cache.yaml"),
	[]byte("cache:\n  ttl: 300\n"), 0o600)
if err != nil {
	fmt.Printf("WriteFile error: %v\n", err)
	return
}

// Read only top-level files (non-recursive).
collector := collectors.NewDirectory(dir, ".yaml", collectors.NewYamlFormat()).
	WithName("config")

builder := config.NewBuilder()

builder = builder.AddCollector(collector)

cfg, errs := builder.Build(context.Background())
if len(errs) > 0 {
	fmt.Printf("Build errors: %v\n", errs)
	return
}

var appName string

_, err = cfg.Get(config.NewKeyPath("app/name"), &appName)
if err != nil {
	fmt.Printf("Get error: %v\n", err)
	return
}

fmt.Printf("App: %s\n", appName)

var dbHost string

_, err = cfg.Get(config.NewKeyPath("database/host"), &dbHost)
if err != nil {
	fmt.Printf("Get error: %v\n", err)
	return
}

fmt.Printf("DB host: %s\n", dbHost)

// Subdirectory files are not read without recursive mode.
_, ok := cfg.Lookup(config.NewKeyPath("cache/ttl"))
fmt.Printf("Cache TTL found (non-recursive): %v\n", ok)

// Enable recursive scanning to include subdirectories.
recursiveCollector := collectors.NewDirectory(dir, ".yaml", collectors.NewYamlFormat()).
	WithRecursive(true)

builder = config.NewBuilder()

builder = builder.AddCollector(recursiveCollector)

cfg, errs = builder.Build(context.Background())
if len(errs) > 0 {
	fmt.Printf("Build errors: %v\n", errs)
	return
}

var ttl int

_, err = cfg.Get(config.NewKeyPath("cache/ttl"), &ttl)
if err != nil {
	fmt.Printf("Get error: %v\n", err)
	return
}

fmt.Printf("Cache TTL (recursive): %d\n", ttl)
Output:
App: myservice
DB host: postgres
Cache TTL found (non-recursive): false
Cache TTL (recursive): 300
Example (EffectiveAll)

Example_effectiveAll demonstrates Config.EffectiveAll() which resolves effective configurations for ALL leaf entities in the hierarchy at once.

data := map[string]any{
	"replication": map[string]any{"failover": "manual"},
	"groups": map[string]any{
		"storages": map[string]any{
			"sharding": map[string]any{"roles": []any{"storage"}},
			"replicasets": map[string]any{
				"s-001": map[string]any{
					"leader": "s-001-a",
					"instances": map[string]any{
						"s-001-a": map[string]any{
							"iproto": map[string]any{"listen": "127.0.0.1:3301"},
						},
						"s-001-b": map[string]any{
							"iproto": map[string]any{"listen": "127.0.0.1:3302"},
						},
					},
				},
			},
		},
	},
}

builder := config.NewBuilder()

builder = builder.AddCollector(collectors.NewMap(data).WithName("config"))

builder = builder.WithInheritance(
	config.Levels(config.Global, "groups", "replicasets", "instances"),
)

cfg, errs := builder.Build(context.Background())
if len(errs) > 0 {
	fmt.Printf("Build errors: %v\n", errs)
	return
}

// EffectiveAll resolves all leaf entities at once.
allConfigs, err := cfg.EffectiveAll()
if err != nil {
	fmt.Printf("EffectiveAll error: %v\n", err)
	return
}

// Sort keys for stable output.
keys := make([]string, 0, len(allConfigs))
for k := range allConfigs {
	keys = append(keys, k)
}

sort.Strings(keys)

for _, key := range keys {
	instanceCfg := allConfigs[key]

	var listen string

	_, err := instanceCfg.Get(config.NewKeyPath("iproto/listen"), &listen)
	if err != nil {
		fmt.Printf("Get error: %v\n", err)
		continue
	}

	var failover string

	_, err = instanceCfg.Get(config.NewKeyPath("replication/failover"), &failover)
	if err != nil {
		fmt.Printf("Get error: %v\n", err)
		continue
	}

	fmt.Printf("%s: listen=%s failover=%s\n", key, listen, failover)
}
Output:
groups/storages/replicasets/s-001/instances/s-001-a: listen=127.0.0.1:3301 failover=manual
groups/storages/replicasets/s-001/instances/s-001-b: listen=127.0.0.1:3302 failover=manual
Example (EnvCollector)

Example_envCollector demonstrates the Env collector which reads configuration from environment variables. It supports prefix filtering, custom delimiters, and custom key transformation functions.

// Set environment variables with a unique prefix.
_ = os.Setenv("EXAMPLEAPP_DB_HOST", "localhost")
_ = os.Setenv("EXAMPLEAPP_DB_PORT", "5432")

defer func() { _ = os.Unsetenv("EXAMPLEAPP_DB_HOST") }()
defer func() { _ = os.Unsetenv("EXAMPLEAPP_DB_PORT") }()

// Basic usage: prefix strips "EXAMPLEAPP_", underscore splits into hierarchy.
envCollector := collectors.NewEnv().
	WithPrefix("EXAMPLEAPP_").
	WithDelimiter("_").
	WithName("env")

builder := config.NewBuilder()

builder = builder.AddCollector(envCollector)

cfg, errs := builder.Build(context.Background())
if len(errs) > 0 {
	fmt.Printf("Build errors: %v\n", errs)
	return
}

var host string

_, err := cfg.Get(config.NewKeyPath("db/host"), &host)
if err != nil {
	fmt.Printf("Get error: %v\n", err)
	return
}

fmt.Printf("DB host: %s\n", host)

var port string

_, err = cfg.Get(config.NewKeyPath("db/port"), &port)
if err != nil {
	fmt.Printf("Get error: %v\n", err)
	return
}

fmt.Printf("DB port: %s\n", port)

// Custom transform: convert env var names to a custom key path.
_ = os.Setenv("EXAMPLEAPP_SERVER__HOST", "0.0.0.0")

defer func() { _ = os.Unsetenv("EXAMPLEAPP_SERVER__HOST") }()

transformCollector := collectors.NewEnv().
	WithPrefix("EXAMPLEAPP_SERVER__").
	WithTransform(func(key string) config.KeyPath {
		// Use double underscore as separator, preserve case.
		parts := strings.Split(strings.ToLower(key), "__")
		return config.NewKeyPathFromSegments(parts)
	}).
	WithName("env-transform")

builder = config.NewBuilder()

builder = builder.AddCollector(transformCollector)

cfg, errs = builder.Build(context.Background())
if len(errs) > 0 {
	fmt.Printf("Build errors: %v\n", errs)
	return
}

var serverHost string

_, err = cfg.Get(config.NewKeyPath("host"), &serverHost)
if err != nil {
	fmt.Printf("Get error: %v\n", err)
	return
}

fmt.Printf("Server host: %s\n", serverHost)
Output:
DB host: localhost
DB port: 5432
Server host: 0.0.0.0
Example (FileSource)

Example_fileSource demonstrates reading configuration from a single YAML file using collectors.NewFile() as a DataSource with collectors.NewSource().

// Create a temporary YAML file.
dir, err := os.MkdirTemp("", "go-config-file-example-*")
if err != nil {
	fmt.Printf("MkdirTemp error: %v\n", err)
	return
}

defer func() { _ = os.RemoveAll(dir) }()

filePath := filepath.Join(dir, "config.yaml")

err = os.WriteFile(filePath,
	[]byte("server:\n  host: localhost\n  port: 8080\nlog_level: info\n"), 0o600)
if err != nil {
	fmt.Printf("WriteFile error: %v\n", err)
	return
}

// Create a File data source and wrap it with a YAML format.
file := collectors.NewFile(filePath)

collector, err := collectors.NewSource(file, collectors.NewYamlFormat())
if err != nil {
	fmt.Printf("NewSource error: %v\n", err)
	return
}

builder := config.NewBuilder()

builder = builder.AddCollector(collector)

cfg, errs := builder.Build(context.Background())
if len(errs) > 0 {
	fmt.Printf("Build errors: %v\n", errs)
	return
}

var host string

_, err = cfg.Get(config.NewKeyPath("server/host"), &host)
if err != nil {
	fmt.Printf("Get error: %v\n", err)
	return
}

fmt.Printf("Host: %s\n", host)

var port int

_, err = cfg.Get(config.NewKeyPath("server/port"), &port)
if err != nil {
	fmt.Printf("Get error: %v\n", err)
	return
}

fmt.Printf("Port: %d\n", port)

var logLevel string

_, err = cfg.Get(config.NewKeyPath("log_level"), &logLevel)
if err != nil {
	fmt.Printf("Get error: %v\n", err)
	return
}

fmt.Printf("Log level: %s\n", logLevel)
Output:
Host: localhost
Port: 8080
Log level: info
Example (InheritanceBasic)

Example_inheritanceBasic demonstrates hierarchical configuration inheritance. It shows how to define a hierarchy (global → groups → replicasets → instances) and resolve effective configuration for a leaf entity.

builder := config.NewBuilder()

// Build configuration with hierarchy.
builder = builder.AddCollector(collectors.NewMap(map[string]any{
	"credentials": map[string]any{
		"users": map[string]any{
			"replicator": map[string]any{
				"password": "secret",
				"roles":    []any{"replication"},
			},
		},
	},
	"replication": map[string]any{"failover": "manual"},
	"groups": map[string]any{
		"storages": map[string]any{
			"sharding": map[string]any{"roles": []any{"storage"}},
			"replicasets": map[string]any{
				"s-001": map[string]any{
					"leader": "s-001-a",
					"instances": map[string]any{
						"s-001-a": map[string]any{
							"iproto": map[string]any{
								"listen": []any{map[string]any{"uri": "127.0.0.1:3301"}},
							},
						},
					},
				},
			},
		},
	},
}).WithName("config"))

// Register inheritance hierarchy.
builder = builder.WithInheritance(
	config.Levels(config.Global, "groups", "replicasets", "instances"),
)

cfg, errs := builder.Build(context.Background())
if len(errs) > 0 {
	fmt.Printf("Build errors: %v\n", errs)
	return
}

// Get effective config for a specific instance.
instanceCfg, err := cfg.Effective(config.NewKeyPath("groups/storages/replicasets/s-001/instances/s-001-a"))
if err != nil {
	fmt.Printf("Effective error: %v\n", err)
	return
}

// Retrieve inherited values.
var failover string

_, err = instanceCfg.Get(config.NewKeyPath("replication/failover"), &failover)
if err != nil {
	fmt.Printf("Get failover error: %v\n", err)
} else {
	fmt.Printf("Failover: %s\n", failover)
}

var roles []string

_, err = instanceCfg.Get(config.NewKeyPath("sharding/roles"), &roles)
if err != nil {
	fmt.Printf("Get roles error: %v\n", err)
} else {
	fmt.Printf("Roles: %v\n", roles)
}

var leader string

_, err = instanceCfg.Get(config.NewKeyPath("leader"), &leader)
if err != nil {
	fmt.Printf("Get leader error: %v\n", err)
} else {
	fmt.Printf("Leader: %s\n", leader)
}
Output:
Failover: manual
Roles: [storage]
Leader: s-001-a
Example (InheritanceDefaults)

Example_inheritanceDefaults demonstrates how to set default values that apply to every leaf entity unless overridden.

builder := config.NewBuilder()

// Minimal configuration with replicaset and an instance.
builder = builder.AddCollector(collectors.NewMap(map[string]any{
	"groups": map[string]any{
		"storages": map[string]any{
			"replicasets": map[string]any{
				"s-001": map[string]any{
					"leader": "s-001-a",
					"instances": map[string]any{
						"s-001-a": map[string]any{},
					},
				},
			},
		},
	},
}).WithName("config"))

// Register hierarchy with defaults.
builder = builder.WithInheritance(
	config.Levels(config.Global, "groups", "replicasets", "instances"),
	config.WithDefaults(map[string]any{
		"replication": map[string]any{"failover": "manual"},
		"snapshot":    map[string]any{"dir": "/default/snapshots"},
	}),
)

cfg, errs := builder.Build(context.Background())
if len(errs) > 0 {
	fmt.Printf("Build errors: %v\n", errs)
	return
}

// Get effective config for instance (leaf entity).
instanceCfg, err := cfg.Effective(config.NewKeyPath("groups/storages/replicasets/s-001/instances/s-001-a"))
if err != nil {
	fmt.Printf("Effective error: %v\n", err)
	return
}

var failover string

_, err = instanceCfg.Get(config.NewKeyPath("replication/failover"), &failover)
if err != nil {
	fmt.Printf("Get failover error: %v\n", err)
} else {
	fmt.Printf("Failover (default): %s\n", failover)
}

var snapshotDir string

_, err = instanceCfg.Get(config.NewKeyPath("snapshot/dir"), &snapshotDir)
if err != nil {
	fmt.Printf("Get snapshot.dir error: %v\n", err)
} else {
	fmt.Printf("Snapshot dir (default): %s\n", snapshotDir)
}

var leader string

_, err = instanceCfg.Get(config.NewKeyPath("leader"), &leader)
if err != nil {
	fmt.Printf("Get leader error: %v\n", err)
} else {
	fmt.Printf("Leader (inherited from replicaset): %s\n", leader)
}
Output:
Failover (default): manual
Snapshot dir (default): /default/snapshots
Leader (inherited from replicaset): s-001-a
Example (InheritanceExclusions)

Example_inheritanceExclusions demonstrates how to exclude certain keys from inheritance using WithNoInherit and WithNoInheritFrom.

builder := config.NewBuilder()

// Configuration with global, group, replicaset, and instance values.
builder = builder.AddCollector(collectors.NewMap(map[string]any{
	"snapshot": map[string]any{"dir": "/global/snapshots"},
	"groups": map[string]any{
		"storages": map[string]any{
			"snapshot": map[string]any{"dir": "/group/snapshots"},
			"leader":   "group-leader", // Excluded from inheritance.
			"replicasets": map[string]any{
				"s-001": map[string]any{
					"leader": "replicaset-leader",
					"instances": map[string]any{
						"s-001-a": map[string]any{
							"iproto": map[string]any{
								"listen": []any{map[string]any{"uri": "127.0.0.1:3301"}},
							},
						},
					},
				},
			},
		},
	},
}).WithName("config"))

// Register hierarchy with exclusions.
builder = builder.WithInheritance(
	config.Levels(config.Global, "groups", "replicasets", "instances"),
	config.WithNoInherit("leader"),                          // Leader never inherited.
	config.WithNoInheritFrom(config.Global, "snapshot.dir"), // Global snapshot.dir not inherited.
)

cfg, errs := builder.Build(context.Background())
if len(errs) > 0 {
	fmt.Printf("Build errors: %v\n", errs)
	return
}

// Get effective config for instance (leaf entity).
instanceCfg, err := cfg.Effective(config.NewKeyPath("groups/storages/replicasets/s-001/instances/s-001-a"))
if err != nil {
	fmt.Printf("Effective error: %v\n", err)
	return
}

var snapshotDir string

_, err = instanceCfg.Get(config.NewKeyPath("snapshot/dir"), &snapshotDir)
if err != nil {
	fmt.Printf("Get snapshot.dir error: %v\n", err)
} else {
	fmt.Printf("Snapshot dir (global excluded, group inherited): %s\n", snapshotDir)
}

var leader string

_, err = instanceCfg.Get(config.NewKeyPath("leader"), &leader)
if err != nil {
	fmt.Printf("Get leader error: %v\n", err)
} else {
	fmt.Printf("Leader (not inherited from group, replicaset value): %s\n", leader)
}
Output:
Snapshot dir (global excluded, group inherited): /group/snapshots
Get leader error: key not found: leader
Example (InheritanceMergeStrategies)

Example_inheritanceMergeStrategies demonstrates different merge strategies during inheritance: MergeReplace (default), MergeAppend, and MergeDeep.

builder := config.NewBuilder()

// Configuration with values at group, replicaset, and instance levels.
builder = builder.AddCollector(collectors.NewMap(map[string]any{
	"groups": map[string]any{
		"storages": map[string]any{
			"roles": []any{"storage"}, // Slice for append.
			"credentials": map[string]any{ // Map for deep merge.
				"users": map[string]any{
					"admin": map[string]any{"password": "admin123"},
				},
			},
			"replicasets": map[string]any{
				"s-001": map[string]any{
					"roles": []any{"metrics"}, // Append to parent slice.
					"credentials": map[string]any{ // Deep merge with parent map.
						"users": map[string]any{
							"monitor": map[string]any{"password": "monitor123"},
						},
					},
					"leader": "s-001-a", // Replace (default).
					"instances": map[string]any{
						"s-001-a": map[string]any{
							"roles": []any{"cache"}, // Further append.
							"credentials": map[string]any{ // Deep merge.
								"users": map[string]any{
									"operator": map[string]any{"password": "op123"},
								},
							},
							"iproto": map[string]any{
								"listen": []any{map[string]any{"uri": "127.0.0.1:3301"}},
							},
						},
					},
				},
			},
		},
	},
}).WithName("config"))

// Register hierarchy with custom merge strategies.
builder = builder.WithInheritance(
	config.Levels(config.Global, "groups", "replicasets", "instances"),
	config.WithInheritMerge("roles", config.MergeAppend),
	config.WithInheritMerge("credentials", config.MergeDeep),
)

cfg, errs := builder.Build(context.Background())
if len(errs) > 0 {
	fmt.Printf("Build errors: %v\n", errs)
	return
}

// Get effective config for instance (leaf entity).
instanceCfg, err := cfg.Effective(config.NewKeyPath("groups/storages/replicasets/s-001/instances/s-001-a"))
if err != nil {
	fmt.Printf("Effective error: %v\n", err)
	return
}

var roles []string

_, err = instanceCfg.Get(config.NewKeyPath("roles"), &roles)
if err != nil {
	fmt.Printf("Get roles error: %v\n", err)
} else {
	fmt.Printf("Roles (appended across 3 levels): %v\n", roles)
}

// Check merged credentials from all three levels.
var adminPass, monitorPass, operatorPass string

_, err = instanceCfg.Get(config.NewKeyPath("credentials/users/admin/password"), &adminPass)
if err != nil {
	fmt.Printf("Get admin password error: %v\n", err)
} else {
	fmt.Printf("Admin password (from group): %s\n", adminPass)
}

_, err = instanceCfg.Get(config.NewKeyPath("credentials/users/monitor/password"), &monitorPass)
if err != nil {
	fmt.Printf("Get monitor password error: %v\n", err)
} else {
	fmt.Printf("Monitor password (from replicaset): %s\n", monitorPass)
}

_, err = instanceCfg.Get(config.NewKeyPath("credentials/users/operator/password"), &operatorPass)
if err != nil {
	fmt.Printf("Get operator password error: %v\n", err)
} else {
	fmt.Printf("Operator password (from instance): %s\n", operatorPass)
}

var leader string

_, err = instanceCfg.Get(config.NewKeyPath("leader"), &leader)
if err != nil {
	fmt.Printf("Get leader error: %v\n", err)
} else {
	fmt.Printf("Leader (replaced from replicaset): %s\n", leader)
}
Output:
Roles (appended across 3 levels): [storage metrics cache]
Admin password (from group): admin123
Monitor password (from replicaset): monitor123
Operator password (from instance): op123
Leader (replaced from replicaset): s-001-a
Example (LoggingMerger)

Example_loggingMerger demonstrates a custom merger that logs all merge operations for auditing and debugging purposes.

// Create a logging merger that tracks operations.
logger := &loggingMerger{
	prefix: "[CONFIG]",
}

data := map[string]any{
	"database": map[string]any{
		"host": "localhost",
		"port": 5432,
	},
}

builder := config.NewBuilder()

builder = builder.WithMerger(logger)
builder = builder.AddCollector(collectors.NewMap(data).WithName("db-config"))

cfg, _ := builder.Build(context.Background())

var host string

_, _ = cfg.Get(config.NewKeyPath("database/host"), &host)
fmt.Printf("Host: %s\n", host)
Output:
[CONFIG] Merging database/host = localhost (from db-config)
[CONFIG] Merging database/port = 5432 (from db-config)
Host: localhost
Example (MultipleCollectorPriority)

Example_multipleCollectorPriority demonstrates priority-based merging across multiple collectors, where later collectors override earlier ones.

// First collector: defaults (lowest priority).
defaults := collectors.NewMap(map[string]any{
	"server": map[string]any{
		"host":    "0.0.0.0",
		"port":    8080,
		"timeout": 30,
	},
	"log_level": "info",
}).WithName("defaults")

// Second collector: environment-specific overrides.
envOverrides := collectors.NewMap(map[string]any{
	"server": map[string]any{
		"host": "prod.example.com",
		"port": 443,
	},
	"log_level": "warn",
}).WithName("production")

// Third collector: local overrides (highest priority).
localOverrides := collectors.NewMap(map[string]any{
	"log_level": "debug",
}).WithName("local")

// Build with collectors in priority order (first = lowest, last = highest).
builder := config.NewBuilder()

builder = builder.AddCollector(defaults)
builder = builder.AddCollector(envOverrides)
builder = builder.AddCollector(localOverrides)

cfg, errs := builder.Build(context.Background())
if len(errs) > 0 {
	fmt.Printf("Build errors: %v\n", errs)
	return
}

// "host" and "port" overridden by production, "timeout" from defaults.
var host string

_, _ = cfg.Get(config.NewKeyPath("server/host"), &host)
fmt.Printf("Host: %s\n", host)

var port int

_, _ = cfg.Get(config.NewKeyPath("server/port"), &port)
fmt.Printf("Port: %d\n", port)

var timeout int

_, _ = cfg.Get(config.NewKeyPath("server/timeout"), &timeout)
fmt.Printf("Timeout: %d\n", timeout)

// "log_level" overridden by local (highest priority).
var logLevel string

meta, _ := cfg.Get(config.NewKeyPath("log_level"), &logLevel)
fmt.Printf("Log level: %s (from %s)\n", logLevel, meta.Source.Name)
Output:
Host: prod.example.com
Port: 443
Timeout: 30
Log level: debug (from local)
Example (SliceConfig)

Example_sliceConfig demonstrates Config.Slice() for extracting a sub-configuration as a separate Config object.

data := map[string]any{
	"server": map[string]any{
		"http": map[string]any{
			"port": 8080,
			"host": "0.0.0.0",
		},
		"grpc": map[string]any{
			"port": 9090,
		},
	},
}

builder := config.NewBuilder()

builder = builder.AddCollector(collectors.NewMap(data).WithName("config"))

cfg, errs := builder.Build(context.Background())
if len(errs) > 0 {
	fmt.Printf("Build errors: %v\n", errs)
	return
}

// Slice extracts "server/http" as a standalone Config.
httpCfg, err := cfg.Slice(config.NewKeyPath("server/http"))
if err != nil {
	fmt.Printf("Slice error: %v\n", err)
	return
}

// Access values relative to the sliced root.
var port int

_, err = httpCfg.Get(config.NewKeyPath("port"), &port)
if err != nil {
	fmt.Printf("Get error: %v\n", err)
	return
}

fmt.Printf("HTTP port: %d\n", port)

var host string

_, err = httpCfg.Get(config.NewKeyPath("host"), &host)
if err != nil {
	fmt.Printf("Get error: %v\n", err)
	return
}

fmt.Printf("HTTP host: %s\n", host)

// Slice returns an error for non-existent paths.
_, err = cfg.Slice(config.NewKeyPath("nonexistent"))
fmt.Printf("Nonexistent slice error: %v\n", err)
Output:
HTTP port: 8080
HTTP host: 0.0.0.0
Nonexistent slice error: path not found: nonexistent
Example (SourceBasedMerger)

Example_sourceBasedMerger demonstrates a custom merger that applies different merging strategies based on the collector source.

// Create a merger that only accepts values from specific sources.
sourceMerger := &sourceBasedMerger{
	allowedSources: map[string]bool{
		"production": true,
		"staging":    true,
	},
}

prodData := map[string]any{
	"api": map[string]any{
		"key": "prod-key-123",
	},
}

devData := map[string]any{
	"api": map[string]any{
		"key": "dev-key-456",
	},
}

builder := config.NewBuilder()

builder = builder.WithMerger(sourceMerger)
builder = builder.AddCollector(collectors.NewMap(prodData).WithName("production"))
builder = builder.AddCollector(collectors.NewMap(devData).WithName("development"))

cfg, _ := builder.Build(context.Background())

var apiKey string

_, _ = cfg.Get(config.NewKeyPath("api/key"), &apiKey)

// Only production data was merged.
fmt.Printf("API Key: %s\n", apiKey)
Output:
API Key: prod-key-123
Example (StorageCollector)

Example_storageCollector demonstrates reading multiple configuration documents from a key-value storage under a common prefix using the Storage collector.

// Set up in-memory storage with configuration data.
mock := testutil.NewMockStorage()
testutil.PutIntegrity(mock, "/config/", "app", []byte("port: 8080\nhost: localhost"))

// Create an integrity-typed wrapper and the Storage collector.
typed := testutil.NewRawTyped(mock, "/config/")
collector := collectors.NewStorage(typed, "/config/", collectors.NewYamlFormat())

// Build configuration.
builder := config.NewBuilder()

builder = builder.AddCollector(collector)

cfg, errs := builder.Build(context.Background())
if len(errs) > 0 {
	fmt.Printf("Build errors: %v\n", errs)
	return
}

var port int

_, err := cfg.Get(config.NewKeyPath("port"), &port)
if err != nil {
	fmt.Printf("Get port error: %v\n", err)
	return
}

var host string

_, err = cfg.Get(config.NewKeyPath("host"), &host)
if err != nil {
	fmt.Printf("Get host error: %v\n", err)
	return
}

fmt.Printf("Host: %s\n", host)
fmt.Printf("Port: %d\n", port)
Output:
Host: localhost
Port: 8080
Example (StorageCollectorMultipleKeys)

Example_storageCollectorMultipleKeys demonstrates reading and merging multiple keys from storage into a unified configuration tree. Key names are used only for distinguishing documents; the YAML content determines the tree structure.

mock := testutil.NewMockStorage()
testutil.PutIntegrity(mock, "/config/", "cfg-servers",
	[]byte("server:\n  port: 8080\n  host: localhost"))
testutil.PutIntegrity(mock, "/config/", "cfg-database",
	[]byte("database:\n  driver: postgres\n  port: 5432"))

typed := testutil.NewRawTyped(mock, "/config/")
collector := collectors.NewStorage(typed, "/config/", collectors.NewYamlFormat())

builder := config.NewBuilder()

builder = builder.AddCollector(collector)

cfg, errs := builder.Build(context.Background())
if len(errs) > 0 {
	fmt.Printf("Build errors: %v\n", errs)
	return
}

var host string

_, err := cfg.Get(config.NewKeyPath("server/host"), &host)
if err != nil {
	fmt.Printf("Get error: %v\n", err)
	return
}

fmt.Printf("server host: %s\n", host)

var driver string

_, err = cfg.Get(config.NewKeyPath("database/driver"), &driver)
if err != nil {
	fmt.Printf("Get error: %v\n", err)
	return
}

fmt.Printf("database driver: %s\n", driver)
Output:
server host: localhost
database driver: postgres
Example (StorageCollectorWithMapOverride)

Example_storageCollectorWithMapOverride demonstrates combining a Storage collector with a Map collector, where later collectors override earlier ones.

mock := testutil.NewMockStorage()
testutil.PutIntegrity(mock, "/config/", "db",
	[]byte("db:\n  host: storage-host\n  port: 5432"))

typed := testutil.NewRawTyped(mock, "/config/")
storageCollector := collectors.NewStorage(typed, "/config/", collectors.NewYamlFormat())

// Map collector provides defaults; storage collector overrides the host.
mapCollector := collectors.NewMap(map[string]any{
	"db/host": "override-host",
})

builder := config.NewBuilder()

builder = builder.AddCollector(mapCollector)
builder = builder.AddCollector(storageCollector)

cfg, errs := builder.Build(context.Background())
if len(errs) > 0 {
	fmt.Printf("Build errors: %v\n", errs)
	return
}

var host string

_, err := cfg.Get(config.NewKeyPath("db/host"), &host)
if err != nil {
	fmt.Printf("Get error: %v\n", err)
	return
}

fmt.Printf("Host: %s\n", host)
Output:
Host: storage-host
Example (StorageSource)

Example_storageSource demonstrates using StorageSource as a DataSource to read a single configuration document from storage with integrity verification.

mock := testutil.NewMockStorage()
testutil.PutIntegrity(mock, "/config/", "app",
	[]byte("server:\n  port: 8080\n  host: localhost"))

// Create a StorageSource for a single key.
source := collectors.NewStorageSource(mock, "/config/", "app", nil, nil)

// Use it with a format to build a collector.
collector, err := collectors.NewSource(source, collectors.NewYamlFormat())
if err != nil {
	fmt.Printf("NewSource error: %v\n", err)
	return
}

builder := config.NewBuilder()

builder = builder.AddCollector(collector)

cfg, errs := builder.Build(context.Background())
if len(errs) > 0 {
	fmt.Printf("Build errors: %v\n", errs)
	return
}

var port int

_, err = cfg.Get(config.NewKeyPath("server/port"), &port)
if err != nil {
	fmt.Printf("Get error: %v\n", err)
	return
}

fmt.Printf("Server port: %d\n", port)
Output:
Server port: 8080
Example (StorageSourceFetchStream)

Example_storageSourceFetchStream demonstrates using StorageSource.FetchStream to read raw configuration bytes from storage.

package main

import (
	"context"
	"fmt"
	"io"

	"github.com/tarantool/go-config/collectors"
	"github.com/tarantool/go-config/internal/testutil"
)

func main() {
	mock := testutil.NewMockStorage()
	testutil.PutIntegrity(mock, "/config/", "app", []byte("key: value"))

	source := collectors.NewStorageSource(mock, "/config/", "app", nil, nil)

	ctx := context.Background()

	reader, err := source.FetchStream(ctx)
	if err != nil {
		fmt.Printf("FetchStream error: %v\n", err)
		return
	}

	defer func() { _ = reader.Close() }()

	data, err := io.ReadAll(reader)
	if err != nil {
		fmt.Printf("ReadAll error: %v\n", err)
		return
	}

	fmt.Printf("Data: %s\n", string(data))
	fmt.Printf("Revision: %s\n", source.Revision())

}
Output:
Data: key: value
Revision: 1
Example (TransformingMerger)

Example_transformingMerger demonstrates a custom merger that transforms values based on their path or content before merging.

// Create a transformer that normalizes string values.
transformer := &transformingMerger{
	transforms: map[string]func(any) any{
		"name": func(v any) any {
			if s, ok := v.(string); ok {
				return strings.TrimSpace(strings.ToLower(s))
			}

			return v
		},
		"email": func(v any) any {
			if s, ok := v.(string); ok {
				return strings.TrimSpace(strings.ToLower(s))
			}

			return v
		},
	},
}

data := map[string]any{
	"user": map[string]any{
		"name":  "  John Doe  ",
		"email": "  JOHN.DOE@EXAMPLE.COM  ",
		"age":   30,
	},
}

builder := config.NewBuilder()

builder = builder.WithMerger(transformer)
builder = builder.AddCollector(collectors.NewMap(data).WithName("user-data"))

cfg, _ := builder.Build(context.Background())

var name, email string

_, _ = cfg.Get(config.NewKeyPath("user/name"), &name)
_, _ = cfg.Get(config.NewKeyPath("user/email"), &email)

fmt.Printf("Name: %s\n", name)
fmt.Printf("Email: %s\n", email)
Output:
Name: john doe
Email: john.doe@example.com
Example (ValidatingMerger)

Example_validatingMerger demonstrates a custom merger that validates configuration values before merging them into the tree.

// Create a validating merger that enforces constraints.
validator := &validatingMerger{
	rules: map[string]func(any) error{
		"port": func(v any) error {
			port, ok := v.(int)
			switch {
			case !ok:
				return errors.New("port must be an integer")
			case port < 1 || port > 65535:
				return fmt.Errorf("port must be between 1 and 65535, got %d", port)
			}

			return nil
		},
		"timeout": func(v any) error {
			timeout, ok := v.(int)
			switch {
			case !ok:
				return errors.New("timeout must be an integer")
			case timeout < 0:
				return fmt.Errorf("timeout must be non-negative, got %d", timeout)
			}

			return nil
		},
	},
}

// Valid configuration.
validData := map[string]any{
	"server": map[string]any{
		"port":    8080,
		"timeout": 30,
	},
}

builder := config.NewBuilder()

builder = builder.WithMerger(validator)
builder = builder.AddCollector(collectors.NewMap(validData).WithName("valid"))

cfg, errs := builder.Build(context.Background())

if errs != nil {
	log.Printf("Errors: %v", errs)
} else {
	var port int

	_, err := cfg.Get(config.NewKeyPath("server/port"), &port)
	if err != nil {
		fmt.Printf("Failed to Get 'server/port': %s\n", err)
	} else {
		fmt.Printf("Valid port: %d\n", port)
	}
}

// Invalid configuration.
invalidData := map[string]any{
	"server": map[string]any{
		"port":    99999, // Invalid port.
		"timeout": 30,
	},
}

builder = config.NewBuilder()
builder = builder.WithMerger(validator)
builder = builder.AddCollector(collectors.NewMap(invalidData).WithName("invalid"))

_, errs = builder.Build(context.Background())

if errs != nil {
	fmt.Printf("Validation failed: %v\n", strings.Contains(errs[0].Error(), "port must be between"))
}
Output:
Valid port: 8080
Validation failed: true
Example (Validation)

Example_validation demonstrates JSON Schema validation of configuration.

// Define a simple JSON schema for server configuration.
schema := []byte(`{
		"$schema": "https://json-schema.org/draft/2020-12/schema",
		"type": "object",
		"properties": {
			"server": {
				"type": "object",
				"properties": {
					"port": {
						"type": "integer",
						"minimum": 1024,
						"maximum": 65535
					},
					"host": {
						"type": "string",
						"pattern": "^[a-zA-Z0-9.-]+$"
					}
				},
				"required": ["port", "host"]
			}
		},
		"additionalProperties": false
	}`)

// Create a JSON Schema validator.
validator, err := jsonschema.New(schema)
if err != nil {
	fmt.Printf("Failed to create validator: %v\n", err)
	return
}

// Valid configuration.
validData := map[string]any{
	"server": map[string]any{
		"port": 8080,
		"host": "localhost",
	},
}

builder := config.NewBuilder()

builder = builder.WithValidator(validator)
builder = builder.AddCollector(collectors.NewMap(validData).WithName("valid"))

cfg, errs := builder.Build(context.Background())
if len(errs) > 0 {
	fmt.Printf("Validation errors: %v\n", errs)
} else {
	var port int

	var host string

	_, _ = cfg.Get(config.NewKeyPath("server/port"), &port)
	_, _ = cfg.Get(config.NewKeyPath("server/host"), &host)
	fmt.Printf("Valid configuration: host=%s port=%d\n", host, port)
}

// Invalid configuration (port out of range).
invalidData := map[string]any{
	"server": map[string]any{
		"port": 80,
		"host": "localhost",
	},
}

builder = config.NewBuilder()
builder = builder.WithValidator(validator)
builder = builder.AddCollector(collectors.NewMap(invalidData).WithName("invalid"))

_, errs = builder.Build(context.Background())
if len(errs) > 0 {
	fmt.Printf("Validation failed: %v\n", len(errs) > 0)
}
Output:
Valid configuration: host=localhost port=8080
Validation failed: true
Example (WalkConfig)

Example_walkConfig demonstrates Config.Walk() for iterating over all leaf values in the configuration tree with optional depth control.

data := map[string]any{
	"database": map[string]any{
		"host": "localhost",
		"port": 5432,
		"pool": map[string]any{
			"max_size": 10,
		},
	},
}

builder := config.NewBuilder()

builder = builder.AddCollector(collectors.NewMap(data).WithName("config"))

cfg, errs := builder.Build(context.Background())
if len(errs) > 0 {
	fmt.Printf("Build errors: %v\n", errs)
	return
}

// Walk all leaf values from the root (depth -1 means unlimited).
ctx := context.Background()

valueCh, err := cfg.Walk(ctx, config.NewKeyPath(""), -1)
if err != nil {
	fmt.Printf("Walk error: %v\n", err)
	return
}

allKeys := make([]string, 0, 3)
for val := range valueCh {
	allKeys = append(allKeys, val.Meta().Key.String())
}

sort.Strings(allKeys)

fmt.Printf("All keys: %v\n", allKeys)

// Walk from a sub-path to iterate only within "database".
valueCh, err = cfg.Walk(ctx, config.NewKeyPath("database"), -1)
if err != nil {
	fmt.Printf("Walk error: %v\n", err)
	return
}

subKeys := make([]string, 0, 3)
for val := range valueCh {
	subKeys = append(subKeys, val.Meta().Key.String())
}

sort.Strings(subKeys)

fmt.Printf("Database keys: %v\n", subKeys)

// Walk with depth=2 limits traversal depth (stops before reaching pool/max_size).
valueCh, err = cfg.Walk(ctx, config.NewKeyPath("database"), 2)
if err != nil {
	fmt.Printf("Walk error: %v\n", err)
	return
}

shallowKeys := make([]string, 0, 2)
for val := range valueCh {
	shallowKeys = append(shallowKeys, val.Meta().Key.String())
}

sort.Strings(shallowKeys)

fmt.Printf("Shallow keys (depth=2): %v\n", shallowKeys)
Output:
All keys: [database/host database/pool/max_size database/port]
Database keys: [database/host database/pool/max_size database/port]
Shallow keys (depth=2): [database/host database/port]
Example (WithJSONSchema)

Example_withJSONSchema demonstrates using Builder.WithJSONSchema(reader) and Builder.MustWithJSONSchema(reader) convenience methods for schema validation, as an alternative to manually creating a validator.

schema := `{
		"$schema": "https://json-schema.org/draft/2020-12/schema",
		"type": "object",
		"properties": {
			"server": {
				"type": "object",
				"properties": {
					"port": {
						"type": "integer",
						"minimum": 1024,
						"maximum": 65535
					},
					"host": {
						"type": "string"
					}
				},
				"required": ["port", "host"]
			}
		},
		"additionalProperties": false
	}`

// Using WithJSONSchema with a reader.
validData := map[string]any{
	"server": map[string]any{
		"port": 8080,
		"host": "localhost",
	},
}

builder := config.NewBuilder()

builder, err := builder.WithJSONSchema(strings.NewReader(schema))
if err != nil {
	fmt.Printf("Schema error: %v\n", err)
	return
}

builder = builder.AddCollector(collectors.NewMap(validData).WithName("valid"))

cfg, errs := builder.Build(context.Background())
if len(errs) > 0 {
	fmt.Printf("Validation errors: %v\n", errs)
	return
}

var port int

_, _ = cfg.Get(config.NewKeyPath("server/port"), &port)
fmt.Printf("Valid config port: %d\n", port)

// Using MustWithJSONSchema (panics on invalid schema).
invalidData := map[string]any{
	"server": map[string]any{
		"port": 80, // Below minimum of 1024.
		"host": "localhost",
	},
}

builder = config.NewBuilder()
builder = builder.MustWithJSONSchema(strings.NewReader(schema))
builder = builder.AddCollector(collectors.NewMap(invalidData).WithName("invalid"))

_, errs = builder.Build(context.Background())
fmt.Printf("Validation failed: %v\n", len(errs) > 0)
Output:
Valid config port: 8080
Validation failed: true

Index

Examples

Constants

View Source
const (
	// UnknownSource indicates an undefined source.
	UnknownSource = meta.UnknownSource
	// EnvDefaultSource indicates default values from environment variables (e.g., TT_FOO_DEFAULT).
	EnvDefaultSource = meta.EnvDefaultSource
	// StorageSource indicates an external centralized storage (e.g., Etcd or TcS).
	StorageSource = meta.StorageSource
	// FileSource indicates a local file.
	FileSource = meta.FileSource
	// EnvSource indicates environment variables.
	EnvSource = meta.EnvSource
	// ModifiedSource indicates dynamically modified data (e.g., at runtime) for MutableConfig.
	// Note: ModifiedSource is not implemented yet and is under active development.
	ModifiedSource = meta.ModifiedSource
)
View Source
const Global = ""

Global is a sentinel value for Levels() indicating the root (global) level.

Variables

View Source
var (
	// ErrKeyNotFound is returned when a configuration key is not found.
	ErrKeyNotFound = errors.New("key not found")
	// ErrPathNotFound is returned when a configuration path is not found.
	ErrPathNotFound = errors.New("path not found")
	// ErrValidationFailed is returned when configuration validation fails.
	ErrValidationFailed = errors.New("validation failed")
	// ErrSchemaInvalid is returned when schema parsing fails.
	ErrSchemaInvalid = errors.New("schema invalid")
	// ErrNoInheritance is returned by EffectiveAll() when no inheritance hierarchy is configured.
	ErrNoInheritance = errors.New("no inheritance hierarchy configured")
	// ErrHierarchyMismatch is returned by Effective() when path doesn't match any hierarchy.
	ErrHierarchyMismatch = errors.New("path does not match any inheritance hierarchy")
)
View Source
var Default = &DefaultMerger{}

Default is the default merger instance.

Functions

func Levels

func Levels(levels ...string) []string

Levels defines the hierarchy of structural keys. The first argument must be Global (empty string) to represent the root level. Subsequent arguments are the structural keys in order from top to bottom.

Example:

config.Levels(config.Global, "groups", "replicasets", "instances")

This defines 4 levels:

  • Level 0 (Global): root node, config keys live here
  • Level 1 (Group): under root/groups/<name>
  • Level 2 (Replicaset): under root/groups/<name>/replicasets/<name>
  • Level 3 (Instance): under root/groups/<name>/replicasets/<name>/instances/<name>

func MergeCollector

func MergeCollector(ctx context.Context, root *tree.Node, col Collector) error

MergeCollector reads all values from a collector and merges them into the tree using the default merging logic. The collector's priority is determined by the caller (higher priority collectors should be merged later). This function handles primitive replacement, slice replacement, map recursive merging, and key order preservation based on the collector's KeepOrder flag.

func MergeCollectorWithMerger

func MergeCollectorWithMerger(ctx context.Context, root *tree.Node, col Collector, merger Merger) error

MergeCollectorWithMerger reads all values from a collector and merges them into the tree using the provided merger. Returns a CollectorError if any errors occur during processing. Multiple errors are accumulated and returned together.

Types

type Builder

type Builder struct {
	// contains filtered or unexported fields
}

Builder is a builder for stepwise creation of a Config object.

func NewBuilder

func NewBuilder() Builder

NewBuilder creates a new instance of Builder.

func (*Builder) AddCollector

func (b *Builder) AddCollector(collector Collector) Builder

AddCollector adds a new data source to the build pipeline. The order of adding collectors is critical: each subsequent collector has higher priority than the previous one. Its values will override values from earlier collectors when keys match.

func (*Builder) Build

func (b *Builder) Build(ctx context.Context) (Config, []error)

Build starts the configuration assembly process. It performs reading data from all collectors, merging them, validation against the schema, and returns a ready Config object or an error.

func (*Builder) BuildMutable

func (b *Builder) BuildMutable(ctx context.Context) (MutableConfig, []error)

BuildMutable starts the configuration assembly process but returns a mutable MutableConfig object that allows changes after creation. Note: this method is not implemented yet and is under active development.

func (*Builder) MustWithJSONSchema

func (b *Builder) MustWithJSONSchema(schema io.Reader) Builder

MustWithJSONSchema is like WithJSONSchema but panics on error. Useful for static schema definitions.

func (*Builder) WithInheritance

func (b *Builder) WithInheritance(levels []string, opts ...InheritanceOption) Builder

WithInheritance registers a hierarchy for inheritance resolution. Multiple hierarchies can be registered (e.g., groups and buckets).

The levels parameter defines the structural keys (use Levels() to create). Options configure exclusions, defaults, and merge strategies.

Inheritance is resolved during Build(), after collector merging but before validation. This ensures the validator sees the effective (fully resolved) config for each leaf entity.

func (*Builder) WithJSONSchema

func (b *Builder) WithJSONSchema(schema io.Reader) (Builder, error)

WithJSONSchema creates a JSON Schema validator and sets it. Returns error if schema parsing fails.

func (*Builder) WithMerger

func (b *Builder) WithMerger(merger Merger) Builder

WithMerger sets a custom merger for the configuration assembly. If not set, the default merging logic is used.

func (*Builder) WithValidator

func (b *Builder) WithValidator(validator validator.Validator) Builder

WithValidator sets a custom validator for configuration validation.

type Collector

type Collector interface {
	// Read returns a channel that streams values from the source.
	// The position of each element (its key) is contained in the Meta information of the Value.
	// The channel must be closed by the collector after all data has been sent.
	Read(ctx context.Context) <-chan Value

	// Name returns a human-readable name of the data source for logging and debugging.
	Name() string

	// Source returns the type of the data source (file, environment variable, etc.).
	Source() SourceType

	// Revision returns the revision identifier of the configuration.
	// For sources that do not support versioning, it should return an empty string.
	Revision() RevisionType

	// KeepOrder returns true if the order of keys must be preserved.
	// When true, the collector is considered as the "source of truth" for key order
	// at the corresponding nesting level during merging.
	KeepOrder() bool
}

Collector reads data from a source and streams it as a sequence of values.

type CollectorError

type CollectorError struct {
	CollectorName string
	Err           error
}

CollectorError wraps an error that occurred while processing a collector, providing context about which collector failed.

func NewCollectorError

func NewCollectorError(collectorName string, err error) *CollectorError

NewCollectorError creates a new CollectorError.

func (*CollectorError) Error

func (e *CollectorError) Error() string

func (*CollectorError) Unwrap

func (e *CollectorError) Unwrap() error

type Config

type Config struct {
	// contains filtered or unexported fields
}

Config provides access to the final configuration data.

func (*Config) Effective

func (c *Config) Effective(path KeyPath) (Config, error)

Effective returns the resolved (post-inheritance) config for a specific leaf entity. The path must point to a concrete leaf entity in the hierarchy (e.g., "groups/storages/replicasets/s-001/instances/s-001-a").

If no inheritance was configured in the Builder, returns the raw subtree at the given path as a Config.

The returned Config contains only config keys (no structural keys like "groups", "replicasets", "instances").

func (*Config) EffectiveAll

func (c *Config) EffectiveAll() (map[string]Config, error)

EffectiveAll returns resolved configs for ALL leaf entities found in the hierarchy. The returned map keys are full paths to each leaf entity.

If no inheritance was configured, returns an error.

func (*Config) Get

func (c *Config) Get(path KeyPath, dest any) (MetaInfo, error)

Get is the primary, most convenient method for retrieving a value. It finds the value at the specified path and extracts it into the variable `dest`. Returns metadata and an error if the key is not found or the type cannot be converted.

func (*Config) Lookup

func (c *Config) Lookup(path KeyPath) (Value, bool)

Lookup searches for a value by key. Unlike Get, it does not return an error if the key is not found, but reports it via a boolean flag. Returns a special `Value` object and a flag indicating whether the value was found. This is useful when you need to distinguish between a missing value and a nil value.

func (*Config) MarshalYAML

func (c *Config) MarshalYAML() ([]byte, error)

MarshalYAML serializes the Config object into YAML format. Thanks to key order preservation, the resulting YAML will have a predictable and stable structure.

func (*Config) Slice

func (c *Config) Slice(path KeyPath) (Config, error)

Slice returns a slice of the original config that corresponds to the specified keypath. Used to obtain a sub-configuration as a separate Config object. If the path does not correspond to an object, returns an error. If path is empty (or `nil`), returns a copy of the current Config object.

func (*Config) Stat

func (c *Config) Stat(path KeyPath) (MetaInfo, bool)

Stat returns metadata for a key (source name, revision) without touching the actual value. Useful for debugging and introspection tools.

func (*Config) String

func (c *Config) String() string

String returns a string with the current representation of the configuration according to the YAML format.

func (*Config) Walk

func (c *Config) Walk(ctx context.Context, path KeyPath, depth int) (<-chan Value, error)

Walk returns a channel through which you can iterate over all keys and values in the configuration. This is useful for traversing all parameters without needing to know their keys in advance. path may be empty (or `nil`) to start from the root of the configuration. If depth > 0, only the part of the configuration tree limited by the specified depth is traversed. If depth <= 0, the entire object is traversed.

type DefaultMerger

type DefaultMerger struct{}

DefaultMerger implements Merger with the standard merging logic.

func (*DefaultMerger) CreateContext

func (d *DefaultMerger) CreateContext(collector Collector) MergerContext

CreateContext creates a new merger context for the given collector.

func (*DefaultMerger) MergeValue

func (d *DefaultMerger) MergeValue(ctx MergerContext, root *tree.Node, keyPath keypath.KeyPath, value any) error

MergeValue merges a single value into the tree using the default merging logic.

type DefaultsType

type DefaultsType map[string]any

DefaultsType is a wrapper for default values in inheritance zones.

type InheritMergeStrategy

type InheritMergeStrategy int

InheritMergeStrategy defines how a specific key is merged during inheritance resolution (NOT during collector merging — that is handled by Merger).

const (
	// MergeReplace replaces the value from the parent with the child's value.
	// This is the default strategy for all keys.
	MergeReplace InheritMergeStrategy = iota
	// MergeAppend appends child slice elements to the parent's slice.
	// If the value is not a slice, behaves like MergeReplace.
	MergeAppend
	// MergeDeep recursively merges maps from parent and child.
	// Child keys override parent keys; parent-only keys are preserved.
	// If the value is not a map, behaves like MergeReplace.
	MergeDeep
)

type InheritanceOption

type InheritanceOption func(*inheritanceConfig)

InheritanceOption configures inheritance behavior. Created via With* functions.

func WithDefaults

func WithDefaults(defaults DefaultsType) InheritanceOption

WithDefaults sets default values applied to every resolved leaf entity. Defaults have the lowest priority — any value from any level overrides them.

func WithInheritMerge

func WithInheritMerge(key string, strategy InheritMergeStrategy) InheritanceOption

WithInheritMerge sets the merge strategy for a specific config key prefix during inheritance resolution. This controls how parent and child values are combined when both define the same key.

Example:

WithInheritMerge("roles", config.MergeAppend)

means when group has roles=[storage] and instance has roles=[metrics], the effective roles for that instance will be [storage, metrics].

func WithNoInherit

func WithNoInherit(keys ...string) InheritanceOption

WithNoInherit marks config key prefixes that are never propagated down the hierarchy from any level. The key is excluded from inheritance entirely — it only applies at the level where it is explicitly set.

Example: WithNoInherit("leader") means "leader" set at the replicaset level is NOT copied into instance configs during inheritance resolution. It remains accessible at its original path in the raw tree.

func WithNoInheritFrom

func WithNoInheritFrom(level string, keys ...string) InheritanceOption

WithNoInheritFrom marks config key prefixes that should not be inherited FROM a specific level. The level is identified by its structural key (or Global for the root level).

Example:

WithNoInheritFrom(config.Global, "snapshot.dir")

means snapshot.dir set at the global level does not flow into group/replicaset/instance levels. But snapshot.dir set at the group level DOES flow into replicaset/instance levels.

type KeyPath

type KeyPath = keypath.KeyPath

KeyPath represents a hierarchical key.

For example, for the key "/server/http/port" it will look like []string{"server", "http", "port"}.

func NewKeyPath

func NewKeyPath(p string) KeyPath

NewKeyPath creates a KeyPath from a string, splitting it by "/" slash. This is the main way to create a path from a textual representation.

func NewKeyPathFromSegments

func NewKeyPathFromSegments(segments []string) KeyPath

NewKeyPathFromSegments creates a KeyPath from a slice of segments. This is useful when you already have segments as a slice and don't need string parsing.

func NewKeyPathWithDelim

func NewKeyPathWithDelim(p, delim string) KeyPath

NewKeyPathWithDelim creates a KeyPath from a string, splitting it by the given delimiter. All segments are preserved, including empty ones.

type Merger

type Merger interface {
	// CreateContext creates a new context for processing a collector.
	// Called once per collector before any MergeValue calls.
	//
	// The context should store any state needed for merging values from this collector,
	// such as ordering information, validation state, or statistics.
	//
	// If the collector's KeepOrder returns true, the context should allocate
	// data structures for tracking ordering (typically a map[string][]string).
	CreateContext(collector Collector) MergerContext

	// MergeValue merges a single value into the tree.
	// The method is called for each value produced by the collector.
	//
	// Parameters:
	//   - ctx: the context created by CreateContext for this collector
	//   - root: the root of the configuration tree to merge into
	//   - path: the key path where the value should be merged
	//   - value: the raw value to merge (primitive, slice, or map[string]any)
	//
	// Implementations should:
	//   - Navigate to the appropriate node in the tree using path
	//   - Merge the value according to custom logic or delegate to DefaultMerger
	//   - Call ctx.RecordOrdering if the collector preserves order
	//   - Return an error if merging fails (validation, type mismatch, etc.)
	//
	// The tree is modified in place. Multiple MergeValue calls may update the same
	// nodes if paths overlap (e.g., "a.b" and "a.c" both create children under "a").
	MergeValue(ctx MergerContext, root *tree.Node, path keypath.KeyPath, value any) error
}

Merger defines how values from collectors are merged into the configuration tree. This interface allows customization of the merging process, enabling use cases such as:

  • Validation: reject invalid values before merging
  • Transformation: modify values based on their path or source
  • Selective merging: skip certain paths or sources
  • Auditing: log or track all merge operations
  • Custom conflict resolution: define how to handle duplicate keys

The default merging logic is provided by DefaultMerger, which implements standard last-write-wins semantics with type-aware merging for maps and arrays.

Custom implementations should:

  1. Create a context in CreateContext that tracks state for the collector
  2. Implement MergeValue to handle each value from the collector
  3. Handle ordering properly if the collector's KeepOrder returns true
  4. Return meaningful errors when merging fails

Example custom merger that counts merge operations is located in "merger_custom_test.go".

Use Builder.WithMerger to configure a custom merger:

cfg, errs := config.NewBuilder().
    WithMerger(&countingMerger{}).
    AddCollector(myCollector).
    Build()

type MergerContext

type MergerContext interface {
	// Collector returns the collector being processed.
	Collector() Collector

	// RecordOrdering tracks a child key under its parent for ordering.
	// This should be called for each value when the collector's KeepOrder
	// returns true and ordering needs to be preserved.
	//
	// The parent parameter is the path to the parent node (may be nil for root).
	// The child parameter is the key of the child node to record.
	//
	// Implementations should store this information and apply it in ApplyOrdering.
	RecordOrdering(parent keypath.KeyPath, child string)

	// ApplyOrdering applies recorded ordering to the tree.
	// Called after all values from the collector have been processed.
	//
	// Implementations should iterate through recorded parent-child relationships
	// and call SetOrder on the corresponding tree nodes to preserve insertion order.
	//
	// Returns an error if ordering cannot be applied.
	ApplyOrdering(root *tree.Node) error
}

MergerContext holds state for merging a single collector's values. Implementations can use this to track ordering or other state across multiple MergeValue calls within a single collector.

Custom merger implementations must handle ordering properly when the collector's KeepOrder method returns true. This typically involves:

  1. Allocating a map to track parent-child relationships in CreateContext
  2. Calling RecordOrdering for each value during MergeValue
  3. Implementing ApplyOrdering to set the order on tree nodes

For collectors that do not preserve order (KeepOrder returns false), the ordering methods can be no-ops.

type MetaInfo

type MetaInfo = meta.Info

MetaInfo contains metadata about a value in the configuration. Used to display the actual origin of the obtained value.

type MultiCollector

type MultiCollector interface {
	// Collectors returns the sub-collectors, each representing an
	// independent configuration document. The returned collectors are
	// merged in order (earlier = lower priority).
	Collectors(ctx context.Context) ([]Collector, error)
}

MultiCollector is an optional interface that a Collector may implement to indicate it produces multiple independent configuration documents. When the Builder encounters a MultiCollector, it calls Collectors to expand it into sub-collectors and merges each one independently with its own MergerContext, source name, and revision.

type MutableConfig

type MutableConfig struct {
	Config // Embeds the read-only interface.
	// contains filtered or unexported fields
}

MutableConfig is an extension of Config that allows safe runtime modifications. Note: MutableConfig is not implemented yet and is under active development.

func (*MutableConfig) Delete

func (mc *MutableConfig) Delete(_ KeyPath) bool

Delete removes a key from the configuration. Note: this method is not implemented yet and is under active development.

func (*MutableConfig) Merge

func (mc *MutableConfig) Merge(other *Config) error

Merge merges two configurations so that all values from the new configuration are added or override similar values in the current one. An error may occur if the new values do not conform to the current schema. Note: this method is not implemented yet and is under active development.

func (*MutableConfig) Set

func (mc *MutableConfig) Set(path KeyPath, value any) error

Set sets or overwrites a value at the specified path. The key's metadata must be updated: Source becomes 'ModifiedSource', and Revision is incremented. Note: this method is not implemented yet and is under active development.

func (*MutableConfig) Update

func (mc *MutableConfig) Update(other *Config) error

Update merges two configurations, but applies only those values that already exist in the current config. Everything else is ignored. An error may occur if the new values do not match the type of the current value according to the schema. Note: this method is not implemented yet and is under active development.

type RevisionType

type RevisionType = meta.RevisionType

RevisionType defines a revision identifier of configuration, if applicable. Typically a string (e.g., commit hash, timestamp). If revision is not supported, it should be empty.

type SourceInfo

type SourceInfo = meta.SourceInfo

SourceInfo contains information about the source where the value originated.

type SourceType

type SourceType = meta.SourceType

SourceType defines the type of configuration source.

type Value

type Value = value.Value

Value represents a single value in the configuration.

Directories

Path Synopsis
Package collectors provides standard implementations of the config.Collector interface for loading configuration from various sources.
Package collectors provides standard implementations of the config.Collector interface for loading configuration from various sources.
internal
environ
Package environ provides utilities for parsing environment variables.
Package environ provides utilities for parsing environment variables.
testutil
Package testutil provides test helpers including iterator comparisons (iter.Seq and iter.Seq2) and channel draining utilities.
Package testutil provides test helpers including iterator comparisons (iter.Seq and iter.Seq2) and channel draining utilities.
Package keypath provides the KeyPath type and utilities for working with hierarchical configuration key paths.
Package keypath provides the KeyPath type and utilities for working with hierarchical configuration key paths.
Package meta provides metadata types that describe the origin and version of configuration values.
Package meta provides metadata types that describe the origin and version of configuration values.
Package omap provides a generic ordered map implementation that maintains insertion order of keys.
Package omap provides a generic ordered map implementation that maintains insertion order of keys.
Package tarantool provides a high-level Builder that assembles a Tarantool-compatible configuration from standard sources.
Package tarantool provides a high-level Builder that assembles a Tarantool-compatible configuration from standard sources.
Package tree provides an in-memory configuration tree used as the central data structure by go-config.
Package tree provides an in-memory configuration tree used as the central data structure by go-config.
Package validator defines interfaces and types for configuration validation.
Package validator defines interfaces and types for configuration validation.
validators
jsonschema
Package jsonschema provides JSON Schema validation for go-config.
Package jsonschema provides JSON Schema validation for go-config.
Package value defines the Value interface, the primary abstraction for reading configuration values in go-config.
Package value defines the Value interface, the primary abstraction for reading configuration values in go-config.

Jump to

Keyboard shortcuts

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