Skip to main content

standout_input/
inputs.rs

1use std::any::{Any, TypeId};
2use std::borrow::Cow;
3use std::collections::HashMap;
4use std::fmt;
5
6use crate::collector::{InputSourceKind, ResolvedInput};
7
8#[derive(Default)]
9pub struct Inputs {
10    entries: HashMap<Cow<'static, str>, Entry>,
11}
12
13struct Entry {
14    type_id: TypeId,
15    type_name: &'static str,
16    source: InputSourceKind,
17    value: Box<dyn Any + Send + Sync>,
18}
19
20impl Inputs {
21    pub fn new() -> Self {
22        Self {
23            entries: HashMap::new(),
24        }
25    }
26
27    pub fn insert<T>(
28        &mut self,
29        name: impl Into<Cow<'static, str>>,
30        resolved: ResolvedInput<T>,
31    ) -> Option<InputSourceKind>
32    where
33        T: Send + Sync + 'static,
34    {
35        let prev = self.entries.insert(
36            name.into(),
37            Entry {
38                type_id: TypeId::of::<T>(),
39                type_name: std::any::type_name::<T>(),
40                source: resolved.source,
41                value: Box::new(resolved.value),
42            },
43        );
44        prev.map(|e| e.source)
45    }
46
47    pub fn get<T: 'static>(&self, name: &str) -> Option<&T> {
48        let entry = self.entries.get(name)?;
49        if entry.type_id != TypeId::of::<T>() {
50            return None;
51        }
52        entry.value.downcast_ref::<T>()
53    }
54
55    pub fn get_required<T: 'static>(&self, name: &str) -> Result<&T, MissingInput> {
56        let Some(entry) = self.entries.get(name) else {
57            return Err(MissingInput::NotRegistered {
58                name: name.to_string(),
59            });
60        };
61        if entry.type_id != TypeId::of::<T>() {
62            return Err(MissingInput::TypeMismatch {
63                name: name.to_string(),
64                expected: std::any::type_name::<T>(),
65                actual: entry.type_name,
66            });
67        }
68        entry
69            .value
70            .downcast_ref::<T>()
71            .ok_or_else(|| MissingInput::TypeMismatch {
72                name: name.to_string(),
73                expected: std::any::type_name::<T>(),
74                actual: entry.type_name,
75            })
76    }
77
78    pub fn source_of(&self, name: &str) -> Option<InputSourceKind> {
79        self.entries.get(name).map(|e| e.source)
80    }
81
82    pub fn contains(&self, name: &str) -> bool {
83        self.entries.contains_key(name)
84    }
85
86    pub fn len(&self) -> usize {
87        self.entries.len()
88    }
89
90    pub fn is_empty(&self) -> bool {
91        self.entries.is_empty()
92    }
93
94    pub fn iter_sources(&self) -> impl Iterator<Item = (&str, InputSourceKind)> + '_ {
95        self.entries
96            .iter()
97            .map(|(name, entry)| (name.as_ref(), entry.source))
98    }
99}
100
101impl fmt::Debug for Inputs {
102    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103        let mut s = f.debug_struct("Inputs");
104        for (name, entry) in &self.entries {
105            s.field(
106                name.as_ref(),
107                &format_args!("{} from {}", entry.type_name, entry.source),
108            );
109        }
110        s.finish()
111    }
112}
113
114#[derive(Debug, thiserror::Error)]
115pub enum MissingInput {
116    #[error("no input named `{name}` was registered for this command")]
117    NotRegistered { name: String },
118    #[error("input `{name}` is registered as `{actual}`, not `{expected}`")]
119    TypeMismatch {
120        name: String,
121        expected: &'static str,
122        actual: &'static str,
123    },
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    fn arg<T>(value: T) -> ResolvedInput<T> {
131        ResolvedInput {
132            value,
133            source: InputSourceKind::Arg,
134        }
135    }
136
137    #[test]
138    fn insert_and_get() {
139        let mut inputs = Inputs::new();
140        inputs.insert("body", arg("hello".to_string()));
141
142        let body: &String = inputs.get("body").unwrap();
143        assert_eq!(body, "hello");
144    }
145
146    #[test]
147    fn get_missing_returns_none() {
148        let inputs = Inputs::new();
149        assert!(inputs.get::<String>("missing").is_none());
150    }
151
152    #[test]
153    fn get_wrong_type_returns_none() {
154        let mut inputs = Inputs::new();
155        inputs.insert("body", arg("hello".to_string()));
156        assert!(inputs.get::<u32>("body").is_none());
157    }
158
159    #[test]
160    fn get_required_reports_missing() {
161        let inputs = Inputs::new();
162        let err = inputs.get_required::<String>("body").unwrap_err();
163        assert!(matches!(err, MissingInput::NotRegistered { .. }));
164        assert!(err.to_string().contains("body"));
165    }
166
167    #[test]
168    fn get_required_reports_type_mismatch() {
169        let mut inputs = Inputs::new();
170        inputs.insert("body", arg("hello".to_string()));
171        let err = inputs.get_required::<u32>("body").unwrap_err();
172        match err {
173            MissingInput::TypeMismatch {
174                ref name,
175                expected,
176                actual,
177            } => {
178                assert_eq!(name, "body");
179                assert!(expected.contains("u32"));
180                assert!(actual.contains("String"));
181            }
182            other => panic!("expected TypeMismatch, got {:?}", other),
183        }
184    }
185
186    #[test]
187    fn accepts_owned_string_name() {
188        let mut inputs = Inputs::new();
189        let runtime_name: String = format!("input_{}", 42);
190        inputs.insert(runtime_name.clone(), arg("x".to_string()));
191
192        assert_eq!(inputs.get::<String>(runtime_name.as_str()).unwrap(), "x");
193    }
194
195    #[test]
196    fn two_inputs_of_same_type_do_not_collide() {
197        let mut inputs = Inputs::new();
198        inputs.insert("body", arg("the body".to_string()));
199        inputs.insert("title", arg("the title".to_string()));
200
201        assert_eq!(inputs.get::<String>("body").unwrap(), "the body");
202        assert_eq!(inputs.get::<String>("title").unwrap(), "the title");
203    }
204
205    #[test]
206    fn insert_returns_previous_source() {
207        let mut inputs = Inputs::new();
208        assert!(inputs.insert("body", arg("first".to_string())).is_none());
209        let prev = inputs.insert(
210            "body",
211            ResolvedInput {
212                value: "second".to_string(),
213                source: InputSourceKind::Stdin,
214            },
215        );
216        assert_eq!(prev, Some(InputSourceKind::Arg));
217        assert_eq!(inputs.source_of("body"), Some(InputSourceKind::Stdin));
218    }
219
220    #[test]
221    fn source_of_and_contains() {
222        let mut inputs = Inputs::new();
223        assert!(!inputs.contains("body"));
224        inputs.insert("body", arg("x".to_string()));
225        assert!(inputs.contains("body"));
226        assert_eq!(inputs.source_of("body"), Some(InputSourceKind::Arg));
227        assert_eq!(inputs.source_of("missing"), None);
228    }
229
230    #[test]
231    fn iter_sources_yields_all_entries() {
232        let mut inputs = Inputs::new();
233        inputs.insert("body", arg("x".to_string()));
234        inputs.insert(
235            "yes",
236            ResolvedInput {
237                value: true,
238                source: InputSourceKind::Flag,
239            },
240        );
241
242        let mut pairs: Vec<_> = inputs.iter_sources().collect();
243        pairs.sort_by_key(|(name, _)| *name);
244        assert_eq!(
245            pairs,
246            vec![
247                ("body", InputSourceKind::Arg),
248                ("yes", InputSourceKind::Flag)
249            ]
250        );
251    }
252
253    #[test]
254    fn len_and_is_empty() {
255        let mut inputs = Inputs::new();
256        assert!(inputs.is_empty());
257        assert_eq!(inputs.len(), 0);
258        inputs.insert("body", arg("x".to_string()));
259        assert!(!inputs.is_empty());
260        assert_eq!(inputs.len(), 1);
261    }
262}