From a646146f752511f3b28f6984a63b5d53e6828557 Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Mon, 25 Nov 2019 15:07:26 +0100 Subject: [PATCH] replace builder-pattern api macro parser with json Signed-off-by: Wolfgang Bumiller --- proxmox-api-macro/src/api.rs | 735 ++++++++++++++------------------ proxmox-api-macro/src/lib.rs | 42 +- proxmox-api-macro/src/util.rs | 154 ++++--- proxmox-api-macro/tests/api1.rs | 43 +- 4 files changed, 469 insertions(+), 505 deletions(-) diff --git a/proxmox-api-macro/src/api.rs b/proxmox-api-macro/src/api.rs index 85769361..0f2cd863 100644 --- a/proxmox-api-macro/src/api.rs +++ b/proxmox-api-macro/src/api.rs @@ -1,6 +1,8 @@ extern crate proc_macro; extern crate proc_macro2; +use std::collections::HashMap; +use std::convert::{TryFrom, TryInto}; use std::mem; use failure::Error; @@ -13,498 +15,397 @@ use syn::spanned::Spanned; use syn::Ident; use syn::{parenthesized, Token}; -/// Any 'keywords' we introduce as part of our schema related api macro syntax. -mod token { - syn::custom_keyword!(optional); +use crate::util::SimpleIdent; + +/// Most of our schema definition consists of a json-like notation. +/// For parsing we mostly just need to destinguish between objects and non-objects. +/// For specific expression types we match on the contained expression later on. +enum JSONValue { + Object(JSONObject), + Expr(syn::Expr), } -/// Our syntax elements which represent an API Schema implement this. This is similar to -/// `quote::ToTokens`, but rather than translating back into the input, this produces the resulting -/// `proxmox::api::schema::Schema` instantiation. -/// -/// For example: -/// ```ignore -/// Schema { -/// item_type: "Boolean", -/// paren_token: ..., -/// description: Some("Some value"), -/// comma_token: ..., -/// item: SchemaItem::Boolean(SchemaItemBoolean { -/// default_value: Some(DefaultValue { -/// default_token: ..., -/// colon: ..., -/// value: syn::ExprLit(syn::LitBool(true)), // simplified... -/// }), -/// }), -/// constraints: Vec::new(), -/// }.to_schema(ts); -/// ``` -/// -/// produces: -/// -/// ```ignore -/// ::proxmox::api::schema::BooleanSchema::new("Some value") -/// .default(true) -/// ``` -trait ToSchema { - fn to_schema(&self, ts: &mut TokenStream) -> Result<(), Error>; - - #[inline] - fn add_constraints(&self, ts: &mut TokenStream) -> Result<(), Error> { - let _ = ts; - Ok(()) - } -} - -/// A generic schema entry. -/// -/// Since all our schema types have at least a description, we define this "top level" schema -/// syntax element which parses the description as first parameter (if it is available), and then -/// parses the remaining parts as `SchemaItem`. -/// -/// ```text -/// Object ( "Description", { Elements } ) .default_key("hello") -/// ^^^^^^ ~ ^^^^^^^^^^^^^^ ~~~~~~~~~~~~ ^ ~~~~~~~~~~~~~~~~~~~~~ -/// item_type description item constraints -/// ``` -struct Schema { - pub item_type: Ident, - pub paren_token: syn::token::Paren, - pub description: Option, - pub comma_token: Option, - pub item: SchemaItem, - pub constraints: Vec, -} - -impl ToSchema for Schema { - fn to_schema(&self, ts: &mut TokenStream) -> Result<(), Error> { - let item_type = &self.item_type; - let schema_type = Ident::new( - &format!("{}Schema", item_type.to_string()), - item_type.span(), - ); - let description = self - .description - .as_ref() - .ok_or_else(|| format_err!(item_type => "missing description"))?; - - let mut item = TokenStream::new(); - self.item.to_schema(&mut item)?; - - ts.extend(quote! { - ::proxmox::api::schema::#schema_type::new( - #description, - #item - ) - }); - self.item.add_constraints(ts)?; - - for constraint in self.constraints.iter() { - ts.extend(quote! { . #constraint }); - } - - Ok(()) - } -} - -impl Parse for Schema { - fn parse(input: ParseStream) -> syn::Result { - let item_type: Ident = input.parse()?; - let item_type_span = item_type.span(); - let item_type_str = item_type.to_string(); - let content; - let mut comma_token = None; - Ok(Self { - item_type, - paren_token: parenthesized!(content in input), - description: { - let lookahead = content.lookahead1(); - if lookahead.peek(syn::LitStr) { - let desc = content.parse()?; - if !content.is_empty() { - comma_token = Some(content.parse()?); - } - Some(desc) - } else { - None - } - }, - comma_token, - item: { - match item_type_str.as_str() { - "Null" => content.parse().map(SchemaItem::Null)?, - "Boolean" => content.parse().map(SchemaItem::Boolean)?, - "Integer" => content.parse().map(SchemaItem::Integer)?, - "String" => content.parse().map(SchemaItem::String)?, - "Object" => content.parse().map(SchemaItem::Object)?, - "Array" => content.parse().map(SchemaItem::Array)?, - _ => bail!(item_type_span, "unknown schema type"), - } - }, - constraints: { - let mut constraints = Vec::::new(); - while input.lookahead1().peek(Token![.]) { - let _dot: Token![.] = input.parse()?; - constraints.push(input.parse()?); - } - constraints - }, - }) - } -} - -/// This is the collection of possible schema elements we have. -/// -/// Its `ToSchema` implementation simply defers to the inner types. It has no `Parse` -/// implementation directly. This is handled by the parser for `Schema`. -enum SchemaItem { - Null(SchemaItemNull), - Boolean(SchemaItemBoolean), - Integer(SchemaItemInteger), - String(SchemaItemString), - Object(SchemaItemObject), - Array(SchemaItemArray), -} - -impl ToSchema for SchemaItem { - fn to_schema(&self, ts: &mut TokenStream) -> Result<(), Error> { +impl JSONValue { + /// When we expect an object, it's nicer to know why/what kind, so instead of + /// `TryInto` we provide this method: + fn into_object(self, expected: &str) -> Result { match self { - SchemaItem::Null(i) => i.to_schema(ts), - SchemaItem::Boolean(i) => i.to_schema(ts), - SchemaItem::Integer(i) => i.to_schema(ts), - SchemaItem::String(i) => i.to_schema(ts), - SchemaItem::Object(i) => i.to_schema(ts), - SchemaItem::Array(i) => i.to_schema(ts), - } - } - - #[inline] - fn add_constraints(&self, ts: &mut TokenStream) -> Result<(), Error> { - match self { - SchemaItem::Null(i) => i.add_constraints(ts), - SchemaItem::Boolean(i) => i.add_constraints(ts), - SchemaItem::Integer(i) => i.add_constraints(ts), - SchemaItem::String(i) => i.add_constraints(ts), - SchemaItem::Object(i) => i.add_constraints(ts), - SchemaItem::Array(i) => i.add_constraints(ts), + JSONValue::Object(s) => Ok(s), + JSONValue::Expr(e) => bail!(e => "expected {}", expected), } } } -/// A "default key" for an object schema. -/// -/// This serves mostly as an example of how we could extend the macro syntax. -/// This is used typing the following: -/// -/// ```ignore -/// Object("Description", default: "foo", { "foo": String("Foo"), "bar": String("Bar") }) -/// ``` -/// -/// instead of: -/// -/// ```ignore -/// Object("Description", { "foo": String("Foo"), "bar": String("Bar") }).default_key("foo") -/// ``` -struct DefaultKey { - pub default_token: Token![default], - pub colon: Token![:], - pub key_name: syn::LitStr, - pub comma_token: Token![,], -} - -impl Parse for DefaultKey { - fn parse(input: ParseStream) -> syn::Result { - Ok(Self { - default_token: input.parse()?, - colon: input.parse()?, - key_name: input.parse()?, - comma_token: input.parse()?, - }) +/// Expect a json value to be an expression, not an object: +impl TryFrom for syn::Expr { + type Error = syn::Error; + fn try_from(value: JSONValue) -> Result { + match value { + JSONValue::Object(s) => bail!(s.brace_token.span, "unexpected object"), + JSONValue::Expr(e) => Ok(e), + } } } -/// An object schema. This currently allows parsing a default key as an example of what we could do -/// instead of keeping the builder-pattern syntax within the macro invocation. -/// -/// The elements then follow enclosed in braces: -/// -/// ```ignore -/// Object("Description", { "key1": Integer("Key One"), optional "key2": Integer("Key Two") }) -/// ``` -struct SchemaItemObject { - pub default_key: Option, - pub brace_token: syn::token::Brace, - pub elements: Punctuated, -} - -impl ToSchema for SchemaItemObject { - fn to_schema(&self, ts: &mut TokenStream) -> Result<(), Error> { - let mut elements: Vec<&ObjectElement> = self.elements.iter().collect(); - elements.sort_by(|a, b| a.cmp(b)); - - let mut elem_ts = TokenStream::new(); - for element in elements { - if !elem_ts.is_empty() { - elem_ts.extend(quote![, ]); +/// Expect a json value to be a literal string: +impl TryFrom for syn::LitStr { + type Error = syn::Error; + fn try_from(value: JSONValue) -> Result { + let expr = syn::Expr::try_from(value)?; + if let syn::Expr::Lit(lit) = expr { + if let syn::Lit::Str(lit) = lit.lit { + return Ok(lit); } - - element.to_schema(&mut elem_ts)?; + bail!(lit => "expected string literal"); } - - ts.extend(quote! { & [ #elem_ts ] }); - - Ok(()) - } - - fn add_constraints(&self, ts: &mut TokenStream) -> Result<(), Error> { - if let Some(def) = &self.default_key { - let key = &def.key_name; - ts.extend(quote! { .default_key(#key) }); - } - Ok(()) + bail!(expr => "expected string literal"); } } -impl Parse for SchemaItemObject { +/// Expect a json value to be a literal boolean: +impl TryFrom for syn::LitBool { + type Error = syn::Error; + fn try_from(value: JSONValue) -> Result { + let expr = syn::Expr::try_from(value)?; + if let syn::Expr::Lit(lit) = expr { + if let syn::Lit::Bool(lit) = lit.lit { + return Ok(lit); + } + bail!(lit => "expected literal boolean"); + } + bail!(expr => "expected literal boolean"); + } +} + +/// Expect a json value to be an identifier: +impl TryFrom for Ident { + type Error = syn::Error; + fn try_from(value: JSONValue) -> Result { + let expr = syn::Expr::try_from(value)?; + let span = expr.span(); + if let syn::Expr::Path(path) = expr { + let mut iter = path.path.segments.into_pairs(); + let segment = iter + .next() + .ok_or_else(|| format_err!(span, "expected an identify, got an empty path"))? + .into_value(); + if iter.next().is_some() { + bail!(span, "expected an identifier, not a path"); + } + if !segment.arguments.is_empty() { + bail!(segment.arguments => "unexpected path arguments, expected an identifier"); + } + return Ok(segment.ident); + } + bail!(expr => "expected an identifier"); + } +} + +/// Expect a json value to be our "simple" identifier, which can be either an Ident or a String, or +/// the 'type' keyword: +impl TryFrom for SimpleIdent { + type Error = syn::Error; + fn try_from(value: JSONValue) -> Result { + Ok(SimpleIdent::from(Ident::try_from(value)?)) + } +} + +/// Parsing a json value should be simple enough: braces means we have an object, otherwise it must +/// be an "expression". +impl Parse for JSONValue { fn parse(input: ParseStream) -> syn::Result { - let elements; - Ok(Self { - default_key: { - let lookahead = input.lookahead1(); - if lookahead.peek(Token![default]) { - Some(input.parse()?) - } else { - None - } - }, - brace_token: syn::braced!(elements in input), - elements: elements.parse_terminated(ObjectElement::parse)?, - }) - } -} - -/// This represents a member in the comma separated list of fields of an object. -/// -/// ```text -/// Object("Description", { "key1": Integer("Key One"), optional "key2": Integer("Key Two") }) -/// ^^^^^^^^^^^^^^^^^^^^^^^^^^ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -/// one `ObjectElement` another `ObjectElement` -/// ``` -struct ObjectElement { - pub optional: Option, - pub field_name: syn::LitStr, - pub colon: Token![:], - pub item: Schema, -} - -impl ObjectElement { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.field_name.suffix().cmp(other.field_name.suffix()) - } -} - -impl ToSchema for ObjectElement { - fn to_schema(&self, ts: &mut TokenStream) -> Result<(), Error> { - let mut schema = TokenStream::new(); - self.item.to_schema(&mut schema)?; - - let name = &self.field_name; - - let optional = if self.optional.is_some() { - quote!(true) + let lookahead = input.lookahead1(); + Ok(if lookahead.peek(syn::token::Brace) { + JSONValue::Object(input.parse()?) } else { - quote!(false) - }; - - ts.extend(quote! { - (#name, #optional, & #schema .schema()) - }); - - Ok(()) - } -} - -impl Parse for ObjectElement { - fn parse(input: ParseStream) -> syn::Result { - Ok(Self { - optional: input.parse()?, - field_name: input.parse()?, - colon: input.parse()?, - item: input.parse()?, + JSONValue::Expr(input.parse()?) }) } } -/// Array schemas simply contain their inner type. -/// -/// ```ignore -/// Array("Some data", Integer("A data element")) -/// ``` -struct SchemaItemArray { - pub item_schema: Box, +/// The "core" of our schema is a json object. +struct JSONObject { + pub brace_token: syn::token::Brace, + pub elements: HashMap, } -impl ToSchema for SchemaItemArray { - fn to_schema(&self, ts: &mut TokenStream) -> Result<(), Error> { - ts.extend(quote! { & }); - self.item_schema.to_schema(ts)?; - self.item_schema.add_constraints(ts)?; - ts.extend(quote! { .schema() }); - Ok(()) - } -} +//impl TryFrom for JSONObject { +// type Error = syn::Error; +// +// fn try_from(value: JSONValue) -> Result { +// value.into_object() +// } +//} -impl Parse for SchemaItemArray { +impl Parse for JSONObject { fn parse(input: ParseStream) -> syn::Result { + let content; Ok(Self { - item_schema: Box::new(input.parse()?), + brace_token: syn::braced!(content in input), + elements: { + let map_elems: Punctuated = + content.parse_terminated(JSONMapEntry::parse)?; + let mut elems = HashMap::with_capacity(map_elems.len()); + for c in map_elems { + if elems.insert(c.key.clone().into(), c.value).is_some() { + bail!(&c.key => "duplicate '{}' in schema", c.key); + } + } + elems + }, }) } } -/// The `Null` schema. -struct SchemaItemNull {} +impl JSONObject { + fn span(&self) -> Span { + self.brace_token.span + } -impl ToSchema for SchemaItemNull { - fn to_schema(&self, _ts: &mut TokenStream) -> Result<(), Error> { - Ok(()) + fn remove(&mut self, name: &str) -> Option { + self.elements.remove(name) + } + + fn remove_required_element(&mut self, name: &str) -> Result { + self.remove(name) + .ok_or_else(|| format_err!(self.span(), "missing required element: {}", name)) } } -impl Parse for SchemaItemNull { - fn parse(_input: ParseStream) -> syn::Result { - Ok(Self {}) +impl IntoIterator for JSONObject { + type Item = as IntoIterator>::Item; + type IntoIter = as IntoIterator>::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.elements.into_iter() } } -/// A default value. Similar to the default keys in objects, this is an example of a different -/// syntax instead of the builder pattern. -/// -/// ```ignore -/// String("Something", default: "The default value") -/// ``` -/// -/// instead of: -/// -/// ```ignore -/// String("Something").default("The default value") -/// ``` -struct DefaultValue { - pub default_token: Token![default], - pub colon: Token![:], - pub value: syn::Expr, +/// An element in a json style map. +struct JSONMapEntry { + pub key: SimpleIdent, + pub colon_token: Token![:], + pub value: JSONValue, } -impl ToSchema for DefaultValue { - fn to_schema(&self, _ts: &mut TokenStream) -> Result<(), Error> { - Ok(()) - } - - fn add_constraints(&self, ts: &mut TokenStream) -> Result<(), Error> { - let value = &self.value; - ts.extend(quote! { .default(#value) }); - Ok(()) - } -} - -impl Parse for DefaultValue { +impl Parse for JSONMapEntry { fn parse(input: ParseStream) -> syn::Result { Ok(Self { - default_token: input.parse()?, - colon: input.parse()?, + key: input.parse()?, + colon_token: input.parse()?, value: input.parse()?, }) } } -macro_rules! try_parse_default_value { - ($input:expr) => {{ - let input = $input; - let lookahead = input.lookahead1(); - if lookahead.peek(Token![default]) { - Some(input.parse()?) - } else { - None - } - }}; +/// The main `Schema` type. +/// +/// We have 2 fixed keys: `type` and `description`. The remaining keys depend on the `type`. +/// Generally, we create the following mapping: +/// +/// ```text +/// { +/// type: Object, +/// description: "text", +/// foo: bar, // "unknown", will be added as a builder-pattern method +/// elements: { ... } +/// } +/// ``` +/// +/// to: +/// +/// ```text +/// { +/// ObjectSchema::new("text", &[ ... ]).foo(bar) +/// } +/// ``` +struct Schema { + span: Span, + + /// Common in all schema entry types: + description: Option, + + /// The specific schema type (Object, String, ...) + item: SchemaItem, + + /// The remaining key-value pairs the `SchemaItem` parser did not extract will be appended as + /// builder-pattern method calls to this schema. + properties: Vec<(Ident, syn::Expr)>, } -/// A boolean schema entry. -struct SchemaItemBoolean { - pub default_value: Option, -} - -impl ToSchema for SchemaItemBoolean { - fn to_schema(&self, _ts: &mut TokenStream) -> Result<(), Error> { - Ok(()) - } - - fn add_constraints(&self, ts: &mut TokenStream) -> Result<(), Error> { - if let Some(def) = &self.default_value { - def.add_constraints(ts)?; - } - Ok(()) - } -} - -impl Parse for SchemaItemBoolean { +/// We parse this in 2 steps: first we parse a `JSONValue`, then we "parse" that further. +impl Parse for Schema { fn parse(input: ParseStream) -> syn::Result { + let obj: JSONObject = input.parse()?; + Self::try_from(obj) + } +} + +/// Shortcut: +impl TryFrom for Schema { + type Error = syn::Error; + + fn try_from(value: JSONValue) -> Result { + Self::try_from(value.into_object("a schema definition")?) + } +} + +/// To go from a `JSONObject` to a `Schema` we first extract the description, as it is a common +/// element in all schema entries, then we parse the specific `SchemaItem`, and collect all the +/// remaining "unused" keys as "constraints"/"properties" which will be appended as builder-pattern +/// method calls when translating the object to a schema definition. +impl TryFrom for Schema { + type Error = syn::Error; + + fn try_from(mut obj: JSONObject) -> Result { + let description = obj + .remove("description") + .map(|v| v.try_into()) + .transpose()?; + Ok(Self { - default_value: try_parse_default_value!(input), + span: obj.brace_token.span, + description, + item: SchemaItem::try_extract_from(&mut obj)?, + properties: obj.into_iter().try_fold( + Vec::new(), + |mut properties, (key, value)| -> Result<_, syn::Error> { + properties.push((Ident::from(key), value.try_into()?)); + Ok(properties) + }, + )?, }) } } -/// An integer schema entry. -struct SchemaItemInteger { - pub default_value: Option, -} +impl Schema { + fn to_schema(&self, ts: &mut TokenStream) -> Result<(), Error> { + // First defer to the SchemaItem's `.to_schema()` method: + let description = self + .description + .as_ref() + .ok_or_else(|| format_err!(self.span, "missing description"))?; + + self.item.to_schema(ts, description)?; + + // Then append all the remaining builder-pattern properties: + for prop in self.properties.iter() { + let key = &prop.0; + let value = &prop.1; + ts.extend(quote! { .#key(#value) }); + } -impl ToSchema for SchemaItemInteger { - fn to_schema(&self, _ts: &mut TokenStream) -> Result<(), Error> { Ok(()) } +} - fn add_constraints(&self, ts: &mut TokenStream) -> Result<(), Error> { - if let Some(def) = &self.default_value { - def.add_constraints(ts)?; +enum SchemaItem { + Null, + Boolean, + Integer, + String, + Object(SchemaObject), + Array(SchemaArray), +} + +impl SchemaItem { + fn try_extract_from(obj: &mut JSONObject) -> Result { + match SimpleIdent::try_from(obj.remove_required_element("type")?)?.as_str() { + "Null" => Ok(SchemaItem::Null), + "Boolean" => Ok(SchemaItem::Boolean), + "Integer" => Ok(SchemaItem::Integer), + "String" => Ok(SchemaItem::String), + "Object" => Ok(SchemaItem::Object(SchemaObject::try_extract_from(obj)?)), + "Array" => Ok(SchemaItem::Array(SchemaArray::try_extract_from(obj)?)), + ty => bail!(obj.span(), "unknown type name '{}'", ty), + } + } + + fn to_schema(&self, ts: &mut TokenStream, description: &syn::LitStr) -> Result<(), Error> { + ts.extend(quote! { ::proxmox::api::schema }); + match self { + SchemaItem::Null => ts.extend(quote! { ::NullSchema::new(#description) }), + SchemaItem::Boolean => ts.extend(quote! { ::BooleanSchema::new(#description) }), + SchemaItem::Integer => ts.extend(quote! { ::IntegerSchema::new(#description) }), + SchemaItem::String => ts.extend(quote! { ::StringSchema::new(#description) }), + SchemaItem::Object(obj) => { + let mut elems = TokenStream::new(); + obj.to_schema_inner(&mut elems)?; + ts.extend(quote! { ::ObjectSchema::new(#description, &[#elems]) }) + } + SchemaItem::Array(array) => { + let mut items = TokenStream::new(); + array.to_schema_inner(&mut items)?; + ts.extend(quote! { ::ArraySchema::new(#description, &#items.schema()) }) + } } Ok(()) } } -impl Parse for SchemaItemInteger { - fn parse(input: ParseStream) -> syn::Result { +/// Contains a sorted list of elements: +struct SchemaObject { + elements: Vec<(String, bool, Schema)>, +} + +impl SchemaObject { + fn try_extract_from(obj: &mut JSONObject) -> Result { Ok(Self { - default_value: try_parse_default_value!(input), + elements: obj + .remove_required_element("elements")? + .into_object("object field definition")? + .into_iter() + .try_fold( + Vec::new(), + |mut elements, (key, value)| -> Result<_, syn::Error> { + let mut schema: JSONObject = + value.into_object("schema definition for field")?; + let optional: bool = schema + .remove("optional") + .map(|opt| -> Result { + let v: syn::LitBool = opt.try_into()?; + Ok(v.value) + }) + .transpose()? + .unwrap_or(false); + elements.push((key.to_string(), optional, schema.try_into()?)); + Ok(elements) + }, + ) + // This must be kept sorted! + .map(|mut elements| { + elements.sort_by(|a, b| (a.0).cmp(&b.0)); + elements + })?, }) } -} -/// An string schema entry. -struct SchemaItemString { - pub default_value: Option, -} - -impl ToSchema for SchemaItemString { - fn to_schema(&self, _ts: &mut TokenStream) -> Result<(), Error> { - Ok(()) - } - - fn add_constraints(&self, ts: &mut TokenStream) -> Result<(), Error> { - if let Some(def) = &self.default_value { - def.add_constraints(ts)?; + fn to_schema_inner(&self, ts: &mut TokenStream) -> Result<(), Error> { + for element in self.elements.iter() { + let key = &element.0; + let optional = element.1; + let mut schema = TokenStream::new(); + element.2.to_schema(&mut schema)?; + ts.extend(quote! { (#key, #optional, &#schema.schema()), }); } Ok(()) } } -impl Parse for SchemaItemString { - fn parse(input: ParseStream) -> syn::Result { +struct SchemaArray { + item: Box, +} + +impl SchemaArray { + fn try_extract_from(obj: &mut JSONObject) -> Result { Ok(Self { - default_value: try_parse_default_value!(input), + item: Box::new(obj.remove_required_element("items")?.try_into()?), }) } + + fn to_schema_inner(&self, ts: &mut TokenStream) -> Result<(), Error> { + self.item.to_schema(ts) + } } /// We get macro attributes like `#[input(THIS)]` with the parenthesis around `THIS` included. diff --git a/proxmox-api-macro/src/lib.rs b/proxmox-api-macro/src/lib.rs index e4060c0b..2ac4b93b 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 util; fn handle_error(mut item: TokenStream, data: Result) -> TokenStream { match data { @@ -70,15 +71,38 @@ fn router_do(item: TokenStream) -> Result { use serde_json::Value; #[api] - #[input(Object({ - "username": String("User name.").max_length(64), - "password": String("The secret password or a valid ticket."), - }))] - #[returns(Object("Returns a ticket", { - "username": String("User name."), - "ticket": String("Auth ticket."), - "CSRFPreventionToken": String("Cross Site Request Forgerty Prevention Token."), - }))] + #[input({ + type: Object, + elements: { + username: { + type: String, + description: "User name", + max_length: 64, + }, + password: { + type: String, + description: "The secret password or a valid ticket.", + }, + } + })] + #[returns({ + type: Object, + description: "Returns a ticket", + elements: { + "username": { + type: String, + description: "User name.", + }, + "ticket": { + type: String, + description: "Auth ticket.", + }, + "CSRFPreventionToken": { + type: String, + description: "Cross Site Request Forgerty Prevention Token.", + }, + }, + })] /// Create or verify authentication ticket. /// /// Returns: ... diff --git a/proxmox-api-macro/src/util.rs b/proxmox-api-macro/src/util.rs index e7cb7939..83bc5750 100644 --- a/proxmox-api-macro/src/util.rs +++ b/proxmox-api-macro/src/util.rs @@ -1,86 +1,104 @@ -use proc_macro2::Ident; +use std::borrow::Borrow; +use std::fmt; +use proc_macro2::{Ident, TokenStream}; use syn::parse::{Parse, ParseStream}; -use syn::punctuated::Punctuated; -use syn::{parenthesized, Token}; +use syn::Token; -macro_rules! c_format_err { - ($span:expr => $($msg:tt)*) => { syn::Error::new_spanned($span, format!($($msg)*)) }; - ($span:expr, $($msg:tt)*) => { syn::Error::new($span, format!($($msg)*)) }; -} +/// A more relaxed version of Ident which allows hyphens. +#[derive(Clone, Debug)] +pub struct SimpleIdent(Ident, String); -macro_rules! c_bail { - ($span:expr => $($msg:tt)*) => { return Err(c_format_err!($span => $($msg)*).into()) }; - ($span:expr, $($msg:tt)*) => { return Err(c_format_err!($span, $($msg)*).into()) }; -} +impl SimpleIdent { + //pub fn new(name: String, span: Span) -> Self { + // Self(Ident::new(&name, span), name) + //} -/// Convert `this_kind_of_text` to `ThisKindOfText`. -pub fn to_camel_case(text: &str) -> String { - let mut out = String::new(); - - let mut capitalize = true; - for c in text.chars() { - if c == '_' { - capitalize = true; - } else if capitalize { - out.extend(c.to_uppercase()); - capitalize = false; - } else { - out.push(c); - } + #[inline] + pub fn as_str(&self) -> &str { + &self.1 } - out + //#[inline] + //pub fn span(&self) -> Span { + // self.0.span() + //} } -/// Convert `ThisKindOfText` to `this_kind_of_text`. -pub fn to_underscore_case(text: &str) -> String { - let mut out = String::new(); +impl Eq for SimpleIdent {} - for c in text.chars() { - if c.is_uppercase() { - if !out.is_empty() { - out.push('_'); - } - out.extend(c.to_lowercase()); - } else { - out.push(c); - } +impl PartialEq for SimpleIdent { + fn eq(&self, other: &Self) -> bool { + self.1 == other.1 } - - out } -pub struct ApiAttr { - pub paren_token: syn::token::Paren, - pub items: Punctuated, +impl std::hash::Hash for SimpleIdent { + fn hash(&self, state: &mut H) { + std::hash::Hash::hash(&self.1, state) + } } -impl Parse for ApiAttr { +impl From for SimpleIdent { + fn from(ident: Ident) -> Self { + let s = ident.to_string(); + Self(ident, s) + } +} + +impl From for Ident { + fn from(this: SimpleIdent) -> Ident { + this.0 + } +} + +impl fmt::Display for SimpleIdent { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Display::fmt(&self.0, f) + } +} + +impl std::ops::Deref for SimpleIdent { + type Target = Ident; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::ops::DerefMut for SimpleIdent { + #[inline] + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl Borrow for SimpleIdent { + #[inline] + fn borrow(&self) -> &str { + self.as_str() + } +} + +impl quote::ToTokens for SimpleIdent { + fn to_tokens(&self, tokens: &mut TokenStream) { + self.0.to_tokens(tokens) + } +} + +/// Note that the 'type' keyword is handled separately in `syn`. It's not an `Ident`: +impl Parse for SimpleIdent { fn parse(input: ParseStream) -> syn::Result { - let content; - #[allow(clippy::eval_order_dependence)] - Ok(ApiAttr { - paren_token: parenthesized!(content in input), - items: content.parse_terminated(ApiItem::parse)?, - }) - } -} - -pub enum ApiItem { - Rename(syn::LitStr), -} - -impl Parse for ApiItem { - fn parse(input: ParseStream) -> syn::Result { - let what: Ident = input.parse()?; - let what_str = what.to_string(); - match what_str.as_str() { - "rename" => { - let _: Token![=] = input.parse()?; - Ok(ApiItem::Rename(input.parse()?)) - } - _ => c_bail!(what => "unrecognized api attribute: {}", what_str), - } + let lookahead = input.lookahead1(); + Ok(Self::from(if lookahead.peek(Token![type]) { + let ty: Token![type] = input.parse()?; + Ident::new("type", ty.span) + } else if lookahead.peek(syn::LitStr) { + let s: syn::LitStr = input.parse()?; + Ident::new(&s.value(), s.span()) + } else { + input.parse()? + })) } } diff --git a/proxmox-api-macro/tests/api1.rs b/proxmox-api-macro/tests/api1.rs index 37feb662..fa9e9355 100644 --- a/proxmox-api-macro/tests/api1.rs +++ b/proxmox-api-macro/tests/api1.rs @@ -7,17 +7,38 @@ use failure::Error; use serde_json::Value; #[api] -#[input(Object(default: "test", { - "username": String("User name.").max_length(64), - "password": String("The secret password or a valid ticket."), - optional "test": Integer("What?", default: 3), - "data": Array("Some Integers", Integer("Some Thing").maximum(4)), -}))] -#[returns(Object("Returns a ticket", { - "username": String("User name."), - "ticket": String("Auth ticket."), - "CSRFPreventionToken": String("Cross Site Request Forgerty Prevention Token."), -}))] +#[input({ + type: Object, + elements: { + username: { + type: String, + description: "User name", + max_length: 64, + }, + password: { + type: String, + description: "The secret password or a valid ticket.", + }, + } +})] +#[returns({ + type: Object, + description: "Returns a ticket", + elements: { + "username": { + type: String, + description: "User name.", + }, + "ticket": { + type: String, + description: "Auth ticket.", + }, + "CSRFPreventionToken": { + type: String, + description: "Cross Site Request Forgerty Prevention Token.", + }, + }, +})] /// Create or verify authentication ticket. /// /// Returns: ...