nimbus/
strings.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use crate::{NimbusError, Result};
6use serde_json::{value::Value, Map};
7
8#[allow(dead_code)]
9pub fn fmt<T: serde::Serialize>(template: &str, context: &T) -> Result<String> {
10    let obj: Value = match serde_json::to_value(context) {
11        Ok(v) => v,
12        Err(e) => {
13            return Err(NimbusError::JSONError(
14                "obj = nimbus::strings::fmt::serde_json::to_value".into(),
15                e.to_string(),
16            ))
17        }
18    };
19
20    fmt_with_value(template, &obj)
21}
22
23#[allow(dead_code)]
24pub fn fmt_with_value(template: &str, value: &Value) -> Result<String> {
25    if let Value::Object(map) = value {
26        Ok(fmt_with_map(template, map))
27    } else {
28        Err(NimbusError::EvaluationError(
29            "Can only format json objects".to_string(),
30        ))
31    }
32}
33
34pub fn fmt_with_map(input: &str, context: &Map<String, Value>) -> String {
35    use unicode_segmentation::UnicodeSegmentation;
36    let mut output = String::with_capacity(input.len());
37
38    let mut iter = input.grapheme_indices(true);
39    let mut last_index = 0;
40
41    // This is exceedingly simple; never refer to this as a parser.
42    while let Some((index, c)) = iter.next() {
43        if c == "{" {
44            let open_index = index;
45            for (index, c) in iter.by_ref() {
46                if c == "}" {
47                    let close_index = index;
48                    let field_name = &input[open_index + 1..close_index];
49
50                    // If we decided to embed JEXL into this templating language,
51                    // this would be the place to put it.
52                    // However, we'd likely want to make this be able to detect balanced braces,
53                    // which this does not.
54                    let replace_string = match context.get(field_name) {
55                        Some(Value::Bool(v)) => v.to_string(),
56                        Some(Value::String(v)) => v.to_string(),
57                        Some(Value::Number(v)) => v.to_string(),
58                        _ => format!("{{{v}}}", v = field_name),
59                    };
60
61                    output.push_str(&input[last_index..open_index]);
62                    output.push_str(&replace_string);
63
64                    // +1 skips the closing }
65                    last_index = close_index + 1;
66                    break;
67                }
68            }
69        }
70    }
71
72    output.push_str(&input[last_index..input.len()]);
73
74    output
75}
76
77#[cfg(test)]
78mod unit_tests {
79    use serde_json::json;
80
81    use super::*;
82
83    #[test]
84    fn smoke_tests() {
85        let c = json!({
86            "string": "STRING".to_string(),
87            "number": 42,
88            "boolean": true,
89        });
90        let c = c.as_object().unwrap();
91
92        assert_eq!(
93            fmt_with_map("A {string}, a {number}, a {boolean}.", c),
94            "A STRING, a 42, a true.".to_string()
95        );
96    }
97
98    #[test]
99    fn test_unicode_boundaries() {
100        let c = json!({
101            "empty": "".to_string(),
102            "unicode": "a̐éö̲".to_string(),
103            "a̐éö̲": "unicode".to_string(),
104        });
105        let c = c.as_object().unwrap();
106
107        assert_eq!(fmt_with_map("fîré{empty}ƒøüX", c), "fîréƒøüX".to_string());
108        assert_eq!(fmt_with_map("a̐éö̲{unicode}a̐éö̲", c), "a̐éö̲a̐éö̲a̐éö̲".to_string());
109        assert_eq!(
110            fmt_with_map("is this {a̐éö̲}?", c),
111            "is this unicode?".to_string()
112        );
113    }
114
115    #[test]
116    fn test_pathological_cases() {
117        let c = json!({
118            "empty": "".to_string(),
119        });
120        let c = c.as_object().unwrap();
121
122        assert_eq!(
123            fmt_with_map("A {notthere}.", c),
124            "A {notthere}.".to_string()
125        );
126        assert_eq!(
127            fmt_with_map("aa { unclosed", c),
128            "aa { unclosed".to_string()
129        );
130    }
131}