Skip to main content

tabnas/
utility.rs

1// Copyright (c) 2013-2026 Richard Rodger, MIT License
2
3//! Cross-runtime utility primitives used by grammar and option handling.
4
5use serde_json::{Map, Value};
6use std::fmt;
7use std::sync::Arc;
8
9const DANGEROUS_KEYS: [&str; 3] = ["__proto__", "constructor", "prototype"];
10
11/// Recursively merge JSON data. Objects merge by key, arrays by index, and
12/// unlike container kinds or scalar values are replaced by the overlay.
13pub fn deep(mut base: Value, overlays: impl IntoIterator<Item = Value>) -> Value {
14    for overlay in overlays {
15        base = deep_one(base, overlay);
16    }
17    base
18}
19
20fn deep_one(base: Value, overlay: Value) -> Value {
21    match (base, overlay) {
22        (Value::Object(mut base), Value::Object(overlay)) => {
23            for (key, value) in overlay {
24                if DANGEROUS_KEYS.contains(&key.as_str()) {
25                    continue;
26                }
27                let merged = match base.shift_remove(&key) {
28                    Some(previous) => deep_one(previous, value),
29                    None => clone_safe(value),
30                };
31                base.insert(key, merged);
32            }
33            Value::Object(base)
34        }
35        (Value::Array(base), Value::Array(overlay)) => {
36            let length = base.len().max(overlay.len());
37            let mut base = base.into_iter();
38            let mut overlay = overlay.into_iter();
39            Value::Array(
40                (0..length)
41                    .map(|_| match (base.next(), overlay.next()) {
42                        (Some(base), Some(overlay)) => deep_one(base, overlay),
43                        (Some(base), None) => clone_safe(base),
44                        (None, Some(overlay)) => clone_safe(overlay),
45                        (None, None) => unreachable!("length is derived from both iterators"),
46                    })
47                    .collect(),
48            )
49        }
50        (_, overlay) => clone_safe(overlay),
51    }
52}
53
54fn clone_safe(value: Value) -> Value {
55    match value {
56        Value::Object(map) => Value::Object(
57            map.into_iter()
58                .filter(|(key, _)| !DANGEROUS_KEYS.contains(&key.as_str()))
59                .map(|(key, value)| (key, clone_safe(value)))
60                .collect(),
61        ),
62        Value::Array(values) => Value::Array(values.into_iter().map(clone_safe).collect()),
63        scalar => scalar,
64    }
65}
66
67/// Render JSON-like data to at most `max_len` characters, marking truncation
68/// with dots using the canonical Tabnas behavior.
69pub fn str_value(value: &Value, max_len: i64) -> String {
70    if max_len <= 0 {
71        return String::new();
72    }
73    let rendered = match value {
74        Value::String(value) => value.clone(),
75        _ => serde_json::to_string(value).unwrap_or_default(),
76    };
77    let chars: Vec<char> = rendered.chars().collect();
78    let max_len = max_len as usize;
79    let output = if chars.len() > max_len {
80        if max_len >= 4 {
81            chars[..max_len - 3].iter().collect::<String>() + "..."
82        } else {
83            ".".repeat(max_len)
84        }
85    } else {
86        rendered
87    };
88    output
89        .chars()
90        .take(max_len)
91        .map(|value| match value {
92            '\r' | '\n' | '\t' => '.',
93            value => value,
94        })
95        .collect()
96}
97
98/// Final arbitrary list transform used by `ListMods`. The callback receives
99/// the already delete/move-adjusted list. Returning `None` retains that list,
100/// including any in-place changes; returning `Some` replaces it.
101pub type ListModifier<T> = Arc<dyn Fn(&mut Vec<T>) -> Option<Vec<T>> + Send + Sync>;
102
103pub struct ListMods<T = Value> {
104    pub delete: Vec<isize>,
105    pub move_items: Vec<isize>,
106    pub custom: Option<ListModifier<T>>,
107}
108
109impl<T> ListMods<T> {
110    pub fn with_custom(
111        mut self,
112        custom: impl Fn(&mut Vec<T>) -> Option<Vec<T>> + Send + Sync + 'static,
113    ) -> Self {
114        self.custom = Some(Arc::new(custom));
115        self
116    }
117}
118
119impl<T> Default for ListMods<T> {
120    fn default() -> Self {
121        Self {
122            delete: Vec::new(),
123            move_items: Vec::new(),
124            custom: None,
125        }
126    }
127}
128
129impl<T> Clone for ListMods<T> {
130    fn clone(&self) -> Self {
131        Self {
132            delete: self.delete.clone(),
133            move_items: self.move_items.clone(),
134            custom: self.custom.clone(),
135        }
136    }
137}
138
139impl<T> fmt::Debug for ListMods<T> {
140    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
141        formatter
142            .debug_struct("ListMods")
143            .field("delete", &self.delete)
144            .field("move_items", &self.move_items)
145            .field("custom", &self.custom.as_ref().map(|_| "<function>"))
146            .finish()
147    }
148}
149
150impl<T> PartialEq for ListMods<T> {
151    fn eq(&self, other: &Self) -> bool {
152        self.delete == other.delete
153            && self.move_items == other.move_items
154            && match (&self.custom, &other.custom) {
155                (None, None) => true,
156                (Some(left), Some(right)) => Arc::ptr_eq(left, right),
157                _ => false,
158            }
159    }
160}
161
162impl<T> Eq for ListMods<T> {}
163
164/// Apply canonical delete-then-move list modifications.
165pub fn modlist<T>(list: Vec<T>, mods: Option<&ListMods<T>>) -> Vec<T> {
166    let Some(mods) = mods else { return list };
167    let mut list: Vec<Option<T>> = list.into_iter().map(Some).collect();
168    for raw in &mods.delete {
169        let index = if *raw < 0 {
170            list.len().checked_sub(raw.unsigned_abs())
171        } else {
172            Some(*raw as usize)
173        };
174        if let Some(item) = index.and_then(|index| list.get_mut(index)) {
175            *item = None;
176        }
177    }
178    for pair in mods.move_items.chunks_exact(2) {
179        if list.is_empty() {
180            break;
181        }
182        let length = list.len() as isize;
183        let from = pair[0].rem_euclid(length) as usize;
184        let to = pair[1].rem_euclid(length) as usize;
185        let item = list.remove(from);
186        list.insert(to, item);
187    }
188    let mut list: Vec<T> = list.into_iter().flatten().collect();
189    if let Some(custom) = &mods.custom {
190        if let Some(replacement) = custom(&mut list) {
191            list = replacement;
192        }
193    }
194    list
195}
196
197/// Substitute `{a.b.0}` paths from an object or array. Missing paths retain
198/// their placeholder, matching the TypeScript and Go utilities.
199pub fn str_inject(template: &str, values: Option<&Value>) -> String {
200    let Some(values @ (Value::Object(_) | Value::Array(_))) = values else {
201        return template.to_string();
202    };
203    let mut output = String::with_capacity(template.len());
204    let mut rest = template;
205    while let Some(open) = rest.find('{') {
206        output.push_str(&rest[..open]);
207        let after_open = &rest[open + 1..];
208        let Some(close) = after_open.find('}') else {
209            output.push_str(&rest[open..]);
210            return output;
211        };
212        let path = &after_open[..close];
213        if let Some(value) = resolve_path(values, path) {
214            output.push_str(&compact(value));
215        } else {
216            output.push_str(&rest[open..open + close + 2]);
217        }
218        rest = &after_open[close + 1..];
219    }
220    output.push_str(rest);
221    output
222}
223
224fn resolve_path<'a>(mut value: &'a Value, path: &str) -> Option<&'a Value> {
225    for part in path.split('.') {
226        value = match value {
227            Value::Object(map) => map.get(part)?,
228            Value::Array(array) => array.get(part.parse::<usize>().ok()?)?,
229            _ => return None,
230        };
231    }
232    Some(value)
233}
234
235fn compact(value: &Value) -> String {
236    match value {
237        Value::Null => "null".into(),
238        Value::Bool(value) => value.to_string(),
239        Value::Number(value) => value.to_string(),
240        Value::String(value) => value.clone(),
241        Value::Array(values) => {
242            format!(
243                "[{}]",
244                values.iter().map(compact).collect::<Vec<_>>().join(",")
245            )
246        }
247        Value::Object(values) => compact_object(values),
248    }
249}
250
251fn compact_object(values: &Map<String, Value>) -> String {
252    let fields = values
253        .iter()
254        .map(|(key, value)| format!("{key}:{}", compact(value)))
255        .collect::<Vec<_>>()
256        .join(",");
257    format!("{{{fields}}}")
258}