diff --git a/proxmox-api-macro/src/api/mod.rs b/proxmox-api-macro/src/api/mod.rs index abd1b036..e346101b 100644 --- a/proxmox-api-macro/src/api/mod.rs +++ b/proxmox-api-macro/src/api/mod.rs @@ -357,6 +357,13 @@ impl SchemaItem { ts.extend(quote_spanned! { obj.span => ::proxmox_schema::ObjectSchema::new(#description, &[#elems]) }); + if obj + .additional_properties + .as_ref() + .is_some_and(|a| a.to_bool()) + { + ts.extend(quote_spanned! { obj.span => .additional_properties(true) }); + } } SchemaItem::Array(array) => { let description = check_description()?; @@ -516,6 +523,51 @@ impl ObjectEntry { pub struct SchemaObject { span: Span, properties_: Vec, + additional_properties: Option, +} + +#[derive(Clone)] +pub enum AdditionalProperties { + /// `additional_properties: false`. + No, + /// `additional_properties: true`. + Ignored, + /// `additional_properties: "field_name"`. + Field(syn::LitStr), +} + +impl TryFrom for AdditionalProperties { + type Error = syn::Error; + + fn try_from(value: JSONValue) -> Result { + let span = value.span(); + if let JSONValue::Expr(syn::Expr::Lit(expr_lit)) = value { + match expr_lit.lit { + syn::Lit::Str(s) => return Ok(Self::Field(s)), + syn::Lit::Bool(b) => { + return Ok(if b.value() { Self::Ignored } else { Self::No }); + } + _ => (), + } + } + bail!( + span, + "invalid value for additional_properties, expected boolean or field name" + ); + } +} + +impl AdditionalProperties { + pub fn to_option_string(&self) -> Option { + match self { + Self::Field(name) => Some(name.value()), + _ => None, + } + } + + pub fn to_bool(&self) -> bool { + !matches!(self, Self::No) + } } impl SchemaObject { @@ -523,6 +575,7 @@ impl SchemaObject { Self { span, properties_: Vec::new(), + additional_properties: None, } } @@ -574,6 +627,10 @@ impl SchemaObject { fn try_extract_from(obj: &mut JSONObject) -> Result { let mut this = Self { span: obj.span(), + additional_properties: obj + .remove("additional_properties") + .map(AdditionalProperties::try_from) + .transpose()?, properties_: obj .remove_required_element("properties")? .into_object("object field definition")? diff --git a/proxmox-api-macro/src/api/structs.rs b/proxmox-api-macro/src/api/structs.rs index aac74817..ee537ff0 100644 --- a/proxmox-api-macro/src/api/structs.rs +++ b/proxmox-api-macro/src/api/structs.rs @@ -141,9 +141,15 @@ fn handle_regular_struct( // fields if there are any. let mut schema_fields: HashMap = HashMap::new(); + let mut additional_properties = None; + // We also keep a reference to the SchemaObject around since we derive missing fields // automatically. if let SchemaItem::Object(obj) = &mut schema.item { + additional_properties = obj + .additional_properties + .as_ref() + .and_then(|a| a.to_option_string()); for field in obj.properties_mut() { schema_fields.insert(field.name.as_str().to_string(), field); } @@ -178,6 +184,12 @@ fn handle_regular_struct( } }; + if additional_properties.as_deref() == Some(name.as_ref()) { + // we just *skip* the additional properties field, it is supposed to be a flattened + // `HashMap` collecting all the values that have no schema + continue; + } + match schema_fields.remove(&name) { Some(field_def) => { if attrs.flatten { diff --git a/proxmox-api-macro/tests/types.rs b/proxmox-api-macro/tests/types.rs index b4b7250e..e645fa84 100644 --- a/proxmox-api-macro/tests/types.rs +++ b/proxmox-api-macro/tests/types.rs @@ -3,6 +3,8 @@ #![allow(dead_code)] +use std::collections::HashMap; + use proxmox_api_macro::api; use proxmox_schema as schema; use proxmox_schema::{ApiType, EnumEntry}; @@ -11,6 +13,8 @@ use anyhow::Error; use serde::Deserialize; use serde_json::Value; +pub const TEXT_SCHEMA: schema::Schema = schema::StringSchema::new("Text.").schema(); + #[api( type: String, description: "A string", @@ -186,3 +190,27 @@ fn string_check_schema_test() { pub struct RenamedAndDescribed { a_field: String, } + +#[api( + properties: {}, + additional_properties: "rest", +)] +#[derive(Deserialize)] +/// Some Description. +pub struct UnspecifiedData { + /// Text. + field: String, + + /// Remaining data. + rest: HashMap, +} + +#[test] +fn additional_properties_test() { + const TEST_UNSPECIFIED: ::proxmox_schema::Schema = + ::proxmox_schema::ObjectSchema::new("Some Description.", &[("field", false, &TEXT_SCHEMA)]) + .additional_properties(true) + .schema(); + + assert_eq!(TEST_UNSPECIFIED, UnspecifiedData::API_SCHEMA); +}