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}