Skip to main content

tanzim_load/
closure.rs

1//! Custom loader backed by a closure.
2//!
3//! Use this when configuration comes from a source that isn't built-in and you don't want to
4//! define a whole type just to implement [`Load`]. Wrap a closure of the same shape
5//! as [`Load::load`] — `Fn(Source) -> Result<Vec<Payload>, Error>` — and the
6//! resulting [`Closure`] *is* a `Load`, so it plugs straight into the pipeline.
7//!
8//! For anything with non-trivial state or option handling, prefer a real `impl Load` (see the
9//! `Load` trait docs). Reach for `Closure` for small, stateless, or one-off adapters.
10//!
11//! The single source string passed to [`Closure::new`] can be widened afterwards with
12//! [`Closure::with_supported_source_list`], and the loader's [`name`](crate::Load::name) with
13//! [`Closure::with_name`].
14//!
15//! # Example
16//!
17//! ```
18//! use tanzim_load::{closure::Closure, Error, Load, Payload, Source};
19//!
20//! # fn example() -> Result<(), tanzim_load::Error> {
21//! let loader = Closure::new(
22//!     "static",
23//!     |source: Source| {
24//!         Ok(vec![Payload {
25//!             source: source.clone(),
26//!             maybe_name: Some("demo".into()),
27//!             maybe_format: Some("json".into()),
28//!             content: br#"{"hello":"world"}"#.to_vec(),
29//!         }])
30//!     },
31//!     "demo",
32//! );
33//! # Ok(())
34//! # }
35//! ```
36
37use crate::{Error, Load, Payload, Source};
38
39/// Boxed loader function: maps a [`Source`] to its loaded [`Payload`]s.
40type LoaderFn = Box<dyn Fn(Source) -> Result<Vec<Payload>, Error> + Send + Sync + 'static>;
41
42/// A [`Load`] implementation whose behaviour is supplied by a closure.
43///
44/// Reach for this instead of a full `impl Load` when the loader is small, stateless, or a one-off
45/// adapter. See the [module docs](self) for a complete example.
46pub struct Closure {
47    name: String,
48    loader: LoaderFn,
49    supported_source_list: Vec<String>,
50}
51
52impl Closure {
53    /// Build a closure-backed loader.
54    ///
55    /// - `name` — the loader [`name`](Load::name) used in error messages.
56    /// - `loader` — the closure run by [`load`](Load::load); same shape as the trait method.
57    /// - `source` — the single source string this loader handles (widen later with
58    ///   [`Closure::with_supported_source_list`]).
59    pub fn new<N, L, S>(name: N, loader: L, source: S) -> Self
60    where
61        N: Into<String>,
62        L: Fn(Source) -> Result<Vec<Payload>, Error> + Send + Sync + 'static,
63        S: Into<String>,
64    {
65        Self {
66            name: name.into(),
67            loader: Box::new(loader),
68            supported_source_list: vec![source.into()],
69        }
70    }
71
72    /// Override the loader name reported by [`Load::name`].
73    pub fn with_name<N: AsRef<str>>(mut self, name: N) -> Self {
74        self.name = name.as_ref().to_string();
75        self
76    }
77
78    /// Replace the list of source strings this loader handles (e.g. `["http", "https"]`).
79    pub fn with_supported_source_list<S: AsRef<str>>(
80        mut self,
81        supported_source_list: Vec<S>,
82    ) -> Self {
83        self.supported_source_list = supported_source_list
84            .into_iter()
85            .map(|source| source.as_ref().to_string())
86            .collect();
87        self
88    }
89}
90
91impl Load for Closure {
92    fn name(&self) -> &str {
93        self.name.as_str()
94    }
95
96    fn supported_source_list(&self) -> Vec<String> {
97        self.supported_source_list.clone()
98    }
99
100    fn load(&self, source: Source) -> Result<Vec<Payload>, Error> {
101        (self.loader)(source)
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108    use tanzim_source::SourceBuilder;
109
110    #[test]
111    fn closure_loader_delegates_to_function() {
112        let loader = Closure::new(
113            "custom",
114            |source: Source| {
115                let resource = source.resource().to_string();
116                Ok(vec![Payload {
117                    source,
118                    maybe_name: Some("demo".into()),
119                    maybe_format: Some("txt".into()),
120                    content: resource.into_bytes(),
121                }])
122            },
123            "custom",
124        );
125        assert_eq!(loader.name(), "custom");
126        assert_eq!(loader.supported_source_list(), vec!["custom".to_string()]);
127        let source = SourceBuilder::new()
128            .with_source("custom")
129            .with_resource("hello")
130            .build()
131            .unwrap();
132        let loaded = loader.load(source).unwrap();
133        assert_eq!(loaded.len(), 1);
134        assert_eq!(loaded[0].content, b"hello");
135    }
136
137    #[test]
138    fn closure_loader_with_name_and_supported_source_list() {
139        let loader = Closure::new("old", |_source: Source| Ok(vec![]), "mock")
140            .with_name("custom")
141            .with_supported_source_list(vec!["mock", "other"]);
142        assert_eq!(loader.name(), "custom");
143        assert_eq!(
144            loader.supported_source_list(),
145            vec!["mock".to_string(), "other".to_string()]
146        );
147        let source = SourceBuilder::new().with_source("other").build().unwrap();
148        assert!(loader.load(source).unwrap().is_empty());
149    }
150}