mint_url.rs 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  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, Serialize, Deserialize)]
  22. pub struct MintUrl(String);
  23. impl MintUrl {
  24. fn format_url(url: &str) -> Result<String, Error> {
  25. ensure_cdk!(!url.is_empty(), Error::InvalidUrl);
  26. let url = url.trim_end_matches('/');
  27. // https://URL.com/path/TO/resource -> https://url.com/path/TO/resource
  28. let protocol = url
  29. .split("://")
  30. .nth(0)
  31. .ok_or(Error::InvalidUrl)?
  32. .to_lowercase();
  33. let host = url
  34. .split("://")
  35. .nth(1)
  36. .ok_or(Error::InvalidUrl)?
  37. .split('/')
  38. .nth(0)
  39. .ok_or(Error::InvalidUrl)?
  40. .to_lowercase();
  41. let path = url
  42. .split("://")
  43. .nth(1)
  44. .ok_or(Error::InvalidUrl)?
  45. .split('/')
  46. .skip(1)
  47. .collect::<Vec<&str>>()
  48. .join("/");
  49. let mut formatted_url = format!("{}://{}", protocol, host);
  50. if !path.is_empty() {
  51. formatted_url.push_str(&format!("/{}/", path));
  52. }
  53. Ok(formatted_url)
  54. }
  55. /// Join onto url
  56. pub fn join(&self, path: &str) -> Result<Url, Error> {
  57. Url::parse(&self.0)
  58. .and_then(|url| url.join(path))
  59. .map_err(Into::into)
  60. }
  61. /// Append path elements onto the URL
  62. pub fn join_paths(&self, path_elements: &[&str]) -> Result<Url, Error> {
  63. self.join(&path_elements.join("/"))
  64. }
  65. }
  66. impl FromStr for MintUrl {
  67. type Err = Error;
  68. fn from_str(url: &str) -> Result<Self, Self::Err> {
  69. let formatted_url = Self::format_url(url);
  70. match formatted_url {
  71. Ok(url) => Ok(Self(url)),
  72. Err(_) => Err(Error::InvalidUrl),
  73. }
  74. }
  75. }
  76. impl fmt::Display for MintUrl {
  77. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  78. write!(f, "{}", self.0)
  79. }
  80. }
  81. #[cfg(test)]
  82. mod tests {
  83. use super::*;
  84. #[test]
  85. fn test_trim_trailing_slashes() {
  86. let very_unformatted_url = "http://url-to-check.com////";
  87. let unformatted_url = "http://url-to-check.com/";
  88. let formatted_url = "http://url-to-check.com";
  89. let very_trimmed_url = MintUrl::from_str(very_unformatted_url).unwrap();
  90. assert_eq!(formatted_url, very_trimmed_url.to_string());
  91. let trimmed_url = MintUrl::from_str(unformatted_url).unwrap();
  92. assert_eq!(formatted_url, trimmed_url.to_string());
  93. let unchanged_url = MintUrl::from_str(formatted_url).unwrap();
  94. assert_eq!(formatted_url, unchanged_url.to_string());
  95. }
  96. #[test]
  97. fn test_case_insensitive() {
  98. let wrong_cased_url = "http://URL-to-check.com";
  99. let correct_cased_url = "http://url-to-check.com";
  100. let cased_url_formatted = MintUrl::from_str(wrong_cased_url).unwrap();
  101. assert_eq!(correct_cased_url, cased_url_formatted.to_string());
  102. let wrong_cased_url_with_path = "http://URL-to-check.com/PATH/to/check";
  103. let correct_cased_url_with_path = "http://url-to-check.com/PATH/to/check/";
  104. let cased_url_with_path_formatted = MintUrl::from_str(wrong_cased_url_with_path).unwrap();
  105. assert_eq!(
  106. correct_cased_url_with_path,
  107. cased_url_with_path_formatted.to_string()
  108. );
  109. }
  110. #[test]
  111. fn test_join_paths() {
  112. let url_no_path = "http://url-to-check.com";
  113. let url = MintUrl::from_str(url_no_path).unwrap();
  114. assert_eq!(
  115. format!("{url_no_path}/hello/world"),
  116. url.join_paths(&["hello", "world"]).unwrap().to_string()
  117. );
  118. let url_no_path_with_slash = "http://url-to-check.com/";
  119. let url = MintUrl::from_str(url_no_path_with_slash).unwrap();
  120. assert_eq!(
  121. format!("{url_no_path_with_slash}hello/world"),
  122. url.join_paths(&["hello", "world"]).unwrap().to_string()
  123. );
  124. let url_with_path = "http://url-to-check.com/my/path";
  125. let url = MintUrl::from_str(url_with_path).unwrap();
  126. assert_eq!(
  127. format!("{url_with_path}/hello/world"),
  128. url.join_paths(&["hello", "world"]).unwrap().to_string()
  129. );
  130. let url_with_path_with_slash = "http://url-to-check.com/my/path/";
  131. let url = MintUrl::from_str(url_with_path_with_slash).unwrap();
  132. assert_eq!(
  133. format!("{url_with_path_with_slash}hello/world"),
  134. url.join_paths(&["hello", "world"]).unwrap().to_string()
  135. );
  136. }
  137. }