use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Url {
Unresolved(String),
Resolved(ResolvedUrl),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ResolvedUrl {
pub href: String,
pub kind: UrlKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum UrlKind {
Internal,
Wikilink,
External,
AssetNewtab,
Asset,
Anchor,
Mailto,
Tel,
}
impl Url {
pub fn unresolved(input: impl Into<String>) -> Self {
Url::Unresolved(input.into())
}
pub fn resolved(href: impl Into<String>, kind: UrlKind) -> Self {
Url::Resolved(ResolvedUrl {
href: href.into(),
kind,
})
}
pub fn is_unresolved(&self) -> bool {
matches!(self, Url::Unresolved(_))
}
pub fn is_resolved(&self) -> bool {
matches!(self, Url::Resolved(_))
}
}
impl ResolvedUrl {
pub fn new(href: impl Into<String>, kind: UrlKind) -> Self {
ResolvedUrl {
href: href.into(),
kind,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unresolved_construction_carries_input_verbatim() {
let u = Url::unresolved("docs/");
match u {
Url::Unresolved(s) => assert_eq!(s, "docs/"),
Url::Resolved(_) => panic!("expected Unresolved"),
}
}
#[test]
fn unresolved_preserves_moss_resolved_prefix() {
let u = Url::unresolved("moss-resolved:docs/index.md");
assert!(u.is_unresolved());
match u {
Url::Unresolved(s) => assert_eq!(s, "moss-resolved:docs/index.md"),
_ => unreachable!(),
}
}
#[test]
fn resolved_with_wikilink_kind() {
let u = Url::resolved("../docs/", UrlKind::Wikilink);
assert!(u.is_resolved());
let Url::Resolved(r) = &u else {
panic!("expected Resolved, got {u:?}")
};
assert_eq!(r.href, "../docs/");
assert_eq!(r.kind, UrlKind::Wikilink);
}
#[test]
fn resolved_kinds_are_distinct() {
let kinds = [
UrlKind::Internal,
UrlKind::Wikilink,
UrlKind::External,
UrlKind::AssetNewtab,
UrlKind::Asset,
UrlKind::Anchor,
UrlKind::Mailto,
UrlKind::Tel,
];
let unique: std::collections::HashSet<_> = kinds.iter().collect();
assert_eq!(unique.len(), kinds.len());
}
#[test]
fn state_distinction_via_is_methods() {
let u = Url::unresolved("foo");
assert!(u.is_unresolved());
assert!(!u.is_resolved());
let r = Url::resolved("foo", UrlKind::Internal);
assert!(r.is_resolved());
assert!(!r.is_unresolved());
}
#[test]
fn serde_round_trip_unresolved() {
let u = Url::unresolved("docs/");
let s = serde_json::to_string(&u).expect("serialize");
let back: Url = serde_json::from_str(&s).expect("deserialize");
assert_eq!(u, back);
}
#[test]
fn serde_round_trip_resolved() {
let u = Url::resolved("../docs/", UrlKind::Wikilink);
let s = serde_json::to_string(&u).expect("serialize");
let back: Url = serde_json::from_str(&s).expect("deserialize");
assert_eq!(u, back);
}
#[test]
fn serde_uses_externally_tagged_form() {
let u = Url::unresolved("x");
let s = serde_json::to_string(&u).expect("serialize");
assert_eq!(s, r#"{"unresolved":"x"}"#);
let r = Url::resolved("x", UrlKind::Internal);
let s = serde_json::to_string(&r).expect("serialize");
assert_eq!(s, r#"{"resolved":{"href":"x","kind":"internal"}}"#);
}
}