api-macro: allow declaring an additional-properties field
Object schemas can now declare a field which causes 'additional_properties' to be set to true and the field being ignored in the schema. This allows adding a flattened HashMap<String, Value> to gather the additional unspecified properties. #[api(additional_properties: "rest")] struct Something { #[serde(flatten)] rest: HashMap<String, Value>, } Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
This commit is contained in:
parent
e72528ca70
commit
2b3c356ece
@ -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<ObjectEntry>,
|
||||
additional_properties: Option<AdditionalProperties>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum AdditionalProperties {
|
||||
/// `additional_properties: false`.
|
||||
No,
|
||||
/// `additional_properties: true`.
|
||||
Ignored,
|
||||
/// `additional_properties: "field_name"`.
|
||||
Field(syn::LitStr),
|
||||
}
|
||||
|
||||
impl TryFrom<JSONValue> for AdditionalProperties {
|
||||
type Error = syn::Error;
|
||||
|
||||
fn try_from(value: JSONValue) -> Result<Self, Self::Error> {
|
||||
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<String> {
|
||||
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<Self, syn::Error> {
|
||||
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")?
|
||||
|
@ -141,9 +141,15 @@ fn handle_regular_struct(
|
||||
// fields if there are any.
|
||||
let mut schema_fields: HashMap<String, &mut ObjectEntry> = 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<String, Value>` collecting all the values that have no schema
|
||||
continue;
|
||||
}
|
||||
|
||||
match schema_fields.remove(&name) {
|
||||
Some(field_def) => {
|
||||
if attrs.flatten {
|
||||
|
@ -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<String, Value>,
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user