mirror of
https://git.proxmox.com/git/proxmox
synced 2025-08-07 11:06:02 +00:00
api-macro: prepare to support serde::rename_all
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
This commit is contained in:
parent
3626f57d2c
commit
799c993d63
@ -66,7 +66,7 @@ pub fn handle_enum(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut renamed = false;
|
let mut renamed = false;
|
||||||
for attrib in &mut variant.attrs {
|
for attrib in &variant.attrs {
|
||||||
if !attrib.path.is_ident("serde") {
|
if !attrib.path.is_ident("serde") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ macro_rules! bail {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mod api;
|
mod api;
|
||||||
|
mod serde;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
fn handle_error(mut item: TokenStream, data: Result<TokenStream, Error>) -> TokenStream {
|
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;
|
# use proxmox_api_macro::api;
|
||||||
|
200
proxmox-api-macro/src/serde.rs
Normal file
200
proxmox-api-macro/src/serde.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
@ -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 {
|
impl Borrow<str> for FieldName {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn borrow(&self) -> &str {
|
fn borrow(&self) -> &str {
|
||||||
@ -501,3 +507,19 @@ fn is_option_type(ty: &syn::Type) -> Option<&syn::Type> {
|
|||||||
}
|
}
|
||||||
None
|
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)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user