1use std::collections::BTreeMap;
2use std::sync::{LazyLock, PoisonError, RwLock};
3
4static BOUND: LazyLock<RwLock<BTreeMap<String, String>>> = LazyLock::new(RwLock::default);
5
6pub fn bind(key: &str, value: &str) {
7 BOUND
8 .write()
9 .unwrap_or_else(PoisonError::into_inner)
10 .insert(key.to_string(), value.to_string());
11}
12
13pub fn bound(key: &str) -> Option<String> {
14 BOUND
15 .read()
16 .unwrap_or_else(PoisonError::into_inner)
17 .get(key)
18 .cloned()
19}
20
21fn is_key(key: &str) -> bool {
22 !key.is_empty()
23 && key
24 .chars()
25 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.')
26}
27
28pub fn filled(text: &str) -> String {
29 let mut out = String::with_capacity(text.len());
30 let mut rest = text;
31 while let Some(open) = rest.find('{') {
32 out.push_str(&rest[..open]);
33 let after = &rest[open + 1..];
34 let value = after
35 .find('}')
36 .map(|close| &after[..close])
37 .filter(|key| is_key(key))
38 .and_then(|key| bound(key).map(|value| (key.len(), value)));
39 match value {
40 Some((len, value)) => {
41 out.push_str(&value);
42 rest = &after[len + 1..];
43 }
44 None => {
45 out.push('{');
46 rest = after;
47 }
48 }
49 }
50 out.push_str(rest);
51 out
52}
53
54#[cfg(test)]
55mod tests {
56 use super::*;
57
58 #[test]
59 fn a_bound_key_fills_its_token_and_anything_else_stays_as_written() {
60 bind("bind-test-version", "1.4.2");
61 bind("bind-test-name", "sample");
62 assert_eq!(filled("v{bind-test-version}"), "v1.4.2");
63 assert_eq!(
64 filled("{bind-test-name} {bind-test-version}!"),
65 "sample 1.4.2!"
66 );
67 assert_eq!(filled("{bind-test-unbound}"), "{bind-test-unbound}");
68 assert_eq!(filled("{ bind-test-name }"), "{ bind-test-name }");
69 assert_eq!(filled("{{bind-test-name}}"), "{sample}");
70 assert_eq!(filled("{bind-test-name"), "{bind-test-name");
71 assert_eq!(filled("}{}{"), "}{}{");
72 assert_eq!(filled("plain"), "plain");
73 bind("bind-test-name", "other");
74 assert_eq!(filled("{bind-test-name}"), "other");
75 }
76}