From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id 2F5AA61EA1 for ; Fri, 18 Dec 2020 12:38:18 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id CD6B22FBB8 for ; Fri, 18 Dec 2020 12:37:47 +0100 (CET) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [212.186.127.180]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS id 17DAA2F82A for ; Fri, 18 Dec 2020 12:37:33 +0100 (CET) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id 7937C453B4 for ; Fri, 18 Dec 2020 12:26:23 +0100 (CET) From: Wolfgang Bumiller To: pbs-devel@lists.proxmox.com Date: Fri, 18 Dec 2020 12:25:53 +0100 Message-Id: <20201218112608.6845-6-w.bumiller@proxmox.com> X-Mailer: git-send-email 2.20.1 In-Reply-To: <20201218112608.6845-1-w.bumiller@proxmox.com> References: <20201218112608.6845-1-w.bumiller@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL 0.028 Adjusted score from AWL reputation of From: address KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [format.rs, router.rs, parameters.properties, getopts.rs, completion.rs, schema.properties, command.rs, schema.rs] Subject: [pbs-devel] [PATCH proxmox 05/18] schema: allow AllOf schema as method parameter X-BeenThere: pbs-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Backup Server development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Fri, 18 Dec 2020 11:38:18 -0000 Signed-off-by: Wolfgang Bumiller --- 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) -> 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, - 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 { let mut done: HashMap = 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>( args: &[T], - schema: &ObjectSchema, + schema: ParameterSchema, errors: &mut ParameterError, ) -> (Vec<(String, String)>, Vec) { let mut data: Vec<(String, String)> = vec![]; @@ -149,7 +150,7 @@ pub fn parse_arguments>( args: &[T], arg_param: &[&str], fixed_param: &HashMap<&'static str, String>, - schema: &ObjectSchema, + schema: ParameterSchema, ) -> Result<(Value, Vec), 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 - + Send - + Sync - + 'static); +pub type ApiHandlerFn = + &'static (dyn Fn(Value, &ApiMethod, &mut dyn RpcEnvironment) -> Result + + 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>; + + 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 { } /// Parse a complex property string (`ApiStringFormat::PropertyString`) -pub fn parse_property_string(value_str: &str, schema: &Schema) -> Result { +pub fn parse_property_string(value_str: &str, schema: &'static Schema) -> Result { 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 { let mut array: Vec = vec![]; @@ -839,16 +840,24 @@ pub fn parse_simple_value(value_str: &str, schema: &Schema) -> Result>( data: &[(String, String)], - schema: &ObjectSchema, + schema: T, + test_required: bool, +) -> Result { + do_parse_parameter_strings(data, schema.into(), test_required) +} + +fn do_parse_parameter_strings( + data: &[(String, String)], + schema: ParameterSchema, test_required: bool, ) -> Result { 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>( query: &str, - schema: &ObjectSchema, + schema: T, test_required: bool, ) -> Result { 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