mint_url.rs 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. // Copyright (c) 2022-2023 Yuki Kishimoto
  2. // Distributed under the MIT software license
  3. //! Url
  4. use core::fmt;
  5. use core::str::FromStr;
  6. use serde::{Deserialize, Serialize};
  7. use thiserror::Error;
  8. use url::{ParseError, Url};
  9. use crate::ensure_cdk;
  10. /// Url Error
  11. #[derive(Debug, Error, PartialEq, Eq)]
  12. pub enum Error {
  13. /// Url error
  14. #[error(transparent)]
  15. Url(#[from] ParseError),
  16. /// Invalid URL structure
  17. #[error("Invalid URL")]
  18. InvalidUrl,
  19. }
  20. /// MintUrl Url
  21. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
  22. pub struct MintUrl(String);
  23. impl Serialize for MintUrl {
  24. fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
  25. where
  26. S: serde::Serializer,
  27. {
  28. // Use the to_string implementation to get the correctly formatted URL
  29. serializer.serialize_str(&self.to_string())
  30. }
  31. }
  32. impl<'de> Deserialize<'de> for MintUrl {
  33. fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
  34. where
  35. D: serde::Deserializer<'de>,
  36. {
  37. // Deserialize as a string and then use from_str to parse it correctly
  38. let s = String::deserialize(deserializer)?;
  39. MintUrl::from_str(&s).map_err(serde::de::Error::custom)
  40. }
  41. }
  42. impl MintUrl {
  43. fn format_url(url: &str) -> Result<String, Error> {
  44. ensure_cdk!(!url.is_empty(), Error::InvalidUrl);
  45. let url = url.trim_end_matches('/');
  46. // https://URL.com/path/TO/resource -> https://url.com/path/TO/resource
  47. let protocol = url
  48. .split("://")
  49. .nth(0)
  50. .ok_or(Error::InvalidUrl)?
  51. .to_lowercase();
  52. let host = url
  53. .split("://")
  54. .nth(1)
  55. .ok_or(Error::InvalidUrl)?
  56. .split('/')
  57. .nth(0)
  58. .ok_or(Error::InvalidUrl)?
  59. .to_lowercase();
  60. let path = url
  61. .split("://")
  62. .nth(1)
  63. .ok_or(Error::InvalidUrl)?
  64. .split('/')
  65. .skip(1)
  66. .collect::<Vec<&str>>()
  67. .join("/");
  68. let mut formatted_url = format!("{protocol}://{host}");
  69. if !path.is_empty() {
  70. formatted_url.push_str(&format!("/{path}"));
  71. }
  72. Ok(formatted_url)
  73. }
  74. /// Join onto url
  75. pub fn join(&self, path: &str) -> Result<Url, Error> {
  76. let url = Url::parse(&self.0)?;
  77. // Get the current path segments
  78. let base_path = url.path();
  79. // Check if the path has a trailing slash to avoid double slashes
  80. let normalized_path = if base_path.ends_with('/') {
  81. format!("{base_path}{path}")
  82. } else {
  83. format!("{base_path}/{path}")
  84. };
  85. // Create a new URL with the combined path
  86. let mut result = url.clone();
  87. result.set_path(&normalized_path);
  88. Ok(result)
  89. }
  90. /// Append path elements onto the URL
  91. pub fn join_paths(&self, path_elements: &[&str]) -> Result<Url, Error> {
  92. self.join(&path_elements.join("/"))
  93. }
  94. }
  95. impl FromStr for MintUrl {
  96. type Err = Error;
  97. fn from_str(url: &str) -> Result<Self, Self::Err> {
  98. let formatted_url = Self::format_url(url);
  99. match formatted_url {
  100. Ok(url) => Ok(Self(url)),
  101. Err(_) => Err(Error::InvalidUrl),
  102. }
  103. }
  104. }
  105. impl fmt::Display for MintUrl {
  106. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  107. write!(f, "{}", self.0)
  108. }
  109. }
  110. #[cfg(test)]
  111. mod tests {
  112. use super::*;
  113. use crate::Token;
  114. #[test]
  115. fn test_trim_trailing_slashes() {
  116. let very_unformatted_url = "http://url-to-check.com////";
  117. let unformatted_url = "http://url-to-check.com/";
  118. let formatted_url = "http://url-to-check.com";
  119. let very_trimmed_url = MintUrl::from_str(very_unformatted_url).unwrap();
  120. assert_eq!(formatted_url, very_trimmed_url.to_string());
  121. let trimmed_url = MintUrl::from_str(unformatted_url).unwrap();
  122. assert_eq!(formatted_url, trimmed_url.to_string());
  123. let unchanged_url = MintUrl::from_str(formatted_url).unwrap();
  124. assert_eq!(formatted_url, unchanged_url.to_string());
  125. }
  126. #[test]
  127. fn test_case_insensitive() {
  128. let wrong_cased_url = "http://URL-to-check.com";
  129. let correct_cased_url = "http://url-to-check.com";
  130. let cased_url_formatted = MintUrl::from_str(wrong_cased_url).unwrap();
  131. assert_eq!(correct_cased_url, cased_url_formatted.to_string());
  132. let wrong_cased_url_with_path = "http://URL-to-check.com/PATH/to/check";
  133. let correct_cased_url_with_path = "http://url-to-check.com/PATH/to/check";
  134. let cased_url_with_path_formatted = MintUrl::from_str(wrong_cased_url_with_path).unwrap();
  135. assert_eq!(
  136. correct_cased_url_with_path,
  137. cased_url_with_path_formatted.to_string()
  138. );
  139. }
  140. #[test]
  141. fn test_join_paths() {
  142. let url_no_path = "http://url-to-check.com";
  143. let url = MintUrl::from_str(url_no_path).unwrap();
  144. assert_eq!(
  145. format!("{url_no_path}/hello/world"),
  146. url.join_paths(&["hello", "world"]).unwrap().to_string()
  147. );
  148. let url_no_path_with_slash = "http://url-to-check.com/";
  149. let url = MintUrl::from_str(url_no_path_with_slash).unwrap();
  150. assert_eq!(
  151. format!("{url_no_path_with_slash}hello/world"),
  152. url.join_paths(&["hello", "world"]).unwrap().to_string()
  153. );
  154. let url_with_path = "http://url-to-check.com/my/path";
  155. let url = MintUrl::from_str(url_with_path).unwrap();
  156. assert_eq!(
  157. format!("{url_with_path}/hello/world"),
  158. url.join_paths(&["hello", "world"]).unwrap().to_string()
  159. );
  160. let url_with_path_with_slash = "http://url-to-check.com/my/path/";
  161. let url = MintUrl::from_str(url_with_path_with_slash).unwrap();
  162. assert_eq!(
  163. format!("{url_with_path_with_slash}hello/world"),
  164. url.join_paths(&["hello", "world"]).unwrap().to_string()
  165. );
  166. }
  167. #[test]
  168. fn test_mint_url_slash_eqality() {
  169. let mint_url_with_slash_str = "https://mint.minibits.cash/Bitcoin/";
  170. let mint_url_with_slash = MintUrl::from_str(mint_url_with_slash_str).unwrap();
  171. let mint_url_without_slash_str = "https://mint.minibits.cash/Bitcoin";
  172. let mint_url_without_slash = MintUrl::from_str(mint_url_without_slash_str).unwrap();
  173. assert_eq!(mint_url_with_slash, mint_url_without_slash);
  174. assert_eq!(
  175. mint_url_with_slash.to_string(),
  176. mint_url_without_slash_str.to_string()
  177. );
  178. }
  179. #[test]
  180. fn test_token_equality_trailing_slash() {
  181. let token_with_slash = Token::from_str("cashuBo2FteCNodHRwczovL21pbnQubWluaWJpdHMuY2FzaC9CaXRjb2luL2F1Y3NhdGF0gaJhaUgAUAVQ8ElBRmFwgqRhYQhhc3hAYzg2NTZhZDg4MzVmOWVmMzVkYWQ1MTZjNGU5ZTU5ZjA3YzFmODg0NTc2NWY3M2FhNWMyMjVhOGI4MGM0ZGM0ZmFjWCECNpnvLdFcsaVbCPUlOzr78XtBoD3mm3jQcldsQ6iKUBFhZKNhZVggrER4tfjjiH0e-lf9H---us1yjQQi__ZCFB9yFwH4jDphc1ggZfP2KcQOWA110vLz11caZF1PuXN606caPO2ZCAhfdvphclggadgz0psQELNif3xJ5J2d_TJWtRKfDFSj7h2ZD4WSFeykYWECYXN4QGZlNjAzNjA1NWM1MzVlZTBlYjI3MjQ1NmUzNjJlNmNkOWViNDNkMWQxODg0M2MzMDQ4MGU0YzE2YjI0MDY5MDZhY1ghAilA3g2_NriE94uTPISd2CM-90x53mK5QNM2iyTFDlnTYWSjYWVYIExR7bUzqM6-lRU7PbbEfnPW1vnSzCEN4SArmJZqp_7bYXNYIJMKRTSlXumUjPWXX5V8-hGPSZ-OXZJiEWm6_IB93OUDYXJYIB8YsigK7dMX59Oiy4Rh05xU0n0rVAPV7g_YFx564ZVa").unwrap();
  182. let token_without_slash = Token::from_str("cashuBo2FteCJodHRwczovL21pbnQubWluaWJpdHMuY2FzaC9CaXRjb2luYXVjc2F0YXSBomFpSABQBVDwSUFGYXCCpGFhCGFzeEBjODY1NmFkODgzNWY5ZWYzNWRhZDUxNmM0ZTllNTlmMDdjMWY4ODQ1NzY1ZjczYWE1YzIyNWE4YjgwYzRkYzRmYWNYIQI2me8t0VyxpVsI9SU7Ovvxe0GgPeabeNByV2xDqIpQEWFko2FlWCCsRHi1-OOIfR76V_0f7766zXKNBCL_9kIUH3IXAfiMOmFzWCBl8_YpxA5YDXXS8vPXVxpkXU-5c3rTpxo87ZkICF92-mFyWCBp2DPSmxAQs2J_fEnknZ39Mla1Ep8MVKPuHZkPhZIV7KRhYQJhc3hAZmU2MDM2MDU1YzUzNWVlMGViMjcyNDU2ZTM2MmU2Y2Q5ZWI0M2QxZDE4ODQzYzMwNDgwZTRjMTZiMjQwNjkwNmFjWCECKUDeDb82uIT3i5M8hJ3YIz73THneYrlA0zaLJMUOWdNhZKNhZVggTFHttTOozr6VFTs9tsR-c9bW-dLMIQ3hICuYlmqn_tthc1ggkwpFNKVe6ZSM9ZdflXz6EY9Jn45dkmIRabr8gH3c5QNhclggHxiyKArt0xfn06LLhGHTnFTSfStUA9XuD9gXHnrhlVo").unwrap();
  183. let url_with_slash = token_with_slash.mint_url().unwrap();
  184. let url_without_slash = token_without_slash.mint_url().unwrap();
  185. assert_eq!(url_without_slash.to_string(), url_with_slash.to_string());
  186. assert_eq!(url_without_slash, url_with_slash);
  187. }
  188. }