glossa_dsl/resolver/
lookup_value.rs

1use tap::{Pipe, Tap};
2use tinyvec::TinyVec;
3
4// use super::{ResolverResult, Resolver};
5use crate::{
6  MiniStr,
7  error::{ResolverError, ResolverResult},
8  parsers::context::Context,
9  resolver::{BTreeRawMap, Resolver},
10  template::Template,
11};
12
13impl Resolver {
14  /// Core resolution method
15  ///
16  /// > If the context is empty, you can directly use [Self::try_get].
17  ///
18  /// ## Algorithm
19  ///
20  /// 1. Context sorting for O(log n) parameter lookups
21  /// 2. Recursive template evaluation
22  /// 3. Branch prediction for conditional blocks
23  ///
24  /// ## Example
25  ///
26  /// ```
27  /// use glossa_dsl::{Resolver, error::ResolverError};
28  ///
29  /// let res: Resolver = [
30  ///   ("h", "Hello"),
31  ///   ("greeting", "{h} {$🐱}"),
32  /// ]
33  /// .try_into()?;
34  ///
35  /// let ctx = [("🐱", "喵 ฅ(°ω°ฅ)")];
36  ///
37  /// let text = res.get_with_context("greeting", &ctx)?;
38  /// assert_eq!(text, "Hello 喵 ฅ(°ω°ฅ)");
39  ///
40  /// # Ok::<(), ResolverError>(())
41  /// ```
42  ///
43  /// See also: [Self::get_with_ctx_map]
44  pub fn get_with_context(
45    &self,
46    var_name: &str,
47    context: &[(&str, &str)],
48  ) -> ResolverResult<MiniStr> {
49    let process = |ctx| self.try_get_template_and_process(var_name, ctx);
50
51    match context.is_empty() {
52      true => return process(&Context::Empty),
53      _ => context
54        .iter()
55        .copied()
56        .collect::<TinyVec<[(&str, &str); 5]>>()
57        .tap_mut(|x| x.sort_unstable_by_key(|&(k, _)| k)),
58    }
59    .as_ref()
60    .pipe(Context::Slice)
61    .pipe_ref(process)
62  }
63
64  /// Similar to [Self::get_with_context], but the context is
65  /// `BTreeMap<MiniStr, MiniStr>` instead of `&[(&str, &str)]`.
66  pub fn get_with_ctx_btree_map(
67    &self,
68    var_name: &str,
69    context_map: &BTreeRawMap,
70  ) -> ResolverResult<MiniStr> {
71    let process = |ctx| self.try_get_template_and_process(var_name, ctx);
72
73    match context_map.is_empty() {
74      true => Context::Empty,
75      _ => context_map.pipe(Context::BTree),
76    }
77    .pipe_ref(process)
78  }
79
80  ///  Similar to [Self::get_with_context], but no context.
81  ///
82  /// ## Example
83  ///
84  /// ```
85  /// use glossa_dsl::{Resolver, error::ResolverError};
86  ///
87  /// let res: Resolver = [
88  ///   ("🐱", "ฅ(°ω°ฅ)"),
89  ///   ("hi", "Hello"),
90  ///   ("greeting", "{ hi } { 🐱 }"),
91  /// ]
92  /// .try_into()?;
93  ///
94  /// let text = res.try_get("greeting")?;
95  /// assert_eq!(text, "Hello ฅ(°ω°ฅ)");
96  ///
97  /// # Ok::<(), ResolverError>(())
98  /// ```
99  pub fn try_get(&self, var_name: &str) -> ResolverResult<MiniStr> {
100    let process = |ctx| self.try_get_template_and_process(var_name, ctx);
101    process(&Context::Empty)
102  }
103
104  #[cfg(feature = "std")]
105  /// Similar to [Self::get_with_context], but the context is
106  /// [`&ContextMap`](crate::ContextMap) instead of `&[(&str, &str)]`.
107  ///
108  /// > If the parameter you need to pass is a Context HashMap that owns the
109  /// > data internally (e.g., `HashMap<KString, CompactString>`), instead of
110  /// > `HashMap<&str, &str>`, please use [Self::get_with_ctx_map_buf].
111  ///
112  /// ## Example
113  ///
114  /// ```
115  /// use glossa_dsl::Resolver;
116  ///
117  /// let res: Resolver = [
118  ///   ("g", "Good"),
119  ///   ("greeting", "{g} { time-period }! { $name }"),
120  ///   (
121  ///     "time-period",
122  ///     "$period ->
123  ///       [morning] Morning
124  ///       *[other] {$period}",
125  ///   ),
126  /// ]
127  /// .try_into()?;
128  ///
129  /// let ctx_map = [("name", "Tom"), ("period", "night")]
130  ///   .into_iter()
131  ///   .collect();
132  ///
133  /// let text = res.get_with_ctx_map("greeting", &ctx_map)?;
134  /// assert_eq!(text, "Good night! Tom");
135  ///
136  /// # Ok::<(), glossa_dsl::error::ResolverError>(())
137  /// ```
138  pub fn get_with_ctx_map(
139    &self,
140    var_name: &str,
141    context_map: &crate::ContextMap,
142  ) -> ResolverResult<MiniStr> {
143    let process = |ctx| self.try_get_template_and_process(var_name, ctx);
144
145    match context_map.is_empty() {
146      true => Context::Empty,
147      _ => context_map.pipe(Context::Map),
148    }
149    .pipe_ref(process)
150  }
151
152  #[cfg(feature = "std")]
153  /// Similar to [Self::get_with_ctx_map], but the context is
154  /// [`&ContextMapBuf`](crate::ContextMapBuf) instead of
155  /// [`&ContextMap`](crate::ContextMap).
156  ///
157  /// ## Example
158  ///
159  /// ```
160  /// use glossa_dsl::Resolver;
161  ///
162  /// let res: Resolver = [
163  ///   ("greeting", "{$hi} { $name }"),
164  /// ]
165  /// .try_into()?;
166  ///
167  /// let ctx_map = [("name", "Tom"), ("hi", "Hello!")]
168  ///   .into_iter()
169  ///   .map(|(k, v)| (k.into(), v.into()) )
170  ///   .collect();
171  ///
172  /// let text = res.get_with_ctx_map("greeting", &ctx_map)?;
173  /// assert_eq!(text, "Hello! Tom");
174  ///
175  /// # Ok::<(), glossa_dsl::error::ResolverError>(())
176  /// ```
177  pub fn get_with_ctx_map_buf(
178    &self,
179    var_name: &str,
180    context_map: &crate::ContextMapBuf,
181  ) -> ResolverResult<MiniStr> {
182    let process = |ctx| self.try_get_template_and_process(var_name, ctx);
183
184    match context_map.is_empty() {
185      true => Context::Empty,
186      _ => context_map.pipe(Context::MapBuf),
187    }
188    .pipe_ref(process)
189  }
190
191  pub(crate) fn try_get_template(&self, key: &str) -> ResolverResult<&Template> {
192    self
193      .0
194      .get(key)
195      .ok_or_else(|| ResolverError::UndefinedVariable(key.into()))
196  }
197}
198
199#[cfg(test)]
200mod tests {
201  use super::*;
202
203  #[test]
204  #[ignore]
205  #[cfg(feature = "std")]
206  fn test_get_with_ctx_map() -> ResolverResult<()> {
207    let res: Resolver = [
208      ("g", "Good"),
209      ("greeting", "{g} { time-period }! { $name }"),
210      (
211        "time-period",
212        "$period ->
213          [morning] Morning
214          *[other] {$period}",
215      ),
216    ]
217    .try_into()?;
218
219    let ctx_map = [("name", "Tom"), ("period", "night")]
220      .into_iter()
221      .collect();
222
223    let text = res.get_with_ctx_map("greeting", &ctx_map)?;
224    assert_eq!(text, "Good night! Tom");
225    Ok(())
226  }
227
228  #[test]
229  fn test_get_with_btree_map() -> ResolverResult<()> {
230    let res: Resolver = [
231      ("greeting", "Good { time-period }! { $name }"),
232      (
233        "time-period",
234        "$period ->
235          [morning] Morning
236          *[other] {$period}",
237      ),
238    ]
239    .try_into()?;
240
241    let ctx_map = [("name", "Tom"), ("period", "morning")]
242      .into_iter()
243      .map(|(k, v)| (k.into(), v.into()))
244      .collect();
245
246    let text = res.get_with_ctx_btree_map("greeting", &ctx_map)?;
247    assert_eq!(text, "Good Morning! Tom");
248    Ok(())
249  }
250}