Implement Group derive proc macro
This commit is contained in:
parent
a1acc26027
commit
1276a82e54
6 changed files with 117 additions and 32 deletions
7
Cargo.lock
generated
7
Cargo.lock
generated
|
|
@ -136,6 +136,12 @@ version = "1.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
|
checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "case"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fd6c0e7b807d60291f42f33f58480c0bfafe28ed08286446f45e463728cf9c1c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "caseless"
|
name = "caseless"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
|
|
@ -294,6 +300,7 @@ dependencies = [
|
||||||
name = "cove-macro"
|
name = "cove-macro"
|
||||||
version = "0.6.1"
|
version = "0.6.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"case",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.15",
|
"syn 2.0.15",
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ version = { workspace = true }
|
||||||
edition = { workspace = true }
|
edition = { workspace = true }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
case = "1.0.0"
|
||||||
proc-macro2 = "1.0.56"
|
proc-macro2 = "1.0.56"
|
||||||
quote = "1.0.26"
|
quote = "1.0.26"
|
||||||
syn = "2.0.15"
|
syn = "2.0.15"
|
||||||
|
|
|
||||||
|
|
@ -2,39 +2,9 @@ use proc_macro2::TokenStream;
|
||||||
use quote::quote;
|
use quote::quote;
|
||||||
use syn::punctuated::Punctuated;
|
use syn::punctuated::Punctuated;
|
||||||
use syn::spanned::Spanned;
|
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> {
|
use crate::util::docstring;
|
||||||
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"))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Given a struct field, this finds all key-value pairs of the form
|
/// Given a struct field, this finds all key-value pairs of the form
|
||||||
/// `#[document(key = value, ...)]`.
|
/// `#[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};
|
use syn::{parse_macro_input, DeriveInput};
|
||||||
|
|
||||||
mod document;
|
mod document;
|
||||||
|
mod group;
|
||||||
|
mod util;
|
||||||
|
|
||||||
#[proc_macro_derive(Document, attributes(document))]
|
#[proc_macro_derive(Document, attributes(document))]
|
||||||
pub fn derive_document(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
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(),
|
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