mirror of
https://git.proxmox.com/git/proxmox
synced 2025-08-06 13:14:42 +00:00
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 =>
|
ts.extend(quote_spanned! { obj.span =>
|
||||||
::proxmox_schema::ObjectSchema::new(#description, &[#elems])
|
::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) => {
|
SchemaItem::Array(array) => {
|
||||||
let description = check_description()?;
|
let description = check_description()?;
|
||||||
@ -516,6 +523,51 @@ impl ObjectEntry {
|
|||||||
pub struct SchemaObject {
|
pub struct SchemaObject {
|
||||||
span: Span,
|
span: Span,
|
||||||
properties_: Vec<ObjectEntry>,
|
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 {
|
impl SchemaObject {
|
||||||
@ -523,6 +575,7 @@ impl SchemaObject {
|
|||||||
Self {
|
Self {
|
||||||
span,
|
span,
|
||||||
properties_: Vec::new(),
|
properties_: Vec::new(),
|
||||||
|
additional_properties: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -574,6 +627,10 @@ impl SchemaObject {
|
|||||||
fn try_extract_from(obj: &mut JSONObject) -> Result<Self, syn::Error> {
|
fn try_extract_from(obj: &mut JSONObject) -> Result<Self, syn::Error> {
|
||||||
let mut this = Self {
|
let mut this = Self {
|
||||||
span: obj.span(),
|
span: obj.span(),
|
||||||
|
additional_properties: obj
|
||||||
|
.remove("additional_properties")
|
||||||
|
.map(AdditionalProperties::try_from)
|
||||||
|
.transpose()?,
|
||||||
properties_: obj
|
properties_: obj
|
||||||
.remove_required_element("properties")?
|
.remove_required_element("properties")?
|
||||||
.into_object("object field definition")?
|
.into_object("object field definition")?
|
||||||
|
@ -141,9 +141,15 @@ fn handle_regular_struct(
|
|||||||
// fields if there are any.
|
// fields if there are any.
|
||||||
let mut schema_fields: HashMap<String, &mut ObjectEntry> = HashMap::new();
|
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
|
// We also keep a reference to the SchemaObject around since we derive missing fields
|
||||||
// automatically.
|
// automatically.
|
||||||
if let SchemaItem::Object(obj) = &mut schema.item {
|
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() {
|
for field in obj.properties_mut() {
|
||||||
schema_fields.insert(field.name.as_str().to_string(), field);
|
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) {
|
match schema_fields.remove(&name) {
|
||||||
Some(field_def) => {
|
Some(field_def) => {
|
||||||
if attrs.flatten {
|
if attrs.flatten {
|
||||||
|
@ -3,6 +3,8 @@
|
|||||||
|
|
||||||
#![allow(dead_code)]
|
#![allow(dead_code)]
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use proxmox_api_macro::api;
|
use proxmox_api_macro::api;
|
||||||
use proxmox_schema as schema;
|
use proxmox_schema as schema;
|
||||||
use proxmox_schema::{ApiType, EnumEntry};
|
use proxmox_schema::{ApiType, EnumEntry};
|
||||||
@ -11,6 +13,8 @@ use anyhow::Error;
|
|||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
|
pub const TEXT_SCHEMA: schema::Schema = schema::StringSchema::new("Text.").schema();
|
||||||
|
|
||||||
#[api(
|
#[api(
|
||||||
type: String,
|
type: String,
|
||||||
description: "A string",
|
description: "A string",
|
||||||
@ -186,3 +190,27 @@ fn string_check_schema_test() {
|
|||||||
pub struct RenamedAndDescribed {
|
pub struct RenamedAndDescribed {
|
||||||
a_field: String,
|
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