mint_url.rs 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  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. /// Url Error
  10. #[derive(Debug, Error, PartialEq, Eq)]
  11. pub enum Error {
  12. /// Url error
  13. #[error(transparent)]
  14. Url(#[from] ParseError),
  15. /// Invalid URL structure
  16. #[error("Invalid URL")]
  17. InvalidUrl,
  18. }
  19. /// MintUrl Url
  20. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
  21. pub struct MintUrl(String);
  22. impl MintUrl {
  23. fn format_url(url: &str) -> Result<String, Error> {
  24. if url.is_empty() {
  25. return Err(Error::InvalidUrl);
  26. }
  27. let url = url.trim_end_matches('/');
  28. // https://URL.com/path/TO/resource -> https://url.com/path/TO/resource
  29. let protocol = url
  30. .split("://")
  31. .nth(0)
  32. .ok_or(Error::InvalidUrl)?
  33. .to_lowercase();
  34. let host = url
  35. .split("://")
  36. .nth(1)
  37. .ok_or(Error::InvalidUrl)?
  38. .split('/')
  39. .nth(0)
  40. .ok_or(Error::InvalidUrl)?
  41. .to_lowercase();
  42. let path = url
  43. .split("://")
  44. .nth(1)
  45. .ok_or(Error::InvalidUrl)?
  46. .split('/')
  47. .skip(1)
  48. .collect::<Vec<&str>>()
  49. .join("/");
  50. let mut formatted_url = format!("{}://{}", protocol, host);
  51. if !path.is_empty() {
  52. formatted_url.push_str(&format!("/{}", path));
  53. }
  54. Ok(formatted_url)
  55. }
  56. /// Join onto url
  57. pub fn join(&self, path: &str) -> Result<Url, Error> {
  58. Url::parse(&self.0)
  59. .and_then(|url| url.join(path))
  60. .map_err(Into::into)
  61. }
  62. /// Append path elements onto the URL
  63. pub fn join_paths(&self, path_elements: &[&str]) -> Result<Url, Error> {
  64. self.join(&path_elements.join("/"))
  65. }
  66. }
  67. impl FromStr for MintUrl {
  68. type Err = Error;
  69. fn from_str(url: &str) -> Result<Self, Self::Err> {
  70. let formatted_url = Self::format_url(url);
  71. match formatted_url {
  72. Ok(url) => Ok(Self(url)),
  73. Err(_) => Err(Error::InvalidUrl),
  74. }
  75. }
  76. }
  77. impl fmt::Display for MintUrl {
  78. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  79. write!(f, "{}", self.0)
  80. }
  81. }
  82. #[cfg(test)]
  83. mod tests {
  84. use super::*;
  85. #[test]
  86. fn test_trim_trailing_slashes() {
  87. let very_unformatted_url = "http://url-to-check.com////";
  88. let unformatted_url = "http://url-to-check.com/";
  89. let formatted_url = "http://url-to-check.com";
  90. let very_trimmed_url = MintUrl::from_str(very_unformatted_url).unwrap();
  91. assert_eq!(formatted_url, very_trimmed_url.to_string());
  92. let trimmed_url = MintUrl::from_str(unformatted_url).unwrap();
  93. assert_eq!(formatted_url, trimmed_url.to_string());
  94. let unchanged_url = MintUrl::from_str(formatted_url).unwrap();
  95. assert_eq!(formatted_url, unchanged_url.to_string());
  96. }
  97. #[test]
  98. fn test_case_insensitive() {
  99. let wrong_cased_url = "http://URL-to-check.com";
  100. let correct_cased_url = "http://url-to-check.com";
  101. let cased_url_formatted = MintUrl::from_str(wrong_cased_url).unwrap();
  102. assert_eq!(correct_cased_url, cased_url_formatted.to_string());
  103. let wrong_cased_url_with_path = "http://URL-to-check.com/PATH/to/check";
  104. let correct_cased_url_with_path = "http://url-to-check.com/PATH/to/check";
  105. let cased_url_with_path_formatted = MintUrl::from_str(wrong_cased_url_with_path).unwrap();
  106. assert_eq!(
  107. correct_cased_url_with_path,
  108. cased_url_with_path_formatted.to_string()
  109. );
  110. }
  111. }