use std::fmt;
use quote::ToTokens;
#[derive(Clone)]
pub struct TypeKey {
canon: std::rc::Rc<str>,
}
impl PartialEq for TypeKey {
fn eq(&self, other: &Self) -> bool {
self.canon == other.canon
}
}
impl Eq for TypeKey {}
impl std::hash::Hash for TypeKey {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.canon.hash(state)
}
}
impl PartialOrd for TypeKey {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for TypeKey {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.canon.cmp(&other.canon)
}
}
impl fmt::Debug for TypeKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("TypeKey").field(&&*self.canon).finish()
}
}
#[derive(Debug)]
pub struct TypeKeyParseError {
pub input: String,
pub error: syn::Error,
}
impl fmt::Display for TypeKeyParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "invalid type `{}`: {}", self.input, self.error)
}
}
impl std::error::Error for TypeKeyParseError {}
impl TypeKey {
pub fn parse(s: &str) -> Result<Self, TypeKeyParseError> {
let ty: syn::Type = syn::parse_str(s).map_err(|error| TypeKeyParseError {
input: s.to_string(),
error,
})?;
Ok(Self::from_type(&ty))
}
pub fn from_type(ty: &syn::Type) -> Self {
Self {
canon: crate::flat::canonical_type(ty)
.to_token_stream()
.to_string()
.into(),
}
}
pub fn from_ident(ident: &syn::Ident) -> Self {
Self::from_type(&syn::parse_quote!(#ident))
}
pub fn as_str(&self) -> &str {
&self.canon
}
pub fn ident(&self) -> Option<syn::Ident> {
let (ident, generic) = self.path_segments()?.pop()?;
if generic {
return None;
}
Some(ident)
}
pub fn short_name(&self) -> Option<String> {
Some(self.path_segments()?.pop()?.0.to_string())
}
fn path_segments(&self) -> Option<Vec<(syn::Ident, bool)>> {
let mut rest: &str = &self.canon;
if rest.starts_with('<') {
rest = rest[close_angle(rest)? + 1..]
.trim_start()
.strip_prefix("::")?;
}
let bytes = rest.as_bytes();
let mut out = Vec::new();
let mut depth = 0usize;
let mut start = 0usize;
let mut ident_end: Option<usize> = None;
let mut i = 0usize;
while i < bytes.len() {
match bytes[i] {
b'<' => {
if depth == 0 && ident_end.is_none() {
ident_end = Some(i);
}
depth += 1;
}
b'>' if i > 0 && bytes[i - 1] == b'-' => {}
b'>' => depth = depth.saturating_sub(1),
b':' if depth == 0 && bytes.get(i + 1) == Some(&b':') => {
out.push(segment(rest, start, ident_end, i)?);
i += 2;
start = i;
ident_end = None;
continue;
}
_ => {}
}
i += 1;
}
out.push(segment(rest, start, ident_end, bytes.len())?);
Some(out)
}
}
fn segment(
s: &str,
start: usize,
ident_end: Option<usize>,
end: usize,
) -> Option<(syn::Ident, bool)> {
let text = s[start..ident_end.unwrap_or(end)].trim();
Some((
syn::parse_str::<syn::Ident>(text).ok()?,
ident_end.is_some(),
))
}
fn close_angle(s: &str) -> Option<usize> {
let bytes = s.as_bytes();
let mut depth = 0usize;
for (i, b) in bytes.iter().enumerate() {
match b {
b'<' => depth += 1,
b'>' if i > 0 && bytes[i - 1] == b'-' => {}
b'>' => {
depth = depth.saturating_sub(1);
if depth == 0 {
return Some(i);
}
}
_ => {}
}
}
None
}
impl fmt::Display for TypeKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.canon)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types_util::bare_path_ident;
const SHAPES: &[&str] = &[
"Foo",
"a::Foo",
"a::b::Foo",
"std::string::String",
"Vec<u8>",
"Vec<a::B>",
"Vec<Vec<u8>>",
"a::Foo<u8>::Bar",
"Foo<u8>::Assoc",
"<T as Tr>::Item",
"Vec<fn() -> a::B>",
"Publisher<'static>",
"Option<Box<Node>>",
"&Foo",
"&mut Foo",
"&[u8]",
"[u8; 4]",
"()",
"(u8, u8)",
"(a::B, c::D)",
"*const u8",
"dyn Error",
"fn() -> u8",
"fn(u8) -> a::B",
];
#[test]
fn key_name_accessors_match_the_syn_walks() {
for spec in SHAPES {
let ty: syn::Type = syn::parse_str(spec).expect("test shape parses");
let key = TypeKey::from_type(&ty);
assert_eq!(
key.ident(),
bare_path_ident(&crate::flat::canonical_type(&ty)),
"ident() disagrees with bare_path_ident on `{spec}` (canon `{key}`)"
);
let expected_short = match &crate::flat::canonical_type(&ty) {
syn::Type::Path(tp) => tp.path.segments.last().map(|s| s.ident.to_string()),
_ => None,
};
assert_eq!(
key.short_name(),
expected_short,
"short_name() disagrees with the last-segment rule on `{spec}` (canon `{key}`)"
);
}
}
#[test]
fn short_name_reads_through_last_segment_generics_and_ident_does_not() {
let key = TypeKey::from_type(&syn::parse_quote!(Publisher<'static>));
assert_eq!(key.short_name().as_deref(), Some("Publisher"));
assert_eq!(key.ident(), None);
let nested = TypeKey::from_type(&syn::parse_quote!(a::Foo<u8>::Bar));
assert_eq!(nested.short_name().as_deref(), Some("Bar"));
assert_eq!(
nested.ident().map(|i| i.to_string()).as_deref(),
Some("Bar")
);
}
#[test]
fn qualified_self_paths_name_their_tail() {
let key = TypeKey::from_type(&syn::parse_quote!(<T as Tr>::Item));
assert_eq!(key.short_name().as_deref(), Some("Item"));
assert_eq!(key.ident().map(|i| i.to_string()).as_deref(), Some("Item"));
}
#[test]
fn separators_inside_generic_arguments_are_not_path_separators() {
for (spec, expected) in [
("Vec<a::B>", Some("Vec")),
("Vec<fn() -> a::B>", Some("Vec")),
("Vec<Vec<a::B>>", Some("Vec")),
] {
let key = TypeKey::from_type(&syn::parse_str(spec).expect("test shape"));
assert_eq!(key.short_name().as_deref(), expected, "on `{spec}`");
}
}
#[test]
fn ident_round_trips_through_from_ident() {
let ident = syn::Ident::new("ZKeyExpr", proc_macro2::Span::call_site());
let key = TypeKey::from_ident(&ident);
assert_eq!(key.ident().as_ref(), Some(&ident));
assert_eq!(key.short_name().as_deref(), Some("ZKeyExpr"));
}
}