mirror of
https://git.proxmox.com/git/proxmox
synced 2025-08-06 23:23:20 +00:00
import a first draft of api macros
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
This commit is contained in:
parent
e06151b90e
commit
5721d21a5e
625
proxmox-api-macro/src/api.rs
Normal file
625
proxmox-api-macro/src/api.rs
Normal file
@ -0,0 +1,625 @@
|
|||||||
|
extern crate proc_macro;
|
||||||
|
extern crate proc_macro2;
|
||||||
|
|
||||||
|
use std::mem;
|
||||||
|
|
||||||
|
use failure::Error;
|
||||||
|
|
||||||
|
use proc_macro2::{Span, TokenStream};
|
||||||
|
use quote::{quote, quote_spanned};
|
||||||
|
use syn::parse::{Parse, ParseStream};
|
||||||
|
use syn::punctuated::Punctuated;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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<syn::LitStr>,
|
||||||
|
pub comma_token: Option<Token![,]>,
|
||||||
|
pub item: SchemaItem,
|
||||||
|
pub constraints: Vec<syn::ExprCall>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Self> {
|
||||||
|
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::<syn::ExprCall>::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> {
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
default_token: input.parse()?,
|
||||||
|
colon: input.parse()?,
|
||||||
|
key_name: input.parse()?,
|
||||||
|
comma_token: input.parse()?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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<DefaultKey>,
|
||||||
|
pub brace_token: syn::token::Brace,
|
||||||
|
pub elements: Punctuated<ObjectElement, Token![,]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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![, ]);
|
||||||
|
}
|
||||||
|
|
||||||
|
element.to_schema(&mut elem_ts)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parse for SchemaItemObject {
|
||||||
|
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||||
|
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<token::optional>,
|
||||||
|
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)
|
||||||
|
} else {
|
||||||
|
quote!(false)
|
||||||
|
};
|
||||||
|
|
||||||
|
ts.extend(quote! {
|
||||||
|
(#name, #optional, & #schema .schema())
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parse for ObjectElement {
|
||||||
|
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
optional: input.parse()?,
|
||||||
|
field_name: input.parse()?,
|
||||||
|
colon: input.parse()?,
|
||||||
|
item: input.parse()?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Array schemas simply contain their inner type.
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// Array("Some data", Integer("A data element"))
|
||||||
|
/// ```
|
||||||
|
struct SchemaItemArray {
|
||||||
|
pub item_schema: Box<Schema>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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 Parse for SchemaItemArray {
|
||||||
|
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
item_schema: Box::new(input.parse()?),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The `Null` schema.
|
||||||
|
struct SchemaItemNull {}
|
||||||
|
|
||||||
|
impl ToSchema for SchemaItemNull {
|
||||||
|
fn to_schema(&self, _ts: &mut TokenStream) -> Result<(), Error> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parse for SchemaItemNull {
|
||||||
|
fn parse(_input: ParseStream) -> syn::Result<Self> {
|
||||||
|
Ok(Self {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
default_token: input.parse()?,
|
||||||
|
colon: 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
|
||||||
|
}
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A boolean schema entry.
|
||||||
|
struct SchemaItemBoolean {
|
||||||
|
pub default_value: Option<DefaultValue>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
default_value: try_parse_default_value!(input),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An integer schema entry.
|
||||||
|
struct SchemaItemInteger {
|
||||||
|
pub default_value: Option<DefaultValue>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parse for SchemaItemInteger {
|
||||||
|
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
default_value: try_parse_default_value!(input),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An string schema entry.
|
||||||
|
struct SchemaItemString {
|
||||||
|
pub default_value: Option<DefaultValue>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parse for SchemaItemString {
|
||||||
|
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
default_value: try_parse_default_value!(input),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// We get macro attributes like `#[input(THIS)]` with the parenthesis around `THIS` included.
|
||||||
|
struct Parenthesized<T: Parse> {
|
||||||
|
pub token: syn::token::Paren,
|
||||||
|
pub content: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Parse> Parse for Parenthesized<T> {
|
||||||
|
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||||
|
let content;
|
||||||
|
Ok(Self {
|
||||||
|
token: parenthesized!(content in input),
|
||||||
|
content: content.parse()?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// We get macro attributes like `#[doc = "TEXT"]` with the `=` included.
|
||||||
|
struct BareAssignment<T: Parse> {
|
||||||
|
pub token: Token![=],
|
||||||
|
pub content: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Parse> Parse for BareAssignment<T> {
|
||||||
|
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
token: input.parse()?,
|
||||||
|
content: input.parse()?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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(crate) fn api(_attr: TokenStream, item: TokenStream) -> Result<TokenStream, Error> {
|
||||||
|
let mut func: syn::ItemFn = syn::parse2(item)?;
|
||||||
|
|
||||||
|
let sig_span = func.sig.span();
|
||||||
|
|
||||||
|
let mut protected = false;
|
||||||
|
|
||||||
|
let mut input_schema = None;
|
||||||
|
let mut returns_schema = None;
|
||||||
|
let mut doc_comment = String::new();
|
||||||
|
let doc_span = Span::call_site(); // FIXME: set to first doc comment
|
||||||
|
for attr in mem::replace(&mut func.attrs, Vec::new()) {
|
||||||
|
// don't mess with #![...]
|
||||||
|
if let syn::AttrStyle::Inner(_) = &attr.style {
|
||||||
|
func.attrs.push(attr);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if attr.path.is_ident("doc") {
|
||||||
|
let doc: BareAssignment<syn::LitStr> = syn::parse2(attr.tokens.clone())?;
|
||||||
|
doc_comment.push_str(&doc.content.value());
|
||||||
|
func.attrs.push(attr);
|
||||||
|
} else if attr.path.is_ident("input") {
|
||||||
|
let input: Parenthesized<Schema> = syn::parse2(attr.tokens)?;
|
||||||
|
input_schema = Some(input.content);
|
||||||
|
} else if attr.path.is_ident("returns") {
|
||||||
|
let input: Parenthesized<Schema> = syn::parse2(attr.tokens)?;
|
||||||
|
returns_schema = Some(input.content);
|
||||||
|
} else if attr.path.is_ident("protected") {
|
||||||
|
if attr.tokens.is_empty() {
|
||||||
|
protected = true;
|
||||||
|
} else {
|
||||||
|
let value: Parenthesized<syn::LitBool> = syn::parse2(attr.tokens)?;
|
||||||
|
protected = value.content.value;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
func.attrs.push(attr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut input_schema =
|
||||||
|
input_schema.ok_or_else(|| format_err!(sig_span, "missing input schema"))?;
|
||||||
|
|
||||||
|
if input_schema.description.is_none() {
|
||||||
|
input_schema.description = Some(syn::LitStr::new(&doc_comment, doc_span));
|
||||||
|
}
|
||||||
|
|
||||||
|
let input_schema = {
|
||||||
|
let mut ts = TokenStream::new();
|
||||||
|
input_schema.to_schema(&mut ts)?;
|
||||||
|
ts
|
||||||
|
};
|
||||||
|
|
||||||
|
let returns_schema =
|
||||||
|
returns_schema.ok_or_else(|| format_err!(sig_span, "missing returns schema"))?;
|
||||||
|
|
||||||
|
let returns_schema = {
|
||||||
|
let mut ts = TokenStream::new();
|
||||||
|
returns_schema.to_schema(&mut ts)?;
|
||||||
|
ts
|
||||||
|
};
|
||||||
|
|
||||||
|
let vis = &func.vis;
|
||||||
|
let func_name = &func.sig.ident;
|
||||||
|
let api_method_name = Ident::new(
|
||||||
|
&format!("API_METHOD_{}", func_name.to_string().to_uppercase()),
|
||||||
|
func.sig.ident.span(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(quote_spanned! { sig_span =>
|
||||||
|
#vis const #api_method_name: ::proxmox::api::ApiMethod =
|
||||||
|
::proxmox::api::ApiMethod::new(
|
||||||
|
&::proxmox::api::ApiHandler::Sync(&#func_name),
|
||||||
|
&#input_schema,
|
||||||
|
)
|
||||||
|
.returns(& #returns_schema .schema())
|
||||||
|
.protected(#protected);
|
||||||
|
#func
|
||||||
|
})
|
||||||
|
//Ok(quote::quote!(#func))
|
||||||
|
}
|
@ -1 +1,129 @@
|
|||||||
#![recursion_limit = "256"]
|
#![recursion_limit = "256"]
|
||||||
|
|
||||||
|
extern crate proc_macro;
|
||||||
|
extern crate proc_macro2;
|
||||||
|
|
||||||
|
use failure::Error;
|
||||||
|
|
||||||
|
use proc_macro::TokenStream as TokenStream_1;
|
||||||
|
use proc_macro2::TokenStream;
|
||||||
|
|
||||||
|
macro_rules! format_err {
|
||||||
|
($span:expr => $($msg:tt)*) => { syn::Error::new_spanned($span, format!($($msg)*)) };
|
||||||
|
($span:expr, $($msg:tt)*) => { syn::Error::new($span, format!($($msg)*)) };
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! bail {
|
||||||
|
($span:expr => $($msg:tt)*) => { return Err(format_err!($span => $($msg)*).into()) };
|
||||||
|
($span:expr, $($msg:tt)*) => { return Err(format_err!($span, $($msg)*).into()) };
|
||||||
|
}
|
||||||
|
|
||||||
|
mod api;
|
||||||
|
|
||||||
|
fn handle_error(mut item: TokenStream, data: Result<TokenStream, Error>) -> TokenStream {
|
||||||
|
match data {
|
||||||
|
Ok(output) => output,
|
||||||
|
Err(err) => match err.downcast::<syn::Error>() {
|
||||||
|
Ok(err) => {
|
||||||
|
item.extend(err.to_compile_error());
|
||||||
|
item
|
||||||
|
}
|
||||||
|
Err(err) => panic!("error in api/router macro: {}", err),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Macro for building a Router:
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// router! {
|
||||||
|
/// pub const ROUTER = {
|
||||||
|
/// "access": {
|
||||||
|
/// "ticket": {
|
||||||
|
/// post = create_ticket,
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// };
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// #[api]
|
||||||
|
/// fn create_ticket(param: Value) -> Result<Value, Error> { ... }
|
||||||
|
/// ```
|
||||||
|
#[proc_macro]
|
||||||
|
pub fn router(item: TokenStream_1) -> TokenStream_1 {
|
||||||
|
let item: TokenStream = item.into();
|
||||||
|
handle_error(item.clone(), router_do(item)).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn router_do(item: TokenStream) -> Result<TokenStream, Error> {
|
||||||
|
Ok(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Macro for building an API method:
|
||||||
|
|
||||||
|
```
|
||||||
|
# use proxmox_api_macro::api;
|
||||||
|
# use proxmox::api::{ApiMethod, RpcEnvironment};
|
||||||
|
|
||||||
|
use failure::Error;
|
||||||
|
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."),
|
||||||
|
}))]
|
||||||
|
/// Create or verify authentication ticket.
|
||||||
|
///
|
||||||
|
/// Returns: ...
|
||||||
|
fn create_ticket(
|
||||||
|
_param: Value,
|
||||||
|
_info: &ApiMethod,
|
||||||
|
_rpcenv: &mut dyn RpcEnvironment,
|
||||||
|
) -> Result<Value, Error> {
|
||||||
|
panic!("implement me");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The above code expands to:
|
||||||
|
|
||||||
|
```ignore
|
||||||
|
const API_METHOD_CREATE_TICKET: ApiMethod =
|
||||||
|
ApiMethod::new(
|
||||||
|
&ApiHandler::Sync(&create_ticket),
|
||||||
|
&ObjectSchema::new(
|
||||||
|
"Create or verify authentication ticket",
|
||||||
|
&[ // Sorted:
|
||||||
|
("password", false, &StringSchema::new("The secret password or a valid ticket.")
|
||||||
|
.schema()),
|
||||||
|
("username", false, &StringSchema::new("User name.")
|
||||||
|
.max_length(64)
|
||||||
|
.schema()),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.returns(
|
||||||
|
&ObjectSchema::new(
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.protected(false);
|
||||||
|
fn create_ticket(
|
||||||
|
param: Value,
|
||||||
|
info: &ApiMethod,
|
||||||
|
rpcenv: &mut dyn RpcEnvironment,
|
||||||
|
) -> Result<Value, Error> {
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
*/
|
||||||
|
#[proc_macro_attribute]
|
||||||
|
pub fn api(attr: TokenStream_1, item: TokenStream_1) -> TokenStream_1 {
|
||||||
|
let item: TokenStream = item.into();
|
||||||
|
handle_error(item.clone(), api::api(attr.into(), item)).into()
|
||||||
|
}
|
||||||
|
30
proxmox-api-macro/tests/api1.rs
Normal file
30
proxmox-api-macro/tests/api1.rs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
|
use proxmox::api::{ApiMethod, RpcEnvironment};
|
||||||
|
use proxmox_api_macro::api;
|
||||||
|
|
||||||
|
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."),
|
||||||
|
}))]
|
||||||
|
/// Create or verify authentication ticket.
|
||||||
|
///
|
||||||
|
/// Returns: ...
|
||||||
|
fn create_ticket(
|
||||||
|
_param: Value,
|
||||||
|
_info: &ApiMethod,
|
||||||
|
_rpcenv: &mut dyn RpcEnvironment,
|
||||||
|
) -> Result<Value, Error> {
|
||||||
|
panic!("implement me");
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user