Use serde's default annotation for Document

This commit is contained in:
Joscha 2023-04-27 20:11:16 +02:00
parent d29441bf02
commit 458025b8bf
7 changed files with 194 additions and 67 deletions

View file

@ -5,6 +5,15 @@ use std::path::PathBuf;
use cove_input::KeyBinding; use cove_input::KeyBinding;
pub use cove_macro::Document; pub use cove_macro::Document;
use serde::Serialize;
pub(crate) fn toml_value_as_markdown<T: Serialize>(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)] #[derive(Clone, Default)]
pub struct ValueInfo { pub struct ValueInfo {
@ -118,7 +127,7 @@ impl Doc {
entries entries
} }
pub fn format_as_markdown(&self) -> String { pub fn as_markdown(&self) -> String {
// Print entries in alphabetical order to make generated documentation // Print entries in alphabetical order to make generated documentation
// format more stable. // format more stable.
let mut entries = self.entries(); let mut entries = self.entries();

View file

@ -29,7 +29,6 @@ impl Document for RoomsSortOrder {
pub struct EuphRoom { pub struct EuphRoom {
/// Whether to automatically join this room on startup. /// Whether to automatically join this room on startup.
#[serde(default)] #[serde(default)]
#[document(default = "`false`")]
pub autojoin: bool, pub autojoin: bool,
/// If set, cove will set this username upon joining if there is no username /// 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 /// username even if there is already a different username associated with
/// the current session. /// the current session.
#[serde(default)] #[serde(default)]
#[document(default = "`false`")]
pub force_username: bool, pub force_username: bool,
/// If set, cove will try once to use this password to authenticate, should /// If set, cove will try once to use this password to authenticate, should

View file

@ -256,8 +256,11 @@ impl Default for EditorOp {
#[derive(Debug, Default, Deserialize, Document)] #[derive(Debug, Default, Deserialize, Document)]
pub struct Editor { pub struct Editor {
#[serde(default)] #[serde(default)]
#[document(no_default)]
pub cursor: EditorCursor, pub cursor: EditorCursor,
#[serde(default)] #[serde(default)]
#[document(no_default)]
pub action: EditorOp, pub action: EditorOp,
} }
@ -346,21 +349,33 @@ impl Default for TreeOp {
#[derive(Debug, Default, Deserialize, Document)] #[derive(Debug, Default, Deserialize, Document)]
pub struct Tree { pub struct Tree {
#[serde(default)] #[serde(default)]
#[document(no_default)]
pub cursor: TreeCursor, pub cursor: TreeCursor,
#[serde(default)] #[serde(default)]
#[document(no_default)]
pub action: TreeOp, pub action: TreeOp,
} }
#[derive(Debug, Default, Deserialize, Document)] #[derive(Debug, Default, Deserialize, Document)]
pub struct Keys { pub struct Keys {
#[serde(default)] #[serde(default)]
#[document(no_default)]
pub general: General, pub general: General,
#[serde(default)] #[serde(default)]
#[document(no_default)]
pub scroll: Scroll, pub scroll: Scroll,
#[serde(default)] #[serde(default)]
#[document(no_default)]
pub cursor: Cursor, pub cursor: Cursor,
#[serde(default)] #[serde(default)]
#[document(no_default)]
pub editor: Editor, pub editor: Editor,
#[serde(default)] #[serde(default)]
#[document(no_default)]
pub tree: Tree, pub tree: Tree,
} }

View file

@ -80,9 +80,12 @@ pub struct Config {
#[document(default = "`alphabet`")] #[document(default = "`alphabet`")]
pub rooms_sort_order: RoomsSortOrder, pub rooms_sort_order: RoomsSortOrder,
#[serde(default)]
#[document(no_default)]
pub euph: Euph, pub euph: Euph,
#[serde(default)] #[serde(default)]
#[document(no_default)]
pub keys: Keys, pub keys: Keys,
} }

View file

@ -1,89 +1,142 @@
use proc_macro2::TokenStream; use proc_macro2::TokenStream;
use quote::quote; use quote::quote;
use syn::punctuated::Punctuated;
use syn::spanned::Spanned; 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 enum SerdeDefault {
/// `#[document(key = value, ...)]`. Default(Type),
fn document_attributes(field: &Field) -> syn::Result<Vec<MetaNameValue>> { Path(ExprPath),
let mut attrs = vec![];
for attr in field
.attrs
.iter()
.filter(|attr| attr.path().is_ident("document"))
{
let args =
attr.parse_args_with(Punctuated::<MetaNameValue, Token![,]>::parse_terminated)?;
attrs.extend(args);
}
Ok(attrs)
} }
fn field_doc(field: &Field) -> syn::Result<Option<TokenStream>> { #[derive(Default)]
let Some(ident) = field.ident.as_ref() else { return Ok(None); }; struct FieldInfo {
let ident = ident.to_string(); description: Option<String>,
let ty = &field.ty; metavar: Option<LitStr>,
default: Option<LitStr>,
let mut setters = vec![]; serde_default: Option<SerdeDefault>,
no_default: bool,
}
impl FieldInfo {
fn initialize_from_field(&mut self, field: &Field) -> syn::Result<()> {
let docstring = docstring(field)?; let docstring = docstring(field)?;
if !docstring.is_empty() { if !docstring.is_empty() {
setters.push(quote! { self.description = Some(docstring);
doc.description = Some(#docstring.to_string());
});
} }
for attr in document_attributes(field)? { for arg in util::attribute_arguments(field, "document")? {
let value = attr.value; if arg.path.is_ident("metavar") {
if attr.path.is_ident("default") { // Parse `#[document(metavar = "bla")]`
setters.push(quote! { doc.value_info.default = Some(#value.to_string()); }); if let Some(metavar) = arg.value.and_then(util::into_litstr) {
} else if attr.path.is_ident("metavar") { self.metavar = Some(metavar);
setters.push(quote! { doc.wrap_info.metavar = Some(#value.to_string()); });
} else { } else {
return Err(syn::Error::new(attr.path.span(), "unknown argument name")); 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")?;
} }
} }
Ok(Some(quote! { // Find `#[serde(default)]` or `#[serde(default = "bla")]`.
fields.insert( for arg in util::attribute_arguments(field, "serde")? {
#ident.to_string(), if arg.path.is_ident("default") {
{ if let Some(value) = arg.value {
let mut doc = <#ty as Document>::doc(); if let Some(path) = util::into_litstr(value) {
#( #setters )* self.serde_default = Some(SerdeDefault::Path(path.parse()?));
Box::new(doc) }
} else {
self.serde_default = Some(SerdeDefault::Default(field.ty.clone()));
}
}
}
Ok(())
}
fn from_field(field: &Field) -> syn::Result<Self> {
let mut result = Self::default();
result.initialize_from_field(field)?;
Ok(result)
} }
);
}))
} }
pub fn derive_impl(input: DeriveInput) -> syn::Result<TokenStream> { pub fn derive_impl(input: DeriveInput) -> syn::Result<TokenStream> {
let Data::Struct(data) = input.data else { 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(); let mut fields = vec![];
for field in data.fields.iter() { for field in data.fields {
if let Some(field) = field_doc(field)? { let Some(ident) = field.ident.as_ref() else {
fields.push(field); 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 ident = input.ident;
let tokens = quote!( let tokens = quote!(
impl crate::doc::Document for #ident { impl crate::doc::Document for #ident {
fn doc() -> crate::doc::Doc { fn doc() -> crate::doc::Doc {
use ::std::{boxed::Box, collections::HashMap}; let mut fields = ::std::collections::HashMap::new();
use crate::doc::{Doc, Document};
let mut fields = HashMap::new();
#( #fields )* #( #fields )*
let mut doc = Doc::default(); let mut doc = crate::doc::Doc::default();
doc.struct_info.fields = fields; doc.struct_info.fields = fields;
doc doc
} }

View file

@ -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<T>(span: Span, message: &str) -> syn::Result<T> {
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<LitStr> {
match expr { match expr {
Expr::Lit(ExprLit { Expr::Lit(ExprLit {
lit: Lit::Str(lit), .. lit: Lit::Str(lit), ..
@ -19,7 +35,7 @@ pub fn docstring(field: &Field) -> syn::Result<String> {
.iter() .iter()
.filter(|attr| attr.path().is_ident("doc")) .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 = lit.value();
let value = value let value = value
.strip_prefix(' ') .strip_prefix(' ')
@ -31,3 +47,36 @@ pub fn docstring(field: &Field) -> syn::Result<String> {
Ok(lines.join("\n")) Ok(lines.join("\n"))
} }
pub struct AttributeArgument {
pub path: Path,
pub value: Option<Expr>,
}
impl Parse for AttributeArgument {
fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result<Self> {
let path = Path::parse(input)?;
let value = if input.peek(Token![=]) {
input.parse::<Token![=]>()?;
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<Vec<AttributeArgument>> {
let mut attrs = vec![];
for attr in field.attrs.iter().filter(|attr| attr.path().is_ident(path)) {
let args =
attr.parse_args_with(Punctuated::<AttributeArgument, Token![,]>::parse_terminated)?;
attrs.extend(args);
}
Ok(attrs)
}

View file

@ -226,5 +226,5 @@ async fn clear_cookies(config: &'static Config, dirs: &ProjectDirs) -> anyhow::R
} }
fn help_config() { fn help_config() {
print!("{}", Config::doc().format_as_markdown()); print!("{}", Config::doc().as_markdown());
} }