use std::collections::BTreeMap;
use ipld_core::ipld::Ipld;
use serde_json::Value;
use thiserror::Error;
pub const IPLD_MAX_DEPTH: usize = 64;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum JsonIpldError {
#[error("json_to_ipld: nesting exceeds depth cap of {cap}")]
DepthExceeded {
cap: usize,
},
#[error("json_to_ipld: unsigned integer {value} exceeds i64::MAX; send as a string if id-like")]
UnsignedOverflow {
value: String,
},
#[error("json_to_ipld: unsupported JSON number {value}")]
UnsupportedNumber {
value: String,
},
}
pub fn json_to_ipld(v: &Value) -> Result<Ipld, JsonIpldError> {
json_to_ipld_at(v, 0)
}
fn json_to_ipld_at(v: &Value, depth: usize) -> Result<Ipld, JsonIpldError> {
if depth >= IPLD_MAX_DEPTH {
return Err(JsonIpldError::DepthExceeded {
cap: IPLD_MAX_DEPTH,
});
}
Ok(match v {
Value::Null => Ipld::Null,
Value::Bool(b) => Ipld::Bool(*b),
Value::Number(n) => {
if let Some(i) = n.as_i64() {
Ipld::Integer(i128::from(i))
} else if n.is_u64() {
return Err(JsonIpldError::UnsignedOverflow {
value: n.to_string(),
});
} else if let Some(f) = n.as_f64() {
Ipld::Float(f)
} else {
return Err(JsonIpldError::UnsupportedNumber {
value: n.to_string(),
});
}
}
Value::String(s) => Ipld::String(s.clone()),
Value::Array(xs) => Ipld::List(
xs.iter()
.map(|x| json_to_ipld_at(x, depth + 1))
.collect::<Result<Vec<_>, _>>()?,
),
Value::Object(m) => {
let mut out = BTreeMap::new();
for (k, v) in m {
out.insert(k.clone(), json_to_ipld_at(v, depth + 1)?);
}
Ipld::Map(out)
}
})
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn null_bool_string_roundtrip() {
assert_eq!(json_to_ipld(&Value::Null).unwrap(), Ipld::Null);
assert_eq!(json_to_ipld(&json!(true)).unwrap(), Ipld::Bool(true));
assert_eq!(
json_to_ipld(&json!("hello")).unwrap(),
Ipld::String("hello".to_string())
);
}
#[test]
fn i64_as_integer() {
assert_eq!(
json_to_ipld(&json!(42_i64)).unwrap(),
Ipld::Integer(42_i128)
);
assert_eq!(
json_to_ipld(&json!(i64::MIN)).unwrap(),
Ipld::Integer(i128::from(i64::MIN))
);
assert_eq!(
json_to_ipld(&json!(i64::MAX)).unwrap(),
Ipld::Integer(i128::from(i64::MAX))
);
}
#[test]
fn u64_gt_i64_max_rejected() {
let err = json_to_ipld(&json!(u64::MAX)).unwrap_err();
assert!(matches!(err, JsonIpldError::UnsignedOverflow { .. }));
}
#[test]
fn float_preserved() {
assert_eq!(json_to_ipld(&json!(1.5_f64)).unwrap(), Ipld::Float(1.5));
}
#[test]
fn deeply_nested_rejected() {
let mut v = Value::Null;
for _ in 0..128 {
v = Value::Array(vec![v]);
}
let err = json_to_ipld(&v).unwrap_err();
assert!(matches!(
err,
JsonIpldError::DepthExceeded {
cap: IPLD_MAX_DEPTH
}
));
}
#[test]
fn nested_map_respects_cap() {
let mut v = Value::Null;
for _ in 0..65 {
let mut m = serde_json::Map::new();
m.insert("a".into(), v);
v = Value::Object(m);
}
let err = json_to_ipld(&v).unwrap_err();
assert!(matches!(err, JsonIpldError::DepthExceeded { .. }));
}
#[test]
fn shallow_nesting_ok() {
let mut v = Value::Null;
for _ in 0..10 {
v = Value::Array(vec![v]);
}
let _ = json_to_ipld(&v).unwrap();
}
#[test]
fn array_and_object_mixed() {
let v = json!({
"name": "a",
"xs": [1, 2, 3],
"meta": { "kind": "note", "active": true }
});
let out = json_to_ipld(&v).unwrap();
let Ipld::Map(m) = out else {
panic!("expected map");
};
assert!(m.contains_key("name"));
assert!(m.contains_key("xs"));
assert!(m.contains_key("meta"));
}
}