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}