polywrap_uri/
lib.rs

1use std::{fmt::Display, str::FromStr};
2
3use regex::Regex;
4use serde::{Deserialize, Serialize};
5
6/// Represents a Wrap URI.
7#[derive(Clone, Serialize, Deserialize, Debug, Hash, Eq)]
8pub struct Uri {
9    /// The URI's authority.
10    authority: String,
11    /// The URI's path.
12    path: String,
13    /// A string representation of the full Wrap URI.
14    uri: String,
15}
16
17/// Represents an error that occurs while parsing a wrap URI.
18#[derive(Debug, Clone, PartialEq)]
19pub struct ParseError(pub String);
20
21impl Display for ParseError {
22    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23        write!(f, "{:?}", self.0)
24    }
25}
26
27impl std::error::Error for ParseError {}
28
29impl Uri {
30    fn try_from_string(uri: &str) -> Result<Uri, ParseError> {
31        let mut processed = uri.to_string();
32
33        while processed.starts_with('/') {
34            processed = processed[1..].to_string();
35        }
36
37        let wrap_scheme_idx = processed.find("wrap://");
38
39        if wrap_scheme_idx.is_none() {
40            processed = format!("wrap://{processed}");
41        }
42
43        if wrap_scheme_idx.is_some() && wrap_scheme_idx.unwrap() != 0 {
44            return Err(ParseError(String::from(
45                "The wrap:// scheme must be at the beginning of the URI string".to_string(),
46            )));
47        }
48
49        let reg = Regex::new("wrap://([a-z][a-z0-9-_.]+)/(.*)").unwrap();
50
51        let captures = reg.captures(&processed);
52
53        if captures.as_ref().is_none() || captures.as_ref().unwrap().len() != 3 {
54            return Err(ParseError(format!(
55                r#"URI is malformed, here are some examples of valid URIs:
56            wrap://ipfs/QmHASH
57            wrap://wrapscan.io/polywrap/cool-wrap
58            wrapscan.io/user/wrap
59            wrap://ens/domain.eth
60            Invalid URI Received: {uri}"#,
61            )));
62        }
63
64        let result = captures.unwrap();
65
66        Ok(Uri {
67            authority: result[1].to_string(),
68            path: result[2].to_string(),
69            uri: processed.to_string(),
70        })
71    }
72
73    /// Construct a Wrap URI from its parts.
74    /// 
75    /// # Safety
76    /// This blindly builds the URI from its parts without performing any checks.
77    /// You need to ensure that all parts represent the same, valid Wrap URI.
78    pub unsafe fn from_parts(authority: String, path: String, uri: String) -> Uri {
79        Uri {
80            authority: authority,
81            path: path,
82            uri: uri,
83        }
84    }
85
86    /// Return the URI's authority.
87    pub fn authority(&self) -> &str {
88        &self.authority
89    }
90
91    /// Return the URI's path.
92    pub fn path(&self) -> &str {
93        &self.path
94    }
95
96    /// Return the URI as a string.
97    pub fn uri(&self) -> &str {
98        &self.uri
99    }
100}
101
102impl PartialEq for Uri {
103    fn eq(&self, other: &Self) -> bool {
104        self.uri == other.uri
105    }
106}
107
108impl Display for Uri {
109    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
110        write!(f, "{}", self.uri)
111    }
112}
113
114impl From<Uri> for String {
115    fn from(uri: Uri) -> Self {
116        uri.to_string()
117    }
118}
119
120impl TryFrom<String> for Uri {
121    type Error = ParseError;
122
123    fn try_from(uri: String) -> Result<Self, Self::Error> {
124        Uri::try_from_string(&uri)
125    }
126}
127
128impl TryFrom<&str> for Uri {
129    type Error = ParseError;
130
131    fn try_from(uri: &str) -> Result<Self, Self::Error> {
132        Uri::try_from_string(uri)
133    }
134}
135
136impl FromStr for Uri {
137    type Err = ParseError;
138
139    fn from_str(name: &str) -> Result<Self, Self::Err> {
140        Uri::try_from_string(name)
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147    use std::convert::TryInto;
148
149    #[test]
150    fn try_from_string_valid() {
151        assert!(Uri::try_from_string("wrap://ipfs/QmHASH").is_ok());
152        assert!(Uri::try_from_string("////wrap://ipfs/QmHASH").is_ok());
153        assert!(Uri::try_from_string("wrapscan.io/user/cool-wrap").is_ok());
154    }
155
156    #[test]
157    fn try_from_string_invalid() {
158        assert!(Uri::try_from_string("wraps://ipfs/QmHASH").is_err());
159        assert!(Uri::try_from_string("ipfs/QmHASHwrap://").is_err());
160        assert!(Uri::try_from_string("").is_err());
161    }
162
163    #[test]
164    fn from_parts() {
165        let uri =
166            unsafe { Uri::from_parts("authority".to_owned(), "path".to_owned(), "uri".to_owned()) };
167        assert_eq!(uri.authority(), "authority");
168        assert_eq!(uri.path(), "path");
169        assert_eq!(uri.uri(), "uri");
170    }
171
172    #[test]
173    fn equality() {
174        let (uri1, uri2, uri3) = unsafe {
175            (
176                Uri::from_parts("authority".to_owned(), "path".to_owned(), "uri".to_owned()),
177                Uri::from_parts("authority".to_owned(), "path".to_owned(), "uri".to_owned()),
178                Uri::from_parts(
179                    "authority".to_owned(),
180                    "path".to_owned(),
181                    "different".to_owned(),
182                ),
183            )
184        };
185
186        assert_eq!(uri1, uri2);
187        assert_ne!(uri1, uri3);
188    }
189
190    #[test]
191    fn from() {
192        let uri = Uri::try_from_string("wrap://auth/path").unwrap();
193        let string: String = uri.into();
194        assert_eq!(string, "wrap://auth/path");
195    }
196
197    #[test]
198    fn string_try_into() {
199        let uri: Result<Uri, ParseError> = "wrap://ipfs/QmHASH".try_into();
200        assert!(uri.is_ok());
201
202        let bad_uri: Result<Uri, ParseError> = "bad_uri".try_into();
203        assert!(bad_uri.is_err());
204    }
205
206    #[test]
207    fn parse_str() {
208        let uri: Uri = "wrap://ipfs/QmHASH".parse().unwrap();
209        assert_eq!(uri.uri(), "wrap://ipfs/QmHASH");
210
211        let bad_uri: Result<Uri, ParseError> = "bad_uri".parse();
212        assert!(bad_uri.is_err());
213    }
214
215    #[test]
216    fn display() {
217        let uri = Uri::try_from_string("wrap://authority/uri").unwrap();
218        assert_eq!(format!("{}", uri), "wrap://authority/uri");
219    }
220}