api-macro: support external types

See the test example:

assuming a `pub struct Foo` which implements `Serialize` and
`Deserialize`, we also expect it to provide a
`pub const Foo::API_SCHEMA: &Schema` like so:

    #[derive(Deserialize, Serialize)]
    pub struct StrongString(String);
    impl StrongString {
        pub const API_SCHEMA: &'static Schema =
            &StringSchema::new("Some generic string")
                .format(&ApiStringFormat::Enum(&["a", "b"]))
                .schema();
    }

Then we can use:

    #[api(
        input: {
            properties: {
                arg: { type: StrongString },
            }
        },
        ...
    )]
    fn my_api_func(arg: StrongString) -> Result<...> {
        ...
    }

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
This commit is contained in:
Wolfgang Bumiller 2019-11-28 10:42:34 +01:00
parent 2fc2df9a78
commit 4f042f8133
4 changed files with 176 additions and 45 deletions

View File

@ -3,11 +3,12 @@ use std::convert::{TryFrom, TryInto};
use failure::Error;
use proc_macro2::{Span, TokenStream};
use quote::quote;
use quote::{quote, quote_spanned};
use syn::parse::{Parse, ParseStream, Parser};
use syn::Ident;
use syn::spanned::Spanned;
use syn::{ExprPath, Ident};
use crate::util::{JSONObject, JSONValue, SimpleIdent};
use crate::util::{JSONObject, JSONValue};
mod method;
@ -92,23 +93,24 @@ impl TryFrom<JSONObject> for Schema {
}
impl Schema {
fn to_typed_schema(&self, ts: &mut TokenStream) -> Result<(), Error> {
self.item.to_schema(
ts,
self.description.as_ref(),
&self.span,
&self.properties,
true,
)
}
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) });
}
Ok(())
self.item.to_schema(
ts,
self.description.as_ref(),
&self.span,
&self.properties,
false,
)
}
fn as_object(&self) -> Option<&SchemaObject> {
@ -130,53 +132,138 @@ enum SchemaItem {
String,
Object(SchemaObject),
Array(SchemaArray),
ExternType(ExprPath),
}
impl SchemaItem {
/// If there's a `type` specified, parse it as that type. Otherwise check for keys which
/// uniqueply identify the type, such as "properties" for type `Object`.
fn try_extract_from(obj: &mut JSONObject) -> Result<Self, syn::Error> {
let ty = obj.remove("type").map(SimpleIdent::try_from).transpose()?;
let ty = match &ty {
Some(ty) => ty.as_str(),
let ty = obj.remove("type").map(ExprPath::try_from).transpose()?;
let ty = match ty {
Some(ty) => ty,
None => {
if obj.contains_key("properties") {
"Object"
return Ok(SchemaItem::Object(SchemaObject::try_extract_from(obj)?));
} else if obj.contains_key("items") {
"Array"
return Ok(SchemaItem::Array(SchemaArray::try_extract_from(obj)?));
} else {
bail!(obj.span(), "failed to guess 'type' in schema definition");
}
}
};
match ty {
"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),
if !ty.attrs.is_empty() {
bail!(ty => "unexpected attributes on type path");
}
if ty.qself.is_some() || ty.path.segments.len() != 1 {
return Ok(SchemaItem::ExternType(ty));
}
let name = &ty
.path
.segments
.first()
.ok_or_else(|| format_err!(&ty.path => "invalid empty path"))?
.ident;
if name == "Null" {
Ok(SchemaItem::Null)
} else if name == "Boolean" {
Ok(SchemaItem::Boolean)
} else if name == "Integer" {
Ok(SchemaItem::Integer)
} else if name == "String" {
Ok(SchemaItem::String)
} else if name == "Object" {
Ok(SchemaItem::Object(SchemaObject::try_extract_from(obj)?))
} else if name == "Array" {
Ok(SchemaItem::Array(SchemaArray::try_extract_from(obj)?))
} else {
Ok(SchemaItem::ExternType(ty))
}
}
fn to_schema(&self, ts: &mut TokenStream, description: &syn::LitStr) -> Result<(), Error> {
ts.extend(quote! { ::proxmox::api::schema });
fn to_inner_schema(
&self,
ts: &mut TokenStream,
description: Option<&syn::LitStr>,
span: &Span,
properties: &[(Ident, syn::Expr)],
) -> Result<bool, Error> {
let description = description.ok_or_else(|| format_err!(*span, "missing description"));
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::Null => {
let description = description?;
ts.extend(quote! { ::proxmox::api::schema::NullSchema::new(#description) });
}
SchemaItem::Boolean => {
let description = description?;
ts.extend(quote! { ::proxmox::api::schema::BooleanSchema::new(#description) });
}
SchemaItem::Integer => {
let description = description?;
ts.extend(quote! { ::proxmox::api::schema::IntegerSchema::new(#description) });
}
SchemaItem::String => {
let description = description?;
ts.extend(quote! { ::proxmox::api::schema::StringSchema::new(#description) });
}
SchemaItem::Object(obj) => {
let description = description?;
let mut elems = TokenStream::new();
obj.to_schema_inner(&mut elems)?;
ts.extend(quote! { ::ObjectSchema::new(#description, &[#elems]) })
ts.extend(
quote! { ::proxmox::api::schema::ObjectSchema::new(#description, &[#elems]) },
);
}
SchemaItem::Array(array) => {
let description = description?;
let mut items = TokenStream::new();
array.to_schema_inner(&mut items)?;
ts.extend(quote! { ::ArraySchema::new(#description, &#items.schema()) })
array.to_schema(&mut items)?;
ts.extend(quote! {
::proxmox::api::schema::ArraySchema::new(#description, #items)
});
}
SchemaItem::ExternType(path) => {
if !properties.is_empty() {
bail!(&properties[0].0 => "additional properties not allowed on external type");
}
ts.extend(quote_spanned! { path.span() => #path::API_SCHEMA });
return Ok(true);
}
}
// Then append all the remaining builder-pattern properties:
for prop in properties {
let key = &prop.0;
let value = &prop.1;
ts.extend(quote! { .#key(#value) });
}
Ok(false)
}
fn to_schema(
&self,
ts: &mut TokenStream,
description: Option<&syn::LitStr>,
span: &Span,
properties: &[(Ident, syn::Expr)],
typed: bool,
) -> Result<(), Error> {
if typed {
let _: bool = self.to_inner_schema(ts, description, span, properties)?;
return Ok(());
}
let mut inner_ts = TokenStream::new();
if self.to_inner_schema(&mut inner_ts, description, span, properties)? {
ts.extend(inner_ts);
} else {
ts.extend(quote! { & #inner_ts .schema() });
}
Ok(())
}
@ -225,7 +312,7 @@ impl SchemaObject {
let optional = element.1;
let mut schema = TokenStream::new();
element.2.to_schema(&mut schema)?;
ts.extend(quote! { (#key, #optional, &#schema.schema()), });
ts.extend(quote! { (#key, #optional, #schema), });
}
Ok(())
}
@ -252,7 +339,7 @@ impl SchemaArray {
})
}
fn to_schema_inner(&self, ts: &mut TokenStream) -> Result<(), Error> {
fn to_schema(&self, ts: &mut TokenStream) -> Result<(), Error> {
self.item.to_schema(ts)
}
}

View File

@ -44,7 +44,7 @@ pub fn handle_method(mut attribs: JSONObject, mut func: syn::ItemFn) -> Result<T
let input_schema = {
let mut ts = TokenStream::new();
input_schema.to_schema(&mut ts)?;
input_schema.to_typed_schema(&mut ts)?;
ts
};
@ -67,7 +67,7 @@ pub fn handle_method(mut attribs: JSONObject, mut func: syn::ItemFn) -> Result<T
&::proxmox::api::ApiHandler::Sync(&#api_func_name),
&#input_schema,
)
.returns(& #returns_schema .schema())
.returns(#returns_schema)
.protected(#protected);
#wrapper_ts
#func

View File

@ -208,6 +208,18 @@ impl TryFrom<JSONValue> for SimpleIdent {
}
}
/// Expect a json value to be a path. This means it's supposed to be an expression which evaluates
/// to a path.
impl TryFrom<JSONValue> for syn::ExprPath {
type Error = syn::Error;
fn try_from(value: JSONValue) -> Result<Self, syn::Error> {
match syn::Expr::try_from(value)? {
syn::Expr::Path(path) => Ok(path),
other => bail!(other => "expected a type path"),
}
}
}
/// Parsing a json value should be simple enough: braces means we have an object, otherwise it must
/// be an "expression".
impl Parse for JSONValue {

View File

@ -0,0 +1,32 @@
//! This should test the usage of "external" types. For any unrecognized schema type we expect the
//! type's impl to provide an `pub const API_SCHEMA: &Schema`.
use proxmox::api::schema;
use proxmox_api_macro::api;
use failure::Error;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct OkString(String);
impl OkString {
pub const API_SCHEMA: &'static schema::Schema = &schema::StringSchema::new("A string")
.format(&schema::ApiStringFormat::Enum(&["ok", "not-ok"]))
.schema();
}
// Initial test:
#[api(
input: {
properties: {
arg: { type: OkString },
}
},
returns: { type: Boolean },
)]
/// Check a string.
///
/// Returns: Whether the string was "ok".
pub fn string_check(arg: OkString) -> Result<bool, Error> {
Ok(arg.0 == "ok")
}