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
19fn 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 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 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}