From 458025b8bf185d4b95da3c7462ad0dade8fb1fd7 Mon Sep 17 00:00:00 2001 From: Joscha Date: Thu, 27 Apr 2023 20:11:16 +0200 Subject: [PATCH] Use serde's default annotation for Document --- cove-config/src/doc.rs | 11 ++- cove-config/src/euph.rs | 2 - cove-config/src/keys.rs | 15 ++++ cove-config/src/lib.rs | 3 + cove-macro/src/document.rs | 173 ++++++++++++++++++++++++------------- cove-macro/src/util.rs | 55 +++++++++++- cove/src/main.rs | 2 +- 7 files changed, 194 insertions(+), 67 deletions(-) diff --git a/cove-config/src/doc.rs b/cove-config/src/doc.rs index 9253364..da27b89 100644 --- a/cove-config/src/doc.rs +++ b/cove-config/src/doc.rs @@ -5,6 +5,15 @@ use std::path::PathBuf; use cove_input::KeyBinding; pub use cove_macro::Document; +use serde::Serialize; + +pub(crate) fn toml_value_as_markdown(value: &T) -> String { + let mut result = String::new(); + value + .serialize(toml::ser::ValueSerializer::new(&mut result)) + .expect("not a valid toml value"); + format!("`{result}`") +} #[derive(Clone, Default)] pub struct ValueInfo { @@ -118,7 +127,7 @@ impl Doc { entries } - pub fn format_as_markdown(&self) -> String { + pub fn as_markdown(&self) -> String { // Print entries in alphabetical order to make generated documentation // format more stable. let mut entries = self.entries(); diff --git a/cove-config/src/euph.rs b/cove-config/src/euph.rs index 2c50c24..d2fa84a 100644 --- a/cove-config/src/euph.rs +++ b/cove-config/src/euph.rs @@ -29,7 +29,6 @@ impl Document for RoomsSortOrder { pub struct EuphRoom { /// Whether to automatically join this room on startup. #[serde(default)] - #[document(default = "`false`")] pub autojoin: bool, /// If set, cove will set this username upon joining if there is no username @@ -40,7 +39,6 @@ pub struct EuphRoom { /// username even if there is already a different username associated with /// the current session. #[serde(default)] - #[document(default = "`false`")] pub force_username: bool, /// If set, cove will try once to use this password to authenticate, should diff --git a/cove-config/src/keys.rs b/cove-config/src/keys.rs index 5e2e45e..77029dc 100644 --- a/cove-config/src/keys.rs +++ b/cove-config/src/keys.rs @@ -256,8 +256,11 @@ impl Default for EditorOp { #[derive(Debug, Default, Deserialize, Document)] pub struct Editor { #[serde(default)] + #[document(no_default)] pub cursor: EditorCursor, + #[serde(default)] + #[document(no_default)] pub action: EditorOp, } @@ -346,21 +349,33 @@ impl Default for TreeOp { #[derive(Debug, Default, Deserialize, Document)] pub struct Tree { #[serde(default)] + #[document(no_default)] pub cursor: TreeCursor, + #[serde(default)] + #[document(no_default)] pub action: TreeOp, } #[derive(Debug, Default, Deserialize, Document)] pub struct Keys { #[serde(default)] + #[document(no_default)] pub general: General, + #[serde(default)] + #[document(no_default)] pub scroll: Scroll, + #[serde(default)] + #[document(no_default)] pub cursor: Cursor, + #[serde(default)] + #[document(no_default)] pub editor: Editor, + #[serde(default)] + #[document(no_default)] pub tree: Tree, } diff --git a/cove-config/src/lib.rs b/cove-config/src/lib.rs index 13d6ead..6d00391 100644 --- a/cove-config/src/lib.rs +++ b/cove-config/src/lib.rs @@ -80,9 +80,12 @@ pub struct Config { #[document(default = "`alphabet`")] pub rooms_sort_order: RoomsSortOrder, + #[serde(default)] + #[document(no_default)] pub euph: Euph, #[serde(default)] + #[document(no_default)] pub keys: Keys, } diff --git a/cove-macro/src/document.rs b/cove-macro/src/document.rs index e610977..e6845d2 100644 --- a/cove-macro/src/document.rs +++ b/cove-macro/src/document.rs @@ -1,89 +1,142 @@ use proc_macro2::TokenStream; use quote::quote; -use syn::punctuated::Punctuated; use syn::spanned::Spanned; -use syn::{Data, DeriveInput, Field, MetaNameValue, Token}; +use syn::{Data, DeriveInput, ExprPath, Field, LitStr, Type}; -use crate::util::docstring; +use crate::util::{self, docstring}; -/// 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) +enum SerdeDefault { + Default(Type), + Path(ExprPath), } -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; +#[derive(Default)] +struct FieldInfo { + description: Option, + metavar: Option, + default: Option, + serde_default: Option, + no_default: bool, +} - 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")); +impl FieldInfo { + fn initialize_from_field(&mut self, field: &Field) -> syn::Result<()> { + let docstring = docstring(field)?; + if !docstring.is_empty() { + self.description = Some(docstring); } + + for arg in util::attribute_arguments(field, "document")? { + if arg.path.is_ident("metavar") { + // Parse `#[document(metavar = "bla")]` + if let Some(metavar) = arg.value.and_then(util::into_litstr) { + self.metavar = Some(metavar); + } else { + util::bail(arg.path.span(), "must be of the form `key = \"value\"`")?; + } + } else if arg.path.is_ident("default") { + // Parse `#[document(default = "bla")]` + if let Some(value) = arg.value.and_then(util::into_litstr) { + self.default = Some(value); + } else { + util::bail(arg.path.span(), "must be of the form `key = \"value\"`")?; + } + } else if arg.path.is_ident("no_default") { + // Parse #[document(no_default)] + if arg.value.is_some() { + util::bail(arg.path.span(), "must not have a value")?; + } + self.no_default = true; + } else { + util::bail(arg.path.span(), "unknown argument name")?; + } + } + + // Find `#[serde(default)]` or `#[serde(default = "bla")]`. + for arg in util::attribute_arguments(field, "serde")? { + if arg.path.is_ident("default") { + if let Some(value) = arg.value { + if let Some(path) = util::into_litstr(value) { + self.serde_default = Some(SerdeDefault::Path(path.parse()?)); + } + } else { + self.serde_default = Some(SerdeDefault::Default(field.ty.clone())); + } + } + } + + Ok(()) } - Ok(Some(quote! { - fields.insert( - #ident.to_string(), - { - let mut doc = <#ty as Document>::doc(); - #( #setters )* - Box::new(doc) - } - ); - })) + fn from_field(field: &Field) -> syn::Result { + let mut result = Self::default(); + result.initialize_from_field(field)?; + Ok(result) + } } 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")); + 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 mut fields = vec![]; + for field in data.fields { + let Some(ident) = field.ident.as_ref() else { + return util::bail(field.span(), "must not be a tuple struct"); + }; + let ident = ident.to_string(); + + let info = FieldInfo::from_field(&field)?; + + let mut setters = vec![]; + if let Some(description) = info.description { + setters.push(quote! { + doc.description = Some(#description.to_string()); + }); } + if let Some(metavar) = info.metavar { + setters.push(quote! { + doc.wrap_info.metavar = Some(#metavar.to_string()); + }); + } + if info.no_default { + } else if let Some(default) = info.default { + setters.push(quote! { + doc.value_info.default = Some(#default.to_string()); + }); + } else if let Some(serde_default) = info.serde_default { + setters.push(match serde_default { + SerdeDefault::Default(ty) => quote! { + doc.value_info.default = Some(crate::doc::toml_value_as_markdown(&<#ty as Default>::default())); + }, + SerdeDefault::Path(path) => quote! { + doc.value_info.default = Some(crate::doc::toml_value_as_markdown(&#path())); + }, + }); + } + + let ty = field.ty; + fields.push(quote! { + fields.insert( + #ident.to_string(), + { + let mut doc = <#ty as crate::doc::Document>::doc(); + #( #setters )* + ::std::boxed::Box::new(doc) + } + ); + }); } 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(); + let mut fields = ::std::collections::HashMap::new(); #( #fields )* - let mut doc = Doc::default(); + let mut doc = crate::doc::Doc::default(); doc.struct_info.fields = fields; doc } diff --git a/cove-macro/src/util.rs b/cove-macro/src/util.rs index c733c69..cf21159 100644 --- a/cove-macro/src/util.rs +++ b/cove-macro/src/util.rs @@ -1,6 +1,22 @@ -use syn::{Expr, ExprLit, Field, Lit, LitStr}; +use proc_macro2::Span; +use syn::parse::Parse; +use syn::punctuated::Punctuated; +use syn::{Expr, ExprLit, Field, Lit, LitStr, Path, Token}; -pub fn strlit(expr: &Expr) -> Option<&LitStr> { +pub fn bail(span: Span, message: &str) -> syn::Result { + Err(syn::Error::new(span, message)) +} + +pub fn litstr(expr: &Expr) -> Option<&LitStr> { + match expr { + Expr::Lit(ExprLit { + lit: Lit::Str(lit), .. + }) => Some(lit), + _ => None, + } +} + +pub fn into_litstr(expr: Expr) -> Option { match expr { Expr::Lit(ExprLit { lit: Lit::Str(lit), .. @@ -19,7 +35,7 @@ pub fn docstring(field: &Field) -> syn::Result { .iter() .filter(|attr| attr.path().is_ident("doc")) { - if let Some(lit) = strlit(&attr.meta.require_name_value()?.value) { + if let Some(lit) = litstr(&attr.meta.require_name_value()?.value) { let value = lit.value(); let value = value .strip_prefix(' ') @@ -31,3 +47,36 @@ pub fn docstring(field: &Field) -> syn::Result { Ok(lines.join("\n")) } + +pub struct AttributeArgument { + pub path: Path, + pub value: Option, +} + +impl Parse for AttributeArgument { + fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result { + let path = Path::parse(input)?; + let value = if input.peek(Token![=]) { + input.parse::()?; + Some(Expr::parse(input)?) + } else { + None + }; + Ok(Self { path, value }) + } +} + +/// Given a struct field, this finds all arguments of the form `#[path(key)]` +/// and `#[path(key = value)]`. Multiple arguments may be specified in a single +/// annotation, e.g. `#[foo(bar, baz = true)]`. +pub fn attribute_arguments(field: &Field, path: &str) -> syn::Result> { + let mut attrs = vec![]; + + for attr in field.attrs.iter().filter(|attr| attr.path().is_ident(path)) { + let args = + attr.parse_args_with(Punctuated::::parse_terminated)?; + attrs.extend(args); + } + + Ok(attrs) +} diff --git a/cove/src/main.rs b/cove/src/main.rs index 2bd8271..e7bc101 100644 --- a/cove/src/main.rs +++ b/cove/src/main.rs @@ -226,5 +226,5 @@ async fn clear_cookies(config: &'static Config, dirs: &ProjectDirs) -> anyhow::R } fn help_config() { - print!("{}", Config::doc().format_as_markdown()); + print!("{}", Config::doc().as_markdown()); }