1use std::collections::BTreeMap;
34
35use crate::cid::{Cid, CidError};
36use crate::encoding::{base64_decode, base64_encode};
37
38pub const LINK_KEY: &str = "$link";
40
41pub const BYTES_KEY: &str = "$bytes";
43
44pub const TYPE_KEY: &str = "$type";
46
47pub const BLOB_TYPE: &str = "blob";
49
50#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
58pub enum Value {
59 Null,
61 Bool(bool),
63 Integer(i64),
65 String(String),
67 Bytes(Vec<u8>),
69 Link(Cid),
71 List(Vec<Value>),
73 Map(BTreeMap<String, Value>),
76}
77
78#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct DataError {
85 pub path: String,
87 pub kind: ErrorKind,
89}
90
91#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
93pub enum ErrorKind {
94 #[error("the data model requires an object here, not {0}")]
96 NotAnObject(&'static str),
97 #[error("{0} is not an integer the data model can hold")]
99 NotAnInteger(String),
100 #[error("$type must be a non-empty string")]
102 BadType,
103 #[error("{0} must be the only key in its object")]
105 TaggedNotAlone(&'static str),
106 #[error("{0} must hold a string")]
108 TaggedNotAString(&'static str),
109 #[error("$link is not a usable cid: {0}")]
111 Link(#[from] CidError),
112 #[error("$bytes is not valid base64: {0}")]
114 Bytes(#[from] base64::DecodeError),
115 #[error("{0}")]
118 Blob(String),
119}
120
121impl std::fmt::Display for DataError {
122 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123 let where_ = if self.path.is_empty() {
124 "the top level"
125 } else {
126 &self.path
127 };
128 write!(f, "at {where_}: {}", self.kind)
129 }
130}
131
132impl std::error::Error for DataError {
133 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
134 Some(&self.kind)
135 }
136}
137
138impl DataError {
139 fn at(path: &str, kind: impl Into<ErrorKind>) -> Self {
141 Self {
142 path: path.to_owned(),
143 kind: kind.into(),
144 }
145 }
146}
147
148fn field(path: &str, key: &str) -> String {
150 if path.is_empty() {
151 key.to_owned()
152 } else {
153 format!("{path}.{key}")
154 }
155}
156
157fn index(path: &str, position: usize) -> String {
159 format!("{path}[{position}]")
160}
161
162fn json_type(json: &serde_json::Value) -> &'static str {
164 match json {
165 serde_json::Value::Null => "null",
166 serde_json::Value::Bool(_) => "a boolean",
167 serde_json::Value::Number(_) => "a number",
168 serde_json::Value::String(_) => "a string",
169 serde_json::Value::Array(_) => "an array",
170 serde_json::Value::Object(_) => "an object",
171 }
172}
173
174impl Value {
175 pub fn from_json(json: &serde_json::Value) -> Result<Self, DataError> {
184 Self::parse(json, "")
185 }
186
187 pub fn object_from_json(json: &serde_json::Value) -> Result<Self, DataError> {
194 if !json.is_object() {
195 return Err(DataError::at("", ErrorKind::NotAnObject(json_type(json))));
196 }
197 Self::parse(json, "")
198 }
199
200 fn parse(json: &serde_json::Value, path: &str) -> Result<Self, DataError> {
202 match json {
203 serde_json::Value::Null => Ok(Self::Null),
204 serde_json::Value::Bool(value) => Ok(Self::Bool(*value)),
205 serde_json::Value::Number(number) => parse_number(number, path),
206 serde_json::Value::String(text) => Ok(Self::String(text.clone())),
207 serde_json::Value::Array(items) => items
208 .iter()
209 .enumerate()
210 .map(|(position, item)| Self::parse(item, &index(path, position)))
211 .collect::<Result<Vec<_>, _>>()
212 .map(Self::List),
213 serde_json::Value::Object(map) => parse_object(map, path),
214 }
215 }
216
217 pub fn to_json(&self) -> serde_json::Value {
223 match self {
224 Self::Null => serde_json::Value::Null,
225 Self::Bool(value) => serde_json::Value::Bool(*value),
226 Self::Integer(value) => serde_json::Value::Number((*value).into()),
227 Self::String(text) => serde_json::Value::String(text.clone()),
228 Self::Bytes(bytes) => {
229 serde_json::json!({ BYTES_KEY: base64_encode(bytes) })
230 }
231 Self::Link(cid) => serde_json::json!({ LINK_KEY: cid.to_string() }),
232 Self::List(items) => {
233 serde_json::Value::Array(items.iter().map(Self::to_json).collect())
234 }
235 Self::Map(entries) => serde_json::Value::Object(
236 entries
237 .iter()
238 .map(|(key, value)| (key.clone(), value.to_json()))
239 .collect(),
240 ),
241 }
242 }
243}
244
245fn parse_number(number: &serde_json::Number, path: &str) -> Result<Value, DataError> {
247 if let Some(value) = number.as_i64() {
248 return Ok(Value::Integer(value));
249 }
250 if let Some(value) = number.as_f64() {
256 if value.fract() == 0.0 && value >= -(2f64.powi(63)) && value < 2f64.powi(63) {
257 return Ok(Value::Integer(value as i64));
258 }
259 }
260 Err(DataError::at(
261 path,
262 ErrorKind::NotAnInteger(number.to_string()),
263 ))
264}
265
266fn parse_object(
268 map: &serde_json::Map<String, serde_json::Value>,
269 path: &str,
270) -> Result<Value, DataError> {
271 if let Some(value) = map.get(LINK_KEY) {
272 return parse_link(map, value, path);
273 }
274 if let Some(value) = map.get(BYTES_KEY) {
275 return parse_bytes(map, value, path);
276 }
277 if let Some(declared) = map.get(TYPE_KEY) {
278 match declared.as_str() {
279 Some(BLOB_TYPE) => return parse_blob(map, path),
280 Some(name) if !name.is_empty() => {}
281 _ => return Err(DataError::at(path, ErrorKind::BadType)),
282 }
283 }
284 let mut entries = BTreeMap::new();
285 for (key, value) in map {
286 entries.insert(key.clone(), Value::parse(value, &field(path, key))?);
287 }
288 Ok(Value::Map(entries))
289}
290
291fn parse_link(
293 map: &serde_json::Map<String, serde_json::Value>,
294 value: &serde_json::Value,
295 path: &str,
296) -> Result<Value, DataError> {
297 if map.len() != 1 {
298 return Err(DataError::at(path, ErrorKind::TaggedNotAlone(LINK_KEY)));
299 }
300 let text = value
301 .as_str()
302 .ok_or_else(|| DataError::at(path, ErrorKind::TaggedNotAString(LINK_KEY)))?;
303 Cid::parse(text)
304 .map(Value::Link)
305 .map_err(|err| DataError::at(path, err))
306}
307
308fn parse_bytes(
310 map: &serde_json::Map<String, serde_json::Value>,
311 value: &serde_json::Value,
312 path: &str,
313) -> Result<Value, DataError> {
314 if map.len() != 1 {
315 return Err(DataError::at(path, ErrorKind::TaggedNotAlone(BYTES_KEY)));
316 }
317 let text = value
318 .as_str()
319 .ok_or_else(|| DataError::at(path, ErrorKind::TaggedNotAString(BYTES_KEY)))?;
320 base64_decode(text)
321 .map(Value::Bytes)
322 .map_err(|err| DataError::at(path, err))
323}
324
325const BLOB_KEYS: [&str; 4] = [TYPE_KEY, "ref", "mimeType", "size"];
327
328fn parse_blob(
338 map: &serde_json::Map<String, serde_json::Value>,
339 path: &str,
340) -> Result<Value, DataError> {
341 let blob = |reason: String| DataError::at(path, ErrorKind::Blob(reason));
342 for key in map.keys() {
343 if !BLOB_KEYS.contains(&key.as_str()) {
344 return Err(blob(format!("a blob has no {key:?} field")));
345 }
346 }
347 for key in BLOB_KEYS {
348 if !map.contains_key(key) {
349 return Err(blob(format!("a blob needs a {key:?} field")));
350 }
351 }
352 let is_a_link = || blob("a blob's ref must be a $link".to_owned());
353 let referenced = map["ref"].as_object().ok_or_else(is_a_link)?;
354 let link = referenced.get(LINK_KEY).ok_or_else(is_a_link)?;
355 let reference = parse_link(referenced, link, &field(path, "ref"))?;
356 if !map["mimeType"].is_string() {
357 return Err(blob("a blob's mimeType must be a string".to_owned()));
358 }
359 let size = parse_number(
360 map["size"]
361 .as_number()
362 .ok_or_else(|| blob("a blob's size must be an integer".to_owned()))?,
363 &field(path, "size"),
364 )?;
365 if !matches!(size, Value::Integer(bytes) if bytes >= 0) {
366 return Err(blob("a blob's size must be an integer".to_owned()));
367 }
368
369 let mut entries = BTreeMap::new();
370 entries.insert(TYPE_KEY.to_owned(), Value::String(BLOB_TYPE.to_owned()));
371 entries.insert("ref".to_owned(), reference);
372 entries.insert(
373 "mimeType".to_owned(),
374 Value::String(map["mimeType"].as_str().unwrap_or_default().to_owned()),
375 );
376 entries.insert("size".to_owned(), size);
377 Ok(Value::Map(entries))
378}
379
380#[cfg(test)]
381mod tests {
382 use super::*;
383 use serde_json::json;
384
385 fn parse(json: serde_json::Value) -> Result<Value, DataError> {
386 Value::object_from_json(&json)
387 }
388
389 #[test]
390 fn an_integer_written_as_a_float_is_an_integer() {
391 assert_eq!(
392 parse(json!({"a": 123.0})),
393 parse(json!({"a": 123})),
394 "the data model has one number, so these are one value"
395 );
396 }
397
398 #[test]
399 fn a_number_past_i64_has_nowhere_to_go() {
400 assert!(Value::from_json(&json!(1e300)).is_err());
401 }
402
403 #[test]
404 fn the_error_names_where_it_was() {
405 let err = parse(json!({"a": {"b": [1, 1.5]}})).unwrap_err();
406 assert_eq!(err.path, "a.b[1]");
407 }
408
409 #[test]
410 fn a_link_beside_another_key_is_refused() {
411 let err = parse(json!({
412 "l": {LINK_KEY: "bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a", "x": 1}
413 }))
414 .unwrap_err();
415 assert_eq!(err.kind, ErrorKind::TaggedNotAlone(LINK_KEY));
416 }
417
418 #[test]
419 fn a_blob_keeps_only_the_four_fields_it_has() {
420 let blob = json!({
421 "b": {
422 TYPE_KEY: "blob",
423 "ref": {LINK_KEY: "bafkreiccldh766hwcnuxnf2wh6jgzepf2nlu2lvcllt63eww5p6chi4ity"},
424 "mimeType": "image/jpeg",
425 "size": 10000,
426 }
427 });
428 let parsed = parse(blob.clone()).expect("a well-formed blob");
429 assert_eq!(parsed.to_json(), blob);
430 }
431
432 #[test]
433 fn a_negative_blob_size_is_not_a_size() {
434 let err = parse(json!({
435 "b": {
436 TYPE_KEY: "blob",
437 "ref": {LINK_KEY: "bafkreiccldh766hwcnuxnf2wh6jgzepf2nlu2lvcllt63eww5p6chi4ity"},
438 "mimeType": "image/jpeg",
439 "size": -1,
440 }
441 }))
442 .unwrap_err();
443 assert!(matches!(err.kind, ErrorKind::Blob(_)));
444 }
445
446 #[test]
447 fn a_type_that_is_not_blob_is_left_alone_beyond_being_a_string() {
448 let record = json!({TYPE_KEY: "com.example.blah", "a": 1, "b": "blah"});
449 assert_eq!(parse(record.clone()).unwrap().to_json(), record);
450 }
451
452 #[test]
453 fn bytes_round_trip_through_the_json_representation() {
454 let value = json!({"b": {BYTES_KEY: "nFERjvLLiw9qm45JrqH9QTzyC2Lu1Xb4ne6+sBrCzI0"}});
455 assert_eq!(parse(value.clone()).unwrap().to_json(), value);
456 }
457}