convex/value/json/
mod.rs

1use std::{
2    cmp::Ordering,
3    collections::BTreeMap,
4    num::FpCategory,
5};
6
7use anyhow::Context;
8use serde_json::{
9    json,
10    Value as JsonValue,
11};
12
13use crate::value::Value;
14
15mod bytes;
16mod float;
17mod integer;
18
19/// Is a floating point number native zero?
20fn is_negative_zero(n: f64) -> bool {
21    matches!(n.total_cmp(&-0.0), Ordering::Equal)
22}
23
24impl From<Value> for JsonValue {
25    fn from(value: Value) -> JsonValue {
26        match value {
27            Value::Null => JsonValue::Null,
28            Value::Int64(n) => json!({ "$integer": integer::JsonInteger::encode(n) }),
29            Value::Float64(n) => {
30                let mut is_special = is_negative_zero(n);
31                is_special |= match n.classify() {
32                    FpCategory::Zero | FpCategory::Normal | FpCategory::Subnormal => false,
33                    FpCategory::Infinite | FpCategory::Nan => true,
34                };
35                if is_special {
36                    json!({ "$float": float::JsonFloat::encode(n) })
37                } else {
38                    json!(n)
39                }
40            },
41            Value::Boolean(b) => json!(b),
42            Value::String(s) => json!(s),
43            Value::Bytes(b) => json!({ "$bytes": bytes::JsonBytes::encode(&b) }),
44            Value::Array(a) => JsonValue::from(a),
45            Value::Object(o) => o.into_iter().collect(),
46        }
47    }
48}
49
50impl TryFrom<JsonValue> for Value {
51    type Error = anyhow::Error;
52
53    fn try_from(value: JsonValue) -> anyhow::Result<Self> {
54        let r = match value {
55            JsonValue::Null => Self::Null,
56            JsonValue::Bool(b) => Self::from(b),
57            JsonValue::Number(n) => {
58                // TODO: JSON supports arbitrary precision numbers?
59                let n = n
60                    .as_f64()
61                    .context("Arbitrary precision JSON integers unsupported")?;
62                Value::from(n)
63            },
64            JsonValue::String(s) => Self::from(s),
65            JsonValue::Array(arr) => {
66                let mut out = Vec::with_capacity(arr.len());
67                for a in arr {
68                    out.push(Value::try_from(a)?);
69                }
70                Value::Array(out)
71            },
72            JsonValue::Object(map) => {
73                if map.len() == 1 {
74                    let (key, value) = map.into_iter().next().unwrap();
75                    match &key[..] {
76                        "$bytes" => {
77                            let i: String = serde_json::from_value(value)?;
78                            Self::Bytes(bytes::JsonBytes::decode(i)?)
79                        },
80                        "$integer" => {
81                            let i: String = serde_json::from_value(value)?;
82                            Self::from(integer::JsonInteger::decode(i)?)
83                        },
84                        "$float" => {
85                            let i: String = serde_json::from_value(value)?;
86                            let n = float::JsonFloat::decode(i)?;
87                            // Float64s encoded as a $float object must not fit into a regular
88                            // `number`.
89                            if !is_negative_zero(n) {
90                                if let FpCategory::Normal | FpCategory::Subnormal = n.classify() {
91                                    anyhow::bail!("Float64 {} should be encoded as a number", n);
92                                }
93                            }
94                            Self::from(n)
95                        },
96                        "$set" => {
97                            anyhow::bail!(
98                                "Received a Set which is no longer supported as a Convex type, \
99                                 with values: {value}"
100                            );
101                        },
102                        "$map" => {
103                            anyhow::bail!(
104                                "Received a Map which is no longer supported as a Convex type, \
105                                 with values: {value}"
106                            );
107                        },
108                        _ => {
109                            let mut fields = BTreeMap::new();
110                            fields.insert(key, Self::try_from(value)?);
111                            Self::Object(fields)
112                        },
113                    }
114                } else {
115                    let mut fields = BTreeMap::new();
116                    for (key, value) in map {
117                        fields.insert(key, Self::try_from(value)?);
118                    }
119                    Self::Object(fields)
120                }
121            },
122        };
123        Ok(r)
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use convex_sync_types::testing::assert_roundtrips;
130    use proptest::prelude::*;
131    use serde_json::Value as JsonValue;
132
133    use crate::Value;
134
135    proptest! {
136        #![proptest_config(
137            ProptestConfig { failure_persistence: None, ..ProptestConfig::default() }
138        )]
139
140        #[test]
141        fn test_value_roundtrips(value in any::<Value>()) {
142            assert_roundtrips::<Value, JsonValue>(value);
143        }
144    }
145
146    #[test]
147    fn test_value_roundtrips_trophies() {
148        let trophies = vec![
149            Value::Float64(1.0),
150            Value::Float64(f64::NAN),
151            Value::Array(vec![Value::Float64(f64::NAN)]),
152        ];
153        for trophy in trophies {
154            assert_roundtrips::<Value, JsonValue>(trophy);
155        }
156    }
157}