diff --git a/proxmox-api-macro/src/api/enums.rs b/proxmox-api-macro/src/api/enums.rs index 9ac64d6e..1d3e34a0 100644 --- a/proxmox-api-macro/src/api/enums.rs +++ b/proxmox-api-macro/src/api/enums.rs @@ -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; } diff --git a/proxmox-api-macro/src/lib.rs b/proxmox-api-macro/src/lib.rs index d37b41f1..01dcd95f 100644 --- a/proxmox-api-macro/src/lib.rs +++ b/proxmox-api-macro/src/lib.rs @@ -19,6 +19,7 @@ macro_rules! bail { } mod api; +mod serde; mod util; fn handle_error(mut item: TokenStream, data: Result) -> TokenStream { @@ -46,7 +47,7 @@ fn router_do(item: TokenStream) -> Result { } /** - Macro for building an API method: + Macro for building API methods and types: ``` # use proxmox_api_macro::api; diff --git a/proxmox-api-macro/src/serde.rs b/proxmox-api-macro/src/serde.rs new file mode 100644 index 00000000..83709f6c --- /dev/null +++ b/proxmox-api-macro/src/serde.rs @@ -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 { + 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 { + 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, +} + +impl TryFrom<&[syn::Attribute]> for ContainerAttrib { + type Error = syn::Error; + + fn try_from(attributes: &[syn::Attribute]) -> Result { + 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, +} + +impl TryFrom<&[syn::Attribute]> for SerdeAttrib { + type Error = syn::Error; + + fn try_from(attributes: &[syn::Attribute]) -> Result { + 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) + } +} diff --git a/proxmox-api-macro/src/util.rs b/proxmox-api-macro/src/util.rs index 748d6986..9e420b16 100644 --- a/proxmox-api-macro/src/util.rs +++ b/proxmox-api-macro/src/util.rs @@ -93,6 +93,12 @@ impl From for FieldName { } } +impl From<&syn::LitStr> for FieldName { + fn from(s: &syn::LitStr) -> Self { + Self::new(s.value(), s.span()) + } +} + impl Borrow 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, +} + +impl Parse for AttrArgs { + fn parse(input: ParseStream) -> syn::Result { + let content; + Ok(Self { + _paren_token: syn::parenthesized!(content in input), + args: Punctuated::parse_terminated(&content)?, + }) + } +}