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