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
496
497
498
# 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` fournit le chargement d'arbres de configuration et des
assistants CLI pour les applications Rust qui utilisent des fichiers de
configuration en couches.

Manuel du projet : <https://developerworks.github.io/rust-config-tree/>. Les
manuels en plusieurs langues sont publies comme sites mdBook independants avec
des liens de changement de langue.

Il gere :

- le chargement d'un schema `confique` dans un objet de configuration
  directement utilisable via des fournisseurs Figment d'execution ;
- les gestionnaires de commandes `config-template`, `config-schema`,
  `config-validate`, `completions`, `install-completions` et
  `uninstall-completions` ;
- la generation de schemas JSON Draft 7 pour la racine et les sections, pour la
  completion et les controles de schema de base dans l'editeur ;
- la generation de modeles de configuration YAML, TOML, JSON et JSON5 ;
- les liaisons de schema pour les modeles TOML, YAML, JSON et JSON5 ;
- la traversee recursive des inclusions ;
- le chargement de `.env` avant la fusion des valeurs d'environnement ;
- le suivi des sources via les metadonnees Figment ;
- les journaux de suivi des sources au niveau TRACE via `tracing` ;
- la resolution des chemins d'inclusion relatifs depuis le fichier qui les
  declare ;
- la normalisation lexicale des chemins ;
- la detection des cycles d'inclusion ;
- un ordre de traversee deterministe ;
- la collecte miroir des cibles de modeles ;
- decoupage opt-in des modeles YAML pour les sections imbriquees
  marquees `x-tree-split`.

Les applications fournissent leur schema en derivant `confique::Config` et en
implementant `ConfigSchema` pour exposer le champ d'inclusion du schema.

## Installation

```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"] }
```

## Schema de configuration

Le schema de votre application possede le champ d'inclusion. `rust-config-tree`
n'a besoin que d'un petit adaptateur qui extrait les inclusions depuis la couche
intermediaire `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()
    }
}
```

Les chemins d'inclusion relatifs sont resolus depuis le fichier qui les
declare :

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

mode: shadow
```

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

Chargez le schema final avec `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` charge le premier fichier `.env` trouve en remontant depuis le
repertoire du fichier de configuration racine avant de demander a Figment de
lire les variables d'environnement declarees par le schema. Les valeurs deja
presentes dans l'environnement du processus sont conservees et ont priorite sur
les valeurs de `.env`.

Le chargement d'execution est effectue via Figment. `confique` reste
responsable des metadonnees de schema, des valeurs par defaut, de la validation
et de la generation de modeles. Les noms de variables d'environnement sont lus
depuis `#[config(env = "...")]` ; le chargeur n'utilise pas `Env::split("_")`
ni `Env::split("__")`, donc une variable comme `APP_DATABASE_POOL_SIZE` peut
correspondre a un champ nomme `database.pool_size`.

`load_config` ne lit pas les arguments de ligne de commande, car les drapeaux
CLI sont propres a chaque application. Ajoutez les remplacements CLI en
fusionnant un fournisseur apres `build_config_figment`, puis validez avec
`load_config_from_figment` :

Les noms de drapeaux CLI ne sont pas derives des chemins de configuration.
Utilisez des drapeaux d'application normaux comme `--server-port` ou
`--database-url` ; ne vous appuyez pas sur `--server.port` ou `a.b.c` sauf si
l'application implemente deliberement cet analyseur. La forme imbriquee
serialisee du remplacement decide quelle cle de configuration est remplacee.

Seules les valeurs representees dans le fournisseur `CliOverrides` de
l'application peuvent remplacer la configuration. C'est prevu pour les
parametres d'execution souvent ajustes, lorsqu'il vaut mieux changer un drapeau
pour une execution que modifier un fichier de configuration. Gardez la
configuration stable dans les fichiers et n'exposez comme drapeaux CLI que les
remplacements temporaires voulus.

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

Avec des remplacements CLI fusionnes ainsi, la priorite d'execution est :

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

Utilisez `load_config_with_figment` lorsque l'appelant a besoin des metadonnees
de source :

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

Le chargeur emet aussi le suivi des sources de configuration avec
`tracing::trace!`. Ces evenements ne sont produits que lorsque TRACE est active
par le subscriber `tracing` de l'application. Si `tracing` est initialise apres
le chargement de la configuration, appelez `trace_config_sources::<AppConfig>(&figment)`
apres avoir installe le subscriber.

## Generation de modeles

Les modeles sont rendus avec le meme schema et les memes regles de traversee
d'inclusions. Le format de sortie est deduit du chemin de sortie :

- `.yaml` et `.yml` generent du YAML ;
- `.toml` genere du TOML ;
- `.json` et `.json5` generent des modeles compatibles JSON5 ;
- les extensions inconnues ou absentes generent du YAML.

Utilisez `write_config_schemas` pour creer des schemas JSON Draft 7 pour la
configuration racine et les sections imbriquees decoupees. Les schemas generes omettent
les contraintes `required` afin que les IDE puissent proposer la completion pour
des fichiers de configuration partiels sans signaler de champs manquants. Les
fichiers `*.schema.json` generes servent uniquement a la completion IDE et aux
controles d'editeur de base ; ils ne decident pas si une valeur de champ concrete
est valide pour l'application. La validation de valeur doit etre implementee dans
le code avec `#[config(validate = Self::validate)]`, puis executee par
`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.

Marquez un champ feuille avec `#[schemars(extend("x-env-only" = true))]` lorsque la valeur doit venir uniquement de variables d environnement. Les modeles generes et les schemas JSON omettent les champs env-only, et les objets parents devenus vides sont supprimes.

Pour un schema avec les sections `server` et `log` marquees `x-tree-split`, cela ecrit
`schemas/myapp.schema.json`, `schemas/server.schema.json` et
`schemas/log.schema.json`. Le schema racine ne contient que les champs qui
appartiennent au fichier racine, comme `include` et les champs scalaires racine.
Il omet intentionnellement les proprietes des sections decoupees, donc `server`
et `log` ne sont completes que lors de l'edition de leurs propres fichiers YAML
de section. Les sections imbriquees non marquees restent dans le schema racine.

Utilisez `write_config_templates` pour creer un modele racine et chaque fichier
modele accessible depuis son arbre d'inclusion :

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

Utilisez `write_config_templates_with_schema` lorsque les modeles TOML, YAML,
JSON et JSON5 generes doivent lier ces schemas pour la completion et les
controles de schema de base dans l'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(())
}
```

Les cibles racine TOML et YAML lient le schema racine et ne completent pas les
champs des sections enfants. Les cibles YAML de section separee lient leur
schema de section correspondant ; par exemple `log.yaml` recoit
`# yaml-language-server: $schema=./schemas/log.schema.json`. Les cibles JSON et
JSON5 recoivent un champ racine `$schema` que VS Code peut reconnaitre.
VS Code `json.schemas` reste une autre facon de lier le schema.

La generation de modeles choisit son arbre source dans cet ordre :

- un chemin de configuration existant ;
- un chemin de modele de sortie existant ;
- le chemin de sortie, traite comme un nouvel arbre de modeles vide.

Si un noeud source n'a pas de liste d'inclusions, `rust-config-tree` derive les
fichiers modeles enfants depuis les sections `confique` imbriquees marquees `x-tree-split`. Avec le
schema ci-dessus, une source `config.example.yaml` vide produit :

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

Le modele racine recoit un bloc d'inclusion pour `server.yaml`. Les
cibles YAML qui correspondent a une section imbriquee, comme
`server.yaml`, ne contiennent que cette section. Les sections encore plus
imbriquees ne sont separees recursivement que lorsque ces champs portent aussi `x-tree-split`.

Remplacez `template_path_for_section` lorsqu'une section doit etre generee a un
autre chemin :

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

Le chemin de section par defaut est `<section>.yaml` pour les sections
imbriquees de premier niveau. Les enfants imbriques sont places sous le stem du
fichier parent, par exemple `trading/risk.yaml`.

## Integration CLI

Aplatissez `ConfigCommand` dans votre enum de commandes clap existante pour
ajouter :

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

L'application consommatrice garde son propre type `Parser` et sa propre enum de
commandes. `rust-config-tree` ne fournit que des sous-commandes reutilisables :

1. Ajoutez `#[command(subcommand)] command: Command` au parser de l'application.
2. Ajoutez `#[command(flatten)] Config(ConfigCommand)` a l'enum `Subcommand` de
   l'application.
3. Clap developpe les variantes aplaties au meme niveau de sous-commandes que
   les commandes propres a l'application.
4. Faites correspondre cette variante et appelez
   `handle_config_command::<Cli, AppConfig>`.

Les drapeaux de remplacement propres a l'application restent sur son propre
parser. Par exemple, `--server-port` peut correspondre a `server.port` en
construisant une valeur imbriquee
`CliOverrides { server: Some(CliServerOverrides { port }) }` et en la
fusionnant avec `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>` ecrit les modeles sous
`config/<root_config_name>/` avec le nom de fichier choisi. Si un chemin est
fourni, seul son nom de fichier est utilise. Si aucun nom de fichier de sortie
n'est fourni, il ecrit
`config/<root_config_name>/<root_config_name>.example.yaml`. Ajoutez
`--schema <path>` pour lier les modeles TOML, YAML, JSON et JSON5 a un ensemble
de schemas JSON generes. Les modeles JSON et JSON5 recoivent un champ `$schema`
reconnu par VS Code. Cela ecrit aussi le schema racine et les schemas de
section au chemin de schema choisi.

`config-schema --output <path>` ecrit le schema JSON Draft 7 racine et les
schemas de section. Les sections imbriquees non marquees restent dans le schema racine. Si aucun chemin de sortie n'est fourni, le schema racine est
ecrit dans `config/<root_config_name>/<root_config_name>.schema.json`.

`config-validate` charge l'arbre complet de configuration d'execution et lance
les valeurs par defaut et la validation `confique`, y compris les validateurs
declares avec `#[config(validate = Self::validate)]`. Utilisez les schemas
d'editeur pour une completion non bruyante pendant l'edition de fichiers
separes ; utilisez cette commande pour les champs obligatoires et la validation
finale de la configuration. Elle affiche `Configuration is ok` lorsque la
validation reussit.

`completions <shell>` imprime les completions sur stdout.

`install-completions <shell>` ecrit les completions sous le repertoire home de
l'utilisateur et met a jour le fichier de demarrage du shell lorsque le shell le
requiert. Bash, Elvish, Fish, PowerShell et Zsh sont pris en charge.

`uninstall-completions <shell>` supprime le fichier de completion du binaire
courant et supprime le bloc de demarrage gere quand ce shell en utilise un.

## API d'arbre de plus bas niveau

Utilisez `load_config_tree` lorsque vous n'utilisez pas `confique` ou lorsque
vous avez besoin d'un acces direct aux resultats de traversee :

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

L'API d'arbre normalise les chemins lexicalement, rejette les chemins
d'inclusion vides, detecte les cycles d'inclusion recursifs et ignore les
fichiers deja charges par une autre branche d'inclusion.

## Licence

Sous licence, au choix :

- Apache License, Version 2.0
- MIT license