From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) by lore.proxmox.com (Postfix) with ESMTPS id E0A821FF13E for ; Fri, 03 Apr 2026 18:55:00 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id AABB97AEB; Fri, 3 Apr 2026 18:55:31 +0200 (CEST) From: Christoph Heiss To: pdm-devel@lists.proxmox.com Subject: [PATCH proxmox v3 02/38] schema: oneOf: allow single string variant Date: Fri, 3 Apr 2026 18:53:34 +0200 Message-ID: <20260403165437.2166551-3-c.heiss@proxmox.com> X-Mailer: git-send-email 2.53.0 In-Reply-To: <20260403165437.2166551-1-c.heiss@proxmox.com> References: <20260403165437.2166551-1-c.heiss@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1775235236329 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.063 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: CJL3QRWGRA6Q4YF5KKIATRIDYTJPYSVR X-Message-ID-Hash: CJL3QRWGRA6Q4YF5KKIATRIDYTJPYSVR X-MailFrom: c.heiss@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox Datacenter Manager development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: This allows a `OneOfSchema` to additionally have a single string variant, i.e. allows to deserialize from either a plain string or some object. Signed-off-by: Christoph Heiss --- Changes v2 -> v3: * new patch proxmox-schema/src/schema.rs | 68 +++++++++++++++++++++++-- proxmox-schema/tests/schema.rs | 91 +++++++++++++++++++++++++++++++++- 2 files changed, 155 insertions(+), 4 deletions(-) diff --git a/proxmox-schema/src/schema.rs b/proxmox-schema/src/schema.rs index 47ee94df..24815bdb 100644 --- a/proxmox-schema/src/schema.rs +++ b/proxmox-schema/src/schema.rs @@ -912,6 +912,20 @@ const fn assert_one_of_list_is_sorted(list: &[(&str, &Schema)]) { } } +const fn assert_one_of_zero_or_one_string_schema(list: &[(&str, &Schema)]) { + let mut i = 0; + let mut already_seen = false; + while i != list.len() { + if let Schema::String(_) = list[i].1 { + if already_seen { + panic!("oneOf can have only zero or one string variants"); + } + already_seen = true; + } + i += 1; + } +} + impl OneOfSchema { /// Create a new `oneOf` schema. /// @@ -947,6 +961,27 @@ impl OneOfSchema { /// ).schema(); /// ``` /// + /// There is also support for the data to be either a string or some object: + /// + /// ``` + /// # use proxmox_schema::{OneOfSchema, ObjectSchema, Schema, StringSchema}; + /// # const SCHEMA_V1: Schema = ObjectSchema::new( + /// # "Some Object", + /// # &[ + /// # ("key1", false, &StringSchema::new("A String").schema()), + /// # ("key2", false, &StringSchema::new("Another String").schema()), + /// # ], + /// # ).schema(); + /// const SCHEMA: Schema = OneOfSchema::new( + /// "A plain string or some enum", + /// &("type", false, &StringSchema::new("v1 or v2").schema()), + /// &[ + /// ("plain-string", &StringSchema::new("some string").schema()), + /// ("v1", &SCHEMA_V1), + /// ], + /// ).schema(); + /// ``` + /// /// These will panic: /// /// ```compile_fail,E0080 @@ -1001,12 +1036,28 @@ impl OneOfSchema { /// ], /// ).schema(); /// ``` + /// + /// ```compile_fail,E0080 + /// # use proxmox_schema::{OneOfSchema, ObjectSchema, Schema, StringSchema}; + /// # const SCHEMA_V1: Schema = &StringSchema::new("A String").schema() + /// # const SCHEMA_V2: Schema = &StringSchema::new("Another String").schema() + /// const SCHEMA: Schema = OneOfSchema::new( + /// "Some enum", + /// &("type", false, &StringSchema::new("v1 or v2").schema()), + /// &[ + /// ("v1", &SCHEMA_V1), + /// // more than one string schema: + /// ("v2", &SCHEMA_V2), + /// ], + /// ).schema(); + /// ``` pub const fn new( description: &'static str, type_property_entry: &'static SchemaPropertyEntry, list: &'static [(&'static str, &'static Schema)], ) -> Self { assert_one_of_list_is_sorted(list); + assert_one_of_zero_or_one_string_schema(list); Self { description, type_property_entry, @@ -1065,6 +1116,12 @@ impl OneOfSchema { ) -> Result { ParameterSchema::from(self).parse_parameter_strings(data, test_required) } + + fn string_variant(&self) -> Option<&Schema> { + self.list + .iter() + .find_map(|(_, item)| matches!(item, Schema::String(_)).then_some(&**item)) + } } mod private { @@ -1271,11 +1328,12 @@ impl ObjectSchemaType for OneOfSchema { } fn additional_properties(&self) -> bool { - self.list.iter().any(|(_, schema)| { - schema + self.list.iter().any(|(_, schema)| match schema { + Schema::String(_) => false, + _ => schema .any_object() .expect("non-object-schema in `OneOfSchema`") - .additional_properties() + .additional_properties(), }) } @@ -1286,6 +1344,10 @@ impl ObjectSchemaType for OneOfSchema { fn verify_json(&self, data: &Value) -> Result<(), Error> { let map = match data { Value::Object(map) => map, + Value::String(_) => match self.string_variant() { + Some(schema) => return schema.verify_json(data), + None => bail!("Expected object - got string value."), + }, Value::Array(_) => bail!("Expected object - got array."), _ => bail!("Expected object - got scalar value."), }; diff --git a/proxmox-schema/tests/schema.rs b/proxmox-schema/tests/schema.rs index 24c32bef..22d6538e 100644 --- a/proxmox-schema/tests/schema.rs +++ b/proxmox-schema/tests/schema.rs @@ -1,5 +1,5 @@ use anyhow::bail; -use serde_json::Value; +use serde_json::{json, Value}; use url::form_urlencoded; use proxmox_schema::*; @@ -390,3 +390,92 @@ fn test_verify_complex_array() { assert!(res.is_err()); } } + +#[test] +fn test_one_of_schema_string_variant() { + const OBJECT1_SCHEMA: Schema = ObjectSchema::new( + "Object 1", + &[ + ("a", false, &StringSchema::new("A property").schema()), + ("type", false, &StringSchema::new("v1 or v2").schema()), + ], + ) + .schema(); + const OBJECT2_SCHEMA: Schema = ObjectSchema::new( + "Object 2", + &[ + ( + "b", + true, + &StringSchema::new("A optional property").schema(), + ), + ("type", false, &StringSchema::new("v1 or v2").schema()), + ], + ) + .schema(); + + const NO_STRING_VARIANT_SCHEMA: OneOfSchema = OneOfSchema::new( + "An oneOf schema", + &("type", false, &StringSchema::new("v1 or v2").schema()), + &[("v1", &OBJECT1_SCHEMA), ("v2", &OBJECT2_SCHEMA)], + ); + + const ONE_STRING_VARIANT_SCHEMA: OneOfSchema = OneOfSchema::new( + "An oneOf schema with a string variant", + &( + "type", + false, + &StringSchema::new("string or v1 or v2").schema(), + ), + &[ + ( + "name does not matter", + &StringSchema::new("A string").schema(), + ), + ("v1", &OBJECT1_SCHEMA), + ("v2", &OBJECT2_SCHEMA), + ], + ); + + NO_STRING_VARIANT_SCHEMA + .verify_json(&json!({ + "type": "v1", "a": "foo" + })) + .expect("should verify"); + + ONE_STRING_VARIANT_SCHEMA + .verify_json(&json!({ + "type": "v2", "b": "foo" + })) + .expect("should verify"); + + ONE_STRING_VARIANT_SCHEMA + .verify_json(&json!("plain string")) + .expect("should verify"); +} + +#[test] +#[should_panic(expected = "oneOf can have only zero or one string variants")] +fn test_one_of_schema_with_multiple_string_variant() { + const OBJECT1_SCHEMA: Schema = ObjectSchema::new( + "Object 1", + &[ + ("a", false, &StringSchema::new("A property").schema()), + ("type", false, &StringSchema::new("v1 or v2").schema()), + ], + ) + .schema(); + const TYPE_SCHEMA: Schema = StringSchema::new("string or string or v1").schema(); + const STRING1_SCHEMA: Schema = StringSchema::new("A string").schema(); + const STRING2_SCHEMA: Schema = StringSchema::new("Another string").schema(); + + let _ = OneOfSchema::new( + "An invalid oneOf schema with multiple string variant", + &("type", false, &TYPE_SCHEMA), + &[ + ("string variant 1", &STRING1_SCHEMA), + ("v1", &OBJECT1_SCHEMA), + ("whoops", &STRING2_SCHEMA), + ], + ); +} -- 2.53.0