mirror of
https://github.com/google/alioth.git
synced 2024-10-22 22:46:38 +00:00
feat(aco): add trait Help
and the derive macro
Trait `Help` returns a description of a type. It is used to generate the help message for the command line interface. The derive macro implements `Help` for enums and structs and includes inline documentation in the description. Signed-off-by: Changyuan Lyu <changyuanl@google.com>
This commit is contained in:
parent
a2b74f4a4a
commit
54549ce6b6
9 changed files with 397 additions and 3 deletions
10
Cargo.lock
generated
10
Cargo.lock
generated
|
@ -460,9 +460,19 @@ version = "0.4.0"
|
|||
dependencies = [
|
||||
"assert_matches",
|
||||
"serde",
|
||||
"serde-aco-derive",
|
||||
"serde_bytes",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde-aco-derive"
|
||||
version = "0.4.0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_bytes"
|
||||
version = "0.11.15"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
[workspace]
|
||||
members = ["alioth", "alioth-cli", "macros", "serde-aco"]
|
||||
members = ["alioth", "alioth-cli", "macros", "serde-aco", "serde-aco-derive"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
|
@ -16,7 +16,11 @@ snafu = "0.8.4"
|
|||
macros = { version = "0.4.0", path = "macros", package = "alioth-macros" }
|
||||
alioth = { version = "0.4.0", path = "alioth" }
|
||||
serde-aco = { version = "0.4.0", path = "serde-aco" }
|
||||
serde-aco-derive = { version = "0.4.0", path = "serde-aco-derive" }
|
||||
assert_matches = "1"
|
||||
proc-macro2 = "1"
|
||||
syn = { version = "2", features = ["full"] }
|
||||
quote = { version = "1" }
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
|
|
|
@ -11,5 +11,5 @@ license.workspace = true
|
|||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
syn = { version = "2", features = ["full"] }
|
||||
quote = { version = "1" }
|
||||
syn.workspace = true
|
||||
quote.workspace = true
|
||||
|
|
15
serde-aco-derive/Cargo.toml
Normal file
15
serde-aco-derive/Cargo.toml
Normal file
|
@ -0,0 +1,15 @@
|
|||
[package]
|
||||
name = "serde-aco-derive"
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
syn.workspace = true
|
||||
quote.workspace = true
|
||||
proc-macro2.workspace = true
|
247
serde-aco-derive/src/help.rs
Normal file
247
serde-aco-derive/src/help.rs
Normal file
|
@ -0,0 +1,247 @@
|
|||
// Copyright 2024 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::cmp::Ordering;
|
||||
use std::iter::zip;
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use quote::quote;
|
||||
use syn::meta::ParseNestedMeta;
|
||||
use syn::punctuated::Punctuated;
|
||||
use syn::{
|
||||
parse_macro_input, Attribute, Data, DataEnum, DataStruct, DeriveInput, Expr, ExprLit, Fields,
|
||||
FieldsNamed, FieldsUnnamed, Ident, Lit, Meta, MetaNameValue, Token,
|
||||
};
|
||||
|
||||
fn get_doc_from_attrs(attrs: &[Attribute]) -> String {
|
||||
let mut lines = vec![];
|
||||
for attr in attrs.iter() {
|
||||
let Meta::NameValue(MetaNameValue {
|
||||
path,
|
||||
value: Expr::Lit(ExprLit {
|
||||
lit: Lit::Str(s), ..
|
||||
}),
|
||||
..
|
||||
}) = &attr.meta
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if path.is_ident("doc") {
|
||||
let v = s.value();
|
||||
let mut trimmed = v.trim_end();
|
||||
if let Some(t) = trimmed.strip_prefix(' ') {
|
||||
trimmed = t;
|
||||
}
|
||||
if !trimmed.is_empty() {
|
||||
lines.push(trimmed.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
lines.join("\n")
|
||||
}
|
||||
|
||||
fn get_serde_aliases_from_attrs(ident: &Ident, attrs: &[Attribute]) -> Vec<String> {
|
||||
let mut aliases = vec![];
|
||||
for attr in attrs.iter() {
|
||||
if !attr.path().is_ident("serde") {
|
||||
continue;
|
||||
}
|
||||
let Ok(nested) = attr.parse_args_with(Punctuated::<Meta, Token![,]>::parse_terminated)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
for meta in nested {
|
||||
let Meta::NameValue(MetaNameValue {
|
||||
path,
|
||||
value:
|
||||
Expr::Lit(ExprLit {
|
||||
lit: Lit::Str(s), ..
|
||||
}),
|
||||
..
|
||||
}) = meta
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if !path.is_ident("alias") {
|
||||
continue;
|
||||
}
|
||||
aliases.push(s.value());
|
||||
}
|
||||
}
|
||||
aliases.push(ident.to_string());
|
||||
aliases.sort_by(|l, r| {
|
||||
if l.len() != r.len() {
|
||||
l.len().cmp(&r.len())
|
||||
} else {
|
||||
for (a, b) in zip(l.chars(), r.chars()) {
|
||||
if a == b {
|
||||
continue;
|
||||
}
|
||||
if a.is_lowercase() == b.is_lowercase() {
|
||||
return a.cmp(&b);
|
||||
} else if a.is_lowercase() {
|
||||
return Ordering::Less;
|
||||
} else {
|
||||
return Ordering::Greater;
|
||||
}
|
||||
}
|
||||
Ordering::Equal
|
||||
}
|
||||
});
|
||||
aliases
|
||||
}
|
||||
|
||||
fn is_flattened(attrs: &[Attribute]) -> bool {
|
||||
for attr in attrs.iter() {
|
||||
if !attr.path().is_ident("serde_aco") {
|
||||
continue;
|
||||
}
|
||||
let mut flattened = false;
|
||||
let is_flatten = |meta: ParseNestedMeta| {
|
||||
if meta.path.is_ident("flatten") {
|
||||
flattened = true;
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
if attr.parse_nested_meta(is_flatten).is_err() {
|
||||
continue;
|
||||
}
|
||||
if flattened {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn derive_named_struct_help(name: &Ident, fields: &FieldsNamed) -> TokenStream2 {
|
||||
let mut field_doc = vec![];
|
||||
let mut flatten_fields = vec![];
|
||||
for field in fields.named.iter() {
|
||||
let ident = field.ident.as_ref().unwrap();
|
||||
let aliases = get_serde_aliases_from_attrs(ident, &field.attrs);
|
||||
let ident = &aliases[0];
|
||||
let ty = &field.ty;
|
||||
let doc = get_doc_from_attrs(&field.attrs);
|
||||
let field_help = quote! {
|
||||
FieldHelp {
|
||||
ident: #ident,
|
||||
doc: #doc,
|
||||
ty: <#ty as Help>::help(),
|
||||
}
|
||||
};
|
||||
if is_flattened(&field.attrs) {
|
||||
flatten_fields.push(field_help);
|
||||
} else {
|
||||
field_doc.push(field_help);
|
||||
}
|
||||
}
|
||||
if flatten_fields.is_empty() {
|
||||
quote! {
|
||||
TypedHelp::Struct{
|
||||
name: stringify!(#name),
|
||||
fields: vec![#(#field_doc,)*],
|
||||
}
|
||||
}
|
||||
} else {
|
||||
quote! {{
|
||||
let mut fields = vec![#(#field_doc,)*];
|
||||
let mut flatted_fields = vec![#(#flatten_fields,)*];
|
||||
for mut field in flatted_fields {
|
||||
match field.ty {
|
||||
TypedHelp::Enum { variants, .. } => field.ty = TypedHelp::FlattenedEnum { variants },
|
||||
TypedHelp::Option(mut o) => match *o {
|
||||
TypedHelp::Enum { variants, .. } => {
|
||||
*o = TypedHelp::FlattenedEnum { variants };
|
||||
field.ty = TypedHelp::Option(o);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
},
|
||||
_ => unreachable!(),
|
||||
};
|
||||
fields.push(field);
|
||||
}
|
||||
TypedHelp::Struct{
|
||||
name: stringify!(#name),
|
||||
fields,
|
||||
}
|
||||
}}
|
||||
}
|
||||
}
|
||||
|
||||
fn derive_unnamed_struct_help(fields: &FieldsUnnamed) -> TokenStream2 {
|
||||
if let Some(first) = fields.unnamed.first() {
|
||||
let ty = &first.ty;
|
||||
quote! { <#ty as Help>::help() }
|
||||
} else if fields.unnamed.is_empty() {
|
||||
quote! { TypedHelp::Unit }
|
||||
} else {
|
||||
panic!("Unnamed struct must have only one field")
|
||||
}
|
||||
}
|
||||
|
||||
fn derive_struct_help(name: &Ident, data: &DataStruct) -> TokenStream2 {
|
||||
match &data.fields {
|
||||
Fields::Named(fields) => derive_named_struct_help(name, fields),
|
||||
Fields::Unnamed(fields) => derive_unnamed_struct_help(fields),
|
||||
Fields::Unit => quote! { TypedHelp::Unit },
|
||||
}
|
||||
}
|
||||
|
||||
fn derive_enum_help(name: &Ident, data: &DataEnum) -> TokenStream2 {
|
||||
let mut variants = vec![];
|
||||
for variant in data.variants.iter() {
|
||||
let doc = get_doc_from_attrs(&variant.attrs);
|
||||
let ty = match &variant.fields {
|
||||
Fields::Unit => quote! {TypedHelp::Unit},
|
||||
Fields::Named(fields) => derive_named_struct_help(name, fields),
|
||||
Fields::Unnamed(fields) => derive_unnamed_struct_help(fields),
|
||||
};
|
||||
let aliases = get_serde_aliases_from_attrs(&variant.ident, &variant.attrs);
|
||||
let ident = &aliases[0];
|
||||
variants.push(quote! {
|
||||
FieldHelp {
|
||||
ident: #ident,
|
||||
doc: #doc,
|
||||
ty: #ty,
|
||||
}
|
||||
})
|
||||
}
|
||||
quote! {
|
||||
TypedHelp::Enum {
|
||||
name: stringify!(#name),
|
||||
variants: vec![#(#variants,)*],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn derive_help(input: TokenStream) -> TokenStream {
|
||||
let input = parse_macro_input!(input as DeriveInput);
|
||||
let ty_name = &input.ident;
|
||||
let body = match &input.data {
|
||||
Data::Struct(data) => derive_struct_help(ty_name, data),
|
||||
Data::Enum(data) => derive_enum_help(ty_name, data),
|
||||
Data::Union(_) => unimplemented!("Data::Union not supported"),
|
||||
};
|
||||
TokenStream::from(quote! {
|
||||
const _:() = {
|
||||
use ::serde_aco::{Help, TypedHelp, FieldHelp};
|
||||
impl Help for #ty_name {
|
||||
fn help() -> TypedHelp {
|
||||
#body
|
||||
}
|
||||
}
|
||||
};
|
||||
})
|
||||
}
|
22
serde-aco-derive/src/lib.rs
Normal file
22
serde-aco-derive/src/lib.rs
Normal file
|
@ -0,0 +1,22 @@
|
|||
// Copyright 2024 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
mod help;
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
|
||||
#[proc_macro_derive(Help, attributes(serde_aco))]
|
||||
pub fn derive_help(input: TokenStream) -> TokenStream {
|
||||
help::derive_help(input)
|
||||
}
|
|
@ -9,6 +9,7 @@ repository.workspace = true
|
|||
|
||||
[dependencies]
|
||||
serde.workspace = true
|
||||
serde-aco-derive.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
assert_matches.workspace = true
|
||||
|
|
93
serde-aco/src/help.rs
Normal file
93
serde-aco/src/help.rs
Normal file
|
@ -0,0 +1,93 @@
|
|||
// Copyright 2024 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::ffi::{CStr, CString, OsStr, OsString};
|
||||
use std::num::NonZero;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
pub use serde_aco_derive::Help;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FieldHelp {
|
||||
pub ident: &'static str,
|
||||
pub doc: &'static str,
|
||||
pub ty: TypedHelp,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum TypedHelp {
|
||||
Struct {
|
||||
name: &'static str,
|
||||
fields: Vec<FieldHelp>,
|
||||
},
|
||||
Enum {
|
||||
name: &'static str,
|
||||
variants: Vec<FieldHelp>,
|
||||
},
|
||||
FlattenedEnum {
|
||||
variants: Vec<FieldHelp>,
|
||||
},
|
||||
String,
|
||||
Int,
|
||||
Float,
|
||||
Bool,
|
||||
Unit,
|
||||
Custom {
|
||||
desc: &'static str,
|
||||
},
|
||||
Option(Box<TypedHelp>),
|
||||
}
|
||||
|
||||
pub trait Help {
|
||||
fn help() -> TypedHelp;
|
||||
}
|
||||
|
||||
macro_rules! impl_help_for_num_types {
|
||||
($help_type:ident, $($ty:ty),+) => {
|
||||
$(impl Help for $ty {
|
||||
fn help() -> TypedHelp {
|
||||
TypedHelp::$help_type
|
||||
}
|
||||
})+
|
||||
$(impl Help for NonZero<$ty> {
|
||||
fn help() -> TypedHelp {
|
||||
TypedHelp::$help_type
|
||||
}
|
||||
})+
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! impl_help_for_types {
|
||||
($help_type:ident, $($ty:ty),+) => {
|
||||
$(impl Help for $ty {
|
||||
fn help() -> TypedHelp {
|
||||
TypedHelp::$help_type
|
||||
}
|
||||
})+
|
||||
};
|
||||
}
|
||||
|
||||
impl_help_for_num_types!(Int, i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize);
|
||||
impl_help_for_types!(Float, f32, f64);
|
||||
impl_help_for_types!(Bool, bool);
|
||||
impl_help_for_types!(String, &str, String, CStr, CString, &OsStr, OsString, &Path, PathBuf);
|
||||
|
||||
impl<T> Help for Option<T>
|
||||
where
|
||||
T: Help,
|
||||
{
|
||||
fn help() -> TypedHelp {
|
||||
TypedHelp::Option(Box::new(T::help()))
|
||||
}
|
||||
}
|
|
@ -14,6 +14,8 @@
|
|||
|
||||
mod de;
|
||||
mod error;
|
||||
mod help;
|
||||
|
||||
pub use de::{from_arg, from_args, Deserializer};
|
||||
pub use error::{Error, Result};
|
||||
pub use help::{FieldHelp, Help, TypedHelp};
|
||||
|
|
Loading…
Reference in a new issue