pub(crate) struct LinkUrl {
pub(crate) url: url::Url,
pub(crate) name: Option<String>,
#[expect(unused)]
pub(crate) version: Option<String>,
}
pub(crate) fn parse_link_url(url: &str) -> Option<LinkUrl> {
let url = url::Url::parse(url).ok()?;
let segments = url.path_segments()?;
let mut reversed_segments = segments.rev();
let Some(mut maybe_version_or_name) = reversed_segments.next() else {
return Some(LinkUrl {
url,
name: None,
version: None,
});
};
if url.scheme() == "file" && maybe_version_or_name == "build" {
let Some(next) = reversed_segments.next() else {
return Some(LinkUrl {
url,
name: None,
version: None,
});
};
maybe_version_or_name = next;
}
if is_valid_version(maybe_version_or_name) {
let name = reversed_segments
.next()
.filter(|s| is_valid_graphql_name(s))
.map(String::from);
let version = Some(maybe_version_or_name.to_owned());
Some(LinkUrl { url, name, version })
} else if is_valid_graphql_name(maybe_version_or_name) {
let name = Some(maybe_version_or_name.to_owned());
Some(LinkUrl {
url,
name,
version: None,
})
} else {
Some(LinkUrl {
url,
name: None,
version: None,
})
}
}
fn is_valid_version(s: &str) -> bool {
let mut chars = s.chars();
let Some('v') = chars.next() else { return false };
let Some(digit) = chars.next() else { return false };
if !digit.is_ascii_digit() {
return false;
};
chars.all(|char| char.is_ascii_digit() || char == '.')
}
fn is_valid_graphql_name(s: &str) -> bool {
let mut chars = s.chars();
let Some(first_char) = chars.next() else {
return false;
};
if !first_char.is_ascii_alphabetic() && first_char != '_' {
return false;
}
for c in chars {
if !c.is_ascii_alphanumeric() && c != '_' {
return false;
}
}
true
}