Skip to main content

mars_schema/
ordered.rs

1//! A JSON-shaped value that keeps mapping key order.
2//!
3//! The reading carries authored values (frontmatter, harness overrides, metadata) and the
4//! emitter takes them back; both must preserve the writer's key order, which
5//! `serde_json::Value` does not (and `serde_json/preserve_order` would change every JSON
6//! output of the binary through feature unification).
7
8use std::fmt;
9
10use indexmap::IndexMap;
11use serde::de::{self, Deserialize, Deserializer, MapAccess, SeqAccess, Visitor};
12use serde::ser::{Serialize, SerializeMap, SerializeSeq, Serializer};
13use serde_json::Number;
14
15/// Largest integer magnitude JavaScript represents unambiguously (`Number.MAX_SAFE_INTEGER`):
16/// 2^53 is also what 2^53 + 1 rounds to.
17pub(crate) const MAX_SAFE_INTEGER: u64 = (1 << 53) - 1;
18
19#[derive(Debug, Clone, PartialEq)]
20pub enum OrderedValue {
21    Null,
22    Bool(bool),
23    Number(Number),
24    String(String),
25    Array(Vec<OrderedValue>),
26    /// Keys in authored order. Equality ignores order; serialization does not.
27    Object(IndexMap<String, OrderedValue>),
28}
29
30impl OrderedValue {
31    /// The same value as a `serde_json::Value` (mapping keys then sort).
32    pub fn to_json(&self) -> serde_json::Value {
33        match self {
34            Self::Null => serde_json::Value::Null,
35            Self::Bool(v) => (*v).into(),
36            Self::Number(v) => serde_json::Value::Number(v.clone()),
37            Self::String(v) => v.clone().into(),
38            Self::Array(values) => values.iter().map(Self::to_json).collect(),
39            Self::Object(map) => map
40                .iter()
41                .map(|(key, value)| (key.clone(), value.to_json()))
42                .collect::<serde_json::Map<_, _>>()
43                .into(),
44        }
45    }
46
47    /// Convert YAML, refusing what JSON cannot hold: tags, non-string keys, and integers
48    /// beyond ±(2^53 − 1).
49    pub(crate) fn from_yaml(value: &serde_yaml::Value) -> Result<Self, String> {
50        use serde_yaml::Value;
51        Ok(match value {
52            Value::Null => Self::Null,
53            Value::Bool(v) => Self::Bool(*v),
54            Value::Number(v) => {
55                let beyond = v
56                    .as_i64()
57                    .is_some_and(|n| n.unsigned_abs() > MAX_SAFE_INTEGER)
58                    || v.as_u64().is_some_and(|n| n > MAX_SAFE_INTEGER);
59                if beyond {
60                    return Err(format!(
61                        "integer {v} is beyond ±(2^53 − 1), which JavaScript cannot hold exactly"
62                    ));
63                }
64                Self::Number(
65                    serde_json::to_value(v)
66                        .map_err(|e| e.to_string())
67                        .and_then(|n| match n {
68                            serde_json::Value::Number(n) => Ok(n),
69                            _ => Err("YAML number has no JSON form (NaN or infinity)".to_string()),
70                        })?,
71                )
72            }
73            Value::String(v) => Self::String(v.clone()),
74            Value::Sequence(values) => Self::Array(
75                values
76                    .iter()
77                    .map(Self::from_yaml)
78                    .collect::<Result<_, _>>()?,
79            ),
80            Value::Mapping(values) => {
81                let mut out = IndexMap::with_capacity(values.len());
82                for (key, value) in values {
83                    let Some(key) = key.as_str() else {
84                        return Err("YAML mapping contains a non-string key".into());
85                    };
86                    reject_unholdable_key(key)?;
87                    out.insert(key.to_owned(), Self::from_yaml(value)?);
88                }
89                Self::Object(out)
90            }
91            Value::Tagged(_) => return Err("YAML tags have no JSON representation".into()),
92        })
93    }
94}
95
96/// `__proto__` set on a plain JavaScript object changes its prototype instead of adding a key,
97/// so a reading could not carry it.
98pub(crate) fn reject_unholdable_key(key: &str) -> Result<(), String> {
99    if key == "__proto__" {
100        Err("key `__proto__` cannot be held as a plain property by JavaScript".into())
101    } else {
102        Ok(())
103    }
104}
105
106impl Serialize for OrderedValue {
107    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
108        match self {
109            Self::Null => serializer.serialize_unit(),
110            Self::Bool(v) => serializer.serialize_bool(*v),
111            Self::Number(v) => v.serialize(serializer),
112            Self::String(v) => serializer.serialize_str(v),
113            Self::Array(values) => {
114                let mut seq = serializer.serialize_seq(Some(values.len()))?;
115                for value in values {
116                    seq.serialize_element(value)?;
117                }
118                seq.end()
119            }
120            Self::Object(map) => {
121                let mut out = serializer.serialize_map(Some(map.len()))?;
122                for (key, value) in map {
123                    out.serialize_entry(key, value)?;
124                }
125                out.end()
126            }
127        }
128    }
129}
130
131/// Reads any JSON-shaped input in stream order. NaN and Infinity are refused rather than
132/// turned into `null`.
133impl<'de> Deserialize<'de> for OrderedValue {
134    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
135        deserializer.deserialize_any(OrderedVisitor)
136    }
137}
138
139struct OrderedVisitor;
140
141impl<'de> Visitor<'de> for OrderedVisitor {
142    type Value = OrderedValue;
143
144    fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
145        f.write_str("a JSON value")
146    }
147
148    fn visit_bool<E>(self, v: bool) -> Result<OrderedValue, E> {
149        Ok(OrderedValue::Bool(v))
150    }
151    fn visit_i64<E>(self, v: i64) -> Result<OrderedValue, E> {
152        Ok(OrderedValue::Number(v.into()))
153    }
154    fn visit_u64<E>(self, v: u64) -> Result<OrderedValue, E> {
155        Ok(OrderedValue::Number(v.into()))
156    }
157    fn visit_i128<E: de::Error>(self, v: i128) -> Result<OrderedValue, E> {
158        i64::try_from(v)
159            .map(|v| OrderedValue::Number(v.into()))
160            .map_err(|_| E::custom("integer is beyond ±(2^53 − 1)"))
161    }
162    fn visit_u128<E: de::Error>(self, v: u128) -> Result<OrderedValue, E> {
163        u64::try_from(v)
164            .map(|v| OrderedValue::Number(v.into()))
165            .map_err(|_| E::custom("integer is beyond ±(2^53 − 1)"))
166    }
167    fn visit_f64<E: de::Error>(self, v: f64) -> Result<OrderedValue, E> {
168        Number::from_f64(v)
169            .map(OrderedValue::Number)
170            .ok_or_else(|| E::custom("NaN and Infinity cannot be written as YAML"))
171    }
172    fn visit_str<E>(self, v: &str) -> Result<OrderedValue, E> {
173        Ok(OrderedValue::String(v.to_owned()))
174    }
175    fn visit_unit<E>(self) -> Result<OrderedValue, E> {
176        Ok(OrderedValue::Null)
177    }
178    fn visit_none<E>(self) -> Result<OrderedValue, E> {
179        Ok(OrderedValue::Null)
180    }
181    fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<OrderedValue, A::Error> {
182        let mut out = Vec::new();
183        while let Some(value) = seq.next_element()? {
184            out.push(value);
185        }
186        Ok(OrderedValue::Array(out))
187    }
188    fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<OrderedValue, A::Error> {
189        let mut out = IndexMap::new();
190        while let Some((key, value)) = map.next_entry::<String, OrderedValue>()? {
191            out.insert(key, value);
192        }
193        Ok(OrderedValue::Object(out))
194    }
195}
196
197/// A YAML value as plain text for a diagnostic: compact JSON when it has a JSON form (strings
198/// quoted, so an empty or multi-line one stays visible), else its YAML text. Never Rust `Debug`.
199pub(crate) fn plain(value: &serde_yaml::Value) -> String {
200    match OrderedValue::from_yaml(value) {
201        Ok(json) => serde_json::to_string(&json).unwrap_or_default(),
202        Err(_) => serde_yaml::to_string(value)
203            .map(|text| text.trim_end().to_owned())
204            .unwrap_or_default(),
205    }
206}