Skip to main content

usage/spec/
view.rs

1use std::fmt::{Display, Formatter};
2
3use kdl::{KdlEntry, KdlNode};
4use serde::Serialize;
5
6use crate::error::Result;
7use crate::spec::context::ParsingContext;
8use crate::spec::helpers::{string_entry, NodeHelper};
9
10/// A named executable surface derived from one command in the canonical spec.
11///
12/// Multicall binaries use this to describe an applet without copying or mutating the
13/// generated command tree. `root` is a space-separated command path. Root global flags may be
14/// carried wholesale with `globals=#true`, or selected explicitly with `global` children.
15#[derive(Debug, Default, Clone, Serialize)]
16#[non_exhaustive]
17pub struct SpecView {
18    /// Stable identifier used to select the view.
19    pub id: String,
20    /// Program name shown in prose. Defaults to [`Self::id`].
21    pub name: String,
22    /// Executable name used in usage lines. Defaults to [`Self::name`].
23    pub bin: String,
24    /// Command path promoted to the view's root.
25    pub root: String,
26    /// Carry every root global into the promoted command.
27    pub all_globals: bool,
28    /// Root-global selectors to carry when [`Self::all_globals`] is false.
29    pub globals: Vec<String>,
30}
31
32impl SpecView {
33    pub(crate) fn parse(ctx: &ParsingContext, node: &NodeHelper<'_>) -> Result<Self> {
34        let id = node.arg(0)?.ensure_string()?;
35        let mut view = Self {
36            name: id.clone(),
37            bin: id.clone(),
38            id,
39            ..Self::default()
40        };
41        for (key, value) in node.props() {
42            match key {
43                "name" => view.name = value.ensure_string()?,
44                "bin" => view.bin = value.ensure_string()?,
45                "root" => view.root = value.ensure_string()?,
46                "globals" => view.all_globals = value.ensure_bool()?,
47                key => bail_parse!(ctx, value.entry.span(), "unsupported view key {key}"),
48            }
49        }
50        for child in node.children() {
51            match child.name() {
52                "global" => {
53                    for selector in child.args() {
54                        view.globals.push(selector.ensure_string()?);
55                    }
56                }
57                key => bail_parse!(
58                    ctx,
59                    child.node.name().span(),
60                    "unsupported view value {key}"
61                ),
62            }
63        }
64        if view.id.is_empty() {
65            bail_parse!(ctx, node.span(), "a view needs a non-empty identifier");
66        }
67        if view.name.is_empty() || view.bin.is_empty() {
68            bail_parse!(ctx, node.span(), "a view's name and bin cannot be empty");
69        }
70        if view.root.trim().is_empty() {
71            bail_parse!(
72                ctx,
73                node.span(),
74                "a view needs the command path it promotes in `root`"
75            );
76        }
77        Ok(view)
78    }
79}
80
81impl Display for SpecView {
82    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
83        let mut node = KdlNode::new("view");
84        node.push(string_entry(None, &self.id));
85        if self.name != self.id {
86            node.push(string_entry(Some("name"), &self.name));
87        }
88        if self.bin != self.name {
89            node.push(string_entry(Some("bin"), &self.bin));
90        }
91        node.push(string_entry(Some("root"), &self.root));
92        if self.all_globals {
93            node.push(KdlEntry::new_prop("globals", true));
94        }
95        if !self.globals.is_empty() {
96            let mut children = kdl::KdlDocument::new();
97            let mut global = KdlNode::new("global");
98            for selector in &self.globals {
99                global.push(string_entry(None, selector));
100            }
101            children.nodes_mut().push(global);
102            node.set_children(children);
103        }
104        write!(f, "{node}")
105    }
106}