diff --git a/typify-impl/src/merge.rs b/typify-impl/src/merge.rs index 48450f0b..029fc961 100644 --- a/typify-impl/src/merge.rs +++ b/typify-impl/src/merge.rs @@ -415,13 +415,35 @@ fn try_merge_with_each_subschema( joined_schemas } +fn merge_schema_not( + schema: &Schema, + not_schema: &Schema, + defs: &BTreeMap, +) -> Schema { + match (schema, not_schema) { + (_, Schema::Bool(true)) | (Schema::Bool(false), _) => Schema::Bool(false), + + (any, Schema::Bool(false)) => any.clone(), + + // TODO I don't know how to subtract something from nothing... + (Schema::Bool(true), Schema::Object(_)) => todo!(), + + (Schema::Object(schema_object), any_not) => { + match try_merge_schema_not(schema_object.clone(), any_not, defs) { + Ok(schema_obj) => Schema::Object(schema_obj), + Err(_) => Schema::Bool(false), + } + } + } +} + /// "Subtract" the "not" schema from the schema object. /// /// TODO Exactly where and how we handle not constructions is... tricky! As we /// find and support more and more useful uses of not we will likely move some /// of this into the conversion methods. fn try_merge_schema_not( - mut schema_object: SchemaObject, + schema_object: SchemaObject, not_schema: &Schema, defs: &BTreeMap, ) -> Result { @@ -435,77 +457,8 @@ fn try_merge_schema_not( Schema::Bool(true) => Err(()), // ... whereas subtracting nothing leaves everything. Schema::Bool(false) => Ok(schema_object), - - Schema::Object(SchemaObject { - // I don't think there's any significance to the schema metadata - // with respect to the types we might generate. - metadata: _, - // TODO we should should check instance_type and then walk through - // validation of each type based on the specific validation. - instance_type: _, - format: _, - enum_values: _, - const_value: _, - subschemas, - number: _, - string: _, - array: _, - object, - // TODO we might want to chase these references but need to take - // care to handle circular references. - reference: _, - extensions: _, - }) => { - if let Some(not_object) = object { - // TODO this is incomplete, but seems sufficient for the - // schemas we've seen in the wild. - if let Some(ObjectValidation { - required, - properties, - .. - }) = schema_object.object.as_deref_mut() - { - // TODO This is completely wrong for arrays of len > 1. - // We need to treat required: [x, y] like it's: - // not: - // allOf: - // required: [x] - // required: [y] - // Then we can transform them into: - // anyOf: - // not: - // required: [x] - // not: - // required: [y] - // Which in turn can become: - // oneOf: - // not: - // required: [x] - // not: - // required: [y] - // not: - // required: [x, y] - for not_required in ¬_object.required { - // A property can't be both required and not required - // therefore this schema is unsatisfiable. - if required.contains(not_required) { - return Err(()); - } - // Set the property's schema to false i.e. that the - // presence of any value would be invalid. We ignore - // the return value as it doesn't matter if the - // property was there previously or not. - let _ = properties.insert(not_required.clone(), Schema::Bool(false)); - } - } - } - - if let Some(not_subschemas) = subschemas { - schema_object = try_merge_with_subschemas_not(schema_object, not_subschemas, defs)?; - } - - Ok(schema_object) - } + // Do the real work. + Schema::Object(not_object) => try_merge_schema_object_not(schema_object, not_object, defs), } } @@ -604,6 +557,84 @@ fn try_merge_with_subschemas_not( } } +fn try_merge_schema_object_not( + mut schema_object: SchemaObject, + not_object: &SchemaObject, + defs: &BTreeMap, +) -> Result { + // Examine enum values + match (&mut schema_object.enum_values, ¬_object.enum_values) { + // Nothing to do. + (_, None) => {} + // TODO not sure quite what to do, so we'll ignore for now. + (None, Some(_)) => {} + (Some(values), Some(not_values)) => { + values.retain(|value| !not_values.contains(value)); + if values.is_empty() { + return Err(()); + } + } + } + + match (&mut schema_object.object, ¬_object.object) { + // Nothing to do. + (_, None) => {} + + // TODO Not sure how to enforce the inverse here... + (None, Some(_)) => {} + + // In the interesting case, we need to "subtract" object attributes. + (Some(obj), Some(not_obj)) => { + for (prop_name, prop_schema) in &mut obj.properties { + if let Some(not_prop_schema) = not_obj.properties.get(prop_name) { + // For properties in both, we merge those schemas. Note + // that if such a merging is unsatisfiable *and* the + // property is required, we'll take the appropriate action + // later. + *prop_schema = merge_schema_not(prop_schema, not_prop_schema, defs); + } + } + + for prop_name in not_obj.properties.keys() { + if !obj.properties.contains_key(prop_name) { + // There's a property in the "not" that isn't in the + // object. Most precisely we would say "this property may + // have any value as long as it doesn't match this schema". + // That's a little tricky right now, so instead we'll say + // "you may not have a property with this name". + let _ = obj + .properties + .insert(prop_name.clone(), Schema::Bool(false)); + } + } + + for not_required in ¬_obj.required { + if !not_obj.properties.contains_key(not_required) { + // No value is permissible + let _ = obj + .properties + .insert(not_required.clone(), Schema::Bool(false)); + } + } + + // If any of the previous steps resulted in a required property + // being invalid, we note that here and identify the full schema as + // invalid. + for required in &obj.required { + if let Some(Schema::Bool(false)) = obj.properties.get(required) { + return Err(()); + } + } + } + } + + if let Some(not_subschemas) = ¬_object.subschemas { + schema_object = try_merge_with_subschemas_not(schema_object, not_subschemas, defs)?; + } + + Ok(schema_object) +} + /// Merge instance types which could be None (meaning type is valid), a /// singleton type, or an array of types. An error result indicates that the /// types were non-overlappin and therefore incompatible. diff --git a/typify/tests/schemas/merged-schemas.json b/typify/tests/schemas/merged-schemas.json index 22724ac7..16b42cde 100644 --- a/typify/tests/schemas/merged-schemas.json +++ b/typify/tests/schemas/merged-schemas.json @@ -490,6 +490,38 @@ "additionalProperties": false } ] + }, + "unchanged-by-merge": { + "allOf": [ + { + "type": "object", + "properties": { + "tag": { + "enum": [ + "something" + ] + } + }, + "required": [ + "tag" + ] + }, + { + "not": { + "type": "object", + "properties": { + "tag": { + "enum": [ + "something_else" + ] + } + }, + "required": [ + "tag" + ] + } + } + ] } } } diff --git a/typify/tests/schemas/merged-schemas.rs b/typify/tests/schemas/merged-schemas.rs index c515ccfb..327f29de 100644 --- a/typify/tests/schemas/merged-schemas.rs +++ b/typify/tests/schemas/merged-schemas.rs @@ -863,6 +863,130 @@ impl TrimFat { Default::default() } } +#[doc = "`UnchangedByMerge`"] +#[doc = r""] +#[doc = r"
JSON schema"] +#[doc = r""] +#[doc = r" ```json"] +#[doc = "{"] +#[doc = " \"allOf\": ["] +#[doc = " {"] +#[doc = " \"type\": \"object\","] +#[doc = " \"required\": ["] +#[doc = " \"tag\""] +#[doc = " ],"] +#[doc = " \"properties\": {"] +#[doc = " \"tag\": {"] +#[doc = " \"enum\": ["] +#[doc = " \"something\""] +#[doc = " ]"] +#[doc = " }"] +#[doc = " }"] +#[doc = " },"] +#[doc = " {"] +#[doc = " \"not\": {"] +#[doc = " \"type\": \"object\","] +#[doc = " \"required\": ["] +#[doc = " \"tag\""] +#[doc = " ],"] +#[doc = " \"properties\": {"] +#[doc = " \"tag\": {"] +#[doc = " \"enum\": ["] +#[doc = " \"something_else\""] +#[doc = " ]"] +#[doc = " }"] +#[doc = " }"] +#[doc = " }"] +#[doc = " }"] +#[doc = " ]"] +#[doc = "}"] +#[doc = r" ```"] +#[doc = r"
"] +#[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)] +pub struct UnchangedByMerge { + pub tag: UnchangedByMergeTag, +} +impl ::std::convert::From<&UnchangedByMerge> for UnchangedByMerge { + fn from(value: &UnchangedByMerge) -> Self { + value.clone() + } +} +impl UnchangedByMerge { + pub fn builder() -> builder::UnchangedByMerge { + Default::default() + } +} +#[doc = "`UnchangedByMergeTag`"] +#[doc = r""] +#[doc = r"
JSON schema"] +#[doc = r""] +#[doc = r" ```json"] +#[doc = "{"] +#[doc = " \"enum\": ["] +#[doc = " \"something\""] +#[doc = " ]"] +#[doc = "}"] +#[doc = r" ```"] +#[doc = r"
"] +#[derive( + :: serde :: Deserialize, + :: serde :: Serialize, + Clone, + Copy, + Debug, + Eq, + Hash, + Ord, + PartialEq, + PartialOrd, +)] +pub enum UnchangedByMergeTag { + #[serde(rename = "something")] + Something, +} +impl ::std::convert::From<&Self> for UnchangedByMergeTag { + fn from(value: &UnchangedByMergeTag) -> Self { + value.clone() + } +} +impl ::std::fmt::Display for UnchangedByMergeTag { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + match *self { + Self::Something => write!(f, "something"), + } + } +} +impl ::std::str::FromStr for UnchangedByMergeTag { + type Err = self::error::ConversionError; + fn from_str(value: &str) -> ::std::result::Result { + match value { + "something" => Ok(Self::Something), + _ => Err("invalid value".into()), + } + } +} +impl ::std::convert::TryFrom<&str> for UnchangedByMergeTag { + type Error = self::error::ConversionError; + fn try_from(value: &str) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<&::std::string::String> for UnchangedByMergeTag { + type Error = self::error::ConversionError; + fn try_from( + value: &::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<::std::string::String> for UnchangedByMergeTag { + type Error = self::error::ConversionError; + fn try_from( + value: ::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} #[doc = "`Unresolvable`"] #[doc = r""] #[doc = r"
JSON schema"] @@ -1973,6 +2097,42 @@ pub mod builder { } } #[derive(Clone, Debug)] + pub struct UnchangedByMerge { + tag: ::std::result::Result, + } + impl ::std::default::Default for UnchangedByMerge { + fn default() -> Self { + Self { + tag: Err("no value supplied for tag".to_string()), + } + } + } + impl UnchangedByMerge { + pub fn tag(mut self, value: T) -> Self + where + T: ::std::convert::TryInto, + T::Error: ::std::fmt::Display, + { + self.tag = value + .try_into() + .map_err(|e| format!("error converting supplied value for tag: {}", e)); + self + } + } + impl ::std::convert::TryFrom for super::UnchangedByMerge { + type Error = super::error::ConversionError; + fn try_from( + value: UnchangedByMerge, + ) -> ::std::result::Result { + Ok(Self { tag: value.tag? }) + } + } + impl ::std::convert::From for UnchangedByMerge { + fn from(value: super::UnchangedByMerge) -> Self { + Self { tag: Ok(value.tag) } + } + } + #[derive(Clone, Debug)] pub struct Unsatisfiable3A { action: ::std::result::Result< ::std::option::Option,