Skip to main content

tanzim_parse/
closure.rs

1//! Custom parser backed by a closure.
2//!
3//! Use this when a format isn't built-in and you don't want to define a whole type just to
4//! implement [`Parse`]. Wrap a closure of the same shape as
5//! [`Parse::parse`] — see [`BoxedParseFn`] — and the resulting
6//! [`Closure`] *is* a `Parse`, so it plugs straight into the pipeline. Optionally attach a
7//! [`BoxedValidatorFn`] with [`Closure::with_validator`] to take part in format auto-detection.
8//!
9//! For anything with non-trivial state, prefer a real `impl Parse`. Reach for `Closure` for
10//! small, stateless, or one-off parsers.
11//!
12//! # Example
13//!
14//! ```
15//! use tanzim_parse::{closure::Closure, Parse};
16//! use tanzim_value::{LocatedValue, Location, Value};
17//!
18//! let parser = Closure::new(
19//!     "upper",
20//!     "txt",
21//!     Box::new(|source, resource, bytes| {
22//!         Ok(LocatedValue {
23//!             value: Value::String(String::from_utf8_lossy(bytes).to_uppercase()),
24//!             location: Location::at(source, resource, None, None, None),
25//!         })
26//!     }),
27//! );
28//! let value = parser.parse("file", "test.txt", b"hello").unwrap();
29//! assert_eq!(value.value.as_string().unwrap(), "HELLO");
30//! ```
31
32use crate::Parse;
33use tanzim_value::{Error, LocatedValue};
34
35/// The parse closure driving a [`Closure`] parser — same contract as
36/// [`Parse::parse`].
37///
38/// Called with, in order: the source kind (`&str`), the resource identifier (`&str`), and the raw
39/// `&[u8]` bytes. Return a [`LocatedValue`] tree (ideally with a [`Location`](tanzim_value::Location)
40/// on every node), or an [`Error`] on failure.
41pub type BoxedParseFn = Box<dyn Fn(&str, &str, &[u8]) -> Result<LocatedValue, Error>>;
42
43/// The optional auto-detection probe for a [`Closure`] parser — same contract as
44/// [`Parse::is_format_supported`].
45///
46/// Given the raw bytes, return `Some(true)` if confident, `Some(false)` if definitely not this
47/// format, or `None` to abstain. The default (when none is set) abstains with `None`.
48pub type BoxedValidatorFn = Box<dyn Fn(&[u8]) -> Option<bool>>;
49
50/// A [`Parse`] implementation whose behaviour is supplied by closures.
51///
52/// Reach for this instead of a full `impl Parse` when the parser is small, stateless, or a
53/// one-off adapter. See the [module docs](self) for a complete example.
54pub struct Closure {
55    name: String,
56    parser: BoxedParseFn,
57    validator: BoxedValidatorFn,
58    supported_format_list: Vec<String>,
59}
60
61impl Closure {
62    /// Build a closure-backed parser.
63    ///
64    /// - `name` — the parser [`name`](crate::Parse::name) used in error messages.
65    /// - `supported_format` — the single format extension this parser handles (widen later with
66    ///   [`Closure::with_format_list`]).
67    /// - `parser` — the closure run by [`parse`](crate::Parse::parse).
68    ///
69    /// The auto-detection probe defaults to abstaining (`None`); set one with
70    /// [`Closure::with_validator`].
71    pub fn new<N: AsRef<str>, F: AsRef<str>>(
72        name: N,
73        supported_format: F,
74        parser: BoxedParseFn,
75    ) -> Self {
76        Self {
77            name: name.as_ref().to_string(),
78            parser,
79            validator: Box::new(|_| None),
80            supported_format_list: vec![supported_format.as_ref().to_string()],
81        }
82    }
83
84    /// Attach an auto-detection probe (see [`BoxedValidatorFn`]) used when a payload has no format
85    /// hint.
86    pub fn with_validator(mut self, validator: BoxedValidatorFn) -> Self {
87        self.validator = validator;
88        self
89    }
90
91    /// Replace the list of format extensions this parser handles (e.g. `["yml", "yaml"]`).
92    pub fn with_format_list<N: AsRef<str>>(mut self, format_list: &[N]) -> Self {
93        let mut formats = Vec::new();
94        for format in format_list {
95            formats.push(format.as_ref().to_string());
96        }
97        self.supported_format_list = formats;
98        self
99    }
100}
101
102impl Parse for Closure {
103    fn name(&self) -> &str {
104        self.name.as_str()
105    }
106
107    fn supported_format_list(&self) -> Vec<String> {
108        self.supported_format_list.clone()
109    }
110
111    fn parse(&self, source: &str, resource: &str, bytes: &[u8]) -> Result<LocatedValue, Error> {
112        (self.parser)(source, resource, bytes)
113    }
114
115    fn is_format_supported(&self, bytes: &[u8]) -> Option<bool> {
116        (self.validator)(bytes)
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123    use tanzim_value::{Location, Value};
124
125    #[test]
126    fn closure_parser_delegates_to_function() {
127        let parser = Closure::new(
128            "upper",
129            "txt",
130            Box::new(|source, resource, bytes| {
131                Ok(LocatedValue {
132                    value: Value::String(String::from_utf8_lossy(bytes).to_uppercase()),
133                    location: Location::at(source, resource, None, None, None),
134                })
135            }),
136        )
137        .with_validator(Box::new(|bytes| Some(!bytes.is_empty())));
138        let parsed = parser.parse("file", "test.txt", b"hello").unwrap();
139        assert_eq!(parsed.value.as_string().unwrap(), "HELLO");
140        assert_eq!(parser.is_format_supported(b"x"), Some(true));
141        assert_eq!(parser.is_format_supported(b""), Some(false));
142    }
143}