all lists on lists.proxmox.com
 help / color / mirror / Atom feed
From: Wolfgang Bumiller <w.bumiller@proxmox.com>
To: pbs-devel@lists.proxmox.com
Subject: [pbs-devel] [PATCH proxmox 04/18] schema: support AllOf schemas
Date: Fri, 18 Dec 2020 12:25:52 +0100	[thread overview]
Message-ID: <20201218112608.6845-5-w.bumiller@proxmox.com> (raw)
In-Reply-To: <20201218112608.6845-1-w.bumiller@proxmox.com>

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





  parent reply	other threads:[~2020-12-18 11:38 UTC|newest]

Thread overview: 23+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
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 ` Wolfgang Bumiller [this message]
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

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20201218112608.6845-5-w.bumiller@proxmox.com \
    --to=w.bumiller@proxmox.com \
    --cc=pbs-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
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