From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id 803D36E165 for ; Tue, 29 Mar 2022 10:56:59 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 757F22C8B9 for ; Tue, 29 Mar 2022 10:56:59 +0200 (CEST) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [94.136.29.106]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS id 7441F2C8AE for ; Tue, 29 Mar 2022 10:56:57 +0200 (CEST) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id 4863E418B9 for ; Tue, 29 Mar 2022 10:56:57 +0200 (CEST) From: Wolfgang Bumiller To: pbs-devel@lists.proxmox.com Date: Tue, 29 Mar 2022 10:56:50 +0200 Message-Id: <20220329085650.37508-1-w.bumiller@proxmox.com> X-Mailer: git-send-email 2.30.2 MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL 0.298 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment PROLO_LEO2 0.1 Meta Catches all Leo drug variations so far SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record T_SCC_BODY_TEXT_LINE -0.01 - URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [0.properties, schema.rs] Subject: [pbs-devel] [PATCH proxmox] schema: implement Serialize for Schema w/ perl support X-BeenThere: pbs-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Backup Server development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Tue, 29 Mar 2022 08:56:59 -0000 This implements `Serialize` for the Schema, optionally enabling perl compatibility via xsubs for the validator methods. NOTE: Currently, when serializing to perl, patterns are serialized as regex strings and thus subject to differences between perl and the regex crate. We may want to instead warp these in xsubs running the regex crate's pattern matcher instead, in which case perl wouldn't see the pattern regex at all. Also NOTE: The 'default_key' object schema property is moved into the corresponding key as a boolean as we do in the JSONSchema in perl. Signed-off-by: Wolfgang Bumiller --- This also allows us to simply drop the custom serialization in the `docgen` binary in pbs, and is otherwise a big step towards using rust schemas in perl code. proxmox-schema/Cargo.toml | 2 + proxmox-schema/src/schema.rs | 253 +++++++++++++++++++- proxmox-schema/tests/schema_verification.rs | 30 +++ 3 files changed, 280 insertions(+), 5 deletions(-) diff --git a/proxmox-schema/Cargo.toml b/proxmox-schema/Cargo.toml index 19d35e2..7204db3 100644 --- a/proxmox-schema/Cargo.toml +++ b/proxmox-schema/Cargo.toml @@ -22,6 +22,8 @@ nix = { version = "0.19", optional = true } proxmox-api-macro = { path = "../proxmox-api-macro", optional = true, version = "1.0.0" } +perlmod = { version = "0.13.1", optional = true } + [dev-dependencies] url = "2.1" serde = { version = "1.0", features = [ "derive" ] } diff --git a/proxmox-schema/src/schema.rs b/proxmox-schema/src/schema.rs index 39aca45..b41742a 100644 --- a/proxmox-schema/src/schema.rs +++ b/proxmox-schema/src/schema.rs @@ -7,6 +7,7 @@ use std::fmt; use anyhow::{bail, format_err, Error}; +use serde::Serialize; use serde_json::{json, Value}; use crate::ConstRegexPattern; @@ -170,11 +171,13 @@ impl<'a> FromIterator<(&'a str, Error)> for ParameterError { } /// Data type to describe boolean values -#[derive(Debug)] +#[derive(Debug, Serialize)] #[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))] pub struct BooleanSchema { pub description: &'static str, + /// Optional default value. + #[serde(skip_serializing_if = "Option::is_none")] pub default: Option, } @@ -205,15 +208,21 @@ impl BooleanSchema { } /// Data type to describe integer values. -#[derive(Debug)] +#[derive(Debug, Serialize)] #[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))] pub struct IntegerSchema { pub description: &'static str, + /// Optional minimum. + #[serde(skip_serializing_if = "Option::is_none")] pub minimum: Option, + /// Optional maximum. + #[serde(skip_serializing_if = "Option::is_none")] pub maximum: Option, + /// Optional default. + #[serde(skip_serializing_if = "Option::is_none")] pub default: Option, } @@ -281,14 +290,20 @@ impl IntegerSchema { } /// Data type to describe (JSON like) number value -#[derive(Debug)] +#[derive(Debug, Serialize)] pub struct NumberSchema { pub description: &'static str, + /// Optional minimum. + #[serde(skip_serializing_if = "Option::is_none")] pub minimum: Option, + /// Optional maximum. + #[serde(skip_serializing_if = "Option::is_none")] pub maximum: Option, + /// Optional default. + #[serde(skip_serializing_if = "Option::is_none")] pub default: Option, } @@ -495,19 +510,60 @@ impl StringSchema { } } +impl Serialize for StringSchema { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeMap; + + let mut map = serializer.serialize_map(None)?; + + map.serialize_entry("description", self.description)?; + if let Some(v) = &self.default { + map.serialize_entry("default", v)?; + } + if let Some(v) = &self.min_length { + map.serialize_entry("minLength", v)?; + } + if let Some(v) = &self.max_length { + map.serialize_entry("maxLength", v)?; + } + if let Some(v) = &self.format { + map = v.serialize_inner(map)?; + } + if let Some(v) = &self.type_text { + map.serialize_entry("typetext", v)?; + } else if let Some(ApiStringFormat::PropertyString(schema)) = &self.format { + map.serialize_entry( + "typetext", + &crate::format::get_property_string_type_text(schema), + )?; + } + + map.end() + } +} + /// Data type to describe array of values. /// /// All array elements are of the same type, as defined in the `items` /// schema. -#[derive(Debug)] +#[derive(Debug, Serialize)] #[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))] +#[serde(rename_all = "camelCase")] pub struct ArraySchema { pub description: &'static str, + /// Element type schema. pub items: &'static Schema, + /// Optional minimal length. + #[serde(skip_serializing_if = "Option::is_none")] pub min_length: Option, + /// Optional maximal length. + #[serde(skip_serializing_if = "Option::is_none")] pub max_length: Option, } @@ -656,6 +712,63 @@ impl ObjectSchema { } } +impl Serialize for ObjectSchema { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeMap; + + struct Helper<'a>(&'a ObjectSchema); + + impl Serialize for Helper<'_> { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut map = serializer.serialize_map(Some(self.0.properties.len()))?; + for (name, optional, schema) in self.0.properties { + map.serialize_entry( + name, + &PropertyEntrySerializer { + schema: *schema, + optional: *optional, + default_key: self.0.default_key == Some(name), + }, + )?; + } + map.end() + } + } + + let mut map = serializer.serialize_map(None)?; + + map.serialize_entry("description", self.description)?; + map.serialize_entry("additionalProperties", &self.additional_properties)?; + + if !self.properties.is_empty() { + map.serialize_entry("properties", &Helper(self))?; + } + + map.end() + } +} + +#[derive(Serialize)] +struct PropertyEntrySerializer { + #[serde(flatten)] + schema: &'static Schema, + #[serde(skip_serializing_if = "is_false")] + optional: bool, + #[serde(skip_serializing_if = "is_false")] + default_key: bool, +} + +#[inline] +fn is_false(v: &bool) -> bool { + !*v +} + /// Combines multiple *object* schemas into one. /// /// Note that these are limited to object schemas. Other schemas will produce errors. @@ -714,6 +827,48 @@ impl AllOfSchema { } } +impl Serialize for AllOfSchema { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeMap; + + struct Helper<'a>(&'a AllOfSchema); + + impl Serialize for Helper<'_> { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut map = serializer.serialize_map(None)?; + for (name, optional, schema) in self.0.properties() { + map.serialize_entry( + name, + &PropertyEntrySerializer { + schema: *schema, + optional: *optional, + default_key: false, + }, + )?; + } + map.end() + } + } + + let mut map = serializer.serialize_map(None)?; + + map.serialize_entry("description", self.description)?; + map.serialize_entry("additionalProperties", &self.additional_properties())?; + + if self.properties().next().is_some() { + map.serialize_entry("properties", &Helper(self))?; + } + + map.end() + } +} + /// Beside [`ObjectSchema`] we also have an [`AllOfSchema`] which also represents objects. pub trait ObjectSchemaType { fn description(&self) -> &'static str; @@ -868,8 +1023,9 @@ impl Iterator for ObjectPropertyIterator { /// ], /// ).schema(); /// ``` -#[derive(Debug)] +#[derive(Debug, Serialize)] #[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))] +#[serde(tag = "type", rename_all = "lowercase")] pub enum Schema { Null, Boolean(BooleanSchema), @@ -878,6 +1034,9 @@ pub enum Schema { String(StringSchema), Object(ObjectSchema), Array(ArraySchema), + + // NOTE: In perl we do not currently support `type = AllOf` directly. + #[serde(rename = "object")] AllOf(AllOfSchema), } @@ -1137,6 +1296,90 @@ pub enum ApiStringFormat { VerifyFn(ApiStringVerifyFn), } +impl ApiStringFormat { + fn serialize_inner(&self, mut map: M) -> Result { + match self { + ApiStringFormat::Enum(entries) => { + struct Helper(&'static [EnumEntry]); + + impl Serialize for Helper { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeSeq; + + let mut array = serializer.serialize_seq(Some(self.0.len()))?; + for entry in self.0 { + array.serialize_element(entry.value)?; + } + array.end() + } + } + + map.serialize_entry("enum", &Helper(entries))?; + } + ApiStringFormat::Pattern(pattern) => { + map.serialize_entry("pattern", &pattern.regex_string)?; + } + ApiStringFormat::PropertyString(schema) => { + map.serialize_entry("format", schema)?; + } + ApiStringFormat::VerifyFn(func) => { + #[cfg(feature = "perlmod")] + if perlmod::ser::is_active() { + map.serialize_entry( + "format", + &serialize_verify_fn::make_format_validator(*func), + )?; + } + #[cfg(not(features = "perlmod"))] + let _ = func; + } + } + + Ok(map) + } +} + +impl Serialize for ApiStringFormat { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeMap; + self.serialize_inner(serializer.serialize_map(Some(1))?)? + .end() + } +} + +#[cfg(feature = "perlmod")] +mod serialize_verify_fn { + use super::ApiStringVerifyFn; + + static FN_TAG: perlmod::MagicTag<()> = perlmod::MagicTag::new(); + + #[perlmod::export(xs_name = "call_validator_xsub")] + fn call_validator(#[cv] cv: perlmod::Value, s: &str) -> Result<(), anyhow::Error> { + let mg = cv + .find_raw_magic(None, Some(FN_TAG.as_ref())) + .ok_or_else(|| { + anyhow::format_err!("validator function without appropriate magic tag") + })?; + let mg: ApiStringVerifyFn = unsafe { ::core::mem::transmute(mg.ptr()) }; + mg(s) + } + + pub(super) fn make_format_validator(func: ApiStringVerifyFn) -> perlmod::RawValue { + unsafe { + let cv = perlmod::Value::new_xsub(call_validator_xsub); + cv.add_raw_magic(None, None, Some(FN_TAG.as_ref()), func as *const _, 0); + perlmod::Value::new_ref(&cv) + } + .into() + } +} + /// Type of a verification function for [`StringSchema`]s. pub type ApiStringVerifyFn = fn(&str) -> Result<(), Error>; diff --git a/proxmox-schema/tests/schema_verification.rs b/proxmox-schema/tests/schema_verification.rs index 09d7d87..506ef0f 100644 --- a/proxmox-schema/tests/schema_verification.rs +++ b/proxmox-schema/tests/schema_verification.rs @@ -193,3 +193,33 @@ fn verify_nested_property3() -> Result<(), Error> { Ok(()) } + +#[test] +fn verify_perl_json_schema() { + let schema = + serde_json::to_string(&SIMPLE_OBJECT_SCHEMA).expect("failed to serialize object schema"); + assert_eq!( + schema, + "\ + {\ + \"type\":\"object\",\ + \"description\":\"simple object schema\",\ + \"additionalProperties\":false,\ + \"properties\":{\ + \"prop1\":{\ + \"type\":\"string\",\ + \"description\":\"A test string\"\ + },\ + \"prop2\":{\ + \"type\":\"string\",\ + \"description\":\"A test string\",\ + \"optional\":true\ + },\ + \"prop3\":{\ + \"type\":\"string\",\ + \"description\":\"A test string\"\ + }\ + }\ + }" + ); +} -- 2.30.2