api-macro: prepare to support serde::rename_all

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
This commit is contained in:
Wolfgang Bumiller 2020-01-07 15:10:55 +01:00
parent 3626f57d2c
commit 799c993d63
4 changed files with 225 additions and 2 deletions

View File

@ -66,7 +66,7 @@ pub fn handle_enum(
}
let mut renamed = false;
for attrib in &mut variant.attrs {
for attrib in &variant.attrs {
if !attrib.path.is_ident("serde") {
continue;
}

View File

@ -19,6 +19,7 @@ macro_rules! bail {
}
mod api;
mod serde;
mod util;
fn handle_error(mut item: TokenStream, data: Result<TokenStream, Error>) -> TokenStream {
@ -46,7 +47,7 @@ fn router_do(item: TokenStream) -> Result<TokenStream, Error> {
}
/**
Macro for building an API method:
Macro for building API methods and types:
```
# use proxmox_api_macro::api;

View File

@ -0,0 +1,200 @@
//! Serde support module.
//!
//! The `#![api]` macro needs to be able to cope with some `#[serde(...)]` attributes such as
//! `rename` and `rename_all`.
use std::convert::TryFrom;
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::Token;
use crate::util::{AttrArgs, FieldName};
/// Serde name types.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RenameAll {
LowerCase,
UpperCase,
PascalCase,
CamelCase,
SnakeCase,
ScreamingSnakeCase,
KebabCase,
ScreamingKebabCase,
}
impl TryFrom<&syn::Lit> for RenameAll {
type Error = syn::Error;
fn try_from(s: &syn::Lit) -> Result<Self, syn::Error> {
match s {
syn::Lit::Str(s) => Self::try_from(s),
_ => bail!(s => "expected rename type as string"),
}
}
}
impl TryFrom<&syn::LitStr> for RenameAll {
type Error = syn::Error;
fn try_from(s: &syn::LitStr) -> Result<Self, syn::Error> {
let s = s.value();
if s == "lowercase" {
Ok(RenameAll::LowerCase)
} else if s == "UPPERCASE" {
Ok(RenameAll::UpperCase)
} else if s == "PascalCase" {
Ok(RenameAll::PascalCase)
} else if s == "camelCase" {
Ok(RenameAll::CamelCase)
} else if s == "snake_case" {
Ok(RenameAll::SnakeCase)
} else if s == "SCREAMING_SNAKE_CASE" {
Ok(RenameAll::ScreamingSnakeCase)
} else if s == "kebab-case" {
Ok(RenameAll::KebabCase)
} else if s == "SCREAMING-KEBAB-CASE" {
Ok(RenameAll::ScreamingKebabCase)
} else {
bail!(&s => "unhandled `rename_all` type: {}", s.to_string())
}
}
}
impl RenameAll {
/// Like in serde, we assume that fields are in `snake_case` and enum variants are in
/// `PascalCase`, so we only perform the changes required for fields here!
pub fn apply_to_field(&self, s: &str) -> String {
match self {
RenameAll::SnakeCase => s.to_owned(), // this is our source type
RenameAll::ScreamingSnakeCase => s.to_uppercase(), // capitalized source type
RenameAll::LowerCase => s.to_lowercase(),
RenameAll::UpperCase => s.to_uppercase(),
RenameAll::PascalCase => {
// Strip underscores and capitalize instead:
let mut out = String::new();
let mut cap = true;
for c in s.chars() {
if c == '_' {
cap = true;
} else if cap {
cap = false;
out.push(c.to_ascii_uppercase());
} else {
out.push(c.to_ascii_lowercase());
}
}
out
}
RenameAll::CamelCase => {
let s = RenameAll::PascalCase.apply_to_field(s);
s[..1].to_ascii_lowercase() + &s[1..]
}
RenameAll::KebabCase => s.replace('_', "-"),
RenameAll::ScreamingKebabCase => s.replace('_', "-").to_ascii_uppercase(),
}
}
/// Like in serde, we assume that fields are in `snake_case` and enum variants are in
/// `PascalCase`, so we only perform the changes required for enum variants here!
pub fn apply_to_variant(&self, s: &str) -> String {
match self {
RenameAll::PascalCase => s.to_owned(), // this is our source type
RenameAll::CamelCase => s[..1].to_ascii_lowercase() + &s[1..],
RenameAll::LowerCase => s.to_lowercase(),
RenameAll::UpperCase => s.to_uppercase(),
RenameAll::SnakeCase => {
// Relatively simple: all lower-case, and new words get split by underscores:
let mut out = String::new();
for (i, c) in s.char_indices() {
if i > 0 && c.is_uppercase() {
out.push('_');
}
out.push(c.to_ascii_lowercase());
}
out
}
RenameAll::KebabCase => RenameAll::SnakeCase.apply_to_variant(s).replace('_', "-"),
RenameAll::ScreamingSnakeCase => RenameAll::SnakeCase
.apply_to_variant(s)
.to_ascii_uppercase(),
RenameAll::ScreamingKebabCase => RenameAll::KebabCase
.apply_to_variant(s)
.to_ascii_uppercase(),
}
}
}
/// `serde` container attributes we support
#[derive(Default)]
pub struct ContainerAttrib {
rename_all: Option<RenameAll>,
}
impl TryFrom<&[syn::Attribute]> for ContainerAttrib {
type Error = syn::Error;
fn try_from(attributes: &[syn::Attribute]) -> Result<Self, syn::Error> {
let mut this: Self = Default::default();
for attrib in attributes {
if !attrib.path.is_ident("serde") {
continue;
}
let args: AttrArgs = syn::parse2(attrib.tokens.clone())?;
for arg in args.args {
if let syn::NestedMeta::Meta(syn::Meta::NameValue(var)) = arg {
if var.path.is_ident("rename_all") {
let rename_all = RenameAll::try_from(&var.lit)?;
if this.rename_all.is_some() && this.rename_all != Some(rename_all) {
bail!(var.lit => "multiple conflicting 'rename_all' attributes");
}
this.rename_all = Some(rename_all);
}
}
}
}
Ok(this)
}
}
/// `serde` field/variant attributes we support
#[derive(Default)]
pub struct SerdeAttrib {
rename: Option<FieldName>,
}
impl TryFrom<&[syn::Attribute]> for SerdeAttrib {
type Error = syn::Error;
fn try_from(attributes: &[syn::Attribute]) -> Result<Self, syn::Error> {
let mut this: Self = Default::default();
for attrib in attributes {
if !attrib.path.is_ident("serde") {
continue;
}
let args: AttrArgs = syn::parse2(attrib.tokens.clone())?;
for arg in args.args {
if let syn::NestedMeta::Meta(syn::Meta::NameValue(var)) = arg {
if var.path.is_ident("rename") {
match var.lit {
syn::Lit::Str(lit) => {
let rename = FieldName::from(&lit);
if this.rename.is_some() && this.rename.as_ref() != Some(&rename) {
bail!(lit => "multiple conflicting 'rename' attributes");
}
this.rename = Some(rename);
}
_ => bail!(var.lit => "'rename' value must be a string literal"),
}
}
}
}
}
Ok(this)
}
}

View File

@ -93,6 +93,12 @@ impl From<Ident> for FieldName {
}
}
impl From<&syn::LitStr> for FieldName {
fn from(s: &syn::LitStr) -> Self {
Self::new(s.value(), s.span())
}
}
impl Borrow<str> for FieldName {
#[inline]
fn borrow(&self) -> &str {
@ -501,3 +507,19 @@ fn is_option_type(ty: &syn::Type) -> Option<&syn::Type> {
}
None
}
/// `parse_macro_input!` expects a TokenStream_1
pub struct AttrArgs {
_paren_token: syn::token::Paren,
pub args: Punctuated<syn::NestedMeta, Token![,]>,
}
impl Parse for AttrArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let content;
Ok(Self {
_paren_token: syn::parenthesized!(content in input),
args: Punctuated::parse_terminated(&content)?,
})
}
}