1use serde_json::Value as Json;
21
22use crate::date::ParseDate;
23use crate::error::{ErrorCode, ParseError};
24use crate::value::{base64_decode, ParseMap, ParseValue};
25
26pub fn classify(value: Json) -> Result<ParseValue, ParseError> {
31 classify_at(value, true)
32}
33
34pub fn classify_nested(value: Json) -> Result<ParseValue, ParseError> {
39 classify_at(value, false)
40}
41
42fn classify_at(value: Json, top_level: bool) -> Result<ParseValue, ParseError> {
43 match value {
44 Json::Null => Ok(ParseValue::Null),
45 Json::Bool(b) => Ok(ParseValue::Bool(b)),
46 Json::Number(n) => n
47 .as_f64()
48 .map(ParseValue::Number)
49 .ok_or_else(|| ParseError::invalid_json(format!("number out of range: {n}"))),
50 Json::String(s) => Ok(ParseValue::String(s)),
51 Json::Array(items) => items
52 .into_iter()
53 .map(classify_nested)
54 .collect::<Result<Vec<_>, _>>()
55 .map(ParseValue::Array),
56 Json::Object(map) => classify_object(map, top_level),
57 }
58}
59
60fn classify_object(
61 map: serde_json::Map<String, Json>,
62 top_level: bool,
63) -> Result<ParseValue, ParseError> {
64 let tag = match map.get("__type") {
65 Some(Json::String(t)) => t.clone(),
66 _ => return plain_object(map),
69 };
70
71 match tag.as_str() {
72 "Date" => {
73 let iso = require_str(&map, "iso", "Date")?;
74 Ok(ParseValue::Date(ParseDate::parse_iso(iso)?))
75 }
76 "Pointer" => Ok(ParseValue::Pointer {
77 class_name: require_str(&map, "className", "Pointer")?.to_string(),
78 object_id: require_str(&map, "objectId", "Pointer")?.to_string(),
79 }),
80 "GeoPoint" => Ok(ParseValue::GeoPoint {
81 latitude: require_f64(&map, "latitude", "GeoPoint")?,
82 longitude: require_f64(&map, "longitude", "GeoPoint")?,
83 }),
84 "Bytes" => {
85 let b64 = require_str(&map, "base64", "Bytes")?;
86 base64_decode(b64)
87 .map(ParseValue::Bytes)
88 .ok_or_else(|| ParseError::incorrect_type("invalid base64 in Bytes".to_string()))
89 }
90 "File" => Ok(ParseValue::File {
91 name: require_str(&map, "name", "File")?.to_string(),
92 url: match map.get("url") {
93 Some(Json::String(u)) => Some(u.clone()),
94 _ => None,
95 },
96 }),
97 "Polygon" => {
98 let coords = match map.get("coordinates") {
99 Some(Json::Array(a)) => a,
100 _ => {
101 return Err(ParseError::incorrect_type(
102 "Polygon requires a coordinates array".to_string(),
103 ))
104 }
105 };
106 let mut out = Vec::with_capacity(coords.len());
107 for pair in coords {
108 match pair {
109 Json::Array(p) if p.len() == 2 => {
111 let lat = p[0].as_f64();
112 let lng = p[1].as_f64();
113 match (lat, lng) {
114 (Some(a), Some(b)) => out.push((a, b)),
115 _ => {
116 return Err(ParseError::incorrect_type(
117 "Polygon coordinates must be numbers".to_string(),
118 ))
119 }
120 }
121 }
122 _ => {
123 return Err(ParseError::incorrect_type(
124 "Polygon coordinates must be [latitude, longitude] pairs".to_string(),
125 ))
126 }
127 }
128 }
129 Ok(ParseValue::Polygon(out))
130 }
131 "Relation" => Ok(ParseValue::Relation {
132 class_name: require_str(&map, "className", "Relation")?.to_string(),
133 }),
134 other => {
135 if top_level {
136 Err(ParseError::new(
138 ErrorCode::IncorrectType,
139 format!("invalid type: {other}"),
140 ))
141 } else {
142 plain_object(map)
144 }
145 }
146 }
147}
148
149fn plain_object(map: serde_json::Map<String, Json>) -> Result<ParseValue, ParseError> {
150 let mut out = ParseMap::with_capacity(map.len());
151 for (k, v) in map {
152 out.insert(k, classify_nested(v)?);
153 }
154 Ok(ParseValue::Object(out))
155}
156
157fn require_str<'a>(
158 map: &'a serde_json::Map<String, Json>,
159 key: &str,
160 tag: &str,
161) -> Result<&'a str, ParseError> {
162 match map.get(key) {
163 Some(Json::String(s)) => Ok(s),
164 _ => Err(ParseError::incorrect_type(format!(
165 "{tag} requires a string {key}"
166 ))),
167 }
168}
169
170fn require_f64(
171 map: &serde_json::Map<String, Json>,
172 key: &str,
173 tag: &str,
174) -> Result<f64, ParseError> {
175 match map.get(key).and_then(|v| v.as_f64()) {
176 Some(n) => Ok(n),
177 None => Err(ParseError::incorrect_type(format!(
178 "{tag} requires a numeric {key}"
179 ))),
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186 use crate::value::deep_strict_eq;
187
188 fn j(s: &str) -> Json {
189 serde_json::from_str(s).expect("test literal must be valid JSON")
190 }
191
192 fn round_trips(src: &str) {
194 let v = classify(j(src)).expect("classify failed");
195 let encoded = v.to_json();
196 assert_eq!(encoded, src, "encoding changed the bytes");
197 let again = classify(j(&encoded)).expect("re-classify failed");
198 assert!(deep_strict_eq(&v, &again), "value changed on round trip");
199 }
200
201 #[test]
202 fn primitives_round_trip() {
203 for s in [
204 "null",
205 "true",
206 "false",
207 "0",
208 "100",
209 "-1.5",
210 "0.000001",
211 "100000000000000000000",
212 r#""hello""#,
213 r#""with \"quotes\" and \n""#,
214 "[]",
215 "[1,2,3]",
216 "{}",
217 ] {
218 round_trips(s);
219 }
220 }
221
222 #[test]
223 fn tagged_types_round_trip() {
224 round_trips(r#"{"__type":"Date","iso":"2026-08-14T13:34:33.581Z"}"#);
225 round_trips(r#"{"__type":"Pointer","className":"_User","objectId":"abc123"}"#);
226 round_trips(r#"{"__type":"GeoPoint","latitude":40,"longitude":-75.5}"#);
227 round_trips(r#"{"__type":"Bytes","base64":"aGVsbG8="}"#);
228 round_trips(r#"{"__type":"File","name":"a.png","url":"http://x/a.png"}"#);
229 round_trips(r#"{"__type":"File","name":"a.png"}"#);
230 round_trips(r#"{"__type":"Polygon","coordinates":[[0,0],[1,0],[1,1],[0,0]]}"#);
231 round_trips(r#"{"__type":"Relation","className":"Post"}"#);
232 }
233
234 #[test]
235 fn bytes_decode_to_real_octets() {
236 let v = classify(j(r#"{"__type":"Bytes","base64":"aGVsbG8="}"#)).unwrap();
237 match v {
238 ParseValue::Bytes(b) => assert_eq!(b, b"hello"),
239 other => panic!("expected Bytes, got {other:?}"),
240 }
241 }
242
243 #[test]
244 fn object_key_order_survives_decoding() {
245 let src = r#"{"zebra":1,"apple":2,"mango":3}"#;
246 let v = classify(j(src)).unwrap();
247 assert_eq!(v.to_json(), src, "key order must survive the decoder too");
248 }
249
250 #[test]
253 fn unknown_type_is_rejected_at_top_level_and_kept_when_nested() {
254 let err = classify(j(r#"{"__type":"Wat","x":1}"#)).unwrap_err();
255 assert_eq!(err.code, ErrorCode::IncorrectType);
256
257 let nested = classify(j(r#"{"field":{"__type":"Wat","x":1}}"#)).unwrap();
259 assert_eq!(nested.to_json(), r#"{"field":{"__type":"Wat","x":1}}"#);
260
261 let in_array = classify(j(r#"[{"__type":"Wat"}]"#)).unwrap();
263 assert_eq!(in_array.to_json(), r#"[{"__type":"Wat"}]"#);
264 }
265
266 #[test]
267 fn a_non_string_type_tag_is_just_an_object() {
268 let v = classify(j(r#"{"__type":7}"#)).unwrap();
270 assert_eq!(v.to_json(), r#"{"__type":7}"#);
271 }
272
273 #[test]
274 fn malformed_tagged_values_carry_the_right_code() {
275 for (src, code) in [
276 (
277 r#"{"__type":"Pointer","className":"A"}"#,
278 ErrorCode::IncorrectType,
279 ),
280 (
281 r#"{"__type":"GeoPoint","latitude":"x","longitude":1}"#,
282 ErrorCode::IncorrectType,
283 ),
284 (
285 r#"{"__type":"Bytes","base64":"not base64!!"}"#,
286 ErrorCode::IncorrectType,
287 ),
288 (
289 r#"{"__type":"Polygon","coordinates":[[1]]}"#,
290 ErrorCode::IncorrectType,
291 ),
292 (
293 r#"{"__type":"Date","iso":"nonsense"}"#,
294 ErrorCode::InvalidJson,
295 ),
296 ] {
297 let e = classify(j(src)).unwrap_err();
298 assert_eq!(e.code, code, "wrong code for {src}");
299 }
300 }
301
302 #[test]
303 fn null_is_a_value_not_an_absence() {
304 let v = classify(j(r#"{"a":null}"#)).unwrap();
306 assert_eq!(v.to_json(), r#"{"a":null}"#);
307 }
308
309 #[test]
310 fn deeply_nested_structures_survive() {
311 let src = r#"{"a":[{"b":[{"__type":"Pointer","className":"C","objectId":"x"}]}]}"#;
312 round_trips(src);
313 }
314}