standout_input/collector.rs
1//! Core input collector trait.
2//!
3//! The [`InputCollector`] trait defines the interface for all input sources.
4//! Implementations can be composed into chains with fallback behavior.
5
6use clap::ArgMatches;
7
8use crate::InputError;
9
10/// A source that can collect input of type T.
11///
12/// Input collectors are the building blocks of input chains. Each collector
13/// represents one way to obtain input: from CLI arguments, stdin, environment
14/// variables, editors, or interactive prompts.
15///
16/// # Implementation Guidelines
17///
18/// - [`is_available`](Self::is_available) should return `false` if this source
19/// cannot provide input in the current environment (e.g., no TTY for prompts,
20/// stdin not piped for stdin source).
21///
22/// - [`collect`](Self::collect) should return `Ok(None)` to indicate "try the
23/// next source" and `Ok(Some(value))` when input was successfully collected.
24/// Return `Err` only for actual failures.
25///
26/// - Interactive collectors should implement [`can_retry`](Self::can_retry) to
27/// return `true`, allowing validation failures to re-prompt the user.
28///
29/// # Example
30///
31/// ```ignore
32/// use standout_input::{InputCollector, InputError};
33/// use clap::ArgMatches;
34///
35/// struct FixedValue(String);
36///
37/// impl InputCollector<String> for FixedValue {
38/// fn name(&self) -> &'static str { "fixed" }
39///
40/// fn is_available(&self, _: &ArgMatches) -> bool { true }
41///
42/// fn collect(&self, _: &ArgMatches) -> Result<Option<String>, InputError> {
43/// Ok(Some(self.0.clone()))
44/// }
45/// }
46/// ```
47pub trait InputCollector<T>: Send + Sync {
48 /// Human-readable name for this collector.
49 ///
50 /// Used in error messages and debugging. Examples: "argument", "stdin",
51 /// "editor", "prompt".
52 fn name(&self) -> &'static str;
53
54 /// Check if this collector can provide input in the current environment.
55 ///
56 /// Returns `false` if:
57 /// - Interactive collector but no TTY available
58 /// - Stdin source but stdin is not piped
59 /// - Argument source but argument was not provided
60 ///
61 /// The chain will skip unavailable collectors and try the next one.
62 fn is_available(&self, matches: &ArgMatches) -> bool;
63
64 /// Attempt to collect input from this source.
65 ///
66 /// # Returns
67 ///
68 /// - `Ok(Some(value))` - Input was successfully collected
69 /// - `Ok(None)` - This source has no input; try the next one in the chain
70 /// - `Err(e)` - Collection failed; abort the chain with this error
71 fn collect(&self, matches: &ArgMatches) -> Result<Option<T>, InputError>;
72
73 /// Validate the collected value.
74 ///
75 /// Called after successful collection. Override to add source-specific
76 /// validation that can trigger re-prompting for interactive sources.
77 ///
78 /// Default implementation accepts all values.
79 fn validate(&self, _value: &T) -> Result<(), String> {
80 Ok(())
81 }
82
83 /// Whether this collector supports retry on validation failure.
84 ///
85 /// Interactive collectors (prompts, editor) should return `true` to allow
86 /// re-prompting when validation fails. Non-interactive sources (args,
87 /// stdin) should return `false`.
88 ///
89 /// Default is `false`.
90 fn can_retry(&self) -> bool {
91 false
92 }
93}
94
95/// Returns a process-wide static [`ArgMatches`] with no arguments.
96///
97/// Used by the `.prompt()` shortcuts on interactive sources, which need to
98/// satisfy `InputCollector::collect`'s `&ArgMatches` parameter even when no
99/// CLI parser is involved (the typical case for wizard/REPL flows that own
100/// their own driver). Initialized lazily on first call and reused thereafter.
101#[cfg(any(feature = "editor", feature = "simple-prompts", feature = "inquire"))]
102pub(crate) fn empty_matches() -> &'static ArgMatches {
103 use std::sync::OnceLock;
104 static MATCHES: OnceLock<ArgMatches> = OnceLock::new();
105 MATCHES.get_or_init(|| {
106 clap::Command::new("__standout_input_prompt__")
107 .no_binary_name(true)
108 .try_get_matches_from(std::iter::empty::<&str>())
109 .expect("empty command always parses with no args")
110 })
111}
112
113/// Information about how input was resolved.
114#[derive(Debug, Clone, PartialEq, Eq)]
115pub struct ResolvedInput<T> {
116 /// The resolved value.
117 pub value: T,
118 /// Which source provided the value.
119 pub source: InputSourceKind,
120}
121
122/// The kind of source that provided input.
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124pub enum InputSourceKind {
125 /// From a CLI argument.
126 Arg,
127 /// From a CLI flag.
128 Flag,
129 /// From piped stdin.
130 Stdin,
131 /// From an environment variable.
132 Env,
133 /// From the system clipboard.
134 Clipboard,
135 /// From an external editor.
136 Editor,
137 /// From an interactive prompt.
138 Prompt,
139 /// From a default value.
140 Default,
141}
142
143impl std::fmt::Display for InputSourceKind {
144 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
145 match self {
146 Self::Arg => write!(f, "argument"),
147 Self::Flag => write!(f, "flag"),
148 Self::Stdin => write!(f, "stdin"),
149 Self::Env => write!(f, "environment variable"),
150 Self::Clipboard => write!(f, "clipboard"),
151 Self::Editor => write!(f, "editor"),
152 Self::Prompt => write!(f, "prompt"),
153 Self::Default => write!(f, "default"),
154 }
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161
162 #[test]
163 fn source_kind_display() {
164 assert_eq!(InputSourceKind::Arg.to_string(), "argument");
165 assert_eq!(InputSourceKind::Stdin.to_string(), "stdin");
166 assert_eq!(InputSourceKind::Editor.to_string(), "editor");
167 }
168}