1use std::fmt::Display;
2
3use crate::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#[derive(Debug, Default, Clone, Serialize)]
11#[non_exhaustive]
12pub struct SpecMount {
13 pub run: String,
14 #[serde(skip_serializing_if = "Option::is_none")]
16 pub synopsis: Option<String>,
17 pub overrides_default: bool,
26}
27
28impl SpecMount {
29 pub fn new(run: impl Into<String>) -> Self {
31 Self {
32 run: run.into(),
33 synopsis: None,
34 overrides_default: false,
35 }
36 }
37
38 pub fn overriding_default(run: impl Into<String>) -> Self {
40 Self {
41 run: run.into(),
42 synopsis: None,
43 overrides_default: true,
44 }
45 }
46}
47
48impl SpecMount {
49 pub(crate) fn parse(ctx: &ParsingContext, node: &NodeHelper) -> Result<Self> {
50 let mut mount = SpecMount::default();
51 for (k, v) in node.props() {
52 match k {
53 "run" => mount.run = v.ensure_string()?,
54 "synopsis" => mount.synopsis = Some(v.ensure_string()?),
55 "overrides_default" => mount.overrides_default = v.ensure_bool()?,
56 k => bail_parse!(ctx, v.entry.span(), "unsupported mount key {k}"),
57 }
58 }
59 for child in node.children() {
60 match child.name() {
61 "run" => mount.run = child.arg(0)?.ensure_string()?,
62 "synopsis" => mount.synopsis = Some(child.arg(0)?.ensure_string()?),
63 "overrides_default" => mount.overrides_default = child.arg(0)?.ensure_bool()?,
64 k => bail_parse!(
65 ctx,
66 child.node.name().span(),
67 "unsupported mount value key {k}"
68 ),
69 }
70 }
71 if mount.run.is_empty() {
72 bail_parse!(ctx, node.span(), "mount run is required")
73 }
74 Ok(mount)
75 }
76 pub fn usage(&self) -> String {
77 format!("mount:{}", self.run)
78 }
79}
80
81impl From<&SpecMount> for KdlNode {
82 fn from(mount: &SpecMount) -> KdlNode {
83 let mut node = KdlNode::new("mount");
84 node.push(string_entry(Some("run"), &mount.run));
85 if let Some(synopsis) = &mount.synopsis {
86 node.push(string_entry(Some("synopsis"), synopsis));
87 }
88 if mount.overrides_default {
89 node.push(KdlEntry::new_prop("overrides_default", true));
90 }
91 node
92 }
93}
94
95impl Display for SpecMount {
96 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97 write!(f, "{}", self.usage())
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 #[test]
104 fn unresolved_mount_synopsis_round_trips_and_renders_without_discovery() {
105 let spec: crate::Spec = "bin ex\ncmd run {\n mount run=\"this-command-must-never-run\" synopsis=\"[TASK] [ARGS]…\"\n}\n".parse().unwrap();
106 let run = &spec.cmd.subcommands["run"];
107 assert_eq!(run.usage, "run [TASK] [ARGS]…");
108 let again: crate::Spec = spec.to_string().parse().unwrap();
109 assert_eq!(
110 again.cmd.subcommands["run"].mounts[0].synopsis.as_deref(),
111 Some("[TASK] [ARGS]…")
112 );
113 let json = serde_json::to_value(&spec).unwrap();
114 assert_eq!(
115 json["cmd"]["subcommands"]["run"]["usage"],
116 "run [TASK] [ARGS]…"
117 );
118 #[cfg(feature = "markdown")]
119 assert!(crate::docs::markdown::MarkdownRenderer::new(spec.clone())
120 .render_cmd(run)
121 .unwrap()
122 .contains("run [TASK] [ARGS]…"));
123 #[cfg(feature = "cli-help")]
124 assert!(crate::docs::cli::render_help(&spec, run, true).contains("run [TASK] [ARGS]…"));
125 }
126
127 #[test]
128 fn subcommand_required_controls_custom_placeholder() {
129 for (required, expected) in [(true, "<ACTION>"), (false, "[ACTION]")] {
130 let source = format!(
131 "bin ex\nsubcommand_required #{required}\nsubcommand_value_name ACTION\ncmd go\n"
132 );
133 let spec: crate::Spec = source.parse().unwrap();
134 assert_eq!(spec.cmd.usage, expected);
135 }
136 let spec: crate::Spec = "bin ex\nmount run=\"never-run\"\n".parse().unwrap();
137 assert_eq!(spec.cmd.usage, "");
138 }
139}