Skip to main content

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}