Skip to main content

harn_vm/value/
build.rs

1//! Ergonomic builders for the string-keyed `VmValue` maps that back
2//! `VmValue::Dict`.
3//!
4//! Harn builtins assemble result dicts by hand, and the raw
5//! `map.insert("key".to_string(), VmValue::String(std::sync::Arc::from(v)))`
6//! spelling is both verbose and — once rustfmt wraps it — four lines per
7//! field. [`VmDictExt`] collapses each field to a single call while keeping
8//! the receiver, so the common shapes read as `dict.put_str("key", v)` /
9//! `dict.put_opt_str("key", maybe.as_deref())`. There is no behavioural
10//! change: every method is a thin wrapper over the map's `insert`.
11
12use std::collections::BTreeMap;
13
14use super::{DictMap, VmValue};
15
16/// Minimal string-keyed insert, implemented for both the transient `BTreeMap`
17/// builders still used at call sites and the persistent [`DictMap`] that backs
18/// a live `VmValue::Dict`. Keeps [`VmDictExt`] a single generic impl rather
19/// than two near-identical copies.
20pub trait DictInsert {
21    fn dict_insert(&mut self, key: String, value: VmValue);
22}
23
24impl DictInsert for BTreeMap<String, VmValue> {
25    fn dict_insert(&mut self, key: String, value: VmValue) {
26        self.insert(key, value);
27    }
28}
29
30impl DictInsert for DictMap {
31    fn dict_insert(&mut self, key: String, value: VmValue) {
32        self.insert(key, value);
33    }
34}
35
36/// Field-insertion helpers for a `VmValue::Dict` backing map.
37///
38/// Implemented for any [`DictInsert`] map (the transient `BTreeMap` builders
39/// and the persistent [`DictMap`]), so the methods are unavailable on unrelated
40/// maps and cannot be applied by mistake.
41pub trait VmDictExt {
42    /// Inserts `value` under `key` (owning the key as a `String`).
43    fn put(&mut self, key: &str, value: VmValue);
44    /// Inserts a string value built via [`VmValue::string`].
45    fn put_str(&mut self, key: &str, value: impl AsRef<str>);
46    /// Inserts a string value only when `value` is `Some`.
47    fn put_opt_str(&mut self, key: &str, value: Option<impl AsRef<str>>);
48    /// Inserts a `VmValue` only when `value` is `Some`.
49    fn put_opt(&mut self, key: &str, value: Option<VmValue>);
50    /// Inserts a boolean value.
51    fn put_bool(&mut self, key: &str, value: bool);
52    /// Inserts an integer value.
53    fn put_int(&mut self, key: &str, value: i64);
54}
55
56/// `retain` for the persistent [`DictMap`].
57///
58/// `imbl::OrdMap` has no in-place `retain` (it is structurally shared), so this
59/// rebuilds the map keeping only the entries for which `keep` returns true. The
60/// closure signature mirrors `BTreeMap::retain` (`&K, &mut V`) so call sites
61/// read identically. Cold-path helper (filter/dedup), so the rebuild cost is
62/// not on any hot loop.
63pub trait DictRetain {
64    fn retain(&mut self, keep: impl FnMut(&String, &mut VmValue) -> bool);
65}
66
67impl DictRetain for DictMap {
68    fn retain(&mut self, mut keep: impl FnMut(&String, &mut VmValue) -> bool) {
69        let mut result = DictMap::new();
70        for (k, v) in self.iter() {
71            let mut v = v.clone();
72            if keep(k, &mut v) {
73                result.insert(k.clone(), v);
74            }
75        }
76        *self = result;
77    }
78}
79
80impl<M: DictInsert> VmDictExt for M {
81    fn put(&mut self, key: &str, value: VmValue) {
82        self.dict_insert(key.to_string(), value);
83    }
84
85    fn put_str(&mut self, key: &str, value: impl AsRef<str>) {
86        self.dict_insert(key.to_string(), VmValue::string(value));
87    }
88
89    fn put_opt_str(&mut self, key: &str, value: Option<impl AsRef<str>>) {
90        if let Some(value) = value {
91            self.dict_insert(key.to_string(), VmValue::string(value));
92        }
93    }
94
95    fn put_opt(&mut self, key: &str, value: Option<VmValue>) {
96        if let Some(value) = value {
97            self.dict_insert(key.to_string(), value);
98        }
99    }
100
101    fn put_bool(&mut self, key: &str, value: bool) {
102        self.dict_insert(key.to_string(), VmValue::Bool(value));
103    }
104
105    fn put_int(&mut self, key: &str, value: i64) {
106        self.dict_insert(key.to_string(), VmValue::Int(value));
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn put_str_matches_manual_construction() {
116        let mut dict = BTreeMap::new();
117        dict.put_str("k", "v");
118        match dict.get("k") {
119            Some(VmValue::String(s)) => assert_eq!(s.as_ref(), "v"),
120            other => panic!("expected string value, got {other:?}"),
121        }
122    }
123
124    #[test]
125    fn put_opt_str_skips_none_and_inserts_some() {
126        let mut dict = BTreeMap::new();
127        dict.put_opt_str("present", Some("v"));
128        dict.put_opt_str("absent", None::<&str>);
129        assert!(dict.contains_key("present"));
130        assert!(!dict.contains_key("absent"));
131    }
132
133    #[test]
134    fn put_handles_scalars() {
135        let mut dict = BTreeMap::new();
136        dict.put_bool("b", true);
137        dict.put_int("n", 7);
138        dict.put("nil", VmValue::Nil);
139        assert!(matches!(dict.get("b"), Some(VmValue::Bool(true))));
140        assert!(matches!(dict.get("n"), Some(VmValue::Int(7))));
141        assert!(matches!(dict.get("nil"), Some(VmValue::Nil)));
142    }
143}