From e5d610270bed47a91ecca8d51adc705bed58cda7 Mon Sep 17 00:00:00 2001 From: Matthias Beyer Date: Sat, 4 Dec 2021 17:03:09 +0100 Subject: Rewrite using "ipfs" crate Signed-off-by: Matthias Beyer --- Cargo.toml | 15 +++-- src/cid.rs | 14 ----- src/client.rs | 103 +++++++++++++++++--------------- src/ipfs_client.rs | 20 +------ src/main.rs | 1 - src/profile.rs | 144 +++++++++++++++++---------------------------- src/types/datetime.rs | 23 ++++++++ src/types/encodable_cid.rs | 28 --------- src/types/mod.rs | 2 - src/types/node.rs | 83 ++++++++++++++++++++------ src/types/payload.rs | 58 ++++++++++++++++-- 11 files changed, 259 insertions(+), 232 deletions(-) delete mode 100644 src/types/encodable_cid.rs diff --git a/Cargo.toml b/Cargo.toml index 1d2638d..553f189 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,17 +19,14 @@ edition = "2018" anyhow = "1" async-trait = "0.1" chrono = { version = "0.4", features = ["serde"] } -cid = "0.7" +cid = "0.5" clap = "3.0.0-beta.5" daglib = { git = "https://git.sr.ht/~matthiasbeyer/daglib", branch = "master" } env_logger = "0.8" futures = "0.3" -ipfs-api-backend-hyper = { version = "0.2", features = [ "with-hyper-tls" ] } log = "0.4" tokio = { version = "1", features = ["full", "rt", "macros"] } mime = "0.3" -libipld-cbor = "0.12" -libipld = "0.12" rand_core = { version = "0.6", features = ["getrandom"] } rand_os = "0.2" ed25519-dalek = "*" @@ -38,3 +35,13 @@ serde = "1" serde_json = "1" getset = "0.1" xdg = "2.4" +libp2p = "0.41" +tracing = "0.1" + +[dependencies.ipfs] +git = "https://github.com/rs-ipfs/rust-ipfs/" +rev = "ad3ab49b4d9236363969b0f74f14aabc7c906b3b" + + +[dev-dependencies] +multibase = "0.8" diff --git a/src/cid.rs b/src/cid.rs index 5aef970..2957cc5 100644 --- a/src/cid.rs +++ b/src/cid.rs @@ -21,20 +21,6 @@ pub trait TryToCid { fn try_to_cid(self) -> Result; } -impl TryToCid for ipfs_api_backend_hyper::response::AddResponse { - fn try_to_cid(self) -> Result { - log::debug!("Transforming to CID => {:?}", self); - string_to_cid(self.hash) - } -} - -impl TryToCid for ipfs_api_backend_hyper::response::DagPutResponse { - fn try_to_cid(self) -> Result { - log::debug!("Transforming to CID => {:?}", self); - string_to_cid(self.cid.cid_string) - } -} - impl daglib::NodeId for Cid { } diff --git a/src/client.rs b/src/client.rs index f234e33..fd18ec8 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,13 +1,9 @@ -use std::io::Cursor; +use std::convert::TryFrom; use anyhow::Result; -use futures::FutureExt; -use futures::TryFutureExt; use futures::TryStreamExt; -use ipfs_api_backend_hyper::IpfsApi; +use ipfs::Cid; -use crate::cid::Cid; -use crate::cid::TryToCid; use crate::config::Config; use crate::ipfs_client::IpfsClient; use crate::types::Node; @@ -34,13 +30,10 @@ impl Client { } pub async fn post_text_blob(&self, text: String) -> Result { - let reader = Cursor::new(text); - self.ipfs - .add(reader) + .put_dag(text.into()) .await .map_err(anyhow::Error::from) - .and_then(crate::ipfs_client::backend::response::AddResponse::try_to_cid) } /// Post a text node @@ -70,46 +63,39 @@ impl Client { } async fn post_payload(&self, payload: Payload) -> Result { - self.post_serializable(&payload).await + self.post(payload).await } async fn post_node(&self, node: Node) -> Result { - self.post_serializable(&node).await + self.post(node).await } - async fn post_serializable(&self, s: &S) -> Result { - let payload_s = serde_json::to_string(s)?; - let payload_c = Cursor::new(payload_s); - self.ipfs - .dag_put(payload_c) - .await - .map_err(anyhow::Error::from) - .and_then(crate::ipfs_client::backend::response::DagPutResponse::try_to_cid) + async fn post>(&self, s: S) -> Result { + self.ipfs.put_dag(s.into()).await.map_err(anyhow::Error::from) } pub async fn get_node(&self, cid: Cid) -> Result { - self.get_deserializeable::(cid).await + self.get::(cid).await } pub async fn get_payload(&self, cid: Cid) -> Result { - self.get_deserializeable::(cid).await + self.get::(cid).await } - async fn get_deserializeable(&self, cid: Cid) -> Result { - let bytes = self.ipfs - .dag_get(cid.as_ref()) - .map_ok(|chunk| chunk.to_vec()) - .try_concat() + async fn get>(&self, cid: Cid) -> Result { + let ipld = self.ipfs + .get_dag(ipfs::IpfsPath::new(ipfs::path::PathRoot::Ipld(cid))) .await?; - let s = String::from_utf8(bytes)?; - serde_json::from_str(&s).map_err(anyhow::Error::from) + D::try_from(ipld) } pub async fn get_content_text(&self, cid: Cid) -> Result { + let starting_point = ipfs::path::IpfsPath::new(ipfs::path::PathRoot::Ipld(cid)); + let bytes = self.ipfs - .cat(cid.as_ref()) - .map_ok(|chunk| chunk.to_vec()) + .cat_unixfs(starting_point, None) + .await? .try_concat() .await?; @@ -123,8 +109,10 @@ fn now() -> DateTime { #[cfg(test)] mod tests { - use ipfs_api_backend_hyper::TryFromUri; - use crate::cid::string_to_cid; + use std::convert::TryFrom; + + use cid::Cid; + use crate::client::Client; use crate::config::Config; use crate::ipfs_client::IpfsClient; @@ -136,22 +124,32 @@ mod tests { chrono::prelude::Utc.ymd(y, m, d).and_hms(hr, min, sec).into() } + async fn mk_ipfs() -> IpfsClient { + let mut opts = ipfs::IpfsOptions::inmemory_with_generated_keys(); + opts.mdns = false; + let (ipfs, fut): (ipfs::Ipfs, _) = ipfs::UninitializedIpfs::new(opts).start().await.unwrap(); + tokio::task::spawn(fut); + ipfs + } + #[tokio::test] async fn test_post_text_blob() { let _ = env_logger::try_init(); - let ipfs = IpfsClient::from_str("http://localhost:5001").unwrap(); + let ipfs = mk_ipfs().await; let config = Config::default(); let client = Client::new(ipfs, config); let cid = client.post_text_blob(String::from("text")).await; assert!(cid.is_ok()); - assert_eq!(cid.unwrap().as_ref(), "QmY2T5EfgLn8qWCt8eus6VX1gJuAp1nmUSdmoehgMxznAf"); + let cid = cid.unwrap(); + let expected_cid = Cid::try_from("bafyreienmqqpz622nxgi7xvcx2jf7p3lyagqkwcj5ieil3mhx2zckfl35u").unwrap(); + assert_eq!(cid, expected_cid, "{} != {}", cid, expected_cid); } #[tokio::test] async fn test_post_text_node() { let _ = env_logger::try_init(); - let ipfs = IpfsClient::from_str("http://localhost:5001").unwrap(); + let ipfs = mk_ipfs().await; let config = Config::default(); let client = Client::new(ipfs, config); @@ -159,13 +157,15 @@ mod tests { let cid = client.post_text_node_with_datetime(Vec::new(), String::from("text"), datetime).await; assert!(cid.is_ok()); - assert_eq!(cid.unwrap().as_ref(), "bafyreifah3uwad7vm6o2zz3dsluscbjznmlgrgqqstk3s3djrdyvwgsulq"); + let cid = cid.unwrap(); + let expected_cid = Cid::try_from("bafyreifah3uwad7vm6o2zz3dsluscbjznmlgrgqqstk3s3djrdyvwgsulq").unwrap(); + assert_eq!(cid, expected_cid, "{} != {}", cid, expected_cid); } #[tokio::test] async fn test_post_text_node_roundtrip() { let _ = env_logger::try_init(); - let ipfs = IpfsClient::from_str("http://localhost:5001").unwrap(); + let ipfs = mk_ipfs().await; let config = Config::default(); let client = Client::new(ipfs, config); @@ -176,7 +176,8 @@ mod tests { let cid = client.post_text_node_with_datetime(Vec::new(), String::from(text), datetime.clone()).await; assert!(cid.is_ok()); let cid = cid.unwrap(); - assert_eq!(cid.as_ref(), "bafyreiazg25u4bbymcpebwuadr42lwvhpf7diohojeenmsf3rt42v3kbdy"); + let expected_cid = Cid::try_from("bafyreiazg25u4bbymcpebwuadr42lwvhpf7diohojeenmsf3rt42v3kbdy").unwrap(); + assert_eq!(cid, expected_cid, "{} != {}", cid, expected_cid); let node = client.get_node(cid).await; assert!(node.is_ok()); @@ -202,7 +203,7 @@ mod tests { #[tokio::test] async fn test_post_text_chain() { let _ = env_logger::try_init(); - let ipfs = IpfsClient::from_str("http://localhost:5001").unwrap(); + let ipfs = mk_ipfs().await; let config = Config::default(); let client = Client::new(ipfs, config); @@ -212,7 +213,7 @@ mod tests { (mkdate(2021, 11, 27, 12, 32, 0), "text3", "bafyreica4fz6spaiuk3nd6ybfquj3ysn6nlxuoxcd54xblibpirisjlhkm"), ]; - let mut prev: Option = None; + let mut prev: Option = None; for (datetime, text, expected_cid) in chain_elements { let parents = if let Some(previous) = prev.as_ref() { vec![previous.clone()] @@ -223,7 +224,8 @@ mod tests { let cid = client.post_text_node_with_datetime(parents, String::from(text), datetime.clone()).await; assert!(cid.is_ok()); let cid = cid.unwrap(); - assert_eq!(cid.as_ref(), expected_cid); + let expected_cid = Cid::try_from(expected_cid).unwrap(); + assert_eq!(cid, expected_cid, "{} != {}", cid, expected_cid); prev = Some(cid); } } @@ -231,12 +233,12 @@ mod tests { #[tokio::test] async fn test_post_text_dag() { let _ = env_logger::try_init(); - let ipfs = IpfsClient::from_str("http://localhost:5001").unwrap(); + let ipfs = mk_ipfs().await; let config = Config::default(); let client = Client::new(ipfs, config); async fn post_chain(client: &Client, chain_elements: &Vec<(DateTime, &str, &str)>) { - let mut prev: Option = None; + let mut prev: Option = None; for (datetime, text, expected_cid) in chain_elements { let parents = if let Some(previous) = prev.as_ref() { vec![previous.clone()] @@ -247,7 +249,8 @@ mod tests { let cid = client.post_text_node_with_datetime(parents, String::from(*text), datetime.clone()).await; assert!(cid.is_ok()); let cid = cid.unwrap(); - assert_eq!(cid.as_ref(), *expected_cid); + let expected_cid = Cid::try_from(*expected_cid).unwrap(); + assert_eq!(cid, expected_cid, "{} != {}", cid, expected_cid); prev = Some(cid); } } @@ -277,14 +280,15 @@ mod tests { let cid = client.post_text_node_with_datetime(Vec::new(), String::from("text6"), mkdate(2021, 11, 27, 12, 32, 0)).await; assert!(cid.is_ok()); let cid = cid.unwrap(); - assert_eq!(cid.as_ref(), "bafyreihrqhbsmqfkzmbsxvkvwtp4eekeri6m6afejf2wmk6gj64b2qwgsa"); + let expected_cid = Cid::try_from("bafyreihrqhbsmqfkzmbsxvkvwtp4eekeri6m6afejf2wmk6gj64b2qwgsa").unwrap(); + assert_eq!(cid, expected_cid, "{} != {}", cid, expected_cid); let parents = vec![ // latest node in chain_1_elements - string_to_cid(String::from("bafyreica4fz6spaiuk3nd6ybfquj3ysn6nlxuoxcd54xblibpirisjlhkm")).unwrap(), + ipfs::Cid::try_from("bafyreica4fz6spaiuk3nd6ybfquj3ysn6nlxuoxcd54xblibpirisjlhkm").unwrap(), // latest node in chain_2_elements - string_to_cid(String::from("bafyreica4fz6spaiuk3nd6ybfquj3ysn6nlxuoxcd54xblibpirisjl2km")).unwrap(), + ipfs::Cid::try_from("bafyreica4fz6spaiuk3nd6ybfquj3ysn6nlxuoxcd54xblibpirisjl2km").unwrap(), // single node "text6" cid @@ -293,7 +297,8 @@ mod tests { let cid = client.post_text_node_with_datetime(parents, String::from("text7"), mkdate(2021, 11, 27, 12, 32, 0)).await; assert!(cid.is_ok()); let cid = cid.unwrap(); - assert_eq!(cid.as_ref(), "bafyreibnwo6phfbi5m6lzjfiaem4xtvjpeq5nnbsicie6whlnkoakgiyua"); + let expected_cid = Cid::try_from("bafyreibnwo6phfbi5m6lzjfiaem4xtvjpeq5nnbsicie6whlnkoakgiyua").unwrap(); + assert_eq!(cid, expected_cid, "{} != {}", cid, expected_cid); } } diff --git a/src/ipfs_client.rs b/src/ipfs_client.rs index 13d8bde..97af6ff 100644 --- a/src/ipfs_client.rs +++ b/src/ipfs_client.rs @@ -1,19 +1 @@ -pub use ipfs_api_backend_hyper as backend; -pub type IpfsClient = backend::IpfsClient; - - -#[cfg(test)] -mod tests { - use ipfs_api_backend_hyper::TryFromUri; - use super::IpfsClient; - - #[test] - fn test_connect_str() { - let _ = IpfsClient::from_str("http://localhost:5001").unwrap(); - } - - #[test] - fn test_connect_host_and_port() { - let _ = IpfsClient::from_host_and_port(http::uri::Scheme::HTTP, "localhost", 5001).unwrap(); - } -} +pub type IpfsClient = ipfs::Ipfs; diff --git a/src/main.rs b/src/main.rs index f1fdcc0..ef413c6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,5 @@ use anyhow::Result; -pub mod cid; pub mod cli; pub mod client; pub mod config; diff --git a/src/profile.rs b/src/profile.rs index 21ab4e0..1b6f583 100644 --- a/src/profile.rs +++ b/src/profile.rs @@ -1,27 +1,62 @@ +use std::path::Path; use std::path::PathBuf; use anyhow::Result; -use ipfs_api_backend_hyper::IpfsApi; -use tokio::io::AsyncWriteExt; use tokio::io::AsyncReadExt; use crate::client::Client; -use crate::cid::Cid; +use crate::config::Config; +use crate::ipfs_client::IpfsClient; -#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[derive(Debug)] pub struct Profile { - key_name: String, - key_id: String, + client: Client, } impl Profile { - pub async fn create(name: &str, client: &Client) -> Result { - let key = client.ipfs.key_gen(name, ipfs_api_backend_hyper::KeyType::Ed25519, 64).await?; + pub async fn create(state_dir: &Path, name: &str, config: Config) -> Result { + let bootstrap = vec![]; // TODO + let mdns = false; // TODO + let keypair = ipfs::Keypair::generate_ed25519(); + Self::write_to_statedir(state_dir, name, &keypair).await?; + + let options = ipfs::IpfsOptions { + ipfs_path: Self::ipfs_path(state_dir, name).await?, + keypair, + bootstrap, + mdns, + kad_protocol: None, + listening_addrs: vec![], + span: Some(tracing::trace_span!("distrox-ipfs")), + }; + + let (ipfs, fut): (ipfs::Ipfs<_>, _) = ipfs::UninitializedIpfs::::new(options) + .start() + .await?; + tokio::task::spawn(fut); + Ok(Self::new(ipfs, config)) + } + + async fn new_inmemory(config: Config) -> Result { + let mut opts = ipfs::IpfsOptions::inmemory_with_generated_keys(); + opts.mdns = false; + let (ipfs, fut): (ipfs::Ipfs, _) = ipfs::UninitializedIpfs::new(opts).start().await.unwrap(); + tokio::task::spawn(fut); + Ok(Self::new(ipfs, config)) + } + + fn new(ipfs: IpfsClient, config: Config) -> Self { + Profile { client: Client::new(ipfs, config) } + } - Ok(Profile { - key_name: key.name, - key_id: key.id - }) + async fn write_to_statedir(_state_dir: &Path, _name: &str, _keypair: &ipfs::Keypair) -> Result<()> { + unimplemented!() + } + + async fn ipfs_path(state_dir: &Path, name: &str) -> Result { + let path = state_dir.join(name).join("ipfs"); + tokio::fs::create_dir_all(&path).await?; + Ok(path) } pub fn config_path(name: &str) -> String { @@ -38,62 +73,13 @@ impl Profile { }) } - /// Store the Profile on disk - pub async fn write_to_filesystem(&self) -> Result<()> { - let config_path = Self::config_file_path(&self.key_name)?; - - let mut config_file = tokio::fs::OpenOptions::new() - .write(true) - .create(true) - .truncate(true) - .open(config_path) - .await?; - - let config = serde_json::to_string(&self)?; - config_file.write_all(config.as_bytes()).await?; - config_file.sync_all().await?; - Ok(()) - } - /// Load the Profile from disk and ensure the keys exist in IPFS - pub async fn load_from_filesystem(name: &str, client: &Client) -> Result> { - let config_path = Self::config_file_path(name)?; - let file_reader = tokio::fs::OpenOptions::new() - .read(true) - .open(config_path) - .await - .map(tokio::io::BufReader::new)?; - - Self::load_from_reader(file_reader, name, client).await - } - - async fn load_from_reader(mut r: R, name: &str, client: &Client) -> Result> { - let mut buf = String::new(); - let _ = r.read_to_string(&mut buf).await?; - let config: Self = serde_json::from_str(&buf)?; - - client.ipfs - .key_list() - .await? - .keys - .into_iter() - .find(|keypair| keypair.name == name) - .map(|_| Ok(config)) - .transpose() + pub async fn load_from_filesystem(_name: &str, _client: &Client) -> Result> { + unimplemented!() } - pub async fn publish(&self, client: &Client, cid: Cid) -> Result<()> { - let path = format!("/ipfs/{}", cid.as_ref()); - let resolve = true; - let lifetime = Some("10m"); - let ttl = None; - - let publish_response = client.ipfs - .name_publish(&path, resolve, lifetime, ttl, Some(&self.key_name)) - .await?; - - log::debug!("Publish response = {{ name: {}, value: {} }}", publish_response.name, publish_response.value); - Ok(()) + async fn load_from_reader(_r: R, _name: &str, _client: &Client) -> Result> { + unimplemented!() } } @@ -105,35 +91,11 @@ mod tests { use crate::config::Config; use crate::ipfs_client::IpfsClient; - use ipfs_api_backend_hyper::TryFromUri; - - async fn mk_client() -> Client { - let ipfs = IpfsClient::from_str("http://localhost:5001").unwrap(); - let config = Config::default(); - Client::new(ipfs, config) - } - - macro_rules! run_test { - ($name:ident, $client:ident, $test:block) => { - $client.ipfs.key_rm($name).await; - { - $test - } - $client.ipfs.key_rm($name).await; - } - } - #[tokio::test] async fn test_create_profile() { let _ = env_logger::try_init(); - let client = mk_client().await; - let name = "test_create_profile"; - run_test!(name, client, - { - let p = Profile::create(name, &client).await; - assert!(p.is_ok()); - } - ); + let profile = Profile::new_inmemory(Config::default()).await; + assert!(profile.is_ok()); } } diff --git a/src/types/datetime.rs b/src/types/datetime.rs index 6735731..00d739a 100644 --- a/src/types/datetime.rs +++ b/src/types/datetime.rs @@ -1,9 +1,32 @@ +use std::convert::TryFrom; use anyhow::Error; +use anyhow::Result; #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] #[serde(transparent)] pub struct DateTime(chrono::DateTime); +impl Into for DateTime { + fn into(self) -> ipfs::Ipld { + ipfs::Ipld::String(self.0.to_rfc3339()) + } +} + +impl TryFrom for DateTime { + type Error = Error; + + fn try_from(ipld: ipfs::Ipld) -> Result { + match ipld { + ipfs::Ipld::String(s) => chrono::DateTime::parse_from_rfc3339(&s) + .map(|dt| dt.with_timezone(&chrono::Utc)) + .map(DateTime) + .map_err(Error::from), + _ => anyhow::bail!("Expected string for timestamp"), + } + } +} + + impl From> for DateTime { fn from(dt: chrono::DateTime) -> Self { DateTime(dt) diff --git a/src/types/encodable_cid.rs b/src/types/encodable_cid.rs deleted file mode 100644 index 014d661..0000000 --- a/src/types/encodable_cid.rs +++ /dev/null @@ -1,28 +0,0 @@ -use std::collections::HashMap; - -/// An DAG-JSON encodable cid -/// -/// this is a hack. DAG-JSON expects a linked CID to be of the form -/// -/// "/": "" -/// -/// (see https://ipld.io/docs/codecs/known/dag-json/) -/// -/// so we have a wrapper type here to make the CID encodable -#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub struct EncodableCid(HashMap); - -impl From for EncodableCid { - fn from(cid: crate::cid::Cid) -> Self { - let mut hm = HashMap::new(); - hm.insert(String::from("/"), cid); - Self(hm) - } -} - -impl Into for EncodableCid { - fn into(self) -> crate::cid::Cid { - self.0.get("/").unwrap().clone() - } -} - diff --git a/src/types/mod.rs b/src/types/mod.rs index 228c363..7382f16 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -1,5 +1,3 @@ -mod encodable_cid; - mod node; pub use node::*; diff --git a/src/types/node.rs b/src/types/node.rs index aead32c..eb5679b 100644 --- a/src/types/node.rs +++ b/src/types/node.rs @@ -1,43 +1,88 @@ -#[derive(Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, getset::Getters)] +use anyhow::Result; + +use std::convert::TryFrom; + +#[derive(Debug, Eq, PartialEq, getset::Getters)] pub struct Node { /// Version - #[serde(rename = "v")] #[getset(get = "pub")] version: String, /// Parent Nodes, identified by cid - parents: Vec, + parents: Vec, /// The actual payload of the node, which is stored in another document identified by this cid - payload: crate::types::encodable_cid::EncodableCid, + payload: ipfs::Cid, +} + +impl Into for Node { + fn into(self) -> ipfs::Ipld { + let mut map = std::collections::BTreeMap::new(); + map.insert(String::from("version"), ipfs::Ipld::String(self.version)); + map.insert(String::from("parents"), ipfs::Ipld::List(self.parents.into_iter().map(ipfs::Ipld::Link).collect())); + map.insert(String::from("payload"), ipfs::Ipld::Link(self.payload)); + ipfs::Ipld::Map(map) + } } -impl daglib::Node for Node { - type Id = crate::cid::Cid; +impl TryFrom for Node { + type Error = anyhow::Error; - fn parent_ids(&self) -> Vec { - self.parents() + fn try_from(ipld: ipfs::Ipld) -> Result { + let missing_field = |name: &'static str| move || anyhow::anyhow!("Missing field {}", name); + let field_wrong_type = |name: &str, expty: &str| anyhow::bail!("Field {} has wrong type, expected {}", name, expty); + match ipld { + ipfs::Ipld::Map(map) => { + let version = match map.get("version").ok_or_else(missing_field("version"))? { + ipfs::Ipld::String(s) => s.to_string(), + _ => return field_wrong_type("version", "String") + }; + + let parents = match map.get("parents").ok_or_else(missing_field("parents"))? { + ipfs::Ipld::List(s) => { + s.into_iter() + .map(|parent| -> Result { + match parent { + ipfs::Ipld::Link(cid) => Ok(cid.clone()), + _ => anyhow::bail!("Field in parents has wrong type, expected Link"), + } + }) + .collect::>>()? + }, + _ => return field_wrong_type("parents", "Vec") + }; + + let payload = match map.get("payload").ok_or_else(missing_field("payload"))? { + ipfs::Ipld::Link(cid) => cid.clone(), + _ => return field_wrong_type("payload", "Link") + }; + + Ok(Node { + version, + parents, + payload + }) + } + + _ => anyhow::bail!("Unexpected type, expected map") + } } } impl Node { - pub fn new(version: String, parents: Vec, payload: crate::cid::Cid) -> Self { + pub fn new(version: String, parents: Vec, payload: ipfs::Cid) -> Self { Self { version, - parents: parents.into_iter().map(crate::types::encodable_cid::EncodableCid::from).collect(), - payload: payload.into() + parents, + payload, } } - pub fn parents(&self) -> Vec { - self.parents - .clone() - .into_iter() - .map(crate::types::encodable_cid::EncodableCid::into) - .collect() + pub fn parents(&self) -> Vec { + self.parents.clone() } - pub fn payload(&self) -> crate::cid::Cid { - self.payload.clone().into() + pub fn payload(&self) -> ipfs::Cid { + self.payload.clone() } } diff --git a/src/types/payload.rs b/src/types/payload.rs index cb12845..a11b215 100644 --- a/src/types/payload.rs +++ b/src/types/payload.rs @@ -1,6 +1,10 @@ +use std::convert::TryFrom; + +use anyhow::Result; + use crate::types::DateTime; -#[derive(Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, getset::Getters)] +#[derive(Debug, Eq, PartialEq, getset::Getters)] pub struct Payload { // TODO: Make this a mime::Mime, but as this type does not impl Serialize/Deserialize, we // cannot do this trivially yet @@ -10,15 +14,59 @@ pub struct Payload { #[getset(get = "pub")] timestamp: DateTime, - content: crate::types::encodable_cid::EncodableCid, + content: ipfs::Cid, +} + +impl Into for Payload { + fn into(self) -> ipfs::Ipld { + let mut map = std::collections::BTreeMap::new(); + map.insert(String::from("mime"), ipfs::Ipld::String(self.mime)); + map.insert(String::from("timestamp"), self.timestamp.into()); + map.insert(String::from("content"), ipfs::Ipld::Link(self.content)); + ipfs::Ipld::Map(map) + } +} + +impl TryFrom for Payload { + type Error = anyhow::Error; + + fn try_from(ipld: ipfs::Ipld) -> Result { + let missing_field = |name: &'static str| move || anyhow::anyhow!("Missing field {}", name); + let field_wrong_type = |name: &str, expty: &str| anyhow::bail!("Field {} has wrong type, expected {}", name, expty); + match ipld { + ipfs::Ipld::Map(map) => { + let mime = match map.get("mime").ok_or_else(missing_field("mime"))? { + ipfs::Ipld::String(s) => s.to_owned(), + _ => return field_wrong_type("mime", "String") + }; + + let timestamp = map.get("timestamp") + .ok_or_else(missing_field("timestamp"))?; + let timestamp = DateTime::try_from(timestamp.clone())?; // TODO dont clone + + let content = match map.get("content").ok_or_else(missing_field("content"))? { + ipfs::Ipld::Link(cid) => cid.clone(), + _ => return field_wrong_type("content", "Link") + }; + + Ok(Payload { + mime, + timestamp, + content + }) + }, + + _ => anyhow::bail!("Unexpected type, expected map"), + } + } } impl Payload { - pub fn new(mime: String, timestamp: DateTime, content: crate::cid::Cid) -> Self { + pub fn new(mime: String, timestamp: DateTime, content: ipfs::Cid) -> Self { Self { mime, timestamp, content: content.into() } } - pub fn content(&self) -> crate::cid::Cid { - self.content.clone().into() + pub fn content(&self) -> ipfs::Cid { + self.content.clone() } } -- cgit v1.2.3