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}