mirror of
https://git.proxmox.com/git/proxmox
synced 2025-08-02 12:33:30 +00:00
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:
parent
2fc2df9a78
commit
4f042f8133
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
32
proxmox-api-macro/tests/types.rs
Normal file
32
proxmox-api-macro/tests/types.rs
Normal 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")
|
||||
}
|
Loading…
Reference in New Issue
Block a user