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