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;
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)]
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();

View file

@ -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

View file

@ -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,
}

View file

@ -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,
}

View file

@ -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<Vec<MetaNameValue>> {
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);
enum SerdeDefault {
Default(Type),
Path(ExprPath),
}
Ok(attrs)
#[derive(Default)]
struct FieldInfo {
description: Option<String>,
metavar: Option<LitStr>,
default: Option<LitStr>,
serde_default: Option<SerdeDefault>,
no_default: bool,
}
fn field_doc(field: &Field) -> syn::Result<Option<TokenStream>> {
let Some(ident) = field.ident.as_ref() else { return Ok(None); };
let ident = ident.to_string();
let ty = &field.ty;
let mut setters = vec![];
impl FieldInfo {
fn initialize_from_field(&mut self, field: &Field) -> syn::Result<()> {
let docstring = docstring(field)?;
if !docstring.is_empty() {
setters.push(quote! {
doc.description = Some(#docstring.to_string());
});
self.description = Some(docstring);
}
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()); });
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 {
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! {
fields.insert(
#ident.to_string(),
{
let mut doc = <#ty as Document>::doc();
#( #setters )*
Box::new(doc)
// 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(())
}
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> {
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
}

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 {
Expr::Lit(ExprLit {
lit: Lit::Str(lit), ..
@ -19,7 +35,7 @@ pub fn docstring(field: &Field) -> syn::Result<String> {
.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<String> {
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() {
print!("{}", Config::doc().format_as_markdown());
print!("{}", Config::doc().as_markdown());
}