Skip to main content

formars_core/
value.rs

1/// Dependency-free dynamic value tree — the erased currency of the [`crate::schema::DynSchema`] view.
2///
3/// Object entries preserve insertion order (DV-1) and integer/float variants
4/// are kept strictly apart so number fidelity cannot silently degrade.
5#[derive(Clone, Debug, PartialEq)]
6pub enum Value {
7    /// Absent value.
8    Null,
9    /// Boolean.
10    Bool(bool),
11    /// Signed integer with exact precision.
12    I64(i64),
13    /// IEEE-754 double.
14    F64(f64),
15    /// UTF-8 string.
16    String(Box<str>),
17    /// Ordered sequence.
18    Array(Vec<Value>),
19    /// Insertion-ordered map.
20    Object(Object),
21}
22
23/// Insertion-ordered object: entries stay in declaration order (DV-1).
24#[derive(Clone, Debug, Default, PartialEq)]
25pub struct Object {
26    /// Key-value pairs in insertion order.
27    pub entries: Vec<(StringKey, Value)>,
28}
29
30/// Owned object key.
31#[derive(Clone, Debug, PartialEq, Eq, Hash)]
32pub struct StringKey(Box<str>);
33
34impl StringKey {
35    /// Creates a key from a string slice.
36    #[must_use]
37    pub fn new(s: &str) -> Self {
38        Self(s.into())
39    }
40}
41
42impl AsRef<str> for StringKey {
43    fn as_ref(&self) -> &str {
44        &self.0
45    }
46}
47
48impl Object {
49    /// Creates an empty object.
50    #[must_use]
51    pub const fn new() -> Self {
52        Self {
53            entries: Vec::new(),
54        }
55    }
56
57    /// Inserts a key-value pair. An existing key keeps its position but its
58    /// value is replaced (last write wins, matching JSON semantics); a new
59    /// key is appended at the end (insertion order preserved).
60    pub fn insert(&mut self, key: &str, value: Value) {
61        match self.entries.iter_mut().find(|(k, _)| k.as_ref() == key) {
62            Some((_, v)) => *v = value,
63            None => self.entries.push((StringKey(key.into()), value)),
64        }
65    }
66
67    /// Iterates entries in insertion order.
68    pub fn iter(&self) -> impl Iterator<Item = (&StringKey, &Value)> {
69        self.entries.iter().map(|(k, v)| (k, v))
70    }
71
72    /// Linear-scan lookup over the ordered entries; `Some` only when the
73    /// key is present (presence is structural — the value itself is not
74    /// inspected, so a present [`Value::Null`] still counts).
75    #[must_use]
76    pub fn get(&self, key: &str) -> Option<&Value> {
77        self.entries
78            .iter()
79            .find(|(k, _)| k.as_ref() == key)
80            .map(|(_, v)| v)
81    }
82}
83
84/// Lossless conversion of validated schema outputs into [`Value`]s.
85///
86/// Implemented for the realistic form-coercion targets; `u64`/`usize` are
87/// deliberately excluded because they do not fit the `I64` fidelity rules
88/// (DV-1) without silent truncation.
89///
90/// The exclusion is enforced at compile time: `coerced::<u64>()` is a valid
91/// standalone schema, but it cannot back an object field because the
92/// [`crate::schema::ObjectChild`] impl requires `T: ToValue`.
93///
94/// ```compile_fail,E0277
95/// use formars_core::prelude::*;
96///
97/// let _ = object().field("n", coerced::<u64>()); // u64 lacks ToValue
98/// ```
99pub trait ToValue {
100    /// Converts the value into its exact [`Value`] representation.
101    fn to_value(self) -> Value;
102}
103
104impl ToValue for i8 {
105    fn to_value(self) -> Value {
106        Value::I64(i64::from(self))
107    }
108}
109
110impl ToValue for i16 {
111    fn to_value(self) -> Value {
112        Value::I64(i64::from(self))
113    }
114}
115
116impl ToValue for i32 {
117    fn to_value(self) -> Value {
118        Value::I64(i64::from(self))
119    }
120}
121
122impl ToValue for i64 {
123    fn to_value(self) -> Value {
124        Value::I64(self)
125    }
126}
127
128impl ToValue for u8 {
129    fn to_value(self) -> Value {
130        Value::I64(i64::from(self))
131    }
132}
133
134impl ToValue for u16 {
135    fn to_value(self) -> Value {
136        Value::I64(i64::from(self))
137    }
138}
139
140impl ToValue for u32 {
141    fn to_value(self) -> Value {
142        Value::I64(i64::from(self))
143    }
144}
145
146impl ToValue for f32 {
147    fn to_value(self) -> Value {
148        Value::F64(f64::from(self))
149    }
150}
151
152impl ToValue for f64 {
153    fn to_value(self) -> Value {
154        Value::F64(self)
155    }
156}
157
158impl ToValue for String {
159    fn to_value(self) -> Value {
160        Value::String(self.into())
161    }
162}
163
164impl ToValue for bool {
165    fn to_value(self) -> Value {
166        Value::Bool(self)
167    }
168}
169
170impl Value {
171    /// Strict accessor: `Some` only for the String variant.
172    #[must_use]
173    pub fn as_str(&self) -> Option<&str> {
174        match self {
175            Self::String(s) => Some(s),
176            _ => None,
177        }
178    }
179
180    /// Strict accessor: `Some` only for the Bool variant.
181    #[must_use]
182    pub fn as_bool(&self) -> Option<bool> {
183        match self {
184            Self::Bool(b) => Some(*b),
185            _ => None,
186        }
187    }
188
189    /// Strict: never widens or narrows between integer and float variants.
190    #[must_use]
191    pub fn as_i64(&self) -> Option<i64> {
192        match self {
193            Self::I64(i) => Some(*i),
194            _ => None,
195        }
196    }
197
198    /// Strict: never coerces from `I64`; explicit converters provided instead.
199    #[must_use]
200    pub fn as_f64(&self) -> Option<f64> {
201        match self {
202            Self::F64(f) => Some(*f),
203            _ => None,
204        }
205    }
206}
207
208impl From<&str> for Value {
209    fn from(s: &str) -> Self {
210        Self::String(s.into())
211    }
212}
213
214impl From<String> for Value {
215    fn from(s: String) -> Self {
216        Self::String(s.into())
217    }
218}
219
220impl From<bool> for Value {
221    fn from(b: bool) -> Self {
222        Self::Bool(b)
223    }
224}
225
226impl From<i64> for Value {
227    fn from(i: i64) -> Self {
228        Self::I64(i)
229    }
230}
231
232impl From<f64> for Value {
233    fn from(f: f64) -> Self {
234        Self::F64(f)
235    }
236}
237
238#[cfg(test)]
239mod tests {
240    use super::{Object, ToValue, Value};
241
242    #[test]
243    fn d5_get_finds_present_key() {
244        let mut o = Object::new();
245        o.insert("name", Value::String("Ada".into()));
246        assert_eq!(o.get("name"), Some(&Value::String("Ada".into())));
247    }
248
249    #[test]
250    fn d5_get_misses_absent_key() {
251        let mut o = Object::new();
252        o.insert("name", Value::String("Ada".into()));
253        assert_eq!(o.get("age"), None);
254    }
255
256    #[test]
257    fn d5_get_on_empty_object() {
258        let o = Object::new();
259        assert_eq!(o.get("anything"), None);
260    }
261
262    #[test]
263    fn d4_tovalue_mappings_are_pinned() {
264        assert_eq!((-1i8).to_value(), Value::I64(-1));
265        assert_eq!((-2i16).to_value(), Value::I64(-2));
266        assert_eq!((-3i32).to_value(), Value::I64(-3));
267        assert_eq!((-4i64).to_value(), Value::I64(-4));
268        assert_eq!(1u8.to_value(), Value::I64(1));
269        assert_eq!(2u16.to_value(), Value::I64(2));
270        assert_eq!(3u32.to_value(), Value::I64(3));
271        assert_eq!(0.5f32.to_value(), Value::F64(0.5));
272        assert_eq!(1.5f64.to_value(), Value::F64(1.5));
273        assert_eq!(String::from("hi").to_value(), Value::String("hi".into()));
274        assert_eq!(true.to_value(), Value::Bool(true));
275        assert_eq!(false.to_value(), Value::Bool(false));
276    }
277
278    #[test]
279    fn dv1_duplicate_insert_last_write_wins_in_place() {
280        let mut o = Object::new();
281        o.insert("a", Value::I64(1));
282        o.insert("b", Value::I64(2));
283        o.insert("a", Value::I64(3));
284        assert_eq!(o.get("a"), Some(&Value::I64(3)), "last write wins");
285        assert_eq!(o.entries.len(), 2, "no duplicate entry appended");
286        let keys: Vec<&str> = o.iter().map(|(k, _)| k.as_ref()).collect();
287        assert_eq!(keys, vec!["a", "b"], "original position kept");
288    }
289
290    #[test]
291    fn dv1_ordered_entries_preserved() {
292        let mut o = Object::new();
293        o.insert("b", Value::I64(1));
294        o.insert("a", Value::I64(2));
295        o.insert("c", Value::I64(3));
296        let keys: Vec<&str> = o.iter().map(|(k, _)| k.as_ref()).collect();
297        assert_eq!(keys, vec!["b", "a", "c"]);
298    }
299
300    #[test]
301    fn dv1_number_fidelity_i64_max_and_f64() {
302        let v = Value::I64(i64::MAX);
303        assert_eq!(v.as_i64(), Some(i64::MAX));
304        let f = Value::F64(0.1);
305        assert_eq!(f.as_f64(), Some(0.1));
306        assert_eq!(f.as_i64(), None, "strict: no silent F64-to-I64 cast");
307    }
308
309    #[test]
310    fn accessor_strictness_no_silent_casts() {
311        assert_eq!(Value::String("x".into()).as_str(), Some("x"));
312        assert_eq!(Value::Bool(true).as_bool(), Some(true));
313        assert_eq!(Value::Null.as_str(), None);
314        assert_eq!(Value::F64(1.0).as_i64(), None);
315        assert_eq!(Value::I64(1).as_f64(), None, "strict: no silent I64-to-F64");
316    }
317
318    #[test]
319    fn from_impls_build_expected_variants() {
320        assert_eq!(Value::from("hi"), Value::String("hi".into()));
321        assert_eq!(Value::from(String::from("hi")), Value::String("hi".into()));
322        assert_eq!(Value::from(true), Value::Bool(true));
323        assert_eq!(Value::from(7i64), Value::I64(7));
324        assert_eq!(Value::from(0.5f64), Value::F64(0.5));
325    }
326}
327
328/// JSON (de)serialization bridge for [`Value`], enabled by the `serde` feature.
329#[cfg(feature = "serde")]
330pub mod json;