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
# 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`는 계층형 설정 파일을 사용하는 Rust 애플리케이션을 위한
설정 트리 로딩과 CLI 헬퍼를 제공합니다.

프로젝트 매뉴얼: <https://developerworks.github.io/rust-config-tree/>.
언어별 매뉴얼은 언어 전환 링크가 있는 독립 mdBook 사이트로 게시됩니다.

처리하는 기능:

- Figment 런타임 프로바이더를 통해 `confique` 스키마를 바로 사용할 수 있는 설정 객체로 로드
- `config-template`, `config-schema`, `config-validate`, `completions`,
  `install-completions`, `uninstall-completions` 명령 핸들러
- 에디터 완성과 기본 schema 검사를 위한 Draft 7 루트 및 섹션 JSON Schema 생성
- YAML, TOML, JSON, JSON5 설정 템플릿 생성
- TOML, YAML, JSON, JSON5 템플릿 스키마 바인딩
- 재귀 include 순회
- 환경 값 병합 전에 `.env` 로드
- Figment 메타데이터를 통한 소스 추적
- `tracing`을 통한 TRACE 레벨 소스 추적 로그
- include를 선언한 파일 기준의 상대 include 경로 해석
- 사전식 경로 정규화
- include 순환 감지
- 결정적인 순회 순서
- 미러링된 템플릿 대상 수집
- `x-tree-split`로 표시한 중첩 스키마 섹션의 YAML 템플릿 분할

애플리케이션은 `confique::Config`를 derive하고 `ConfigSchema`를 구현해
스키마의 include 필드를 노출합니다.

## Install

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

## Configuration Schema

애플리케이션 스키마가 include 필드를 소유합니다. `rust-config-tree`에는 중간
`confique` 레이어에서 include를 추출하는 작은 어댑터만 필요합니다.

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

상대 include 경로는 그 경로를 선언한 파일을 기준으로 해석됩니다.

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

mode: shadow
```

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

`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`는 Figment가 스키마에 선언된 환경 변수를 읽기 전에, 루트 설정 파일의
디렉터리에서 위로 올라가며 처음 발견한 `.env` 파일을 로드합니다. 이미 프로세스
환경에 있는 값은 보존되며 `.env` 값보다 우선합니다.

런타임 설정 로딩은 Figment를 통해 수행됩니다. `confique`는 스키마 메타데이터,
기본값, 검증, 템플릿 생성을 계속 담당합니다. 환경 변수 이름은
`#[config(env = "...")]`에서 읽습니다. 로더는 `Env::split("_")`나
`Env::split("__")`를 사용하지 않으므로, `APP_DATABASE_POOL_SIZE` 같은 변수는
`database.pool_size` 필드에 매핑될 수 있습니다.

`load_config`는 명령줄 인자를 읽지 않습니다. CLI 플래그는 애플리케이션마다 다르기
때문입니다. CLI 오버라이드는 `build_config_figment` 뒤에 프로바이더를 병합한 다음
`load_config_from_figment`로 검증해서 추가합니다.

CLI 플래그 이름은 설정 경로에서 derive되지 않습니다. `--server.port`나 `a.b.c`에
의존하지 말고 `--server-port` 또는 `--database-url` 같은 일반 애플리케이션
플래그를 사용하세요. 어떤 설정 키가 override되는지는 중첩 직렬화 override
형태가 결정합니다.

애플리케이션의 `CliOverrides` 프로바이더에 표현된 값만 설정을 override할 수
있습니다. 이는 설정 파일을 편집하는 것보다 한 번의 실행에 플래그를 바꾸는 편이
나은, 자주 조정되는 런타임 파라미터를 위한 것입니다. 안정적인 설정은 파일에 두고,
의도적인 임시 override만 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)
}
```

이 방식으로 CLI override를 병합하면 런타임 우선순위는 다음과 같습니다.

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

호출자가 소스 메타데이터를 필요로 하면 `load_config_with_figment`를 사용하세요.

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

로더는 `tracing::trace!`로 설정 소스 추적도 내보냅니다. 이 이벤트는
애플리케이션의 tracing subscriber에서 TRACE가 활성화된 경우에만 생성됩니다. 설정
로드 이후 tracing을 초기화한다면 subscriber 설치 후
`trace_config_sources::<AppConfig>(&figment)`를 호출하세요.

## Template Generation

템플릿은 동일한 스키마와 include 순회 규칙으로 렌더링됩니다. 출력 형식은 출력
경로에서 추론됩니다.

- `.yaml``.yml`은 YAML 생성
- `.toml`은 TOML 생성
- `.json``.json5`는 JSON5 호환 템플릿 생성
- 알 수 없거나 없는 확장자는 YAML 생성

`write_config_schemas`를 사용해 루트 설정과 분할된 중첩 섹션의 Draft 7 JSON Schema를
생성합니다. 생성된 스키마는 `required` 제약을 생략하므로, IDE가 부분 설정 파일에
대해 누락 필드 오류를 보고하지 않고 완성을 제공할 수 있습니다.
생성된 `*.schema.json` 파일은 IDE 완성과 기본 에디터 검사 전용이며, 구체적인
필드 값이 애플리케이션에서 유효한지는 판단하지 않습니다. 필드 값 유효성 검사는
코드에서 `#[config(validate = Self::validate)]`로 구현하고, `load_config` 또는
`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.

값을 환경 변수로만 제공해야 하는 leaf 필드에는 `#[schemars(extend("x-env-only" = true))]`를 붙입니다. 생성된 템플릿과 JSON Schema는 env-only 필드를 생략하며, 이 생략으로 비게 된 부모 객체도 함께 제거합니다.

`server`와 `log` 섹션이 `x-tree-split`로 표시된 스키마라면 `schemas/myapp.schema.json`,
`schemas/server.schema.json`, `schemas/log.schema.json`을 씁니다. 루트 스키마에는
`include`와 루트 스칼라 필드처럼 루트 설정 파일에 속하는 필드만 포함됩니다.
분할된 섹션 프로퍼티는 의도적으로 생략되어, `server`와 `log`는 각자의 섹션 YAML
파일을 편집할 때만 완성됩니다. 표시하지 않은 중첩 섹션은 루트 스키마에 남습니다.

`write_config_templates`를 사용해 루트 템플릿과 include 트리에서 도달 가능한 모든
템플릿 파일을 생성합니다.

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

생성된 TOML, YAML, JSON 및 JSON5 템플릿이 IDE 완성과 기본 스키마 검사를 위해
해당 스키마에 바인딩되어야 한다면 `write_config_templates_with_schema`를
사용하세요.

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

TOML 및 YAML 루트 대상은 루트 스키마를 바인딩하며 자식 섹션 필드를 완성하지
않습니다. 분할된 섹션 YAML 대상은 대응하는 섹션 스키마를 바인딩합니다. 예를 들어
`log.yaml`은
`# yaml-language-server: $schema=./schemas/log.schema.json`를 받습니다. JSON 및
JSON5 대상은 VS Code가 인식할 수 있는 루트 `$schema` 필드를 받습니다.
VS Code `json.schemas` 같은 에디터 설정도 대체 바인딩 경로로 사용할 수 있습니다.

템플릿 생성은 다음 순서로 소스 트리를 선택합니다.

- 기존 설정 경로
- 기존 출력 템플릿 경로
- 새 빈 템플릿 트리로 간주한 출력 경로

소스 노드에 include 목록이 없으면 `rust-config-tree`는 `x-tree-split`로 표시한 중첩 `confique` 섹션에서
자식 템플릿 파일을 derive합니다. 위 스키마에서 빈 `config.example.yaml` 소스는
다음을 생성합니다.

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

루트 템플릿은 `server.yaml` include 블록을 받습니다. `server.yaml`
처럼 중첩 섹션에 매핑되는 YAML 대상은 해당 섹션만 포함합니다. 더 깊은 중첩
섹션은 해당 필드도 `x-tree-split`를 가질 때만 재귀 분할됩니다.

섹션이 다른 경로에 생성되어야 한다면 `template_path_for_section`을 override하세요.

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

기본 섹션 경로는 최상위 중첩 섹션에 대해 `<section>.yaml`입니다. 중첩
자식은 부모 파일 stem 아래에 배치됩니다. 예: `trading/risk.yaml`.

## CLI Integration

기존 clap 명령 enum에 `ConfigCommand`를 flatten하면 다음을 추가할 수 있습니다.

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

사용 애플리케이션은 자체 `Parser` 타입과 자체 명령 enum을 유지합니다.
`rust-config-tree`는 재사용 가능한 하위 명령만 제공합니다.

1. 애플리케이션 parser에 `#[command(subcommand)] command: Command`를 추가합니다.
2. 애플리케이션의 `Subcommand` enum에 `#[command(flatten)] Config(ConfigCommand)`를 추가합니다.
3. Clap은 flatten된 variant를 애플리케이션 자체 명령과 같은 하위 명령 레벨로 확장합니다.
4. 해당 variant를 match하고 `handle_config_command::<Cli, AppConfig>`를 호출합니다.

애플리케이션별 설정 override 플래그는 애플리케이션 자체 parser에 둡니다. 예를
들어 `--server-port`는 중첩된
`CliOverrides { server: Some(CliServerOverrides { port }) }` 값을 만들고
`Serialized::defaults`로 병합해서 `server.port`에 매핑할 수 있습니다.

```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>`은 `config/<root_config_name>/` 아래에
템플릿을 쓰고 선택한 파일 이름을 사용합니다. 경로를 제공하면 파일 이름만
사용합니다. 출력 파일 이름을 제공하지 않으면
`config/<root_config_name>/<root_config_name>.example.yaml`을 씁니다.
TOML, YAML, JSON 및 JSON5 템플릿을 생성된 JSON Schema 집합에 바인딩하려면
`--schema <path>`를 추가하세요. JSON 및 JSON5 템플릿은 VS Code가 인식하는
`$schema` 필드를 받습니다. 이 옵션은 선택한 스키마 경로에 루트 스키마와 섹션
스키마도 씁니다.

`config-schema --output <path>`는 루트 Draft 7 JSON Schema와 섹션 스키마를
씁니다. 출력 경로를 제공하지 않으면 루트 스키마는
`config/<root_config_name>/<root_config_name>.schema.json`에 쓰입니다.

`config-validate`는 전체 런타임 설정 트리를 로드하고 `confique` 기본값과 검증을
실행합니다. 여기에는 `#[config(validate = Self::validate)]`로 선언한 validator도
포함됩니다. 분할 파일 편집 중에는 노이즈 없는 완성을 위해 에디터 스키마를
사용하고, 필수 필드와 최종 설정 검증에는 이 명령을 사용하세요. 검증이 성공하면
`Configuration is ok`를 출력합니다.

`completions <shell>`은 완성을 stdout으로 출력합니다.

`install-completions <shell>`은 사용자 홈 디렉터리 아래에 완성 파일을 쓰고,
필요한 셸에서는 시작 파일을 업데이트합니다. Bash, Elvish, Fish, PowerShell,
Zsh가 지원됩니다.

`uninstall-completions <shell>`은 현재 binary의 completion 파일을 삭제하고,
해당 shell이 managed startup block을 사용하는 경우 그 block도 삭제합니다.

## Lower-Level Tree API

`confique`를 사용하지 않거나 순회 결과에 직접 접근해야 한다면 `load_config_tree`를
사용하세요.

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

트리 API는 경로를 사전식으로 정규화하고, 빈 include 경로를 거부하며, 재귀 include
순환을 감지하고, 다른 include branch를 통해 이미 로드된 파일을 건너뜁니다.

## License

다음 중 하나의 라이선스로 제공됩니다.

- Apache License, Version 2.0
- MIT license

선택에 따라 사용할 수 있습니다.