Skip to main content

eth_avatars/modules/ipfs/
mod.rs

1use std::str::FromStr;
2use strum::{Display, EnumString};
3
4use crate::{Locator, LocatorError, Resource};
5
6pub mod gateway;
7pub use gateway::IpfsGateway;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumString, Display)]
10#[strum(ascii_case_insensitive, serialize_all = "snake_case")]
11pub enum IpfsSchema {
12    Ipfs,
13    Ipns,
14}
15
16#[derive(Debug, PartialEq, Eq)]
17pub struct Ipfs {
18    pub schema: IpfsSchema,
19    pub cid: String,
20    pub path: Option<String>,
21}
22
23impl Locator for Ipfs {
24    fn of(resource: &Resource) -> Option<&Self> {
25        match resource {
26            Resource::Ipfs(ipfs) => Some(ipfs),
27            _ => None,
28        }
29    }
30}
31
32impl FromStr for Ipfs {
33    type Err = LocatorError;
34
35    fn from_str(s: &str) -> Result<Self, Self::Err> {
36        let (schema, rest) = s.split_once(':').ok_or(LocatorError::IpfsSchema)?;
37        let schema: IpfsSchema = schema.parse().map_err(|_| LocatorError::IpfsSchema)?;
38        let rest = rest.trim_start_matches('/');
39
40        let (cid, path) = match rest.split_once('/') {
41            Some((cid, path)) => (cid, Some(path)),
42            None => (rest, None),
43        };
44
45        if cid.is_empty() {
46            return Err(LocatorError::IpfsEmptyCid);
47        }
48
49        Ok(Self {
50            schema,
51            cid: cid.to_string(),
52            path: path.filter(|path| !path.is_empty()).map(str::to_string),
53        })
54    }
55}
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60
61    #[tokio::test]
62    async fn ipfs_url_no_path() {
63        let input: Ipfs = "ipfs://bafkreifnrjhkl7ccr2ifwn2n7ap6dh2way25a6w5x2szegvj5pt4b5nvfu"
64            .parse()
65            .unwrap();
66        assert_eq!(input.schema, IpfsSchema::Ipfs);
67        assert_eq!(
68            input.cid,
69            "bafkreifnrjhkl7ccr2ifwn2n7ap6dh2way25a6w5x2szegvj5pt4b5nvfu"
70        );
71        assert_eq!(input.path, None);
72    }
73
74    #[tokio::test]
75    async fn ipfs_url_short_schema() {
76        let input: Ipfs = "ipfs:/bafkreifnrjhkl7ccr2ifwn2n7ap6dh2way25a6w5x2szegvj5pt4b5nvfu"
77            .parse()
78            .unwrap();
79        assert_eq!(input.schema, IpfsSchema::Ipfs);
80        assert_eq!(
81            input.cid,
82            "bafkreifnrjhkl7ccr2ifwn2n7ap6dh2way25a6w5x2szegvj5pt4b5nvfu"
83        );
84        assert_eq!(input.path, None);
85    }
86
87    #[tokio::test]
88    async fn ipfs_url_ipns() {
89        let input: Ipfs = "ipns://bafkreifnrjhkl7ccr2ifwn2n7ap6dh2way25a6w5x2szegvj5pt4b5nvfu"
90            .parse()
91            .unwrap();
92        assert_eq!(input.schema, IpfsSchema::Ipns);
93        assert_eq!(
94            input.cid,
95            "bafkreifnrjhkl7ccr2ifwn2n7ap6dh2way25a6w5x2szegvj5pt4b5nvfu"
96        );
97        assert_eq!(input.path, None);
98    }
99
100    #[tokio::test]
101    async fn ipfs_url_path() {
102        let input: Ipfs =
103            "ipfs://bafkreifnrjhkl7ccr2ifwn2n7ap6dh2way25a6w5x2szegvj5pt4b5nvfu/hello/world.txt"
104                .parse()
105                .unwrap();
106        assert_eq!(input.schema, IpfsSchema::Ipfs);
107        assert_eq!(
108            input.cid,
109            "bafkreifnrjhkl7ccr2ifwn2n7ap6dh2way25a6w5x2szegvj5pt4b5nvfu"
110        );
111        assert_eq!(input.path.unwrap(), "hello/world.txt");
112    }
113}