Skip to content

Commit

Permalink
Add support for patternProperties in particular circumstances (#637)
Browse files Browse the repository at this point in the history
  • Loading branch information
novafacing authored Aug 6, 2024
1 parent 208704a commit fe57b80
Show file tree
Hide file tree
Showing 4 changed files with 383 additions and 16 deletions.
81 changes: 81 additions & 0 deletions typify-impl/src/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1132,6 +1132,49 @@ impl TypeSpace {
Ok((TypeEntryDetails::Unit.into(), metadata))
}

/// Determine whether a schema's property name validation constraints can be handled
fn can_handle_pattern_properties(validation: &ObjectValidation) -> bool {
if !validation.required.is_empty() {
return false;
}

if !validation.properties.is_empty() {
return false;
}

// Ensure we have at least one pattern property and all pattern property
// schemas are the same
let Some(first_schema) = validation.pattern_properties.values().next() else {
return false;
};

if !validation
.pattern_properties
.values()
.all(|schema| schema == first_schema)
{
return false;
}

// Ensure any additional properties are a false or null schema
if validation.additional_properties.as_ref().map(AsRef::as_ref) == Some(&Schema::Bool(true))
|| matches!(
validation.additional_properties.as_ref().map(AsRef::as_ref),
Some(&Schema::Object(_))
)
{
return false;
}

// Ensure there are no additional property names constraints, to avoid a
// collision between different types of constraints interacting unexpectedly
if validation.property_names.is_some() {
return false;
}

true
}

fn convert_object<'a>(
&mut self,
type_name: Name,
Expand Down Expand Up @@ -1163,6 +1206,44 @@ impl TypeSpace {
)?;
Ok((type_entry, metadata))
}

Some(validation) if Self::can_handle_pattern_properties(validation) => {
let pattern = validation
.pattern_properties
.keys()
.cloned()
.collect::<Vec<_>>()
.join("|");

// Construct a schema to use for property name validation
let property_names = Some(Box::new(Schema::Object(SchemaObject {
string: Some(Box::new(StringValidation {
max_length: None,
min_length: None,
pattern: Some(pattern),
})),
..Default::default()
})));

// Construct schema to use for property value validation
let additional_properties = Some(Box::new(
validation
.pattern_properties
.values()
.next()
.cloned()
.unwrap_or_else(|| unreachable!("pattern_properties cannot be empty here")),
));

let type_entry = self.make_map(
type_name.into_option(),
&property_names,
&additional_properties,
)?;

Ok((type_entry, metadata))
}

None => {
let type_entry = self.make_map(type_name.into_option(), &None, &None)?;
Ok((type_entry, metadata))
Expand Down
163 changes: 147 additions & 16 deletions typify-impl/tests/vega.out
Original file line number Diff line number Diff line change
Expand Up @@ -29966,13 +29966,28 @@ impl ToString for Element {
#[doc = r" ```"]
#[doc = r" </details>"]
#[derive(Clone, Debug, serde :: Deserialize, serde :: Serialize)]
#[serde(deny_unknown_fields)]
pub struct Encode {}
pub struct Encode(pub std::collections::HashMap<EncodeKey, EncodeEntry>);
impl std::ops::Deref for Encode {
type Target = std::collections::HashMap<EncodeKey, EncodeEntry>;
fn deref(&self) -> &std::collections::HashMap<EncodeKey, EncodeEntry> {
&self.0
}
}
impl From<Encode> for std::collections::HashMap<EncodeKey, EncodeEntry> {
fn from(value: Encode) -> Self {
value.0
}
}
impl From<&Encode> for Encode {
fn from(value: &Encode) -> Self {
value.clone()
}
}
impl From<std::collections::HashMap<EncodeKey, EncodeEntry>> for Encode {
fn from(value: std::collections::HashMap<EncodeKey, EncodeEntry>) -> Self {
Self(value)
}
}
#[doc = "EncodeEntry"]
#[doc = r""]
#[doc = r" <details><summary>JSON schema</summary>"]
Expand Down Expand Up @@ -30419,6 +30434,74 @@ impl From<&EncodeEntry> for EncodeEntry {
value.clone()
}
}
#[doc = "EncodeKey"]
#[doc = r""]
#[doc = r" <details><summary>JSON schema</summary>"]
#[doc = r""]
#[doc = r" ```json"]
#[doc = "{"]
#[doc = " \"type\": \"string\","]
#[doc = " \"pattern\": \"^.+$\""]
#[doc = "}"]
#[doc = r" ```"]
#[doc = r" </details>"]
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, serde :: Serialize)]
pub struct EncodeKey(String);
impl std::ops::Deref for EncodeKey {
type Target = String;
fn deref(&self) -> &String {
&self.0
}
}
impl From<EncodeKey> for String {
fn from(value: EncodeKey) -> Self {
value.0
}
}
impl From<&EncodeKey> for EncodeKey {
fn from(value: &EncodeKey) -> Self {
value.clone()
}
}
impl std::str::FromStr for EncodeKey {
type Err = self::error::ConversionError;
fn from_str(value: &str) -> Result<Self, self::error::ConversionError> {
if regress::Regex::new("^.+$").unwrap().find(value).is_none() {
return Err("doesn't match pattern \"^.+$\"".into());
}
Ok(Self(value.to_string()))
}
}
impl std::convert::TryFrom<&str> for EncodeKey {
type Error = self::error::ConversionError;
fn try_from(value: &str) -> Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl std::convert::TryFrom<&String> for EncodeKey {
type Error = self::error::ConversionError;
fn try_from(value: &String) -> Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl std::convert::TryFrom<String> for EncodeKey {
type Error = self::error::ConversionError;
fn try_from(value: String) -> Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl<'de> serde::Deserialize<'de> for EncodeKey {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
String::deserialize(deserializer)?
.parse()
.map_err(|e: self::error::ConversionError| {
<D::Error as serde::de::Error>::custom(e.to_string())
})
}
}
#[doc = "Everything"]
#[doc = r""]
#[doc = r" <details><summary>JSON schema</summary>"]
Expand Down Expand Up @@ -105101,7 +105184,7 @@ impl From<NumberValue> for TitleVariant1Dy {
#[derive(Clone, Debug, serde :: Deserialize, serde :: Serialize)]
pub struct TitleVariant1Encode {
#[serde(flatten, default, skip_serializing_if = "Option::is_none")]
pub subtype_0: Option<TitleVariant1EncodeSubtype0>,
pub subtype_0: Option<std::collections::HashMap<TitleVariant1EncodeSubtype0Key, EncodeEntry>>,
#[serde(flatten, default, skip_serializing_if = "Option::is_none")]
pub subtype_1: Option<TitleVariant1EncodeSubtype1>,
}
Expand All @@ -105110,30 +105193,78 @@ impl From<&TitleVariant1Encode> for TitleVariant1Encode {
value.clone()
}
}
#[doc = "TitleVariant1EncodeSubtype0"]
#[doc = "TitleVariant1EncodeSubtype0Key"]
#[doc = r""]
#[doc = r" <details><summary>JSON schema</summary>"]
#[doc = r""]
#[doc = r" ```json"]
#[doc = "{"]
#[doc = " \"type\": \"object\","]
#[doc = " \"patternProperties\": {"]
#[doc = " \"^(?!interactive|name|style).+$\": {"]
#[doc = " \"$ref\": \"#/definitions/encodeEntry\""]
#[doc = " }"]
#[doc = " },"]
#[doc = " \"additionalProperties\": false"]
#[doc = " \"type\": \"string\","]
#[doc = " \"pattern\": \"^(?!interactive|name|style).+$\""]
#[doc = "}"]
#[doc = r" ```"]
#[doc = r" </details>"]
#[derive(Clone, Debug, serde :: Deserialize, serde :: Serialize)]
#[serde(deny_unknown_fields)]
pub struct TitleVariant1EncodeSubtype0 {}
impl From<&TitleVariant1EncodeSubtype0> for TitleVariant1EncodeSubtype0 {
fn from(value: &TitleVariant1EncodeSubtype0) -> Self {
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, serde :: Serialize)]
pub struct TitleVariant1EncodeSubtype0Key(String);
impl std::ops::Deref for TitleVariant1EncodeSubtype0Key {
type Target = String;
fn deref(&self) -> &String {
&self.0
}
}
impl From<TitleVariant1EncodeSubtype0Key> for String {
fn from(value: TitleVariant1EncodeSubtype0Key) -> Self {
value.0
}
}
impl From<&TitleVariant1EncodeSubtype0Key> for TitleVariant1EncodeSubtype0Key {
fn from(value: &TitleVariant1EncodeSubtype0Key) -> Self {
value.clone()
}
}
impl std::str::FromStr for TitleVariant1EncodeSubtype0Key {
type Err = self::error::ConversionError;
fn from_str(value: &str) -> Result<Self, self::error::ConversionError> {
if regress::Regex::new("^(?!interactive|name|style).+$")
.unwrap()
.find(value)
.is_none()
{
return Err("doesn't match pattern \"^(?!interactive|name|style).+$\"".into());
}
Ok(Self(value.to_string()))
}
}
impl std::convert::TryFrom<&str> for TitleVariant1EncodeSubtype0Key {
type Error = self::error::ConversionError;
fn try_from(value: &str) -> Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl std::convert::TryFrom<&String> for TitleVariant1EncodeSubtype0Key {
type Error = self::error::ConversionError;
fn try_from(value: &String) -> Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl std::convert::TryFrom<String> for TitleVariant1EncodeSubtype0Key {
type Error = self::error::ConversionError;
fn try_from(value: String) -> Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl<'de> serde::Deserialize<'de> for TitleVariant1EncodeSubtype0Key {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
String::deserialize(deserializer)?
.parse()
.map_err(|e: self::error::ConversionError| {
<D::Error as serde::de::Error>::custom(e.to_string())
})
}
}
#[doc = "TitleVariant1EncodeSubtype1"]
#[doc = r""]
#[doc = r" <details><summary>JSON schema</summary>"]
Expand Down
20 changes: 20 additions & 0 deletions typify/tests/schemas/property-pattern.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "test grammar for pattern properties",
"type": "object",
"required": [
"rules"
],
"additionalProperties": false,
"properties": {
"rules": {
"type": "object",
"patternProperties": {
"^[a-zA-Z_]\\w*$": {
"type": "string"
}
},
"additionalProperties": false
}
}
}
Loading

0 comments on commit fe57b80

Please sign in to comment.