use std::{
collections::{BTreeMap, BTreeSet},
str::FromStr,
};
use anyhow::{anyhow, Result};
use cid::Cid;
use libipld_cbor::DagCborCodec;
use serde::{Deserialize, Serialize};
use ucan::{crypto::KeyMaterial, Ucan};
use crate::data::Header;
use noosphere_storage::{base64_encode, BlockStore, BlockStoreSend};
use super::{ContentType, Link};
pub type LamportOrder = u32;
#[derive(Debug, Default, Eq, PartialEq, Clone, Serialize, Deserialize, Hash)]
pub struct MemoIpld {
pub parent: Option<Link<MemoIpld>>,
pub headers: Vec<(String, String)>,
pub body: Cid,
}
impl MemoIpld {
pub fn lamport_order(&self) -> LamportOrder {
if let Some(lamport_order) = self.get_first_header(&Header::LamportOrder) {
u32::from_str(&lamport_order).unwrap_or_default()
} else {
0u32
}
}
pub fn get_proof(&self) -> Result<Option<Ucan>> {
if let Some(since_proof) = self.get_first_header(&Header::Proof) {
if let Ok(ucan) = Ucan::from_str(&since_proof) {
return Ok(Some(ucan));
}
};
Ok(None)
}
pub fn require_proof(&self) -> Result<Ucan> {
if let Some(ucan) = self.get_proof()? {
Ok(ucan)
} else {
Err(anyhow!("No valid 'proof' header found"))
}
}
pub async fn compare_body<S: BlockStore>(&self, store: &S) -> Result<bool> {
let parent_cid = match &self.parent {
Some(cid) => cid,
None => return Ok(true),
};
let MemoIpld {
body: parent_body, ..
} = store.load::<DagCborCodec, _>(parent_cid).await?;
Ok(self.body != parent_body)
}
pub async fn diff_headers(&self, other: &MemoIpld) -> Result<Vec<(String, String)>> {
let headers: BTreeMap<String, String> = self.headers.clone().into_iter().collect();
let other_headers: BTreeMap<String, String> = other.headers.clone().into_iter().collect();
let mut diff = Vec::new();
for (name, value) in headers {
if let Some(other_value) = other_headers.get(&name) {
if value != *other_value {
diff.push((name, value))
}
} else {
diff.push((name, value))
}
}
Ok(diff)
}
pub async fn for_body<S: BlockStore, Body: Serialize + BlockStoreSend>(
store: &mut S,
body: Body,
) -> Result<MemoIpld> {
let body_cid = store.save::<DagCborCodec, _>(body).await?;
Ok(MemoIpld {
parent: None,
headers: Vec::new(),
body: body_cid,
})
}
pub async fn branch_from<S: BlockStore>(cid: &Link<MemoIpld>, store: &S) -> Result<Self> {
match store.load::<DagCborCodec, MemoIpld>(cid).await {
Ok(mut memo) => {
memo.parent = Some(cid.clone());
memo.remove_header(&Header::Signature);
memo.remove_header(&Header::Proof);
memo.replace_headers(vec![(
Header::LamportOrder.to_string(),
(memo.lamport_order() + 1).to_string(),
)]);
Ok(memo)
}
Err(error) => Err(anyhow!(error)),
}
}
pub async fn sign<Credential: KeyMaterial>(
&mut self,
credential: &Credential,
proof: Option<&Ucan>,
) -> Result<()> {
let signature = base64_encode(&credential.sign(&self.body.to_bytes()).await?)?;
self.replace_first_header(&Header::Signature, &signature);
if let Some(proof) = proof {
self.replace_first_header(&Header::Proof, &proof.encode()?);
} else {
self.remove_header(&Header::Proof)
}
let did = credential.get_did().await?;
self.replace_first_header(&Header::Author, &did);
Ok(())
}
pub fn get_header(&self, name: &str) -> Vec<String> {
let lower_name = name.to_lowercase();
self.headers
.iter()
.filter_map(|(a_name, a_value)| {
if a_name.to_lowercase() == lower_name {
Some(a_value.clone())
} else {
None
}
})
.collect()
}
pub fn get_first_header(&self, name: &str) -> Option<String> {
let lower_name = name.to_lowercase();
for (a_name, a_value) in &self.headers {
if a_name.to_lowercase() == lower_name {
return Some(a_value.clone());
}
}
None
}
pub fn expect_header(&self, name: &str, value: &str) -> Result<()> {
let lower_name = name.to_lowercase();
for (a_name, a_value) in self.headers.iter() {
if a_name.to_lowercase() == lower_name && a_value == value {
return Ok(());
}
}
Err(anyhow!(
"Expected to find a header {:?} that is {:?}",
name,
value
))
}
pub fn replace_first_header(&mut self, name: &str, value: &str) {
let mut found = 0usize;
self.headers = self
.headers
.clone()
.into_iter()
.filter_map(|(a_name, a_value)| {
if a_name.to_lowercase() == name.to_lowercase() {
let replacement = if found == 0 {
Some((name.to_string(), value.to_string()))
} else {
None
};
found += 1;
replacement
} else {
Some((a_name, a_value))
}
})
.collect();
if found == 0 {
self.headers.push((name.to_string(), value.to_string()))
}
}
pub fn replace_headers(&mut self, mut new_headers: Vec<(String, String)>) {
let new_header_set = new_headers
.iter()
.fold(BTreeSet::new(), |mut set, (key, _)| {
set.insert(key.to_lowercase());
set
});
let mut modified_headers: Vec<(String, String)> = self
.headers
.clone()
.into_iter()
.filter(|(key, _)| !new_header_set.contains(&key.to_lowercase()))
.collect();
modified_headers.append(&mut new_headers);
self.headers = modified_headers;
}
pub fn remove_header(&mut self, name: &str) {
let lower_name = name.to_lowercase();
self.headers = self
.headers
.clone()
.into_iter()
.filter(|(a_name, _)| a_name.to_lowercase() != lower_name)
.collect();
}
pub fn content_type(&self) -> Option<ContentType> {
if let Some(content_type) = self.get_first_header(&Header::ContentType) {
if let Ok(content_type) = ContentType::from_str(&content_type) {
Some(content_type)
} else {
None
}
} else {
None
}
}
}
#[cfg(test)]
mod test {
use anyhow::Result;
use libipld_cbor::DagCborCodec;
use libipld_core::{ipld::Ipld, raw::RawCodec};
#[cfg(target_arch = "wasm32")]
use wasm_bindgen_test::wasm_bindgen_test;
use serde::{Deserialize, Serialize};
use crate::data::MemoIpld;
use noosphere_storage::{
block_deserialize, block_encode, block_serialize, BlockStore, MemoryStore,
};
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
async fn it_round_trips_as_cbor() {
let (body_cid, _) = block_encode::<RawCodec, _>(&Ipld::Bytes(b"foobar".to_vec())).unwrap();
let mut store = MemoryStore::default();
let memo = MemoIpld {
parent: None,
headers: Vec::new(),
body: body_cid,
};
let memo_cid = store.save::<DagCborCodec, _>(&memo).await.unwrap();
let loaded_memo = store
.load::<DagCborCodec, MemoIpld>(&memo_cid)
.await
.unwrap();
assert_eq!(memo, loaded_memo);
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
async fn it_can_store_and_load_a_structured_body() {
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
struct Structured {
foo: String,
}
let mut store = MemoryStore::default();
let structured = Structured {
foo: String::from("bar"),
};
let body_cid = store.save::<DagCborCodec, _>(&structured).await.unwrap();
let memo = MemoIpld {
parent: None,
headers: Vec::new(),
body: body_cid,
};
let (_, memo_bytes) = block_serialize::<DagCborCodec, _>(&memo).unwrap();
let decoded_memo = block_deserialize::<DagCborCodec, MemoIpld>(&memo_bytes).unwrap();
let decoded_body: Structured = store
.load::<DagCborCodec, Structured>(&decoded_memo.body)
.await
.unwrap();
assert_eq!(decoded_body.foo, String::from("bar"));
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
async fn it_can_causally_order_a_lineage_of_memos() -> Result<()> {
let mut store = MemoryStore::default();
let mut memo = MemoIpld::default();
let mut memo_cid = store.save::<DagCborCodec, _>(&memo).await?;
let mut memos = vec![memo];
for _ in 0..5 {
memo = MemoIpld::branch_from(&memo_cid.into(), &mut store).await?;
memo_cid = store.save::<DagCborCodec, _>(&memo).await?;
memos.push(memo);
}
let first_memo = memos.get(0).unwrap();
let third_memo = memos.get(2).unwrap();
let fourth_memo = memos.get(3).unwrap();
let fifth_memo = memos.get(4).unwrap();
assert!(third_memo.lamport_order() < fourth_memo.lamport_order());
assert!(fifth_memo.lamport_order() > fourth_memo.lamport_order());
assert!(first_memo.lamport_order() < third_memo.lamport_order());
assert!(first_memo.lamport_order() < fifth_memo.lamport_order());
Ok(())
}
}