Объявление секции конфига
Каждый компонент / плагин / сервис приложения должен объявить свою секцию конфига — верхнеуровневый ключ в YAML и schema, которая этому ключу соответствует.
Шаг 1. Выбрать имя секции
Соглашения по именованию:
| Рекомендация | Пример |
|---|---|
snake_case | database, payment_provider, rate_limit |
| Существительное в ед. числе | database, не databases |
Без _config / _settings суффикса | database, не database_config |
| Короткое, но уникальное | 2–3 слова через _ |
Для плагинов имя секции нормативно: <kind>.<name> (ADR-0001 §4).
Шаг 2. Написать schema
- Python
- TypeScript
- Go
from pydantic import BaseModel, Field, field_validator
class DatabaseConfig(BaseModel):
host: str
port: int = Field(5432, ge=1, le=65535)
name: str
user: str
password: str = Field(..., min_length=1)
pool_size: int = Field(20, ge=1, le=1000)
ssl: bool = False
@field_validator("host")
@classmethod
def must_not_be_wildcard(cls, v: str) -> str:
if v in ("0.0.0.0", "*"):
raise ValueError("host must be a concrete address")
return v
import { z } from "zod";
export const DatabaseConfig = z.object({
host: z.string().refine(
(v) => v !== "0.0.0.0" && v !== "*",
"host must be a concrete address",
),
port: z.number().int().min(1).max(65535).default(5432),
name: z.string(),
user: z.string(),
password: z.string().min(1),
pool_size: z.number().int().min(1).max(1000).default(20),
ssl: z.boolean().default(false),
});
export type DatabaseConfig = z.infer<typeof DatabaseConfig>;
type DatabaseConfig struct {
Host string `yaml:"host" validate:"required,ne=0.0.0.0"`
Port int `yaml:"port" validate:"min=1,max=65535"`
Name string `yaml:"name" validate:"required"`
User string `yaml:"user" validate:"required"`
Password string `yaml:"password" validate:"required,min=1"`
PoolSize int `yaml:"pool_size" validate:"min=1,max=1000"`
SSL bool `yaml:"ssl"`
}
func defaultDatabaseConfig() DatabaseConfig {
return DatabaseConfig{
Port: 5432,
PoolSize: 20,
SSL: false,
}
}
Требования к schema:
- Все обязательные поля явно помечены (
Field(...)без default в pydantic;z.string()без.default()в zod;validate:"required"в Go struct-tag). - Значения по умолчанию прописаны для всех опциональных полей.
- Валидация диапазонов (min / max / pattern) — декларативно, чтобы не разруливать в рантайме.
- Типизированные поля — избегайте
Any/unknown; каждое поле с явным типом.
Шаг 3. Прочитать секцию
Метод различается по имени между биндингами (get_section / getSection / GetSection):
- Python
- TypeScript
- Go
from dagstack.config import Config
config = Config.load("app-config.yaml")
db_cfg = config.get_section("database", DatabaseConfig)
# db_cfg — инстанс DatabaseConfig, уже валидирован.
import { Config } from "@dagstack/config";
const config = await Config.load("app-config.yaml");
const dbCfg = config.getSection("database", DatabaseConfigSchema);
// dbCfg — значение z.infer<typeof DatabaseConfigSchema>, уже валидирован.
cfg, _ := config.Load(context.Background(), "app-config.yaml")
dbCfg := defaultDatabaseConfig()
if err := cfg.GetSection("database", &dbCfg); err != nil {
return err
}
Метод выполняет:
- Извлекает поддерево по path.
- Применяет schema-валидацию.
- Возвращает типизированный объект.
При неудаче → ConfigError(validation_failed) с указанием поля, ожидаемого типа и актуального значения (секретные поля замаскированы).
Шаг 4. Изоляция — читай только свою секцию
Правило: компонент читает только свою секцию. Чтение чужих секций — антипаттерн.
- Python
- TypeScript
- Go
# Правильно — в сервисе работы с БД:
db_cfg = config.get_section("database", DatabaseConfig)
# Неправильно — в БД-сервисе читаем чужую секцию:
cache_cfg = config.get_section("cache", CacheConfig)
# Сервис БД теперь зависит от структуры cache.
// Правильно — в сервисе работы с БД:
const dbCfg = config.getSection("database", DatabaseConfigSchema);
// Неправильно — в БД-сервисе читаем чужую секцию:
const cacheCfg = config.getSection("cache", CacheConfigSchema);
// Сервис БД теперь зависит от структуры cache.
// Правильно — в сервисе работы с БД:
var dbCfg DatabaseConfig
_ = cfg.GetSection("database", &dbCfg)
// Неправильно — в БД-сервисе читаем чужую секцию:
var cacheCfg CacheConfig
_ = cfg.GetSection("cache", &cacheCfg)
// Сервис БД теперь зависит от структуры cache.
Причины:
- Связность. Если сервис A читает конфиг сервиса B, изменение schema сервиса B ломает сервис A. Скрытая связь без видимых границ.
- Тестируемость. Мокать только
database-секцию проще, чем мокать весь конфиг со всеми чужими секциями. - Безопасность. В production-сценарии сервисы могут иметь разные права на чтение секций (через governance-middleware); жёстко прописанное чтение чужой секции обходит политику доступа.
Правильная альтернатива: если два сервиса должны координироваться через общую настройку (например, общий max_payload_mb), вынесите её в отдельную секцию shared, которую читают оба:
shared:
max_payload_mb: 10
database:
host: "localhost"
pool_size: 20
api:
host: "0.0.0.0"
port: 8080
Шаг 5. Значения по умолчанию для опциональных полей
Все опциональные поля декларируются со значениями по умолчанию в schema, не в коде:
- Python
- TypeScript
- Go
class DatabaseConfig(BaseModel):
host: str # обязательное
user: str # обязательное
password: str # обязательное
port: int = 5432 # значение по умолчанию в schema
pool_size: int = 20 # значение по умолчанию в schema
ssl: bool = False # значение по умолчанию в schema
const DatabaseConfigSchema = z.object({
host: z.string(), // обязательное
user: z.string(), // обязательное
password: z.string(), // обязательное
port: z.number().int().default(5432), // значение по умолчанию в schema
pool_size: z.number().int().default(20), // значение по умолчанию в schema
ssl: z.boolean().default(false), // значение по умолчанию в schema
});
// Go задаёт default через заполнение zero-value struct ДО GetSection.
// GetSection заполняет только те поля, что присутствуют в YAML —
// остальные остаются с изначальным значением.
dbCfg := DatabaseConfig{
Port: 5432,
PoolSize: 20,
SSL: false,
}
_ = cfg.GetSection("database", &dbCfg)
// host/user/password — обязательные (валидируются внешне или через
// go-playground/validator struct-тегами).
Анти-паттерн — оборонять default в коде после чтения:
- Python
- TypeScript
- Go
# НЕПРАВИЛЬНО — значения по умолчанию в коде:
cfg = config.get_section("database", DatabaseConfig)
port = cfg.port or 5432 # ← такое значение по умолчанию делает schema нечестной
pool = cfg.pool_size or 20
// НЕПРАВИЛЬНО — значения по умолчанию в коде:
const cfg = config.getSection("database", DatabaseConfigSchema);
const port = cfg.port ?? 5432; // ← такое значение по умолчанию делает schema нечестной
const pool = cfg.pool_size ?? 20;
// НЕПРАВИЛЬНО — default в коде после вызова:
var cfg DatabaseConfig
_ = config.GetSection("database", &cfg)
if cfg.Port == 0 { cfg.Port = 5432 } // ← schema нечестная
if cfg.PoolSize == 0 { cfg.PoolSize = 20 }
Если значение по умолчанию задано в schema, то get_section / getSection / GetSection всегда возвращает полностью заполненный объект. Коду не нужно гадать.
Шаг 6. Документирование секции
Каждая секция должна быть задокументирована:
- Описание каждого поля — комментарий / docstring в schema.
- Пример конфига — пример
app-config.yamlв README компонента. - Пояснения по диапазонам — почему
pool_sizeот 1 до 1000, а не без верхней границы.
- Python
- TypeScript
- Go
class DatabaseConfig(BaseModel):
"""Configuration for application database connection.
Note: password is masked in diagnostic output automatically.
"""
host: str = Field(
...,
description="Database server host.",
examples=["localhost", "db.internal.example.com"],
)
port: int = Field(
5432,
description="TCP port of the database server.",
ge=1,
le=65535,
)
pool_size: int = Field(
20,
description="Connection pool size. Range: 1-1000. Typical dev: 5-10, prod: 50-200.",
ge=1,
le=1000,
)
pydantic.Field description / examples попадают в JSON-schema,
который затем отображается в IDE-подсказках и в автогенерируемой
документации.
/**
* Configuration for application database connection.
* Note: password is masked in diagnostic output automatically.
*/
const DatabaseConfigSchema = z.object({
host: z
.string()
.describe("Database server host, e.g. 'localhost' or 'db.internal.example.com'."),
port: z
.number()
.int()
.min(1)
.max(65535)
.default(5432)
.describe("TCP port of the database server."),
pool_size: z
.number()
.int()
.min(1)
.max(1000)
.default(20)
.describe("Connection pool size. Typical dev: 5-10, prod: 50-200."),
});
type DatabaseConfig = z.infer<typeof DatabaseConfigSchema>;
zod .describe() метаданные доступны через DatabaseConfigSchema._def.description
и попадают в zod-to-json-schema конверсию для автогенерации документации.
// DatabaseConfig — configuration for application database connection.
//
// Note: password is masked in diagnostic output automatically.
type DatabaseConfig struct {
// Host is the database server host, e.g. "localhost" or
// "db.internal.example.com".
Host string `yaml:"host" validate:"required"`
// Port is the TCP port of the database server. Range: 1-65535.
Port int `yaml:"port" validate:"gte=1,lte=65535" default:"5432"`
// PoolSize is the connection pool size. Typical dev: 5-10,
// prod: 50-200. Range: 1-1000.
PoolSize int `yaml:"pool_size" validate:"gte=1,lte=1000" default:"20"`
}
Go doc-комментарии отображаются в go doc / pkg.go.dev / IDE-подсказках
автоматически. Струкcт-теги (validate: / default:) интерпретируются
go-playground/validator + creasty/defaults на этапе
GetSection(path, &dbCfg).
См. также
- Подстановка env-переменных — как секретные поля получаются из env.
- Секреты — автоматическое маскирование password / *_token / *_key.
- Руководство: Тестирование — как тестировать секцию с mock-конфигом.
- ADR-0001 §4 Config access API — нормативный контракт getSection.