iref_enum/
lib.rs

1//! This is a companion crate for `iref` providing a derive macro to declare
2//! enum types that converts into/from IRIs.
3//!
4//! Storage and comparison of IRIs can be costly. One may prefer the use of an enum
5//! type representing known IRIs with cheap conversion functions between the two.
6//! This crate provides a way to declare such enums in an simple way through the
7//! use of a `IriEnum` derive macro.
8//! This macro will implement `TryFrom<&Iri>` and `AsRef<Iri>` for you.
9//!
10//! ## Basic usage
11//!
12//! Use `#[derive(IriEnum)]` attribute to generate the implementation of
13//! `TryFrom<&Iri>` and `AsRef<Iri>` for the enum type.
14//! The IRI of each variant is defined with the `iri` attribute:
15//! ```rust
16//! use iref_enum::IriEnum;
17//!
18//! #[derive(IriEnum, PartialEq, Debug)]
19//! pub enum Vocab {
20//!   #[iri("https://schema.org/name")] Name,
21//!   #[iri("https://schema.org/knows")] Knows
22//! }
23//!
24//! let term: Vocab = static_iref::iri!("https://schema.org/name").try_into().unwrap();
25//! assert_eq!(term, Vocab::Name)
26//! ```
27//!
28//! Each variant must have at most one parameter.
29//! If it has a parameter, its type must implement `TryFrom<&Iri>` and
30//! `AsRef<Iri>`.
31//!
32//! ## Compact IRIs
33//!
34//! The derive macro also support compact IRIs using the special `iri_prefix` attribute.
35//! First declare a prefix associated to a given `IRI`.
36//! Then any `iri` attribute of the form `prefix:suffix` we be expanded into the concatenation of the prefix IRI and `suffix`.
37//!
38//! ```rust
39//! # use iref_enum::IriEnum;
40//! #[derive(IriEnum)]
41//! #[iri_prefix("schema" = "https://schema.org/")]
42//! pub enum Vocab {
43//!   #[iri("schema:name")] Name,
44//!   #[iri("schema:knows")] Knows
45//! }
46//! ```
47use iref::IriBuf;
48use proc_macro::TokenStream;
49use proc_macro2::TokenTree;
50use quote::quote;
51use std::collections::HashMap;
52
53macro_rules! error {
54	( $( $x:expr ),* ) => {
55		{
56			let msg = format!($($x),*);
57			let tokens: TokenStream = format!("compile_error!(\"{}\");", msg).parse().unwrap();
58			tokens
59		}
60	};
61}
62
63fn filter_attribute(
64	attr: syn::Attribute,
65	name: &str,
66) -> Result<Option<proc_macro2::TokenStream>, TokenStream> {
67	if let Some(attr_id) = attr.path.get_ident() {
68		if attr_id == name {
69			if let Some(TokenTree::Group(group)) = attr.tokens.into_iter().next() {
70				Ok(Some(group.stream()))
71			} else {
72				Err(error!("malformed `{}` attribute", name))
73			}
74		} else {
75			Ok(None)
76		}
77	} else {
78		Ok(None)
79	}
80}
81
82fn expand_iri(value: &str, prefixes: &HashMap<String, IriBuf>) -> Result<IriBuf, ()> {
83	if let Some(index) = value.find(':') {
84		if index > 0 {
85			let (prefix, suffix) = value.split_at(index);
86			let suffix = &suffix[1..suffix.len()];
87
88			if !suffix.starts_with("//") {
89				if let Some(base_iri) = prefixes.get(prefix) {
90					let concat = base_iri.as_str().to_string() + suffix;
91					if let Ok(iri) = IriBuf::new(concat) {
92						return Ok(iri);
93					} else {
94						return Err(());
95					}
96				}
97			}
98		}
99	}
100
101	if let Ok(iri) = IriBuf::new(value.to_owned()) {
102		Ok(iri)
103	} else {
104		Err(())
105	}
106}
107
108#[proc_macro_derive(IriEnum, attributes(iri_prefix, iri))]
109pub fn iri_enum_derive(input: TokenStream) -> TokenStream {
110	let ast: syn::DeriveInput = syn::parse(input).unwrap();
111
112	let mut prefixes = HashMap::new();
113	for attr in ast.attrs {
114		match filter_attribute(attr, "iri_prefix") {
115			Ok(Some(tokens)) => {
116				let mut tokens = tokens.into_iter();
117				if let Some(token) = tokens.next() {
118					if let Ok(prefix) = string_literal_token(token) {
119						if tokens.next().is_some() {
120							if let Some(token) = tokens.next() {
121								if let Ok(iri) = string_literal_token(token) {
122									match IriBuf::new(iri) {
123										Ok(iri) => {
124											prefixes.insert(prefix, iri);
125										}
126										Err(e) => {
127											return error!(
128												"invalid IRI `{}` for prefix `{}`",
129												e.0, prefix
130											);
131										}
132									}
133								} else {
134									return error!("expected a string literal");
135								}
136							} else {
137								return error!("expected a string literal");
138							}
139						} else {
140							return error!("expected `=` literal");
141						}
142					} else {
143						return error!("expected a string literal");
144					}
145				} else {
146					return error!("expected a string literal");
147				}
148			}
149			Ok(None) => (),
150			Err(tokens) => return tokens,
151		}
152	}
153
154	match ast.data {
155		syn::Data::Enum(e) => {
156			let type_id = ast.ident;
157			let mut try_from = proc_macro2::TokenStream::new();
158			let mut try_from_default = quote! { Err(()) };
159			let mut into = proc_macro2::TokenStream::new();
160
161			for variant in e.variants {
162				let variant_ident = variant.ident;
163				let mut variant_iri: Option<IriBuf> = None;
164
165				for attr in variant.attrs {
166					match filter_attribute(attr, "iri") {
167						Ok(Some(tokens)) => match string_literal(tokens) {
168							Ok(str) => {
169								if let Ok(iri) = expand_iri(str.as_str(), &prefixes) {
170									variant_iri = Some(iri)
171								} else {
172									return error!(
173										"invalid IRI `{}` for variant `{}`",
174										str, variant_ident
175									);
176								}
177							}
178							Err(_) => return error!("malformed `iri` attribute"),
179						},
180						Ok(None) => (),
181						Err(tokens) => return tokens,
182					}
183				}
184
185				match variant.fields {
186					syn::Fields::Unit => {
187						if let Some(iri) = variant_iri {
188							let iri = iri.as_str();
189
190							try_from.extend(quote! {
191								_ if iri == static_iref::iri!(#iri) => Ok(#type_id::#variant_ident),
192							});
193
194							into.extend(quote! {
195								#type_id::#variant_ident => static_iref::iri!(#iri),
196							});
197						} else {
198							return error!("missing IRI for enum variant `{}`", variant_ident);
199						}
200					}
201					syn::Fields::Named(_) => {
202						return error!("variants with named fields are unsupported")
203					}
204					syn::Fields::Unnamed(fields) => {
205						if fields.unnamed.len() == 1 {
206							let field = fields.unnamed.into_iter().next().unwrap();
207							let ty = field.ty;
208
209							try_from_default = quote! {
210								match #ty::try_from(iri) {
211									Ok(value) => Ok(#type_id::#variant_ident(value)),
212									Err(_) => {
213										#try_from_default
214									}
215								}
216							};
217
218							into.extend(quote! {
219								#type_id::#variant_ident(v) => v.into(),
220							});
221						} else {
222							return error!(
223								"variants with named more than one field are unsupported"
224							);
225						}
226					}
227				}
228			}
229
230			let output = quote! {
231				impl<'a> ::std::convert::TryFrom<&'a ::iref::Iri> for #type_id {
232					type Error = ();
233
234					#[inline]
235					fn try_from(iri: &'a ::iref::Iri) -> ::std::result::Result<#type_id, ()> {
236						match iri {
237							#try_from
238							_ => #try_from_default
239						}
240					}
241				}
242
243				impl<'a, 'i> From<&'a #type_id> for &'i ::iref::Iri {
244					#[inline]
245					fn from(vocab: &'a #type_id) -> &'i ::iref::Iri {
246						match vocab {
247							#into
248						}
249					}
250				}
251
252				impl<'i> From<#type_id> for &'i ::iref::Iri {
253					#[inline]
254					fn from(vocab: #type_id) -> &'i ::iref::Iri {
255						<&::iref::Iri as From<&#type_id>>::from(&vocab)
256					}
257				}
258
259				impl<'a, 'i> From<&'a #type_id> for &'i ::iref::IriRef {
260					#[inline]
261					fn from(vocab: &'a #type_id) -> &'i ::iref::IriRef {
262						<&::iref::Iri as From<&#type_id>>::from(vocab).as_iri_ref()
263					}
264				}
265
266				impl<'i> From<#type_id> for &'i ::iref::IriRef {
267					#[inline]
268					fn from(vocab: #type_id) -> &'i ::iref::IriRef {
269						<&::iref::Iri as From<#type_id>>::from(vocab).as_iri_ref()
270					}
271				}
272
273				impl AsRef<iref::Iri> for #type_id {
274					#[inline]
275					fn as_ref(&self) -> &::iref::Iri {
276						<&::iref::Iri as From<&#type_id>>::from(self)
277					}
278				}
279
280				impl AsRef<iref::IriRef> for #type_id {
281					#[inline]
282					fn as_ref(&self) -> &::iref::IriRef {
283						<&::iref::IriRef as From<&#type_id>>::from(self)
284					}
285				}
286			};
287
288			output.into()
289		}
290		_ => {
291			error!("only enums are handled by IriEnum")
292		}
293	}
294}
295
296fn string_literal(tokens: proc_macro2::TokenStream) -> Result<String, &'static str> {
297	if let Some(token) = tokens.into_iter().next() {
298		string_literal_token(token)
299	} else {
300		Err("expected one string parameter")
301	}
302}
303
304fn string_literal_token(token: proc_macro2::TokenTree) -> Result<String, &'static str> {
305	if let TokenTree::Literal(lit) = token {
306		let str = lit.to_string();
307
308		if str.len() >= 2 {
309			let mut buffer = String::with_capacity(str.len() - 2);
310			for (i, c) in str.chars().enumerate() {
311				if i == 0 || i == str.len() - 1 {
312					if c != '"' {
313						return Err("expected string literal");
314					}
315				} else {
316					buffer.push(c)
317				}
318			}
319
320			Ok(buffer)
321		} else {
322			Err("expected string literal")
323		}
324	} else {
325		Err("expected string literal")
326	}
327}