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
# 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` fornece carregamento de arvores de configuracao e auxiliares
de CLI para aplicacoes Rust que usam arquivos de configuracao em camadas.

Manual do projeto: <https://developerworks.github.io/rust-config-tree/>. Os
manuais de cada idioma sao publicados como sites mdBook independentes com links
para troca de idioma.

Ele lida com:

- carregamento de um esquema `confique` em um objeto de configuracao diretamente
  utilizavel por meio de provedores Figment em tempo de execucao
- manipuladores dos comandos `config-template`, `config-schema`,
  `config-validate`, `completions`, `install-completions` e
  `uninstall-completions`
- geracao de JSON Schema Draft 7 para a raiz e para secoes, para completamento e
  verificacoes basicas de esquema no editor
- geracao de modelos de configuracao para YAML, TOML, JSON e JSON5
- vinculos de esquema para modelos TOML, YAML, JSON e JSON5
- travessia recursiva de includes
- carregamento de `.env` antes que valores de ambiente sejam mesclados
- rastreamento de origem por metadados Figment
- logs de rastreamento de origem em nivel TRACE por `tracing`
- caminhos de include relativos resolvidos a partir do arquivo que os declara
- normalizacao lexical de caminhos
- deteccao de ciclos de include
- ordem de travessia deterministica
- coleta espelhada de destinos de modelo
- divisao opt-in de modelos YAML para secoes marcadas com `x-tree-split`

As aplicacoes fornecem seu esquema derivando `confique::Config` e implementando
`ConfigSchema` para expor o campo de include do esquema.

## Instalacao

```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 configuracao

O esquema da sua aplicacao e dono do campo de include. `rust-config-tree` so
precisa de um pequeno adaptador que extraia includes da camada intermediaria do
`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()
    }
}
```

Caminhos de include relativos sao resolvidos a partir do arquivo que os declara:

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

mode: shadow
```

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

Carregue o esquema final com `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` carrega o primeiro arquivo `.env` encontrado ao subir a partir do
diretorio do arquivo de configuracao raiz antes de pedir ao Figment que leia as
variaveis de ambiente declaradas no esquema. Valores ja presentes no ambiente do
processo sao preservados e tem precedencia sobre valores de `.env`.

O carregamento de configuracao em tempo de execucao e realizado pelo Figment.
`confique` continua responsavel por metadados de esquema, padroes, validacao e
geracao de modelos. Nomes de variaveis de ambiente sao lidos de
`#[config(env = "...")]`; o carregador nao usa `Env::split("_")` nem
`Env::split("__")`, entao uma variavel como `APP_DATABASE_POOL_SIZE` pode mapear
para um campo chamado `database.pool_size`.

`load_config` nao le argumentos de linha de comando porque flags de CLI sao
especificas da aplicacao. Adicione sobrescritas de CLI mesclando um provedor apos
`build_config_figment` e depois valide com `load_config_from_figment`:

Nomes de flags de CLI nao sao derivados de caminhos de configuracao. Use flags
normais da aplicacao, como `--server-port` ou `--database-url`; nao dependa de
`--server.port` ou `a.b.c` a menos que a aplicacao implemente esse parser
deliberadamente. O formato serializado aninhado da sobrescrita decide qual chave
de configuracao sera sobrescrita.

Somente valores representados no provedor `CliOverrides` da aplicacao podem
sobrescrever a configuracao. Isso e voltado a parametros de tempo de execucao
ajustados com frequencia, quando alterar uma flag em uma execucao e melhor que
editar um arquivo de configuracao. Mantenha configuracao estavel em arquivos e
exponha apenas sobrescritas temporarias deliberadas como flags de CLI.

```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)
}
```

Com sobrescritas de CLI mescladas dessa forma, a precedencia em tempo de
execucao e:

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

Use `load_config_with_figment` quando o chamador precisar de metadados de
origem:

```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(())
}
```

O carregador tambem emite rastreamento de origem de configuracao com
`tracing::trace!`. Esses eventos sao produzidos apenas quando TRACE esta
habilitado pelo subscriber de `tracing` da aplicacao. Se `tracing` for
inicializado depois do carregamento da configuracao, chame
`trace_config_sources::<AppConfig>(&figment)` depois de instalar o subscriber.

## Geracao de modelos

Modelos sao renderizados com o mesmo esquema e as mesmas regras de travessia de
includes. O formato de saida e inferido pelo caminho de saida:

- `.yaml` e `.yml` geram YAML
- `.toml` gera TOML
- `.json` e `.json5` geram modelos compativeis com JSON5
- extensoes desconhecidas ou ausentes geram YAML

Use `write_config_schemas` para criar JSON Schemas Draft 7 para a configuracao
raiz e secoes aninhadas marcadas para divisao. Os esquemas gerados omitem restricoes `required` para
que IDEs possam oferecer completamento em arquivos de configuracao parciais sem
relatar campos ausentes. Os arquivos `*.schema.json` gerados servem apenas para
completamento de IDE e verificacoes basicas do editor; eles nao decidem se um
valor concreto de campo e valido para a aplicacao. A validacao de valores deve
ser implementada no codigo com `#[config(validate = Self::validate)]` e
executada por `load_config` ou `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.

Marque um campo folha com `#[schemars(extend("x-env-only" = true))]` quando o valor deve vir somente de variaveis de ambiente. Os modelos gerados e os JSON Schemas omitem campos env-only, e objetos pai que ficarem vazios tambem sao removidos.

Para um esquema com secoes `server` e `log` marcadas com `x-tree-split`, isso grava
`schemas/myapp.schema.json`, `schemas/server.schema.json` e
`schemas/log.schema.json`. O esquema raiz contem apenas campos que pertencem ao
arquivo de configuracao raiz, como `include` e campos escalares de raiz. Ele
omite intencionalmente propriedades de secoes divididas, entao `server` e `log`
sao completados somente ao editar seus proprios arquivos YAML de secao.

Use `write_config_templates` para criar um modelo raiz e todos os arquivos de
modelo alcancaveis pela arvore 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(())
}
```

Use `write_config_templates_with_schema` quando modelos TOML, YAML, JSON e JSON5
gerados devem vincular esses esquemas para completamento e verificacoes basicas
de esquema no 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(())
}
```

Destinos raiz TOML e YAML vinculam o esquema raiz e nao completam campos de
secoes filhas. Destinos YAML de secao dividida vinculam o esquema da secao
correspondente; por exemplo, `log.yaml` recebe
`# yaml-language-server: $schema=./schemas/log.schema.json`. Destinos JSON e
JSON5 recebem um campo raiz `$schema` que o VS Code pode reconhecer. VS Code
`json.schemas` continua sendo um caminho alternativo de vinculo.

A geracao de modelos escolhe sua arvore de origem nesta ordem:

- um caminho de configuracao existente
- um caminho de modelo de saida existente
- o caminho de saida, tratado como uma nova arvore de modelo vazia

Se um no de origem nao tiver lista de includes, `rust-config-tree` deriva
arquivos de modelo filhos a partir de secoes `confique` aninhadas marcadas com `x-tree-split`. Com o esquema
acima, uma origem `config.example.yaml` vazia produz:

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

O modelo raiz recebe um bloco de include para `server.yaml`. Destinos YAML
que mapeiam para uma secao aninhada, como `server.yaml`, contem apenas
essa secao. Secoes ainda mais aninhadas so sao divididas recursivamente quando esses
campos tambem carregam `x-tree-split`.

Sobrescreva `template_path_for_section` quando uma secao deve ser gerada em um
caminho diferente:

```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,
        }
    }
}
```

O caminho de secao padrao e `<section>.yaml` para secoes aninhadas de
primeiro nivel. Filhos aninhados sao colocados sob o stem do arquivo pai, por
exemplo `trading/risk.yaml`.

## Integracao de CLI

Achate `ConfigCommand` no enum de comandos clap existente para adicionar:

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

A aplicacao consumidora mantem seu proprio tipo `Parser` e seu proprio enum de
comandos. `rust-config-tree` contribui apenas subcomandos reutilizaveis:

1. Adicione `#[command(subcommand)] command: Command` ao parser da aplicacao.
2. Adicione `#[command(flatten)] Config(ConfigCommand)` ao enum `Subcommand` da
   aplicacao.
3. Clap expande as variantes achatadas para o mesmo nivel de subcomando dos
   comandos proprios da aplicacao.
4. Faca match dessa variante e chame `handle_config_command::<Cli, AppConfig>`.

Flags de sobrescrita de configuracao especificas da aplicacao ficam no proprio
parser da aplicacao. Por exemplo, `--server-port` pode mapear para `server.port`
ao construir um valor aninhado
`CliOverrides { server: Some(CliServerOverrides { port }) }` e mescla-lo com
`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>` grava modelos em
`config/<root_config_name>/` usando o nome de arquivo selecionado. Se um caminho
for fornecido, somente o nome do arquivo e usado. Se nenhum nome de arquivo de
saida for fornecido, ele grava
`config/<root_config_name>/<root_config_name>.example.yaml`. Adicione
`--schema <path>` para vincular modelos TOML, YAML, JSON e JSON5 a um conjunto
de JSON Schema gerado. Modelos JSON e JSON5 recebem um campo `$schema`
reconhecido pelo VS Code. Isso tambem grava o esquema raiz e os esquemas de
secao no caminho de esquema selecionado.

`config-schema --output <path>` grava o JSON Schema Draft 7 raiz e esquemas de
secao. Se nenhum caminho de saida for fornecido, o esquema raiz e gravado em
`config/<root_config_name>/<root_config_name>.schema.json`.

`config-validate` carrega a arvore de configuracao completa em tempo de
execucao e executa padroes e validacao do `confique`, incluindo validadores
declarados com `#[config(validate = Self::validate)]`. Use esquemas de editor
para completamento sem ruido ao editar arquivos divididos; use este comando para
campos obrigatorios e validacao final da configuracao. Ele imprime
`Configuration is ok` quando a validacao tem sucesso.

`completions <shell>` imprime completions em stdout.

`install-completions <shell>` grava completions sob o diretorio home do usuario
e atualiza o arquivo de inicializacao do shell quando o shell exige isso. Bash,
Elvish, Fish, PowerShell e Zsh sao suportados.

`uninstall-completions <shell>` remove o arquivo de completion do binario atual
e remove o bloco gerenciado de inicializacao quando esse shell usa um.

## API de arvore de nivel mais baixo

Use `load_config_tree` quando voce nao usa `confique` ou quando precisa de
acesso direto aos resultados da travessia:

```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(())
}
```

A API de arvore normaliza caminhos lexicalmente, rejeita caminhos de include
vazios, detecta ciclos de include recursivos e ignora arquivos que ja foram
carregados por outro ramo de include.

## Licenca

Licenciado sob uma das seguintes, a sua escolha:

- Apache License, Version 2.0
- MIT license