placeholder/
lib.rs

1//! Placeholder - A Placeholder Templating Engine without the complexity
2//!
3//! # Example 1
4//!
5//! ```
6//! use placeholder::render;
7//! use std::collections::HashMap;
8//!
9//! fn main() {
10//!   let template = String::from("<h1>{greet} {name}</h1><p>Do you like {food}?</p>");
11//!
12//!   let mut values = HashMap::new();
13//!   values.insert(String::from("greet"), String::from("Hello"));
14//!   values.insert(String::from("name"), String::from("Homer"));
15//!   values.insert(String::from("food"), String::from("Donuts"));
16//!
17//!   assert!(render(&template, &values)
18//!     == Ok(String::from("<h1>Hello Homer</h1><p>Do you like Donuts?</p>")));
19//!
20//! }
21//! ```
22//!
23//! # Example 2 (missing placeholder values)
24//!
25//! ```
26//! use placeholder::render;
27//! use std::collections::HashMap;
28//!
29//! fn main() {
30//!   let template = String::from("<h1>{greet} {name}</h1>");
31//!
32//!   let mut values = HashMap::new();
33//!   values.insert(String::from("greet"), String::from("Hello"));
34//!
35//!   assert!(render(&template, &values)
36//!     == Err(String::from("name")));
37//! }
38//! ```
39
40use lazy_static::lazy_static;
41use regex::Regex;
42use std::collections::HashMap;
43
44lazy_static! {
45    // Compile these once at startup
46    static ref MATCH_START: Regex = Regex::new(r"^[{](\w+)[}]").unwrap();
47    static ref MATCH_OTHER: Regex = Regex::new(r"[^{][{](\w+)[}]").unwrap();
48}
49
50/// Render the template with placeholder values
51///
52/// # Parameters
53///
54/// `template` is the template text containing placeholders in the form `{name}`
55///
56/// `values` is the HashMap containing placeholder values to replace within `template`
57///
58/// # Returns
59///
60/// `Ok(output)` is the template text with all its placeholders replaced with their corresponding
61/// placeholder values
62///
63/// `Err(name)` is the name of the placeholder missing from `values`
64///
65/// # Example 1
66///
67/// ```
68/// use placeholder::render;
69/// use std::collections::HashMap;
70///
71/// fn main() {
72///   let template = String::from("<h1>{greet} {name}</h1><p>Do you like {food}?</p>");
73///
74///   let mut values = HashMap::new();
75///   values.insert(String::from("greet"), String::from("Hello"));
76///   values.insert(String::from("name"), String::from("Homer"));
77///   values.insert(String::from("food"), String::from("Donuts"));
78///
79///   assert!(render(&template, &values)
80///     == Ok(String::from("<h1>Hello Homer</h1><p>Do you like Donuts?</p>")));
81///
82/// }
83/// ```
84///
85/// # Example 2 (missing placeholder values)
86///
87/// ```
88/// use placeholder::render;
89/// use std::collections::HashMap;
90///
91/// fn main() {
92///   let template = String::from("<h1>{greet} {name}</h1>");
93///
94///   let mut values = HashMap::new();
95///   values.insert(String::from("greet"), String::from("Hello"));
96///
97///   assert!(render(&template, &values)
98///     == Err(String::from("name")));
99/// }
100/// ```
101pub fn render(template: &str, values: &HashMap<String, String>) -> Result<String, String> {
102    // Instead of a doing this all within a single regular expression, we split it into two so that
103    // we're not branching per iteration for a possible "start of string" placeholder
104
105    let mut output = match MATCH_START.captures(template) {
106        None => template.to_string(),
107        Some(capture) => match capture.get(1) {
108            None => panic!("at the disco"),
109            Some(key) => match values.get(key.as_str()) {
110                None => return Err(key.as_str().to_string()),
111                Some(value) => Regex::new(format!("^[{{]{}[}}]", key.as_str()).as_str())
112                    .unwrap()
113                    .replace(template, value)
114                    .to_string(),
115            },
116        },
117    };
118
119    loop {
120        match MATCH_OTHER.captures(&output) {
121            None => break,
122            Some(capture) => match capture.get(1) {
123                None => panic!("at the disco"),
124                Some(key) => match values.get(key.as_str()) {
125                    None => return Err(key.as_str().to_string()),
126                    Some(value) => {
127                        output =
128                            Regex::new(format!("([^{{])[{{]{}[}}]", key.as_str()).as_str())
129                                .unwrap()
130                                .replace(&output, format!("${{1}}{}", value))
131                                .to_string()
132                    }
133                },
134            },
135        }
136    }
137
138    Ok(output)
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    #[test]
146    fn empty_string() {
147        let before = String::from("");
148        let after = String::from("");
149        let values = HashMap::new();
150
151        assert!(render(&before, &values) == Ok(after));
152    }
153
154    #[test]
155    fn no_substitution() {
156        let before = String::from("Hello world");
157        let after = String::from("Hello world");
158        let values = HashMap::new();
159
160        assert!(render(&before, &values) == Ok(after));
161    }
162
163    #[test]
164    fn ignore_escaped() {
165        let before = String::from("Hello {{middle} w{{orld");
166        let after = String::from("Hello {{middle} w{{orld");
167        let values = HashMap::new();
168
169        assert!(render(&before, &values) == Ok(after));
170    }
171
172    #[test]
173    fn missing_start_value() {
174        let before = String::from("{start} world");
175        let values = HashMap::new();
176
177        assert!(render(&before, &values) == Err(String::from("start")));
178    }
179
180    #[test]
181    fn missing_middle_value() {
182        let before = String::from("Hello {middle} world");
183        let values = HashMap::new();
184
185        assert!(render(&before, &values) == Err(String::from("middle")));
186    }
187    #[test]
188    fn missing_end_value() {
189        let before = String::from("Hello {end}");
190        let values = HashMap::new();
191
192        assert!(render(&before, &values) == Err(String::from("end")));
193    }
194
195    #[test]
196    fn missing_one_value() {
197        let before = String::from("{start} {middle} world");
198
199        let mut values = HashMap::new();
200        values.insert(String::from("start"), String::from("Hello"));
201
202        assert!(render(&before, &values) == Err(String::from("middle")));
203    }
204
205    #[test]
206    fn missing_one_value_again() {
207        let before = String::from("{start} {middle} world");
208
209        let mut values = HashMap::new();
210        values.insert(String::from("middle"), String::from("beautiful"));
211
212        assert!(render(&before, &values) == Err(String::from("start")));
213    }
214
215    #[test]
216    fn start() {
217        let before = String::from("{start} world");
218        let after = String::from("Hello world");
219
220        let mut values = HashMap::new();
221        values.insert(String::from("start"), String::from("Hello"));
222
223        assert!(render(&before, &values) == Ok(after));
224    }
225
226    #[test]
227    fn end() {
228        let before = String::from("Hello {end}");
229        let after = String::from("Hello world");
230
231        let mut values = HashMap::new();
232        values.insert(String::from("end"), String::from("world"));
233
234        assert!(render(&before, &values) == Ok(after));
235    }
236
237    #[test]
238    fn middle() {
239        let before = String::from("Hello {middle} world");
240        let after = String::from("Hello beautiful world");
241
242        let mut values = HashMap::new();
243        values.insert(String::from("middle"), String::from("beautiful"));
244
245        assert!(render(&before, &values) == Ok(after));
246    }
247
248    #[test]
249    fn hello_beautiful_world() {
250        let before = String::from("{start} {middle} {end}");
251        let after = String::from("Hello beautiful world");
252
253        let mut values = HashMap::new();
254        values.insert(String::from("start"), String::from("Hello"));
255        values.insert(String::from("middle"), String::from("beautiful"));
256        values.insert(String::from("end"), String::from("world"));
257
258        assert!(render(&before, &values) == Ok(after));
259    }
260
261    #[test]
262    fn multi_line_hello() {
263        let before = String::from("{start} is a\n{middle} test to see\nif the regex {end}");
264        let after = String::from("This is a\nmulti-line test to see\nif the regex works");
265
266        let mut values = HashMap::new();
267        values.insert(String::from("start"), String::from("This"));
268        values.insert(String::from("middle"), String::from("multi-line"));
269        values.insert(String::from("end"), String::from("works"));
270
271        assert!(render(&before, &values) == Ok(after));
272    }
273
274    #[test]
275    fn a_longer_test() {
276        let before =
277            String::from(format!("{}\n{}\n{}\n{}\n{}",
278            "No society can surely {fourth}e flourishing {first} happy, {third} which {second} far greater part {third} {second}",
279            "mem{fourth}ers are poor {first} misera{fourth}le. It is but equity, besides, that they who feed,",
280            "clothe, {first} lodge {second} whole body {third} {second} people, should have such a share {third} {second}",
281            "produce {third} their own la{fourth}our as to be themselves tolera{fourth}ly well fed, clothed, {first}",
282            "lodged."));
283
284        let after =
285            String::from(format!("{}\n{}\n{}\n{}\n{}",
286            "No society can surely be flourishing and happy, of which the far greater part of the",
287            "members are poor and miserable. It is but equity, besides, that they who feed,",
288            "clothe, and lodge the whole body of the people, should have such a share of the",
289            "produce of their own labour as to be themselves tolerably well fed, clothed, and",
290            "lodged."));
291
292        let mut values = HashMap::new();
293        values.insert(String::from("first"), String::from("and"));
294        values.insert(String::from("second"), String::from("the"));
295        values.insert(String::from("third"), String::from("of"));
296        values.insert(String::from("fourth"), String::from("b"));
297        values.insert(String::from("fifth"), String::from("these"));
298        values.insert(String::from("sixth"), String::from("last"));
299        values.insert(String::from("seventh"), String::from("ones"));
300        values.insert(String::from("eighth"), String::from("do"));
301        values.insert(String::from("ninth"), String::from("not"));
302        values.insert(String::from("tenth"), String::from("exist"));
303
304        assert!(render(&before, &values) == Ok(after));
305    }
306}