import sorted-data proc macro

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
This commit is contained in:
Wolfgang Bumiller 2019-11-21 10:40:08 +01:00
parent 729fed5eac
commit ceb3ff5544
4 changed files with 161 additions and 0 deletions

View File

@ -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!

15
sorted-data/Cargo.toml Normal file
View File

@ -0,0 +1,15 @@
[package]
name = "sorted-data"
version = "0.1.0"
authors = ["Wolfgang Bumiller <w.bumiller@proxmox.com>"]
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" ] }

116
sorted-data/src/lib.rs Normal file
View File

@ -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, Error>) -> TokenStream {
match data {
Ok(output) => output,
Err(err) => match err.downcast::<syn::Error>() {
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<TokenStream, Error> {
let mut item: syn::Item = syn::parse2(item)?;
SortedData.visit_item_mut(&mut item);
Ok(quote!(#item))
}
fn sort_data(data: TokenStream) -> Result<TokenStream, Error> {
let mut array: syn::ExprArray = syn::parse2(data)?;
let span = array.span();
let mut fields: Vec<syn::Expr> = 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))
}

28
sorted-data/tests/test.rs Normal file
View File

@ -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)]);
}