nimbus/
strings.rs
1use 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 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 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 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}