variable_resolver/
lib.rs

1#![forbid(unsafe_code)]
2
3use error::{CollectBlocksFromTemplateError, DecodeStringError};
4
5pub mod error;
6
7pub fn decode_string<'a>(
8    text: impl AsRef<str>,
9    resolve_variable: impl Fn(&str) -> Option<&'a str>,
10) -> Result<String, DecodeStringError> {
11    let text = text.as_ref();
12
13    let blocks = collect_blocks_from_string(text)?;
14
15    let mut ret = String::new();
16
17    for block in blocks {
18        if block.is_between_double_curly {
19            ret.push_str(resolve_variable(block.value).ok_or_else(|| {
20                DecodeStringError::CouldNotResolveVariable {
21                    variable_name: block.value.to_string(),
22                }
23            })?);
24        } else {
25            ret.push_str(&block.value.replace("{{{", "{{").replace("}}}", "}}"));
26        }
27    }
28
29    Ok(ret)
30}
31
32#[derive(Debug, Eq, PartialEq)]
33pub struct Block<'a> {
34    index: usize,
35    value: &'a str,
36    is_between_double_curly: bool,
37}
38
39pub fn collect_blocks_from_string(
40    value: &str,
41) -> Result<Vec<Block<'_>>, CollectBlocksFromTemplateError> {
42    let mut ret = Vec::new();
43
44    let mut is_between_double_curly = false;
45    let mut current_block_start_offset = 0;
46
47    let mut index = 0;
48    while index < value.len() {
49        if is_between_double_curly {
50            if value[index..].starts_with("}}") {
51                ret.push(Block {
52                    index: current_block_start_offset,
53                    value: &value[current_block_start_offset..index],
54                    is_between_double_curly: true,
55                });
56
57                is_between_double_curly = false;
58                current_block_start_offset = index + 2;
59
60                index += 2;
61            } else {
62                index += 1;
63            }
64        } else if value[index..].starts_with("{{{") {
65            index += 3;
66        } else if value[index..].starts_with("{{") {
67            if current_block_start_offset != index {
68                ret.push(Block {
69                    index: current_block_start_offset,
70                    value: &value[current_block_start_offset..index],
71                    is_between_double_curly: false,
72                });
73            }
74
75            is_between_double_curly = true;
76            current_block_start_offset = index + 2;
77
78            index += 2;
79        } else if value[index..].starts_with("}}}") {
80            index += 3;
81        } else if value[index..].starts_with("}}") {
82            return Err(CollectBlocksFromTemplateError::ThereIsNoOpenedBlock {
83                block_end_offset: index,
84            });
85        } else {
86            index += 1;
87        }
88    }
89
90    if is_between_double_curly {
91        Err(CollectBlocksFromTemplateError::OpenedBlockIsNotClosed {
92            block_start_offset: current_block_start_offset,
93        })
94    } else {
95        if current_block_start_offset != index {
96            ret.push(Block {
97                index: current_block_start_offset,
98                value: &value[current_block_start_offset..],
99                is_between_double_curly: false,
100            });
101        }
102        Ok(ret)
103    }
104}
105
106#[cfg(test)]
107mod tests {
108
109    use crate::{
110        decode_string,
111        error::{CollectBlocksFromTemplateError, DecodeStringError},
112        Block,
113    };
114
115    #[test]
116    fn collect_blocks_from_string() {
117        let blocks = super::collect_blocks_from_string("foo{{HOME}}bar").unwrap();
118        assert_eq!(blocks.len(), 3);
119        assert_eq!(
120            blocks[0],
121            Block {
122                index: 0,
123                value: "foo",
124                is_between_double_curly: false,
125            },
126        );
127        assert_eq!(
128            blocks[1],
129            Block {
130                index: 5,
131                value: "HOME",
132                is_between_double_curly: true,
133            },
134        );
135        assert_eq!(
136            blocks[2],
137            Block {
138                index: 11,
139                value: "bar",
140                is_between_double_curly: false,
141            }
142        );
143
144        let blocks = super::collect_blocks_from_string("{{HOME}}foobar").unwrap();
145        assert_eq!(blocks.len(), 2);
146        assert_eq!(
147            blocks[0],
148            Block {
149                index: 2,
150                value: "HOME",
151                is_between_double_curly: true,
152            }
153        );
154        assert_eq!(
155            blocks[1],
156            Block {
157                index: 8,
158                value: "foobar",
159                is_between_double_curly: false,
160            }
161        );
162
163        let blocks = super::collect_blocks_from_string("foobar{{HOME}}").unwrap();
164        assert_eq!(blocks.len(), 2);
165        assert_eq!(
166            blocks[0],
167            Block {
168                index: 0,
169                value: "foobar",
170                is_between_double_curly: false,
171            }
172        );
173        assert_eq!(
174            blocks[1],
175            Block {
176                index: 8,
177                value: "HOME",
178                is_between_double_curly: true,
179            }
180        );
181
182        let blocks =
183            super::collect_blocks_from_string("{{A}}f{{B}}o{{C}}o{{D}}b{{E}}a{{F}}r{{G}}").unwrap();
184        assert_eq!(blocks.len(), 13);
185        assert_eq!(
186            blocks[0],
187            Block {
188                index: 2,
189                value: "A",
190                is_between_double_curly: true,
191            },
192        );
193        assert_eq!(
194            blocks[1],
195            Block {
196                index: 5,
197                value: "f",
198                is_between_double_curly: false,
199            },
200        );
201        assert_eq!(
202            blocks[2],
203            Block {
204                index: 8,
205                value: "B",
206                is_between_double_curly: true,
207            },
208        );
209        assert_eq!(
210            blocks[3],
211            Block {
212                index: 11,
213                value: "o",
214                is_between_double_curly: false,
215            },
216        );
217        assert_eq!(
218            blocks[4],
219            Block {
220                index: 14,
221                value: "C",
222                is_between_double_curly: true,
223            },
224        );
225        assert_eq!(
226            blocks[5],
227            Block {
228                index: 17,
229                value: "o",
230                is_between_double_curly: false,
231            },
232        );
233        assert_eq!(
234            blocks[6],
235            Block {
236                index: 20,
237                value: "D",
238                is_between_double_curly: true,
239            },
240        );
241        assert_eq!(
242            blocks[7],
243            Block {
244                index: 23,
245                value: "b",
246                is_between_double_curly: false,
247            },
248        );
249        assert_eq!(
250            blocks[8],
251            Block {
252                index: 26,
253                value: "E",
254                is_between_double_curly: true,
255            },
256        );
257        assert_eq!(
258            blocks[9],
259            Block {
260                index: 29,
261                value: "a",
262                is_between_double_curly: false,
263            },
264        );
265        assert_eq!(
266            blocks[10],
267            Block {
268                index: 32,
269                value: "F",
270                is_between_double_curly: true,
271            },
272        );
273        assert_eq!(
274            blocks[11],
275            Block {
276                index: 35,
277                value: "r",
278                is_between_double_curly: false,
279            },
280        );
281        assert_eq!(
282            blocks[12],
283            Block {
284                index: 38,
285                value: "G",
286                is_between_double_curly: true,
287            },
288        );
289
290        let blocks = super::collect_blocks_from_string("{{VAR{N}AME}}").unwrap();
291        assert_eq!(blocks.len(), 1);
292        assert_eq!(
293            blocks[0],
294            Block {
295                index: 2,
296                value: "VAR{N}AME",
297                is_between_double_curly: true,
298            }
299        );
300
301        let blocks = super::collect_blocks_from_string("{foobar").unwrap();
302        assert_eq!(blocks.len(), 1);
303        assert_eq!(
304            blocks[0],
305            Block {
306                index: 0,
307                value: "{foobar",
308                is_between_double_curly: false,
309            }
310        );
311
312        assert!(matches!(
313            super::collect_blocks_from_string("{{VAR{N}}}AME}}"),
314            Err(CollectBlocksFromTemplateError::ThereIsNoOpenedBlock {
315                block_end_offset: 13
316            })
317        ));
318
319        assert!(matches!(
320            super::collect_blocks_from_string("{{foobar"),
321            Err(CollectBlocksFromTemplateError::OpenedBlockIsNotClosed {
322                block_start_offset: 2
323            })
324        ));
325        assert!(matches!(
326            super::collect_blocks_from_string("{{foobar}"),
327            Err(CollectBlocksFromTemplateError::OpenedBlockIsNotClosed {
328                block_start_offset: 2
329            })
330        ));
331        assert!(matches!(
332            super::collect_blocks_from_string("foo{{bar"),
333            Err(CollectBlocksFromTemplateError::OpenedBlockIsNotClosed {
334                block_start_offset: 5
335            })
336        ));
337        assert!(matches!(
338            super::collect_blocks_from_string("foo{{bar}"),
339            Err(CollectBlocksFromTemplateError::OpenedBlockIsNotClosed {
340                block_start_offset: 5
341            })
342        ));
343        assert!(matches!(
344            super::collect_blocks_from_string("foobar{{"),
345            Err(CollectBlocksFromTemplateError::OpenedBlockIsNotClosed {
346                block_start_offset: 8
347            })
348        ));
349        assert!(matches!(
350            super::collect_blocks_from_string("foobar{{}"),
351            Err(CollectBlocksFromTemplateError::OpenedBlockIsNotClosed {
352                block_start_offset: 8
353            })
354        ));
355        assert!(matches!(
356            super::collect_blocks_from_string("foobar{}}"),
357            Err(CollectBlocksFromTemplateError::ThereIsNoOpenedBlock {
358                block_end_offset: 7,
359            })
360        ));
361    }
362
363    #[test]
364    fn decode_greeting() {
365        let resolve_variable = |varname: &str| match varname {
366            "name" => Some("Jane"),
367            _ => None,
368        };
369
370        assert_eq!(
371            decode_string("Hello, {{name}}", resolve_variable).unwrap(),
372            "Hello, Jane".to_string(),
373        );
374
375        assert_eq!(
376            decode_string("Hello, {{{name}}}", resolve_variable).unwrap(),
377            "Hello, {{name}}".to_string(),
378        );
379
380        assert_eq!(
381            decode_string("Hello, {{{{name}}}}", resolve_variable).unwrap(),
382            "Hello, {{{name}}}".to_string(),
383        );
384
385        assert_eq!(
386            decode_string("Hello, {{{{{name}}}}}", resolve_variable).unwrap(),
387            "Hello, {{Jane}}".to_string(),
388        );
389
390        assert_eq!(
391            decode_string("Hello, {{{{{{name}}}}}}", resolve_variable).unwrap(),
392            "Hello, {{{{name}}}}".to_string(),
393        );
394    }
395
396    #[test]
397    fn decode() {
398        let resolve_variable = |varname: &str| match varname {
399            "HOME" => Some("__HOME__"),
400            "ROOTHOME" => Some("__ROOT__"),
401            _ => None,
402        };
403
404        assert_eq!(
405            decode_string("foo{{HOME}}bar{{ROOTHOME}}", resolve_variable).unwrap(),
406            "foo__HOME__bar__ROOT__".to_string(),
407        );
408        assert_eq!(
409            decode_string("{{HOME}}foobar{{ROOTHOME}}", resolve_variable).unwrap(),
410            "__HOME__foobar__ROOT__".to_string(),
411        );
412        assert_eq!(
413            decode_string("foo{bar{{HOME}}", resolve_variable).unwrap(),
414            "foo{bar__HOME__".to_string(),
415        );
416        assert_eq!(
417            decode_string("f{{{o}o{{HOME}}bar", resolve_variable).unwrap(),
418            "f{{o}o__HOME__bar".to_string(),
419        );
420        assert_eq!(
421            decode_string("f{o}}}o{{HOME}}bar", resolve_variable).unwrap(),
422            "f{o}}o__HOME__bar".to_string(),
423        );
424        assert_eq!(
425            decode_string("f{{{o}}}o{{HOME}}bar", resolve_variable).unwrap(),
426            "f{{o}}o__HOME__bar".to_string(),
427        );
428        assert_eq!(
429            decode_string("f{{{{o}}}}o{{HOME}}bar", resolve_variable).unwrap(),
430            "f{{{o}}}o__HOME__bar".to_string(),
431        );
432        assert_eq!(
433            decode_string("f{{o}o{{HOME}}bar", resolve_variable)
434                .err()
435                .unwrap(),
436            DecodeStringError::CouldNotResolveVariable {
437                variable_name: "o}o{{HOME".to_string(),
438            },
439        );
440        assert_eq!(
441            decode_string("foo{{INVALID_VARNAME}}bar", resolve_variable)
442                .err()
443                .unwrap(),
444            DecodeStringError::CouldNotResolveVariable {
445                variable_name: "INVALID_VARNAME".to_string(),
446            },
447        );
448
449        assert_eq!(
450            decode_string("{", resolve_variable).unwrap(),
451            "{".to_string(),
452        );
453        assert_eq!(
454            decode_string("{{{", resolve_variable).unwrap(),
455            "{{".to_string(),
456        );
457        assert_eq!(
458            decode_string("{{{{", resolve_variable).unwrap(),
459            "{{{".to_string(),
460        );
461        assert_eq!(
462            decode_string("{{{{{{", resolve_variable).unwrap(),
463            "{{{{".to_string(),
464        );
465        assert_eq!(
466            decode_string("{{{{{{{", resolve_variable).unwrap(),
467            "{{{{{".to_string(),
468        );
469        assert_eq!(
470            decode_string("{{{{{{{{{", resolve_variable).unwrap(),
471            "{{{{{{".to_string(),
472        );
473    }
474}