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}