diff --git a/cove-macro/src/document.rs b/cove-macro/src/document.rs new file mode 100644 index 0000000..c2841a9 --- /dev/null +++ b/cove-macro/src/document.rs @@ -0,0 +1,124 @@ +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}; + +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")) +} + +/// Given a struct field, this finds all key-value pairs of the form +/// `#[document(key = value, ...)]`. +fn document_attributes(field: &Field) -> syn::Result> { + let mut attrs = vec![]; + + for attr in field + .attrs + .iter() + .filter(|attr| attr.path().is_ident("document")) + { + let args = + attr.parse_args_with(Punctuated::::parse_terminated)?; + attrs.extend(args); + } + + Ok(attrs) +} + +fn field_doc(field: &Field) -> syn::Result> { + let Some(ident) = field.ident.as_ref() else { return Ok(None); }; + let ident = ident.to_string(); + let ty = &field.ty; + + let mut setters = vec![]; + + let docstring = docstring(field)?; + if !docstring.is_empty() { + setters.push(quote! { + doc.description = Some(#docstring.to_string()); + }); + } + + for attr in document_attributes(field)? { + let value = attr.value; + if attr.path.is_ident("default") { + setters.push(quote! { doc.value_info.default = Some(#value.to_string()); }); + } else if attr.path.is_ident("metavar") { + setters.push(quote! { doc.wrap_info.metavar = Some(#value.to_string()); }); + } else { + return Err(syn::Error::new(attr.path.span(), "unknown argument name")); + } + } + + Ok(Some(quote! { + fields.insert( + #ident.to_string(), + { + let mut doc = <#ty as Document>::doc(); + #( #setters )* + Box::new(doc) + } + ); + })) +} + +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 mut fields = Vec::new(); + for field in data.fields.iter() { + if let Some(field) = field_doc(field)? { + fields.push(field); + } + } + + let ident = input.ident; + let tokens = quote!( + impl crate::doc::Document for #ident { + fn doc() -> crate::doc::Doc { + use ::std::{boxed::Box, collections::HashMap}; + use crate::doc::{Doc, Document}; + + let mut fields = HashMap::new(); + #( #fields )* + + let mut doc = Doc::default(); + doc.struct_info.fields = fields; + doc + } + } + ); + + Ok(tokens) +} diff --git a/cove-macro/src/lib.rs b/cove-macro/src/lib.rs index 5e7145d..b05190e 100644 --- a/cove-macro/src/lib.rs +++ b/cove-macro/src/lib.rs @@ -9,137 +9,14 @@ // Clippy lints #![warn(clippy::use_self)] -use proc_macro2::TokenStream; -use quote::quote; -use syn::punctuated::Punctuated; -use syn::spanned::Spanned; -use syn::{ - parse_macro_input, Data, DeriveInput, Expr, ExprLit, Field, Lit, LitStr, MetaNameValue, Token, -}; +use syn::{parse_macro_input, DeriveInput}; -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")) -} - -/// Given a struct field, this finds all key-value pairs of the form -/// `#[document(key = value, ...)]`. -fn document_attributes(field: &Field) -> syn::Result> { - let mut attrs = vec![]; - - for attr in field - .attrs - .iter() - .filter(|attr| attr.path().is_ident("document")) - { - let args = - attr.parse_args_with(Punctuated::::parse_terminated)?; - attrs.extend(args); - } - - Ok(attrs) -} - -fn field_doc(field: &Field) -> syn::Result> { - let Some(ident) = field.ident.as_ref() else { return Ok(None); }; - let ident = ident.to_string(); - let ty = &field.ty; - - let mut setters = vec![]; - - let docstring = docstring(field)?; - if !docstring.is_empty() { - setters.push(quote! { - doc.description = Some(#docstring.to_string()); - }); - } - - for attr in document_attributes(field)? { - let value = attr.value; - if attr.path.is_ident("default") { - setters.push(quote! { doc.value_info.default = Some(#value.to_string()); }); - } else if attr.path.is_ident("metavar") { - setters.push(quote! { doc.wrap_info.metavar = Some(#value.to_string()); }); - } else { - return Err(syn::Error::new(attr.path.span(), "unknown argument name")); - } - } - - Ok(Some(quote! { - fields.insert( - #ident.to_string(), - { - let mut doc = <#ty as Document>::doc(); - #( #setters )* - Box::new(doc) - } - ); - })) -} - -fn derive_document_impl(input: DeriveInput) -> syn::Result { - let Data::Struct(data) = input.data else { - return Err(syn::Error::new(input.span(), "Must be a struct")); - }; - - let mut fields = Vec::new(); - for field in data.fields.iter() { - if let Some(field) = field_doc(field)? { - fields.push(field); - } - } - - let ident = input.ident; - let tokens = quote!( - impl crate::doc::Document for #ident { - fn doc() -> crate::doc::Doc { - use ::std::{boxed::Box, collections::HashMap}; - use crate::doc::{Doc, Document}; - - let mut fields = HashMap::new(); - #( #fields )* - - let mut doc = Doc::default(); - doc.struct_info.fields = fields; - doc - } - } - ); - - Ok(tokens) -} +mod document; #[proc_macro_derive(Document, attributes(document))] pub fn derive_document(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let input = parse_macro_input!(input as DeriveInput); - match derive_document_impl(input) { + match document::derive_impl(input) { Ok(tokens) => tokens.into(), Err(err) => err.into_compile_error().into(), }