Skip to main content

standout_input/
input_sources.rs

1use std::fmt;
2use std::sync::Arc;
3
4use crate::env::{ClipboardReader, RealClipboard, RealStdin, StdinReader};
5use crate::responder::PromptResponder;
6
7#[derive(Clone)]
8pub struct InputSources {
9    stdin: Arc<dyn StdinReader>,
10    clipboard: Arc<dyn ClipboardReader>,
11    responder: Option<Arc<dyn PromptResponder>>,
12}
13
14impl fmt::Debug for InputSources {
15    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
16        f.debug_struct("InputSources")
17            .field("has_responder", &self.responder.is_some())
18            .finish_non_exhaustive()
19    }
20}
21
22impl InputSources {
23    pub fn new(
24        stdin: impl StdinReader + 'static,
25        clipboard: impl ClipboardReader + 'static,
26        responder: Option<Arc<dyn PromptResponder>>,
27    ) -> Self {
28        Self {
29            stdin: Arc::new(stdin),
30            clipboard: Arc::new(clipboard),
31            responder,
32        }
33    }
34
35    pub fn from_process() -> Self {
36        Self::new(RealStdin, RealClipboard, None)
37    }
38
39    pub fn with_stdin(mut self, stdin: impl StdinReader + 'static) -> Self {
40        self.stdin = Arc::new(stdin);
41        self
42    }
43
44    pub fn with_clipboard(mut self, clipboard: impl ClipboardReader + 'static) -> Self {
45        self.clipboard = Arc::new(clipboard);
46        self
47    }
48
49    pub fn with_responder(mut self, responder: Arc<dyn PromptResponder>) -> Self {
50        self.responder = Some(responder);
51        self
52    }
53
54    pub fn stdin(&self) -> &dyn StdinReader {
55        self.stdin.as_ref()
56    }
57
58    pub fn stdin_arc(&self) -> Arc<dyn StdinReader> {
59        Arc::clone(&self.stdin)
60    }
61
62    pub fn clipboard(&self) -> &dyn ClipboardReader {
63        self.clipboard.as_ref()
64    }
65
66    pub fn clipboard_arc(&self) -> Arc<dyn ClipboardReader> {
67        Arc::clone(&self.clipboard)
68    }
69
70    pub fn responder(&self) -> Option<&dyn PromptResponder> {
71        self.responder.as_deref()
72    }
73
74    pub fn responder_arc(&self) -> Option<Arc<dyn PromptResponder>> {
75        self.responder.clone()
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82    use crate::env::{MockClipboard, MockStdin};
83
84    fn sample() -> InputSources {
85        InputSources::new(MockStdin::piped("hello"), MockClipboard::empty(), None)
86    }
87
88    #[test]
89    fn input_sources_constructs_from_explicit_readers() {
90        let sources = sample();
91        assert!(!sources.stdin().is_terminal());
92        assert_eq!(sources.stdin().read_to_string().unwrap(), "hello");
93        assert_eq!(sources.clipboard().read().unwrap(), None);
94        assert!(sources.responder().is_none());
95    }
96
97    #[test]
98    fn input_sources_is_not_copy() {
99        struct Probe<U>(std::marker::PhantomData<U>);
100        trait AmbiguousIfImpl<A> {
101            fn check() {}
102        }
103        impl<U> AmbiguousIfImpl<()> for Probe<U> {}
104        impl<U: Copy> AmbiguousIfImpl<u8> for Probe<U> {}
105        let _ = <Probe<InputSources> as AmbiguousIfImpl<_>>::check;
106
107        let sources = sample();
108        let moved = sources;
109        assert!(!moved.stdin().is_terminal());
110    }
111
112    #[test]
113    fn input_sources_is_clone() {
114        let sources = sample();
115        let cloned = sources.clone();
116        assert_eq!(cloned.stdin().read_to_string().unwrap(), "hello");
117        assert_eq!(sources.stdin().read_to_string().unwrap(), "hello");
118    }
119
120    #[test]
121    fn input_sources_debug_is_structural() {
122        let sources = sample();
123        let debug = format!("{sources:?}");
124        assert!(debug.contains("InputSources"));
125        assert!(debug.contains("has_responder: false"));
126    }
127
128    #[test]
129    fn from_process_constructs_production_sources() {
130        let sources = InputSources::from_process();
131        let _ = sources.stdin().is_terminal();
132        assert!(sources.responder().is_none());
133    }
134
135    #[test]
136    fn builders_replace_readers() {
137        let sources = InputSources::from_process()
138            .with_stdin(MockStdin::piped("in"))
139            .with_clipboard(MockClipboard::with_content("clip"));
140        assert_eq!(sources.stdin().read_to_string().unwrap(), "in");
141        assert_eq!(
142            sources.clipboard().read().unwrap(),
143            Some("clip".to_string())
144        );
145    }
146}