From 30a1c0b9ae11baf8ac8ffe534c03b4d31484b668 Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Thu, 28 Nov 2019 12:21:46 +0100 Subject: [PATCH] api-macro: experimental enum support Signed-off-by: Wolfgang Bumiller --- proxmox-api-macro/src/api.rs | 2 + proxmox-api-macro/src/api/enums.rs | 92 ++++++++++++++++++++++++++++ proxmox-api-macro/src/api/structs.rs | 4 -- proxmox-api-macro/src/util.rs | 38 +++++++++++- proxmox-api-macro/tests/types.rs | 7 +++ 5 files changed, 136 insertions(+), 7 deletions(-) create mode 100644 proxmox-api-macro/src/api/enums.rs diff --git a/proxmox-api-macro/src/api.rs b/proxmox-api-macro/src/api.rs index 6a88671e..f079e967 100644 --- a/proxmox-api-macro/src/api.rs +++ b/proxmox-api-macro/src/api.rs @@ -10,6 +10,7 @@ use syn::{ExprPath, Ident}; use crate::util::{JSONObject, JSONValue}; +mod enums; mod method; mod structs; @@ -356,6 +357,7 @@ pub(crate) fn api(attr: TokenStream, item: TokenStream) -> Result method::handle_method(attribs, item), syn::Item::Struct(item) => structs::handle_struct(attribs, item), + syn::Item::Enum(item) => enums::handle_enum(attribs, item), _ => bail!(item => "api macro only works on functions"), } } diff --git a/proxmox-api-macro/src/api/enums.rs b/proxmox-api-macro/src/api/enums.rs new file mode 100644 index 00000000..1643fb2e --- /dev/null +++ b/proxmox-api-macro/src/api/enums.rs @@ -0,0 +1,92 @@ +use std::convert::TryInto; +use std::mem; + +use failure::Error; + +use proc_macro2::{Ident, Span, TokenStream}; +use quote::{quote, quote_spanned}; +use syn::punctuated::Punctuated; +use syn::Token; + +use super::Schema; +use crate::util::{JSONObject, JSONValue, SimpleIdent}; + +/// Enums, provided they're simple enums, simply get an enum string schema attached to them. +pub fn handle_enum( + mut attribs: JSONObject, + mut enum_ty: syn::ItemEnum, +) -> Result { + if !attribs.contains_key("type") { + attribs.insert( + SimpleIdent::new("type".to_string(), Span::call_site()), + JSONValue::new_ident(Ident::new("String", enum_ty.enum_token.span)), + ); + } + + if let Some(fmt) = attribs.get("format") { + bail!(fmt.span(), "illegal key 'format', will be autogenerated"); + } + + let schema = { + let schema: Schema = attribs.try_into()?; + let mut ts = TokenStream::new(); + schema.to_typed_schema(&mut ts)?; + ts + }; + + // with_capacity(enum_ty.variants.len()); + // doesn't exist O.o + let mut variants = Punctuated::::new(); + for variant in &mut enum_ty.variants { + match &variant.fields { + syn::Fields::Unit => (), + _ => bail!(variant => "api macro does not support enums with fields"), + } + + let mut renamed = false; + for attrib in &mut variant.attrs { + if !attrib.path.is_ident("api") { + continue; + } + + attrib.path = syn::parse2(quote! { serde })?; + + let mut obj: JSONObject = + syn::parse2(mem::replace(&mut attrib.tokens, TokenStream::new()))?; + match obj.remove("rename") { + Some(JSONValue::Expr(syn::Expr::Lit(lit))) => { + if let syn::Lit::Str(lit) = lit.lit { + attrib.tokens.extend(quote! { rename = #lit }); + variants.push(lit); + renamed = true; + } else { + bail!(attrib => "'rename' must be a literal string"); + } + } + Some(_) => bail!(attrib => "'rename' must be a literal string"), + None => (), + } + + if !obj.is_empty() { + bail!(attrib => "unknown fields in attribute"); + } + } + + if !renamed { + let name = &variant.ident; + variants.push(syn::LitStr::new(&name.to_string(), name.span())); + } + } + + let name = &enum_ty.ident; + + Ok(quote_spanned! { name.span() => + #enum_ty + impl #name { + pub const API_SCHEMA: &'static ::proxmox::api::schema::Schema = + & #schema + .format(&::proxmox::api::schema::ApiStringFormat::Enum(&[#variants])) + .schema(); + } + }) +} diff --git a/proxmox-api-macro/src/api/structs.rs b/proxmox-api-macro/src/api/structs.rs index 8be40b70..82cac929 100644 --- a/proxmox-api-macro/src/api/structs.rs +++ b/proxmox-api-macro/src/api/structs.rs @@ -8,10 +8,6 @@ use quote::quote_spanned; use super::Schema; use crate::util::JSONObject; -/// Parse `input`, `returns` and `protected` attributes out of an function annotated -/// with an `#[api]` attribute and produce a `const ApiMethod` named after the function. -/// -/// See the top level macro documentation for a complete example. pub fn handle_struct(attribs: JSONObject, stru: syn::ItemStruct) -> Result { let schema = { let schema: Schema = attribs.try_into()?; diff --git a/proxmox-api-macro/src/util.rs b/proxmox-api-macro/src/util.rs index b660d50b..e55572bf 100644 --- a/proxmox-api-macro/src/util.rs +++ b/proxmox-api-macro/src/util.rs @@ -14,9 +14,9 @@ use syn::Token; pub struct SimpleIdent(Ident, String); impl SimpleIdent { - //pub fn new(name: String, span: Span) -> Self { - // Self(Ident::new(&name, span), name) - //} + pub fn new(name: String, span: Span) -> Self { + Self(Ident::new(&name, span), name) + } #[inline] pub fn as_str(&self) -> &str { @@ -124,6 +124,38 @@ impl JSONValue { JSONValue::Expr(e) => bail!(e => "expected {}", expected), } } + + pub fn new_string(value: &str, span: Span) -> JSONValue { + JSONValue::Expr(syn::Expr::Lit(syn::ExprLit { + attrs: Vec::new(), + lit: syn::Lit::Str(syn::LitStr::new(value, span)), + })) + } + + pub fn new_ident(ident: Ident) -> JSONValue { + JSONValue::Expr(syn::Expr::Path(syn::ExprPath { + attrs: Vec::new(), + qself: None, + path: syn::Path { + leading_colon: None, + segments: { + let mut p = Punctuated::new(); + p.push(syn::PathSegment { + ident, + arguments: Default::default(), + }); + p + }, + }, + })) + } + + pub fn span(&self) -> Span { + match self { + JSONValue::Object(obj) => obj.brace_token.span, + JSONValue::Expr(expr) => expr.span(), + } + } } /// Expect a json value to be an expression, not an object: diff --git a/proxmox-api-macro/tests/types.rs b/proxmox-api-macro/tests/types.rs index 43f256a0..96da6fdf 100644 --- a/proxmox-api-macro/tests/types.rs +++ b/proxmox-api-macro/tests/types.rs @@ -23,6 +23,13 @@ impl OkString { .schema(); } +#[api(description: "A selection of either A, B or C")] +pub enum Selection { + A, + B, + C, +} + // Initial test: #[api( input: {