use crate::error::Error;
use ipld_core::ipld::Ipld;
use ipld_core::serde::to_ipld;
use serde::{de, ser};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::fmt;
use std::ops::{Deref, DerefMut};
mod cid_link;
pub use cid_link::CidLink;
mod integer;
pub use integer::*;
pub mod string;
use string::RecordKey;
pub trait Collection: fmt::Debug {
const NSID: &'static str;
type Record: fmt::Debug + de::DeserializeOwned + Serialize;
fn nsid() -> string::Nsid {
Self::NSID.parse().expect("Self::NSID should be a valid NSID")
}
fn repo_path(rkey: &RecordKey) -> String {
format!("{}/{}", Self::NSID, rkey.as_str())
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(untagged)]
pub enum BlobRef {
Typed(TypedBlobRef),
Untyped(UnTypedBlobRef),
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(tag = "$type", rename_all = "lowercase")]
pub enum TypedBlobRef {
Blob(Blob),
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct UnTypedBlobRef {
pub cid: String,
pub mime_type: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Blob {
pub r#ref: CidLink,
pub mime_type: String,
pub size: usize, }
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct Object<T> {
#[serde(flatten)]
pub data: T,
#[serde(flatten)]
pub extra_data: Ipld,
}
impl<T> From<T> for Object<T> {
fn from(data: T) -> Self {
Self { data, extra_data: Ipld::Map(std::collections::BTreeMap::new()) }
}
}
impl<T> Deref for Object<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.data
}
}
impl<T> DerefMut for Object<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.data
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(untagged)]
pub enum Union<T> {
Refs(T),
Unknown(UnknownData),
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct UnknownData {
#[serde(rename = "$type")]
pub r#type: String,
#[serde(flatten)]
pub data: Ipld,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(untagged)]
pub enum Unknown {
Object(BTreeMap<String, DataModel>),
Null,
Other(DataModel),
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(try_from = "Ipld")]
pub struct DataModel(#[serde(serialize_with = "serialize_data_model")] Ipld);
fn serialize_data_model<S>(ipld: &Ipld, serializer: S) -> Result<S::Ok, S::Error>
where
S: ser::Serializer,
{
match ipld {
Ipld::Float(_) => Err(ser::Error::custom("float values are not allowed in ATProtocol")),
Ipld::List(list) => {
if list.iter().any(|value| matches!(value, Ipld::Float(_))) {
Err(ser::Error::custom("float values are not allowed in ATProtocol"))
} else {
list.iter().cloned().map(DataModel).collect::<Vec<_>>().serialize(serializer)
}
}
Ipld::Map(map) => {
if map.values().any(|value| matches!(value, Ipld::Float(_))) {
Err(ser::Error::custom("float values are not allowed in ATProtocol"))
} else {
map.iter()
.map(|(k, v)| (k, DataModel(v.clone())))
.collect::<BTreeMap<_, _>>()
.serialize(serializer)
}
}
Ipld::Link(link) => CidLink(*link).serialize(serializer),
_ => ipld.serialize(serializer),
}
}
impl Deref for DataModel {
type Target = Ipld;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for DataModel {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl TryFrom<Ipld> for DataModel {
type Error = Error;
fn try_from(value: Ipld) -> Result<Self, Self::Error> {
match value {
Ipld::Float(_) => Err(Error::NotAllowed),
Ipld::List(list) => {
if list.iter().any(|value| matches!(value, Ipld::Float(_))) {
Err(Error::NotAllowed)
} else {
Ok(DataModel(Ipld::List(list)))
}
}
Ipld::Map(map) => {
if map.values().any(|value| matches!(value, Ipld::Float(_))) {
Err(Error::NotAllowed)
} else {
Ok(DataModel(Ipld::Map(map)))
}
}
data => Ok(DataModel(data)),
}
}
}
pub trait TryFromUnknown: Sized {
type Error;
fn try_from_unknown(value: Unknown) -> Result<Self, Self::Error>;
}
impl<T> TryFromUnknown for T
where
T: de::DeserializeOwned,
{
type Error = Error;
fn try_from_unknown(value: Unknown) -> Result<Self, Self::Error> {
let json = serde_json::to_vec(&value).unwrap();
Ok(serde_json::from_slice(&json).unwrap())
}
}
pub trait TryIntoUnknown {
type Error;
fn try_into_unknown(self) -> Result<Unknown, Self::Error>;
}
impl<T> TryIntoUnknown for T
where
T: Serialize,
{
type Error = Error;
fn try_into_unknown(self) -> Result<Unknown, Self::Error> {
Ok(Unknown::Other(to_ipld(self)?.try_into()?))
}
}
#[cfg(test)]
mod tests {
use super::*;
use ipld_core::cid::Cid;
use serde_json::{from_str, to_string};
const CID_LINK_JSON: &str =
r#"{"$link":"bafkreibme22gw2h7y2h7tg2fhqotaqjucnbc24deqo72b6mkl2egezxhvy"}"#;
#[test]
fn cid_link_serde_json() {
let deserialized =
from_str::<CidLink>(CID_LINK_JSON).expect("failed to deserialize cid-link");
let serialized = to_string(&deserialized).expect("failed to serialize cid-link");
assert_eq!(serialized, CID_LINK_JSON);
}
#[test]
fn blob_ref_typed_deserialize_json() {
let json = format!(
r#"{{"$type":"blob","ref":{},"mimeType":"text/plain","size":0}}"#,
CID_LINK_JSON
);
let deserialized = from_str::<BlobRef>(&json).expect("failed to deserialize blob-ref");
assert_eq!(
deserialized,
BlobRef::Typed(TypedBlobRef::Blob(Blob {
r#ref: CidLink::try_from(
"bafkreibme22gw2h7y2h7tg2fhqotaqjucnbc24deqo72b6mkl2egezxhvy"
)
.expect("failed to create cid-link"),
mime_type: "text/plain".into(),
size: 0
}))
);
}
#[test]
fn blob_ref_untyped_deserialize_json() {
let json = r#"{"cid":"bafkreibme22gw2h7y2h7tg2fhqotaqjucnbc24deqo72b6mkl2egezxhvy","mimeType":"text/plain"}"#;
let deserialized = from_str::<BlobRef>(json).expect("failed to deserialize blob-ref");
assert_eq!(
deserialized,
BlobRef::Untyped(UnTypedBlobRef {
cid: "bafkreibme22gw2h7y2h7tg2fhqotaqjucnbc24deqo72b6mkl2egezxhvy".into(),
mime_type: "text/plain".into(),
})
);
}
#[test]
fn blob_ref_serialize_json() {
let blob_ref = BlobRef::Typed(TypedBlobRef::Blob(Blob {
r#ref: CidLink::try_from("bafkreibme22gw2h7y2h7tg2fhqotaqjucnbc24deqo72b6mkl2egezxhvy")
.expect("failed to create cid-link"),
mime_type: "text/plain".into(),
size: 0,
}));
let serialized = to_string(&blob_ref).expect("failed to serialize blob-ref");
assert_eq!(
serialized,
format!(
r#"{{"$type":"blob","ref":{},"mimeType":"text/plain","size":0}}"#,
CID_LINK_JSON
)
);
}
#[test]
fn blob_ref_deserialize_dag_cbor() {
let dag_cbor = [
0xa4, 0x65, 0x24, 0x74, 0x79, 0x70, 0x65, 0x64, 0x62, 0x6c, 0x6f, 0x62, 0x63, 0x72,
0x65, 0x66, 0xd8, 0x2a, 0x58, 0x25, 0x00, 0x01, 0x55, 0x12, 0x20, 0x2c, 0x26, 0xb4,
0x6b, 0x68, 0xff, 0xc6, 0x8f, 0xf9, 0x9b, 0x45, 0x3c, 0x1d, 0x30, 0x41, 0x34, 0x13,
0x42, 0x2d, 0x70, 0x64, 0x83, 0xbf, 0xa0, 0xf9, 0x8a, 0x5e, 0x88, 0x62, 0x66, 0xe7,
0xae, 0x68, 0x6d, 0x69, 0x6d, 0x65, 0x54, 0x79, 0x70, 0x65, 0x6a, 0x74, 0x65, 0x78,
0x74, 0x2f, 0x70, 0x6c, 0x61, 0x69, 0x6e, 0x64, 0x73, 0x69, 0x7a, 0x65, 0x00,
];
let deserialized = serde_ipld_dagcbor::from_slice::<BlobRef>(dag_cbor.as_slice())
.expect("failed to deserialize blob-ref");
assert_eq!(
deserialized,
BlobRef::Typed(TypedBlobRef::Blob(Blob {
r#ref: CidLink::try_from(
"bafkreibme22gw2h7y2h7tg2fhqotaqjucnbc24deqo72b6mkl2egezxhvy"
)
.expect("failed to create cid-link"),
mime_type: "text/plain".into(),
size: 0,
}))
);
}
#[test]
fn data_model() {
assert!(DataModel::try_from(Ipld::Null).is_ok());
assert!(DataModel::try_from(Ipld::Bool(true)).is_ok());
assert!(DataModel::try_from(Ipld::Integer(1)).is_ok());
assert!(DataModel::try_from(Ipld::Float(1.5)).is_err(), "float value should fail");
assert!(DataModel::try_from(Ipld::String("s".into())).is_ok());
assert!(DataModel::try_from(Ipld::Bytes(vec![0x01])).is_ok());
assert!(DataModel::try_from(Ipld::List(vec![Ipld::Bool(true)])).is_ok());
assert!(
DataModel::try_from(Ipld::List(vec![Ipld::Bool(true), Ipld::Float(1.5)])).is_err(),
"list with float value should fail"
);
assert!(DataModel::try_from(Ipld::Map(BTreeMap::from_iter([(
String::from("k"),
Ipld::Bool(true)
)])))
.is_ok());
assert!(
DataModel::try_from(Ipld::Map(BTreeMap::from_iter([(
String::from("k"),
Ipld::Float(1.5)
)])))
.is_err(),
"map with float value should fail"
);
assert!(DataModel::try_from(Ipld::Link(
Cid::try_from("bafkreibme22gw2h7y2h7tg2fhqotaqjucnbc24deqo72b6mkl2egezxhvy")
.expect("failed to create cid")
))
.is_ok());
}
#[test]
fn union() {
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(tag = "$type")]
enum FooRefs {
#[serde(rename = "example.com#bar")]
Bar(Box<Bar>),
#[serde(rename = "example.com#baz")]
Baz(Box<Baz>),
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
struct Bar {
bar: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
struct Baz {
baz: i32,
}
type Foo = Union<FooRefs>;
let foo = serde_json::from_str::<Foo>(r#"{"$type":"example.com#bar","bar":"bar"}"#)
.expect("failed to deserialize foo");
assert_eq!(foo, Union::Refs(FooRefs::Bar(Box::new(Bar { bar: String::from("bar") }))));
let foo = serde_json::from_str::<Foo>(r#"{"$type":"example.com#baz","baz":42}"#)
.expect("failed to deserialize foo");
assert_eq!(foo, Union::Refs(FooRefs::Baz(Box::new(Baz { baz: 42 }))));
let foo = serde_json::from_str::<Foo>(r#"{"$type":"example.com#foo","foo":true}"#)
.expect("failed to deserialize foo");
assert_eq!(
foo,
Union::Unknown(UnknownData {
r#type: String::from("example.com#foo"),
data: Ipld::Map(BTreeMap::from_iter([(String::from("foo"), Ipld::Bool(true))]))
})
);
}
#[test]
fn unknown_serialize() {
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
struct Foo {
foo: Unknown,
}
let foo = Foo {
foo: Unknown::Object(BTreeMap::from_iter([(
String::from("bar"),
DataModel(Ipld::String(String::from("bar"))),
)])),
};
let serialized = to_string(&foo).expect("failed to serialize foo");
assert_eq!(serialized, r#"{"foo":{"bar":"bar"}}"#);
}
#[test]
fn unknown_deserialize() {
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
struct Foo {
foo: Unknown,
}
{
let json = r#"{
"foo": {
"$type": "example.com#foo",
"bar": "bar"
}
}"#;
let deserialized = from_str::<Foo>(json).expect("failed to deserialize foo");
assert_eq!(
deserialized,
Foo {
foo: Unknown::Object(BTreeMap::from_iter([
(String::from("bar"), DataModel(Ipld::String(String::from("bar")))),
(
String::from("$type"),
DataModel(Ipld::String(String::from("example.com#foo")))
)
]))
}
);
}
{
let json = r#"{
"foo": {}
}"#;
let deserialized = from_str::<Foo>(json).expect("failed to deserialize foo");
assert_eq!(deserialized, Foo { foo: Unknown::Object(BTreeMap::default()) });
}
{
let json = r#"{
"foo": {
"bar": "bar"
}
}"#;
let deserialized = from_str::<Foo>(json).expect("failed to deserialize foo");
assert_eq!(
deserialized,
Foo {
foo: Unknown::Object(BTreeMap::from_iter([(
String::from("bar"),
DataModel(Ipld::String(String::from("bar")))
)]))
}
);
}
{
let json = r#"{
"foo": null
}"#;
let deserialized = from_str::<Foo>(json).expect("failed to deserialize foo");
assert_eq!(deserialized, Foo { foo: Unknown::Null });
}
{
let json = r#"{
"foo": 42
}"#;
let deserialized = from_str::<Foo>(json).expect("failed to deserialize foo");
assert_eq!(deserialized, Foo { foo: Unknown::Other(DataModel(Ipld::Integer(42))) });
}
{
let json = r#"{
"foo": 42.195
}"#;
assert!(from_str::<Foo>(json).is_err());
}
}
#[test]
fn unknown_try_from() {
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(tag = "$type")]
enum Foo {
#[serde(rename = "example.com#bar")]
Bar(Box<Bar>),
#[serde(rename = "example.com#baz")]
Baz(Box<Baz>),
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
struct Bar {
bar: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
struct Baz {
baz: i32,
}
{
let unknown = Unknown::Object(BTreeMap::from_iter([
(String::from("$type"), DataModel(Ipld::String(String::from("example.com#bar")))),
(String::from("bar"), DataModel(Ipld::String(String::from("barbar")))),
]));
let bar = Bar::try_from_unknown(unknown.clone()).expect("failed to convert to Bar");
assert_eq!(bar, Bar { bar: String::from("barbar") });
let barbaz = Foo::try_from_unknown(unknown).expect("failed to convert to Bar");
assert_eq!(barbaz, Foo::Bar(Box::new(Bar { bar: String::from("barbar") })));
}
{
let unknown = Unknown::Object(BTreeMap::from_iter([
(String::from("$type"), DataModel(Ipld::String(String::from("example.com#baz")))),
(String::from("baz"), DataModel(Ipld::Integer(42))),
]));
let baz = Baz::try_from_unknown(unknown.clone()).expect("failed to convert to Baz");
assert_eq!(baz, Baz { baz: 42 });
let barbaz = Foo::try_from_unknown(unknown).expect("failed to convert to Bar");
assert_eq!(barbaz, Foo::Baz(Box::new(Baz { baz: 42 })));
}
}
#[test]
fn serialize_unknown_from_cid_link() {
{
let cid_link =
CidLink::try_from("bafkreibme22gw2h7y2h7tg2fhqotaqjucnbc24deqo72b6mkl2egezxhvy")
.expect("failed to create cid-link");
let unknown = cid_link.try_into_unknown().expect("failed to convert to unknown");
assert_eq!(
serde_json::to_string(&unknown).expect("failed to serialize unknown"),
r#"{"$link":"bafkreibme22gw2h7y2h7tg2fhqotaqjucnbc24deqo72b6mkl2egezxhvy"}"#
);
}
{
let cid_link =
CidLink::try_from("bafkreibme22gw2h7y2h7tg2fhqotaqjucnbc24deqo72b6mkl2egezxhvy")
.expect("failed to create cid-link");
let blob_ref = BlobRef::Typed(TypedBlobRef::Blob(Blob {
r#ref: cid_link,
mime_type: "text/plain".into(),
size: 0,
}));
let unknown = blob_ref.try_into_unknown().expect("failed to convert to unknown");
let serialized = serde_json::to_string(&unknown).expect("failed to serialize unknown");
assert!(
serialized.contains("bafkreibme22gw2h7y2h7tg2fhqotaqjucnbc24deqo72b6mkl2egezxhvy"),
"serialized unknown should contain cid string: {serialized}"
);
}
}
}