* [pbs-devel] [PATCH proxmox 00/18] Optional Return Types and AllOf schema
@ 2020-12-18 11:25 Wolfgang Bumiller
2020-12-18 11:25 ` [pbs-devel] [PATCH proxmox 01/18] formatting fixup Wolfgang Bumiller
` (20 more replies)
0 siblings, 21 replies; 23+ messages in thread
From: Wolfgang Bumiller @ 2020-12-18 11:25 UTC (permalink / raw)
To: pbs-devel
This series extends the schema and #[api] macro a bit:
* Optional return types (requested by Dietmar)
Changes `ApiMethod` to allow marking the return type as optional.
Affects mostly generated documentation & api macro in the proxmox crate.
The generated documentation can probably be improved.
How we utilize this is up to the user (backup crate).
* AllOf schema (we really do need this one way or another, so here's
aproposed mechanism)
This is a "limited" variant of the `AllOf` schema found in JSON
Schema & openapi specs.
This affects a lot of code paths which previously directly
interacted with an `ObjectSchema`.
Changes `ApiMethod` to take a `ParameterSchema` instead of an
`ObjectSchema`, which is a subset of `Schema` consisting of only
`ObjectSchema` and `AllOfSchema`. All three of those now also
implement the `ObjectSchemaType` trait containing all the common
functionality (property lookup, property iteration, "additional
property" query, ...).
As for the `#[api]` macro side, we don't allow directly writing an
`allOf` schema instead of an object schema inside the macro.
Only 2 cases are supported:
* For `struct`s we act on `#[serde(flatten)]`, so any struct which
is an API type can now `flatten` other types into it via serde
(takes care of deserialization), while still staying a valid
`#[api]` type.
* For api *methods*, parameters may now be marked as `flatten`
directly in the object schema:
```
(...)
properties: {
"argname": {
type: Foo,
flatten: true,
}
(...)
}
(...)
```
This also includes 2 patches for temporary changelog updates mentioning
the breaking changes.
The backup side of this series also extends the `verify-api` test to
ensure we don't have duplicate keys in parameters after "flattening"
them.
Wolfgang Bumiller (18):
formatting fixup
schema: support optional return values
api-macro: support optional return values
schema: support AllOf schemas
schema: allow AllOf schema as method parameter
api-macro: add 'flatten' to SerdeAttrib
api-macro: forbid flattened fields
api-macro: add more standard Maybe methods
api-macro: suport AllOf on structs
schema: ExtractValueDeserializer
api-macro: object schema entry tuple -> struct
api-macro: more tuple refactoring
api-macro: factor parameter extraction into a function
api-macro: support flattened parameters
schema: ParameterSchema at 'api' level
proxmox: temporary d/changelog update
macro: temporary d/changelog update
proxmox changelog update
proxmox-api-macro/debian/changelog | 10 +
proxmox-api-macro/src/api/method.rs | 513 +++++++++++++++++++-------
proxmox-api-macro/src/api/mod.rs | 136 +++++--
proxmox-api-macro/src/api/structs.rs | 142 ++++++-
proxmox-api-macro/src/serde.rs | 11 +-
proxmox-api-macro/src/util.rs | 25 +-
proxmox-api-macro/tests/allof.rs | 245 ++++++++++++
proxmox-api-macro/tests/api1.rs | 10 +-
proxmox-api-macro/tests/ext-schema.rs | 2 +-
proxmox-api-macro/tests/types.rs | 7 +-
proxmox/debian/changelog | 18 +
proxmox/src/api/cli/command.rs | 2 +-
proxmox/src/api/cli/completion.rs | 11 +-
proxmox/src/api/cli/format.rs | 22 +-
proxmox/src/api/cli/getopts.rs | 19 +-
proxmox/src/api/cli/text_table.rs | 49 ++-
proxmox/src/api/de.rs | 270 ++++++++++++++
proxmox/src/api/format.rs | 29 +-
proxmox/src/api/mod.rs | 5 +-
proxmox/src/api/router.rs | 117 +++++-
proxmox/src/api/schema.rs | 178 ++++++++-
proxmox/src/api/section_config.rs | 2 +-
proxmox/src/tools/io/read.rs | 16 +-
23 files changed, 1569 insertions(+), 270 deletions(-)
create mode 100644 proxmox-api-macro/tests/allof.rs
create mode 100644 proxmox/src/api/de.rs
Wolfgang Bumiller (2):
adaptions for proxmox 0.9 and proxmox-api-macro 0.3
tests: verify-api: check AllOf schemas
src/api2/admin/datastore.rs | 8 ++--
src/bin/proxmox-backup-client.rs | 12 ++---
src/bin/proxmox-backup-manager.rs | 12 ++---
src/bin/proxmox-tape.rs | 4 +-
src/bin/proxmox_backup_client/benchmark.rs | 5 +-
src/bin/proxmox_backup_client/key.rs | 10 +++-
src/bin/proxmox_backup_client/snapshot.rs | 9 ++--
src/bin/proxmox_backup_client/task.rs | 4 +-
src/bin/proxmox_backup_manager/acl.rs | 2 +-
src/bin/proxmox_backup_manager/datastore.rs | 4 +-
src/bin/proxmox_backup_manager/disk.rs | 8 ++--
src/bin/proxmox_backup_manager/dns.rs | 2 +-
src/bin/proxmox_backup_manager/network.rs | 2 +-
src/bin/proxmox_backup_manager/remote.rs | 4 +-
.../proxmox_backup_manager/subscription.rs | 2 +-
src/bin/proxmox_backup_manager/sync.rs | 4 +-
src/bin/proxmox_backup_manager/user.rs | 4 +-
src/bin/proxmox_tape/changer.rs | 8 ++--
src/bin/proxmox_tape/drive.rs | 6 +--
src/bin/proxmox_tape/media.rs | 2 +-
src/bin/proxmox_tape/pool.rs | 4 +-
src/server/rest.rs | 9 ++--
tests/verify-api.rs | 46 +++++++++++++++++--
23 files changed, 110 insertions(+), 61 deletions(-)
--
2.20.1
^ permalink raw reply [flat|nested] 23+ messages in thread
* [pbs-devel] [PATCH proxmox 01/18] formatting fixup
2020-12-18 11:25 [pbs-devel] [PATCH proxmox 00/18] Optional Return Types and AllOf schema Wolfgang Bumiller
@ 2020-12-18 11:25 ` Wolfgang Bumiller
2020-12-18 11:25 ` [pbs-devel] [PATCH proxmox 02/18] schema: support optional return values Wolfgang Bumiller
` (19 subsequent siblings)
20 siblings, 0 replies; 23+ messages in thread
From: Wolfgang Bumiller @ 2020-12-18 11:25 UTC (permalink / raw)
To: pbs-devel
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
---
proxmox/src/tools/io/read.rs | 16 ++++++++++------
1 file changed, 10 insertions(+), 6 deletions(-)
diff --git a/proxmox/src/tools/io/read.rs b/proxmox/src/tools/io/read.rs
index 9bba54b..eed96b7 100644
--- a/proxmox/src/tools/io/read.rs
+++ b/proxmox/src/tools/io/read.rs
@@ -298,17 +298,21 @@ impl<R: io::Read> ReadExt for R {
loop {
match self.read(&mut buf) {
Ok(0) => {
- if read_bytes == 0 { return Ok(false); }
+ if read_bytes == 0 {
+ return Ok(false);
+ }
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
- "failed to fill whole buffer")
- );
+ "failed to fill whole buffer",
+ ));
}
- Ok(n) => {
+ Ok(n) => {
let tmp = buf;
buf = &mut tmp[n..];
read_bytes += n;
- if buf.is_empty() { return Ok(true); }
+ if buf.is_empty() {
+ return Ok(true);
+ }
}
Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => {}
Err(e) => return Err(e),
@@ -318,7 +322,7 @@ impl<R: io::Read> ReadExt for R {
fn skip_to_end(&mut self) -> io::Result<usize> {
let mut skipped_bytes = 0;
- let mut buf = unsafe { vec::uninitialized(32*1024) };
+ let mut buf = unsafe { vec::uninitialized(32 * 1024) };
loop {
match self.read(&mut buf) {
Ok(0) => return Ok(skipped_bytes),
--
2.20.1
^ permalink raw reply [flat|nested] 23+ messages in thread
* [pbs-devel] [PATCH proxmox 02/18] schema: support optional return values
2020-12-18 11:25 [pbs-devel] [PATCH proxmox 00/18] Optional Return Types and AllOf schema Wolfgang Bumiller
2020-12-18 11:25 ` [pbs-devel] [PATCH proxmox 01/18] formatting fixup Wolfgang Bumiller
@ 2020-12-18 11:25 ` Wolfgang Bumiller
2020-12-18 11:25 ` [pbs-devel] [PATCH proxmox 03/18] api-macro: " Wolfgang Bumiller
` (18 subsequent siblings)
20 siblings, 0 replies; 23+ messages in thread
From: Wolfgang Bumiller @ 2020-12-18 11:25 UTC (permalink / raw)
To: pbs-devel
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
---
proxmox/src/api/cli/format.rs | 9 +++++++--
proxmox/src/api/format.rs | 13 +++++++++---
proxmox/src/api/router.rs | 37 ++++++++++++++++++++++++++++++-----
3 files changed, 49 insertions(+), 10 deletions(-)
diff --git a/proxmox/src/api/cli/format.rs b/proxmox/src/api/cli/format.rs
index 61d530a..a4e7c02 100644
--- a/proxmox/src/api/cli/format.rs
+++ b/proxmox/src/api/cli/format.rs
@@ -5,6 +5,7 @@ use serde_json::Value;
use std::collections::HashSet;
use crate::api::format::*;
+use crate::api::router::ReturnType;
use crate::api::schema::*;
use super::{value_to_text, TableFormatOptions};
@@ -32,16 +33,20 @@ pub fn format_and_print_result(result: &Value, output_format: &str) {
/// formatted tables with borders.
pub fn format_and_print_result_full(
result: &mut Value,
- schema: &Schema,
+ return_type: &ReturnType,
output_format: &str,
options: &TableFormatOptions,
) {
+ if return_type.optional && result.is_null() {
+ return;
+ }
+
if output_format == "json-pretty" {
println!("{}", serde_json::to_string_pretty(&result).unwrap());
} else if output_format == "json" {
println!("{}", serde_json::to_string(&result).unwrap());
} else if output_format == "text" {
- if let Err(err) = value_to_text(std::io::stdout(), result, schema, options) {
+ if let Err(err) = value_to_text(std::io::stdout(), result, &return_type.schema, options) {
eprintln!("unable to format result: {}", err);
}
} else {
diff --git a/proxmox/src/api/format.rs b/proxmox/src/api/format.rs
index 6916b26..eac2214 100644
--- a/proxmox/src/api/format.rs
+++ b/proxmox/src/api/format.rs
@@ -4,6 +4,7 @@ use anyhow::Error;
use std::io::Write;
+use crate::api::router::ReturnType;
use crate::api::schema::*;
use crate::api::{ApiHandler, ApiMethod};
@@ -197,8 +198,14 @@ fn dump_api_parameters(param: &ObjectSchema) -> String {
res
}
-fn dump_api_return_schema(schema: &Schema) -> String {
- let mut res = String::from("*Returns*: ");
+fn dump_api_return_schema(returns: &ReturnType) -> String {
+ let schema = &returns.schema;
+
+ let mut res = if returns.optional {
+ "*Returns* (optionally): ".to_string()
+ } else {
+ "*Returns*: ".to_string()
+ };
let type_text = get_schema_type_text(schema, ParameterDisplayStyle::Config);
res.push_str(&format!("**{}**\n\n", type_text));
@@ -243,7 +250,7 @@ fn dump_method_definition(method: &str, path: &str, def: Option<&ApiMethod>) ->
Some(api_method) => {
let param_descr = dump_api_parameters(api_method.parameters);
- let return_descr = dump_api_return_schema(api_method.returns);
+ let return_descr = dump_api_return_schema(&api_method.returns);
let mut method = method;
diff --git a/proxmox/src/api/router.rs b/proxmox/src/api/router.rs
index f08f9a8..9fb9ec1 100644
--- a/proxmox/src/api/router.rs
+++ b/proxmox/src/api/router.rs
@@ -398,6 +398,33 @@ pub struct ApiAccess {
pub permission: &'static Permission,
}
+#[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))]
+pub struct ReturnType {
+ /// A return type may be optional, meaning the method may return null or some fixed data.
+ ///
+ /// If true, the return type in pseudo openapi terms would be `"oneOf": [ "null", "T" ]`.
+ pub optional: bool,
+
+ /// The method's return type.
+ pub schema: &'static schema::Schema,
+}
+
+impl std::fmt::Debug for ReturnType {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ if self.optional {
+ write!(f, "optional {:?}", self.schema)
+ } else {
+ write!(f, "{:?}", self.schema)
+ }
+ }
+}
+
+impl ReturnType {
+ pub const fn new(optional: bool, schema: &'static Schema) -> Self {
+ Self { optional, schema }
+ }
+}
+
/// This struct defines a synchronous API call which returns the result as json `Value`
#[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))]
pub struct ApiMethod {
@@ -410,7 +437,7 @@ pub struct ApiMethod {
/// Parameter type Schema
pub parameters: &'static schema::ObjectSchema,
/// Return type Schema
- pub returns: &'static schema::Schema,
+ pub returns: ReturnType,
/// Handler function
pub handler: &'static ApiHandler,
/// Access Permissions
@@ -433,7 +460,7 @@ impl ApiMethod {
Self {
parameters,
handler,
- returns: &NULL_SCHEMA,
+ returns: ReturnType::new(false, &NULL_SCHEMA),
protected: false,
reload_timezone: false,
access: ApiAccess {
@@ -447,7 +474,7 @@ impl ApiMethod {
Self {
parameters,
handler: &DUMMY_HANDLER,
- returns: &NULL_SCHEMA,
+ returns: ReturnType::new(false, &NULL_SCHEMA),
protected: false,
reload_timezone: false,
access: ApiAccess {
@@ -457,8 +484,8 @@ impl ApiMethod {
}
}
- pub const fn returns(mut self, schema: &'static Schema) -> Self {
- self.returns = schema;
+ pub const fn returns(mut self, returns: ReturnType) -> Self {
+ self.returns = returns;
self
}
--
2.20.1
^ permalink raw reply [flat|nested] 23+ messages in thread
* [pbs-devel] [PATCH proxmox 03/18] api-macro: support optional return values
2020-12-18 11:25 [pbs-devel] [PATCH proxmox 00/18] Optional Return Types and AllOf schema Wolfgang Bumiller
2020-12-18 11:25 ` [pbs-devel] [PATCH proxmox 01/18] formatting fixup Wolfgang Bumiller
2020-12-18 11:25 ` [pbs-devel] [PATCH proxmox 02/18] schema: support optional return values Wolfgang Bumiller
@ 2020-12-18 11:25 ` Wolfgang Bumiller
2020-12-18 11:25 ` [pbs-devel] [PATCH proxmox 04/18] schema: support AllOf schemas Wolfgang Bumiller
` (17 subsequent siblings)
20 siblings, 0 replies; 23+ messages in thread
From: Wolfgang Bumiller @ 2020-12-18 11:25 UTC (permalink / raw)
To: pbs-devel
The return specification can now include an `optional`
field.
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
---
proxmox-api-macro/src/api/method.rs | 88 +++++++++++++++++++++------
proxmox-api-macro/src/api/mod.rs | 6 +-
proxmox-api-macro/src/api/structs.rs | 7 +--
proxmox-api-macro/src/util.rs | 4 +-
proxmox-api-macro/tests/api1.rs | 10 +--
proxmox-api-macro/tests/ext-schema.rs | 2 +-
proxmox-api-macro/tests/types.rs | 7 ++-
7 files changed, 89 insertions(+), 35 deletions(-)
diff --git a/proxmox-api-macro/src/api/method.rs b/proxmox-api-macro/src/api/method.rs
index 0d05cbb..3e85d8c 100644
--- a/proxmox-api-macro/src/api/method.rs
+++ b/proxmox-api-macro/src/api/method.rs
@@ -20,6 +20,66 @@ use syn::Ident;
use super::{Schema, SchemaItem};
use crate::util::{self, FieldName, JSONObject, JSONValue, Maybe};
+/// A return type in a schema can have an `optional` flag. Other than that it is just a regular
+/// schema.
+pub struct ReturnType {
+ /// If optional, we store `Some(span)`, otherwise `None`.
+ optional: Option<Span>,
+
+ schema: Schema,
+}
+
+impl ReturnType {
+ fn to_schema(&self, ts: &mut TokenStream) -> Result<(), Error> {
+ let optional = match self.optional {
+ Some(span) => quote_spanned! { span => true },
+ None => quote! { false },
+ };
+
+ let mut out = TokenStream::new();
+ self.schema.to_schema(&mut out)?;
+
+ ts.extend(quote! {
+ ::proxmox::api::router::ReturnType::new( #optional , &#out )
+ });
+ Ok(())
+ }
+}
+
+impl TryFrom<JSONValue> for ReturnType {
+ type Error = syn::Error;
+
+ fn try_from(value: JSONValue) -> Result<Self, syn::Error> {
+ Self::try_from(value.into_object("a return type definition")?)
+ }
+}
+
+/// To go from a `JSONObject` to a `ReturnType` we first extract the `optional` flag, then forward
+/// to the `Schema` parser.
+impl TryFrom<JSONObject> for ReturnType {
+ type Error = syn::Error;
+
+ fn try_from(mut obj: JSONObject) -> Result<Self, syn::Error> {
+ let optional = match obj.remove("optional") {
+ Some(value) => {
+ let span = value.span();
+ let is_optional: bool = value.try_into()?;
+ if is_optional {
+ Some(span)
+ } else {
+ None
+ }
+ }
+ None => None,
+ };
+
+ Ok(Self {
+ optional,
+ schema: obj.try_into()?,
+ })
+ }
+}
+
/// Parse `input`, `returns` and `protected` attributes out of an function annotated
/// with an `#[api]` attribute and produce a `const ApiMethod` named after the function.
///
@@ -35,7 +95,7 @@ pub fn handle_method(mut attribs: JSONObject, mut func: syn::ItemFn) -> Result<T
},
};
- let mut returns_schema: Option<Schema> = attribs
+ let mut return_type: Option<ReturnType> = attribs
.remove("returns")
.map(|ret| ret.into_object("return schema definition")?.try_into())
.transpose()?;
@@ -78,7 +138,7 @@ pub fn handle_method(mut attribs: JSONObject, mut func: syn::ItemFn) -> Result<T
let (doc_comment, doc_span) = util::get_doc_comments(&func.attrs)?;
util::derive_descriptions(
&mut input_schema,
- &mut returns_schema,
+ return_type.as_mut().map(|rs| &mut rs.schema),
&doc_comment,
doc_span,
)?;
@@ -89,7 +149,7 @@ pub fn handle_method(mut attribs: JSONObject, mut func: syn::ItemFn) -> Result<T
let is_async = func.sig.asyncness.is_some();
let api_func_name = handle_function_signature(
&mut input_schema,
- &mut returns_schema,
+ &mut return_type,
&mut func,
&mut wrapper_ts,
&mut default_consts,
@@ -110,10 +170,6 @@ pub fn handle_method(mut attribs: JSONObject, mut func: syn::ItemFn) -> Result<T
&format!("API_METHOD_{}", func_name.to_string().to_uppercase()),
func.sig.ident.span(),
);
- let return_schema_name = Ident::new(
- &format!("API_RETURN_SCHEMA_{}", func_name.to_string().to_uppercase()),
- func.sig.ident.span(),
- );
let input_schema_name = Ident::new(
&format!(
"API_PARAMETER_SCHEMA_{}",
@@ -122,15 +178,11 @@ pub fn handle_method(mut attribs: JSONObject, mut func: syn::ItemFn) -> Result<T
func.sig.ident.span(),
);
- let mut returns_schema_definition = TokenStream::new();
let mut returns_schema_setter = TokenStream::new();
- if let Some(schema) = returns_schema {
+ if let Some(return_type) = return_type {
let mut inner = TokenStream::new();
- schema.to_schema(&mut inner)?;
- returns_schema_definition = quote_spanned! { func.sig.span() =>
- pub const #return_schema_name: ::proxmox::api::schema::Schema = #inner;
- };
- returns_schema_setter = quote! { .returns(&#return_schema_name) };
+ return_type.to_schema(&mut inner)?;
+ returns_schema_setter = quote! { .returns(#inner) };
}
let api_handler = if is_async {
@@ -140,8 +192,6 @@ pub fn handle_method(mut attribs: JSONObject, mut func: syn::ItemFn) -> Result<T
};
Ok(quote_spanned! { func.sig.span() =>
- #returns_schema_definition
-
pub const #input_schema_name: ::proxmox::api::schema::ObjectSchema =
#input_schema;
@@ -189,7 +239,7 @@ fn check_input_type(input: &syn::FnArg) -> Result<(&syn::PatType, &syn::PatIdent
fn handle_function_signature(
input_schema: &mut Schema,
- returns_schema: &mut Option<Schema>,
+ return_type: &mut Option<ReturnType>,
func: &mut syn::ItemFn,
wrapper_ts: &mut TokenStream,
default_consts: &mut TokenStream,
@@ -322,7 +372,7 @@ fn handle_function_signature(
create_wrapper_function(
input_schema,
- returns_schema,
+ return_type,
param_list,
func,
wrapper_ts,
@@ -379,7 +429,7 @@ fn is_value_type(ty: &syn::Type) -> bool {
fn create_wrapper_function(
_input_schema: &Schema,
- _returns_schema: &Option<Schema>,
+ _returns_schema: &Option<ReturnType>,
param_list: Vec<(FieldName, ParameterType)>,
func: &syn::ItemFn,
wrapper_ts: &mut TokenStream,
diff --git a/proxmox-api-macro/src/api/mod.rs b/proxmox-api-macro/src/api/mod.rs
index 81d9177..815e167 100644
--- a/proxmox-api-macro/src/api/mod.rs
+++ b/proxmox-api-macro/src/api/mod.rs
@@ -15,7 +15,7 @@ use proc_macro2::{Span, TokenStream};
use quote::{quote, quote_spanned};
use syn::parse::{Parse, ParseStream, Parser};
use syn::spanned::Spanned;
-use syn::{ExprPath, Ident};
+use syn::{Expr, ExprPath, Ident};
use crate::util::{FieldName, JSONObject, JSONValue, Maybe};
@@ -217,7 +217,7 @@ pub enum SchemaItem {
Object(SchemaObject),
Array(SchemaArray),
ExternType(ExprPath),
- ExternSchema(ExprPath),
+ ExternSchema(Expr),
Inferred(Span),
}
@@ -225,7 +225,7 @@ 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> {
- if let Some(ext) = obj.remove("schema").map(ExprPath::try_from).transpose()? {
+ if let Some(ext) = obj.remove("schema").map(Expr::try_from).transpose()? {
return Ok(SchemaItem::ExternSchema(ext));
}
diff --git a/proxmox-api-macro/src/api/structs.rs b/proxmox-api-macro/src/api/structs.rs
index 887ffaa..71cdc8a 100644
--- a/proxmox-api-macro/src/api/structs.rs
+++ b/proxmox-api-macro/src/api/structs.rs
@@ -41,7 +41,7 @@ pub fn handle_struct(attribs: JSONObject, stru: syn::ItemStruct) -> Result<Token
fn get_struct_description(schema: &mut Schema, stru: &syn::ItemStruct) -> Result<(), Error> {
if schema.description.is_none() {
let (doc_comment, doc_span) = util::get_doc_comments(&stru.attrs)?;
- util::derive_descriptions(schema, &mut None, &doc_comment, doc_span)?;
+ util::derive_descriptions(schema, None, &doc_comment, doc_span)?;
}
Ok(())
@@ -184,8 +184,7 @@ fn handle_regular_struct(attribs: JSONObject, stru: syn::ItemStruct) -> Result<T
let bad_fields = util::join(", ", schema_fields.keys());
error!(
schema.span,
- "struct does not contain the following fields: {}",
- bad_fields
+ "struct does not contain the following fields: {}", bad_fields
);
}
@@ -211,7 +210,7 @@ fn handle_regular_field(
if schema.description.is_none() {
let (doc_comment, doc_span) = util::get_doc_comments(&field.attrs)?;
- util::derive_descriptions(schema, &mut None, &doc_comment, doc_span)?;
+ util::derive_descriptions(schema, None, &doc_comment, doc_span)?;
}
util::infer_type(schema, &field.ty)?;
diff --git a/proxmox-api-macro/src/util.rs b/proxmox-api-macro/src/util.rs
index 3cd29a0..e74e04b 100644
--- a/proxmox-api-macro/src/util.rs
+++ b/proxmox-api-macro/src/util.rs
@@ -428,7 +428,7 @@ pub fn get_doc_comments(attributes: &[syn::Attribute]) -> Result<(String, Span),
pub fn derive_descriptions(
input_schema: &mut Schema,
- returns_schema: &mut Option<Schema>,
+ returns_schema: Option<&mut Schema>,
doc_comment: &str,
doc_span: Span,
) -> Result<(), Error> {
@@ -447,7 +447,7 @@ pub fn derive_descriptions(
}
if let Some(second) = parts.next() {
- if let Some(ref mut returns_schema) = returns_schema {
+ if let Some(returns_schema) = returns_schema {
if returns_schema.description.is_none() {
returns_schema.description =
Maybe::Derived(syn::LitStr::new(second.trim(), doc_span));
diff --git a/proxmox-api-macro/tests/api1.rs b/proxmox-api-macro/tests/api1.rs
index ff37c74..88adb40 100644
--- a/proxmox-api-macro/tests/api1.rs
+++ b/proxmox-api-macro/tests/api1.rs
@@ -82,7 +82,8 @@ fn create_ticket_schema_check() {
],
),
)
- .returns(
+ .returns(::proxmox::api::router::ReturnType::new(
+ false,
&::proxmox::api::schema::ObjectSchema::new(
"A ticket.",
&[
@@ -107,7 +108,7 @@ fn create_ticket_schema_check() {
],
)
.schema(),
- )
+ ))
.access(Some("Only root can access this."), &Permission::Superuser)
.protected(true);
assert_eq!(TEST_METHOD, API_METHOD_CREATE_TICKET);
@@ -184,7 +185,8 @@ fn create_ticket_direct_schema_check() {
],
),
)
- .returns(
+ .returns(::proxmox::api::router::ReturnType::new(
+ false,
&::proxmox::api::schema::ObjectSchema::new(
"A ticket.",
&[
@@ -209,7 +211,7 @@ fn create_ticket_direct_schema_check() {
],
)
.schema(),
- )
+ ))
.access(None, &Permission::World)
.protected(true);
assert_eq!(TEST_METHOD, API_METHOD_CREATE_TICKET_DIRECT);
diff --git a/proxmox-api-macro/tests/ext-schema.rs b/proxmox-api-macro/tests/ext-schema.rs
index e9f847d..9fce967 100644
--- a/proxmox-api-macro/tests/ext-schema.rs
+++ b/proxmox-api-macro/tests/ext-schema.rs
@@ -120,7 +120,7 @@ pub fn get_some_text() -> Result<String, Error> {
returns: {
properties: {
"text": {
- schema: API_RETURN_SCHEMA_GET_SOME_TEXT
+ schema: *API_METHOD_GET_SOME_TEXT.returns.schema
},
},
},
diff --git a/proxmox-api-macro/tests/types.rs b/proxmox-api-macro/tests/types.rs
index e121c3f..80d09ba 100644
--- a/proxmox-api-macro/tests/types.rs
+++ b/proxmox-api-macro/tests/types.rs
@@ -144,7 +144,7 @@ fn selection_test() {
selection: { type: Selection },
}
},
- returns: { type: Boolean },
+ returns: { optional: true, type: Boolean },
)]
/// Check a string.
///
@@ -167,7 +167,10 @@ fn string_check_schema_test() {
],
),
)
- .returns(&::proxmox::api::schema::BooleanSchema::new("Whether the string was \"ok\".").schema())
+ .returns(::proxmox::api::router::ReturnType::new(
+ true,
+ &::proxmox::api::schema::BooleanSchema::new("Whether the string was \"ok\".").schema(),
+ ))
.protected(false);
assert_eq!(TEST_METHOD, API_METHOD_STRING_CHECK);
--
2.20.1
^ permalink raw reply [flat|nested] 23+ messages in thread
* [pbs-devel] [PATCH proxmox 04/18] schema: support AllOf schemas
2020-12-18 11:25 [pbs-devel] [PATCH proxmox 00/18] Optional Return Types and AllOf schema Wolfgang Bumiller
` (2 preceding siblings ...)
2020-12-18 11:25 ` [pbs-devel] [PATCH proxmox 03/18] api-macro: " Wolfgang Bumiller
@ 2020-12-18 11:25 ` Wolfgang Bumiller
2020-12-18 11:25 ` [pbs-devel] [PATCH proxmox 05/18] schema: allow AllOf schema as method parameter Wolfgang Bumiller
` (16 subsequent siblings)
20 siblings, 0 replies; 23+ messages in thread
From: Wolfgang Bumiller @ 2020-12-18 11:25 UTC (permalink / raw)
To: pbs-devel
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
---
proxmox/src/api/cli/text_table.rs | 49 ++++++----
proxmox/src/api/format.rs | 14 ++-
proxmox/src/api/schema.rs | 151 ++++++++++++++++++++++++++++--
proxmox/src/api/section_config.rs | 2 +-
4 files changed, 183 insertions(+), 33 deletions(-)
diff --git a/proxmox/src/api/cli/text_table.rs b/proxmox/src/api/cli/text_table.rs
index 7e19ed1..131e667 100644
--- a/proxmox/src/api/cli/text_table.rs
+++ b/proxmox/src/api/cli/text_table.rs
@@ -56,24 +56,25 @@ fn data_to_text(data: &Value, schema: &Schema) -> Result<String, Error> {
// makes no sense to display Null columns
bail!("internal error");
}
- Schema::Boolean(_boolean_schema) => match data.as_bool() {
+ Schema::Boolean(_) => match data.as_bool() {
Some(value) => Ok(String::from(if value { "1" } else { "0" })),
None => bail!("got unexpected data (expected bool)."),
},
- Schema::Integer(_integer_schema) => match data.as_i64() {
+ Schema::Integer(_) => match data.as_i64() {
Some(value) => Ok(format!("{}", value)),
None => bail!("got unexpected data (expected integer)."),
},
- Schema::Number(_number_schema) => match data.as_f64() {
+ Schema::Number(_) => match data.as_f64() {
Some(value) => Ok(format!("{}", value)),
None => bail!("got unexpected data (expected number)."),
},
- Schema::String(_string_schema) => match data.as_str() {
+ Schema::String(_) => match data.as_str() {
Some(value) => Ok(value.to_string()),
None => bail!("got unexpected data (expected string)."),
},
- Schema::Object(_object_schema) => Ok(data.to_string()),
- Schema::Array(_array_schema) => Ok(data.to_string()),
+ Schema::Object(_) => Ok(data.to_string()),
+ Schema::Array(_) => Ok(data.to_string()),
+ Schema::AllOf(_) => Ok(data.to_string()),
}
}
@@ -325,14 +326,14 @@ struct TableColumn {
right_align: bool,
}
-fn format_table<W: Write>(
+fn format_table<W: Write, I: Iterator<Item = &'static SchemaPropertyEntry>>(
output: W,
list: &mut Vec<Value>,
- schema: &ObjectSchema,
+ schema: &dyn ObjectSchemaType<PropertyIter = I>,
options: &TableFormatOptions,
) -> Result<(), Error> {
let properties_to_print = if options.column_config.is_empty() {
- extract_properties_to_print(schema)
+ extract_properties_to_print(schema.properties())
} else {
options
.column_config
@@ -579,14 +580,14 @@ fn render_table<W: Write>(
Ok(())
}
-fn format_object<W: Write>(
+fn format_object<W: Write, I: Iterator<Item = &'static SchemaPropertyEntry>>(
output: W,
data: &Value,
- schema: &ObjectSchema,
+ schema: &dyn ObjectSchemaType<PropertyIter = I>,
options: &TableFormatOptions,
) -> Result<(), Error> {
let properties_to_print = if options.column_config.is_empty() {
- extract_properties_to_print(schema)
+ extract_properties_to_print(schema.properties())
} else {
options
.column_config
@@ -702,19 +703,23 @@ fn format_object<W: Write>(
render_table(output, &tabledata, &column_names, options)
}
-fn extract_properties_to_print(schema: &ObjectSchema) -> Vec<String> {
+fn extract_properties_to_print<I>(properties: I) -> Vec<String>
+where
+ I: Iterator<Item = &'static SchemaPropertyEntry>,
+{
let mut result = Vec::new();
+ let mut opt_properties = Vec::new();
- for (name, optional, _prop_schema) in schema.properties {
- if !*optional {
- result.push(name.to_string());
- }
- }
- for (name, optional, _prop_schema) in schema.properties {
+ for (name, optional, _prop_schema) in properties {
if *optional {
+ opt_properties.push(name.to_string());
+ } else {
result.push(name.to_string());
}
}
+
+ result.extend(opt_properties);
+
result
}
@@ -759,11 +764,17 @@ pub fn value_to_text<W: Write>(
Schema::Object(object_schema) => {
format_table(output, list, object_schema, options)?;
}
+ Schema::AllOf(all_of_schema) => {
+ format_table(output, list, all_of_schema, options)?;
+ }
_ => {
unimplemented!();
}
}
}
+ Schema::AllOf(all_of_schema) => {
+ format_object(output, data, all_of_schema, options)?;
+ }
}
Ok(())
}
diff --git a/proxmox/src/api/format.rs b/proxmox/src/api/format.rs
index eac2214..719d862 100644
--- a/proxmox/src/api/format.rs
+++ b/proxmox/src/api/format.rs
@@ -96,6 +96,7 @@ pub fn get_schema_type_text(schema: &Schema, _style: ParameterDisplayStyle) -> S
},
Schema::Object(_) => String::from("<object>"),
Schema::Array(_) => String::from("<array>"),
+ Schema::AllOf(_) => String::from("<object>"),
}
}
@@ -115,6 +116,7 @@ pub fn get_property_description(
Schema::Integer(ref schema) => (schema.description, schema.default.map(|v| v.to_string())),
Schema::Number(ref schema) => (schema.description, schema.default.map(|v| v.to_string())),
Schema::Object(ref schema) => (schema.description, None),
+ Schema::AllOf(ref schema) => (schema.description, None),
Schema::Array(ref schema) => (schema.description, None),
};
@@ -156,13 +158,16 @@ pub fn get_property_description(
}
}
-fn dump_api_parameters(param: &ObjectSchema) -> String {
- let mut res = wrap_text("", "", param.description, 80);
+fn dump_api_parameters<I>(param: &dyn ObjectSchemaType<PropertyIter = I>) -> String
+where
+ I: Iterator<Item = &'static SchemaPropertyEntry>,
+{
+ let mut res = wrap_text("", "", param.description(), 80);
let mut required_list: Vec<String> = Vec::new();
let mut optional_list: Vec<String> = Vec::new();
- for (prop, optional, schema) in param.properties {
+ for (prop, optional, schema) in param.properties() {
let param_descr = get_property_description(
prop,
&schema,
@@ -237,6 +242,9 @@ fn dump_api_return_schema(returns: &ReturnType) -> String {
Schema::Object(obj_schema) => {
res.push_str(&dump_api_parameters(obj_schema));
}
+ Schema::AllOf(all_of_schema) => {
+ res.push_str(&dump_api_parameters(all_of_schema));
+ }
}
res.push('\n');
diff --git a/proxmox/src/api/schema.rs b/proxmox/src/api/schema.rs
index d675f8c..f1ceddd 100644
--- a/proxmox/src/api/schema.rs
+++ b/proxmox/src/api/schema.rs
@@ -397,6 +397,13 @@ impl ArraySchema {
}
}
+/// Property entry in an object schema:
+///
+/// - `name`: The name of the property
+/// - `optional`: Set when the property is optional
+/// - `schema`: Property type schema
+pub type SchemaPropertyEntry = (&'static str, bool, &'static Schema);
+
/// Lookup table to Schema properties
///
/// Stores a sorted list of `(name, optional, schema)` tuples:
@@ -409,7 +416,7 @@ impl ArraySchema {
/// a binary search to find items.
///
/// This is a workaround unless RUST can const_fn `Hash::new()`
-pub type SchemaPropertyMap = &'static [(&'static str, bool, &'static Schema)];
+pub type SchemaPropertyMap = &'static [SchemaPropertyEntry];
/// Data type to describe objects (maps).
#[derive(Debug)]
@@ -462,6 +469,126 @@ impl ObjectSchema {
}
}
+/// Combines multiple *object* schemas into one.
+///
+/// Note that these are limited to object schemas. Other schemas will produce errors.
+///
+/// Technically this could also contain an `additional_properties` flag, however, in the JSON
+/// Schema[1], this is not supported, so here we simply assume additional properties to be allowed.
+#[derive(Debug)]
+#[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))]
+pub struct AllOfSchema {
+ pub description: &'static str,
+
+ /// The parameter is checked against all of the schemas in the list. Note that all schemas must
+ /// be object schemas.
+ pub list: &'static [&'static Schema],
+}
+
+impl AllOfSchema {
+ pub const fn new(description: &'static str, list: &'static [&'static Schema]) -> Self {
+ Self { description, list }
+ }
+
+ pub const fn schema(self) -> Schema {
+ Schema::AllOf(self)
+ }
+
+ pub fn lookup(&self, key: &str) -> Option<(bool, &Schema)> {
+ for entry in self.list {
+ match entry {
+ Schema::Object(s) => {
+ if let Some(v) = s.lookup(key) {
+ return Some(v);
+ }
+ }
+ _ => panic!("non-object-schema in `AllOfSchema`"),
+ }
+ }
+
+ None
+ }
+}
+
+/// Beside [`ObjectSchema`] we also have an [`AllOfSchema`] which also represents objects.
+pub trait ObjectSchemaType {
+ type PropertyIter: Iterator<Item = &'static SchemaPropertyEntry>;
+
+ fn description(&self) -> &'static str;
+ fn lookup(&self, key: &str) -> Option<(bool, &Schema)>;
+ fn properties(&self) -> Self::PropertyIter;
+ fn additional_properties(&self) -> bool;
+}
+
+impl ObjectSchemaType for ObjectSchema {
+ type PropertyIter = std::slice::Iter<'static, SchemaPropertyEntry>;
+
+ fn description(&self) -> &'static str {
+ self.description
+ }
+
+ fn lookup(&self, key: &str) -> Option<(bool, &Schema)> {
+ ObjectSchema::lookup(self, key)
+ }
+
+ fn properties(&self) -> Self::PropertyIter {
+ self.properties.into_iter()
+ }
+
+ fn additional_properties(&self) -> bool {
+ self.additional_properties
+ }
+}
+
+impl ObjectSchemaType for AllOfSchema {
+ type PropertyIter = AllOfProperties;
+
+ fn description(&self) -> &'static str {
+ self.description
+ }
+
+ fn lookup(&self, key: &str) -> Option<(bool, &Schema)> {
+ AllOfSchema::lookup(self, key)
+ }
+
+ fn properties(&self) -> Self::PropertyIter {
+ AllOfProperties {
+ schemas: self.list.into_iter(),
+ properties: None,
+ }
+ }
+
+ fn additional_properties(&self) -> bool {
+ true
+ }
+}
+
+#[doc(hidden)]
+pub struct AllOfProperties {
+ schemas: std::slice::Iter<'static, &'static Schema>,
+ properties: Option<std::slice::Iter<'static, SchemaPropertyEntry>>,
+}
+
+impl Iterator for AllOfProperties {
+ type Item = &'static SchemaPropertyEntry;
+
+ fn next(&mut self) -> Option<&'static SchemaPropertyEntry> {
+ loop {
+ match self.properties.as_mut().and_then(Iterator::next) {
+ Some(item) => return Some(item),
+ None => match self.schemas.next()? {
+ Schema::Object(o) => self.properties = Some(o.properties()),
+ _ => {
+ // this case is actually illegal
+ self.properties = None;
+ continue;
+ }
+ },
+ }
+ }
+ }
+}
+
/// Schemas are used to describe complex data types.
///
/// All schema types implement constant builder methods, and a final
@@ -501,6 +628,7 @@ pub enum Schema {
String(StringSchema),
Object(ObjectSchema),
Array(ArraySchema),
+ AllOf(AllOfSchema),
}
/// A string enum entry. An enum entry must have a value and a description.
@@ -818,21 +946,18 @@ pub fn parse_query_string(
/// Verify JSON value with `schema`.
pub fn verify_json(data: &Value, schema: &Schema) -> Result<(), Error> {
match schema {
- Schema::Object(object_schema) => {
- verify_json_object(data, &object_schema)?;
- }
- Schema::Array(array_schema) => {
- verify_json_array(data, &array_schema)?;
- }
Schema::Null => {
if !data.is_null() {
bail!("Expected Null, but value is not Null.");
}
}
+ Schema::Object(object_schema) => verify_json_object(data, object_schema)?,
+ Schema::Array(array_schema) => verify_json_array(data, &array_schema)?,
Schema::Boolean(boolean_schema) => verify_json_boolean(data, &boolean_schema)?,
Schema::Integer(integer_schema) => verify_json_integer(data, &integer_schema)?,
Schema::Number(number_schema) => verify_json_number(data, &number_schema)?,
Schema::String(string_schema) => verify_json_string(data, &string_schema)?,
+ Schema::AllOf(all_of_schema) => verify_json_object(data, all_of_schema)?,
}
Ok(())
}
@@ -890,14 +1015,20 @@ pub fn verify_json_array(data: &Value, schema: &ArraySchema) -> Result<(), Error
}
/// Verify JSON value using an `ObjectSchema`.
-pub fn verify_json_object(data: &Value, schema: &ObjectSchema) -> Result<(), Error> {
+pub fn verify_json_object<I>(
+ data: &Value,
+ schema: &dyn ObjectSchemaType<PropertyIter = I>,
+) -> Result<(), Error>
+where
+ I: Iterator<Item = &'static SchemaPropertyEntry>,
+{
let map = match data {
Value::Object(ref map) => map,
Value::Array(_) => bail!("Expected object - got array."),
_ => bail!("Expected object - got scalar value."),
};
- let additional_properties = schema.additional_properties;
+ let additional_properties = schema.additional_properties();
for (key, value) in map {
if let Some((_optional, prop_schema)) = schema.lookup(&key) {
@@ -917,7 +1048,7 @@ pub fn verify_json_object(data: &Value, schema: &ObjectSchema) -> Result<(), Err
}
}
- for (name, optional, _prop_schema) in schema.properties {
+ for (name, optional, _prop_schema) in schema.properties() {
if !(*optional) && data[name] == Value::Null {
bail!(
"property '{}': property is missing and it is not optional.",
diff --git a/proxmox/src/api/section_config.rs b/proxmox/src/api/section_config.rs
index 30eb784..c15d813 100644
--- a/proxmox/src/api/section_config.rs
+++ b/proxmox/src/api/section_config.rs
@@ -310,7 +310,7 @@ impl SectionConfig {
if section_id.chars().any(|c| c.is_control()) {
bail!("detected unexpected control character in section ID.");
}
- if let Err(err) = verify_json_object(section_config, &plugin.properties) {
+ if let Err(err) = verify_json_object(section_config, plugin.properties) {
bail!("verify section '{}' failed - {}", section_id, err);
}
--
2.20.1
^ permalink raw reply [flat|nested] 23+ messages in thread
* [pbs-devel] [PATCH proxmox 05/18] schema: allow AllOf schema as method parameter
2020-12-18 11:25 [pbs-devel] [PATCH proxmox 00/18] Optional Return Types and AllOf schema Wolfgang Bumiller
` (3 preceding siblings ...)
2020-12-18 11:25 ` [pbs-devel] [PATCH proxmox 04/18] schema: support AllOf schemas Wolfgang Bumiller
@ 2020-12-18 11:25 ` Wolfgang Bumiller
2020-12-18 11:25 ` [pbs-devel] [PATCH proxmox 06/18] api-macro: add 'flatten' to SerdeAttrib Wolfgang Bumiller
` (15 subsequent siblings)
20 siblings, 0 replies; 23+ messages in thread
From: Wolfgang Bumiller @ 2020-12-18 11:25 UTC (permalink / raw)
To: pbs-devel
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
---
proxmox/src/api/cli/command.rs | 2 +-
proxmox/src/api/cli/completion.rs | 11 +++--
proxmox/src/api/cli/format.rs | 13 +++--
proxmox/src/api/cli/getopts.rs | 19 ++++++--
proxmox/src/api/format.rs | 2 +-
proxmox/src/api/router.rs | 80 +++++++++++++++++++++++++++----
proxmox/src/api/schema.rs | 27 +++++++----
7 files changed, 122 insertions(+), 32 deletions(-)
diff --git a/proxmox/src/api/cli/command.rs b/proxmox/src/api/cli/command.rs
index 50f5979..fa447ba 100644
--- a/proxmox/src/api/cli/command.rs
+++ b/proxmox/src/api/cli/command.rs
@@ -32,7 +32,7 @@ fn parse_arguments(prefix: &str, cli_cmd: &CliCommand, args: Vec<String>) -> Res
&args,
cli_cmd.arg_param,
&cli_cmd.fixed_param,
- &cli_cmd.info.parameters,
+ cli_cmd.info.parameters,
) {
Ok((p, r)) => (p, r),
Err(err) => {
diff --git a/proxmox/src/api/cli/completion.rs b/proxmox/src/api/cli/completion.rs
index 2bb8197..42b3915 100644
--- a/proxmox/src/api/cli/completion.rs
+++ b/proxmox/src/api/cli/completion.rs
@@ -1,10 +1,11 @@
use super::*;
+use crate::api::router::ParameterSchema;
use crate::api::schema::*;
fn record_done_argument(
done: &mut HashMap<String, String>,
- parameters: &ObjectSchema,
+ parameters: ParameterSchema,
key: &str,
value: &str,
) {
@@ -119,11 +120,11 @@ fn get_simple_completion(
let mut errors = ParameterError::new(); // we simply ignore any parsing errors here
let (data, _remaining) = getopts::parse_argument_list(
&args[0..args.len() - 1],
- &cli_cmd.info.parameters,
+ cli_cmd.info.parameters,
&mut errors,
);
for (key, value) in &data {
- record_done_argument(done, &cli_cmd.info.parameters, key, value);
+ record_done_argument(done, cli_cmd.info.parameters, key, value);
}
}
@@ -148,7 +149,7 @@ fn get_simple_completion(
}
let mut completions = Vec::new();
- for (name, _optional, _schema) in cli_cmd.info.parameters.properties {
+ for (name, _optional, _schema) in cli_cmd.info.parameters.properties() {
if done.contains_key(*name) {
continue;
}
@@ -210,7 +211,7 @@ fn get_nested_completion(def: &CommandLineInterface, args: &[String]) -> Vec<Str
CommandLineInterface::Simple(cli_cmd) => {
let mut done: HashMap<String, String> = HashMap::new();
cli_cmd.fixed_param.iter().for_each(|(key, value)| {
- record_done_argument(&mut done, &cli_cmd.info.parameters, &key, &value);
+ record_done_argument(&mut done, cli_cmd.info.parameters, &key, &value);
});
get_simple_completion(cli_cmd, &mut done, &cli_cmd.arg_param, args)
}
diff --git a/proxmox/src/api/cli/format.rs b/proxmox/src/api/cli/format.rs
index a4e7c02..f780b1a 100644
--- a/proxmox/src/api/cli/format.rs
+++ b/proxmox/src/api/cli/format.rs
@@ -110,7 +110,7 @@ pub fn generate_usage_str(
let mut options = String::new();
- for (prop, optional, param_schema) in schema.properties {
+ for (prop, optional, param_schema) in schema.properties() {
if done_hash.contains(prop) {
continue;
}
@@ -150,11 +150,18 @@ pub fn generate_usage_str(
DocumentationFormat::Long => format!("{}{}{}{}\n", indent, prefix, args, option_indicator),
DocumentationFormat::Full => format!(
"{}{}{}{}\n\n{}\n\n",
- indent, prefix, args, option_indicator, schema.description
+ indent,
+ prefix,
+ args,
+ option_indicator,
+ schema.description()
),
DocumentationFormat::ReST => format!(
"``{}{}{}``\n\n{}\n\n",
- prefix, args, option_indicator, schema.description
+ prefix,
+ args,
+ option_indicator,
+ schema.description()
),
};
diff --git a/proxmox/src/api/cli/getopts.rs b/proxmox/src/api/cli/getopts.rs
index 6fd48e8..adf0658 100644
--- a/proxmox/src/api/cli/getopts.rs
+++ b/proxmox/src/api/cli/getopts.rs
@@ -3,6 +3,7 @@ use std::collections::HashMap;
use anyhow::*;
use serde_json::Value;
+use crate::api::router::ParameterSchema;
use crate::api::schema::*;
#[derive(Debug)]
@@ -57,7 +58,7 @@ fn parse_argument(arg: &str) -> RawArgument {
/// Returns parsed data and the remaining arguments as two separate array
pub(crate) fn parse_argument_list<T: AsRef<str>>(
args: &[T],
- schema: &ObjectSchema,
+ schema: ParameterSchema,
errors: &mut ParameterError,
) -> (Vec<(String, String)>, Vec<String>) {
let mut data: Vec<(String, String)> = vec![];
@@ -149,7 +150,7 @@ pub fn parse_arguments<T: AsRef<str>>(
args: &[T],
arg_param: &[&str],
fixed_param: &HashMap<&'static str, String>,
- schema: &ObjectSchema,
+ schema: ParameterSchema,
) -> Result<(Value, Vec<String>), ParameterError> {
let mut errors = ParameterError::new();
@@ -229,7 +230,12 @@ fn test_boolean_arg() {
variants.push((vec!["--enable", "false"], false));
for (args, expect) in variants {
- let res = parse_arguments(&args, &vec![], &HashMap::new(), &PARAMETERS);
+ let res = parse_arguments(
+ &args,
+ &vec![],
+ &HashMap::new(),
+ ParameterSchema::from(&PARAMETERS),
+ );
assert!(res.is_ok());
if let Ok((options, remaining)) = res {
assert!(options["enable"] == expect);
@@ -249,7 +255,12 @@ fn test_argument_paramenter() {
);
let args = vec!["-enable", "local"];
- let res = parse_arguments(&args, &vec!["storage"], &HashMap::new(), &PARAMETERS);
+ let res = parse_arguments(
+ &args,
+ &vec!["storage"],
+ &HashMap::new(),
+ ParameterSchema::from(&PARAMETERS),
+ );
assert!(res.is_ok());
if let Ok((options, remaining)) = res {
assert!(options["enable"] == true);
diff --git a/proxmox/src/api/format.rs b/proxmox/src/api/format.rs
index 719d862..2ce9597 100644
--- a/proxmox/src/api/format.rs
+++ b/proxmox/src/api/format.rs
@@ -256,7 +256,7 @@ fn dump_method_definition(method: &str, path: &str, def: Option<&ApiMethod>) ->
match def {
None => None,
Some(api_method) => {
- let param_descr = dump_api_parameters(api_method.parameters);
+ let param_descr = dump_api_parameters(&api_method.parameters);
let return_descr = dump_api_return_schema(&api_method.returns);
diff --git a/proxmox/src/api/router.rs b/proxmox/src/api/router.rs
index 9fb9ec1..2f4b6c4 100644
--- a/proxmox/src/api/router.rs
+++ b/proxmox/src/api/router.rs
@@ -10,7 +10,7 @@ use hyper::Body;
use percent_encoding::percent_decode_str;
use serde_json::Value;
-use crate::api::schema::{self, ObjectSchema, Schema};
+use crate::api::schema::{self, AllOfSchema, ObjectSchema, Schema};
use crate::api::RpcEnvironment;
use super::Permission;
@@ -36,10 +36,11 @@ use super::Permission;
/// &ObjectSchema::new("Hello World Example", &[])
/// );
/// ```
-pub type ApiHandlerFn = &'static (dyn Fn(Value, &ApiMethod, &mut dyn RpcEnvironment) -> Result<Value, Error>
- + Send
- + Sync
- + 'static);
+pub type ApiHandlerFn =
+ &'static (dyn Fn(Value, &ApiMethod, &mut dyn RpcEnvironment) -> Result<Value, Error>
+ + Send
+ + Sync
+ + 'static);
/// Asynchronous API handlers
///
@@ -67,7 +68,11 @@ pub type ApiHandlerFn = &'static (dyn Fn(Value, &ApiMethod, &mut dyn RpcEnvironm
/// &ObjectSchema::new("Hello World Example (async)", &[])
/// );
/// ```
-pub type ApiAsyncHandlerFn = &'static (dyn for<'a> Fn(Value, &'static ApiMethod, &'a mut dyn RpcEnvironment) -> ApiFuture<'a>
+pub type ApiAsyncHandlerFn = &'static (dyn for<'a> Fn(
+ Value,
+ &'static ApiMethod,
+ &'a mut dyn RpcEnvironment,
+) -> ApiFuture<'a>
+ Send
+ Sync);
@@ -425,6 +430,59 @@ impl ReturnType {
}
}
+/// Parameters are objects, but we have two types of object schemas, the regular one and the
+/// `AllOf` schema.
+#[derive(Clone, Copy, Debug)]
+#[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))]
+pub enum ParameterSchema {
+ Object(&'static ObjectSchema),
+ AllOf(&'static AllOfSchema),
+}
+
+impl schema::ObjectSchemaType for ParameterSchema {
+ type PropertyIter = Box<dyn Iterator<Item = &'static schema::SchemaPropertyEntry>>;
+
+ fn description(&self) -> &'static str {
+ match self {
+ ParameterSchema::Object(o) => o.description(),
+ ParameterSchema::AllOf(o) => o.description(),
+ }
+ }
+
+ fn lookup(&self, key: &str) -> Option<(bool, &Schema)> {
+ match self {
+ ParameterSchema::Object(o) => o.lookup(key),
+ ParameterSchema::AllOf(o) => o.lookup(key),
+ }
+ }
+
+ fn properties(&self) -> Self::PropertyIter {
+ match self {
+ ParameterSchema::Object(o) => Box::new(o.properties()),
+ ParameterSchema::AllOf(o) => Box::new(o.properties()),
+ }
+ }
+
+ fn additional_properties(&self) -> bool {
+ match self {
+ ParameterSchema::Object(o) => o.additional_properties(),
+ ParameterSchema::AllOf(o) => o.additional_properties(),
+ }
+ }
+}
+
+impl From<&'static ObjectSchema> for ParameterSchema {
+ fn from(schema: &'static ObjectSchema) -> Self {
+ ParameterSchema::Object(schema)
+ }
+}
+
+impl From<&'static AllOfSchema> for ParameterSchema {
+ fn from(schema: &'static AllOfSchema) -> Self {
+ ParameterSchema::AllOf(schema)
+ }
+}
+
/// This struct defines a synchronous API call which returns the result as json `Value`
#[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))]
pub struct ApiMethod {
@@ -435,7 +493,7 @@ pub struct ApiMethod {
/// should do a tzset afterwards
pub reload_timezone: bool,
/// Parameter type Schema
- pub parameters: &'static schema::ObjectSchema,
+ pub parameters: ParameterSchema,
/// Return type Schema
pub returns: ReturnType,
/// Handler function
@@ -456,7 +514,7 @@ impl std::fmt::Debug for ApiMethod {
}
impl ApiMethod {
- pub const fn new(handler: &'static ApiHandler, parameters: &'static ObjectSchema) -> Self {
+ pub const fn new_full(handler: &'static ApiHandler, parameters: ParameterSchema) -> Self {
Self {
parameters,
handler,
@@ -470,9 +528,13 @@ impl ApiMethod {
}
}
+ pub const fn new(handler: &'static ApiHandler, parameters: &'static ObjectSchema) -> Self {
+ Self::new_full(handler, ParameterSchema::Object(parameters))
+ }
+
pub const fn new_dummy(parameters: &'static ObjectSchema) -> Self {
Self {
- parameters,
+ parameters: ParameterSchema::Object(parameters),
handler: &DUMMY_HANDLER,
returns: ReturnType::new(false, &NULL_SCHEMA),
protected: false,
diff --git a/proxmox/src/api/schema.rs b/proxmox/src/api/schema.rs
index f1ceddd..1378d78 100644
--- a/proxmox/src/api/schema.rs
+++ b/proxmox/src/api/schema.rs
@@ -10,6 +10,7 @@ use anyhow::{bail, format_err, Error};
use serde_json::{json, Value};
use url::form_urlencoded;
+use super::router::ParameterSchema;
use crate::api::const_regex::ConstRegexPattern;
/// Error type for schema validation
@@ -764,7 +765,7 @@ pub fn parse_boolean(value_str: &str) -> Result<bool, Error> {
}
/// Parse a complex property string (`ApiStringFormat::PropertyString`)
-pub fn parse_property_string(value_str: &str, schema: &Schema) -> Result<Value, Error> {
+pub fn parse_property_string(value_str: &str, schema: &'static Schema) -> Result<Value, Error> {
match schema {
Schema::Object(object_schema) => {
let mut param_list: Vec<(String, String)> = vec![];
@@ -783,7 +784,7 @@ pub fn parse_property_string(value_str: &str, schema: &Schema) -> Result<Value,
}
}
- parse_parameter_strings(¶m_list, &object_schema, true).map_err(Error::from)
+ parse_parameter_strings(¶m_list, object_schema, true).map_err(Error::from)
}
Schema::Array(array_schema) => {
let mut array: Vec<Value> = vec![];
@@ -839,16 +840,24 @@ pub fn parse_simple_value(value_str: &str, schema: &Schema) -> Result<Value, Err
///
/// - `test_required`: is set, checks if all required properties are
/// present.
-pub fn parse_parameter_strings(
+pub fn parse_parameter_strings<T: Into<ParameterSchema>>(
data: &[(String, String)],
- schema: &ObjectSchema,
+ schema: T,
+ test_required: bool,
+) -> Result<Value, ParameterError> {
+ do_parse_parameter_strings(data, schema.into(), test_required)
+}
+
+fn do_parse_parameter_strings(
+ data: &[(String, String)],
+ schema: ParameterSchema,
test_required: bool,
) -> Result<Value, ParameterError> {
let mut params = json!({});
let mut errors = ParameterError::new();
- let additional_properties = schema.additional_properties;
+ let additional_properties = schema.additional_properties();
for (key, value) in data {
if let Some((_optional, prop_schema)) = schema.lookup(&key) {
@@ -911,7 +920,7 @@ pub fn parse_parameter_strings(
}
if test_required && errors.is_empty() {
- for (name, optional, _prop_schema) in schema.properties {
+ for (name, optional, _prop_schema) in schema.properties() {
if !(*optional) && params[name] == Value::Null {
errors.push(format_err!(
"parameter '{}': parameter is missing and it is not optional.",
@@ -931,16 +940,16 @@ pub fn parse_parameter_strings(
/// Parse a `form_urlencoded` query string and verify with object schema
/// - `test_required`: is set, checks if all required properties are
/// present.
-pub fn parse_query_string(
+pub fn parse_query_string<T: Into<ParameterSchema>>(
query: &str,
- schema: &ObjectSchema,
+ schema: T,
test_required: bool,
) -> Result<Value, ParameterError> {
let param_list: Vec<(String, String)> = form_urlencoded::parse(query.as_bytes())
.into_owned()
.collect();
- parse_parameter_strings(¶m_list, schema, test_required)
+ parse_parameter_strings(¶m_list, schema.into(), test_required)
}
/// Verify JSON value with `schema`.
--
2.20.1
^ permalink raw reply [flat|nested] 23+ messages in thread
* [pbs-devel] [PATCH proxmox 06/18] api-macro: add 'flatten' to SerdeAttrib
2020-12-18 11:25 [pbs-devel] [PATCH proxmox 00/18] Optional Return Types and AllOf schema Wolfgang Bumiller
` (4 preceding siblings ...)
2020-12-18 11:25 ` [pbs-devel] [PATCH proxmox 05/18] schema: allow AllOf schema as method parameter Wolfgang Bumiller
@ 2020-12-18 11:25 ` Wolfgang Bumiller
2020-12-18 11:25 ` [pbs-devel] [PATCH proxmox 07/18] api-macro: forbid flattened fields Wolfgang Bumiller
` (14 subsequent siblings)
20 siblings, 0 replies; 23+ messages in thread
From: Wolfgang Bumiller @ 2020-12-18 11:25 UTC (permalink / raw)
To: pbs-devel
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
---
proxmox-api-macro/src/serde.rs | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/proxmox-api-macro/src/serde.rs b/proxmox-api-macro/src/serde.rs
index a08f461..2829030 100644
--- a/proxmox-api-macro/src/serde.rs
+++ b/proxmox-api-macro/src/serde.rs
@@ -159,12 +159,15 @@ impl TryFrom<&[syn::Attribute]> for ContainerAttrib {
#[derive(Default)]
pub struct SerdeAttrib {
pub rename: Option<FieldName>,
+ pub flatten: bool,
}
impl TryFrom<&[syn::Attribute]> for SerdeAttrib {
type Error = syn::Error;
fn try_from(attributes: &[syn::Attribute]) -> Result<Self, syn::Error> {
+ use syn::{Meta, NestedMeta};
+
let mut this: Self = Default::default();
for attrib in attributes {
@@ -174,8 +177,8 @@ impl TryFrom<&[syn::Attribute]> for SerdeAttrib {
let args: AttrArgs = syn::parse2(attrib.tokens.clone())?;
for arg in args.args {
- if let syn::NestedMeta::Meta(syn::Meta::NameValue(var)) = arg {
- if var.path.is_ident("rename") {
+ match arg {
+ NestedMeta::Meta(Meta::NameValue(var)) if var.path.is_ident("rename") => {
match var.lit {
syn::Lit::Str(lit) => {
let rename = FieldName::from(&lit);
@@ -187,6 +190,10 @@ impl TryFrom<&[syn::Attribute]> for SerdeAttrib {
_ => error!(var.lit => "'rename' value must be a string literal"),
}
}
+ NestedMeta::Meta(Meta::Path(path)) if path.is_ident("flatten") => {
+ this.flatten = true;
+ }
+ _ => continue,
}
}
}
--
2.20.1
^ permalink raw reply [flat|nested] 23+ messages in thread
* [pbs-devel] [PATCH proxmox 07/18] api-macro: forbid flattened fields
2020-12-18 11:25 [pbs-devel] [PATCH proxmox 00/18] Optional Return Types and AllOf schema Wolfgang Bumiller
` (5 preceding siblings ...)
2020-12-18 11:25 ` [pbs-devel] [PATCH proxmox 06/18] api-macro: add 'flatten' to SerdeAttrib Wolfgang Bumiller
@ 2020-12-18 11:25 ` Wolfgang Bumiller
2020-12-18 11:25 ` [pbs-devel] [PATCH proxmox 08/18] api-macro: add more standard Maybe methods Wolfgang Bumiller
` (13 subsequent siblings)
20 siblings, 0 replies; 23+ messages in thread
From: Wolfgang Bumiller @ 2020-12-18 11:25 UTC (permalink / raw)
To: pbs-devel
They don't appear in the json data structure and therefore
should not be named separately in the schema. Structs with
flattened fields will become an `AllOf` schema instead.
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
---
proxmox-api-macro/src/api/structs.rs | 14 +++++++++++++-
1 file changed, 13 insertions(+), 1 deletion(-)
diff --git a/proxmox-api-macro/src/api/structs.rs b/proxmox-api-macro/src/api/structs.rs
index 71cdc8a..a101308 100644
--- a/proxmox-api-macro/src/api/structs.rs
+++ b/proxmox-api-macro/src/api/structs.rs
@@ -162,8 +162,20 @@ fn handle_regular_struct(attribs: JSONObject, stru: syn::ItemStruct) -> Result<T
}
};
+ if attrs.flatten {
+ if let Some(field) = schema_fields.remove(&name) {
+ error!(
+ field.0.span(),
+ "flattened field should not appear in schema, \
+ its name does not appear in serialized data",
+ );
+ }
+ }
+
match schema_fields.remove(&name) {
- Some(field_def) => handle_regular_field(field_def, field, false)?,
+ Some(field_def) => {
+ handle_regular_field(field_def, field, false)?;
+ }
None => {
let mut field_def = (
FieldName::new(name.clone(), span),
--
2.20.1
^ permalink raw reply [flat|nested] 23+ messages in thread
* [pbs-devel] [PATCH proxmox 08/18] api-macro: add more standard Maybe methods
2020-12-18 11:25 [pbs-devel] [PATCH proxmox 00/18] Optional Return Types and AllOf schema Wolfgang Bumiller
` (6 preceding siblings ...)
2020-12-18 11:25 ` [pbs-devel] [PATCH proxmox 07/18] api-macro: forbid flattened fields Wolfgang Bumiller
@ 2020-12-18 11:25 ` Wolfgang Bumiller
2020-12-18 11:25 ` [pbs-devel] [PATCH proxmox 09/18] api-macro: suport AllOf on structs Wolfgang Bumiller
` (12 subsequent siblings)
20 siblings, 0 replies; 23+ messages in thread
From: Wolfgang Bumiller @ 2020-12-18 11:25 UTC (permalink / raw)
To: pbs-devel
Note that any methods added there should be oriented around
`Option`.
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
---
proxmox-api-macro/src/util.rs | 21 +++++++++++++++++++++
1 file changed, 21 insertions(+)
diff --git a/proxmox-api-macro/src/util.rs b/proxmox-api-macro/src/util.rs
index e74e04b..87e6414 100644
--- a/proxmox-api-macro/src/util.rs
+++ b/proxmox-api-macro/src/util.rs
@@ -584,6 +584,12 @@ pub enum Maybe<T> {
None,
}
+impl<T> Default for Maybe<T> {
+ fn default() -> Self {
+ Maybe::None
+ }
+}
+
impl<T> Maybe<T> {
pub fn as_ref(&self) -> Maybe<&T> {
match self {
@@ -600,6 +606,13 @@ impl<T> Maybe<T> {
}
}
+ pub fn ok(self) -> Option<T> {
+ match self {
+ Maybe::Explicit(v) | Maybe::Derived(v) => Some(v),
+ Maybe::None => None,
+ }
+ }
+
pub fn ok_or_else<E, F>(self, other: F) -> Result<T, E>
where
F: FnOnce() -> E,
@@ -613,6 +626,14 @@ impl<T> Maybe<T> {
pub fn is_none(&self) -> bool {
matches!(self, Maybe::None)
}
+
+ pub fn is_explicit(&self) -> bool {
+ matches!(self, Maybe::Explicit(_))
+ }
+
+ pub fn take(&mut self) -> Self {
+ std::mem::take(self)
+ }
}
impl<T> Into<Option<T>> for Maybe<T> {
--
2.20.1
^ permalink raw reply [flat|nested] 23+ messages in thread
* [pbs-devel] [PATCH proxmox 09/18] api-macro: suport AllOf on structs
2020-12-18 11:25 [pbs-devel] [PATCH proxmox 00/18] Optional Return Types and AllOf schema Wolfgang Bumiller
` (7 preceding siblings ...)
2020-12-18 11:25 ` [pbs-devel] [PATCH proxmox 08/18] api-macro: add more standard Maybe methods Wolfgang Bumiller
@ 2020-12-18 11:25 ` Wolfgang Bumiller
2020-12-18 11:25 ` [pbs-devel] [PATCH proxmox 10/18] schema: ExtractValueDeserializer Wolfgang Bumiller
` (11 subsequent siblings)
20 siblings, 0 replies; 23+ messages in thread
From: Wolfgang Bumiller @ 2020-12-18 11:25 UTC (permalink / raw)
To: pbs-devel
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
---
proxmox-api-macro/src/api/mod.rs | 19 +++++
proxmox-api-macro/src/api/structs.rs | 123 ++++++++++++++++++++++++---
proxmox-api-macro/tests/allof.rs | 118 +++++++++++++++++++++++++
3 files changed, 246 insertions(+), 14 deletions(-)
create mode 100644 proxmox-api-macro/tests/allof.rs
diff --git a/proxmox-api-macro/src/api/mod.rs b/proxmox-api-macro/src/api/mod.rs
index 815e167..3cf1309 100644
--- a/proxmox-api-macro/src/api/mod.rs
+++ b/proxmox-api-macro/src/api/mod.rs
@@ -397,6 +397,11 @@ impl SchemaObject {
}
}
+ #[inline]
+ pub fn is_empty(&self) -> bool {
+ self.properties_.is_empty()
+ }
+
#[inline]
fn properties_mut(&mut self) -> &mut [(FieldName, bool, Schema)] {
&mut self.properties_
@@ -458,6 +463,20 @@ impl SchemaObject {
.find(|p| p.0.as_ident_str() == key)
}
+ fn remove_property_by_ident(&mut self, key: &str) -> bool {
+ match self
+ .properties_
+ .iter()
+ .position(|(name, _, _)| name.as_ident_str() == key)
+ {
+ Some(index) => {
+ self.properties_.remove(index);
+ true
+ }
+ None => false,
+ }
+ }
+
fn extend_properties(&mut self, new_fields: Vec<(FieldName, bool, Schema)>) {
self.properties_.extend(new_fields);
self.sort_properties();
diff --git a/proxmox-api-macro/src/api/structs.rs b/proxmox-api-macro/src/api/structs.rs
index a101308..db6a290 100644
--- a/proxmox-api-macro/src/api/structs.rs
+++ b/proxmox-api-macro/src/api/structs.rs
@@ -21,7 +21,7 @@ use quote::quote_spanned;
use super::Schema;
use crate::api::{self, SchemaItem};
use crate::serde;
-use crate::util::{self, FieldName, JSONObject};
+use crate::util::{self, FieldName, JSONObject, Maybe};
pub fn handle_struct(attribs: JSONObject, stru: syn::ItemStruct) -> Result<TokenStream, Error> {
match &stru.fields {
@@ -142,6 +142,9 @@ fn handle_regular_struct(attribs: JSONObject, stru: syn::ItemStruct) -> Result<T
let container_attrs = serde::ContainerAttrib::try_from(&stru.attrs[..])?;
+ let mut all_of_schemas = TokenStream::new();
+ let mut to_remove = Vec::new();
+
if let syn::Fields::Named(ref fields) = &stru.fields {
for field in &fields.named {
let attrs = serde::SerdeAttrib::try_from(&field.attrs[..])?;
@@ -162,19 +165,34 @@ fn handle_regular_struct(attribs: JSONObject, stru: syn::ItemStruct) -> Result<T
}
};
- if attrs.flatten {
- if let Some(field) = schema_fields.remove(&name) {
- error!(
- field.0.span(),
- "flattened field should not appear in schema, \
- its name does not appear in serialized data",
- );
- }
- }
-
match schema_fields.remove(&name) {
Some(field_def) => {
+ if attrs.flatten {
+ to_remove.push(name.clone());
+
+ let name = &field_def.0;
+ let optional = &field_def.1;
+ let schema = &field_def.2;
+ if schema.description.is_explicit() {
+ error!(
+ name.span(),
+ "flattened field should not have a description, \
+ it does not appear in serialized data as a field",
+ );
+ }
+
+ if *optional {
+ error!(name.span(), "optional flattened fields are not supported");
+ }
+ }
+
handle_regular_field(field_def, field, false)?;
+
+ if attrs.flatten {
+ all_of_schemas.extend(quote::quote! {&});
+ field_def.2.to_schema(&mut all_of_schemas)?;
+ all_of_schemas.extend(quote::quote! {,});
+ }
}
None => {
let mut field_def = (
@@ -183,7 +201,15 @@ fn handle_regular_struct(attribs: JSONObject, stru: syn::ItemStruct) -> Result<T
Schema::blank(span),
);
handle_regular_field(&mut field_def, field, true)?;
- new_fields.push(field_def);
+
+ if attrs.flatten {
+ all_of_schemas.extend(quote::quote! {&});
+ field_def.2.to_schema(&mut all_of_schemas)?;
+ all_of_schemas.extend(quote::quote! {,});
+ to_remove.push(name.clone());
+ } else {
+ new_fields.push(field_def);
+ }
}
}
}
@@ -200,14 +226,83 @@ fn handle_regular_struct(attribs: JSONObject, stru: syn::ItemStruct) -> Result<T
);
}
- // add the fields we derived:
if let api::SchemaItem::Object(ref mut obj) = &mut schema.item {
+ // remove flattened fields
+ for field in to_remove {
+ if !obj.remove_property_by_ident(&field) {
+ error!(
+ schema.span,
+ "internal error: failed to remove property {:?} from object schema", field,
+ );
+ }
+ }
+
+ // add derived fields
obj.extend_properties(new_fields);
} else {
panic!("handle_regular_struct with non-object schema");
}
- finish_schema(schema, &stru, &stru.ident)
+ if all_of_schemas.is_empty() {
+ finish_schema(schema, &stru, &stru.ident)
+ } else {
+ let name = &stru.ident;
+
+ // take out the inner object schema's description
+ let description = match schema.description.take().ok() {
+ Some(description) => description,
+ None => {
+ error!(schema.span, "missing description on api type struct");
+ syn::LitStr::new("<missing description>", schema.span)
+ }
+ };
+ // and replace it with a "dummy"
+ schema.description = Maybe::Derived(syn::LitStr::new(
+ &format!("<INNER: {}>", description.value()),
+ description.span(),
+ ));
+
+ // now check if it even has any fields
+ let has_fields = match &schema.item {
+ api::SchemaItem::Object(obj) => !obj.is_empty(),
+ _ => panic!("object schema is not an object schema?"),
+ };
+
+ let (inner_schema, inner_schema_ref) = if has_fields {
+ // if it does, we need to create an "inner" schema to merge into the AllOf schema
+ let obj_schema = {
+ let mut ts = TokenStream::new();
+ schema.to_schema(&mut ts)?;
+ ts
+ };
+
+ (
+ quote_spanned!(name.span() =>
+ const INNER_API_SCHEMA: ::proxmox::api::schema::Schema = #obj_schema;
+ ),
+ quote_spanned!(name.span() => &Self::INNER_API_SCHEMA,),
+ )
+ } else {
+ // otherwise it stays empty
+ (TokenStream::new(), TokenStream::new())
+ };
+
+ Ok(quote_spanned!(name.span() =>
+ #stru
+ impl #name {
+ #inner_schema
+ pub const API_SCHEMA: ::proxmox::api::schema::Schema =
+ ::proxmox::api::schema::AllOfSchema::new(
+ #description,
+ &[
+ #inner_schema_ref
+ #all_of_schemas
+ ],
+ )
+ .schema();
+ }
+ ))
+ }
}
/// Field handling:
diff --git a/proxmox-api-macro/tests/allof.rs b/proxmox-api-macro/tests/allof.rs
new file mode 100644
index 0000000..56e86d7
--- /dev/null
+++ b/proxmox-api-macro/tests/allof.rs
@@ -0,0 +1,118 @@
+//! Testing the `AllOf` schema on structs and methods.
+
+use proxmox::api::schema;
+use proxmox_api_macro::api;
+
+use serde::{Deserialize, Serialize};
+
+pub const NAME_SCHEMA: schema::Schema = schema::StringSchema::new("Name.").schema();
+pub const VALUE_SCHEMA: schema::Schema = schema::IntegerSchema::new("Value.").schema();
+pub const INDEX_SCHEMA: schema::Schema = schema::IntegerSchema::new("Index.").schema();
+pub const TEXT_SCHEMA: schema::Schema = schema::StringSchema::new("Text.").schema();
+
+#[api(
+ properties: {
+ name: { schema: NAME_SCHEMA },
+ value: { schema: VALUE_SCHEMA },
+ }
+)]
+/// Name and value.
+#[derive(Deserialize, Serialize)]
+struct NameValue {
+ name: String,
+ value: u64,
+}
+
+#[api(
+ properties: {
+ index: { schema: INDEX_SCHEMA },
+ text: { schema: TEXT_SCHEMA },
+ }
+)]
+/// Index and text.
+#[derive(Deserialize, Serialize)]
+struct IndexText {
+ index: u64,
+ text: String,
+}
+
+#[api(
+ properties: {
+ nv: { type: NameValue },
+ it: { type: IndexText },
+ },
+)]
+/// Name, value, index and text.
+#[derive(Deserialize, Serialize)]
+struct Nvit {
+ #[serde(flatten)]
+ nv: NameValue,
+
+ #[serde(flatten)]
+ it: IndexText,
+}
+
+#[test]
+fn test_nvit() {
+ const TEST_NAME_VALUE_SCHEMA: ::proxmox::api::schema::Schema =
+ ::proxmox::api::schema::ObjectSchema::new(
+ "Name and value.",
+ &[
+ ("name", false, &NAME_SCHEMA),
+ ("value", false, &VALUE_SCHEMA),
+ ],
+ )
+ .schema();
+
+ const TEST_SCHEMA: ::proxmox::api::schema::Schema = ::proxmox::api::schema::AllOfSchema::new(
+ "Name, value, index and text.",
+ &[&TEST_NAME_VALUE_SCHEMA, &IndexText::API_SCHEMA],
+ )
+ .schema();
+
+ assert_eq!(TEST_SCHEMA, Nvit::API_SCHEMA);
+}
+
+#[api(
+ properties: {
+ nv: { type: NameValue },
+ it: { type: IndexText },
+ },
+)]
+/// Extra Schema
+#[derive(Deserialize, Serialize)]
+struct WithExtra {
+ #[serde(flatten)]
+ nv: NameValue,
+
+ #[serde(flatten)]
+ it: IndexText,
+
+ /// Extra field.
+ extra: String,
+}
+
+#[test]
+fn test_extra() {
+ const INNER_SCHEMA: ::proxmox::api::schema::Schema = ::proxmox::api::schema::ObjectSchema::new(
+ "<INNER: Extra Schema>",
+ &[(
+ "extra",
+ false,
+ &::proxmox::api::schema::StringSchema::new("Extra field.").schema(),
+ )],
+ )
+ .schema();
+
+ const TEST_SCHEMA: ::proxmox::api::schema::Schema = ::proxmox::api::schema::AllOfSchema::new(
+ "Extra Schema",
+ &[
+ &INNER_SCHEMA,
+ &NameValue::API_SCHEMA,
+ &IndexText::API_SCHEMA,
+ ],
+ )
+ .schema();
+
+ assert_eq!(TEST_SCHEMA, WithExtra::API_SCHEMA);
+}
--
2.20.1
^ permalink raw reply [flat|nested] 23+ messages in thread
* [pbs-devel] [PATCH proxmox 10/18] schema: ExtractValueDeserializer
2020-12-18 11:25 [pbs-devel] [PATCH proxmox 00/18] Optional Return Types and AllOf schema Wolfgang Bumiller
` (8 preceding siblings ...)
2020-12-18 11:25 ` [pbs-devel] [PATCH proxmox 09/18] api-macro: suport AllOf on structs Wolfgang Bumiller
@ 2020-12-18 11:25 ` Wolfgang Bumiller
2020-12-18 11:25 ` [pbs-devel] [PATCH proxmox 11/18] api-macro: object schema entry tuple -> struct Wolfgang Bumiller
` (10 subsequent siblings)
20 siblings, 0 replies; 23+ messages in thread
From: Wolfgang Bumiller @ 2020-12-18 11:25 UTC (permalink / raw)
To: pbs-devel
A deserializer which takes an `&mut Value` and an object
schema reference and deserializes by extracting (removing)
the values from the references serde Value.
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
---
proxmox/src/api/de.rs | 270 +++++++++++++++++++++++++++++++++++++++++
proxmox/src/api/mod.rs | 2 +
2 files changed, 272 insertions(+)
create mode 100644 proxmox/src/api/de.rs
diff --git a/proxmox/src/api/de.rs b/proxmox/src/api/de.rs
new file mode 100644
index 0000000..b48fd85
--- /dev/null
+++ b/proxmox/src/api/de.rs
@@ -0,0 +1,270 @@
+//! Partial object deserialization by extracting object portions from a Value using an api schema.
+
+use std::fmt;
+
+use serde::de::{self, IntoDeserializer, Visitor};
+use serde_json::Value;
+
+use crate::api::schema::{ObjectSchema, ObjectSchemaType, Schema};
+
+pub struct Error {
+ inner: anyhow::Error,
+}
+
+impl fmt::Debug for Error {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ fmt::Debug::fmt(&self.inner, f)
+ }
+}
+
+impl fmt::Display for Error {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ fmt::Display::fmt(&self.inner, f)
+ }
+}
+
+impl std::error::Error for Error {}
+
+impl serde::de::Error for Error {
+ fn custom<T: fmt::Display>(msg: T) -> Self {
+ Self {
+ inner: anyhow::format_err!("{}", msg),
+ }
+ }
+}
+
+impl From<serde_json::Error> for Error {
+ fn from(inner: serde_json::Error) -> Self {
+ Error {
+ inner: inner.into(),
+ }
+ }
+}
+
+pub struct ExtractValueDeserializer<'o> {
+ object: &'o mut serde_json::Map<String, Value>,
+ schema: &'static ObjectSchema,
+}
+
+impl<'o> ExtractValueDeserializer<'o> {
+ pub fn try_new(
+ object: &'o mut serde_json::Map<String, Value>,
+ schema: &'static Schema,
+ ) -> Option<Self> {
+ match schema {
+ Schema::Object(schema) => Some(Self { object, schema }),
+ _ => None,
+ }
+ }
+}
+
+macro_rules! deserialize_non_object {
+ ($name:ident) => {
+ fn $name<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
+ where
+ V: Visitor<'de>,
+ {
+ Err(de::Error::custom(
+ "deserializing partial object into type which is not an object",
+ ))
+ }
+ };
+ ($name:ident ( $($args:tt)* )) => {
+ fn $name<V>(self, $($args)*, _visitor: V) -> Result<V::Value, Self::Error>
+ where
+ V: Visitor<'de>,
+ {
+ Err(de::Error::custom(
+ "deserializing partial object into type which is not an object",
+ ))
+ }
+ };
+}
+
+impl<'de> de::Deserializer<'de> for ExtractValueDeserializer<'de> {
+ type Error = Error;
+
+ fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Error>
+ where
+ V: Visitor<'de>,
+ {
+ self.deserialize_map(visitor)
+ }
+
+ fn deserialize_map<V>(self, visitor: V) -> Result<V::Value, Error>
+ where
+ V: Visitor<'de>,
+ {
+ visitor.visit_map(MapAccess::<'de>::new(
+ self.object,
+ self.schema.properties().map(|(name, _, _)| *name),
+ ))
+ }
+
+ fn deserialize_struct<V>(
+ self,
+ _name: &'static str,
+ _fields: &'static [&'static str],
+ visitor: V,
+ ) -> Result<V::Value, Error>
+ where
+ V: Visitor<'de>,
+ {
+ visitor.visit_map(MapAccess::<'de>::new(
+ self.object,
+ self.schema.properties().map(|(name, _, _)| *name),
+ ))
+ }
+
+ deserialize_non_object!(deserialize_i8);
+ deserialize_non_object!(deserialize_i16);
+ deserialize_non_object!(deserialize_i32);
+ deserialize_non_object!(deserialize_i64);
+ deserialize_non_object!(deserialize_u8);
+ deserialize_non_object!(deserialize_u16);
+ deserialize_non_object!(deserialize_u32);
+ deserialize_non_object!(deserialize_u64);
+ deserialize_non_object!(deserialize_f32);
+ deserialize_non_object!(deserialize_f64);
+ deserialize_non_object!(deserialize_char);
+ deserialize_non_object!(deserialize_bool);
+ deserialize_non_object!(deserialize_str);
+ deserialize_non_object!(deserialize_string);
+ deserialize_non_object!(deserialize_bytes);
+ deserialize_non_object!(deserialize_byte_buf);
+ deserialize_non_object!(deserialize_option);
+ deserialize_non_object!(deserialize_seq);
+ deserialize_non_object!(deserialize_unit);
+ deserialize_non_object!(deserialize_identifier);
+ deserialize_non_object!(deserialize_unit_struct(_: &'static str));
+ deserialize_non_object!(deserialize_newtype_struct(_: &'static str));
+ deserialize_non_object!(deserialize_tuple(_: usize));
+ deserialize_non_object!(deserialize_tuple_struct(_: &'static str, _: usize));
+ deserialize_non_object!(deserialize_enum(
+ _: &'static str,
+ _: &'static [&'static str]
+ ));
+
+ fn deserialize_ignored_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+ where
+ V: Visitor<'de>,
+ {
+ visitor.visit_unit()
+ }
+}
+
+struct MapAccess<'o, I> {
+ object: &'o mut serde_json::Map<String, Value>,
+ iter: I,
+ value: Option<Value>,
+}
+
+impl<'o, I> MapAccess<'o, I>
+where
+ I: Iterator<Item = &'static str>,
+{
+ fn new(object: &'o mut serde_json::Map<String, Value>, iter: I) -> Self {
+ Self {
+ object,
+ iter,
+ value: None,
+ }
+ }
+}
+
+impl<'de, I> de::MapAccess<'de> for MapAccess<'de, I>
+where
+ I: Iterator<Item = &'static str>,
+{
+ type Error = Error;
+
+ fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>, Error>
+ where
+ K: de::DeserializeSeed<'de>,
+ {
+ loop {
+ return match self.iter.next() {
+ Some(key) => match self.object.remove(key) {
+ Some(value) => {
+ self.value = Some(value);
+ seed.deserialize(key.into_deserializer()).map(Some)
+ }
+ None => continue,
+ },
+ None => Ok(None),
+ };
+ }
+ }
+
+ fn next_value_seed<V>(&mut self, seed: V) -> Result<V::Value, Error>
+ where
+ V: de::DeserializeSeed<'de>,
+ {
+ match self.value.take() {
+ Some(value) => seed.deserialize(value).map_err(Error::from),
+ None => Err(de::Error::custom("value is missing")),
+ }
+ }
+}
+
+#[test]
+fn test_extraction() {
+ use serde::Deserialize;
+
+ use crate::api::schema::StringSchema;
+
+ #[derive(Deserialize)]
+ struct Foo {
+ foo1: String,
+ foo2: String,
+ }
+
+ const SIMPLE_STRING: Schema = StringSchema::new("simple").schema();
+ const FOO_SCHEMA: Schema = ObjectSchema::new(
+ "A Foo",
+ &[
+ ("foo1", false, &SIMPLE_STRING),
+ ("foo2", false, &SIMPLE_STRING),
+ ],
+ )
+ .schema();
+
+ #[derive(Deserialize)]
+ struct Bar {
+ bar1: String,
+ bar2: String,
+ }
+
+ const BAR_SCHEMA: Schema = ObjectSchema::new(
+ "A Bar",
+ &[
+ ("bar1", false, &SIMPLE_STRING),
+ ("bar2", false, &SIMPLE_STRING),
+ ],
+ )
+ .schema();
+
+ let mut data = serde_json::json!({
+ "foo1": "hey1",
+ "foo2": "hey2",
+ "bar1": "there1",
+ "bar2": "there2",
+ });
+
+ let data = data.as_object_mut().unwrap();
+
+ let foo: Foo =
+ Foo::deserialize(ExtractValueDeserializer::try_new(data, &FOO_SCHEMA).unwrap()).unwrap();
+
+ assert!(data.remove("foo1").is_none());
+ assert!(data.remove("foo2").is_none());
+ assert_eq!(foo.foo1, "hey1");
+ assert_eq!(foo.foo2, "hey2");
+
+ let bar =
+ Bar::deserialize(ExtractValueDeserializer::try_new(data, &BAR_SCHEMA).unwrap()).unwrap();
+
+ assert!(data.is_empty());
+ assert_eq!(bar.bar1, "there1");
+ assert_eq!(bar.bar2, "there2");
+}
diff --git a/proxmox/src/api/mod.rs b/proxmox/src/api/mod.rs
index b0b8333..8c6f597 100644
--- a/proxmox/src/api/mod.rs
+++ b/proxmox/src/api/mod.rs
@@ -48,3 +48,5 @@ pub use router::{
#[cfg(feature = "cli")]
pub mod cli;
+
+pub mod de;
--
2.20.1
^ permalink raw reply [flat|nested] 23+ messages in thread
* [pbs-devel] [PATCH proxmox 11/18] api-macro: object schema entry tuple -> struct
2020-12-18 11:25 [pbs-devel] [PATCH proxmox 00/18] Optional Return Types and AllOf schema Wolfgang Bumiller
` (9 preceding siblings ...)
2020-12-18 11:25 ` [pbs-devel] [PATCH proxmox 10/18] schema: ExtractValueDeserializer Wolfgang Bumiller
@ 2020-12-18 11:25 ` Wolfgang Bumiller
2020-12-18 11:26 ` [pbs-devel] [PATCH proxmox 12/18] api-macro: more tuple refactoring Wolfgang Bumiller
` (9 subsequent siblings)
20 siblings, 0 replies; 23+ messages in thread
From: Wolfgang Bumiller @ 2020-12-18 11:25 UTC (permalink / raw)
To: pbs-devel
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
---
proxmox-api-macro/src/api/method.rs | 79 +++++++++++++---------------
proxmox-api-macro/src/api/mod.rs | 51 +++++++++++-------
proxmox-api-macro/src/api/structs.rs | 36 ++++++-------
3 files changed, 89 insertions(+), 77 deletions(-)
diff --git a/proxmox-api-macro/src/api/method.rs b/proxmox-api-macro/src/api/method.rs
index 3e85d8c..d7b0021 100644
--- a/proxmox-api-macro/src/api/method.rs
+++ b/proxmox-api-macro/src/api/method.rs
@@ -263,16 +263,14 @@ fn handle_function_signature(
};
// For any named type which exists on the function signature...
- if let Some((_ident, optional, ref mut schema)) =
- input_schema.find_obj_property_by_ident_mut(&pat.ident.to_string())
- {
+ if let Some(entry) = input_schema.find_obj_property_by_ident_mut(&pat.ident.to_string()) {
// try to infer the type in the schema if it is not specified explicitly:
- let is_option = util::infer_type(schema, &*pat_type.ty)?;
- let has_default = schema.find_schema_property("default").is_some();
- if !is_option && *optional && !has_default {
+ let is_option = util::infer_type(&mut entry.schema, &*pat_type.ty)?;
+ let has_default = entry.schema.find_schema_property("default").is_some();
+ if !is_option && entry.optional && !has_default {
error!(pat_type => "optional types need a default or be an Option<T>");
}
- if has_default && !*optional {
+ if has_default && !entry.optional {
error!(pat_type => "non-optional parameter cannot have a default");
}
} else {
@@ -313,40 +311,39 @@ fn handle_function_signature(
// bail out with an error.
let pat_ident = pat.ident.unraw();
let mut param_name: FieldName = pat_ident.clone().into();
- let param_type = if let Some((name, optional, schema)) =
- input_schema.find_obj_property_by_ident(&pat_ident.to_string())
- {
- if let SchemaItem::Inferred(span) = &schema.item {
- bail!(*span, "failed to infer type");
- }
- param_name = name.clone();
- // Found an explicit parameter: extract it:
- ParameterType::Other(&pat_type.ty, *optional, schema)
- } else if is_api_method_type(&pat_type.ty) {
- if api_method_param.is_some() {
- error!(pat_type => "multiple ApiMethod parameters found");
- continue;
- }
- api_method_param = Some(param_list.len());
- ParameterType::ApiMethod
- } else if is_rpc_env_type(&pat_type.ty) {
- if rpc_env_param.is_some() {
- error!(pat_type => "multiple RpcEnvironment parameters found");
- continue;
- }
- rpc_env_param = Some(param_list.len());
- ParameterType::RpcEnv
- } else if is_value_type(&pat_type.ty) {
- if value_param.is_some() {
- error!(pat_type => "multiple additional Value parameters found");
+ let param_type =
+ if let Some(entry) = input_schema.find_obj_property_by_ident(&pat_ident.to_string()) {
+ if let SchemaItem::Inferred(span) = &entry.schema.item {
+ bail!(*span, "failed to infer type");
+ }
+ param_name = entry.name.clone();
+ // Found an explicit parameter: extract it:
+ ParameterType::Other(&pat_type.ty, entry.optional, &entry.schema)
+ } else if is_api_method_type(&pat_type.ty) {
+ if api_method_param.is_some() {
+ error!(pat_type => "multiple ApiMethod parameters found");
+ continue;
+ }
+ api_method_param = Some(param_list.len());
+ ParameterType::ApiMethod
+ } else if is_rpc_env_type(&pat_type.ty) {
+ if rpc_env_param.is_some() {
+ error!(pat_type => "multiple RpcEnvironment parameters found");
+ continue;
+ }
+ rpc_env_param = Some(param_list.len());
+ ParameterType::RpcEnv
+ } else if is_value_type(&pat_type.ty) {
+ if value_param.is_some() {
+ error!(pat_type => "multiple additional Value parameters found");
+ continue;
+ }
+ value_param = Some(param_list.len());
+ ParameterType::Value
+ } else {
+ error!(&pat_ident => "unexpected parameter {:?}", pat_ident.to_string());
continue;
- }
- value_param = Some(param_list.len());
- ParameterType::Value
- } else {
- error!(&pat_ident => "unexpected parameter {:?}", pat_ident.to_string());
- continue;
- };
+ };
param_list.push((param_name, param_type));
}
@@ -594,7 +591,7 @@ impl<'a> DefaultParameters<'a> {
fn get_default(&self, param_tokens: TokenStream) -> Result<syn::Expr, syn::Error> {
let param_name: syn::LitStr = syn::parse2(param_tokens)?;
match self.0.find_obj_property_by_ident(¶m_name.value()) {
- Some((_ident, _optional, schema)) => match schema.find_schema_property("default") {
+ Some(entry) => match entry.schema.find_schema_property("default") {
Some(def) => Ok(def.clone()),
None => bail!(param_name => "no default found in schema"),
},
diff --git a/proxmox-api-macro/src/api/mod.rs b/proxmox-api-macro/src/api/mod.rs
index 3cf1309..4690654 100644
--- a/proxmox-api-macro/src/api/mod.rs
+++ b/proxmox-api-macro/src/api/mod.rs
@@ -176,15 +176,12 @@ impl Schema {
}
}
- fn find_obj_property_by_ident(&self, key: &str) -> Option<&(FieldName, bool, Schema)> {
+ fn find_obj_property_by_ident(&self, key: &str) -> Option<&ObjectEntry> {
self.as_object()
.and_then(|obj| obj.find_property_by_ident(key))
}
- fn find_obj_property_by_ident_mut(
- &mut self,
- key: &str,
- ) -> Option<&mut (FieldName, bool, Schema)> {
+ fn find_obj_property_by_ident_mut(&mut self, key: &str) -> Option<&mut ObjectEntry> {
self.as_object_mut()
.and_then(|obj| obj.find_property_by_ident_mut(key))
}
@@ -384,10 +381,26 @@ impl SchemaItem {
}
}
+pub struct ObjectEntry {
+ pub name: FieldName,
+ pub optional: bool,
+ pub schema: Schema,
+}
+
+impl ObjectEntry {
+ pub fn new(name: FieldName, optional: bool, schema: Schema) -> Self {
+ Self {
+ name,
+ optional,
+ schema,
+ }
+ }
+}
+
#[derive(Default)]
/// Contains a sorted list of properties:
pub struct SchemaObject {
- properties_: Vec<(FieldName, bool, Schema)>,
+ properties_: Vec<ObjectEntry>,
}
impl SchemaObject {
@@ -403,12 +416,12 @@ impl SchemaObject {
}
#[inline]
- fn properties_mut(&mut self) -> &mut [(FieldName, bool, Schema)] {
+ fn properties_mut(&mut self) -> &mut [ObjectEntry] {
&mut self.properties_
}
fn sort_properties(&mut self) {
- self.properties_.sort_by(|a, b| (a.0).cmp(&b.0));
+ self.properties_.sort_by(|a, b| (a.name).cmp(&b.name));
}
fn try_extract_from(obj: &mut JSONObject) -> Result<Self, syn::Error> {
@@ -432,7 +445,7 @@ impl SchemaObject {
.transpose()?
.unwrap_or(false);
- properties.push((key, optional, schema.try_into()?));
+ properties.push(ObjectEntry::new(key, optional, schema.try_into()?));
Ok(properties)
},
@@ -444,30 +457,32 @@ impl SchemaObject {
fn to_schema_inner(&self, ts: &mut TokenStream) -> Result<(), Error> {
for element in self.properties_.iter() {
- let key = element.0.as_str();
- let optional = element.1;
+ let key = element.name.as_str();
+ let optional = element.optional;
let mut schema = TokenStream::new();
- element.2.to_schema(&mut schema)?;
+ element.schema.to_schema(&mut schema)?;
ts.extend(quote! { (#key, #optional, &#schema), });
}
Ok(())
}
- fn find_property_by_ident(&self, key: &str) -> Option<&(FieldName, bool, Schema)> {
- self.properties_.iter().find(|p| p.0.as_ident_str() == key)
+ fn find_property_by_ident(&self, key: &str) -> Option<&ObjectEntry> {
+ self.properties_
+ .iter()
+ .find(|p| p.name.as_ident_str() == key)
}
- fn find_property_by_ident_mut(&mut self, key: &str) -> Option<&mut (FieldName, bool, Schema)> {
+ fn find_property_by_ident_mut(&mut self, key: &str) -> Option<&mut ObjectEntry> {
self.properties_
.iter_mut()
- .find(|p| p.0.as_ident_str() == key)
+ .find(|p| p.name.as_ident_str() == key)
}
fn remove_property_by_ident(&mut self, key: &str) -> bool {
match self
.properties_
.iter()
- .position(|(name, _, _)| name.as_ident_str() == key)
+ .position(|entry| entry.name.as_ident_str() == key)
{
Some(index) => {
self.properties_.remove(index);
@@ -477,7 +492,7 @@ impl SchemaObject {
}
}
- fn extend_properties(&mut self, new_fields: Vec<(FieldName, bool, Schema)>) {
+ fn extend_properties(&mut self, new_fields: Vec<ObjectEntry>) {
self.properties_.extend(new_fields);
self.sort_properties();
}
diff --git a/proxmox-api-macro/src/api/structs.rs b/proxmox-api-macro/src/api/structs.rs
index db6a290..e55378d 100644
--- a/proxmox-api-macro/src/api/structs.rs
+++ b/proxmox-api-macro/src/api/structs.rs
@@ -19,7 +19,7 @@ use proc_macro2::{Ident, Span, TokenStream};
use quote::quote_spanned;
use super::Schema;
-use crate::api::{self, SchemaItem};
+use crate::api::{self, ObjectEntry, SchemaItem};
use crate::serde;
use crate::util::{self, FieldName, JSONObject, Maybe};
@@ -126,19 +126,19 @@ fn handle_regular_struct(attribs: JSONObject, stru: syn::ItemStruct) -> Result<T
//
// NOTE: We remove references we're "done with" and in the end fail with a list of extraneous
// fields if there are any.
- let mut schema_fields: HashMap<String, &mut (FieldName, bool, Schema)> = HashMap::new();
+ let mut schema_fields: HashMap<String, &mut ObjectEntry> = HashMap::new();
// We also keep a reference to the SchemaObject around since we derive missing fields
// automatically.
if let api::SchemaItem::Object(ref mut obj) = &mut schema.item {
for field in obj.properties_mut() {
- schema_fields.insert(field.0.as_str().to_string(), field);
+ schema_fields.insert(field.name.as_str().to_string(), field);
}
} else {
error!(schema.span, "structs need an object schema");
}
- let mut new_fields: Vec<(FieldName, bool, Schema)> = Vec::new();
+ let mut new_fields: Vec<ObjectEntry> = Vec::new();
let container_attrs = serde::ContainerAttrib::try_from(&stru.attrs[..])?;
@@ -170,19 +170,19 @@ fn handle_regular_struct(attribs: JSONObject, stru: syn::ItemStruct) -> Result<T
if attrs.flatten {
to_remove.push(name.clone());
- let name = &field_def.0;
- let optional = &field_def.1;
- let schema = &field_def.2;
- if schema.description.is_explicit() {
+ if field_def.schema.description.is_explicit() {
error!(
- name.span(),
+ field_def.name.span(),
"flattened field should not have a description, \
it does not appear in serialized data as a field",
);
}
- if *optional {
- error!(name.span(), "optional flattened fields are not supported");
+ if field_def.optional {
+ error!(
+ field_def.name.span(),
+ "optional flattened fields are not supported"
+ );
}
}
@@ -190,12 +190,12 @@ fn handle_regular_struct(attribs: JSONObject, stru: syn::ItemStruct) -> Result<T
if attrs.flatten {
all_of_schemas.extend(quote::quote! {&});
- field_def.2.to_schema(&mut all_of_schemas)?;
+ field_def.schema.to_schema(&mut all_of_schemas)?;
all_of_schemas.extend(quote::quote! {,});
}
}
None => {
- let mut field_def = (
+ let mut field_def = ObjectEntry::new(
FieldName::new(name.clone(), span),
false,
Schema::blank(span),
@@ -204,7 +204,7 @@ fn handle_regular_struct(attribs: JSONObject, stru: syn::ItemStruct) -> Result<T
if attrs.flatten {
all_of_schemas.extend(quote::quote! {&});
- field_def.2.to_schema(&mut all_of_schemas)?;
+ field_def.schema.to_schema(&mut all_of_schemas)?;
all_of_schemas.extend(quote::quote! {,});
to_remove.push(name.clone());
} else {
@@ -309,11 +309,11 @@ fn handle_regular_struct(attribs: JSONObject, stru: syn::ItemStruct) -> Result<T
///
/// For each field we derive the description from doc-attributes if available.
fn handle_regular_field(
- field_def: &mut (FieldName, bool, Schema),
+ field_def: &mut ObjectEntry,
field: &syn::Field,
derived: bool, // whether this field was missing in the schema
) -> Result<(), Error> {
- let schema: &mut Schema = &mut field_def.2;
+ let schema: &mut Schema = &mut field_def.schema;
if schema.description.is_none() {
let (doc_comment, doc_span) = util::get_doc_comments(&field.attrs)?;
@@ -324,8 +324,8 @@ fn handle_regular_field(
if is_option_type(&field.ty) {
if derived {
- field_def.1 = true;
- } else if !field_def.1 {
+ field_def.optional = true;
+ } else if !field_def.optional {
error!(&field.ty => "non-optional Option type?");
}
}
--
2.20.1
^ permalink raw reply [flat|nested] 23+ messages in thread
* [pbs-devel] [PATCH proxmox 12/18] api-macro: more tuple refactoring
2020-12-18 11:25 [pbs-devel] [PATCH proxmox 00/18] Optional Return Types and AllOf schema Wolfgang Bumiller
` (10 preceding siblings ...)
2020-12-18 11:25 ` [pbs-devel] [PATCH proxmox 11/18] api-macro: object schema entry tuple -> struct Wolfgang Bumiller
@ 2020-12-18 11:26 ` Wolfgang Bumiller
2020-12-18 11:26 ` [pbs-devel] [PATCH proxmox 13/18] api-macro: factor parameter extraction into a function Wolfgang Bumiller
` (8 subsequent siblings)
20 siblings, 0 replies; 23+ messages in thread
From: Wolfgang Bumiller @ 2020-12-18 11:26 UTC (permalink / raw)
To: pbs-devel
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
---
proxmox-api-macro/src/api/method.rs | 30 ++++++++++++++++++-----------
1 file changed, 19 insertions(+), 11 deletions(-)
diff --git a/proxmox-api-macro/src/api/method.rs b/proxmox-api-macro/src/api/method.rs
index d7b0021..3d6e9ed 100644
--- a/proxmox-api-macro/src/api/method.rs
+++ b/proxmox-api-macro/src/api/method.rs
@@ -17,7 +17,7 @@ use syn::spanned::Spanned;
use syn::visit_mut::{self, VisitMut};
use syn::Ident;
-use super::{Schema, SchemaItem};
+use super::{ObjectEntry, Schema, SchemaItem};
use crate::util::{self, FieldName, JSONObject, JSONValue, Maybe};
/// A return type in a schema can have an `optional` flag. Other than that it is just a regular
@@ -218,7 +218,12 @@ enum ParameterType<'a> {
Value,
ApiMethod,
RpcEnv,
- Other(&'a syn::Type, bool, &'a Schema),
+ Normal(NormalParameter<'a>),
+}
+
+struct NormalParameter<'a> {
+ ty: &'a syn::Type,
+ entry: &'a ObjectEntry,
}
fn check_input_type(input: &syn::FnArg) -> Result<(&syn::PatType, &syn::PatIdent), syn::Error> {
@@ -318,7 +323,10 @@ fn handle_function_signature(
}
param_name = entry.name.clone();
// Found an explicit parameter: extract it:
- ParameterType::Other(&pat_type.ty, entry.optional, &entry.schema)
+ ParameterType::Normal(NormalParameter {
+ ty: &pat_type.ty,
+ entry: &entry,
+ })
} else if is_api_method_type(&pat_type.ty) {
if api_method_param.is_some() {
error!(pat_type => "multiple ApiMethod parameters found");
@@ -449,7 +457,7 @@ fn create_wrapper_function(
ParameterType::Value => args.extend(quote_spanned! { span => input_params, }),
ParameterType::ApiMethod => args.extend(quote_spanned! { span => api_method_param, }),
ParameterType::RpcEnv => args.extend(quote_spanned! { span => rpc_env_param, }),
- ParameterType::Other(ty, optional, schema) => {
+ ParameterType::Normal(param) => {
let name_str = syn::LitStr::new(name.as_str(), span);
let arg_name =
Ident::new(&format!("input_arg_{}", name.as_ident().to_string()), span);
@@ -462,8 +470,8 @@ fn create_wrapper_function(
.map(::serde_json::from_value)
.transpose()?
});
- let default_value = schema.find_schema_property("default");
- if !optional {
+ let default_value = param.entry.schema.find_schema_property("default");
+ if !param.entry.optional {
// Non-optional types need to be extracted out of the option though (unless
// they have a default):
//
@@ -477,7 +485,7 @@ fn create_wrapper_function(
))?
});
}
- let no_option_type = util::is_option_type(ty).is_none();
+ let no_option_type = util::is_option_type(param.ty).is_none();
if let Some(def) = &default_value {
let name_uc = name.as_ident().to_string().to_uppercase();
@@ -486,20 +494,20 @@ fn create_wrapper_function(
span,
);
// strip possible Option<> from this type:
- let ty = util::is_option_type(ty).unwrap_or(ty);
+ let ty = util::is_option_type(param.ty).unwrap_or(param.ty);
default_consts.extend(quote_spanned! { span =>
pub const #name: #ty = #def;
});
- if optional && no_option_type {
+ if param.entry.optional && no_option_type {
// Optional parameter without an Option<T> type requires a default:
body.extend(quote_spanned! { span =>
.unwrap_or(#name)
});
}
- } else if optional && no_option_type {
+ } else if param.entry.optional && no_option_type {
// FIXME: we should not be able to reach this without having produced another
// error above already anyway?
- error!(ty => "Optional parameter without Option<T> requires a default");
+ error!(param.ty => "Optional parameter without Option<T> requires a default");
// we produced an error so just write something that will compile
body.extend(quote_spanned! { span =>
.unwrap_or_else(|| unreachable!())
--
2.20.1
^ permalink raw reply [flat|nested] 23+ messages in thread
* [pbs-devel] [PATCH proxmox 13/18] api-macro: factor parameter extraction into a function
2020-12-18 11:25 [pbs-devel] [PATCH proxmox 00/18] Optional Return Types and AllOf schema Wolfgang Bumiller
` (11 preceding siblings ...)
2020-12-18 11:26 ` [pbs-devel] [PATCH proxmox 12/18] api-macro: more tuple refactoring Wolfgang Bumiller
@ 2020-12-18 11:26 ` Wolfgang Bumiller
2020-12-18 11:26 ` [pbs-devel] [PATCH proxmox 14/18] api-macro: support flattened parameters Wolfgang Bumiller
` (7 subsequent siblings)
20 siblings, 0 replies; 23+ messages in thread
From: Wolfgang Bumiller @ 2020-12-18 11:26 UTC (permalink / raw)
To: pbs-devel
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
---
proxmox-api-macro/src/api/method.rs | 142 +++++++++++++++++-----------
1 file changed, 85 insertions(+), 57 deletions(-)
diff --git a/proxmox-api-macro/src/api/method.rs b/proxmox-api-macro/src/api/method.rs
index 3d6e9ed..ff5d3e0 100644
--- a/proxmox-api-macro/src/api/method.rs
+++ b/proxmox-api-macro/src/api/method.rs
@@ -458,63 +458,15 @@ fn create_wrapper_function(
ParameterType::ApiMethod => args.extend(quote_spanned! { span => api_method_param, }),
ParameterType::RpcEnv => args.extend(quote_spanned! { span => rpc_env_param, }),
ParameterType::Normal(param) => {
- let name_str = syn::LitStr::new(name.as_str(), span);
- let arg_name =
- Ident::new(&format!("input_arg_{}", name.as_ident().to_string()), span);
-
- // Optional parameters are expected to be Option<> types in the real function
- // signature, so we can just keep the returned Option from `input_map.remove()`.
- body.extend(quote_spanned! { span =>
- let #arg_name = input_map
- .remove(#name_str)
- .map(::serde_json::from_value)
- .transpose()?
- });
- let default_value = param.entry.schema.find_schema_property("default");
- if !param.entry.optional {
- // Non-optional types need to be extracted out of the option though (unless
- // they have a default):
- //
- // Whether the parameter is optional should have been verified by the schema
- // verifier already, so here we just use anyhow::bail! instead of building a
- // proper http error!
- body.extend(quote_spanned! { span =>
- .ok_or_else(|| ::anyhow::format_err!(
- "missing non-optional parameter: {}",
- #name_str,
- ))?
- });
- }
- let no_option_type = util::is_option_type(param.ty).is_none();
-
- if let Some(def) = &default_value {
- let name_uc = name.as_ident().to_string().to_uppercase();
- let name = Ident::new(
- &format!("API_METHOD_{}_PARAM_DEFAULT_{}", func_uc, name_uc),
- span,
- );
- // strip possible Option<> from this type:
- let ty = util::is_option_type(param.ty).unwrap_or(param.ty);
- default_consts.extend(quote_spanned! { span =>
- pub const #name: #ty = #def;
- });
- if param.entry.optional && no_option_type {
- // Optional parameter without an Option<T> type requires a default:
- body.extend(quote_spanned! { span =>
- .unwrap_or(#name)
- });
- }
- } else if param.entry.optional && no_option_type {
- // FIXME: we should not be able to reach this without having produced another
- // error above already anyway?
- error!(param.ty => "Optional parameter without Option<T> requires a default");
- // we produced an error so just write something that will compile
- body.extend(quote_spanned! { span =>
- .unwrap_or_else(|| unreachable!())
- });
- }
- body.extend(quote_spanned! { span => ; });
- args.extend(quote_spanned! { span => #arg_name, });
+ extract_normal_parameter(
+ param,
+ &mut body,
+ &mut args,
+ &func_uc,
+ name,
+ span,
+ default_consts,
+ )?;
}
}
}
@@ -573,6 +525,82 @@ fn create_wrapper_function(
Ok(api_func_name)
}
+fn extract_normal_parameter(
+ param: NormalParameter,
+ body: &mut TokenStream,
+ args: &mut TokenStream,
+ func_uc: &str,
+ name: FieldName,
+ name_span: Span,
+ default_consts: &mut TokenStream,
+) -> Result<(), Error> {
+ let span = name_span; // renamed during refactorization
+ let name_str = syn::LitStr::new(name.as_str(), span);
+ let arg_name = Ident::new(&format!("input_arg_{}", name.as_ident().to_string()), span);
+
+ // Optional parameters are expected to be Option<> types in the real function
+ // signature, so we can just keep the returned Option from `input_map.remove()`.
+ body.extend(quote_spanned! { span =>
+ let #arg_name = input_map
+ .remove(#name_str)
+ .map(::serde_json::from_value)
+ .transpose()?
+ });
+
+ let default_value = param.entry.schema.find_schema_property("default");
+ if !param.entry.optional {
+ // Non-optional types need to be extracted out of the option though (unless
+ // they have a default):
+ //
+ // Whether the parameter is optional should have been verified by the schema
+ // verifier already, so here we just use anyhow::bail! instead of building a
+ // proper http error!
+ body.extend(quote_spanned! { span =>
+ .ok_or_else(|| ::anyhow::format_err!(
+ "missing non-optional parameter: {}",
+ #name_str,
+ ))?
+ });
+ }
+
+ let no_option_type = util::is_option_type(param.ty).is_none();
+
+ if let Some(def) = &default_value {
+ let name_uc = name.as_ident().to_string().to_uppercase();
+ let name = Ident::new(
+ &format!("API_METHOD_{}_PARAM_DEFAULT_{}", func_uc, name_uc),
+ span,
+ );
+
+ // strip possible Option<> from this type:
+ let ty = util::is_option_type(param.ty).unwrap_or(param.ty);
+ default_consts.extend(quote_spanned! { span =>
+ pub const #name: #ty = #def;
+ });
+
+ if param.entry.optional && no_option_type {
+ // Optional parameter without an Option<T> type requires a default:
+ body.extend(quote_spanned! { span =>
+ .unwrap_or(#name)
+ });
+ }
+ } else if param.entry.optional && no_option_type {
+ // FIXME: we should not be able to reach this without having produced another
+ // error above already anyway?
+ error!(param.ty => "Optional parameter without Option<T> requires a default");
+
+ // we produced an error so just write something that will compile
+ body.extend(quote_spanned! { span =>
+ .unwrap_or_else(|| unreachable!())
+ });
+ }
+
+ body.extend(quote_spanned! { span => ; });
+ args.extend(quote_spanned! { span => #arg_name, });
+
+ Ok(())
+}
+
struct DefaultParameters<'a>(&'a Schema);
impl<'a> VisitMut for DefaultParameters<'a> {
--
2.20.1
^ permalink raw reply [flat|nested] 23+ messages in thread
* [pbs-devel] [PATCH proxmox 14/18] api-macro: support flattened parameters
2020-12-18 11:25 [pbs-devel] [PATCH proxmox 00/18] Optional Return Types and AllOf schema Wolfgang Bumiller
` (12 preceding siblings ...)
2020-12-18 11:26 ` [pbs-devel] [PATCH proxmox 13/18] api-macro: factor parameter extraction into a function Wolfgang Bumiller
@ 2020-12-18 11:26 ` Wolfgang Bumiller
2020-12-18 11:26 ` [pbs-devel] [PATCH proxmox 15/18] schema: ParameterSchema at 'api' level Wolfgang Bumiller
` (6 subsequent siblings)
20 siblings, 0 replies; 23+ messages in thread
From: Wolfgang Bumiller @ 2020-12-18 11:26 UTC (permalink / raw)
To: pbs-devel
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
---
proxmox-api-macro/src/api/method.rs | 286 +++++++++++++++++++++-------
proxmox-api-macro/src/api/mod.rs | 64 ++++++-
proxmox-api-macro/tests/allof.rs | 137 ++++++++++++-
3 files changed, 414 insertions(+), 73 deletions(-)
diff --git a/proxmox-api-macro/src/api/method.rs b/proxmox-api-macro/src/api/method.rs
index ff5d3e0..23501bc 100644
--- a/proxmox-api-macro/src/api/method.rs
+++ b/proxmox-api-macro/src/api/method.rs
@@ -85,7 +85,7 @@ impl TryFrom<JSONObject> for ReturnType {
///
/// See the top level macro documentation for a complete example.
pub fn handle_method(mut attribs: JSONObject, mut func: syn::ItemFn) -> Result<TokenStream, Error> {
- let mut input_schema: Schema = match attribs.remove("input") {
+ let input_schema: Schema = match attribs.remove("input") {
Some(input) => input.into_object("input schema definition")?.try_into()?,
None => Schema {
span: Span::call_site(),
@@ -95,6 +95,19 @@ pub fn handle_method(mut attribs: JSONObject, mut func: syn::ItemFn) -> Result<T
},
};
+ let mut input_schema = if input_schema.as_object().is_some() {
+ input_schema
+ } else {
+ error!(
+ input_schema.span,
+ "method input schema must be an object schema"
+ );
+ let mut schema = Schema::empty_object(input_schema.span);
+ schema.description = input_schema.description;
+ schema.properties = input_schema.properties;
+ schema
+ };
+
let mut return_type: Option<ReturnType> = attribs
.remove("returns")
.map(|ret| ret.into_object("return schema definition")?.try_into())
@@ -158,25 +171,15 @@ pub fn handle_method(mut attribs: JSONObject, mut func: syn::ItemFn) -> Result<T
// input schema is done, let's give the method body a chance to extract default parameters:
DefaultParameters(&input_schema).visit_item_fn_mut(&mut func);
- let input_schema = {
- let mut ts = TokenStream::new();
- input_schema.to_typed_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(),
);
- let input_schema_name = Ident::new(
- &format!(
- "API_PARAMETER_SCHEMA_{}",
- func_name.to_string().to_uppercase()
- ),
- func.sig.ident.span(),
- );
+
+ let (input_schema_code, input_schema_parameter) =
+ serialize_input_schema(input_schema, &func.sig.ident, func.sig.span())?;
let mut returns_schema_setter = TokenStream::new();
if let Some(return_type) = return_type {
@@ -192,13 +195,12 @@ pub fn handle_method(mut attribs: JSONObject, mut func: syn::ItemFn) -> Result<T
};
Ok(quote_spanned! { func.sig.span() =>
- pub const #input_schema_name: ::proxmox::api::schema::ObjectSchema =
- #input_schema;
+ #input_schema_code
#vis const #api_method_name: ::proxmox::api::ApiMethod =
- ::proxmox::api::ApiMethod::new(
+ ::proxmox::api::ApiMethod::new_full(
&#api_handler,
- &#input_schema_name,
+ #input_schema_parameter,
)
#returns_schema_setter
#access_setter
@@ -538,69 +540,221 @@ fn extract_normal_parameter(
let name_str = syn::LitStr::new(name.as_str(), span);
let arg_name = Ident::new(&format!("input_arg_{}", name.as_ident().to_string()), span);
+ let default_value = param.entry.schema.find_schema_property("default");
+
// Optional parameters are expected to be Option<> types in the real function
// signature, so we can just keep the returned Option from `input_map.remove()`.
- body.extend(quote_spanned! { span =>
- let #arg_name = input_map
- .remove(#name_str)
- .map(::serde_json::from_value)
- .transpose()?
- });
+ match param.entry.flatten {
+ None => {
+ // regular parameter, we just remove it and call `from_value`.
- let default_value = param.entry.schema.find_schema_property("default");
- if !param.entry.optional {
- // Non-optional types need to be extracted out of the option though (unless
- // they have a default):
- //
- // Whether the parameter is optional should have been verified by the schema
- // verifier already, so here we just use anyhow::bail! instead of building a
- // proper http error!
- body.extend(quote_spanned! { span =>
- .ok_or_else(|| ::anyhow::format_err!(
- "missing non-optional parameter: {}",
- #name_str,
- ))?
- });
- }
+ body.extend(quote_spanned! { span =>
+ let #arg_name = input_map
+ .remove(#name_str)
+ .map(::serde_json::from_value)
+ .transpose()?
+ });
- let no_option_type = util::is_option_type(param.ty).is_none();
+ if !param.entry.optional {
+ // Non-optional types need to be extracted out of the option though (unless
+ // they have a default):
+ //
+ // Whether the parameter is optional should have been verified by the schema
+ // verifier already, so here we just use anyhow::bail! instead of building a
+ // proper http error!
+ body.extend(quote_spanned! { span =>
+ .ok_or_else(|| ::anyhow::format_err!(
+ "missing non-optional parameter: {}",
+ #name_str,
+ ))?
+ });
+ }
- if let Some(def) = &default_value {
- let name_uc = name.as_ident().to_string().to_uppercase();
- let name = Ident::new(
- &format!("API_METHOD_{}_PARAM_DEFAULT_{}", func_uc, name_uc),
- span,
- );
+ let no_option_type = util::is_option_type(param.ty).is_none();
- // strip possible Option<> from this type:
- let ty = util::is_option_type(param.ty).unwrap_or(param.ty);
- default_consts.extend(quote_spanned! { span =>
- pub const #name: #ty = #def;
- });
+ if let Some(def) = &default_value {
+ let name_uc = name.as_ident().to_string().to_uppercase();
+ let name = Ident::new(
+ &format!("API_METHOD_{}_PARAM_DEFAULT_{}", func_uc, name_uc),
+ span,
+ );
+
+ // strip possible Option<> from this type:
+ let ty = util::is_option_type(param.ty).unwrap_or(param.ty);
+ default_consts.extend(quote_spanned! { span =>
+ pub const #name: #ty = #def;
+ });
+
+ if param.entry.optional && no_option_type {
+ // Optional parameter without an Option<T> type requires a default:
+ body.extend(quote_spanned! { span =>
+ .unwrap_or(#name)
+ });
+ }
+ } else if param.entry.optional && no_option_type {
+ // FIXME: we should not be able to reach this without having produced another
+ // error above already anyway?
+ error!(param.ty => "Optional parameter without Option<T> requires a default");
+
+ // we produced an error so just write something that will compile
+ body.extend(quote_spanned! { span =>
+ .unwrap_or_else(|| unreachable!())
+ });
+ }
- if param.entry.optional && no_option_type {
- // Optional parameter without an Option<T> type requires a default:
- body.extend(quote_spanned! { span =>
- .unwrap_or(#name)
- });
+ body.extend(quote_spanned! { span => ; });
+ }
+ Some(flatten_span) => {
+ // Flattened parameter, we need ot use our special partial-object deserializer.
+ // Also note that we do not support simply nesting schemas. We need a referenced type.
+ // Otherwise the expanded code here gets ugly and we'd need to make sure we pull out
+ // nested schemas into named variables first... No thanks.
+
+ if default_value.is_some() {
+ error!(
+ default_value =>
+ "flattened parameter cannot have a default as it cannot be optional",
+ );
+ }
+
+ if let Some(schema_ref) = param.entry.schema.to_schema_reference() {
+ let ty = param.ty;
+ body.extend(quote_spanned! { span =>
+ let #arg_name = <#ty as ::serde::Deserialize>::deserialize(
+ ::proxmox::api::de::ExtractValueDeserializer::try_new(
+ input_map,
+ #schema_ref,
+ )
+ .ok_or_else(|| ::anyhow::format_err!(
+ "flattened parameter {:?} has invalid schema",
+ #name_str,
+ ))?,
+ )?;
+ });
+ } else {
+ error!(
+ flatten_span,
+ "flattened parameter schema must be a schema reference"
+ );
+ body.extend(quote_spanned! { span => let #arg_name = unreachable!(); });
+ }
}
- } else if param.entry.optional && no_option_type {
- // FIXME: we should not be able to reach this without having produced another
- // error above already anyway?
- error!(param.ty => "Optional parameter without Option<T> requires a default");
-
- // we produced an error so just write something that will compile
- body.extend(quote_spanned! { span =>
- .unwrap_or_else(|| unreachable!())
- });
}
- body.extend(quote_spanned! { span => ; });
args.extend(quote_spanned! { span => #arg_name, });
Ok(())
}
+/// Returns a tuple containing the schema code first and the `ParameterSchema` parameter for the
+/// `ApiMethod` second.
+fn serialize_input_schema(
+ mut input_schema: Schema,
+ func_name: &Ident,
+ func_sig_span: Span,
+) -> Result<(TokenStream, TokenStream), Error> {
+ let input_schema_name = Ident::new(
+ &format!(
+ "API_PARAMETER_SCHEMA_{}",
+ func_name.to_string().to_uppercase()
+ ),
+ func_name.span(),
+ );
+
+ let (flattened, has_params) = match &mut input_schema.item {
+ SchemaItem::Object(obj) => {
+ let flattened = obj.drain_filter(|entry| entry.flatten.is_none());
+ (flattened, !obj.is_empty())
+ }
+ _ => (Vec::new(), true),
+ };
+
+ if flattened.is_empty() {
+ let mut ts = TokenStream::new();
+ input_schema.to_typed_schema(&mut ts)?;
+ return Ok((
+ quote_spanned! { func_sig_span =>
+ pub const #input_schema_name: ::proxmox::api::schema::ObjectSchema = #ts;
+ },
+ quote_spanned! { func_sig_span =>
+ ::proxmox::api::router::ParameterSchema::Object(&#input_schema_name)
+ },
+ ));
+ }
+
+ let mut all_of_schemas = TokenStream::new();
+ for entry in flattened {
+ if entry.optional {
+ error!(
+ entry.schema.span,
+ "optional flattened parameters are not supported"
+ );
+ }
+
+ all_of_schemas.extend(quote::quote! {&});
+ entry.schema.to_schema(&mut all_of_schemas)?;
+ all_of_schemas.extend(quote::quote! {,});
+ }
+
+ let description = match input_schema.description.take().ok() {
+ Some(description) => description,
+ None => {
+ error!(input_schema.span, "missing description on api type struct");
+ syn::LitStr::new("<missing description>", input_schema.span)
+ }
+ };
+ // and replace it with a "dummy"
+ input_schema.description = Maybe::Derived(syn::LitStr::new(
+ &format!("<INNER: {}>", description.value()),
+ description.span(),
+ ));
+
+ let (inner_schema, inner_schema_ref) = if has_params {
+ // regular parameters go into the "inner" schema to merge into the AllOfSchema
+ let inner_schema_name = Ident::new(
+ &format!(
+ "API_REGULAR_PARAMETER_SCHEMA_{}",
+ func_name.to_string().to_uppercase()
+ ),
+ func_name.span(),
+ );
+
+ let obj_schema = {
+ let mut ts = TokenStream::new();
+ input_schema.to_schema(&mut ts)?;
+ ts
+ };
+
+ (
+ quote_spanned!(func_sig_span =>
+ const #inner_schema_name: ::proxmox::api::schema::Schema = #obj_schema;
+ ),
+ quote_spanned!(func_sig_span => &#inner_schema_name,),
+ )
+ } else {
+ // otherwise it stays empty
+ (TokenStream::new(), TokenStream::new())
+ };
+
+ Ok((
+ quote_spanned! { func_sig_span =>
+ #inner_schema
+
+ pub const #input_schema_name: ::proxmox::api::schema::AllOfSchema =
+ ::proxmox::api::schema::AllOfSchema::new(
+ #description,
+ &[
+ #inner_schema_ref
+ #all_of_schemas
+ ],
+ );
+ },
+ quote_spanned! { func_sig_span =>
+ ::proxmox::api::router::ParameterSchema::AllOf(&#input_schema_name)
+ },
+ ))
+}
+
struct DefaultParameters<'a>(&'a Schema);
impl<'a> VisitMut for DefaultParameters<'a> {
diff --git a/proxmox-api-macro/src/api/mod.rs b/proxmox-api-macro/src/api/mod.rs
index 4690654..5994f19 100644
--- a/proxmox-api-macro/src/api/mod.rs
+++ b/proxmox-api-macro/src/api/mod.rs
@@ -142,6 +142,17 @@ impl Schema {
}
}
+ /// Create the token stream for a reference schema (`ExternType` or `ExternSchema`).
+ fn to_schema_reference(&self) -> Option<TokenStream> {
+ match &self.item {
+ SchemaItem::ExternType(path) => {
+ Some(quote_spanned! { path.span() => &#path::API_SCHEMA })
+ }
+ SchemaItem::ExternSchema(path) => Some(quote_spanned! { path.span() => &#path }),
+ _ => None,
+ }
+ }
+
fn to_typed_schema(&self, ts: &mut TokenStream) -> Result<(), Error> {
self.item.to_schema(
ts,
@@ -323,7 +334,8 @@ impl SchemaItem {
}
SchemaItem::ExternType(path) => {
if !properties.is_empty() {
- error!(&properties[0].0 => "additional properties not allowed on external type");
+ error!(&properties[0].0 =>
+ "additional properties not allowed on external type");
}
if let Maybe::Explicit(description) = description {
error!(description => "description not allowed on external type");
@@ -385,6 +397,10 @@ pub struct ObjectEntry {
pub name: FieldName,
pub optional: bool,
pub schema: Schema,
+
+ /// This is only valid for methods. Methods should reset this to false after dealing with it,
+ /// as encountering this during schema serialization will always cause an error.
+ pub flatten: Option<Span>,
}
impl ObjectEntry {
@@ -393,8 +409,14 @@ impl ObjectEntry {
name,
optional,
schema,
+ flatten: None,
}
}
+
+ pub fn with_flatten(mut self, flatten: Option<Span>) -> Self {
+ self.flatten = flatten;
+ self
+ }
}
#[derive(Default)]
@@ -420,6 +442,24 @@ impl SchemaObject {
&mut self.properties_
}
+ fn drain_filter<F>(&mut self, mut func: F) -> Vec<ObjectEntry>
+ where
+ F: FnMut(&ObjectEntry) -> bool,
+ {
+ let mut out = Vec::new();
+
+ let mut i = 0;
+ while i != self.properties_.len() {
+ if !func(&self.properties_[i]) {
+ out.push(self.properties_.remove(i));
+ } else {
+ i += 1;
+ }
+ }
+
+ out
+ }
+
fn sort_properties(&mut self) {
self.properties_.sort_by(|a, b| (a.name).cmp(&b.name));
}
@@ -445,7 +485,19 @@ impl SchemaObject {
.transpose()?
.unwrap_or(false);
- properties.push(ObjectEntry::new(key, optional, schema.try_into()?));
+ let flatten: Option<Span> = schema
+ .remove_entry("flatten")
+ .map(|(field, value)| -> Result<(Span, bool), syn::Error> {
+ let v: syn::LitBool = value.try_into()?;
+ Ok((field.span(), v.value))
+ })
+ .transpose()?
+ .and_then(|(span, value)| if value { Some(span) } else { None });
+
+ properties.push(
+ ObjectEntry::new(key, optional, schema.try_into()?)
+ .with_flatten(flatten),
+ );
Ok(properties)
},
@@ -457,6 +509,14 @@ impl SchemaObject {
fn to_schema_inner(&self, ts: &mut TokenStream) -> Result<(), Error> {
for element in self.properties_.iter() {
+ if let Some(span) = element.flatten {
+ error!(
+ span,
+ "`flatten` attribute is only available on method parameters, \
+ use #[serde(flatten)] in structs"
+ );
+ }
+
let key = element.name.as_str();
let optional = element.optional;
let mut schema = TokenStream::new();
diff --git a/proxmox-api-macro/tests/allof.rs b/proxmox-api-macro/tests/allof.rs
index 56e86d7..1c1b9a9 100644
--- a/proxmox-api-macro/tests/allof.rs
+++ b/proxmox-api-macro/tests/allof.rs
@@ -1,10 +1,12 @@
//! Testing the `AllOf` schema on structs and methods.
+use anyhow::Error;
+use serde::{Deserialize, Serialize};
+use serde_json::{json, Value};
+
use proxmox::api::schema;
use proxmox_api_macro::api;
-use serde::{Deserialize, Serialize};
-
pub const NAME_SCHEMA: schema::Schema = schema::StringSchema::new("Name.").schema();
pub const VALUE_SCHEMA: schema::Schema = schema::IntegerSchema::new("Value.").schema();
pub const INDEX_SCHEMA: schema::Schema = schema::IntegerSchema::new("Index.").schema();
@@ -18,7 +20,7 @@ pub const TEXT_SCHEMA: schema::Schema = schema::StringSchema::new("Text.").schem
)]
/// Name and value.
#[derive(Deserialize, Serialize)]
-struct NameValue {
+pub struct NameValue {
name: String,
value: u64,
}
@@ -31,7 +33,7 @@ struct NameValue {
)]
/// Index and text.
#[derive(Deserialize, Serialize)]
-struct IndexText {
+pub struct IndexText {
index: u64,
text: String,
}
@@ -44,7 +46,7 @@ struct IndexText {
)]
/// Name, value, index and text.
#[derive(Deserialize, Serialize)]
-struct Nvit {
+pub struct Nvit {
#[serde(flatten)]
nv: NameValue,
@@ -116,3 +118,128 @@ fn test_extra() {
assert_eq!(TEST_SCHEMA, WithExtra::API_SCHEMA);
}
+
+#[api(
+ input: {
+ properties: {
+ nv: { flatten: true, type: NameValue },
+ it: { flatten: true, type: IndexText },
+ },
+ },
+)]
+/// Hello method.
+pub fn hello(it: IndexText, nv: NameValue) -> Result<(NameValue, IndexText), Error> {
+ Ok((nv, it))
+}
+
+#[test]
+fn hello_schema_check() {
+ const TEST_METHOD: ::proxmox::api::ApiMethod = ::proxmox::api::ApiMethod::new_full(
+ &::proxmox::api::ApiHandler::Sync(&api_function_hello),
+ ::proxmox::api::router::ParameterSchema::AllOf(&::proxmox::api::schema::AllOfSchema::new(
+ "Hello method.",
+ &[&IndexText::API_SCHEMA, &NameValue::API_SCHEMA],
+ )),
+ );
+ assert_eq!(TEST_METHOD, API_METHOD_HELLO);
+}
+
+#[api(
+ input: {
+ properties: {
+ nv: { flatten: true, type: NameValue },
+ it: { flatten: true, type: IndexText },
+ extra: { description: "An extra field." },
+ },
+ },
+)]
+/// Extra method.
+pub fn with_extra(
+ it: IndexText,
+ nv: NameValue,
+ extra: String,
+) -> Result<(NameValue, IndexText, String), Error> {
+ Ok((nv, it, extra))
+}
+
+#[test]
+fn with_extra_schema_check() {
+ const INNER_SCHEMA: ::proxmox::api::schema::Schema = ::proxmox::api::schema::ObjectSchema::new(
+ "<INNER: Extra method.>",
+ &[(
+ "extra",
+ false,
+ &::proxmox::api::schema::StringSchema::new("An extra field.").schema(),
+ )],
+ )
+ .schema();
+
+ const TEST_METHOD: ::proxmox::api::ApiMethod = ::proxmox::api::ApiMethod::new_full(
+ &::proxmox::api::ApiHandler::Sync(&api_function_with_extra),
+ ::proxmox::api::router::ParameterSchema::AllOf(&::proxmox::api::schema::AllOfSchema::new(
+ "Extra method.",
+ &[
+ &INNER_SCHEMA,
+ &IndexText::API_SCHEMA,
+ &NameValue::API_SCHEMA,
+ ],
+ )),
+ );
+ assert_eq!(TEST_METHOD, API_METHOD_WITH_EXTRA);
+}
+
+struct RpcEnv;
+impl proxmox::api::RpcEnvironment for RpcEnv {
+ fn result_attrib_mut(&mut self) -> &mut Value {
+ panic!("result_attrib_mut called");
+ }
+
+ fn result_attrib(&self) -> &Value {
+ panic!("result_attrib called");
+ }
+
+ /// The environment type
+ fn env_type(&self) -> proxmox::api::RpcEnvironmentType {
+ panic!("env_type called");
+ }
+
+ /// Set authentication id
+ fn set_auth_id(&mut self, user: Option<String>) {
+ let _ = user;
+ panic!("set_auth_id called");
+ }
+
+ /// Get authentication id
+ fn get_auth_id(&self) -> Option<String> {
+ panic!("get_auth_id called");
+ }
+}
+
+#[test]
+fn test_invocations() {
+ let mut env = RpcEnv;
+ let value = api_function_hello(
+ json!({"name":"Bob", "value":3, "index":4, "text":"Text"}),
+ &API_METHOD_HELLO,
+ &mut env,
+ )
+ .expect("hello function should work");
+
+ assert_eq!(value[0]["name"], "Bob");
+ assert_eq!(value[0]["value"], 3);
+ assert_eq!(value[1]["index"], 4);
+ assert_eq!(value[1]["text"], "Text");
+
+ let value = api_function_with_extra(
+ json!({"name":"Alice", "value":8, "index":2, "text":"Paragraph", "extra":"Some Extra"}),
+ &API_METHOD_WITH_EXTRA,
+ &mut env,
+ )
+ .expect("`with_extra` function should work");
+
+ assert_eq!(value[0]["name"], "Alice");
+ assert_eq!(value[0]["value"], 8);
+ assert_eq!(value[1]["index"], 2);
+ assert_eq!(value[1]["text"], "Paragraph");
+ assert_eq!(value[2], "Some Extra");
+}
--
2.20.1
^ permalink raw reply [flat|nested] 23+ messages in thread
* [pbs-devel] [PATCH proxmox 15/18] schema: ParameterSchema at 'api' level
2020-12-18 11:25 [pbs-devel] [PATCH proxmox 00/18] Optional Return Types and AllOf schema Wolfgang Bumiller
` (13 preceding siblings ...)
2020-12-18 11:26 ` [pbs-devel] [PATCH proxmox 14/18] api-macro: support flattened parameters Wolfgang Bumiller
@ 2020-12-18 11:26 ` Wolfgang Bumiller
2020-12-18 11:26 ` [pbs-devel] [PATCH proxmox 16/18] proxmox: temporary d/changelog update Wolfgang Bumiller
` (5 subsequent siblings)
20 siblings, 0 replies; 23+ messages in thread
From: Wolfgang Bumiller @ 2020-12-18 11:26 UTC (permalink / raw)
To: pbs-devel
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
---
proxmox/src/api/mod.rs | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/proxmox/src/api/mod.rs b/proxmox/src/api/mod.rs
index 8c6f597..5319cc1 100644
--- a/proxmox/src/api/mod.rs
+++ b/proxmox/src/api/mod.rs
@@ -43,7 +43,8 @@ pub mod router;
#[cfg(feature = "router")]
#[doc(inline)]
pub use router::{
- ApiFuture, ApiHandler, ApiMethod, ApiResponseFuture, Router, SubRoute, SubdirMap,
+ ApiFuture, ApiHandler, ApiMethod, ApiResponseFuture, ParameterSchema, Router, SubRoute,
+ SubdirMap,
};
#[cfg(feature = "cli")]
--
2.20.1
^ permalink raw reply [flat|nested] 23+ messages in thread
* [pbs-devel] [PATCH proxmox 16/18] proxmox: temporary d/changelog update
2020-12-18 11:25 [pbs-devel] [PATCH proxmox 00/18] Optional Return Types and AllOf schema Wolfgang Bumiller
` (14 preceding siblings ...)
2020-12-18 11:26 ` [pbs-devel] [PATCH proxmox 15/18] schema: ParameterSchema at 'api' level Wolfgang Bumiller
@ 2020-12-18 11:26 ` Wolfgang Bumiller
2020-12-18 11:26 ` [pbs-devel] [PATCH proxmox 17/18] macro: " Wolfgang Bumiller
` (4 subsequent siblings)
20 siblings, 0 replies; 23+ messages in thread
From: Wolfgang Bumiller @ 2020-12-18 11:26 UTC (permalink / raw)
To: pbs-devel
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
---
proxmox/debian/changelog | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/proxmox/debian/changelog b/proxmox/debian/changelog
index 5e8c449..050619d 100644
--- a/proxmox/debian/changelog
+++ b/proxmox/debian/changelog
@@ -1,3 +1,13 @@
+rust-proxmox (0.9.0-1) UNRELEASED; urgency=medium
+
+ * `ApiMethod.returns` is now a `router::ReturnType` rather than a direct
+ `&Schema` reference and can be marked as `optional`.
+
+ * Added an `AllOfSchema` (`Schema::AllOf`) where multiple object schemas can
+ be combined like with JSONSchema/openapi's `allOf` property.
+
+ -- Proxmox Support Team <support@proxmox.com> Fri, 11 Dec 2020 14:55:29 +0100
+
rust-proxmox (0.8.1-1) unstable; urgency=medium
* trait ReadExt: add read_exact_or_eof and skip_to_end
--
2.20.1
^ permalink raw reply [flat|nested] 23+ messages in thread
* [pbs-devel] [PATCH proxmox 17/18] macro: temporary d/changelog update
2020-12-18 11:25 [pbs-devel] [PATCH proxmox 00/18] Optional Return Types and AllOf schema Wolfgang Bumiller
` (15 preceding siblings ...)
2020-12-18 11:26 ` [pbs-devel] [PATCH proxmox 16/18] proxmox: temporary d/changelog update Wolfgang Bumiller
@ 2020-12-18 11:26 ` Wolfgang Bumiller
2020-12-18 11:26 ` [pbs-devel] [PATCH proxmox 18/18] proxmox changelog update Wolfgang Bumiller
` (3 subsequent siblings)
20 siblings, 0 replies; 23+ messages in thread
From: Wolfgang Bumiller @ 2020-12-18 11:26 UTC (permalink / raw)
To: pbs-devel
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
---
proxmox-api-macro/debian/changelog | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/proxmox-api-macro/debian/changelog b/proxmox-api-macro/debian/changelog
index 0c72dce..c514474 100644
--- a/proxmox-api-macro/debian/changelog
+++ b/proxmox-api-macro/debian/changelog
@@ -1,3 +1,13 @@
+rust-proxmox-api-macro (0.3.0-1) UNRELEASED; urgency=medium
+
+ * removed `pub const API_RETURN_SCHEMA_*` generation
+ This could already be accessed via the public `API_METHOD_FOO.returns`.
+
+ * Note that a previous `schema: API_RETURN_SCHEMA_FOO` must now dereference
+ the schema via: `schema: *API_METHOD_FOO.returns.schema`.
+
+ -- Proxmox Support Team <support@proxmox.com> Fri, 11 Dec 2020 14:56:02 +0100
+
rust-proxmox-api-macro (0.2.4-1) unstable; urgency=medium
* support raw parameter name identifiers (eg. `r#type`)
--
2.20.1
^ permalink raw reply [flat|nested] 23+ messages in thread
* [pbs-devel] [PATCH proxmox 18/18] proxmox changelog update
2020-12-18 11:25 [pbs-devel] [PATCH proxmox 00/18] Optional Return Types and AllOf schema Wolfgang Bumiller
` (16 preceding siblings ...)
2020-12-18 11:26 ` [pbs-devel] [PATCH proxmox 17/18] macro: " Wolfgang Bumiller
@ 2020-12-18 11:26 ` Wolfgang Bumiller
2020-12-18 11:26 ` [pbs-devel] [PATCH backup 1/2] adaptions for proxmox 0.9 and proxmox-api-macro 0.3 Wolfgang Bumiller
` (2 subsequent siblings)
20 siblings, 0 replies; 23+ messages in thread
From: Wolfgang Bumiller @ 2020-12-18 11:26 UTC (permalink / raw)
To: pbs-devel
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
---
proxmox/debian/changelog | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/proxmox/debian/changelog b/proxmox/debian/changelog
index 050619d..3505bfc 100644
--- a/proxmox/debian/changelog
+++ b/proxmox/debian/changelog
@@ -6,6 +6,14 @@ rust-proxmox (0.9.0-1) UNRELEASED; urgency=medium
* Added an `AllOfSchema` (`Schema::AllOf`) where multiple object schemas can
be combined like with JSONSchema/openapi's `allOf` property.
+ * `ApiMethod.parameters` is now a `ParameterSchema` instead of simply an
+ `ObjectSchema`.
+
+ * There's now an `ObjectSchemaType` trait implemented by `ObjectSchema`,
+ `AllOfSchema` as well as `ParameterSchema` for simplicity. Some of the
+ verifiers/parsers now use the trait to cope with both types with minimal
+ changes.
+
-- Proxmox Support Team <support@proxmox.com> Fri, 11 Dec 2020 14:55:29 +0100
rust-proxmox (0.8.1-1) unstable; urgency=medium
--
2.20.1
^ permalink raw reply [flat|nested] 23+ messages in thread
* [pbs-devel] [PATCH backup 1/2] adaptions for proxmox 0.9 and proxmox-api-macro 0.3
2020-12-18 11:25 [pbs-devel] [PATCH proxmox 00/18] Optional Return Types and AllOf schema Wolfgang Bumiller
` (17 preceding siblings ...)
2020-12-18 11:26 ` [pbs-devel] [PATCH proxmox 18/18] proxmox changelog update Wolfgang Bumiller
@ 2020-12-18 11:26 ` Wolfgang Bumiller
2020-12-18 11:26 ` [pbs-devel] [PATCH backup 2/2] tests: verify-api: check AllOf schemas Wolfgang Bumiller
2020-12-22 6:45 ` [pbs-devel] [PATCH proxmox 00/18] Optional Return Types and AllOf schema Dietmar Maurer
20 siblings, 0 replies; 23+ messages in thread
From: Wolfgang Bumiller @ 2020-12-18 11:26 UTC (permalink / raw)
To: pbs-devel
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
---
src/api2/admin/datastore.rs | 8 ++++----
src/bin/proxmox-backup-client.rs | 12 ++++++------
src/bin/proxmox-backup-manager.rs | 12 ++++++------
src/bin/proxmox-tape.rs | 4 ++--
src/bin/proxmox_backup_client/benchmark.rs | 5 +++--
src/bin/proxmox_backup_client/key.rs | 10 ++++++++--
src/bin/proxmox_backup_client/snapshot.rs | 9 +++++----
src/bin/proxmox_backup_client/task.rs | 4 ++--
src/bin/proxmox_backup_manager/acl.rs | 2 +-
src/bin/proxmox_backup_manager/datastore.rs | 4 ++--
src/bin/proxmox_backup_manager/disk.rs | 8 ++++----
src/bin/proxmox_backup_manager/dns.rs | 2 +-
src/bin/proxmox_backup_manager/network.rs | 2 +-
src/bin/proxmox_backup_manager/remote.rs | 4 ++--
src/bin/proxmox_backup_manager/subscription.rs | 2 +-
src/bin/proxmox_backup_manager/sync.rs | 4 ++--
src/bin/proxmox_backup_manager/user.rs | 4 ++--
src/bin/proxmox_tape/changer.rs | 8 ++++----
src/bin/proxmox_tape/drive.rs | 6 +++---
src/bin/proxmox_tape/media.rs | 2 +-
src/bin/proxmox_tape/pool.rs | 4 ++--
src/server/rest.rs | 9 +++++----
22 files changed, 67 insertions(+), 58 deletions(-)
diff --git a/src/api2/admin/datastore.rs b/src/api2/admin/datastore.rs
index 16fee943..32352e5c 100644
--- a/src/api2/admin/datastore.rs
+++ b/src/api2/admin/datastore.rs
@@ -15,7 +15,7 @@ use proxmox::api::{
api, ApiResponseFuture, ApiHandler, ApiMethod, Router,
RpcEnvironment, RpcEnvironmentType, Permission
};
-use proxmox::api::router::SubdirMap;
+use proxmox::api::router::{ReturnType, SubdirMap};
use proxmox::api::schema::*;
use proxmox::tools::fs::{replace_file, CreateOptions};
use proxmox::{http_err, identity, list_subdirs_api_method, sortable};
@@ -148,7 +148,7 @@ fn get_all_snapshot_files(
},
)]
/// List backup groups.
-fn list_groups(
+pub fn list_groups(
store: String,
rpcenv: &mut dyn RpcEnvironment,
) -> Result<Vec<GroupListItem>, Error> {
@@ -772,7 +772,7 @@ pub const API_RETURN_SCHEMA_PRUNE: Schema = ArraySchema::new(
&PruneListItem::API_SCHEMA
).schema();
-const API_METHOD_PRUNE: ApiMethod = ApiMethod::new(
+pub const API_METHOD_PRUNE: ApiMethod = ApiMethod::new(
&ApiHandler::Sync(&prune),
&ObjectSchema::new(
"Prune the datastore.",
@@ -787,7 +787,7 @@ const API_METHOD_PRUNE: ApiMethod = ApiMethod::new(
("store", false, &DATASTORE_SCHEMA),
])
))
- .returns(&API_RETURN_SCHEMA_PRUNE)
+ .returns(ReturnType::new(false, &API_RETURN_SCHEMA_PRUNE))
.access(None, &Permission::Privilege(
&["datastore", "{store}"],
PRIV_DATASTORE_MODIFY | PRIV_DATASTORE_PRUNE,
diff --git a/src/bin/proxmox-backup-client.rs b/src/bin/proxmox-backup-client.rs
index 6cf81952..b8f09a4a 100644
--- a/src/bin/proxmox-backup-client.rs
+++ b/src/bin/proxmox-backup-client.rs
@@ -412,9 +412,9 @@ async fn list_backup_groups(param: Value) -> Result<Value, Error> {
let mut data: Value = result["data"].take();
- let info = &proxmox_backup::api2::admin::datastore::API_RETURN_SCHEMA_LIST_GROUPS;
+ let return_type = &proxmox_backup::api2::admin::datastore::API_METHOD_LIST_GROUPS.returns;
- format_and_print_result_full(&mut data, info, &output_format, &options);
+ format_and_print_result_full(&mut data, return_type, &output_format, &options);
Ok(Value::Null)
}
@@ -1458,7 +1458,7 @@ async fn prune_async(mut param: Value) -> Result<Value, Error> {
.column(ColumnConfig::new("keep").renderer(render_prune_action).header("action"))
;
- let info = &proxmox_backup::api2::admin::datastore::API_RETURN_SCHEMA_PRUNE;
+ let return_type = &proxmox_backup::api2::admin::datastore::API_METHOD_PRUNE.returns;
let mut data = result["data"].take();
@@ -1469,7 +1469,7 @@ async fn prune_async(mut param: Value) -> Result<Value, Error> {
data = list.into();
}
- format_and_print_result_full(&mut data, info, &output_format, &options);
+ format_and_print_result_full(&mut data, return_type, &output_format, &options);
Ok(Value::Null)
}
@@ -1522,9 +1522,9 @@ async fn status(param: Value) -> Result<Value, Error> {
.column(ColumnConfig::new("used").renderer(render_total_percentage))
.column(ColumnConfig::new("avail").renderer(render_total_percentage));
- let schema = &API_RETURN_SCHEMA_STATUS;
+ let return_type = &API_METHOD_STATUS.returns;
- format_and_print_result_full(&mut data, schema, &output_format, &options);
+ format_and_print_result_full(&mut data, return_type, &output_format, &options);
Ok(Value::Null)
}
diff --git a/src/bin/proxmox-backup-manager.rs b/src/bin/proxmox-backup-manager.rs
index 8ad4c7dc..ff2a1dc1 100644
--- a/src/bin/proxmox-backup-manager.rs
+++ b/src/bin/proxmox-backup-manager.rs
@@ -106,11 +106,11 @@ async fn garbage_collection_status(param: Value) -> Result<Value, Error> {
let mut result = client.get(&path, None).await?;
let mut data = result["data"].take();
- let schema = &api2::admin::datastore::API_RETURN_SCHEMA_GARBAGE_COLLECTION_STATUS;
+ let return_type = &api2::admin::datastore::API_METHOD_GARBAGE_COLLECTION_STATUS.returns;
let options = default_table_format_options();
- format_and_print_result_full(&mut data, schema, &output_format, &options);
+ format_and_print_result_full(&mut data, return_type, &output_format, &options);
Ok(Value::Null)
}
@@ -172,7 +172,7 @@ async fn task_list(param: Value) -> Result<Value, Error> {
let mut result = client.get("api2/json/nodes/localhost/tasks", Some(args)).await?;
let mut data = result["data"].take();
- let schema = &api2::node::tasks::API_RETURN_SCHEMA_LIST_TASKS;
+ let return_type = &api2::node::tasks::API_METHOD_LIST_TASKS.returns;
let options = default_table_format_options()
.column(ColumnConfig::new("starttime").right_align(false).renderer(tools::format::render_epoch))
@@ -180,7 +180,7 @@ async fn task_list(param: Value) -> Result<Value, Error> {
.column(ColumnConfig::new("upid"))
.column(ColumnConfig::new("status").renderer(tools::format::render_task_status));
- format_and_print_result_full(&mut data, schema, &output_format, &options);
+ format_and_print_result_full(&mut data, return_type, &output_format, &options);
Ok(Value::Null)
}
@@ -370,9 +370,9 @@ async fn get_versions(verbose: bool, param: Value) -> Result<Value, Error> {
.column(ColumnConfig::new("Version"))
.column(ColumnConfig::new("ExtraInfo").header("Extra Info"))
;
- let schema = &crate::api2::node::apt::API_RETURN_SCHEMA_GET_VERSIONS;
+ let return_type = &crate::api2::node::apt::API_METHOD_GET_VERSIONS.returns;
- format_and_print_result_full(&mut packages, schema, &output_format, &options);
+ format_and_print_result_full(&mut packages, return_type, &output_format, &options);
Ok(Value::Null)
}
diff --git a/src/bin/proxmox-tape.rs b/src/bin/proxmox-tape.rs
index 9ed9aca9..5c3e8036 100644
--- a/src/bin/proxmox-tape.rs
+++ b/src/bin/proxmox-tape.rs
@@ -311,7 +311,7 @@ async fn read_label(
.column(ColumnConfig::new("media-set-ctime").renderer(render_epoch))
;
- format_and_print_result_full(&mut data, info.returns, &output_format, &options);
+ format_and_print_result_full(&mut data, &info.returns, &output_format, &options);
Ok(())
}
@@ -383,7 +383,7 @@ async fn inventory(
.column(ColumnConfig::new("uuid"))
;
- format_and_print_result_full(&mut data, info.returns, &output_format, &options);
+ format_and_print_result_full(&mut data, &info.returns, &output_format, &options);
Ok(())
}
diff --git a/src/bin/proxmox_backup_client/benchmark.rs b/src/bin/proxmox_backup_client/benchmark.rs
index e53e43ce..2ecb3dc9 100644
--- a/src/bin/proxmox_backup_client/benchmark.rs
+++ b/src/bin/proxmox_backup_client/benchmark.rs
@@ -15,6 +15,7 @@ use proxmox::api::{
format_and_print_result_full,
default_table_format_options,
},
+ router::ReturnType,
};
use proxmox_backup::backup::{
@@ -178,7 +179,7 @@ fn render_result(
) -> Result<(), Error> {
let mut data = serde_json::to_value(benchmark_result)?;
- let schema = &BenchmarkResult::API_SCHEMA;
+ let return_type = ReturnType::new(false, &BenchmarkResult::API_SCHEMA);
let render_speed = |value: &Value, _record: &Value| -> Result<String, Error> {
match value["speed"].as_f64() {
@@ -211,7 +212,7 @@ fn render_result(
.right_align(false).renderer(render_speed));
- format_and_print_result_full(&mut data, schema, output_format, &options);
+ format_and_print_result_full(&mut data, &return_type, output_format, &options);
Ok(())
}
diff --git a/src/bin/proxmox_backup_client/key.rs b/src/bin/proxmox_backup_client/key.rs
index e49131c1..109f0384 100644
--- a/src/bin/proxmox_backup_client/key.rs
+++ b/src/bin/proxmox_backup_client/key.rs
@@ -15,6 +15,7 @@ use proxmox::api::cli::{
get_output_format,
OUTPUT_FORMAT,
};
+use proxmox::api::router::ReturnType;
use proxmox::sys::linux::tty;
use proxmox::tools::fs::{file_get_contents, replace_file, CreateOptions};
@@ -382,9 +383,14 @@ fn show_key(
.column(ColumnConfig::new("modified").renderer(tools::format::render_epoch))
.column(ColumnConfig::new("fingerprint"));
- let schema = &KeyInfo::API_SCHEMA;
+ let return_type = ReturnType::new(false, &KeyInfo::API_SCHEMA);
- format_and_print_result_full(&mut serde_json::to_value(info)?, schema, &output_format, &options);
+ format_and_print_result_full(
+ &mut serde_json::to_value(info)?,
+ &return_type,
+ &output_format,
+ &options,
+ );
Ok(())
}
diff --git a/src/bin/proxmox_backup_client/snapshot.rs b/src/bin/proxmox_backup_client/snapshot.rs
index 7be0480f..3bdc5f33 100644
--- a/src/bin/proxmox_backup_client/snapshot.rs
+++ b/src/bin/proxmox_backup_client/snapshot.rs
@@ -97,9 +97,9 @@ async fn list_snapshots(param: Value) -> Result<Value, Error> {
.column(ColumnConfig::new("files").renderer(render_files))
;
- let info = &proxmox_backup::api2::admin::datastore::API_RETURN_SCHEMA_LIST_SNAPSHOTS;
+ let return_type = &proxmox_backup::api2::admin::datastore::API_METHOD_LIST_SNAPSHOTS.returns;
- format_and_print_result_full(&mut data, info, &output_format, &options);
+ format_and_print_result_full(&mut data, return_type, &output_format, &options);
Ok(Value::Null)
}
@@ -144,13 +144,14 @@ async fn list_snapshot_files(param: Value) -> Result<Value, Error> {
record_repository(&repo);
- let info = &proxmox_backup::api2::admin::datastore::API_RETURN_SCHEMA_LIST_SNAPSHOT_FILES;
+ let return_type =
+ &proxmox_backup::api2::admin::datastore::API_METHOD_LIST_SNAPSHOT_FILES.returns;
let mut data: Value = result["data"].take();
let options = default_table_format_options();
- format_and_print_result_full(&mut data, info, &output_format, &options);
+ format_and_print_result_full(&mut data, return_type, &output_format, &options);
Ok(Value::Null)
}
diff --git a/src/bin/proxmox_backup_client/task.rs b/src/bin/proxmox_backup_client/task.rs
index 6f567f22..e4adaf58 100644
--- a/src/bin/proxmox_backup_client/task.rs
+++ b/src/bin/proxmox_backup_client/task.rs
@@ -64,7 +64,7 @@ async fn task_list(param: Value) -> Result<Value, Error> {
let mut result = client.get("api2/json/nodes/localhost/tasks", Some(args)).await?;
let mut data = result["data"].take();
- let schema = &proxmox_backup::api2::node::tasks::API_RETURN_SCHEMA_LIST_TASKS;
+ let return_type = &proxmox_backup::api2::node::tasks::API_METHOD_LIST_TASKS.returns;
let options = default_table_format_options()
.column(ColumnConfig::new("starttime").right_align(false).renderer(tools::format::render_epoch))
@@ -72,7 +72,7 @@ async fn task_list(param: Value) -> Result<Value, Error> {
.column(ColumnConfig::new("upid"))
.column(ColumnConfig::new("status").renderer(tools::format::render_task_status));
- format_and_print_result_full(&mut data, schema, &output_format, &options);
+ format_and_print_result_full(&mut data, return_type, &output_format, &options);
Ok(Value::Null)
}
diff --git a/src/bin/proxmox_backup_manager/acl.rs b/src/bin/proxmox_backup_manager/acl.rs
index 7511c8cb..b23943ca 100644
--- a/src/bin/proxmox_backup_manager/acl.rs
+++ b/src/bin/proxmox_backup_manager/acl.rs
@@ -47,7 +47,7 @@ fn list_acls(param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<Value, Err
.column(ColumnConfig::new("propagate"))
.column(ColumnConfig::new("roleid"));
- format_and_print_result_full(&mut data, info.returns, &output_format, &options);
+ format_and_print_result_full(&mut data, &info.returns, &output_format, &options);
Ok(Value::Null)
}
diff --git a/src/bin/proxmox_backup_manager/datastore.rs b/src/bin/proxmox_backup_manager/datastore.rs
index 94590aaf..7e596a1b 100644
--- a/src/bin/proxmox_backup_manager/datastore.rs
+++ b/src/bin/proxmox_backup_manager/datastore.rs
@@ -32,7 +32,7 @@ fn list_datastores(param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<Valu
.column(ColumnConfig::new("path"))
.column(ColumnConfig::new("comment"));
- format_and_print_result_full(&mut data, info.returns, &output_format, &options);
+ format_and_print_result_full(&mut data, &info.returns, &output_format, &options);
Ok(Value::Null)
}
@@ -62,7 +62,7 @@ fn show_datastore(param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<Value
};
let options = default_table_format_options();
- format_and_print_result_full(&mut data, info.returns, &output_format, &options);
+ format_and_print_result_full(&mut data, &info.returns, &output_format, &options);
Ok(Value::Null)
}
diff --git a/src/bin/proxmox_backup_manager/disk.rs b/src/bin/proxmox_backup_manager/disk.rs
index 70de4775..164f8831 100644
--- a/src/bin/proxmox_backup_manager/disk.rs
+++ b/src/bin/proxmox_backup_manager/disk.rs
@@ -59,7 +59,7 @@ fn list_disks(mut param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<Value
.column(ColumnConfig::new("status"))
;
- format_and_print_result_full(&mut data, info.returns, &output_format, &options);
+ format_and_print_result_full(&mut data, &info.returns, &output_format, &options);
Ok(Value::Null)
}
@@ -100,7 +100,7 @@ fn smart_attributes(mut param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result
let mut data = data["attributes"].take();
let options = default_table_format_options();
- format_and_print_result_full(&mut data, API_METHOD_SMART_ATTRIBUTES.returns, &output_format, &options);
+ format_and_print_result_full(&mut data, &API_METHOD_SMART_ATTRIBUTES.returns, &output_format, &options);
Ok(Value::Null)
}
@@ -227,7 +227,7 @@ fn list_zpools(mut param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<Valu
.column(ColumnConfig::new("alloc").right_align(true).renderer(render_usage))
.column(ColumnConfig::new("health"));
- format_and_print_result_full(&mut data, info.returns, &output_format, &options);
+ format_and_print_result_full(&mut data, &info.returns, &output_format, &options);
Ok(Value::Null)
}
@@ -274,7 +274,7 @@ fn list_datastore_mounts(mut param: Value, rpcenv: &mut dyn RpcEnvironment) -> R
.column(ColumnConfig::new("filesystem"))
.column(ColumnConfig::new("options"));
- format_and_print_result_full(&mut data, info.returns, &output_format, &options);
+ format_and_print_result_full(&mut data, &info.returns, &output_format, &options);
Ok(Value::Null)
}
diff --git a/src/bin/proxmox_backup_manager/dns.rs b/src/bin/proxmox_backup_manager/dns.rs
index c735b22e..9a91cb06 100644
--- a/src/bin/proxmox_backup_manager/dns.rs
+++ b/src/bin/proxmox_backup_manager/dns.rs
@@ -35,7 +35,7 @@ fn get_dns(mut param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<Value, E
.column(ColumnConfig::new("dns2"))
.column(ColumnConfig::new("dns3"));
- format_and_print_result_full(&mut data, info.returns, &output_format, &options);
+ format_and_print_result_full(&mut data, &info.returns, &output_format, &options);
Ok(Value::Null)
}
diff --git a/src/bin/proxmox_backup_manager/network.rs b/src/bin/proxmox_backup_manager/network.rs
index d7f6382f..c9489f7d 100644
--- a/src/bin/proxmox_backup_manager/network.rs
+++ b/src/bin/proxmox_backup_manager/network.rs
@@ -88,7 +88,7 @@ fn list_network_devices(mut param: Value, rpcenv: &mut dyn RpcEnvironment) -> Re
.column(ColumnConfig::new("gateway").header("gateway").renderer(render_gateway))
.column(ColumnConfig::new("bridge_ports").header("ports/slaves").renderer(render_ports));
- format_and_print_result_full(&mut data, info.returns, &output_format, &options);
+ format_and_print_result_full(&mut data, &info.returns, &output_format, &options);
Ok(Value::Null)
}
diff --git a/src/bin/proxmox_backup_manager/remote.rs b/src/bin/proxmox_backup_manager/remote.rs
index 04d714ef..b1c4aa45 100644
--- a/src/bin/proxmox_backup_manager/remote.rs
+++ b/src/bin/proxmox_backup_manager/remote.rs
@@ -34,7 +34,7 @@ fn list_remotes(param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<Value,
.column(ColumnConfig::new("fingerprint"))
.column(ColumnConfig::new("comment"));
- format_and_print_result_full(&mut data, info.returns, &output_format, &options);
+ format_and_print_result_full(&mut data, &info.returns, &output_format, &options);
Ok(Value::Null)
}
@@ -64,7 +64,7 @@ fn show_remote(param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<Value, E
};
let options = default_table_format_options();
- format_and_print_result_full(&mut data, info.returns, &output_format, &options);
+ format_and_print_result_full(&mut data, &info.returns, &output_format, &options);
Ok(Value::Null)
}
diff --git a/src/bin/proxmox_backup_manager/subscription.rs b/src/bin/proxmox_backup_manager/subscription.rs
index 79813a35..cd69d8d3 100644
--- a/src/bin/proxmox_backup_manager/subscription.rs
+++ b/src/bin/proxmox_backup_manager/subscription.rs
@@ -27,7 +27,7 @@ fn get(param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<Value, Error> {
};
let options = default_table_format_options();
- format_and_print_result_full(&mut data, info.returns, &output_format, &options);
+ format_and_print_result_full(&mut data, &info.returns, &output_format, &options);
Ok(Value::Null)
}
diff --git a/src/bin/proxmox_backup_manager/sync.rs b/src/bin/proxmox_backup_manager/sync.rs
index f21ecb5f..f05f0c8d 100644
--- a/src/bin/proxmox_backup_manager/sync.rs
+++ b/src/bin/proxmox_backup_manager/sync.rs
@@ -35,7 +35,7 @@ fn list_sync_jobs(param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<Value
.column(ColumnConfig::new("schedule"))
.column(ColumnConfig::new("comment"));
- format_and_print_result_full(&mut data, info.returns, &output_format, &options);
+ format_and_print_result_full(&mut data, &info.returns, &output_format, &options);
Ok(Value::Null)
}
@@ -65,7 +65,7 @@ fn show_sync_job(param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<Value,
};
let options = default_table_format_options();
- format_and_print_result_full(&mut data, info.returns, &output_format, &options);
+ format_and_print_result_full(&mut data, &info.returns, &output_format, &options);
Ok(Value::Null)
}
diff --git a/src/bin/proxmox_backup_manager/user.rs b/src/bin/proxmox_backup_manager/user.rs
index 516d27f2..8d78b08c 100644
--- a/src/bin/proxmox_backup_manager/user.rs
+++ b/src/bin/proxmox_backup_manager/user.rs
@@ -46,7 +46,7 @@ fn list_users(param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<Value, Er
.column(ColumnConfig::new("email"))
.column(ColumnConfig::new("comment"));
- format_and_print_result_full(&mut data, info.returns, &output_format, &options);
+ format_and_print_result_full(&mut data, &info.returns, &output_format, &options);
Ok(Value::Null)
}
@@ -87,7 +87,7 @@ fn list_tokens(param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<Value, E
)
.column(ColumnConfig::new("comment"));
- format_and_print_result_full(&mut data, info.returns, &output_format, &options);
+ format_and_print_result_full(&mut data, &info.returns, &output_format, &options);
Ok(Value::Null)
}
diff --git a/src/bin/proxmox_tape/changer.rs b/src/bin/proxmox_tape/changer.rs
index 0a8b794e..9db1644e 100644
--- a/src/bin/proxmox_tape/changer.rs
+++ b/src/bin/proxmox_tape/changer.rs
@@ -104,7 +104,7 @@ fn list_changers(
.column(ColumnConfig::new("serial"))
;
- format_and_print_result_full(&mut data, info.returns, &output_format, &options);
+ format_and_print_result_full(&mut data, &info.returns, &output_format, &options);
Ok(())
}
@@ -139,7 +139,7 @@ fn scan_for_changers(
.column(ColumnConfig::new("serial"))
;
- format_and_print_result_full(&mut data, info.returns, &output_format, &options);
+ format_and_print_result_full(&mut data, &info.returns, &output_format, &options);
Ok(())
}
@@ -175,7 +175,7 @@ fn get_config(
.column(ColumnConfig::new("path"))
;
- format_and_print_result_full(&mut data, info.returns, &output_format, &options);
+ format_and_print_result_full(&mut data, &info.returns, &output_format, &options);
Ok(())
}
@@ -213,7 +213,7 @@ async fn get_status(
.column(ColumnConfig::new("loaded-slot"))
;
- format_and_print_result_full(&mut data, info.returns, &output_format, &options);
+ format_and_print_result_full(&mut data, &info.returns, &output_format, &options);
Ok(())
}
diff --git a/src/bin/proxmox_tape/drive.rs b/src/bin/proxmox_tape/drive.rs
index 0949d450..73e34afc 100644
--- a/src/bin/proxmox_tape/drive.rs
+++ b/src/bin/proxmox_tape/drive.rs
@@ -108,7 +108,7 @@ fn list_drives(
.column(ColumnConfig::new("serial"))
;
- format_and_print_result_full(&mut data, info.returns, &output_format, &options);
+ format_and_print_result_full(&mut data, &info.returns, &output_format, &options);
Ok(())
}
@@ -143,7 +143,7 @@ fn scan_for_drives(
.column(ColumnConfig::new("serial"))
;
- format_and_print_result_full(&mut data, info.returns, &output_format, &options);
+ format_and_print_result_full(&mut data, &info.returns, &output_format, &options);
Ok(())
}
@@ -182,7 +182,7 @@ fn get_config(
.column(ColumnConfig::new("changer-drive-id"))
;
- format_and_print_result_full(&mut data, info.returns, &output_format, &options);
+ format_and_print_result_full(&mut data, &info.returns, &output_format, &options);
Ok(())
}
diff --git a/src/bin/proxmox_tape/media.rs b/src/bin/proxmox_tape/media.rs
index 374cb891..0767bdb0 100644
--- a/src/bin/proxmox_tape/media.rs
+++ b/src/bin/proxmox_tape/media.rs
@@ -106,7 +106,7 @@ async fn list_media(
.column(ColumnConfig::new("media-set-uuid"))
;
- format_and_print_result_full(&mut data, info.returns, &output_format, &options);
+ format_and_print_result_full(&mut data, &info.returns, &output_format, &options);
Ok(())
}
diff --git a/src/bin/proxmox_tape/pool.rs b/src/bin/proxmox_tape/pool.rs
index 23e8e83e..4d477aaf 100644
--- a/src/bin/proxmox_tape/pool.rs
+++ b/src/bin/proxmox_tape/pool.rs
@@ -92,7 +92,7 @@ fn list_pools(
.column(ColumnConfig::new("template"))
;
- format_and_print_result_full(&mut data, info.returns, &output_format, &options);
+ format_and_print_result_full(&mut data, &info.returns, &output_format, &options);
Ok(())
}
@@ -131,7 +131,7 @@ fn get_config(
.column(ColumnConfig::new("template"))
;
- format_and_print_result_full(&mut data, info.returns, &output_format, &options);
+ format_and_print_result_full(&mut data, &info.returns, &output_format, &options);
Ok(())
}
diff --git a/src/server/rest.rs b/src/server/rest.rs
index da110507..c1c4fd55 100644
--- a/src/server/rest.rs
+++ b/src/server/rest.rs
@@ -26,13 +26,14 @@ use proxmox::api::{
ApiHandler,
ApiMethod,
HttpError,
+ ParameterSchema,
Permission,
RpcEnvironment,
RpcEnvironmentType,
check_api_permission,
};
use proxmox::api::schema::{
- ObjectSchema,
+ ObjectSchemaType,
parse_parameter_strings,
parse_simple_value,
verify_json_object,
@@ -233,7 +234,7 @@ impl tower_service::Service<Request<Body>> for ApiService {
}
fn parse_query_parameters<S: 'static + BuildHasher + Send>(
- param_schema: &ObjectSchema,
+ param_schema: ParameterSchema,
form: &str, // x-www-form-urlencoded body data
parts: &Parts,
uri_param: &HashMap<String, String, S>,
@@ -264,7 +265,7 @@ fn parse_query_parameters<S: 'static + BuildHasher + Send>(
}
async fn get_request_parameters<S: 'static + BuildHasher + Send>(
- param_schema: &ObjectSchema,
+ param_schema: ParameterSchema,
parts: Parts,
req_body: Body,
uri_param: HashMap<String, String, S>,
@@ -305,7 +306,7 @@ async fn get_request_parameters<S: 'static + BuildHasher + Send>(
params[&k] = parse_simple_value(&v, prop_schema)?;
}
}
- verify_json_object(¶ms, param_schema)?;
+ verify_json_object(¶ms, ¶m_schema)?;
return Ok(params);
} else {
parse_query_parameters(param_schema, utf8_data, &parts, &uri_param)
--
2.20.1
^ permalink raw reply [flat|nested] 23+ messages in thread
* [pbs-devel] [PATCH backup 2/2] tests: verify-api: check AllOf schemas
2020-12-18 11:25 [pbs-devel] [PATCH proxmox 00/18] Optional Return Types and AllOf schema Wolfgang Bumiller
` (18 preceding siblings ...)
2020-12-18 11:26 ` [pbs-devel] [PATCH backup 1/2] adaptions for proxmox 0.9 and proxmox-api-macro 0.3 Wolfgang Bumiller
@ 2020-12-18 11:26 ` Wolfgang Bumiller
2020-12-22 6:45 ` [pbs-devel] [PATCH proxmox 00/18] Optional Return Types and AllOf schema Dietmar Maurer
20 siblings, 0 replies; 23+ messages in thread
From: Wolfgang Bumiller @ 2020-12-18 11:26 UTC (permalink / raw)
To: pbs-devel
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
---
tests/verify-api.rs | 46 ++++++++++++++++++++++++++++++++++++++++++---
1 file changed, 43 insertions(+), 3 deletions(-)
diff --git a/tests/verify-api.rs b/tests/verify-api.rs
index 944d24cb..7b7371f6 100644
--- a/tests/verify-api.rs
+++ b/tests/verify-api.rs
@@ -1,3 +1,5 @@
+use std::collections::HashSet;
+
use anyhow::{bail, format_err, Error};
use proxmox_backup::api2;
@@ -31,11 +33,41 @@ fn verify_object_schema(schema: &ObjectSchema) -> Result<(), Error> {
Ok(())
}
+// verify entries in an AllOf schema are actually object schemas and that they don't contain
+// duplicate keys
+fn verify_all_of_schema(schema: &AllOfSchema) -> Result<(), Error> {
+ for entry in schema.list {
+ match entry {
+ Schema::Object(obj) => verify_object_schema(obj)?,
+ _ => bail!("AllOf schema with a non-object schema entry!"),
+ }
+ }
+
+ let mut keys = HashSet::<&'static str>::new();
+ let mut dupes = String::new();
+ for property in schema.properties() {
+ if keys.insert(property.0) {
+ if dupes.is_empty() {
+ dupes.push_str(", ");
+ }
+ dupes.push_str(property.0);
+ }
+ }
+ if !dupes.is_empty() {
+ bail!("Duplicate keys found in AllOf schema: {}", dupes);
+ }
+
+ Ok(())
+}
+
fn verify_schema(schema: &Schema) -> Result<(), Error> {
match schema {
Schema::Object(obj_schema) => {
verify_object_schema(obj_schema)?;
}
+ Schema::AllOf(all_of_schema) => {
+ verify_all_of_schema(all_of_schema)?;
+ }
Schema::Array(arr_schema) => {
verify_schema(arr_schema.items)?;
}
@@ -68,10 +100,18 @@ fn verify_api_method(
info: &ApiMethod
) -> Result<(), Error>
{
- verify_object_schema(info.parameters)
- .map_err(|err| format_err!("{} {} parameters: {}", method, path, err))?;
+ match &info.parameters {
+ ParameterSchema::Object(obj) => {
+ verify_object_schema(obj)
+ .map_err(|err| format_err!("{} {} parameters: {}", method, path, err))?;
+ }
+ ParameterSchema::AllOf(all_of) => {
+ verify_all_of_schema(all_of)
+ .map_err(|err| format_err!("{} {} parameters: {}", method, path, err))?;
+ }
+ }
- verify_schema(info.returns)
+ verify_schema(info.returns.schema)
.map_err(|err| format_err!("{} {} returns: {}", method, path, err))?;
verify_access_permissions(info.access.permission)
--
2.20.1
^ permalink raw reply [flat|nested] 23+ messages in thread
* Re: [pbs-devel] [PATCH proxmox 00/18] Optional Return Types and AllOf schema
2020-12-18 11:25 [pbs-devel] [PATCH proxmox 00/18] Optional Return Types and AllOf schema Wolfgang Bumiller
` (19 preceding siblings ...)
2020-12-18 11:26 ` [pbs-devel] [PATCH backup 2/2] tests: verify-api: check AllOf schemas Wolfgang Bumiller
@ 2020-12-22 6:45 ` Dietmar Maurer
2020-12-22 6:51 ` Dietmar Maurer
20 siblings, 1 reply; 23+ messages in thread
From: Dietmar Maurer @ 2020-12-22 6:45 UTC (permalink / raw)
To: Proxmox Backup Server development discussion, Wolfgang Bumiller
applied, but now I am unable to build the debian packages:
# make deb
./build.sh proxmox
Something failed: ErrorMessage { msg: "failed to find proxmox v0.9.0 (/home/dietmar/rust/proxmox/proxmox) in path source" }
make: *** [Makefile:21: proxmox-deb] Error 1
any ideas?
^ permalink raw reply [flat|nested] 23+ messages in thread
* Re: [pbs-devel] [PATCH proxmox 00/18] Optional Return Types and AllOf schema
2020-12-22 6:45 ` [pbs-devel] [PATCH proxmox 00/18] Optional Return Types and AllOf schema Dietmar Maurer
@ 2020-12-22 6:51 ` Dietmar Maurer
0 siblings, 0 replies; 23+ messages in thread
From: Dietmar Maurer @ 2020-12-22 6:51 UTC (permalink / raw)
To: Proxmox Backup Server development discussion, Wolfgang Bumiller
Ignore me, I found the problem
Versions in Cargo.toml needed an update...
> On 12/22/2020 7:45 AM Dietmar Maurer <dietmar@proxmox.com> wrote:
>
>
> applied, but now I am unable to build the debian packages:
>
> # make deb
> ./build.sh proxmox
> Something failed: ErrorMessage { msg: "failed to find proxmox v0.9.0 (/home/dietmar/rust/proxmox/proxmox) in path source" }
> make: *** [Makefile:21: proxmox-deb] Error 1
>
>
> any ideas?
>
>
> _______________________________________________
> pbs-devel mailing list
> pbs-devel@lists.proxmox.com
> https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel
^ permalink raw reply [flat|nested] 23+ messages in thread
end of thread, other threads:[~2020-12-22 6:52 UTC | newest]
Thread overview: 23+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2020-12-18 11:25 [pbs-devel] [PATCH proxmox 00/18] Optional Return Types and AllOf schema Wolfgang Bumiller
2020-12-18 11:25 ` [pbs-devel] [PATCH proxmox 01/18] formatting fixup Wolfgang Bumiller
2020-12-18 11:25 ` [pbs-devel] [PATCH proxmox 02/18] schema: support optional return values Wolfgang Bumiller
2020-12-18 11:25 ` [pbs-devel] [PATCH proxmox 03/18] api-macro: " Wolfgang Bumiller
2020-12-18 11:25 ` [pbs-devel] [PATCH proxmox 04/18] schema: support AllOf schemas Wolfgang Bumiller
2020-12-18 11:25 ` [pbs-devel] [PATCH proxmox 05/18] schema: allow AllOf schema as method parameter Wolfgang Bumiller
2020-12-18 11:25 ` [pbs-devel] [PATCH proxmox 06/18] api-macro: add 'flatten' to SerdeAttrib Wolfgang Bumiller
2020-12-18 11:25 ` [pbs-devel] [PATCH proxmox 07/18] api-macro: forbid flattened fields Wolfgang Bumiller
2020-12-18 11:25 ` [pbs-devel] [PATCH proxmox 08/18] api-macro: add more standard Maybe methods Wolfgang Bumiller
2020-12-18 11:25 ` [pbs-devel] [PATCH proxmox 09/18] api-macro: suport AllOf on structs Wolfgang Bumiller
2020-12-18 11:25 ` [pbs-devel] [PATCH proxmox 10/18] schema: ExtractValueDeserializer Wolfgang Bumiller
2020-12-18 11:25 ` [pbs-devel] [PATCH proxmox 11/18] api-macro: object schema entry tuple -> struct Wolfgang Bumiller
2020-12-18 11:26 ` [pbs-devel] [PATCH proxmox 12/18] api-macro: more tuple refactoring Wolfgang Bumiller
2020-12-18 11:26 ` [pbs-devel] [PATCH proxmox 13/18] api-macro: factor parameter extraction into a function Wolfgang Bumiller
2020-12-18 11:26 ` [pbs-devel] [PATCH proxmox 14/18] api-macro: support flattened parameters Wolfgang Bumiller
2020-12-18 11:26 ` [pbs-devel] [PATCH proxmox 15/18] schema: ParameterSchema at 'api' level Wolfgang Bumiller
2020-12-18 11:26 ` [pbs-devel] [PATCH proxmox 16/18] proxmox: temporary d/changelog update Wolfgang Bumiller
2020-12-18 11:26 ` [pbs-devel] [PATCH proxmox 17/18] macro: " Wolfgang Bumiller
2020-12-18 11:26 ` [pbs-devel] [PATCH proxmox 18/18] proxmox changelog update Wolfgang Bumiller
2020-12-18 11:26 ` [pbs-devel] [PATCH backup 1/2] adaptions for proxmox 0.9 and proxmox-api-macro 0.3 Wolfgang Bumiller
2020-12-18 11:26 ` [pbs-devel] [PATCH backup 2/2] tests: verify-api: check AllOf schemas Wolfgang Bumiller
2020-12-22 6:45 ` [pbs-devel] [PATCH proxmox 00/18] Optional Return Types and AllOf schema Dietmar Maurer
2020-12-22 6:51 ` Dietmar Maurer
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal