Skip to main content

usage/spec/
clause.rs

1use crate::error::Result;
2use crate::kdl::{KdlDocument, KdlEntry, KdlNode};
3use crate::spec::context::ParsingContext;
4use crate::spec::helpers::{string_entry, NodeHelper};
5use crate::{SpecArg, SpecFlag};
6use serde::Serialize;
7use std::collections::HashSet;
8
9/// A repeatable group of scoped flags and positional arguments.
10#[derive(Debug, Default, Clone, Serialize)]
11#[non_exhaustive]
12pub struct SpecClause {
13    pub name: String,
14    pub separator: Option<String>,
15    pub flags: Vec<SpecFlag>,
16    pub args: Vec<SpecArg>,
17    #[serde(skip_serializing_if = "Option::is_none")]
18    pub help: Option<String>,
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub help_long: Option<String>,
21    pub usage: String,
22}
23
24impl SpecClause {
25    pub(crate) fn conflicting_flag_spelling(&self, command_flags: &[SpecFlag]) -> Option<String> {
26        fn spellings(flag: &SpecFlag) -> impl Iterator<Item = String> + '_ {
27            flag.long
28                .iter()
29                .chain(&flag.hidden_aliases)
30                .map(|name| format!("--{name}"))
31                .chain(
32                    flag.short
33                        .iter()
34                        .chain(&flag.hidden_short_aliases)
35                        .map(|name| format!("-{name}")),
36                )
37                .chain(flag.negate.iter().cloned())
38        }
39
40        let mut seen = command_flags
41            .iter()
42            .flat_map(spellings)
43            .collect::<HashSet<_>>();
44        self.flags
45            .iter()
46            .flat_map(spellings)
47            .find(|spelling| !seen.insert(spelling.clone()))
48    }
49
50    pub(crate) fn parse(ctx: &ParsingContext, node: &NodeHelper) -> Result<Self> {
51        let mut clause = Self {
52            name: node.arg(0)?.ensure_string()?,
53            ..Self::default()
54        };
55        for (key, value) in node.props() {
56            match key {
57                "separator" => clause.separator = Some(value.ensure_string()?),
58                "help" => clause.help = Some(value.ensure_string()?),
59                "help_long" | "long_help" => clause.help_long = Some(value.ensure_string()?),
60                key => bail_parse!(ctx, value.entry.span(), "unsupported clause key {key}"),
61            }
62        }
63        for child in node.children() {
64            match child.name() {
65                "arg" => clause.args.push(SpecArg::parse(ctx, &child)?),
66                "flag" => clause.flags.push(SpecFlag::parse(ctx, &child)?),
67                key => bail_parse!(
68                    ctx,
69                    child.node.name().span(),
70                    "unsupported clause child {key}"
71                ),
72            }
73        }
74        if clause.name.is_empty() {
75            bail_parse!(ctx, node.span(), "a clause needs a name");
76        }
77        if clause.separator.as_ref().is_some_and(String::is_empty) {
78            bail_parse!(ctx, node.span(), "clause separator cannot be empty");
79        }
80        if clause
81            .separator
82            .as_ref()
83            .is_some_and(|separator| separator.starts_with('-'))
84        {
85            bail_parse!(ctx, node.span(), "clause separator cannot start with `-`");
86        }
87        if clause.args.is_empty() {
88            bail_parse!(
89                ctx,
90                node.span(),
91                "clause {} needs at least one argument",
92                clause.name
93            );
94        }
95        if clause.separator.is_none()
96            && (clause.args.len() != 1 || !clause.args[0].required || clause.args[0].var)
97        {
98            bail_parse!(
99                ctx,
100                node.span(),
101                "an implicit clause needs exactly one required, non-variadic positional argument"
102            );
103        }
104        clause.usage = clause.usage();
105        Ok(clause)
106    }
107
108    pub fn usage(&self) -> String {
109        let inner = self
110            .args
111            .iter()
112            .map(SpecArg::usage)
113            .collect::<Vec<_>>()
114            .join(" ");
115        match &self.separator {
116            Some(separator) => format!("[{inner} [{separator} {inner}]…]"),
117            None => {
118                let Some(arg) = self.args.first() else {
119                    return String::new();
120                };
121                let mut arg = arg.clone();
122                arg.required = false;
123                arg.var = true;
124                arg.usage()
125            }
126        }
127    }
128}
129
130impl From<&SpecClause> for KdlNode {
131    fn from(clause: &SpecClause) -> Self {
132        let mut node = KdlNode::new("clause");
133        node.push(KdlEntry::new(clause.name.clone()));
134        if let Some(separator) = &clause.separator {
135            node.push(string_entry(Some("separator"), separator));
136        }
137        if let Some(help) = &clause.help {
138            node.push(string_entry(Some("help"), help));
139        }
140        if let Some(help) = &clause.help_long {
141            node.push(string_entry(Some("help_long"), help));
142        }
143        let children = node.children_mut().get_or_insert_with(KdlDocument::new);
144        children
145            .nodes_mut()
146            .extend(clause.flags.iter().map(Into::into));
147        children
148            .nodes_mut()
149            .extend(clause.args.iter().map(Into::into));
150        node
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::SpecClause;
157
158    #[test]
159    fn an_empty_default_clause_has_an_empty_synopsis() {
160        assert_eq!(SpecClause::default().usage(), "");
161    }
162}