Skip to main content

usage/spec/
helpers.rs

1use indexmap::IndexMap;
2use kdl::{KdlEntry, KdlNode, KdlValue};
3use miette::SourceSpan;
4use std::fmt::Debug;
5use std::ops::RangeBounds;
6
7use crate::error::UsageErr;
8use crate::spec::context::ParsingContext;
9
10#[derive(Debug)]
11pub struct NodeHelper<'a> {
12    pub(crate) node: &'a KdlNode,
13    pub(crate) ctx: &'a ParsingContext,
14}
15
16impl<'a> NodeHelper<'a> {
17    pub(crate) fn new(ctx: &'a ParsingContext, node: &'a KdlNode) -> Self {
18        Self { node, ctx }
19    }
20
21    pub(crate) fn name(&self) -> &str {
22        self.node.name().value()
23    }
24    pub(crate) fn span(&self) -> SourceSpan {
25        (self.node.span().offset(), self.node.span().len()).into()
26    }
27    pub(crate) fn ensure_arg_len<R>(&self, range: R) -> Result<&Self, UsageErr>
28    where
29        R: RangeBounds<usize> + Debug,
30    {
31        let count = self.args().count();
32        if !range.contains(&count) {
33            let ctx = self.ctx;
34            let span = self.span();
35            bail_parse!(ctx, span, "expected {range:?} arguments, got {count}",)
36        }
37        Ok(self)
38    }
39    pub(crate) fn get(&self, key: &str) -> Option<ParseEntry<'_>> {
40        self.node.entry(key).map(|e| ParseEntry::new(self.ctx, e))
41    }
42    pub(crate) fn arg(&self, i: usize) -> Result<ParseEntry<'_>, UsageErr> {
43        if let Some(entry) = self.args().nth(i) {
44            return Ok(entry);
45        }
46        bail_parse!(self.ctx, self.span(), "missing argument")
47    }
48    pub(crate) fn args(&self) -> impl Iterator<Item = ParseEntry<'_>> + '_ {
49        self.node
50            .entries()
51            .iter()
52            .filter(|e| e.name().is_none())
53            .map(|e| ParseEntry::new(self.ctx, e))
54    }
55    pub(crate) fn props(&self) -> IndexMap<&str, ParseEntry<'_>> {
56        self.node
57            .entries()
58            .iter()
59            .filter_map(|e| {
60                e.name()
61                    .map(|key| (key.value(), ParseEntry::new(self.ctx, e)))
62            })
63            .collect()
64    }
65    pub(crate) fn children(&self) -> Vec<Self> {
66        self.node
67            .children()
68            .map(|c| {
69                c.nodes()
70                    .iter()
71                    .map(|n| NodeHelper::new(self.ctx, n))
72                    .collect()
73            })
74            .unwrap_or_default()
75    }
76}
77
78#[derive(Debug)]
79pub(crate) struct ParseEntry<'a> {
80    pub(crate) ctx: &'a ParsingContext,
81    pub(crate) entry: &'a KdlEntry,
82    pub(crate) value: &'a KdlValue,
83}
84
85impl<'a> ParseEntry<'a> {
86    fn new(ctx: &'a ParsingContext, entry: &'a KdlEntry) -> Self {
87        Self {
88            ctx,
89            entry,
90            value: entry.value(),
91        }
92    }
93
94    fn span(&self) -> SourceSpan {
95        (self.entry.span().offset(), self.entry.span().len()).into()
96    }
97}
98
99impl ParseEntry<'_> {
100    pub fn ensure_usize(&self) -> Result<usize, UsageErr> {
101        match self.value.as_integer() {
102            Some(i) => Ok(i as usize),
103            None => bail_parse!(self.ctx, self.span(), "expected usize"),
104        }
105    }
106    #[allow(dead_code)]
107    pub fn ensure_f64(&self) -> Result<f64, UsageErr> {
108        match self.value.as_float() {
109            Some(f) => Ok(f),
110            None => bail_parse!(self.ctx, self.span(), "expected float"),
111        }
112    }
113    pub fn ensure_bool(&self) -> Result<bool, UsageErr> {
114        match self.value.as_bool() {
115            Some(b) => Ok(b),
116            None => bail_parse!(self.ctx, self.span(), "expected bool"),
117        }
118    }
119    pub fn ensure_string(&self) -> Result<String, UsageErr> {
120        match self.value.as_string() {
121            Some(s) => Ok(s.to_string()),
122            None => bail_parse!(self.ctx, self.span(), "expected string"),
123        }
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130    use kdl::KdlDocument;
131    use std::path::Path;
132
133    fn parse_node(input: &str) -> (ParsingContext, KdlDocument) {
134        let ctx = ParsingContext::new(Path::new("test.kdl"), input);
135        let doc: KdlDocument = input.parse().unwrap();
136        (ctx, doc)
137    }
138
139    #[test]
140    fn test_node_helper_name() {
141        let (ctx, doc) = parse_node("test_node \"arg1\"");
142        let node = doc.nodes().first().unwrap();
143        let helper = NodeHelper::new(&ctx, node);
144        assert_eq!(helper.name(), "test_node");
145    }
146
147    #[test]
148    fn test_node_helper_arg() {
149        let (ctx, doc) = parse_node("node \"first\" \"second\"");
150        let node = doc.nodes().first().unwrap();
151        let helper = NodeHelper::new(&ctx, node);
152
153        assert_eq!(helper.arg(0).unwrap().ensure_string().unwrap(), "first");
154        assert_eq!(helper.arg(1).unwrap().ensure_string().unwrap(), "second");
155    }
156
157    #[test]
158    fn test_node_helper_args_count() {
159        let (ctx, doc) = parse_node("node \"a\" \"b\" \"c\"");
160        let node = doc.nodes().first().unwrap();
161        let helper = NodeHelper::new(&ctx, node);
162
163        assert_eq!(helper.args().count(), 3);
164    }
165
166    #[test]
167    fn test_node_helper_props() {
168        let (ctx, doc) = parse_node("node key1=\"value1\" key2=\"value2\"");
169        let node = doc.nodes().first().unwrap();
170        let helper = NodeHelper::new(&ctx, node);
171
172        let props = helper.props();
173        assert_eq!(props.len(), 2);
174        assert_eq!(props["key1"].ensure_string().unwrap(), "value1");
175        assert_eq!(props["key2"].ensure_string().unwrap(), "value2");
176    }
177
178    #[test]
179    fn test_node_helper_get() {
180        let (ctx, doc) = parse_node("node name=\"test\"");
181        let node = doc.nodes().first().unwrap();
182        let helper = NodeHelper::new(&ctx, node);
183
184        assert!(helper.get("name").is_some());
185        assert!(helper.get("nonexistent").is_none());
186    }
187
188    #[test]
189    fn test_node_helper_children() {
190        let (ctx, doc) = parse_node("parent { child1; child2 }");
191        let node = doc.nodes().first().unwrap();
192        let helper = NodeHelper::new(&ctx, node);
193
194        let children = helper.children();
195        assert_eq!(children.len(), 2);
196        assert_eq!(children[0].name(), "child1");
197        assert_eq!(children[1].name(), "child2");
198    }
199
200    #[test]
201    fn test_node_helper_ensure_arg_len_valid() {
202        let (ctx, doc) = parse_node("node \"a\" \"b\"");
203        let node = doc.nodes().first().unwrap();
204        let helper = NodeHelper::new(&ctx, node);
205
206        assert!(helper.ensure_arg_len(2..=2).is_ok());
207        assert!(helper.ensure_arg_len(1..=3).is_ok());
208        assert!(helper.ensure_arg_len(0..).is_ok());
209    }
210
211    #[test]
212    fn test_node_helper_ensure_arg_len_invalid() {
213        let (ctx, doc) = parse_node("node \"a\"");
214        let node = doc.nodes().first().unwrap();
215        let helper = NodeHelper::new(&ctx, node);
216
217        assert!(helper.ensure_arg_len(2..=2).is_err());
218    }
219
220    #[test]
221    fn test_parse_entry_ensure_usize() {
222        let (ctx, doc) = parse_node("node 42");
223        let node = doc.nodes().first().unwrap();
224        let helper = NodeHelper::new(&ctx, node);
225
226        assert_eq!(helper.arg(0).unwrap().ensure_usize().unwrap(), 42);
227    }
228
229    #[test]
230    fn test_parse_entry_ensure_bool() {
231        let (ctx, doc) = parse_node("node #true");
232        let node = doc.nodes().first().unwrap();
233        let helper = NodeHelper::new(&ctx, node);
234
235        assert!(helper.arg(0).unwrap().ensure_bool().unwrap());
236    }
237
238    #[test]
239    fn test_parse_entry_ensure_string() {
240        let (ctx, doc) = parse_node("node \"hello\"");
241        let node = doc.nodes().first().unwrap();
242        let helper = NodeHelper::new(&ctx, node);
243
244        assert_eq!(helper.arg(0).unwrap().ensure_string().unwrap(), "hello");
245    }
246
247    #[test]
248    fn test_parse_entry_type_mismatch() {
249        let (ctx, doc) = parse_node("node \"not_a_number\"");
250        let node = doc.nodes().first().unwrap();
251        let helper = NodeHelper::new(&ctx, node);
252
253        assert!(helper.arg(0).unwrap().ensure_usize().is_err());
254    }
255}