From 62573a4982689981823a967c7ca1a5629c3554a3 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Mon, 12 Jan 2026 16:06:52 -0400 Subject: [PATCH] [WIP] Add many more e2e TypeScript usage checks Signed-off-by: Juan Cruz Viotti --- src/generator/typescript.cc | 34 +- src/ir/include/sourcemeta/codegen/ir.h | 2 +- src/ir/ir_default_compiler.h | 8 +- .../additional_properties_true/expected.d.ts | 6 +- .../2020-12/all_type_values/test.ts | 293 ++++++++++++++++ .../typescript/2020-12/anchor_refs/test.ts | 106 ++++++ .../2020-12/complex_nested_object/test.ts | 330 ++++++++++++++++++ .../typescript/2020-12/const_keyword/test.ts | 173 +++++++++ .../2020-12/deeply_nested_refs/expected.d.ts | 3 + .../2020-12/deeply_nested_refs/test.ts | 225 ++++++++++++ .../typescript/2020-12/defs_and_refs/test.ts | 222 ++++++++++++ .../2020-12/embedded_resources/test.ts | 46 +++ .../2020-12/enum_complex_types/test.ts | 137 ++++++++ .../implicit_types_and_reused_refs/test.ts | 255 ++++++++++++++ .../expected.d.ts | 3 + .../expected.d.ts | 1 + .../test.ts | 25 ++ .../2020-12/tuples_and_arrays/expected.d.ts | 24 +- .../2020-12/tuples_and_arrays/test.ts | 158 +++++++++ .../2020-12/vocabulary_ignored/test.ts | 65 ++++ test/e2e/typescript/CMakeLists.txt | 12 +- test/generator/generator_typescript_test.cc | 12 + test/ir/ir_2020_12_test.cc | 68 ++-- 23 files changed, 2150 insertions(+), 58 deletions(-) create mode 100644 test/e2e/typescript/2020-12/all_type_values/test.ts create mode 100644 test/e2e/typescript/2020-12/anchor_refs/test.ts create mode 100644 test/e2e/typescript/2020-12/complex_nested_object/test.ts create mode 100644 test/e2e/typescript/2020-12/const_keyword/test.ts create mode 100644 test/e2e/typescript/2020-12/deeply_nested_refs/test.ts create mode 100644 test/e2e/typescript/2020-12/defs_and_refs/test.ts create mode 100644 test/e2e/typescript/2020-12/embedded_resources/test.ts create mode 100644 test/e2e/typescript/2020-12/enum_complex_types/test.ts create mode 100644 test/e2e/typescript/2020-12/implicit_types_and_reused_refs/test.ts create mode 100644 test/e2e/typescript/2020-12/object_with_optional_string_property/test.ts create mode 100644 test/e2e/typescript/2020-12/tuples_and_arrays/test.ts create mode 100644 test/e2e/typescript/2020-12/vocabulary_ignored/test.ts diff --git a/src/generator/typescript.cc b/src/generator/typescript.cc index 8d4a492..7d4f79f 100644 --- a/src/generator/typescript.cc +++ b/src/generator/typescript.cc @@ -81,16 +81,27 @@ auto TypeScript::operator()(const IREnumeration &entry) const -> void { auto TypeScript::operator()(const IRObject &entry) const -> void { const auto type_name{sourcemeta::core::mangle(entry.pointer, this->prefix)}; - const auto has_additional{entry.additional.has_value()}; + const auto has_typed_additional{ + std::holds_alternative(entry.additional)}; + const auto allows_any_additional{ + std::holds_alternative(entry.additional) && + std::get(entry.additional)}; - if (has_additional && entry.members.empty()) { + if (has_typed_additional && entry.members.empty()) { this->output << "export type " << type_name << " = Recordpointer, - this->prefix) + << sourcemeta::core::mangle( + std::get(entry.additional).pointer, + this->prefix) << ">;\n"; return; } + if (allows_any_additional && entry.members.empty()) { + this->output << "export type " << type_name + << " = Record;\n"; + return; + } + this->output << "export interface " << type_name << " {\n"; // We always quote property names for safety. JSON Schema allows any string @@ -110,20 +121,29 @@ auto TypeScript::operator()(const IRObject &entry) const -> void { << ";\n"; } - if (has_additional) { + if (allows_any_additional) { + this->output << " [key: string]: unknown | undefined;\n"; + } else if (has_typed_additional) { // TypeScript index signatures must be a supertype of all property value // types. We use a union of all member types plus the additional properties // type plus undefined (for optional properties). this->output << " [key: string]:\n"; + this->output << " // As a notable limitation, TypeScript requires index " + "signatures\n"; + this->output << " // to also include the types of all of its " + "properties, so we must\n"; + this->output << " // match a superset of what JSON Schema allows\n"; for (const auto &[member_name, member_value] : entry.members) { this->output << " " << sourcemeta::core::mangle(member_value.pointer, this->prefix) << " |\n"; } + this->output << " " - << sourcemeta::core::mangle(entry.additional->pointer, - this->prefix) + << sourcemeta::core::mangle( + std::get(entry.additional).pointer, + this->prefix) << " |\n"; this->output << " undefined;\n"; } diff --git a/src/ir/include/sourcemeta/codegen/ir.h b/src/ir/include/sourcemeta/codegen/ir.h index 4af466b..c137a31 100644 --- a/src/ir/include/sourcemeta/codegen/ir.h +++ b/src/ir/include/sourcemeta/codegen/ir.h @@ -64,7 +64,7 @@ struct IRObjectValue : IRType { struct IRObject : IRType { // To preserve the user's ordering std::vector> members; - std::optional additional; + std::variant additional; }; /// @ingroup ir diff --git a/src/ir/ir_default_compiler.h b/src/ir/ir_default_compiler.h index 11f3c54..b69d97b 100644 --- a/src/ir/ir_default_compiler.h +++ b/src/ir/ir_default_compiler.h @@ -96,12 +96,12 @@ auto handle_object(const sourcemeta::core::JSON &schema, members.emplace_back(entry.first, std::move(member_value)); } - std::optional additional{std::nullopt}; + std::variant additional{true}; if (subschema.defines("additionalProperties")) { const auto &additional_schema{subschema.at("additionalProperties")}; - const auto is_false{additional_schema.is_boolean() && - !additional_schema.to_boolean()}; - if (!is_false) { + if (additional_schema.is_boolean()) { + additional = additional_schema.to_boolean(); + } else { auto additional_pointer{sourcemeta::core::to_pointer(location.pointer)}; additional_pointer.push_back("additionalProperties"); additional = IRType{.pointer = std::move(additional_pointer)}; diff --git a/test/e2e/typescript/2020-12/additional_properties_true/expected.d.ts b/test/e2e/typescript/2020-12/additional_properties_true/expected.d.ts index 8f311fa..d1ecf6e 100644 --- a/test/e2e/typescript/2020-12/additional_properties_true/expected.d.ts +++ b/test/e2e/typescript/2020-12/additional_properties_true/expected.d.ts @@ -8,8 +8,7 @@ export type FlexibleRecord_AdditionalProperties_AnyOf_ZIndex4 = string; export type FlexibleRecord_AdditionalProperties_AnyOf_ZIndex3 = unknown[]; -export interface FlexibleRecord_AdditionalProperties_AnyOf_ZIndex2 { -} +export type FlexibleRecord_AdditionalProperties_AnyOf_ZIndex2 = Record; export type FlexibleRecord_AdditionalProperties_AnyOf_ZIndex1 = boolean; @@ -27,6 +26,9 @@ export interface FlexibleRecord { "name": FlexibleRecord_Properties_Name; "count"?: FlexibleRecord_Properties_Count; [key: string]: + // As a notable limitation, TypeScript requires index signatures + // to also include the types of all of its properties, so we must + // match a superset of what JSON Schema allows FlexibleRecord_Properties_Name | FlexibleRecord_Properties_Count | FlexibleRecord_AdditionalProperties | diff --git a/test/e2e/typescript/2020-12/all_type_values/test.ts b/test/e2e/typescript/2020-12/all_type_values/test.ts new file mode 100644 index 0000000..48e1103 --- /dev/null +++ b/test/e2e/typescript/2020-12/all_type_values/test.ts @@ -0,0 +1,293 @@ +import { + AllTypes, + AllTypes_Properties_ObjectField, + AllTypes_Properties_NestedTypes, + AllTypes_Properties_MultiType, + AllTypes_Properties_ArrayField +} from "./expected"; + +const minimal: AllTypes = { + stringField: "hello", + numberField: 3.14, + integerField: 42, + booleanField: true, + nullField: null, + arrayField: [ "a", "b", "c" ], + objectField: {} +}; + +const complete: AllTypes = { + stringField: "hello", + numberField: -123.456, + integerField: 0, + booleanField: false, + nullField: null, + arrayField: [], + objectField: { nested: "value" }, + multiType: "string value", + nestedTypes: { + deepBoolean: true, + deepNull: null, + deepInteger: 100 + } +}; + +const multiTypeString: AllTypes = { + stringField: "test", + numberField: 1, + integerField: 1, + booleanField: true, + nullField: null, + arrayField: [], + objectField: {}, + multiType: "hello world" +}; + +const multiTypeNumber: AllTypes = { + stringField: "test", + numberField: 1, + integerField: 1, + booleanField: true, + nullField: null, + arrayField: [], + objectField: {}, + multiType: 42.5 +}; + +const multiTypeNull: AllTypes = { + stringField: "test", + numberField: 1, + integerField: 1, + booleanField: true, + nullField: null, + arrayField: [], + objectField: {}, + multiType: null +}; + +const invalidStringField: AllTypes = { + // @ts-expect-error + stringField: 123, + numberField: 1, + integerField: 1, + booleanField: true, + nullField: null, + arrayField: [], + objectField: {} +}; + +const invalidNumberField: AllTypes = { + stringField: "test", + // @ts-expect-error + numberField: "3.14", + integerField: 1, + booleanField: true, + nullField: null, + arrayField: [], + objectField: {} +}; + +const invalidIntegerField: AllTypes = { + stringField: "test", + numberField: 1, + // @ts-expect-error + integerField: "42", + booleanField: true, + nullField: null, + arrayField: [], + objectField: {} +}; + +const invalidBooleanField: AllTypes = { + stringField: "test", + numberField: 1, + integerField: 1, + // @ts-expect-error + booleanField: "true", + nullField: null, + arrayField: [], + objectField: {} +}; + +const invalidNullFieldString: AllTypes = { + stringField: "test", + numberField: 1, + integerField: 1, + booleanField: true, + // @ts-expect-error + nullField: "not null", + arrayField: [], + objectField: {} +}; + +const invalidNullFieldUndefined: AllTypes = { + stringField: "test", + numberField: 1, + integerField: 1, + booleanField: true, + // @ts-expect-error + nullField: undefined, + arrayField: [], + objectField: {} +}; + +const invalidArrayItems: AllTypes = { + stringField: "test", + numberField: 1, + integerField: 1, + booleanField: true, + nullField: null, + // @ts-expect-error + arrayField: [ 1, 2, 3 ], + objectField: {} +}; + +// @ts-expect-error +const missingRequired: AllTypes = { + stringField: "test", + numberField: 1, + integerField: 1, + booleanField: true, + nullField: null, + arrayField: [] +}; + +const invalidMultiType: AllTypes = { + stringField: "test", + numberField: 1, + integerField: 1, + booleanField: true, + nullField: null, + arrayField: [], + objectField: {}, + // @ts-expect-error + multiType: true +}; + +const invalidMultiTypeArray: AllTypes = { + stringField: "test", + numberField: 1, + integerField: 1, + booleanField: true, + nullField: null, + arrayField: [], + objectField: {}, + // @ts-expect-error + multiType: [ 1, 2, 3 ] +}; + +const invalidObjectFieldExtra: AllTypes = { + stringField: "test", + numberField: 1, + integerField: 1, + booleanField: true, + nullField: null, + arrayField: [], + objectField: { + nested: "ok", + // @ts-expect-error + extra: "not allowed" + } +}; + +const invalidObjectFieldType: AllTypes = { + stringField: "test", + numberField: 1, + integerField: 1, + booleanField: true, + nullField: null, + arrayField: [], + objectField: { + // @ts-expect-error + nested: 123 + } +}; + +const invalidNestedTypesExtra: AllTypes = { + stringField: "test", + numberField: 1, + integerField: 1, + booleanField: true, + nullField: null, + arrayField: [], + objectField: {}, + nestedTypes: { + deepBoolean: true, + // @ts-expect-error + extra: "not allowed" + } +}; + +const invalidNestedTypesBoolean: AllTypes = { + stringField: "test", + numberField: 1, + integerField: 1, + booleanField: true, + nullField: null, + arrayField: [], + objectField: {}, + nestedTypes: { + // @ts-expect-error + deepBoolean: "yes" + } +}; + +const invalidNestedTypesNull: AllTypes = { + stringField: "test", + numberField: 1, + integerField: 1, + booleanField: true, + nullField: null, + arrayField: [], + objectField: {}, + nestedTypes: { + // @ts-expect-error + deepNull: "null" + } +}; + +const invalidNestedTypesInteger: AllTypes = { + stringField: "test", + numberField: 1, + integerField: 1, + booleanField: true, + nullField: null, + arrayField: [], + objectField: {}, + nestedTypes: { + // @ts-expect-error + deepInteger: "100" + } +}; + +const invalidRootExtra: AllTypes = { + stringField: "test", + numberField: 1, + integerField: 1, + booleanField: true, + nullField: null, + arrayField: [], + objectField: {}, + // @ts-expect-error + unknownField: "not allowed" +}; + +const arrayField: AllTypes_Properties_ArrayField = [ "a", "b" ]; +// @ts-expect-error +const invalidArrayFieldType: AllTypes_Properties_ArrayField = [ 1, 2 ]; + +const multiType1: AllTypes_Properties_MultiType = "string"; +const multiType2: AllTypes_Properties_MultiType = 42; +const multiType3: AllTypes_Properties_MultiType = null; +// @ts-expect-error +const invalidMultiTypeStandalone: AllTypes_Properties_MultiType = true; + +const objectField: AllTypes_Properties_ObjectField = {}; +const objectField2: AllTypes_Properties_ObjectField = { nested: "value" }; +// @ts-expect-error +const invalidObjectFieldStandalone: AllTypes_Properties_ObjectField = { nested: 123 }; + +const nestedTypes: AllTypes_Properties_NestedTypes = {}; +const nestedTypes2: AllTypes_Properties_NestedTypes = { deepBoolean: false, deepNull: null, deepInteger: 50 }; +// @ts-expect-error +const invalidNestedTypesStandalone: AllTypes_Properties_NestedTypes = { deepBoolean: "yes" }; diff --git a/test/e2e/typescript/2020-12/anchor_refs/test.ts b/test/e2e/typescript/2020-12/anchor_refs/test.ts new file mode 100644 index 0000000..14a4c97 --- /dev/null +++ b/test/e2e/typescript/2020-12/anchor_refs/test.ts @@ -0,0 +1,106 @@ +import { + ColorScheme, + ColorScheme_X24Defs_UColor +} from "./expected"; + +// Valid: minimal required fields +const minimal: ColorScheme = { + primary: { r: 255, g: 0, b: 0 }, + secondary: { r: 0, g: 255, b: 0 } +}; + +// Valid: all fields including optional +const complete: ColorScheme = { + primary: { r: 255, g: 0, b: 0, alpha: 1.0 }, + secondary: { r: 0, g: 255, b: 0, alpha: 0.8 }, + background: { r: 255, g: 255, b: 255, alpha: 1.0 } +}; + +// Valid: color with optional alpha +const colorWithAlpha: ColorScheme_X24Defs_UColor = { + r: 128, + g: 128, + b: 128, + alpha: 0.5 +}; + +// Valid: color without alpha +const colorWithoutAlpha: ColorScheme_X24Defs_UColor = { + r: 0, + g: 0, + b: 0 +}; + +// Invalid: missing required r +// @ts-expect-error +const missingR: ColorScheme_X24Defs_UColor = { + g: 255, + b: 255 +}; + +// Invalid: missing required g +// @ts-expect-error +const missingG: ColorScheme_X24Defs_UColor = { + r: 255, + b: 255 +}; + +// Invalid: missing required b +// @ts-expect-error +const missingB: ColorScheme_X24Defs_UColor = { + r: 255, + g: 255 +}; + +// Invalid: r must be number +const invalidR: ColorScheme_X24Defs_UColor = { + // @ts-expect-error + r: "255", + g: 0, + b: 0 +}; + +// Invalid: alpha must be number +const invalidAlpha: ColorScheme_X24Defs_UColor = { + r: 255, + g: 0, + b: 0, + // @ts-expect-error + alpha: "1.0" +}; + +// Invalid: extra property on color (additionalProperties: false) +const invalidColorExtra: ColorScheme_X24Defs_UColor = { + r: 255, + g: 0, + b: 0, + // @ts-expect-error + name: "red" +}; + +// Invalid: missing required primary +// @ts-expect-error +const missingPrimary: ColorScheme = { + secondary: { r: 0, g: 255, b: 0 } +}; + +// Invalid: missing required secondary +// @ts-expect-error +const missingSecondary: ColorScheme = { + primary: { r: 255, g: 0, b: 0 } +}; + +// Invalid: extra property on root (additionalProperties: false) +const invalidRootExtra: ColorScheme = { + primary: { r: 255, g: 0, b: 0 }, + secondary: { r: 0, g: 255, b: 0 }, + // @ts-expect-error + accent: { r: 255, g: 255, b: 0 } +}; + +// Invalid: primary with wrong type +const invalidPrimaryType: ColorScheme = { + // @ts-expect-error + primary: "red", + secondary: { r: 0, g: 255, b: 0 } +}; diff --git a/test/e2e/typescript/2020-12/complex_nested_object/test.ts b/test/e2e/typescript/2020-12/complex_nested_object/test.ts new file mode 100644 index 0000000..587b710 --- /dev/null +++ b/test/e2e/typescript/2020-12/complex_nested_object/test.ts @@ -0,0 +1,330 @@ +import { + Record, + Record_X24Defs_UTimestamp, + Record_X24Defs_UFullName, + Record_X24Defs_ULocation, + Record_Properties_Items_Items, + Record_Properties_Entity +} from "./expected"; + + +// Valid: Timestamp with all required fields +const timestamp: Record_X24Defs_UTimestamp = { + rawValue: "2024-01-15", + year: 2024, + month: 1, + day: 15 +}; + +// Valid: Timestamp with optional isoFormat +const timestampWithIso: Record_X24Defs_UTimestamp = { + rawValue: "2024-01-15", + year: 2024, + month: 1, + day: 15, + isoFormat: "2024-01-15T00:00:00Z" +}; + +// Invalid: Timestamp missing required rawValue +// @ts-expect-error - rawValue is required +const timestampMissingRaw: Record_X24Defs_UTimestamp = { + year: 2024, + month: 1, + day: 15 +}; + +// Valid: FullName with required fields only +const fullNameMinimal: Record_X24Defs_UFullName = { + givenName: "John", + familyName: "Doe" +}; + +// Valid: FullName with all fields +const fullNameComplete: Record_X24Defs_UFullName = { + rawValue: "Dr. John Michael Doe Jr.", + givenName: "John", + middleName: "Michael", + familyName: "Doe", + suffix: "Jr.", + prefix: "Dr." +}; + +// Valid: FullName with null nullable fields (type: ["string", "null"]) +const fullNameNulls: Record_X24Defs_UFullName = { + givenName: "Jane", + familyName: "Smith", + middleName: null, + suffix: null, + prefix: null +}; + +// Invalid: FullName missing required givenName +// @ts-expect-error - givenName is required +const fullNameMissingGiven: Record_X24Defs_UFullName = { + familyName: "Doe" +}; + +// Valid: Location with all required fields +const location: Record_X24Defs_ULocation = { + rawValue: "123 Main St, City, Region 12345, Country", + line1: "123 Main St", + city: "City", + region: "Region", + postalCode: "12345", + country: "Country" +}; + +// Valid: Location with all fields including nullable line2 +const locationComplete: Record_X24Defs_ULocation = { + rawValue: "123 Main St, Apt 4, City, District, Region 12345, Country", + line1: "123 Main St", + line2: "Apt 4", + city: "City", + district: "District", + region: "Region", + postalCode: "12345", + country: "Country" +}; + +// Valid: Location with null line2 +const locationNullLine2: Record_X24Defs_ULocation = { + rawValue: "123 Main St, City, Region 12345, Country", + line1: "123 Main St", + line2: null, + city: "City", + region: "Region", + postalCode: "12345", + country: "Country" +}; + +// Valid: Entity with required fields +const entity: Record_Properties_Entity = { + fullName: { givenName: "John", familyName: "Doe" }, + birthDate: { rawValue: "1990-05-15", year: 1990, month: 5, day: 15 }, + locations: [ + { + rawValue: "123 Main St", + line1: "123 Main St", + city: "City", + region: "Region", + postalCode: "12345", + country: "US" + } + ] +}; + +// Valid: Entity with optional fields +const entityComplete: Record_Properties_Entity = { + fullName: { givenName: "John", familyName: "Doe" }, + birthDate: { rawValue: "1990-05-15", year: 1990, month: 5, day: 15 }, + category: "individual", + classification: "standard", + locations: [] +}; + +// Invalid: Entity missing required fullName +// @ts-expect-error - fullName is required +const entityMissingFullName: Record_Properties_Entity = { + birthDate: { rawValue: "1990-05-15", year: 1990, month: 5, day: 15 }, + locations: [] +}; + +// Valid: Item with required fields +const item: Record_Properties_Items_Items = { + itemId: "item-001", + sequenceNumber: "001", + description: "Test item", + occurredAt: { rawValue: "2024-01-15", year: 2024, month: 1, day: 15 }, + severity: "high" +}; + +// Valid: Item with nullable fields as strings +const itemWithStrings: Record_Properties_Items_Items = { + itemId: "item-002", + sequenceNumber: "002", + description: "Another item", + code: "ABC123", + occurredAt: { rawValue: "2024-01-15", year: 2024, month: 1, day: 15 }, + severity: "medium", + resolution: "Resolved", + remarks: "Some remarks" +}; + +// Valid: Item with nullable fields as null +const itemWithNulls: Record_Properties_Items_Items = { + itemId: "item-003", + sequenceNumber: "003", + description: "Item with nulls", + code: null, + occurredAt: { rawValue: "2024-01-15", year: 2024, month: 1, day: 15 }, + severity: "low", + resolution: null, + remarks: null +}; + +// Valid: Item with resolvedAt as Timestamp (anyOf [$ref, null]) +const itemResolved: Record_Properties_Items_Items = { + itemId: "item-004", + sequenceNumber: "004", + description: "Resolved item", + occurredAt: { rawValue: "2024-01-15", year: 2024, month: 1, day: 15 }, + severity: "high", + resolvedAt: { rawValue: "2024-01-20", year: 2024, month: 1, day: 20 } +}; + +// Valid: Item with resolvedAt as null +const itemUnresolved: Record_Properties_Items_Items = { + itemId: "item-005", + sequenceNumber: "005", + description: "Unresolved item", + occurredAt: { rawValue: "2024-01-15", year: 2024, month: 1, day: 15 }, + severity: "high", + resolvedAt: null +}; + +// Invalid: Item missing required fields +// @ts-expect-error - itemId is required +const itemMissingId: Record_Properties_Items_Items = { + sequenceNumber: "001", + description: "Missing ID", + occurredAt: { rawValue: "2024-01-15", year: 2024, month: 1, day: 15 }, + severity: "high" +}; + +// Valid: minimal Record +const minimalRecord: Record = { + recordId: "rec-001", + organizationName: "Acme Corp", + region: "US-WEST", + entity: { + fullName: { givenName: "John", familyName: "Doe" }, + birthDate: { rawValue: "1990-01-01", year: 1990, month: 1, day: 1 }, + locations: [] + }, + items: [ + { + itemId: "item-001", + sequenceNumber: "001", + description: "First item", + occurredAt: { rawValue: "2024-01-01", year: 2024, month: 1, day: 1 }, + severity: "low" + } + ] +}; + +// Valid: complete Record with all fields +const completeRecord: Record = { + recordId: "rec-002", + referenceCode: "REF-12345", + organizationName: "Acme Corp", + createdAt: { rawValue: "2024-01-01", year: 2024, month: 1, day: 1 }, + region: "US-EAST", + locationInfo: { + stateCode: "NY", + areaCode: "212" + }, + entity: { + fullName: { + rawValue: "John Michael Doe", + givenName: "John", + middleName: "Michael", + familyName: "Doe", + prefix: null, + suffix: null + }, + birthDate: { rawValue: "1985-06-15", year: 1985, month: 6, day: 15 }, + category: "individual", + classification: "premium", + locations: [ + { + rawValue: "123 Main St, New York, NY 10001, US", + line1: "123 Main St", + line2: null, + city: "New York", + district: "Manhattan", + region: "NY", + postalCode: "10001", + country: "US" + } + ] + }, + notes: "Important client", + items: [ + { + itemId: "i-001", + sequenceNumber: "001", + description: "Initial item", + code: "CODE-A", + occurredAt: { rawValue: "2024-01-10", year: 2024, month: 1, day: 10 }, + severity: "high", + resolution: "Addressed", + resolvedAt: { rawValue: "2024-01-15", year: 2024, month: 1, day: 15 }, + outcome: "success", + remarks: "Handled promptly", + category: "support", + subCategory: "billing", + meta: { origin: "web", originId: "web-123" } + }, + { + itemId: "i-002", + sequenceNumber: "002", + description: "Follow-up item", + code: null, + occurredAt: { rawValue: "2024-01-20", year: 2024, month: 1, day: 20 }, + severity: "low", + resolution: null, + resolvedAt: null, + remarks: null + } + ], + meta: { + origin: "api", + originId: "api-456" + } +}; + +// Invalid: Record missing required recordId +// @ts-expect-error - recordId is required +const recordMissingId: Record = { + organizationName: "Acme", + region: "US", + entity: { + fullName: { givenName: "John", familyName: "Doe" }, + birthDate: { rawValue: "1990-01-01", year: 1990, month: 1, day: 1 }, + locations: [] + }, + items: [] +}; + +// Invalid: extra property on Record (additionalProperties: false) +const recordExtraProperty: Record = { + recordId: "rec-001", + organizationName: "Acme", + region: "US", + entity: { + fullName: { givenName: "John", familyName: "Doe" }, + birthDate: { rawValue: "1990-01-01", year: 1990, month: 1, day: 1 }, + locations: [] + }, + items: [], + // @ts-expect-error - extra property not allowed + customField: "not allowed" +}; + +// Invalid: extra property on nested meta (additionalProperties: false) +const recordMetaExtra: Record = { + recordId: "rec-001", + organizationName: "Acme", + region: "US", + entity: { + fullName: { givenName: "John", familyName: "Doe" }, + birthDate: { rawValue: "1990-01-01", year: 1990, month: 1, day: 1 }, + locations: [] + }, + items: [], + meta: { + origin: "test", + // @ts-expect-error - extra property not allowed on meta + extra: "not allowed" + } +}; diff --git a/test/e2e/typescript/2020-12/const_keyword/test.ts b/test/e2e/typescript/2020-12/const_keyword/test.ts new file mode 100644 index 0000000..b5e199c --- /dev/null +++ b/test/e2e/typescript/2020-12/const_keyword/test.ts @@ -0,0 +1,173 @@ +import { ConstTest, ConstTest_Properties_Nested } from "./expected"; + + +// Valid: all required fields with exact const values +const valid: ConstTest = { + version: "1.0.0", + enabled: true, + mode: "production", + count: 42, + nothing: null +}; + +// Valid: with optional fields +const complete: ConstTest = { + version: "1.0.0", + enabled: true, + mode: "production", + count: 42, + nothing: null, + optionalFlag: false, + nested: { + fixedValue: "fixed", + fixedNumber: 100 + } +}; + +// Valid: nested with partial fields (all are optional in nested) +const partialNested: ConstTest = { + version: "1.0.0", + enabled: true, + mode: "production", + count: 42, + nothing: null, + nested: {} +}; + +// Invalid: version must be exactly "1.0.0" +const invalidVersion: ConstTest = { + // @ts-expect-error - version must be literal "1.0.0" + version: "2.0.0", + enabled: true, + mode: "production", + count: 42, + nothing: null +}; + +// Invalid: enabled must be exactly true +const invalidEnabled: ConstTest = { + version: "1.0.0", + // @ts-expect-error - enabled must be literal true + enabled: false, + mode: "production", + count: 42, + nothing: null +}; + +// Invalid: mode must be exactly "production" +const invalidMode: ConstTest = { + version: "1.0.0", + enabled: true, + // @ts-expect-error - mode must be literal "production" + mode: "development", + count: 42, + nothing: null +}; + +// Invalid: count must be exactly 42 +const invalidCount: ConstTest = { + version: "1.0.0", + enabled: true, + mode: "production", + // @ts-expect-error - count must be literal 42 + count: 100, + nothing: null +}; + +// Invalid: nothing must be exactly null (not string) +const invalidNothingString: ConstTest = { + version: "1.0.0", + enabled: true, + mode: "production", + count: 42, + // @ts-expect-error + nothing: "not null" +}; + +const invalidNothingUndefined: ConstTest = { + version: "1.0.0", + enabled: true, + mode: "production", + count: 42, + // @ts-expect-error + nothing: undefined +}; + +// Invalid: optionalFlag must be exactly false +const invalidOptionalFlag: ConstTest = { + version: "1.0.0", + enabled: true, + mode: "production", + count: 42, + nothing: null, + // @ts-expect-error - optionalFlag must be literal false + optionalFlag: true +}; + +// Invalid: nested.fixedValue must be exactly "fixed" +const invalidNestedValue: ConstTest = { + version: "1.0.0", + enabled: true, + mode: "production", + count: 42, + nothing: null, + nested: { + // @ts-expect-error - fixedValue must be literal "fixed" + fixedValue: "other" + } +}; + +// Invalid: nested.fixedNumber must be exactly 100 +const invalidNestedNumber: ConstTest = { + version: "1.0.0", + enabled: true, + mode: "production", + count: 42, + nothing: null, + nested: { + // @ts-expect-error - fixedNumber must be literal 100 + fixedNumber: 200 + } +}; + +// Invalid: missing required field +// @ts-expect-error - version is required +const missingVersion: ConstTest = { + enabled: true, + mode: "production", + count: 42, + nothing: null +}; + +// Invalid: extra property (additionalProperties: false) +const extraProperty: ConstTest = { + version: "1.0.0", + enabled: true, + mode: "production", + count: 42, + nothing: null, + // @ts-expect-error - extra property not allowed + extra: "not allowed" +}; + +// Invalid: extra property on nested (additionalProperties: false) +const nestedExtraProperty: ConstTest = { + version: "1.0.0", + enabled: true, + mode: "production", + count: 42, + nothing: null, + nested: { + fixedValue: "fixed", + // @ts-expect-error - extra property not allowed + extra: "not allowed" + } +}; + +// Test standalone nested type +const nested1: ConstTest_Properties_Nested = {}; +const nested2: ConstTest_Properties_Nested = { fixedValue: "fixed" }; +const nested3: ConstTest_Properties_Nested = { fixedNumber: 100 }; +const nested4: ConstTest_Properties_Nested = { fixedValue: "fixed", fixedNumber: 100 }; +// @ts-expect-error - fixedValue must be "fixed" +const invalidNested: ConstTest_Properties_Nested = { fixedValue: "wrong" }; diff --git a/test/e2e/typescript/2020-12/deeply_nested_refs/expected.d.ts b/test/e2e/typescript/2020-12/deeply_nested_refs/expected.d.ts index bff636f..a00a88e 100644 --- a/test/e2e/typescript/2020-12/deeply_nested_refs/expected.d.ts +++ b/test/e2e/typescript/2020-12/deeply_nested_refs/expected.d.ts @@ -163,6 +163,9 @@ export interface ApiResponse_X24Defs_UEntity_Properties_Relationships { "parent"?: ApiResponse_X24Defs_UEntity_Properties_Relationships_Properties_Parent; "children"?: ApiResponse_X24Defs_UEntity_Properties_Relationships_Properties_Children; [key: string]: + // As a notable limitation, TypeScript requires index signatures + // to also include the types of all of its properties, so we must + // match a superset of what JSON Schema allows ApiResponse_X24Defs_UEntity_Properties_Relationships_Properties_Parent | ApiResponse_X24Defs_UEntity_Properties_Relationships_Properties_Children | ApiResponse_X24Defs_UEntity_Properties_Relationships_AdditionalProperties | diff --git a/test/e2e/typescript/2020-12/deeply_nested_refs/test.ts b/test/e2e/typescript/2020-12/deeply_nested_refs/test.ts new file mode 100644 index 0000000..c9fa8c6 --- /dev/null +++ b/test/e2e/typescript/2020-12/deeply_nested_refs/test.ts @@ -0,0 +1,225 @@ +import { + ApiResponse, + ApiResponse_X24Defs_UEntity, + ApiResponse_X24Defs_UEntityAttributes, + ApiResponse_X24Defs_UPaginationInfo, + ApiResponse_X24Defs_UAppliedFilters +} from "./expected"; + + +// Valid: minimal response +const minimal: ApiResponse = { + status: "success", + data: { + items: [], + pagination: { + page: 1, + pageSize: 10, + totalItems: 0, + totalPages: 0 + } + }, + meta: { + requestId: "req-123", + timestamp: "2024-01-01T00:00:00Z" + } +}; + +// Valid: with error status +const errorResponse: ApiResponse = { + status: "error", + errorCode: "NOT_FOUND", + data: { + items: [], + pagination: { page: 1, pageSize: 10, totalItems: 0, totalPages: 0 } + }, + meta: { requestId: "req-456", timestamp: "2024-01-01T00:00:00Z" } +}; + +// Valid: errorCode can be null +const errorCodeNull: ApiResponse = { + status: "pending", + errorCode: null, + data: { + items: [], + pagination: { page: 1, pageSize: 10, totalItems: 0, totalPages: 0 } + }, + meta: { requestId: "req-789", timestamp: "2024-01-01T00:00:00Z" } +}; + +// Invalid: status must be one of the enum values +const invalidStatus: ApiResponse = { + // @ts-expect-error - status must be success|error|pending + status: "unknown", + data: { + items: [], + pagination: { page: 1, pageSize: 10, totalItems: 0, totalPages: 0 } + }, + meta: { requestId: "req", timestamp: "ts" } +}; + +// Valid: Entity with type enum +const validEntity: ApiResponse_X24Defs_UEntity = { + id: "user-1", + type: "user", + attributes: { + name: "John Doe", + createdAt: "2024-01-01" + } +}; + +// Invalid: Entity type must be from enum +const invalidEntityType: ApiResponse_X24Defs_UEntity = { + id: "e1", + // @ts-expect-error - type must be user|organization|resource + type: "admin", + attributes: { name: "test", createdAt: "2024-01-01" } +}; + +// Valid: Entity with relationships including additionalProperties +const entityWithRelationships: ApiResponse_X24Defs_UEntity = { + id: "org-1", + type: "organization", + attributes: { name: "Acme Corp", createdAt: "2024-01-01" }, + relationships: { + parent: { id: "parent-1", type: "organization" }, + children: [ + { id: "child-1", type: "user" }, + { id: "child-2", type: "resource" } + ], + // Additional properties should be allowed (additionalProperties: EntityReference) + manager: { id: "manager-1", type: "user" }, + subsidiary: { id: "sub-1", type: "organization" } + } +}; + +// Valid: EntityAttributes with metadata (additionalProperties: string|number|boolean) +const attributesWithMetadata: ApiResponse_X24Defs_UEntityAttributes = { + name: "Test Entity", + createdAt: "2024-01-01", + description: "A test entity", + updatedAt: "2024-06-01", + tags: [ "tag1", "tag2" ], + metadata: { + stringValue: "hello", + numberValue: 42, + booleanValue: true + } +}; + +// Invalid: metadata values must be string|number|boolean, not object +const invalidMetadata: ApiResponse_X24Defs_UEntityAttributes = { + name: "test", + createdAt: "2024-01-01", + metadata: { + // @ts-expect-error - must be string|number|boolean, not object + nested: { foo: "bar" } + } +}; + +// Invalid: metadata values must be string|number|boolean, not array +const invalidMetadataArray: ApiResponse_X24Defs_UEntityAttributes = { + name: "test", + createdAt: "2024-01-01", + metadata: { + // @ts-expect-error - must be string|number|boolean, not array + list: [ 1, 2, 3 ] + } +}; + +// Valid: PaginationInfo with boolean enum fields +const pagination: ApiResponse_X24Defs_UPaginationInfo = { + page: 1, + pageSize: 20, + totalItems: 100, + totalPages: 5, + hasNextPage: true, + hasPreviousPage: false +}; + +// Valid: AppliedFilters with sortBy enum including null +const filters: ApiResponse_X24Defs_UAppliedFilters = { + search: "query", + sortBy: "name", + sortOrder: "asc" +}; + +// Valid: sortBy can be null (it's in the enum) +const filtersWithNullSort: ApiResponse_X24Defs_UAppliedFilters = { + sortBy: null, + sortOrder: "desc" +}; + +// Invalid: sortBy must be from enum +const invalidSortBy: ApiResponse_X24Defs_UAppliedFilters = { + // @ts-expect-error - sortBy must be name|createdAt|updatedAt|null + sortBy: "id" +}; + +// Invalid: sortOrder must be asc or desc +const invalidSortOrder: ApiResponse_X24Defs_UAppliedFilters = { + // @ts-expect-error - sortOrder must be asc|desc + sortOrder: "random" +}; + +// Valid: types array with enum items +const filtersWithTypes: ApiResponse_X24Defs_UAppliedFilters = { + types: [ "user", "organization" ] +}; + +// Invalid: types must be from enum +const invalidTypes: ApiResponse_X24Defs_UAppliedFilters = { + // @ts-expect-error - type items must be user|organization|resource + types: [ "user", "admin" ] +}; + +// Valid: full response with all features +const fullResponse: ApiResponse = { + status: "success", + errorCode: null, + data: { + items: [ + { + id: "user-1", + type: "user", + attributes: { + name: "Alice", + description: "Developer", + createdAt: "2024-01-01", + updatedAt: "2024-06-01", + tags: [ "developer", "team-a" ], + metadata: { department: "Engineering", level: 5, active: true } + }, + relationships: { + parent: { id: "org-1", type: "organization" }, + children: [], + mentor: { id: "user-2", type: "user" } + } + } + ], + pagination: { + page: 1, + pageSize: 10, + totalItems: 1, + totalPages: 1, + hasNextPage: false, + hasPreviousPage: false + }, + filters: { + search: "alice", + dateRange: { start: "2024-01-01", end: "2024-12-31" }, + types: [ "user" ], + sortBy: "createdAt", + sortOrder: "desc" + } + }, + meta: { + requestId: "req-abc", + timestamp: "2024-06-15T12:00:00Z", + version: "2.0", + deprecationWarnings: [ + { message: "Field deprecated", field: "oldField", suggestedAlternative: "newField" }, + { message: "Another warning", field: "anotherField", suggestedAlternative: null } + ] + } +}; diff --git a/test/e2e/typescript/2020-12/defs_and_refs/test.ts b/test/e2e/typescript/2020-12/defs_and_refs/test.ts new file mode 100644 index 0000000..b1f3ca8 --- /dev/null +++ b/test/e2e/typescript/2020-12/defs_and_refs/test.ts @@ -0,0 +1,222 @@ +import { + SocialPlatform, + SocialPlatform_X24Defs_X55ser, + SocialPlatform_X24Defs_UPost, + SocialPlatform_X24Defs_USettings, + SocialPlatform_X24Defs_UTheme, + SocialPlatform_X24Defs_UPostStatus +} from "./expected"; + + +// Valid: minimal required fields +const minimal: SocialPlatform = { + user: { + id: "550e8400-e29b-41d4-a716-446655440000", + username: "john_doe", + email: "john@example.com" + }, + posts: [], + settings: {} +}; + +// Valid: user with profile +const userWithProfile: SocialPlatform_X24Defs_X55ser = { + id: "550e8400-e29b-41d4-a716-446655440001", + username: "jane_doe", + email: "jane@example.com", + profile: { + bio: "Software developer", + avatar: "https://example.com/avatar.png", + location: "San Francisco" + } +}; + +// Valid: profile with null values (anyOf [string, null]) +const userWithNullProfile: SocialPlatform_X24Defs_X55ser = { + id: "550e8400-e29b-41d4-a716-446655440002", + username: "bob", + email: "bob@example.com", + profile: { + bio: null, + location: null + } +}; + +// Valid: post with all fields +const validPost: SocialPlatform_X24Defs_UPost = { + id: "550e8400-e29b-41d4-a716-446655440003", + title: "Hello World", + content: "This is my first post", + author: { + id: "550e8400-e29b-41d4-a716-446655440001", + username: "jane", + email: "jane@example.com" + }, + tags: [ + { name: "tech" }, + { name: "programming", slug: "programming" } + ], + status: "published" +}; + +// Valid: Theme enum values +const lightTheme: SocialPlatform_X24Defs_UTheme = "light"; +const darkTheme: SocialPlatform_X24Defs_UTheme = "dark"; +const systemTheme: SocialPlatform_X24Defs_UTheme = "system"; + +// Invalid: Theme must be from enum +// @ts-expect-error - theme must be light|dark|system +const invalidTheme: SocialPlatform_X24Defs_UTheme = "blue"; + +// Valid: PostStatus enum values +const draftStatus: SocialPlatform_X24Defs_UPostStatus = "draft"; +const publishedStatus: SocialPlatform_X24Defs_UPostStatus = "published"; +const archivedStatus: SocialPlatform_X24Defs_UPostStatus = "archived"; + +// Invalid: PostStatus must be from enum +// @ts-expect-error - status must be draft|published|archived +const invalidStatus: SocialPlatform_X24Defs_UPostStatus = "deleted"; + +// Valid: settings with all fields +const fullSettings: SocialPlatform_X24Defs_USettings = { + theme: "dark", + notifications: { + email: true, + push: false, + sms: false + }, + privacy: { + profileVisible: true, + showEmail: false + } +}; + +// Valid: pinnedPost as Post +const withPinnedPost: SocialPlatform = { + user: { id: "uuid-1", username: "test", email: "test@test.com" }, + posts: [], + settings: {}, + pinnedPost: { + id: "post-uuid", + title: "Pinned Post", + author: { id: "uuid-1", username: "test", email: "test@test.com" } + } +}; + +// Valid: pinnedPost as null +const withNullPinnedPost: SocialPlatform = { + user: { id: "uuid-1", username: "test", email: "test@test.com" }, + posts: [], + settings: {}, + pinnedPost: null +}; + +// Invalid: missing required user +// @ts-expect-error - user is required +const missingUser: SocialPlatform = { + posts: [], + settings: {} +}; + +// Invalid: missing required posts +// @ts-expect-error - posts is required +const missingPosts: SocialPlatform = { + user: { id: "uuid", username: "test", email: "test@test.com" }, + settings: {} +}; + +// Invalid: User missing required id +const invalidUserMissingId: SocialPlatform = { + // @ts-expect-error - id is required + user: { username: "test", email: "test@test.com" }, + posts: [], + settings: {} +}; + +// Invalid: User missing required email +const invalidUserMissingEmail: SocialPlatform = { + // @ts-expect-error - email is required + user: { id: "uuid", username: "test" }, + posts: [], + settings: {} +}; + +// Invalid: Post missing required title +// @ts-expect-error - title is required +const invalidPost: SocialPlatform_X24Defs_UPost = { + id: "post-id", + author: { id: "uuid", username: "test", email: "test@test.com" } +}; + +// Invalid: Post with invalid status enum +const invalidPostStatus: SocialPlatform_X24Defs_UPost = { + id: "post-id", + title: "Test", + author: { id: "uuid", username: "test", email: "test@test.com" }, + // @ts-expect-error - status must be draft|published|archived + status: "hidden" +}; + +// Invalid: extra property on User (additionalProperties: false) +const userExtraProperty: SocialPlatform_X24Defs_X55ser = { + id: "uuid", + username: "test", + email: "test@test.com", + // @ts-expect-error - extra property not allowed + nickname: "testy" +}; + +// Valid: full example +const fullExample: SocialPlatform = { + user: { + id: "550e8400-e29b-41d4-a716-446655440000", + username: "alice_dev", + email: "alice@example.com", + profile: { + bio: "Full-stack developer", + avatar: "https://example.com/alice.jpg", + location: "New York" + } + }, + posts: [ + { + id: "550e8400-e29b-41d4-a716-446655440010", + title: "Getting Started with TypeScript", + content: "TypeScript is great...", + author: { + id: "550e8400-e29b-41d4-a716-446655440000", + username: "alice_dev", + email: "alice@example.com" + }, + tags: [ + { name: "typescript", slug: "typescript" }, + { name: "tutorial" } + ], + status: "published" + }, + { + id: "550e8400-e29b-41d4-a716-446655440011", + title: "Draft: Advanced Patterns", + author: { + id: "550e8400-e29b-41d4-a716-446655440000", + username: "alice_dev", + email: "alice@example.com" + }, + status: "draft" + } + ], + settings: { + theme: "dark", + notifications: { email: true, push: true, sms: false }, + privacy: { profileVisible: true, showEmail: false } + }, + followers: [ + { id: "follower-1", username: "bob", email: "bob@example.com" }, + { id: "follower-2", username: "charlie", email: "charlie@example.com", profile: { bio: null } } + ], + pinnedPost: { + id: "550e8400-e29b-41d4-a716-446655440010", + title: "Getting Started with TypeScript", + author: { id: "550e8400-e29b-41d4-a716-446655440000", username: "alice_dev", email: "alice@example.com" } + } +}; diff --git a/test/e2e/typescript/2020-12/embedded_resources/test.ts b/test/e2e/typescript/2020-12/embedded_resources/test.ts new file mode 100644 index 0000000..6e3d4be --- /dev/null +++ b/test/e2e/typescript/2020-12/embedded_resources/test.ts @@ -0,0 +1,46 @@ +import { DocumentSystem, DocumentSystem_X24Defs_UItem } from "./expected"; + + +// Valid: minimal +const valid: DocumentSystem = { + item: { name: "test item" } +}; + +// Invalid: missing required item +// @ts-expect-error +const missingItem: DocumentSystem = {}; + +// Invalid: item missing required name +const missingName: DocumentSystem = { + // @ts-expect-error + item: {} +}; + +// Invalid: item name must be string +const invalidName: DocumentSystem = { + item: { + // @ts-expect-error + name: 123 + } +}; + +// Invalid: extra property on item (additionalProperties: false) +const invalidItemExtra: DocumentSystem = { + item: { + name: "test", + // @ts-expect-error + description: "not allowed" + } +}; + +// Invalid: extra property on root (additionalProperties: false) +const invalidRootExtra: DocumentSystem = { + item: { name: "test" }, + // @ts-expect-error + extra: "not allowed" +}; + +// Test standalone Item type +const validItem: DocumentSystem_X24Defs_UItem = { name: "standalone" }; +// @ts-expect-error +const invalidItem: DocumentSystem_X24Defs_UItem = { name: 123 }; diff --git a/test/e2e/typescript/2020-12/enum_complex_types/test.ts b/test/e2e/typescript/2020-12/enum_complex_types/test.ts new file mode 100644 index 0000000..fc36510 --- /dev/null +++ b/test/e2e/typescript/2020-12/enum_complex_types/test.ts @@ -0,0 +1,137 @@ +import { + ComplexEnum, + ComplexEnum_Properties_Status, + ComplexEnum_Properties_Coordinates, + ComplexEnum_Properties_FixedConfig, + ComplexEnum_Properties_FixedList +} from "./expected"; + + +// Valid: empty object (all optional) +const empty: ComplexEnum = {}; + +// Valid: status with each enum value +const status200: ComplexEnum = { + status: { code: 200, message: "OK" } +}; + +const status404: ComplexEnum = { + status: { code: 404, message: "Not Found" } +}; + +const status500: ComplexEnum = { + status: { code: 500, message: "Internal Server Error" } +}; + +// Valid: coordinates with each enum value +const coordsOrigin: ComplexEnum = { + coordinates: [ 0, 0 ] +}; + +const coordsPositive: ComplexEnum = { + coordinates: [ 1, 1 ] +}; + +const coordsNegative: ComplexEnum = { + coordinates: [ -1, -1 ] +}; + +// Valid: fixedConfig with exact const value +const withFixedConfig: ComplexEnum = { + fixedConfig: { enabled: true, maxRetries: 3 } +}; + +// Valid: fixedList with exact const value +const withFixedList: ComplexEnum = { + fixedList: [ "read", "write", "execute" ] +}; + +// Valid: all fields +const complete: ComplexEnum = { + status: { code: 200, message: "OK" }, + coordinates: [ 0, 0 ], + fixedConfig: { enabled: true, maxRetries: 3 }, + fixedList: [ "read", "write", "execute" ] +}; + +// Invalid: status with wrong code +const invalidStatusCode: ComplexEnum = { + // @ts-expect-error - status must be one of the enum objects + status: { code: 201, message: "Created" } +}; + +// Invalid: status with mismatched code/message +const invalidStatusMismatch: ComplexEnum = { + // @ts-expect-error - code 200 must have message "OK" + status: { code: 200, message: "Not Found" } +}; + +// Invalid: coordinates not in enum +const invalidCoords: ComplexEnum = { + // @ts-expect-error - coordinates must be [0,0], [1,1], or [-1,-1] + coordinates: [ 2, 2 ] +}; + +// Invalid: coordinates with mixed values +const invalidCoordsMixed: ComplexEnum = { + // @ts-expect-error - coordinates must be exactly one of the enum values + coordinates: [ 0, 1 ] +}; + +// Invalid: fixedConfig with wrong enabled value +const invalidFixedConfigEnabled: ComplexEnum = { + // @ts-expect-error - fixedConfig must be exactly {enabled: true, maxRetries: 3} + fixedConfig: { enabled: false, maxRetries: 3 } +}; + +// Invalid: fixedConfig with wrong maxRetries value +const invalidFixedConfigRetries: ComplexEnum = { + // @ts-expect-error - fixedConfig must be exactly {enabled: true, maxRetries: 3} + fixedConfig: { enabled: true, maxRetries: 5 } +}; + +// Invalid: fixedList with wrong values +const invalidFixedListValues: ComplexEnum = { + // @ts-expect-error - fixedList must be exactly ["read", "write", "execute"] + fixedList: [ "read", "write", "delete" ] +}; + +// Invalid: fixedList with wrong order +const invalidFixedListOrder: ComplexEnum = { + // @ts-expect-error - fixedList must be exactly ["read", "write", "execute"] + fixedList: [ "write", "read", "execute" ] +}; + +// Invalid: fixedList with different length +const invalidFixedListLength: ComplexEnum = { + // @ts-expect-error - fixedList must be exactly 3 elements + fixedList: [ "read", "write" ] +}; + +// Invalid: extra property (additionalProperties: false) +const extraProperty: ComplexEnum = { + status: { code: 200, message: "OK" }, + // @ts-expect-error - extra property not allowed + extra: "not allowed" +}; + +// Test standalone types +const validStatus1: ComplexEnum_Properties_Status = { code: 200, message: "OK" }; +const validStatus2: ComplexEnum_Properties_Status = { code: 404, message: "Not Found" }; +const validStatus3: ComplexEnum_Properties_Status = { code: 500, message: "Internal Server Error" }; +// @ts-expect-error - must be one of the enum values +const invalidStatusStandalone: ComplexEnum_Properties_Status = { code: 201, message: "Created" }; + +const validCoords1: ComplexEnum_Properties_Coordinates = [ 0, 0 ]; +const validCoords2: ComplexEnum_Properties_Coordinates = [ 1, 1 ]; +const validCoords3: ComplexEnum_Properties_Coordinates = [ -1, -1 ]; +// @ts-expect-error - must be one of the enum values +const invalidCoordsStandalone: ComplexEnum_Properties_Coordinates = [ 0, 1 ]; + +const validConfig: ComplexEnum_Properties_FixedConfig = { enabled: true, maxRetries: 3 }; +// @ts-expect-error - must match const exactly +const invalidConfig: ComplexEnum_Properties_FixedConfig = { enabled: true, maxRetries: 10 }; + +const validList: ComplexEnum_Properties_FixedList = [ "read", "write", "execute" ]; +// @ts-expect-error - must match const exactly +const invalidList: ComplexEnum_Properties_FixedList = [ "read", "write" ]; diff --git a/test/e2e/typescript/2020-12/implicit_types_and_reused_refs/test.ts b/test/e2e/typescript/2020-12/implicit_types_and_reused_refs/test.ts new file mode 100644 index 0000000..184a535 --- /dev/null +++ b/test/e2e/typescript/2020-12/implicit_types_and_reused_refs/test.ts @@ -0,0 +1,255 @@ +import { + DocSystem, + DocSystem_X24Defs_X55ser, + DocSystem_X24Defs_UTimestamp, + DocSystem_Properties_Document, + DocSystem_Properties_Permissions, + DocSystem_Properties_History_Items +} from "./expected"; + + +// Valid: minimal required fields +const minimal: DocSystem = { + document: { + id: "doc-uuid-1", + title: "Test Document", + content: { + format: "markdown", + body: "# Hello" + }, + author: { + id: "user-uuid-1", + email: "author@example.com" + } + }, + permissions: { + owner: { id: "user-uuid-1", email: "owner@example.com" }, + readers: [], + editors: [] + }, + history: [] +}; + +// Valid: User with all fields including role enum +const userWithRole: DocSystem_X24Defs_X55ser = { + id: "user-123", + email: "user@example.com", + displayName: "John Doe", + role: "admin" +}; + +// Valid: User with null displayName (anyOf [string, null]) +const userNullDisplay: DocSystem_X24Defs_X55ser = { + id: "user-123", + email: "user@example.com", + displayName: null +}; + +// Invalid: User role must be from enum +const invalidRole: DocSystem_X24Defs_X55ser = { + id: "user-123", + email: "user@example.com", + // @ts-expect-error - role must be admin|editor|viewer|guest + role: "superuser" +}; + +// Valid: Timestamp with all fields +const timestamp: DocSystem_X24Defs_UTimestamp = { + unix: 1704067200, + iso: "2024-01-01T00:00:00Z", + timezone: "UTC" +}; + +// Valid: Timestamp with null timezone +const timestampNullTz: DocSystem_X24Defs_UTimestamp = { + unix: 1704067200, + iso: "2024-01-01T00:00:00Z", + timezone: null +}; + +// Invalid: Timestamp missing required unix +// @ts-expect-error - unix is required +const timestampMissingUnix: DocSystem_X24Defs_UTimestamp = { + iso: "2024-01-01T00:00:00Z" +}; + +// Valid: Document with format enum values +const docMarkdown: DocSystem_Properties_Document = { + id: "doc-1", + title: "Markdown Doc", + content: { format: "markdown", body: "# Title" }, + author: { id: "user-1", email: "a@b.com" } +}; + +const docHtml: DocSystem_Properties_Document = { + id: "doc-2", + title: "HTML Doc", + content: { format: "html", body: "

Title

" }, + author: { id: "user-1", email: "a@b.com" } +}; + +const docPlaintext: DocSystem_Properties_Document = { + id: "doc-3", + title: "Plain Doc", + content: { format: "plaintext", body: "Title" }, + author: { id: "user-1", email: "a@b.com" } +}; + +// Invalid: format must be from enum +const invalidFormat: DocSystem_Properties_Document = { + id: "doc-1", + title: "Doc", + content: { + // @ts-expect-error - format must be markdown|html|plaintext + format: "rtf", + body: "text" + }, + author: { id: "user-1", email: "a@b.com" } +}; + +// Valid: permissions with isPublic boolean enum +const permissionsPublic: DocSystem_Properties_Permissions = { + owner: { id: "user-1", email: "owner@a.com" }, + readers: [], + editors: [], + isPublic: true +}; + +const permissionsPrivate: DocSystem_Properties_Permissions = { + owner: { id: "user-1", email: "owner@a.com" }, + readers: [], + editors: [], + isPublic: false +}; + +// Valid: permissions with expiresAt as Timestamp +const permissionsWithExpiry: DocSystem_Properties_Permissions = { + owner: { id: "user-1", email: "owner@a.com" }, + readers: [], + editors: [], + expiresAt: { unix: 1704067200, iso: "2024-01-01T00:00:00Z" } +}; + +// Valid: permissions with expiresAt as null +const permissionsNoExpiry: DocSystem_Properties_Permissions = { + owner: { id: "user-1", email: "owner@a.com" }, + readers: [], + editors: [], + expiresAt: null +}; + +// Valid: history item with all action enum values +const historyCreated: DocSystem_Properties_History_Items = { + action: "created", + actor: { id: "user-1", email: "a@b.com" }, + timestamp: { unix: 1704067200, iso: "2024-01-01T00:00:00Z" } +}; + +const historyUpdated: DocSystem_Properties_History_Items = { + action: "updated", + actor: { id: "user-1", email: "a@b.com" }, + timestamp: { unix: 1704067200, iso: "2024-01-01T00:00:00Z" }, + details: { field: "title", oldValue: "Old", newValue: "New" } +}; + +const historyDeleted: DocSystem_Properties_History_Items = { + action: "deleted", + actor: { id: "user-1", email: "a@b.com" }, + timestamp: { unix: 1704067200, iso: "2024-01-01T00:00:00Z" } +}; + +const historyRestored: DocSystem_Properties_History_Items = { + action: "restored", + actor: { id: "user-1", email: "a@b.com" }, + timestamp: { unix: 1704067200, iso: "2024-01-01T00:00:00Z" } +}; + +const historyShared: DocSystem_Properties_History_Items = { + action: "shared", + actor: { id: "user-1", email: "a@b.com" }, + timestamp: { unix: 1704067200, iso: "2024-01-01T00:00:00Z" } +}; + +// Invalid: action must be from enum +const historyInvalidAction: DocSystem_Properties_History_Items = { + // @ts-expect-error - action must be created|updated|deleted|restored|shared + action: "archived", + actor: { id: "user-1", email: "a@b.com" }, + timestamp: { unix: 1704067200, iso: "2024-01-01T00:00:00Z" } +}; + +// Valid: history with details having number oldValue/newValue +const historyNumericChange: DocSystem_Properties_History_Items = { + action: "updated", + actor: { id: "user-1", email: "a@b.com" }, + timestamp: { unix: 1704067200, iso: "2024-01-01T00:00:00Z" }, + details: { field: "version", oldValue: 1, newValue: 2 } +}; + +// Valid: history with details as null +const historyNullDetails: DocSystem_Properties_History_Items = { + action: "created", + actor: { id: "user-1", email: "a@b.com" }, + timestamp: { unix: 1704067200, iso: "2024-01-01T00:00:00Z" }, + details: null +}; + +// Valid: full example with relatedDocuments +const fullExample: DocSystem = { + document: { + id: "doc-main", + title: "Main Document", + content: { + format: "markdown", + body: "# Main\n\nContent here", + summary: "A summary" + }, + author: { id: "author-1", email: "author@example.com", displayName: "Author", role: "editor" }, + reviewers: [ + { id: "reviewer-1", email: "r1@example.com", role: "viewer" }, + { id: "reviewer-2", email: "r2@example.com", displayName: null } + ], + tags: [ + { name: "important", color: "red" }, + { name: "draft", color: null } + ], + metadata: { + createdAt: { unix: 1704067200, iso: "2024-01-01T00:00:00Z" }, + updatedAt: { unix: 1706745600, iso: "2024-02-01T00:00:00Z", timezone: "America/New_York" }, + version: 3 + } + }, + permissions: { + owner: { id: "author-1", email: "author@example.com" }, + readers: [ { id: "reader-1", email: "reader@example.com" } ], + editors: [ { id: "editor-1", email: "editor@example.com", role: "editor" } ], + isPublic: false, + expiresAt: { unix: 1735689600, iso: "2025-01-01T00:00:00Z" } + }, + history: [ + { action: "created", actor: { id: "author-1", email: "author@example.com" }, timestamp: { unix: 1704067200, iso: "2024-01-01T00:00:00Z" } }, + { action: "updated", actor: { id: "editor-1", email: "editor@example.com" }, timestamp: { unix: 1706745600, iso: "2024-02-01T00:00:00Z" }, details: { field: "title", oldValue: "Draft", newValue: "Main Document" } } + ], + relatedDocuments: [ + { id: "related-1", relationship: "parent", title: "Parent Doc" }, + { id: "related-2", relationship: "child" }, + { id: "related-3", relationship: "sibling", title: null }, + { id: "related-4", relationship: "reference", title: "Referenced Doc" } + ] +}; + +// Invalid: relationship must be from enum +const invalidRelationship: DocSystem = { + document: { + id: "doc-1", + title: "Doc", + content: { format: "markdown", body: "text" }, + author: { id: "user-1", email: "a@b.com" } + }, + permissions: { owner: { id: "user-1", email: "a@b.com" }, readers: [], editors: [] }, + history: [], + relatedDocuments: [ + // @ts-expect-error - relationship must be parent|child|sibling|reference + { id: "rel-1", relationship: "linked" } + ] +}; diff --git a/test/e2e/typescript/2020-12/object_with_additional_properties/expected.d.ts b/test/e2e/typescript/2020-12/object_with_additional_properties/expected.d.ts index b931cdb..1646a0b 100644 --- a/test/e2e/typescript/2020-12/object_with_additional_properties/expected.d.ts +++ b/test/e2e/typescript/2020-12/object_with_additional_properties/expected.d.ts @@ -8,6 +8,9 @@ export interface Person { "name": Person_Properties_Name; "age"?: Person_Properties_Age; [key: string]: + // As a notable limitation, TypeScript requires index signatures + // to also include the types of all of its properties, so we must + // match a superset of what JSON Schema allows Person_Properties_Name | Person_Properties_Age | Person_AdditionalProperties | diff --git a/test/e2e/typescript/2020-12/object_with_optional_string_property/expected.d.ts b/test/e2e/typescript/2020-12/object_with_optional_string_property/expected.d.ts index f7871c0..58ff7cb 100644 --- a/test/e2e/typescript/2020-12/object_with_optional_string_property/expected.d.ts +++ b/test/e2e/typescript/2020-12/object_with_optional_string_property/expected.d.ts @@ -2,4 +2,5 @@ export type MyObject_Properties_Foo = string; export interface MyObject { "foo"?: MyObject_Properties_Foo; + [key: string]: unknown | undefined; } diff --git a/test/e2e/typescript/2020-12/object_with_optional_string_property/test.ts b/test/e2e/typescript/2020-12/object_with_optional_string_property/test.ts new file mode 100644 index 0000000..3479499 --- /dev/null +++ b/test/e2e/typescript/2020-12/object_with_optional_string_property/test.ts @@ -0,0 +1,25 @@ +import { MyObject } from "./expected"; + +// Valid: empty object +const empty: MyObject = {}; + +// Valid: with optional foo +const withFoo: MyObject = { + foo: "hello" +}; + +// Invalid: foo must be string +const invalidFoo: MyObject = { + // @ts-expect-error + foo: 123 +}; + +// Valid: with extra properties (additionalProperties not set means any allowed) +const withExtra: MyObject = { + foo: "hello", + bar: "extra" +}; + +// Assignment from variable should work (TypeScript allows this) +const extraData = { foo: "hello", bar: "extra", count: 42 }; +const assignedFromVariable: MyObject = extraData; diff --git a/test/e2e/typescript/2020-12/tuples_and_arrays/expected.d.ts b/test/e2e/typescript/2020-12/tuples_and_arrays/expected.d.ts index c61f6ba..6a8be54 100644 --- a/test/e2e/typescript/2020-12/tuples_and_arrays/expected.d.ts +++ b/test/e2e/typescript/2020-12/tuples_and_arrays/expected.d.ts @@ -10,8 +10,7 @@ export type DataPipeline_Properties_Stages_Items_Properties_OutputType_AnyOf_ZIn export type DataPipeline_Properties_Stages_Items_Properties_OutputType_AnyOf_ZIndex3 = [DataPipeline_Properties_Stages_Items_Properties_OutputType_AnyOf_ZIndex3_PrefixItems_ZIndex0, DataPipeline_Properties_Stages_Items_Properties_OutputType_AnyOf_ZIndex3_PrefixItems_ZIndex1, ...DataPipeline_Properties_Stages_Items_Properties_OutputType_AnyOf_ZIndex3_Items[]]; -export interface DataPipeline_Properties_Stages_Items_Properties_OutputType_AnyOf_ZIndex2 { -} +export type DataPipeline_Properties_Stages_Items_Properties_OutputType_AnyOf_ZIndex2 = Record; export type DataPipeline_Properties_Stages_Items_Properties_OutputType_AnyOf_ZIndex1 = boolean; @@ -41,8 +40,7 @@ export type DataPipeline_Properties_Stages_Items_Properties_Metrics_AnyOf_ZIndex export type DataPipeline_Properties_Stages_Items_Properties_Metrics_AnyOf_ZIndex3 = [DataPipeline_Properties_Stages_Items_Properties_Metrics_AnyOf_ZIndex3_PrefixItems_ZIndex0, DataPipeline_Properties_Stages_Items_Properties_Metrics_AnyOf_ZIndex3_PrefixItems_ZIndex1, DataPipeline_Properties_Stages_Items_Properties_Metrics_AnyOf_ZIndex3_PrefixItems_ZIndex2, ...DataPipeline_Properties_Stages_Items_Properties_Metrics_AnyOf_ZIndex3_Items[]]; -export interface DataPipeline_Properties_Stages_Items_Properties_Metrics_AnyOf_ZIndex2 { -} +export type DataPipeline_Properties_Stages_Items_Properties_Metrics_AnyOf_ZIndex2 = Record; export type DataPipeline_Properties_Stages_Items_Properties_Metrics_AnyOf_ZIndex1 = boolean; @@ -83,8 +81,7 @@ export interface DataPipeline_Properties_Stages_Items_Properties_InputTypes_AnyO export type DataPipeline_Properties_Stages_Items_Properties_InputTypes_AnyOf_ZIndex3 = [DataPipeline_Properties_Stages_Items_Properties_InputTypes_AnyOf_ZIndex3_PrefixItems_ZIndex0, DataPipeline_Properties_Stages_Items_Properties_InputTypes_AnyOf_ZIndex3_PrefixItems_ZIndex1, ...DataPipeline_Properties_Stages_Items_Properties_InputTypes_AnyOf_ZIndex3_Items[]]; -export interface DataPipeline_Properties_Stages_Items_Properties_InputTypes_AnyOf_ZIndex2 { -} +export type DataPipeline_Properties_Stages_Items_Properties_InputTypes_AnyOf_ZIndex2 = Record; export type DataPipeline_Properties_Stages_Items_Properties_InputTypes_AnyOf_ZIndex1 = boolean; @@ -141,8 +138,7 @@ export type DataPipeline_Properties_Pipeline_Properties_Version_AnyOf_ZIndex3_It export type DataPipeline_Properties_Pipeline_Properties_Version_AnyOf_ZIndex3 = [DataPipeline_Properties_Pipeline_Properties_Version_AnyOf_ZIndex3_PrefixItems_ZIndex0, DataPipeline_Properties_Pipeline_Properties_Version_AnyOf_ZIndex3_PrefixItems_ZIndex1, DataPipeline_Properties_Pipeline_Properties_Version_AnyOf_ZIndex3_PrefixItems_ZIndex2, ...DataPipeline_Properties_Pipeline_Properties_Version_AnyOf_ZIndex3_Items[]]; -export interface DataPipeline_Properties_Pipeline_Properties_Version_AnyOf_ZIndex2 { -} +export type DataPipeline_Properties_Pipeline_Properties_Version_AnyOf_ZIndex2 = Record; export type DataPipeline_Properties_Pipeline_Properties_Version_AnyOf_ZIndex1 = boolean; @@ -176,8 +172,7 @@ export type DataPipeline_Properties_Pipeline_Properties_Coordinates_AnyOf_ZIndex export type DataPipeline_Properties_Pipeline_Properties_Coordinates_AnyOf_ZIndex3 = [DataPipeline_Properties_Pipeline_Properties_Coordinates_AnyOf_ZIndex3_PrefixItems_ZIndex0, DataPipeline_Properties_Pipeline_Properties_Coordinates_AnyOf_ZIndex3_PrefixItems_ZIndex1, DataPipeline_Properties_Pipeline_Properties_Coordinates_AnyOf_ZIndex3_PrefixItems_ZIndex2, ...DataPipeline_Properties_Pipeline_Properties_Coordinates_AnyOf_ZIndex3_Items[]]; -export interface DataPipeline_Properties_Pipeline_Properties_Coordinates_AnyOf_ZIndex2 { -} +export type DataPipeline_Properties_Pipeline_Properties_Coordinates_AnyOf_ZIndex2 = Record; export type DataPipeline_Properties_Pipeline_Properties_Coordinates_AnyOf_ZIndex1 = boolean; @@ -222,8 +217,7 @@ export type DataPipeline_Properties_Metadata_Properties_Flags_AnyOf_ZIndex3_Item export type DataPipeline_Properties_Metadata_Properties_Flags_AnyOf_ZIndex3 = [DataPipeline_Properties_Metadata_Properties_Flags_AnyOf_ZIndex3_PrefixItems_ZIndex0, DataPipeline_Properties_Metadata_Properties_Flags_AnyOf_ZIndex3_PrefixItems_ZIndex1, DataPipeline_Properties_Metadata_Properties_Flags_AnyOf_ZIndex3_PrefixItems_ZIndex2, ...DataPipeline_Properties_Metadata_Properties_Flags_AnyOf_ZIndex3_Items[]]; -export interface DataPipeline_Properties_Metadata_Properties_Flags_AnyOf_ZIndex2 { -} +export type DataPipeline_Properties_Metadata_Properties_Flags_AnyOf_ZIndex2 = Record; export type DataPipeline_Properties_Metadata_Properties_Flags_AnyOf_ZIndex1 = boolean; @@ -251,8 +245,7 @@ export type DataPipeline_Properties_Metadata_Properties_Authors_Items_AnyOf_ZInd export type DataPipeline_Properties_Metadata_Properties_Authors_Items_AnyOf_ZIndex3 = [DataPipeline_Properties_Metadata_Properties_Authors_Items_AnyOf_ZIndex3_PrefixItems_ZIndex0, DataPipeline_Properties_Metadata_Properties_Authors_Items_AnyOf_ZIndex3_PrefixItems_ZIndex1, ...DataPipeline_Properties_Metadata_Properties_Authors_Items_AnyOf_ZIndex3_Items[]]; -export interface DataPipeline_Properties_Metadata_Properties_Authors_Items_AnyOf_ZIndex2 { -} +export type DataPipeline_Properties_Metadata_Properties_Authors_Items_AnyOf_ZIndex2 = Record; export type DataPipeline_Properties_Metadata_Properties_Authors_Items_AnyOf_ZIndex1 = boolean; @@ -300,8 +293,7 @@ export type DataPipeline_Properties_Connections_Items_AnyOf_ZIndex3_Items = neve export type DataPipeline_Properties_Connections_Items_AnyOf_ZIndex3 = [DataPipeline_Properties_Connections_Items_AnyOf_ZIndex3_PrefixItems_ZIndex0, DataPipeline_Properties_Connections_Items_AnyOf_ZIndex3_PrefixItems_ZIndex1, DataPipeline_Properties_Connections_Items_AnyOf_ZIndex3_PrefixItems_ZIndex2, ...DataPipeline_Properties_Connections_Items_AnyOf_ZIndex3_Items[]]; -export interface DataPipeline_Properties_Connections_Items_AnyOf_ZIndex2 { -} +export type DataPipeline_Properties_Connections_Items_AnyOf_ZIndex2 = Record; export type DataPipeline_Properties_Connections_Items_AnyOf_ZIndex1 = boolean; diff --git a/test/e2e/typescript/2020-12/tuples_and_arrays/test.ts b/test/e2e/typescript/2020-12/tuples_and_arrays/test.ts new file mode 100644 index 0000000..5969279 --- /dev/null +++ b/test/e2e/typescript/2020-12/tuples_and_arrays/test.ts @@ -0,0 +1,158 @@ +import { + DataPipeline, + DataPipeline_Properties_Pipeline, + DataPipeline_Properties_Stages_Items +} from "./expected"; + + +// Valid: minimal required fields +const minimal: DataPipeline = { + pipeline: { + id: "pipeline-1", + version: [ 1, 0, 0 ], + coordinates: [ 0, 0, 0 ] + }, + stages: [], + connections: [] +}; + +// Valid: version tuple +const validVersion: DataPipeline = { + pipeline: { + id: "p1", + version: [ 1, 2, 3 ], + coordinates: [ 0, 0, 0 ] + }, + stages: [], + connections: [] +}; + +// Valid: coordinates can have more than 3 numbers (items: { type: number }) +const validCoordinatesExtended: DataPipeline = { + pipeline: { + id: "p1", + version: [ 1, 0, 0 ], + coordinates: [ 1.5, 2.5, 3.5, 4.5, 5.5 ] + }, + stages: [], + connections: [] +}; + +// Valid: stage with inputTypes tuple +const validStage: DataPipeline_Properties_Stages_Items = { + name: "transform", + inputTypes: [ "string", "required" ], + outputType: [ "output", "sync" ] +}; + +// Valid: stage inputTypes with additional items +const validStageExtended: DataPipeline_Properties_Stages_Items = { + name: "transform", + inputTypes: [ "number", "optional", { typeName: "custom" } ], + outputType: [ "result", "async" ] +}; + +// These would be valid in the generated TS but invalid per intended schema semantics: +const versionAsNumber: DataPipeline = { + pipeline: { + id: "p1", + version: 100, // Allowed because schema lacks type: "array" + coordinates: [ 0, 0, 0 ] + }, + stages: [], + connections: [] +}; + +const versionAsString: DataPipeline = { + pipeline: { + id: "p1", + version: "1.0.0", // Allowed because schema lacks type: "array" + coordinates: [ 0, 0, 0 ] + }, + stages: [], + connections: [] +}; + +// Invalid: missing required pipeline +// @ts-expect-error +const missingPipeline: DataPipeline = { + stages: [], + connections: [] +}; + +// Invalid: missing required id in pipeline +const missingPipelineId: DataPipeline = { + // @ts-expect-error + pipeline: { + version: [ 1, 0, 0 ], + coordinates: [ 0, 0, 0 ] + }, + stages: [], + connections: [] +}; + +// Invalid: extra property on pipeline (additionalProperties: false) +const invalidPipelineExtra: DataPipeline = { + pipeline: { + id: "p1", + version: [ 1, 0, 0 ], + coordinates: [ 0, 0, 0 ], + // @ts-expect-error + extra: "not allowed" + }, + stages: [], + connections: [] +}; + +// Invalid: stage missing required name +// @ts-expect-error +const invalidStageMissingName: DataPipeline_Properties_Stages_Items = { + inputTypes: [ "string", "required" ], + outputType: [ "out", "sync" ] +}; + +// Invalid: extra property on stage (additionalProperties: false) +const invalidStageExtra: DataPipeline_Properties_Stages_Items = { + name: "test", + inputTypes: [ "string", "required" ], + outputType: [ "out", "sync" ], + // @ts-expect-error + extra: "not allowed" +}; + +// Valid: full example +const fullExample: DataPipeline = { + pipeline: { + id: "data-etl", + version: [ 2, 1, 0 ], + coordinates: [ 10.5, 20.5, 30.5 ], + tags: [ "production", "etl" ] + }, + stages: [ + { + name: "extract", + inputTypes: [ "object", null ], + outputType: [ "raw-data", "async" ], + config: { timeout: 5000, retries: 3 } + }, + { + name: "transform", + inputTypes: [ "string", "required", { typeName: "custom", nullable: false } ], + outputType: [ "transformed", "sync" ], + metrics: [ 1.0, 2.0, 3.0 ] + } + ], + connections: [ + [ "extract", "transform", { weight: 1.0 } ], + [ "transform", "load", { weight: 0.5, bidirectional: false } ] + ], + metadata: { + createdAt: "2024-01-01", + modifiedAt: "2024-06-01", + authors: [ + [ "John", "Doe" ], + [ "Jane", "Smith", "PhD" ] + ], + flags: [ true, false, true ] + } +}; diff --git a/test/e2e/typescript/2020-12/vocabulary_ignored/test.ts b/test/e2e/typescript/2020-12/vocabulary_ignored/test.ts new file mode 100644 index 0000000..9758221 --- /dev/null +++ b/test/e2e/typescript/2020-12/vocabulary_ignored/test.ts @@ -0,0 +1,65 @@ +import { VocabTest } from "./expected"; + +// additionalProperties: false + +// Valid: minimal required fields +const minimal: VocabTest = { + name: "test", + value: 42 +}; + +// Valid: all fields +const complete: VocabTest = { + name: "test", + value: 42, + optional: true +}; + +// Valid: optional as false +const optionalFalse: VocabTest = { + name: "test", + value: 42, + optional: false +}; + +// Invalid: missing required name +// @ts-expect-error +const missingName: VocabTest = { + value: 42 +}; + +// Invalid: missing required value +// @ts-expect-error +const missingValue: VocabTest = { + name: "test" +}; + +// Invalid: name must be string +const invalidName: VocabTest = { + // @ts-expect-error + name: 123, + value: 42 +}; + +// Invalid: value must be number +const invalidValue: VocabTest = { + name: "test", + // @ts-expect-error + value: "42" +}; + +// Invalid: optional must be boolean +const invalidOptional: VocabTest = { + name: "test", + value: 42, + // @ts-expect-error + optional: "yes" +}; + +// Invalid: extra property (additionalProperties: false) +const invalidExtra: VocabTest = { + name: "test", + value: 42, + // @ts-expect-error + extra: "not allowed" +}; diff --git a/test/e2e/typescript/CMakeLists.txt b/test/e2e/typescript/CMakeLists.txt index 4c819bd..1b433a4 100644 --- a/test/e2e/typescript/CMakeLists.txt +++ b/test/e2e/typescript/CMakeLists.txt @@ -25,14 +25,10 @@ foreach(DIALECT_DIRECTORY ${TYPESCRIPT_DIALECT_DIRECTORIES}) if(IS_DIRECTORY ${CASE_DIRECTORY}) get_filename_component(CASE_NAME ${CASE_DIRECTORY} NAME) list(APPEND E2E_SCHEMAS "${CASE_DIRECTORY}/schema.json") - add_test(NAME "e2e.typescript.tsc.${DIALECT_NAME}.${CASE_NAME}/expected.d.ts" - COMMAND "${TSC_BIN}" --noEmit "${CASE_DIRECTORY}/expected.d.ts") - - # TODO: Make this required for every case - if(EXISTS "${CASE_DIRECTORY}/test.ts") - add_test(NAME "e2e.typescript.tsc.${DIALECT_NAME}.${CASE_NAME}/test.ts" - COMMAND "${TSC_BIN}" --noEmit "${CASE_DIRECTORY}/test.ts") - endif() + add_test(NAME "e2e.typescript.tsc.${DIALECT_NAME}.${CASE_NAME}" + COMMAND "${TSC_BIN}" --strict --noEmit + "${CASE_DIRECTORY}/expected.d.ts" + "${CASE_DIRECTORY}/test.ts") endif() endforeach() endif() diff --git a/test/generator/generator_typescript_test.cc b/test/generator/generator_typescript_test.cc index 6409da5..89ad0ae 100644 --- a/test/generator/generator_typescript_test.cc +++ b/test/generator/generator_typescript_test.cc @@ -800,6 +800,9 @@ export interface Person { "name": Person_Properties_Name; "age"?: Person_Properties_Age; [key: string]: + // As a notable limitation, TypeScript requires index signatures + // to also include the types of all of its properties, so we must + // match a superset of what JSON Schema allows Person_Properties_Name | Person_Properties_Age | Person_AdditionalProperties | @@ -842,6 +845,9 @@ export type Item_AdditionalProperties = string; export interface Item { "id": Item_Properties_Id; [key: string]: + // As a notable limitation, TypeScript requires index signatures + // to also include the types of all of its properties, so we must + // match a superset of what JSON Schema allows Item_Properties_Id | Item_AdditionalProperties | undefined; @@ -901,6 +907,9 @@ export type MyObject_AdditionalProperties = never; export interface MyObject { "foo"?: MyObject_Properties_Foo; [key: string]: + // As a notable limitation, TypeScript requires index signatures + // to also include the types of all of its properties, so we must + // match a superset of what JSON Schema allows MyObject_Properties_Foo | MyObject_AdditionalProperties | undefined; @@ -989,6 +998,9 @@ export type Test_AdditionalProperties = export interface Test { "name"?: Test_Properties_Name; [key: string]: + // As a notable limitation, TypeScript requires index signatures + // to also include the types of all of its properties, so we must + // match a superset of what JSON Schema allows Test_Properties_Name | Test_AdditionalProperties | undefined; diff --git a/test/ir/ir_2020_12_test.cc b/test/ir/ir_2020_12_test.cc index 2cc0fe7..3e19e03 100644 --- a/test/ir/ir_2020_12_test.cc +++ b/test/ir/ir_2020_12_test.cc @@ -53,7 +53,9 @@ TEST(IR_2020_12, test_2) { EXPECT_AS_STRING( std::get(result.at(1)).members.at(0).second.pointer, "/properties/foo"); - EXPECT_FALSE(std::get(result.at(1)).additional.has_value()); + EXPECT_TRUE(std::holds_alternative( + std::get(result.at(1)).additional)); + EXPECT_TRUE(std::get(std::get(result.at(1)).additional)); } TEST(IR_2020_12, test_3) { @@ -315,7 +317,9 @@ TEST(IR_2020_12, object_type_only) { EXPECT_TRUE(std::holds_alternative(result.at(0))); EXPECT_AS_STRING(std::get(result.at(0)).pointer, ""); EXPECT_TRUE(std::get(result.at(0)).members.empty()); - EXPECT_FALSE(std::get(result.at(0)).additional.has_value()); + EXPECT_TRUE(std::holds_alternative( + std::get(result.at(0)).additional)); + EXPECT_TRUE(std::get(std::get(result.at(0)).additional)); } TEST(IR_2020_12, object_empty_properties) { @@ -337,7 +341,9 @@ TEST(IR_2020_12, object_empty_properties) { EXPECT_TRUE(std::holds_alternative(result.at(0))); EXPECT_AS_STRING(std::get(result.at(0)).pointer, ""); EXPECT_TRUE(std::get(result.at(0)).members.empty()); - EXPECT_FALSE(std::get(result.at(0)).additional.has_value()); + EXPECT_TRUE(std::holds_alternative( + std::get(result.at(0)).additional)); + EXPECT_TRUE(std::get(std::get(result.at(0)).additional)); } TEST(IR_2020_12, object_with_additional_properties) { @@ -372,9 +378,11 @@ TEST(IR_2020_12, object_with_additional_properties) { std::get(result.at(2)).members.at(0).second.pointer, "/properties/foo"); - EXPECT_TRUE(std::get(result.at(2)).additional.has_value()); - EXPECT_AS_STRING(std::get(result.at(2)).additional->pointer, - "/additionalProperties"); + EXPECT_TRUE(std::holds_alternative( + std::get(result.at(2)).additional)); + EXPECT_AS_STRING( + std::get(std::get(result.at(2)).additional).pointer, + "/additionalProperties"); } TEST(IR_2020_12, object_with_impossible_property) { @@ -406,7 +414,9 @@ TEST(IR_2020_12, object_with_impossible_property) { EXPECT_AS_STRING( std::get(result.at(1)).members.at(0).second.pointer, "/properties/foo"); - EXPECT_FALSE(std::get(result.at(1)).additional.has_value()); + EXPECT_TRUE(std::holds_alternative( + std::get(result.at(1)).additional)); + EXPECT_TRUE(std::get(std::get(result.at(1)).additional)); } TEST(IR_2020_12, object_with_impossible_additional_properties) { @@ -442,7 +452,9 @@ TEST(IR_2020_12, object_with_impossible_additional_properties) { std::get(result.at(2)).members.at(0).second.pointer, "/properties/foo"); - EXPECT_FALSE(std::get(result.at(2)).additional.has_value()); + EXPECT_TRUE(std::holds_alternative( + std::get(result.at(2)).additional)); + EXPECT_FALSE(std::get(std::get(result.at(2)).additional)); } TEST(IR_2020_12, array_with_items) { @@ -652,7 +664,9 @@ TEST(IR_2020_12, ref_recursive_to_root) { EXPECT_AS_STRING( std::get(result.at(1)).members.at(0).second.pointer, "/properties/child"); - EXPECT_FALSE(std::get(result.at(1)).additional.has_value()); + EXPECT_TRUE(std::holds_alternative( + std::get(result.at(1)).additional)); + EXPECT_TRUE(std::get(std::get(result.at(1)).additional)); } TEST(IR_2020_12, nested_object_with_required_property) { @@ -691,7 +705,9 @@ TEST(IR_2020_12, nested_object_with_required_property) { EXPECT_AS_STRING( std::get(result.at(1)).members.at(0).second.pointer, "/properties/nested/properties/name"); - EXPECT_FALSE(std::get(result.at(1)).additional.has_value()); + EXPECT_TRUE(std::holds_alternative( + std::get(result.at(1)).additional)); + EXPECT_TRUE(std::get(std::get(result.at(1)).additional)); EXPECT_TRUE(std::holds_alternative(result.at(2))); EXPECT_AS_STRING(std::get(result.at(2)).pointer, ""); @@ -702,7 +718,9 @@ TEST(IR_2020_12, nested_object_with_required_property) { EXPECT_AS_STRING( std::get(result.at(2)).members.at(0).second.pointer, "/properties/nested"); - EXPECT_FALSE(std::get(result.at(2)).additional.has_value()); + EXPECT_TRUE(std::holds_alternative( + std::get(result.at(2)).additional)); + EXPECT_TRUE(std::get(std::get(result.at(2)).additional)); } TEST(IR_2020_12, array_without_items) { @@ -758,7 +776,9 @@ TEST(IR_2020_12, object_with_additional_properties_true) { EXPECT_AS_STRING(std::get(result.at(4)).pointer, "/additionalProperties/anyOf/2"); EXPECT_EQ(std::get(result.at(4)).members.size(), 0); - EXPECT_FALSE(std::get(result.at(4)).additional.has_value()); + EXPECT_TRUE(std::holds_alternative( + std::get(result.at(4)).additional)); + EXPECT_TRUE(std::get(std::get(result.at(4)).additional)); EXPECT_IR_SCALAR(result, 5, Boolean, "/additionalProperties/anyOf/1"); EXPECT_IR_SCALAR(result, 6, Null, "/additionalProperties/anyOf/0"); @@ -789,9 +809,11 @@ TEST(IR_2020_12, object_with_additional_properties_true) { EXPECT_AS_STRING( std::get(result.at(8)).members.at(0).second.pointer, "/properties/name"); - EXPECT_TRUE(std::get(result.at(8)).additional.has_value()); - EXPECT_AS_STRING(std::get(result.at(8)).additional->pointer, - "/additionalProperties"); + EXPECT_TRUE(std::holds_alternative( + std::get(result.at(8)).additional)); + EXPECT_AS_STRING( + std::get(std::get(result.at(8)).additional).pointer, + "/additionalProperties"); } TEST(IR_2020_12, object_only_additional_properties) { @@ -815,9 +837,11 @@ TEST(IR_2020_12, object_only_additional_properties) { EXPECT_TRUE(std::holds_alternative(result.at(1))); EXPECT_AS_STRING(std::get(result.at(1)).pointer, ""); EXPECT_EQ(std::get(result.at(1)).members.size(), 0); - EXPECT_TRUE(std::get(result.at(1)).additional.has_value()); - EXPECT_AS_STRING(std::get(result.at(1)).additional->pointer, - "/additionalProperties"); + EXPECT_TRUE(std::holds_alternative( + std::get(result.at(1)).additional)); + EXPECT_AS_STRING( + std::get(std::get(result.at(1)).additional).pointer, + "/additionalProperties"); } TEST(IR_2020_12, embedded_resource_with_nested_id_no_duplicates) { @@ -868,7 +892,9 @@ TEST(IR_2020_12, embedded_resource_with_nested_id_no_duplicates) { EXPECT_AS_STRING( std::get(result.at(4)).members.at(0).second.pointer, "/$defs/Item/properties/name"); - EXPECT_FALSE(std::get(result.at(4)).additional.has_value()); + EXPECT_TRUE(std::holds_alternative( + std::get(result.at(4)).additional)); + EXPECT_FALSE(std::get(std::get(result.at(4)).additional)); EXPECT_TRUE(std::holds_alternative(result.at(5))); EXPECT_AS_STRING(std::get(result.at(5)).pointer, ""); @@ -878,5 +904,7 @@ TEST(IR_2020_12, embedded_resource_with_nested_id_no_duplicates) { EXPECT_AS_STRING( std::get(result.at(5)).members.at(0).second.pointer, "/properties/item"); - EXPECT_FALSE(std::get(result.at(5)).additional.has_value()); + EXPECT_TRUE(std::holds_alternative( + std::get(result.at(5)).additional)); + EXPECT_FALSE(std::get(std::get(result.at(5)).additional)); }