Skip to main content

bicmath_core/
value.rs

1//! The recursive wire value model.
2//!
3//! Canonical scalar encoding (used everywhere, including nested arrays and
4//! records):
5//!
6//! ```json
7//! {"kind":"integer","value":"9007199254740993"}
8//! {"kind":"decimal","value":"0.10"}
9//! {"kind":"rational","numerator":"1","denominator":"3"}
10//! {"kind":"float64","value":"0.3333333333333333"}
11//! ```
12//!
13//! Structured values:
14//!
15//! ```json
16//! {"kind":"quantity","value":{"kind":"decimal","value":"9.81"},"dimension":{"length":1,"time":-2}}
17//! {"kind":"money","amount":{"kind":"decimal","value":"10.00"},"currency":"USD"}
18//! {"kind":"matrix","rows":2,"cols":2,"data":[...]}
19//! {"kind":"bound","unbounded":true}
20//! ```
21//!
22//! Plain JSON objects are records. To keep the encoding unambiguous, record
23//! keys must not be the reserved key `"kind"`; validation rejects such records.
24//! Plain JSON integers and decimal numbers are interpreted as exact integers and
25//! decimals. `float64` is only produced by an explicit canonical value.
26
27use std::collections::BTreeMap;
28use std::fmt;
29
30use num_bigint::BigInt;
31use num_rational::BigRational;
32use num_traits::Zero;
33use serde::de::Error as DeError;
34use serde::{Deserialize, Deserializer, Serialize, Serializer};
35
36use crate::error::{EngineError, ErrorCode};
37use crate::limits::Limits;
38use crate::number::{Decimal, Float64, Number};
39
40/// SI-style dimension vector. Angle is tracked separately from the seven SI
41/// base dimensions so trigonometric functions can require angle/dimensionless
42/// inputs explicitly.
43#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
44pub struct Dimension {
45    /// length, mass, time, electric current, thermodynamic temperature,
46    /// amount of substance, luminous intensity, plane angle.
47    exponents: [i8; 8],
48}
49
50pub const DIM_LENGTH: usize = 0;
51pub const DIM_MASS: usize = 1;
52pub const DIM_TIME: usize = 2;
53pub const DIM_CURRENT: usize = 3;
54pub const DIM_TEMPERATURE: usize = 4;
55pub const DIM_AMOUNT: usize = 5;
56pub const DIM_LUMINOUS: usize = 6;
57pub const DIM_ANGLE: usize = 7;
58
59pub const DIMENSION_NAMES: [&str; 8] = [
60    "length",
61    "mass",
62    "time",
63    "current",
64    "temperature",
65    "amount",
66    "luminous",
67    "angle",
68];
69
70impl Dimension {
71    pub const DIMENSIONLESS: Dimension = Dimension { exponents: [0; 8] };
72    pub const ANGLE: Dimension = Dimension {
73        exponents: [0, 0, 0, 0, 0, 0, 0, 1],
74    };
75
76    pub fn new(exponents: [i8; 8]) -> Dimension {
77        Dimension { exponents }
78    }
79
80    pub fn exponents(&self) -> [i8; 8] {
81        self.exponents
82    }
83
84    pub fn get(&self, index: usize) -> i8 {
85        self.exponents[index]
86    }
87
88    pub fn is_dimensionless(&self) -> bool {
89        self.exponents.iter().all(|e| *e == 0)
90    }
91
92    pub fn is_angle(&self) -> bool {
93        self.exponents[..7].iter().all(|e| *e == 0) && self.exponents[DIM_ANGLE] != 0
94    }
95
96    pub fn is_angle_only(&self) -> bool {
97        self.exponents[..7].iter().all(|e| *e == 0) && self.exponents[DIM_ANGLE] == 1
98    }
99
100    pub fn multiply(&self, other: &Dimension) -> Result<Dimension, EngineError> {
101        let mut out = [0i8; 8];
102        for (slot, (left, right)) in out
103            .iter_mut()
104            .zip(self.exponents.iter().zip(other.exponents.iter()))
105        {
106            let value = *left as i32 + *right as i32;
107            if value > i8::MAX as i32 || value < i8::MIN as i32 {
108                return Err(EngineError::new(
109                    ErrorCode::DomainViolation,
110                    "dimension exponent overflow",
111                ));
112            }
113            *slot = value as i8;
114        }
115        Ok(Dimension { exponents: out })
116    }
117
118    pub fn divide(&self, other: &Dimension) -> Result<Dimension, EngineError> {
119        let mut out = [0i8; 8];
120        for (slot, (left, right)) in out
121            .iter_mut()
122            .zip(self.exponents.iter().zip(other.exponents.iter()))
123        {
124            let value = *left as i32 - *right as i32;
125            if value > i8::MAX as i32 || value < i8::MIN as i32 {
126                return Err(EngineError::new(
127                    ErrorCode::DomainViolation,
128                    "dimension exponent overflow",
129                ));
130            }
131            *slot = value as i8;
132        }
133        Ok(Dimension { exponents: out })
134    }
135
136    pub fn pow(&self, exponent: i32) -> Result<Dimension, EngineError> {
137        let mut out = [0i8; 8];
138        for (slot, source) in out.iter_mut().zip(self.exponents.iter()) {
139            let value = *source as i32 * exponent;
140            if value > i8::MAX as i32 || value < i8::MIN as i32 {
141                return Err(EngineError::new(
142                    ErrorCode::DomainViolation,
143                    "dimension exponent overflow",
144                ));
145            }
146            *slot = value as i8;
147        }
148        Ok(Dimension { exponents: out })
149    }
150
151    pub fn to_json(&self) -> serde_json::Value {
152        let mut map = serde_json::Map::new();
153        for (i, name) in DIMENSION_NAMES.iter().enumerate() {
154            if self.exponents[i] != 0 {
155                map.insert(
156                    (*name).to_string(),
157                    serde_json::Value::from(self.exponents[i]),
158                );
159            }
160        }
161        serde_json::Value::Object(map)
162    }
163
164    pub fn from_json(value: &serde_json::Value) -> Result<Dimension, EngineError> {
165        let object = value
166            .as_object()
167            .ok_or_else(|| EngineError::malformed("dimension must be a JSON object"))?;
168        let mut exponents = [0i8; 8];
169        for (key, raw) in object {
170            let index = DIMENSION_NAMES
171                .iter()
172                .position(|name| name == key)
173                .ok_or_else(|| {
174                    EngineError::malformed(format!("unknown dimension component {key:?}"))
175                })?;
176            let value = raw.as_i64().ok_or_else(|| {
177                EngineError::malformed(format!("dimension component {key:?} must be an integer"))
178            })?;
179            if !(-128..=127).contains(&value) {
180                return Err(EngineError::malformed(format!(
181                    "dimension component {key:?} out of range"
182                )));
183            }
184            exponents[index] = value as i8;
185        }
186        Ok(Dimension { exponents })
187    }
188
189    /// Human-readable dimension string such as `m*s^-2`.
190    pub fn symbol(&self) -> String {
191        const SYMBOLS: [&str; 8] = ["m", "kg", "s", "A", "K", "mol", "cd", "rad"];
192        if self.is_dimensionless() {
193            return "1".to_string();
194        }
195        let mut parts = Vec::new();
196        for (i, symbol) in SYMBOLS.iter().enumerate() {
197            match self.exponents[i] {
198                0 => {}
199                1 => parts.push((*symbol).to_string()),
200                e => parts.push(format!("{symbol}^{e}")),
201            }
202        }
203        parts.join("*")
204    }
205}
206
207impl Serialize for Dimension {
208    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
209        self.to_json().serialize(serializer)
210    }
211}
212
213impl<'de> Deserialize<'de> for Dimension {
214    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
215        let raw = serde_json::Value::deserialize(deserializer)?;
216        Dimension::from_json(&raw).map_err(D::Error::custom)
217    }
218}
219
220impl fmt::Display for Dimension {
221    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
222        f.write_str(&self.symbol())
223    }
224}
225
226/// A tagged confidence-bound endpoint. A mathematically unbounded endpoint is
227/// represented as [`Bound::Unbounded`], never as a float infinity.
228#[derive(Clone, Debug, PartialEq)]
229pub enum Bound {
230    Unbounded,
231    Finite(Number),
232}
233
234/// The recursive wire value.
235#[derive(Clone, Debug, PartialEq)]
236pub enum Value {
237    Null,
238    Bool(bool),
239    Text(String),
240    Number(Number),
241    Array(Vec<Value>),
242    Record(BTreeMap<String, Value>),
243    Quantity {
244        value: Box<Value>,
245        dimension: Dimension,
246    },
247    Money {
248        amount: Box<Value>,
249        currency: String,
250    },
251    Matrix {
252        rows: u32,
253        cols: u32,
254        data: Vec<Value>,
255    },
256    Bound(Bound),
257}
258
259/// Reserved key that distinguishes tagged values from records.
260pub const RESERVED_KIND_KEY: &str = "kind";
261
262impl Value {
263    pub fn number(number: Number) -> Value {
264        Value::Number(number)
265    }
266
267    pub fn decimal(decimal: Decimal) -> Value {
268        Value::Number(Number::Decimal(decimal))
269    }
270
271    pub fn integer(value: BigInt) -> Value {
272        Value::Number(Number::Integer(value))
273    }
274
275    pub fn text(value: impl Into<String>) -> Value {
276        Value::Text(value.into())
277    }
278
279    pub fn record(entries: impl IntoIterator<Item = (impl Into<String>, Value)>) -> Value {
280        Value::Record(entries.into_iter().map(|(k, v)| (k.into(), v)).collect())
281    }
282
283    pub fn kind_name(&self) -> &'static str {
284        match self {
285            Value::Null => "null",
286            Value::Bool(_) => "boolean",
287            Value::Text(_) => "text",
288            Value::Number(n) => n.kind_name(),
289            Value::Array(_) => "array",
290            Value::Record(_) => "record",
291            Value::Quantity { .. } => "quantity",
292            Value::Money { .. } => "money",
293            Value::Matrix { .. } => "matrix",
294            Value::Bound(_) => "bound",
295        }
296    }
297
298    pub fn is_null(&self) -> bool {
299        matches!(self, Value::Null)
300    }
301
302    pub fn as_number(&self) -> Result<&Number, EngineError> {
303        match self {
304            Value::Number(n) => Ok(n),
305            other => Err(EngineError::malformed(format!(
306                "expected a number, found {}",
307                other.kind_name()
308            ))),
309        }
310    }
311
312    pub fn as_array(&self) -> Result<&[Value], EngineError> {
313        match self {
314            Value::Array(items) => Ok(items),
315            other => Err(EngineError::malformed(format!(
316                "expected an array, found {}",
317                other.kind_name()
318            ))),
319        }
320    }
321
322    pub fn as_record(&self) -> Result<&BTreeMap<String, Value>, EngineError> {
323        match self {
324            Value::Record(fields) => Ok(fields),
325            other => Err(EngineError::malformed(format!(
326                "expected a record, found {}",
327                other.kind_name()
328            ))),
329        }
330    }
331
332    pub fn as_text(&self) -> Result<&str, EngineError> {
333        match self {
334            Value::Text(text) => Ok(text),
335            other => Err(EngineError::malformed(format!(
336                "expected text, found {}",
337                other.kind_name()
338            ))),
339        }
340    }
341
342    pub fn as_bool(&self) -> Result<bool, EngineError> {
343        match self {
344            Value::Bool(value) => Ok(*value),
345            other => Err(EngineError::malformed(format!(
346                "expected a boolean, found {}",
347                other.kind_name()
348            ))),
349        }
350    }
351
352    pub fn as_money(&self) -> Result<(&Number, &str), EngineError> {
353        match self {
354            Value::Money { amount, currency } => Ok((amount.as_number()?, currency)),
355            other => Err(EngineError::malformed(format!(
356                "expected money, found {}",
357                other.kind_name()
358            ))),
359        }
360    }
361
362    pub fn as_quantity(&self) -> Result<(&Number, Dimension), EngineError> {
363        match self {
364            Value::Quantity { value, dimension } => Ok((value.as_number()?, *dimension)),
365            other => Err(EngineError::malformed(format!(
366                "expected a quantity, found {}",
367                other.kind_name()
368            ))),
369        }
370    }
371
372    /// Check structural depth and basic size limits.
373    pub fn check_limits(&self, limits: &Limits, depth: usize) -> Result<(), EngineError> {
374        if depth > limits.max_recursion_depth {
375            return Err(EngineError::new(
376                ErrorCode::ResourceLimit,
377                format!(
378                    "value nesting depth exceeds the limit of {}",
379                    limits.max_recursion_depth
380                ),
381            ));
382        }
383        match self {
384            Value::Array(items) => {
385                if items.len() > limits.max_array_len {
386                    return Err(EngineError::new(
387                        ErrorCode::ResourceLimit,
388                        format!(
389                            "array length {} exceeds the limit of {}",
390                            items.len(),
391                            limits.max_array_len
392                        ),
393                    ));
394                }
395                for item in items {
396                    item.check_limits(limits, depth + 1)?;
397                }
398            }
399            Value::Record(fields) => {
400                for value in fields.values() {
401                    value.check_limits(limits, depth + 1)?;
402                }
403            }
404            Value::Quantity { value, .. } => value.check_limits(limits, depth + 1)?,
405            Value::Money { amount, .. } => amount.check_limits(limits, depth + 1)?,
406            Value::Matrix { rows, cols, data } => {
407                if (*rows as usize).saturating_mul(*cols as usize) > limits.max_matrix_elements {
408                    return Err(EngineError::new(
409                        ErrorCode::ResourceLimit,
410                        format!(
411                            "matrix {}x{} exceeds the element limit of {}",
412                            rows, cols, limits.max_matrix_elements
413                        ),
414                    ));
415                }
416                for item in data {
417                    item.check_limits(limits, depth + 1)?;
418                }
419            }
420            _ => {}
421        }
422        Ok(())
423    }
424
425    /// Convert plain JSON into a typed value. JSON numbers are interpreted as
426    /// exact integers or decimals; `float64` requires the canonical form.
427    pub fn from_json(raw: &serde_json::Value, limits: &Limits) -> Result<Value, EngineError> {
428        Value::from_json_at(raw, limits, 0)
429    }
430
431    fn from_json_at(
432        raw: &serde_json::Value,
433        limits: &Limits,
434        depth: usize,
435    ) -> Result<Value, EngineError> {
436        if depth > limits.max_recursion_depth {
437            return Err(EngineError::new(
438                ErrorCode::ResourceLimit,
439                format!(
440                    "value nesting depth exceeds the limit of {}",
441                    limits.max_recursion_depth
442                ),
443            ));
444        }
445        match raw {
446            serde_json::Value::Null => Ok(Value::Null),
447            serde_json::Value::Bool(value) => Ok(Value::Bool(*value)),
448            serde_json::Value::String(text) => {
449                if text.len() > limits.max_string_len {
450                    return Err(EngineError::new(
451                        ErrorCode::ResourceLimit,
452                        format!(
453                            "string length {} exceeds the limit of {}",
454                            text.len(),
455                            limits.max_string_len
456                        ),
457                    ));
458                }
459                Ok(Value::Text(text.clone()))
460            }
461            serde_json::Value::Number(number) => {
462                let text = number.to_string();
463                if text.contains('.') || text.contains('e') || text.contains('E') {
464                    Ok(Value::Number(Number::Decimal(Decimal::parse(
465                        &text, limits,
466                    )?)))
467                } else {
468                    let value = text.parse::<BigInt>().map_err(|_| {
469                        EngineError::malformed(format!("invalid JSON integer {text:?}"))
470                    })?;
471                    if value.bits() > limits.max_integer_bits as u64 {
472                        return Err(EngineError::new(
473                            ErrorCode::ResourceLimit,
474                            format!("integer needs {} bits, exceeding the limit", value.bits()),
475                        ));
476                    }
477                    Ok(Value::Number(Number::Integer(value)))
478                }
479            }
480            serde_json::Value::Array(items) => {
481                if items.len() > limits.max_array_len {
482                    return Err(EngineError::new(
483                        ErrorCode::ResourceLimit,
484                        format!(
485                            "array length {} exceeds the limit of {}",
486                            items.len(),
487                            limits.max_array_len
488                        ),
489                    ));
490                }
491                let mut out = Vec::with_capacity(items.len());
492                for item in items {
493                    out.push(Value::from_json_at(item, limits, depth + 1)?);
494                }
495                Ok(Value::Array(out))
496            }
497            serde_json::Value::Object(object) => {
498                if let Some(tagged) = parse_tagged(object, limits, depth)? {
499                    return Ok(tagged);
500                }
501                let mut fields = BTreeMap::new();
502                for (key, value) in object {
503                    if key == RESERVED_KIND_KEY {
504                        return Err(EngineError::malformed(
505                            "record key \"kind\" is reserved for tagged values",
506                        ));
507                    }
508                    fields.insert(key.clone(), Value::from_json_at(value, limits, depth + 1)?);
509                }
510                Ok(Value::Record(fields))
511            }
512        }
513    }
514}
515
516fn parse_tagged(
517    object: &serde_json::Map<String, serde_json::Value>,
518    limits: &Limits,
519    depth: usize,
520) -> Result<Option<Value>, EngineError> {
521    let Some(kind) = object.get(RESERVED_KIND_KEY).and_then(|v| v.as_str()) else {
522        return Ok(None);
523    };
524    let number_from = |raw: &serde_json::Value| -> Result<Number, EngineError> {
525        Value::from_json_at(raw, limits, depth + 1)?
526            .as_number()
527            .cloned()
528    };
529    match kind {
530        "integer" => {
531            let raw = object
532                .get("value")
533                .ok_or_else(|| EngineError::malformed("integer value is missing"))?;
534            let text = match raw {
535                serde_json::Value::String(s) => s.clone(),
536                other => other.to_string(),
537            };
538            if !text
539                .strip_prefix(['+', '-'])
540                .unwrap_or(&text)
541                .bytes()
542                .all(|b| b.is_ascii_digit())
543                || text.is_empty()
544                || text.strip_prefix(['+', '-']).unwrap_or(&text).is_empty()
545            {
546                return Err(EngineError::malformed(format!(
547                    "invalid integer payload {text:?}"
548                )));
549            }
550            let value = text
551                .parse::<BigInt>()
552                .map_err(|_| EngineError::malformed(format!("invalid integer payload {text:?}")))?;
553            if value.bits() > limits.max_integer_bits as u64 {
554                return Err(EngineError::new(
555                    ErrorCode::ResourceLimit,
556                    "integer payload exceeds the bit limit",
557                ));
558            }
559            Ok(Some(Value::Number(Number::Integer(value))))
560        }
561        "decimal" => {
562            let raw = object
563                .get("value")
564                .ok_or_else(|| EngineError::malformed("decimal value is missing"))?;
565            let text = match raw {
566                serde_json::Value::String(s) => s.clone(),
567                other => other.to_string(),
568            };
569            Ok(Some(Value::Number(Number::Decimal(Decimal::parse(
570                &text, limits,
571            )?))))
572        }
573        "rational" => {
574            let numer = object
575                .get("numerator")
576                .and_then(|v| v.as_str())
577                .ok_or_else(|| EngineError::malformed("rational numerator must be a string"))?;
578            let denom = object
579                .get("denominator")
580                .and_then(|v| v.as_str())
581                .ok_or_else(|| EngineError::malformed("rational denominator must be a string"))?;
582            let numer = numer
583                .parse::<BigInt>()
584                .map_err(|_| EngineError::malformed("invalid rational numerator"))?;
585            let denom = denom
586                .parse::<BigInt>()
587                .map_err(|_| EngineError::malformed("invalid rational denominator"))?;
588            if denom.is_zero() {
589                return Err(EngineError::division_by_zero(
590                    "rational denominator must not be zero",
591                ));
592            }
593            Ok(Some(Value::Number(Number::Rational(BigRational::new(
594                numer, denom,
595            )))))
596        }
597        "float64" => {
598            let raw = object
599                .get("value")
600                .ok_or_else(|| EngineError::malformed("float64 value is missing"))?;
601            let text = match raw {
602                serde_json::Value::String(s) => s.clone(),
603                other => other.to_string(),
604            };
605            let value: f64 = text
606                .parse()
607                .map_err(|_| EngineError::malformed(format!("invalid float64 payload {text:?}")))?;
608            Ok(Some(Value::Number(Number::Float64(Float64::new(value)?))))
609        }
610        "quantity" => {
611            let raw = object
612                .get("value")
613                .ok_or_else(|| EngineError::malformed("quantity value is missing"))?;
614            let value = Value::from_json_at(raw, limits, depth + 1)?;
615            if !matches!(value, Value::Number(_)) {
616                return Err(EngineError::malformed(
617                    "quantity value must be a scalar number",
618                ));
619            }
620            let dimension = match object.get("dimension") {
621                Some(raw) => Dimension::from_json(raw)?,
622                None => Dimension::DIMENSIONLESS,
623            };
624            Ok(Some(Value::Quantity {
625                value: Box::new(value),
626                dimension,
627            }))
628        }
629        "money" => {
630            let raw = object
631                .get("amount")
632                .ok_or_else(|| EngineError::malformed("money amount is missing"))?;
633            let amount = number_from(raw)?;
634            let currency = object
635                .get("currency")
636                .and_then(|v| v.as_str())
637                .ok_or_else(|| EngineError::malformed("money currency is missing"))?
638                .to_string();
639            validate_currency(&currency)?;
640            Ok(Some(Value::Money {
641                amount: Box::new(Value::Number(amount)),
642                currency,
643            }))
644        }
645        "matrix" => {
646            let rows = object
647                .get("rows")
648                .and_then(|v| v.as_u64())
649                .ok_or_else(|| EngineError::malformed("matrix rows is missing"))?;
650            let cols = object
651                .get("cols")
652                .and_then(|v| v.as_u64())
653                .ok_or_else(|| EngineError::malformed("matrix cols is missing"))?;
654            if rows == 0 || cols == 0 {
655                return Err(EngineError::malformed("matrix dimensions must be positive"));
656            }
657            if (rows as usize).saturating_mul(cols as usize) > limits.max_matrix_elements {
658                return Err(EngineError::new(
659                    ErrorCode::ResourceLimit,
660                    format!(
661                        "matrix {rows}x{cols} exceeds the element limit of {}",
662                        limits.max_matrix_elements
663                    ),
664                ));
665            }
666            let raw = object
667                .get("data")
668                .and_then(|v| v.as_array())
669                .ok_or_else(|| EngineError::malformed("matrix data must be an array"))?;
670            if raw.len() != (rows as usize) * (cols as usize) {
671                return Err(EngineError::malformed(format!(
672                    "matrix data length {} does not match {}x{}",
673                    raw.len(),
674                    rows,
675                    cols
676                )));
677            }
678            let mut data = Vec::with_capacity(raw.len());
679            for item in raw {
680                let value = Value::from_json_at(item, limits, depth + 1)?;
681                if !matches!(value, Value::Number(_)) {
682                    return Err(EngineError::malformed(
683                        "matrix elements must be scalar numbers",
684                    ));
685                }
686                data.push(value);
687            }
688            Ok(Some(Value::Matrix {
689                rows: rows as u32,
690                cols: cols as u32,
691                data,
692            }))
693        }
694        "bound" => {
695            if object.get("unbounded").and_then(|v| v.as_bool()) == Some(true) {
696                Ok(Some(Value::Bound(Bound::Unbounded)))
697            } else if let Some(raw) = object.get("value") {
698                Ok(Some(Value::Bound(Bound::Finite(number_from(raw)?))))
699            } else {
700                Err(EngineError::malformed(
701                    "bound must carry either \"unbounded\": true or a finite value",
702                ))
703            }
704        }
705        other => Err(EngineError::malformed(format!(
706            "unknown tagged value kind {other:?}"
707        ))),
708    }
709}
710
711/// Validate a currency identifier: 2-12 uppercase letters/digits, starting with
712/// a letter. Nonstandard units may be used but must be explicit.
713pub fn validate_currency(currency: &str) -> Result<(), EngineError> {
714    let mut chars = currency.chars();
715    match chars.next() {
716        Some(c) if c.is_ascii_alphabetic() => {}
717        _ => {
718            return Err(EngineError::malformed(format!(
719                "invalid currency identifier {currency:?}"
720            )));
721        }
722    }
723    if currency.len() < 2 || currency.len() > 12 {
724        return Err(EngineError::malformed(format!(
725            "invalid currency identifier {currency:?}: length must be 2-12"
726        )));
727    }
728    if !currency
729        .chars()
730        .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit())
731    {
732        return Err(EngineError::malformed(format!(
733            "invalid currency identifier {currency:?}: expected uppercase letters and digits"
734        )));
735    }
736    Ok(())
737}
738
739impl Serialize for Value {
740    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
741        use serde::ser::SerializeMap;
742        match self {
743            Value::Null => serializer.serialize_none(),
744            Value::Bool(value) => serializer.serialize_bool(*value),
745            Value::Text(text) => serializer.serialize_str(text),
746            Value::Number(number) => number.serialize(serializer),
747            Value::Array(items) => items.serialize(serializer),
748            Value::Record(fields) => fields.serialize(serializer),
749            Value::Quantity { value, dimension } => {
750                let mut map = serializer.serialize_map(Some(3))?;
751                map.serialize_entry("kind", "quantity")?;
752                map.serialize_entry("value", value)?;
753                map.serialize_entry("dimension", dimension)?;
754                map.end()
755            }
756            Value::Money { amount, currency } => {
757                let mut map = serializer.serialize_map(Some(3))?;
758                map.serialize_entry("kind", "money")?;
759                map.serialize_entry("amount", amount)?;
760                map.serialize_entry("currency", currency)?;
761                map.end()
762            }
763            Value::Matrix { rows, cols, data } => {
764                let mut map = serializer.serialize_map(Some(4))?;
765                map.serialize_entry("kind", "matrix")?;
766                map.serialize_entry("rows", rows)?;
767                map.serialize_entry("cols", cols)?;
768                map.serialize_entry("data", data)?;
769                map.end()
770            }
771            Value::Bound(bound) => {
772                let mut map = serializer.serialize_map(Some(2))?;
773                map.serialize_entry("kind", "bound")?;
774                match bound {
775                    Bound::Unbounded => map.serialize_entry("unbounded", &true)?,
776                    Bound::Finite(number) => map.serialize_entry("value", number)?,
777                }
778                map.end()
779            }
780        }
781    }
782}
783
784impl<'de> Deserialize<'de> for Value {
785    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
786        let raw = serde_json::Value::deserialize(deserializer)?;
787        Value::from_json(&raw, &Limits::conservative()).map_err(D::Error::custom)
788    }
789}
790
791impl Serialize for Number {
792    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
793        use serde::ser::SerializeMap;
794        match self {
795            Number::Integer(value) => {
796                let mut map = serializer.serialize_map(Some(2))?;
797                map.serialize_entry("kind", "integer")?;
798                map.serialize_entry("value", &value.to_string())?;
799                map.end()
800            }
801            Number::Decimal(value) => {
802                let mut map = serializer.serialize_map(Some(2))?;
803                map.serialize_entry("kind", "decimal")?;
804                map.serialize_entry("value", &value.to_plain_string())?;
805                map.end()
806            }
807            Number::Rational(value) => {
808                let mut map = serializer.serialize_map(Some(3))?;
809                map.serialize_entry("kind", "rational")?;
810                map.serialize_entry("numerator", &value.numer().to_string())?;
811                map.serialize_entry("denominator", &value.denom().to_string())?;
812                map.end()
813            }
814            Number::Float64(value) => {
815                let mut map = serializer.serialize_map(Some(2))?;
816                map.serialize_entry("kind", "float64")?;
817                map.serialize_entry("value", &value.to_string())?;
818                map.end()
819            }
820        }
821    }
822}
823
824impl<'de> Deserialize<'de> for Number {
825    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
826        let value = Value::deserialize(deserializer)?;
827        match value {
828            Value::Number(number) => Ok(number),
829            other => Err(D::Error::custom(format!(
830                "expected a number, found {}",
831                other.kind_name()
832            ))),
833        }
834    }
835}
836
837#[cfg(test)]
838mod tests {
839    use super::*;
840
841    #[test]
842    fn scalar_round_trip_preserves_exact_payloads() {
843        let raw = serde_json::json!({"kind": "integer", "value": "9007199254740993"});
844        let value: Value = serde_json::from_value(raw.clone()).unwrap();
845        let encoded = serde_json::to_value(&value).unwrap();
846        assert_eq!(encoded, raw);
847    }
848
849    #[test]
850    fn decimal_scale_survives_round_trip() {
851        let raw = serde_json::json!({"kind": "decimal", "value": "0.10"});
852        let value: Value = serde_json::from_value(raw.clone()).unwrap();
853        let encoded = serde_json::to_value(&value).unwrap();
854        assert_eq!(encoded, raw);
855    }
856
857    #[test]
858    fn plain_json_integers_and_decimals_are_exact() {
859        let value: Value = serde_json::from_value(serde_json::json!(9007199254740993u64)).unwrap();
860        assert_eq!(
861            serde_json::to_value(&value).unwrap(),
862            serde_json::json!({"kind": "integer", "value": "9007199254740993"})
863        );
864        let value: Value = serde_json::from_value(serde_json::json!(0.10)).unwrap();
865        assert_eq!(
866            serde_json::to_value(&value).unwrap(),
867            serde_json::json!({"kind": "decimal", "value": "0.1"})
868        );
869    }
870
871    #[test]
872    fn quantity_round_trip() {
873        let raw = serde_json::json!({
874            "kind": "quantity",
875            "value": {"kind": "decimal", "value": "9.81"},
876            "dimension": {"length": 1, "time": -2}
877        });
878        let value: Value = serde_json::from_value(raw.clone()).unwrap();
879        assert_eq!(serde_json::to_value(&value).unwrap(), raw);
880    }
881
882    #[test]
883    fn money_requires_valid_currency() {
884        let raw = serde_json::json!({
885            "kind": "money",
886            "amount": {"kind": "decimal", "value": "10.00"},
887            "currency": "usd"
888        });
889        assert!(serde_json::from_value::<Value>(raw).is_err());
890    }
891
892    #[test]
893    fn record_key_kind_is_rejected() {
894        let raw = serde_json::json!({"kind": "not-a-real-tag", "value": 1});
895        assert!(serde_json::from_value::<Value>(raw).is_err());
896    }
897
898    #[test]
899    fn matrix_shape_is_checked() {
900        let raw = serde_json::json!({
901            "kind": "matrix", "rows": 2, "cols": 2,
902            "data": [1, 2, 3]
903        });
904        assert!(serde_json::from_value::<Value>(raw).is_err());
905    }
906}