Implement Group derive proc macro
This commit is contained in:
parent
a1acc26027
commit
1276a82e54
6 changed files with 117 additions and 32 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
63
cove-macro/src/group.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -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
33
cove-macro/src/util.rs
Normal 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"))
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue