lr2-oxytabler 0.9.0

Table manager for Lunatic Rave 2
use anyhow::{Context as _, Result};

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ResolvedUrl(pub String);

pub fn extract_raw_header_url(html: &str) -> Result<&str> {
    let lhs_search = r#"<meta name="bmstable" content=""#;
    let lhs = html.find(lhs_search).context("missing bmstable meta")?;
    let html = &html[lhs + lhs_search.len()..];
    let rhs = html.find('"').context("missing bmstable meta rhs")?;
    Ok(&html[..rhs])
}

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 {
    pub fn resolve_json_url(&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];
        format!("{url_prefix}{raw_url}")
            .try_into()
            .context("don't expect this to fail actually")
    }

    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 table URL: {url}"
        );
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use test_log::test;

    #[test]
    fn test_extract_header_url() {
        use super::extract_raw_header_url;

        assert_eq!(
            extract_raw_header_url(
                r#"<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"><html><head>
<meta name"blahblah" /><meta name="bmstable" content="header.json" />"#
            )
            .ok(),
            Some("header.json")
        );

        assert_eq!(
            extract_raw_header_url(r#"<meta name="bmstable" content="header.json"/>"#).ok(),
            Some("header.json")
        );

        assert_eq!(
            format!("{}", extract_raw_header_url("").unwrap_err()),
            "missing bmstable meta"
        );

        assert_eq!(
            format!(
                "{}",
                extract_raw_header_url(r#"<meta name="bmstable" content="header.json"#)
                    .unwrap_err()
            ),
            "missing bmstable meta rhs"
        );

        // In practice, looks like it's fair to check for literal match like we do, instead of
        // parsing.
        assert_eq!(
            format!(
                "{}",
                extract_raw_header_url(r#"<meta  name="bmstable"  content="header.json"  />"#)
                    .unwrap_err()
            ),
            "missing bmstable meta"
        );
    }

    #[test]
    fn test_json_table_header_score_url() {
        use crate::ResolvedUrl;

        assert_eq!(
            ResolvedUrl::try_from("https://stellabms.xyz/fr/table.html")
                .unwrap()
                .resolve_json_url("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()
                .resolve_json_url("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()
                .resolve_json_url("file:///c")
                .ok()
                .map(|url| url.0)
                .as_deref(),
            Some("file:///c")
        );

        assert_eq!(
            ResolvedUrl::try_from("file://a/b")
                .unwrap()
                .resolve_json_url("c")
                .ok()
                .map(|url| url.0)
                .as_deref(),
            Some("file://a/c")
        );

        assert_eq!(
            ResolvedUrl::try_from("file://D:\\a\\b")
                .unwrap()
                .resolve_json_url("file://D:\\c")
                .ok()
                .map(|url| url.0)
                .as_deref(),
            Some("file://D:\\c")
        );

        assert_eq!(
            ResolvedUrl::try_from("file://D:\\a\\b")
                .unwrap()
                .resolve_json_url("c")
                .ok()
                .map(|url| url.0)
                .as_deref(),
            Some("file://D:\\a\\c")
        );
    }

    #[test]
    fn resolved_url() {
        use crate::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://");
    }
}