Skip to main content

usage/spec/
helpers.rs

1use indexmap::IndexMap;
2use kdl::{KdlEntry, KdlEntryFormat, 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/// Compute the number of `#` characters needed for a raw multiline string.
11/// We need `n` such that the value does not contain `"""` followed by `n` `#` characters.
12fn raw_multiline_hash_count(value: &str) -> usize {
13    let mut max_count = 0;
14    for line in value.lines() {
15        for (idx, _) in line.match_indices("\"\"\"") {
16            let after = &line[idx + 3..];
17            let count = after.chars().take_while(|&c| c == '#').count();
18            max_count = max_count.max(count);
19        }
20    }
21    max_count + 1
22}
23
24/// A KDL quoted string, with everything that has to be escaped, escaped.
25fn escape_string(value: &str) -> String {
26    let mut out = String::with_capacity(value.len() + 2);
27    out.push('"');
28    for ch in value.chars() {
29        match ch {
30            '"' => out.push_str("\\\""),
31            '\\' => out.push_str("\\\\"),
32            '\n' => out.push_str("\\n"),
33            '\r' => out.push_str("\\r"),
34            '\t' => out.push_str("\\t"),
35            c if c.is_control() => out.push_str(&format!("\\u{{{:x}}}", c as u32)),
36            c => out.push(c),
37        }
38    }
39    out.push('"');
40    out
41}
42
43/// An entry format that keeps a literal representation as written.
44fn quoted_format(value_repr: &str) -> KdlEntryFormat {
45    KdlEntryFormat {
46        value_repr: value_repr.to_string(),
47        leading: " ".into(),
48        trailing: "".into(),
49        after_ty: "".into(),
50        before_ty_name: "".into(),
51        after_ty_name: "".into(),
52        after_key: "".into(),
53        after_eq: "".into(),
54        autoformat_keep: true,
55    }
56}
57
58/// Create a KdlEntry for a string value, using KDL raw multiline string syntax (`#"""..."""#`)
59/// when the value contains newlines. The number of `#` characters is automatically determined
60/// to ensure the value can be embedded safely.
61pub(crate) fn string_entry(key: Option<&str>, value: &str) -> KdlEntry {
62    let mut entry = match key {
63        Some(k) => KdlEntry::new_prop(k, KdlValue::String(value.to_string())),
64        None => KdlEntry::new(KdlValue::String(value.to_string())),
65    };
66    // Two kinds of value the kdl crate renders in a form this crate cannot read
67    // back. Both produced specs that failed to reparse, which the argv round-trip
68    // tests caught.
69    //
70    // A node argument starting with a dash: KDL reads `overrides "--keep"` but not
71    // `overrides --keep`. Properties are left alone, since `negate=--no-color`
72    // renders and parses today and quoting it would rewrite every committed spec
73    // for no gain.
74    let dashed_argument = key.is_none() && value.starts_with('-');
75    // A control character other than a newline or tab, which KDL requires as an
76    // escape rather than a literal. Help text really does contain these: a CLI that
77    // colors its help has an escape character in the middle of it.
78    let has_control = value
79        .chars()
80        .any(|c| c.is_control() && c != '\n' && c != '\t');
81    if dashed_argument || has_control {
82        entry.set_format(quoted_format(&escape_string(value)));
83        return entry;
84    }
85    if value.contains('\n') {
86        let n = raw_multiline_hash_count(value);
87        let hashes = "#".repeat(n);
88        let repr = format!("{hashes}\"\"\"\n{value}\n\"\"\"{hashes}");
89        entry.set_format(KdlEntryFormat {
90            value_repr: repr,
91            leading: " ".into(),
92            trailing: "".into(),
93            after_ty: "".into(),
94            before_ty_name: "".into(),
95            after_ty_name: "".into(),
96            after_key: "".into(),
97            after_eq: "".into(),
98            autoformat_keep: true,
99        });
100    }
101    entry
102}
103
104#[derive(Debug)]
105pub struct NodeHelper<'a> {
106    pub(crate) node: &'a KdlNode,
107    pub(crate) ctx: &'a ParsingContext,
108}
109
110impl<'a> NodeHelper<'a> {
111    pub(crate) fn new(ctx: &'a ParsingContext, node: &'a KdlNode) -> Self {
112        Self { node, ctx }
113    }
114
115    pub(crate) fn name(&self) -> &str {
116        self.node.name().value()
117    }
118    pub(crate) fn span(&self) -> SourceSpan {
119        (self.node.span().offset(), self.node.span().len()).into()
120    }
121    pub(crate) fn ensure_arg_len<R>(&self, range: R) -> Result<&Self, UsageErr>
122    where
123        R: RangeBounds<usize> + Debug,
124    {
125        let count = self.args().count();
126        if !range.contains(&count) {
127            let ctx = self.ctx;
128            let span = self.span();
129            bail_parse!(ctx, span, "expected {range:?} arguments, got {count}",)
130        }
131        Ok(self)
132    }
133    pub(crate) fn get(&self, key: &str) -> Option<ParseEntry<'_>> {
134        self.node.entry(key).map(|e| ParseEntry::new(self.ctx, e))
135    }
136    pub(crate) fn arg(&self, i: usize) -> Result<ParseEntry<'_>, UsageErr> {
137        if let Some(entry) = self.args().nth(i) {
138            return Ok(entry);
139        }
140        bail_parse!(self.ctx, self.span(), "missing argument")
141    }
142    pub(crate) fn args(&self) -> impl Iterator<Item = ParseEntry<'_>> + '_ {
143        self.node
144            .entries()
145            .iter()
146            .filter(|e| e.name().is_none())
147            .map(|e| ParseEntry::new(self.ctx, e))
148    }
149    pub(crate) fn props(&self) -> IndexMap<&str, ParseEntry<'_>> {
150        self.node
151            .entries()
152            .iter()
153            .filter_map(|e| {
154                e.name()
155                    .map(|key| (key.value(), ParseEntry::new(self.ctx, e)))
156            })
157            .collect()
158    }
159    pub(crate) fn children(&self) -> Vec<Self> {
160        self.node
161            .children()
162            .map(|c| {
163                c.nodes()
164                    .iter()
165                    .map(|n| NodeHelper::new(self.ctx, n))
166                    .collect()
167            })
168            .unwrap_or_default()
169    }
170}
171
172#[derive(Debug)]
173pub(crate) struct ParseEntry<'a> {
174    pub(crate) ctx: &'a ParsingContext,
175    pub(crate) entry: &'a KdlEntry,
176    pub(crate) value: &'a KdlValue,
177}
178
179impl<'a> ParseEntry<'a> {
180    fn new(ctx: &'a ParsingContext, entry: &'a KdlEntry) -> Self {
181        Self {
182            ctx,
183            entry,
184            value: entry.value(),
185        }
186    }
187
188    fn span(&self) -> SourceSpan {
189        (self.entry.span().offset(), self.entry.span().len()).into()
190    }
191}
192
193impl ParseEntry<'_> {
194    pub fn ensure_usize(&self) -> Result<usize, UsageErr> {
195        match self.value.as_integer() {
196            Some(i) => Ok(i as usize),
197            None => bail_parse!(self.ctx, self.span(), "expected usize"),
198        }
199    }
200    #[allow(dead_code)]
201    pub fn ensure_f64(&self) -> Result<f64, UsageErr> {
202        match self.value.as_float() {
203            Some(f) => Ok(f),
204            None => bail_parse!(self.ctx, self.span(), "expected float"),
205        }
206    }
207    pub fn ensure_bool(&self) -> Result<bool, UsageErr> {
208        match self.value.as_bool() {
209            Some(b) => Ok(b),
210            None => bail_parse!(self.ctx, self.span(), "expected bool"),
211        }
212    }
213    pub fn ensure_string(&self) -> Result<String, UsageErr> {
214        match self.value.as_string() {
215            Some(s) => Ok(s.to_string()),
216            None => bail_parse!(self.ctx, self.span(), "expected string"),
217        }
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224    use kdl::KdlDocument;
225    use std::path::Path;
226
227    fn parse_node(input: &str) -> (ParsingContext, KdlDocument) {
228        let ctx = ParsingContext::new(Path::new("test.kdl"), input);
229        let doc: KdlDocument = input.parse().unwrap();
230        (ctx, doc)
231    }
232
233    #[test]
234    fn test_node_helper_name() {
235        let (ctx, doc) = parse_node("test_node \"arg1\"");
236        let node = doc.nodes().first().unwrap();
237        let helper = NodeHelper::new(&ctx, node);
238        assert_eq!(helper.name(), "test_node");
239    }
240
241    #[test]
242    fn test_node_helper_arg() {
243        let (ctx, doc) = parse_node("node \"first\" \"second\"");
244        let node = doc.nodes().first().unwrap();
245        let helper = NodeHelper::new(&ctx, node);
246
247        assert_eq!(helper.arg(0).unwrap().ensure_string().unwrap(), "first");
248        assert_eq!(helper.arg(1).unwrap().ensure_string().unwrap(), "second");
249    }
250
251    #[test]
252    fn test_node_helper_args_count() {
253        let (ctx, doc) = parse_node("node \"a\" \"b\" \"c\"");
254        let node = doc.nodes().first().unwrap();
255        let helper = NodeHelper::new(&ctx, node);
256
257        assert_eq!(helper.args().count(), 3);
258    }
259
260    #[test]
261    fn test_node_helper_props() {
262        let (ctx, doc) = parse_node("node key1=\"value1\" key2=\"value2\"");
263        let node = doc.nodes().first().unwrap();
264        let helper = NodeHelper::new(&ctx, node);
265
266        let props = helper.props();
267        assert_eq!(props.len(), 2);
268        assert_eq!(props["key1"].ensure_string().unwrap(), "value1");
269        assert_eq!(props["key2"].ensure_string().unwrap(), "value2");
270    }
271
272    #[test]
273    fn test_node_helper_get() {
274        let (ctx, doc) = parse_node("node name=\"test\"");
275        let node = doc.nodes().first().unwrap();
276        let helper = NodeHelper::new(&ctx, node);
277
278        assert!(helper.get("name").is_some());
279        assert!(helper.get("nonexistent").is_none());
280    }
281
282    #[test]
283    fn test_node_helper_children() {
284        let (ctx, doc) = parse_node("parent { child1; child2 }");
285        let node = doc.nodes().first().unwrap();
286        let helper = NodeHelper::new(&ctx, node);
287
288        let children = helper.children();
289        assert_eq!(children.len(), 2);
290        assert_eq!(children[0].name(), "child1");
291        assert_eq!(children[1].name(), "child2");
292    }
293
294    #[test]
295    fn test_node_helper_ensure_arg_len_valid() {
296        let (ctx, doc) = parse_node("node \"a\" \"b\"");
297        let node = doc.nodes().first().unwrap();
298        let helper = NodeHelper::new(&ctx, node);
299
300        assert!(helper.ensure_arg_len(2..=2).is_ok());
301        assert!(helper.ensure_arg_len(1..=3).is_ok());
302        assert!(helper.ensure_arg_len(0..).is_ok());
303    }
304
305    #[test]
306    fn test_node_helper_ensure_arg_len_invalid() {
307        let (ctx, doc) = parse_node("node \"a\"");
308        let node = doc.nodes().first().unwrap();
309        let helper = NodeHelper::new(&ctx, node);
310
311        assert!(helper.ensure_arg_len(2..=2).is_err());
312    }
313
314    #[test]
315    fn test_parse_entry_ensure_usize() {
316        let (ctx, doc) = parse_node("node 42");
317        let node = doc.nodes().first().unwrap();
318        let helper = NodeHelper::new(&ctx, node);
319
320        assert_eq!(helper.arg(0).unwrap().ensure_usize().unwrap(), 42);
321    }
322
323    #[test]
324    fn test_parse_entry_ensure_bool() {
325        let (ctx, doc) = parse_node("node #true");
326        let node = doc.nodes().first().unwrap();
327        let helper = NodeHelper::new(&ctx, node);
328
329        assert!(helper.arg(0).unwrap().ensure_bool().unwrap());
330    }
331
332    #[test]
333    fn test_parse_entry_ensure_string() {
334        let (ctx, doc) = parse_node("node \"hello\"");
335        let node = doc.nodes().first().unwrap();
336        let helper = NodeHelper::new(&ctx, node);
337
338        assert_eq!(helper.arg(0).unwrap().ensure_string().unwrap(), "hello");
339    }
340
341    #[test]
342    fn test_parse_entry_type_mismatch() {
343        let (ctx, doc) = parse_node("node \"not_a_number\"");
344        let node = doc.nodes().first().unwrap();
345        let helper = NodeHelper::new(&ctx, node);
346
347        assert!(helper.arg(0).unwrap().ensure_usize().is_err());
348    }
349}