[pbs-devel] [PATCH proxmox 04/18] schema: support AllOf schemas

Wolfgang Bumiller w.bumiller at proxmox.com
Fri Dec 18 12:25:52 CET 2020


Signed-off-by: Wolfgang Bumiller <w.bumiller at 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






More information about the pbs-devel mailing list