import a first draft of api macros

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
This commit is contained in:
Wolfgang Bumiller 2019-11-22 09:54:29 +01:00
parent e06151b90e
commit 5721d21a5e
3 changed files with 783 additions and 0 deletions

View 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))
}

View File

@ -1 +1,129 @@
#![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()
}

View 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");
}