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 = Box<dyn Fn(&Source, &[u8], &[Source]) -> Result<LocatedValue, Error>>;
48
49/// The optional auto-detection probe for a [`Closure`] parser — same contract as
50/// [`Parse::is_format_supported`].
51///
52/// Given the raw bytes, return `Some(true)` if confident, `Some(false)` if definitely not this
53/// format, or `None` to abstain. The default (when none is set) abstains with `None`.
54pub type BoxedValidatorFn = Box<dyn Fn(&[u8]) -> Option<bool>>;
55
56/// A [`Parse`] implementation whose behaviour is supplied by closures.
57///
58/// Reach for this instead of a full `impl Parse` when the parser is small, stateless, or a
59/// one-off adapter. See the [module docs](self) for a complete example.
60pub struct Closure {
61 name: String,
62 parser: BoxedParseFn,
63 validator: BoxedValidatorFn,
64 supported_format_list: Vec<String>,
65}
66
67impl Closure {
68 /// Build a closure-backed parser.
69 ///
70 /// - `name` — the parser [`name`](crate::Parse::name) used in error messages.
71 /// - `supported_format` — the single format extension this parser handles (widen later with
72 /// [`Closure::with_format_list`]).
73 /// - `parser` — the closure run by [`parse`](crate::Parse::parse).
74 ///
75 /// The auto-detection probe defaults to abstaining (`None`); set one with
76 /// [`Closure::with_validator`].
77 pub fn new<N: AsRef<str>, F: AsRef<str>>(
78 name: N,
79 supported_format: F,
80 parser: BoxedParseFn,
81 ) -> Self {
82 Self {
83 name: name.as_ref().to_string(),
84 parser,
85 validator: Box::new(|_| None),
86 supported_format_list: vec![supported_format.as_ref().to_string()],
87 }
88 }
89
90 /// Attach an auto-detection probe (see [`BoxedValidatorFn`]) used when a payload has no format
91 /// hint.
92 pub fn with_validator(mut self, validator: BoxedValidatorFn) -> Self {
93 self.validator = validator;
94 self
95 }
96
97 /// Replace the list of format extensions this parser handles (e.g. `["yml", "yaml"]`).
98 pub fn with_format_list<N: AsRef<str>>(mut self, format_list: &[N]) -> Self {
99 let mut formats = Vec::new();
100 for format in format_list {
101 formats.push(format.as_ref().to_string());
102 }
103 self.supported_format_list = formats;
104 self
105 }
106}
107
108impl Parse for Closure {
109 fn name(&self) -> &str {
110 self.name.as_str()
111 }
112
113 fn supported_format_list(&self) -> Vec<String> {
114 self.supported_format_list.clone()
115 }
116
117 fn parse(
118 &self,
119 source: &Source,
120 bytes: &[u8],
121 other_source_list: &[Source],
122 ) -> Result<LocatedValue, Error> {
123 (self.parser)(source, bytes, other_source_list)
124 }
125
126 fn is_format_supported(&self, bytes: &[u8]) -> Option<bool> {
127 (self.validator)(bytes)
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134 use tanzim_source::SourceBuilder;
135 use tanzim_value::{Location, Value};
136
137 #[test]
138 fn closure_parser_delegates_to_function() {
139 let parser = Closure::new(
140 "upper",
141 "txt",
142 Box::new(|source, bytes, _other_source_list| {
143 Ok(LocatedValue::new(
144 Value::String(String::from_utf8_lossy(bytes).to_uppercase()),
145 Location::in_source(source.clone(), None, None, None),
146 ))
147 }),
148 )
149 .with_validator(Box::new(|bytes| Some(!bytes.is_empty())));
150 let source = SourceBuilder::new()
151 .with_source("file")
152 .with_resource("test.txt")
153 .build()
154 .unwrap();
155 let parsed = parser.parse(&source, b"hello", &[]).unwrap();
156 assert_eq!(parsed.value().as_string().unwrap(), "HELLO");
157 assert_eq!(parser.is_format_supported(b"x"), Some(true));
158 assert_eq!(parser.is_format_supported(b""), Some(false));
159 }
160
161 #[test]
162 fn closure_parser_with_format_list() {
163 let parser = Closure::new(
164 "yaml",
165 "yml",
166 Box::new(|source, bytes, _other_source_list| {
167 Ok(LocatedValue::new(
168 Value::String(String::from_utf8_lossy(bytes).to_string()),
169 Location::at(source.source(), source.resource(), None, None, None),
170 ))
171 }),
172 )
173 .with_format_list(&["yml", "yaml"]);
174 assert_eq!(
175 parser.supported_format_list(),
176 vec!["yml".to_string(), "yaml".to_string()]
177 );
178 }
179}