use iri_rs_core::{IriBuf, Positions};
use proc_macro::TokenStream;
use proc_macro_crate::{FoundCrate, crate_name};
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote};
use std::collections::HashMap;
use syn::{
Attribute,
Data,
DeriveInput,
Fields,
LitStr,
Meta,
Token,
parse::{Parse, ParseStream},
parse_macro_input,
parse2,
};
macro_rules! bail {
($span:expr, $($arg:tt)*) => {
return syn::Error::new($span, format!($($arg)*)).to_compile_error().into()
};
}
fn core_path() -> TokenStream2 {
match crate_name("iri-rs") {
Ok(FoundCrate::Itself) => quote!(::iri_rs::__private),
Ok(FoundCrate::Name(n)) => {
let id = format_ident!("{}", n);
quote!(::#id::__private)
}
Err(_) => match crate_name("iri-rs-core").expect("expected `iri-rs` or `iri-rs-core` in dependencies") {
FoundCrate::Itself => quote!(crate),
FoundCrate::Name(n) => {
let id = format_ident!("{}", n);
quote!(::#id)
}
},
}
}
struct PrefixArgs {
name: LitStr,
iri: LitStr,
}
impl Parse for PrefixArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let name: LitStr = input.parse()?;
let _: Token![=] = input.parse()?;
let iri: LitStr = input.parse()?;
Ok(Self { name, iri })
}
}
struct IriArg(LitStr);
impl Parse for IriArg {
fn parse(input: ParseStream) -> syn::Result<Self> {
Ok(Self(input.parse()?))
}
}
fn meta_list_tokens<'a>(attr: &'a Attribute, name: &str) -> Option<&'a TokenStream2> {
match &attr.meta {
Meta::List(list) if list.path.is_ident(name) => Some(&list.tokens),
_ => None,
}
}
fn expand_iri(value: &str, prefixes: &HashMap<String, IriBuf>) -> Result<IriBuf, String> {
if let Some(index) = value.find(':') {
if index > 0 {
let (prefix, rest) = value.split_at(index);
let suffix = &rest[1..];
if !suffix.starts_with("//") {
if let Some(base) = prefixes.get(prefix) {
let concat = format!("{}{}", base.as_str(), suffix);
return IriBuf::new(concat.clone()).map_err(|_| format!("invalid IRI `{}`", concat));
}
}
}
}
IriBuf::new(value.to_owned()).map_err(|_| format!("invalid IRI `{}`", value))
}
fn positions_tokens(core: &TokenStream2, p: Positions) -> TokenStream2 {
let s = p.scheme_end;
let a = p.authority_end;
let pe = p.path_end;
let q = p.query_end;
quote! {
#core::Positions {
scheme_end: #s,
authority_end: #a,
path_end: #pe,
query_end: #q,
}
}
}
fn iri_const_tokens(core: &TokenStream2, iri: &IriBuf) -> TokenStream2 {
let s = iri.as_str();
let p = positions_tokens(core, iri.positions());
quote! {
#core::Iri::<&'static str>::from_raw_parts(#s, #p)
}
}
#[proc_macro_derive(IriEnum, attributes(iri_prefix, iri))]
pub fn iri_enum_derive(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
let core = core_path();
let mut prefixes: HashMap<String, IriBuf> = HashMap::new();
for attr in &ast.attrs {
let Some(tokens) = meta_list_tokens(attr, "iri_prefix") else {
continue;
};
let args: PrefixArgs = match parse2(tokens.clone()) {
Ok(a) => a,
Err(e) => return e.to_compile_error().into(),
};
match IriBuf::new(args.iri.value()) {
Ok(iri) => {
prefixes.insert(args.name.value(), iri);
}
Err(e) => bail!(args.iri.span(), "invalid IRI `{}` for prefix", e.0),
}
}
let Data::Enum(data) = ast.data else {
bail!(ast.ident.span(), "IriEnum can only be derived for enums");
};
let type_id = ast.ident;
let mut try_from_arms = TokenStream2::new();
let mut try_from_default = quote! { ::core::result::Result::Err(()) };
let mut into_arms = TokenStream2::new();
for variant in data.variants {
let variant_ident = variant.ident;
let mut variant_iri: Option<IriBuf> = None;
for attr in &variant.attrs {
let Some(tokens) = meta_list_tokens(attr, "iri") else {
continue;
};
let IriArg(lit) = match parse2(tokens.clone()) {
Ok(a) => a,
Err(e) => return e.to_compile_error().into(),
};
match expand_iri(&lit.value(), &prefixes) {
Ok(iri) => variant_iri = Some(iri),
Err(msg) => bail!(lit.span(), "{} for variant `{}`", msg, variant_ident),
}
}
match variant.fields {
Fields::Unit => {
let Some(iri) = variant_iri else {
bail!(variant_ident.span(), "missing `#[iri(...)]` attribute for unit variant `{}`", variant_ident);
};
let raw = iri.as_str();
let iri_expr = iri_const_tokens(&core, &iri);
try_from_arms.extend(quote! {
#raw => ::core::result::Result::Ok(#type_id::#variant_ident),
});
into_arms.extend(quote! {
#type_id::#variant_ident => #iri_expr,
});
}
Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {
let ty = fields.unnamed.into_iter().next().unwrap().ty;
try_from_default = quote! {
match <#ty as ::core::convert::TryFrom<&#core::Iri<__T>>>::try_from(iri) {
::core::result::Result::Ok(value) => ::core::result::Result::Ok(#type_id::#variant_ident(value)),
::core::result::Result::Err(_) => { #try_from_default }
}
};
into_arms.extend(quote! {
#type_id::#variant_ident(v) => <&#ty as ::core::convert::Into<#core::Iri<&'static str>>>::into(v),
});
}
Fields::Named(_) => bail!(variant_ident.span(), "variants with named fields are unsupported"),
Fields::Unnamed(_) => bail!(variant_ident.span(), "variants with more than one field are unsupported"),
}
}
let output = quote! {
impl<__T> ::core::convert::TryFrom<&#core::Iri<__T>> for #type_id
where
__T: ::core::ops::Deref<Target = str>,
{
type Error = ();
#[inline]
fn try_from(iri: &#core::Iri<__T>) -> ::core::result::Result<Self, ()> {
match iri.as_str() {
#try_from_arms
_ => #try_from_default,
}
}
}
impl ::core::convert::From<&#type_id> for #core::Iri<&'static str> {
#[inline]
fn from(value: &#type_id) -> Self {
match value {
#into_arms
}
}
}
impl ::core::convert::From<#type_id> for #core::Iri<&'static str> {
#[inline]
fn from(value: #type_id) -> Self {
<&#type_id as ::core::convert::Into<#core::Iri<&'static str>>>::into(&value)
}
}
};
output.into()
}