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;
use crate::{authority::Authorization, data::Header};
use noosphere_storage::{base64_encode, BlockStore, BlockStoreSend};
use super::ContentType;
#[derive(Debug, Default, Eq, PartialEq, Clone, Serialize, Deserialize, Hash)]
pub struct MemoIpld {
pub parent: Option<Cid>,
pub headers: Vec<(String, String)>,
pub body: Cid,
}
impl MemoIpld {
pub async fn try_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: &Cid, store: &S) -> Result<Self> {
match store.load::<DagCborCodec, MemoIpld>(cid).await {
Ok(mut memo) => {
memo.parent = Some(*cid);
memo.remove_header(&Header::Signature.to_string());
memo.remove_header(&Header::Proof.to_string());
Ok(memo)
}
Err(error) => Err(anyhow!(error)),
}
}
pub async fn sign<Credential: KeyMaterial>(
&mut self,
credential: &Credential,
authorization: Option<&Authorization>,
) -> Result<()> {
let signature = base64_encode(&credential.sign(&self.body.to_bytes()).await?)?;
self.replace_first_header(&Header::Signature.to_string(), &signature);
if let Some(authorization) = authorization {
self.replace_first_header(
&Header::Proof.to_string(),
&Cid::try_from(authorization)?.to_string(),
);
} else {
self.remove_header(&Header::Proof.to_string())
}
let did = credential.get_did().await?;
self.replace_first_header(&Header::Author.to_string(), &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);
set
});
let mut modified_headers: Vec<(String, String)> = self
.headers
.clone()
.into_iter()
.filter(|(key, _)| !new_header_set.contains(key))
.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.to_string()) {
if let Ok(content_type) = ContentType::from_str(&content_type) {
Some(content_type)
} else {
None
}
} else {
None
}
}
}
#[cfg(test)]
mod test {
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"));
}
}