| description | Config Bus turns Config into a namespace registry: per-package namespaces with JSON Schema validation, env-var routing, defaults, multi-source merge, hot-reload; enabled by apcore key. |
|---|
Type: Implementation guide. Normative spec: PROTOCOL_SPEC §9.4 Config Bus Architecture.
The Config Bus (§9.4–§9.13) turns Config into an ecosystem-level namespace registry. Any package can register a named configuration namespace with optional JSON Schema validation, environment variable routing, default values, and hot-reload support. Multiple sources can be merged into a namespace at runtime (YAML files, in-memory dicts).
Namespace mode is activated automatically when the loaded YAML contains a top-level apcore: key. Legacy mode (flat YAML) is fully backward compatible.
| Mode | Activation | Description |
|---|---|---|
| Legacy | No apcore: key in YAML |
Flat YAML; all keys resolved by dot-path |
| Namespace | apcore: key present |
Each top-level key is an independent namespace |
Namespaces are registered globally (class-level) before loading a config file. The env_prefix is auto-derived from the namespace name when not specified:
=== "Python"
```python
# Python — minimal (env_prefix auto-derived as "MY_PLUGIN")
Config.register_namespace("my-plugin")
# Python — full options
Config.register_namespace(
"my-plugin",
env_prefix="MY_PLUGIN", # optional, auto-derived if omitted
defaults={"timeout": 5000, "retries": 3},
env_style="auto", # "auto" (default), "nested", or "flat"
max_depth=5, # max nesting depth (default 5)
env_map={"REDIS_URL": "cache"}, # bare env var → namespace key
schema=None, # optional JSON Schema
)
```
=== "TypeScript"
```typescript
// TypeScript — minimal
Config.registerNamespace({ name: 'my-plugin' });
// TypeScript — full options
Config.registerNamespace({
name: 'my-plugin',
envPrefix: 'MY_PLUGIN',
defaults: { timeout: 5000, retries: 3 },
envStyle: 'auto',
maxDepth: 5,
envMap: { REDIS_URL: 'cache' },
});
```
=== "Rust"
```rust
use apcore::config::DEFAULT_MAX_DEPTH;
use apcore::{Config, EnvStyle, NamespaceRegistration};
use std::collections::HashMap;
Config::register_namespace(NamespaceRegistration {
name: "my-plugin".into(),
env_prefix: None, // auto-derived as "MY_PLUGIN"
defaults: Some(serde_json::json!({ "timeout": 5000, "retries": 3 })),
env_style: EnvStyle::Auto,
max_depth: DEFAULT_MAX_DEPTH,
env_map: Some(HashMap::from([("REDIS_URL".into(), "cache".into())])),
schema: None,
})?;
```
When env_prefix is not provided (or None), it is auto-derived from the namespace name:
name.upper().replace("-", "_")
"myapp" → "MYAPP"
"apcore-mcp" → "APCORE_MCP"
"my-plugin" → "MY_PLUGIN"
Use explicit env_prefix only when the derived name doesn't fit (e.g., name="reach" but you want env_prefix="REACHFORGE").
| Error Code | Cause |
|---|---|
CONFIG_NAMESPACE_RESERVED |
Name is in the reserved set (apcore, _config) |
CONFIG_NAMESPACE_DUPLICATE |
Name already registered |
CONFIG_ENV_PREFIX_CONFLICT |
envPrefix already in use |
CONFIG_ENV_MAP_CONFLICT |
A bare env var name in env_map is already claimed by another mapping |
There are three ways environment variables can feed into config:
=== "Python"
```python
from apcore import Config
Config.env_map({"PORT": "port", "DATABASE_URL": "db_url"})
# PORT=3000 → config.get("port") = 3000
# DATABASE_URL=pg:// → config.get("db_url") = "pg://"
```
=== "TypeScript"
```typescript
import { Config } from 'apcore-js';
Config.envMap({ PORT: 'port', DATABASE_URL: 'db_url' });
// PORT=3000 → config.get("port") = 3000
// DATABASE_URL=pg:// → config.get("db_url") = "pg://"
```
=== "Rust"
```rust
use apcore::Config;
use std::collections::HashMap;
Config::env_map(HashMap::from([
("PORT".to_string(), "port".to_string()),
("DATABASE_URL".to_string(), "db_url".to_string()),
]))?;
// PORT=3000 → config.get("port") = 3000
// DATABASE_URL=pg:// → config.get("db_url") = "pg://"
```
Global env_map maps to the config root level, not inside any namespace. This is for well-known env vars (PORT, DATABASE_URL, HOST) that don't conceptually belong to a namespace.
=== "Python"
```python
from apcore import Config
Config.register_namespace("myapp", env_map={"REDIS_URL": "cache_url"})
# REDIS_URL=redis://... → config.get("myapp.cache_url") = "redis://..."
```
=== "TypeScript"
```typescript
import { Config } from 'apcore-js';
Config.registerNamespace({
name: 'myapp',
envMap: { REDIS_URL: 'cache_url' },
});
// REDIS_URL=redis://... → config.get("myapp.cache_url") = "redis://..."
```
=== "Rust"
```rust
use apcore::config::DEFAULT_MAX_DEPTH;
use apcore::{Config, EnvStyle, NamespaceRegistration};
use std::collections::HashMap;
Config::register_namespace(NamespaceRegistration {
name: "myapp".into(),
env_prefix: None,
defaults: None,
schema: None,
env_style: EnvStyle::Auto,
max_depth: DEFAULT_MAX_DEPTH,
env_map: Some(HashMap::from([("REDIS_URL".into(), "cache_url".into())])),
})?;
// REDIS_URL=redis://... → config.get("myapp.cache_url") = "redis://..."
```
Namespace env_map maps into the specified namespace. Use for env vars with well-known names that belong to a specific namespace.
=== "Python"
```python
from apcore import Config
Config.register_namespace("myapp") # env_prefix auto-derived as "MYAPP"
# MYAPP_DEBUG=true → config.get("myapp.debug") = True
# MYAPP_API_TIMEOUT=60 → depends on env_style (see below)
```
=== "TypeScript"
```typescript
import { Config } from 'apcore-js';
Config.registerNamespace({ name: 'myapp' }); // envPrefix auto-derived as "MYAPP"
// MYAPP_DEBUG=true → config.get("myapp.debug") = true
// MYAPP_API_TIMEOUT=60 → depends on envStyle (see below)
```
=== "Rust"
```rust
use apcore::config::DEFAULT_MAX_DEPTH;
use apcore::{Config, EnvStyle, NamespaceRegistration};
Config::register_namespace(NamespaceRegistration {
name: "myapp".into(),
env_prefix: None, // auto-derived as "MYAPP"
defaults: None,
schema: None,
env_style: EnvStyle::Auto,
max_depth: DEFAULT_MAX_DEPTH,
env_map: None,
})?;
// MYAPP_DEBUG=true → config.get("myapp.debug") = true
// MYAPP_API_TIMEOUT=60 → depends on env_style (see below)
```
env_map is exact-match. It does not go through env_style conversion. Only prefix-based routing uses env_style.
For each environment variable, the system checks in this order:
- Global env_map — exact match → top-level config key
- Namespace env_map — exact match → namespace config key
- Prefix-based dispatch — longest-prefix-match → env_style conversion
First match wins. An env var is processed by at most one source.
The same bare env var name cannot appear in more than one env_map (global or namespace). Attempting to register a duplicate raises CONFIG_ENV_MAP_CONFLICT:
=== "Python"
```python
from apcore import Config
Config.env_map({"PORT": "port"})
Config.register_namespace("myapp", env_map={"PORT": "server_port"})
# → ConfigEnvMapConflictError: "PORT" already mapped by "__global__"
```
=== "TypeScript"
```typescript
import { Config } from 'apcore-js';
Config.envMap({ PORT: 'port' });
Config.registerNamespace({
name: 'myapp',
envMap: { PORT: 'server_port' },
});
// → ConfigEnvMapConflictError: "PORT" already mapped by "__global__"
```
=== "Rust"
```rust
use apcore::config::DEFAULT_MAX_DEPTH;
use apcore::{Config, EnvStyle, NamespaceRegistration};
use std::collections::HashMap;
Config::env_map(HashMap::from([("PORT".into(), "port".into())]))?;
Config::register_namespace(NamespaceRegistration {
name: "myapp".into(),
env_prefix: None,
defaults: None,
schema: None,
env_style: EnvStyle::Auto,
max_depth: DEFAULT_MAX_DEPTH,
env_map: Some(HashMap::from([("PORT".into(), "server_port".into())])),
})?;
// → Err(ConfigEnvMapConflictError) — "PORT" already mapped by "__global__"
```
env_style controls how the suffix after env_prefix is converted to a config key. Only affects prefix-based routing (not env_map).
Matches the env var suffix against the defaults tree structure. Flat keys match flat, nested paths match nested:
=== "Python"
```python
from apcore import Config
Config.register_namespace(
"myapp",
defaults={"devto_api_key": "", "publish": {"delay": 5, "retry": 3}},
)
# MYAPP_DEVTO_API_KEY=abc → myapp.devto_api_key (flat key found in defaults)
# MYAPP_PUBLISH_DELAY=10 → myapp.publish.delay (nested path found in defaults)
# MYAPP_UNKNOWN_KEY=x → myapp.unknown.key (not in defaults → fallback to nested)
```
=== "TypeScript"
```typescript
import { Config } from 'apcore-js';
Config.registerNamespace({
name: 'myapp',
defaults: { devto_api_key: '', publish: { delay: 5, retry: 3 } },
});
// MYAPP_DEVTO_API_KEY=abc → myapp.devto_api_key (flat key found in defaults)
// MYAPP_PUBLISH_DELAY=10 → myapp.publish.delay (nested path found in defaults)
// MYAPP_UNKNOWN_KEY=x → myapp.unknown.key (not in defaults → fallback to nested)
```
=== "Rust"
```rust
use apcore::config::DEFAULT_MAX_DEPTH;
use apcore::{Config, EnvStyle, NamespaceRegistration};
Config::register_namespace(NamespaceRegistration {
name: "myapp".into(),
env_prefix: None,
defaults: Some(serde_json::json!({
"devto_api_key": "",
"publish": { "delay": 5, "retry": 3 },
})),
schema: None,
env_style: EnvStyle::Auto,
max_depth: DEFAULT_MAX_DEPTH,
env_map: None,
})?;
// MYAPP_DEVTO_API_KEY=abc → myapp.devto_api_key (flat key found in defaults)
// MYAPP_PUBLISH_DELAY=10 → myapp.publish.delay (nested path found in defaults)
// MYAPP_UNKNOWN_KEY=x → myapp.unknown.key (not in defaults → fallback to nested)
```
When defaults is not provided, auto falls back entirely to nested behavior.
Single _ → . (section separator), double __ → literal _:
MYAPP_API_TIMEOUT=60 → myapp.api.timeout
MYAPP_API_SERVER__URL=http: → myapp.api.server_url
No conversion. Suffix lowercased as-is:
MYAPP_DEVTO_API_KEY=abc → myapp.devto_api_key
MYAPP_LLM_MODEL=gemini → myapp.llm_model
| Scenario | Style |
|---|---|
| Mixed flat keys + nested sections | auto (default, don't specify) |
Pure hierarchical config (e.g., api.server.url) |
nested |
Pure flat snake_case config (e.g., devto_api_key) |
flat |
Limits the nesting depth for nested and auto styles. Default: 5. After max_depth segments, remaining _ characters are preserved as literal underscores.
max_depth=5 (default):
A_B_C_D_E_F_G → a.b.c.d.e_f_g (5 segments, F_G kept literal)
max_depth=3:
A_B_C_D_E → a.b.c_d_e (3 segments)
Ignored for flat style.
From highest to lowest:
env_map / env_prefix overrides > YAML file > namespace defaults
=== "Python"
```python
from dataclasses import dataclass
from apcore import Config
config = Config.load("apcore.yaml")
# Full namespace dict
plugin_cfg = config.namespace("my-plugin") # dict[str, Any]
# Dot-path access
timeout = config.get("my-plugin.timeout", 5000)
# Top-level key (from global env_map)
port = config.get("port")
# Typed deserialization
@dataclass
class PluginConfig:
timeout: int = 5000
retries: int = 3
typed = config.bind("my-plugin", PluginConfig)
```
=== "TypeScript"
```typescript
import { Config } from 'apcore-js';
const config = Config.load('apcore.yaml');
const pluginCfg = config.namespace('my-plugin');
const timeout = config.get('my-plugin.timeout', 5000);
const port = config.get('port'); // from global env_map
class PluginConfig {
timeout: number;
retries: number;
constructor(data: Record<string, unknown>) {
this.timeout = (data['timeout'] as number) ?? 5000;
this.retries = (data['retries'] as number) ?? 3;
}
}
const typed = config.bind('my-plugin', PluginConfig);
```
=== "Rust"
```rust
use apcore::Config;
use std::path::Path;
let config = Config::load(Path::new("apcore.yaml"))?;
let plugin_cfg = config.namespace("my-plugin");
let timeout: u64 = config.get_typed("my-plugin.timeout")?;
#[derive(serde::Deserialize)]
struct PluginConfig { timeout: u64, retries: u32 }
let typed: PluginConfig = config.bind("my-plugin")?;
```
Attach data from a file or in-memory dict to a namespace. Mounted data is merged over namespace defaults.
=== "Python"
```python
config.mount("my-plugin", source={"timeout": 10000}) # dict source
config.mount("my-plugin", source="./my-plugin.yaml") # file source
```
=== "TypeScript"
```typescript
config.mount('my-plugin', { fromDict: { timeout: 10000 } });
config.mount('my-plugin', { fromFile: './my-plugin.yaml' });
```
=== "Rust"
```rust
config.mount("my-plugin", MountSource::Dict(data))?;
config.mount("my-plugin", MountSource::File("./my-plugin.yaml".into()))?;
```
Error: CONFIG_MOUNT_ERROR if namespace is _config, source file is missing, or file is not a valid YAML mapping.
Deserialize a namespace subtree into a typed value:
| Language | API |
|---|---|
| Python | config.bind(namespace, dataclass_type) → instance |
| TypeScript | config.bind<T>(namespace, ClassName) → T |
| Rust | config.bind::<T>(namespace) → Result<T, ...> |
Error: CONFIG_BIND_ERROR if instantiation fails.
Re-read the source YAML, re-detect mode, re-apply namespace defaults, env overrides, validation, and mounts:
=== "Python"
```python
config.reload()
```
=== "TypeScript"
```typescript
config.reload();
```
=== "Rust"
```rust
config.reload()?;
```
apcore pre-registers two namespaces at startup:
| Namespace | Env prefix | Description |
|---|---|---|
observability |
APCORE_OBSERVABILITY |
Tracing, metrics, logging, error history, platform notify config |
sys_modules |
APCORE_SYS |
System modules enable/disable and threshold config |
Config.discover() (or Config.load() with no path) searches these locations in order:
$APCORE_CONFIG_FILEenvironment variable (any path/filename)./project.yaml./project.yml./apcore.yaml./apcore.yml~/.config/apcore/config.yaml(XDG)
Falls back to Config.from_defaults() if nothing is found.
Any YAML filename works when loaded explicitly:
=== "Python"
```python
import os
from apcore import Config
Config.load("my-custom-config.yaml") # explicit path
os.environ["APCORE_CONFIG_FILE"] = "custom.yaml" # via env var
Config.discover() # auto-discovery
```
=== "TypeScript"
```typescript
import process from 'node:process';
import { Config } from 'apcore-js';
Config.load('my-custom-config.yaml'); // explicit path
process.env.APCORE_CONFIG_FILE = 'custom.yaml'; // via env var
Config.discover(); // auto-discovery
```
=== "Rust"
```rust
use std::path::Path;
use apcore::Config;
Config::load(Path::new("my-custom-config.yaml"))?; // explicit path
std::env::set_var("APCORE_CONFIG_FILE", "custom.yaml"); // via env var
Config::discover()?; // auto-discovery
```
Standard flat YAML — backward compatible with all previous apcore versions:
version: "1.0.0"
executor:
default_timeout: 30000
extensions:
root: ./extensionsActivated by the presence of a top-level apcore: key:
apcore:
version: "1.0.0"
_config:
strict: true # Reject unknown namespace keys
observability:
tracing:
enabled: true
sampling_rate: 1.0
my-plugin:
timeout: 10000
retries: 5The _config reserved namespace controls validation behavior. strict: true causes validate() to reject unknown top-level keys.
validate() enforces the same required-field set and value constraints in all three SDKs, in both legacy and namespace mode. Any violation is reported as ConfigError(code=CONFIG_INVALID). (Prior to this contract each SDK enforced a different subset, so the same config could pass in one SDK and fail in another — implementations MUST converge on the set below.)
- None.
validate()takes no parameters; it reads the instance it is called on.
Two views of that instance are read, and the distinction is load-bearing:
- the declared document (
Config.get_declared()/getDeclared()) — the parsed file before the default table is merged. Required-field checks read this one, for the reason given below. - the effective document (the merged view) — value constraints read this one, so a constraint applies to the value that will actually be used.
In namespace mode the registered namespaces are read as well, since each may carry a JSON Schema the corresponding subtree must satisfy.
A key is required only when it has no canonical default (PROTOCOL_SPEC §9.1). Exactly two qualify — absence of either MUST be rejected with CONFIG_INVALID:
versionproject.name
extensions.root, schema.root, acl.root and acl.default_effect were previously on this list. They all carry defaults in schemas/defaults.schema.json, so requiring them rejected configurations the framework resolves perfectly well; they were removed from schemas/apcore-config.schema.json's required array for the same reason.
Requiredness is evaluated against the declared document, before the default table is merged. An implementation that deep-merges its defaults into the parsed document and then checks for required fields can never fail the check — the merge has already supplied every key. That is not validation, it is dead code that looks like validation, and it is how all three SDKs came to ship a required-field list that could not fire. Each SDK exposes the pre-merge view for this purpose (Config.get_declared() / getDeclared()).
A consequence worth stating: a defaults-only configuration (Config.from_defaults()) declares nothing, so validate() on it MUST fail. The no-config bootstrap path is unaffected — Config.load() with no discoverable file returns the defaults without validating them.
Out-of-range values MUST be rejected with CONFIG_INVALID:
| Key | Constraint |
|---|---|
acl.default_effect |
one of allow, deny |
observability.tracing.sampling_rate |
number 0.0 ≤ x ≤ 1.0 |
extensions.max_depth |
integer in [1, 16] (discovery-recursion safety cap) |
executor.default_timeout, executor.global_timeout |
integer ≥ 0 (milliseconds) |
executor.max_call_depth, executor.max_module_repeat |
integer ≥ 1 |
sys_modules.error_history.max_entries_per_module |
integer ≥ 1 |
sys_modules.error_history.max_total_entries |
integer ≥ 1 |
sys_modules.events.thresholds.error_rate |
number 0.0 ≤ x ≤ 1.0 |
sys_modules.events.thresholds.latency_p99_ms |
number > 0 |
⚠️ Booleans are rejected for all numeric fields.
middleware.circuit_breaker.*is not a configuration namespace. Earlier revisions listed four such keys here, and all three SDKs validated them — butapcore-config.schema.jsondeclaresMiddlewareConfigas{ disabled }withadditionalProperties: false, so a config that set them was rejected by the canonical schema and accepted by every SDK, then ignored at runtime: no SDK ever read them. They have been removed from all three constraint tables. A circuit breaker is configured through its constructor options (open_threshold0.5,window_size20,recovery_window_ms30000,min_samples5 — identical in all three SDKs) or through the declarative middleware-chain config, which is per-entry rather than global.
- For each registered namespace that declares a JSON Schema, the namespace subtree MUST validate against that schema; a failure is
CONFIG_INVALID. - Under
_config.strict: true, an unregistered top-level namespace MUST be rejected withCONFIG_INVALID.
This applies to the value-constraint table above, not to the required-field set. Every constraint listed is enforced by all three SDKs; an implementation missing one aligns up, and dropping a constraint weakens the configuration gate.
The required-field set is governed by a different rule and was deliberately narrowed — see above. It is anchored to PROTOCOL_SPEC §9.1 ("required only when no canonical default exists"), not to any reference SDK. An earlier revision of this contract anchored it to
apcore-pythonas "the superset"; that was a mistake, because Python's required-field check was unreachable dead code — it merged its defaults in before checking. Deferring to whichever SDK enforces the most is only sound when that SDK's enforcement actually runs.
A violation of any rule above is reported with error code CONFIG_INVALID. The carrier differs by language and the contract is on the code, not the type name:
| SDK | On failure |
|---|---|
| apcore-python | raises ConfigError, whose constructor fixes code="CONFIG_INVALID" |
| apcore-typescript | throws ConfigError, same code |
| apcore-rust | returns Err(ModuleError) carrying the equivalent code — there is no ConfigError type in apcore-rust |
An implementation MUST NOT report a validation failure under any other code, and MUST NOT report the first violation only when several are present if its host language can carry a list — the value of validate() is telling an operator everything that is wrong in one pass.
On success the method yields nothing. The shape is the language's idiomatic "nothing", not a shared type:
| SDK | Signature |
|---|---|
| apcore-python | def validate(self) -> None |
| apcore-typescript | validate(): void |
| apcore-rust | pub fn validate(&self) -> Result<(), ModuleError> — success is Ok(()) |
Rust returns a Result because it signals failure by return value rather than by unwinding; Python and TypeScript signal by raising. Both satisfy this contract. An SDK MUST NOT return a boolean or a "list of problems" in place of raising or returning an error — a caller that forgets to inspect it gets a silently invalid configuration, which is the failure mode this method exists to prevent.
async:falsein all three SDKs. Validation reads in-memory state and performs no I/O.thread_safe:true— a pure read of the instance.pure:true— no mutation, no event emission, no logging required.idempotent:true— repeated calls on an unchangedConfigyield the same outcome.reentrant:true.
!!! warning "A defaults-only Config MUST fail validation"
Config.from_defaults() declares nothing, so the required-field check — which
reads the declared view — finds neither version nor project.name. That
is intended, and it is the observable difference between "the framework can
run with defaults" and "this configuration was written down". The no-config
bootstrap path is unaffected: Config.load() with no discoverable file
returns the defaults without validating them.
=== "Python"
```python
from apcore import Config
namespaces = Config.registered_namespaces()
# [{"name": "observability", "env_prefix": "APCORE_OBSERVABILITY", "has_schema": False}, ...]
```
=== "TypeScript"
```typescript
import { Config } from 'apcore-js';
const namespaces = Config.registeredNamespaces();
// [{ name: 'observability', envPrefix: 'APCORE_OBSERVABILITY', hasSchema: false }, ...]
```
=== "Rust"
```rust
use apcore::Config;
let namespaces = Config::registered_namespaces();
// Vec<NamespaceInfo> with name, env_prefix, has_schema fields
```
=== "Python"
```python
from apcore import Config
Config.register_namespace("myapp")
cfg = Config.load("apcore.yaml")
cfg.get("myapp.debug") # reads from MYAPP_DEBUG env var or YAML
```
=== "TypeScript"
```typescript
import { Config } from 'apcore-js';
Config.registerNamespace({ name: 'myapp' });
const cfg = Config.load('apcore.yaml');
cfg.get('myapp.debug'); // reads from MYAPP_DEBUG env var or YAML
```
=== "Rust"
```rust
use std::path::Path;
use apcore::config::DEFAULT_MAX_DEPTH;
use apcore::{Config, EnvStyle, NamespaceRegistration};
Config::register_namespace(NamespaceRegistration {
name: "myapp".into(),
env_prefix: None,
defaults: None,
schema: None,
env_style: EnvStyle::Auto,
max_depth: DEFAULT_MAX_DEPTH,
env_map: None,
})?;
let cfg = Config::load(Path::new("apcore.yaml"))?;
let _debug = cfg.get("myapp.debug"); // reads from MYAPP_DEBUG env var or YAML
```
=== "Python"
```python
from apcore import Config
Config.env_map({"PORT": "port"})
Config.register_namespace("myapp", env_map={"REDIS_URL": "cache_url"})
cfg = Config.load("apcore.yaml")
cfg.get("port") # from PORT env var (top-level)
cfg.get("myapp.cache_url") # from REDIS_URL env var (namespace)
cfg.get("myapp.debug") # from MYAPP_DEBUG env var (prefix)
```
=== "TypeScript"
```typescript
import { Config } from 'apcore-js';
Config.envMap({ PORT: 'port' });
Config.registerNamespace({
name: 'myapp',
envMap: { REDIS_URL: 'cache_url' },
});
const cfg = Config.load('apcore.yaml');
cfg.get('port'); // from PORT env var (top-level)
cfg.get('myapp.cache_url'); // from REDIS_URL env var (namespace)
cfg.get('myapp.debug'); // from MYAPP_DEBUG env var (prefix)
```
=== "Rust"
```rust
use std::collections::HashMap;
use std::path::Path;
use apcore::config::DEFAULT_MAX_DEPTH;
use apcore::{Config, EnvStyle, NamespaceRegistration};
Config::env_map(HashMap::from([("PORT".into(), "port".into())]))?;
Config::register_namespace(NamespaceRegistration {
name: "myapp".into(),
env_prefix: None,
defaults: None,
schema: None,
env_style: EnvStyle::Auto,
max_depth: DEFAULT_MAX_DEPTH,
env_map: Some(HashMap::from([("REDIS_URL".into(), "cache_url".into())])),
})?;
let cfg = Config::load(Path::new("apcore.yaml"))?;
cfg.get("port"); // from PORT env var (top-level)
cfg.get("myapp.cache_url"); // from REDIS_URL env var (namespace)
cfg.get("myapp.debug"); // from MYAPP_DEBUG env var (prefix)
```
=== "Python"
```python
from apcore import Config
Config.env_map({"PORT": "port", "DATABASE_URL": "db_url"})
Config.register_namespace(
"myapp",
defaults={"devto_api_key": "", "publish": {"delay": 5}},
env_map={"REDIS_URL": "cache_url"},
)
# Environment:
# PORT=3000
# DATABASE_URL=postgres://prod
# REDIS_URL=redis://localhost
# MYAPP_DEVTO_API_KEY=abc123
# MYAPP_PUBLISH_DELAY=10
cfg = Config.load("apcore.yaml")
cfg.get("port") # → 3000 (global env_map)
cfg.get("db_url") # → "postgres://prod" (global env_map)
cfg.get("myapp.cache_url") # → "redis://localhost" (namespace env_map)
cfg.get("myapp.devto_api_key") # → "abc123" (auto: flat key in defaults)
cfg.get("myapp.publish.delay") # → 10 (auto: nested key in defaults)
```
=== "TypeScript"
```typescript
import { Config } from 'apcore-js';
Config.envMap({ PORT: 'port', DATABASE_URL: 'db_url' });
Config.registerNamespace({
name: 'myapp',
defaults: { devto_api_key: '', publish: { delay: 5 } },
envMap: { REDIS_URL: 'cache_url' },
});
// Environment:
// PORT=3000
// DATABASE_URL=postgres://prod
// REDIS_URL=redis://localhost
// MYAPP_DEVTO_API_KEY=abc123
// MYAPP_PUBLISH_DELAY=10
const cfg = Config.load('apcore.yaml');
cfg.get('port'); // → 3000 (global env_map)
cfg.get('db_url'); // → "postgres://prod" (global env_map)
cfg.get('myapp.cache_url'); // → "redis://localhost" (namespace env_map)
cfg.get('myapp.devto_api_key'); // → "abc123" (auto: flat key in defaults)
cfg.get('myapp.publish.delay'); // → 10 (auto: nested key in defaults)
```
=== "Rust"
```rust
use std::collections::HashMap;
use std::path::Path;
use apcore::config::DEFAULT_MAX_DEPTH;
use apcore::{Config, EnvStyle, NamespaceRegistration};
Config::env_map(HashMap::from([
("PORT".into(), "port".into()),
("DATABASE_URL".into(), "db_url".into()),
]))?;
Config::register_namespace(NamespaceRegistration {
name: "myapp".into(),
env_prefix: None,
defaults: Some(serde_json::json!({
"devto_api_key": "",
"publish": { "delay": 5 },
})),
schema: None,
env_style: EnvStyle::Auto,
max_depth: DEFAULT_MAX_DEPTH,
env_map: Some(HashMap::from([("REDIS_URL".into(), "cache_url".into())])),
})?;
// Environment:
// PORT=3000
// DATABASE_URL=postgres://prod
// REDIS_URL=redis://localhost
// MYAPP_DEVTO_API_KEY=abc123
// MYAPP_PUBLISH_DELAY=10
let cfg = Config::load(Path::new("apcore.yaml"))?;
cfg.get("port"); // → 3000 (global env_map)
cfg.get("db_url"); // → "postgres://prod" (global env_map)
cfg.get("myapp.cache_url"); // → "redis://localhost" (namespace env_map)
cfg.get("myapp.devto_api_key"); // → "abc123" (auto: flat key in defaults)
cfg.get("myapp.publish.delay"); // → 10 (auto: nested key in defaults)
```
name(str/string/&str, required) — namespace name; must not be"apcore"or"_config"(reserved); reject withConfigNamespaceReservedError(code=CONFIG_NAMESPACE_RESERVED)env_prefix(str/string/&str, optional) — env var prefix; auto-derived fromnamewhen absent; reject duplicate prefix withConfigEnvPrefixConflictError(code=CONFIG_ENV_PREFIX_CONFLICT)defaults(dict/object/Value, optional) — default values merged under this namespace before YAML/env overridesenv_style(str/string/EnvStyle, optional) —"auto"(default),"nested", or"flat"; controls env var suffix conversionmax_depth(int/number/u32, optional) — maximum nesting depth for nested/auto routing; default 5env_map(dict/object/HashMap, optional) — bare env var → namespace key mapping; each key must be globally unique; reject conflicts withConfigEnvMapConflictError(code=CONFIG_ENV_MAP_CONFLICT)schema(dict/object/Value, optional) — JSON Schema Draft 2020-12 for namespace values; validation runs at load time
ConfigNamespaceReservedError(code=CONFIG_NAMESPACE_RESERVED)—nameis"apcore"or"_config"ConfigNamespaceDuplicateError(code=CONFIG_NAMESPACE_DUPLICATE)— namespace already registered under this nameConfigEnvPrefixConflictError(code=CONFIG_ENV_PREFIX_CONFLICT)— derived or explicitenv_prefixalready claimed by another namespaceConfigEnvMapConflictError(code=CONFIG_ENV_MAP_CONFLICT)— a bare env var inenv_mapis already mapped by another namespace or the global env_map
- On success: void/None/() — namespace is registered class-globally for all future
Config.load()calls
- async: false
- thread_safe: false (call before any concurrent
Config.load()) - pure: false (mutates class-level namespace registry)
- idempotent: false (duplicate registration raises an error)
path(str/string/&str or Path, optional) — file path to a YAML config file; if absent, falls back toConfig.discover()search order
ConfigNotFoundError(code=CONFIG_NOT_FOUND)— path was provided but does not exist on diskConfigInvalidError(code=CONFIG_INVALID)— file exists but is not valid YAML or its structure cannot be parsed
- On success:
Configinstance with namespace data, env overrides, and defaults merged
- async: false
- thread_safe: false (safe to call from multiple threads only if namespace registry is fully populated and frozen)
- pure: false (reads filesystem and environment variables)
- idempotent: true (loading the same file twice produces equivalent Config instances)
key(str/string/&str, required) — dot-path key (e.g.,"my-plugin.timeout"or"port"); empty string is rejected withValueError/ConfigInvalidErrordefault(Any/unknown/Value, optional) — value returned when key is absent; when absent and key is missing, returnsNone/null/None
- No errors raised under normal operation (missing key returns
defaultorNone)
- On success: the resolved value at
key, ordefaultif absent
- async: false
- thread_safe: true
- pure: true (no side effects; reads merged config snapshot)
- idempotent: true
name(str/string/&str, required) — registered namespace name; returns empty dict/object when namespace has no values (does not raise)
- No errors under normal operation; unregistered namespace returns empty result
- On success:
dict[str, Any]/Record<string, unknown>/HashMap<String, Value>— all values under that namespace, merged from defaults + YAML + env overrides
- async: false
- thread_safe: true
- pure: true
namespace(str/string/&str, required) — namespace name to deserializetype(type/class/generic, required) — dataclass type (Python), constructor class (TypeScript), or Rust genericT: Deserialize
ConfigBindError(code=CONFIG_BIND_ERROR)— deserialization/instantiation fails (type mismatch, missing required field, constructor raises)
- On success: an instance of
typepopulated with namespace values
- async: false
- thread_safe: true
- pure: true (reads snapshot; does not mutate config)
namespace(str/string/&str, required) — target namespace; must NOT be"_config"(reserved)source(dict/object/MountSource, required) — either an in-memory dict or a path to a YAML file (relative paths resolved from CWD)
ConfigMountError(code=CONFIG_MOUNT_ERROR)— namespace is_config; or source is a file path that does not exist; or file is not a valid YAML mapping
- On success: void/None/() — source data is merged into the namespace, over defaults, under env overrides
- async: false
- thread_safe: false (do not call concurrently with reads)
- pure: false (mutates config state)
- idempotent: false (mounting the same source twice stacks; call once per source)
- No inputs
ConfigNotFoundError(code=CONFIG_NOT_FOUND)— source file no longer existsConfigInvalidError(code=CONFIG_INVALID)— source file is now invalid YAML
- On success: void/None/() — config is refreshed from disk; env overrides and mounts are re-applied
- async: false
- thread_safe: false (call outside of concurrent request handling; no in-flight read protection)
- pure: false (re-reads filesystem and environment variables)
- idempotent: true (calling reload twice with unchanged files produces identical state)
| File | Purpose |
|---|---|
src/apcore/config.py (Python) |
Config class, register_namespace(), env_map(), namespace(), bind(), mount() |
src/config.ts (TypeScript) |
Config class with identical API shape |
src/config.rs (Rust) |
Config struct, NamespaceRegistration, EnvStyle, MountSource, ConfigMode |
- Namespace registration: reserved name rejection, duplicate rejection, env prefix conflict, env_map conflict.
- Env routing: longest-prefix-match, env_style (auto/nested/flat), max_depth enforcement, env_map dispatch.
- env_prefix auto-derivation: name → uppercase, hyphen → underscore.
- Global env_map: bare env var → top-level key in both legacy and namespace mode.
- Mount: dict mount merge, file mount parse,
_configrejection, missing file error. - Bind: successful deserialization, bind error on invalid data.
- Mode detection: legacy YAML activates legacy mode,
apcore:key activates namespace mode. - Config discovery:
$APCORE_CONFIG_FILEenv precedence, CWD candidates, XDG path, fallback to defaults. - Hot reload: changes in YAML picked up, mounts re-applied.