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 49AC061EB7 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 04C272FBB9 for ; Fri, 18 Dec 2020 12:37:48 +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 469282F838 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 5EEC0453A0 for ; Fri, 18 Dec 2020 12:26:21 +0100 (CET) From: Wolfgang Bumiller To: pbs-devel@lists.proxmox.com Date: Fri, 18 Dec 2020 12:25:52 +0100 Message-Id: <20201218112608.6845-5-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.027 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. [schema.rs, schema.properties, format.rs, self.properties, param.properties] Subject: [pbs-devel] [PATCH proxmox 04/18] schema: support AllOf schemas 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/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 { // 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( +fn format_table>( output: W, list: &mut Vec, - schema: &ObjectSchema, + schema: &dyn ObjectSchemaType, 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( Ok(()) } -fn format_object( +fn format_object>( output: W, data: &Value, - schema: &ObjectSchema, + schema: &dyn ObjectSchemaType, 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( render_table(output, &tabledata, &column_names, options) } -fn extract_properties_to_print(schema: &ObjectSchema) -> Vec { +fn extract_properties_to_print(properties: I) -> Vec +where + I: Iterator, +{ 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( 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(""), Schema::Array(_) => String::from(""), + Schema::AllOf(_) => String::from(""), } } @@ -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(param: &dyn ObjectSchemaType) -> String +where + I: Iterator, +{ + let mut res = wrap_text("", "", param.description(), 80); let mut required_list: Vec = Vec::new(); let mut optional_list: Vec = 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; + + 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>, +} + +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( + data: &Value, + schema: &dyn ObjectSchemaType, +) -> Result<(), Error> +where + I: Iterator, +{ 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