Implement Group derive proc macro

This commit is contained in:
Joscha 2023-04-26 22:21:19 +02:00
parent a1acc26027
commit 1276a82e54
6 changed files with 117 additions and 32 deletions

7
Cargo.lock generated
View file

@ -136,6 +136,12 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
[[package]]
name = "case"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd6c0e7b807d60291f42f33f58480c0bfafe28ed08286446f45e463728cf9c1c"
[[package]]
name = "caseless"
version = "0.2.1"
@ -294,6 +300,7 @@ dependencies = [
name = "cove-macro"
version = "0.6.1"
dependencies = [
"case",
"proc-macro2",
"quote",
"syn 2.0.15",

View file

@ -4,6 +4,7 @@ version = { workspace = true }
edition = { workspace = true }
[dependencies]
case = "1.0.0"
proc-macro2 = "1.0.56"
quote = "1.0.26"
syn = "2.0.15"

View file

@ -2,39 +2,9 @@ use proc_macro2::TokenStream;
use quote::quote;
use syn::punctuated::Punctuated;
use syn::spanned::Spanned;
use syn::{Data, DeriveInput, Expr, ExprLit, Field, Lit, LitStr, MetaNameValue, Token};
use syn::{Data, DeriveInput, Field, MetaNameValue, Token};
fn strlit(expr: &Expr) -> Option<&LitStr> {
match expr {
Expr::Lit(ExprLit {
lit: Lit::Str(lit), ..
}) => Some(lit),
_ => None,
}
}
/// Given a struct field, this finds all attributes like `#[doc = "bla"]`,
/// unindents, concatenates and returns them.
fn docstring(field: &Field) -> syn::Result<String> {
let mut lines = vec![];
for attr in field
.attrs
.iter()
.filter(|attr| attr.path().is_ident("doc"))
{
if let Some(lit) = strlit(&attr.meta.require_name_value()?.value) {
let value = lit.value();
let value = value
.strip_prefix(' ')
.map(|value| value.to_string())
.unwrap_or(value);
lines.push(value);
}
}
Ok(lines.join("\n"))
}
use crate::util::docstring;
/// Given a struct field, this finds all key-value pairs of the form
/// `#[document(key = value, ...)]`.

63
cove-macro/src/group.rs Normal file
View file

@ -0,0 +1,63 @@
use case::CaseExt;
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::spanned::Spanned;
use syn::{Data, DeriveInput};
use crate::util;
fn decapitalize(s: &str) -> String {
let mut chars = s.chars();
if let Some(char) = chars.next() {
char.to_lowercase().chain(chars).collect()
} else {
String::new()
}
}
pub fn derive_impl(input: DeriveInput) -> syn::Result<TokenStream> {
let Data::Struct(data) = input.data else {
return Err(syn::Error::new(input.span(), "Must be a struct"));
};
let struct_ident = input.ident;
let enum_ident = format_ident!("{}Action", struct_ident);
let mut enum_variants = vec![];
let mut match_cases = vec![];
for field in &data.fields {
if let Some(field_ident) = &field.ident {
let docstring = util::docstring(field)?;
let variant_ident = format_ident!("{}", field_ident.to_string().to_camel());
enum_variants.push(quote! {
#[doc = #docstring]
#variant_ident,
});
let description = decapitalize(&docstring);
let description = description.strip_suffix('.').unwrap_or(&description);
match_cases.push(quote!{
() if input.matches(&self.#field_ident, #description) => Some(Self::Action::#variant_ident),
});
}
}
Ok(quote! {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum #enum_ident {
#( #enum_variants )*
}
impl crate::Group for #struct_ident {
type Action = #enum_ident;
fn action(&self, input: &mut crate::Input) -> Option<Self::Action> {
match () {
#( #match_cases )*
() => None,
}
}
}
})
}

View file

@ -12,6 +12,8 @@
use syn::{parse_macro_input, DeriveInput};
mod document;
mod group;
mod util;
#[proc_macro_derive(Document, attributes(document))]
pub fn derive_document(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
@ -21,3 +23,12 @@ pub fn derive_document(input: proc_macro::TokenStream) -> proc_macro::TokenStrea
Err(err) => err.into_compile_error().into(),
}
}
#[proc_macro_derive(Group)]
pub fn derive_group(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input = parse_macro_input!(input as DeriveInput);
match group::derive_impl(input) {
Ok(tokens) => tokens.into(),
Err(err) => err.into_compile_error().into(),
}
}

33
cove-macro/src/util.rs Normal file
View file

@ -0,0 +1,33 @@
use syn::{Expr, ExprLit, Field, Lit, LitStr};
pub fn strlit(expr: &Expr) -> Option<&LitStr> {
match expr {
Expr::Lit(ExprLit {
lit: Lit::Str(lit), ..
}) => Some(lit),
_ => None,
}
}
/// Given a struct field, this finds all attributes like `#[doc = "bla"]`,
/// unindents, concatenates and returns them.
pub fn docstring(field: &Field) -> syn::Result<String> {
let mut lines = vec![];
for attr in field
.attrs
.iter()
.filter(|attr| attr.path().is_ident("doc"))
{
if let Some(lit) = strlit(&attr.meta.require_name_value()?.value) {
let value = lit.value();
let value = value
.strip_prefix(' ')
.map(|value| value.to_string())
.unwrap_or(value);
lines.push(value);
}
}
Ok(lines.join("\n"))
}