rust-config-tree 0.1.9

Recursive include tree utilities for layered configuration files.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
# rust-config-tree

[English]README.md | [中文]README.zh.md | [日本語]README.ja.md | [한국어]README.ko.md | [Français]README.fr.md | [Deutsch]README.de.md | [Español]README.es.md | [Português]README.pt.md | [Svenska]README.sv.md | [Suomi]README.fi.md | [Nederlands]README.nl.md

`rust-config-tree` proporciona carga de árboles de configuración y ayudantes de
CLI para aplicaciones Rust que usan archivos de configuración por capas.

Manual del proyecto: <https://developerworks.github.io/rust-config-tree/>.
Los manuales por idioma se publican como sitios mdBook independientes con
enlaces para cambiar de idioma.

Gestiona:

- cargar un esquema `confique` en un objeto de configuración directamente
  utilizable mediante proveedores de Figment en tiempo de ejecución
- manejadores de comandos `config-template`, `config-schema`,
  `config-validate`, `completions`, `install-completions` y
  `uninstall-completions`
- generación de JSON Schema Draft 7 para la raíz y las secciones, útil para
  completado y comprobaciones básicas de esquema en editores
- generación de plantillas de configuración para YAML, TOML, JSON y JSON5
- enlaces de esquema para plantillas TOML, YAML, JSON y JSON5
- recorrido recursivo de includes
- carga de `.env` antes de fusionar valores de entorno
- seguimiento de origen mediante metadatos de Figment
- logs de seguimiento de origen en nivel TRACE mediante `tracing`
- rutas de include relativas resueltas desde el archivo que las declara
- normalización léxica de rutas
- detección de ciclos de include
- orden de recorrido determinista
- recopilación reflejada de destinos de plantilla
- división opt-in de plantillas YAML para secciones marcadas con `x-tree-split`

Las aplicaciones proporcionan su esquema derivando `confique::Config` e
implementando `ConfigSchema` para exponer el campo de includes del esquema.

## Instalación

```toml
[dependencies]
rust-config-tree = "0.1"
confique = { version = "0.4", features = ["yaml", "toml", "json5"] }
figment = { version = "0.10", features = ["yaml", "toml", "json", "env"] }
schemars = { version = "1", features = ["derive"] }
serde = { version = "1", features = ["derive"] }
clap = { version = "4", features = ["derive"] }
```

## Esquema de configuración

El esquema de la aplicación es dueño del campo de includes. `rust-config-tree`
solo necesita un pequeño adaptador que extrae los includes de la capa
intermedia de `confique`.

```rust
use std::path::PathBuf;

use confique::Config;
use schemars::JsonSchema;
use rust_config_tree::ConfigSchema;

#[derive(Debug, Config, JsonSchema)]
struct AppConfig {
    #[config(default = [])]
    include: Vec<PathBuf>,

    #[config(default = "paper")]
    mode: String,

    #[config(nested)]
    #[schemars(extend("x-tree-split" = true))]
    server: ServerConfig,
}

#[derive(Debug, Config, JsonSchema)]
struct ServerConfig {
    #[config(default = 8080)]
    port: u16,
}

impl ConfigSchema for AppConfig {
    fn include_paths(layer: &<Self as Config>::Layer) -> Vec<PathBuf> {
        layer.include.clone().unwrap_or_default()
    }
}
```

Las rutas de include relativas se resuelven desde el archivo que las declara:

```yaml
# config.yaml
include:
  - config/server.yaml

mode: shadow
```

```yaml
# config/server.yaml
server:
  port: 7777
```

Carga el esquema final con `load_config`:

```rust
use rust_config_tree::load_config;

fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let config = load_config::<AppConfig>("config.yaml")?;
    println!("{config:#?}");

    Ok(())
}
```

`load_config` carga el primer archivo `.env` encontrado al caminar hacia arriba
desde el directorio del archivo de configuración raíz antes de pedir a Figment
que lea las variables de entorno declaradas por el esquema. Los valores ya
presentes en el entorno del proceso se conservan y tienen prioridad sobre los
valores de `.env`.

La carga de configuración en tiempo de ejecución se realiza mediante Figment.
`confique` sigue siendo responsable de los metadatos del esquema, valores por
defecto, validación y generación de plantillas. Los nombres de variables de
entorno se leen de `#[config(env = "...")]`; el cargador no usa
`Env::split("_")` ni `Env::split("__")`, por lo que una variable como
`APP_DATABASE_POOL_SIZE` puede mapearse a un campo llamado
`database.pool_size`.

`load_config` no lee argumentos de línea de comandos porque las flags de CLI
son específicas de la aplicación. Añade overrides de CLI fusionando un
proveedor después de `build_config_figment` y luego valida con
`load_config_from_figment`:

Los nombres de flags de CLI no se derivan de rutas de configuración. Usa flags
normales de aplicación como `--server-port` o `--database-url`; no dependas de
`--server.port` o `a.b.c` salvo que la aplicación implemente deliberadamente
ese parser. La forma serializada anidada del override decide qué clave de
configuración se sobrescribe.

Solo los valores representados en el proveedor `CliOverrides` de la aplicación
pueden sobrescribir la configuración. Esto está pensado para parámetros de
tiempo de ejecución que se ajustan con frecuencia, donde cambiar una flag para
una ejecución es mejor que editar un archivo de configuración. Mantén la
configuración estable en archivos y expón como flags solo overrides temporales
deliberados.

```rust
use figment::providers::Serialized;
use serde::Serialize;
use rust_config_tree::{build_config_figment, load_config_from_figment};

#[derive(Debug, Serialize)]
struct CliOverrides {
    #[serde(skip_serializing_if = "Option::is_none")]
    mode: Option<String>,
}

fn load_with_cli_overrides(cli_mode: Option<String>) -> Result<AppConfig, Box<dyn std::error::Error + Send + Sync>> {
    let cli_overrides = CliOverrides {
        mode: cli_mode,
    };

    let figment = build_config_figment::<AppConfig>("config.yaml")?
        .merge(Serialized::defaults(cli_overrides));

    let config = load_config_from_figment::<AppConfig>(&figment)?;
    Ok(config)
}
```

Con overrides de CLI fusionados de esta forma, la precedencia en tiempo de
ejecución es:

```text
command-line overrides
  > environment variables
    > config files
      > confique code defaults
```

Usa `load_config_with_figment` cuando el llamador necesita metadatos de origen:

```rust
use rust_config_tree::load_config_with_figment;

fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let (config, figment) = load_config_with_figment::<AppConfig>("config.yaml")?;

    if let Some(metadata) = figment.find_metadata("mode") {
        let source = metadata.interpolate(&figment::Profile::Default, &["mode"]);
        println!("mode came from {source}");
    }

    println!("{config:#?}");

    Ok(())
}
```

El cargador también emite seguimiento de origen de configuración con
`tracing::trace!`. Esos eventos solo se producen cuando TRACE está habilitado
por el subscriber de `tracing` de la aplicación. Si `tracing` se inicializa
después de cargar la configuración, llama a
`trace_config_sources::<AppConfig>(&figment)` después de instalar el subscriber.

## Generación de plantillas

Las plantillas se renderizan con el mismo esquema y las mismas reglas de
recorrido de includes. El formato de salida se infiere de la ruta de salida:

- `.yaml` y `.yml` generan YAML
- `.toml` genera TOML
- `.json` y `.json5` generan plantillas compatibles con JSON5
- extensiones desconocidas o ausentes generan YAML

Usa `write_config_schemas` para crear JSON Schemas Draft 7 para la
configuración raíz y las secciones marcadas con `x-tree-split`. Los esquemas generados omiten
restricciones `required` para que los IDE puedan ofrecer completado en archivos
de configuración parciales sin informar campos faltantes. Los archivos
`*.schema.json` generados sirven solo para completado de IDE y comprobaciones
básicas del editor; no deciden si un valor concreto de campo es válido para la
aplicación. La validación de valores debe implementarse en código con
`#[config(validate = Self::validate)]` y ejecutarse mediante `load_config` o
`config-validate`:

```rust
use rust_config_tree::write_config_schemas;

fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    write_config_schemas::<AppConfig>("schemas/myapp.schema.json")?;

    Ok(())
}
```

Mark a nested field with `#[schemars(extend("x-tree-split" = true))]` when it
should be generated as its own `*.yaml` template and
`<section>.schema.json` schema. Unmarked nested fields stay in the parent
template and parent schema.

Marca un campo hoja con `#[schemars(extend("x-env-only" = true))]` cuando el valor debe venir solo de variables de entorno. Las plantillas generadas y los JSON Schemas omiten los campos env-only, y tambien se eliminan los objetos padre que queden vacios.

Para un esquema con secciones `server` y `log` marcadas con `x-tree-split`, esto escribe
`schemas/myapp.schema.json`, `schemas/server.schema.json` y
`schemas/log.schema.json`. El esquema raíz contiene solo campos que pertenecen
al archivo de configuración raíz, como `include` y campos escalares raíz. Omite
deliberadamente las propiedades de secciones divididas, de modo que `server` y
`log` solo se completan al editar sus propios archivos YAML de sección.

Usa `write_config_templates` para crear una plantilla raíz y todos los archivos
de plantilla alcanzables desde su árbol de includes:

```rust
use rust_config_tree::write_config_templates;

fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    write_config_templates::<AppConfig>("config.yaml", "config.example.yaml")?;

    Ok(())
}
```

Usa `write_config_templates_with_schema` cuando las plantillas TOML, YAML, JSON
y JSON5 generadas deban enlazar esos esquemas para completado y comprobaciones
básicas de esquema en el IDE:

```rust
use rust_config_tree::write_config_templates_with_schema;

fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    write_config_templates_with_schema::<AppConfig>(
        "config.toml",
        "config.example.toml",
        "schemas/myapp.schema.json",
    )?;

    Ok(())
}
```

Los destinos raíz TOML y YAML enlazan el esquema raíz y no completan campos de
secciones hijas. Los destinos YAML de secciones divididas enlazan su esquema de
sección correspondiente; por ejemplo, `log.yaml` recibe
`# yaml-language-server: $schema=./schemas/log.schema.json`. Los destinos JSON
y JSON5 reciben un campo raíz `$schema` que VS Code puede reconocer.
VS Code `json.schemas` sigue siendo una ruta de enlace alternativa.

La generación de plantillas elige su árbol fuente en este orden:

- una ruta de configuración existente
- una ruta de plantilla de salida existente
- la ruta de salida, tratada como un nuevo árbol de plantillas vacío

Si un nodo fuente no tiene lista de includes, `rust-config-tree` deriva
archivos de plantilla hijos desde las secciones anidadas de `confique` marcadas con `x-tree-split`. Con el
esquema anterior, una fuente `config.example.yaml` vacía produce:

```text
config.example.yaml
server.yaml
```

La plantilla raíz recibe un bloque include para `server.yaml`. Los
destinos YAML que se mapean a una sección anidada, como `server.yaml`,
contienen solo esa sección. Las secciones anidadas mas profundas solo se dividen
recursivamente cuando esos campos tambien llevan `x-tree-split`.

Sobrescribe `template_path_for_section` cuando una sección deba generarse en
una ruta distinta:

```rust
use std::path::PathBuf;

use confique::Config;
use rust_config_tree::ConfigSchema;

impl ConfigSchema for AppConfig {
    fn include_paths(layer: &<Self as Config>::Layer) -> Vec<PathBuf> {
        layer.include.clone().unwrap_or_default()
    }

    fn template_path_for_section(section_path: &[&str]) -> Option<PathBuf> {
        match section_path {
            ["server"] => Some(PathBuf::from("examples/server.yaml")),
            _ => None,
        }
    }
}
```

La ruta de sección por defecto es `<section>.yaml` para secciones
anidadas de primer nivel. Los hijos anidados se colocan bajo el stem del archivo
padre; por ejemplo, `trading/risk.yaml`.

## Integración CLI

Aplana `ConfigCommand` dentro de tu enum de comandos clap existente para añadir:

- `config-template`
- `config-schema`
- `config-validate`
- `completions`
- `install-completions`
- `uninstall-completions`

La aplicación consumidora conserva su propio tipo `Parser` y su propio enum de
comandos. `rust-config-tree` solo aporta subcomandos reutilizables:

1. Añade `#[command(subcommand)] command: Command` al parser de la aplicación.
2. Añade `#[command(flatten)] Config(ConfigCommand)` al enum `Subcommand` de la
   aplicación.
3. Clap expande las variantes aplanadas en el mismo nivel de subcomandos que
   los comandos propios de la aplicación.
4. Haz match de esa variante y llama a
   `handle_config_command::<Cli, AppConfig>`.

Las flags de override de configuración específicas de la aplicación permanecen
en el parser propio de la aplicación. Por ejemplo, `--server-port` puede
mapearse a `server.port` construyendo un valor anidado
`CliOverrides { server: Some(CliServerOverrides { port }) }` y fusionándolo con
`Serialized::defaults`.

```rust
use std::path::PathBuf;

use clap::{Parser, Subcommand};
use confique::Config;
use schemars::JsonSchema;
use rust_config_tree::{ConfigCommand, ConfigSchema, handle_config_command, load_config};

#[derive(Debug, Config, JsonSchema)]
struct AppConfig {
    #[config(default = [])]
    include: Vec<PathBuf>,
    #[config(default = "paper")]
    mode: String,
}

impl ConfigSchema for AppConfig {
    fn include_paths(layer: &<Self as Config>::Layer) -> Vec<PathBuf> {
        layer.include.clone().unwrap_or_default()
    }
}

#[derive(Debug, Parser)]
#[command(name = "demo")]
struct Cli {
    #[arg(long, default_value = "config.yaml")]
    config: PathBuf,
    #[arg(long)]
    server_port: Option<u16>,
    #[command(subcommand)]
    command: Command,
}

#[derive(Debug, Subcommand)]
enum Command {
    Run,
    #[command(flatten)]
    Config(ConfigCommand),
}

fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let cli = Cli::parse();
    match cli.command {
        Command::Run => {
            let config = load_config::<AppConfig>(&cli.config)?;
            println!("{config:#?}");
        }
        Command::Config(command) => {
            handle_config_command::<Cli, AppConfig>(command, &cli.config)?;
        }
    }

    Ok(())
}
```

`config-template --output <file-name>` escribe plantillas bajo
`config/<root_config_name>/` usando el nombre de archivo seleccionado. Si se
proporciona una ruta, solo se usa su nombre de archivo. Si no se proporciona un
nombre de archivo de salida, escribe
`config/<root_config_name>/<root_config_name>.example.yaml`. Añade
`--schema <path>` para enlazar plantillas TOML, YAML, JSON y JSON5 a un
conjunto de JSON Schema generado. Las plantillas JSON y JSON5 reciben un campo
`$schema` que VS Code reconoce. Esto también escribe el esquema raíz y los
esquemas de sección en la ruta de esquema seleccionada.

`config-schema --output <path>` escribe el JSON Schema Draft 7 raíz y los
esquemas de sección. Si no se proporciona ruta de salida, el esquema raíz se
escribe en `config/<root_config_name>/<root_config_name>.schema.json`.

`config-validate` carga el árbol completo de configuración en tiempo de
ejecución y ejecuta los valores por defecto y la validación de `confique`,
incluidos los validadores declarados con `#[config(validate = Self::validate)]`.
Usa los esquemas del editor para completado sin ruido mientras editas archivos
divididos; usa este comando para campos obligatorios y validación final de la
configuración. Imprime `Configuration is ok` cuando la validación tiene éxito.

`completions <shell>` imprime completions a stdout.

`install-completions <shell>` escribe completions bajo el directorio home del
usuario y actualiza el archivo de inicio del shell cuando el shell lo requiere.
Se admiten Bash, Elvish, Fish, PowerShell y Zsh.

`uninstall-completions <shell>` elimina el archivo de completion del binario
actual y elimina el bloque administrado del archivo de inicio cuando ese shell
usa uno.

## API de árbol de bajo nivel

Usa `load_config_tree` cuando no uses `confique` o cuando necesites acceso
directo a los resultados del recorrido:

```rust
use std::{fs, io, path::{Path, PathBuf}};

use rust_config_tree::{ConfigSource, load_config_tree};

fn load_source(path: &Path) -> io::Result<ConfigSource<String>> {
    let content = fs::read_to_string(path)?;
    let includes = content
        .lines()
        .filter_map(|line| line.strip_prefix("include: "))
        .map(PathBuf::from)
        .collect();

    Ok(ConfigSource::new(content, includes))
}

fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let tree = load_config_tree("config.yaml", load_source)?;

    for node in tree.nodes() {
        println!("{}", node.path().display());
    }

    Ok(())
}
```

La API de árbol normaliza rutas léxicamente, rechaza rutas de include vacías,
detecta ciclos de include recursivos y omite archivos que ya se cargaron por
otra rama de include.

## Licencia

Licenciado bajo cualquiera de:

- Apache License, Version 2.0
- MIT license

a tu elección.