Skip to main content

appcore_args/
spec.rs

1// =============================================================================
2//        #######
3//     ###       ###     F: spec.rs
4//    ##   ## ##   ##    P: AppCore-Runtime
5//         ## ##
6//                       C: 2026/08/19 12:52:57 by dnettoRaw
7//    ##   ## ##   ##    U: 2026/08/19 13:34:54 by dnettoRaw
8//      ###########      S: 1.0.1-rc.8
9// =============================================================================
10
11//! Defines bounded spec contracts and behavior for this crate.
12
13use std::fmt;
14
15#[derive(Clone, Debug, PartialEq, Eq)]
16pub struct CliSpec {
17    name: String,
18    about: String,
19    version: Option<String>,
20    commands: Vec<CommandSpec>,
21    options: Vec<OptionSpec>,
22    arguments: Vec<ArgumentSpec>,
23    command_required: bool,
24}
25
26#[derive(Clone, Debug, PartialEq, Eq)]
27pub struct CommandSpec {
28    name: String,
29    aliases: Vec<String>,
30    about: String,
31    commands: Vec<CommandSpec>,
32    options: Vec<OptionSpec>,
33    arguments: Vec<ArgumentSpec>,
34    command_required: bool,
35    hidden: bool,
36}
37
38#[derive(Clone, Debug, PartialEq, Eq)]
39pub struct OptionSpec {
40    long: String,
41    short: Option<char>,
42    value: ValueMode,
43    value_name: String,
44    value_type: ValueType,
45    possible_values: Vec<String>,
46    about: String,
47    required: bool,
48    repeatable: bool,
49    detached_optional_value: bool,
50    terminal: bool,
51    hidden: bool,
52    conflicts_with: Vec<String>,
53    requires: Vec<String>,
54    pub(crate) environment: Option<String>,
55    pub(crate) default: Option<std::ffi::OsString>,
56    pub(crate) kind: crate::ValueKind,
57    pub(crate) exclusive_group: Option<String>,
58}
59
60#[derive(Clone, Debug, PartialEq, Eq)]
61pub struct ArgumentSpec {
62    name: String,
63    about: String,
64    value_type: ValueType,
65    possible_values: Vec<String>,
66    required: bool,
67    multiple: bool,
68    pub(crate) kind: crate::ValueKind,
69}
70
71#[derive(Clone, Copy, Debug, PartialEq, Eq)]
72pub enum ValueMode {
73    Forbidden,
74    Required,
75    Optional,
76}
77
78#[derive(Clone, Copy, Debug, PartialEq, Eq)]
79pub enum ValueType {
80    String,
81    Bool,
82    I64,
83    U64,
84}
85
86impl fmt::Display for ValueType {
87    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
88        formatter.write_str(match self {
89            Self::String => "text",
90            Self::Bool => "true or false",
91            Self::I64 => "a signed integer",
92            Self::U64 => "an unsigned integer",
93        })
94    }
95}
96
97#[derive(Clone, Debug, PartialEq, Eq)]
98pub struct SpecError {
99    message: String,
100}
101
102impl CliSpec {
103    pub fn new(name: impl Into<String>) -> Self {
104        Self {
105            name: name.into(),
106            about: String::new(),
107            version: None,
108            commands: Vec::new(),
109            options: Vec::new(),
110            arguments: Vec::new(),
111            command_required: false,
112        }
113    }
114    pub fn about(mut self, about: impl Into<String>) -> Self {
115        self.about = about.into();
116        self
117    }
118    pub fn version(mut self, version: impl Into<String>) -> Self {
119        self.version = Some(version.into());
120        self
121    }
122    pub fn command(mut self, command: CommandSpec) -> Self {
123        self.commands.push(command);
124        self
125    }
126    pub fn option(mut self, option: OptionSpec) -> Self {
127        self.options.push(option);
128        self
129    }
130    pub fn argument(mut self, argument: ArgumentSpec) -> Self {
131        self.arguments.push(argument);
132        self
133    }
134    pub fn command_required(mut self, required: bool) -> Self {
135        self.command_required = required;
136        self
137    }
138    pub fn validate(&self) -> Result<(), SpecError> {
139        crate::spec_validation::validate_spec(self)
140    }
141    pub fn name(&self) -> &str {
142        &self.name
143    }
144    pub fn about_text(&self) -> &str {
145        &self.about
146    }
147    pub fn version_text(&self) -> Option<&str> {
148        self.version.as_deref()
149    }
150    pub fn commands(&self) -> &[CommandSpec] {
151        &self.commands
152    }
153    pub fn options(&self) -> &[OptionSpec] {
154        &self.options
155    }
156    pub fn arguments(&self) -> &[ArgumentSpec] {
157        &self.arguments
158    }
159    pub fn is_command_required(&self) -> bool {
160        self.command_required
161    }
162}
163
164impl CommandSpec {
165    pub fn new(name: impl Into<String>) -> Self {
166        Self {
167            name: name.into(),
168            aliases: Vec::new(),
169            about: String::new(),
170            commands: Vec::new(),
171            options: Vec::new(),
172            arguments: Vec::new(),
173            command_required: false,
174            hidden: false,
175        }
176    }
177    pub fn alias(mut self, alias: impl Into<String>) -> Self {
178        self.aliases.push(alias.into());
179        self
180    }
181    pub fn about(mut self, about: impl Into<String>) -> Self {
182        self.about = about.into();
183        self
184    }
185    pub fn command(mut self, command: CommandSpec) -> Self {
186        self.commands.push(command);
187        self
188    }
189    pub fn option(mut self, option: OptionSpec) -> Self {
190        self.options.push(option);
191        self
192    }
193    pub fn argument(mut self, argument: ArgumentSpec) -> Self {
194        self.arguments.push(argument);
195        self
196    }
197    pub fn command_required(mut self, required: bool) -> Self {
198        self.command_required = required;
199        self
200    }
201    pub fn hidden(mut self, hidden: bool) -> Self {
202        self.hidden = hidden;
203        self
204    }
205    pub fn name(&self) -> &str {
206        &self.name
207    }
208    pub fn aliases(&self) -> &[String] {
209        &self.aliases
210    }
211    pub fn matches(&self, value: &str) -> bool {
212        self.name == value || self.aliases.iter().any(|alias| alias == value)
213    }
214    pub fn about_text(&self) -> &str {
215        &self.about
216    }
217    pub fn commands(&self) -> &[CommandSpec] {
218        &self.commands
219    }
220    pub fn options(&self) -> &[OptionSpec] {
221        &self.options
222    }
223    pub fn arguments(&self) -> &[ArgumentSpec] {
224        &self.arguments
225    }
226    pub fn is_command_required(&self) -> bool {
227        self.command_required
228    }
229    pub fn is_hidden(&self) -> bool {
230        self.hidden
231    }
232}
233
234impl OptionSpec {
235    pub fn flag(long: impl Into<String>) -> Self {
236        Self::new(long, ValueMode::Forbidden)
237    }
238    pub fn value(long: impl Into<String>) -> Self {
239        Self::new(long, ValueMode::Required)
240    }
241    fn new(long: impl Into<String>, value: ValueMode) -> Self {
242        Self {
243            long: long.into(),
244            short: None,
245            value,
246            value_name: "VALUE".into(),
247            value_type: ValueType::String,
248            possible_values: Vec::new(),
249            about: String::new(),
250            required: false,
251            repeatable: false,
252            detached_optional_value: false,
253            terminal: false,
254            hidden: false,
255            conflicts_with: Vec::new(),
256            requires: Vec::new(),
257            environment: None,
258            default: None,
259            kind: crate::ValueKind::Text,
260            exclusive_group: None,
261        }
262    }
263    pub fn short(mut self, short: char) -> Self {
264        self.short = Some(short);
265        self
266    }
267    pub fn optional_value(mut self) -> Self {
268        self.value = ValueMode::Optional;
269        self
270    }
271    pub fn value_name(mut self, name: impl Into<String>) -> Self {
272        self.value_name = name.into();
273        self
274    }
275    pub fn value_type(mut self, value_type: ValueType) -> Self {
276        self.value_type = value_type;
277        self
278    }
279    pub fn possible_value(mut self, value: impl Into<String>) -> Self {
280        self.possible_values.push(value.into());
281        self
282    }
283    pub fn about(mut self, about: impl Into<String>) -> Self {
284        self.about = about.into();
285        self
286    }
287    pub fn required(mut self, required: bool) -> Self {
288        self.required = required;
289        self
290    }
291    pub fn repeatable(mut self, repeatable: bool) -> Self {
292        self.repeatable = repeatable;
293        self
294    }
295    /// Allows an optional value to be supplied as the following argument.
296    ///
297    /// Optional values remain attached-only by default because a detached text
298    /// value may otherwise consume a positional argument. Prefer a bounded
299    /// [`ValueType`] such as [`ValueType::Bool`] when enabling this behavior.
300    pub fn detached_optional_value(mut self, enabled: bool) -> Self {
301        self.detached_optional_value = enabled;
302        self
303    }
304    pub fn terminal(mut self, terminal: bool) -> Self {
305        self.terminal = terminal;
306        self
307    }
308    pub fn hidden(mut self, hidden: bool) -> Self {
309        self.hidden = hidden;
310        self
311    }
312    pub fn conflicts_with(mut self, long: impl Into<String>) -> Self {
313        self.conflicts_with.push(long.into());
314        self
315    }
316    pub fn requires(mut self, long: impl Into<String>) -> Self {
317        self.requires.push(long.into());
318        self
319    }
320    /// Uses a declared environment variable when argv omits this option.
321    pub fn env(mut self, name: impl Into<String>) -> Self {
322        self.environment = Some(name.into());
323        self
324    }
325    /// Uses one fallback value after argv and environment resolution.
326    pub fn default_value(mut self, value: impl Into<std::ffi::OsString>) -> Self {
327        self.default = Some(value.into());
328        self
329    }
330    /// Selects additional native path or network-address validation.
331    pub fn value_kind(mut self, kind: crate::ValueKind) -> Self {
332        self.kind = kind;
333        self
334    }
335    /// Joins an at-most-one group. Requirements may reference the group name.
336    pub fn exclusive_group(mut self, name: impl Into<String>) -> Self {
337        self.exclusive_group = Some(name.into());
338        self
339    }
340    pub fn long(&self) -> &str {
341        &self.long
342    }
343    pub fn short_name(&self) -> Option<char> {
344        self.short
345    }
346    pub fn value_mode(&self) -> ValueMode {
347        self.value
348    }
349    pub fn value_name_text(&self) -> &str {
350        &self.value_name
351    }
352    pub fn value_type_kind(&self) -> ValueType {
353        self.value_type
354    }
355    pub fn possible_values(&self) -> &[String] {
356        &self.possible_values
357    }
358    pub fn about_text(&self) -> &str {
359        &self.about
360    }
361    pub fn is_required(&self) -> bool {
362        self.required
363    }
364    pub fn is_repeatable(&self) -> bool {
365        self.repeatable
366    }
367    pub fn accepts_detached_optional_value(&self) -> bool {
368        self.detached_optional_value
369    }
370    pub fn is_terminal(&self) -> bool {
371        self.terminal
372    }
373    pub fn is_hidden(&self) -> bool {
374        self.hidden
375    }
376    pub fn conflicts(&self) -> &[String] {
377        &self.conflicts_with
378    }
379    pub fn requirements(&self) -> &[String] {
380        &self.requires
381    }
382}
383
384impl ArgumentSpec {
385    pub fn new(name: impl Into<String>) -> Self {
386        Self {
387            name: name.into(),
388            about: String::new(),
389            value_type: ValueType::String,
390            possible_values: Vec::new(),
391            required: false,
392            multiple: false,
393            kind: crate::ValueKind::Text,
394        }
395    }
396    pub fn about(mut self, about: impl Into<String>) -> Self {
397        self.about = about.into();
398        self
399    }
400    pub fn value_type(mut self, value_type: ValueType) -> Self {
401        self.value_type = value_type;
402        self
403    }
404    pub fn possible_value(mut self, value: impl Into<String>) -> Self {
405        self.possible_values.push(value.into());
406        self
407    }
408    pub fn required(mut self, required: bool) -> Self {
409        self.required = required;
410        self
411    }
412    pub fn multiple(mut self, multiple: bool) -> Self {
413        self.multiple = multiple;
414        self
415    }
416    /// Selects native path or address validation for this positional argument.
417    pub fn value_kind(mut self, kind: crate::ValueKind) -> Self {
418        self.kind = kind;
419        self
420    }
421    pub fn name(&self) -> &str {
422        &self.name
423    }
424    pub fn about_text(&self) -> &str {
425        &self.about
426    }
427    pub fn value_type_kind(&self) -> ValueType {
428        self.value_type
429    }
430    pub fn possible_values(&self) -> &[String] {
431        &self.possible_values
432    }
433    pub fn is_required(&self) -> bool {
434        self.required
435    }
436    pub fn is_multiple(&self) -> bool {
437        self.multiple
438    }
439}
440
441impl SpecError {
442    fn new(message: impl Into<String>) -> Self {
443        Self {
444            message: message.into(),
445        }
446    }
447    pub(crate) fn new_internal(message: impl Into<String>) -> Self {
448        Self::new(message)
449    }
450}
451impl fmt::Display for SpecError {
452    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
453        f.write_str(&self.message)
454    }
455}
456impl std::error::Error for SpecError {}