diff --git a/Cargo.lock b/Cargo.lock index ecdc77d..bf3763b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/cove-macro/Cargo.toml b/cove-macro/Cargo.toml index 66ec539..731111a 100644 --- a/cove-macro/Cargo.toml +++ b/cove-macro/Cargo.toml @@ -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" diff --git a/cove-macro/src/document.rs b/cove-macro/src/document.rs index c2841a9..e610977 100644 --- a/cove-macro/src/document.rs +++ b/cove-macro/src/document.rs @@ -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 { - 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, ...)]`. diff --git a/cove-macro/src/group.rs b/cove-macro/src/group.rs new file mode 100644 index 0000000..3e0995c --- /dev/null +++ b/cove-macro/src/group.rs @@ -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 { + 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 { + match () { + #( #match_cases )* + () => None, + } + } + } + }) +} diff --git a/cove-macro/src/lib.rs b/cove-macro/src/lib.rs index b05190e..f064a73 100644 --- a/cove-macro/src/lib.rs +++ b/cove-macro/src/lib.rs @@ -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(), + } +} diff --git a/cove-macro/src/util.rs b/cove-macro/src/util.rs new file mode 100644 index 0000000..c733c69 --- /dev/null +++ b/cove-macro/src/util.rs @@ -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 { + 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")) +}