tmpl_resolver/
lib.rs

1#![cfg_attr(__unstable_doc, feature(doc_auto_cfg, doc_notable_trait))]
2#![cfg_attr(not(feature = "std"), no_std)]
3/*!
4# tmpl_resolver
5
6A lightweight template resolution engine with conditional logic support.
7
8## Key Concepts
9
10- **Templates**: Contain either direct text parts or conditional selectors
11- **Selectors**: Enable branch logic based on parameter values
12- **Variable Resolution**: Recursive resolution with context-aware lookup
13
14## Features
15
16- `[]`
17  - Minimal configuration for `no_std` use
18- ["std"]
19  - Enables standard library
20  - Uses ahash::HashMap for faster lookups
21- ["serde"]
22  - Adds serialization capabilities
23  - Enables template storage/transmission
24- ["bincode"]
25  - Efficient binary serialization
26- ["toml"]
27  - Enables `ResolverError::{DecodeTomlError, EncodeTomlError}`
28
29## Examples
30
31### Basic
32
33```rust
34use tmpl_resolver::{TemplateResolver, error::ResolverResult};
35
36fn main() -> ResolverResult<()> {
37  let resolver: TemplateResolver = [
38      ("h", "Hello"),
39      ("greeting", "{h} { $name }! Today is {$day}")
40    ]
41    .try_into()?;
42
43  let result = resolver.get_with_context("greeting", &[("name", "Alice"), ("day", "Monday")])?;
44  assert_eq!(result, "Hello Alice! Today is Monday");
45  Ok(())
46}
47```
48
49### Conditional Logic
50
51```rust
52use tmpl_resolver::{TemplateResolver, error::ResolverResult};
53
54fn main() -> ResolverResult<()> {
55  let selector_msg = [(
56    "message",
57    r#"$status ->
58      [success] Operation succeeded!
59      [error] Error occurred!
60      *[default] Unknown status: {$status}
61    "#
62  )];
63
64  let resolver: TemplateResolver = selector_msg.try_into()?;
65
66  let success_msg = resolver.get_with_context("message", &[("status", "success")])?;
67
68  assert_eq!(success_msg, "Operation succeeded!");
69  Ok(())
70}
71```
72
73### Escape
74
75- `"{{ a   }}"` => `"a"`
76- `"{{{a}}}"` => `"a"`
77- `"{{{{  a  }}}}"` => `"a"`
78- `"{{    {a}    }}"` => `"{a}"`
79- `"{{a}"` => ❌ nom Error, code: take_until
80- `"{{{    {{a}}    }}}"` => `"{{a}}"`
81- `"{{{    {{ a }}    }}}"` => `"{{ a }}"`
82- `"{{{ {{a} }}}"` => `"{{a}"`
83
84```rust
85use tmpl_resolver::{error::ResolverResult, TemplateResolver};
86
87fn main() -> ResolverResult<()> {
88  let resolver: TemplateResolver = [
89    ("h", "Hello { $name }"),
90    ("how_are_you", "How Are You"),
91    ("greeting", "{h}!{{ how_are_you }}? {{     {$name} }}"),
92  ]
93  .try_into()?;
94
95  // dbg!(&resolver);
96
97  let ctx = [("name", "Alice")];
98
99  let result = resolver.get_with_context("greeting", &ctx)?;
100  assert_eq!(result, "Hello Alice!how_are_you? {$name}");
101  Ok(())
102}
103```
104*/
105extern crate alloc;
106
107pub mod error;
108pub use error::{ResolverError as Error, ResolverResult as Result};
109
110mod parsers;
111pub(crate) mod part;
112
113pub mod resolver;
114pub(crate) use resolver::MiniStr;
115pub use resolver::TemplateResolver;
116
117#[cfg(feature = "std")]
118pub type ContextMap<'a> = ahash::HashMap<&'a str, &'a str>;
119#[cfg(feature = "std")]
120pub type ContextMapBuf = ahash::HashMap<kstring::KString, MiniStr>;
121
122pub(crate) mod selector;
123pub(crate) mod template;
124pub use template::Template;
125
126#[cfg(test)]
127#[cfg(not(feature = "std"))]
128mod no_std_tests {
129  use testutils::simple_benchmark;
130
131  // extern crate std;
132  use super::*;
133  use crate::error::ResolverResult;
134
135  fn init_ast() -> ResolverResult<resolver::TemplateResolver> {
136    [("g", "Good"), ("greeting", "{g} {$period}! { $name }")]
137      .as_ref()
138      .try_into()
139  }
140
141  #[test]
142  fn get_text() -> ResolverResult<()> {
143    let text = init_ast()?
144      .get_with_context("greeting", &[("name", "Tom"), ("period", "Morning")])?;
145
146    assert_eq!(text, "Good Morning! Tom");
147    Ok(())
148  }
149
150  /// - debug: 5.791µs
151  /// - release: 1.958µs
152  #[ignore]
153  #[test]
154  fn bench_no_std_get_text() {
155    let ast = init_ast().expect("Failed to init template resolver");
156
157    simple_benchmark(|| {
158      ast.get_with_context("greeting", &[("name", "Tom"), ("period", "Morning")])
159    });
160  }
161}
162
163#[cfg(all(feature = "std", feature = "serde"))]
164#[cfg(test)]
165mod tests {
166  use std::fs;
167
168  use ahash::HashMap;
169  use kstring::KString;
170  use testutils::simple_benchmark;
171
172  use super::*;
173  use crate::error::ResolverResult;
174  type TomlResult<T> = core::result::Result<T, toml::de::Error>;
175
176  fn raw_toml_to_hashmap() -> TomlResult<HashMap<KString, MiniStr>> {
177    let text = r##"
178g = "Good"
179time-period = """
180$period ->
181  [morning] {g} Morning
182  [evening] {g} evening
183  *[other] {g} {$period}
184"""
185
186href = """
187
188<a href=""></a>
189end
190
191"""
192
193gender = """
194
195$attr ->
196  [male] Mr.
197  *[female] Ms.
198"""
199greeting = "{ time-period }! { gender }{ $name }"
200    "##;
201
202    toml::from_str(text)
203  }
204
205  #[ignore]
206  #[test]
207  fn dbg_tomlmap() {
208    let _ = dbg!(raw_toml_to_hashmap());
209  }
210
211  #[test]
212  fn get_text() -> ResolverResult<()> {
213    let raw = raw_toml_to_hashmap().expect("Failed to deser toml");
214    let resolver = resolver::TemplateResolver::try_from_raw(raw)?;
215    let text = resolver.get_with_context(
216      "greeting",
217      &[
218        ("period", "evening"),
219        ("name", "Alice"),
220        ("attr", "unknown"),
221      ],
222    )?;
223    assert_eq!(text, "Good evening! Ms.Alice");
224
225    Ok(())
226  }
227
228  #[ignore]
229  fn encode_ast_to_json() -> anyhow::Result<String> {
230    let raw = raw_toml_to_hashmap()?;
231    let resolver = resolver::TemplateResolver::try_from_raw(raw)?;
232    let json_str = serde_json::to_string_pretty(&resolver)?;
233    // println!("{toml_str}");
234    Ok(json_str)
235  }
236
237  #[cfg(feature = "bincode")]
238  #[ignore]
239  #[test]
240  fn test_serde_bincode_from_json_str() -> anyhow::Result<()> {
241    let json_str = encode_ast_to_json()?;
242    let data = serde_json::from_str::<resolver::TemplateResolver>(&json_str)?;
243    let cfg = bincode::config::standard().with_no_limit();
244    let buf = bincode::serde::encode_to_vec(data, cfg)?;
245    fs::write("tmp.bincode", &buf)?;
246    let (data, n) = bincode::serde::borrow_decode_from_slice::<
247      resolver::TemplateResolver,
248      _,
249    >(&buf, cfg)?;
250    dbg!(data, n);
251    Ok(())
252  }
253
254  #[cfg(feature = "bincode")]
255  #[ignore]
256  #[test]
257  fn test_deser_bincode_from_file() -> anyhow::Result<()> {
258    let cfg = bincode::config::standard();
259
260    let buf = fs::read("tmp.bincode")?;
261    let now = std::time::Instant::now();
262    let (data, _u) =
263      bincode::serde::decode_from_slice::<resolver::TemplateResolver, _>(&buf, cfg)?;
264    let elapsed = now.elapsed();
265    dbg!(&data);
266    eprintln!("elapsed: {elapsed:?}");
267
268    Ok(())
269  }
270
271  /// - debug: 6.75µs
272  /// - release: 1.541µs
273  #[test]
274  #[ignore]
275  fn bench_resolve() -> anyhow::Result<()> {
276    let raw = raw_toml_to_hashmap()?;
277    let resolver =
278      resolver::TemplateResolver::try_from_raw(raw).expect("Invalid template");
279    dbg!(&resolver);
280
281    simple_benchmark(|| {
282      resolver.get_with_context(
283        "greeting",
284        &[
285          ("attr", "unknown"),
286          ("period", "evening"),
287          ("name", "Alice"),
288          // ("aa", ""),
289          // ("bb", ""),
290          // ("cc", ""),
291        ],
292      )
293    });
294    Ok(())
295  }
296}