Skip to content

Commit

Permalink
Add rename_all & rename config options
Browse files Browse the repository at this point in the history
  • Loading branch information
cyqsimon committed Oct 20, 2024
1 parent 6be0d53 commit 9e0eae0
Show file tree
Hide file tree
Showing 8 changed files with 337 additions and 17 deletions.
1 change: 1 addition & 0 deletions documented-macros/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ pub mod attr;
#[cfg(feature = "customise")]
pub mod customise_core;
pub mod derive;
pub mod derive_fields;
4 changes: 4 additions & 0 deletions documented-macros/src/config/attr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ mod customise {
// I'd love to macro this if declarative macros can expand to a full match arm,
// but no: https://github.com/rust-lang/rfcs/issues/2654
match opt.data {
Data::RenameAll(..) => Err(syn::Error::new(
opt.span,
"This config option is not applicable here",
))?,
Data::Vis(vis) => {
config.custom_vis.replace(vis);
}
Expand Down
53 changes: 51 additions & 2 deletions documented-macros/src/config/customise_core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ mod kw {
use syn::custom_keyword;

custom_keyword!(vis);
custom_keyword!(rename_all);
custom_keyword!(rename);
custom_keyword!(default);
custom_keyword!(trim);
Expand Down Expand Up @@ -43,6 +44,7 @@ impl Parse for ConfigOption {
input.parse::<Token![=]>()?;
let data = match kind {
Kind::Vis => Data::Vis(input.parse()?),
Kind::RenameAll => Data::RenameAll(input.parse()?),
Kind::Rename => Data::Rename(input.parse()?),
Kind::Default => Data::Default(input.parse()?),
Kind::Trim => Data::Trim(input.parse()?),
Expand All @@ -52,6 +54,45 @@ impl Parse for ConfigOption {
}
}

/// All supported cases of `rename_all`.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct LitCase(convert_case::Case);
impl Parse for LitCase {
fn parse(input: ParseStream) -> syn::Result<Self> {
use convert_case::Case as C;

const SUPPORTED_CASES: [(&str, C); 8] = [
("lowercase", C::Lower),
("UPPERCASE", C::Upper),
("PascalCase", C::Pascal),
("camelCase", C::Camel),
("snake_case", C::Snake),
("SCREAMING_SNAKE_CASE", C::UpperSnake),
("kebab-case", C::Kebab),
("SCREAMING-KEBAB-CASE", C::UpperKebab),
];

let arg = input.parse::<LitStr>()?;
let Some(case) = SUPPORTED_CASES
.into_iter()
.find_map(|(name, case)| (name == arg.value()).then_some(case))
else {
let options = SUPPORTED_CASES.map(|(name, _)| name).join(", ");
Err(Error::new(
arg.span(),
format!("Case must be one of {options}."),
))?
};

Ok(Self(case))
}
}
impl LitCase {
pub fn value(&self) -> convert_case::Case {
self.0
}
}

/// The data of all known configuration options.
#[derive(Clone, Debug, PartialEq, Eq, strum::EnumDiscriminants)]
#[strum_discriminants(
Expand All @@ -66,9 +107,14 @@ pub enum ConfigOptionData {
/// E.g. `vis = pub(crate)`.
Vis(Visibility),

/// Custom name for generated constant.
/// Custom casing of key names for the generated constants.
///
/// E.g. `rename_all = "kebab-case"`.
RenameAll(LitCase),

/// Custom key name for the generated constant.
///
/// E.g. `rename = "CUSTOM_NAME_DOCS"`.
/// E.g. `rename = "custom_field_name`, `rename = "CUSTOM_NAME_DOCS"`.
Rename(LitStr),

/// Use some default value when doc comments are absent.
Expand All @@ -88,6 +134,9 @@ impl Parse for ConfigOptionKind {
let ty = if lookahead.peek(kw::vis) {
input.parse::<kw::vis>()?;
Self::Vis
} else if lookahead.peek(kw::rename_all) {
input.parse::<kw::rename_all>()?;
Self::RenameAll
} else if lookahead.peek(kw::rename) {
input.parse::<kw::rename>()?;
Self::Rename
Expand Down
12 changes: 8 additions & 4 deletions documented-macros/src/config/derive.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
//! Generic configuration for derive macros.
//!
//! If a macro needs specialised configuration, this file can be used as a
//! starting template.
use syn::Expr;

/// Configurable options for derive macros via helper attributes.
Expand Down Expand Up @@ -50,10 +55,9 @@ mod customise {
let mut config = Self::default();
for opt in opts {
match opt.data {
Data::Vis(..) | Data::Rename(..) => Err(syn::Error::new(
opt.span,
"This config option is not applicable to derive macros",
))?,
Data::Vis(..) | Data::RenameAll(..) | Data::Rename(..) => Err(
syn::Error::new(opt.span, "This config option is not applicable here"),
)?,
Data::Default(expr) => {
config.default_value.replace(expr);
}
Expand Down
150 changes: 150 additions & 0 deletions documented-macros/src/config/derive_fields.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
//! Specialised configuration for `DocumentedFields` and `DocumentedFieldsOpt`.
use convert_case::Case;
use syn::Expr;

/// Defines how to rename a particular field.
#[derive(Clone, Debug, PartialEq, Eq)]
#[allow(dead_code)]
pub enum RenameMode {
/// Use the original name, converted to another case.
ToCase(Case),
/// Use a custom name.
Custom(String),
}

#[cfg_attr(feature = "customise", optfield::optfield(
pub DeriveFieldsBaseCustomisations,
attrs = (derive(Clone, Debug, Default, PartialEq, Eq)),
merge_fn = pub apply_base_customisations,
doc = "Parsed user-defined customisations of configurable options.\n\
Specialised variant for the type base of `DocumentedFields` and `DocumentedFieldsOpt`.\n\
\n\
Expected parse stream format: `<KW> = <VAL>, <KW> = <VAL>, ...`"
))]
#[cfg_attr(feature = "customise", optfield::optfield(
pub DeriveFieldsCustomisations,
attrs = (derive(Clone, Debug, Default, PartialEq, Eq)),
merge_fn = pub apply_field_customisations,
doc = "Parsed user-defined customisations of configurable options.\n\
Specialised variant for each field of `DocumentedFields` and `DocumentedFieldsOpt`.\n\
\n\
Expected parse stream format: `<KW> = <VAL>, <KW> = <VAL>, ...`"
))]
/// Configurable options for each field via helper attributes.
///
/// Initial values are set to default.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DeriveFieldsConfig {
// optfield does not rewrap `Option` by default, which is the desired behavior
// see https://docs.rs/optfield/latest/optfield/#rewrapping-option-fields
pub rename_mode: Option<RenameMode>,
pub default_value: Option<Expr>,
pub trim: bool,
}
impl Default for DeriveFieldsConfig {
fn default() -> Self {
Self {
rename_mode: None,
default_value: None,
trim: true,
}
}
}

#[cfg(feature = "customise")]
mod customise {
use crate::config::{
customise_core::{ConfigOption, ConfigOptionData},
derive_fields::{
DeriveFieldsBaseCustomisations, DeriveFieldsConfig, DeriveFieldsCustomisations,
RenameMode,
},
};

impl DeriveFieldsConfig {
/// Return a new instance of this config with base customisations applied.
pub fn with_base_customisations(
&self,
customisations: DeriveFieldsBaseCustomisations,
) -> Self {
let mut new = self.clone();
new.apply_base_customisations(customisations);
new
}

/// Return a new instance of this config with field customisations applied.
pub fn with_field_customisations(
&self,
customisations: DeriveFieldsCustomisations,
) -> Self {
let mut new = self.clone();
new.apply_field_customisations(customisations);
new
}
}

impl TryFrom<Vec<ConfigOption>> for DeriveFieldsBaseCustomisations {
type Error = syn::Error;

/// Duplicate option rejection should be handled upstream.
fn try_from(opts: Vec<ConfigOption>) -> Result<Self, Self::Error> {
use ConfigOptionData as Data;

let mut config = Self::default();
for opt in opts {
match opt.data {
Data::Vis(..) | Data::Rename(..) => Err(syn::Error::new(
opt.span,
"This config option is not applicable here",
))?,
Data::RenameAll(case) => {
config.rename_mode.replace(RenameMode::ToCase(case.value()));
}
Data::Default(expr) => {
config.default_value.replace(expr);
}
Data::Trim(trim) => {
config.trim.replace(trim.value());
}
}
}
Ok(config)
}
}

impl TryFrom<Vec<ConfigOption>> for DeriveFieldsCustomisations {
type Error = syn::Error;

/// Duplicate option rejection should be handled upstream.
fn try_from(opts: Vec<ConfigOption>) -> Result<Self, Self::Error> {
use ConfigOptionData as Data;

let mut config = Self::default();
for opt in opts {
match opt.data {
Data::Vis(..) => Err(syn::Error::new(
opt.span,
"This config option is not applicable here",
))?,
Data::RenameAll(case) => {
// `rename` always has priority over `rename_all`
if !matches!(config.rename_mode, Some(RenameMode::Custom(_))) {
config.rename_mode.replace(RenameMode::ToCase(case.value()));
}
}
Data::Rename(name) => {
config.rename_mode.replace(RenameMode::Custom(name.value()));
}
Data::Default(expr) => {
config.default_value.replace(expr);
}
Data::Trim(trim) => {
config.trim.replace(trim.value());
}
}
}
Ok(config)
}
}
}
29 changes: 20 additions & 9 deletions documented-macros/src/derive_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
//! All functions in this module use the dependency injection pattern to
//! generate the correct trait implementation for both macro variants.
use convert_case::Casing;
use proc_macro2::{Span, TokenStream};
use quote::{quote, ToTokens, TokenStreamExt};
use syn::{
Expand All @@ -13,7 +14,10 @@ use syn::{
#[cfg(feature = "customise")]
use crate::config::customise_core::get_customisations_from_attrs;
use crate::{
config::derive::DeriveConfig,
config::{
derive::DeriveConfig,
derive_fields::{DeriveFieldsConfig, RenameMode},
},
util::{crate_module_path, get_docs},
};

Expand Down Expand Up @@ -110,10 +114,10 @@ pub fn documented_fields_impl(input: DeriveInput, docs_ty: DocType) -> syn::Resu

// `#[documented_fields(...)]` on container type
#[cfg(not(feature = "customise"))]
let base_config = DeriveConfig::default();
let base_config = DeriveFieldsConfig::default();
#[cfg(feature = "customise")]
let base_config = get_customisations_from_attrs(&input.attrs, "documented_fields")
.map(|c| DeriveConfig::default().with_customisations(c))?;
.map(|c| DeriveFieldsConfig::default().with_base_customisations(c))?;

let fields_attrs: Vec<_> = match input.data.clone() {
Data::Enum(DataEnum { variants, .. }) => variants
Expand All @@ -131,29 +135,36 @@ pub fn documented_fields_impl(input: DeriveInput, docs_ty: DocType) -> syn::Resu
.collect(),
};

let (field_idents, field_docs) = fields_attrs
let (field_names, field_docs) = fields_attrs
.into_iter()
.map(|(span, ident, attrs)| {
#[cfg(not(feature = "customise"))]
let config = base_config.clone();
#[cfg(feature = "customise")]
let config = get_customisations_from_attrs(&attrs, "documented_fields")
.map(|c| base_config.with_customisations(c))?;
.map(|c| base_config.with_field_customisations(c))?;
let name = match config.rename_mode {
None => ident.map(|ident| ident.to_string()),
Some(RenameMode::ToCase(case)) => {
ident.map(|ident| ident.to_string().to_case(case))
}
Some(RenameMode::Custom(name)) => Some(name),
};
get_docs(&attrs, config.trim)
.and_then(|docs_opt| {
docs_ty.docs_handler_opt()(docs_opt, config.default_value, span)
})
.map(|docs| (ident, docs))
.map(|docs| (name, docs))
})
.collect::<syn::Result<Vec<_>>>()?
.into_iter()
.unzip::<_, _, Vec<_>, Vec<_>>();

let phf_match_arms = field_idents
let phf_match_arms = field_names
.into_iter()
.enumerate()
.filter_map(|(i, o)| o.map(|ident| (i, ident.to_string())))
.map(|(i, ident)| quote! { #ident => #i, })
.filter_map(|(i, name)| name.map(|n| (i, n)))
.map(|(i, name)| quote! { #name => #i, })
.collect::<Vec<_>>();

let documented_module_path = crate_module_path();
Expand Down
Loading

0 comments on commit 9e0eae0

Please sign in to comment.