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_source::SourceBuilder;
17//! use tanzim_value::{LocatedValue, Location, Value};
18//!
19//! let parser = Closure::new(
20//!     "upper",
21//!     "txt",
22//!     Box::new(|source, bytes, _other_source_list| {
23//!         Ok(LocatedValue::new(
24//!             Value::String(String::from_utf8_lossy(bytes).to_uppercase()),
25//!             Location::in_source(source.clone(), None, None, None),
26//!         ))
27//!     }),
28//! );
29//! let source = SourceBuilder::new()
30//!     .with_source("file")
31//!     .with_resource("test.txt")
32//!     .build()
33//!     .unwrap();
34//! let value = parser.parse(&source, b"hello", &[]).unwrap();
35//! assert_eq!(value.value().as_string().unwrap(), "HELLO");
36//! ```
37
38use crate::{Parse, Source};
39use tanzim_value::{Error, LocatedValue};
40
41/// The parse closure driving a [`Closure`] parser — same contract as
42/// [`Parse::parse`].
43///
44/// Called with the [`Source`] declaration and the raw `&[u8]` bytes. Return a [`LocatedValue`]
45/// tree (ideally with a [`Location`](tanzim_value::Location) on every node), or an [`Error`] on
46/// failure.
47pub type BoxedParseFn =
48    Box<dyn Fn(&Source, &[u8], &[Source]) -> Result<LocatedValue, Error> + Send + Sync>;
49
50/// The optional auto-detection probe for a [`Closure`] parser — same contract as
51/// [`Parse::is_format_supported`].
52///
53/// Given the raw bytes, return `Some(true)` if confident, `Some(false)` if definitely not this
54/// format, or `None` to abstain. The default (when none is set) abstains with `None`.
55pub type BoxedValidatorFn = Box<dyn Fn(&[u8]) -> Option<bool> + Send + Sync>;
56
57/// A [`Parse`] implementation whose behaviour is supplied by closures.
58///
59/// Reach for this instead of a full `impl Parse` when the parser is small, stateless, or a
60/// one-off adapter. See the [module docs](self) for a complete example.
61pub struct Closure {
62    name: String,
63    parser: BoxedParseFn,
64    validator: BoxedValidatorFn,
65    supported_format_list: Vec<String>,
66}
67
68impl Closure {
69    /// Build a closure-backed parser.
70    ///
71    /// - `name` — the parser [`name`](crate::Parse::name) used in error messages.
72    /// - `supported_format` — the single format extension this parser handles (widen later with
73    ///   [`Closure::with_format_list`]).
74    /// - `parser` — the closure run by [`parse`](crate::Parse::parse).
75    ///
76    /// The auto-detection probe defaults to abstaining (`None`); set one with
77    /// [`Closure::with_validator`].
78    pub fn new<N: AsRef<str>, F: AsRef<str>>(
79        name: N,
80        supported_format: F,
81        parser: BoxedParseFn,
82    ) -> Self {
83        Self {
84            name: name.as_ref().to_string(),
85            parser,
86            validator: Box::new(|_| None),
87            supported_format_list: vec![supported_format.as_ref().to_string()],
88        }
89    }
90
91    /// Attach an auto-detection probe (see [`BoxedValidatorFn`]) used when a payload has no format
92    /// hint.
93    pub fn with_validator(mut self, validator: BoxedValidatorFn) -> Self {
94        self.validator = validator;
95        self
96    }
97
98    /// Replace the list of format extensions this parser handles (e.g. `["yml", "yaml"]`).
99    pub fn with_format_list<N: AsRef<str>>(mut self, format_list: &[N]) -> Self {
100        let mut formats = Vec::new();
101        for format in format_list {
102            formats.push(format.as_ref().to_string());
103        }
104        self.supported_format_list = formats;
105        self
106    }
107}
108
109impl Parse for Closure {
110    fn name(&self) -> &str {
111        self.name.as_str()
112    }
113
114    fn supported_format_list(&self) -> Vec<String> {
115        self.supported_format_list.clone()
116    }
117
118    fn parse(
119        &self,
120        source: &Source,
121        bytes: &[u8],
122        other_source_list: &[Source],
123    ) -> Result<LocatedValue, Error> {
124        (self.parser)(source, bytes, other_source_list)
125    }
126
127    fn is_format_supported(&self, bytes: &[u8]) -> Option<bool> {
128        (self.validator)(bytes)
129    }
130}