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}