Skip to main content

ralph/contracts/
cli_spec.rs

1//! CLI specification contract emitted as deterministic JSON.
2//!
3//! Responsibilities:
4//! - Define the versioned, serialized data model (`CliSpec`, `CommandSpec`, `ArgSpec`) for emitting
5//!   a machine-readable description of Ralph's clap CLI.
6//! - Provide a stable contract suitable for tooling (docs generation, wrappers, completions).
7//!
8//! Not handled here:
9//! - Extracting data from `clap::Command` (see `crate::cli_spec`).
10//! - CLI command wiring, IO, or printing (see `crate::commands` when integrated).
11//!
12//! Invariants/assumptions:
13//! - `CliSpec.version` is bumped only for breaking JSON changes.
14//! - `CommandSpec.path` is the full command path from the root (e.g. `["ralph","run","one"]`).
15//! - `CommandSpec` and `ArgSpec` vectors are expected to be deterministically sorted by the emitter.
16//! - `ArgSpec.possible_values` is expected to be deterministically sorted by the emitter.
17
18use schemars::JsonSchema;
19use serde::{Deserialize, Serialize};
20
21/// Current JSON format version for `CliSpec`.
22pub const CLI_SPEC_VERSION: u32 = 2;
23
24/// Root CLI spec document.
25#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
26#[serde(deny_unknown_fields)]
27pub struct CliSpec {
28    /// JSON format version.
29    pub version: u32,
30
31    /// Root command and its full subcommand tree.
32    pub root: CommandSpec,
33}
34
35/// A command/subcommand and its arguments.
36#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
37#[serde(deny_unknown_fields)]
38pub struct CommandSpec {
39    /// Command name (the last segment of `path`).
40    pub name: String,
41
42    /// Full path from the root command, inclusive.
43    pub path: Vec<String>,
44
45    /// Short description shown in `--help`.
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub about: Option<String>,
48
49    /// Long description shown in `--help`.
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub long_about: Option<String>,
52
53    /// Extra help appended after long help.
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub after_long_help: Option<String>,
56
57    /// Whether the command is hidden from normal help output.
58    pub hidden: bool,
59
60    /// Arguments available at this command level (including hidden and generated help/version args).
61    pub args: Vec<ArgSpec>,
62
63    /// Nested subcommands (including hidden/internal subcommands).
64    pub subcommands: Vec<CommandSpec>,
65}
66
67/// A single CLI argument/flag.
68#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
69#[serde(deny_unknown_fields)]
70pub struct ArgSpec {
71    /// Clap argument id (stable identifier used for conflict groups, etc.).
72    pub id: String,
73
74    /// Long flag name without leading `--`.
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub long: Option<String>,
77
78    /// Short flag letter (without leading `-`).
79    #[serde(skip_serializing_if = "Option::is_none")]
80    pub short: Option<char>,
81
82    /// Help text shown in `--help`.
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub help: Option<String>,
85
86    /// Long help text shown in `--help`.
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub long_help: Option<String>,
89
90    /// Whether the argument is required.
91    pub required: bool,
92
93    /// Default values (as shown by clap and used when the argument is absent).
94    ///
95    /// This is always present; an empty list means there is no configured default.
96    pub default_values: Vec<String>,
97
98    /// Enumerated possible values for the argument value parser (if known).
99    ///
100    /// This is always present; an empty list means clap does not advertise a finite set of
101    /// possible values for this argument.
102    pub possible_values: Vec<String>,
103
104    /// Whether the argument's value is parsed as a clap `ValueEnum` type.
105    ///
106    /// This is intended for tooling (e.g., rendering dropdowns) and is a best-effort reflection of
107    /// the clap configuration.
108    pub value_enum: bool,
109
110    /// Minimum number of values this argument accepts per occurrence.
111    pub num_args_min: usize,
112
113    /// Maximum number of values this argument accepts per occurrence (inclusive).
114    ///
115    /// `None` means unbounded.
116    pub num_args_max: Option<usize>,
117
118    /// Whether the argument is global (propagates to subcommands).
119    pub global: bool,
120
121    /// Whether the argument is hidden from normal help output.
122    pub hidden: bool,
123
124    /// Whether the argument is positional.
125    pub positional: bool,
126
127    /// For positional arguments, the 1-based index.
128    #[serde(skip_serializing_if = "Option::is_none")]
129    pub index: Option<usize>,
130
131    /// The clap action driving how values are applied (e.g. `Set`, `SetTrue`, `Append`).
132    pub action: String,
133}