api-macro: allow inferring some types automatically

non-optional boolean, string and integer types can be
inferred from the function

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
This commit is contained in:
Wolfgang Bumiller 2019-12-02 11:45:37 +01:00
parent 7fd69f8b12
commit d78659a2c0
3 changed files with 113 additions and 15 deletions

View File

@ -14,6 +14,10 @@ mod enums;
mod method;
mod structs;
pub const INTNAMES: &[&'static str] = &[
"Integer", "i8", "i16", "i32", "i64", "isize", "u8", "u16", "u32", "u64", "usize",
];
/// The main `Schema` type.
///
/// We have 2 fixed keys: `type` and `description`. The remaining keys depend on the `type`.
@ -122,9 +126,21 @@ impl Schema {
}
}
fn as_object_mut(&mut self) -> Option<&mut SchemaObject> {
match &mut self.item {
SchemaItem::Object(obj) => Some(obj),
_ => None,
}
}
fn find_object_property(&self, key: &str) -> Option<(bool, &Schema)> {
self.as_object().and_then(|obj| obj.find_property(key))
}
fn find_object_property_mut(&mut self, key: &str) -> Option<(&mut bool, &mut Schema)> {
self.as_object_mut()
.and_then(|obj| obj.find_property_mut(key))
}
}
enum SchemaItem {
@ -135,6 +151,7 @@ enum SchemaItem {
Object(SchemaObject),
Array(SchemaArray),
ExternType(ExprPath),
Inferred(Span),
}
impl SchemaItem {
@ -150,7 +167,7 @@ impl SchemaItem {
} else if obj.contains_key("items") {
return Ok(SchemaItem::Array(SchemaArray::try_extract_from(obj)?));
} else {
bail!(obj.span(), "failed to guess 'type' in schema definition");
return Ok(SchemaItem::Inferred(obj.span()));
}
}
};
@ -170,9 +187,6 @@ impl SchemaItem {
.ok_or_else(|| format_err!(&ty.path => "invalid empty path"))?
.ident;
const INTNAMES: &[&'static str] = &[
"Integer", "i8", "i16", "i32", "i64", "isize", "u8", "u16", "u32", "u64", "usize",
];
if name == "Null" {
Ok(SchemaItem::Null)
} else if name == "Boolean" || name == "bool" {
@ -239,6 +253,9 @@ impl SchemaItem {
ts.extend(quote_spanned! { path.span() => #path::API_SCHEMA });
return Ok(true);
}
SchemaItem::Inferred(span) => {
bail!(*span, "failed to guess 'type' in schema definition");
}
}
// Then append all the remaining builder-pattern properties:
@ -332,6 +349,19 @@ impl SchemaObject {
Err(_) => None,
}
}
fn find_property_mut(&mut self, key: &str) -> Option<(&mut bool, &mut Schema)> {
match self
.properties
.binary_search_by(|prope| prope.0.as_str().cmp(key))
{
Ok(idx) => {
let props = &mut self.properties[idx];
Some((&mut props.1, &mut props.2))
}
Err(_) => None,
}
}
}
struct SchemaArray {

View File

@ -8,7 +8,7 @@ use quote::{quote, quote_spanned};
use syn::spanned::Spanned;
use syn::Ident;
use super::Schema;
use super::{Schema, SchemaItem};
use crate::util::{BareAssignment, JSONObject, SimpleIdent};
/// Parse `input`, `returns` and `protected` attributes out of an function annotated
@ -21,7 +21,7 @@ pub fn handle_method(mut attribs: JSONObject, mut func: syn::ItemFn) -> Result<T
None => Schema {
span: Span::call_site(),
description: None,
item: super::SchemaItem::Object(Default::default()),
item: SchemaItem::Object(Default::default()),
properties: Vec::new(),
},
};
@ -164,6 +164,22 @@ enum ParameterType<'a> {
Other(&'a syn::Type, bool, &'a Schema),
}
fn check_input_type(input: &syn::FnArg) -> Result<(&syn::PatType, &syn::PatIdent), Error> {
// `self` types are not supported:
let pat_type = match input {
syn::FnArg::Receiver(r) => bail!(r => "methods taking a 'self' are not supported"),
syn::FnArg::Typed(pat_type) => pat_type,
};
// Normally function parameters are simple Ident patterns. Anything else is an error.
let pat = match &*pat_type.pat {
syn::Pat::Ident(pat) => pat,
_ => bail!(pat_type => "unsupported parameter type"),
};
Ok((pat_type, pat))
}
fn handle_function_signature(
input_schema: &mut Schema,
returns_schema: &mut Option<Schema>,
@ -183,17 +199,46 @@ fn handle_function_signature(
let mut param_list = Vec::<(SimpleIdent, ParameterType)>::new();
for input in sig.inputs.iter() {
// `self` types are not supported:
let pat_type = match input {
syn::FnArg::Receiver(r) => bail!(r => "methods taking a 'self' are not supported"),
syn::FnArg::Typed(pat_type) => pat_type,
let (pat_type, pat) = check_input_type(input)?;
// For any named type which exists on the function signature...
let schema: &mut Schema = if let Some((_optional, schema)) =
input_schema.find_object_property_mut(&pat.ident.to_string())
{
// ... if it has no `type` property (item = SchemaItem::Inferred), get a mutable
// reference to the schema, so that we can...
match &mut schema.item {
SchemaItem::Inferred(_span) => schema,
// other types are fine:
_ => continue,
}
} else {
continue;
};
// Normally function parameters are simple Ident patterns. Anything else is an error.
let pat = match &*pat_type.pat {
syn::Pat::Ident(pat) => pat,
_ => bail!(pat_type => "unsupported parameter type"),
};
// ... infer the type from the function parameters:
match &*pat_type.ty {
syn::Type::Path(path) if path.qself.is_none() => {
if path.path.is_ident("String") {
schema.item = SchemaItem::String;
continue;
} else if path.path.is_ident("bool") {
schema.item = SchemaItem::Boolean;
continue;
} else if super::INTNAMES.iter().any(|n| path.path.is_ident(n)) {
schema.item = SchemaItem::Integer;
continue;
}
}
_ => (),
}
// if we can't, bail out:
bail!(&pat_type.ty => "cannot infer parameter type from this rust type");
}
for input in sig.inputs.iter() {
let (pat_type, pat) = check_input_type(input)?;
// Here's the deal: we need to distinguish between parameters we need to extract before
// calling the function, a general "Value" parameter covering all the remaining json
@ -223,6 +268,10 @@ fn handle_function_signature(
let param_type = if let Some((optional, schema)) =
input_schema.find_object_property(&pat.ident.to_string())
{
match &schema.item {
SchemaItem::Inferred(span) => bail!(*span, "failed to infer type"),
_ => (),
}
// Found an explicit parameter: extract it:
ParameterType::Other(&pat_type.ty, optional, schema)
} else if is_api_method_type(&pat_type.ty) {

View File

@ -0,0 +1,19 @@
use proxmox_api_macro::api;
use failure::Error;
use serde_json::Value;
#[api(
input: {
properties: {
message: {
description: "The message to print",
}
}
}
)]
/// Print the given message.
pub fn hello(message: String) -> Result<(), Error> {
println!("Hello there. {}", message);
Ok(())
}