diff --git a/Cargo.toml b/Cargo.toml index bda503f6..454b3a16 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,8 @@ members = [ "proxmox-sys", "proxmox", + "sorted-data", + # This is an api server test and may be temporarily broken by changes to # proxmox-api or proxmox-api-macro, but should ultimately be updated to work # again as it's supposed to serve as an example code! diff --git a/sorted-data/Cargo.toml b/sorted-data/Cargo.toml new file mode 100644 index 00000000..5dff0ed1 --- /dev/null +++ b/sorted-data/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "sorted-data" +version = "0.1.0" +authors = ["Wolfgang Bumiller "] +edition = "2018" + +[lib] +proc-macro = true + +[dependencies] +#failure = { version = "0.1", default-features = false, features = ["std"] } +failure = "0.1" +proc-macro2 = "1.0" +quote = "1.0" +syn = { version = "1.0", features = [ "full", "visit-mut" ] } diff --git a/sorted-data/src/lib.rs b/sorted-data/src/lib.rs new file mode 100644 index 00000000..699f3fd2 --- /dev/null +++ b/sorted-data/src/lib.rs @@ -0,0 +1,116 @@ +extern crate proc_macro; +extern crate proc_macro2; + +use std::iter::FromIterator; +use std::mem; + +use failure::Error; + +use proc_macro::TokenStream as TokenStream_1; +use proc_macro2::TokenStream; +use quote::quote; +use syn::punctuated::Punctuated; +use syn::spanned::Spanned; +use syn::visit_mut::VisitMut; +use syn::Ident; + +macro_rules! format_err { + ($span:expr => $($msg:tt)*) => { syn::Error::new_spanned($span, format!($($msg)*)) }; + ($span:expr, $($msg:tt)*) => { syn::Error::new($span, format!($($msg)*)) }; +} + +//macro_rules! bail { +// ($span:expr => $($msg:tt)*) => { return Err(format_err!($span => $($msg)*).into()) }; +// ($span:expr, $($msg:tt)*) => { return Err(format_err!($span, $($msg)*).into()) }; +//} + +fn handle_error(data: Result) -> TokenStream { + match data { + Ok(output) => output, + Err(err) => match err.downcast::() { + Ok(err) => err.to_compile_error(), + Err(err) => panic!("error in sorted_struct macro: {}", err), + }, + } +} + +/// Enable the `sorted!` expression-position macro in a statement. +#[proc_macro_attribute] +pub fn sortable(_attr: TokenStream_1, item: TokenStream_1) -> TokenStream_1 { + handle_error(sortable_do(item.into())).into() +} + +struct SortedData; + +impl VisitMut for SortedData { + fn visit_expr_macro_mut(&mut self, i: &mut syn::ExprMacro) { + if i.mac.path.is_ident("sorted") { + let span = i.mac.path.span(); + i.mac.path.segments = Punctuated::new(); + i.mac.path.segments.push(syn::PathSegment { + ident: Ident::new("identity", span), + arguments: Default::default(), + }); + + let tokens = mem::replace(&mut i.mac.tokens, TokenStream::new()); + i.mac.tokens = handle_error(sort_data(tokens)); + } + // and recurse: + self.visit_macro_mut(&mut i.mac) + } +} + +fn sortable_do(item: TokenStream) -> Result { + let mut item: syn::Item = syn::parse2(item)?; + SortedData.visit_item_mut(&mut item); + Ok(quote!(#item)) +} + +fn sort_data(data: TokenStream) -> Result { + let mut array: syn::ExprArray = syn::parse2(data)?; + let span = array.span(); + + let mut fields: Vec = mem::replace(&mut array.elems, Punctuated::new()) + .into_iter() + .collect(); + + let mut err = None; + fields.sort_by(|a, b| { + if err.is_some() { + return std::cmp::Ordering::Equal; + } + + use syn::{Expr, Lit}; + match (a, b) { + // We can sort an array of literals: + (Expr::Lit(a), Expr::Lit(b)) => match (&a.lit, &b.lit) { + (Lit::Str(a), Lit::Str(b)) => return a.value().cmp(&b.value()), + _ => err = Some(format_err!(span, "can only sort by string literals!")), + }, + + // We can sort an array of tuples where the first element is a literal: + (Expr::Tuple(a), Expr::Tuple(b)) => match (a.elems.first(), b.elems.first()) { + (Some(Expr::Lit(a)), Some(Expr::Lit(b))) => match (&a.lit, &b.lit) { + (Lit::Str(a), Lit::Str(b)) => return a.value().cmp(&b.value()), + _ => err = Some(format_err!(span, "can only sort by string literals!")), + }, + _ => { + err = Some(format_err!( + span, + "can only sort tuples starting with literals!" + )) + } + }, + _ => err = Some(format_err!(span, "don't know how to sort this data!")), + } + std::cmp::Ordering::Equal + }); + + if let Some(err) = err { + return Err(err.into()); + } + + array.elems = Punctuated::from_iter(fields); + + Ok(quote!(#array)) +} diff --git a/sorted-data/tests/test.rs b/sorted-data/tests/test.rs new file mode 100644 index 00000000..ea3f9e3e --- /dev/null +++ b/sorted-data/tests/test.rs @@ -0,0 +1,28 @@ +use sorted_data::sortable; + +// The way #[sorted] works we require an 'identity' macro due to the inability of the syntax tree +// visitor to change the type of a syntax tree element. +// +// Iow.: it replaces `sorted!([3, 2, 1])` with `identity!([1, 2, 3])`. +macro_rules! identity { + ($($x:tt)*) => { $($x)* } +} + +// In a normal project we would use this Cargo.toml line: +// +// [dependencies] +// proxmox = { version = "0.1", features = [ "sortable-macro" ] } +// +// Then: +// use proxmox::{sortable, identity}; + +#[test] +fn test_id() { + #[sortable] + const FOO: [&str; 3] = sorted!(["3", "2", "1"]); + assert_eq!(FOO, ["1", "2", "3"]); + + #[sortable] + const FOO2: [(&str, usize); 3] = sorted!([("3", 1), ("2", 2), ("1", 3)]); + assert_eq!(FOO2, [("1", 3), ("2", 2), ("3", 1)]); +}