standout_input/sources/
stdin.rs1use std::sync::Arc;
4
5use clap::ArgMatches;
6
7use crate::collector::InputCollector;
8use crate::env::{DefaultStdin, StdinReader};
9use crate::InputError;
10
11#[derive(Clone)]
38pub struct StdinSource<R: StdinReader = DefaultStdin> {
39 reader: Arc<R>,
40 trim: bool,
41}
42
43impl StdinSource<DefaultStdin> {
44 pub fn new() -> Self {
52 Self {
53 reader: Arc::new(DefaultStdin),
54 trim: true,
55 }
56 }
57}
58
59impl Default for StdinSource<DefaultStdin> {
60 fn default() -> Self {
61 Self::new()
62 }
63}
64
65impl<R: StdinReader> StdinSource<R> {
66 pub fn with_reader(reader: R) -> Self {
70 Self {
71 reader: Arc::new(reader),
72 trim: true,
73 }
74 }
75
76 pub fn trim(mut self, trim: bool) -> Self {
80 self.trim = trim;
81 self
82 }
83}
84
85impl<R: StdinReader + 'static> InputCollector<String> for StdinSource<R> {
86 fn name(&self) -> &'static str {
87 "stdin"
88 }
89
90 fn is_available(&self, _matches: &ArgMatches) -> bool {
91 !self.reader.is_terminal()
93 }
94
95 fn collect(&self, _matches: &ArgMatches) -> Result<Option<String>, InputError> {
96 if self.reader.is_terminal() {
97 return Ok(None);
98 }
99
100 let content = self
101 .reader
102 .read_to_string()
103 .map_err(InputError::StdinFailed)?;
104
105 if content.is_empty() {
106 return Ok(None);
107 }
108
109 let result = if self.trim {
110 content.trim().to_string()
111 } else {
112 content
113 };
114
115 if result.is_empty() {
116 Ok(None)
117 } else {
118 Ok(Some(result))
119 }
120 }
121}
122
123pub fn read_if_piped() -> Result<Option<String>, InputError> {
129 let reader = DefaultStdin;
130 if reader.is_terminal() {
131 return Ok(None);
132 }
133
134 let content = reader.read_to_string().map_err(InputError::StdinFailed)?;
135
136 if content.trim().is_empty() {
137 Ok(None)
138 } else {
139 Ok(Some(content.trim().to_string()))
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146 use crate::env::MockStdin;
147 use clap::Command;
148
149 fn empty_matches() -> ArgMatches {
150 Command::new("test").try_get_matches_from(["test"]).unwrap()
151 }
152
153 #[test]
154 fn stdin_available_when_piped() {
155 let source = StdinSource::with_reader(MockStdin::piped("content"));
156 assert!(source.is_available(&empty_matches()));
157 }
158
159 #[test]
160 fn stdin_unavailable_when_terminal() {
161 let source = StdinSource::with_reader(MockStdin::terminal());
162 assert!(!source.is_available(&empty_matches()));
163 }
164
165 #[test]
166 fn stdin_reads_piped_content() {
167 let source = StdinSource::with_reader(MockStdin::piped("hello world"));
168 let result = source.collect(&empty_matches()).unwrap();
169 assert_eq!(result, Some("hello world".to_string()));
170 }
171
172 #[test]
173 fn stdin_trims_whitespace() {
174 let source = StdinSource::with_reader(MockStdin::piped(" hello \n"));
175 let result = source.collect(&empty_matches()).unwrap();
176 assert_eq!(result, Some("hello".to_string()));
177 }
178
179 #[test]
180 fn stdin_no_trim() {
181 let source = StdinSource::with_reader(MockStdin::piped(" hello \n")).trim(false);
182 let result = source.collect(&empty_matches()).unwrap();
183 assert_eq!(result, Some(" hello \n".to_string()));
184 }
185
186 #[test]
187 fn stdin_returns_none_for_empty() {
188 let source = StdinSource::with_reader(MockStdin::piped_empty());
189 let result = source.collect(&empty_matches()).unwrap();
190 assert_eq!(result, None);
191 }
192
193 #[test]
194 fn stdin_returns_none_for_whitespace_only() {
195 let source = StdinSource::with_reader(MockStdin::piped(" \n\t "));
196 let result = source.collect(&empty_matches()).unwrap();
197 assert_eq!(result, None);
198 }
199
200 #[test]
201 fn stdin_returns_none_when_terminal() {
202 let source = StdinSource::with_reader(MockStdin::terminal());
203 let result = source.collect(&empty_matches()).unwrap();
204 assert_eq!(result, None);
205 }
206}