use indexmap::IndexMap;
use serde_json::Value as JsonValue;
use crate::validation::data_errors::DataValidationError;
use crate::validation::data_types::{Blob, Bytes, CIDLink, DataValue};
use crate::validation::flags::ValidateFlags;
pub fn parse_json(
value: &JsonValue,
flags: ValidateFlags,
) -> Result<DataValue, DataValidationError> {
match value {
JsonValue::Null => Ok(DataValue::Null),
JsonValue::Bool(b) => Ok(DataValue::Boolean(*b)),
JsonValue::Number(n) => {
if let Some(i) = n.as_i64() {
Ok(DataValue::Integer(i))
} else if let Some(f) = n.as_f64() {
if f.is_nan() || f.is_infinite() {
return Err(DataValidationError::DataModelInvalid {
reason: "NaN and Infinity are not valid in ATProtocol data model"
.to_string(),
});
}
Ok(DataValue::Float(f))
} else {
Err(DataValidationError::DataModelInvalid {
reason: "Invalid number value".to_string(),
})
}
}
JsonValue::String(s) => Ok(DataValue::String(s.clone())),
JsonValue::Array(arr) => {
let values: Result<Vec<DataValue>, DataValidationError> =
arr.iter().map(|v| parse_json(v, flags)).collect();
Ok(DataValue::Array(values?))
}
JsonValue::Object(obj) => parse_object(obj, flags),
}
}
fn parse_object(
obj: &serde_json::Map<String, JsonValue>,
flags: ValidateFlags,
) -> Result<DataValue, DataValidationError> {
if let Some(bytes_val) = obj.get("$bytes") {
if obj.len() != 1 {
return Err(DataValidationError::DataModelInvalid {
reason: "$bytes object must only have $bytes field".to_string(),
});
}
let bytes_str =
bytes_val
.as_str()
.ok_or_else(|| DataValidationError::DataModelInvalid {
reason: "$bytes value must be a string".to_string(),
})?;
return Ok(DataValue::Bytes(Bytes::new(bytes_str)));
}
if let Some(link_val) = obj.get("$link") {
if obj.len() != 1 {
return Err(DataValidationError::DataModelInvalid {
reason: "$link object must only have $link field".to_string(),
});
}
let link_str = link_val
.as_str()
.ok_or_else(|| DataValidationError::DataModelInvalid {
reason: "$link value must be a string".to_string(),
})?;
return Ok(DataValue::Link(CIDLink::new(link_str)));
}
if let Some(type_val) = obj.get("$type") {
let type_str = type_val
.as_str()
.ok_or_else(|| DataValidationError::DataModelInvalid {
reason: "$type value must be a string".to_string(),
})?;
if type_str == "blob" {
return parse_blob(obj, flags);
}
}
let mut result = IndexMap::new();
for (key, value) in obj {
if key.starts_with('$') && key != "$type" {
return Err(DataValidationError::DataModelInvalid {
reason: format!(
"Unknown $ field '{}' - only $type, $link, and $bytes are allowed",
key
),
});
}
result.insert(key.clone(), parse_json(value, flags)?);
}
Ok(DataValue::Object(result))
}
fn parse_blob(
obj: &serde_json::Map<String, JsonValue>,
flags: ValidateFlags,
) -> Result<DataValue, DataValidationError> {
let mime_type = obj
.get("mimeType")
.and_then(|v| v.as_str())
.ok_or_else(|| DataValidationError::DataModelInvalid {
reason: "blob must have mimeType field".to_string(),
})?
.to_string();
let size = obj.get("size").and_then(|v| v.as_u64()).ok_or_else(|| {
DataValidationError::DataModelInvalid {
reason: "blob must have size field".to_string(),
}
})?;
let ref_link = if let Some(ref_obj) = obj.get("ref") {
let ref_map = ref_obj
.as_object()
.ok_or_else(|| DataValidationError::DataModelInvalid {
reason: "blob ref must be an object".to_string(),
})?;
let link = ref_map
.get("$link")
.and_then(|v| v.as_str())
.ok_or_else(|| DataValidationError::DataModelInvalid {
reason: "blob ref must have $link field".to_string(),
})?;
Some(CIDLink::new(link))
} else {
None
};
let cid = obj.get("cid").and_then(|v| v.as_str()).map(String::from);
if ref_link.is_none() && cid.is_none() {
return Err(DataValidationError::DataModelInvalid {
reason: "blob must have either ref or cid field".to_string(),
});
}
if cid.is_some() && ref_link.is_none() && !flags.contains(ValidateFlags::ALLOW_LEGACY_BLOB) {
return Err(DataValidationError::LegacyBlobNotAllowed);
}
Ok(DataValue::Blob(Blob {
type_marker: "blob".to_string(),
ref_link,
mime_type,
size,
cid,
}))
}
pub fn to_json(value: &DataValue) -> JsonValue {
match value {
DataValue::Null => JsonValue::Null,
DataValue::Boolean(b) => JsonValue::Bool(*b),
DataValue::Integer(i) => JsonValue::Number((*i).into()),
DataValue::Float(f) => serde_json::Number::from_f64(*f)
.map(JsonValue::Number)
.unwrap_or(JsonValue::Null),
DataValue::String(s) => JsonValue::String(s.clone()),
DataValue::Bytes(b) => {
let mut obj = serde_json::Map::new();
obj.insert("$bytes".to_string(), JsonValue::String(b.bytes.clone()));
JsonValue::Object(obj)
}
DataValue::Link(l) => {
let mut obj = serde_json::Map::new();
obj.insert("$link".to_string(), JsonValue::String(l.link.clone()));
JsonValue::Object(obj)
}
DataValue::Blob(b) => {
let mut obj = serde_json::Map::new();
obj.insert("$type".to_string(), JsonValue::String("blob".to_string()));
obj.insert(
"mimeType".to_string(),
JsonValue::String(b.mime_type.clone()),
);
obj.insert("size".to_string(), JsonValue::Number(b.size.into()));
if let Some(ref link) = b.ref_link {
let mut ref_obj = serde_json::Map::new();
ref_obj.insert("$link".to_string(), JsonValue::String(link.link.clone()));
obj.insert("ref".to_string(), JsonValue::Object(ref_obj));
}
if let Some(ref cid) = b.cid {
obj.insert("cid".to_string(), JsonValue::String(cid.clone()));
}
JsonValue::Object(obj)
}
DataValue::Array(arr) => JsonValue::Array(arr.iter().map(to_json).collect()),
DataValue::Object(map) => {
let obj: serde_json::Map<String, JsonValue> =
map.iter().map(|(k, v)| (k.clone(), to_json(v))).collect();
JsonValue::Object(obj)
}
}
}
#[cfg(test)]
#[allow(clippy::approx_constant)]
mod tests {
use super::*;
#[test]
fn test_parse_primitives() {
let flags = ValidateFlags::empty();
assert!(matches!(
parse_json(&JsonValue::Null, flags).unwrap(),
DataValue::Null
));
assert!(matches!(
parse_json(&JsonValue::Bool(true), flags).unwrap(),
DataValue::Boolean(true)
));
assert!(matches!(
parse_json(&serde_json::json!(42), flags).unwrap(),
DataValue::Integer(42)
));
assert!(matches!(
parse_json(&serde_json::json!(3.14), flags).unwrap(),
DataValue::Float(_)
));
assert!(matches!(
parse_json(&serde_json::json!("hello"), flags).unwrap(),
DataValue::String(_)
));
}
#[test]
fn test_parse_array() {
let flags = ValidateFlags::empty();
let json = serde_json::json!([1, 2, 3]);
let result = parse_json(&json, flags).unwrap();
assert!(result.is_array());
assert_eq!(result.as_array().unwrap().len(), 3);
}
#[test]
fn test_parse_object() {
let flags = ValidateFlags::empty();
let json = serde_json::json!({"name": "test", "count": 42});
let result = parse_json(&json, flags).unwrap();
assert!(result.is_object());
let obj = result.as_object().unwrap();
assert_eq!(obj.get("name").unwrap().as_string(), Some("test"));
assert_eq!(obj.get("count").unwrap().as_integer(), Some(42));
}
#[test]
fn test_parse_bytes() {
let flags = ValidateFlags::empty();
let json = serde_json::json!({"$bytes": "SGVsbG8="});
let result = parse_json(&json, flags).unwrap();
assert!(result.is_bytes());
assert_eq!(result.as_bytes().unwrap().bytes, "SGVsbG8=");
}
#[test]
fn test_parse_link() {
let flags = ValidateFlags::empty();
let json = serde_json::json!({"$link": "bafytest"});
let result = parse_json(&json, flags).unwrap();
assert!(result.is_link());
assert_eq!(result.as_link().unwrap().link, "bafytest");
}
#[test]
fn test_parse_modern_blob() {
let flags = ValidateFlags::empty();
let json = serde_json::json!({
"$type": "blob",
"ref": {"$link": "bafytest"},
"mimeType": "image/png",
"size": 1024
});
let result = parse_json(&json, flags).unwrap();
assert!(result.is_blob());
let blob = result.as_blob().unwrap();
assert_eq!(blob.mime_type, "image/png");
assert_eq!(blob.size, 1024);
assert!(blob.is_modern());
}
#[test]
fn test_parse_legacy_blob_allowed() {
let flags = ValidateFlags::ALLOW_LEGACY_BLOB;
let json = serde_json::json!({
"$type": "blob",
"cid": "bafytest",
"mimeType": "image/png",
"size": 1024
});
let result = parse_json(&json, flags).unwrap();
assert!(result.is_blob());
assert!(result.as_blob().unwrap().is_legacy());
}
#[test]
fn test_parse_legacy_blob_not_allowed() {
let flags = ValidateFlags::empty();
let json = serde_json::json!({
"$type": "blob",
"cid": "bafytest",
"mimeType": "image/png",
"size": 1024
});
let result = parse_json(&json, flags);
assert!(matches!(
result,
Err(DataValidationError::LegacyBlobNotAllowed)
));
}
#[test]
fn test_parse_object_with_type() {
let flags = ValidateFlags::empty();
let json = serde_json::json!({
"$type": "app.bsky.feed.post",
"text": "Hello world"
});
let result = parse_json(&json, flags).unwrap();
assert!(result.is_object());
assert_eq!(result.get_type(), Some("app.bsky.feed.post"));
}
#[test]
fn test_invalid_dollar_field() {
let flags = ValidateFlags::empty();
let json = serde_json::json!({"$unknown": "value"});
let result = parse_json(&json, flags);
assert!(result.is_err());
}
#[test]
fn test_roundtrip() {
let flags = ValidateFlags::empty();
let json = serde_json::json!({
"string": "hello",
"number": 42,
"float": 3.14,
"bool": true,
"null": null,
"array": [1, 2, 3],
"nested": {"a": 1}
});
let data = parse_json(&json, flags).unwrap();
let back = to_json(&data);
assert_eq!(json, back);
}
}