usage/spec/
config.rs

1use std::collections::BTreeMap;
2
3use kdl::{KdlDocument, KdlEntry, KdlNode};
4use serde::Serialize;
5
6use crate::error::UsageErr;
7use crate::spec::context::ParsingContext;
8use crate::spec::data_types::SpecDataTypes;
9use crate::spec::helpers::NodeHelper;
10
11#[derive(Debug, Default, Clone, Serialize)]
12pub struct SpecConfig {
13    pub props: BTreeMap<String, SpecConfigProp>,
14}
15
16impl SpecConfig {
17    pub(crate) fn parse(ctx: &ParsingContext, node: &NodeHelper) -> Result<Self, UsageErr> {
18        let mut config = Self::default();
19        for node in node.children() {
20            node.ensure_arg_len(1..=1)?;
21            match node.name() {
22                "prop" => {
23                    let key = node.arg(0)?;
24                    let key = key.ensure_string()?.to_string();
25                    let mut prop = SpecConfigProp::default();
26                    for (k, v) in node.props() {
27                        match k {
28                            "default" => prop.default = v.value.to_string().into(),
29                            "default_note" => prop.default_note = Some(v.ensure_string()?),
30                            "data_type" => prop.data_type = v.ensure_string()?.parse()?,
31                            "env" => prop.env = v.ensure_string()?.to_string().into(),
32                            "help" => prop.help = v.ensure_string()?.to_string().into(),
33                            "long_help" => prop.long_help = v.ensure_string()?.to_string().into(),
34                            k => bail_parse!(ctx, node.span(), "unsupported config prop key {k}"),
35                        }
36                    }
37                    config.props.insert(key, prop);
38                }
39                k => bail_parse!(ctx, node.node.name().span(), "unsupported config key {k}"),
40            }
41        }
42        Ok(config)
43    }
44
45    pub(crate) fn merge(&mut self, other: &Self) {
46        for (key, prop) in &other.props {
47            self.props
48                .entry(key.to_string())
49                .or_insert_with(|| prop.clone());
50        }
51    }
52}
53
54impl SpecConfig {
55    pub fn is_empty(&self) -> bool {
56        self.props.is_empty()
57    }
58}
59
60#[derive(Debug, Clone, Serialize)]
61pub struct SpecConfigProp {
62    pub default: Option<String>,
63    pub default_note: Option<String>,
64    pub data_type: SpecDataTypes,
65    pub env: Option<String>,
66    pub help: Option<String>,
67    pub long_help: Option<String>,
68}
69
70impl SpecConfigProp {
71    fn to_kdl_node(&self, key: String) -> KdlNode {
72        let mut node = KdlNode::new("prop");
73        node.push(KdlEntry::new(key));
74        if let Some(default) = &self.default {
75            node.push(KdlEntry::new_prop("default", default.clone()));
76        }
77        if let Some(default_note) = &self.default_note {
78            node.push(KdlEntry::new_prop("default_note", default_note.clone()));
79        }
80        if let Some(env) = &self.env {
81            node.push(KdlEntry::new_prop("env", env.clone()));
82        }
83        if let Some(help) = &self.help {
84            node.push(KdlEntry::new_prop("help", help.clone()));
85        }
86        if let Some(long_help) = &self.long_help {
87            node.push(KdlEntry::new_prop("long_help", long_help.clone()));
88        }
89        node
90    }
91}
92
93impl Default for SpecConfigProp {
94    fn default() -> Self {
95        Self {
96            default: None,
97            default_note: None,
98            data_type: SpecDataTypes::Null,
99            env: None,
100            help: None,
101            long_help: None,
102        }
103    }
104}
105
106impl From<&SpecConfig> for KdlNode {
107    fn from(config: &SpecConfig) -> Self {
108        let mut node = KdlNode::new("config");
109        for (key, prop) in &config.props {
110            let doc = node.children_mut().get_or_insert_with(KdlDocument::new);
111            doc.nodes_mut().push(prop.to_kdl_node(key.to_string()));
112        }
113        node
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use crate::Spec;
120    use insta::assert_snapshot;
121
122    #[test]
123    fn test_config_defaults() {
124        let spec = Spec::parse(
125            &Default::default(),
126            r#"
127config {
128    prop "color" default=#true env="COLOR" help="Enable color output"
129    prop "user" default="admin" env="USER" help="User to run as"
130    prop "jobs" default=4 env="JOBS" help="Number of jobs to run"
131    prop "timeout" default=1.5 env="TIMEOUT" help="Timeout in seconds" \
132        long_help="Timeout in seconds, can be fractional"
133}
134        "#,
135        )
136        .unwrap();
137
138        assert_snapshot!(spec, @r##"
139        config {
140            prop color default="#true" env=COLOR help="Enable color output"
141            prop jobs default="4" env=JOBS help="Number of jobs to run"
142            prop timeout default="1.5" env=TIMEOUT help="Timeout in seconds" long_help="Timeout in seconds, can be fractional"
143            prop user default=admin env=USER help="User to run as"
144        }
145        "##);
146    }
147}