standout_input/sources/
stdin.rs1use std::sync::Arc;
2
3use clap::ArgMatches;
4
5use crate::collector::InputCollector;
6use crate::env::{RealStdin, StdinReader};
7use crate::InputError;
8use crate::InputSources;
9
10#[derive(Clone)]
11pub struct StdinSource {
12 reader: Option<Arc<dyn StdinReader>>,
13 trim: bool,
14}
15
16impl StdinSource {
17 pub fn new() -> Self {
18 Self {
19 reader: None,
20 trim: true,
21 }
22 }
23
24 pub fn with_reader(reader: impl StdinReader + 'static) -> Self {
25 Self {
26 reader: Some(Arc::new(reader)),
27 trim: true,
28 }
29 }
30
31 pub fn with_shared_reader(reader: Arc<dyn StdinReader>) -> Self {
32 Self {
33 reader: Some(reader),
34 trim: true,
35 }
36 }
37
38 pub fn trim(mut self, trim: bool) -> Self {
39 self.trim = trim;
40 self
41 }
42
43 fn reader(&self) -> &dyn StdinReader {
44 self.reader
45 .as_deref()
46 .unwrap_or(&RealStdin as &dyn StdinReader)
47 }
48}
49
50impl Default for StdinSource {
51 fn default() -> Self {
52 Self::new()
53 }
54}
55
56impl InputCollector<String> for StdinSource {
57 fn name(&self) -> &'static str {
58 "stdin"
59 }
60
61 fn is_available(&self, _matches: &ArgMatches) -> bool {
62 !self.reader().is_terminal()
63 }
64
65 fn collect(&self, _matches: &ArgMatches) -> Result<Option<String>, InputError> {
66 if self.reader().is_terminal() {
67 return Ok(None);
68 }
69
70 let content = self
71 .reader()
72 .read_to_string()
73 .map_err(InputError::StdinFailed)?;
74
75 if content.is_empty() {
76 return Ok(None);
77 }
78
79 let result = if self.trim {
80 content.trim().to_string()
81 } else {
82 content
83 };
84
85 if result.is_empty() {
86 Ok(None)
87 } else {
88 Ok(Some(result))
89 }
90 }
91
92 fn bind_sources(&self, sources: &InputSources) -> Option<Box<dyn InputCollector<String>>> {
93 if self.reader.is_some() {
94 return None;
95 }
96 Some(Box::new(Self {
97 reader: Some(sources.stdin_arc()),
98 trim: self.trim,
99 }))
100 }
101}
102
103pub fn read_if_piped() -> Result<Option<String>, InputError> {
104 read_if_piped_from(&InputSources::from_process())
105}
106
107pub fn read_if_piped_from(sources: &InputSources) -> Result<Option<String>, InputError> {
108 let reader = sources.stdin();
109 if reader.is_terminal() {
110 return Ok(None);
111 }
112
113 let content = reader.read_to_string().map_err(InputError::StdinFailed)?;
114
115 if content.trim().is_empty() {
116 Ok(None)
117 } else {
118 Ok(Some(content.trim().to_string()))
119 }
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125 use crate::env::MockStdin;
126 use clap::Command;
127
128 fn empty_matches() -> ArgMatches {
129 Command::new("test").try_get_matches_from(["test"]).unwrap()
130 }
131
132 #[test]
133 fn stdin_available_when_piped() {
134 let source = StdinSource::with_reader(MockStdin::piped("content"));
135 assert!(source.is_available(&empty_matches()));
136 }
137
138 #[test]
139 fn stdin_unavailable_when_terminal() {
140 let source = StdinSource::with_reader(MockStdin::terminal());
141 assert!(!source.is_available(&empty_matches()));
142 }
143
144 #[test]
145 fn stdin_reads_piped_content() {
146 let source = StdinSource::with_reader(MockStdin::piped("hello world"));
147 let result = source.collect(&empty_matches()).unwrap();
148 assert_eq!(result, Some("hello world".to_string()));
149 }
150
151 #[test]
152 fn stdin_trims_whitespace() {
153 let source = StdinSource::with_reader(MockStdin::piped(" hello \n"));
154 let result = source.collect(&empty_matches()).unwrap();
155 assert_eq!(result, Some("hello".to_string()));
156 }
157
158 #[test]
159 fn stdin_no_trim() {
160 let source = StdinSource::with_reader(MockStdin::piped(" hello \n")).trim(false);
161 let result = source.collect(&empty_matches()).unwrap();
162 assert_eq!(result, Some(" hello \n".to_string()));
163 }
164
165 #[test]
166 fn stdin_returns_none_for_empty() {
167 let source = StdinSource::with_reader(MockStdin::piped_empty());
168 let result = source.collect(&empty_matches()).unwrap();
169 assert_eq!(result, None);
170 }
171
172 #[test]
173 fn stdin_returns_none_for_whitespace_only() {
174 let source = StdinSource::with_reader(MockStdin::piped(" \n\t "));
175 let result = source.collect(&empty_matches()).unwrap();
176 assert_eq!(result, None);
177 }
178
179 #[test]
180 fn stdin_returns_none_when_terminal() {
181 let source = StdinSource::with_reader(MockStdin::terminal());
182 let result = source.collect(&empty_matches()).unwrap();
183 assert_eq!(result, None);
184 }
185
186 #[test]
187 fn bind_sources_uses_invocation_stdin() {
188 let source = StdinSource::new();
189 let sources = InputSources::from_process().with_stdin(MockStdin::piped("bound"));
190 let bound = source.bind_sources(&sources).expect("unbound source binds");
191 let result = bound.collect(&empty_matches()).unwrap();
192 assert_eq!(result, Some("bound".to_string()));
193 }
194
195 #[test]
196 fn bind_sources_keeps_explicit_reader() {
197 let source = StdinSource::with_reader(MockStdin::piped("explicit"));
198 let sources = InputSources::from_process().with_stdin(MockStdin::piped("ignored"));
199 assert!(source.bind_sources(&sources).is_none());
200 assert_eq!(
201 source.collect(&empty_matches()).unwrap(),
202 Some("explicit".to_string())
203 );
204 }
205}