Skip to main content

config_commands/
config_commands.rs

1//! Embeds the reusable `config-*` subcommands in an application CLI.
2
3use std::{
4    fs, io,
5    path::PathBuf,
6    time::{SystemTime, UNIX_EPOCH},
7};
8
9use clap::{Parser, Subcommand};
10use confique::Config;
11use rust_config_tree::{
12    cli::{ConfigCommand, handle_config_command},
13    config::{ConfigSchema, load_config},
14};
15use schemars::JsonSchema;
16
17#[derive(Debug, Parser)]
18#[command(name = "config-commands")]
19struct Cli {
20    #[arg(long)]
21    config: Option<PathBuf>,
22
23    #[command(subcommand)]
24    command: Option<Command>,
25}
26
27#[derive(Debug, Subcommand)]
28enum Command {
29    Run,
30
31    /// Flatten the crate-provided config commands into this example CLI.
32    #[command(flatten)]
33    Config(ConfigCommand),
34}
35
36#[derive(Debug, Config, JsonSchema)]
37pub struct AppConfig {
38    #[config(default = [])]
39    pub include: Vec<PathBuf>,
40
41    #[config(default = "demo")]
42    pub mode: String,
43
44    #[config(nested)]
45    pub server: ServerConfig,
46}
47
48#[derive(Debug, Config, JsonSchema)]
49pub struct ServerConfig {
50    #[config(default = "127.0.0.1")]
51    pub bind: String,
52
53    #[config(default = 8080)]
54    pub port: u16,
55}
56
57/// Exposes the example's include list to the config command handlers.
58impl ConfigSchema for AppConfig {
59    fn include_paths(layer: &<Self as Config>::Layer) -> Vec<PathBuf> {
60        layer.include.clone().unwrap_or_default()
61    }
62}
63
64/// Parses the example CLI and dispatches either app run or config commands.
65fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
66    let cli = Cli::parse();
67    let config_path = match &cli.config {
68        Some(path) => path.clone(),
69        None => write_demo_config()?,
70    };
71
72    match cli.command.unwrap_or(Command::Run) {
73        Command::Run => {
74            let config = load_config::<AppConfig>(&config_path)?;
75            println!("config path: {}", config_path.display());
76            println!("include count: {}", config.include.len());
77            println!("mode: {}", config.mode);
78            println!("server bind: {}", config.server.bind);
79            println!("server port: {}", config.server.port);
80        }
81        Command::Config(command) => {
82            handle_config_command::<Cli, AppConfig>(command, &config_path)?;
83        }
84    }
85
86    Ok(())
87}
88
89/// Creates a minimal config file used when `--config` is omitted.
90fn write_demo_config() -> io::Result<PathBuf> {
91    let dir = temp_example_dir("config-commands")?;
92    let root_config = dir.join("config.yaml");
93
94    fs::write(
95        &root_config,
96        r#"
97mode: local
98server:
99  bind: 0.0.0.0
100  port: 3000
101"#
102        .trim_start(),
103    )?;
104
105    Ok(root_config)
106}
107
108/// Creates a unique temporary directory for one example run.
109fn temp_example_dir(name: &str) -> io::Result<PathBuf> {
110    let nanos = SystemTime::now()
111        .duration_since(UNIX_EPOCH)
112        .unwrap_or_default()
113        .as_nanos();
114    let dir = std::env::temp_dir().join(format!("rust-config-tree-{name}-{nanos}"));
115    fs::create_dir_all(&dir)?;
116    Ok(dir)
117}