Skip to main content

usage/spec/
helpers.rs

1use crate::kdl::{KdlEntry, KdlEntryFormat, KdlNode, KdlValue};
2use crate::miette::SourceSpan;
3use indexmap::IndexMap;
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.
12///
13/// Scans overlapping offsets: `""""#` contains `"""#` starting at the second
14/// quote, which `str::match_indices` skips. One hash would then emit a closer
15/// that sits inside the payload and truncates the string.
16fn raw_multiline_hash_count(value: &str) -> usize {
17    let mut max_count = 0;
18    let bytes = value.as_bytes();
19    for i in 0..bytes.len().saturating_sub(2) {
20        if bytes[i] == b'"' && bytes[i + 1] == b'"' && bytes[i + 2] == b'"' {
21            let count = value[i + 3..].chars().take_while(|&c| c == '#').count();
22            max_count = max_count.max(count);
23        }
24    }
25    max_count + 1
26}
27
28/// A KDL quoted string, with everything that has to be escaped, escaped.
29fn escape_string(value: &str) -> String {
30    let mut out = String::with_capacity(value.len() + 2);
31    out.push('"');
32    for ch in value.chars() {
33        match ch {
34            '"' => out.push_str("\\\""),
35            '\\' => out.push_str("\\\\"),
36            '\n' => out.push_str("\\n"),
37            '\r' => out.push_str("\\r"),
38            '\t' => out.push_str("\\t"),
39            c if c.is_control() => out.push_str(&format!("\\u{{{:x}}}", c as u32)),
40            c => out.push(c),
41        }
42    }
43    out.push('"');
44    out
45}
46
47/// An entry format that keeps a literal representation as written.
48fn quoted_format(value_repr: &str) -> KdlEntryFormat {
49    KdlEntryFormat {
50        value_repr: value_repr.to_string(),
51        leading: " ".into(),
52        trailing: "".into(),
53        after_ty: "".into(),
54        before_ty_name: "".into(),
55        after_ty_name: "".into(),
56        after_key: "".into(),
57        after_eq: "".into(),
58        autoformat_keep: true,
59    }
60}
61
62/// Create a KdlEntry for a string value, using KDL raw multiline string syntax (`#"""..."""#`)
63/// when the value contains newlines. The number of `#` characters is automatically determined
64/// to ensure the value can be embedded safely.
65pub(crate) fn string_entry(key: Option<&str>, value: &str) -> KdlEntry {
66    let mut entry = match key {
67        Some(k) => KdlEntry::new_prop(k, KdlValue::String(value.to_string())),
68        None => KdlEntry::new(KdlValue::String(value.to_string())),
69    };
70    // Two kinds of value the kdl crate renders in a form this crate cannot read
71    // back. Both produced specs that failed to reparse, which the argv round-trip
72    // tests caught.
73    //
74    // A node argument starting with a dash: KDL reads `overrides "--keep"` but not
75    // `overrides --keep`. Properties are left alone, since `negate=--no-color`
76    // renders and parses today and quoting it would rewrite every committed spec
77    // for no gain.
78    let dashed_argument = key.is_none() && value.starts_with('-');
79    // A control character other than a newline or tab, which KDL requires as an
80    // escape rather than a literal. Help text really does contain these: a CLI that
81    // colors its help has an escape character in the middle of it.
82    let has_control = value
83        .chars()
84        .any(|c| c.is_control() && c != '\n' && c != '\t');
85    if dashed_argument || has_control {
86        entry.set_format(quoted_format(&escape_string(value)));
87        return entry;
88    }
89    if value.contains('\n') {
90        let n = raw_multiline_hash_count(value);
91        let hashes = "#".repeat(n);
92        let repr = format!("{hashes}\"\"\"\n{value}\n\"\"\"{hashes}");
93        entry.set_format(KdlEntryFormat {
94            value_repr: repr,
95            leading: " ".into(),
96            trailing: "".into(),
97            after_ty: "".into(),
98            before_ty_name: "".into(),
99            after_ty_name: "".into(),
100            after_key: "".into(),
101            after_eq: "".into(),
102            autoformat_keep: true,
103        });
104    }
105    entry
106}
107
108/// Whether a selector can be written as `conflicts="…"` and read back as the same one.
109///
110/// A value with a dashed word among several is taken for a list of flags, so a selector
111/// shaped like that has to be written as a child node instead.
112pub(crate) fn fits_selector_property(selector: &str) -> bool {
113    selector.split_whitespace().nth(1).is_none()
114        || !selector
115            .split_whitespace()
116            .any(|word| word.starts_with('-'))
117}
118
119#[derive(Debug)]
120pub struct NodeHelper<'a> {
121    pub(crate) node: &'a KdlNode,
122    pub(crate) ctx: &'a ParsingContext,
123}
124
125impl<'a> NodeHelper<'a> {
126    pub(crate) fn new(ctx: &'a ParsingContext, node: &'a KdlNode) -> Self {
127        Self { node, ctx }
128    }
129
130    pub(crate) fn name(&self) -> &str {
131        self.node.name().value()
132    }
133    pub(crate) fn span(&self) -> SourceSpan {
134        (self.node.span().offset(), self.node.span().len()).into()
135    }
136    pub(crate) fn ensure_arg_len<R>(&self, range: R) -> Result<&Self, UsageErr>
137    where
138        R: RangeBounds<usize> + Debug,
139    {
140        let count = self.args().count();
141        if !range.contains(&count) {
142            let ctx = self.ctx;
143            let span = self.span();
144            bail_parse!(ctx, span, "expected {range:?} arguments, got {count}",)
145        }
146        Ok(self)
147    }
148    /// Refuse a property written twice, which would otherwise keep only the last.
149    pub(crate) fn ensure_props_unrepeated(&self, keys: &[&str]) -> Result<(), UsageErr> {
150        for key in keys {
151            let mut written = self
152                .node
153                .entries()
154                .iter()
155                .filter(|e| e.name().is_some_and(|name| name.value() == *key));
156            if let (Some(_), Some(second)) = (written.next(), written.next()) {
157                bail_parse!(
158                    self.ctx,
159                    second.span(),
160                    "{key} is given more than once; the second would replace the first, so write several as a child node instead: {key} \"--a\" \"--b\""
161                );
162            }
163        }
164        Ok(())
165    }
166    pub(crate) fn get(&self, key: &str) -> Option<ParseEntry<'_>> {
167        self.node.entry(key).map(|e| ParseEntry::new(self.ctx, e))
168    }
169    pub(crate) fn arg(&self, i: usize) -> Result<ParseEntry<'_>, UsageErr> {
170        if let Some(entry) = self.args().nth(i) {
171            return Ok(entry);
172        }
173        bail_parse!(self.ctx, self.span(), "missing argument")
174    }
175    pub(crate) fn args(&self) -> impl Iterator<Item = ParseEntry<'_>> + '_ {
176        self.node
177            .entries()
178            .iter()
179            .filter(|e| e.name().is_none())
180            .map(|e| ParseEntry::new(self.ctx, e))
181    }
182    pub(crate) fn props(&self) -> IndexMap<&str, ParseEntry<'_>> {
183        self.node
184            .entries()
185            .iter()
186            .filter_map(|e| {
187                e.name()
188                    .map(|key| (key.value(), ParseEntry::new(self.ctx, e)))
189            })
190            .collect()
191    }
192    pub(crate) fn children(&self) -> Vec<Self> {
193        self.node
194            .children()
195            .map(|c| {
196                c.nodes()
197                    .iter()
198                    .map(|n| NodeHelper::new(self.ctx, n))
199                    .collect()
200            })
201            .unwrap_or_default()
202    }
203}
204
205#[derive(Debug)]
206pub(crate) struct ParseEntry<'a> {
207    pub(crate) ctx: &'a ParsingContext,
208    pub(crate) entry: &'a KdlEntry,
209    pub(crate) value: &'a KdlValue,
210}
211
212impl<'a> ParseEntry<'a> {
213    fn new(ctx: &'a ParsingContext, entry: &'a KdlEntry) -> Self {
214        Self {
215            ctx,
216            entry,
217            value: entry.value(),
218        }
219    }
220
221    fn span(&self) -> SourceSpan {
222        (self.entry.span().offset(), self.entry.span().len()).into()
223    }
224}
225
226impl ParseEntry<'_> {
227    pub fn ensure_usize(&self) -> Result<usize, UsageErr> {
228        match self.value.as_integer() {
229            Some(i) => Ok(i as usize),
230            None => bail_parse!(self.ctx, self.span(), "expected usize"),
231        }
232    }
233    #[allow(dead_code)]
234    pub fn ensure_f64(&self) -> Result<f64, UsageErr> {
235        match self.value.as_float() {
236            Some(f) => Ok(f),
237            None => bail_parse!(self.ctx, self.span(), "expected float"),
238        }
239    }
240    pub fn ensure_bool(&self) -> Result<bool, UsageErr> {
241        match self.value.as_bool() {
242            Some(b) => Ok(b),
243            None => bail_parse!(self.ctx, self.span(), "expected bool"),
244        }
245    }
246    pub fn ensure_string(&self) -> Result<String, UsageErr> {
247        match self.value.as_string() {
248            Some(s) => Ok(s.to_string()),
249            None => bail_parse!(self.ctx, self.span(), "expected string"),
250        }
251    }
252
253    /// Set a relationship list (`conflicts`, `requires`, …) from its property form.
254    ///
255    /// The property names exactly one flag or argument. A value like `"--b --c"` is a
256    /// single selector that matches nothing, so it is refused rather than left to enforce
257    /// nothing. Several targets are written as a child node: `conflicts "--b" "--c"`.
258    ///
259    /// Only a value with a dashed word is refused. A bare selector names a positional, and
260    /// a positional's name may hold a space (`arg "[input file]"`), so `"input file"` is
261    /// kept as the one name it may be.
262    pub fn set_selector(&self, key: &str, list: &mut Vec<String>) -> Result<(), UsageErr> {
263        let selector = self.ensure_string()?;
264        if !fits_selector_property(&selector) {
265            bail_parse!(
266                self.ctx,
267                self.span(),
268                "{key} takes one flag or argument per property, but {selector:?} has several; write them as a child node instead: {key} \"--a\" \"--b\""
269            );
270        }
271        list.push(selector);
272        Ok(())
273    }
274}
275
276#[cfg(test)]
277mod tests {
278    use super::*;
279    use crate::kdl::KdlDocument;
280    use std::path::Path;
281
282    fn parse_node(input: &str) -> (ParsingContext, KdlDocument) {
283        let ctx = ParsingContext::new(Path::new("test.kdl"), input);
284        let doc: KdlDocument = input.parse().unwrap();
285        (ctx, doc)
286    }
287
288    #[test]
289    fn test_node_helper_name() {
290        let (ctx, doc) = parse_node("test_node \"arg1\"");
291        let node = doc.nodes().first().unwrap();
292        let helper = NodeHelper::new(&ctx, node);
293        assert_eq!(helper.name(), "test_node");
294    }
295
296    #[test]
297    fn test_node_helper_arg() {
298        let (ctx, doc) = parse_node("node \"first\" \"second\"");
299        let node = doc.nodes().first().unwrap();
300        let helper = NodeHelper::new(&ctx, node);
301
302        assert_eq!(helper.arg(0).unwrap().ensure_string().unwrap(), "first");
303        assert_eq!(helper.arg(1).unwrap().ensure_string().unwrap(), "second");
304    }
305
306    #[test]
307    fn test_node_helper_args_count() {
308        let (ctx, doc) = parse_node("node \"a\" \"b\" \"c\"");
309        let node = doc.nodes().first().unwrap();
310        let helper = NodeHelper::new(&ctx, node);
311
312        assert_eq!(helper.args().count(), 3);
313    }
314
315    #[test]
316    fn test_node_helper_props() {
317        let (ctx, doc) = parse_node("node key1=\"value1\" key2=\"value2\"");
318        let node = doc.nodes().first().unwrap();
319        let helper = NodeHelper::new(&ctx, node);
320
321        let props = helper.props();
322        assert_eq!(props.len(), 2);
323        assert_eq!(props["key1"].ensure_string().unwrap(), "value1");
324        assert_eq!(props["key2"].ensure_string().unwrap(), "value2");
325    }
326
327    #[test]
328    fn test_node_helper_get() {
329        let (ctx, doc) = parse_node("node name=\"test\"");
330        let node = doc.nodes().first().unwrap();
331        let helper = NodeHelper::new(&ctx, node);
332
333        assert!(helper.get("name").is_some());
334        assert!(helper.get("nonexistent").is_none());
335    }
336
337    #[test]
338    fn test_node_helper_children() {
339        let (ctx, doc) = parse_node("parent { child1; child2 }");
340        let node = doc.nodes().first().unwrap();
341        let helper = NodeHelper::new(&ctx, node);
342
343        let children = helper.children();
344        assert_eq!(children.len(), 2);
345        assert_eq!(children[0].name(), "child1");
346        assert_eq!(children[1].name(), "child2");
347    }
348
349    #[test]
350    fn test_node_helper_ensure_arg_len_valid() {
351        let (ctx, doc) = parse_node("node \"a\" \"b\"");
352        let node = doc.nodes().first().unwrap();
353        let helper = NodeHelper::new(&ctx, node);
354
355        assert!(helper.ensure_arg_len(2..=2).is_ok());
356        assert!(helper.ensure_arg_len(1..=3).is_ok());
357        assert!(helper.ensure_arg_len(0..).is_ok());
358    }
359
360    #[test]
361    fn test_node_helper_ensure_arg_len_invalid() {
362        let (ctx, doc) = parse_node("node \"a\"");
363        let node = doc.nodes().first().unwrap();
364        let helper = NodeHelper::new(&ctx, node);
365
366        assert!(helper.ensure_arg_len(2..=2).is_err());
367    }
368
369    #[test]
370    fn test_parse_entry_ensure_usize() {
371        let (ctx, doc) = parse_node("node 42");
372        let node = doc.nodes().first().unwrap();
373        let helper = NodeHelper::new(&ctx, node);
374
375        assert_eq!(helper.arg(0).unwrap().ensure_usize().unwrap(), 42);
376    }
377
378    #[test]
379    fn test_parse_entry_ensure_bool() {
380        let (ctx, doc) = parse_node("node #true");
381        let node = doc.nodes().first().unwrap();
382        let helper = NodeHelper::new(&ctx, node);
383
384        assert!(helper.arg(0).unwrap().ensure_bool().unwrap());
385    }
386
387    #[test]
388    fn test_parse_entry_ensure_string() {
389        let (ctx, doc) = parse_node("node \"hello\"");
390        let node = doc.nodes().first().unwrap();
391        let helper = NodeHelper::new(&ctx, node);
392
393        assert_eq!(helper.arg(0).unwrap().ensure_string().unwrap(), "hello");
394    }
395
396    #[test]
397    fn test_parse_entry_type_mismatch() {
398        let (ctx, doc) = parse_node("node \"not_a_number\"");
399        let node = doc.nodes().first().unwrap();
400        let helper = NodeHelper::new(&ctx, node);
401
402        assert!(helper.arg(0).unwrap().ensure_usize().is_err());
403    }
404
405    #[test]
406    fn overlapping_quotes_raise_the_raw_multiline_hash_count() {
407        assert_eq!(raw_multiline_hash_count("\"\"\"#"), 2);
408        assert_eq!(raw_multiline_hash_count("\"\"\"\"#"), 2);
409        assert_eq!(raw_multiline_hash_count("\"\"\"\"##"), 3);
410        assert_eq!(raw_multiline_hash_count("plain"), 1);
411    }
412
413    #[test]
414    fn overlapping_quotes_round_trip_through_parse() {
415        for value in [
416            "before\n\"\"\"\"#\nafter",
417            "before\n\"\"\"\"##\nafter",
418            "line\n\"\"\"#\nstill",
419        ] {
420            let mut node = KdlNode::new("about");
421            node.push(string_entry(None, value));
422            let kdl = format!("name ex\nbin ex\n{node}\n");
423            let spec: crate::Spec = kdl
424                .parse()
425                .unwrap_or_else(|e| panic!("emitted spec must parse: {e}\n{kdl}"));
426            assert_eq!(spec.about.as_deref(), Some(value), "{kdl}");
427        }
428    }
429}