From fc7cf276462bc0106846e42456d43bbb9b508dbb Mon Sep 17 00:00:00 2001 From: Phil Date: Tue, 7 Jan 2025 10:31:42 -0500 Subject: [PATCH 1/2] derive-typescript: fix codegen for empty enums We saw a derivation that failed validation because the generated Typescript code was syntactically invalid. This was caused by the inferred `Shape` having an empty set of enum variants (`enum_: Some([])`). The empty enum variants cause the generated code to omit any sort of type, which causes Deno to fail to parse it. I'm not completely certain whether the fault is with the Typescript code generation or the Shape inferrence, but it's pretty easy to filter out empty enums in the code generation, which results in generated code correctly having the `never` type. I also added a debug log of the generated types file, so that this type of thing is easier to debug in the future. --- crates/derive-typescript/src/codegen/mapper.rs | 2 +- crates/derive-typescript/src/lib.rs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/derive-typescript/src/codegen/mapper.rs b/crates/derive-typescript/src/codegen/mapper.rs index 29e9ce6523..aea65f6515 100644 --- a/crates/derive-typescript/src/codegen/mapper.rs +++ b/crates/derive-typescript/src/codegen/mapper.rs @@ -115,7 +115,7 @@ impl Mapper { return AST::Unknown; } // Is this an enum? Just emit the variants. - if let Some(enum_) = &shape.enum_ { + if let Some(enum_) = shape.enum_.as_ref().filter(|e| !e.is_empty()) { return AST::Union { variants: enum_ .iter() diff --git a/crates/derive-typescript/src/lib.rs b/crates/derive-typescript/src/lib.rs index 2eb719aeca..7aef478c2f 100644 --- a/crates/derive-typescript/src/lib.rs +++ b/crates/derive-typescript/src/lib.rs @@ -178,6 +178,7 @@ fn validate(validate: derive::request::Validate) -> anyhow::Result = vec![ (types_url.clone(), types_content.clone()), From 85c444f1bc59dd14798bebad7332db9e6d41e007 Mon Sep 17 00:00:00 2001 From: Phil Date: Tue, 7 Jan 2025 10:45:16 -0500 Subject: [PATCH 2/2] doc: add a test case documenting strange inference behavior --- crates/doc/src/shape/inference.rs | 78 +++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/crates/doc/src/shape/inference.rs b/crates/doc/src/shape/inference.rs index 0ab7a2e6d4..55db27a2a8 100644 --- a/crates/doc/src/shape/inference.rs +++ b/crates/doc/src/shape/inference.rs @@ -590,6 +590,84 @@ mod test { ); } + // This test documents the behavior in the corneer case where the + // intersection of two object schemas forbids an enum property. + #[test] + fn test_enum_property_empty_intersection() { + let shape = shape_from( + r#"{ + "type": "object", + "allOf": [ + { + "properties": { + "foo": { + "type": "integer", + "enum": [1, 2, 3] + } + } + }, + { + "properties": { + "bar": { "type": "string" } + }, + "additionalProperties": false + } + ] + }"#, + ); + + let foo_shape = shape + .object + .properties + .iter() + .find(|p| p.name.as_ref() == "foo") + .unwrap(); + + // The `type_` is empty because the property is not allowed to be present. + // Note that the `enum_` is still `Some`, though the set of values is empty. + insta::assert_debug_snapshot!(foo_shape, @r###" + ObjProperty { + name: "foo", + is_required: false, + shape: Shape { + type_: , + enum_: Some( + [], + ), + title: None, + description: None, + reduction: Unset, + provenance: Inline, + default: None, + secret: None, + annotations: {}, + array: ArrayShape { + additional_items: None, + max_items: None, + min_items: 0, + tuple: [], + }, + numeric: NumericShape { + minimum: None, + maximum: None, + }, + object: ObjShape { + additional_properties: None, + pattern_properties: [], + properties: [], + }, + string: StringShape { + content_encoding: None, + content_type: None, + format: None, + max_length: None, + min_length: 0, + }, + }, + } + "###); + } + #[test] fn test_enum_type_extraction() { assert_eq!(