Use serde's default annotation for Document
This commit is contained in:
parent
d29441bf02
commit
458025b8bf
7 changed files with 194 additions and 67 deletions
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue