use anyhow::{Context as _, Result};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ResolvedUrl(String);
impl TryFrom<&str> for ResolvedUrl {
type Error = anyhow::Error;
fn try_from(url: &str) -> Result<Self> {
Self::validate(url)?;
Ok(Self(url.trim_matches(' ').to_string()))
}
}
impl TryFrom<String> for ResolvedUrl {
type Error = anyhow::Error;
fn try_from(url: String) -> Result<Self> {
Self::try_from(url.as_str())
}
}
impl ResolvedUrl {
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
pub fn with_last_segment(&self, raw_url: &str) -> Result<Self> {
if let Ok(url) = raw_url.try_into() {
return Ok(url);
}
let last_slash = self.0.rfind(['/', '\\']).context("No slash in URL")?;
let url_prefix = &self.0[..=last_slash];
Self::try_from(format!("{url_prefix}{raw_url}"))
}
pub fn validate(url: &str) -> Result<()> {
let url = url.trim_matches(' ');
anyhow::ensure!(
url.starts_with("https://") || url.starts_with("http://") || url.starts_with("file://"),
"Invalid protocol in URL: {url}"
);
Ok(())
}
}
#[cfg(test)]
mod tests {
use test_log::test;
#[test]
fn test_json_table_header_score_url() {
use super::ResolvedUrl;
assert_eq!(
ResolvedUrl::try_from("https://stellabms.xyz/fr/table.html")
.unwrap()
.with_last_segment("score.json")
.ok()
.map(|url| url.0)
.as_deref(),
Some("https://stellabms.xyz/fr/score.json")
);
assert_eq!(
ResolvedUrl::try_from("http://flowermaster.web.fc2.com/lrnanido/gla/LN.html")
.unwrap()
.with_last_segment("http://flowermaster.web.fc2.com/lrnanido/gla/score.json")
.ok()
.map(|url| url.0)
.as_deref(),
Some("http://flowermaster.web.fc2.com/lrnanido/gla/score.json")
);
assert_eq!(
ResolvedUrl::try_from("file:///a/b")
.unwrap()
.with_last_segment("file:///c")
.ok()
.map(|url| url.0)
.as_deref(),
Some("file:///c")
);
assert_eq!(
ResolvedUrl::try_from("file://a/b")
.unwrap()
.with_last_segment("c")
.ok()
.map(|url| url.0)
.as_deref(),
Some("file://a/c")
);
assert_eq!(
ResolvedUrl::try_from("file://D:\\a\\b")
.unwrap()
.with_last_segment("file://D:\\c")
.ok()
.map(|url| url.0)
.as_deref(),
Some("file://D:\\c")
);
assert_eq!(
ResolvedUrl::try_from("file://D:\\a\\b")
.unwrap()
.with_last_segment("c")
.ok()
.map(|url| url.0)
.as_deref(),
Some("file://D:\\a\\c")
);
}
#[test]
fn constructs_url() {
use super::ResolvedUrl;
assert_eq!(ResolvedUrl::try_from(" file:// ").unwrap().0, "file://");
assert_eq!(ResolvedUrl::try_from(" http:// ").unwrap().0, "http://");
assert_eq!(ResolvedUrl::try_from(" https:// ").unwrap().0, "https://");
assert_eq!(
ResolvedUrl::try_from(" invalid:// ")
.unwrap_err()
.to_string(),
"Invalid protocol in URL: invalid://"
);
}
}