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/// Information about how input was resolved.
96#[derive(Debug, Clone, PartialEq, Eq)]
97pub struct ResolvedInput<T> {
98    /// The resolved value.
99    pub value: T,
100    /// Which source provided the value.
101    pub source: InputSourceKind,
102}
103
104/// The kind of source that provided input.
105#[derive(Debug, Clone, Copy, PartialEq, Eq)]
106pub enum InputSourceKind {
107    /// From a CLI argument.
108    Arg,
109    /// From a CLI flag.
110    Flag,
111    /// From piped stdin.
112    Stdin,
113    /// From an environment variable.
114    Env,
115    /// From the system clipboard.
116    Clipboard,
117    /// From an external editor.
118    Editor,
119    /// From an interactive prompt.
120    Prompt,
121    /// From a default value.
122    Default,
123}
124
125impl std::fmt::Display for InputSourceKind {
126    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127        match self {
128            Self::Arg => write!(f, "argument"),
129            Self::Flag => write!(f, "flag"),
130            Self::Stdin => write!(f, "stdin"),
131            Self::Env => write!(f, "environment variable"),
132            Self::Clipboard => write!(f, "clipboard"),
133            Self::Editor => write!(f, "editor"),
134            Self::Prompt => write!(f, "prompt"),
135            Self::Default => write!(f, "default"),
136        }
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    #[test]
145    fn source_kind_display() {
146        assert_eq!(InputSourceKind::Arg.to_string(), "argument");
147        assert_eq!(InputSourceKind::Stdin.to_string(), "stdin");
148        assert_eq!(InputSourceKind::Editor.to_string(), "editor");
149    }
150}