use base64::engine::{general_purpose, Engine};
use serde_json::Map;
use serde_json::Value as SerdeValue;
use tracing::instrument;
use crate::{
datatype::DataType, errors::AtomicResult, resources::PropVals, Resource, Storelike, Value,
};
pub fn resources_to_json_ad(
resources: &[Resource],
origin: &str,
resolve_subjects: bool,
) -> AtomicResult<String> {
let mut strings = Vec::new();
for r in resources {
let subject = if resolve_subjects {
r.get_subject().resolve(origin)
} else {
r.get_subject().to_string()
};
let res =
propvals_to_json_ad_map(r.get_propvals(), Some(subject), origin, resolve_subjects)?;
strings.push(serde_json::to_string(&res)?);
}
let str = strings.join(",");
Ok(format!("[{}]", str))
}
pub fn val_to_serde(
value: Value,
origin: &str,
resolve_subjects: bool,
) -> AtomicResult<SerdeValue> {
let json_val: SerdeValue = match value {
Value::AtomicUrl(val) => {
if resolve_subjects {
SerdeValue::String(val.resolve(origin))
} else {
SerdeValue::String(val.to_string())
}
}
Value::Date(val) => SerdeValue::String(val),
Value::Integer(val) => serde_json::from_str(&val.to_string()).unwrap_or_default(),
Value::Float(val) => serde_json::from_str(&val.to_string()).unwrap_or_default(),
Value::Markdown(val) => SerdeValue::String(val),
Value::Uri(val) => SerdeValue::String(val),
Value::Json(val) => val,
Value::ResourceArray(val) => {
let mut vec: Vec<SerdeValue> = Vec::new();
for resource in val {
match resource {
crate::values::SubResource::Nested(pv) => {
vec.push(crate::serialize::propvals_to_json_ad_map(
&pv,
None,
origin,
resolve_subjects,
)?);
}
crate::values::SubResource::Subject(s) => {
if resolve_subjects {
vec.push(SerdeValue::String(s.resolve(origin)))
} else {
vec.push(SerdeValue::String(s.to_string()))
}
}
}
}
SerdeValue::Array(vec)
}
Value::Slug(val) => SerdeValue::String(val),
Value::String(val) => SerdeValue::String(val),
Value::Timestamp(val) => SerdeValue::Number(val.into()),
Value::Unsupported(val) => SerdeValue::String(val.value),
Value::Boolean(val) => SerdeValue::Bool(val),
Value::NestedResource(res) => match res {
crate::values::SubResource::Nested(propvals) => {
propvals_to_json_ad_map(&propvals, None, origin, resolve_subjects)?
}
crate::values::SubResource::Subject(s) => {
if resolve_subjects {
SerdeValue::String(s.resolve(origin))
} else {
SerdeValue::String(s.to_string())
}
}
},
Value::LoroDoc(val) => SerdeValue::String(general_purpose::STANDARD.encode(val)),
Value::LocalizedText(map) => {
let mut obj = Map::new();
for (tag, s) in map {
obj.insert(tag, SerdeValue::String(s));
}
SerdeValue::Object(obj)
}
};
Ok(json_val)
}
#[instrument(skip_all)]
pub fn propvals_to_json_ad_map(
propvals: &PropVals,
subject: Option<String>,
origin: &str,
resolve_subjects: bool,
) -> AtomicResult<serde_json::Value> {
let mut root = Map::new();
for (prop_url, value) in propvals.iter() {
root.insert(
prop_url.clone(),
val_to_serde(value.clone(), origin, resolve_subjects)?,
);
}
if let Some(sub) = subject {
root.insert("@id".into(), SerdeValue::String(sub));
}
let obj = SerdeValue::Object(root);
Ok(obj)
}
pub async fn propvals_to_json_ld(
propvals: &PropVals,
subject: Option<String>,
store: &impl Storelike,
json_ld: bool,
origin: Option<&str>,
) -> AtomicResult<serde_json::Value> {
let origin = origin.unwrap_or("http://localhost");
let mut root = Map::new();
let mut context = Map::new();
for (prop_url, value) in propvals.iter() {
let property = store.get_property(prop_url).await?;
if json_ld {
let ctx_value: SerdeValue = match value.datatype() {
DataType::AtomicUrl => {
let mut obj = Map::new();
obj.insert("@id".into(), prop_url.as_str().into());
obj.insert("@type".into(), "@id".into());
obj.into()
}
DataType::Date => {
let mut obj = Map::new();
obj.insert("@id".into(), prop_url.as_str().into());
obj.insert(
"@type".into(),
"http://www.w3.org/2001/XMLSchema#date".into(),
);
obj.into()
}
DataType::Integer => {
let mut obj = Map::new();
obj.insert("@id".into(), prop_url.as_str().into());
obj.insert(
"@type".into(),
"http://www.w3.org/2001/XMLSchema#integer".into(),
);
obj.into()
}
DataType::Markdown => prop_url.as_str().into(),
DataType::LocalizedText => {
let mut obj = Map::new();
obj.insert("@id".into(), prop_url.as_str().into());
obj.insert("@container".into(), "@language".into());
obj.into()
}
DataType::ResourceArray => {
let mut obj = Map::new();
obj.insert("@id".into(), prop_url.as_str().into());
obj.insert("@container".into(), "@list".into());
obj.into()
}
_other => prop_url.as_str().into(),
};
context.insert(property.shortname.as_str().into(), ctx_value);
}
let key = property.shortname;
root.insert(key, val_to_serde(value.clone(), origin, true)?);
}
if let Some(sub) = subject {
root.insert("@id".into(), SerdeValue::String(sub));
}
if json_ld {
root.insert("@context".into(), context.into());
}
let obj = SerdeValue::Object(root);
Ok(obj)
}
pub fn serialize_json_array(items: &[String]) -> AtomicResult<String> {
let string = serde_json::to_string(items)?;
Ok(string)
}
#[cfg(feature = "rdf")]
pub async fn atoms_to_ntriples(
atoms: Vec<crate::Atom>,
store: &impl Storelike,
) -> AtomicResult<String> {
use rio_api::formatter::TriplesFormatter;
use rio_api::model::{Literal, NamedNode, Term, Triple};
use rio_turtle::NTriplesFormatter;
let mut formatter = NTriplesFormatter::new(Vec::default());
for atom in atoms {
let subject = NamedNode {
iri: atom.subject.as_str(),
}
.into();
let predicate = NamedNode {
iri: &atom.property,
};
let datatype = store.get_property(&atom.property).await?.data_type;
let value = &atom.value.to_string();
let datatype_url = datatype.to_string();
let object: Term = match &datatype {
DataType::AtomicUrl => NamedNode { iri: value }.into(),
DataType::String => Literal::Simple { value }.into(),
_dt => Literal::Typed {
value,
datatype: NamedNode { iri: &datatype_url },
}
.into(),
};
formatter.format(&Triple {
subject,
predicate,
object,
})?
}
let out = String::from_utf8(formatter.finish()?)?;
Ok(out)
}
#[cfg(feature = "rdf")]
pub async fn atoms_to_turtle(
atoms: Vec<crate::Atom>,
store: &impl Storelike,
) -> AtomicResult<String> {
use rio_api::formatter::TriplesFormatter;
use rio_api::model::{Literal, NamedNode, Term, Triple};
use rio_turtle::TurtleFormatter;
let mut formatter = TurtleFormatter::new(Vec::default());
for atom in atoms {
let subject = NamedNode {
iri: atom.subject.as_str(),
}
.into();
let predicate = NamedNode {
iri: &atom.property,
};
let datatype = store.get_property(&atom.property).await?.data_type;
let value = &atom.value.to_string();
let datatype_url = datatype.to_string();
let object: Term = match &datatype {
DataType::AtomicUrl => NamedNode { iri: value }.into(),
DataType::String => Literal::Simple { value }.into(),
_dt => Literal::Typed {
value,
datatype: NamedNode { iri: &datatype_url },
}
.into(),
};
formatter.format(&Triple {
subject,
predicate,
object,
})?
}
let out = String::from_utf8(formatter.finish()?)?;
Ok(out)
}
pub enum Format {
Json,
JsonAd,
JsonLd,
NTriples,
Pretty,
}
#[cfg(test)]
mod test {
use super::*;
use crate::Storelike;
fn strip_loro_update(value: &mut serde_json::Value) {
if let Some(obj) = value.as_object_mut() {
obj.remove(crate::urls::LORO_UPDATE);
obj.remove("loro-update");
if let Some(ctx) = obj.get_mut("@context").and_then(|c| c.as_object_mut()) {
ctx.remove("loro-update");
}
}
}
#[tokio::test]
async fn serialize_json_ad() {
let store = crate::Store::init().await.unwrap();
store.populate().await.unwrap();
let json = store
.get_resource(&crate::urls::AGENT.into())
.await
.unwrap()
.to_json_ad(None)
.unwrap();
println!("json-ad: {}", json);
let correct_json = r#"{
"@id": "https://atomicdata.dev/classes/Agent",
"https://atomicdata.dev/properties/description": "An Agent is a user that can create or modify data. For DID-based agents (did:ad:agent:{publicKey}), the public key is derived from the subject.",
"https://atomicdata.dev/properties/isA": [
"https://atomicdata.dev/classes/Class"
],
"https://atomicdata.dev/properties/parent": "https://atomicdata.dev/classes",
"https://atomicdata.dev/properties/recommends": [
"https://atomicdata.dev/properties/publicKey",
"https://atomicdata.dev/properties/name",
"https://atomicdata.dev/properties/description",
"https://atomicdata.dev/properties/personalDrive",
"https://atomicdata.dev/properties/sharedWithMe",
"https://atomicdata.dev/properties/drives"
],
"https://atomicdata.dev/properties/shortname": "agent"
}"#;
let correct_value: serde_json::Value = serde_json::from_str(correct_json).unwrap();
let mut our_value: serde_json::Value = serde_json::from_str(&json).unwrap();
strip_loro_update(&mut our_value);
assert_eq!(our_value, correct_value)
}
#[test]
fn serialize_json_ad_multiple() {
let vec = vec![Resource::new("subject".into())];
let serialized = resources_to_json_ad(&vec, "http://localhost/", true).unwrap();
let correct_json = r#"[{"@id":"http://localhost/subject"}]"#;
assert_eq!(serialized, correct_json);
}
#[tokio::test]
async fn serialize_json() {
let store = crate::Store::init().await.unwrap();
store.populate().await.unwrap();
let json = store
.get_resource(&crate::urls::AGENT.into())
.await
.unwrap()
.to_json(&store, None)
.await
.unwrap();
println!("json: {}", json);
let correct_json = r#"{
"@id": "https://atomicdata.dev/classes/Agent",
"description": "An Agent is a user that can create or modify data. For DID-based agents (did:ad:agent:{publicKey}), the public key is derived from the subject.",
"is-a": [
"https://atomicdata.dev/classes/Class"
],
"parent": "https://atomicdata.dev/classes",
"recommends": [
"https://atomicdata.dev/properties/publicKey",
"https://atomicdata.dev/properties/name",
"https://atomicdata.dev/properties/description",
"https://atomicdata.dev/properties/personalDrive",
"https://atomicdata.dev/properties/sharedWithMe",
"https://atomicdata.dev/properties/drives"
],
"shortname": "agent"
}"#;
let correct_value: serde_json::Value = serde_json::from_str(correct_json).unwrap();
let mut our_value: serde_json::Value = serde_json::from_str(&json).unwrap();
strip_loro_update(&mut our_value);
assert_eq!(our_value, correct_value)
}
#[tokio::test]
async fn serialize_json_ld() {
let store = crate::Store::init().await.unwrap();
store.populate().await.unwrap();
let json = store
.get_resource(&crate::urls::AGENT.into())
.await
.unwrap()
.to_json_ld(&store, None)
.await
.unwrap();
println!("json: {}", json);
let correct_json = r#"{
"@context": {
"description": "https://atomicdata.dev/properties/description",
"is-a": {
"@container": "@list",
"@id": "https://atomicdata.dev/properties/isA"
},
"parent": {
"@id": "https://atomicdata.dev/properties/parent",
"@type": "@id"
},
"recommends": {
"@container": "@list",
"@id": "https://atomicdata.dev/properties/recommends"
},
"shortname": "https://atomicdata.dev/properties/shortname"
},
"@id": "https://atomicdata.dev/classes/Agent",
"description": "An Agent is a user that can create or modify data. For DID-based agents (did:ad:agent:{publicKey}), the public key is derived from the subject.",
"is-a": [
"https://atomicdata.dev/classes/Class"
],
"parent": "https://atomicdata.dev/classes",
"recommends": [
"https://atomicdata.dev/properties/publicKey",
"https://atomicdata.dev/properties/name",
"https://atomicdata.dev/properties/description",
"https://atomicdata.dev/properties/personalDrive",
"https://atomicdata.dev/properties/sharedWithMe",
"https://atomicdata.dev/properties/drives"
],
"shortname": "agent"
}"#;
let correct_value: serde_json::Value = serde_json::from_str(correct_json).unwrap();
let mut our_value: serde_json::Value = serde_json::from_str(&json).unwrap();
strip_loro_update(&mut our_value);
assert_eq!(our_value, correct_value)
}
#[tokio::test]
#[cfg(feature = "rdf")]
async fn serialize_ntriples() {
use crate::Storelike;
let store = crate::Store::init().await.unwrap();
store.populate().await.unwrap();
let subject = crate::urls::DESCRIPTION;
let resource = store.get_resource(&subject.into()).await.unwrap();
let atoms = resource.to_atoms();
let serialized = atoms_to_ntriples(atoms, &store).await.unwrap();
let _out = r#"
<https://atomicdata.dev/properties/description> <https://atomicdata.dev/properties/description> "A textual description of the thing."^^<https://atomicdata.dev/datatypes/markdown> .
<https://atomicdata.dev/properties/description> <https://atomicdata.dev/properties/isA> "[\"https://atomicdata.dev/classes/Property\"]"^^<https://atomicdata.dev/datatypes/resourceArray> .
<https://atomicdata.dev/properties/description> <https://atomicdata.dev/properties/datatype> <https://atomicdata.dev/datatypes/markdown> .
<https://atomicdata.dev/properties/description> <https://atomicdata.dev/properties/shortname> "description"^^<https://atomicdata.dev/datatypes/slug> ."#;
assert!(serialized.contains(r#""description"^^<https://atomicdata.dev/datatypes/slug>"#));
assert!(serialized.lines().count() == 5);
}
}