grab/
builder.rs

1//! This module contains [Config] and its sister [Builder] which are used for converting input
2//! into a concrete [Input] kind which can be then be accessed and read from. The two main methods
3//! for this conversion are [Config::parse] and [Config::parse_os], which take as some hopefully
4//! parsable input, like say `@/some/file/path` for reading from a file or `-` for reading from
5//! [Stdin](std::io::Stdin).
6
7use crate::{
8    error::input::InputError,
9    input::Input,
10    parsers::{File, InputType, Parser, Stdin, Text, WeightedParser as WP},
11};
12
13use std::{ffi::OsStr, fmt};
14
15/// Represents a set of parsers that will be called in ascending order according to their weight
16/// until the list is exhausted or a parser returns successfully.
17///
18/// Typically, you can construct one via a [Builder], however if you aren't interested in
19/// customizing your parser config, you can also use [Config::default].
20#[derive(Clone)]
21pub struct Config {
22    inner: Builder,
23}
24
25impl Config {
26    /// Attempt to parse the input into a concrete handle which can be [accessed](Input::access)
27    pub fn parse(&self, input: &str) -> Result<Input, InputError> {
28        self.parse_str(input).map(Input::from_input_type)
29    }
30
31    /// Attempt to parse the given [OsStr] into a concrete handle which can be
32    /// [accessed](Input::access).
33    pub fn parse_os(&self, input: &OsStr) -> Result<Input, InputError> {
34        self.parse_os_str(input).map(Input::from_input_type)
35    }
36
37    /// Generates a list of parsers from the available, sorts them by weight,
38    /// then applies the given closure to the sorted list
39    fn with_parsers<F, R>(&self, f: F) -> R
40    where
41        F: FnMut(&[Option<&dyn WP>]) -> R,
42    {
43        let b = &self.inner;
44        let mut callback = f;
45
46        let mut list = [
47            b.file.as_ref().map(|p| p as &dyn WP),
48            b.stdin.as_ref().map(|p| p as &dyn WP),
49            b.text.as_ref().map(|p| p as &dyn WP),
50        ];
51
52        // Sort parsers by weight, with lower numbers taking
53        // priority.
54        list.sort_by_key(|opt| opt.map(|p| p.weight()));
55
56        callback(&list)
57    }
58
59    /// Iterates over the given list of parsers, trying the given closure on each
60    /// and returning the first success.
61    ///
62    /// Notably, this function _does not_ provide the input on which a parser
63    /// operates, this should be pulled in by the closure.
64    fn apply<'a, F, I>(&self, parsers: I, mut f: F) -> Result<InputType, InputError>
65    where
66        F: FnMut(&dyn WP) -> Result<InputType, InputError>,
67        I: IntoIterator<Item = &'a dyn WP>,
68    {
69        let mut error: Option<InputError> = None;
70
71        for parser in parsers {
72            match f(parser) {
73                Ok(success) => return Ok(success),
74                Err(e) => match error {
75                    Some(ref mut prev) => {
76                        prev.extend(e);
77                    }
78                    None => error = Some(e),
79                },
80            }
81        }
82
83        Err(error.expect("Config should never have less than one parser, this is a bug"))
84    }
85}
86
87impl Parser for Config {
88    fn parse_str(&self, input: &str) -> Result<InputType, InputError> {
89        self.with_parsers(|parsers| {
90            let iter = parsers.iter().filter_map(|o| *o);
91            self.apply(iter, |p| p.parse_str(input))
92        })
93    }
94
95    fn parse_os_str(&self, input: &OsStr) -> Result<InputType, InputError> {
96        self.with_parsers(|parsers| {
97            let iter = parsers.iter().filter_map(|o| *o);
98            self.apply(iter, |p| p.parse_os_str(input))
99        })
100    }
101
102    fn parse_bytes(&self, input: &[u8]) -> Result<InputType, InputError> {
103        self.with_parsers(|parsers| {
104            let iter = parsers.iter().filter_map(|o| *o);
105            self.apply(iter, |p| p.parse_bytes(input))
106        })
107    }
108}
109
110impl fmt::Debug for Config {
111    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112        let mut dbg = f.debug_struct("Config");
113
114        if let Some(text) = &self.inner.text {
115            dbg.field("text", &text);
116        }
117
118        if let Some(stdin) = &self.inner.stdin {
119            dbg.field("stdin", &stdin);
120        }
121
122        if let Some(file) = &self.inner.file {
123            dbg.field("file", &file);
124        }
125
126        dbg.finish()
127    }
128}
129
130impl Default for Config {
131    fn default() -> Self {
132        let cfg = Builder::new().with(|b| b.text().stdin().file());
133
134        debug_assert!(cfg.is_valid());
135
136        Self { inner: cfg }
137    }
138}
139
140/// A [Config] builder, you can use this struct to customize which parsers are available to be called
141/// when attempting to parse input.
142///
143/// If you just want the default configuration, use [Config::default] and skip this struct
144/// completely.
145#[derive(Debug, Clone, Default)]
146pub struct Builder {
147    stdin: Option<Stdin>,
148    file: Option<File>,
149    text: Option<Text>,
150}
151
152impl Builder {
153    /// Create a new, empty config builder
154    pub fn new() -> Self {
155        Self::default()
156    }
157
158    /// Convenience function for applying configuration options
159    pub fn with<F>(self, f: F) -> Self
160    where
161        F: FnMut(&mut Self) -> &mut Self,
162    {
163        let mut this = self;
164        let mut actions = f;
165
166        actions(&mut this);
167
168        this
169    }
170
171    /// Consume this builder returning a [Config] that can be
172    /// used for parsing.
173    ///
174    /// # Panics
175    ///
176    /// Panics if there are no parsers set
177    pub fn build(self) -> Config {
178        assert!(
179            self.is_valid(),
180            "A grab::Builder must contain at least one parser"
181        );
182
183        Config { inner: self }
184    }
185
186    /// Attempt to create a [Config] from the given parser,
187    /// if the current configuration is valid, returning the
188    /// builder otherwise.
189    ///
190    /// This is the safe variant of [build][Builder::build]
191    pub fn try_build(self) -> Result<Config, Self> {
192        if self.is_valid() {
193            return Ok(Config { inner: self });
194        }
195
196        Err(self)
197    }
198
199    /// Enable [text](Text) parsing, with the default parser
200    pub fn text(&mut self) -> &mut Self {
201        self.with_text(Text::new())
202    }
203
204    /// Enable [text](Text) parsing, with the given parser
205    pub fn with_text(&mut self, t: Text) -> &mut Self {
206        self.text = Some(t);
207
208        self
209    }
210
211    /// Enable [stdin](Stdin) parsing with the default parser
212    pub fn stdin(&mut self) -> &mut Self {
213        self.with_stdin(Stdin::new())
214    }
215
216    /// Enable [stdin](Stdin) parsing, using the given parser
217    pub fn with_stdin(&mut self, s: Stdin) -> &mut Self {
218        self.stdin = Some(s);
219
220        self
221    }
222
223    /// Enable [file path](File) parsing with the default parser
224    pub fn file(&mut self) -> &mut Self {
225        self.with_file(File::new())
226    }
227
228    /// Enable [file path](File) parsing, using the given parser
229    pub fn with_file(&mut self, f: File) -> &mut Self {
230        self.file = Some(f);
231
232        self
233    }
234
235    /// Checks if you can successfully convert into a [Config]
236    pub fn is_valid(&self) -> bool {
237        let b = self;
238
239        b.text.is_some() || b.stdin.is_some() || b.file.is_some()
240    }
241}
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246
247    #[test]
248    fn config_default_is_valid() {
249        let _cfg = Config::default();
250    }
251
252    #[test]
253    fn builder_set_text() {
254        let b = Builder::new().with(|this| this.text());
255
256        assert!(b.text.is_some())
257    }
258
259    #[test]
260    fn builder_set_file() {
261        let b = Builder::new().with(|this| this.file());
262
263        assert!(b.file.is_some())
264    }
265
266    #[test]
267    fn builder_set_stdin() {
268        let b = Builder::new().with(|this| this.stdin());
269
270        assert!(b.stdin.is_some())
271    }
272
273    #[test]
274    fn sorted_by_weight_ascending() {
275        let cfg = Config::default();
276
277        let mut last = 0;
278        cfg.with_parsers(|list| {
279            // TODO: replace with list.is_sorted_by(|wp| wp.weight()) when method is stabilized
280            for wp in list.iter().filter_map(|o| *o) {
281                let weight = wp.weight();
282
283                assert!(weight >= last);
284
285                last = weight;
286            }
287        })
288    }
289
290    #[test]
291    fn config_default_parse_stdin() {
292        let input = "-";
293        let cfg = Config::default();
294
295        let t = cfg.parse_str(input).expect("a successful parse");
296
297        match t {
298            InputType::Stdin => {}
299            bad => panic!("expected Stdin, got: {:?}", bad),
300        }
301    }
302
303    #[test]
304    fn config_default_parse_file() {
305        let input = "@some/relative/path";
306        let cfg = Config::default();
307
308        let t = cfg.parse_str(input).expect("a successful parse");
309
310        match t {
311            InputType::File(_) => {}
312            bad => panic!("expected File, got: {:?}", bad),
313        }
314    }
315
316    #[test]
317    fn config_default_parse_text() {
318        let input = "basic textual input";
319        let cfg = Config::default();
320
321        let t = cfg.parse_str(input).expect("a successful parse");
322
323        match t {
324            InputType::UTF8(_) => {}
325            bad => panic!("expected Text, got: {:?}", bad),
326        }
327    }
328}