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;
6use serde::Serialize;
7
8/// A repeatable, separator-delimited group of positional arguments.
9#[derive(Debug, Default, Clone, Serialize)]
10#[non_exhaustive]
11pub struct SpecClause {
12    pub name: String,
13    pub separator: String,
14    pub args: Vec<SpecArg>,
15    #[serde(skip_serializing_if = "Option::is_none")]
16    pub help: Option<String>,
17    #[serde(skip_serializing_if = "Option::is_none")]
18    pub help_long: Option<String>,
19    pub usage: String,
20}
21
22impl SpecClause {
23    pub(crate) fn parse(ctx: &ParsingContext, node: &NodeHelper) -> Result<Self> {
24        let mut clause = Self {
25            name: node.arg(0)?.ensure_string()?,
26            ..Self::default()
27        };
28        for (key, value) in node.props() {
29            match key {
30                "separator" => clause.separator = value.ensure_string()?,
31                "help" => clause.help = Some(value.ensure_string()?),
32                "help_long" | "long_help" => clause.help_long = Some(value.ensure_string()?),
33                key => bail_parse!(ctx, value.entry.span(), "unsupported clause key {key}"),
34            }
35        }
36        for child in node.children() {
37            match child.name() {
38                "arg" => clause.args.push(SpecArg::parse(ctx, &child)?),
39                key => bail_parse!(
40                    ctx,
41                    child.node.name().span(),
42                    "unsupported clause child {key}"
43                ),
44            }
45        }
46        if clause.name.is_empty() {
47            bail_parse!(ctx, node.span(), "a clause needs a name");
48        }
49        if clause.separator.is_empty() {
50            bail_parse!(
51                ctx,
52                node.span(),
53                "clause {} needs a non-empty separator",
54                clause.name
55            );
56        }
57        if clause.separator.starts_with('-') {
58            bail_parse!(ctx, node.span(), "clause separator cannot start with `-`");
59        }
60        if clause.args.is_empty() {
61            bail_parse!(
62                ctx,
63                node.span(),
64                "clause {} needs at least one argument",
65                clause.name
66            );
67        }
68        clause.usage = clause.usage();
69        Ok(clause)
70    }
71
72    pub fn usage(&self) -> String {
73        let inner = self
74            .args
75            .iter()
76            .map(SpecArg::usage)
77            .collect::<Vec<_>>()
78            .join(" ");
79        format!("{inner} [{} {inner}]…", self.separator)
80    }
81}
82
83impl From<&SpecClause> for KdlNode {
84    fn from(clause: &SpecClause) -> Self {
85        let mut node = KdlNode::new("clause");
86        node.push(KdlEntry::new(clause.name.clone()));
87        node.push(string_entry(Some("separator"), &clause.separator));
88        if let Some(help) = &clause.help {
89            node.push(string_entry(Some("help"), help));
90        }
91        if let Some(help) = &clause.help_long {
92            node.push(string_entry(Some("help_long"), help));
93        }
94        let children = node.children_mut().get_or_insert_with(KdlDocument::new);
95        children
96            .nodes_mut()
97            .extend(clause.args.iter().map(Into::into));
98        node
99    }
100}