1use iri_rs_core::{IriBuf, Positions};
32use proc_macro::TokenStream;
33use proc_macro_crate::{FoundCrate, crate_name};
34use proc_macro2::TokenStream as TokenStream2;
35use quote::{format_ident, quote};
36use std::collections::HashMap;
37use syn::{
38 Attribute,
39 Data,
40 DeriveInput,
41 Fields,
42 LitStr,
43 Meta,
44 Token,
45 parse::{Parse, ParseStream},
46 parse_macro_input,
47 parse2,
48};
49
50macro_rules! bail {
51 ($span:expr, $($arg:tt)*) => {
52 return syn::Error::new($span, format!($($arg)*)).to_compile_error().into()
53 };
54}
55
56fn core_path() -> TokenStream2 {
57 match crate_name("iri-rs") {
58 Ok(FoundCrate::Itself) => quote!(::iri_rs::__private),
59 Ok(FoundCrate::Name(n)) => {
60 let id = format_ident!("{}", n);
61 quote!(::#id::__private)
62 }
63 Err(_) => match crate_name("iri-rs-core").expect("expected `iri-rs` or `iri-rs-core` in dependencies") {
64 FoundCrate::Itself => quote!(crate),
65 FoundCrate::Name(n) => {
66 let id = format_ident!("{}", n);
67 quote!(::#id)
68 }
69 },
70 }
71}
72
73struct PrefixArgs {
74 name: LitStr,
75 iri: LitStr,
76}
77
78impl Parse for PrefixArgs {
79 fn parse(input: ParseStream) -> syn::Result<Self> {
80 let name: LitStr = input.parse()?;
81 let _: Token![=] = input.parse()?;
82 let iri: LitStr = input.parse()?;
83 Ok(Self { name, iri })
84 }
85}
86
87struct IriArg(LitStr);
88
89impl Parse for IriArg {
90 fn parse(input: ParseStream) -> syn::Result<Self> {
91 Ok(Self(input.parse()?))
92 }
93}
94
95fn meta_list_tokens<'a>(attr: &'a Attribute, name: &str) -> Option<&'a TokenStream2> {
96 match &attr.meta {
97 Meta::List(list) if list.path.is_ident(name) => Some(&list.tokens),
98 _ => None,
99 }
100}
101
102fn expand_iri(value: &str, prefixes: &HashMap<String, IriBuf>) -> Result<IriBuf, String> {
103 if let Some(index) = value.find(':') {
104 if index > 0 {
105 let (prefix, rest) = value.split_at(index);
106 let suffix = &rest[1..];
107 if !suffix.starts_with("//") {
108 if let Some(base) = prefixes.get(prefix) {
109 let concat = format!("{}{}", base.as_str(), suffix);
110 return IriBuf::new(concat.clone()).map_err(|_| format!("invalid IRI `{concat}`"));
111 }
112 }
113 }
114 }
115 IriBuf::new(value.to_owned()).map_err(|_| format!("invalid IRI `{value}`"))
116}
117
118fn positions_tokens(core: &TokenStream2, p: Positions) -> TokenStream2 {
119 let s = p.scheme_end;
120 let a = p.authority_end;
121 let pe = p.path_end;
122 let q = p.query_end;
123 quote! {
124 #core::Positions {
125 scheme_end: #s,
126 authority_end: #a,
127 path_end: #pe,
128 query_end: #q,
129 }
130 }
131}
132
133fn iri_const_tokens(core: &TokenStream2, iri: &IriBuf) -> TokenStream2 {
134 let s = iri.as_str();
135 let p = positions_tokens(core, iri.positions());
136 quote! {
137 #core::Iri::<&'static str>::from_raw_parts(#s, #p)
138 }
139}
140
141#[proc_macro_derive(IriEnum, attributes(iri_prefix, iri))]
142#[allow(clippy::too_many_lines, clippy::missing_panics_doc)]
148pub fn iri_enum_derive(input: TokenStream) -> TokenStream {
149 let ast = parse_macro_input!(input as DeriveInput);
150 let core = core_path();
151
152 let mut prefixes: HashMap<String, IriBuf> = HashMap::new();
153 for attr in &ast.attrs {
154 let Some(tokens) = meta_list_tokens(attr, "iri_prefix") else {
155 continue;
156 };
157 let args: PrefixArgs = match parse2(tokens.clone()) {
158 Ok(a) => a,
159 Err(e) => return e.to_compile_error().into(),
160 };
161 match IriBuf::new(args.iri.value()) {
162 Ok(iri) => {
163 prefixes.insert(args.name.value(), iri);
164 }
165 Err(e) => bail!(args.iri.span(), "invalid IRI `{}` for prefix", e.0),
166 }
167 }
168
169 let Data::Enum(data) = ast.data else {
170 bail!(ast.ident.span(), "IriEnum can only be derived for enums");
171 };
172
173 let type_id = ast.ident;
174 let mut try_from_arms = TokenStream2::new();
175 let mut try_from_default = quote! { ::core::result::Result::Err(()) };
176 let mut into_arms = TokenStream2::new();
177
178 for variant in data.variants {
179 let variant_ident = variant.ident;
180 let mut variant_iri: Option<IriBuf> = None;
181
182 for attr in &variant.attrs {
183 let Some(tokens) = meta_list_tokens(attr, "iri") else {
184 continue;
185 };
186 let IriArg(lit) = match parse2(tokens.clone()) {
187 Ok(a) => a,
188 Err(e) => return e.to_compile_error().into(),
189 };
190 match expand_iri(&lit.value(), &prefixes) {
191 Ok(iri) => variant_iri = Some(iri),
192 Err(msg) => bail!(lit.span(), "{} for variant `{}`", msg, variant_ident),
193 }
194 }
195
196 match variant.fields {
197 Fields::Unit => {
198 let Some(iri) = variant_iri else {
199 bail!(variant_ident.span(), "missing `#[iri(...)]` attribute for unit variant `{}`", variant_ident);
200 };
201 let raw = iri.as_str();
202 let iri_expr = iri_const_tokens(&core, &iri);
203 try_from_arms.extend(quote! {
204 #raw => ::core::result::Result::Ok(#type_id::#variant_ident),
205 });
206 into_arms.extend(quote! {
207 #type_id::#variant_ident => #iri_expr,
208 });
209 }
210 Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {
211 let ty = fields.unnamed.into_iter().next().unwrap().ty;
212 try_from_default = quote! {
213 match <#ty as ::core::convert::TryFrom<&#core::Iri<__T>>>::try_from(iri) {
214 ::core::result::Result::Ok(value) => ::core::result::Result::Ok(#type_id::#variant_ident(value)),
215 ::core::result::Result::Err(_) => { #try_from_default }
216 }
217 };
218 into_arms.extend(quote! {
219 #type_id::#variant_ident(v) => <&#ty as ::core::convert::Into<#core::Iri<&'static str>>>::into(v),
220 });
221 }
222 Fields::Named(_) => bail!(variant_ident.span(), "variants with named fields are unsupported"),
223 Fields::Unnamed(_) => bail!(variant_ident.span(), "variants with more than one field are unsupported"),
224 }
225 }
226
227 let output = quote! {
228 impl<__T> ::core::convert::TryFrom<&#core::Iri<__T>> for #type_id
229 where
230 __T: ::core::ops::Deref<Target = str>,
231 {
232 type Error = ();
233
234 #[inline]
235 fn try_from(iri: &#core::Iri<__T>) -> ::core::result::Result<Self, ()> {
236 match iri.as_str() {
237 #try_from_arms
238 _ => #try_from_default,
239 }
240 }
241 }
242
243 impl ::core::convert::From<&#type_id> for #core::Iri<&'static str> {
244 #[inline]
245 fn from(value: &#type_id) -> Self {
246 match value {
247 #into_arms
248 }
249 }
250 }
251
252 impl ::core::convert::From<#type_id> for #core::Iri<&'static str> {
253 #[inline]
254 fn from(value: #type_id) -> Self {
255 <&#type_id as ::core::convert::Into<#core::Iri<&'static str>>>::into(&value)
256 }
257 }
258 };
259
260 output.into()
261}