markings/
lib.rs

1//! A simple string-based template 'language'
2//!
3//! This simply allows you to replace ${key} in a string with a 'Val' that imls. `std::fmt::Display`
4//!
5//! # Simple usage
6//! ```
7//! use markings::{Args, Template, Opts};
8//! // template strings are simply just ${key} markers in a string
9//! // they are replaced with a cooresponding value when .apply() is used
10//! let input = "hello ${name}, an answer: ${greeting}.";
11//!
12//! // parse a template with the default options
13//! // templates are clonable, they are 'consumed' on application.
14//! let template = Template::parse(&input, Opts::default()).unwrap();
15//!
16//! // construct some replacement args, this is reusable
17//! let args = Args::new()
18//!      // with constructs a key:val pair,
19//!      // key must be a &str,
20//!      // value is anything that implements std::fmt::Display
21//!     .with("name", &"test-user")
22//!     .with("greeting", &false);
23//!
24//! // apply the pre-computed args to the template, consuming the template
25//! let output = template.apply(&args).unwrap();
26//! assert_eq!(output, "hello test-user, an answer: false.");
27//! ```
28
29use std::collections::HashMap;
30
31/// An error produced by this crate
32#[derive(Debug)]
33pub enum Error {
34    /// Mismatched braces were found
35    ///
36    /// `open` count and `closed` count
37    MismatchedBraces { open: usize, close: usize },
38
39    /// Expected a closing brace for open brace
40    ///
41    /// `head` is the offset for the nearest open brace
42    ExpectedClosing { head: usize },
43
44    /// Expected a opening brace for close brace
45    ///
46    /// `tail` is the offset for the nearest close brace
47    ExpectedOpening { tail: usize },
48
49    /// Nested template was found
50    ///
51    /// `pos` is where the template begins
52    NestedTemplate { pos: usize },
53
54    /// Duplicate keys were found, but not configured in [`Opts`](./struct.Opts.html)
55    DuplicateKeys,
56
57    /// An empty template was found, but not configured in [`Opts`](./struct.Opts.html)
58    EmptyTemplate,
59
60    /// Optional keys were found, but not configured in [`Opts`](./struct.Opts.html)
61    OptionalKeys,
62}
63
64impl std::fmt::Display for Error {
65    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66        use Error::*;
67        match self {
68            MismatchedBraces { open, close } => write!(
69                f,
70                "found {} open braces, and {} closed braces. a mistmatch",
71                open, close
72            ),
73            ExpectedClosing { head } => write!(f, "expected closing bracket from offset {}", head),
74            ExpectedOpening { tail } => write!(f, "expected opening bracket from offset {}", tail),
75            NestedTemplate { pos } => write!(f, "nested template starting at offset: {}", pos),
76            DuplicateKeys => f.write_str("duplicate keys were found"),
77            EmptyTemplate => f.write_str("empty template was found"),
78            OptionalKeys => f.write_str("optional keys were found"),
79        }
80    }
81}
82impl std::error::Error for Error {}
83
84#[derive(Debug, Clone)]
85struct State<'a> {
86    keys: Vec<&'a str>,
87}
88
89impl<'a> State<'a> {
90    fn new(keys: Vec<&'a str>) -> Self {
91        Self { keys }
92    }
93
94    fn has_keys(&self) -> bool {
95        !self.keys.is_empty()
96    }
97
98    fn remove(&mut self, key: &str) -> Option<(&'a str, usize)> {
99        if self.keys.is_empty() {
100            return None;
101        }
102
103        let mut out = None;
104        let mut i = 0;
105        while i != self.keys.len() {
106            if self.keys[i] == key {
107                let s = self.keys.remove(i);
108                let (_, count) = out.get_or_insert_with(|| (s, 0));
109                *count += 1;
110            } else {
111                i += 1;
112            }
113        }
114        out
115    }
116
117    fn has_duplicates(&self) -> bool {
118        let mut set = std::collections::HashSet::new();
119        self.keys.iter().any(|key| !set.insert(key))
120    }
121}
122
123/// Templates allows for string replacement by **name**
124///
125/// ```
126/// use markings::{Template, Args, Opts};
127/// // parse a template using the default options
128/// // the template is clonable so you don't have to reparse it
129/// let template = Template::parse("hello, ${world}${end}", Opts::default())
130///     .unwrap();
131///
132/// // build re-usable args that act as the replacements for the keys in the template
133/// let args = Args::new()
134///     .with("world", &"world")
135///     .with("end", &(0x21 as char));
136///
137/// // apply the args to the template, consuming the template
138/// let template = template
139///     .apply(&args)
140///     .unwrap();
141///
142/// // you'll get a String out, hopefully, that has your new message
143/// assert_eq!(template, "hello, world!");
144/// ```
145/// See [`Template::apply`](./fn.Template.apply.html) for applying arguments to this template.
146///
147/// See [`Opts`](./struct.Opts.html) for a way to change the behavior of the parser
148#[derive(Clone, Debug)]
149pub struct Template<'a> {
150    data: String, // total string
151    state: State<'a>,
152    opts: Opts,
153}
154
155impl<'a> Template<'a> {
156    /// Parses a new template from a string
157    ///
158    /// The syntax is extremely basic: just `${key}`
159    ///
160    /// The *key* gets replaced by a *value* matching it during the [`Template::apply`](./struct.Template.html#method.apply) call
161    pub fn parse(input: &'a str, opts: Opts) -> Result<Self, Error> {
162        let state = State::new(Self::find_keys(input)?);
163        opts.validate(&state)?;
164        Ok(Self {
165            data: input.to_string(),
166            state,
167            opts,
168        })
169    }
170
171    /// Was this template empty?
172    pub fn is_empty(&self) -> bool {
173        self.opts.empty_template
174    }
175
176    /// Apply the arguments to the template
177    ///
178    /// One can use the [`Args`](./struct.Args.html) builder to make this less tedious
179    pub fn apply<'k>(mut self, args: &Args<'k>) -> Result<String, Error> {
180        for (key, val) in &args.mapping {
181            let matches = self.state.remove(key);
182            match matches {
183                Some((match_, _)) => {
184                    let s = self.data.replace(&format!("${{{}}}", match_), &val);
185                    std::mem::replace(&mut self.data, s);
186                }
187                None if self.opts.optional_keys || self.is_empty() => continue,
188                _ => return Err(Error::OptionalKeys),
189            }
190        }
191
192        self.data.shrink_to_fit();
193        Ok(self.data)
194    }
195
196    /// Find all the *keys* in the input string, returning them in a Vec
197    ///
198    /// This is exposed as a convenient function for doing pre-parsing.
199    ///
200    /// This returns an error if there are:
201    /// * nested templates
202    /// * mismatched braces
203    ///
204    /// ```
205    /// # use markings::Template;
206    /// let keys = Template::find_keys("${this} is a ${test} ${with some keys}").unwrap();
207    /// assert_eq!(keys, vec!["this", "test", "with some keys"]);
208    /// ```
209    pub fn find_keys(input: &str) -> Result<Vec<&str>, Error> {
210        let mut heads = vec![];
211        let mut tails = vec![];
212
213        let mut last = None;
214        let mut iter = input.char_indices().peekable();
215        while let Some((pos, ch)) = iter.next() {
216            if ch == '$' && iter.peek().map(|&(_, d)| d == '{').unwrap_or_default() {
217                last.replace(pos);
218                heads.push(pos);
219                iter.next();
220            }
221            if ch == '{' && last.is_some() {
222                return Err(Error::NestedTemplate { pos });
223            }
224
225            if ch == '}' && last.is_some() {
226                tails.push(pos);
227                last.take();
228            }
229        }
230
231        if heads.len() != tails.len() {
232            return Err(Error::MismatchedBraces {
233                open: heads.len(),
234                close: tails.len(),
235            });
236        }
237
238        tails.reverse();
239
240        let mut keys = Vec::with_capacity(heads.len());
241        for head in heads {
242            let tail = tails.pop().ok_or_else(|| Error::ExpectedClosing { head })?;
243            if tail > head {
244                keys.push(&input[head + 2..tail]);
245            } else {
246                return Err(Error::ExpectedOpening { tail });
247            }
248        }
249
250        if !tails.is_empty() {
251            return Err(Error::MismatchedBraces {
252                open: 0,
253                close: tails.len(),
254            });
255        }
256
257        Ok(keys)
258    }
259}
260
261/// `Opts` are a set of options to configure how a template will be **parsed** and **applied**
262///
263/// ### The default options would fail if
264/// - there is an empty template (e.g. no replacement keys)
265/// - there are duplicate keys
266/// - apply will fail if the exact keys aren't applied
267///
268/// ## default options
269/// ```
270/// # use markings::{Template, Opts};
271/// let input = "this is a ${name}.";
272/// let template = Template::parse(&input, Opts::default()).unwrap();
273/// ```
274/// ## various options
275/// ```
276/// # use markings::{Template, Opts};
277/// // this will allow these options in the parsing/application
278/// let opts = Opts::default()
279///     .optional_keys()  // optional keys -- args aren't required to match the template keys
280///     .duplicate_keys() // duplicate keys -- duplicate keys in the template will use the same argument
281///     .empty_template() // templates can just be strings that act as an "identity"
282///     .build();
283///
284/// let input = "this is a ${name}.";
285/// let template = Template::parse(&input, opts).unwrap();
286#[derive(Default, Copy, Clone, Debug, PartialEq)]
287pub struct Opts {
288    optional_keys: bool,
289    duplicate_keys: bool,
290    empty_template: bool,
291}
292
293impl Opts {
294    /// Allow optional keys
295    ///
296    /// Keys found in the template application don't have to appear in the template
297    pub fn optional_keys(&mut self) -> &mut Self {
298        self.optional_keys = !self.optional_keys;
299        self
300    }
301
302    /// Allow duplicate keys
303    ///
304    /// Multiple keys in the template will be replaced by the same argument
305    pub fn duplicate_keys(&mut self) -> &mut Self {
306        self.duplicate_keys = !self.duplicate_keys;
307        self
308    }
309
310    /// Allows for an empty template -- e.g. a template without any args
311    ///
312    /// When args are applied to this, the original string is returned
313    pub fn empty_template(&mut self) -> &mut Self {
314        self.empty_template = !self.empty_template;
315        self
316    }
317
318    /// Construct the option set
319    pub fn build(self) -> Self {
320        self
321    }
322
323    fn validate(self, keys: &State<'_>) -> Result<(), Error> {
324        if !self.empty_template && !keys.has_keys() {
325            return Err(Error::EmptyTemplate);
326        }
327        if !self.duplicate_keys && keys.has_duplicates() {
328            return Err(Error::DuplicateKeys);
329        }
330        Ok(())
331    }
332}
333
334/// This is an easy way to build an argument mapping for the [`template application`](./struct.Template.html#method.apply) method
335///
336/// The *key* must be a [`&str`](https://doc.rust-lang.org/std/primitive.str.html) while the *value* can be any [`std::fmt::Display`](https://doc.rust-lang.org/std/path/struct.Display.html) trait object
337///
338/// **note** The keys are unique, duplicates will be replaced by the last one
339/// ```
340/// # use markings::Args;
341/// let args = Args::new()
342///     .with("key1", &false)
343///     .with("key2", &"message")
344///     .with("key3", &41)
345///     .with("key3", &42);
346/// # assert_eq!(args.len(), 3)
347/// ```
348#[derive(Default, Clone)]
349pub struct Args<'k> {
350    mapping: HashMap<std::borrow::Cow<'k, str>, String>,
351}
352
353impl<'k> Args<'k> {
354    /// Create a new Args builder
355    pub fn new() -> Self {
356        Self {
357            mapping: HashMap::new(),
358        }
359    }
360
361    /// Length of the args
362    pub fn len(&self) -> usize {
363        self.mapping.len()
364    }
365
366    /// Whether the args is empty
367    pub fn is_empty(&self) -> bool {
368        self.mapping.is_empty()
369    }
370
371    /// Maps a key to a type that implements [`std::fmt::Display`](https://doc.rust-lang.org/std/fmt/trait.Display.html)
372    pub fn with(
373        mut self,
374        key: impl Into<std::borrow::Cow<'k, str>>,
375        val: impl std::fmt::Display,
376    ) -> Self {
377        self.mapping.insert(key.into(), val.to_string().into());
378        self
379    }
380
381    pub fn iter(&self) -> impl Iterator<Item = (&'_ std::borrow::Cow<'k, str>, &'_ String)> + '_ {
382        self.mapping.iter()
383    }
384}
385
386pub type ArgsIntoIter<'k> = std::collections::hash_map::IntoIter<std::borrow::Cow<'k, str>, String>;
387
388impl<'k> IntoIterator for Args<'k> {
389    type Item = (std::borrow::Cow<'k, str>, String);
390    type IntoIter = ArgsIntoIter<'k>;
391    fn into_iter(self) -> Self::IntoIter {
392        self.mapping.into_iter()
393    }
394}
395
396impl<'k, K, V> std::iter::FromIterator<(K, V)> for Args<'k>
397where
398    K: Into<std::borrow::Cow<'k, str>>,
399    V: std::fmt::Display,
400{
401    fn from_iter<T: IntoIterator<Item = (K, V)>>(iter: T) -> Self {
402        Self {
403            mapping: iter
404                .into_iter()
405                .map(|(k, v)| (k.into(), v.to_string()))
406                .collect(),
407        }
408    }
409}
410
411#[cfg(test)]
412mod tests {
413    use super::*;
414
415    #[test]
416    fn duplicate_key() {
417        let args = Args::new()
418            .with("a", &true)
419            .with("a", &false)
420            .with("a", &true);
421
422        let v = args
423            .into_iter()
424            .map(|(k, v)| (k, v.to_string()))
425            .collect::<Vec<_>>();
426        assert_eq!(v, vec![("a".into(), "true".to_string())]);
427    }
428
429    #[test]
430    fn duplicates() {
431        let state = State::new(vec!["a", "b", "c"]);
432        assert!(!state.has_duplicates());
433
434        let state = State::new(vec!["a", "b", "a", "c"]);
435        assert!(state.has_duplicates());
436    }
437
438    #[test]
439    fn basic() {
440        let p = Template::parse("${a} ${b}${c}", Default::default()).unwrap();
441        let a = Args::new().with("a", &0).with("b", &1).with("c", &2);
442        let t = p.apply(&a).unwrap();
443        assert_eq!(t, "0 12");
444    }
445
446    #[test]
447    fn apply_iter() {
448        let mut base = (b'a'..=b'z')
449            .map(|c| format!("${{{}}}", c as char))
450            .collect::<Vec<_>>()
451            .join(" ");
452
453        for c in b'a'..=b'z' {
454            let t = Template::parse(&base, Default::default()).unwrap();
455            let a = Args::new().with(format!("{}", c as char), format!("{} = {}", c as char, c));
456            base = t.apply(&a).unwrap();
457        }
458
459        let expected = "a = 97 b = 98 c = 99 d = 100 e = 101 \
460                        f = 102 g = 103 h = 104 i = 105 j = 106 \
461                        k = 107 l = 108 m = 109 n = 110 o = 111 \
462                        p = 112 q = 113 r = 114 s = 115 t = 116 \
463                        u = 117 v = 118 w = 119 x = 120 y = 121 \
464                        z = 122";
465
466        assert_eq!(base, expected);
467    }
468
469    #[test]
470    fn owned_key() {
471        let args: Args<'static> = Args::new().with("foo".to_string(), 42);
472        assert_eq!(args.len(), 1);
473    }
474
475    #[test]
476    fn with_args() {
477        let template = "you've reached a max of ${max} credits, \
478                        out of ${total} total credits with ${success} \
479                        successes and ${failure} failures. and I've \
480                        'collected' ${overall_total} credits from all of \
481                        the failures.";
482
483        let t = Template::parse(&template, Default::default()).unwrap();
484        let parts = Args::new()
485            .with("max", &"218,731")
486            .with("total", &"706,917")
487            .with("success", &"169")
488            .with("failure", &"174")
489            .with("overall_total", &"1,629,011");
490
491        let expected = "you've reached a max of 218,731 credits, \
492                        out of 706,917 total credits with 169 \
493                        successes and 174 failures. and I've \
494                        'collected' 1,629,011 credits from all of \
495                        the failures.";
496
497        assert_eq!(t.apply(&parts).unwrap(), expected);
498    }
499
500    #[test]
501    fn empty_template() {
502        let input = "";
503        Template::parse(&input, Default::default()).unwrap_err(); // TODO assert this error
504
505        let template = Template::parse(&input, Opts::default().empty_template().build()).unwrap();
506        assert!(template.is_empty());
507        assert_eq!(input, template.apply(&Args::new()).unwrap());
508
509        let input = "foobar baz quux {{something}}";
510        Template::parse(&input, Default::default()).unwrap_err(); // TODO assert this error
511
512        let template = Template::parse(&input, Opts::default().empty_template().build()).unwrap();
513        assert!(template.is_empty());
514        assert_eq!(input, template.apply(&Args::new()).unwrap());
515    }
516
517    #[test]
518    fn duplicate_keys() {
519        let input = "${one} and ${two} and ${one}";
520        Template::parse(&input, Default::default()).unwrap_err(); //TODO assert this error
521
522        let input = "${one} and ${two} and ${one}";
523        let template = Template::parse(&input, Opts::default().duplicate_keys().build()).unwrap();
524        let parts = Args::new().with("one", &1).with("two", &2);
525        assert_eq!("1 and 2 and 1", template.apply(&parts).unwrap());
526    }
527
528    #[test]
529    fn optional_keys() {
530        let input = "${foo} ${bar} ${baz}";
531
532        let parts = Args::new().with("foo", &false).with("unknown", &true);
533
534        let template = Template::parse(&input, Default::default()).unwrap();
535        template.apply(&parts).unwrap_err(); // TODO assert this error
536
537        let template = Template::parse(&input, Opts::default().optional_keys().build()).unwrap();
538        assert_eq!("false ${bar} ${baz}", template.apply(&parts).unwrap());
539    }
540
541    #[test]
542    fn empty_template_replace() {
543        let template =
544            Template::parse("${short_name}", Opts::default().empty_template().build()).unwrap();
545        let parts = Args::new().with("short_name", &1);
546        assert_eq!("1", template.apply(&parts).unwrap());
547    }
548
549    #[test]
550    fn args_owned() {
551        let args = Args::new().with("foo", 42).with("bar", false);
552        let template = Template::parse("${foo} ${bar}", Default::default()).unwrap();
553        let s = template.apply(&args).unwrap();
554        assert_eq!(s, "42 false");
555
556        let key = "foo".to_string();
557        let args: Args = Args::new().with(&key, &42).with("bar", false);
558        let template = Template::parse("${foo} ${bar}", Default::default()).unwrap();
559        let s = template.apply(&args).unwrap();
560        assert_eq!(s, "42 false");
561    }
562}