From fd8ff0fc693d328440d6f9c91a0f7c7c4147e4db Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Tue, 21 Apr 2026 10:32:22 -0400 Subject: [PATCH 1/5] Merge the Codegen project into Blaze Mostly out of convenience. So we don't have to be switching between projects and upgrading them all the time. Lots of time wasted due to that. Signed-off-by: Juan Cruz Viotti --- .gitattributes | 5 + .github/workflows/ci.yml | 5 + .github/workflows/website-build.yml | 1 + .github/workflows/website-deploy.yml | 1 + CMakeLists.txt | 9 + Makefile | 9 +- cmake/FindCore.cmake | 1 - config.cmake.in | 7 + contrib/CMakeLists.txt | 9 + contrib/typescript.cc | 74 + package-lock.json | 55 + package.json | 9 + src/codegen/CMakeLists.txt | 20 + src/codegen/codegen.cc | 137 ++ src/codegen/codegen_default_compiler.h | 766 ++++++++ src/codegen/codegen_mangle.cc | 90 + src/codegen/codegen_symbol.cc | 119 ++ src/codegen/codegen_typescript.cc | 328 ++++ .../include/sourcemeta/blaze/codegen.h | 176 ++ .../include/sourcemeta/blaze/codegen_error.h | 178 ++ .../sourcemeta/blaze/codegen_typescript.h | 63 + test/codegen/CMakeLists.txt | 18 + test/codegen/codegen_2020_12_test.cc | 1626 +++++++++++++++++ test/codegen/codegen_symbol_test.cc | 121 ++ test/codegen/codegen_test.cc | 56 + test/codegen/codegen_test_utils.h | 98 + .../additional_properties_false/expected.d.ts | 10 + .../additional_properties_false/options.json | 3 + .../additional_properties_false/schema.json | 14 + .../additional_properties_false/test.ts | 73 + .../additional_properties_true/expected.d.ts | 11 + .../additional_properties_true/options.json | 3 + .../additional_properties_true/schema.json | 14 + .../additional_properties_true/test.ts | 69 + .../2020-12/all_type_values/expected.d.ts | 60 + .../2020-12/all_type_values/options.json | 3 + .../2020-12/all_type_values/schema.json | 65 + .../2020-12/all_type_values/test.ts | 293 +++ .../2020-12/allof_intersection/expected.d.ts | 19 + .../2020-12/allof_intersection/options.json | 3 + .../2020-12/allof_intersection/schema.json | 21 + .../2020-12/allof_intersection/test.ts | 19 + .../2020-12/allof_refs/expected.d.ts | 23 + .../2020-12/allof_refs/options.json | 3 + .../typescript/2020-12/allof_refs/schema.json | 25 + .../e2e/typescript/2020-12/allof_refs/test.ts | 19 + .../allof_single_element/expected.d.ts | 7 + .../2020-12/allof_single_element/options.json | 3 + .../2020-12/allof_single_element/schema.json | 13 + .../2020-12/allof_single_element/test.ts | 10 + .../2020-12/anchor_refs/expected.d.ts | 30 + .../2020-12/anchor_refs/options.json | 3 + .../2020-12/anchor_refs/schema.json | 25 + .../typescript/2020-12/anchor_refs/test.ts | 106 ++ .../2020-12/bundled_schema/expected.d.ts | 37 + .../2020-12/bundled_schema/options.json | 3 + .../2020-12/bundled_schema/schema.json | 42 + .../typescript/2020-12/bundled_schema/test.ts | 102 ++ .../bundled_schema_file_uris/expected.d.ts | 32 + .../bundled_schema_file_uris/options.json | 3 + .../bundled_schema_file_uris/schema.json | 34 + .../2020-12/bundled_schema_file_uris/test.ts | 80 + .../complex_nested_object/expected.d.ts | 245 +++ .../complex_nested_object/options.json | 3 + .../2020-12/complex_nested_object/schema.json | 246 +++ .../2020-12/complex_nested_object/test.ts | 330 ++++ .../2020-12/const_keyword/expected.d.ts | 34 + .../2020-12/const_keyword/options.json | 3 + .../2020-12/const_keyword/schema.json | 39 + .../typescript/2020-12/const_keyword/test.ts | 173 ++ .../expected.d.ts | 22 + .../options.json | 3 + .../schema.json | 13 + .../test.ts | 32 + .../2020-12/deeply_nested_refs/expected.d.ts | 230 +++ .../2020-12/deeply_nested_refs/options.json | 3 + .../2020-12/deeply_nested_refs/schema.json | 239 +++ .../2020-12/deeply_nested_refs/test.ts | 225 +++ .../2020-12/defs_and_refs/expected.d.ts | 157 ++ .../2020-12/defs_and_refs/options.json | 3 + .../2020-12/defs_and_refs/schema.json | 126 ++ .../typescript/2020-12/defs_and_refs/test.ts | 222 +++ .../dynamic_anchor_passthrough/expected.d.ts | 10 + .../dynamic_anchor_passthrough/options.json | 3 + .../dynamic_anchor_passthrough/schema.json | 11 + .../dynamic_anchor_passthrough/test.ts | 18 + .../dynamic_ref_generic_list/expected.d.ts | 33 + .../dynamic_ref_generic_list/options.json | 3 + .../dynamic_ref_generic_list/schema.json | 22 + .../2020-12/dynamic_ref_generic_list/test.ts | 23 + .../expected.d.ts | 13 + .../dynamic_ref_multiple_anchors/options.json | 3 + .../dynamic_ref_multiple_anchors/schema.json | 22 + .../dynamic_ref_multiple_anchors/test.ts | 29 + .../dynamic_ref_single_anchor/expected.d.ts | 5 + .../dynamic_ref_single_anchor/options.json | 3 + .../dynamic_ref_single_anchor/schema.json | 11 + .../2020-12/dynamic_ref_single_anchor/test.ts | 11 + .../2020-12/embedded_resources/expected.d.ts | 15 + .../2020-12/embedded_resources/options.json | 3 + .../2020-12/embedded_resources/schema.json | 21 + .../2020-12/embedded_resources/test.ts | 46 + .../2020-12/enum_complex_types/expected.d.ts | 28 + .../2020-12/enum_complex_types/options.json | 3 + .../2020-12/enum_complex_types/schema.json | 27 + .../2020-12/enum_complex_types/test.ts | 137 ++ .../enum_with_complex_values/expected.d.ts | 19 + .../enum_with_complex_values/options.json | 3 + .../enum_with_complex_values/schema.json | 19 + .../2020-12/enum_with_complex_values/test.ts | 59 + .../if_then_else_objects/expected.d.ts | 25 + .../2020-12/if_then_else_objects/options.json | 3 + .../2020-12/if_then_else_objects/schema.json | 18 + .../2020-12/if_then_else_objects/test.ts | 31 + .../expected.d.ts | 16 + .../if_then_else_validation_only/options.json | 3 + .../if_then_else_validation_only/schema.json | 7 + .../if_then_else_validation_only/test.ts | 14 + .../expected.d.ts | 235 +++ .../options.json | 3 + .../schema.json | 181 ++ .../implicit_types_and_reused_refs/test.ts | 255 +++ .../expected.d.ts | 18 + .../options.json | 3 + .../schema.json | 16 + .../object_with_additional_properties/test.ts | 33 + .../expected.d.ts | 6 + .../options.json | 3 + .../schema.json | 9 + .../test.ts | 25 + .../2020-12/oneof_union/expected.d.ts | 25 + .../2020-12/oneof_union/options.json | 3 + .../2020-12/oneof_union/schema.json | 21 + .../typescript/2020-12/oneof_union/test.ts | 53 + .../expected.d.ts | 10 + .../options.json | 3 + .../schema.json | 11 + .../test.ts | 30 + .../expected.d.ts | 11 + .../options.json | 3 + .../schema.json | 11 + .../test.ts | 13 + .../expected.d.ts | 16 + .../options.json | 3 + .../schema.json | 11 + .../pattern_properties_empty_pattern/test.ts | 20 + .../expected.d.ts | 25 + .../options.json | 3 + .../schema.json | 14 + .../pattern_properties_full_hybrid/test.ts | 59 + .../expected.d.ts | 8 + .../options.json | 1 + .../schema.json | 8 + .../test.ts | 28 + .../expected.d.ts | 9 + .../options.json | 3 + .../schema.json | 8 + .../test.ts | 24 + .../expected.d.ts | 16 + .../options.json | 1 + .../schema.json | 11 + .../test.ts | 30 + .../expected.d.ts | 8 + .../options.json | 1 + .../schema.json | 10 + .../test.ts | 18 + .../expected.d.ts | 5 + .../options.json | 1 + .../schema.json | 7 + .../test.ts | 13 + .../expected.d.ts | 10 + .../options.json | 3 + .../schema.json | 9 + .../test.ts | 18 + .../expected.d.ts | 10 + .../options.json | 3 + .../schema.json | 9 + .../test.ts | 29 + .../expected.d.ts | 16 + .../options.json | 3 + .../schema.json | 14 + .../test.ts | 53 + .../expected.d.ts | 6 + .../options.json | 3 + .../schema.json | 7 + .../pattern_properties_prefix_only/test.ts | 20 + .../expected.d.ts | 18 + .../options.json | 3 + .../schema.json | 11 + .../test.ts | 30 + .../expected.d.ts | 9 + .../options.json | 3 + .../schema.json | 11 + .../test.ts | 27 + .../2020-12/recursive_schema/expected.d.ts | 15 + .../2020-12/recursive_schema/options.json | 3 + .../2020-12/recursive_schema/schema.json | 14 + .../2020-12/recursive_schema/test.ts | 80 + .../special_property_names/expected.d.ts | 28 + .../special_property_names/options.json | 3 + .../special_property_names/schema.json | 15 + .../2020-12/special_property_names/test.ts | 35 + .../2020-12/tuples_and_arrays/expected.d.ts | 319 ++++ .../2020-12/tuples_and_arrays/options.json | 3 + .../2020-12/tuples_and_arrays/schema.json | 145 ++ .../2020-12/tuples_and_arrays/test.ts | 158 ++ .../2020-12/vocabulary_ignored/expected.d.ts | 13 + .../2020-12/vocabulary_ignored/options.json | 3 + .../2020-12/vocabulary_ignored/schema.json | 23 + .../2020-12/vocabulary_ignored/test.ts | 65 + test/codegen/e2e/typescript/CMakeLists.txt | 34 + test/codegen/e2e/typescript/e2e.cc | 81 + test/packaging/find_package/CMakeLists.txt | 1 + test/packaging/find_package/hello.cc | 1 + 214 files changed, 11030 insertions(+), 2 deletions(-) create mode 100644 contrib/typescript.cc create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/codegen/CMakeLists.txt create mode 100644 src/codegen/codegen.cc create mode 100644 src/codegen/codegen_default_compiler.h create mode 100644 src/codegen/codegen_mangle.cc create mode 100644 src/codegen/codegen_symbol.cc create mode 100644 src/codegen/codegen_typescript.cc create mode 100644 src/codegen/include/sourcemeta/blaze/codegen.h create mode 100644 src/codegen/include/sourcemeta/blaze/codegen_error.h create mode 100644 src/codegen/include/sourcemeta/blaze/codegen_typescript.h create mode 100644 test/codegen/CMakeLists.txt create mode 100644 test/codegen/codegen_2020_12_test.cc create mode 100644 test/codegen/codegen_symbol_test.cc create mode 100644 test/codegen/codegen_test.cc create mode 100644 test/codegen/codegen_test_utils.h create mode 100644 test/codegen/e2e/typescript/2020-12/additional_properties_false/expected.d.ts create mode 100644 test/codegen/e2e/typescript/2020-12/additional_properties_false/options.json create mode 100644 test/codegen/e2e/typescript/2020-12/additional_properties_false/schema.json create mode 100644 test/codegen/e2e/typescript/2020-12/additional_properties_false/test.ts create mode 100644 test/codegen/e2e/typescript/2020-12/additional_properties_true/expected.d.ts create mode 100644 test/codegen/e2e/typescript/2020-12/additional_properties_true/options.json create mode 100644 test/codegen/e2e/typescript/2020-12/additional_properties_true/schema.json create mode 100644 test/codegen/e2e/typescript/2020-12/additional_properties_true/test.ts create mode 100644 test/codegen/e2e/typescript/2020-12/all_type_values/expected.d.ts create mode 100644 test/codegen/e2e/typescript/2020-12/all_type_values/options.json create mode 100644 test/codegen/e2e/typescript/2020-12/all_type_values/schema.json create mode 100644 test/codegen/e2e/typescript/2020-12/all_type_values/test.ts create mode 100644 test/codegen/e2e/typescript/2020-12/allof_intersection/expected.d.ts create mode 100644 test/codegen/e2e/typescript/2020-12/allof_intersection/options.json create mode 100644 test/codegen/e2e/typescript/2020-12/allof_intersection/schema.json create mode 100644 test/codegen/e2e/typescript/2020-12/allof_intersection/test.ts create mode 100644 test/codegen/e2e/typescript/2020-12/allof_refs/expected.d.ts create mode 100644 test/codegen/e2e/typescript/2020-12/allof_refs/options.json create mode 100644 test/codegen/e2e/typescript/2020-12/allof_refs/schema.json create mode 100644 test/codegen/e2e/typescript/2020-12/allof_refs/test.ts create mode 100644 test/codegen/e2e/typescript/2020-12/allof_single_element/expected.d.ts create mode 100644 test/codegen/e2e/typescript/2020-12/allof_single_element/options.json create mode 100644 test/codegen/e2e/typescript/2020-12/allof_single_element/schema.json create mode 100644 test/codegen/e2e/typescript/2020-12/allof_single_element/test.ts create mode 100644 test/codegen/e2e/typescript/2020-12/anchor_refs/expected.d.ts create mode 100644 test/codegen/e2e/typescript/2020-12/anchor_refs/options.json create mode 100644 test/codegen/e2e/typescript/2020-12/anchor_refs/schema.json create mode 100644 test/codegen/e2e/typescript/2020-12/anchor_refs/test.ts create mode 100644 test/codegen/e2e/typescript/2020-12/bundled_schema/expected.d.ts create mode 100644 test/codegen/e2e/typescript/2020-12/bundled_schema/options.json create mode 100644 test/codegen/e2e/typescript/2020-12/bundled_schema/schema.json create mode 100644 test/codegen/e2e/typescript/2020-12/bundled_schema/test.ts create mode 100644 test/codegen/e2e/typescript/2020-12/bundled_schema_file_uris/expected.d.ts create mode 100644 test/codegen/e2e/typescript/2020-12/bundled_schema_file_uris/options.json create mode 100644 test/codegen/e2e/typescript/2020-12/bundled_schema_file_uris/schema.json create mode 100644 test/codegen/e2e/typescript/2020-12/bundled_schema_file_uris/test.ts create mode 100644 test/codegen/e2e/typescript/2020-12/complex_nested_object/expected.d.ts create mode 100644 test/codegen/e2e/typescript/2020-12/complex_nested_object/options.json create mode 100644 test/codegen/e2e/typescript/2020-12/complex_nested_object/schema.json create mode 100644 test/codegen/e2e/typescript/2020-12/complex_nested_object/test.ts create mode 100644 test/codegen/e2e/typescript/2020-12/const_keyword/expected.d.ts create mode 100644 test/codegen/e2e/typescript/2020-12/const_keyword/options.json create mode 100644 test/codegen/e2e/typescript/2020-12/const_keyword/schema.json create mode 100644 test/codegen/e2e/typescript/2020-12/const_keyword/test.ts create mode 100644 test/codegen/e2e/typescript/2020-12/control_characters_in_property_names/expected.d.ts create mode 100644 test/codegen/e2e/typescript/2020-12/control_characters_in_property_names/options.json create mode 100644 test/codegen/e2e/typescript/2020-12/control_characters_in_property_names/schema.json create mode 100644 test/codegen/e2e/typescript/2020-12/control_characters_in_property_names/test.ts create mode 100644 test/codegen/e2e/typescript/2020-12/deeply_nested_refs/expected.d.ts create mode 100644 test/codegen/e2e/typescript/2020-12/deeply_nested_refs/options.json create mode 100644 test/codegen/e2e/typescript/2020-12/deeply_nested_refs/schema.json create mode 100644 test/codegen/e2e/typescript/2020-12/deeply_nested_refs/test.ts create mode 100644 test/codegen/e2e/typescript/2020-12/defs_and_refs/expected.d.ts create mode 100644 test/codegen/e2e/typescript/2020-12/defs_and_refs/options.json create mode 100644 test/codegen/e2e/typescript/2020-12/defs_and_refs/schema.json create mode 100644 test/codegen/e2e/typescript/2020-12/defs_and_refs/test.ts create mode 100644 test/codegen/e2e/typescript/2020-12/dynamic_anchor_passthrough/expected.d.ts create mode 100644 test/codegen/e2e/typescript/2020-12/dynamic_anchor_passthrough/options.json create mode 100644 test/codegen/e2e/typescript/2020-12/dynamic_anchor_passthrough/schema.json create mode 100644 test/codegen/e2e/typescript/2020-12/dynamic_anchor_passthrough/test.ts create mode 100644 test/codegen/e2e/typescript/2020-12/dynamic_ref_generic_list/expected.d.ts create mode 100644 test/codegen/e2e/typescript/2020-12/dynamic_ref_generic_list/options.json create mode 100644 test/codegen/e2e/typescript/2020-12/dynamic_ref_generic_list/schema.json create mode 100644 test/codegen/e2e/typescript/2020-12/dynamic_ref_generic_list/test.ts create mode 100644 test/codegen/e2e/typescript/2020-12/dynamic_ref_multiple_anchors/expected.d.ts create mode 100644 test/codegen/e2e/typescript/2020-12/dynamic_ref_multiple_anchors/options.json create mode 100644 test/codegen/e2e/typescript/2020-12/dynamic_ref_multiple_anchors/schema.json create mode 100644 test/codegen/e2e/typescript/2020-12/dynamic_ref_multiple_anchors/test.ts create mode 100644 test/codegen/e2e/typescript/2020-12/dynamic_ref_single_anchor/expected.d.ts create mode 100644 test/codegen/e2e/typescript/2020-12/dynamic_ref_single_anchor/options.json create mode 100644 test/codegen/e2e/typescript/2020-12/dynamic_ref_single_anchor/schema.json create mode 100644 test/codegen/e2e/typescript/2020-12/dynamic_ref_single_anchor/test.ts create mode 100644 test/codegen/e2e/typescript/2020-12/embedded_resources/expected.d.ts create mode 100644 test/codegen/e2e/typescript/2020-12/embedded_resources/options.json create mode 100644 test/codegen/e2e/typescript/2020-12/embedded_resources/schema.json create mode 100644 test/codegen/e2e/typescript/2020-12/embedded_resources/test.ts create mode 100644 test/codegen/e2e/typescript/2020-12/enum_complex_types/expected.d.ts create mode 100644 test/codegen/e2e/typescript/2020-12/enum_complex_types/options.json create mode 100644 test/codegen/e2e/typescript/2020-12/enum_complex_types/schema.json create mode 100644 test/codegen/e2e/typescript/2020-12/enum_complex_types/test.ts create mode 100644 test/codegen/e2e/typescript/2020-12/enum_with_complex_values/expected.d.ts create mode 100644 test/codegen/e2e/typescript/2020-12/enum_with_complex_values/options.json create mode 100644 test/codegen/e2e/typescript/2020-12/enum_with_complex_values/schema.json create mode 100644 test/codegen/e2e/typescript/2020-12/enum_with_complex_values/test.ts create mode 100644 test/codegen/e2e/typescript/2020-12/if_then_else_objects/expected.d.ts create mode 100644 test/codegen/e2e/typescript/2020-12/if_then_else_objects/options.json create mode 100644 test/codegen/e2e/typescript/2020-12/if_then_else_objects/schema.json create mode 100644 test/codegen/e2e/typescript/2020-12/if_then_else_objects/test.ts create mode 100644 test/codegen/e2e/typescript/2020-12/if_then_else_validation_only/expected.d.ts create mode 100644 test/codegen/e2e/typescript/2020-12/if_then_else_validation_only/options.json create mode 100644 test/codegen/e2e/typescript/2020-12/if_then_else_validation_only/schema.json create mode 100644 test/codegen/e2e/typescript/2020-12/if_then_else_validation_only/test.ts create mode 100644 test/codegen/e2e/typescript/2020-12/implicit_types_and_reused_refs/expected.d.ts create mode 100644 test/codegen/e2e/typescript/2020-12/implicit_types_and_reused_refs/options.json create mode 100644 test/codegen/e2e/typescript/2020-12/implicit_types_and_reused_refs/schema.json create mode 100644 test/codegen/e2e/typescript/2020-12/implicit_types_and_reused_refs/test.ts create mode 100644 test/codegen/e2e/typescript/2020-12/object_with_additional_properties/expected.d.ts create mode 100644 test/codegen/e2e/typescript/2020-12/object_with_additional_properties/options.json create mode 100644 test/codegen/e2e/typescript/2020-12/object_with_additional_properties/schema.json create mode 100644 test/codegen/e2e/typescript/2020-12/object_with_additional_properties/test.ts create mode 100644 test/codegen/e2e/typescript/2020-12/object_with_optional_string_property/expected.d.ts create mode 100644 test/codegen/e2e/typescript/2020-12/object_with_optional_string_property/options.json create mode 100644 test/codegen/e2e/typescript/2020-12/object_with_optional_string_property/schema.json create mode 100644 test/codegen/e2e/typescript/2020-12/object_with_optional_string_property/test.ts create mode 100644 test/codegen/e2e/typescript/2020-12/oneof_union/expected.d.ts create mode 100644 test/codegen/e2e/typescript/2020-12/oneof_union/options.json create mode 100644 test/codegen/e2e/typescript/2020-12/oneof_union/schema.json create mode 100644 test/codegen/e2e/typescript/2020-12/oneof_union/test.ts create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_additional_false/expected.d.ts create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_additional_false/options.json create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_additional_false/schema.json create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_additional_false/test.ts create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_additional_true/expected.d.ts create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_additional_true/options.json create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_additional_true/schema.json create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_additional_true/test.ts create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_empty_pattern/expected.d.ts create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_empty_pattern/options.json create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_empty_pattern/schema.json create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_empty_pattern/test.ts create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_full_hybrid/expected.d.ts create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_full_hybrid/options.json create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_full_hybrid/schema.json create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_full_hybrid/test.ts create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_mixed_prefix_and_fallback/expected.d.ts create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_mixed_prefix_and_fallback/options.json create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_mixed_prefix_and_fallback/schema.json create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_mixed_prefix_and_fallback/test.ts create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_multiple_prefixes/expected.d.ts create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_multiple_prefixes/options.json create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_multiple_prefixes/schema.json create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_multiple_prefixes/test.ts create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_non_prefix_additional_false/expected.d.ts create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_non_prefix_additional_false/options.json create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_non_prefix_additional_false/schema.json create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_non_prefix_additional_false/test.ts create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_non_prefix_fallback/expected.d.ts create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_non_prefix_fallback/options.json create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_non_prefix_fallback/schema.json create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_non_prefix_fallback/test.ts create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_non_prefix_only/expected.d.ts create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_non_prefix_only/options.json create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_non_prefix_only/schema.json create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_non_prefix_only/test.ts create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_overlapping_prefixes/expected.d.ts create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_overlapping_prefixes/options.json create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_overlapping_prefixes/schema.json create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_overlapping_prefixes/test.ts create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_overlapping_same_type/expected.d.ts create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_overlapping_same_type/options.json create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_overlapping_same_type/schema.json create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_overlapping_same_type/test.ts create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_partial_overlap/expected.d.ts create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_partial_overlap/options.json create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_partial_overlap/schema.json create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_partial_overlap/test.ts create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_prefix_only/expected.d.ts create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_prefix_only/options.json create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_prefix_only/schema.json create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_prefix_only/test.ts create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_typed_additional/expected.d.ts create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_typed_additional/options.json create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_typed_additional/schema.json create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_typed_additional/test.ts create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_with_properties/expected.d.ts create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_with_properties/options.json create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_with_properties/schema.json create mode 100644 test/codegen/e2e/typescript/2020-12/pattern_properties_with_properties/test.ts create mode 100644 test/codegen/e2e/typescript/2020-12/recursive_schema/expected.d.ts create mode 100644 test/codegen/e2e/typescript/2020-12/recursive_schema/options.json create mode 100644 test/codegen/e2e/typescript/2020-12/recursive_schema/schema.json create mode 100644 test/codegen/e2e/typescript/2020-12/recursive_schema/test.ts create mode 100644 test/codegen/e2e/typescript/2020-12/special_property_names/expected.d.ts create mode 100644 test/codegen/e2e/typescript/2020-12/special_property_names/options.json create mode 100644 test/codegen/e2e/typescript/2020-12/special_property_names/schema.json create mode 100644 test/codegen/e2e/typescript/2020-12/special_property_names/test.ts create mode 100644 test/codegen/e2e/typescript/2020-12/tuples_and_arrays/expected.d.ts create mode 100644 test/codegen/e2e/typescript/2020-12/tuples_and_arrays/options.json create mode 100644 test/codegen/e2e/typescript/2020-12/tuples_and_arrays/schema.json create mode 100644 test/codegen/e2e/typescript/2020-12/tuples_and_arrays/test.ts create mode 100644 test/codegen/e2e/typescript/2020-12/vocabulary_ignored/expected.d.ts create mode 100644 test/codegen/e2e/typescript/2020-12/vocabulary_ignored/options.json create mode 100644 test/codegen/e2e/typescript/2020-12/vocabulary_ignored/schema.json create mode 100644 test/codegen/e2e/typescript/2020-12/vocabulary_ignored/test.ts create mode 100644 test/codegen/e2e/typescript/CMakeLists.txt create mode 100644 test/codegen/e2e/typescript/e2e.cc diff --git a/.gitattributes b/.gitattributes index f461d4695..5cc910b25 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,6 @@ +* text=auto +*.cc text eol=lf +*.h text eol=lf +*.json text eol=lf +*.ts text eol=lf /vendor/** linguist-generated=true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a7717825e..4e46b0470 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -97,6 +97,10 @@ jobs: env: HOMEBREW_NO_ANALYTICS: 1 HOMEBREW_NO_AUTO_UPDATE: 1 + - uses: actions/setup-node@v4 + with: + node-version: 'latest' + - run: npm ci - run: cmake --version - name: Configure Blaze (static) if: matrix.platform.type == 'static' @@ -123,6 +127,7 @@ jobs: -DCMAKE_COMPILE_WARNING_AS_ERROR:BOOL=ON ${{ matrix.platform.options }} - run: cmake --build ./build --config Release --target clang_format_test + - run: cmake --build ./build --config Release --target jsonschema_metaschema - run: cmake --build ./build --config Release --parallel 4 - run: > cmake --install ./build --prefix ./build/dist --config Release --verbose diff --git a/.github/workflows/website-build.yml b/.github/workflows/website-build.yml index 99e7fdd30..077de2623 100644 --- a/.github/workflows/website-build.yml +++ b/.github/workflows/website-build.yml @@ -22,6 +22,7 @@ jobs: -DBLAZE_TEST:BOOL=OFF -DBLAZE_CONFIGURATION:BOOL=OFF -DBLAZE_ALTERSCHEMA:BOOL=OFF + -DBLAZE_CODEGEN:BOOL=OFF -DBLAZE_TESTS:BOOL=OFF -DBLAZE_DOCS:BOOL=ON - run: cmake --build ./build --config Release --target doxygen diff --git a/.github/workflows/website-deploy.yml b/.github/workflows/website-deploy.yml index 180e16d02..668d0b338 100644 --- a/.github/workflows/website-deploy.yml +++ b/.github/workflows/website-deploy.yml @@ -33,6 +33,7 @@ jobs: -DBLAZE_TEST:BOOL=OFF -DBLAZE_CONFIGURATION:BOOL=OFF -DBLAZE_ALTERSCHEMA:BOOL=OFF + -DBLAZE_CODEGEN:BOOL=OFF -DBLAZE_TESTS:BOOL=OFF -DBLAZE_DOCS:BOOL=ON - run: cmake --build ./build --config Release --target doxygen diff --git a/CMakeLists.txt b/CMakeLists.txt index 8a4459fdf..b61b0cabc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,6 +11,7 @@ option(BLAZE_OUTPUT "Build the Blaze output formats library" ON) option(BLAZE_TEST "Build the Blaze test runner library" ON) option(BLAZE_CONFIGURATION "Build the Blaze configuration file library" ON) option(BLAZE_ALTERSCHEMA "Build the Blaze alterschema rule library" ON) +option(BLAZE_CODEGEN "Build the Blaze codegen library" ON) option(BLAZE_TESTS "Build the Blaze tests" OFF) option(BLAZE_BENCHMARK "Build the Blaze benchmarks" OFF) option(BLAZE_CONTRIB "Build the Blaze contrib programs" OFF) @@ -67,6 +68,10 @@ if(BLAZE_ALTERSCHEMA) add_subdirectory(src/alterschema) endif() +if(BLAZE_CODEGEN) + add_subdirectory(src/codegen) +endif() + if(BLAZE_CONTRIB) add_subdirectory(contrib) endif() @@ -134,6 +139,10 @@ if(BLAZE_TESTS) add_subdirectory(test/alterschema) endif() + if(BLAZE_CODEGEN) + add_subdirectory(test/codegen) + endif() + if(PROJECT_IS_TOP_LEVEL) # Otherwise we need the child project to link # against the sanitizers too. diff --git a/Makefile b/Makefile index 58d53dbc8..18ace8ed5 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,7 @@ # Programs CMAKE = cmake CTEST = ctest +NPM = npm # Options PRESET = Debug @@ -8,7 +9,10 @@ SHARED = OFF all: configure compile test -configure: .always +node_modules: package.json package-lock.json + $(NPM) ci + +configure: node_modules .always $(CMAKE) -S . -B ./build \ -DCMAKE_BUILD_TYPE:STRING=$(PRESET) \ -DCMAKE_COMPILE_WARNING_AS_ERROR:BOOL=ON \ @@ -39,6 +43,9 @@ test: .always benchmark: .always $(CMAKE) --build ./build --config $(PRESET) --target benchmark_all +lint: .always + $(CMAKE) --build ./build --config $(PRESET) --target jsonschema_metaschema + doxygen: .always $(CMAKE) --build ./build --config $(PRESET) --target doxygen diff --git a/cmake/FindCore.cmake b/cmake/FindCore.cmake index 3f19abbc5..5c8701948 100644 --- a/cmake/FindCore.cmake +++ b/cmake/FindCore.cmake @@ -5,7 +5,6 @@ if(NOT Core_FOUND) set(SOURCEMETA_CORE_INSTALL OFF CACHE BOOL "disable installation") endif() - set(SOURCEMETA_CORE_EXTENSION_OPTIONS OFF CACHE BOOL "disable") set(SOURCEMETA_CORE_EXTENSION_BUILD OFF CACHE BOOL "disable") set(SOURCEMETA_CORE_CONTRIB_GOOGLETEST ${BLAZE_TESTS} CACHE BOOL "GoogleTest") diff --git a/config.cmake.in b/config.cmake.in index 8bcccb78b..81aee4026 100644 --- a/config.cmake.in +++ b/config.cmake.in @@ -10,6 +10,7 @@ if(NOT BLAZE_COMPONENTS) list(APPEND BLAZE_COMPONENTS test) list(APPEND BLAZE_COMPONENTS configuration) list(APPEND BLAZE_COMPONENTS alterschema) + list(APPEND BLAZE_COMPONENTS codegen) endif() include(CMakeFindDependencyMacro) @@ -35,6 +36,12 @@ foreach(component ${BLAZE_COMPONENTS}) include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_blaze_compiler.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_blaze_output.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_blaze_alterschema.cmake") + elseif(component STREQUAL "codegen") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_blaze_evaluator.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_blaze_compiler.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_blaze_output.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_blaze_alterschema.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_blaze_codegen.cmake") else() message(FATAL_ERROR "Unknown Blaze component: ${component}") endif() diff --git a/contrib/CMakeLists.txt b/contrib/CMakeLists.txt index b807dfbf8..b21f9bd11 100644 --- a/contrib/CMakeLists.txt +++ b/contrib/CMakeLists.txt @@ -60,6 +60,15 @@ if(BLAZE_COMPILER AND BLAZE_EVALUATOR) endif() endif() +if(BLAZE_CODEGEN) + sourcemeta_executable(NAMESPACE sourcemeta PROJECT blaze NAME contrib_typescript + FOLDER "Blaze/Contrib" SOURCES typescript.cc) + target_link_libraries(sourcemeta_blaze_contrib_typescript + PRIVATE sourcemeta::blaze::codegen) + target_link_libraries(sourcemeta_blaze_contrib_typescript + PRIVATE sourcemeta::core::options) +endif() + if(BLAZE_ALTERSCHEMA) sourcemeta_executable(NAMESPACE sourcemeta PROJECT blaze NAME contrib_canonicalize diff --git a/contrib/typescript.cc b/contrib/typescript.cc new file mode 100644 index 000000000..b6c55ee6a --- /dev/null +++ b/contrib/typescript.cc @@ -0,0 +1,74 @@ +#include +#include + +#include +#include +#include + +#include // EXIT_SUCCESS, EXIT_FAILURE +#include // std::filesystem::path +#include // std::cout, std::cerr +#include // std::ostringstream +#include // std::string + +auto main(int argc, char *argv[]) -> int { + sourcemeta::core::Options options; + options.option("default-prefix", {"p"}); + options.parse(argc, argv); + + const auto &positional_arguments{options.positional()}; + if (positional_arguments.empty()) { + std::cerr << "error: missing schema path\n"; + return EXIT_FAILURE; + } + + const std::filesystem::path schema_path{positional_arguments.front()}; + + try { + const auto schema{sourcemeta::core::read_json(schema_path)}; + + const auto result{ + sourcemeta::blaze::compile(schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, + sourcemeta::blaze::default_compiler)}; + + const std::string prefix{ + options.contains("default-prefix") + ? std::string{options.at("default-prefix").front()} + : "Schema"}; + + sourcemeta::blaze::generate(std::cout, + result, prefix); + } catch (const sourcemeta::blaze::UnsupportedKeywordError &error) { + std::ostringstream pointer; + sourcemeta::core::stringify(error.pointer(), pointer); + std::cerr << "error: " << error.what() << "\n"; + std::cerr << " keyword: " << error.keyword() << "\n"; + std::cerr << " location: " << pointer.str() << "\n"; + std::cerr << " schema: "; + sourcemeta::core::prettify(error.json(), std::cerr); + std::cerr << "\n"; + return EXIT_FAILURE; + } catch (const sourcemeta::blaze::UnsupportedKeywordValueError &error) { + std::ostringstream pointer; + sourcemeta::core::stringify(error.pointer(), pointer); + std::cerr << "error: " << error.what() << "\n"; + std::cerr << " keyword: " << error.keyword() << "\n"; + std::cerr << " location: " << pointer.str() << "\n"; + std::cerr << " schema: "; + sourcemeta::core::prettify(error.json(), std::cerr); + std::cerr << "\n"; + return EXIT_FAILURE; + } catch (const sourcemeta::blaze::UnexpectedSchemaError &error) { + std::ostringstream pointer; + sourcemeta::core::stringify(error.pointer(), pointer); + std::cerr << "error: " << error.what() << "\n"; + std::cerr << " location: " << pointer.str() << "\n"; + std::cerr << " schema: "; + sourcemeta::core::prettify(error.json(), std::cerr); + std::cerr << "\n"; + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..761699c81 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,55 @@ +{ + "name": "@sourcemeta/blaze", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@sourcemeta/blaze", + "version": "0.0.1", + "devDependencies": { + "@sourcemeta/jsonschema": "^14.0.3", + "typescript": "^5.9.3" + } + }, + "node_modules/@sourcemeta/jsonschema": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/@sourcemeta/jsonschema/-/jsonschema-14.0.3.tgz", + "integrity": "sha512-5hUX6Avayk9Q0ma5k55CM7uxqVxTQZ7gcYrRXDXLBRz4efzr8cpck4zLn/5P1EupiOz3cgTY//alOI1W0LVM1A==", + "cpu": [ + "x64", + "arm64" + ], + "dev": true, + "license": "AGPL-3.0", + "os": [ + "darwin", + "linux", + "win32" + ], + "bin": { + "jsonschema": "npm/cli.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sourcemeta" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 000000000..e2acefeea --- /dev/null +++ b/package.json @@ -0,0 +1,9 @@ +{ + "name": "@sourcemeta/blaze", + "version": "0.0.1", + "private": true, + "devDependencies": { + "@sourcemeta/jsonschema": "^14.0.3", + "typescript": "^5.9.3" + } +} diff --git a/src/codegen/CMakeLists.txt b/src/codegen/CMakeLists.txt new file mode 100644 index 000000000..099c0cda9 --- /dev/null +++ b/src/codegen/CMakeLists.txt @@ -0,0 +1,20 @@ +sourcemeta_library(NAMESPACE sourcemeta PROJECT blaze NAME codegen + FOLDER "Blaze/Codegen" + PRIVATE_HEADERS error.h typescript.h + SOURCES + codegen.cc + codegen_symbol.cc + codegen_default_compiler.h + codegen_typescript.cc + codegen_mangle.cc) + +if(BLAZE_INSTALL) + sourcemeta_library_install(NAMESPACE sourcemeta PROJECT blaze NAME codegen) +endif() + +target_link_libraries(sourcemeta_blaze_codegen PUBLIC + sourcemeta::core::json) +target_link_libraries(sourcemeta_blaze_codegen PUBLIC + sourcemeta::core::jsonschema) +target_link_libraries(sourcemeta_blaze_codegen PRIVATE + sourcemeta::blaze::alterschema) diff --git a/src/codegen/codegen.cc b/src/codegen/codegen.cc new file mode 100644 index 000000000..c953c788e --- /dev/null +++ b/src/codegen/codegen.cc @@ -0,0 +1,137 @@ +#include +#include + +#include // std::ranges::sort +#include // assert +#include // std::unordered_set + +#include "codegen_default_compiler.h" + +namespace { + +auto is_validation_subschema( + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + const sourcemeta::core::SchemaWalker &walker, + const sourcemeta::core::SchemaResolver &resolver) -> bool { + if (!location.parent.has_value()) { + return false; + } + + const auto &parent{location.parent.value()}; + if (parent.size() >= location.pointer.size()) { + return false; + } + + const auto &keyword_token{location.pointer.at(parent.size())}; + if (!keyword_token.is_property()) { + return false; + } + + const auto parent_location{frame.traverse(parent)}; + if (!parent_location.has_value()) { + return false; + } + + const auto vocabularies{ + frame.vocabularies(parent_location.value().get(), resolver)}; + const auto &walker_result{walker(keyword_token.to_property(), vocabularies)}; + using Type = sourcemeta::core::SchemaKeywordType; + if (walker_result.type == Type::ApplicatorValueTraverseAnyPropertyKey || + walker_result.type == Type::ApplicatorValueTraverseAnyItem) { + return true; + } + + return is_validation_subschema(frame, parent_location.value().get(), walker, + resolver); +} + +} // anonymous namespace + +namespace sourcemeta::blaze { + +auto compile(const sourcemeta::core::JSON &input, + const sourcemeta::core::SchemaWalker &walker, + const sourcemeta::core::SchemaResolver &resolver, + const CodegenCompiler &compiler, + const std::string_view default_dialect, + const std::string_view default_id) -> IRResult { + // -------------------------------------------------------------------------- + // (1) Bundle the schema to resolve external references + // -------------------------------------------------------------------------- + + auto schema{sourcemeta::core::bundle(input, walker, resolver, default_dialect, + default_id)}; + + // -------------------------------------------------------------------------- + // (2) Canonicalize the schema for easier analysis + // -------------------------------------------------------------------------- + + sourcemeta::blaze::SchemaTransformer canonicalizer; + sourcemeta::blaze::add(canonicalizer, + sourcemeta::blaze::AlterSchemaMode::Canonicalizer); + [[maybe_unused]] const auto canonicalized{canonicalizer.apply( + schema, walker, resolver, + [](const auto &, const auto, const auto, const auto &, + [[maybe_unused]] const auto applied) { assert(applied); }, + default_dialect, default_id)}; + assert(canonicalized.first); + + // -------------------------------------------------------------------------- + // (3) Frame the resulting schema with instance location information + // -------------------------------------------------------------------------- + + sourcemeta::core::SchemaFrame frame{ + sourcemeta::core::SchemaFrame::Mode::References}; + frame.analyse(schema, walker, resolver, default_dialect, default_id); + + // -------------------------------------------------------------------------- + // (4) Convert every subschema into a code generation object + // -------------------------------------------------------------------------- + + std::unordered_set + visited; + IRResult result; + for (const auto &[key, location] : frame.locations()) { + if (location.type != + sourcemeta::core::SchemaFrame::LocationType::Resource && + location.type != + sourcemeta::core::SchemaFrame::LocationType::Subschema) { + continue; + } + + // Framing may report resource twice or more given default identifiers and + // nested resources + const auto [visited_iterator, inserted] = visited.insert(location.pointer); + if (!inserted) { + continue; + } + + // Skip subschemas under validation-only keywords that do not contribute + // to the type structure (like `contains`) + if (is_validation_subschema(frame, location, walker, resolver)) { + continue; + } + + const auto &subschema{sourcemeta::core::get(schema, location.pointer)}; + result.push_back(compiler(schema, frame, location, resolver, subschema)); + } + + // -------------------------------------------------------------------------- + // (5) Sort entries so that dependencies come before dependents + // -------------------------------------------------------------------------- + + std::ranges::sort( + result, [](const IREntity &left, const IREntity &right) -> bool { + return std::visit([](const auto &entry) { return entry.pointer; }, + right) < + std::visit([](const auto &entry) { return entry.pointer; }, + left); + }); + + return result; +} + +} // namespace sourcemeta::blaze diff --git a/src/codegen/codegen_default_compiler.h b/src/codegen/codegen_default_compiler.h new file mode 100644 index 000000000..c18aedc9d --- /dev/null +++ b/src/codegen/codegen_default_compiler.h @@ -0,0 +1,766 @@ +#ifndef SOURCEMETA_BLAZE_CODEGEN_DEFAULT_COMPILER_H_ +#define SOURCEMETA_BLAZE_CODEGEN_DEFAULT_COMPILER_H_ + +#include + +#include +#include +#include + +#include // assert +#include // std::string_view +#include // std::unordered_set + +// We do not check vocabularies here because the canonicaliser ensures +// we never get an official keyword when its vocabulary is not present +// NOLINTNEXTLINE(cppcoreguidelines-macro-usage) +#define ONLY_WHITELIST_KEYWORDS(schema, subschema, pointer, ...) \ + { \ + static const std::unordered_set allowed{__VA_ARGS__}; \ + for (const auto &entry : (subschema).as_object()) { \ + if (!allowed.contains(entry.first)) { \ + throw sourcemeta::blaze::UnsupportedKeywordError( \ + (schema), (pointer), entry.first, \ + "Unsupported keyword in subschema"); \ + } \ + } \ + } + +namespace sourcemeta::blaze { + +auto handle_impossible(const sourcemeta::core::JSON &, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + const sourcemeta::core::Vocabularies &, + const sourcemeta::core::SchemaResolver &, + const sourcemeta::core::JSON &) -> IRImpossible { + return IRImpossible{ + {.pointer = sourcemeta::core::to_pointer(location.pointer), + .symbol = symbol(frame, location)}}; +} + +auto handle_any(const sourcemeta::core::JSON &, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + const sourcemeta::core::Vocabularies &, + const sourcemeta::core::SchemaResolver &, + const sourcemeta::core::JSON &) -> IRAny { + return IRAny{{.pointer = sourcemeta::core::to_pointer(location.pointer), + .symbol = symbol(frame, location)}}; +} + +auto handle_string(const sourcemeta::core::JSON &schema, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + const sourcemeta::core::Vocabularies &, + const sourcemeta::core::SchemaResolver &, + const sourcemeta::core::JSON &subschema) -> IRScalar { + ONLY_WHITELIST_KEYWORDS(schema, subschema, location.pointer, + {"$schema", + "$id", + "$anchor", + "$dynamicAnchor", + "$defs", + "$vocabulary", + "type", + "minLength", + "maxLength", + "pattern", + "format", + "title", + "description", + "default", + "deprecated", + "readOnly", + "writeOnly", + "examples", + "contentEncoding", + "contentMediaType", + "contentSchema"}); + return IRScalar{{.pointer = sourcemeta::core::to_pointer(location.pointer), + .symbol = symbol(frame, location)}, + IRScalarType::String}; +} + +auto handle_object(const sourcemeta::core::JSON &schema, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + const sourcemeta::core::Vocabularies &, + const sourcemeta::core::SchemaResolver &, + const sourcemeta::core::JSON &subschema) -> IRObject { + ONLY_WHITELIST_KEYWORDS( + schema, subschema, location.pointer, + {"$defs", "$schema", "$id", "$anchor", "$dynamicAnchor", "$vocabulary", + "type", "properties", "required", + // Note that most programming languages CANNOT represent the idea + // of additional properties, mainly if they differ from the types of the + // other properties. Therefore, we whitelist this, but we consider it to + // be the responsability of the validator + "additionalProperties", "minProperties", "maxProperties", + "propertyNames", "patternProperties", "title", "description", "default", + "deprecated", "readOnly", "writeOnly", "examples"}); + + std::vector> members; + + // Guaranteed by canonicalisation + assert(subschema.defines("properties")); + + const auto &properties{subschema.at("properties")}; + + std::unordered_set required_set; + if (subschema.defines("required")) { + const auto &required{subschema.at("required")}; + for (const auto &item : required.as_array()) { + // Guaranteed by canonicalisation + assert(properties.defines(item.to_string())); + required_set.insert(item.to_string()); + } + } + + for (const auto &entry : properties.as_object()) { + auto property_pointer{sourcemeta::core::to_pointer(location.pointer)}; + property_pointer.push_back("properties"); + property_pointer.push_back(entry.first); + + const auto property_location{ + frame.traverse(sourcemeta::core::to_weak_pointer(property_pointer))}; + assert(property_location.has_value()); + + IRObjectValue member_value{ + {.pointer = std::move(property_pointer), + .symbol = symbol(frame, property_location.value().get())}, + required_set.contains(entry.first), + false}; + + members.emplace_back(entry.first, std::move(member_value)); + } + + std::variant additional{true}; + if (subschema.defines("additionalProperties")) { + const auto &additional_schema{subschema.at("additionalProperties")}; + 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"); + + const auto additional_location{frame.traverse( + sourcemeta::core::to_weak_pointer(additional_pointer))}; + assert(additional_location.has_value()); + + additional = + IRType{.pointer = std::move(additional_pointer), + .symbol = symbol(frame, additional_location.value().get())}; + } + } + + std::vector pattern; + if (subschema.defines("patternProperties")) { + const auto &pattern_props{subschema.at("patternProperties")}; + for (const auto &entry : pattern_props.as_object()) { + auto pattern_pointer{sourcemeta::core::to_pointer(location.pointer)}; + pattern_pointer.push_back("patternProperties"); + pattern_pointer.push_back(entry.first); + + const auto pattern_location{ + frame.traverse(sourcemeta::core::to_weak_pointer(pattern_pointer))}; + assert(pattern_location.has_value()); + + std::optional prefix{std::nullopt}; + const auto regex{sourcemeta::core::to_regex(entry.first)}; + if (regex.has_value() && + std::holds_alternative( + regex.value())) { + prefix = std::get(regex.value()); + } + + pattern.push_back(IRObjectPatternProperty{ + {.pointer = std::move(pattern_pointer), + .symbol = symbol(frame, pattern_location.value().get())}, + std::move(prefix)}); + } + } + + return IRObject{{.pointer = sourcemeta::core::to_pointer(location.pointer), + .symbol = symbol(frame, location)}, + std::move(members), + std::move(additional), + std::move(pattern)}; +} + +auto handle_integer(const sourcemeta::core::JSON &schema, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + const sourcemeta::core::Vocabularies &, + const sourcemeta::core::SchemaResolver &, + const sourcemeta::core::JSON &subschema) -> IRScalar { + ONLY_WHITELIST_KEYWORDS(schema, subschema, location.pointer, + {"$schema", "$id", "$anchor", "$dynamicAnchor", + "$defs", "$vocabulary", "type", "minimum", "maximum", + "exclusiveMinimum", "exclusiveMaximum", "multipleOf", + "title", "description", "default", "deprecated", + "readOnly", "writeOnly", "examples"}); + return IRScalar{{.pointer = sourcemeta::core::to_pointer(location.pointer), + .symbol = symbol(frame, location)}, + IRScalarType::Integer}; +} + +auto handle_number(const sourcemeta::core::JSON &schema, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + const sourcemeta::core::Vocabularies &, + const sourcemeta::core::SchemaResolver &, + const sourcemeta::core::JSON &subschema) -> IRScalar { + ONLY_WHITELIST_KEYWORDS(schema, subschema, location.pointer, + {"$schema", "$id", "$anchor", "$dynamicAnchor", + "$defs", "$vocabulary", "type", "minimum", "maximum", + "exclusiveMinimum", "exclusiveMaximum", "multipleOf", + "title", "description", "default", "deprecated", + "readOnly", "writeOnly", "examples"}); + return IRScalar{{.pointer = sourcemeta::core::to_pointer(location.pointer), + .symbol = symbol(frame, location)}, + IRScalarType::Number}; +} + +auto handle_array(const sourcemeta::core::JSON &schema, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + const sourcemeta::core::Vocabularies &vocabularies, + const sourcemeta::core::SchemaResolver &, + const sourcemeta::core::JSON &subschema) -> IREntity { + ONLY_WHITELIST_KEYWORDS(schema, subschema, location.pointer, + {"$schema", "$id", "$anchor", + "$dynamicAnchor", "$defs", "$vocabulary", + "type", "items", "minItems", + "maxItems", "uniqueItems", "contains", + "minContains", "maxContains", "additionalItems", + "prefixItems", "title", "description", + "default", "deprecated", "readOnly", + "writeOnly", "examples"}); + + using Vocabularies = sourcemeta::core::Vocabularies; + + if (vocabularies.contains( + Vocabularies::Known::JSON_Schema_2020_12_Applicator) && + subschema.defines("prefixItems")) { + const auto &prefix_items{subschema.at("prefixItems")}; + assert(prefix_items.is_array()); + + std::vector tuple_items; + for (std::size_t index = 0; index < prefix_items.size(); ++index) { + auto item_pointer{sourcemeta::core::to_pointer(location.pointer)}; + item_pointer.push_back("prefixItems"); + item_pointer.push_back(index); + + const auto item_location{ + frame.traverse(sourcemeta::core::to_weak_pointer(item_pointer))}; + assert(item_location.has_value()); + + tuple_items.push_back( + {.pointer = std::move(item_pointer), + .symbol = symbol(frame, item_location.value().get())}); + } + + std::optional additional{std::nullopt}; + if (subschema.defines("items")) { + auto additional_pointer{sourcemeta::core::to_pointer(location.pointer)}; + additional_pointer.push_back("items"); + + const auto additional_location{frame.traverse( + sourcemeta::core::to_weak_pointer(additional_pointer))}; + assert(additional_location.has_value()); + + additional = + IRType{.pointer = std::move(additional_pointer), + .symbol = symbol(frame, additional_location.value().get())}; + } + + return IRTuple{{.pointer = sourcemeta::core::to_pointer(location.pointer), + .symbol = symbol(frame, location)}, + std::move(tuple_items), + std::move(additional)}; + } + + if (vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2019_09_Applicator, + Vocabularies::Known::JSON_Schema_Draft_7, + Vocabularies::Known::JSON_Schema_Draft_6, + Vocabularies::Known::JSON_Schema_Draft_4, + Vocabularies::Known::JSON_Schema_Draft_3}) && + subschema.defines("items") && subschema.at("items").is_array()) { + const auto &items_array{subschema.at("items")}; + + std::vector tuple_items; + for (std::size_t index = 0; index < items_array.size(); ++index) { + auto item_pointer{sourcemeta::core::to_pointer(location.pointer)}; + item_pointer.push_back("items"); + item_pointer.push_back(index); + + const auto item_location{ + frame.traverse(sourcemeta::core::to_weak_pointer(item_pointer))}; + assert(item_location.has_value()); + + tuple_items.push_back( + {.pointer = std::move(item_pointer), + .symbol = symbol(frame, item_location.value().get())}); + } + + std::optional additional{std::nullopt}; + if (subschema.defines("additionalItems")) { + auto additional_pointer{sourcemeta::core::to_pointer(location.pointer)}; + additional_pointer.push_back("additionalItems"); + + const auto additional_location{frame.traverse( + sourcemeta::core::to_weak_pointer(additional_pointer))}; + assert(additional_location.has_value()); + + additional = + IRType{.pointer = std::move(additional_pointer), + .symbol = symbol(frame, additional_location.value().get())}; + } + + return IRTuple{{.pointer = sourcemeta::core::to_pointer(location.pointer), + .symbol = symbol(frame, location)}, + std::move(tuple_items), + std::move(additional)}; + } + + std::optional items_type{std::nullopt}; + if (subschema.defines("items")) { + auto items_pointer{sourcemeta::core::to_pointer(location.pointer)}; + items_pointer.push_back("items"); + + const auto items_location{ + frame.traverse(sourcemeta::core::to_weak_pointer(items_pointer))}; + assert(items_location.has_value()); + + items_type = IRType{.pointer = std::move(items_pointer), + .symbol = symbol(frame, items_location.value().get())}; + } + + return IRArray{{.pointer = sourcemeta::core::to_pointer(location.pointer), + .symbol = symbol(frame, location)}, + std::move(items_type)}; +} + +auto handle_enum(const sourcemeta::core::JSON &schema, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + const sourcemeta::core::Vocabularies &, + const sourcemeta::core::SchemaResolver &, + const sourcemeta::core::JSON &subschema) -> IREntity { + ONLY_WHITELIST_KEYWORDS(schema, subschema, location.pointer, + {"$schema", "$id", "$anchor", "$dynamicAnchor", + "$defs", "$vocabulary", "enum", "title", + "description", "default", "deprecated", "readOnly", + "writeOnly", "examples"}); + const auto &enum_json{subschema.at("enum")}; + + // Boolean and null special cases + if (enum_json.size() == 1 && enum_json.at(0).is_null()) { + return IRScalar{{.pointer = sourcemeta::core::to_pointer(location.pointer), + .symbol = symbol(frame, location)}, + IRScalarType::Null}; + } else if (enum_json.size() == 2) { + const auto &first{enum_json.at(0)}; + const auto &second{enum_json.at(1)}; + if ((first.is_boolean() && second.is_boolean()) && + (first.to_boolean() != second.to_boolean())) { + return IRScalar{ + {.pointer = sourcemeta::core::to_pointer(location.pointer), + .symbol = symbol(frame, location)}, + IRScalarType::Boolean}; + } + } + + std::vector values{enum_json.as_array().cbegin(), + enum_json.as_array().cend()}; + return IREnumeration{ + {.pointer = sourcemeta::core::to_pointer(location.pointer), + .symbol = symbol(frame, location)}, + std::move(values)}; +} + +auto handle_anyof(const sourcemeta::core::JSON &schema, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + const sourcemeta::core::Vocabularies &, + const sourcemeta::core::SchemaResolver &, + const sourcemeta::core::JSON &subschema) -> IREntity { + ONLY_WHITELIST_KEYWORDS( + schema, subschema, location.pointer, + {"$schema", "$id", "$anchor", "$dynamicAnchor", "$defs", "$vocabulary", + "anyOf", "title", "description", "default", "deprecated", "readOnly", + "writeOnly", "examples", "unevaluatedProperties", "unevaluatedItems"}); + + const auto &any_of{subschema.at("anyOf")}; + assert(any_of.is_array()); + assert(any_of.size() >= 2); + + std::vector branches; + for (std::size_t index = 0; index < any_of.size(); ++index) { + auto branch_pointer{sourcemeta::core::to_pointer(location.pointer)}; + branch_pointer.push_back("anyOf"); + branch_pointer.push_back(index); + + const auto branch_location{ + frame.traverse(sourcemeta::core::to_weak_pointer(branch_pointer))}; + assert(branch_location.has_value()); + + branches.push_back( + {.pointer = std::move(branch_pointer), + .symbol = symbol(frame, branch_location.value().get())}); + } + + return IRUnion{{.pointer = sourcemeta::core::to_pointer(location.pointer), + .symbol = symbol(frame, location)}, + std::move(branches)}; +} + +auto handle_oneof(const sourcemeta::core::JSON &schema, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + const sourcemeta::core::Vocabularies &, + const sourcemeta::core::SchemaResolver &, + const sourcemeta::core::JSON &subschema) -> IREntity { + ONLY_WHITELIST_KEYWORDS( + schema, subschema, location.pointer, + {"$schema", "$id", "$anchor", "$dynamicAnchor", "$defs", "$vocabulary", + "oneOf", "title", "description", "default", "deprecated", "readOnly", + "writeOnly", "examples", "unevaluatedProperties", "unevaluatedItems"}); + + const auto &one_of{subschema.at("oneOf")}; + assert(one_of.is_array()); + assert(one_of.size() >= 2); + + std::vector branches; + for (std::size_t index = 0; index < one_of.size(); ++index) { + auto branch_pointer{sourcemeta::core::to_pointer(location.pointer)}; + branch_pointer.push_back("oneOf"); + branch_pointer.push_back(index); + + const auto branch_location{ + frame.traverse(sourcemeta::core::to_weak_pointer(branch_pointer))}; + assert(branch_location.has_value()); + + branches.push_back( + {.pointer = std::move(branch_pointer), + .symbol = symbol(frame, branch_location.value().get())}); + } + + return IRUnion{{.pointer = sourcemeta::core::to_pointer(location.pointer), + .symbol = symbol(frame, location)}, + std::move(branches)}; +} + +auto handle_ref(const sourcemeta::core::JSON &schema, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + const sourcemeta::core::Vocabularies &, + const sourcemeta::core::SchemaResolver &, + const sourcemeta::core::JSON &subschema) -> IREntity { + ONLY_WHITELIST_KEYWORDS(schema, subschema, location.pointer, + {"$schema", "$id", "$anchor", "$dynamicAnchor", + "$defs", "$vocabulary", "$ref", "title", + "description", "default", "deprecated", "readOnly", + "writeOnly", "examples"}); + + auto ref_pointer{sourcemeta::core::to_pointer(location.pointer)}; + ref_pointer.push_back("$ref"); + const auto ref_weak_pointer{sourcemeta::core::to_weak_pointer(ref_pointer)}; + + const auto &references{frame.references()}; + const auto reference{references.find( + {sourcemeta::core::SchemaReferenceType::Static, ref_weak_pointer})}; + assert(reference != references.cend()); + + const auto &destination{reference->second.destination}; + const auto target{frame.traverse(destination)}; + if (!target.has_value()) { + throw UnexpectedSchemaError(schema, location.pointer, + "Could not resolve reference destination"); + } + + const auto &target_location{target.value().get()}; + + return IRReference{ + {.pointer = sourcemeta::core::to_pointer(location.pointer), + .symbol = symbol(frame, location)}, + {.pointer = sourcemeta::core::to_pointer(target_location.pointer), + .symbol = symbol(frame, target_location)}}; +} + +auto handle_dynamic_ref(const sourcemeta::core::JSON &schema, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + const sourcemeta::core::Vocabularies &, + const sourcemeta::core::SchemaResolver &, + const sourcemeta::core::JSON &subschema) -> IREntity { + ONLY_WHITELIST_KEYWORDS(schema, subschema, location.pointer, + {"$schema", "$id", "$anchor", "$dynamicAnchor", + "$defs", "$vocabulary", "$dynamicRef", "title", + "description", "default", "deprecated", "readOnly", + "writeOnly", "examples"}); + + auto ref_pointer{sourcemeta::core::to_pointer(location.pointer)}; + ref_pointer.push_back("$dynamicRef"); + const auto ref_weak_pointer{sourcemeta::core::to_weak_pointer(ref_pointer)}; + + const auto &references{frame.references()}; + + // Note: The frame internally converts single-target dynamic references to + // static reference + const auto static_reference{references.find( + {sourcemeta::core::SchemaReferenceType::Static, ref_weak_pointer})}; + if (static_reference != references.cend()) { + const auto &destination{static_reference->second.destination}; + const auto target{frame.traverse(destination)}; + if (!target.has_value()) { + throw UnexpectedSchemaError(schema, location.pointer, + "Could not resolve reference destination"); + } + + const auto &target_location{target.value().get()}; + + return IRReference{ + {.pointer = sourcemeta::core::to_pointer(location.pointer), + .symbol = symbol(frame, location)}, + {.pointer = sourcemeta::core::to_pointer(target_location.pointer), + .symbol = symbol(frame, target_location)}}; + } + + // Multi-target dynamic reference: find all dynamic anchors with the matching + // fragment and emit a union of all possible targets + const auto dynamic_reference{references.find( + {sourcemeta::core::SchemaReferenceType::Dynamic, ref_weak_pointer})}; + assert(dynamic_reference != references.cend()); + assert(dynamic_reference->second.fragment.has_value()); + const auto &fragment{dynamic_reference->second.fragment.value()}; + + std::vector branches; + for (const auto &[key, entry] : frame.locations()) { + if (key.first != sourcemeta::core::SchemaReferenceType::Dynamic || + entry.type != sourcemeta::core::SchemaFrame::LocationType::Anchor) { + continue; + } + + const sourcemeta::core::URI anchor_uri{key.second}; + const auto anchor_fragment{anchor_uri.fragment()}; + if (!anchor_fragment.has_value() || anchor_fragment.value() != fragment) { + continue; + } + + branches.push_back({.pointer = sourcemeta::core::to_pointer(entry.pointer), + .symbol = symbol(frame, entry)}); + } + + assert(!branches.empty()); + return IRUnion{{.pointer = sourcemeta::core::to_pointer(location.pointer), + .symbol = symbol(frame, location)}, + std::move(branches)}; +} + +auto handle_allof(const sourcemeta::core::JSON &schema, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + const sourcemeta::core::Vocabularies &, + const sourcemeta::core::SchemaResolver &, + const sourcemeta::core::JSON &subschema) -> IREntity { + ONLY_WHITELIST_KEYWORDS( + schema, subschema, location.pointer, + {"$schema", "$id", "$anchor", "$dynamicAnchor", "$defs", "$vocabulary", + "allOf", "title", "description", "default", "deprecated", "readOnly", + "writeOnly", "examples", "unevaluatedProperties", "unevaluatedItems"}); + + const auto &all_of{subschema.at("allOf")}; + assert(all_of.is_array()); + + if (all_of.size() == 1) { + auto target_pointer{sourcemeta::core::to_pointer(location.pointer)}; + target_pointer.push_back("allOf"); + target_pointer.push_back(static_cast(0)); + + const auto target_location{ + frame.traverse(sourcemeta::core::to_weak_pointer(target_pointer))}; + assert(target_location.has_value()); + + return IRReference{ + {.pointer = sourcemeta::core::to_pointer(location.pointer), + .symbol = symbol(frame, location)}, + {.pointer = std::move(target_pointer), + .symbol = symbol(frame, target_location.value().get())}}; + } + + std::vector branches; + for (std::size_t index = 0; index < all_of.size(); ++index) { + auto branch_pointer{sourcemeta::core::to_pointer(location.pointer)}; + branch_pointer.push_back("allOf"); + branch_pointer.push_back(index); + + const auto branch_location{ + frame.traverse(sourcemeta::core::to_weak_pointer(branch_pointer))}; + assert(branch_location.has_value()); + + branches.push_back( + {.pointer = std::move(branch_pointer), + .symbol = symbol(frame, branch_location.value().get())}); + } + + return IRIntersection{ + {.pointer = sourcemeta::core::to_pointer(location.pointer), + .symbol = symbol(frame, location)}, + std::move(branches)}; +} + +auto handle_if_then_else( + const sourcemeta::core::JSON &schema, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + const sourcemeta::core::Vocabularies &, + const sourcemeta::core::SchemaResolver &, + const sourcemeta::core::JSON &subschema) -> IREntity { + ONLY_WHITELIST_KEYWORDS(schema, subschema, location.pointer, + {"$schema", "$id", "$anchor", "$dynamicAnchor", + "$defs", "$vocabulary", "if", "then", "else", + "title", "description", "default", "deprecated", + "readOnly", "writeOnly", "examples", + "unevaluatedProperties", "unevaluatedItems"}); + + assert(subschema.defines("if")); + assert(subschema.defines("then")); + assert(subschema.defines("else")); + + auto if_pointer{sourcemeta::core::to_pointer(location.pointer)}; + if_pointer.push_back("if"); + const auto if_location{ + frame.traverse(sourcemeta::core::to_weak_pointer(if_pointer))}; + assert(if_location.has_value()); + + auto then_pointer{sourcemeta::core::to_pointer(location.pointer)}; + then_pointer.push_back("then"); + const auto then_location{ + frame.traverse(sourcemeta::core::to_weak_pointer(then_pointer))}; + assert(then_location.has_value()); + + auto else_pointer{sourcemeta::core::to_pointer(location.pointer)}; + else_pointer.push_back("else"); + const auto else_location{ + frame.traverse(sourcemeta::core::to_weak_pointer(else_pointer))}; + assert(else_location.has_value()); + + return IRConditional{ + {.pointer = sourcemeta::core::to_pointer(location.pointer), + .symbol = symbol(frame, location)}, + {.pointer = std::move(if_pointer), + .symbol = symbol(frame, if_location.value().get())}, + {.pointer = std::move(then_pointer), + .symbol = symbol(frame, then_location.value().get())}, + {.pointer = std::move(else_pointer), + .symbol = symbol(frame, else_location.value().get())}}; +} + +auto default_compiler(const sourcemeta::core::JSON &schema, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + const sourcemeta::core::SchemaResolver &resolver, + const sourcemeta::core::JSON &subschema) -> IREntity { + const auto vocabularies{frame.vocabularies(location, resolver)}; + assert(!vocabularies.empty()); + + // Be strict with vocabulary support + using Vocabularies = sourcemeta::core::Vocabularies; + static const std::unordered_set supported{ + Vocabularies::Known::JSON_Schema_2020_12_Core, + Vocabularies::Known::JSON_Schema_2020_12_Applicator, + Vocabularies::Known::JSON_Schema_2020_12_Validation, + Vocabularies::Known::JSON_Schema_2020_12_Unevaluated, + Vocabularies::Known::JSON_Schema_2020_12_Content, + Vocabularies::Known::JSON_Schema_2020_12_Meta_Data, + Vocabularies::Known::JSON_Schema_2020_12_Format_Annotation, + Vocabularies::Known::JSON_Schema_2020_12_Format_Assertion, + Vocabularies::Known::JSON_Schema_2019_09_Core, + Vocabularies::Known::JSON_Schema_2019_09_Applicator, + Vocabularies::Known::JSON_Schema_2019_09_Validation, + Vocabularies::Known::JSON_Schema_2019_09_Content, + Vocabularies::Known::JSON_Schema_2019_09_Meta_Data, + Vocabularies::Known::JSON_Schema_2019_09_Format, + Vocabularies::Known::JSON_Schema_Draft_7, + Vocabularies::Known::JSON_Schema_Draft_6, + Vocabularies::Known::JSON_Schema_Draft_4}; + vocabularies.throw_if_any_unsupported(supported, + "Unsupported required vocabulary"); + + // The canonicaliser ensures that every subschema schema is only in one of the + // following shapes + + if (subschema.is_boolean()) { + if (subschema.to_boolean()) { + return handle_any(schema, frame, location, vocabularies, resolver, + subschema); + } else { + return handle_impossible(schema, frame, location, vocabularies, resolver, + subschema); + } + } else if (subschema.defines("type")) { + const auto &type_value{subschema.at("type")}; + if (!type_value.is_string()) { + throw UnsupportedKeywordValueError(schema, location.pointer, "type", + "Expected a string value"); + } + + const auto &type_string{type_value.to_string()}; + + // The canonicaliser transforms any other type + if (type_string == "string") { + return handle_string(schema, frame, location, vocabularies, resolver, + subschema); + } else if (type_string == "object") { + return handle_object(schema, frame, location, vocabularies, resolver, + subschema); + } else if (type_string == "integer") { + return handle_integer(schema, frame, location, vocabularies, resolver, + subschema); + } else if (type_string == "number") { + return handle_number(schema, frame, location, vocabularies, resolver, + subschema); + } else if (type_string == "array") { + return handle_array(schema, frame, location, vocabularies, resolver, + subschema); + } else { + throw UnsupportedKeywordValueError(schema, location.pointer, "type", + "Unsupported type value"); + } + } else if (subschema.defines("enum")) { + return handle_enum(schema, frame, location, vocabularies, resolver, + subschema); + } else if (subschema.defines("anyOf")) { + return handle_anyof(schema, frame, location, vocabularies, resolver, + subschema); + // This is usually a good enough approximation. We usually can't check that + // the other types DO NOT match, but that is in a way a validation concern + } else if (subschema.defines("oneOf")) { + return handle_oneof(schema, frame, location, vocabularies, resolver, + subschema); + } else if (subschema.defines("allOf")) { + return handle_allof(schema, frame, location, vocabularies, resolver, + subschema); + } else if (subschema.defines("$dynamicRef")) { + return handle_dynamic_ref(schema, frame, location, vocabularies, resolver, + subschema); + } else if (subschema.defines("$ref")) { + return handle_ref(schema, frame, location, vocabularies, resolver, + subschema); + } else if (subschema.defines("if")) { + return handle_if_then_else(schema, frame, location, vocabularies, resolver, + subschema); + } else if (subschema.defines("not")) { + throw UnsupportedKeywordError(schema, location.pointer, "not", + "Unsupported keyword in subschema"); + } else { + throw UnexpectedSchemaError(schema, location.pointer, "Unsupported schema"); + } +} + +} // namespace sourcemeta::blaze + +#endif diff --git a/src/codegen/codegen_mangle.cc b/src/codegen/codegen_mangle.cc new file mode 100644 index 000000000..0d6a4a388 --- /dev/null +++ b/src/codegen/codegen_mangle.cc @@ -0,0 +1,90 @@ +#include + +namespace { + +auto is_alpha(char character) -> bool { + return (character >= 'a' && character <= 'z') || + (character >= 'A' && character <= 'Z'); +} + +auto is_digit(char character) -> bool { + return character >= '0' && character <= '9'; +} + +auto to_upper(char character) -> char { + if (character >= 'a' && character <= 'z') { + return static_cast(character - 'a' + 'A'); + } + return character; +} + +auto symbol_to_identifier(const std::string_view prefix, + const std::vector &symbol) + -> std::string { + std::string result{prefix}; + + for (const auto &segment : symbol) { + if (segment.empty()) { + continue; + } + + bool first_in_segment{true}; + for (const auto character : segment) { + if (is_alpha(character)) { + if (first_in_segment) { + result += to_upper(character); + first_in_segment = false; + } else { + result += character; + } + } else if (is_digit(character)) { + if (first_in_segment) { + result += '_'; + } + result += character; + first_in_segment = false; + } else if (character == '_' || character == '$') { + result += character; + first_in_segment = false; + } + } + } + + if (result.empty()) { + return "_"; + } + + if (is_digit(result[0])) { + result.insert(0, "_"); + } + + return result; +} + +} // namespace + +namespace sourcemeta::blaze { + +auto mangle(const std::string_view prefix, + const sourcemeta::core::Pointer &pointer, + const std::vector &symbol, + std::map &cache) + -> const std::string & { + auto name{symbol_to_identifier(prefix, symbol)}; + + while (true) { + auto iterator{cache.find(name)}; + if (iterator != cache.end()) { + if (iterator->second == pointer) { + return iterator->first; + } + + name.insert(0, "_"); + } else { + auto result{cache.insert({std::move(name), pointer})}; + return result.first->first; + } + } +} + +} // namespace sourcemeta::blaze diff --git a/src/codegen/codegen_symbol.cc b/src/codegen/codegen_symbol.cc new file mode 100644 index 000000000..6003dba22 --- /dev/null +++ b/src/codegen/codegen_symbol.cc @@ -0,0 +1,119 @@ +#include + +#include + +#include // std::ranges::reverse +#include // assert +#include // std::filesystem::path +#include // std::istringstream +#include // std::string, std::getline +#include // std::vector + +namespace { + +// Strip all extensions from a filename (e.g., "user.schema.json" -> "user") +auto strip_extensions(const std::string &filename) -> std::string { + std::filesystem::path path{filename}; + while (path.has_extension()) { + path = path.stem(); + } + return path.string(); +} + +// If the input looks like an absolute URI, extract its path segments. +// For file URIs, only the filename (without extensions) is used. +// For other URIs, all path segments are used with extensions stripped from +// the last segment. +// Otherwise, add the input as a single segment. +// Note: segments are added in reverse order because the caller reverses +// the entire result at the end. +auto push_token_segments(std::vector &result, + const std::string &value) -> void { + try { + const sourcemeta::core::URI uri{value}; + if (uri.is_absolute()) { + const auto path{uri.path()}; + if (path.has_value() && !path->empty()) { + std::vector segments; + std::istringstream stream{std::string{path.value()}}; + std::string segment; + while (std::getline(stream, segment, '/')) { + if (!segment.empty()) { + segments.emplace_back(segment); + } + } + + if (!segments.empty()) { + // Strip extensions from the last segment + segments.back() = strip_extensions(segments.back()); + + // For file URIs, only use the filename + if (uri.is_file()) { + result.emplace_back(segments.back()); + } else { + // Reverse segments since the caller will reverse the entire result + std::ranges::reverse(segments); + for (const auto &path_segment : segments) { + result.emplace_back(path_segment); + } + } + + return; + } + } + } + // NOLINTNEXTLINE(bugprone-empty-catch) + } catch (const sourcemeta::core::URIParseError &) { + // Not a valid URI, fall through to default behavior + } + + result.emplace_back(value); +} + +} // namespace + +namespace sourcemeta::blaze { + +auto symbol(const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location) + -> std::vector { + std::vector result; + + auto current_pointer{location.pointer}; + + while (true) { + const auto current_location{frame.traverse(current_pointer)}; + assert(current_location.has_value()); + + if (!current_location->get().parent.has_value()) { + break; + } + + const auto &parent_pointer{current_location->get().parent.value()}; + const auto segments_skipped{current_pointer.size() - parent_pointer.size()}; + assert(segments_skipped >= 1); + + if (segments_skipped >= 2) { + const auto &last_token{current_pointer.back()}; + if (last_token.is_property()) { + push_token_segments(result, last_token.to_property()); + } else { + result.emplace_back(std::to_string(last_token.to_index())); + } + } else { + const auto &token{current_pointer.back()}; + if (token.is_property()) { + push_token_segments(result, token.to_property()); + } else { + result.emplace_back(std::to_string(token.to_index())); + } + } + + current_pointer = parent_pointer; + } + + std::ranges::reverse(result); + return result; +} + +} // namespace sourcemeta::blaze diff --git a/src/codegen/codegen_typescript.cc b/src/codegen/codegen_typescript.cc new file mode 100644 index 000000000..c10bd674c --- /dev/null +++ b/src/codegen/codegen_typescript.cc @@ -0,0 +1,328 @@ +#include + +#include // std::ranges::any_of +#include // std::hex, std::setfill, std::setw +#include // std::ostringstream + +namespace { + +// TODO: Move to Core +auto escape_string(const std::string &input) -> std::string { + std::ostringstream result; + for (const auto character : input) { + switch (character) { + case '\\': + result << "\\\\"; + break; + case '"': + result << "\\\""; + break; + case '\b': + result << "\\b"; + break; + case '\f': + result << "\\f"; + break; + case '\n': + result << "\\n"; + break; + case '\r': + result << "\\r"; + break; + case '\t': + result << "\\t"; + break; + default: + // Escape other control characters (< 0x20) using \uXXXX format + if (static_cast(character) < 0x20) { + result << "\\u" << std::hex << std::setfill('0') << std::setw(4) + << static_cast(static_cast(character)); + } else { + result << character; + } + break; + } + } + + return result.str(); +} + +} // namespace + +namespace sourcemeta::blaze { + +TypeScript::TypeScript(std::ostream &stream, const std::string_view type_prefix) + : output{stream}, prefix{type_prefix} {} + +auto TypeScript::operator()(const IRScalar &entry) -> void { + this->output << "export type " + << mangle(this->prefix, entry.pointer, entry.symbol, this->cache) + << " = "; + + switch (entry.value) { + case IRScalarType::String: + this->output << "string"; + break; + case IRScalarType::Number: + case IRScalarType::Integer: + this->output << "number"; + break; + case IRScalarType::Boolean: + this->output << "boolean"; + break; + case IRScalarType::Null: + this->output << "null"; + break; + } + + this->output << ";\n"; +} + +auto TypeScript::operator()(const IREnumeration &entry) -> void { + this->output << "export type " + << mangle(this->prefix, entry.pointer, entry.symbol, this->cache) + << " = "; + + const char *separator{""}; + for (const auto &value : entry.values) { + this->output << separator; + sourcemeta::core::prettify(value, this->output); + separator = " | "; + } + + this->output << ";\n"; +} + +auto TypeScript::operator()(const IRObject &entry) -> void { + const auto type_name{ + mangle(this->prefix, entry.pointer, entry.symbol, this->cache)}; + 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_typed_additional && entry.members.empty() && entry.pattern.empty()) { + const auto &additional_type{std::get(entry.additional)}; + this->output << "export type " << type_name << " = Recordprefix, additional_type.pointer, + additional_type.symbol, this->cache) + << ">;\n"; + return; + } + + if (allows_any_additional && entry.members.empty() && entry.pattern.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 + // as a property name, but unquoted TypeScript/ECMAScript property names + // must be valid IdentifierName productions (see ECMA-262 section 12.7). + // Quoting allows any string to be used as a property name. + // See: https://tc39.es/ecma262/#sec-names-and-keywords + // See: https://mathiasbynens.be/notes/javascript-properties + for (const auto &[member_name, member_value] : entry.members) { + const auto optional_marker{member_value.required ? "" : "?"}; + const auto readonly_marker{member_value.immutable ? "readonly " : ""}; + + this->output << " " << readonly_marker << "\"" + << escape_string(member_name) << "\"" << optional_marker + << ": " + << mangle(this->prefix, member_value.pointer, + member_value.symbol, this->cache) + << ";\n"; + } + + for (const auto &pattern_property : entry.pattern) { + if (!pattern_property.prefix.has_value()) { + continue; + } + + this->output << " [key: `" << pattern_property.prefix.value() + << "${string}`]: " + << mangle(this->prefix, pattern_property.pointer, + pattern_property.symbol, this->cache); + + // TypeScript requires that a more specific index signature type is + // assignable to any less specific one that overlaps it. When a prefix + // is a sub-prefix of another (i.e. "x-data-" starts with "x-"), + // intersect the types so the constraint is satisfied + for (const auto &other : entry.pattern) { + if (&other == &pattern_property || !other.prefix.has_value()) { + continue; + } + + if (pattern_property.prefix.value().starts_with(other.prefix.value())) { + this->output << " & " + << mangle(this->prefix, other.pointer, other.symbol, + this->cache); + } + } + + this->output << ";\n"; + } + + const auto has_non_prefix_pattern{ + std::ranges::any_of(entry.pattern, [](const auto &pattern_property) { + return !pattern_property.prefix.has_value(); + })}; + + if (allows_any_additional) { + this->output << " [key: string]: unknown | undefined;\n"; + } else if (has_typed_additional || has_non_prefix_pattern) { + // 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 << " " + << mangle(this->prefix, member_value.pointer, + member_value.symbol, this->cache) + << " |\n"; + } + + for (const auto &pattern_property : entry.pattern) { + this->output << " " + << mangle(this->prefix, pattern_property.pointer, + pattern_property.symbol, this->cache) + << " |\n"; + } + + if (has_typed_additional) { + const auto &additional_type{std::get(entry.additional)}; + this->output << " " + << mangle(this->prefix, additional_type.pointer, + additional_type.symbol, this->cache) + << " |\n"; + } + + this->output << " undefined;\n"; + } + + this->output << "}\n"; +} + +auto TypeScript::operator()(const IRImpossible &entry) -> void { + this->output << "export type " + << mangle(this->prefix, entry.pointer, entry.symbol, this->cache) + << " = never;\n"; +} + +auto TypeScript::operator()(const IRAny &entry) -> void { + this->output << "export type " + << mangle(this->prefix, entry.pointer, entry.symbol, this->cache) + << " = unknown;\n"; +} + +auto TypeScript::operator()(const IRArray &entry) -> void { + this->output << "export type " + << mangle(this->prefix, entry.pointer, entry.symbol, this->cache) + << " = "; + + if (entry.items.has_value()) { + this->output << mangle(this->prefix, entry.items->pointer, + entry.items->symbol, this->cache) + << "[]"; + } else { + this->output << "unknown[]"; + } + + this->output << ";\n"; +} + +auto TypeScript::operator()(const IRReference &entry) -> void { + this->output << "export type " + << mangle(this->prefix, entry.pointer, entry.symbol, this->cache) + << " = " + << mangle(this->prefix, entry.target.pointer, + entry.target.symbol, this->cache) + << ";\n"; +} + +auto TypeScript::operator()(const IRTuple &entry) -> void { + this->output << "export type " + << mangle(this->prefix, entry.pointer, entry.symbol, this->cache) + << " = ["; + + const char *separator{""}; + for (const auto &item : entry.items) { + this->output << separator + << mangle(this->prefix, item.pointer, item.symbol, + this->cache); + separator = ", "; + } + + if (entry.additional.has_value()) { + this->output << separator << "..." + << mangle(this->prefix, entry.additional->pointer, + entry.additional->symbol, this->cache) + << "[]"; + } + + this->output << "];\n"; +} + +auto TypeScript::operator()(const IRUnion &entry) -> void { + this->output << "export type " + << mangle(this->prefix, entry.pointer, entry.symbol, this->cache) + << " =\n"; + + const char *separator{""}; + for (const auto &value : entry.values) { + this->output << separator << " " + << mangle(this->prefix, value.pointer, value.symbol, + this->cache); + separator = " |\n"; + } + + this->output << ";\n"; +} + +auto TypeScript::operator()(const IRIntersection &entry) -> void { + this->output << "export type " + << mangle(this->prefix, entry.pointer, entry.symbol, this->cache) + << " =\n"; + + const char *separator{""}; + for (const auto &value : entry.values) { + this->output << separator << " " + << mangle(this->prefix, value.pointer, value.symbol, + this->cache); + separator = " &\n"; + } + + this->output << ";\n"; +} + +auto TypeScript::operator()(const IRConditional &entry) -> void { + // As a notable limitation, TypeScript cannot express the negation of an + // if/then/else condition, so the else branch is wider than what JSON + // Schema allows + this->output << "// (if & then) | else approximation: the else branch is " + "wider than what\n"; + this->output << "// JSON Schema allows, as TypeScript cannot express type " + "negation\n"; + this->output << "export type " + << mangle(this->prefix, entry.pointer, entry.symbol, this->cache) + << " =\n (" + << mangle(this->prefix, entry.condition.pointer, + entry.condition.symbol, this->cache) + << " & " + << mangle(this->prefix, entry.consequent.pointer, + entry.consequent.symbol, this->cache) + << ") | " + << mangle(this->prefix, entry.alternative.pointer, + entry.alternative.symbol, this->cache) + << ";\n"; +} + +} // namespace sourcemeta::blaze diff --git a/src/codegen/include/sourcemeta/blaze/codegen.h b/src/codegen/include/sourcemeta/blaze/codegen.h new file mode 100644 index 000000000..99144e90c --- /dev/null +++ b/src/codegen/include/sourcemeta/blaze/codegen.h @@ -0,0 +1,176 @@ +#ifndef SOURCEMETA_BLAZE_CODEGEN_H_ +#define SOURCEMETA_BLAZE_CODEGEN_H_ + +#ifndef SOURCEMETA_BLAZE_CODEGEN_EXPORT +#include +#endif + +// NOLINTBEGIN(misc-include-cleaner) +#include +#include +// NOLINTEND(misc-include-cleaner) + +#include +#include +#include + +#include // std::uint8_t +#include // std::function +#include // std::map +#include // std::optional, std::nullopt +#include // std::ostream +#include // std::string +#include // std::string_view +#include // std::pair +#include // std::variant, std::visit +#include // std::vector + +/// @defgroup codegen Codegen +/// @brief The codegen JSON Schema code generation package + +namespace sourcemeta::blaze { + +/// @ingroup codegen +enum class IRScalarType : std::uint8_t { + String, + Number, + Integer, + Boolean, + Null +}; + +/// @ingroup codegen +struct IRType { + sourcemeta::core::Pointer pointer; + std::vector symbol; +}; + +/// @ingroup codegen +struct IRScalar : IRType { + IRScalarType value; +}; + +/// @ingroup codegen +struct IREnumeration : IRType { + std::vector values; +}; + +/// @ingroup codegen +struct IRUnion : IRType { + std::vector values; +}; + +/// @ingroup codegen +struct IRIntersection : IRType { + std::vector values; +}; + +/// @ingroup codegen +struct IRObjectValue : IRType { + bool required; + bool immutable; +}; + +/// @ingroup codegen +struct IRObjectPatternProperty : IRType { + std::optional prefix; +}; + +/// @ingroup codegen +struct IRObject : IRType { + // To preserve the user's ordering + std::vector> members; + std::variant additional; + std::vector pattern; +}; + +/// @ingroup codegen +struct IRArray : IRType { + std::optional items; +}; + +/// @ingroup codegen +struct IRTuple : IRType { + std::vector items; + std::optional additional; +}; + +/// @ingroup codegen +struct IRImpossible : IRType {}; + +/// @ingroup codegen +struct IRAny : IRType {}; + +/// @ingroup codegen +struct IRConditional : IRType { + IRType condition; + IRType consequent; + IRType alternative; +}; + +/// @ingroup codegen +struct IRReference : IRType { + IRType target; +}; + +/// @ingroup codegen +using IREntity = std::variant; + +/// @ingroup codegen +using IRResult = std::vector; + +/// @ingroup codegen +using CodegenCompiler = std::function; + +/// @ingroup codegen +SOURCEMETA_BLAZE_CODEGEN_EXPORT +auto default_compiler(const sourcemeta::core::JSON &schema, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + const sourcemeta::core::SchemaResolver &resolver, + const sourcemeta::core::JSON &subschema) -> IREntity; + +/// @ingroup codegen +SOURCEMETA_BLAZE_CODEGEN_EXPORT +auto compile(const sourcemeta::core::JSON &schema, + const sourcemeta::core::SchemaWalker &walker, + const sourcemeta::core::SchemaResolver &resolver, + const CodegenCompiler &compiler, + const std::string_view default_dialect = "", + const std::string_view default_id = "") -> IRResult; + +/// @ingroup codegen +SOURCEMETA_BLAZE_CODEGEN_EXPORT +auto symbol(const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location) + -> std::vector; + +/// @ingroup codegen +SOURCEMETA_BLAZE_CODEGEN_EXPORT +auto mangle(const std::string_view prefix, + const sourcemeta::core::Pointer &pointer, + const std::vector &symbol, + std::map &cache) + -> const std::string &; + +/// @ingroup codegen +template +auto generate(std::ostream &output, const IRResult &result, + const std::string_view prefix = "Schema") -> void { + T visitor{output, prefix}; + const char *separator{""}; + for (const auto &entity : result) { + output << separator; + separator = "\n"; + std::visit(visitor, entity); + } +} + +} // namespace sourcemeta::blaze + +#endif diff --git a/src/codegen/include/sourcemeta/blaze/codegen_error.h b/src/codegen/include/sourcemeta/blaze/codegen_error.h new file mode 100644 index 000000000..d3cbd09de --- /dev/null +++ b/src/codegen/include/sourcemeta/blaze/codegen_error.h @@ -0,0 +1,178 @@ +#ifndef SOURCEMETA_BLAZE_CODEGEN_ERROR_H_ +#define SOURCEMETA_BLAZE_CODEGEN_ERROR_H_ + +#ifndef SOURCEMETA_BLAZE_CODEGEN_EXPORT +#include +#endif + +#include +#include + +#include // std::exception +#include // std::string +#include // std::string_view +#include // std::move + +namespace sourcemeta::blaze { + +// Exporting symbols that depends on the standard C++ library is considered +// safe. +// https://learn.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-2-c4275?view=msvc-170&redirectedfrom=MSDN +#if defined(_MSC_VER) +#pragma warning(disable : 4251 4275) +#endif + +/// @ingroup codegen +/// An error that represents an unsupported keyword during IR compilation +class SOURCEMETA_BLAZE_CODEGEN_EXPORT UnsupportedKeywordError + : public std::exception { +public: + UnsupportedKeywordError(sourcemeta::core::JSON json, + sourcemeta::core::Pointer pointer, + std::string keyword, const char *message) + : json_{std::move(json)}, pointer_{std::move(pointer)}, + keyword_{std::move(keyword)}, message_{message} {} + UnsupportedKeywordError(sourcemeta::core::JSON json, + const sourcemeta::core::WeakPointer &pointer, + std::string keyword, const char *message) + : UnsupportedKeywordError{std::move(json), + sourcemeta::core::to_pointer(pointer), + std::move(keyword), message} {} + UnsupportedKeywordError(sourcemeta::core::JSON json, + sourcemeta::core::Pointer pointer, + std::string keyword, std::string message) = delete; + UnsupportedKeywordError(sourcemeta::core::JSON json, + sourcemeta::core::Pointer pointer, + std::string keyword, std::string &&message) = delete; + UnsupportedKeywordError(sourcemeta::core::JSON json, + sourcemeta::core::Pointer pointer, + std::string keyword, + std::string_view message) = delete; + + [[nodiscard]] auto what() const noexcept -> const char * override { + return this->message_; + } + + [[nodiscard]] auto json() const noexcept -> const sourcemeta::core::JSON & { + return this->json_; + } + + [[nodiscard]] auto pointer() const noexcept + -> const sourcemeta::core::Pointer & { + return this->pointer_; + } + + [[nodiscard]] auto keyword() const noexcept -> std::string_view { + return this->keyword_; + } + +private: + sourcemeta::core::JSON json_; + sourcemeta::core::Pointer pointer_; + std::string keyword_; + const char *message_; +}; + +/// @ingroup codegen +/// An error that represents an unsupported keyword value during IR compilation +class SOURCEMETA_BLAZE_CODEGEN_EXPORT UnsupportedKeywordValueError + : public std::exception { +public: + UnsupportedKeywordValueError(sourcemeta::core::JSON json, + sourcemeta::core::Pointer pointer, + std::string keyword, const char *message) + : json_{std::move(json)}, pointer_{std::move(pointer)}, + keyword_{std::move(keyword)}, message_{message} {} + UnsupportedKeywordValueError(sourcemeta::core::JSON json, + const sourcemeta::core::WeakPointer &pointer, + std::string keyword, const char *message) + : UnsupportedKeywordValueError{std::move(json), + sourcemeta::core::to_pointer(pointer), + std::move(keyword), message} {} + UnsupportedKeywordValueError(sourcemeta::core::JSON json, + sourcemeta::core::Pointer pointer, + std::string keyword, + std::string message) = delete; + UnsupportedKeywordValueError(sourcemeta::core::JSON json, + sourcemeta::core::Pointer pointer, + std::string keyword, + std::string &&message) = delete; + UnsupportedKeywordValueError(sourcemeta::core::JSON json, + sourcemeta::core::Pointer pointer, + std::string keyword, + std::string_view message) = delete; + + [[nodiscard]] auto what() const noexcept -> const char * override { + return this->message_; + } + + [[nodiscard]] auto json() const noexcept -> const sourcemeta::core::JSON & { + return this->json_; + } + + [[nodiscard]] auto pointer() const noexcept + -> const sourcemeta::core::Pointer & { + return this->pointer_; + } + + [[nodiscard]] auto keyword() const noexcept -> std::string_view { + return this->keyword_; + } + +private: + sourcemeta::core::JSON json_; + sourcemeta::core::Pointer pointer_; + std::string keyword_; + const char *message_; +}; + +/// @ingroup codegen +/// An error that represents an unexpected schema during IR compilation +class SOURCEMETA_BLAZE_CODEGEN_EXPORT UnexpectedSchemaError + : public std::exception { +public: + UnexpectedSchemaError(sourcemeta::core::JSON json, + sourcemeta::core::Pointer pointer, const char *message) + : json_{std::move(json)}, pointer_{std::move(pointer)}, + message_{message} {} + UnexpectedSchemaError(sourcemeta::core::JSON json, + const sourcemeta::core::WeakPointer &pointer, + const char *message) + : UnexpectedSchemaError{std::move(json), + sourcemeta::core::to_pointer(pointer), message} {} + UnexpectedSchemaError(sourcemeta::core::JSON json, + sourcemeta::core::Pointer pointer, + std::string message) = delete; + UnexpectedSchemaError(sourcemeta::core::JSON json, + sourcemeta::core::Pointer pointer, + std::string &&message) = delete; + UnexpectedSchemaError(sourcemeta::core::JSON json, + sourcemeta::core::Pointer pointer, + std::string_view message) = delete; + + [[nodiscard]] auto what() const noexcept -> const char * override { + return this->message_; + } + + [[nodiscard]] auto json() const noexcept -> const sourcemeta::core::JSON & { + return this->json_; + } + + [[nodiscard]] auto pointer() const noexcept + -> const sourcemeta::core::Pointer & { + return this->pointer_; + } + +private: + sourcemeta::core::JSON json_; + sourcemeta::core::Pointer pointer_; + const char *message_; +}; + +#if defined(_MSC_VER) +#pragma warning(default : 4251 4275) +#endif + +} // namespace sourcemeta::blaze + +#endif diff --git a/src/codegen/include/sourcemeta/blaze/codegen_typescript.h b/src/codegen/include/sourcemeta/blaze/codegen_typescript.h new file mode 100644 index 000000000..90dfde17e --- /dev/null +++ b/src/codegen/include/sourcemeta/blaze/codegen_typescript.h @@ -0,0 +1,63 @@ +#ifndef SOURCEMETA_BLAZE_CODEGEN_TYPESCRIPT_H_ +#define SOURCEMETA_BLAZE_CODEGEN_TYPESCRIPT_H_ + +#ifndef SOURCEMETA_BLAZE_CODEGEN_EXPORT +#include +#endif + +#include + +#include // std::map +#include // std::ostream +#include // std::string +#include // std::string_view + +namespace sourcemeta::blaze { + +struct IRScalar; +struct IREnumeration; +struct IRObject; +struct IRImpossible; +struct IRAny; +struct IRArray; +struct IRReference; +struct IRTuple; +struct IRUnion; +struct IRIntersection; +struct IRConditional; + +/// @ingroup codegen +class SOURCEMETA_BLAZE_CODEGEN_EXPORT TypeScript { +public: + TypeScript(std::ostream &stream, std::string_view type_prefix); + auto operator()(const IRScalar &entry) -> void; + auto operator()(const IREnumeration &entry) -> void; + auto operator()(const IRObject &entry) -> void; + auto operator()(const IRImpossible &entry) -> void; + auto operator()(const IRAny &entry) -> void; + auto operator()(const IRArray &entry) -> void; + auto operator()(const IRReference &entry) -> void; + auto operator()(const IRTuple &entry) -> void; + auto operator()(const IRUnion &entry) -> void; + auto operator()(const IRIntersection &entry) -> void; + auto operator()(const IRConditional &entry) -> void; + +private: +// Exporting symbols that depends on the standard C++ library is considered +// safe. +// https://learn.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-2-c4275?view=msvc-170&redirectedfrom=MSDN +#if defined(_MSC_VER) +#pragma warning(disable : 4251) +#endif + // NOLINTNEXTLINE(cppcoreguidelines-avoid-const-or-ref-data-members) + std::ostream &output; + std::string_view prefix; + std::map cache; +#if defined(_MSC_VER) +#pragma warning(default : 4251) +#endif +}; + +} // namespace sourcemeta::blaze + +#endif diff --git a/test/codegen/CMakeLists.txt b/test/codegen/CMakeLists.txt new file mode 100644 index 000000000..ed1d355b7 --- /dev/null +++ b/test/codegen/CMakeLists.txt @@ -0,0 +1,18 @@ +sourcemeta_googletest(NAMESPACE sourcemeta PROJECT blaze NAME codegen + FOLDER "Blaze/Codegen" + SOURCES codegen_test.cc codegen_2020_12_test.cc + codegen_symbol_test.cc codegen_test_utils.h) + +target_link_libraries(sourcemeta_blaze_codegen_unit + PRIVATE sourcemeta::blaze::codegen) + +if(BLAZE_CODEGEN) + add_subdirectory(e2e/typescript) +endif() + +if(E2E_SCHEMAS) + add_custom_target(jsonschema_metaschema + COMMAND "${PROJECT_SOURCE_DIR}/node_modules/.bin/jsonschema" + metaschema ${E2E_SCHEMAS} + VERBATIM) +endif() diff --git a/test/codegen/codegen_2020_12_test.cc b/test/codegen/codegen_2020_12_test.cc new file mode 100644 index 000000000..617394166 --- /dev/null +++ b/test/codegen/codegen_2020_12_test.cc @@ -0,0 +1,1626 @@ +#include + +#include + +#include "codegen_test_utils.h" + +TEST(IR_2020_12, test_1) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string" + })JSON")}; + + const auto result{sourcemeta::blaze::compile( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, sourcemeta::blaze::default_compiler)}; + + using namespace sourcemeta::blaze; + + EXPECT_EQ(result.size(), 1); + + EXPECT_IR_SCALAR(result, 0, String, ""); + EXPECT_SYMBOL(std::get(result.at(0)).symbol); +} + +TEST(IR_2020_12, default_dialect_parameter) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "type": "string" + })JSON")}; + + const auto result{sourcemeta::blaze::compile( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, sourcemeta::blaze::default_compiler, + "https://json-schema.org/draft/2020-12/schema")}; + + using namespace sourcemeta::blaze; + + EXPECT_EQ(result.size(), 1); + + EXPECT_IR_SCALAR(result, 0, String, ""); + EXPECT_SYMBOL(std::get(result.at(0)).symbol); +} + +TEST(IR_2020_12, test_2) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "foo": { + "type": "string" + } + } + })JSON")}; + + const auto result{sourcemeta::blaze::compile( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, sourcemeta::blaze::default_compiler)}; + + using namespace sourcemeta::blaze; + + EXPECT_EQ(result.size(), 2); + + EXPECT_IR_SCALAR(result, 0, String, "/properties/foo"); + EXPECT_SYMBOL(std::get(result.at(0)).symbol, "foo"); + + EXPECT_TRUE(std::holds_alternative(result.at(1))); + EXPECT_AS_STRING(std::get(result.at(1)).pointer, ""); + EXPECT_SYMBOL(std::get(result.at(1)).symbol); + EXPECT_EQ(std::get(result.at(1)).members.size(), 1); + EXPECT_EQ(std::get(result.at(1)).members.at(0).first, "foo"); + EXPECT_FALSE(std::get(result.at(1)).members.at(0).second.required); + EXPECT_FALSE(std::get(result.at(1)).members.at(0).second.immutable); + EXPECT_AS_STRING( + std::get(result.at(1)).members.at(0).second.pointer, + "/properties/foo"); + EXPECT_SYMBOL(std::get(result.at(1)).members.at(0).second.symbol, + "foo"); + 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) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "integer" + })JSON")}; + + const auto result{sourcemeta::blaze::compile( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, sourcemeta::blaze::default_compiler)}; + + using namespace sourcemeta::blaze; + + EXPECT_EQ(result.size(), 1); + + EXPECT_IR_SCALAR(result, 0, Integer, ""); + EXPECT_SYMBOL(std::get(result.at(0)).symbol); +} + +TEST(IR_2020_12, test_4) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "number" + })JSON")}; + + const auto result{sourcemeta::blaze::compile( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, sourcemeta::blaze::default_compiler)}; + + using namespace sourcemeta::blaze; + + EXPECT_EQ(result.size(), 1); + + EXPECT_IR_SCALAR(result, 0, Number, ""); + EXPECT_SYMBOL(std::get(result.at(0)).symbol); +} + +TEST(IR_2020_12, test_5) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "integer", + "minimum": 0, + "maximum": 100, + "multipleOf": 5 + })JSON")}; + + const auto result{sourcemeta::blaze::compile( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, sourcemeta::blaze::default_compiler)}; + + using namespace sourcemeta::blaze; + + EXPECT_EQ(result.size(), 1); + + EXPECT_IR_SCALAR(result, 0, Integer, ""); + EXPECT_SYMBOL(std::get(result.at(0)).symbol); +} + +TEST(IR_2020_12, test_6) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "number", + "exclusiveMinimum": 0, + "exclusiveMaximum": 1.5, + "multipleOf": 0.1 + })JSON")}; + + const auto result{sourcemeta::blaze::compile( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, sourcemeta::blaze::default_compiler)}; + + using namespace sourcemeta::blaze; + + EXPECT_EQ(result.size(), 1); + + EXPECT_IR_SCALAR(result, 0, Number, ""); + EXPECT_SYMBOL(std::get(result.at(0)).symbol); +} + +TEST(IR_2020_12, enum_null) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "enum": [ null ] + })JSON")}; + + const auto result{sourcemeta::blaze::compile( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, sourcemeta::blaze::default_compiler)}; + + using namespace sourcemeta::blaze; + + EXPECT_EQ(result.size(), 1); + + EXPECT_IR_SCALAR(result, 0, Null, ""); + EXPECT_SYMBOL(std::get(result.at(0)).symbol); +} + +TEST(IR_2020_12, enum_boolean_true_false) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "enum": [ true, false ] + })JSON")}; + + const auto result{sourcemeta::blaze::compile( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, sourcemeta::blaze::default_compiler)}; + + using namespace sourcemeta::blaze; + + EXPECT_EQ(result.size(), 1); + + EXPECT_IR_SCALAR(result, 0, Boolean, ""); + EXPECT_SYMBOL(std::get(result.at(0)).symbol); +} + +TEST(IR_2020_12, enum_boolean_false_true) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "enum": [ false, true ] + })JSON")}; + + const auto result{sourcemeta::blaze::compile( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, sourcemeta::blaze::default_compiler)}; + + using namespace sourcemeta::blaze; + + EXPECT_EQ(result.size(), 1); + + EXPECT_IR_SCALAR(result, 0, Boolean, ""); + EXPECT_SYMBOL(std::get(result.at(0)).symbol); +} + +TEST(IR_2020_12, enum_string_values) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "enum": [ "foo", "bar", "baz" ] + })JSON")}; + + const auto result{sourcemeta::blaze::compile( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, sourcemeta::blaze::default_compiler)}; + + using namespace sourcemeta::blaze; + + EXPECT_EQ(result.size(), 1); + + EXPECT_TRUE(std::holds_alternative(result.at(0))); + EXPECT_AS_STRING(std::get(result.at(0)).pointer, ""); + EXPECT_SYMBOL(std::get(result.at(0)).symbol); + EXPECT_EQ(std::get(result.at(0)).values.size(), 3); + EXPECT_EQ(std::get(result.at(0)).values.at(0).to_string(), + "foo"); + EXPECT_EQ(std::get(result.at(0)).values.at(1).to_string(), + "bar"); + EXPECT_EQ(std::get(result.at(0)).values.at(2).to_string(), + "baz"); +} + +TEST(IR_2020_12, const_null) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "const": null + })JSON")}; + + const auto result{sourcemeta::blaze::compile( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, sourcemeta::blaze::default_compiler)}; + + using namespace sourcemeta::blaze; + + EXPECT_EQ(result.size(), 1); + + EXPECT_IR_SCALAR(result, 0, Null, ""); + EXPECT_SYMBOL(std::get(result.at(0)).symbol); +} + +TEST(IR_2020_12, const_string) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "const": "hello" + })JSON")}; + + const auto result{sourcemeta::blaze::compile( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, sourcemeta::blaze::default_compiler)}; + + using namespace sourcemeta::blaze; + + EXPECT_EQ(result.size(), 1); + + EXPECT_TRUE(std::holds_alternative(result.at(0))); + EXPECT_AS_STRING(std::get(result.at(0)).pointer, ""); + EXPECT_SYMBOL(std::get(result.at(0)).symbol); + EXPECT_EQ(std::get(result.at(0)).values.size(), 1); + EXPECT_EQ(std::get(result.at(0)).values.at(0).to_string(), + "hello"); +} + +TEST(IR_2020_12, const_integer) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "const": 42 + })JSON")}; + + const auto result{sourcemeta::blaze::compile( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, sourcemeta::blaze::default_compiler)}; + + using namespace sourcemeta::blaze; + + EXPECT_EQ(result.size(), 1); + + EXPECT_TRUE(std::holds_alternative(result.at(0))); + EXPECT_AS_STRING(std::get(result.at(0)).pointer, ""); + EXPECT_SYMBOL(std::get(result.at(0)).symbol); + EXPECT_EQ(std::get(result.at(0)).values.size(), 1); + EXPECT_EQ(std::get(result.at(0)).values.at(0).to_integer(), + 42); +} + +TEST(IR_2020_12, const_boolean_true) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "const": true + })JSON")}; + + const auto result{sourcemeta::blaze::compile( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, sourcemeta::blaze::default_compiler)}; + + using namespace sourcemeta::blaze; + + EXPECT_EQ(result.size(), 1); + + EXPECT_TRUE(std::holds_alternative(result.at(0))); + EXPECT_AS_STRING(std::get(result.at(0)).pointer, ""); + EXPECT_SYMBOL(std::get(result.at(0)).symbol); + EXPECT_EQ(std::get(result.at(0)).values.size(), 1); + EXPECT_TRUE(std::get(result.at(0)).values.at(0).to_boolean()); +} + +TEST(IR_2020_12, object_type_only) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object" + })JSON")}; + + const auto result{sourcemeta::blaze::compile( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, sourcemeta::blaze::default_compiler)}; + + using namespace sourcemeta::blaze; + + EXPECT_EQ(result.size(), 1); + + EXPECT_TRUE(std::holds_alternative(result.at(0))); + EXPECT_AS_STRING(std::get(result.at(0)).pointer, ""); + EXPECT_SYMBOL(std::get(result.at(0)).symbol); + EXPECT_TRUE(std::get(result.at(0)).members.empty()); + 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) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": {} + })JSON")}; + + const auto result{sourcemeta::blaze::compile( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, sourcemeta::blaze::default_compiler)}; + + using namespace sourcemeta::blaze; + + EXPECT_EQ(result.size(), 1); + + EXPECT_TRUE(std::holds_alternative(result.at(0))); + EXPECT_AS_STRING(std::get(result.at(0)).pointer, ""); + EXPECT_SYMBOL(std::get(result.at(0)).symbol); + EXPECT_TRUE(std::get(result.at(0)).members.empty()); + 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) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "foo": { "type": "string" } + }, + "additionalProperties": { "type": "integer" } + })JSON")}; + + const auto result{sourcemeta::blaze::compile( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, sourcemeta::blaze::default_compiler)}; + + using namespace sourcemeta::blaze; + + EXPECT_EQ(result.size(), 3); + + EXPECT_IR_SCALAR(result, 0, String, "/properties/foo"); + EXPECT_SYMBOL(std::get(result.at(0)).symbol, "foo"); + EXPECT_IR_SCALAR(result, 1, Integer, "/additionalProperties"); + EXPECT_SYMBOL(std::get(result.at(1)).symbol, + "additionalProperties"); + + EXPECT_TRUE(std::holds_alternative(result.at(2))); + EXPECT_AS_STRING(std::get(result.at(2)).pointer, ""); + EXPECT_SYMBOL(std::get(result.at(2)).symbol); + EXPECT_EQ(std::get(result.at(2)).members.size(), 1); + EXPECT_TRUE(std::get(result.at(2)).members.at(0).first == "foo"); + EXPECT_FALSE(std::get(result.at(2)).members.at(0).second.required); + EXPECT_FALSE(std::get(result.at(2)).members.at(0).second.immutable); + EXPECT_AS_STRING( + std::get(result.at(2)).members.at(0).second.pointer, + "/properties/foo"); + EXPECT_SYMBOL(std::get(result.at(2)).members.at(0).second.symbol, + "foo"); + + EXPECT_TRUE(std::holds_alternative( + std::get(result.at(2)).additional)); + EXPECT_AS_STRING( + std::get(std::get(result.at(2)).additional).pointer, + "/additionalProperties"); + EXPECT_SYMBOL( + std::get(std::get(result.at(2)).additional).symbol, + "additionalProperties"); +} + +TEST(IR_2020_12, object_with_impossible_property) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "foo": false + } + })JSON")}; + + const auto result{sourcemeta::blaze::compile( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, sourcemeta::blaze::default_compiler)}; + + using namespace sourcemeta::blaze; + + EXPECT_EQ(result.size(), 2); + + EXPECT_IR_IMPOSSIBLE(result, 0, "/properties/foo"); + EXPECT_SYMBOL(std::get(result.at(0)).symbol, "foo"); + + EXPECT_TRUE(std::holds_alternative(result.at(1))); + EXPECT_AS_STRING(std::get(result.at(1)).pointer, ""); + EXPECT_SYMBOL(std::get(result.at(1)).symbol); + EXPECT_EQ(std::get(result.at(1)).members.size(), 1); + EXPECT_TRUE(std::get(result.at(1)).members.at(0).first == "foo"); + EXPECT_FALSE(std::get(result.at(1)).members.at(0).second.required); + EXPECT_FALSE(std::get(result.at(1)).members.at(0).second.immutable); + EXPECT_AS_STRING( + std::get(result.at(1)).members.at(0).second.pointer, + "/properties/foo"); + EXPECT_SYMBOL(std::get(result.at(1)).members.at(0).second.symbol, + "foo"); + 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) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "foo": { "type": "string" } + }, + "additionalProperties": false + })JSON")}; + + const auto result{sourcemeta::blaze::compile( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, sourcemeta::blaze::default_compiler)}; + + using namespace sourcemeta::blaze; + + EXPECT_EQ(result.size(), 3); + + EXPECT_IR_SCALAR(result, 0, String, "/properties/foo"); + EXPECT_SYMBOL(std::get(result.at(0)).symbol, "foo"); + + EXPECT_IR_IMPOSSIBLE(result, 1, "/additionalProperties"); + EXPECT_SYMBOL(std::get(result.at(1)).symbol, + "additionalProperties"); + + EXPECT_TRUE(std::holds_alternative(result.at(2))); + EXPECT_AS_STRING(std::get(result.at(2)).pointer, ""); + EXPECT_SYMBOL(std::get(result.at(2)).symbol); + EXPECT_EQ(std::get(result.at(2)).members.size(), 1); + EXPECT_TRUE(std::get(result.at(2)).members.at(0).first == "foo"); + EXPECT_FALSE(std::get(result.at(2)).members.at(0).second.required); + EXPECT_FALSE(std::get(result.at(2)).members.at(0).second.immutable); + EXPECT_AS_STRING( + std::get(result.at(2)).members.at(0).second.pointer, + "/properties/foo"); + EXPECT_SYMBOL(std::get(result.at(2)).members.at(0).second.symbol, + "foo"); + + 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) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "items": { "type": "string" } + })JSON")}; + + const auto result{sourcemeta::blaze::compile( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, sourcemeta::blaze::default_compiler)}; + + using namespace sourcemeta::blaze; + + EXPECT_EQ(result.size(), 2); + + EXPECT_IR_SCALAR(result, 0, String, "/items"); + EXPECT_SYMBOL(std::get(result.at(0)).symbol, "items"); + + EXPECT_IR_ARRAY(result, 1, "", "/items"); + EXPECT_SYMBOL(std::get(result.at(1)).symbol); + EXPECT_SYMBOL(std::get(result.at(1)).items->symbol, "items"); +} + +TEST(IR_2020_12, array_nested_in_object) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "tags": { + "type": "array", + "items": { "type": "string" } + } + } + })JSON")}; + + const auto result{sourcemeta::blaze::compile( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, sourcemeta::blaze::default_compiler)}; + + using namespace sourcemeta::blaze; + + EXPECT_EQ(result.size(), 3); + + EXPECT_IR_SCALAR(result, 0, String, "/properties/tags/items"); + EXPECT_SYMBOL(std::get(result.at(0)).symbol, "tags", "items"); + + EXPECT_IR_ARRAY(result, 1, "/properties/tags", "/properties/tags/items"); + EXPECT_SYMBOL(std::get(result.at(1)).symbol, "tags"); + EXPECT_SYMBOL(std::get(result.at(1)).items->symbol, "tags", "items"); + + EXPECT_TRUE(std::holds_alternative(result.at(2))); + EXPECT_AS_STRING(std::get(result.at(2)).pointer, ""); + EXPECT_SYMBOL(std::get(result.at(2)).symbol); +} + +TEST(IR_2020_12, tuple_with_prefix_items) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "prefixItems": [ + { "type": "string" }, + { "type": "integer" } + ] + })JSON")}; + + const auto result{sourcemeta::blaze::compile( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, sourcemeta::blaze::default_compiler)}; + + using namespace sourcemeta::blaze; + + EXPECT_EQ(result.size(), 4); + + EXPECT_IR_SCALAR(result, 0, Integer, "/prefixItems/1"); + EXPECT_SYMBOL(std::get(result.at(0)).symbol, "1"); + EXPECT_IR_SCALAR(result, 1, String, "/prefixItems/0"); + EXPECT_SYMBOL(std::get(result.at(1)).symbol, "0"); + EXPECT_IR_ANY(result, 2, "/items"); + EXPECT_SYMBOL(std::get(result.at(2)).symbol, "items"); + + EXPECT_TRUE(std::holds_alternative(result.at(3))); + EXPECT_AS_STRING(std::get(result.at(3)).pointer, ""); + EXPECT_SYMBOL(std::get(result.at(3)).symbol); + EXPECT_EQ(std::get(result.at(3)).items.size(), 2); + EXPECT_AS_STRING(std::get(result.at(3)).items.at(0).pointer, + "/prefixItems/0"); + EXPECT_SYMBOL(std::get(result.at(3)).items.at(0).symbol, "0"); + EXPECT_AS_STRING(std::get(result.at(3)).items.at(1).pointer, + "/prefixItems/1"); + EXPECT_SYMBOL(std::get(result.at(3)).items.at(1).symbol, "1"); + EXPECT_TRUE(std::get(result.at(3)).additional.has_value()); + EXPECT_AS_STRING(std::get(result.at(3)).additional->pointer, + "/items"); + EXPECT_SYMBOL(std::get(result.at(3)).additional->symbol, "items"); +} + +TEST(IR_2020_12, tuple_with_prefix_items_and_items) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "prefixItems": [ + { "type": "string" } + ], + "items": { "type": "boolean" } + })JSON")}; + + const auto result{sourcemeta::blaze::compile( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, sourcemeta::blaze::default_compiler)}; + + using namespace sourcemeta::blaze; + + EXPECT_EQ(result.size(), 3); + + EXPECT_IR_SCALAR(result, 0, String, "/prefixItems/0"); + EXPECT_SYMBOL(std::get(result.at(0)).symbol, "0"); + EXPECT_IR_SCALAR(result, 1, Boolean, "/items"); + EXPECT_SYMBOL(std::get(result.at(1)).symbol, "items"); + + EXPECT_TRUE(std::holds_alternative(result.at(2))); + EXPECT_AS_STRING(std::get(result.at(2)).pointer, ""); + EXPECT_SYMBOL(std::get(result.at(2)).symbol); + EXPECT_EQ(std::get(result.at(2)).items.size(), 1); + EXPECT_AS_STRING(std::get(result.at(2)).items.at(0).pointer, + "/prefixItems/0"); + EXPECT_SYMBOL(std::get(result.at(2)).items.at(0).symbol, "0"); + EXPECT_TRUE(std::get(result.at(2)).additional.has_value()); + EXPECT_AS_STRING(std::get(result.at(2)).additional->pointer, + "/items"); + EXPECT_SYMBOL(std::get(result.at(2)).additional->symbol, "items"); +} + +TEST(IR_2020_12, anyof_two_branches) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "anyOf": [ + { "type": "string" }, + { "type": "integer" } + ] + })JSON")}; + + const auto result{sourcemeta::blaze::compile( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, sourcemeta::blaze::default_compiler)}; + + using namespace sourcemeta::blaze; + + EXPECT_EQ(result.size(), 3); + + EXPECT_IR_SCALAR(result, 0, Integer, "/anyOf/1"); + EXPECT_SYMBOL(std::get(result.at(0)).symbol, "1"); + EXPECT_IR_SCALAR(result, 1, String, "/anyOf/0"); + EXPECT_SYMBOL(std::get(result.at(1)).symbol, "0"); + + EXPECT_TRUE(std::holds_alternative(result.at(2))); + EXPECT_AS_STRING(std::get(result.at(2)).pointer, ""); + EXPECT_SYMBOL(std::get(result.at(2)).symbol); + EXPECT_EQ(std::get(result.at(2)).values.size(), 2); + EXPECT_AS_STRING(std::get(result.at(2)).values.at(0).pointer, + "/anyOf/0"); + EXPECT_SYMBOL(std::get(result.at(2)).values.at(0).symbol, "0"); + EXPECT_AS_STRING(std::get(result.at(2)).values.at(1).pointer, + "/anyOf/1"); + EXPECT_SYMBOL(std::get(result.at(2)).values.at(1).symbol, "1"); +} + +TEST(IR_2020_12, anyof_three_branches) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "anyOf": [ + { "type": "string" }, + { "type": "integer" }, + { "type": "boolean" } + ] + })JSON")}; + + const auto result{sourcemeta::blaze::compile( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, sourcemeta::blaze::default_compiler)}; + + using namespace sourcemeta::blaze; + + EXPECT_EQ(result.size(), 4); + + EXPECT_IR_SCALAR(result, 0, Boolean, "/anyOf/2"); + EXPECT_SYMBOL(std::get(result.at(0)).symbol, "2"); + EXPECT_IR_SCALAR(result, 1, Integer, "/anyOf/1"); + EXPECT_SYMBOL(std::get(result.at(1)).symbol, "1"); + EXPECT_IR_SCALAR(result, 2, String, "/anyOf/0"); + EXPECT_SYMBOL(std::get(result.at(2)).symbol, "0"); + + EXPECT_TRUE(std::holds_alternative(result.at(3))); + EXPECT_AS_STRING(std::get(result.at(3)).pointer, ""); + EXPECT_SYMBOL(std::get(result.at(3)).symbol); + EXPECT_EQ(std::get(result.at(3)).values.size(), 3); + EXPECT_AS_STRING(std::get(result.at(3)).values.at(0).pointer, + "/anyOf/0"); + EXPECT_SYMBOL(std::get(result.at(3)).values.at(0).symbol, "0"); + EXPECT_AS_STRING(std::get(result.at(3)).values.at(1).pointer, + "/anyOf/1"); + EXPECT_SYMBOL(std::get(result.at(3)).values.at(1).symbol, "1"); + EXPECT_AS_STRING(std::get(result.at(3)).values.at(2).pointer, + "/anyOf/2"); + EXPECT_SYMBOL(std::get(result.at(3)).values.at(2).symbol, "2"); +} + +TEST(IR_2020_12, oneof_two_branches) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "oneOf": [ + { "type": "string" }, + { "type": "integer" } + ] + })JSON")}; + + const auto result{sourcemeta::blaze::compile( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, sourcemeta::blaze::default_compiler)}; + + using namespace sourcemeta::blaze; + + // Note: The canonicalizer transforms oneOf to anyOf + EXPECT_EQ(result.size(), 3); + + EXPECT_IR_SCALAR(result, 0, Integer, "/anyOf/1"); + EXPECT_SYMBOL(std::get(result.at(0)).symbol, "1"); + EXPECT_IR_SCALAR(result, 1, String, "/anyOf/0"); + EXPECT_SYMBOL(std::get(result.at(1)).symbol, "0"); + + EXPECT_TRUE(std::holds_alternative(result.at(2))); + EXPECT_AS_STRING(std::get(result.at(2)).pointer, ""); + EXPECT_SYMBOL(std::get(result.at(2)).symbol); + EXPECT_EQ(std::get(result.at(2)).values.size(), 2); + EXPECT_AS_STRING(std::get(result.at(2)).values.at(0).pointer, + "/anyOf/0"); + EXPECT_SYMBOL(std::get(result.at(2)).values.at(0).symbol, "0"); + EXPECT_AS_STRING(std::get(result.at(2)).values.at(1).pointer, + "/anyOf/1"); + EXPECT_SYMBOL(std::get(result.at(2)).values.at(1).symbol, "1"); +} + +TEST(IR_2020_12, oneof_three_branches) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "oneOf": [ + { "type": "string" }, + { "type": "integer" }, + { "type": "boolean" } + ] + })JSON")}; + + const auto result{sourcemeta::blaze::compile( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, sourcemeta::blaze::default_compiler)}; + + using namespace sourcemeta::blaze; + + // Note: The canonicalizer transforms oneOf to anyOf + EXPECT_EQ(result.size(), 4); + + EXPECT_IR_SCALAR(result, 0, Boolean, "/anyOf/2"); + EXPECT_SYMBOL(std::get(result.at(0)).symbol, "2"); + EXPECT_IR_SCALAR(result, 1, Integer, "/anyOf/1"); + EXPECT_SYMBOL(std::get(result.at(1)).symbol, "1"); + EXPECT_IR_SCALAR(result, 2, String, "/anyOf/0"); + EXPECT_SYMBOL(std::get(result.at(2)).symbol, "0"); + + EXPECT_TRUE(std::holds_alternative(result.at(3))); + EXPECT_AS_STRING(std::get(result.at(3)).pointer, ""); + EXPECT_SYMBOL(std::get(result.at(3)).symbol); + EXPECT_EQ(std::get(result.at(3)).values.size(), 3); + EXPECT_AS_STRING(std::get(result.at(3)).values.at(0).pointer, + "/anyOf/0"); + EXPECT_SYMBOL(std::get(result.at(3)).values.at(0).symbol, "0"); + EXPECT_AS_STRING(std::get(result.at(3)).values.at(1).pointer, + "/anyOf/1"); + EXPECT_SYMBOL(std::get(result.at(3)).values.at(1).symbol, "1"); + EXPECT_AS_STRING(std::get(result.at(3)).values.at(2).pointer, + "/anyOf/2"); + EXPECT_SYMBOL(std::get(result.at(3)).values.at(2).symbol, "2"); +} + +TEST(IR_2020_12, ref_recursive_to_root) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "child": { "$ref": "#" } + } + })JSON")}; + + const auto result{sourcemeta::blaze::compile( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, sourcemeta::blaze::default_compiler)}; + + using namespace sourcemeta::blaze; + + EXPECT_EQ(result.size(), 2); + + EXPECT_IR_REFERENCE(result, 0, "/properties/child", ""); + EXPECT_SYMBOL(std::get(result.at(0)).symbol, "child"); + EXPECT_SYMBOL(std::get(result.at(0)).target.symbol); + + EXPECT_TRUE(std::holds_alternative(result.at(1))); + EXPECT_AS_STRING(std::get(result.at(1)).pointer, ""); + EXPECT_SYMBOL(std::get(result.at(1)).symbol); + EXPECT_EQ(std::get(result.at(1)).members.size(), 1); + EXPECT_TRUE(std::get(result.at(1)).members.at(0).first == "child"); + EXPECT_FALSE(std::get(result.at(1)).members.at(0).second.required); + EXPECT_FALSE(std::get(result.at(1)).members.at(0).second.immutable); + EXPECT_AS_STRING( + std::get(result.at(1)).members.at(0).second.pointer, + "/properties/child"); + EXPECT_SYMBOL(std::get(result.at(1)).members.at(0).second.symbol, + "child"); + 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) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "nested": { + "type": "object", + "properties": { + "name": { "type": "string" } + }, + "required": [ "name" ] + } + } + })JSON")}; + + const auto result{sourcemeta::blaze::compile( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, sourcemeta::blaze::default_compiler)}; + + using namespace sourcemeta::blaze; + + EXPECT_EQ(result.size(), 3); + + EXPECT_IR_SCALAR(result, 0, String, "/properties/nested/properties/name"); + EXPECT_SYMBOL(std::get(result.at(0)).symbol, "nested", "name"); + + EXPECT_TRUE(std::holds_alternative(result.at(1))); + EXPECT_AS_STRING(std::get(result.at(1)).pointer, + "/properties/nested"); + EXPECT_SYMBOL(std::get(result.at(1)).symbol, "nested"); + EXPECT_EQ(std::get(result.at(1)).members.size(), 1); + EXPECT_TRUE(std::get(result.at(1)).members.at(0).first == "name"); + EXPECT_TRUE(std::get(result.at(1)).members.at(0).second.required); + EXPECT_FALSE(std::get(result.at(1)).members.at(0).second.immutable); + EXPECT_AS_STRING( + std::get(result.at(1)).members.at(0).second.pointer, + "/properties/nested/properties/name"); + EXPECT_SYMBOL(std::get(result.at(1)).members.at(0).second.symbol, + "nested", "name"); + 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, ""); + EXPECT_SYMBOL(std::get(result.at(2)).symbol); + EXPECT_EQ(std::get(result.at(2)).members.size(), 1); + EXPECT_TRUE(std::get(result.at(2)).members.at(0).first == "nested"); + EXPECT_FALSE(std::get(result.at(2)).members.at(0).second.required); + EXPECT_FALSE(std::get(result.at(2)).members.at(0).second.immutable); + EXPECT_AS_STRING( + std::get(result.at(2)).members.at(0).second.pointer, + "/properties/nested"); + EXPECT_SYMBOL(std::get(result.at(2)).members.at(0).second.symbol, + "nested"); + 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) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "minItems": 0 + })JSON")}; + + const auto result{sourcemeta::blaze::compile( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, sourcemeta::blaze::default_compiler)}; + + using namespace sourcemeta::blaze; + + // Note: The canonicalizer now adds `items: true` for arrays without items + EXPECT_EQ(result.size(), 2); + + EXPECT_IR_ANY(result, 0, "/items"); + EXPECT_SYMBOL(std::get(result.at(0)).symbol, "items"); + + EXPECT_TRUE(std::holds_alternative(result.at(1))); + EXPECT_AS_STRING(std::get(result.at(1)).pointer, ""); + EXPECT_SYMBOL(std::get(result.at(1)).symbol); + EXPECT_TRUE(std::get(result.at(1)).items.has_value()); + EXPECT_AS_STRING(std::get(result.at(1)).items->pointer, "/items"); + EXPECT_SYMBOL(std::get(result.at(1)).items->symbol, "items"); +} + +TEST(IR_2020_12, object_with_additional_properties_true) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "name": { "type": "string" } + }, + "additionalProperties": true + })JSON")}; + + const auto result{sourcemeta::blaze::compile( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, sourcemeta::blaze::default_compiler)}; + + using namespace sourcemeta::blaze; + + // Note: The canonicalizer now keeps additionalProperties: true as IRAny + // instead of expanding it into a union of all types + EXPECT_EQ(result.size(), 3); + + EXPECT_IR_SCALAR(result, 0, String, "/properties/name"); + EXPECT_SYMBOL(std::get(result.at(0)).symbol, "name"); + EXPECT_IR_ANY(result, 1, "/additionalProperties"); + EXPECT_SYMBOL(std::get(result.at(1)).symbol, "additionalProperties"); + + EXPECT_TRUE(std::holds_alternative(result.at(2))); + EXPECT_AS_STRING(std::get(result.at(2)).pointer, ""); + EXPECT_SYMBOL(std::get(result.at(2)).symbol); + EXPECT_EQ(std::get(result.at(2)).members.size(), 1); + EXPECT_TRUE(std::get(result.at(2)).members.at(0).first == "name"); + EXPECT_FALSE(std::get(result.at(2)).members.at(0).second.required); + EXPECT_FALSE(std::get(result.at(2)).members.at(0).second.immutable); + EXPECT_AS_STRING( + std::get(result.at(2)).members.at(0).second.pointer, + "/properties/name"); + EXPECT_SYMBOL(std::get(result.at(2)).members.at(0).second.symbol, + "name"); + // When additionalProperties is a boolean schema, the object stores the + // boolean value directly (while the schema itself is compiled as IRAny) + 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, object_only_additional_properties) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "additionalProperties": { "type": "boolean" } + })JSON")}; + + const auto result{sourcemeta::blaze::compile( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, sourcemeta::blaze::default_compiler)}; + + using namespace sourcemeta::blaze; + + EXPECT_EQ(result.size(), 2); + + EXPECT_IR_SCALAR(result, 0, Boolean, "/additionalProperties"); + EXPECT_SYMBOL(std::get(result.at(0)).symbol, + "additionalProperties"); + + EXPECT_TRUE(std::holds_alternative(result.at(1))); + EXPECT_AS_STRING(std::get(result.at(1)).pointer, ""); + EXPECT_SYMBOL(std::get(result.at(1)).symbol); + EXPECT_EQ(std::get(result.at(1)).members.size(), 0); + EXPECT_TRUE(std::holds_alternative( + std::get(result.at(1)).additional)); + EXPECT_AS_STRING( + std::get(std::get(result.at(1)).additional).pointer, + "/additionalProperties"); + EXPECT_SYMBOL( + std::get(std::get(result.at(1)).additional).symbol, + "additionalProperties"); +} + +TEST(IR_2020_12, embedded_resource_with_nested_id_no_duplicates) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/main", + "type": "object", + "required": [ "item" ], + "properties": { + "item": { "$ref": "https://example.com/item" } + }, + "additionalProperties": false, + "$defs": { + "Item": { + "$id": "https://example.com/item", + "type": "object", + "required": [ "name" ], + "properties": { + "name": { "type": "string" } + }, + "additionalProperties": false + } + } + })JSON")}; + + const auto result{sourcemeta::blaze::compile( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, sourcemeta::blaze::default_compiler)}; + + using namespace sourcemeta::blaze; + + EXPECT_EQ(result.size(), 6); + + EXPECT_IR_REFERENCE(result, 0, "/properties/item", "/$defs/Item"); + EXPECT_SYMBOL(std::get(result.at(0)).symbol, "item"); + EXPECT_SYMBOL(std::get(result.at(0)).target.symbol, "Item"); + + EXPECT_IR_IMPOSSIBLE(result, 1, "/additionalProperties"); + EXPECT_SYMBOL(std::get(result.at(1)).symbol, + "additionalProperties"); + + EXPECT_IR_SCALAR(result, 2, String, "/$defs/Item/properties/name"); + EXPECT_SYMBOL(std::get(result.at(2)).symbol, "Item", "name"); + + EXPECT_IR_IMPOSSIBLE(result, 3, "/$defs/Item/additionalProperties"); + EXPECT_SYMBOL(std::get(result.at(3)).symbol, "Item", + "additionalProperties"); + + EXPECT_TRUE(std::holds_alternative(result.at(4))); + EXPECT_AS_STRING(std::get(result.at(4)).pointer, "/$defs/Item"); + EXPECT_SYMBOL(std::get(result.at(4)).symbol, "Item"); + EXPECT_EQ(std::get(result.at(4)).members.size(), 1); + EXPECT_EQ(std::get(result.at(4)).members.at(0).first, "name"); + EXPECT_TRUE(std::get(result.at(4)).members.at(0).second.required); + EXPECT_AS_STRING( + std::get(result.at(4)).members.at(0).second.pointer, + "/$defs/Item/properties/name"); + EXPECT_SYMBOL(std::get(result.at(4)).members.at(0).second.symbol, + "Item", "name"); + 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, ""); + EXPECT_SYMBOL(std::get(result.at(5)).symbol); + EXPECT_EQ(std::get(result.at(5)).members.size(), 1); + EXPECT_EQ(std::get(result.at(5)).members.at(0).first, "item"); + EXPECT_TRUE(std::get(result.at(5)).members.at(0).second.required); + EXPECT_AS_STRING( + std::get(result.at(5)).members.at(0).second.pointer, + "/properties/item"); + EXPECT_SYMBOL(std::get(result.at(5)).members.at(0).second.symbol, + "item"); + EXPECT_TRUE(std::holds_alternative( + std::get(result.at(5)).additional)); + EXPECT_FALSE(std::get(std::get(result.at(5)).additional)); +} + +TEST(IR_2020_12, boolean_true_schema) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "anything": true + } + })JSON")}; + + const auto result{sourcemeta::blaze::compile( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, sourcemeta::blaze::default_compiler)}; + + using namespace sourcemeta::blaze; + + EXPECT_EQ(result.size(), 2); + + EXPECT_IR_ANY(result, 0, "/properties/anything"); + EXPECT_SYMBOL(std::get(result.at(0)).symbol, "anything"); + + EXPECT_TRUE(std::holds_alternative(result.at(1))); + EXPECT_AS_STRING(std::get(result.at(1)).pointer, ""); + EXPECT_SYMBOL(std::get(result.at(1)).symbol); + EXPECT_EQ(std::get(result.at(1)).members.size(), 1); + EXPECT_EQ(std::get(result.at(1)).members.at(0).first, "anything"); + EXPECT_FALSE(std::get(result.at(1)).members.at(0).second.required); + EXPECT_FALSE(std::get(result.at(1)).members.at(0).second.immutable); + EXPECT_AS_STRING( + std::get(result.at(1)).members.at(0).second.pointer, + "/properties/anything"); + EXPECT_SYMBOL(std::get(result.at(1)).members.at(0).second.symbol, + "anything"); + 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, boolean_false_schema) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "nothing": false + } + })JSON")}; + + const auto result{sourcemeta::blaze::compile( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, sourcemeta::blaze::default_compiler)}; + + using namespace sourcemeta::blaze; + + EXPECT_EQ(result.size(), 2); + + EXPECT_IR_IMPOSSIBLE(result, 0, "/properties/nothing"); + EXPECT_SYMBOL(std::get(result.at(0)).symbol, "nothing"); + + EXPECT_TRUE(std::holds_alternative(result.at(1))); + EXPECT_AS_STRING(std::get(result.at(1)).pointer, ""); + EXPECT_SYMBOL(std::get(result.at(1)).symbol); + EXPECT_EQ(std::get(result.at(1)).members.size(), 1); + EXPECT_EQ(std::get(result.at(1)).members.at(0).first, "nothing"); + EXPECT_FALSE(std::get(result.at(1)).members.at(0).second.required); + EXPECT_FALSE(std::get(result.at(1)).members.at(0).second.immutable); + EXPECT_AS_STRING( + std::get(result.at(1)).members.at(0).second.pointer, + "/properties/nothing"); + EXPECT_SYMBOL(std::get(result.at(1)).members.at(0).second.symbol, + "nothing"); + 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_pattern_properties_prefix) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "name": { "type": "string" } + }, + "patternProperties": { + "^x-": { "type": "string" } + } + })JSON")}; + + const auto result{sourcemeta::blaze::compile( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, sourcemeta::blaze::default_compiler)}; + + using namespace sourcemeta::blaze; + + EXPECT_EQ(result.size(), 3); + + EXPECT_IR_SCALAR(result, 0, String, "/properties/name"); + EXPECT_SYMBOL(std::get(result.at(0)).symbol, "name"); + + EXPECT_IR_SCALAR(result, 1, String, "/patternProperties/^x-"); + + EXPECT_TRUE(std::holds_alternative(result.at(2))); + const auto &object{std::get(result.at(2))}; + EXPECT_AS_STRING(object.pointer, ""); + EXPECT_EQ(object.members.size(), 1); + EXPECT_EQ(object.members.at(0).first, "name"); + + EXPECT_EQ(object.pattern.size(), 1); + EXPECT_AS_STRING(object.pattern.at(0).pointer, "/patternProperties/^x-"); + EXPECT_EQ(object.pattern.at(0).prefix, "x-"); + + EXPECT_TRUE(std::holds_alternative(object.additional)); + EXPECT_TRUE(std::get(object.additional)); +} + +TEST(IR_2020_12, object_with_multiple_pattern_properties) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "patternProperties": { + "^x-": { "type": "string" }, + "^data-": { "type": "integer" } + } + })JSON")}; + + const auto result{sourcemeta::blaze::compile( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, sourcemeta::blaze::default_compiler)}; + + using namespace sourcemeta::blaze; + + ASSERT_FALSE(result.empty()); + EXPECT_TRUE(std::holds_alternative(result.back())); + const auto &object{std::get(result.back())}; + EXPECT_EQ(object.pattern.size(), 2); + EXPECT_EQ(object.pattern.at(0).prefix, "x-"); + EXPECT_EQ(object.pattern.at(1).prefix, "data-"); +} + +TEST(IR_2020_12, object_with_pattern_properties_and_additional_false) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "name": { "type": "string" } + }, + "patternProperties": { + "^x-": { "type": "string" } + }, + "additionalProperties": false + })JSON")}; + + const auto result{sourcemeta::blaze::compile( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, sourcemeta::blaze::default_compiler)}; + + using namespace sourcemeta::blaze; + + ASSERT_FALSE(result.empty()); + EXPECT_TRUE(std::holds_alternative(result.back())); + const auto &object{std::get(result.back())}; + EXPECT_EQ(object.members.size(), 1); + EXPECT_EQ(object.pattern.size(), 1); + EXPECT_EQ(object.pattern.at(0).prefix, "x-"); + EXPECT_TRUE(std::holds_alternative(object.additional)); + EXPECT_FALSE(std::get(object.additional)); +} + +TEST(IR_2020_12, object_with_non_prefix_pattern_properties) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "patternProperties": { + "[a-z]+_id": { "type": "integer" } + } + })JSON")}; + + const auto result{sourcemeta::blaze::compile( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, sourcemeta::blaze::default_compiler)}; + + using namespace sourcemeta::blaze; + + ASSERT_FALSE(result.empty()); + EXPECT_TRUE(std::holds_alternative(result.back())); + const auto &object{std::get(result.back())}; + EXPECT_EQ(object.pattern.size(), 1); + EXPECT_FALSE(object.pattern.at(0).prefix.has_value()); +} + +TEST(IR_2020_12, object_with_mixed_prefix_and_non_prefix_patterns) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "patternProperties": { + "^x-": { "type": "string" }, + "[0-9]+": { "type": "integer" } + } + })JSON")}; + + const auto result{sourcemeta::blaze::compile( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, sourcemeta::blaze::default_compiler)}; + + using namespace sourcemeta::blaze; + + ASSERT_FALSE(result.empty()); + EXPECT_TRUE(std::holds_alternative(result.back())); + const auto &object{std::get(result.back())}; + EXPECT_EQ(object.pattern.size(), 2); + EXPECT_TRUE(object.pattern.at(0).prefix.has_value()); + EXPECT_EQ(object.pattern.at(0).prefix.value(), "x-"); + EXPECT_FALSE(object.pattern.at(1).prefix.has_value()); +} + +TEST(IR_2020_12, dynamic_ref_single_anchor) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "items": { "$dynamicRef": "#item" }, + "$defs": { + "foo": { + "$dynamicAnchor": "item", + "type": "string" + } + } + })JSON")}; + + const auto result{sourcemeta::blaze::compile( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, sourcemeta::blaze::default_compiler)}; + + using namespace sourcemeta::blaze; + + ASSERT_FALSE(result.empty()); + EXPECT_IR_REFERENCE(result, 0, "/items", "/$defs/foo"); + EXPECT_IR_SCALAR(result, 1, String, "/$defs/foo"); +} + +TEST(IR_2020_12, dynamic_ref_multiple_anchors) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/root", + "$ref": "list", + "$defs": { + "stringItem": { + "$dynamicAnchor": "item", + "type": "string" + }, + "list": { + "$id": "list", + "type": "array", + "items": { "$dynamicRef": "#item" }, + "$defs": { + "defaultItem": { + "$dynamicAnchor": "item", + "type": "number" + } + } + } + } + })JSON")}; + + const auto result{sourcemeta::blaze::compile( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, sourcemeta::blaze::default_compiler)}; + + using namespace sourcemeta::blaze; + + ASSERT_EQ(result.size(), 6); + EXPECT_IR_REFERENCE(result, 0, "/allOf/0", "/$defs/list"); + EXPECT_IR_SCALAR(result, 1, String, "/$defs/stringItem"); + EXPECT_TRUE(std::holds_alternative(result.at(2))); + EXPECT_AS_STRING(std::get(result.at(2)).pointer, + "/$defs/list/items"); + EXPECT_EQ(std::get(result.at(2)).values.size(), 2); + EXPECT_IR_SCALAR(result, 3, Number, "/$defs/list/$defs/defaultItem"); + EXPECT_IR_ARRAY(result, 4, "/$defs/list", "/$defs/list/items"); + EXPECT_IR_REFERENCE(result, 5, "", "/allOf/0"); +} + +TEST(IR_2020_12, dynamic_anchor_on_typed_schema) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$dynamicAnchor": "item", + "type": "string" + })JSON")}; + + const auto result{sourcemeta::blaze::compile( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, sourcemeta::blaze::default_compiler)}; + + using namespace sourcemeta::blaze; + + EXPECT_EQ(result.size(), 1); + EXPECT_IR_SCALAR(result, 0, String, ""); +} + +TEST(IR_2020_12, allof_two_objects) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "allOf": [ + { + "type": "object", + "properties": { "name": { "type": "string" } }, + "required": [ "name" ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { "age": { "type": "integer" } }, + "required": [ "age" ], + "additionalProperties": false + } + ] + })JSON")}; + + const auto result{sourcemeta::blaze::compile( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, sourcemeta::blaze::default_compiler)}; + + using namespace sourcemeta::blaze; + + ASSERT_EQ(result.size(), 7); + EXPECT_IR_INTERSECTION(result, 6, "", 2); +} + +TEST(IR_2020_12, allof_ref_and_object) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "allOf": [ + { "$ref": "#/$defs/Base" }, + { + "type": "object", + "properties": { "extra": { "type": "string" } }, + "additionalProperties": false + } + ], + "$defs": { + "Base": { + "type": "object", + "properties": { "id": { "type": "integer" } }, + "required": [ "id" ], + "additionalProperties": false + } + } + })JSON")}; + + const auto result{sourcemeta::blaze::compile( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, sourcemeta::blaze::default_compiler)}; + + using namespace sourcemeta::blaze; + + EXPECT_IR_INTERSECTION(result, result.size() - 1, "", 2); +} + +TEST(IR_2020_12, allof_single_element) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "allOf": [ + { "type": "string" } + ] + })JSON")}; + + const auto result{sourcemeta::blaze::compile( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, sourcemeta::blaze::default_compiler)}; + + using namespace sourcemeta::blaze; + + // The canonicalizer inlines allOf with a single element + ASSERT_EQ(result.size(), 1); + EXPECT_IR_SCALAR(result, 0, String, ""); +} + +TEST(IR_2020_12, allof_three_branches) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "allOf": [ + { + "type": "object", + "properties": { "a": { "type": "string" } }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { "b": { "type": "integer" } }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { "c": { "type": "number" } }, + "additionalProperties": false + } + ] + })JSON")}; + + const auto result{sourcemeta::blaze::compile( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, sourcemeta::blaze::default_compiler)}; + + using namespace sourcemeta::blaze; + + EXPECT_IR_INTERSECTION(result, result.size() - 1, "", 3); +} + +TEST(IR_2020_12, allof_with_defs) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Named": { + "type": "object", + "properties": { "name": { "type": "string" } }, + "required": [ "name" ], + "additionalProperties": false + }, + "Aged": { + "type": "object", + "properties": { "age": { "type": "integer" } }, + "required": [ "age" ], + "additionalProperties": false + } + }, + "allOf": [ + { "$ref": "#/$defs/Named" }, + { "$ref": "#/$defs/Aged" } + ] + })JSON")}; + + const auto result{sourcemeta::blaze::compile( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, sourcemeta::blaze::default_compiler)}; + + using namespace sourcemeta::blaze; + + EXPECT_IR_INTERSECTION(result, result.size() - 1, "", 2); + EXPECT_IR_REFERENCE(result, 0, "/allOf/1", "/$defs/Aged"); + EXPECT_IR_REFERENCE(result, 1, "/allOf/0", "/$defs/Named"); +} + +TEST(IR_2020_12, if_then_else_distinct_object_branches) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "if": { + "type": "object", + "properties": { "kind": { "const": "circle" } }, + "required": [ "kind" ] + }, + "then": { + "type": "object", + "properties": { "radius": { "type": "number" } }, + "required": [ "radius" ] + }, + "else": { + "type": "object", + "properties": { "sides": { "type": "integer" } }, + "required": [ "sides" ] + } + })JSON")}; + + const auto result{sourcemeta::blaze::compile( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, sourcemeta::blaze::default_compiler)}; + + using namespace sourcemeta::blaze; + + EXPECT_IR_CONDITIONAL(result, result.size() - 1, "", "/if", "/then", "/else"); +} + +TEST(IR_2020_12, if_then_else_implicit_else) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "if": { "type": "string" }, + "then": { "type": "string", "minLength": 1 } + })JSON")}; + + const auto result{sourcemeta::blaze::compile( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, sourcemeta::blaze::default_compiler)}; + + using namespace sourcemeta::blaze; + + EXPECT_IR_CONDITIONAL(result, result.size() - 1, "", "/if", "/then", "/else"); +} + +TEST(IR_2020_12, if_then_else_with_type_sibling) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "if": { "type": "string", "maxLength": 10 }, + "then": { "type": "string", "pattern": "^short" }, + "else": { "type": "string", "pattern": "^long" } + })JSON")}; + + const auto result{sourcemeta::blaze::compile( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, sourcemeta::blaze::default_compiler)}; + + using namespace sourcemeta::blaze; + + EXPECT_IR_CONDITIONAL(result, result.size() - 2, "/allOf/0", "/allOf/0/if", + "/allOf/0/then", "/allOf/0/else"); + EXPECT_IR_INTERSECTION(result, result.size() - 1, "", 2); +} + +TEST(IR_2020_12, if_then_else_with_ref_branches) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Circle": { + "type": "object", + "properties": { "radius": { "type": "number" } }, + "required": [ "radius" ], + "additionalProperties": false + }, + "Square": { + "type": "object", + "properties": { "side": { "type": "number" } }, + "required": [ "side" ], + "additionalProperties": false + } + }, + "if": { + "type": "object", + "properties": { "kind": { "const": "circle" } }, + "required": [ "kind" ] + }, + "then": { "$ref": "#/$defs/Circle" }, + "else": { "$ref": "#/$defs/Square" } + })JSON")}; + + const auto result{sourcemeta::blaze::compile( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, sourcemeta::blaze::default_compiler)}; + + using namespace sourcemeta::blaze; + + ASSERT_EQ(result.size(), 11); + EXPECT_IR_REFERENCE(result, 0, "/then", "/$defs/Circle"); + EXPECT_IR_REFERENCE(result, 3, "/else", "/$defs/Square"); + EXPECT_IR_CONDITIONAL(result, 10, "", "/if", "/then", "/else"); +} + +TEST(IR_2020_12, if_then_else_nested_in_object_property) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "value": { + "if": { "type": "string" }, + "then": { "type": "string", "minLength": 1 }, + "else": { "type": "integer" } + } + }, + "additionalProperties": false + })JSON")}; + + const auto result{sourcemeta::blaze::compile( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, sourcemeta::blaze::default_compiler)}; + + using namespace sourcemeta::blaze; + + ASSERT_EQ(result.size(), 6); + EXPECT_IR_SCALAR(result, 0, String, "/properties/value/then"); + EXPECT_IR_SCALAR(result, 1, String, "/properties/value/if"); + EXPECT_IR_SCALAR(result, 2, Integer, "/properties/value/else"); + EXPECT_IR_CONDITIONAL(result, 3, "/properties/value", "/properties/value/if", + "/properties/value/then", "/properties/value/else"); +} diff --git a/test/codegen/codegen_symbol_test.cc b/test/codegen/codegen_symbol_test.cc new file mode 100644 index 000000000..3ed10a8fe --- /dev/null +++ b/test/codegen/codegen_symbol_test.cc @@ -0,0 +1,121 @@ +#include + +#include + +#include + +TEST(IR_Symbol, nested_additional_properties_items) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + })JSON")}; + + sourcemeta::core::SchemaFrame frame{ + sourcemeta::core::SchemaFrame::Mode::References}; + frame.analyse(schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, + "https://json-schema.org/draft/2020-12/schema"); + + const auto location{ + frame.traverse("#/properties/data/additionalProperties/items")}; + EXPECT_TRUE(location.has_value()); + + const auto result{sourcemeta::blaze::symbol(frame, location.value().get())}; + + EXPECT_EQ(result.size(), 3); + EXPECT_EQ(result.at(0), "data"); + EXPECT_EQ(result.at(1), "additionalProperties"); + EXPECT_EQ(result.at(2), "items"); +} + +TEST(IR_Symbol, inside_defs) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "MyType": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + } + } + })JSON")}; + + sourcemeta::core::SchemaFrame frame{ + sourcemeta::core::SchemaFrame::Mode::References}; + frame.analyse(schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, + "https://json-schema.org/draft/2020-12/schema"); + + const auto location{frame.traverse("#/$defs/MyType/properties/name")}; + EXPECT_TRUE(location.has_value()); + + const auto result{sourcemeta::blaze::symbol(frame, location.value().get())}; + + EXPECT_EQ(result.size(), 2); + EXPECT_EQ(result.at(0), "MyType"); + EXPECT_EQ(result.at(1), "name"); +} + +TEST(IR_Symbol, property_named_properties) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "properties": { + "type": "string" + } + } + })JSON")}; + + sourcemeta::core::SchemaFrame frame{ + sourcemeta::core::SchemaFrame::Mode::References}; + frame.analyse(schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, + "https://json-schema.org/draft/2020-12/schema"); + + const auto location{frame.traverse("#/properties/properties")}; + EXPECT_TRUE(location.has_value()); + + const auto result{sourcemeta::blaze::symbol(frame, location.value().get())}; + + EXPECT_EQ(result.size(), 1); + EXPECT_EQ(result.at(0), "properties"); +} + +TEST(IR_Symbol, anyof_child) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "anyOf": [ + { "type": "string" }, + { "type": "number" } + ] + })JSON")}; + + sourcemeta::core::SchemaFrame frame{ + sourcemeta::core::SchemaFrame::Mode::References}; + frame.analyse(schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, + "https://json-schema.org/draft/2020-12/schema"); + + const auto location{frame.traverse("#/anyOf/1")}; + EXPECT_TRUE(location.has_value()); + + const auto result{sourcemeta::blaze::symbol(frame, location.value().get())}; + + EXPECT_EQ(result.size(), 1); + EXPECT_EQ(result.at(0), "1"); +} diff --git a/test/codegen/codegen_test.cc b/test/codegen/codegen_test.cc new file mode 100644 index 000000000..d1ce489d6 --- /dev/null +++ b/test/codegen/codegen_test.cc @@ -0,0 +1,56 @@ +#include + +#include +#include + +TEST(IR, unsupported_dialect_draft3) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-03/schema#", + "type": "string" + })JSON")}; + + EXPECT_THROW(sourcemeta::blaze::compile(schema, + sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, + sourcemeta::blaze::default_compiler), + sourcemeta::core::SchemaVocabularyError); +} + +TEST(IR, unsupported_keyword_error_not) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "not": { "type": "string" } + })JSON")}; + + EXPECT_THROW(sourcemeta::blaze::compile(schema, + sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, + sourcemeta::blaze::default_compiler), + sourcemeta::blaze::UnsupportedKeywordError); +} + +TEST(IR, unsupported_keyword_value_error_type_not_string) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": 123 + })JSON")}; + + EXPECT_THROW(sourcemeta::blaze::compile(schema, + sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, + sourcemeta::blaze::default_compiler), + sourcemeta::blaze::UnsupportedKeywordValueError); +} + +TEST(IR, unsupported_keyword_value_error_unknown_type) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "foo" + })JSON")}; + + EXPECT_THROW(sourcemeta::blaze::compile(schema, + sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, + sourcemeta::blaze::default_compiler), + sourcemeta::blaze::UnsupportedKeywordValueError); +} diff --git a/test/codegen/codegen_test_utils.h b/test/codegen/codegen_test_utils.h new file mode 100644 index 000000000..b3ff08265 --- /dev/null +++ b/test/codegen/codegen_test_utils.h @@ -0,0 +1,98 @@ +#ifndef SOURCEMETA_BLAZE_CODEGEN_TEST_UTILS_H_ +#define SOURCEMETA_BLAZE_CODEGEN_TEST_UTILS_H_ + +// TODO: Have macros for objects and enumerations + +#define EXPECT_AS_STRING(actual, expected) \ + EXPECT_EQ(sourcemeta::core::to_string(actual), expected) + +#define EXPECT_SYMBOL(actual, ...) \ + EXPECT_EQ(actual, (std::vector{__VA_ARGS__})) + +#define EXPECT_IR_SCALAR(result, index, scalar_type, expected_pointer) \ + EXPECT_TRUE( \ + std::holds_alternative(result.at(index))) \ + << "Expected IRScalar at index " << index; \ + EXPECT_AS_STRING( \ + std::get(result.at(index)).pointer, \ + expected_pointer); \ + EXPECT_EQ(std::get(result.at(index)).value, \ + sourcemeta::blaze::IRScalarType::scalar_type) + +#define EXPECT_IR_IMPOSSIBLE(result, index, expected_pointer) \ + EXPECT_TRUE(std::holds_alternative( \ + result.at(index))) \ + << "Expected IRImpossible at index " << index; \ + EXPECT_AS_STRING( \ + std::get(result.at(index)).pointer, \ + expected_pointer) + +#define EXPECT_IR_ANY(result, index, expected_pointer) \ + EXPECT_TRUE( \ + std::holds_alternative(result.at(index))) \ + << "Expected IRAny at index " << index; \ + EXPECT_AS_STRING( \ + std::get(result.at(index)).pointer, \ + expected_pointer) + +#define EXPECT_IR_ARRAY(result, index, expected_pointer, \ + expected_items_pointer) \ + EXPECT_TRUE( \ + std::holds_alternative(result.at(index))) \ + << "Expected IRArray at index " << index; \ + EXPECT_AS_STRING( \ + std::get(result.at(index)).pointer, \ + expected_pointer); \ + EXPECT_TRUE(std::get(result.at(index)) \ + .items.has_value()) \ + << "Expected IRArray items to have a value"; \ + EXPECT_AS_STRING( \ + std::get(result.at(index)).items->pointer, \ + expected_items_pointer) + +#define EXPECT_IR_INTERSECTION(result, index, expected_pointer, \ + expected_count) \ + EXPECT_TRUE(std::holds_alternative( \ + result.at(index))) \ + << "Expected IRIntersection at index " << index; \ + EXPECT_AS_STRING( \ + std::get(result.at(index)).pointer, \ + expected_pointer); \ + EXPECT_EQ(std::get(result.at(index)) \ + .values.size(), \ + expected_count) + +#define EXPECT_IR_CONDITIONAL(result, index, expected_pointer, expected_if, \ + expected_then, expected_else) \ + EXPECT_TRUE(std::holds_alternative( \ + result.at(index))) \ + << "Expected IRConditional at index " << index; \ + EXPECT_AS_STRING( \ + std::get(result.at(index)).pointer, \ + expected_pointer); \ + EXPECT_AS_STRING( \ + std::get(result.at(index)) \ + .condition.pointer, \ + expected_if); \ + EXPECT_AS_STRING( \ + std::get(result.at(index)) \ + .consequent.pointer, \ + expected_then); \ + EXPECT_AS_STRING( \ + std::get(result.at(index)) \ + .alternative.pointer, \ + expected_else) + +#define EXPECT_IR_REFERENCE(result, index, expected_pointer, \ + expected_target_pointer) \ + EXPECT_TRUE(std::holds_alternative( \ + result.at(index))) \ + << "Expected IRReference at index " << index; \ + EXPECT_AS_STRING( \ + std::get(result.at(index)).pointer, \ + expected_pointer); \ + EXPECT_AS_STRING(std::get(result.at(index)) \ + .target.pointer, \ + expected_target_pointer) + +#endif diff --git a/test/codegen/e2e/typescript/2020-12/additional_properties_false/expected.d.ts b/test/codegen/e2e/typescript/2020-12/additional_properties_false/expected.d.ts new file mode 100644 index 000000000..0bf6b6756 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/additional_properties_false/expected.d.ts @@ -0,0 +1,10 @@ +export type StrictPersonName = string; + +export type StrictPersonAge = number; + +export type StrictPersonAdditionalProperties = never; + +export interface StrictPerson { + "name": StrictPersonName; + "age"?: StrictPersonAge; +} diff --git a/test/codegen/e2e/typescript/2020-12/additional_properties_false/options.json b/test/codegen/e2e/typescript/2020-12/additional_properties_false/options.json new file mode 100644 index 000000000..9fb0fdb13 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/additional_properties_false/options.json @@ -0,0 +1,3 @@ +{ + "defaultPrefix": "StrictPerson" +} diff --git a/test/codegen/e2e/typescript/2020-12/additional_properties_false/schema.json b/test/codegen/e2e/typescript/2020-12/additional_properties_false/schema.json new file mode 100644 index 000000000..d1feac2fe --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/additional_properties_false/schema.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "age": { + "type": "integer" + } + }, + "required": [ "name" ], + "additionalProperties": false +} diff --git a/test/codegen/e2e/typescript/2020-12/additional_properties_false/test.ts b/test/codegen/e2e/typescript/2020-12/additional_properties_false/test.ts new file mode 100644 index 000000000..94d036c22 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/additional_properties_false/test.ts @@ -0,0 +1,73 @@ +import { StrictPerson } from "./expected"; + +// Valid: required name only +const person1: StrictPerson = { + name: "John Doe" +}; + +// Valid: name and optional age +const person2: StrictPerson = { + name: "Jane Doe", + age: 25 +}; + +// Invalid: name must be string +const person3: StrictPerson = { + // @ts-expect-error + name: 123 +}; + +// Invalid: age must be number +const person4: StrictPerson = { + name: "John", + // @ts-expect-error + age: "twenty" +}; + +// Invalid: missing required name +// @ts-expect-error +const person5: StrictPerson = { + age: 30 +}; + +// Invalid: extra string property should be rejected +const person6: StrictPerson = { + name: "John", + // @ts-expect-error + nickname: "Johnny" +}; + +// Invalid: extra number property should be rejected +const person7: StrictPerson = { + name: "John", + // @ts-expect-error + score: 100 +}; + +// Invalid: extra boolean property should be rejected +const person8: StrictPerson = { + name: "John", + // @ts-expect-error + active: true +}; + +// Invalid: extra null property should be rejected +const person9: StrictPerson = { + name: "John", + // @ts-expect-error + nothing: null +}; + +// Invalid: extra array property should be rejected +const person10: StrictPerson = { + name: "John", + // @ts-expect-error + tags: [ "a", "b" ] +}; + +// Invalid: extra object property should be rejected +const person11: StrictPerson = { + name: "John", + // @ts-expect-error + metadata: { foo: "bar" } +}; diff --git a/test/codegen/e2e/typescript/2020-12/additional_properties_true/expected.d.ts b/test/codegen/e2e/typescript/2020-12/additional_properties_true/expected.d.ts new file mode 100644 index 000000000..06ae7dfb4 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/additional_properties_true/expected.d.ts @@ -0,0 +1,11 @@ +export type FlexibleRecordName = string; + +export type FlexibleRecordCount = number; + +export type FlexibleRecordAdditionalProperties = unknown; + +export interface FlexibleRecord { + "name": FlexibleRecordName; + "count"?: FlexibleRecordCount; + [key: string]: unknown | undefined; +} diff --git a/test/codegen/e2e/typescript/2020-12/additional_properties_true/options.json b/test/codegen/e2e/typescript/2020-12/additional_properties_true/options.json new file mode 100644 index 000000000..f554b3acd --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/additional_properties_true/options.json @@ -0,0 +1,3 @@ +{ + "defaultPrefix": "FlexibleRecord" +} diff --git a/test/codegen/e2e/typescript/2020-12/additional_properties_true/schema.json b/test/codegen/e2e/typescript/2020-12/additional_properties_true/schema.json new file mode 100644 index 000000000..a6fd6250b --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/additional_properties_true/schema.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "count": { + "type": "integer" + } + }, + "required": [ "name" ], + "additionalProperties": true +} diff --git a/test/codegen/e2e/typescript/2020-12/additional_properties_true/test.ts b/test/codegen/e2e/typescript/2020-12/additional_properties_true/test.ts new file mode 100644 index 000000000..5fceec48b --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/additional_properties_true/test.ts @@ -0,0 +1,69 @@ +import { FlexibleRecord } from "./expected"; + +// Valid: required name only +const record1: FlexibleRecord = { + name: "test" +}; + +// Valid: name and optional count +const record2: FlexibleRecord = { + name: "test", + count: 42 +}; + +// Valid: with string additional property +const record3: FlexibleRecord = { + name: "test", + extra: "some string" +}; + +// Valid: with number additional property +const record4: FlexibleRecord = { + name: "test", + extra: 123 +}; + +// Valid: with boolean additional property +const record5: FlexibleRecord = { + name: "test", + flag: true +}; + +// Valid: with null additional property +const record6: FlexibleRecord = { + name: "test", + nothing: null +}; + +// Valid: with array additional property +const record7: FlexibleRecord = { + name: "test", + items: [ 1, 2, 3 ] +}; + +// Valid: with object additional property +const record8: FlexibleRecord = { + name: "test", + nested: { foo: "bar" } +}; + +// Valid: multiple additional properties of different types +const record9: FlexibleRecord = { + name: "test", + count: 10, + label: "my label", + active: false, + data: [ "a", "b" ] +}; + +// Invalid: name must be string +const record10: FlexibleRecord = { + // @ts-expect-error + name: 123 +}; + +// Invalid: missing required name +// @ts-expect-error +const record11: FlexibleRecord = { + count: 5 +}; diff --git a/test/codegen/e2e/typescript/2020-12/all_type_values/expected.d.ts b/test/codegen/e2e/typescript/2020-12/all_type_values/expected.d.ts new file mode 100644 index 000000000..5f60d9e81 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/all_type_values/expected.d.ts @@ -0,0 +1,60 @@ +export type AllTypesStringField = string; + +export type AllTypesObjectFieldNested = string; + +export type AllTypesObjectFieldAdditionalProperties = never; + +export interface AllTypesObjectField { + "nested"?: AllTypesObjectFieldNested; +} + +export type AllTypesNumberField = number; + +export type AllTypesNullField = null; + +export type AllTypesNestedTypesDeepNull = null; + +export type AllTypesNestedTypesDeepInteger = number; + +export type AllTypesNestedTypesDeepBoolean = boolean; + +export type AllTypesNestedTypesAdditionalProperties = never; + +export interface AllTypesNestedTypes { + "deepBoolean"?: AllTypesNestedTypesDeepBoolean; + "deepNull"?: AllTypesNestedTypesDeepNull; + "deepInteger"?: AllTypesNestedTypesDeepInteger; +} + +export type AllTypesMultiType_2 = null; + +export type AllTypesMultiType_1 = number; + +export type AllTypesMultiType_0 = string; + +export type AllTypesMultiType = + AllTypesMultiType_0 | + AllTypesMultiType_1 | + AllTypesMultiType_2; + +export type AllTypesIntegerField = number; + +export type AllTypesBooleanField = boolean; + +export type AllTypesArrayFieldItems = string; + +export type AllTypesArrayField = AllTypesArrayFieldItems[]; + +export type AllTypesAdditionalProperties = never; + +export interface AllTypes { + "stringField": AllTypesStringField; + "numberField": AllTypesNumberField; + "integerField": AllTypesIntegerField; + "booleanField": AllTypesBooleanField; + "nullField": AllTypesNullField; + "arrayField": AllTypesArrayField; + "objectField": AllTypesObjectField; + "multiType"?: AllTypesMultiType; + "nestedTypes"?: AllTypesNestedTypes; +} diff --git a/test/codegen/e2e/typescript/2020-12/all_type_values/options.json b/test/codegen/e2e/typescript/2020-12/all_type_values/options.json new file mode 100644 index 000000000..70815b482 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/all_type_values/options.json @@ -0,0 +1,3 @@ +{ + "defaultPrefix": "AllTypes" +} diff --git a/test/codegen/e2e/typescript/2020-12/all_type_values/schema.json b/test/codegen/e2e/typescript/2020-12/all_type_values/schema.json new file mode 100644 index 000000000..c2f380185 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/all_type_values/schema.json @@ -0,0 +1,65 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/all-types", + "type": "object", + "required": [ + "stringField", + "numberField", + "integerField", + "booleanField", + "nullField", + "arrayField", + "objectField" + ], + "properties": { + "stringField": { + "type": "string" + }, + "numberField": { + "type": "number" + }, + "integerField": { + "type": "integer" + }, + "booleanField": { + "type": "boolean" + }, + "nullField": { + "type": "null" + }, + "arrayField": { + "type": "array", + "items": { + "type": "string" + } + }, + "objectField": { + "type": "object", + "properties": { + "nested": { + "type": "string" + } + }, + "additionalProperties": false + }, + "multiType": { + "type": [ "string", "number", "null" ] + }, + "nestedTypes": { + "type": "object", + "properties": { + "deepBoolean": { + "type": "boolean" + }, + "deepNull": { + "type": "null" + }, + "deepInteger": { + "type": "integer" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false +} diff --git a/test/codegen/e2e/typescript/2020-12/all_type_values/test.ts b/test/codegen/e2e/typescript/2020-12/all_type_values/test.ts new file mode 100644 index 000000000..b3adca869 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/all_type_values/test.ts @@ -0,0 +1,293 @@ +import { + AllTypes, + AllTypesObjectField, + AllTypesNestedTypes, + AllTypesMultiType, + AllTypesArrayField +} 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: AllTypesArrayField = [ "a", "b" ]; +// @ts-expect-error +const invalidArrayFieldType: AllTypesArrayField = [ 1, 2 ]; + +const multiType1: AllTypesMultiType = "string"; +const multiType2: AllTypesMultiType = 42; +const multiType3: AllTypesMultiType = null; +// @ts-expect-error +const invalidMultiTypeStandalone: AllTypesMultiType = true; + +const objectField: AllTypesObjectField = {}; +const objectField2: AllTypesObjectField = { nested: "value" }; +// @ts-expect-error +const invalidObjectFieldStandalone: AllTypesObjectField = { nested: 123 }; + +const nestedTypes: AllTypesNestedTypes = {}; +const nestedTypes2: AllTypesNestedTypes = { deepBoolean: false, deepNull: null, deepInteger: 50 }; +// @ts-expect-error +const invalidNestedTypesStandalone: AllTypesNestedTypes = { deepBoolean: "yes" }; diff --git a/test/codegen/e2e/typescript/2020-12/allof_intersection/expected.d.ts b/test/codegen/e2e/typescript/2020-12/allof_intersection/expected.d.ts new file mode 100644 index 000000000..6efc9ac21 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/allof_intersection/expected.d.ts @@ -0,0 +1,19 @@ +export type AllOfIntersection_1Age = number; + +export type AllOfIntersection_1AdditionalProperties = never; + +export interface AllOfIntersection_1 { + "age": AllOfIntersection_1Age; +} + +export type AllOfIntersection_0Name = string; + +export type AllOfIntersection_0AdditionalProperties = never; + +export interface AllOfIntersection_0 { + "name": AllOfIntersection_0Name; +} + +export type AllOfIntersection = + AllOfIntersection_0 & + AllOfIntersection_1; diff --git a/test/codegen/e2e/typescript/2020-12/allof_intersection/options.json b/test/codegen/e2e/typescript/2020-12/allof_intersection/options.json new file mode 100644 index 000000000..dcaa629bb --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/allof_intersection/options.json @@ -0,0 +1,3 @@ +{ + "defaultPrefix": "AllOfIntersection" +} diff --git a/test/codegen/e2e/typescript/2020-12/allof_intersection/schema.json b/test/codegen/e2e/typescript/2020-12/allof_intersection/schema.json new file mode 100644 index 000000000..60be50049 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/allof_intersection/schema.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "allOf": [ + { + "type": "object", + "properties": { + "name": { "type": "string" } + }, + "required": [ "name" ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "age": { "type": "integer" } + }, + "required": [ "age" ], + "additionalProperties": false + } + ] +} diff --git a/test/codegen/e2e/typescript/2020-12/allof_intersection/test.ts b/test/codegen/e2e/typescript/2020-12/allof_intersection/test.ts new file mode 100644 index 000000000..af2d8f496 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/allof_intersection/test.ts @@ -0,0 +1,19 @@ +import { AllOfIntersection } from "./expected"; + +// Valid: satisfies both branches +const valid: AllOfIntersection = { + name: "Alice", + age: 30 +}; + +// Invalid: missing age from second branch +// @ts-expect-error +const missingAge: AllOfIntersection = { + name: "Bob" +}; + +// Invalid: missing name from first branch +// @ts-expect-error +const missingName: AllOfIntersection = { + age: 25 +}; diff --git a/test/codegen/e2e/typescript/2020-12/allof_refs/expected.d.ts b/test/codegen/e2e/typescript/2020-12/allof_refs/expected.d.ts new file mode 100644 index 000000000..182fb3a81 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/allof_refs/expected.d.ts @@ -0,0 +1,23 @@ +export type Person_1 = PersonAged; + +export type Person_0 = PersonNamed; + +export type PersonNamedName = string; + +export type PersonNamedAdditionalProperties = never; + +export interface PersonNamed { + "name": PersonNamedName; +} + +export type PersonAgedAge = number; + +export type PersonAgedAdditionalProperties = never; + +export interface PersonAged { + "age": PersonAgedAge; +} + +export type Person = + Person_0 & + Person_1; diff --git a/test/codegen/e2e/typescript/2020-12/allof_refs/options.json b/test/codegen/e2e/typescript/2020-12/allof_refs/options.json new file mode 100644 index 000000000..8d69d111e --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/allof_refs/options.json @@ -0,0 +1,3 @@ +{ + "defaultPrefix": "Person" +} diff --git a/test/codegen/e2e/typescript/2020-12/allof_refs/schema.json b/test/codegen/e2e/typescript/2020-12/allof_refs/schema.json new file mode 100644 index 000000000..d87b63e6a --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/allof_refs/schema.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Named": { + "type": "object", + "properties": { + "name": { "type": "string" } + }, + "required": [ "name" ], + "additionalProperties": false + }, + "Aged": { + "type": "object", + "properties": { + "age": { "type": "integer" } + }, + "required": [ "age" ], + "additionalProperties": false + } + }, + "allOf": [ + { "$ref": "#/$defs/Named" }, + { "$ref": "#/$defs/Aged" } + ] +} diff --git a/test/codegen/e2e/typescript/2020-12/allof_refs/test.ts b/test/codegen/e2e/typescript/2020-12/allof_refs/test.ts new file mode 100644 index 000000000..bdb79344d --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/allof_refs/test.ts @@ -0,0 +1,19 @@ +import { Person } from "./expected"; + +// Valid: satisfies both $ref branches +const valid: Person = { + name: "Alice", + age: 30 +}; + +// Invalid: missing age +// @ts-expect-error +const missingAge: Person = { + name: "Bob" +}; + +// Invalid: missing name +// @ts-expect-error +const missingName: Person = { + age: 25 +}; diff --git a/test/codegen/e2e/typescript/2020-12/allof_single_element/expected.d.ts b/test/codegen/e2e/typescript/2020-12/allof_single_element/expected.d.ts new file mode 100644 index 000000000..4f99d4463 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/allof_single_element/expected.d.ts @@ -0,0 +1,7 @@ +export type WrapperValue = string; + +export type WrapperAdditionalProperties = never; + +export interface Wrapper { + "value": WrapperValue; +} diff --git a/test/codegen/e2e/typescript/2020-12/allof_single_element/options.json b/test/codegen/e2e/typescript/2020-12/allof_single_element/options.json new file mode 100644 index 000000000..b339e6712 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/allof_single_element/options.json @@ -0,0 +1,3 @@ +{ + "defaultPrefix": "Wrapper" +} diff --git a/test/codegen/e2e/typescript/2020-12/allof_single_element/schema.json b/test/codegen/e2e/typescript/2020-12/allof_single_element/schema.json new file mode 100644 index 000000000..118d1ffa8 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/allof_single_element/schema.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "allOf": [ + { + "type": "object", + "properties": { + "value": { "type": "string" } + }, + "required": [ "value" ], + "additionalProperties": false + } + ] +} diff --git a/test/codegen/e2e/typescript/2020-12/allof_single_element/test.ts b/test/codegen/e2e/typescript/2020-12/allof_single_element/test.ts new file mode 100644 index 000000000..fbe5a604c --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/allof_single_element/test.ts @@ -0,0 +1,10 @@ +import { Wrapper } from "./expected"; + +// Valid: single-element allOf acts as the element itself +const valid: Wrapper = { + value: "hello" +}; + +// Invalid: missing required value +// @ts-expect-error +const missingValue: Wrapper = {}; diff --git a/test/codegen/e2e/typescript/2020-12/anchor_refs/expected.d.ts b/test/codegen/e2e/typescript/2020-12/anchor_refs/expected.d.ts new file mode 100644 index 000000000..dacbdad9f --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/anchor_refs/expected.d.ts @@ -0,0 +1,30 @@ +export type ColorSchemeSecondary = ColorSchemeColor; + +export type ColorSchemePrimary = ColorSchemeColor; + +export type ColorSchemeBackground = ColorSchemeColor; + +export type ColorSchemeAdditionalProperties = never; + +export type ColorSchemeColorR = number; + +export type ColorSchemeColorG = number; + +export type ColorSchemeColorB = number; + +export type ColorSchemeColorAlpha = number; + +export type ColorSchemeColorAdditionalProperties = never; + +export interface ColorSchemeColor { + "r": ColorSchemeColorR; + "g": ColorSchemeColorG; + "b": ColorSchemeColorB; + "alpha"?: ColorSchemeColorAlpha; +} + +export interface ColorScheme { + "primary": ColorSchemePrimary; + "secondary": ColorSchemeSecondary; + "background"?: ColorSchemeBackground; +} diff --git a/test/codegen/e2e/typescript/2020-12/anchor_refs/options.json b/test/codegen/e2e/typescript/2020-12/anchor_refs/options.json new file mode 100644 index 000000000..f6da95e09 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/anchor_refs/options.json @@ -0,0 +1,3 @@ +{ + "defaultPrefix": "ColorScheme" +} diff --git a/test/codegen/e2e/typescript/2020-12/anchor_refs/schema.json b/test/codegen/e2e/typescript/2020-12/anchor_refs/schema.json new file mode 100644 index 000000000..30299b4fc --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/anchor_refs/schema.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "required": [ "primary", "secondary" ], + "properties": { + "primary": { "$ref": "#color" }, + "secondary": { "$ref": "#color" }, + "background": { "$ref": "#color" } + }, + "additionalProperties": false, + "$defs": { + "Color": { + "$anchor": "color", + "type": "object", + "required": [ "r", "g", "b" ], + "properties": { + "r": { "type": "integer" }, + "g": { "type": "integer" }, + "b": { "type": "integer" }, + "alpha": { "type": "number" } + }, + "additionalProperties": false + } + } +} diff --git a/test/codegen/e2e/typescript/2020-12/anchor_refs/test.ts b/test/codegen/e2e/typescript/2020-12/anchor_refs/test.ts new file mode 100644 index 000000000..9facb8dab --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/anchor_refs/test.ts @@ -0,0 +1,106 @@ +import { + ColorScheme, + ColorSchemeColor +} 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: ColorSchemeColor = { + r: 128, + g: 128, + b: 128, + alpha: 0.5 +}; + +// Valid: color without alpha +const colorWithoutAlpha: ColorSchemeColor = { + r: 0, + g: 0, + b: 0 +}; + +// Invalid: missing required r +// @ts-expect-error +const missingR: ColorSchemeColor = { + g: 255, + b: 255 +}; + +// Invalid: missing required g +// @ts-expect-error +const missingG: ColorSchemeColor = { + r: 255, + b: 255 +}; + +// Invalid: missing required b +// @ts-expect-error +const missingB: ColorSchemeColor = { + r: 255, + g: 255 +}; + +// Invalid: r must be number +const invalidR: ColorSchemeColor = { + // @ts-expect-error + r: "255", + g: 0, + b: 0 +}; + +// Invalid: alpha must be number +const invalidAlpha: ColorSchemeColor = { + r: 255, + g: 0, + b: 0, + // @ts-expect-error + alpha: "1.0" +}; + +// Invalid: extra property on color (additionalProperties: false) +const invalidColorExtra: ColorSchemeColor = { + 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/codegen/e2e/typescript/2020-12/bundled_schema/expected.d.ts b/test/codegen/e2e/typescript/2020-12/bundled_schema/expected.d.ts new file mode 100644 index 000000000..bf3339a7a --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/bundled_schema/expected.d.ts @@ -0,0 +1,37 @@ +export type ApiResponseMeta = ApiResponseSchemasMetadata; + +export type ApiResponseData = ApiResponseSchemasUser; + +export type ApiResponseAdditionalProperties = never; + +export type ApiResponseSchemasUserName = string; + +export type ApiResponseSchemasUserId = number; + +export type ApiResponseSchemasUserEmail = ApiResponseSchemasEmail; + +export type ApiResponseSchemasUserAdditionalProperties = never; + +export interface ApiResponseSchemasUser { + "id": ApiResponseSchemasUserId; + "name": ApiResponseSchemasUserName; + "email"?: ApiResponseSchemasUserEmail; +} + +export type ApiResponseSchemasMetadataVersion = number; + +export type ApiResponseSchemasMetadataTimestamp = string; + +export type ApiResponseSchemasMetadataAdditionalProperties = never; + +export interface ApiResponseSchemasMetadata { + "timestamp"?: ApiResponseSchemasMetadataTimestamp; + "version"?: ApiResponseSchemasMetadataVersion; +} + +export type ApiResponseSchemasEmail = string; + +export interface ApiResponse { + "data": ApiResponseData; + "meta": ApiResponseMeta; +} diff --git a/test/codegen/e2e/typescript/2020-12/bundled_schema/options.json b/test/codegen/e2e/typescript/2020-12/bundled_schema/options.json new file mode 100644 index 000000000..712012909 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/bundled_schema/options.json @@ -0,0 +1,3 @@ +{ + "defaultPrefix": "ApiResponse" +} diff --git a/test/codegen/e2e/typescript/2020-12/bundled_schema/schema.json b/test/codegen/e2e/typescript/2020-12/bundled_schema/schema.json new file mode 100644 index 000000000..0f40b49e6 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/bundled_schema/schema.json @@ -0,0 +1,42 @@ +{ + "$id": "https://example.com/api/response", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "A bundled API response schema with embedded resource schemas", + "type": "object", + "required": [ "data", "meta" ], + "properties": { + "data": { "$ref": "https://example.com/schemas/user.json" }, + "meta": { "$ref": "https://example.com/schemas/metadata.json" } + }, + "additionalProperties": false, + "$defs": { + "https://example.com/schemas/user.json": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/schemas/user.json", + "type": "object", + "required": [ "id", "name" ], + "properties": { + "id": { "type": "integer" }, + "name": { "type": "string" }, + "email": { "$ref": "https://example.com/schemas/email.schema.json" } + }, + "additionalProperties": false + }, + "https://example.com/schemas/email.schema.json": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/schemas/email.schema.json", + "type": "string", + "format": "email" + }, + "https://example.com/schemas/metadata.json": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/schemas/metadata.json", + "type": "object", + "properties": { + "timestamp": { "type": "string" }, + "version": { "type": "integer" } + }, + "additionalProperties": false + } + } +} diff --git a/test/codegen/e2e/typescript/2020-12/bundled_schema/test.ts b/test/codegen/e2e/typescript/2020-12/bundled_schema/test.ts new file mode 100644 index 000000000..9c116c8db --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/bundled_schema/test.ts @@ -0,0 +1,102 @@ +import { + ApiResponse, + ApiResponseSchemasUser, + ApiResponseSchemasMetadata, + ApiResponseSchemasEmail +} from "./expected"; + + +// Valid: full API response with all fields +const fullResponse: ApiResponse = { + data: { + id: 123, + name: "John Doe", + email: "john@example.com" + }, + meta: { + timestamp: "2024-01-15T10:30:00Z", + version: 1 + } +}; + +// Valid: minimal response (required fields only) +const minimalResponse: ApiResponse = { + data: { + id: 1, + name: "Jane" + }, + meta: {} +}; + +// Valid: user object directly +const user: ApiResponseSchemasUser = { + id: 42, + name: "Test User" +}; + +// Valid: user with email +const userWithEmail: ApiResponseSchemasUser = { + id: 42, + name: "Test User", + email: "test@example.com" +}; + +// Valid: metadata object +const metadata: ApiResponseSchemasMetadata = { + timestamp: "2024-01-15", + version: 2 +}; + +// Valid: email is just a string +const email: ApiResponseSchemasEmail = "user@domain.com"; + +// Invalid: missing required field 'data' +// @ts-expect-error - data is required +const missingData: ApiResponse = { + meta: {} +}; + +// Invalid: missing required field 'meta' +// @ts-expect-error - meta is required +const missingMeta: ApiResponse = { + data: { id: 1, name: "Test" } +}; + +// Invalid: user missing required 'id' +const userMissingId: ApiResponse = { + // @ts-expect-error - id is required on user + data: { + name: "Test" + }, + meta: {} +}; + +// Invalid: user missing required 'name' +const userMissingName: ApiResponse = { + // @ts-expect-error - name is required on user + data: { + id: 1 + }, + meta: {} +}; + +// Invalid: wrong type for user id +const wrongIdType: ApiResponse = { + data: { + // @ts-expect-error - id must be number + id: "not-a-number", + name: "Test" + }, + meta: {} +}; + +// Invalid: extra property on user (additionalProperties: false) +const extraUserProp: ApiResponse = { + data: { + id: 1, + name: "Test", + // @ts-expect-error - extra property not allowed + extra: "not allowed" + }, + meta: {} +}; diff --git a/test/codegen/e2e/typescript/2020-12/bundled_schema_file_uris/expected.d.ts b/test/codegen/e2e/typescript/2020-12/bundled_schema_file_uris/expected.d.ts new file mode 100644 index 000000000..ec34dfd99 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/bundled_schema_file_uris/expected.d.ts @@ -0,0 +1,32 @@ +export type ResponseMeta = ResponseMetadata; + +export type ResponseData = ResponseUser; + +export type ResponseAdditionalProperties = never; + +export type ResponseUserName = string; + +export type ResponseUserId = number; + +export type ResponseUserAdditionalProperties = never; + +export interface ResponseUser { + "id": ResponseUserId; + "name": ResponseUserName; +} + +export type ResponseMetadataVersion = number; + +export type ResponseMetadataTimestamp = string; + +export type ResponseMetadataAdditionalProperties = never; + +export interface ResponseMetadata { + "timestamp"?: ResponseMetadataTimestamp; + "version"?: ResponseMetadataVersion; +} + +export interface Response { + "data": ResponseData; + "meta": ResponseMeta; +} diff --git a/test/codegen/e2e/typescript/2020-12/bundled_schema_file_uris/options.json b/test/codegen/e2e/typescript/2020-12/bundled_schema_file_uris/options.json new file mode 100644 index 000000000..8a23bd85f --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/bundled_schema_file_uris/options.json @@ -0,0 +1,3 @@ +{ + "defaultPrefix": "Response" +} diff --git a/test/codegen/e2e/typescript/2020-12/bundled_schema_file_uris/schema.json b/test/codegen/e2e/typescript/2020-12/bundled_schema_file_uris/schema.json new file mode 100644 index 000000000..e7388fbbe --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/bundled_schema_file_uris/schema.json @@ -0,0 +1,34 @@ +{ + "$id": "file:///schemas/api/response.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "required": [ "data", "meta" ], + "properties": { + "data": { "$ref": "file:///schemas/models/user.json" }, + "meta": { "$ref": "file:///schemas/models/metadata.json" } + }, + "additionalProperties": false, + "$defs": { + "file:///schemas/models/user.json": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "file:///schemas/models/user.json", + "type": "object", + "required": [ "id", "name" ], + "properties": { + "id": { "type": "integer" }, + "name": { "type": "string" } + }, + "additionalProperties": false + }, + "file:///schemas/models/metadata.json": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "file:///schemas/models/metadata.json", + "type": "object", + "properties": { + "timestamp": { "type": "string" }, + "version": { "type": "integer" } + }, + "additionalProperties": false + } + } +} diff --git a/test/codegen/e2e/typescript/2020-12/bundled_schema_file_uris/test.ts b/test/codegen/e2e/typescript/2020-12/bundled_schema_file_uris/test.ts new file mode 100644 index 000000000..428cd157a --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/bundled_schema_file_uris/test.ts @@ -0,0 +1,80 @@ +import { + Response, + ResponseUser, + ResponseMetadata +} from "./expected"; + + +// Valid: full response with all fields +const fullResponse: Response = { + data: { + id: 123, + name: "John Doe" + }, + meta: { + timestamp: "2024-01-15T10:30:00Z", + version: 1 + } +}; + +// Valid: minimal response (required fields only) +const minimalResponse: Response = { + data: { + id: 1, + name: "Jane" + }, + meta: {} +}; + +// Valid: user object directly +const user: ResponseUser = { + id: 42, + name: "Test User" +}; + +// Valid: metadata object +const metadata: ResponseMetadata = { + timestamp: "2024-01-15", + version: 2 +}; + +// Invalid: missing required field 'data' +// @ts-expect-error - data is required +const missingData: Response = { + meta: {} +}; + +// Invalid: missing required field 'meta' +// @ts-expect-error - meta is required +const missingMeta: Response = { + data: { id: 1, name: "Test" } +}; + +// Invalid: user missing required 'id' +const userMissingId: Response = { + // @ts-expect-error - id is required on user + data: { + name: "Test" + }, + meta: {} +}; + +// Invalid: user missing required 'name' +const userMissingName: Response = { + // @ts-expect-error - name is required on user + data: { + id: 1 + }, + meta: {} +}; + +// Invalid: extra property on user (additionalProperties: false) +const extraUserProp: Response = { + data: { + id: 1, + name: "Test", + // @ts-expect-error - extra property not allowed + extra: "not allowed" + }, + meta: {} +}; diff --git a/test/codegen/e2e/typescript/2020-12/complex_nested_object/expected.d.ts b/test/codegen/e2e/typescript/2020-12/complex_nested_object/expected.d.ts new file mode 100644 index 000000000..5ced10ac1 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/complex_nested_object/expected.d.ts @@ -0,0 +1,245 @@ +export type RecordRegion = string; + +export type RecordReferenceCode = string; + +export type RecordRecordId = string; + +export type RecordOrganizationName = string; + +export type RecordNotes = string; + +export type RecordMetaOriginId = string; + +export type RecordMetaOrigin = string; + +export type RecordMetaAdditionalProperties = never; + +export interface RecordMeta { + "origin"?: RecordMetaOrigin; + "originId"?: RecordMetaOriginId; +} + +export type RecordLocationInfoStateCode = string; + +export type RecordLocationInfoAreaCode = string; + +export type RecordLocationInfoAdditionalProperties = never; + +export interface RecordLocationInfo { + "stateCode"?: RecordLocationInfoStateCode; + "areaCode"?: RecordLocationInfoAreaCode; +} + +export type RecordItemsItemsSubCategory = string; + +export type RecordItemsItemsSeverity = string; + +export type RecordItemsItemsSequenceNumber = string; + +export type RecordItemsItemsResolvedAt_1 = null; + +export type RecordItemsItemsResolvedAt_0 = RecordTimestamp; + +export type RecordItemsItemsResolvedAt = + RecordItemsItemsResolvedAt_0 | + RecordItemsItemsResolvedAt_1; + +export type RecordItemsItemsResolution_1 = null; + +export type RecordItemsItemsResolution_0 = string; + +export type RecordItemsItemsResolution = + RecordItemsItemsResolution_0 | + RecordItemsItemsResolution_1; + +export type RecordItemsItemsRemarks_1 = null; + +export type RecordItemsItemsRemarks_0 = string; + +export type RecordItemsItemsRemarks = + RecordItemsItemsRemarks_0 | + RecordItemsItemsRemarks_1; + +export type RecordItemsItemsOutcome = string; + +export type RecordItemsItemsOccurredAt = RecordTimestamp; + +export type RecordItemsItemsMetaOriginId = string; + +export type RecordItemsItemsMetaOrigin = string; + +export type RecordItemsItemsMetaAdditionalProperties = never; + +export interface RecordItemsItemsMeta { + "origin"?: RecordItemsItemsMetaOrigin; + "originId"?: RecordItemsItemsMetaOriginId; +} + +export type RecordItemsItemsItemId = string; + +export type RecordItemsItemsDescription = string; + +export type RecordItemsItemsCode_1 = null; + +export type RecordItemsItemsCode_0 = string; + +export type RecordItemsItemsCode = + RecordItemsItemsCode_0 | + RecordItemsItemsCode_1; + +export type RecordItemsItemsCategory = string; + +export type RecordItemsItemsAdditionalProperties = never; + +export interface RecordItemsItems { + "itemId": RecordItemsItemsItemId; + "sequenceNumber": RecordItemsItemsSequenceNumber; + "description": RecordItemsItemsDescription; + "code"?: RecordItemsItemsCode; + "occurredAt": RecordItemsItemsOccurredAt; + "severity": RecordItemsItemsSeverity; + "resolution"?: RecordItemsItemsResolution; + "resolvedAt"?: RecordItemsItemsResolvedAt; + "outcome"?: RecordItemsItemsOutcome; + "remarks"?: RecordItemsItemsRemarks; + "category"?: RecordItemsItemsCategory; + "subCategory"?: RecordItemsItemsSubCategory; + "meta"?: RecordItemsItemsMeta; +} + +export type RecordItems = RecordItemsItems[]; + +export type RecordEntityLocationsItems = RecordLocation; + +export type RecordEntityLocations = RecordEntityLocationsItems[]; + +export type RecordEntityFullName = RecordFullName; + +export type RecordEntityClassification = string; + +export type RecordEntityCategory = string; + +export type RecordEntityBirthDate = RecordTimestamp; + +export type RecordEntityAdditionalProperties = never; + +export interface RecordEntity { + "fullName": RecordEntityFullName; + "birthDate": RecordEntityBirthDate; + "category"?: RecordEntityCategory; + "classification"?: RecordEntityClassification; + "locations": RecordEntityLocations; +} + +export type RecordCreatedAt = RecordTimestamp; + +export type RecordAdditionalProperties = never; + +export type RecordTimestampYear = number; + +export type RecordTimestampRawValue = string; + +export type RecordTimestampMonth = number; + +export type RecordTimestampIsoFormat = string; + +export type RecordTimestampDay = number; + +export type RecordTimestampAdditionalProperties = never; + +export interface RecordTimestamp { + "rawValue": RecordTimestampRawValue; + "year": RecordTimestampYear; + "month": RecordTimestampMonth; + "day": RecordTimestampDay; + "isoFormat"?: RecordTimestampIsoFormat; +} + +export type RecordLocationRegion = string; + +export type RecordLocationRawValue = string; + +export type RecordLocationPostalCode = string; + +export type RecordLocationLine2_1 = null; + +export type RecordLocationLine2_0 = string; + +export type RecordLocationLine2 = + RecordLocationLine2_0 | + RecordLocationLine2_1; + +export type RecordLocationLine1 = string; + +export type RecordLocationDistrict = string; + +export type RecordLocationCountry = string; + +export type RecordLocationCity = string; + +export type RecordLocationAdditionalProperties = never; + +export interface RecordLocation { + "rawValue": RecordLocationRawValue; + "line1": RecordLocationLine1; + "line2"?: RecordLocationLine2; + "city": RecordLocationCity; + "district"?: RecordLocationDistrict; + "region": RecordLocationRegion; + "postalCode": RecordLocationPostalCode; + "country": RecordLocationCountry; +} + +export type RecordFullNameSuffix_1 = null; + +export type RecordFullNameSuffix_0 = string; + +export type RecordFullNameSuffix = + RecordFullNameSuffix_0 | + RecordFullNameSuffix_1; + +export type RecordFullNameRawValue = string; + +export type RecordFullNamePrefix_1 = null; + +export type RecordFullNamePrefix_0 = string; + +export type RecordFullNamePrefix = + RecordFullNamePrefix_0 | + RecordFullNamePrefix_1; + +export type RecordFullNameMiddleName_1 = null; + +export type RecordFullNameMiddleName_0 = string; + +export type RecordFullNameMiddleName = + RecordFullNameMiddleName_0 | + RecordFullNameMiddleName_1; + +export type RecordFullNameGivenName = string; + +export type RecordFullNameFamilyName = string; + +export type RecordFullNameAdditionalProperties = never; + +export interface RecordFullName { + "rawValue"?: RecordFullNameRawValue; + "givenName": RecordFullNameGivenName; + "middleName"?: RecordFullNameMiddleName; + "familyName": RecordFullNameFamilyName; + "suffix"?: RecordFullNameSuffix; + "prefix"?: RecordFullNamePrefix; +} + +export interface Record { + "recordId": RecordRecordId; + "referenceCode"?: RecordReferenceCode; + "organizationName": RecordOrganizationName; + "createdAt"?: RecordCreatedAt; + "region": RecordRegion; + "locationInfo"?: RecordLocationInfo; + "entity": RecordEntity; + "notes"?: RecordNotes; + "items": RecordItems; + "meta"?: RecordMeta; +} diff --git a/test/codegen/e2e/typescript/2020-12/complex_nested_object/options.json b/test/codegen/e2e/typescript/2020-12/complex_nested_object/options.json new file mode 100644 index 000000000..0546cfc07 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/complex_nested_object/options.json @@ -0,0 +1,3 @@ +{ + "defaultPrefix": "Record" +} diff --git a/test/codegen/e2e/typescript/2020-12/complex_nested_object/schema.json b/test/codegen/e2e/typescript/2020-12/complex_nested_object/schema.json new file mode 100644 index 000000000..5eccaeb36 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/complex_nested_object/schema.json @@ -0,0 +1,246 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "required": [ + "recordId", + "organizationName", + "region", + "entity", + "items" + ], + "properties": { + "recordId": { + "type": "string" + }, + "referenceCode": { + "type": "string" + }, + "organizationName": { + "type": "string" + }, + "createdAt": { + "$ref": "#/$defs/Timestamp" + }, + "region": { + "type": "string" + }, + "locationInfo": { + "type": "object", + "properties": { + "stateCode": { + "type": "string" + }, + "areaCode": { + "type": "string" + } + }, + "additionalProperties": false + }, + "entity": { + "type": "object", + "required": [ "fullName", "birthDate", "locations" ], + "properties": { + "fullName": { + "$ref": "#/$defs/FullName" + }, + "birthDate": { + "$ref": "#/$defs/Timestamp" + }, + "category": { + "type": "string" + }, + "classification": { + "type": "string" + }, + "locations": { + "type": "array", + "items": { + "$ref": "#/$defs/Location" + } + } + }, + "additionalProperties": false + }, + "notes": { + "type": "string" + }, + "items": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": [ + "itemId", + "sequenceNumber", + "description", + "occurredAt", + "severity" + ], + "properties": { + "itemId": { + "type": "string" + }, + "sequenceNumber": { + "type": "string" + }, + "description": { + "type": "string" + }, + "code": { + "type": [ "string", "null" ] + }, + "occurredAt": { + "$ref": "#/$defs/Timestamp" + }, + "severity": { + "type": "string" + }, + "resolution": { + "type": [ "string", "null" ] + }, + "resolvedAt": { + "anyOf": [ + { + "$ref": "#/$defs/Timestamp" + }, + { + "type": "null" + } + ] + }, + "outcome": { + "type": "string" + }, + "remarks": { + "type": [ "string", "null" ] + }, + "category": { + "type": "string" + }, + "subCategory": { + "type": "string" + }, + "meta": { + "type": "object", + "properties": { + "origin": { + "type": "string" + }, + "originId": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "meta": { + "type": "object", + "properties": { + "origin": { + "type": "string" + }, + "originId": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false, + "$defs": { + "Location": { + "type": "object", + "required": [ + "rawValue", + "line1", + "city", + "region", + "postalCode", + "country" + ], + "properties": { + "rawValue": { + "type": "string" + }, + "line1": { + "type": "string" + }, + "line2": { + "type": [ "string", "null" ] + }, + "city": { + "type": "string" + }, + "district": { + "type": "string" + }, + "region": { + "type": "string" + }, + "postalCode": { + "type": "string" + }, + "country": { + "type": "string" + } + }, + "additionalProperties": false + }, + "FullName": { + "type": "object", + "required": [ "givenName", "familyName" ], + "properties": { + "rawValue": { + "type": "string" + }, + "givenName": { + "type": "string" + }, + "middleName": { + "type": [ "string", "null" ] + }, + "familyName": { + "type": "string" + }, + "suffix": { + "type": [ "string", "null" ] + }, + "prefix": { + "type": [ "string", "null" ] + } + }, + "additionalProperties": false + }, + "Timestamp": { + "type": "object", + "required": [ "rawValue", "year", "month", "day" ], + "properties": { + "rawValue": { + "type": "string" + }, + "year": { + "type": "integer", + "maximum": 9999, + "minimum": 1000 + }, + "month": { + "type": "integer", + "maximum": 12, + "minimum": 1 + }, + "day": { + "type": "integer", + "maximum": 31, + "minimum": 1 + }, + "isoFormat": { + "type": "string" + } + }, + "additionalProperties": false + } + } +} diff --git a/test/codegen/e2e/typescript/2020-12/complex_nested_object/test.ts b/test/codegen/e2e/typescript/2020-12/complex_nested_object/test.ts new file mode 100644 index 000000000..9c7d9f4d1 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/complex_nested_object/test.ts @@ -0,0 +1,330 @@ +import { + Record, + RecordTimestamp, + RecordFullName, + RecordLocation, + RecordItemsItems, + RecordEntity +} from "./expected"; + + +// Valid: Timestamp with all required fields +const timestamp: RecordTimestamp = { + rawValue: "2024-01-15", + year: 2024, + month: 1, + day: 15 +}; + +// Valid: Timestamp with optional isoFormat +const timestampWithIso: RecordTimestamp = { + 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: RecordTimestamp = { + year: 2024, + month: 1, + day: 15 +}; + +// Valid: FullName with required fields only +const fullNameMinimal: RecordFullName = { + givenName: "John", + familyName: "Doe" +}; + +// Valid: FullName with all fields +const fullNameComplete: RecordFullName = { + 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: RecordFullName = { + givenName: "Jane", + familyName: "Smith", + middleName: null, + suffix: null, + prefix: null +}; + +// Invalid: FullName missing required givenName +// @ts-expect-error - givenName is required +const fullNameMissingGiven: RecordFullName = { + familyName: "Doe" +}; + +// Valid: Location with all required fields +const location: RecordLocation = { + 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: RecordLocation = { + 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: RecordLocation = { + 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: RecordEntity = { + 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: RecordEntity = { + 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: RecordEntity = { + birthDate: { rawValue: "1990-05-15", year: 1990, month: 5, day: 15 }, + locations: [] +}; + +// Valid: Item with required fields +const item: RecordItemsItems = { + 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: RecordItemsItems = { + 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: RecordItemsItems = { + 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: RecordItemsItems = { + 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: RecordItemsItems = { + 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: RecordItemsItems = { + 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/codegen/e2e/typescript/2020-12/const_keyword/expected.d.ts b/test/codegen/e2e/typescript/2020-12/const_keyword/expected.d.ts new file mode 100644 index 000000000..728463c9c --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/const_keyword/expected.d.ts @@ -0,0 +1,34 @@ +export type ConstTestVersion = "1.0.0"; + +export type ConstTestOptionalFlag = false; + +export type ConstTestNothing = null; + +export type ConstTestNestedFixedValue = "fixed"; + +export type ConstTestNestedFixedNumber = 100; + +export type ConstTestNestedAdditionalProperties = never; + +export interface ConstTestNested { + "fixedValue"?: ConstTestNestedFixedValue; + "fixedNumber"?: ConstTestNestedFixedNumber; +} + +export type ConstTestMode = "production"; + +export type ConstTestEnabled = true; + +export type ConstTestCount = 42; + +export type ConstTestAdditionalProperties = never; + +export interface ConstTest { + "version": ConstTestVersion; + "enabled": ConstTestEnabled; + "mode": ConstTestMode; + "count": ConstTestCount; + "nothing": ConstTestNothing; + "optionalFlag"?: ConstTestOptionalFlag; + "nested"?: ConstTestNested; +} diff --git a/test/codegen/e2e/typescript/2020-12/const_keyword/options.json b/test/codegen/e2e/typescript/2020-12/const_keyword/options.json new file mode 100644 index 000000000..df3c69c8b --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/const_keyword/options.json @@ -0,0 +1,3 @@ +{ + "defaultPrefix": "ConstTest" +} diff --git a/test/codegen/e2e/typescript/2020-12/const_keyword/schema.json b/test/codegen/e2e/typescript/2020-12/const_keyword/schema.json new file mode 100644 index 000000000..35fd88d79 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/const_keyword/schema.json @@ -0,0 +1,39 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/const-test", + "type": "object", + "required": [ "version", "enabled", "mode", "count", "nothing" ], + "properties": { + "version": { + "const": "1.0.0" + }, + "enabled": { + "const": true + }, + "mode": { + "const": "production" + }, + "count": { + "const": 42 + }, + "nothing": { + "const": null + }, + "optionalFlag": { + "const": false + }, + "nested": { + "type": "object", + "properties": { + "fixedValue": { + "const": "fixed" + }, + "fixedNumber": { + "const": 100 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false +} diff --git a/test/codegen/e2e/typescript/2020-12/const_keyword/test.ts b/test/codegen/e2e/typescript/2020-12/const_keyword/test.ts new file mode 100644 index 000000000..8e3fab45f --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/const_keyword/test.ts @@ -0,0 +1,173 @@ +import { ConstTest, ConstTestNested } 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: ConstTestNested = {}; +const nested2: ConstTestNested = { fixedValue: "fixed" }; +const nested3: ConstTestNested = { fixedNumber: 100 }; +const nested4: ConstTestNested = { fixedValue: "fixed", fixedNumber: 100 }; +// @ts-expect-error - fixedValue must be "fixed" +const invalidNested: ConstTestNested = { fixedValue: "wrong" }; diff --git a/test/codegen/e2e/typescript/2020-12/control_characters_in_property_names/expected.d.ts b/test/codegen/e2e/typescript/2020-12/control_characters_in_property_names/expected.d.ts new file mode 100644 index 000000000..101d1086a --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/control_characters_in_property_names/expected.d.ts @@ -0,0 +1,22 @@ +export type TestWithus = string; + +export type TestWithformfeed = string; + +export type TestWithbackspace = string; + +export type TestWithsoh = string; + +export type TestWithnull = string; + +export type TestNormal = string; + +export type TestAdditionalProperties = never; + +export interface Test { + "normal"?: TestNormal; + "with\bbackspace"?: TestWithbackspace; + "with\fformfeed"?: TestWithformfeed; + "with\u0000null"?: TestWithnull; + "with\u0001soh"?: TestWithsoh; + "with\u001fus"?: TestWithus; +} diff --git a/test/codegen/e2e/typescript/2020-12/control_characters_in_property_names/options.json b/test/codegen/e2e/typescript/2020-12/control_characters_in_property_names/options.json new file mode 100644 index 000000000..68213dc09 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/control_characters_in_property_names/options.json @@ -0,0 +1,3 @@ +{ + "defaultPrefix": "Test" +} diff --git a/test/codegen/e2e/typescript/2020-12/control_characters_in_property_names/schema.json b/test/codegen/e2e/typescript/2020-12/control_characters_in_property_names/schema.json new file mode 100644 index 000000000..22958771f --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/control_characters_in_property_names/schema.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "normal": { "type": "string" }, + "with\bbackspace": { "type": "string" }, + "with\fformfeed": { "type": "string" }, + "with\u0000null": { "type": "string" }, + "with\u0001soh": { "type": "string" }, + "with\u001fus": { "type": "string" } + }, + "additionalProperties": false +} diff --git a/test/codegen/e2e/typescript/2020-12/control_characters_in_property_names/test.ts b/test/codegen/e2e/typescript/2020-12/control_characters_in_property_names/test.ts new file mode 100644 index 000000000..aaed97fb2 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/control_characters_in_property_names/test.ts @@ -0,0 +1,32 @@ +import { Test } from "./expected"; + +// Valid: object with control characters in property names +const validObject: Test = { + "normal": "hello", + "with\bbackspace": "value1", + "with\fformfeed": "value2", + "with\u0000null": "value3", + "with\u0001soh": "value4", + "with\u001fus": "value5" +}; + +// Valid: empty object (all properties are optional) +const emptyObject: Test = {}; + +// Valid: partial object +const partialObject: Test = { + "normal": "just normal" +}; + +// Invalid: extra property not allowed +const extraProp: Test = { + "normal": "hello", + // @ts-expect-error - extra property not allowed + "extra": "not allowed" +}; + +// Invalid: wrong type for property +const wrongType: Test = { + // @ts-expect-error - must be string + "normal": 123 +}; diff --git a/test/codegen/e2e/typescript/2020-12/deeply_nested_refs/expected.d.ts b/test/codegen/e2e/typescript/2020-12/deeply_nested_refs/expected.d.ts new file mode 100644 index 000000000..d12b8dfa1 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/deeply_nested_refs/expected.d.ts @@ -0,0 +1,230 @@ +export type ApiResponseStatus = "success" | "error" | "pending"; + +export type ApiResponseMeta = ApiResponseResponseMeta; + +export type ApiResponseErrorCode_1 = null; + +export type ApiResponseErrorCode_0 = string; + +export type ApiResponseErrorCode = + ApiResponseErrorCode_0 | + ApiResponseErrorCode_1; + +export type ApiResponseData = ApiResponsePaginatedResult; + +export type ApiResponseAdditionalProperties = never; + +export type ApiResponseResponseMetaVersion = string; + +export type ApiResponseResponseMetaTimestamp = string; + +export type ApiResponseResponseMetaRequestId = string; + +export type ApiResponseResponseMetaDeprecationWarningsItemsSuggestedAlternative_1 = null; + +export type ApiResponseResponseMetaDeprecationWarningsItemsSuggestedAlternative_0 = string; + +export type ApiResponseResponseMetaDeprecationWarningsItemsSuggestedAlternative = + ApiResponseResponseMetaDeprecationWarningsItemsSuggestedAlternative_0 | + ApiResponseResponseMetaDeprecationWarningsItemsSuggestedAlternative_1; + +export type ApiResponseResponseMetaDeprecationWarningsItemsMessage = string; + +export type ApiResponseResponseMetaDeprecationWarningsItemsField = string; + +export type ApiResponseResponseMetaDeprecationWarningsItemsAdditionalProperties = never; + +export interface ApiResponseResponseMetaDeprecationWarningsItems { + "message": ApiResponseResponseMetaDeprecationWarningsItemsMessage; + "field": ApiResponseResponseMetaDeprecationWarningsItemsField; + "suggestedAlternative"?: ApiResponseResponseMetaDeprecationWarningsItemsSuggestedAlternative; +} + +export type ApiResponseResponseMetaDeprecationWarnings = ApiResponseResponseMetaDeprecationWarningsItems[]; + +export type ApiResponseResponseMetaAdditionalProperties = never; + +export interface ApiResponseResponseMeta { + "requestId": ApiResponseResponseMetaRequestId; + "timestamp": ApiResponseResponseMetaTimestamp; + "version"?: ApiResponseResponseMetaVersion; + "deprecationWarnings"?: ApiResponseResponseMetaDeprecationWarnings; +} + +export type ApiResponsePaginationInfoTotalPages = number; + +export type ApiResponsePaginationInfoTotalItems = number; + +export type ApiResponsePaginationInfoPageSize = number; + +export type ApiResponsePaginationInfoPage = number; + +export type ApiResponsePaginationInfoHasPreviousPage = boolean; + +export type ApiResponsePaginationInfoHasNextPage = boolean; + +export type ApiResponsePaginationInfoAdditionalProperties = never; + +export interface ApiResponsePaginationInfo { + "page": ApiResponsePaginationInfoPage; + "pageSize": ApiResponsePaginationInfoPageSize; + "totalItems": ApiResponsePaginationInfoTotalItems; + "totalPages": ApiResponsePaginationInfoTotalPages; + "hasNextPage"?: ApiResponsePaginationInfoHasNextPage; + "hasPreviousPage"?: ApiResponsePaginationInfoHasPreviousPage; +} + +export type ApiResponsePaginatedResultPagination = ApiResponsePaginationInfo; + +export type ApiResponsePaginatedResultItemsItems = ApiResponseEntity; + +export type ApiResponsePaginatedResultItems = ApiResponsePaginatedResultItemsItems[]; + +export type ApiResponsePaginatedResultFilters = ApiResponseAppliedFilters; + +export type ApiResponsePaginatedResultAdditionalProperties = never; + +export interface ApiResponsePaginatedResult { + "items": ApiResponsePaginatedResultItems; + "pagination": ApiResponsePaginatedResultPagination; + "filters"?: ApiResponsePaginatedResultFilters; +} + +export type ApiResponseEntityReferenceType = string; + +export type ApiResponseEntityReferenceId = string; + +export type ApiResponseEntityReferenceAdditionalProperties = never; + +export interface ApiResponseEntityReference { + "id": ApiResponseEntityReferenceId; + "type": ApiResponseEntityReferenceType; +} + +export type ApiResponseEntityAttributesUpdatedAt_1 = null; + +export type ApiResponseEntityAttributesUpdatedAt_0 = string; + +export type ApiResponseEntityAttributesUpdatedAt = + ApiResponseEntityAttributesUpdatedAt_0 | + ApiResponseEntityAttributesUpdatedAt_1; + +export type ApiResponseEntityAttributesTagsItems = string; + +export type ApiResponseEntityAttributesTags = ApiResponseEntityAttributesTagsItems[]; + +export type ApiResponseEntityAttributesName = string; + +export type ApiResponseEntityAttributesMetadataAdditionalProperties_2 = boolean; + +export type ApiResponseEntityAttributesMetadataAdditionalProperties_1 = number; + +export type ApiResponseEntityAttributesMetadataAdditionalProperties_0 = string; + +export type ApiResponseEntityAttributesMetadataAdditionalProperties = + ApiResponseEntityAttributesMetadataAdditionalProperties_0 | + ApiResponseEntityAttributesMetadataAdditionalProperties_1 | + ApiResponseEntityAttributesMetadataAdditionalProperties_2; + +export type ApiResponseEntityAttributesMetadata = Record; + +export type ApiResponseEntityAttributesDescription_1 = null; + +export type ApiResponseEntityAttributesDescription_0 = string; + +export type ApiResponseEntityAttributesDescription = + ApiResponseEntityAttributesDescription_0 | + ApiResponseEntityAttributesDescription_1; + +export type ApiResponseEntityAttributesCreatedAt = string; + +export type ApiResponseEntityAttributesAdditionalProperties = never; + +export interface ApiResponseEntityAttributes { + "name": ApiResponseEntityAttributesName; + "description"?: ApiResponseEntityAttributesDescription; + "createdAt": ApiResponseEntityAttributesCreatedAt; + "updatedAt"?: ApiResponseEntityAttributesUpdatedAt; + "tags"?: ApiResponseEntityAttributesTags; + "metadata"?: ApiResponseEntityAttributesMetadata; +} + +export type ApiResponseEntityType = "user" | "organization" | "resource"; + +export type ApiResponseEntityRelationshipsParent = ApiResponseEntityReference; + +export type ApiResponseEntityRelationshipsChildrenItems = ApiResponseEntityReference; + +export type ApiResponseEntityRelationshipsChildren = ApiResponseEntityRelationshipsChildrenItems[]; + +export type ApiResponseEntityRelationshipsAdditionalProperties = ApiResponseEntityReference; + +export interface ApiResponseEntityRelationships { + "parent"?: ApiResponseEntityRelationshipsParent; + "children"?: ApiResponseEntityRelationshipsChildren; + [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 + ApiResponseEntityRelationshipsParent | + ApiResponseEntityRelationshipsChildren | + ApiResponseEntityRelationshipsAdditionalProperties | + undefined; +} + +export type ApiResponseEntityId = string; + +export type _ApiResponseEntityAttributes = ApiResponseEntityAttributes; + +export type ApiResponseEntityAdditionalProperties = never; + +export interface ApiResponseEntity { + "id": ApiResponseEntityId; + "type": ApiResponseEntityType; + "attributes": _ApiResponseEntityAttributes; + "relationships"?: ApiResponseEntityRelationships; +} + +export type ApiResponseAppliedFiltersTypesItems = "user" | "organization" | "resource"; + +export type ApiResponseAppliedFiltersTypes = ApiResponseAppliedFiltersTypesItems[]; + +export type ApiResponseAppliedFiltersSortOrder = "asc" | "desc"; + +export type ApiResponseAppliedFiltersSortBy = "name" | "createdAt" | "updatedAt" | null; + +export type ApiResponseAppliedFiltersSearch_1 = null; + +export type ApiResponseAppliedFiltersSearch_0 = string; + +export type ApiResponseAppliedFiltersSearch = + ApiResponseAppliedFiltersSearch_0 | + ApiResponseAppliedFiltersSearch_1; + +export type ApiResponseAppliedFiltersDateRangeStart = string; + +export type ApiResponseAppliedFiltersDateRangeEnd = string; + +export type ApiResponseAppliedFiltersDateRangeAdditionalProperties = never; + +export interface ApiResponseAppliedFiltersDateRange { + "start": ApiResponseAppliedFiltersDateRangeStart; + "end": ApiResponseAppliedFiltersDateRangeEnd; +} + +export type ApiResponseAppliedFiltersAdditionalProperties = never; + +export interface ApiResponseAppliedFilters { + "search"?: ApiResponseAppliedFiltersSearch; + "dateRange"?: ApiResponseAppliedFiltersDateRange; + "types"?: ApiResponseAppliedFiltersTypes; + "sortBy"?: ApiResponseAppliedFiltersSortBy; + "sortOrder"?: ApiResponseAppliedFiltersSortOrder; +} + +export interface ApiResponse { + "status": ApiResponseStatus; + "errorCode"?: ApiResponseErrorCode; + "data": ApiResponseData; + "meta": ApiResponseMeta; +} diff --git a/test/codegen/e2e/typescript/2020-12/deeply_nested_refs/options.json b/test/codegen/e2e/typescript/2020-12/deeply_nested_refs/options.json new file mode 100644 index 000000000..712012909 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/deeply_nested_refs/options.json @@ -0,0 +1,3 @@ +{ + "defaultPrefix": "ApiResponse" +} diff --git a/test/codegen/e2e/typescript/2020-12/deeply_nested_refs/schema.json b/test/codegen/e2e/typescript/2020-12/deeply_nested_refs/schema.json new file mode 100644 index 000000000..4c3820ec2 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/deeply_nested_refs/schema.json @@ -0,0 +1,239 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/api-response", + "type": "object", + "required": [ "status", "data", "meta" ], + "properties": { + "status": { + "enum": [ "success", "error", "pending" ] + }, + "errorCode": { + "anyOf": [ + { "type": "string" }, + { "type": "null" } + ] + }, + "data": { + "$ref": "#/$defs/PaginatedResult" + }, + "meta": { + "$ref": "#/$defs/ResponseMeta" + } + }, + "additionalProperties": false, + "$defs": { + "PaginatedResult": { + "type": "object", + "required": [ "items", "pagination" ], + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/$defs/Entity" + } + }, + "pagination": { + "$ref": "#/$defs/PaginationInfo" + }, + "filters": { + "$ref": "#/$defs/AppliedFilters" + } + }, + "additionalProperties": false + }, + "Entity": { + "type": "object", + "required": [ "id", "type", "attributes" ], + "properties": { + "id": { + "type": "string" + }, + "type": { + "enum": [ "user", "organization", "resource" ] + }, + "attributes": { + "$ref": "#/$defs/EntityAttributes" + }, + "relationships": { + "type": "object", + "properties": { + "parent": { + "$ref": "#/$defs/EntityReference" + }, + "children": { + "type": "array", + "items": { + "$ref": "#/$defs/EntityReference" + } + } + }, + "additionalProperties": { + "$ref": "#/$defs/EntityReference" + } + } + }, + "additionalProperties": false + }, + "EntityAttributes": { + "type": "object", + "required": [ "name", "createdAt" ], + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 255 + }, + "description": { + "anyOf": [ + { "type": "string" }, + { "type": "null" } + ] + }, + "createdAt": { + "type": "string" + }, + "updatedAt": { + "anyOf": [ + { "type": "string" }, + { "type": "null" } + ] + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "metadata": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { "type": "string" }, + { "type": "number" }, + { "type": "boolean" } + ] + } + } + }, + "additionalProperties": false + }, + "EntityReference": { + "type": "object", + "required": [ "id", "type" ], + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "additionalProperties": false + }, + "PaginationInfo": { + "type": "object", + "required": [ "page", "pageSize", "totalItems", "totalPages" ], + "properties": { + "page": { + "type": "integer", + "minimum": 1 + }, + "pageSize": { + "type": "integer", + "minimum": 1, + "maximum": 100 + }, + "totalItems": { + "type": "integer", + "minimum": 0 + }, + "totalPages": { + "type": "integer", + "minimum": 0 + }, + "hasNextPage": { + "enum": [ true, false ] + }, + "hasPreviousPage": { + "enum": [ true, false ] + } + }, + "additionalProperties": false + }, + "AppliedFilters": { + "type": "object", + "properties": { + "search": { + "anyOf": [ + { "type": "string" }, + { "type": "null" } + ] + }, + "dateRange": { + "type": "object", + "properties": { + "start": { + "type": "string" + }, + "end": { + "type": "string" + } + }, + "required": [ "start", "end" ], + "additionalProperties": false + }, + "types": { + "type": "array", + "items": { + "enum": [ "user", "organization", "resource" ] + } + }, + "sortBy": { + "enum": [ "name", "createdAt", "updatedAt", null ] + }, + "sortOrder": { + "enum": [ "asc", "desc" ] + } + }, + "additionalProperties": false + }, + "ResponseMeta": { + "type": "object", + "required": [ "requestId", "timestamp" ], + "properties": { + "requestId": { + "type": "string" + }, + "timestamp": { + "type": "string" + }, + "version": { + "type": "string" + }, + "deprecationWarnings": { + "type": "array", + "items": { + "type": "object", + "required": [ "message", "field" ], + "properties": { + "message": { + "type": "string" + }, + "field": { + "type": "string" + }, + "suggestedAlternative": { + "anyOf": [ + { "type": "string" }, + { "type": "null" } + ] + } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false + } + } +} diff --git a/test/codegen/e2e/typescript/2020-12/deeply_nested_refs/test.ts b/test/codegen/e2e/typescript/2020-12/deeply_nested_refs/test.ts new file mode 100644 index 000000000..65258b743 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/deeply_nested_refs/test.ts @@ -0,0 +1,225 @@ +import { + ApiResponse, + ApiResponseEntity, + ApiResponseEntityAttributes, + ApiResponsePaginationInfo, + ApiResponseAppliedFilters +} 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: ApiResponseEntity = { + id: "user-1", + type: "user", + attributes: { + name: "John Doe", + createdAt: "2024-01-01" + } +}; + +// Invalid: Entity type must be from enum +const invalidEntityType: ApiResponseEntity = { + 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: ApiResponseEntity = { + 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: ApiResponseEntityAttributes = { + 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: ApiResponseEntityAttributes = { + 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: ApiResponseEntityAttributes = { + 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: ApiResponsePaginationInfo = { + page: 1, + pageSize: 20, + totalItems: 100, + totalPages: 5, + hasNextPage: true, + hasPreviousPage: false +}; + +// Valid: AppliedFilters with sortBy enum including null +const filters: ApiResponseAppliedFilters = { + search: "query", + sortBy: "name", + sortOrder: "asc" +}; + +// Valid: sortBy can be null (it's in the enum) +const filtersWithNullSort: ApiResponseAppliedFilters = { + sortBy: null, + sortOrder: "desc" +}; + +// Invalid: sortBy must be from enum +const invalidSortBy: ApiResponseAppliedFilters = { + // @ts-expect-error - sortBy must be name|createdAt|updatedAt|null + sortBy: "id" +}; + +// Invalid: sortOrder must be asc or desc +const invalidSortOrder: ApiResponseAppliedFilters = { + // @ts-expect-error - sortOrder must be asc|desc + sortOrder: "random" +}; + +// Valid: types array with enum items +const filtersWithTypes: ApiResponseAppliedFilters = { + types: [ "user", "organization" ] +}; + +// Invalid: types must be from enum +const invalidTypes: ApiResponseAppliedFilters = { + // @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/codegen/e2e/typescript/2020-12/defs_and_refs/expected.d.ts b/test/codegen/e2e/typescript/2020-12/defs_and_refs/expected.d.ts new file mode 100644 index 000000000..4149fc51a --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/defs_and_refs/expected.d.ts @@ -0,0 +1,157 @@ +export type SocialPlatformUser = _SocialPlatformUser; + +export type SocialPlatformSettings = _SocialPlatformSettings; + +export type SocialPlatformPostsItems = SocialPlatformPost; + +export type SocialPlatformPosts = SocialPlatformPostsItems[]; + +export type SocialPlatformPinnedPost_1 = null; + +export type SocialPlatformPinnedPost_0 = SocialPlatformPost; + +export type SocialPlatformPinnedPost = + SocialPlatformPinnedPost_0 | + SocialPlatformPinnedPost_1; + +export type SocialPlatformFollowersItems = _SocialPlatformUser; + +export type SocialPlatformFollowers = SocialPlatformFollowersItems[]; + +export type SocialPlatformAdditionalProperties = never; + +export type SocialPlatformUserUsername = string; + +export type SocialPlatformUserProfile = SocialPlatformProfile; + +export type SocialPlatformUserId = SocialPlatformUUID; + +export type SocialPlatformUserEmail = SocialPlatformEmail; + +export type SocialPlatformUserAdditionalProperties = never; + +export interface _SocialPlatformUser { + "id": SocialPlatformUserId; + "username": SocialPlatformUserUsername; + "email": SocialPlatformUserEmail; + "profile"?: SocialPlatformUserProfile; +} + +export type SocialPlatformUUID = string; + +export type SocialPlatformURL = string; + +export type SocialPlatformTheme = "light" | "dark" | "system"; + +export type SocialPlatformTagSlug = string; + +export type SocialPlatformTagName = string; + +export type SocialPlatformTagAdditionalProperties = never; + +export interface SocialPlatformTag { + "name": SocialPlatformTagName; + "slug"?: SocialPlatformTagSlug; +} + +export type SocialPlatformSettingsTheme = SocialPlatformTheme; + +export type SocialPlatformSettingsPrivacy = SocialPlatformPrivacySettings; + +export type SocialPlatformSettingsNotifications = SocialPlatformNotificationSettings; + +export type SocialPlatformSettingsAdditionalProperties = never; + +export interface _SocialPlatformSettings { + "theme"?: SocialPlatformSettingsTheme; + "notifications"?: SocialPlatformSettingsNotifications; + "privacy"?: SocialPlatformSettingsPrivacy; +} + +export type SocialPlatformProfileLocation_1 = null; + +export type SocialPlatformProfileLocation_0 = string; + +export type SocialPlatformProfileLocation = + SocialPlatformProfileLocation_0 | + SocialPlatformProfileLocation_1; + +export type SocialPlatformProfileBio_1 = null; + +export type SocialPlatformProfileBio_0 = string; + +export type SocialPlatformProfileBio = + SocialPlatformProfileBio_0 | + SocialPlatformProfileBio_1; + +export type SocialPlatformProfileAvatar = SocialPlatformURL; + +export type SocialPlatformProfileAdditionalProperties = never; + +export interface SocialPlatformProfile { + "bio"?: SocialPlatformProfileBio; + "avatar"?: SocialPlatformProfileAvatar; + "location"?: SocialPlatformProfileLocation; +} + +export type SocialPlatformPrivacySettingsShowEmail = boolean; + +export type SocialPlatformPrivacySettingsProfileVisible = boolean; + +export type SocialPlatformPrivacySettingsAdditionalProperties = never; + +export interface SocialPlatformPrivacySettings { + "profileVisible"?: SocialPlatformPrivacySettingsProfileVisible; + "showEmail"?: SocialPlatformPrivacySettingsShowEmail; +} + +export type SocialPlatformPostStatus = "draft" | "published" | "archived"; + +export type SocialPlatformPostTitle = string; + +export type SocialPlatformPostTagsItems = SocialPlatformTag; + +export type SocialPlatformPostTags = SocialPlatformPostTagsItems[]; + +export type _SocialPlatformPostStatus = SocialPlatformPostStatus; + +export type SocialPlatformPostId = SocialPlatformUUID; + +export type SocialPlatformPostContent = string; + +export type SocialPlatformPostAuthor = _SocialPlatformUser; + +export type SocialPlatformPostAdditionalProperties = never; + +export interface SocialPlatformPost { + "id": SocialPlatformPostId; + "title": SocialPlatformPostTitle; + "content"?: SocialPlatformPostContent; + "author": SocialPlatformPostAuthor; + "tags"?: SocialPlatformPostTags; + "status"?: _SocialPlatformPostStatus; +} + +export type SocialPlatformNotificationSettingsSms = boolean; + +export type SocialPlatformNotificationSettingsPush = boolean; + +export type SocialPlatformNotificationSettingsEmail = boolean; + +export type SocialPlatformNotificationSettingsAdditionalProperties = never; + +export interface SocialPlatformNotificationSettings { + "email"?: SocialPlatformNotificationSettingsEmail; + "push"?: SocialPlatformNotificationSettingsPush; + "sms"?: SocialPlatformNotificationSettingsSms; +} + +export type SocialPlatformEmail = string; + +export interface SocialPlatform { + "user": SocialPlatformUser; + "posts": SocialPlatformPosts; + "settings": SocialPlatformSettings; + "followers"?: SocialPlatformFollowers; + "pinnedPost"?: SocialPlatformPinnedPost; +} diff --git a/test/codegen/e2e/typescript/2020-12/defs_and_refs/options.json b/test/codegen/e2e/typescript/2020-12/defs_and_refs/options.json new file mode 100644 index 000000000..576397179 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/defs_and_refs/options.json @@ -0,0 +1,3 @@ +{ + "defaultPrefix": "SocialPlatform" +} diff --git a/test/codegen/e2e/typescript/2020-12/defs_and_refs/schema.json b/test/codegen/e2e/typescript/2020-12/defs_and_refs/schema.json new file mode 100644 index 000000000..053a49d3f --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/defs_and_refs/schema.json @@ -0,0 +1,126 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/defs-refs-test", + "type": "object", + "required": [ "user", "posts", "settings" ], + "properties": { + "user": { "$ref": "#/$defs/User" }, + "posts": { + "type": "array", + "items": { "$ref": "#/$defs/Post" } + }, + "settings": { "$ref": "#/$defs/Settings" }, + "followers": { + "type": "array", + "items": { "$ref": "#/$defs/User" } + }, + "pinnedPost": { + "anyOf": [ + { "$ref": "#/$defs/Post" }, + { "type": "null" } + ] + } + }, + "additionalProperties": false, + "$defs": { + "User": { + "type": "object", + "required": [ "id", "username", "email" ], + "properties": { + "id": { "$ref": "#/$defs/UUID" }, + "username": { "type": "string" }, + "email": { "$ref": "#/$defs/Email" }, + "profile": { "$ref": "#/$defs/Profile" } + }, + "additionalProperties": false + }, + "Profile": { + "type": "object", + "properties": { + "bio": { + "anyOf": [ + { "type": "string" }, + { "type": "null" } + ] + }, + "avatar": { "$ref": "#/$defs/URL" }, + "location": { + "anyOf": [ + { "type": "string" }, + { "type": "null" } + ] + } + }, + "additionalProperties": false + }, + "Post": { + "type": "object", + "required": [ "id", "title", "author" ], + "properties": { + "id": { "$ref": "#/$defs/UUID" }, + "title": { "type": "string" }, + "content": { "type": "string" }, + "author": { "$ref": "#/$defs/User" }, + "tags": { + "type": "array", + "items": { "$ref": "#/$defs/Tag" } + }, + "status": { "$ref": "#/$defs/PostStatus" } + }, + "additionalProperties": false + }, + "Tag": { + "type": "object", + "required": [ "name" ], + "properties": { + "name": { "type": "string" }, + "slug": { "type": "string" } + }, + "additionalProperties": false + }, + "Settings": { + "type": "object", + "properties": { + "theme": { "$ref": "#/$defs/Theme" }, + "notifications": { "$ref": "#/$defs/NotificationSettings" }, + "privacy": { "$ref": "#/$defs/PrivacySettings" } + }, + "additionalProperties": false + }, + "Theme": { + "enum": [ "light", "dark", "system" ] + }, + "NotificationSettings": { + "type": "object", + "properties": { + "email": { "type": "boolean" }, + "push": { "type": "boolean" }, + "sms": { "type": "boolean" } + }, + "additionalProperties": false + }, + "PrivacySettings": { + "type": "object", + "properties": { + "profileVisible": { "type": "boolean" }, + "showEmail": { "type": "boolean" } + }, + "additionalProperties": false + }, + "PostStatus": { + "enum": [ "draft", "published", "archived" ] + }, + "UUID": { + "type": "string", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" + }, + "Email": { + "type": "string", + "format": "email" + }, + "URL": { + "type": "string", + "format": "uri" + } + } +} diff --git a/test/codegen/e2e/typescript/2020-12/defs_and_refs/test.ts b/test/codegen/e2e/typescript/2020-12/defs_and_refs/test.ts new file mode 100644 index 000000000..271a82289 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/defs_and_refs/test.ts @@ -0,0 +1,222 @@ +import { + SocialPlatform, + _SocialPlatformUser, + SocialPlatformPost, + _SocialPlatformSettings, + SocialPlatformTheme, + SocialPlatformPostStatus +} 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: _SocialPlatformUser = { + 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: _SocialPlatformUser = { + id: "550e8400-e29b-41d4-a716-446655440002", + username: "bob", + email: "bob@example.com", + profile: { + bio: null, + location: null + } +}; + +// Valid: post with all fields +const validPost: SocialPlatformPost = { + 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: SocialPlatformTheme = "light"; +const darkTheme: SocialPlatformTheme = "dark"; +const systemTheme: SocialPlatformTheme = "system"; + +// Invalid: Theme must be from enum +// @ts-expect-error - theme must be light|dark|system +const invalidTheme: SocialPlatformTheme = "blue"; + +// Valid: PostStatus enum values +const draftStatus: SocialPlatformPostStatus = "draft"; +const publishedStatus: SocialPlatformPostStatus = "published"; +const archivedStatus: SocialPlatformPostStatus = "archived"; + +// Invalid: PostStatus must be from enum +// @ts-expect-error - status must be draft|published|archived +const invalidStatus: SocialPlatformPostStatus = "deleted"; + +// Valid: settings with all fields +const fullSettings: _SocialPlatformSettings = { + 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: SocialPlatformPost = { + id: "post-id", + author: { id: "uuid", username: "test", email: "test@test.com" } +}; + +// Invalid: Post with invalid status enum +const invalidPostStatus: SocialPlatformPost = { + 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: _SocialPlatformUser = { + 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/codegen/e2e/typescript/2020-12/dynamic_anchor_passthrough/expected.d.ts b/test/codegen/e2e/typescript/2020-12/dynamic_anchor_passthrough/expected.d.ts new file mode 100644 index 000000000..7c95a104a --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/dynamic_anchor_passthrough/expected.d.ts @@ -0,0 +1,10 @@ +export type NodeValue = number; + +export type NodeName = string; + +export type NodeAdditionalProperties = never; + +export interface Node { + "name": NodeName; + "value"?: NodeValue; +} diff --git a/test/codegen/e2e/typescript/2020-12/dynamic_anchor_passthrough/options.json b/test/codegen/e2e/typescript/2020-12/dynamic_anchor_passthrough/options.json new file mode 100644 index 000000000..75db2bffc --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/dynamic_anchor_passthrough/options.json @@ -0,0 +1,3 @@ +{ + "defaultPrefix": "Node" +} diff --git a/test/codegen/e2e/typescript/2020-12/dynamic_anchor_passthrough/schema.json b/test/codegen/e2e/typescript/2020-12/dynamic_anchor_passthrough/schema.json new file mode 100644 index 000000000..92ac94547 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/dynamic_anchor_passthrough/schema.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$dynamicAnchor": "node", + "type": "object", + "required": [ "name" ], + "properties": { + "name": { "type": "string" }, + "value": { "type": "integer" } + }, + "additionalProperties": false +} diff --git a/test/codegen/e2e/typescript/2020-12/dynamic_anchor_passthrough/test.ts b/test/codegen/e2e/typescript/2020-12/dynamic_anchor_passthrough/test.ts new file mode 100644 index 000000000..019526814 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/dynamic_anchor_passthrough/test.ts @@ -0,0 +1,18 @@ +import { Node } from "./expected"; + +const valid: Node = { name: "hello", value: 42 }; + +const nameOnly: Node = { name: "hello" }; + +// @ts-expect-error +const missingName: Node = { value: 42 }; + +// @ts-expect-error +const wrongNameType: Node = { name: 42 }; + +// additionalProperties: false +const extra: Node = { + name: "hello", + // @ts-expect-error + other: "nope" +}; diff --git a/test/codegen/e2e/typescript/2020-12/dynamic_ref_generic_list/expected.d.ts b/test/codegen/e2e/typescript/2020-12/dynamic_ref_generic_list/expected.d.ts new file mode 100644 index 000000000..d10c46724 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/dynamic_ref_generic_list/expected.d.ts @@ -0,0 +1,33 @@ +export type StringList_0 = StringListGenericList; + +export type StringListStringItem = string; + +export type StringListGenericListItems = + StringListGenericListDefaultItem | + StringListStringItem; + +export type StringListGenericListDefaultItem_5 = number; + +export type StringListGenericListDefaultItem_4 = string; + +export type StringListGenericListDefaultItem_3Items = unknown; + +export type StringListGenericListDefaultItem_3 = StringListGenericListDefaultItem_3Items[]; + +export type StringListGenericListDefaultItem_2 = Record; + +export type StringListGenericListDefaultItem_1 = boolean; + +export type StringListGenericListDefaultItem_0 = null; + +export type StringListGenericListDefaultItem = + StringListGenericListDefaultItem_0 | + StringListGenericListDefaultItem_1 | + StringListGenericListDefaultItem_2 | + StringListGenericListDefaultItem_3 | + StringListGenericListDefaultItem_4 | + StringListGenericListDefaultItem_5; + +export type StringListGenericList = StringListGenericListItems[]; + +export type StringList = StringList_0; diff --git a/test/codegen/e2e/typescript/2020-12/dynamic_ref_generic_list/options.json b/test/codegen/e2e/typescript/2020-12/dynamic_ref_generic_list/options.json new file mode 100644 index 000000000..28068f692 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/dynamic_ref_generic_list/options.json @@ -0,0 +1,3 @@ +{ + "defaultPrefix": "StringList" +} diff --git a/test/codegen/e2e/typescript/2020-12/dynamic_ref_generic_list/schema.json b/test/codegen/e2e/typescript/2020-12/dynamic_ref_generic_list/schema.json new file mode 100644 index 000000000..8cdaba7e7 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/dynamic_ref_generic_list/schema.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/string-list", + "$ref": "https://example.com/generic-list", + "$defs": { + "StringItem": { + "$dynamicAnchor": "list-item", + "type": "string" + }, + "GenericList": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/generic-list", + "type": "array", + "items": { "$dynamicRef": "#list-item" }, + "$defs": { + "DefaultItem": { + "$dynamicAnchor": "list-item" + } + } + } + } +} diff --git a/test/codegen/e2e/typescript/2020-12/dynamic_ref_generic_list/test.ts b/test/codegen/e2e/typescript/2020-12/dynamic_ref_generic_list/test.ts new file mode 100644 index 000000000..bb66bdf56 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/dynamic_ref_generic_list/test.ts @@ -0,0 +1,23 @@ +import { StringList } from "./expected"; + +// At runtime, the dynamic ref resolves to the string anchor (the parent +// schema overrides the default). But the codegen emits a union of ALL possible +// targets: the unconstrained default (all JSON types) plus the string anchor. +// So the generated type allows any JSON value as items. The generated types +// are always a superset of what JSON Schema allows, never a subset. +const strings: StringList = [ "hello", "world" ]; + +const numbers: StringList = [ 1, 2, 3 ]; + +const mixed: StringList = [ "hello", 42, true, null ]; + +const objects: StringList = [ { key: "value" } ]; + +const empty: StringList = []; + +// @ts-expect-error +const notArray: StringList = "hello"; + +// undefined is not a JSON type and is not in any anchor's type union +// @ts-expect-error +const undefinedItem: StringList = [ undefined ]; diff --git a/test/codegen/e2e/typescript/2020-12/dynamic_ref_multiple_anchors/expected.d.ts b/test/codegen/e2e/typescript/2020-12/dynamic_ref_multiple_anchors/expected.d.ts new file mode 100644 index 000000000..aff68af53 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/dynamic_ref_multiple_anchors/expected.d.ts @@ -0,0 +1,13 @@ +export type Root_0 = RootList; + +export type RootStringItem = string; + +export type RootListItems = + RootListDefaultItem | + RootStringItem; + +export type RootListDefaultItem = number; + +export type RootList = RootListItems[]; + +export type Root = Root_0; diff --git a/test/codegen/e2e/typescript/2020-12/dynamic_ref_multiple_anchors/options.json b/test/codegen/e2e/typescript/2020-12/dynamic_ref_multiple_anchors/options.json new file mode 100644 index 000000000..facad54f3 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/dynamic_ref_multiple_anchors/options.json @@ -0,0 +1,3 @@ +{ + "defaultPrefix": "Root" +} diff --git a/test/codegen/e2e/typescript/2020-12/dynamic_ref_multiple_anchors/schema.json b/test/codegen/e2e/typescript/2020-12/dynamic_ref_multiple_anchors/schema.json new file mode 100644 index 000000000..00396f1d9 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/dynamic_ref_multiple_anchors/schema.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/root", + "$ref": "list", + "$defs": { + "StringItem": { + "$dynamicAnchor": "item", + "type": "string" + }, + "List": { + "$id": "list", + "type": "array", + "items": { "$dynamicRef": "#item" }, + "$defs": { + "DefaultItem": { + "$dynamicAnchor": "item", + "type": "number" + } + } + } + } +} diff --git a/test/codegen/e2e/typescript/2020-12/dynamic_ref_multiple_anchors/test.ts b/test/codegen/e2e/typescript/2020-12/dynamic_ref_multiple_anchors/test.ts new file mode 100644 index 000000000..2d3e070c7 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/dynamic_ref_multiple_anchors/test.ts @@ -0,0 +1,29 @@ +import { Root } from "./expected"; + +const strings: Root = [ "hello", "world" ]; + +// JSON Schema would reject this (the dynamic scope resolves to the root's +// string anchor at runtime, so only strings are valid). But the codegen emits +// a union of ALL possible dynamic anchor targets (string | number), because +// the actual target depends on the runtime evaluation path. The generated +// types are always a superset of what JSON Schema allows, never a subset. +const numbers: Root = [ 1, 2, 3 ]; + +const mixed: Root = [ "hello", 42 ]; + +const empty: Root = []; + +// @ts-expect-error +const notArray: Root = "hello"; + +// Boolean is not in the union (string | number) +// @ts-expect-error +const invalidItem: Root = [ true ]; + +// Null is not in the union +// @ts-expect-error +const nullItem: Root = [ null ]; + +// Object is not in the union +// @ts-expect-error +const objectItem: Root = [ { key: "value" } ]; diff --git a/test/codegen/e2e/typescript/2020-12/dynamic_ref_single_anchor/expected.d.ts b/test/codegen/e2e/typescript/2020-12/dynamic_ref_single_anchor/expected.d.ts new file mode 100644 index 000000000..94ba4358f --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/dynamic_ref_single_anchor/expected.d.ts @@ -0,0 +1,5 @@ +export type StringArrayItems = StringArrayItemType; + +export type StringArrayItemType = string; + +export type StringArray = StringArrayItems[]; diff --git a/test/codegen/e2e/typescript/2020-12/dynamic_ref_single_anchor/options.json b/test/codegen/e2e/typescript/2020-12/dynamic_ref_single_anchor/options.json new file mode 100644 index 000000000..368747fdf --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/dynamic_ref_single_anchor/options.json @@ -0,0 +1,3 @@ +{ + "defaultPrefix": "StringArray" +} diff --git a/test/codegen/e2e/typescript/2020-12/dynamic_ref_single_anchor/schema.json b/test/codegen/e2e/typescript/2020-12/dynamic_ref_single_anchor/schema.json new file mode 100644 index 000000000..3c7fe9244 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/dynamic_ref_single_anchor/schema.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "items": { "$dynamicRef": "#item" }, + "$defs": { + "ItemType": { + "$dynamicAnchor": "item", + "type": "string" + } + } +} diff --git a/test/codegen/e2e/typescript/2020-12/dynamic_ref_single_anchor/test.ts b/test/codegen/e2e/typescript/2020-12/dynamic_ref_single_anchor/test.ts new file mode 100644 index 000000000..79a72a3a2 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/dynamic_ref_single_anchor/test.ts @@ -0,0 +1,11 @@ +import { StringArray } from "./expected"; + +const valid: StringArray = [ "hello", "world" ]; + +const empty: StringArray = []; + +// @ts-expect-error +const invalidItem: StringArray = [ 42 ]; + +// @ts-expect-error +const notArray: StringArray = "hello"; diff --git a/test/codegen/e2e/typescript/2020-12/embedded_resources/expected.d.ts b/test/codegen/e2e/typescript/2020-12/embedded_resources/expected.d.ts new file mode 100644 index 000000000..7c070e86c --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/embedded_resources/expected.d.ts @@ -0,0 +1,15 @@ +export type DocumentSystemItem = _DocumentSystemItem; + +export type DocumentSystemAdditionalProperties = never; + +export type DocumentSystemItemName = string; + +export type DocumentSystemItemAdditionalProperties = never; + +export interface _DocumentSystemItem { + "name": DocumentSystemItemName; +} + +export interface DocumentSystem { + "item": DocumentSystemItem; +} diff --git a/test/codegen/e2e/typescript/2020-12/embedded_resources/options.json b/test/codegen/e2e/typescript/2020-12/embedded_resources/options.json new file mode 100644 index 000000000..6ad44480c --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/embedded_resources/options.json @@ -0,0 +1,3 @@ +{ + "defaultPrefix": "DocumentSystem" +} diff --git a/test/codegen/e2e/typescript/2020-12/embedded_resources/schema.json b/test/codegen/e2e/typescript/2020-12/embedded_resources/schema.json new file mode 100644 index 000000000..1ecf28c1b --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/embedded_resources/schema.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/main", + "type": "object", + "required": [ "item" ], + "properties": { + "item": { "$ref": "https://example.com/item" } + }, + "additionalProperties": false, + "$defs": { + "Item": { + "$id": "https://example.com/item", + "type": "object", + "required": [ "name" ], + "properties": { + "name": { "type": "string" } + }, + "additionalProperties": false + } + } +} diff --git a/test/codegen/e2e/typescript/2020-12/embedded_resources/test.ts b/test/codegen/e2e/typescript/2020-12/embedded_resources/test.ts new file mode 100644 index 000000000..27ba8f5b3 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/embedded_resources/test.ts @@ -0,0 +1,46 @@ +import { DocumentSystem, _DocumentSystemItem } 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: _DocumentSystemItem = { name: "standalone" }; +// @ts-expect-error +const invalidItem: _DocumentSystemItem = { name: 123 }; diff --git a/test/codegen/e2e/typescript/2020-12/enum_complex_types/expected.d.ts b/test/codegen/e2e/typescript/2020-12/enum_complex_types/expected.d.ts new file mode 100644 index 000000000..e1e866e65 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/enum_complex_types/expected.d.ts @@ -0,0 +1,28 @@ +export type ComplexEnumStatus = { + "code": 200, + "message": "OK" +} | { + "code": 404, + "message": "Not Found" +} | { + "code": 500, + "message": "Internal Server Error" +}; + +export type ComplexEnumFixedList = [ "read", "write", "execute" ]; + +export type ComplexEnumFixedConfig = { + "enabled": true, + "maxRetries": 3 +}; + +export type ComplexEnumCoordinates = [ 0, 0 ] | [ 1, 1 ] | [ -1, -1 ]; + +export type ComplexEnumAdditionalProperties = never; + +export interface ComplexEnum { + "status"?: ComplexEnumStatus; + "coordinates"?: ComplexEnumCoordinates; + "fixedConfig"?: ComplexEnumFixedConfig; + "fixedList"?: ComplexEnumFixedList; +} diff --git a/test/codegen/e2e/typescript/2020-12/enum_complex_types/options.json b/test/codegen/e2e/typescript/2020-12/enum_complex_types/options.json new file mode 100644 index 000000000..a812ef0b4 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/enum_complex_types/options.json @@ -0,0 +1,3 @@ +{ + "defaultPrefix": "ComplexEnum" +} diff --git a/test/codegen/e2e/typescript/2020-12/enum_complex_types/schema.json b/test/codegen/e2e/typescript/2020-12/enum_complex_types/schema.json new file mode 100644 index 000000000..e1bea9641 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/enum_complex_types/schema.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "status": { + "enum": [ + { "code": 200, "message": "OK" }, + { "code": 404, "message": "Not Found" }, + { "code": 500, "message": "Internal Server Error" } + ] + }, + "coordinates": { + "enum": [ + [ 0, 0 ], + [ 1, 1 ], + [ -1, -1 ] + ] + }, + "fixedConfig": { + "const": { "enabled": true, "maxRetries": 3 } + }, + "fixedList": { + "const": [ "read", "write", "execute" ] + } + }, + "additionalProperties": false +} diff --git a/test/codegen/e2e/typescript/2020-12/enum_complex_types/test.ts b/test/codegen/e2e/typescript/2020-12/enum_complex_types/test.ts new file mode 100644 index 000000000..a07de3685 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/enum_complex_types/test.ts @@ -0,0 +1,137 @@ +import { + ComplexEnum, + ComplexEnumStatus, + ComplexEnumCoordinates, + ComplexEnumFixedConfig, + ComplexEnumFixedList +} 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: ComplexEnumStatus = { code: 200, message: "OK" }; +const validStatus2: ComplexEnumStatus = { code: 404, message: "Not Found" }; +const validStatus3: ComplexEnumStatus = { code: 500, message: "Internal Server Error" }; +// @ts-expect-error - must be one of the enum values +const invalidStatusStandalone: ComplexEnumStatus = { code: 201, message: "Created" }; + +const validCoords1: ComplexEnumCoordinates = [ 0, 0 ]; +const validCoords2: ComplexEnumCoordinates = [ 1, 1 ]; +const validCoords3: ComplexEnumCoordinates = [ -1, -1 ]; +// @ts-expect-error - must be one of the enum values +const invalidCoordsStandalone: ComplexEnumCoordinates = [ 0, 1 ]; + +const validConfig: ComplexEnumFixedConfig = { enabled: true, maxRetries: 3 }; +// @ts-expect-error - must match const exactly +const invalidConfig: ComplexEnumFixedConfig = { enabled: true, maxRetries: 10 }; + +const validList: ComplexEnumFixedList = [ "read", "write", "execute" ]; +// @ts-expect-error - must match const exactly +const invalidList: ComplexEnumFixedList = [ "read", "write" ]; diff --git a/test/codegen/e2e/typescript/2020-12/enum_with_complex_values/expected.d.ts b/test/codegen/e2e/typescript/2020-12/enum_with_complex_values/expected.d.ts new file mode 100644 index 000000000..fd047d361 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/enum_with_complex_values/expected.d.ts @@ -0,0 +1,19 @@ +export type SchemaSimpleEnum = "foo" | "bar" | "baz"; + +export type SchemaMixedEnum = "active" | 42 | true | null; + +export type SchemaEnumWithObject = "simple" | { + "type": "complex", + "value": 123 +}; + +export type SchemaEnumWithArray = 1 | [ 1, 2, 3 ]; + +export type SchemaAdditionalProperties = never; + +export interface Schema { + "simpleEnum"?: SchemaSimpleEnum; + "mixedEnum"?: SchemaMixedEnum; + "enumWithObject"?: SchemaEnumWithObject; + "enumWithArray"?: SchemaEnumWithArray; +} diff --git a/test/codegen/e2e/typescript/2020-12/enum_with_complex_values/options.json b/test/codegen/e2e/typescript/2020-12/enum_with_complex_values/options.json new file mode 100644 index 000000000..ece7910a4 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/enum_with_complex_values/options.json @@ -0,0 +1,3 @@ +{ + "defaultPrefix": "Schema" +} diff --git a/test/codegen/e2e/typescript/2020-12/enum_with_complex_values/schema.json b/test/codegen/e2e/typescript/2020-12/enum_with_complex_values/schema.json new file mode 100644 index 000000000..40bd7b33f --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/enum_with_complex_values/schema.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "simpleEnum": { + "enum": [ "foo", "bar", "baz" ] + }, + "mixedEnum": { + "enum": [ "active", 42, true, null ] + }, + "enumWithObject": { + "enum": [ "simple", { "type": "complex", "value": 123 } ] + }, + "enumWithArray": { + "enum": [ 1, [ 1, 2, 3 ] ] + } + }, + "additionalProperties": false +} diff --git a/test/codegen/e2e/typescript/2020-12/enum_with_complex_values/test.ts b/test/codegen/e2e/typescript/2020-12/enum_with_complex_values/test.ts new file mode 100644 index 000000000..7086c2823 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/enum_with_complex_values/test.ts @@ -0,0 +1,59 @@ +import { + Schema, + SchemaSimpleEnum, + SchemaMixedEnum, + SchemaEnumWithObject, + SchemaEnumWithArray +} from "./expected"; + + +// Valid: simple string enum values +const simple1: SchemaSimpleEnum = "foo"; +const simple2: SchemaSimpleEnum = "bar"; +const simple3: SchemaSimpleEnum = "baz"; + +// Invalid: wrong string for simple enum +// @ts-expect-error - must be "foo" | "bar" | "baz" +const simpleInvalid: SchemaSimpleEnum = "invalid"; + +// Valid: mixed enum values +const mixed1: SchemaMixedEnum = "active"; +const mixed2: SchemaMixedEnum = 42; +const mixed3: SchemaMixedEnum = true; +const mixed4: SchemaMixedEnum = null; + +// Invalid: wrong value for mixed enum +// @ts-expect-error - must be "active" | 42 | true | null +const mixedInvalid: SchemaMixedEnum = "inactive"; + +// Valid: enum with object value +const withObj1: SchemaEnumWithObject = "simple"; +const withObj2: SchemaEnumWithObject = { type: "complex", value: 123 }; + +// Invalid: completely wrong type for enum with object +// @ts-expect-error - must be "simple" or the exact object literal +const withObjInvalid: SchemaEnumWithObject = "wrong"; + +// Valid: enum with array value +const withArr1: SchemaEnumWithArray = 1; +const withArr2: SchemaEnumWithArray = [ 1, 2, 3 ]; + +// Invalid: wrong array +// @ts-expect-error - must be exactly [1, 2, 3] +const withArrInvalid: SchemaEnumWithArray = [ 1, 2 ]; + +// Valid: full schema object +const fullSchema: Schema = { + simpleEnum: "foo", + mixedEnum: 42, + enumWithObject: { type: "complex", value: 123 }, + enumWithArray: [ 1, 2, 3 ] +}; + +// Valid: partial schema +const partialSchema: Schema = { + simpleEnum: "bar" +}; + +// Valid: empty schema (all optional) +const emptySchema: Schema = {}; diff --git a/test/codegen/e2e/typescript/2020-12/if_then_else_objects/expected.d.ts b/test/codegen/e2e/typescript/2020-12/if_then_else_objects/expected.d.ts new file mode 100644 index 000000000..7ed8d4d5e --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/if_then_else_objects/expected.d.ts @@ -0,0 +1,25 @@ +export type ShapeThenRadius = number; + +export interface ShapeThen { + "radius": ShapeThenRadius; + [key: string]: unknown | undefined; +} + +export type ShapeIfKind = "circle"; + +export interface ShapeIf { + "kind": ShapeIfKind; + [key: string]: unknown | undefined; +} + +export type ShapeElseSides = number; + +export interface ShapeElse { + "sides": ShapeElseSides; + [key: string]: unknown | undefined; +} + +// (if & then) | else approximation: the else branch is wider than what +// JSON Schema allows, as TypeScript cannot express type negation +export type Shape = + (ShapeIf & ShapeThen) | ShapeElse; diff --git a/test/codegen/e2e/typescript/2020-12/if_then_else_objects/options.json b/test/codegen/e2e/typescript/2020-12/if_then_else_objects/options.json new file mode 100644 index 000000000..fb9189ea8 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/if_then_else_objects/options.json @@ -0,0 +1,3 @@ +{ + "defaultPrefix": "Shape" +} diff --git a/test/codegen/e2e/typescript/2020-12/if_then_else_objects/schema.json b/test/codegen/e2e/typescript/2020-12/if_then_else_objects/schema.json new file mode 100644 index 000000000..2cdec51c2 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/if_then_else_objects/schema.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "if": { + "type": "object", + "properties": { "kind": { "const": "circle" } }, + "required": [ "kind" ] + }, + "then": { + "type": "object", + "properties": { "radius": { "type": "number" } }, + "required": [ "radius" ] + }, + "else": { + "type": "object", + "properties": { "sides": { "type": "integer" } }, + "required": [ "sides" ] + } +} diff --git a/test/codegen/e2e/typescript/2020-12/if_then_else_objects/test.ts b/test/codegen/e2e/typescript/2020-12/if_then_else_objects/test.ts new file mode 100644 index 000000000..cd4163eed --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/if_then_else_objects/test.ts @@ -0,0 +1,31 @@ +import { Shape } from "./expected"; + +// Valid: satisfies the if condition (kind="circle") and then branch (radius) +const circle: Shape = { + kind: "circle", + radius: 5 +}; + +// Valid: does not satisfy the if condition, satisfies else branch (sides) +const polygon: Shape = { + sides: 6 +}; + +// Invalid: satisfies if (kind="circle") but missing then's required radius +// @ts-expect-error +const circleWithoutRadius: Shape = { + kind: "circle" +}; + +// Invalid: does not satisfy if, and missing else's required sides +// @ts-expect-error +const emptyObject: Shape = {}; + +// NOTE: This passes TypeScript but would fail JSON Schema validation. +// The if condition matches (kind is "circle"), so the then branch should +// apply (requiring radius). But our (If & Then) | Else approximation +// allows the else branch to also match when if holds. +const circleMatchingElse: Shape = { + kind: "circle", + sides: 4 +}; diff --git a/test/codegen/e2e/typescript/2020-12/if_then_else_validation_only/expected.d.ts b/test/codegen/e2e/typescript/2020-12/if_then_else_validation_only/expected.d.ts new file mode 100644 index 000000000..e14cb6b51 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/if_then_else_validation_only/expected.d.ts @@ -0,0 +1,16 @@ +export type PatternString_1 = string; + +export type PatternString_0Then = string; + +export type PatternString_0If = string; + +export type PatternString_0Else = string; + +// (if & then) | else approximation: the else branch is wider than what +// JSON Schema allows, as TypeScript cannot express type negation +export type PatternString_0 = + (PatternString_0If & PatternString_0Then) | PatternString_0Else; + +export type PatternString = + PatternString_0 & + PatternString_1; diff --git a/test/codegen/e2e/typescript/2020-12/if_then_else_validation_only/options.json b/test/codegen/e2e/typescript/2020-12/if_then_else_validation_only/options.json new file mode 100644 index 000000000..7684a31df --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/if_then_else_validation_only/options.json @@ -0,0 +1,3 @@ +{ + "defaultPrefix": "PatternString" +} diff --git a/test/codegen/e2e/typescript/2020-12/if_then_else_validation_only/schema.json b/test/codegen/e2e/typescript/2020-12/if_then_else_validation_only/schema.json new file mode 100644 index 000000000..cec189db1 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/if_then_else_validation_only/schema.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "if": { "type": "string", "maxLength": 10 }, + "then": { "type": "string", "pattern": "^short" }, + "else": { "type": "string", "pattern": "^long" } +} diff --git a/test/codegen/e2e/typescript/2020-12/if_then_else_validation_only/test.ts b/test/codegen/e2e/typescript/2020-12/if_then_else_validation_only/test.ts new file mode 100644 index 000000000..8ba5cf2d6 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/if_then_else_validation_only/test.ts @@ -0,0 +1,14 @@ +import { PatternString } from "./expected"; + +// Valid: any string satisfies the type since all branches resolve to string +const short_value: PatternString = "short123"; +const long_value: PatternString = "longstringvalue"; +const empty_value: PatternString = ""; + +// Invalid: not a string +// @ts-expect-error +const number_value: PatternString = 42; + +// Invalid: not a string +// @ts-expect-error +const boolean_value: PatternString = true; diff --git a/test/codegen/e2e/typescript/2020-12/implicit_types_and_reused_refs/expected.d.ts b/test/codegen/e2e/typescript/2020-12/implicit_types_and_reused_refs/expected.d.ts new file mode 100644 index 000000000..b5ffd6c10 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/implicit_types_and_reused_refs/expected.d.ts @@ -0,0 +1,235 @@ +export type DocSystemRelatedDocumentsItemsTitle_1 = null; + +export type DocSystemRelatedDocumentsItemsTitle_0 = string; + +export type DocSystemRelatedDocumentsItemsTitle = + DocSystemRelatedDocumentsItemsTitle_0 | + DocSystemRelatedDocumentsItemsTitle_1; + +export type DocSystemRelatedDocumentsItemsRelationship = "parent" | "child" | "sibling" | "reference"; + +export type DocSystemRelatedDocumentsItemsId = DocSystemUUID; + +export type DocSystemRelatedDocumentsItemsAdditionalProperties = never; + +export interface DocSystemRelatedDocumentsItems { + "id": DocSystemRelatedDocumentsItemsId; + "relationship": DocSystemRelatedDocumentsItemsRelationship; + "title"?: DocSystemRelatedDocumentsItemsTitle; +} + +export type DocSystemRelatedDocuments = DocSystemRelatedDocumentsItems[]; + +export type DocSystemPermissionsReadersItems = DocSystemUser; + +export type DocSystemPermissionsReaders = DocSystemPermissionsReadersItems[]; + +export type DocSystemPermissionsOwner = DocSystemUser; + +export type DocSystemPermissionsIsPublic = boolean; + +export type DocSystemPermissionsExpiresAt_1 = null; + +export type DocSystemPermissionsExpiresAt_0 = DocSystemTimestamp; + +export type DocSystemPermissionsExpiresAt = + DocSystemPermissionsExpiresAt_0 | + DocSystemPermissionsExpiresAt_1; + +export type DocSystemPermissionsEditorsItems = DocSystemUser; + +export type DocSystemPermissionsEditors = DocSystemPermissionsEditorsItems[]; + +export type DocSystemPermissionsAdditionalProperties = never; + +export interface DocSystemPermissions { + "owner": DocSystemPermissionsOwner; + "readers": DocSystemPermissionsReaders; + "editors": DocSystemPermissionsEditors; + "isPublic"?: DocSystemPermissionsIsPublic; + "expiresAt"?: DocSystemPermissionsExpiresAt; +} + +export type DocSystemHistoryItemsTimestamp = DocSystemTimestamp; + +export type DocSystemHistoryItemsDetails_1 = null; + +export type DocSystemHistoryItemsDetails_0OldValue_2 = null; + +export type DocSystemHistoryItemsDetails_0OldValue_1 = number; + +export type DocSystemHistoryItemsDetails_0OldValue_0 = string; + +export type DocSystemHistoryItemsDetails_0OldValue = + DocSystemHistoryItemsDetails_0OldValue_0 | + DocSystemHistoryItemsDetails_0OldValue_1 | + DocSystemHistoryItemsDetails_0OldValue_2; + +export type DocSystemHistoryItemsDetails_0NewValue_2 = null; + +export type DocSystemHistoryItemsDetails_0NewValue_1 = number; + +export type DocSystemHistoryItemsDetails_0NewValue_0 = string; + +export type DocSystemHistoryItemsDetails_0NewValue = + DocSystemHistoryItemsDetails_0NewValue_0 | + DocSystemHistoryItemsDetails_0NewValue_1 | + DocSystemHistoryItemsDetails_0NewValue_2; + +export type DocSystemHistoryItemsDetails_0Field = string; + +export type DocSystemHistoryItemsDetails_0AdditionalProperties = never; + +export interface DocSystemHistoryItemsDetails_0 { + "field"?: DocSystemHistoryItemsDetails_0Field; + "oldValue"?: DocSystemHistoryItemsDetails_0OldValue; + "newValue"?: DocSystemHistoryItemsDetails_0NewValue; +} + +export type DocSystemHistoryItemsDetails = + DocSystemHistoryItemsDetails_0 | + DocSystemHistoryItemsDetails_1; + +export type DocSystemHistoryItemsActor = DocSystemUser; + +export type DocSystemHistoryItemsAction = "created" | "updated" | "deleted" | "restored" | "shared"; + +export type DocSystemHistoryItemsAdditionalProperties = never; + +export interface DocSystemHistoryItems { + "action": DocSystemHistoryItemsAction; + "actor": DocSystemHistoryItemsActor; + "timestamp": DocSystemHistoryItemsTimestamp; + "details"?: DocSystemHistoryItemsDetails; +} + +export type DocSystemHistory = DocSystemHistoryItems[]; + +export type DocSystemDocumentTitle = string; + +export type DocSystemDocumentTagsItemsName = string; + +export type DocSystemDocumentTagsItemsColor_1 = null; + +export type DocSystemDocumentTagsItemsColor_0 = string; + +export type DocSystemDocumentTagsItemsColor = + DocSystemDocumentTagsItemsColor_0 | + DocSystemDocumentTagsItemsColor_1; + +export type DocSystemDocumentTagsItemsAdditionalProperties = never; + +export interface DocSystemDocumentTagsItems { + "name": DocSystemDocumentTagsItemsName; + "color"?: DocSystemDocumentTagsItemsColor; +} + +export type DocSystemDocumentTags = DocSystemDocumentTagsItems[]; + +export type DocSystemDocumentReviewersItems = DocSystemUser; + +export type DocSystemDocumentReviewers = DocSystemDocumentReviewersItems[]; + +export type DocSystemDocumentMetadataVersion = number; + +export type DocSystemDocumentMetadataUpdatedAt = DocSystemTimestamp; + +export type DocSystemDocumentMetadataCreatedAt = DocSystemTimestamp; + +export type DocSystemDocumentMetadataAdditionalProperties = never; + +export interface DocSystemDocumentMetadata { + "createdAt"?: DocSystemDocumentMetadataCreatedAt; + "updatedAt"?: DocSystemDocumentMetadataUpdatedAt; + "version"?: DocSystemDocumentMetadataVersion; +} + +export type DocSystemDocumentId = DocSystemUUID; + +export type DocSystemDocumentContentSummary_1 = null; + +export type DocSystemDocumentContentSummary_0 = string; + +export type DocSystemDocumentContentSummary = + DocSystemDocumentContentSummary_0 | + DocSystemDocumentContentSummary_1; + +export type DocSystemDocumentContentFormat = "markdown" | "html" | "plaintext"; + +export type DocSystemDocumentContentBody = string; + +export type DocSystemDocumentContentAdditionalProperties = never; + +export interface DocSystemDocumentContent { + "format": DocSystemDocumentContentFormat; + "body": DocSystemDocumentContentBody; + "summary"?: DocSystemDocumentContentSummary; +} + +export type DocSystemDocumentAuthor = DocSystemUser; + +export type DocSystemDocumentAdditionalProperties = never; + +export interface DocSystemDocument { + "id": DocSystemDocumentId; + "title": DocSystemDocumentTitle; + "content": DocSystemDocumentContent; + "author": DocSystemDocumentAuthor; + "reviewers"?: DocSystemDocumentReviewers; + "tags"?: DocSystemDocumentTags; + "metadata"?: DocSystemDocumentMetadata; +} + +export type DocSystemAdditionalProperties = never; + +export type DocSystemUserRole = "admin" | "editor" | "viewer" | "guest"; + +export type DocSystemUserId = DocSystemUUID; + +export type DocSystemUserEmail = string; + +export type DocSystemUserDisplayName_1 = null; + +export type DocSystemUserDisplayName_0 = string; + +export type DocSystemUserDisplayName = + DocSystemUserDisplayName_0 | + DocSystemUserDisplayName_1; + +export type DocSystemUserAdditionalProperties = never; + +export interface DocSystemUser { + "id": DocSystemUserId; + "email": DocSystemUserEmail; + "displayName"?: DocSystemUserDisplayName; + "role"?: DocSystemUserRole; +} + +export type DocSystemUUID = string; + +export type DocSystemTimestampUnix = number; + +export type DocSystemTimestampTimezone_1 = null; + +export type DocSystemTimestampTimezone_0 = string; + +export type DocSystemTimestampTimezone = + DocSystemTimestampTimezone_0 | + DocSystemTimestampTimezone_1; + +export type DocSystemTimestampIso = string; + +export type DocSystemTimestampAdditionalProperties = never; + +export interface DocSystemTimestamp { + "unix": DocSystemTimestampUnix; + "iso": DocSystemTimestampIso; + "timezone"?: DocSystemTimestampTimezone; +} + +export interface DocSystem { + "document": DocSystemDocument; + "permissions": DocSystemPermissions; + "history": DocSystemHistory; + "relatedDocuments"?: DocSystemRelatedDocuments; +} diff --git a/test/codegen/e2e/typescript/2020-12/implicit_types_and_reused_refs/options.json b/test/codegen/e2e/typescript/2020-12/implicit_types_and_reused_refs/options.json new file mode 100644 index 000000000..84f2c2a32 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/implicit_types_and_reused_refs/options.json @@ -0,0 +1,3 @@ +{ + "defaultPrefix": "DocSystem" +} diff --git a/test/codegen/e2e/typescript/2020-12/implicit_types_and_reused_refs/schema.json b/test/codegen/e2e/typescript/2020-12/implicit_types_and_reused_refs/schema.json new file mode 100644 index 000000000..4d5a1aef0 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/implicit_types_and_reused_refs/schema.json @@ -0,0 +1,181 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/document-system", + "type": "object", + "required": [ "document", "permissions", "history" ], + "properties": { + "document": { + "type": "object", + "required": [ "id", "title", "content", "author" ], + "properties": { + "id": { "$ref": "#/$defs/UUID" }, + "title": { "type": "string" }, + "content": { + "type": "object", + "required": [ "format", "body" ], + "properties": { + "format": { "enum": [ "markdown", "html", "plaintext" ] }, + "body": { "type": "string" }, + "summary": { + "anyOf": [ + { "type": "string" }, + { "type": "null" } + ] + } + }, + "additionalProperties": false + }, + "author": { "$ref": "#/$defs/User" }, + "reviewers": { + "type": "array", + "items": { "$ref": "#/$defs/User" } + }, + "tags": { + "type": "array", + "items": { + "type": "object", + "required": [ "name" ], + "properties": { + "name": { "type": "string" }, + "color": { + "anyOf": [ + { "type": "string" }, + { "type": "null" } + ] + } + }, + "additionalProperties": false + } + }, + "metadata": { + "type": "object", + "properties": { + "createdAt": { "$ref": "#/$defs/Timestamp" }, + "updatedAt": { "$ref": "#/$defs/Timestamp" }, + "version": { "type": "integer" } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "permissions": { + "type": "object", + "required": [ "owner", "readers", "editors" ], + "properties": { + "owner": { "$ref": "#/$defs/User" }, + "readers": { + "type": "array", + "items": { "$ref": "#/$defs/User" } + }, + "editors": { + "type": "array", + "items": { "$ref": "#/$defs/User" } + }, + "isPublic": { "enum": [ true, false ] }, + "expiresAt": { + "anyOf": [ + { "$ref": "#/$defs/Timestamp" }, + { "type": "null" } + ] + } + }, + "additionalProperties": false + }, + "history": { + "type": "array", + "items": { + "type": "object", + "required": [ "action", "actor", "timestamp" ], + "properties": { + "action": { "enum": [ "created", "updated", "deleted", "restored", "shared" ] }, + "actor": { "$ref": "#/$defs/User" }, + "timestamp": { "$ref": "#/$defs/Timestamp" }, + "details": { + "anyOf": [ + { + "type": "object", + "properties": { + "field": { "type": "string" }, + "oldValue": { + "anyOf": [ + { "type": "string" }, + { "type": "number" }, + { "type": "null" } + ] + }, + "newValue": { + "anyOf": [ + { "type": "string" }, + { "type": "number" }, + { "type": "null" } + ] + } + }, + "additionalProperties": false + }, + { "type": "null" } + ] + } + }, + "additionalProperties": false + } + }, + "relatedDocuments": { + "type": "array", + "items": { + "type": "object", + "required": [ "id", "relationship" ], + "properties": { + "id": { "$ref": "#/$defs/UUID" }, + "relationship": { "enum": [ "parent", "child", "sibling", "reference" ] }, + "title": { + "anyOf": [ + { "type": "string" }, + { "type": "null" } + ] + } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false, + "$defs": { + "UUID": { + "type": "string", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" + }, + "User": { + "type": "object", + "required": [ "id", "email" ], + "properties": { + "id": { "$ref": "#/$defs/UUID" }, + "email": { "type": "string" }, + "displayName": { + "anyOf": [ + { "type": "string" }, + { "type": "null" } + ] + }, + "role": { "enum": [ "admin", "editor", "viewer", "guest" ] } + }, + "additionalProperties": false + }, + "Timestamp": { + "type": "object", + "required": [ "unix", "iso" ], + "properties": { + "unix": { "type": "integer" }, + "iso": { "type": "string" }, + "timezone": { + "anyOf": [ + { "type": "string" }, + { "type": "null" } + ] + } + }, + "additionalProperties": false + } + } +} diff --git a/test/codegen/e2e/typescript/2020-12/implicit_types_and_reused_refs/test.ts b/test/codegen/e2e/typescript/2020-12/implicit_types_and_reused_refs/test.ts new file mode 100644 index 000000000..baa73e677 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/implicit_types_and_reused_refs/test.ts @@ -0,0 +1,255 @@ +import { + DocSystem, + DocSystemUser, + DocSystemTimestamp, + DocSystemDocument, + DocSystemPermissions, + DocSystemHistoryItems +} 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: DocSystemUser = { + id: "user-123", + email: "user@example.com", + displayName: "John Doe", + role: "admin" +}; + +// Valid: User with null displayName (anyOf [string, null]) +const userNullDisplay: DocSystemUser = { + id: "user-123", + email: "user@example.com", + displayName: null +}; + +// Invalid: User role must be from enum +const invalidRole: DocSystemUser = { + 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: DocSystemTimestamp = { + unix: 1704067200, + iso: "2024-01-01T00:00:00Z", + timezone: "UTC" +}; + +// Valid: Timestamp with null timezone +const timestampNullTz: DocSystemTimestamp = { + unix: 1704067200, + iso: "2024-01-01T00:00:00Z", + timezone: null +}; + +// Invalid: Timestamp missing required unix +// @ts-expect-error - unix is required +const timestampMissingUnix: DocSystemTimestamp = { + iso: "2024-01-01T00:00:00Z" +}; + +// Valid: Document with format enum values +const docMarkdown: DocSystemDocument = { + id: "doc-1", + title: "Markdown Doc", + content: { format: "markdown", body: "# Title" }, + author: { id: "user-1", email: "a@b.com" } +}; + +const docHtml: DocSystemDocument = { + id: "doc-2", + title: "HTML Doc", + content: { format: "html", body: "

Title

" }, + author: { id: "user-1", email: "a@b.com" } +}; + +const docPlaintext: DocSystemDocument = { + 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: DocSystemDocument = { + 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: DocSystemPermissions = { + owner: { id: "user-1", email: "owner@a.com" }, + readers: [], + editors: [], + isPublic: true +}; + +const permissionsPrivate: DocSystemPermissions = { + owner: { id: "user-1", email: "owner@a.com" }, + readers: [], + editors: [], + isPublic: false +}; + +// Valid: permissions with expiresAt as Timestamp +const permissionsWithExpiry: DocSystemPermissions = { + 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: DocSystemPermissions = { + owner: { id: "user-1", email: "owner@a.com" }, + readers: [], + editors: [], + expiresAt: null +}; + +// Valid: history item with all action enum values +const historyCreated: DocSystemHistoryItems = { + action: "created", + actor: { id: "user-1", email: "a@b.com" }, + timestamp: { unix: 1704067200, iso: "2024-01-01T00:00:00Z" } +}; + +const historyUpdated: DocSystemHistoryItems = { + 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: DocSystemHistoryItems = { + action: "deleted", + actor: { id: "user-1", email: "a@b.com" }, + timestamp: { unix: 1704067200, iso: "2024-01-01T00:00:00Z" } +}; + +const historyRestored: DocSystemHistoryItems = { + action: "restored", + actor: { id: "user-1", email: "a@b.com" }, + timestamp: { unix: 1704067200, iso: "2024-01-01T00:00:00Z" } +}; + +const historyShared: DocSystemHistoryItems = { + 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: DocSystemHistoryItems = { + // @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: DocSystemHistoryItems = { + 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: DocSystemHistoryItems = { + 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/codegen/e2e/typescript/2020-12/object_with_additional_properties/expected.d.ts b/test/codegen/e2e/typescript/2020-12/object_with_additional_properties/expected.d.ts new file mode 100644 index 000000000..9adb4dbed --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/object_with_additional_properties/expected.d.ts @@ -0,0 +1,18 @@ +export type PersonName = string; + +export type PersonAge = number; + +export type PersonAdditionalProperties = string; + +export interface Person { + "name": PersonName; + "age"?: PersonAge; + [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 + PersonName | + PersonAge | + PersonAdditionalProperties | + undefined; +} diff --git a/test/codegen/e2e/typescript/2020-12/object_with_additional_properties/options.json b/test/codegen/e2e/typescript/2020-12/object_with_additional_properties/options.json new file mode 100644 index 000000000..8d69d111e --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/object_with_additional_properties/options.json @@ -0,0 +1,3 @@ +{ + "defaultPrefix": "Person" +} diff --git a/test/codegen/e2e/typescript/2020-12/object_with_additional_properties/schema.json b/test/codegen/e2e/typescript/2020-12/object_with_additional_properties/schema.json new file mode 100644 index 000000000..b96125fac --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/object_with_additional_properties/schema.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "age": { + "type": "integer" + } + }, + "required": [ "name" ], + "additionalProperties": { + "type": "string" + } +} diff --git a/test/codegen/e2e/typescript/2020-12/object_with_additional_properties/test.ts b/test/codegen/e2e/typescript/2020-12/object_with_additional_properties/test.ts new file mode 100644 index 000000000..a931f17a1 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/object_with_additional_properties/test.ts @@ -0,0 +1,33 @@ +import { Person } from "./expected"; + +const test1: Person = { + name: "John Doe" +}; + +const test2: Person = { + // @ts-expect-error + name: 123 +}; + +const test3: Person = { + name: "John Doe", + age: 30 +}; + +const test4: Person = { + name: "John Doe", + age: 30, + extra: "foo" +}; + +// @ts-expect-error name is required +const test5: Person = { + extra: "foo" +}; + +const test6: Person = { + name: "John Doe", + age: 30, + // @ts-expect-error + extra: true +}; diff --git a/test/codegen/e2e/typescript/2020-12/object_with_optional_string_property/expected.d.ts b/test/codegen/e2e/typescript/2020-12/object_with_optional_string_property/expected.d.ts new file mode 100644 index 000000000..74e74c17d --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/object_with_optional_string_property/expected.d.ts @@ -0,0 +1,6 @@ +export type MyObjectFoo = string; + +export interface MyObject { + "foo"?: MyObjectFoo; + [key: string]: unknown | undefined; +} diff --git a/test/codegen/e2e/typescript/2020-12/object_with_optional_string_property/options.json b/test/codegen/e2e/typescript/2020-12/object_with_optional_string_property/options.json new file mode 100644 index 000000000..1494f4c50 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/object_with_optional_string_property/options.json @@ -0,0 +1,3 @@ +{ + "defaultPrefix": "MyObject" +} diff --git a/test/codegen/e2e/typescript/2020-12/object_with_optional_string_property/schema.json b/test/codegen/e2e/typescript/2020-12/object_with_optional_string_property/schema.json new file mode 100644 index 000000000..ec46a1080 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/object_with_optional_string_property/schema.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "foo": { + "type": "string" + } + } +} diff --git a/test/codegen/e2e/typescript/2020-12/object_with_optional_string_property/test.ts b/test/codegen/e2e/typescript/2020-12/object_with_optional_string_property/test.ts new file mode 100644 index 000000000..347949946 --- /dev/null +++ b/test/codegen/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/codegen/e2e/typescript/2020-12/oneof_union/expected.d.ts b/test/codegen/e2e/typescript/2020-12/oneof_union/expected.d.ts new file mode 100644 index 000000000..d68053790 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/oneof_union/expected.d.ts @@ -0,0 +1,25 @@ +export type OneOfTestValue_2 = boolean; + +export type OneOfTestValue_1 = number; + +export type OneOfTestValue_0 = string; + +export type OneOfTestValue = + OneOfTestValue_0 | + OneOfTestValue_1 | + OneOfTestValue_2; + +export type OneOfTestStatus_1 = "completed" | "cancelled"; + +export type OneOfTestStatus_0 = "pending" | "active"; + +export type OneOfTestStatus = + OneOfTestStatus_0 | + OneOfTestStatus_1; + +export type OneOfTestAdditionalProperties = never; + +export interface OneOfTest { + "value": OneOfTestValue; + "status"?: OneOfTestStatus; +} diff --git a/test/codegen/e2e/typescript/2020-12/oneof_union/options.json b/test/codegen/e2e/typescript/2020-12/oneof_union/options.json new file mode 100644 index 000000000..343a43e3e --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/oneof_union/options.json @@ -0,0 +1,3 @@ +{ + "defaultPrefix": "OneOfTest" +} diff --git a/test/codegen/e2e/typescript/2020-12/oneof_union/schema.json b/test/codegen/e2e/typescript/2020-12/oneof_union/schema.json new file mode 100644 index 000000000..0223b06d7 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/oneof_union/schema.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "value": { + "oneOf": [ + { "type": "string" }, + { "type": "integer" }, + { "type": "boolean" } + ] + }, + "status": { + "oneOf": [ + { "enum": [ "pending", "active" ] }, + { "enum": [ "completed", "cancelled" ] } + ] + } + }, + "required": [ "value" ], + "additionalProperties": false +} diff --git a/test/codegen/e2e/typescript/2020-12/oneof_union/test.ts b/test/codegen/e2e/typescript/2020-12/oneof_union/test.ts new file mode 100644 index 000000000..ae77941fe --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/oneof_union/test.ts @@ -0,0 +1,53 @@ +import { OneOfTest } from "./expected"; + +// Valid: value as string +const withString: OneOfTest = { + value: "hello" +}; + +// Valid: value as integer +const withInteger: OneOfTest = { + value: 42 +}; + +// Valid: value as boolean +const withBoolean: OneOfTest = { + value: true +}; + +// Valid: with status from first enum +const withPendingStatus: OneOfTest = { + value: "test", + status: "pending" +}; + +// Valid: with status from second enum +const withCompletedStatus: OneOfTest = { + value: 123, + status: "completed" +}; + +// Invalid: value cannot be null +const invalidNull: OneOfTest = { + // @ts-expect-error + value: null +}; + +// Invalid: value cannot be object +const invalidObject: OneOfTest = { + // @ts-expect-error + value: { foo: "bar" } +}; + +// Invalid: status must be one of the allowed values +const invalidStatus: OneOfTest = { + value: "test", + // @ts-expect-error + status: "unknown" +}; + +// Invalid: missing required value +// @ts-expect-error +const missingValue: OneOfTest = { + status: "active" +}; diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_additional_false/expected.d.ts b/test/codegen/e2e/typescript/2020-12/pattern_properties_additional_false/expected.d.ts new file mode 100644 index 000000000..ca2c39ed3 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_additional_false/expected.d.ts @@ -0,0 +1,10 @@ +export type StrictExtName = string; + +export type StrictExtX = string; + +export type StrictExtAdditionalProperties = never; + +export interface StrictExt { + "name"?: StrictExtName; + [key: `x-${string}`]: StrictExtX; +} diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_additional_false/options.json b/test/codegen/e2e/typescript/2020-12/pattern_properties_additional_false/options.json new file mode 100644 index 000000000..07e5347d0 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_additional_false/options.json @@ -0,0 +1,3 @@ +{ + "defaultPrefix": "StrictExt" +} diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_additional_false/schema.json b/test/codegen/e2e/typescript/2020-12/pattern_properties_additional_false/schema.json new file mode 100644 index 000000000..7ac5ec5fa --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_additional_false/schema.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "name": { "type": "string" } + }, + "patternProperties": { + "^x-": { "type": "string" } + }, + "additionalProperties": false +} diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_additional_false/test.ts b/test/codegen/e2e/typescript/2020-12/pattern_properties_additional_false/test.ts new file mode 100644 index 000000000..62e67b026 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_additional_false/test.ts @@ -0,0 +1,30 @@ +import { StrictExt } from "./expected"; + +const test1: StrictExt = { + name: "hello" +}; + +const test2: StrictExt = { + name: "hello", + "x-custom": "value" +}; + +// Wrong type for pattern property +const test3: StrictExt = { + name: "hello", + // @ts-expect-error + "x-custom": 42 +}; + +// Non-matching key should be rejected (additionalProperties: false) +const test4: StrictExt = { + name: "hello", + // @ts-expect-error + other: "value" +}; + +// Edge case: key is exactly the prefix with empty suffix +const test5: StrictExt = { + name: "hello", + "x-": "value" +}; diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_additional_true/expected.d.ts b/test/codegen/e2e/typescript/2020-12/pattern_properties_additional_true/expected.d.ts new file mode 100644 index 000000000..f421c6276 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_additional_true/expected.d.ts @@ -0,0 +1,11 @@ +export type OpenExtName = string; + +export type OpenExtX = string; + +export type OpenExtAdditionalProperties = unknown; + +export interface OpenExt { + "name"?: OpenExtName; + [key: `x-${string}`]: OpenExtX; + [key: string]: unknown | undefined; +} diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_additional_true/options.json b/test/codegen/e2e/typescript/2020-12/pattern_properties_additional_true/options.json new file mode 100644 index 000000000..3735e2167 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_additional_true/options.json @@ -0,0 +1,3 @@ +{ + "defaultPrefix": "OpenExt" +} diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_additional_true/schema.json b/test/codegen/e2e/typescript/2020-12/pattern_properties_additional_true/schema.json new file mode 100644 index 000000000..c6dc1087c --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_additional_true/schema.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "name": { "type": "string" } + }, + "patternProperties": { + "^x-": { "type": "string" } + }, + "additionalProperties": true +} diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_additional_true/test.ts b/test/codegen/e2e/typescript/2020-12/pattern_properties_additional_true/test.ts new file mode 100644 index 000000000..2474fb415 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_additional_true/test.ts @@ -0,0 +1,13 @@ +import { OpenExt } from "./expected"; + +const test1: OpenExt = { + name: "hello", + "x-custom": "value", + other: 42 +}; + +const test2: OpenExt = { + name: "hello", + // @ts-expect-error + "x-custom": 123 +}; diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_empty_pattern/expected.d.ts b/test/codegen/e2e/typescript/2020-12/pattern_properties_empty_pattern/expected.d.ts new file mode 100644 index 000000000..8cc294d0a --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_empty_pattern/expected.d.ts @@ -0,0 +1,16 @@ +export type EmptyPatternName = string; + +export type EmptyPattern = number; + +export type EmptyPatternAdditionalProperties = never; + +export interface _EmptyPattern { + "name"?: EmptyPatternName; + [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 + EmptyPatternName | + EmptyPattern | + undefined; +} diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_empty_pattern/options.json b/test/codegen/e2e/typescript/2020-12/pattern_properties_empty_pattern/options.json new file mode 100644 index 000000000..bd1a8efe2 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_empty_pattern/options.json @@ -0,0 +1,3 @@ +{ + "defaultPrefix": "EmptyPattern" +} diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_empty_pattern/schema.json b/test/codegen/e2e/typescript/2020-12/pattern_properties_empty_pattern/schema.json new file mode 100644 index 000000000..9b3476721 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_empty_pattern/schema.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "name": { "type": "string" } + }, + "patternProperties": { + "": { "type": "integer" } + }, + "additionalProperties": false +} diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_empty_pattern/test.ts b/test/codegen/e2e/typescript/2020-12/pattern_properties_empty_pattern/test.ts new file mode 100644 index 000000000..8d1a36de8 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_empty_pattern/test.ts @@ -0,0 +1,20 @@ +import { _EmptyPattern } from "./expected"; + +// Empty regex matches everything, so all keys go through [key: string] +// union (string | number | undefined) +const test1: _EmptyPattern = { + name: "hello", + anything: 42 +}; + +const test2: _EmptyPattern = { + name: "hello", + anything: "also valid" +}; + +// Boolean is not in the union +const test3: _EmptyPattern = { + name: "hello", + // @ts-expect-error + flag: true +}; diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_full_hybrid/expected.d.ts b/test/codegen/e2e/typescript/2020-12/pattern_properties_full_hybrid/expected.d.ts new file mode 100644 index 000000000..937029bd2 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_full_hybrid/expected.d.ts @@ -0,0 +1,25 @@ +export type HybridName = string; + +export type HybridAge = number; + +export type HybridX = string; + +export type Hybrid_09_id = number; + +export type HybridAdditionalProperties = boolean; + +export interface Hybrid { + "name": HybridName; + "age"?: HybridAge; + [key: `x-${string}`]: HybridX; + [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 + HybridName | + HybridAge | + HybridX | + Hybrid_09_id | + HybridAdditionalProperties | + undefined; +} diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_full_hybrid/options.json b/test/codegen/e2e/typescript/2020-12/pattern_properties_full_hybrid/options.json new file mode 100644 index 000000000..e3f42df21 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_full_hybrid/options.json @@ -0,0 +1,3 @@ +{ + "defaultPrefix": "Hybrid" +} diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_full_hybrid/schema.json b/test/codegen/e2e/typescript/2020-12/pattern_properties_full_hybrid/schema.json new file mode 100644 index 000000000..e1424a761 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_full_hybrid/schema.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "name": { "type": "string" }, + "age": { "type": "integer" } + }, + "required": [ "name" ], + "patternProperties": { + "^x-": { "type": "string" }, + "[0-9]+_id": { "type": "integer" } + }, + "additionalProperties": { "type": "boolean" } +} diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_full_hybrid/test.ts b/test/codegen/e2e/typescript/2020-12/pattern_properties_full_hybrid/test.ts new file mode 100644 index 000000000..c712da2a5 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_full_hybrid/test.ts @@ -0,0 +1,59 @@ +import { Hybrid } from "./expected"; + +// All features together +const test1: Hybrid = { + name: "hello", + age: 30, + "x-custom": "extension", + "123_id": 42, + flag: true +}; + +// Required name only +const test2: Hybrid = { + name: "hello" +}; + +// Missing required name +// @ts-expect-error +const test3: Hybrid = { + age: 30 +}; + +// Prefix pattern enforced: x- must be string, not number +const test4: Hybrid = { + name: "hello", + // @ts-expect-error + "x-custom": 42 +}; + +// Array is not in the union +const test5: Hybrid = { + name: "hello", + // @ts-expect-error + extra: [ 1, 2, 3 ] +}; + +// Additional property with boolean (additionalProperties type) +const test6: Hybrid = { + name: "hello", + flag: true +}; + +// JSON Schema would reject this (additionalProperties is boolean, not +// number), but TypeScript allows it because the [key: string] union must +// include all member and pattern types (number from "age" and the non-prefix +// pattern). The generated types are always a superset of what JSON Schema +// allows, never a subset. +const test7: Hybrid = { + name: "hello", + extra: 42 +}; + +// Template literal takes priority over permissive [key: string] union: +// boolean is in the union but x- keys must be string +const test8: Hybrid = { + name: "hello", + // @ts-expect-error + "x-custom": true +}; diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_mixed_prefix_and_fallback/expected.d.ts b/test/codegen/e2e/typescript/2020-12/pattern_properties_mixed_prefix_and_fallback/expected.d.ts new file mode 100644 index 000000000..e3095517d --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_mixed_prefix_and_fallback/expected.d.ts @@ -0,0 +1,8 @@ +export type MixedFallbackX = string; + +export type MixedFallback_09 = number; + +export interface MixedFallback { + [key: `x-${string}`]: MixedFallbackX; + [key: string]: unknown | undefined; +} diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_mixed_prefix_and_fallback/options.json b/test/codegen/e2e/typescript/2020-12/pattern_properties_mixed_prefix_and_fallback/options.json new file mode 100644 index 000000000..84e096866 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_mixed_prefix_and_fallback/options.json @@ -0,0 +1 @@ +{ "defaultPrefix": "MixedFallback" } diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_mixed_prefix_and_fallback/schema.json b/test/codegen/e2e/typescript/2020-12/pattern_properties_mixed_prefix_and_fallback/schema.json new file mode 100644 index 000000000..50fd805fb --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_mixed_prefix_and_fallback/schema.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "patternProperties": { + "^x-": { "type": "string" }, + "[0-9]+": { "type": "integer" } + } +} diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_mixed_prefix_and_fallback/test.ts b/test/codegen/e2e/typescript/2020-12/pattern_properties_mixed_prefix_and_fallback/test.ts new file mode 100644 index 000000000..5d3c2cb3f --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_mixed_prefix_and_fallback/test.ts @@ -0,0 +1,28 @@ +import { MixedFallback } from "./expected"; + +// Prefix pattern enforced: x- key must be string +const test1: MixedFallback = { + "x-foo": "hello" +}; + +// Prefix pattern enforced even with [key: string]: unknown fallback +const test2: MixedFallback = { + // @ts-expect-error + "x-foo": 123 +}; + +// Non-prefix pattern falls back to unknown, so any value works +const test3: MixedFallback = { + "123": 42 +}; + +const test4: MixedFallback = { + "123": "also fine" +}; + +// Mixed together +const test5: MixedFallback = { + "x-custom": "hello", + "456": 99, + other: true +}; diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_multiple_prefixes/expected.d.ts b/test/codegen/e2e/typescript/2020-12/pattern_properties_multiple_prefixes/expected.d.ts new file mode 100644 index 000000000..bc82de56b --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_multiple_prefixes/expected.d.ts @@ -0,0 +1,9 @@ +export type MultiPatternX = string; + +export type MultiPatternData = number; + +export interface MultiPattern { + [key: `x-${string}`]: MultiPatternX; + [key: `data-${string}`]: MultiPatternData; + [key: string]: unknown | undefined; +} diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_multiple_prefixes/options.json b/test/codegen/e2e/typescript/2020-12/pattern_properties_multiple_prefixes/options.json new file mode 100644 index 000000000..d607006aa --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_multiple_prefixes/options.json @@ -0,0 +1,3 @@ +{ + "defaultPrefix": "MultiPattern" +} diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_multiple_prefixes/schema.json b/test/codegen/e2e/typescript/2020-12/pattern_properties_multiple_prefixes/schema.json new file mode 100644 index 000000000..3adde06c9 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_multiple_prefixes/schema.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "patternProperties": { + "^x-": { "type": "string" }, + "^data-": { "type": "integer" } + } +} diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_multiple_prefixes/test.ts b/test/codegen/e2e/typescript/2020-12/pattern_properties_multiple_prefixes/test.ts new file mode 100644 index 000000000..1b52fbc27 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_multiple_prefixes/test.ts @@ -0,0 +1,24 @@ +import { MultiPattern } from "./expected"; + +const test1: MultiPattern = { + "x-foo": "hello" +}; + +const test2: MultiPattern = { + "data-id": 42 +}; + +const test3: MultiPattern = { + "x-foo": "hello", + "data-id": 42 +}; + +const test4: MultiPattern = { + // @ts-expect-error + "x-foo": 123 +}; + +const test5: MultiPattern = { + // @ts-expect-error + "data-id": "not a number" +}; diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_non_prefix_additional_false/expected.d.ts b/test/codegen/e2e/typescript/2020-12/pattern_properties_non_prefix_additional_false/expected.d.ts new file mode 100644 index 000000000..96a1cd0b8 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_non_prefix_additional_false/expected.d.ts @@ -0,0 +1,16 @@ +export type StrictFallbackName = string; + +export type StrictFallbackAz_id = number; + +export type StrictFallbackAdditionalProperties = never; + +export interface StrictFallback { + "name"?: StrictFallbackName; + [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 + StrictFallbackName | + StrictFallbackAz_id | + undefined; +} diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_non_prefix_additional_false/options.json b/test/codegen/e2e/typescript/2020-12/pattern_properties_non_prefix_additional_false/options.json new file mode 100644 index 000000000..269af9e05 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_non_prefix_additional_false/options.json @@ -0,0 +1 @@ +{ "defaultPrefix": "StrictFallback" } diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_non_prefix_additional_false/schema.json b/test/codegen/e2e/typescript/2020-12/pattern_properties_non_prefix_additional_false/schema.json new file mode 100644 index 000000000..66f81b639 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_non_prefix_additional_false/schema.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "name": { "type": "string" } + }, + "patternProperties": { + "[a-z]+_id": { "type": "integer" } + }, + "additionalProperties": false +} diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_non_prefix_additional_false/test.ts b/test/codegen/e2e/typescript/2020-12/pattern_properties_non_prefix_additional_false/test.ts new file mode 100644 index 000000000..42cb1ae68 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_non_prefix_additional_false/test.ts @@ -0,0 +1,30 @@ +import { StrictFallback } from "./expected"; + +const test1: StrictFallback = { + name: "hello", + "user_id": 42 +}; + +// JSON Schema would reject this (additionalProperties: false and "other" +// does not match the pattern), but TypeScript allows it because the +// [key: string] union must include all member types (string from "name"), +// so any string value passes. The generated types are always a superset +// of what JSON Schema allows, never a subset. +const test2: StrictFallback = { + name: "hello", + other: "also string" +}; + +// Boolean is not in the union (string | number | undefined) +const test3: StrictFallback = { + name: "hello", + // @ts-expect-error + flag: true +}; + +// Array is not in the union +const test4: StrictFallback = { + name: "hello", + // @ts-expect-error + items: [ 1, 2, 3 ] +}; diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_non_prefix_fallback/expected.d.ts b/test/codegen/e2e/typescript/2020-12/pattern_properties_non_prefix_fallback/expected.d.ts new file mode 100644 index 000000000..ba14512f2 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_non_prefix_fallback/expected.d.ts @@ -0,0 +1,8 @@ +export type FallbackName = string; + +export type FallbackAz_id = number; + +export interface Fallback { + "name"?: FallbackName; + [key: string]: unknown | undefined; +} diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_non_prefix_fallback/options.json b/test/codegen/e2e/typescript/2020-12/pattern_properties_non_prefix_fallback/options.json new file mode 100644 index 000000000..4975c350a --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_non_prefix_fallback/options.json @@ -0,0 +1 @@ +{ "defaultPrefix": "Fallback" } diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_non_prefix_fallback/schema.json b/test/codegen/e2e/typescript/2020-12/pattern_properties_non_prefix_fallback/schema.json new file mode 100644 index 000000000..1ac6a7e45 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_non_prefix_fallback/schema.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "name": { "type": "string" } + }, + "patternProperties": { + "[a-z]+_id": { "type": "integer" } + } +} diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_non_prefix_fallback/test.ts b/test/codegen/e2e/typescript/2020-12/pattern_properties_non_prefix_fallback/test.ts new file mode 100644 index 000000000..1441ff004 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_non_prefix_fallback/test.ts @@ -0,0 +1,18 @@ +import { Fallback } from "./expected"; + +const test1: Fallback = { + name: "hello", + "user_id": 42 +}; + +// Name wrong type +const test2: Fallback = { + // @ts-expect-error + name: 123 +}; + +// Any extra key is allowed (additionalProperties defaults to true) +const test3: Fallback = { + name: "hello", + extra: true +}; diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_non_prefix_only/expected.d.ts b/test/codegen/e2e/typescript/2020-12/pattern_properties_non_prefix_only/expected.d.ts new file mode 100644 index 000000000..2b4d768cd --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_non_prefix_only/expected.d.ts @@ -0,0 +1,5 @@ +export type NonPrefixAz_id = number; + +export interface NonPrefix { + [key: string]: unknown | undefined; +} diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_non_prefix_only/options.json b/test/codegen/e2e/typescript/2020-12/pattern_properties_non_prefix_only/options.json new file mode 100644 index 000000000..0b8f3c8f1 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_non_prefix_only/options.json @@ -0,0 +1 @@ +{ "defaultPrefix": "NonPrefix" } diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_non_prefix_only/schema.json b/test/codegen/e2e/typescript/2020-12/pattern_properties_non_prefix_only/schema.json new file mode 100644 index 000000000..18bf6d9c1 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_non_prefix_only/schema.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "patternProperties": { + "[a-z]+_id": { "type": "integer" } + } +} diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_non_prefix_only/test.ts b/test/codegen/e2e/typescript/2020-12/pattern_properties_non_prefix_only/test.ts new file mode 100644 index 000000000..73c2cecd8 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_non_prefix_only/test.ts @@ -0,0 +1,13 @@ +import { NonPrefix } from "./expected"; + +// Any key, any value (falls back to [key: string]: unknown) +const test1: NonPrefix = { + "user_id": 42 +}; + +const test2: NonPrefix = { + "user_id": 42, + other: "hello" +}; + +const test3: NonPrefix = {}; diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_overlapping_prefixes/expected.d.ts b/test/codegen/e2e/typescript/2020-12/pattern_properties_overlapping_prefixes/expected.d.ts new file mode 100644 index 000000000..358de600d --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_overlapping_prefixes/expected.d.ts @@ -0,0 +1,10 @@ +export type OverlapXdata = number; + +export type OverlapX = string; + +export type OverlapAdditionalProperties = never; + +export interface Overlap { + [key: `x-${string}`]: OverlapX; + [key: `x-data-${string}`]: OverlapXdata & OverlapX; +} diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_overlapping_prefixes/options.json b/test/codegen/e2e/typescript/2020-12/pattern_properties_overlapping_prefixes/options.json new file mode 100644 index 000000000..4dc52751e --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_overlapping_prefixes/options.json @@ -0,0 +1,3 @@ +{ + "defaultPrefix": "Overlap" +} diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_overlapping_prefixes/schema.json b/test/codegen/e2e/typescript/2020-12/pattern_properties_overlapping_prefixes/schema.json new file mode 100644 index 000000000..5bd6b67e7 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_overlapping_prefixes/schema.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "patternProperties": { + "^x-": { "type": "string" }, + "^x-data-": { "type": "integer" } + }, + "additionalProperties": false +} diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_overlapping_prefixes/test.ts b/test/codegen/e2e/typescript/2020-12/pattern_properties_overlapping_prefixes/test.ts new file mode 100644 index 000000000..e8c78990b --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_overlapping_prefixes/test.ts @@ -0,0 +1,18 @@ +import { Overlap } from "./expected"; + +// Non-overlapping: only matches ^x- +const test1: Overlap = { + "x-foo": "hello" +}; + +// Overlapping: matches both ^x- (string) and ^x-data- (number), +// so no value can satisfy both, which is correct per JSON Schema +const test2: Overlap = { + // @ts-expect-error + "x-data-id": 42 +}; + +const test3: Overlap = { + // @ts-expect-error + "x-data-id": "hello" +}; diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_overlapping_same_type/expected.d.ts b/test/codegen/e2e/typescript/2020-12/pattern_properties_overlapping_same_type/expected.d.ts new file mode 100644 index 000000000..a6985fc21 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_overlapping_same_type/expected.d.ts @@ -0,0 +1,10 @@ +export type SameTypeXdata = string; + +export type SameTypeX = string; + +export type SameTypeAdditionalProperties = never; + +export interface SameType { + [key: `x-${string}`]: SameTypeX; + [key: `x-data-${string}`]: SameTypeXdata & SameTypeX; +} diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_overlapping_same_type/options.json b/test/codegen/e2e/typescript/2020-12/pattern_properties_overlapping_same_type/options.json new file mode 100644 index 000000000..d5d5e46c9 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_overlapping_same_type/options.json @@ -0,0 +1,3 @@ +{ + "defaultPrefix": "SameType" +} diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_overlapping_same_type/schema.json b/test/codegen/e2e/typescript/2020-12/pattern_properties_overlapping_same_type/schema.json new file mode 100644 index 000000000..200f5caac --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_overlapping_same_type/schema.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "patternProperties": { + "^x-": { "type": "string" }, + "^x-data-": { "type": "string" } + }, + "additionalProperties": false +} diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_overlapping_same_type/test.ts b/test/codegen/e2e/typescript/2020-12/pattern_properties_overlapping_same_type/test.ts new file mode 100644 index 000000000..87b70e90b --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_overlapping_same_type/test.ts @@ -0,0 +1,29 @@ +import { SameType } from "./expected"; + +// x-data-* satisfies both ^x- and ^x-data-, both string, so string works +const test1: SameType = { + "x-data-id": "hello" +}; + +// Non-overlapping x- key +const test2: SameType = { + "x-foo": "world" +}; + +// Both together +const test3: SameType = { + "x-foo": "hello", + "x-data-id": "world" +}; + +// Wrong type on overlapping key +const test4: SameType = { + // @ts-expect-error + "x-data-id": 42 +}; + +// Wrong type on non-overlapping key +const test5: SameType = { + // @ts-expect-error + "x-foo": 42 +}; diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_partial_overlap/expected.d.ts b/test/codegen/e2e/typescript/2020-12/pattern_properties_partial_overlap/expected.d.ts new file mode 100644 index 000000000..ab9ed505c --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_partial_overlap/expected.d.ts @@ -0,0 +1,16 @@ +export type PartialId = number; + +export type PartialY = number; + +export type PartialXinternal = boolean; + +export type PartialX = string; + +export type PartialAdditionalProperties = never; + +export interface Partial { + "id": PartialId; + [key: `x-${string}`]: PartialX; + [key: `x-internal-${string}`]: PartialXinternal & PartialX; + [key: `y-${string}`]: PartialY; +} diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_partial_overlap/options.json b/test/codegen/e2e/typescript/2020-12/pattern_properties_partial_overlap/options.json new file mode 100644 index 000000000..bd94f09fe --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_partial_overlap/options.json @@ -0,0 +1,3 @@ +{ + "defaultPrefix": "Partial" +} diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_partial_overlap/schema.json b/test/codegen/e2e/typescript/2020-12/pattern_properties_partial_overlap/schema.json new file mode 100644 index 000000000..f228d383b --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_partial_overlap/schema.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "id": { "type": "integer" } + }, + "required": [ "id" ], + "patternProperties": { + "^x-": { "type": "string" }, + "^x-internal-": { "type": "boolean" }, + "^y-": { "type": "integer" } + }, + "additionalProperties": false +} diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_partial_overlap/test.ts b/test/codegen/e2e/typescript/2020-12/pattern_properties_partial_overlap/test.ts new file mode 100644 index 000000000..e6d4189e6 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_partial_overlap/test.ts @@ -0,0 +1,53 @@ +import { Partial } from "./expected"; + +// Named property only +const test1: Partial = { + id: 1 +}; + +// Non-overlapping x- prefix (string) +const test2: Partial = { + id: 1, + "x-foo": "hello" +}; + +// Non-overlapping y- prefix (integer) +const test3: Partial = { + id: 1, + "y-count": 42 +}; + +// Overlapping x-internal- matches both ^x- and ^x-internal-, +// boolean & string = never, so no value satisfies both +const test4: Partial = { + id: 1, + // @ts-expect-error + "x-internal-debug": true +}; + +const test5: Partial = { + id: 1, + // @ts-expect-error + "x-internal-debug": "hello" +}; + +// Wrong type for x- prefix +const test6: Partial = { + id: 1, + // @ts-expect-error + "x-foo": 42 +}; + +// Wrong type for y- prefix +const test7: Partial = { + id: 1, + // @ts-expect-error + "y-count": "not a number" +}; + +// Non-matching key rejected (additionalProperties: false) +const test8: Partial = { + id: 1, + // @ts-expect-error + other: "value" +}; diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_prefix_only/expected.d.ts b/test/codegen/e2e/typescript/2020-12/pattern_properties_prefix_only/expected.d.ts new file mode 100644 index 000000000..6006b382b --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_prefix_only/expected.d.ts @@ -0,0 +1,6 @@ +export type SchemaX = string; + +export interface Schema { + [key: `x-${string}`]: SchemaX; + [key: string]: unknown | undefined; +} diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_prefix_only/options.json b/test/codegen/e2e/typescript/2020-12/pattern_properties_prefix_only/options.json new file mode 100644 index 000000000..ece7910a4 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_prefix_only/options.json @@ -0,0 +1,3 @@ +{ + "defaultPrefix": "Schema" +} diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_prefix_only/schema.json b/test/codegen/e2e/typescript/2020-12/pattern_properties_prefix_only/schema.json new file mode 100644 index 000000000..e92d03dbb --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_prefix_only/schema.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "patternProperties": { + "^x-": { "type": "string" } + } +} diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_prefix_only/test.ts b/test/codegen/e2e/typescript/2020-12/pattern_properties_prefix_only/test.ts new file mode 100644 index 000000000..1d6787a70 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_prefix_only/test.ts @@ -0,0 +1,20 @@ +import { Schema } from "./expected"; + +const test1: Schema = { + "x-foo": "hello" +}; + +const test2: Schema = { + "x-foo": "hello", + "x-bar": "world" +}; + +const test3: Schema = { + "x-foo": "hello", + other: "value" +}; + +const test4: Schema = { + // @ts-expect-error + "x-foo": 123 +}; diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_typed_additional/expected.d.ts b/test/codegen/e2e/typescript/2020-12/pattern_properties_typed_additional/expected.d.ts new file mode 100644 index 000000000..4396f8062 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_typed_additional/expected.d.ts @@ -0,0 +1,18 @@ +export type MixedName = string; + +export type MixedX = number; + +export type MixedAdditionalProperties = boolean; + +export interface Mixed { + "name"?: MixedName; + [key: `x-${string}`]: MixedX; + [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 + MixedName | + MixedX | + MixedAdditionalProperties | + undefined; +} diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_typed_additional/options.json b/test/codegen/e2e/typescript/2020-12/pattern_properties_typed_additional/options.json new file mode 100644 index 000000000..e221531c6 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_typed_additional/options.json @@ -0,0 +1,3 @@ +{ + "defaultPrefix": "Mixed" +} diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_typed_additional/schema.json b/test/codegen/e2e/typescript/2020-12/pattern_properties_typed_additional/schema.json new file mode 100644 index 000000000..9a35d6ed5 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_typed_additional/schema.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "name": { "type": "string" } + }, + "patternProperties": { + "^x-": { "type": "integer" } + }, + "additionalProperties": { "type": "boolean" } +} diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_typed_additional/test.ts b/test/codegen/e2e/typescript/2020-12/pattern_properties_typed_additional/test.ts new file mode 100644 index 000000000..dfa9bfbc6 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_typed_additional/test.ts @@ -0,0 +1,30 @@ +import { Mixed } from "./expected"; + +const test1: Mixed = { + name: "hello", + "x-count": 42, + extra: true +}; + +// Wrong type on pattern property (string instead of number) +const test2: Mixed = { + name: "hello", + // @ts-expect-error + "x-count": "not a number" +}; + +// Template literal signature takes priority over the permissive +// [key: string] union, so boolean is rejected even though the +// union includes boolean +const test3: Mixed = { + name: "hello", + // @ts-expect-error + "x-count": true +}; + +// Additional property with wrong type (array is not in the union) +const test4: Mixed = { + name: "hello", + // @ts-expect-error + extra: [ 1, 2, 3 ] +}; diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_with_properties/expected.d.ts b/test/codegen/e2e/typescript/2020-12/pattern_properties_with_properties/expected.d.ts new file mode 100644 index 000000000..9f3fad12d --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_with_properties/expected.d.ts @@ -0,0 +1,9 @@ +export type ExtensibleName = string; + +export type ExtensibleX = string; + +export interface Extensible { + "name": ExtensibleName; + [key: `x-${string}`]: ExtensibleX; + [key: string]: unknown | undefined; +} diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_with_properties/options.json b/test/codegen/e2e/typescript/2020-12/pattern_properties_with_properties/options.json new file mode 100644 index 000000000..1ee0865da --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_with_properties/options.json @@ -0,0 +1,3 @@ +{ + "defaultPrefix": "Extensible" +} diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_with_properties/schema.json b/test/codegen/e2e/typescript/2020-12/pattern_properties_with_properties/schema.json new file mode 100644 index 000000000..542ebd4cd --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_with_properties/schema.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "name": { "type": "string" } + }, + "required": [ "name" ], + "patternProperties": { + "^x-": { "type": "string" } + } +} diff --git a/test/codegen/e2e/typescript/2020-12/pattern_properties_with_properties/test.ts b/test/codegen/e2e/typescript/2020-12/pattern_properties_with_properties/test.ts new file mode 100644 index 000000000..bcaaf4f9b --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/pattern_properties_with_properties/test.ts @@ -0,0 +1,27 @@ +import { Extensible } from "./expected"; + +const test1: Extensible = { + name: "hello" +}; + +const test2: Extensible = { + name: "hello", + "x-custom": "value" +}; + +const test3: Extensible = { + // @ts-expect-error + name: 123 +}; + +const test4: Extensible = { + name: "hello", + // @ts-expect-error + "x-custom": 42 +}; + +// Missing required property +// @ts-expect-error +const test5: Extensible = { + "x-custom": "value" +}; diff --git a/test/codegen/e2e/typescript/2020-12/recursive_schema/expected.d.ts b/test/codegen/e2e/typescript/2020-12/recursive_schema/expected.d.ts new file mode 100644 index 000000000..3eaafb5f7 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/recursive_schema/expected.d.ts @@ -0,0 +1,15 @@ +export type TreeNodeParent = TreeNode; + +export type TreeNodeName = string; + +export type TreeNodeChildrenItems = TreeNode; + +export type TreeNodeChildren = TreeNodeChildrenItems[]; + +export type TreeNodeAdditionalProperties = never; + +export interface TreeNode { + "name": TreeNodeName; + "children"?: TreeNodeChildren; + "parent"?: TreeNodeParent; +} diff --git a/test/codegen/e2e/typescript/2020-12/recursive_schema/options.json b/test/codegen/e2e/typescript/2020-12/recursive_schema/options.json new file mode 100644 index 000000000..260f8ce22 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/recursive_schema/options.json @@ -0,0 +1,3 @@ +{ + "defaultPrefix": "TreeNode" +} diff --git a/test/codegen/e2e/typescript/2020-12/recursive_schema/schema.json b/test/codegen/e2e/typescript/2020-12/recursive_schema/schema.json new file mode 100644 index 000000000..31af853ac --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/recursive_schema/schema.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "required": [ "name" ], + "properties": { + "name": { "type": "string" }, + "children": { + "type": "array", + "items": { "$ref": "#" } + }, + "parent": { "$ref": "#" } + }, + "additionalProperties": false +} diff --git a/test/codegen/e2e/typescript/2020-12/recursive_schema/test.ts b/test/codegen/e2e/typescript/2020-12/recursive_schema/test.ts new file mode 100644 index 000000000..5eb383803 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/recursive_schema/test.ts @@ -0,0 +1,80 @@ +import { TreeNode } from "./expected"; + + +// Valid: minimal node with just name +const leaf: TreeNode = { + name: "leaf" +}; + +// Valid: node with children +const parent: TreeNode = { + name: "parent", + children: [ + { name: "child1" }, + { name: "child2" } + ] +}; + +// Valid: deeply nested structure +const root: TreeNode = { + name: "root", + children: [ + { + name: "branch1", + children: [ + { name: "leaf1" }, + { name: "leaf2" } + ] + }, + { + name: "branch2", + children: [] + } + ] +}; + +// Valid: node with parent reference +const childWithParent: TreeNode = { + name: "child", + parent: { name: "myParent" } +}; + +// Valid: complex tree with parent references +const complexTree: TreeNode = { + name: "root", + children: [ + { + name: "child", + parent: { name: "root" }, + children: [ + { name: "grandchild" } + ] + } + ] +}; + +// Invalid: missing required name +// @ts-expect-error - name is required +const missingName: TreeNode = { + children: [] +}; + +// Invalid: wrong type for name +const wrongNameType: TreeNode = { + // @ts-expect-error - name must be string + name: 123 +}; + +// Invalid: children must be array of TreeNode +const wrongChildrenType: TreeNode = { + name: "node", + // @ts-expect-error - children must be array of TreeNode + children: "not an array" +}; + +// Invalid: extra property (additionalProperties: false) +const extraProperty: TreeNode = { + name: "node", + // @ts-expect-error - extra property not allowed + extra: "not allowed" +}; diff --git a/test/codegen/e2e/typescript/2020-12/special_property_names/expected.d.ts b/test/codegen/e2e/typescript/2020-12/special_property_names/expected.d.ts new file mode 100644 index 000000000..c1df1bfe5 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/special_property_names/expected.d.ts @@ -0,0 +1,28 @@ +export type SchemaSayhello = string; + +export type SchemaPathtofile = string; + +export type SchemaMypropertyname = string; + +export type SchemaLine1line2 = string; + +export type SchemaCol1col2 = string; + +export type SchemaClass = string; + +export type Schema_123abc = string; + +export type Schema$specialchars = string; + +export type SchemaAdditionalProperties = never; + +export interface Schema { + "say \"hello\""?: SchemaSayhello; + "path\\to\\file"?: SchemaPathtofile; + "line1\nline2"?: SchemaLine1line2; + "col1\tcol2"?: SchemaCol1col2; + "$special@chars"?: Schema$specialchars; + "my property name"?: SchemaMypropertyname; + "123abc"?: Schema_123abc; + "class"?: SchemaClass; +} diff --git a/test/codegen/e2e/typescript/2020-12/special_property_names/options.json b/test/codegen/e2e/typescript/2020-12/special_property_names/options.json new file mode 100644 index 000000000..ece7910a4 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/special_property_names/options.json @@ -0,0 +1,3 @@ +{ + "defaultPrefix": "Schema" +} diff --git a/test/codegen/e2e/typescript/2020-12/special_property_names/schema.json b/test/codegen/e2e/typescript/2020-12/special_property_names/schema.json new file mode 100644 index 000000000..c64d84d9d --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/special_property_names/schema.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "say \"hello\"": { "type": "string" }, + "path\\to\\file": { "type": "string" }, + "line1\nline2": { "type": "string" }, + "col1\tcol2": { "type": "string" }, + "$special@chars": { "type": "string" }, + "my property name": { "type": "string" }, + "123abc": { "type": "string" }, + "class": { "type": "string" } + }, + "additionalProperties": false +} diff --git a/test/codegen/e2e/typescript/2020-12/special_property_names/test.ts b/test/codegen/e2e/typescript/2020-12/special_property_names/test.ts new file mode 100644 index 000000000..9068d0ff6 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/special_property_names/test.ts @@ -0,0 +1,35 @@ +import { Schema } from "./expected"; + + +// Valid: object with special property names +const valid: Schema = { + "say \"hello\"": "greeting", + "path\\to\\file": "/usr/local/bin", + "line1\nline2": "multiline", + "col1\tcol2": "tabbed", + "$special@chars": "special", + "my property name": "spaced", + "123abc": "starts with number", + "class": "reserved word" +}; + +// Valid: partial object +const partial: Schema = { + "class": "only class property" +}; + +// Valid: empty object (all properties optional) +const empty: Schema = {}; + +// Invalid: extra property (additionalProperties: false) +const extraProperty: Schema = { + "class": "valid", + // @ts-expect-error - extra property not allowed + extra: "not allowed" +}; + +// Invalid: wrong type for property +const wrongType: Schema = { + // @ts-expect-error - class must be string + "class": 123 +}; diff --git a/test/codegen/e2e/typescript/2020-12/tuples_and_arrays/expected.d.ts b/test/codegen/e2e/typescript/2020-12/tuples_and_arrays/expected.d.ts new file mode 100644 index 000000000..f8e4efad0 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/tuples_and_arrays/expected.d.ts @@ -0,0 +1,319 @@ +export type DataPipelineStagesItemsOutputType_5 = number; + +export type DataPipelineStagesItemsOutputType_4 = string; + +export type DataPipelineStagesItemsOutputType_3_1 = "sync" | "async"; + +export type DataPipelineStagesItemsOutputType_3_0 = string; + +export type DataPipelineStagesItemsOutputType_3Items = never; + +export type DataPipelineStagesItemsOutputType_3 = [DataPipelineStagesItemsOutputType_3_0, DataPipelineStagesItemsOutputType_3_1, ...DataPipelineStagesItemsOutputType_3Items[]]; + +export type DataPipelineStagesItemsOutputType_2 = Record; + +export type DataPipelineStagesItemsOutputType_1 = boolean; + +export type DataPipelineStagesItemsOutputType_0 = null; + +export type DataPipelineStagesItemsOutputType = + DataPipelineStagesItemsOutputType_0 | + DataPipelineStagesItemsOutputType_1 | + DataPipelineStagesItemsOutputType_2 | + DataPipelineStagesItemsOutputType_3 | + DataPipelineStagesItemsOutputType_4 | + DataPipelineStagesItemsOutputType_5; + +export type DataPipelineStagesItemsName = string; + +export type DataPipelineStagesItemsMetrics_5 = number; + +export type DataPipelineStagesItemsMetrics_4 = string; + +export type DataPipelineStagesItemsMetrics_3_2 = number; + +export type DataPipelineStagesItemsMetrics_3_1 = number; + +export type DataPipelineStagesItemsMetrics_3_0 = number; + +export type DataPipelineStagesItemsMetrics_3Items = never; + +export type DataPipelineStagesItemsMetrics_3 = [DataPipelineStagesItemsMetrics_3_0, DataPipelineStagesItemsMetrics_3_1, DataPipelineStagesItemsMetrics_3_2, ...DataPipelineStagesItemsMetrics_3Items[]]; + +export type DataPipelineStagesItemsMetrics_2 = Record; + +export type DataPipelineStagesItemsMetrics_1 = boolean; + +export type DataPipelineStagesItemsMetrics_0 = null; + +export type DataPipelineStagesItemsMetrics = + DataPipelineStagesItemsMetrics_0 | + DataPipelineStagesItemsMetrics_1 | + DataPipelineStagesItemsMetrics_2 | + DataPipelineStagesItemsMetrics_3 | + DataPipelineStagesItemsMetrics_4 | + DataPipelineStagesItemsMetrics_5; + +export type DataPipelineStagesItemsInputTypes_5 = number; + +export type DataPipelineStagesItemsInputTypes_4 = string; + +export type DataPipelineStagesItemsInputTypes_3_1_1 = null; + +export type DataPipelineStagesItemsInputTypes_3_1_0 = "required" | "optional"; + +export type DataPipelineStagesItemsInputTypes_3_1 = + DataPipelineStagesItemsInputTypes_3_1_0 | + DataPipelineStagesItemsInputTypes_3_1_1; + +export type DataPipelineStagesItemsInputTypes_3_0 = "string" | "number" | "boolean" | "object" | "array"; + +export type DataPipelineStagesItemsInputTypes_3ItemsTypeName = string; + +export type DataPipelineStagesItemsInputTypes_3ItemsNullable = boolean; + +export type DataPipelineStagesItemsInputTypes_3ItemsAdditionalProperties = never; + +export interface DataPipelineStagesItemsInputTypes_3Items { + "typeName"?: DataPipelineStagesItemsInputTypes_3ItemsTypeName; + "nullable"?: DataPipelineStagesItemsInputTypes_3ItemsNullable; +} + +export type DataPipelineStagesItemsInputTypes_3 = [DataPipelineStagesItemsInputTypes_3_0, DataPipelineStagesItemsInputTypes_3_1, ...DataPipelineStagesItemsInputTypes_3Items[]]; + +export type DataPipelineStagesItemsInputTypes_2 = Record; + +export type DataPipelineStagesItemsInputTypes_1 = boolean; + +export type DataPipelineStagesItemsInputTypes_0 = null; + +export type DataPipelineStagesItemsInputTypes = + DataPipelineStagesItemsInputTypes_0 | + DataPipelineStagesItemsInputTypes_1 | + DataPipelineStagesItemsInputTypes_2 | + DataPipelineStagesItemsInputTypes_3 | + DataPipelineStagesItemsInputTypes_4 | + DataPipelineStagesItemsInputTypes_5; + +export type DataPipelineStagesItemsConfig_1 = null; + +export type DataPipelineStagesItemsConfig_0Timeout = number; + +export type DataPipelineStagesItemsConfig_0Retries = number; + +export type DataPipelineStagesItemsConfig_0AdditionalProperties = never; + +export interface DataPipelineStagesItemsConfig_0 { + "timeout"?: DataPipelineStagesItemsConfig_0Timeout; + "retries"?: DataPipelineStagesItemsConfig_0Retries; +} + +export type DataPipelineStagesItemsConfig = + DataPipelineStagesItemsConfig_0 | + DataPipelineStagesItemsConfig_1; + +export type DataPipelineStagesItemsAdditionalProperties = never; + +export interface DataPipelineStagesItems { + "name": DataPipelineStagesItemsName; + "inputTypes": DataPipelineStagesItemsInputTypes; + "outputType": DataPipelineStagesItemsOutputType; + "config"?: DataPipelineStagesItemsConfig; + "metrics"?: DataPipelineStagesItemsMetrics; +} + +export type DataPipelineStages = DataPipelineStagesItems[]; + +export type DataPipelinePipelineVersion_5 = number; + +export type DataPipelinePipelineVersion_4 = string; + +export type DataPipelinePipelineVersion_3_2 = number; + +export type DataPipelinePipelineVersion_3_1 = number; + +export type DataPipelinePipelineVersion_3_0 = number; + +export type DataPipelinePipelineVersion_3Items = never; + +export type DataPipelinePipelineVersion_3 = [DataPipelinePipelineVersion_3_0, DataPipelinePipelineVersion_3_1, DataPipelinePipelineVersion_3_2, ...DataPipelinePipelineVersion_3Items[]]; + +export type DataPipelinePipelineVersion_2 = Record; + +export type DataPipelinePipelineVersion_1 = boolean; + +export type DataPipelinePipelineVersion_0 = null; + +export type DataPipelinePipelineVersion = + DataPipelinePipelineVersion_0 | + DataPipelinePipelineVersion_1 | + DataPipelinePipelineVersion_2 | + DataPipelinePipelineVersion_3 | + DataPipelinePipelineVersion_4 | + DataPipelinePipelineVersion_5; + +export type DataPipelinePipelineTagsItems = string; + +export type DataPipelinePipelineTags = DataPipelinePipelineTagsItems[]; + +export type DataPipelinePipelineId = string; + +export type DataPipelinePipelineCoordinates_5 = number; + +export type DataPipelinePipelineCoordinates_4 = string; + +export type DataPipelinePipelineCoordinates_3_2 = number; + +export type DataPipelinePipelineCoordinates_3_1 = number; + +export type DataPipelinePipelineCoordinates_3_0 = number; + +export type DataPipelinePipelineCoordinates_3Items = number; + +export type DataPipelinePipelineCoordinates_3 = [DataPipelinePipelineCoordinates_3_0, DataPipelinePipelineCoordinates_3_1, DataPipelinePipelineCoordinates_3_2, ...DataPipelinePipelineCoordinates_3Items[]]; + +export type DataPipelinePipelineCoordinates_2 = Record; + +export type DataPipelinePipelineCoordinates_1 = boolean; + +export type DataPipelinePipelineCoordinates_0 = null; + +export type DataPipelinePipelineCoordinates = + DataPipelinePipelineCoordinates_0 | + DataPipelinePipelineCoordinates_1 | + DataPipelinePipelineCoordinates_2 | + DataPipelinePipelineCoordinates_3 | + DataPipelinePipelineCoordinates_4 | + DataPipelinePipelineCoordinates_5; + +export type DataPipelinePipelineAdditionalProperties = never; + +export interface DataPipelinePipeline { + "id": DataPipelinePipelineId; + "version": DataPipelinePipelineVersion; + "coordinates": DataPipelinePipelineCoordinates; + "tags"?: DataPipelinePipelineTags; +} + +export type DataPipelineMetadataModifiedAt_1 = null; + +export type DataPipelineMetadataModifiedAt_0 = string; + +export type DataPipelineMetadataModifiedAt = + DataPipelineMetadataModifiedAt_0 | + DataPipelineMetadataModifiedAt_1; + +export type DataPipelineMetadataFlags_5 = number; + +export type DataPipelineMetadataFlags_4 = string; + +export type DataPipelineMetadataFlags_3_2 = boolean; + +export type DataPipelineMetadataFlags_3_1 = boolean; + +export type DataPipelineMetadataFlags_3_0 = boolean; + +export type DataPipelineMetadataFlags_3Items = never; + +export type DataPipelineMetadataFlags_3 = [DataPipelineMetadataFlags_3_0, DataPipelineMetadataFlags_3_1, DataPipelineMetadataFlags_3_2, ...DataPipelineMetadataFlags_3Items[]]; + +export type DataPipelineMetadataFlags_2 = Record; + +export type DataPipelineMetadataFlags_1 = boolean; + +export type DataPipelineMetadataFlags_0 = null; + +export type DataPipelineMetadataFlags = + DataPipelineMetadataFlags_0 | + DataPipelineMetadataFlags_1 | + DataPipelineMetadataFlags_2 | + DataPipelineMetadataFlags_3 | + DataPipelineMetadataFlags_4 | + DataPipelineMetadataFlags_5; + +export type DataPipelineMetadataCreatedAt = string; + +export type DataPipelineMetadataAuthorsItems_5 = number; + +export type DataPipelineMetadataAuthorsItems_4 = string; + +export type DataPipelineMetadataAuthorsItems_3_1 = string; + +export type DataPipelineMetadataAuthorsItems_3_0 = string; + +export type DataPipelineMetadataAuthorsItems_3Items = string; + +export type DataPipelineMetadataAuthorsItems_3 = [DataPipelineMetadataAuthorsItems_3_0, DataPipelineMetadataAuthorsItems_3_1, ...DataPipelineMetadataAuthorsItems_3Items[]]; + +export type DataPipelineMetadataAuthorsItems_2 = Record; + +export type DataPipelineMetadataAuthorsItems_1 = boolean; + +export type DataPipelineMetadataAuthorsItems_0 = null; + +export type DataPipelineMetadataAuthorsItems = + DataPipelineMetadataAuthorsItems_0 | + DataPipelineMetadataAuthorsItems_1 | + DataPipelineMetadataAuthorsItems_2 | + DataPipelineMetadataAuthorsItems_3 | + DataPipelineMetadataAuthorsItems_4 | + DataPipelineMetadataAuthorsItems_5; + +export type DataPipelineMetadataAuthors = DataPipelineMetadataAuthorsItems[]; + +export type DataPipelineMetadataAdditionalProperties = never; + +export interface DataPipelineMetadata { + "createdAt"?: DataPipelineMetadataCreatedAt; + "modifiedAt"?: DataPipelineMetadataModifiedAt; + "authors"?: DataPipelineMetadataAuthors; + "flags"?: DataPipelineMetadataFlags; +} + +export type DataPipelineConnectionsItems_5 = number; + +export type DataPipelineConnectionsItems_4 = string; + +export type DataPipelineConnectionsItems_3_2Weight = number; + +export type DataPipelineConnectionsItems_3_2Bidirectional = boolean; + +export type DataPipelineConnectionsItems_3_2AdditionalProperties = never; + +export interface DataPipelineConnectionsItems_3_2 { + "weight": DataPipelineConnectionsItems_3_2Weight; + "bidirectional"?: DataPipelineConnectionsItems_3_2Bidirectional; +} + +export type DataPipelineConnectionsItems_3_1 = string; + +export type DataPipelineConnectionsItems_3_0 = string; + +export type DataPipelineConnectionsItems_3Items = never; + +export type DataPipelineConnectionsItems_3 = [DataPipelineConnectionsItems_3_0, DataPipelineConnectionsItems_3_1, DataPipelineConnectionsItems_3_2, ...DataPipelineConnectionsItems_3Items[]]; + +export type DataPipelineConnectionsItems_2 = Record; + +export type DataPipelineConnectionsItems_1 = boolean; + +export type DataPipelineConnectionsItems_0 = null; + +export type DataPipelineConnectionsItems = + DataPipelineConnectionsItems_0 | + DataPipelineConnectionsItems_1 | + DataPipelineConnectionsItems_2 | + DataPipelineConnectionsItems_3 | + DataPipelineConnectionsItems_4 | + DataPipelineConnectionsItems_5; + +export type DataPipelineConnections = DataPipelineConnectionsItems[]; + +export type DataPipelineAdditionalProperties = never; + +export interface DataPipeline { + "pipeline": DataPipelinePipeline; + "stages": DataPipelineStages; + "connections": DataPipelineConnections; + "metadata"?: DataPipelineMetadata; +} diff --git a/test/codegen/e2e/typescript/2020-12/tuples_and_arrays/options.json b/test/codegen/e2e/typescript/2020-12/tuples_and_arrays/options.json new file mode 100644 index 000000000..695e2f1bd --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/tuples_and_arrays/options.json @@ -0,0 +1,3 @@ +{ + "defaultPrefix": "DataPipeline" +} diff --git a/test/codegen/e2e/typescript/2020-12/tuples_and_arrays/schema.json b/test/codegen/e2e/typescript/2020-12/tuples_and_arrays/schema.json new file mode 100644 index 000000000..0e3f99fa6 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/tuples_and_arrays/schema.json @@ -0,0 +1,145 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/data-pipeline", + "type": "object", + "required": [ "pipeline", "stages", "connections" ], + "properties": { + "pipeline": { + "type": "object", + "required": [ "id", "version", "coordinates" ], + "properties": { + "id": { "type": "string" }, + "version": { + "prefixItems": [ + { "type": "integer", "minimum": 0 }, + { "type": "integer", "minimum": 0 }, + { "type": "integer", "minimum": 0 } + ], + "items": false + }, + "coordinates": { + "prefixItems": [ + { "type": "number" }, + { "type": "number" }, + { "type": "number" } + ], + "items": { "type": "number" } + }, + "tags": { + "type": "array", + "items": { "type": "string" } + } + }, + "additionalProperties": false + }, + "stages": { + "type": "array", + "items": { + "type": "object", + "required": [ "name", "inputTypes", "outputType" ], + "properties": { + "name": { "type": "string" }, + "inputTypes": { + "prefixItems": [ + { "enum": [ "string", "number", "boolean", "object", "array" ] }, + { + "anyOf": [ + { "enum": [ "required", "optional" ] }, + { "type": "null" } + ] + } + ], + "items": { + "type": "object", + "properties": { + "typeName": { "type": "string" }, + "nullable": { "enum": [ true, false ] } + }, + "additionalProperties": false + } + }, + "outputType": { + "prefixItems": [ + { "type": "string" }, + { "enum": [ "sync", "async" ] } + ], + "items": false + }, + "config": { + "anyOf": [ + { + "type": "object", + "properties": { + "timeout": { "type": "integer" }, + "retries": { "type": "integer" } + }, + "additionalProperties": false + }, + { "type": "null" } + ] + }, + "metrics": { + "prefixItems": [ + { "type": "number" }, + { "type": "number" }, + { "type": "number" } + ], + "items": false + } + }, + "additionalProperties": false + } + }, + "connections": { + "type": "array", + "items": { + "prefixItems": [ + { "type": "string" }, + { "type": "string" }, + { + "type": "object", + "properties": { + "weight": { "type": "number" }, + "bidirectional": { "enum": [ true, false ] } + }, + "required": [ "weight" ], + "additionalProperties": false + } + ], + "items": false + } + }, + "metadata": { + "type": "object", + "properties": { + "createdAt": { "type": "string" }, + "modifiedAt": { + "anyOf": [ + { "type": "string" }, + { "type": "null" } + ] + }, + "authors": { + "type": "array", + "items": { + "prefixItems": [ + { "type": "string" }, + { "type": "string" } + ], + "items": { "type": "string" } + } + }, + "flags": { + "prefixItems": [ + { "enum": [ true, false ] }, + { "enum": [ true, false ] }, + { "enum": [ true, false ] } + ], + "items": false + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false +} diff --git a/test/codegen/e2e/typescript/2020-12/tuples_and_arrays/test.ts b/test/codegen/e2e/typescript/2020-12/tuples_and_arrays/test.ts new file mode 100644 index 000000000..785a98f37 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/tuples_and_arrays/test.ts @@ -0,0 +1,158 @@ +import { + DataPipeline, + DataPipelinePipeline, + DataPipelineStagesItems +} 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: DataPipelineStagesItems = { + name: "transform", + inputTypes: [ "string", "required" ], + outputType: [ "output", "sync" ] +}; + +// Valid: stage inputTypes with additional items +const validStageExtended: DataPipelineStagesItems = { + 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: DataPipelineStagesItems = { + inputTypes: [ "string", "required" ], + outputType: [ "out", "sync" ] +}; + +// Invalid: extra property on stage (additionalProperties: false) +const invalidStageExtra: DataPipelineStagesItems = { + 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/codegen/e2e/typescript/2020-12/vocabulary_ignored/expected.d.ts b/test/codegen/e2e/typescript/2020-12/vocabulary_ignored/expected.d.ts new file mode 100644 index 000000000..391882d9c --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/vocabulary_ignored/expected.d.ts @@ -0,0 +1,13 @@ +export type VocabTestValue = number; + +export type VocabTestOptional = boolean; + +export type VocabTestName = string; + +export type VocabTestAdditionalProperties = never; + +export interface VocabTest { + "name": VocabTestName; + "value": VocabTestValue; + "optional"?: VocabTestOptional; +} diff --git a/test/codegen/e2e/typescript/2020-12/vocabulary_ignored/options.json b/test/codegen/e2e/typescript/2020-12/vocabulary_ignored/options.json new file mode 100644 index 000000000..6d2ad06a6 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/vocabulary_ignored/options.json @@ -0,0 +1,3 @@ +{ + "defaultPrefix": "VocabTest" +} diff --git a/test/codegen/e2e/typescript/2020-12/vocabulary_ignored/schema.json b/test/codegen/e2e/typescript/2020-12/vocabulary_ignored/schema.json new file mode 100644 index 000000000..831f93fe8 --- /dev/null +++ b/test/codegen/e2e/typescript/2020-12/vocabulary_ignored/schema.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/vocabulary-test", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/core": true, + "https://json-schema.org/draft/2020-12/vocab/applicator": true, + "https://json-schema.org/draft/2020-12/vocab/validation": true + }, + "type": "object", + "required": [ "name", "value" ], + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "number" + }, + "optional": { + "type": "boolean" + } + }, + "additionalProperties": false +} diff --git a/test/codegen/e2e/typescript/2020-12/vocabulary_ignored/test.ts b/test/codegen/e2e/typescript/2020-12/vocabulary_ignored/test.ts new file mode 100644 index 000000000..9758221ef --- /dev/null +++ b/test/codegen/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/codegen/e2e/typescript/CMakeLists.txt b/test/codegen/e2e/typescript/CMakeLists.txt new file mode 100644 index 000000000..54c3c446e --- /dev/null +++ b/test/codegen/e2e/typescript/CMakeLists.txt @@ -0,0 +1,34 @@ +sourcemeta_googletest(NAMESPACE sourcemeta PROJECT blaze NAME codegen_e2e_typescript + FOLDER "Blaze/Codegen/E2E/TypeScript" + SOURCES e2e.cc) + +target_link_libraries(sourcemeta_blaze_codegen_e2e_typescript_unit + PRIVATE sourcemeta::blaze::codegen) +target_compile_definitions(sourcemeta_blaze_codegen_e2e_typescript_unit + PRIVATE E2E_TYPESCRIPT_PATH="${CMAKE_CURRENT_SOURCE_DIR}") + +if(WIN32) + set(TSC_BIN "${PROJECT_SOURCE_DIR}/node_modules/.bin/tsc.cmd") +else() + set(TSC_BIN "${PROJECT_SOURCE_DIR}/node_modules/.bin/tsc") +endif() + +set(E2E_SCHEMAS "") +file(GLOB TYPESCRIPT_DIALECT_DIRECTORIES "${CMAKE_CURRENT_SOURCE_DIR}/*") +foreach(DIALECT_DIRECTORY ${TYPESCRIPT_DIALECT_DIRECTORIES}) + if(IS_DIRECTORY ${DIALECT_DIRECTORY}) + get_filename_component(DIALECT_NAME ${DIALECT_DIRECTORY} NAME) + file(GLOB TYPESCRIPT_CASE_DIRECTORIES "${DIALECT_DIRECTORY}/*") + foreach(CASE_DIRECTORY ${TYPESCRIPT_CASE_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}" + COMMAND "${TSC_BIN}" --strict --noEmit + "${CASE_DIRECTORY}/expected.d.ts" + "${CASE_DIRECTORY}/test.ts") + endif() + endforeach() + endif() +endforeach() +set(E2E_SCHEMAS "${E2E_SCHEMAS}" PARENT_SCOPE) diff --git a/test/codegen/e2e/typescript/e2e.cc b/test/codegen/e2e/typescript/e2e.cc new file mode 100644 index 000000000..cada8f1dd --- /dev/null +++ b/test/codegen/e2e/typescript/e2e.cc @@ -0,0 +1,81 @@ +#include + +#include + +#include +#include + +#include // std::filesystem +#include // std::ifstream +#include // std::ostringstream +#include // std::string + +class TypeScriptE2ETest : public testing::Test { +public: + explicit TypeScriptE2ETest(const std::filesystem::path &test_directory) + : directory{test_directory} {} + + auto TestBody() -> void override { + const auto schema_path{this->directory / "schema.json"}; + const auto options_path{this->directory / "options.json"}; + const auto expected_path{this->directory / "expected.d.ts"}; + + const auto schema{sourcemeta::core::read_json(schema_path)}; + const auto options{sourcemeta::core::read_json(options_path)}; + + std::ifstream expected_stream{expected_path}; + expected_stream.exceptions(std::ios_base::badbit); + const std::string expected{std::istreambuf_iterator(expected_stream), + std::istreambuf_iterator()}; + + const auto result{ + sourcemeta::blaze::compile(schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, + sourcemeta::blaze::default_compiler)}; + + std::ostringstream output; + if (options.defines("defaultPrefix")) { + sourcemeta::blaze::generate( + output, result, options.at("defaultPrefix").to_string()); + } else { + sourcemeta::blaze::generate(output, + result); + } + + EXPECT_EQ(output.str(), expected) + << "Generated TypeScript does not match expected output"; + } + +private: + const std::filesystem::path directory; +}; + +auto main(int argc, char **argv) -> int { + testing::InitGoogleTest(&argc, argv); + + const std::filesystem::path e2e_path{E2E_TYPESCRIPT_PATH}; + for (const auto &dialect_entry : + std::filesystem::directory_iterator(e2e_path)) { + if (!dialect_entry.is_directory()) { + continue; + } + + const auto dialect_name{dialect_entry.path().filename().string()}; + for (const auto &case_entry : + std::filesystem::directory_iterator(dialect_entry.path())) { + if (!case_entry.is_directory()) { + continue; + } + + const auto case_name{case_entry.path().filename().string()}; + const auto test_name{dialect_name + "/" + case_name}; + testing::RegisterTest("TypeScriptE2E", test_name.c_str(), nullptr, + nullptr, __FILE__, __LINE__, + [=]() -> TypeScriptE2ETest * { + return new TypeScriptE2ETest(case_entry.path()); + }); + } + } + + return RUN_ALL_TESTS(); +} diff --git a/test/packaging/find_package/CMakeLists.txt b/test/packaging/find_package/CMakeLists.txt index d4fc9dc23..005c4edb8 100644 --- a/test/packaging/find_package/CMakeLists.txt +++ b/test/packaging/find_package/CMakeLists.txt @@ -13,3 +13,4 @@ target_link_libraries(blaze_hello PRIVATE sourcemeta::blaze::test) target_link_libraries(blaze_hello PRIVATE sourcemeta::blaze::output) target_link_libraries(blaze_hello PRIVATE sourcemeta::blaze::configuration) target_link_libraries(blaze_hello PRIVATE sourcemeta::blaze::alterschema) +target_link_libraries(blaze_hello PRIVATE sourcemeta::blaze::codegen) diff --git a/test/packaging/find_package/hello.cc b/test/packaging/find_package/hello.cc index e113e39ab..8ef4d6216 100644 --- a/test/packaging/find_package/hello.cc +++ b/test/packaging/find_package/hello.cc @@ -2,6 +2,7 @@ #include #include +#include #include #include #include From 3871d0be1b38b0ce9e10fd9ed2da57f84634f0a1 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Tue, 21 Apr 2026 10:43:54 -0400 Subject: [PATCH 2/5] Fix tests Signed-off-by: Juan Cruz Viotti --- contrib/typescript.cc | 7 +- src/codegen/codegen.cc | 7 +- src/codegen/codegen_default_compiler.h | 218 +++-- src/codegen/codegen_typescript.cc | 38 +- .../include/sourcemeta/blaze/codegen.h | 76 +- .../include/sourcemeta/blaze/codegen_error.h | 121 +-- .../sourcemeta/blaze/codegen_typescript.h | 44 +- test/codegen/codegen_2020_12_test.cc | 878 ++++++++++-------- test/codegen/codegen_symbol_test.cc | 8 +- test/codegen/codegen_test.cc | 14 +- test/codegen/codegen_test_utils.h | 87 +- test/codegen/e2e/typescript/CMakeLists.txt | 2 +- test/codegen/e2e/typescript/e2e.cc | 4 +- 13 files changed, 814 insertions(+), 690 deletions(-) diff --git a/contrib/typescript.cc b/contrib/typescript.cc index b6c55ee6a..0590b4e17 100644 --- a/contrib/typescript.cc +++ b/contrib/typescript.cc @@ -39,7 +39,7 @@ auto main(int argc, char *argv[]) -> int { sourcemeta::blaze::generate(std::cout, result, prefix); - } catch (const sourcemeta::blaze::UnsupportedKeywordError &error) { + } catch (const sourcemeta::blaze::CodegenUnsupportedKeywordError &error) { std::ostringstream pointer; sourcemeta::core::stringify(error.pointer(), pointer); std::cerr << "error: " << error.what() << "\n"; @@ -49,7 +49,8 @@ auto main(int argc, char *argv[]) -> int { sourcemeta::core::prettify(error.json(), std::cerr); std::cerr << "\n"; return EXIT_FAILURE; - } catch (const sourcemeta::blaze::UnsupportedKeywordValueError &error) { + } catch ( + const sourcemeta::blaze::CodegenUnsupportedKeywordValueError &error) { std::ostringstream pointer; sourcemeta::core::stringify(error.pointer(), pointer); std::cerr << "error: " << error.what() << "\n"; @@ -59,7 +60,7 @@ auto main(int argc, char *argv[]) -> int { sourcemeta::core::prettify(error.json(), std::cerr); std::cerr << "\n"; return EXIT_FAILURE; - } catch (const sourcemeta::blaze::UnexpectedSchemaError &error) { + } catch (const sourcemeta::blaze::CodegenUnexpectedSchemaError &error) { std::ostringstream pointer; sourcemeta::core::stringify(error.pointer(), pointer); std::cerr << "error: " << error.what() << "\n"; diff --git a/src/codegen/codegen.cc b/src/codegen/codegen.cc index c953c788e..03d309415 100644 --- a/src/codegen/codegen.cc +++ b/src/codegen/codegen.cc @@ -55,7 +55,7 @@ auto compile(const sourcemeta::core::JSON &input, const sourcemeta::core::SchemaResolver &resolver, const CodegenCompiler &compiler, const std::string_view default_dialect, - const std::string_view default_id) -> IRResult { + const std::string_view default_id) -> CodegenIRResult { // -------------------------------------------------------------------------- // (1) Bundle the schema to resolve external references // -------------------------------------------------------------------------- @@ -93,7 +93,7 @@ auto compile(const sourcemeta::core::JSON &input, sourcemeta::core::WeakPointer::Hasher, sourcemeta::core::WeakPointer::Comparator> visited; - IRResult result; + CodegenIRResult result; for (const auto &[key, location] : frame.locations()) { if (location.type != sourcemeta::core::SchemaFrame::LocationType::Resource && @@ -124,7 +124,8 @@ auto compile(const sourcemeta::core::JSON &input, // -------------------------------------------------------------------------- std::ranges::sort( - result, [](const IREntity &left, const IREntity &right) -> bool { + result, + [](const CodegenIREntity &left, const CodegenIREntity &right) -> bool { return std::visit([](const auto &entry) { return entry.pointer; }, right) < std::visit([](const auto &entry) { return entry.pointer; }, diff --git a/src/codegen/codegen_default_compiler.h b/src/codegen/codegen_default_compiler.h index c18aedc9d..c92041bb8 100644 --- a/src/codegen/codegen_default_compiler.h +++ b/src/codegen/codegen_default_compiler.h @@ -19,7 +19,7 @@ static const std::unordered_set allowed{__VA_ARGS__}; \ for (const auto &entry : (subschema).as_object()) { \ if (!allowed.contains(entry.first)) { \ - throw sourcemeta::blaze::UnsupportedKeywordError( \ + throw sourcemeta::blaze::CodegenUnsupportedKeywordError( \ (schema), (pointer), entry.first, \ "Unsupported keyword in subschema"); \ } \ @@ -33,8 +33,8 @@ auto handle_impossible(const sourcemeta::core::JSON &, const sourcemeta::core::SchemaFrame::Location &location, const sourcemeta::core::Vocabularies &, const sourcemeta::core::SchemaResolver &, - const sourcemeta::core::JSON &) -> IRImpossible { - return IRImpossible{ + const sourcemeta::core::JSON &) -> CodegenIRImpossible { + return CodegenIRImpossible{ {.pointer = sourcemeta::core::to_pointer(location.pointer), .symbol = symbol(frame, location)}}; } @@ -44,9 +44,10 @@ auto handle_any(const sourcemeta::core::JSON &, const sourcemeta::core::SchemaFrame::Location &location, const sourcemeta::core::Vocabularies &, const sourcemeta::core::SchemaResolver &, - const sourcemeta::core::JSON &) -> IRAny { - return IRAny{{.pointer = sourcemeta::core::to_pointer(location.pointer), - .symbol = symbol(frame, location)}}; + const sourcemeta::core::JSON &) -> CodegenIRAny { + return CodegenIRAny{ + {.pointer = sourcemeta::core::to_pointer(location.pointer), + .symbol = symbol(frame, location)}}; } auto handle_string(const sourcemeta::core::JSON &schema, @@ -54,7 +55,7 @@ auto handle_string(const sourcemeta::core::JSON &schema, const sourcemeta::core::SchemaFrame::Location &location, const sourcemeta::core::Vocabularies &, const sourcemeta::core::SchemaResolver &, - const sourcemeta::core::JSON &subschema) -> IRScalar { + const sourcemeta::core::JSON &subschema) -> CodegenIRScalar { ONLY_WHITELIST_KEYWORDS(schema, subschema, location.pointer, {"$schema", "$id", @@ -77,9 +78,10 @@ auto handle_string(const sourcemeta::core::JSON &schema, "contentEncoding", "contentMediaType", "contentSchema"}); - return IRScalar{{.pointer = sourcemeta::core::to_pointer(location.pointer), - .symbol = symbol(frame, location)}, - IRScalarType::String}; + return CodegenIRScalar{ + {.pointer = sourcemeta::core::to_pointer(location.pointer), + .symbol = symbol(frame, location)}, + CodegenIRScalarType::String}; } auto handle_object(const sourcemeta::core::JSON &schema, @@ -87,7 +89,7 @@ auto handle_object(const sourcemeta::core::JSON &schema, const sourcemeta::core::SchemaFrame::Location &location, const sourcemeta::core::Vocabularies &, const sourcemeta::core::SchemaResolver &, - const sourcemeta::core::JSON &subschema) -> IRObject { + const sourcemeta::core::JSON &subschema) -> CodegenIRObject { ONLY_WHITELIST_KEYWORDS( schema, subschema, location.pointer, {"$defs", "$schema", "$id", "$anchor", "$dynamicAnchor", "$vocabulary", @@ -100,7 +102,8 @@ auto handle_object(const sourcemeta::core::JSON &schema, "propertyNames", "patternProperties", "title", "description", "default", "deprecated", "readOnly", "writeOnly", "examples"}); - std::vector> members; + std::vector> + members; // Guaranteed by canonicalisation assert(subschema.defines("properties")); @@ -126,7 +129,7 @@ auto handle_object(const sourcemeta::core::JSON &schema, frame.traverse(sourcemeta::core::to_weak_pointer(property_pointer))}; assert(property_location.has_value()); - IRObjectValue member_value{ + CodegenIRObjectValue member_value{ {.pointer = std::move(property_pointer), .symbol = symbol(frame, property_location.value().get())}, required_set.contains(entry.first), @@ -135,7 +138,7 @@ auto handle_object(const sourcemeta::core::JSON &schema, members.emplace_back(entry.first, std::move(member_value)); } - std::variant additional{true}; + std::variant additional{true}; if (subschema.defines("additionalProperties")) { const auto &additional_schema{subschema.at("additionalProperties")}; if (additional_schema.is_boolean()) { @@ -148,13 +151,13 @@ auto handle_object(const sourcemeta::core::JSON &schema, sourcemeta::core::to_weak_pointer(additional_pointer))}; assert(additional_location.has_value()); - additional = - IRType{.pointer = std::move(additional_pointer), - .symbol = symbol(frame, additional_location.value().get())}; + additional = CodegenIRType{ + .pointer = std::move(additional_pointer), + .symbol = symbol(frame, additional_location.value().get())}; } } - std::vector pattern; + std::vector pattern; if (subschema.defines("patternProperties")) { const auto &pattern_props{subschema.at("patternProperties")}; for (const auto &entry : pattern_props.as_object()) { @@ -174,18 +177,19 @@ auto handle_object(const sourcemeta::core::JSON &schema, prefix = std::get(regex.value()); } - pattern.push_back(IRObjectPatternProperty{ + pattern.push_back(CodegenIRObjectPatternProperty{ {.pointer = std::move(pattern_pointer), .symbol = symbol(frame, pattern_location.value().get())}, std::move(prefix)}); } } - return IRObject{{.pointer = sourcemeta::core::to_pointer(location.pointer), - .symbol = symbol(frame, location)}, - std::move(members), - std::move(additional), - std::move(pattern)}; + return CodegenIRObject{ + {.pointer = sourcemeta::core::to_pointer(location.pointer), + .symbol = symbol(frame, location)}, + std::move(members), + std::move(additional), + std::move(pattern)}; } auto handle_integer(const sourcemeta::core::JSON &schema, @@ -193,16 +197,18 @@ auto handle_integer(const sourcemeta::core::JSON &schema, const sourcemeta::core::SchemaFrame::Location &location, const sourcemeta::core::Vocabularies &, const sourcemeta::core::SchemaResolver &, - const sourcemeta::core::JSON &subschema) -> IRScalar { + const sourcemeta::core::JSON &subschema) + -> CodegenIRScalar { ONLY_WHITELIST_KEYWORDS(schema, subschema, location.pointer, {"$schema", "$id", "$anchor", "$dynamicAnchor", "$defs", "$vocabulary", "type", "minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum", "multipleOf", "title", "description", "default", "deprecated", "readOnly", "writeOnly", "examples"}); - return IRScalar{{.pointer = sourcemeta::core::to_pointer(location.pointer), - .symbol = symbol(frame, location)}, - IRScalarType::Integer}; + return CodegenIRScalar{ + {.pointer = sourcemeta::core::to_pointer(location.pointer), + .symbol = symbol(frame, location)}, + CodegenIRScalarType::Integer}; } auto handle_number(const sourcemeta::core::JSON &schema, @@ -210,16 +216,17 @@ auto handle_number(const sourcemeta::core::JSON &schema, const sourcemeta::core::SchemaFrame::Location &location, const sourcemeta::core::Vocabularies &, const sourcemeta::core::SchemaResolver &, - const sourcemeta::core::JSON &subschema) -> IRScalar { + const sourcemeta::core::JSON &subschema) -> CodegenIRScalar { ONLY_WHITELIST_KEYWORDS(schema, subschema, location.pointer, {"$schema", "$id", "$anchor", "$dynamicAnchor", "$defs", "$vocabulary", "type", "minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum", "multipleOf", "title", "description", "default", "deprecated", "readOnly", "writeOnly", "examples"}); - return IRScalar{{.pointer = sourcemeta::core::to_pointer(location.pointer), - .symbol = symbol(frame, location)}, - IRScalarType::Number}; + return CodegenIRScalar{ + {.pointer = sourcemeta::core::to_pointer(location.pointer), + .symbol = symbol(frame, location)}, + CodegenIRScalarType::Number}; } auto handle_array(const sourcemeta::core::JSON &schema, @@ -227,7 +234,7 @@ auto handle_array(const sourcemeta::core::JSON &schema, const sourcemeta::core::SchemaFrame::Location &location, const sourcemeta::core::Vocabularies &vocabularies, const sourcemeta::core::SchemaResolver &, - const sourcemeta::core::JSON &subschema) -> IREntity { + const sourcemeta::core::JSON &subschema) -> CodegenIREntity { ONLY_WHITELIST_KEYWORDS(schema, subschema, location.pointer, {"$schema", "$id", "$anchor", "$dynamicAnchor", "$defs", "$vocabulary", @@ -246,7 +253,7 @@ auto handle_array(const sourcemeta::core::JSON &schema, const auto &prefix_items{subschema.at("prefixItems")}; assert(prefix_items.is_array()); - std::vector tuple_items; + std::vector tuple_items; for (std::size_t index = 0; index < prefix_items.size(); ++index) { auto item_pointer{sourcemeta::core::to_pointer(location.pointer)}; item_pointer.push_back("prefixItems"); @@ -261,7 +268,7 @@ auto handle_array(const sourcemeta::core::JSON &schema, .symbol = symbol(frame, item_location.value().get())}); } - std::optional additional{std::nullopt}; + std::optional additional{std::nullopt}; if (subschema.defines("items")) { auto additional_pointer{sourcemeta::core::to_pointer(location.pointer)}; additional_pointer.push_back("items"); @@ -270,15 +277,16 @@ auto handle_array(const sourcemeta::core::JSON &schema, sourcemeta::core::to_weak_pointer(additional_pointer))}; assert(additional_location.has_value()); - additional = - IRType{.pointer = std::move(additional_pointer), - .symbol = symbol(frame, additional_location.value().get())}; + additional = CodegenIRType{ + .pointer = std::move(additional_pointer), + .symbol = symbol(frame, additional_location.value().get())}; } - return IRTuple{{.pointer = sourcemeta::core::to_pointer(location.pointer), - .symbol = symbol(frame, location)}, - std::move(tuple_items), - std::move(additional)}; + return CodegenIRTuple{ + {.pointer = sourcemeta::core::to_pointer(location.pointer), + .symbol = symbol(frame, location)}, + std::move(tuple_items), + std::move(additional)}; } if (vocabularies.contains_any( @@ -290,7 +298,7 @@ auto handle_array(const sourcemeta::core::JSON &schema, subschema.defines("items") && subschema.at("items").is_array()) { const auto &items_array{subschema.at("items")}; - std::vector tuple_items; + std::vector tuple_items; for (std::size_t index = 0; index < items_array.size(); ++index) { auto item_pointer{sourcemeta::core::to_pointer(location.pointer)}; item_pointer.push_back("items"); @@ -305,7 +313,7 @@ auto handle_array(const sourcemeta::core::JSON &schema, .symbol = symbol(frame, item_location.value().get())}); } - std::optional additional{std::nullopt}; + std::optional additional{std::nullopt}; if (subschema.defines("additionalItems")) { auto additional_pointer{sourcemeta::core::to_pointer(location.pointer)}; additional_pointer.push_back("additionalItems"); @@ -314,18 +322,19 @@ auto handle_array(const sourcemeta::core::JSON &schema, sourcemeta::core::to_weak_pointer(additional_pointer))}; assert(additional_location.has_value()); - additional = - IRType{.pointer = std::move(additional_pointer), - .symbol = symbol(frame, additional_location.value().get())}; + additional = CodegenIRType{ + .pointer = std::move(additional_pointer), + .symbol = symbol(frame, additional_location.value().get())}; } - return IRTuple{{.pointer = sourcemeta::core::to_pointer(location.pointer), - .symbol = symbol(frame, location)}, - std::move(tuple_items), - std::move(additional)}; + return CodegenIRTuple{ + {.pointer = sourcemeta::core::to_pointer(location.pointer), + .symbol = symbol(frame, location)}, + std::move(tuple_items), + std::move(additional)}; } - std::optional items_type{std::nullopt}; + std::optional items_type{std::nullopt}; if (subschema.defines("items")) { auto items_pointer{sourcemeta::core::to_pointer(location.pointer)}; items_pointer.push_back("items"); @@ -334,13 +343,15 @@ auto handle_array(const sourcemeta::core::JSON &schema, frame.traverse(sourcemeta::core::to_weak_pointer(items_pointer))}; assert(items_location.has_value()); - items_type = IRType{.pointer = std::move(items_pointer), - .symbol = symbol(frame, items_location.value().get())}; + items_type = + CodegenIRType{.pointer = std::move(items_pointer), + .symbol = symbol(frame, items_location.value().get())}; } - return IRArray{{.pointer = sourcemeta::core::to_pointer(location.pointer), - .symbol = symbol(frame, location)}, - std::move(items_type)}; + return CodegenIRArray{ + {.pointer = sourcemeta::core::to_pointer(location.pointer), + .symbol = symbol(frame, location)}, + std::move(items_type)}; } auto handle_enum(const sourcemeta::core::JSON &schema, @@ -348,7 +359,7 @@ auto handle_enum(const sourcemeta::core::JSON &schema, const sourcemeta::core::SchemaFrame::Location &location, const sourcemeta::core::Vocabularies &, const sourcemeta::core::SchemaResolver &, - const sourcemeta::core::JSON &subschema) -> IREntity { + const sourcemeta::core::JSON &subschema) -> CodegenIREntity { ONLY_WHITELIST_KEYWORDS(schema, subschema, location.pointer, {"$schema", "$id", "$anchor", "$dynamicAnchor", "$defs", "$vocabulary", "enum", "title", @@ -358,24 +369,25 @@ auto handle_enum(const sourcemeta::core::JSON &schema, // Boolean and null special cases if (enum_json.size() == 1 && enum_json.at(0).is_null()) { - return IRScalar{{.pointer = sourcemeta::core::to_pointer(location.pointer), - .symbol = symbol(frame, location)}, - IRScalarType::Null}; + return CodegenIRScalar{ + {.pointer = sourcemeta::core::to_pointer(location.pointer), + .symbol = symbol(frame, location)}, + CodegenIRScalarType::Null}; } else if (enum_json.size() == 2) { const auto &first{enum_json.at(0)}; const auto &second{enum_json.at(1)}; if ((first.is_boolean() && second.is_boolean()) && (first.to_boolean() != second.to_boolean())) { - return IRScalar{ + return CodegenIRScalar{ {.pointer = sourcemeta::core::to_pointer(location.pointer), .symbol = symbol(frame, location)}, - IRScalarType::Boolean}; + CodegenIRScalarType::Boolean}; } } std::vector values{enum_json.as_array().cbegin(), enum_json.as_array().cend()}; - return IREnumeration{ + return CodegenIREnumeration{ {.pointer = sourcemeta::core::to_pointer(location.pointer), .symbol = symbol(frame, location)}, std::move(values)}; @@ -386,7 +398,7 @@ auto handle_anyof(const sourcemeta::core::JSON &schema, const sourcemeta::core::SchemaFrame::Location &location, const sourcemeta::core::Vocabularies &, const sourcemeta::core::SchemaResolver &, - const sourcemeta::core::JSON &subschema) -> IREntity { + const sourcemeta::core::JSON &subschema) -> CodegenIREntity { ONLY_WHITELIST_KEYWORDS( schema, subschema, location.pointer, {"$schema", "$id", "$anchor", "$dynamicAnchor", "$defs", "$vocabulary", @@ -397,7 +409,7 @@ auto handle_anyof(const sourcemeta::core::JSON &schema, assert(any_of.is_array()); assert(any_of.size() >= 2); - std::vector branches; + std::vector branches; for (std::size_t index = 0; index < any_of.size(); ++index) { auto branch_pointer{sourcemeta::core::to_pointer(location.pointer)}; branch_pointer.push_back("anyOf"); @@ -412,9 +424,10 @@ auto handle_anyof(const sourcemeta::core::JSON &schema, .symbol = symbol(frame, branch_location.value().get())}); } - return IRUnion{{.pointer = sourcemeta::core::to_pointer(location.pointer), - .symbol = symbol(frame, location)}, - std::move(branches)}; + return CodegenIRUnion{ + {.pointer = sourcemeta::core::to_pointer(location.pointer), + .symbol = symbol(frame, location)}, + std::move(branches)}; } auto handle_oneof(const sourcemeta::core::JSON &schema, @@ -422,7 +435,7 @@ auto handle_oneof(const sourcemeta::core::JSON &schema, const sourcemeta::core::SchemaFrame::Location &location, const sourcemeta::core::Vocabularies &, const sourcemeta::core::SchemaResolver &, - const sourcemeta::core::JSON &subschema) -> IREntity { + const sourcemeta::core::JSON &subschema) -> CodegenIREntity { ONLY_WHITELIST_KEYWORDS( schema, subschema, location.pointer, {"$schema", "$id", "$anchor", "$dynamicAnchor", "$defs", "$vocabulary", @@ -433,7 +446,7 @@ auto handle_oneof(const sourcemeta::core::JSON &schema, assert(one_of.is_array()); assert(one_of.size() >= 2); - std::vector branches; + std::vector branches; for (std::size_t index = 0; index < one_of.size(); ++index) { auto branch_pointer{sourcemeta::core::to_pointer(location.pointer)}; branch_pointer.push_back("oneOf"); @@ -448,9 +461,10 @@ auto handle_oneof(const sourcemeta::core::JSON &schema, .symbol = symbol(frame, branch_location.value().get())}); } - return IRUnion{{.pointer = sourcemeta::core::to_pointer(location.pointer), - .symbol = symbol(frame, location)}, - std::move(branches)}; + return CodegenIRUnion{ + {.pointer = sourcemeta::core::to_pointer(location.pointer), + .symbol = symbol(frame, location)}, + std::move(branches)}; } auto handle_ref(const sourcemeta::core::JSON &schema, @@ -458,7 +472,7 @@ auto handle_ref(const sourcemeta::core::JSON &schema, const sourcemeta::core::SchemaFrame::Location &location, const sourcemeta::core::Vocabularies &, const sourcemeta::core::SchemaResolver &, - const sourcemeta::core::JSON &subschema) -> IREntity { + const sourcemeta::core::JSON &subschema) -> CodegenIREntity { ONLY_WHITELIST_KEYWORDS(schema, subschema, location.pointer, {"$schema", "$id", "$anchor", "$dynamicAnchor", "$defs", "$vocabulary", "$ref", "title", @@ -477,13 +491,13 @@ auto handle_ref(const sourcemeta::core::JSON &schema, const auto &destination{reference->second.destination}; const auto target{frame.traverse(destination)}; if (!target.has_value()) { - throw UnexpectedSchemaError(schema, location.pointer, - "Could not resolve reference destination"); + throw CodegenUnexpectedSchemaError( + schema, location.pointer, "Could not resolve reference destination"); } const auto &target_location{target.value().get()}; - return IRReference{ + return CodegenIRReference{ {.pointer = sourcemeta::core::to_pointer(location.pointer), .symbol = symbol(frame, location)}, {.pointer = sourcemeta::core::to_pointer(target_location.pointer), @@ -495,7 +509,8 @@ auto handle_dynamic_ref(const sourcemeta::core::JSON &schema, const sourcemeta::core::SchemaFrame::Location &location, const sourcemeta::core::Vocabularies &, const sourcemeta::core::SchemaResolver &, - const sourcemeta::core::JSON &subschema) -> IREntity { + const sourcemeta::core::JSON &subschema) + -> CodegenIREntity { ONLY_WHITELIST_KEYWORDS(schema, subschema, location.pointer, {"$schema", "$id", "$anchor", "$dynamicAnchor", "$defs", "$vocabulary", "$dynamicRef", "title", @@ -516,13 +531,13 @@ auto handle_dynamic_ref(const sourcemeta::core::JSON &schema, const auto &destination{static_reference->second.destination}; const auto target{frame.traverse(destination)}; if (!target.has_value()) { - throw UnexpectedSchemaError(schema, location.pointer, - "Could not resolve reference destination"); + throw CodegenUnexpectedSchemaError( + schema, location.pointer, "Could not resolve reference destination"); } const auto &target_location{target.value().get()}; - return IRReference{ + return CodegenIRReference{ {.pointer = sourcemeta::core::to_pointer(location.pointer), .symbol = symbol(frame, location)}, {.pointer = sourcemeta::core::to_pointer(target_location.pointer), @@ -537,7 +552,7 @@ auto handle_dynamic_ref(const sourcemeta::core::JSON &schema, assert(dynamic_reference->second.fragment.has_value()); const auto &fragment{dynamic_reference->second.fragment.value()}; - std::vector branches; + std::vector branches; for (const auto &[key, entry] : frame.locations()) { if (key.first != sourcemeta::core::SchemaReferenceType::Dynamic || entry.type != sourcemeta::core::SchemaFrame::LocationType::Anchor) { @@ -555,9 +570,10 @@ auto handle_dynamic_ref(const sourcemeta::core::JSON &schema, } assert(!branches.empty()); - return IRUnion{{.pointer = sourcemeta::core::to_pointer(location.pointer), - .symbol = symbol(frame, location)}, - std::move(branches)}; + return CodegenIRUnion{ + {.pointer = sourcemeta::core::to_pointer(location.pointer), + .symbol = symbol(frame, location)}, + std::move(branches)}; } auto handle_allof(const sourcemeta::core::JSON &schema, @@ -565,7 +581,7 @@ auto handle_allof(const sourcemeta::core::JSON &schema, const sourcemeta::core::SchemaFrame::Location &location, const sourcemeta::core::Vocabularies &, const sourcemeta::core::SchemaResolver &, - const sourcemeta::core::JSON &subschema) -> IREntity { + const sourcemeta::core::JSON &subschema) -> CodegenIREntity { ONLY_WHITELIST_KEYWORDS( schema, subschema, location.pointer, {"$schema", "$id", "$anchor", "$dynamicAnchor", "$defs", "$vocabulary", @@ -584,14 +600,14 @@ auto handle_allof(const sourcemeta::core::JSON &schema, frame.traverse(sourcemeta::core::to_weak_pointer(target_pointer))}; assert(target_location.has_value()); - return IRReference{ + return CodegenIRReference{ {.pointer = sourcemeta::core::to_pointer(location.pointer), .symbol = symbol(frame, location)}, {.pointer = std::move(target_pointer), .symbol = symbol(frame, target_location.value().get())}}; } - std::vector branches; + std::vector branches; for (std::size_t index = 0; index < all_of.size(); ++index) { auto branch_pointer{sourcemeta::core::to_pointer(location.pointer)}; branch_pointer.push_back("allOf"); @@ -606,7 +622,7 @@ auto handle_allof(const sourcemeta::core::JSON &schema, .symbol = symbol(frame, branch_location.value().get())}); } - return IRIntersection{ + return CodegenIRIntersection{ {.pointer = sourcemeta::core::to_pointer(location.pointer), .symbol = symbol(frame, location)}, std::move(branches)}; @@ -618,7 +634,7 @@ auto handle_if_then_else( const sourcemeta::core::SchemaFrame::Location &location, const sourcemeta::core::Vocabularies &, const sourcemeta::core::SchemaResolver &, - const sourcemeta::core::JSON &subschema) -> IREntity { + const sourcemeta::core::JSON &subschema) -> CodegenIREntity { ONLY_WHITELIST_KEYWORDS(schema, subschema, location.pointer, {"$schema", "$id", "$anchor", "$dynamicAnchor", "$defs", "$vocabulary", "if", "then", "else", @@ -648,7 +664,7 @@ auto handle_if_then_else( frame.traverse(sourcemeta::core::to_weak_pointer(else_pointer))}; assert(else_location.has_value()); - return IRConditional{ + return CodegenIRConditional{ {.pointer = sourcemeta::core::to_pointer(location.pointer), .symbol = symbol(frame, location)}, {.pointer = std::move(if_pointer), @@ -663,7 +679,8 @@ auto default_compiler(const sourcemeta::core::JSON &schema, const sourcemeta::core::SchemaFrame &frame, const sourcemeta::core::SchemaFrame::Location &location, const sourcemeta::core::SchemaResolver &resolver, - const sourcemeta::core::JSON &subschema) -> IREntity { + const sourcemeta::core::JSON &subschema) + -> CodegenIREntity { const auto vocabularies{frame.vocabularies(location, resolver)}; assert(!vocabularies.empty()); @@ -704,8 +721,8 @@ auto default_compiler(const sourcemeta::core::JSON &schema, } else if (subschema.defines("type")) { const auto &type_value{subschema.at("type")}; if (!type_value.is_string()) { - throw UnsupportedKeywordValueError(schema, location.pointer, "type", - "Expected a string value"); + throw CodegenUnsupportedKeywordValueError( + schema, location.pointer, "type", "Expected a string value"); } const auto &type_string{type_value.to_string()}; @@ -727,8 +744,8 @@ auto default_compiler(const sourcemeta::core::JSON &schema, return handle_array(schema, frame, location, vocabularies, resolver, subschema); } else { - throw UnsupportedKeywordValueError(schema, location.pointer, "type", - "Unsupported type value"); + throw CodegenUnsupportedKeywordValueError( + schema, location.pointer, "type", "Unsupported type value"); } } else if (subschema.defines("enum")) { return handle_enum(schema, frame, location, vocabularies, resolver, @@ -754,10 +771,11 @@ auto default_compiler(const sourcemeta::core::JSON &schema, return handle_if_then_else(schema, frame, location, vocabularies, resolver, subschema); } else if (subschema.defines("not")) { - throw UnsupportedKeywordError(schema, location.pointer, "not", - "Unsupported keyword in subschema"); + throw CodegenUnsupportedKeywordError(schema, location.pointer, "not", + "Unsupported keyword in subschema"); } else { - throw UnexpectedSchemaError(schema, location.pointer, "Unsupported schema"); + throw CodegenUnexpectedSchemaError(schema, location.pointer, + "Unsupported schema"); } } diff --git a/src/codegen/codegen_typescript.cc b/src/codegen/codegen_typescript.cc index c10bd674c..c632372c3 100644 --- a/src/codegen/codegen_typescript.cc +++ b/src/codegen/codegen_typescript.cc @@ -54,23 +54,23 @@ namespace sourcemeta::blaze { TypeScript::TypeScript(std::ostream &stream, const std::string_view type_prefix) : output{stream}, prefix{type_prefix} {} -auto TypeScript::operator()(const IRScalar &entry) -> void { +auto TypeScript::operator()(const CodegenIRScalar &entry) -> void { this->output << "export type " << mangle(this->prefix, entry.pointer, entry.symbol, this->cache) << " = "; switch (entry.value) { - case IRScalarType::String: + case CodegenIRScalarType::String: this->output << "string"; break; - case IRScalarType::Number: - case IRScalarType::Integer: + case CodegenIRScalarType::Number: + case CodegenIRScalarType::Integer: this->output << "number"; break; - case IRScalarType::Boolean: + case CodegenIRScalarType::Boolean: this->output << "boolean"; break; - case IRScalarType::Null: + case CodegenIRScalarType::Null: this->output << "null"; break; } @@ -78,7 +78,7 @@ auto TypeScript::operator()(const IRScalar &entry) -> void { this->output << ";\n"; } -auto TypeScript::operator()(const IREnumeration &entry) -> void { +auto TypeScript::operator()(const CodegenIREnumeration &entry) -> void { this->output << "export type " << mangle(this->prefix, entry.pointer, entry.symbol, this->cache) << " = "; @@ -93,17 +93,17 @@ auto TypeScript::operator()(const IREnumeration &entry) -> void { this->output << ";\n"; } -auto TypeScript::operator()(const IRObject &entry) -> void { +auto TypeScript::operator()(const CodegenIRObject &entry) -> void { const auto type_name{ mangle(this->prefix, entry.pointer, entry.symbol, this->cache)}; const auto has_typed_additional{ - std::holds_alternative(entry.additional)}; + std::holds_alternative(entry.additional)}; const auto allows_any_additional{ std::holds_alternative(entry.additional) && std::get(entry.additional)}; if (has_typed_additional && entry.members.empty() && entry.pattern.empty()) { - const auto &additional_type{std::get(entry.additional)}; + const auto &additional_type{std::get(entry.additional)}; this->output << "export type " << type_name << " = Recordprefix, additional_type.pointer, additional_type.symbol, this->cache) @@ -198,7 +198,7 @@ auto TypeScript::operator()(const IRObject &entry) -> void { } if (has_typed_additional) { - const auto &additional_type{std::get(entry.additional)}; + const auto &additional_type{std::get(entry.additional)}; this->output << " " << mangle(this->prefix, additional_type.pointer, additional_type.symbol, this->cache) @@ -211,19 +211,19 @@ auto TypeScript::operator()(const IRObject &entry) -> void { this->output << "}\n"; } -auto TypeScript::operator()(const IRImpossible &entry) -> void { +auto TypeScript::operator()(const CodegenIRImpossible &entry) -> void { this->output << "export type " << mangle(this->prefix, entry.pointer, entry.symbol, this->cache) << " = never;\n"; } -auto TypeScript::operator()(const IRAny &entry) -> void { +auto TypeScript::operator()(const CodegenIRAny &entry) -> void { this->output << "export type " << mangle(this->prefix, entry.pointer, entry.symbol, this->cache) << " = unknown;\n"; } -auto TypeScript::operator()(const IRArray &entry) -> void { +auto TypeScript::operator()(const CodegenIRArray &entry) -> void { this->output << "export type " << mangle(this->prefix, entry.pointer, entry.symbol, this->cache) << " = "; @@ -239,7 +239,7 @@ auto TypeScript::operator()(const IRArray &entry) -> void { this->output << ";\n"; } -auto TypeScript::operator()(const IRReference &entry) -> void { +auto TypeScript::operator()(const CodegenIRReference &entry) -> void { this->output << "export type " << mangle(this->prefix, entry.pointer, entry.symbol, this->cache) << " = " @@ -248,7 +248,7 @@ auto TypeScript::operator()(const IRReference &entry) -> void { << ";\n"; } -auto TypeScript::operator()(const IRTuple &entry) -> void { +auto TypeScript::operator()(const CodegenIRTuple &entry) -> void { this->output << "export type " << mangle(this->prefix, entry.pointer, entry.symbol, this->cache) << " = ["; @@ -271,7 +271,7 @@ auto TypeScript::operator()(const IRTuple &entry) -> void { this->output << "];\n"; } -auto TypeScript::operator()(const IRUnion &entry) -> void { +auto TypeScript::operator()(const CodegenIRUnion &entry) -> void { this->output << "export type " << mangle(this->prefix, entry.pointer, entry.symbol, this->cache) << " =\n"; @@ -287,7 +287,7 @@ auto TypeScript::operator()(const IRUnion &entry) -> void { this->output << ";\n"; } -auto TypeScript::operator()(const IRIntersection &entry) -> void { +auto TypeScript::operator()(const CodegenIRIntersection &entry) -> void { this->output << "export type " << mangle(this->prefix, entry.pointer, entry.symbol, this->cache) << " =\n"; @@ -303,7 +303,7 @@ auto TypeScript::operator()(const IRIntersection &entry) -> void { this->output << ";\n"; } -auto TypeScript::operator()(const IRConditional &entry) -> void { +auto TypeScript::operator()(const CodegenIRConditional &entry) -> void { // As a notable limitation, TypeScript cannot express the negation of an // if/then/else condition, so the else branch is wider than what JSON // Schema allows diff --git a/src/codegen/include/sourcemeta/blaze/codegen.h b/src/codegen/include/sourcemeta/blaze/codegen.h index 99144e90c..ff15f6812 100644 --- a/src/codegen/include/sourcemeta/blaze/codegen.h +++ b/src/codegen/include/sourcemeta/blaze/codegen.h @@ -31,7 +31,7 @@ namespace sourcemeta::blaze { /// @ingroup codegen -enum class IRScalarType : std::uint8_t { +enum class CodegenIRScalarType : std::uint8_t { String, Number, Integer, @@ -40,89 +40,92 @@ enum class IRScalarType : std::uint8_t { }; /// @ingroup codegen -struct IRType { +struct CodegenIRType { sourcemeta::core::Pointer pointer; std::vector symbol; }; /// @ingroup codegen -struct IRScalar : IRType { - IRScalarType value; +struct CodegenIRScalar : CodegenIRType { + CodegenIRScalarType value; }; /// @ingroup codegen -struct IREnumeration : IRType { +struct CodegenIREnumeration : CodegenIRType { std::vector values; }; /// @ingroup codegen -struct IRUnion : IRType { - std::vector values; +struct CodegenIRUnion : CodegenIRType { + std::vector values; }; /// @ingroup codegen -struct IRIntersection : IRType { - std::vector values; +struct CodegenIRIntersection : CodegenIRType { + std::vector values; }; /// @ingroup codegen -struct IRObjectValue : IRType { +struct CodegenIRObjectValue : CodegenIRType { bool required; bool immutable; }; /// @ingroup codegen -struct IRObjectPatternProperty : IRType { +struct CodegenIRObjectPatternProperty : CodegenIRType { std::optional prefix; }; /// @ingroup codegen -struct IRObject : IRType { +struct CodegenIRObject : CodegenIRType { // To preserve the user's ordering - std::vector> members; - std::variant additional; - std::vector pattern; + std::vector> + members; + std::variant additional; + std::vector pattern; }; /// @ingroup codegen -struct IRArray : IRType { - std::optional items; +struct CodegenIRArray : CodegenIRType { + std::optional items; }; /// @ingroup codegen -struct IRTuple : IRType { - std::vector items; - std::optional additional; +struct CodegenIRTuple : CodegenIRType { + std::vector items; + std::optional additional; }; /// @ingroup codegen -struct IRImpossible : IRType {}; +struct CodegenIRImpossible : CodegenIRType {}; /// @ingroup codegen -struct IRAny : IRType {}; +struct CodegenIRAny : CodegenIRType {}; /// @ingroup codegen -struct IRConditional : IRType { - IRType condition; - IRType consequent; - IRType alternative; +struct CodegenIRConditional : CodegenIRType { + CodegenIRType condition; + CodegenIRType consequent; + CodegenIRType alternative; }; /// @ingroup codegen -struct IRReference : IRType { - IRType target; +struct CodegenIRReference : CodegenIRType { + CodegenIRType target; }; /// @ingroup codegen -using IREntity = std::variant; +using CodegenIREntity = + std::variant; /// @ingroup codegen -using IRResult = std::vector; +using CodegenIRResult = std::vector; /// @ingroup codegen -using CodegenCompiler = std::function; @@ -133,7 +136,8 @@ auto default_compiler(const sourcemeta::core::JSON &schema, const sourcemeta::core::SchemaFrame &frame, const sourcemeta::core::SchemaFrame::Location &location, const sourcemeta::core::SchemaResolver &resolver, - const sourcemeta::core::JSON &subschema) -> IREntity; + const sourcemeta::core::JSON &subschema) + -> CodegenIREntity; /// @ingroup codegen SOURCEMETA_BLAZE_CODEGEN_EXPORT @@ -142,7 +146,7 @@ auto compile(const sourcemeta::core::JSON &schema, const sourcemeta::core::SchemaResolver &resolver, const CodegenCompiler &compiler, const std::string_view default_dialect = "", - const std::string_view default_id = "") -> IRResult; + const std::string_view default_id = "") -> CodegenIRResult; /// @ingroup codegen SOURCEMETA_BLAZE_CODEGEN_EXPORT @@ -160,7 +164,7 @@ auto mangle(const std::string_view prefix, /// @ingroup codegen template -auto generate(std::ostream &output, const IRResult &result, +auto generate(std::ostream &output, const CodegenIRResult &result, const std::string_view prefix = "Schema") -> void { T visitor{output, prefix}; const char *separator{""}; diff --git a/src/codegen/include/sourcemeta/blaze/codegen_error.h b/src/codegen/include/sourcemeta/blaze/codegen_error.h index d3cbd09de..bb5fc76f7 100644 --- a/src/codegen/include/sourcemeta/blaze/codegen_error.h +++ b/src/codegen/include/sourcemeta/blaze/codegen_error.h @@ -24,30 +24,32 @@ namespace sourcemeta::blaze { /// @ingroup codegen /// An error that represents an unsupported keyword during IR compilation -class SOURCEMETA_BLAZE_CODEGEN_EXPORT UnsupportedKeywordError +class SOURCEMETA_BLAZE_CODEGEN_EXPORT CodegenUnsupportedKeywordError : public std::exception { public: - UnsupportedKeywordError(sourcemeta::core::JSON json, - sourcemeta::core::Pointer pointer, - std::string keyword, const char *message) + CodegenUnsupportedKeywordError(sourcemeta::core::JSON json, + sourcemeta::core::Pointer pointer, + std::string keyword, const char *message) : json_{std::move(json)}, pointer_{std::move(pointer)}, keyword_{std::move(keyword)}, message_{message} {} - UnsupportedKeywordError(sourcemeta::core::JSON json, - const sourcemeta::core::WeakPointer &pointer, - std::string keyword, const char *message) - : UnsupportedKeywordError{std::move(json), - sourcemeta::core::to_pointer(pointer), - std::move(keyword), message} {} - UnsupportedKeywordError(sourcemeta::core::JSON json, - sourcemeta::core::Pointer pointer, - std::string keyword, std::string message) = delete; - UnsupportedKeywordError(sourcemeta::core::JSON json, - sourcemeta::core::Pointer pointer, - std::string keyword, std::string &&message) = delete; - UnsupportedKeywordError(sourcemeta::core::JSON json, - sourcemeta::core::Pointer pointer, - std::string keyword, - std::string_view message) = delete; + CodegenUnsupportedKeywordError(sourcemeta::core::JSON json, + const sourcemeta::core::WeakPointer &pointer, + std::string keyword, const char *message) + : CodegenUnsupportedKeywordError{std::move(json), + sourcemeta::core::to_pointer(pointer), + std::move(keyword), message} {} + CodegenUnsupportedKeywordError(sourcemeta::core::JSON json, + sourcemeta::core::Pointer pointer, + std::string keyword, + std::string message) = delete; + CodegenUnsupportedKeywordError(sourcemeta::core::JSON json, + sourcemeta::core::Pointer pointer, + std::string keyword, + std::string &&message) = delete; + CodegenUnsupportedKeywordError(sourcemeta::core::JSON json, + sourcemeta::core::Pointer pointer, + std::string keyword, + std::string_view message) = delete; [[nodiscard]] auto what() const noexcept -> const char * override { return this->message_; @@ -75,32 +77,32 @@ class SOURCEMETA_BLAZE_CODEGEN_EXPORT UnsupportedKeywordError /// @ingroup codegen /// An error that represents an unsupported keyword value during IR compilation -class SOURCEMETA_BLAZE_CODEGEN_EXPORT UnsupportedKeywordValueError +class SOURCEMETA_BLAZE_CODEGEN_EXPORT CodegenUnsupportedKeywordValueError : public std::exception { public: - UnsupportedKeywordValueError(sourcemeta::core::JSON json, - sourcemeta::core::Pointer pointer, - std::string keyword, const char *message) + CodegenUnsupportedKeywordValueError(sourcemeta::core::JSON json, + sourcemeta::core::Pointer pointer, + std::string keyword, const char *message) : json_{std::move(json)}, pointer_{std::move(pointer)}, keyword_{std::move(keyword)}, message_{message} {} - UnsupportedKeywordValueError(sourcemeta::core::JSON json, - const sourcemeta::core::WeakPointer &pointer, - std::string keyword, const char *message) - : UnsupportedKeywordValueError{std::move(json), - sourcemeta::core::to_pointer(pointer), - std::move(keyword), message} {} - UnsupportedKeywordValueError(sourcemeta::core::JSON json, - sourcemeta::core::Pointer pointer, - std::string keyword, - std::string message) = delete; - UnsupportedKeywordValueError(sourcemeta::core::JSON json, - sourcemeta::core::Pointer pointer, - std::string keyword, - std::string &&message) = delete; - UnsupportedKeywordValueError(sourcemeta::core::JSON json, - sourcemeta::core::Pointer pointer, - std::string keyword, - std::string_view message) = delete; + CodegenUnsupportedKeywordValueError( + sourcemeta::core::JSON json, const sourcemeta::core::WeakPointer &pointer, + std::string keyword, const char *message) + : CodegenUnsupportedKeywordValueError{ + std::move(json), sourcemeta::core::to_pointer(pointer), + std::move(keyword), message} {} + CodegenUnsupportedKeywordValueError(sourcemeta::core::JSON json, + sourcemeta::core::Pointer pointer, + std::string keyword, + std::string message) = delete; + CodegenUnsupportedKeywordValueError(sourcemeta::core::JSON json, + sourcemeta::core::Pointer pointer, + std::string keyword, + std::string &&message) = delete; + CodegenUnsupportedKeywordValueError(sourcemeta::core::JSON json, + sourcemeta::core::Pointer pointer, + std::string keyword, + std::string_view message) = delete; [[nodiscard]] auto what() const noexcept -> const char * override { return this->message_; @@ -128,27 +130,28 @@ class SOURCEMETA_BLAZE_CODEGEN_EXPORT UnsupportedKeywordValueError /// @ingroup codegen /// An error that represents an unexpected schema during IR compilation -class SOURCEMETA_BLAZE_CODEGEN_EXPORT UnexpectedSchemaError +class SOURCEMETA_BLAZE_CODEGEN_EXPORT CodegenUnexpectedSchemaError : public std::exception { public: - UnexpectedSchemaError(sourcemeta::core::JSON json, - sourcemeta::core::Pointer pointer, const char *message) + CodegenUnexpectedSchemaError(sourcemeta::core::JSON json, + sourcemeta::core::Pointer pointer, + const char *message) : json_{std::move(json)}, pointer_{std::move(pointer)}, message_{message} {} - UnexpectedSchemaError(sourcemeta::core::JSON json, - const sourcemeta::core::WeakPointer &pointer, - const char *message) - : UnexpectedSchemaError{std::move(json), - sourcemeta::core::to_pointer(pointer), message} {} - UnexpectedSchemaError(sourcemeta::core::JSON json, - sourcemeta::core::Pointer pointer, - std::string message) = delete; - UnexpectedSchemaError(sourcemeta::core::JSON json, - sourcemeta::core::Pointer pointer, - std::string &&message) = delete; - UnexpectedSchemaError(sourcemeta::core::JSON json, - sourcemeta::core::Pointer pointer, - std::string_view message) = delete; + CodegenUnexpectedSchemaError(sourcemeta::core::JSON json, + const sourcemeta::core::WeakPointer &pointer, + const char *message) + : CodegenUnexpectedSchemaError{ + std::move(json), sourcemeta::core::to_pointer(pointer), message} {} + CodegenUnexpectedSchemaError(sourcemeta::core::JSON json, + sourcemeta::core::Pointer pointer, + std::string message) = delete; + CodegenUnexpectedSchemaError(sourcemeta::core::JSON json, + sourcemeta::core::Pointer pointer, + std::string &&message) = delete; + CodegenUnexpectedSchemaError(sourcemeta::core::JSON json, + sourcemeta::core::Pointer pointer, + std::string_view message) = delete; [[nodiscard]] auto what() const noexcept -> const char * override { return this->message_; diff --git a/src/codegen/include/sourcemeta/blaze/codegen_typescript.h b/src/codegen/include/sourcemeta/blaze/codegen_typescript.h index 90dfde17e..cac0000f1 100644 --- a/src/codegen/include/sourcemeta/blaze/codegen_typescript.h +++ b/src/codegen/include/sourcemeta/blaze/codegen_typescript.h @@ -14,33 +14,33 @@ namespace sourcemeta::blaze { -struct IRScalar; -struct IREnumeration; -struct IRObject; -struct IRImpossible; -struct IRAny; -struct IRArray; -struct IRReference; -struct IRTuple; -struct IRUnion; -struct IRIntersection; -struct IRConditional; +struct CodegenIRScalar; +struct CodegenIREnumeration; +struct CodegenIRObject; +struct CodegenIRImpossible; +struct CodegenIRAny; +struct CodegenIRArray; +struct CodegenIRReference; +struct CodegenIRTuple; +struct CodegenIRUnion; +struct CodegenIRIntersection; +struct CodegenIRConditional; /// @ingroup codegen class SOURCEMETA_BLAZE_CODEGEN_EXPORT TypeScript { public: TypeScript(std::ostream &stream, std::string_view type_prefix); - auto operator()(const IRScalar &entry) -> void; - auto operator()(const IREnumeration &entry) -> void; - auto operator()(const IRObject &entry) -> void; - auto operator()(const IRImpossible &entry) -> void; - auto operator()(const IRAny &entry) -> void; - auto operator()(const IRArray &entry) -> void; - auto operator()(const IRReference &entry) -> void; - auto operator()(const IRTuple &entry) -> void; - auto operator()(const IRUnion &entry) -> void; - auto operator()(const IRIntersection &entry) -> void; - auto operator()(const IRConditional &entry) -> void; + auto operator()(const CodegenIRScalar &entry) -> void; + auto operator()(const CodegenIREnumeration &entry) -> void; + auto operator()(const CodegenIRObject &entry) -> void; + auto operator()(const CodegenIRImpossible &entry) -> void; + auto operator()(const CodegenIRAny &entry) -> void; + auto operator()(const CodegenIRArray &entry) -> void; + auto operator()(const CodegenIRReference &entry) -> void; + auto operator()(const CodegenIRTuple &entry) -> void; + auto operator()(const CodegenIRUnion &entry) -> void; + auto operator()(const CodegenIRIntersection &entry) -> void; + auto operator()(const CodegenIRConditional &entry) -> void; private: // Exporting symbols that depends on the standard C++ library is considered diff --git a/test/codegen/codegen_2020_12_test.cc b/test/codegen/codegen_2020_12_test.cc index 617394166..fa78f28c3 100644 --- a/test/codegen/codegen_2020_12_test.cc +++ b/test/codegen/codegen_2020_12_test.cc @@ -4,7 +4,7 @@ #include "codegen_test_utils.h" -TEST(IR_2020_12, test_1) { +TEST(Codegen_2020_12, test_1) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "string" @@ -19,10 +19,10 @@ TEST(IR_2020_12, test_1) { EXPECT_EQ(result.size(), 1); EXPECT_IR_SCALAR(result, 0, String, ""); - EXPECT_SYMBOL(std::get(result.at(0)).symbol); + EXPECT_SYMBOL(std::get(result.at(0)).symbol); } -TEST(IR_2020_12, default_dialect_parameter) { +TEST(Codegen_2020_12, default_dialect_parameter) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "type": "string" })JSON")}; @@ -37,10 +37,10 @@ TEST(IR_2020_12, default_dialect_parameter) { EXPECT_EQ(result.size(), 1); EXPECT_IR_SCALAR(result, 0, String, ""); - EXPECT_SYMBOL(std::get(result.at(0)).symbol); + EXPECT_SYMBOL(std::get(result.at(0)).symbol); } -TEST(IR_2020_12, test_2) { +TEST(Codegen_2020_12, test_2) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", @@ -60,26 +60,30 @@ TEST(IR_2020_12, test_2) { EXPECT_EQ(result.size(), 2); EXPECT_IR_SCALAR(result, 0, String, "/properties/foo"); - EXPECT_SYMBOL(std::get(result.at(0)).symbol, "foo"); - - EXPECT_TRUE(std::holds_alternative(result.at(1))); - EXPECT_AS_STRING(std::get(result.at(1)).pointer, ""); - EXPECT_SYMBOL(std::get(result.at(1)).symbol); - EXPECT_EQ(std::get(result.at(1)).members.size(), 1); - EXPECT_EQ(std::get(result.at(1)).members.at(0).first, "foo"); - EXPECT_FALSE(std::get(result.at(1)).members.at(0).second.required); - EXPECT_FALSE(std::get(result.at(1)).members.at(0).second.immutable); + EXPECT_SYMBOL(std::get(result.at(0)).symbol, "foo"); + + EXPECT_TRUE(std::holds_alternative(result.at(1))); + EXPECT_AS_STRING(std::get(result.at(1)).pointer, ""); + EXPECT_SYMBOL(std::get(result.at(1)).symbol); + EXPECT_EQ(std::get(result.at(1)).members.size(), 1); + EXPECT_EQ(std::get(result.at(1)).members.at(0).first, "foo"); + EXPECT_FALSE( + std::get(result.at(1)).members.at(0).second.required); + EXPECT_FALSE( + std::get(result.at(1)).members.at(0).second.immutable); EXPECT_AS_STRING( - std::get(result.at(1)).members.at(0).second.pointer, + std::get(result.at(1)).members.at(0).second.pointer, "/properties/foo"); - EXPECT_SYMBOL(std::get(result.at(1)).members.at(0).second.symbol, - "foo"); + EXPECT_SYMBOL( + std::get(result.at(1)).members.at(0).second.symbol, + "foo"); EXPECT_TRUE(std::holds_alternative( - std::get(result.at(1)).additional)); - EXPECT_TRUE(std::get(std::get(result.at(1)).additional)); + std::get(result.at(1)).additional)); + EXPECT_TRUE( + std::get(std::get(result.at(1)).additional)); } -TEST(IR_2020_12, test_3) { +TEST(Codegen_2020_12, test_3) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "integer" @@ -94,10 +98,10 @@ TEST(IR_2020_12, test_3) { EXPECT_EQ(result.size(), 1); EXPECT_IR_SCALAR(result, 0, Integer, ""); - EXPECT_SYMBOL(std::get(result.at(0)).symbol); + EXPECT_SYMBOL(std::get(result.at(0)).symbol); } -TEST(IR_2020_12, test_4) { +TEST(Codegen_2020_12, test_4) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "number" @@ -112,10 +116,10 @@ TEST(IR_2020_12, test_4) { EXPECT_EQ(result.size(), 1); EXPECT_IR_SCALAR(result, 0, Number, ""); - EXPECT_SYMBOL(std::get(result.at(0)).symbol); + EXPECT_SYMBOL(std::get(result.at(0)).symbol); } -TEST(IR_2020_12, test_5) { +TEST(Codegen_2020_12, test_5) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "integer", @@ -133,10 +137,10 @@ TEST(IR_2020_12, test_5) { EXPECT_EQ(result.size(), 1); EXPECT_IR_SCALAR(result, 0, Integer, ""); - EXPECT_SYMBOL(std::get(result.at(0)).symbol); + EXPECT_SYMBOL(std::get(result.at(0)).symbol); } -TEST(IR_2020_12, test_6) { +TEST(Codegen_2020_12, test_6) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "number", @@ -154,10 +158,10 @@ TEST(IR_2020_12, test_6) { EXPECT_EQ(result.size(), 1); EXPECT_IR_SCALAR(result, 0, Number, ""); - EXPECT_SYMBOL(std::get(result.at(0)).symbol); + EXPECT_SYMBOL(std::get(result.at(0)).symbol); } -TEST(IR_2020_12, enum_null) { +TEST(Codegen_2020_12, enum_null) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "enum": [ null ] @@ -172,10 +176,10 @@ TEST(IR_2020_12, enum_null) { EXPECT_EQ(result.size(), 1); EXPECT_IR_SCALAR(result, 0, Null, ""); - EXPECT_SYMBOL(std::get(result.at(0)).symbol); + EXPECT_SYMBOL(std::get(result.at(0)).symbol); } -TEST(IR_2020_12, enum_boolean_true_false) { +TEST(Codegen_2020_12, enum_boolean_true_false) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "enum": [ true, false ] @@ -190,10 +194,10 @@ TEST(IR_2020_12, enum_boolean_true_false) { EXPECT_EQ(result.size(), 1); EXPECT_IR_SCALAR(result, 0, Boolean, ""); - EXPECT_SYMBOL(std::get(result.at(0)).symbol); + EXPECT_SYMBOL(std::get(result.at(0)).symbol); } -TEST(IR_2020_12, enum_boolean_false_true) { +TEST(Codegen_2020_12, enum_boolean_false_true) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "enum": [ false, true ] @@ -208,10 +212,10 @@ TEST(IR_2020_12, enum_boolean_false_true) { EXPECT_EQ(result.size(), 1); EXPECT_IR_SCALAR(result, 0, Boolean, ""); - EXPECT_SYMBOL(std::get(result.at(0)).symbol); + EXPECT_SYMBOL(std::get(result.at(0)).symbol); } -TEST(IR_2020_12, enum_string_values) { +TEST(Codegen_2020_12, enum_string_values) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "enum": [ "foo", "bar", "baz" ] @@ -225,19 +229,22 @@ TEST(IR_2020_12, enum_string_values) { EXPECT_EQ(result.size(), 1); - EXPECT_TRUE(std::holds_alternative(result.at(0))); - EXPECT_AS_STRING(std::get(result.at(0)).pointer, ""); - EXPECT_SYMBOL(std::get(result.at(0)).symbol); - EXPECT_EQ(std::get(result.at(0)).values.size(), 3); - EXPECT_EQ(std::get(result.at(0)).values.at(0).to_string(), - "foo"); - EXPECT_EQ(std::get(result.at(0)).values.at(1).to_string(), - "bar"); - EXPECT_EQ(std::get(result.at(0)).values.at(2).to_string(), - "baz"); + EXPECT_TRUE(std::holds_alternative(result.at(0))); + EXPECT_AS_STRING(std::get(result.at(0)).pointer, ""); + EXPECT_SYMBOL(std::get(result.at(0)).symbol); + EXPECT_EQ(std::get(result.at(0)).values.size(), 3); + EXPECT_EQ( + std::get(result.at(0)).values.at(0).to_string(), + "foo"); + EXPECT_EQ( + std::get(result.at(0)).values.at(1).to_string(), + "bar"); + EXPECT_EQ( + std::get(result.at(0)).values.at(2).to_string(), + "baz"); } -TEST(IR_2020_12, const_null) { +TEST(Codegen_2020_12, const_null) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "const": null @@ -252,10 +259,10 @@ TEST(IR_2020_12, const_null) { EXPECT_EQ(result.size(), 1); EXPECT_IR_SCALAR(result, 0, Null, ""); - EXPECT_SYMBOL(std::get(result.at(0)).symbol); + EXPECT_SYMBOL(std::get(result.at(0)).symbol); } -TEST(IR_2020_12, const_string) { +TEST(Codegen_2020_12, const_string) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "const": "hello" @@ -269,15 +276,16 @@ TEST(IR_2020_12, const_string) { EXPECT_EQ(result.size(), 1); - EXPECT_TRUE(std::holds_alternative(result.at(0))); - EXPECT_AS_STRING(std::get(result.at(0)).pointer, ""); - EXPECT_SYMBOL(std::get(result.at(0)).symbol); - EXPECT_EQ(std::get(result.at(0)).values.size(), 1); - EXPECT_EQ(std::get(result.at(0)).values.at(0).to_string(), - "hello"); + EXPECT_TRUE(std::holds_alternative(result.at(0))); + EXPECT_AS_STRING(std::get(result.at(0)).pointer, ""); + EXPECT_SYMBOL(std::get(result.at(0)).symbol); + EXPECT_EQ(std::get(result.at(0)).values.size(), 1); + EXPECT_EQ( + std::get(result.at(0)).values.at(0).to_string(), + "hello"); } -TEST(IR_2020_12, const_integer) { +TEST(Codegen_2020_12, const_integer) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "const": 42 @@ -291,15 +299,16 @@ TEST(IR_2020_12, const_integer) { EXPECT_EQ(result.size(), 1); - EXPECT_TRUE(std::holds_alternative(result.at(0))); - EXPECT_AS_STRING(std::get(result.at(0)).pointer, ""); - EXPECT_SYMBOL(std::get(result.at(0)).symbol); - EXPECT_EQ(std::get(result.at(0)).values.size(), 1); - EXPECT_EQ(std::get(result.at(0)).values.at(0).to_integer(), - 42); + EXPECT_TRUE(std::holds_alternative(result.at(0))); + EXPECT_AS_STRING(std::get(result.at(0)).pointer, ""); + EXPECT_SYMBOL(std::get(result.at(0)).symbol); + EXPECT_EQ(std::get(result.at(0)).values.size(), 1); + EXPECT_EQ( + std::get(result.at(0)).values.at(0).to_integer(), + 42); } -TEST(IR_2020_12, const_boolean_true) { +TEST(Codegen_2020_12, const_boolean_true) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "const": true @@ -313,14 +322,15 @@ TEST(IR_2020_12, const_boolean_true) { EXPECT_EQ(result.size(), 1); - EXPECT_TRUE(std::holds_alternative(result.at(0))); - EXPECT_AS_STRING(std::get(result.at(0)).pointer, ""); - EXPECT_SYMBOL(std::get(result.at(0)).symbol); - EXPECT_EQ(std::get(result.at(0)).values.size(), 1); - EXPECT_TRUE(std::get(result.at(0)).values.at(0).to_boolean()); + EXPECT_TRUE(std::holds_alternative(result.at(0))); + EXPECT_AS_STRING(std::get(result.at(0)).pointer, ""); + EXPECT_SYMBOL(std::get(result.at(0)).symbol); + EXPECT_EQ(std::get(result.at(0)).values.size(), 1); + EXPECT_TRUE( + std::get(result.at(0)).values.at(0).to_boolean()); } -TEST(IR_2020_12, object_type_only) { +TEST(Codegen_2020_12, object_type_only) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object" @@ -334,16 +344,17 @@ TEST(IR_2020_12, object_type_only) { EXPECT_EQ(result.size(), 1); - EXPECT_TRUE(std::holds_alternative(result.at(0))); - EXPECT_AS_STRING(std::get(result.at(0)).pointer, ""); - EXPECT_SYMBOL(std::get(result.at(0)).symbol); - EXPECT_TRUE(std::get(result.at(0)).members.empty()); + EXPECT_TRUE(std::holds_alternative(result.at(0))); + EXPECT_AS_STRING(std::get(result.at(0)).pointer, ""); + EXPECT_SYMBOL(std::get(result.at(0)).symbol); + EXPECT_TRUE(std::get(result.at(0)).members.empty()); EXPECT_TRUE(std::holds_alternative( - std::get(result.at(0)).additional)); - EXPECT_TRUE(std::get(std::get(result.at(0)).additional)); + std::get(result.at(0)).additional)); + EXPECT_TRUE( + std::get(std::get(result.at(0)).additional)); } -TEST(IR_2020_12, object_empty_properties) { +TEST(Codegen_2020_12, object_empty_properties) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", @@ -358,16 +369,17 @@ TEST(IR_2020_12, object_empty_properties) { EXPECT_EQ(result.size(), 1); - EXPECT_TRUE(std::holds_alternative(result.at(0))); - EXPECT_AS_STRING(std::get(result.at(0)).pointer, ""); - EXPECT_SYMBOL(std::get(result.at(0)).symbol); - EXPECT_TRUE(std::get(result.at(0)).members.empty()); + EXPECT_TRUE(std::holds_alternative(result.at(0))); + EXPECT_AS_STRING(std::get(result.at(0)).pointer, ""); + EXPECT_SYMBOL(std::get(result.at(0)).symbol); + EXPECT_TRUE(std::get(result.at(0)).members.empty()); EXPECT_TRUE(std::holds_alternative( - std::get(result.at(0)).additional)); - EXPECT_TRUE(std::get(std::get(result.at(0)).additional)); + std::get(result.at(0)).additional)); + EXPECT_TRUE( + std::get(std::get(result.at(0)).additional)); } -TEST(IR_2020_12, object_with_additional_properties) { +TEST(Codegen_2020_12, object_with_additional_properties) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", @@ -386,35 +398,41 @@ TEST(IR_2020_12, object_with_additional_properties) { EXPECT_EQ(result.size(), 3); EXPECT_IR_SCALAR(result, 0, String, "/properties/foo"); - EXPECT_SYMBOL(std::get(result.at(0)).symbol, "foo"); + EXPECT_SYMBOL(std::get(result.at(0)).symbol, "foo"); EXPECT_IR_SCALAR(result, 1, Integer, "/additionalProperties"); - EXPECT_SYMBOL(std::get(result.at(1)).symbol, + EXPECT_SYMBOL(std::get(result.at(1)).symbol, "additionalProperties"); - EXPECT_TRUE(std::holds_alternative(result.at(2))); - EXPECT_AS_STRING(std::get(result.at(2)).pointer, ""); - EXPECT_SYMBOL(std::get(result.at(2)).symbol); - EXPECT_EQ(std::get(result.at(2)).members.size(), 1); - EXPECT_TRUE(std::get(result.at(2)).members.at(0).first == "foo"); - EXPECT_FALSE(std::get(result.at(2)).members.at(0).second.required); - EXPECT_FALSE(std::get(result.at(2)).members.at(0).second.immutable); + EXPECT_TRUE(std::holds_alternative(result.at(2))); + EXPECT_AS_STRING(std::get(result.at(2)).pointer, ""); + EXPECT_SYMBOL(std::get(result.at(2)).symbol); + EXPECT_EQ(std::get(result.at(2)).members.size(), 1); + EXPECT_TRUE(std::get(result.at(2)).members.at(0).first == + "foo"); + EXPECT_FALSE( + std::get(result.at(2)).members.at(0).second.required); + EXPECT_FALSE( + std::get(result.at(2)).members.at(0).second.immutable); EXPECT_AS_STRING( - std::get(result.at(2)).members.at(0).second.pointer, + std::get(result.at(2)).members.at(0).second.pointer, "/properties/foo"); - EXPECT_SYMBOL(std::get(result.at(2)).members.at(0).second.symbol, - "foo"); - - EXPECT_TRUE(std::holds_alternative( - std::get(result.at(2)).additional)); - EXPECT_AS_STRING( - std::get(std::get(result.at(2)).additional).pointer, - "/additionalProperties"); EXPECT_SYMBOL( - std::get(std::get(result.at(2)).additional).symbol, - "additionalProperties"); + std::get(result.at(2)).members.at(0).second.symbol, + "foo"); + + EXPECT_TRUE(std::holds_alternative( + std::get(result.at(2)).additional)); + EXPECT_AS_STRING(std::get( + std::get(result.at(2)).additional) + .pointer, + "/additionalProperties"); + EXPECT_SYMBOL(std::get( + std::get(result.at(2)).additional) + .symbol, + "additionalProperties"); } -TEST(IR_2020_12, object_with_impossible_property) { +TEST(Codegen_2020_12, object_with_impossible_property) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", @@ -432,26 +450,31 @@ TEST(IR_2020_12, object_with_impossible_property) { EXPECT_EQ(result.size(), 2); EXPECT_IR_IMPOSSIBLE(result, 0, "/properties/foo"); - EXPECT_SYMBOL(std::get(result.at(0)).symbol, "foo"); - - EXPECT_TRUE(std::holds_alternative(result.at(1))); - EXPECT_AS_STRING(std::get(result.at(1)).pointer, ""); - EXPECT_SYMBOL(std::get(result.at(1)).symbol); - EXPECT_EQ(std::get(result.at(1)).members.size(), 1); - EXPECT_TRUE(std::get(result.at(1)).members.at(0).first == "foo"); - EXPECT_FALSE(std::get(result.at(1)).members.at(0).second.required); - EXPECT_FALSE(std::get(result.at(1)).members.at(0).second.immutable); + EXPECT_SYMBOL(std::get(result.at(0)).symbol, "foo"); + + EXPECT_TRUE(std::holds_alternative(result.at(1))); + EXPECT_AS_STRING(std::get(result.at(1)).pointer, ""); + EXPECT_SYMBOL(std::get(result.at(1)).symbol); + EXPECT_EQ(std::get(result.at(1)).members.size(), 1); + EXPECT_TRUE(std::get(result.at(1)).members.at(0).first == + "foo"); + EXPECT_FALSE( + std::get(result.at(1)).members.at(0).second.required); + EXPECT_FALSE( + std::get(result.at(1)).members.at(0).second.immutable); EXPECT_AS_STRING( - std::get(result.at(1)).members.at(0).second.pointer, + std::get(result.at(1)).members.at(0).second.pointer, "/properties/foo"); - EXPECT_SYMBOL(std::get(result.at(1)).members.at(0).second.symbol, - "foo"); + EXPECT_SYMBOL( + std::get(result.at(1)).members.at(0).second.symbol, + "foo"); EXPECT_TRUE(std::holds_alternative( - std::get(result.at(1)).additional)); - EXPECT_TRUE(std::get(std::get(result.at(1)).additional)); + 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) { +TEST(Codegen_2020_12, object_with_impossible_additional_properties) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", @@ -470,31 +493,36 @@ TEST(IR_2020_12, object_with_impossible_additional_properties) { EXPECT_EQ(result.size(), 3); EXPECT_IR_SCALAR(result, 0, String, "/properties/foo"); - EXPECT_SYMBOL(std::get(result.at(0)).symbol, "foo"); + EXPECT_SYMBOL(std::get(result.at(0)).symbol, "foo"); EXPECT_IR_IMPOSSIBLE(result, 1, "/additionalProperties"); - EXPECT_SYMBOL(std::get(result.at(1)).symbol, + EXPECT_SYMBOL(std::get(result.at(1)).symbol, "additionalProperties"); - EXPECT_TRUE(std::holds_alternative(result.at(2))); - EXPECT_AS_STRING(std::get(result.at(2)).pointer, ""); - EXPECT_SYMBOL(std::get(result.at(2)).symbol); - EXPECT_EQ(std::get(result.at(2)).members.size(), 1); - EXPECT_TRUE(std::get(result.at(2)).members.at(0).first == "foo"); - EXPECT_FALSE(std::get(result.at(2)).members.at(0).second.required); - EXPECT_FALSE(std::get(result.at(2)).members.at(0).second.immutable); + EXPECT_TRUE(std::holds_alternative(result.at(2))); + EXPECT_AS_STRING(std::get(result.at(2)).pointer, ""); + EXPECT_SYMBOL(std::get(result.at(2)).symbol); + EXPECT_EQ(std::get(result.at(2)).members.size(), 1); + EXPECT_TRUE(std::get(result.at(2)).members.at(0).first == + "foo"); + EXPECT_FALSE( + std::get(result.at(2)).members.at(0).second.required); + EXPECT_FALSE( + std::get(result.at(2)).members.at(0).second.immutable); EXPECT_AS_STRING( - std::get(result.at(2)).members.at(0).second.pointer, + std::get(result.at(2)).members.at(0).second.pointer, "/properties/foo"); - EXPECT_SYMBOL(std::get(result.at(2)).members.at(0).second.symbol, - "foo"); + EXPECT_SYMBOL( + std::get(result.at(2)).members.at(0).second.symbol, + "foo"); EXPECT_TRUE(std::holds_alternative( - std::get(result.at(2)).additional)); - EXPECT_FALSE(std::get(std::get(result.at(2)).additional)); + std::get(result.at(2)).additional)); + EXPECT_FALSE( + std::get(std::get(result.at(2)).additional)); } -TEST(IR_2020_12, array_with_items) { +TEST(Codegen_2020_12, array_with_items) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "array", @@ -510,14 +538,14 @@ TEST(IR_2020_12, array_with_items) { EXPECT_EQ(result.size(), 2); EXPECT_IR_SCALAR(result, 0, String, "/items"); - EXPECT_SYMBOL(std::get(result.at(0)).symbol, "items"); + EXPECT_SYMBOL(std::get(result.at(0)).symbol, "items"); EXPECT_IR_ARRAY(result, 1, "", "/items"); - EXPECT_SYMBOL(std::get(result.at(1)).symbol); - EXPECT_SYMBOL(std::get(result.at(1)).items->symbol, "items"); + EXPECT_SYMBOL(std::get(result.at(1)).symbol); + EXPECT_SYMBOL(std::get(result.at(1)).items->symbol, "items"); } -TEST(IR_2020_12, array_nested_in_object) { +TEST(Codegen_2020_12, array_nested_in_object) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", @@ -538,18 +566,20 @@ TEST(IR_2020_12, array_nested_in_object) { EXPECT_EQ(result.size(), 3); EXPECT_IR_SCALAR(result, 0, String, "/properties/tags/items"); - EXPECT_SYMBOL(std::get(result.at(0)).symbol, "tags", "items"); + EXPECT_SYMBOL(std::get(result.at(0)).symbol, "tags", + "items"); EXPECT_IR_ARRAY(result, 1, "/properties/tags", "/properties/tags/items"); - EXPECT_SYMBOL(std::get(result.at(1)).symbol, "tags"); - EXPECT_SYMBOL(std::get(result.at(1)).items->symbol, "tags", "items"); + EXPECT_SYMBOL(std::get(result.at(1)).symbol, "tags"); + EXPECT_SYMBOL(std::get(result.at(1)).items->symbol, "tags", + "items"); - EXPECT_TRUE(std::holds_alternative(result.at(2))); - EXPECT_AS_STRING(std::get(result.at(2)).pointer, ""); - EXPECT_SYMBOL(std::get(result.at(2)).symbol); + EXPECT_TRUE(std::holds_alternative(result.at(2))); + EXPECT_AS_STRING(std::get(result.at(2)).pointer, ""); + EXPECT_SYMBOL(std::get(result.at(2)).symbol); } -TEST(IR_2020_12, tuple_with_prefix_items) { +TEST(Codegen_2020_12, tuple_with_prefix_items) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "array", @@ -568,29 +598,30 @@ TEST(IR_2020_12, tuple_with_prefix_items) { EXPECT_EQ(result.size(), 4); EXPECT_IR_SCALAR(result, 0, Integer, "/prefixItems/1"); - EXPECT_SYMBOL(std::get(result.at(0)).symbol, "1"); + EXPECT_SYMBOL(std::get(result.at(0)).symbol, "1"); EXPECT_IR_SCALAR(result, 1, String, "/prefixItems/0"); - EXPECT_SYMBOL(std::get(result.at(1)).symbol, "0"); + EXPECT_SYMBOL(std::get(result.at(1)).symbol, "0"); EXPECT_IR_ANY(result, 2, "/items"); - EXPECT_SYMBOL(std::get(result.at(2)).symbol, "items"); + EXPECT_SYMBOL(std::get(result.at(2)).symbol, "items"); - EXPECT_TRUE(std::holds_alternative(result.at(3))); - EXPECT_AS_STRING(std::get(result.at(3)).pointer, ""); - EXPECT_SYMBOL(std::get(result.at(3)).symbol); - EXPECT_EQ(std::get(result.at(3)).items.size(), 2); - EXPECT_AS_STRING(std::get(result.at(3)).items.at(0).pointer, + EXPECT_TRUE(std::holds_alternative(result.at(3))); + EXPECT_AS_STRING(std::get(result.at(3)).pointer, ""); + EXPECT_SYMBOL(std::get(result.at(3)).symbol); + EXPECT_EQ(std::get(result.at(3)).items.size(), 2); + EXPECT_AS_STRING(std::get(result.at(3)).items.at(0).pointer, "/prefixItems/0"); - EXPECT_SYMBOL(std::get(result.at(3)).items.at(0).symbol, "0"); - EXPECT_AS_STRING(std::get(result.at(3)).items.at(1).pointer, + EXPECT_SYMBOL(std::get(result.at(3)).items.at(0).symbol, "0"); + EXPECT_AS_STRING(std::get(result.at(3)).items.at(1).pointer, "/prefixItems/1"); - EXPECT_SYMBOL(std::get(result.at(3)).items.at(1).symbol, "1"); - EXPECT_TRUE(std::get(result.at(3)).additional.has_value()); - EXPECT_AS_STRING(std::get(result.at(3)).additional->pointer, + EXPECT_SYMBOL(std::get(result.at(3)).items.at(1).symbol, "1"); + EXPECT_TRUE(std::get(result.at(3)).additional.has_value()); + EXPECT_AS_STRING(std::get(result.at(3)).additional->pointer, "/items"); - EXPECT_SYMBOL(std::get(result.at(3)).additional->symbol, "items"); + EXPECT_SYMBOL(std::get(result.at(3)).additional->symbol, + "items"); } -TEST(IR_2020_12, tuple_with_prefix_items_and_items) { +TEST(Codegen_2020_12, tuple_with_prefix_items_and_items) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "array", @@ -609,24 +640,25 @@ TEST(IR_2020_12, tuple_with_prefix_items_and_items) { EXPECT_EQ(result.size(), 3); EXPECT_IR_SCALAR(result, 0, String, "/prefixItems/0"); - EXPECT_SYMBOL(std::get(result.at(0)).symbol, "0"); + EXPECT_SYMBOL(std::get(result.at(0)).symbol, "0"); EXPECT_IR_SCALAR(result, 1, Boolean, "/items"); - EXPECT_SYMBOL(std::get(result.at(1)).symbol, "items"); + EXPECT_SYMBOL(std::get(result.at(1)).symbol, "items"); - EXPECT_TRUE(std::holds_alternative(result.at(2))); - EXPECT_AS_STRING(std::get(result.at(2)).pointer, ""); - EXPECT_SYMBOL(std::get(result.at(2)).symbol); - EXPECT_EQ(std::get(result.at(2)).items.size(), 1); - EXPECT_AS_STRING(std::get(result.at(2)).items.at(0).pointer, + EXPECT_TRUE(std::holds_alternative(result.at(2))); + EXPECT_AS_STRING(std::get(result.at(2)).pointer, ""); + EXPECT_SYMBOL(std::get(result.at(2)).symbol); + EXPECT_EQ(std::get(result.at(2)).items.size(), 1); + EXPECT_AS_STRING(std::get(result.at(2)).items.at(0).pointer, "/prefixItems/0"); - EXPECT_SYMBOL(std::get(result.at(2)).items.at(0).symbol, "0"); - EXPECT_TRUE(std::get(result.at(2)).additional.has_value()); - EXPECT_AS_STRING(std::get(result.at(2)).additional->pointer, + EXPECT_SYMBOL(std::get(result.at(2)).items.at(0).symbol, "0"); + EXPECT_TRUE(std::get(result.at(2)).additional.has_value()); + EXPECT_AS_STRING(std::get(result.at(2)).additional->pointer, "/items"); - EXPECT_SYMBOL(std::get(result.at(2)).additional->symbol, "items"); + EXPECT_SYMBOL(std::get(result.at(2)).additional->symbol, + "items"); } -TEST(IR_2020_12, anyof_two_branches) { +TEST(Codegen_2020_12, anyof_two_branches) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "anyOf": [ @@ -644,23 +676,25 @@ TEST(IR_2020_12, anyof_two_branches) { EXPECT_EQ(result.size(), 3); EXPECT_IR_SCALAR(result, 0, Integer, "/anyOf/1"); - EXPECT_SYMBOL(std::get(result.at(0)).symbol, "1"); + EXPECT_SYMBOL(std::get(result.at(0)).symbol, "1"); EXPECT_IR_SCALAR(result, 1, String, "/anyOf/0"); - EXPECT_SYMBOL(std::get(result.at(1)).symbol, "0"); + EXPECT_SYMBOL(std::get(result.at(1)).symbol, "0"); - EXPECT_TRUE(std::holds_alternative(result.at(2))); - EXPECT_AS_STRING(std::get(result.at(2)).pointer, ""); - EXPECT_SYMBOL(std::get(result.at(2)).symbol); - EXPECT_EQ(std::get(result.at(2)).values.size(), 2); - EXPECT_AS_STRING(std::get(result.at(2)).values.at(0).pointer, + EXPECT_TRUE(std::holds_alternative(result.at(2))); + EXPECT_AS_STRING(std::get(result.at(2)).pointer, ""); + EXPECT_SYMBOL(std::get(result.at(2)).symbol); + EXPECT_EQ(std::get(result.at(2)).values.size(), 2); + EXPECT_AS_STRING(std::get(result.at(2)).values.at(0).pointer, "/anyOf/0"); - EXPECT_SYMBOL(std::get(result.at(2)).values.at(0).symbol, "0"); - EXPECT_AS_STRING(std::get(result.at(2)).values.at(1).pointer, + EXPECT_SYMBOL(std::get(result.at(2)).values.at(0).symbol, + "0"); + EXPECT_AS_STRING(std::get(result.at(2)).values.at(1).pointer, "/anyOf/1"); - EXPECT_SYMBOL(std::get(result.at(2)).values.at(1).symbol, "1"); + EXPECT_SYMBOL(std::get(result.at(2)).values.at(1).symbol, + "1"); } -TEST(IR_2020_12, anyof_three_branches) { +TEST(Codegen_2020_12, anyof_three_branches) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "anyOf": [ @@ -679,28 +713,31 @@ TEST(IR_2020_12, anyof_three_branches) { EXPECT_EQ(result.size(), 4); EXPECT_IR_SCALAR(result, 0, Boolean, "/anyOf/2"); - EXPECT_SYMBOL(std::get(result.at(0)).symbol, "2"); + EXPECT_SYMBOL(std::get(result.at(0)).symbol, "2"); EXPECT_IR_SCALAR(result, 1, Integer, "/anyOf/1"); - EXPECT_SYMBOL(std::get(result.at(1)).symbol, "1"); + EXPECT_SYMBOL(std::get(result.at(1)).symbol, "1"); EXPECT_IR_SCALAR(result, 2, String, "/anyOf/0"); - EXPECT_SYMBOL(std::get(result.at(2)).symbol, "0"); + EXPECT_SYMBOL(std::get(result.at(2)).symbol, "0"); - EXPECT_TRUE(std::holds_alternative(result.at(3))); - EXPECT_AS_STRING(std::get(result.at(3)).pointer, ""); - EXPECT_SYMBOL(std::get(result.at(3)).symbol); - EXPECT_EQ(std::get(result.at(3)).values.size(), 3); - EXPECT_AS_STRING(std::get(result.at(3)).values.at(0).pointer, + EXPECT_TRUE(std::holds_alternative(result.at(3))); + EXPECT_AS_STRING(std::get(result.at(3)).pointer, ""); + EXPECT_SYMBOL(std::get(result.at(3)).symbol); + EXPECT_EQ(std::get(result.at(3)).values.size(), 3); + EXPECT_AS_STRING(std::get(result.at(3)).values.at(0).pointer, "/anyOf/0"); - EXPECT_SYMBOL(std::get(result.at(3)).values.at(0).symbol, "0"); - EXPECT_AS_STRING(std::get(result.at(3)).values.at(1).pointer, + EXPECT_SYMBOL(std::get(result.at(3)).values.at(0).symbol, + "0"); + EXPECT_AS_STRING(std::get(result.at(3)).values.at(1).pointer, "/anyOf/1"); - EXPECT_SYMBOL(std::get(result.at(3)).values.at(1).symbol, "1"); - EXPECT_AS_STRING(std::get(result.at(3)).values.at(2).pointer, + EXPECT_SYMBOL(std::get(result.at(3)).values.at(1).symbol, + "1"); + EXPECT_AS_STRING(std::get(result.at(3)).values.at(2).pointer, "/anyOf/2"); - EXPECT_SYMBOL(std::get(result.at(3)).values.at(2).symbol, "2"); + EXPECT_SYMBOL(std::get(result.at(3)).values.at(2).symbol, + "2"); } -TEST(IR_2020_12, oneof_two_branches) { +TEST(Codegen_2020_12, oneof_two_branches) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "oneOf": [ @@ -719,23 +756,25 @@ TEST(IR_2020_12, oneof_two_branches) { EXPECT_EQ(result.size(), 3); EXPECT_IR_SCALAR(result, 0, Integer, "/anyOf/1"); - EXPECT_SYMBOL(std::get(result.at(0)).symbol, "1"); + EXPECT_SYMBOL(std::get(result.at(0)).symbol, "1"); EXPECT_IR_SCALAR(result, 1, String, "/anyOf/0"); - EXPECT_SYMBOL(std::get(result.at(1)).symbol, "0"); + EXPECT_SYMBOL(std::get(result.at(1)).symbol, "0"); - EXPECT_TRUE(std::holds_alternative(result.at(2))); - EXPECT_AS_STRING(std::get(result.at(2)).pointer, ""); - EXPECT_SYMBOL(std::get(result.at(2)).symbol); - EXPECT_EQ(std::get(result.at(2)).values.size(), 2); - EXPECT_AS_STRING(std::get(result.at(2)).values.at(0).pointer, + EXPECT_TRUE(std::holds_alternative(result.at(2))); + EXPECT_AS_STRING(std::get(result.at(2)).pointer, ""); + EXPECT_SYMBOL(std::get(result.at(2)).symbol); + EXPECT_EQ(std::get(result.at(2)).values.size(), 2); + EXPECT_AS_STRING(std::get(result.at(2)).values.at(0).pointer, "/anyOf/0"); - EXPECT_SYMBOL(std::get(result.at(2)).values.at(0).symbol, "0"); - EXPECT_AS_STRING(std::get(result.at(2)).values.at(1).pointer, + EXPECT_SYMBOL(std::get(result.at(2)).values.at(0).symbol, + "0"); + EXPECT_AS_STRING(std::get(result.at(2)).values.at(1).pointer, "/anyOf/1"); - EXPECT_SYMBOL(std::get(result.at(2)).values.at(1).symbol, "1"); + EXPECT_SYMBOL(std::get(result.at(2)).values.at(1).symbol, + "1"); } -TEST(IR_2020_12, oneof_three_branches) { +TEST(Codegen_2020_12, oneof_three_branches) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "oneOf": [ @@ -755,28 +794,31 @@ TEST(IR_2020_12, oneof_three_branches) { EXPECT_EQ(result.size(), 4); EXPECT_IR_SCALAR(result, 0, Boolean, "/anyOf/2"); - EXPECT_SYMBOL(std::get(result.at(0)).symbol, "2"); + EXPECT_SYMBOL(std::get(result.at(0)).symbol, "2"); EXPECT_IR_SCALAR(result, 1, Integer, "/anyOf/1"); - EXPECT_SYMBOL(std::get(result.at(1)).symbol, "1"); + EXPECT_SYMBOL(std::get(result.at(1)).symbol, "1"); EXPECT_IR_SCALAR(result, 2, String, "/anyOf/0"); - EXPECT_SYMBOL(std::get(result.at(2)).symbol, "0"); + EXPECT_SYMBOL(std::get(result.at(2)).symbol, "0"); - EXPECT_TRUE(std::holds_alternative(result.at(3))); - EXPECT_AS_STRING(std::get(result.at(3)).pointer, ""); - EXPECT_SYMBOL(std::get(result.at(3)).symbol); - EXPECT_EQ(std::get(result.at(3)).values.size(), 3); - EXPECT_AS_STRING(std::get(result.at(3)).values.at(0).pointer, + EXPECT_TRUE(std::holds_alternative(result.at(3))); + EXPECT_AS_STRING(std::get(result.at(3)).pointer, ""); + EXPECT_SYMBOL(std::get(result.at(3)).symbol); + EXPECT_EQ(std::get(result.at(3)).values.size(), 3); + EXPECT_AS_STRING(std::get(result.at(3)).values.at(0).pointer, "/anyOf/0"); - EXPECT_SYMBOL(std::get(result.at(3)).values.at(0).symbol, "0"); - EXPECT_AS_STRING(std::get(result.at(3)).values.at(1).pointer, + EXPECT_SYMBOL(std::get(result.at(3)).values.at(0).symbol, + "0"); + EXPECT_AS_STRING(std::get(result.at(3)).values.at(1).pointer, "/anyOf/1"); - EXPECT_SYMBOL(std::get(result.at(3)).values.at(1).symbol, "1"); - EXPECT_AS_STRING(std::get(result.at(3)).values.at(2).pointer, + EXPECT_SYMBOL(std::get(result.at(3)).values.at(1).symbol, + "1"); + EXPECT_AS_STRING(std::get(result.at(3)).values.at(2).pointer, "/anyOf/2"); - EXPECT_SYMBOL(std::get(result.at(3)).values.at(2).symbol, "2"); + EXPECT_SYMBOL(std::get(result.at(3)).values.at(2).symbol, + "2"); } -TEST(IR_2020_12, ref_recursive_to_root) { +TEST(Codegen_2020_12, ref_recursive_to_root) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", @@ -794,27 +836,32 @@ TEST(IR_2020_12, ref_recursive_to_root) { EXPECT_EQ(result.size(), 2); EXPECT_IR_REFERENCE(result, 0, "/properties/child", ""); - EXPECT_SYMBOL(std::get(result.at(0)).symbol, "child"); - EXPECT_SYMBOL(std::get(result.at(0)).target.symbol); - - EXPECT_TRUE(std::holds_alternative(result.at(1))); - EXPECT_AS_STRING(std::get(result.at(1)).pointer, ""); - EXPECT_SYMBOL(std::get(result.at(1)).symbol); - EXPECT_EQ(std::get(result.at(1)).members.size(), 1); - EXPECT_TRUE(std::get(result.at(1)).members.at(0).first == "child"); - EXPECT_FALSE(std::get(result.at(1)).members.at(0).second.required); - EXPECT_FALSE(std::get(result.at(1)).members.at(0).second.immutable); + EXPECT_SYMBOL(std::get(result.at(0)).symbol, "child"); + EXPECT_SYMBOL(std::get(result.at(0)).target.symbol); + + EXPECT_TRUE(std::holds_alternative(result.at(1))); + EXPECT_AS_STRING(std::get(result.at(1)).pointer, ""); + EXPECT_SYMBOL(std::get(result.at(1)).symbol); + EXPECT_EQ(std::get(result.at(1)).members.size(), 1); + EXPECT_TRUE(std::get(result.at(1)).members.at(0).first == + "child"); + EXPECT_FALSE( + std::get(result.at(1)).members.at(0).second.required); + EXPECT_FALSE( + std::get(result.at(1)).members.at(0).second.immutable); EXPECT_AS_STRING( - std::get(result.at(1)).members.at(0).second.pointer, + std::get(result.at(1)).members.at(0).second.pointer, "/properties/child"); - EXPECT_SYMBOL(std::get(result.at(1)).members.at(0).second.symbol, - "child"); + EXPECT_SYMBOL( + std::get(result.at(1)).members.at(0).second.symbol, + "child"); EXPECT_TRUE(std::holds_alternative( - std::get(result.at(1)).additional)); - EXPECT_TRUE(std::get(std::get(result.at(1)).additional)); + 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) { +TEST(Codegen_2020_12, nested_object_with_required_property) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", @@ -838,43 +885,54 @@ TEST(IR_2020_12, nested_object_with_required_property) { EXPECT_EQ(result.size(), 3); EXPECT_IR_SCALAR(result, 0, String, "/properties/nested/properties/name"); - EXPECT_SYMBOL(std::get(result.at(0)).symbol, "nested", "name"); + EXPECT_SYMBOL(std::get(result.at(0)).symbol, "nested", + "name"); - EXPECT_TRUE(std::holds_alternative(result.at(1))); - EXPECT_AS_STRING(std::get(result.at(1)).pointer, + EXPECT_TRUE(std::holds_alternative(result.at(1))); + EXPECT_AS_STRING(std::get(result.at(1)).pointer, "/properties/nested"); - EXPECT_SYMBOL(std::get(result.at(1)).symbol, "nested"); - EXPECT_EQ(std::get(result.at(1)).members.size(), 1); - EXPECT_TRUE(std::get(result.at(1)).members.at(0).first == "name"); - EXPECT_TRUE(std::get(result.at(1)).members.at(0).second.required); - EXPECT_FALSE(std::get(result.at(1)).members.at(0).second.immutable); + EXPECT_SYMBOL(std::get(result.at(1)).symbol, "nested"); + EXPECT_EQ(std::get(result.at(1)).members.size(), 1); + EXPECT_TRUE(std::get(result.at(1)).members.at(0).first == + "name"); + EXPECT_TRUE( + std::get(result.at(1)).members.at(0).second.required); + EXPECT_FALSE( + std::get(result.at(1)).members.at(0).second.immutable); EXPECT_AS_STRING( - std::get(result.at(1)).members.at(0).second.pointer, + std::get(result.at(1)).members.at(0).second.pointer, "/properties/nested/properties/name"); - EXPECT_SYMBOL(std::get(result.at(1)).members.at(0).second.symbol, - "nested", "name"); + EXPECT_SYMBOL( + std::get(result.at(1)).members.at(0).second.symbol, + "nested", "name"); 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, ""); - EXPECT_SYMBOL(std::get(result.at(2)).symbol); - EXPECT_EQ(std::get(result.at(2)).members.size(), 1); - EXPECT_TRUE(std::get(result.at(2)).members.at(0).first == "nested"); - EXPECT_FALSE(std::get(result.at(2)).members.at(0).second.required); - EXPECT_FALSE(std::get(result.at(2)).members.at(0).second.immutable); + 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, ""); + EXPECT_SYMBOL(std::get(result.at(2)).symbol); + EXPECT_EQ(std::get(result.at(2)).members.size(), 1); + EXPECT_TRUE(std::get(result.at(2)).members.at(0).first == + "nested"); + EXPECT_FALSE( + std::get(result.at(2)).members.at(0).second.required); + EXPECT_FALSE( + std::get(result.at(2)).members.at(0).second.immutable); EXPECT_AS_STRING( - std::get(result.at(2)).members.at(0).second.pointer, + std::get(result.at(2)).members.at(0).second.pointer, "/properties/nested"); - EXPECT_SYMBOL(std::get(result.at(2)).members.at(0).second.symbol, - "nested"); + EXPECT_SYMBOL( + std::get(result.at(2)).members.at(0).second.symbol, + "nested"); EXPECT_TRUE(std::holds_alternative( - std::get(result.at(2)).additional)); - EXPECT_TRUE(std::get(std::get(result.at(2)).additional)); + std::get(result.at(2)).additional)); + EXPECT_TRUE( + std::get(std::get(result.at(2)).additional)); } -TEST(IR_2020_12, array_without_items) { +TEST(Codegen_2020_12, array_without_items) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "array", @@ -891,17 +949,18 @@ TEST(IR_2020_12, array_without_items) { EXPECT_EQ(result.size(), 2); EXPECT_IR_ANY(result, 0, "/items"); - EXPECT_SYMBOL(std::get(result.at(0)).symbol, "items"); - - EXPECT_TRUE(std::holds_alternative(result.at(1))); - EXPECT_AS_STRING(std::get(result.at(1)).pointer, ""); - EXPECT_SYMBOL(std::get(result.at(1)).symbol); - EXPECT_TRUE(std::get(result.at(1)).items.has_value()); - EXPECT_AS_STRING(std::get(result.at(1)).items->pointer, "/items"); - EXPECT_SYMBOL(std::get(result.at(1)).items->symbol, "items"); + EXPECT_SYMBOL(std::get(result.at(0)).symbol, "items"); + + EXPECT_TRUE(std::holds_alternative(result.at(1))); + EXPECT_AS_STRING(std::get(result.at(1)).pointer, ""); + EXPECT_SYMBOL(std::get(result.at(1)).symbol); + EXPECT_TRUE(std::get(result.at(1)).items.has_value()); + EXPECT_AS_STRING(std::get(result.at(1)).items->pointer, + "/items"); + EXPECT_SYMBOL(std::get(result.at(1)).items->symbol, "items"); } -TEST(IR_2020_12, object_with_additional_properties_true) { +TEST(Codegen_2020_12, object_with_additional_properties_true) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", @@ -917,35 +976,42 @@ TEST(IR_2020_12, object_with_additional_properties_true) { using namespace sourcemeta::blaze; - // Note: The canonicalizer now keeps additionalProperties: true as IRAny - // instead of expanding it into a union of all types + // Note: The canonicalizer now keeps additionalProperties: true as + // CodegenIRAny instead of expanding it into a union of all types EXPECT_EQ(result.size(), 3); EXPECT_IR_SCALAR(result, 0, String, "/properties/name"); - EXPECT_SYMBOL(std::get(result.at(0)).symbol, "name"); + EXPECT_SYMBOL(std::get(result.at(0)).symbol, "name"); EXPECT_IR_ANY(result, 1, "/additionalProperties"); - EXPECT_SYMBOL(std::get(result.at(1)).symbol, "additionalProperties"); - - EXPECT_TRUE(std::holds_alternative(result.at(2))); - EXPECT_AS_STRING(std::get(result.at(2)).pointer, ""); - EXPECT_SYMBOL(std::get(result.at(2)).symbol); - EXPECT_EQ(std::get(result.at(2)).members.size(), 1); - EXPECT_TRUE(std::get(result.at(2)).members.at(0).first == "name"); - EXPECT_FALSE(std::get(result.at(2)).members.at(0).second.required); - EXPECT_FALSE(std::get(result.at(2)).members.at(0).second.immutable); + EXPECT_SYMBOL(std::get(result.at(1)).symbol, + "additionalProperties"); + + EXPECT_TRUE(std::holds_alternative(result.at(2))); + EXPECT_AS_STRING(std::get(result.at(2)).pointer, ""); + EXPECT_SYMBOL(std::get(result.at(2)).symbol); + EXPECT_EQ(std::get(result.at(2)).members.size(), 1); + EXPECT_TRUE(std::get(result.at(2)).members.at(0).first == + "name"); + EXPECT_FALSE( + std::get(result.at(2)).members.at(0).second.required); + EXPECT_FALSE( + std::get(result.at(2)).members.at(0).second.immutable); EXPECT_AS_STRING( - std::get(result.at(2)).members.at(0).second.pointer, + std::get(result.at(2)).members.at(0).second.pointer, "/properties/name"); - EXPECT_SYMBOL(std::get(result.at(2)).members.at(0).second.symbol, - "name"); + EXPECT_SYMBOL( + std::get(result.at(2)).members.at(0).second.symbol, + "name"); // When additionalProperties is a boolean schema, the object stores the - // boolean value directly (while the schema itself is compiled as IRAny) + // boolean value directly (while the schema itself is compiled as + // CodegenIRAny) EXPECT_TRUE(std::holds_alternative( - std::get(result.at(2)).additional)); - EXPECT_TRUE(std::get(std::get(result.at(2)).additional)); + std::get(result.at(2)).additional)); + EXPECT_TRUE( + std::get(std::get(result.at(2)).additional)); } -TEST(IR_2020_12, object_only_additional_properties) { +TEST(Codegen_2020_12, object_only_additional_properties) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", @@ -961,24 +1027,26 @@ TEST(IR_2020_12, object_only_additional_properties) { EXPECT_EQ(result.size(), 2); EXPECT_IR_SCALAR(result, 0, Boolean, "/additionalProperties"); - EXPECT_SYMBOL(std::get(result.at(0)).symbol, + EXPECT_SYMBOL(std::get(result.at(0)).symbol, "additionalProperties"); - EXPECT_TRUE(std::holds_alternative(result.at(1))); - EXPECT_AS_STRING(std::get(result.at(1)).pointer, ""); - EXPECT_SYMBOL(std::get(result.at(1)).symbol); - EXPECT_EQ(std::get(result.at(1)).members.size(), 0); - EXPECT_TRUE(std::holds_alternative( - std::get(result.at(1)).additional)); - EXPECT_AS_STRING( - std::get(std::get(result.at(1)).additional).pointer, - "/additionalProperties"); - EXPECT_SYMBOL( - std::get(std::get(result.at(1)).additional).symbol, - "additionalProperties"); + EXPECT_TRUE(std::holds_alternative(result.at(1))); + EXPECT_AS_STRING(std::get(result.at(1)).pointer, ""); + EXPECT_SYMBOL(std::get(result.at(1)).symbol); + EXPECT_EQ(std::get(result.at(1)).members.size(), 0); + EXPECT_TRUE(std::holds_alternative( + std::get(result.at(1)).additional)); + EXPECT_AS_STRING(std::get( + std::get(result.at(1)).additional) + .pointer, + "/additionalProperties"); + EXPECT_SYMBOL(std::get( + std::get(result.at(1)).additional) + .symbol, + "additionalProperties"); } -TEST(IR_2020_12, embedded_resource_with_nested_id_no_duplicates) { +TEST(Codegen_2020_12, embedded_resource_with_nested_id_no_duplicates) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://example.com/main", @@ -1010,52 +1078,62 @@ TEST(IR_2020_12, embedded_resource_with_nested_id_no_duplicates) { EXPECT_EQ(result.size(), 6); EXPECT_IR_REFERENCE(result, 0, "/properties/item", "/$defs/Item"); - EXPECT_SYMBOL(std::get(result.at(0)).symbol, "item"); - EXPECT_SYMBOL(std::get(result.at(0)).target.symbol, "Item"); + EXPECT_SYMBOL(std::get(result.at(0)).symbol, "item"); + EXPECT_SYMBOL(std::get(result.at(0)).target.symbol, + "Item"); EXPECT_IR_IMPOSSIBLE(result, 1, "/additionalProperties"); - EXPECT_SYMBOL(std::get(result.at(1)).symbol, + EXPECT_SYMBOL(std::get(result.at(1)).symbol, "additionalProperties"); EXPECT_IR_SCALAR(result, 2, String, "/$defs/Item/properties/name"); - EXPECT_SYMBOL(std::get(result.at(2)).symbol, "Item", "name"); + EXPECT_SYMBOL(std::get(result.at(2)).symbol, "Item", "name"); EXPECT_IR_IMPOSSIBLE(result, 3, "/$defs/Item/additionalProperties"); - EXPECT_SYMBOL(std::get(result.at(3)).symbol, "Item", + EXPECT_SYMBOL(std::get(result.at(3)).symbol, "Item", "additionalProperties"); - EXPECT_TRUE(std::holds_alternative(result.at(4))); - EXPECT_AS_STRING(std::get(result.at(4)).pointer, "/$defs/Item"); - EXPECT_SYMBOL(std::get(result.at(4)).symbol, "Item"); - EXPECT_EQ(std::get(result.at(4)).members.size(), 1); - EXPECT_EQ(std::get(result.at(4)).members.at(0).first, "name"); - EXPECT_TRUE(std::get(result.at(4)).members.at(0).second.required); + EXPECT_TRUE(std::holds_alternative(result.at(4))); + EXPECT_AS_STRING(std::get(result.at(4)).pointer, + "/$defs/Item"); + EXPECT_SYMBOL(std::get(result.at(4)).symbol, "Item"); + EXPECT_EQ(std::get(result.at(4)).members.size(), 1); + EXPECT_EQ(std::get(result.at(4)).members.at(0).first, + "name"); + EXPECT_TRUE( + std::get(result.at(4)).members.at(0).second.required); EXPECT_AS_STRING( - std::get(result.at(4)).members.at(0).second.pointer, + std::get(result.at(4)).members.at(0).second.pointer, "/$defs/Item/properties/name"); - EXPECT_SYMBOL(std::get(result.at(4)).members.at(0).second.symbol, - "Item", "name"); + EXPECT_SYMBOL( + std::get(result.at(4)).members.at(0).second.symbol, + "Item", "name"); 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, ""); - EXPECT_SYMBOL(std::get(result.at(5)).symbol); - EXPECT_EQ(std::get(result.at(5)).members.size(), 1); - EXPECT_EQ(std::get(result.at(5)).members.at(0).first, "item"); - EXPECT_TRUE(std::get(result.at(5)).members.at(0).second.required); + 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, ""); + EXPECT_SYMBOL(std::get(result.at(5)).symbol); + EXPECT_EQ(std::get(result.at(5)).members.size(), 1); + EXPECT_EQ(std::get(result.at(5)).members.at(0).first, + "item"); + EXPECT_TRUE( + std::get(result.at(5)).members.at(0).second.required); EXPECT_AS_STRING( - std::get(result.at(5)).members.at(0).second.pointer, + std::get(result.at(5)).members.at(0).second.pointer, "/properties/item"); - EXPECT_SYMBOL(std::get(result.at(5)).members.at(0).second.symbol, - "item"); + EXPECT_SYMBOL( + std::get(result.at(5)).members.at(0).second.symbol, + "item"); EXPECT_TRUE(std::holds_alternative( - std::get(result.at(5)).additional)); - EXPECT_FALSE(std::get(std::get(result.at(5)).additional)); + std::get(result.at(5)).additional)); + EXPECT_FALSE( + std::get(std::get(result.at(5)).additional)); } -TEST(IR_2020_12, boolean_true_schema) { +TEST(Codegen_2020_12, boolean_true_schema) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", @@ -1073,26 +1151,31 @@ TEST(IR_2020_12, boolean_true_schema) { EXPECT_EQ(result.size(), 2); EXPECT_IR_ANY(result, 0, "/properties/anything"); - EXPECT_SYMBOL(std::get(result.at(0)).symbol, "anything"); - - EXPECT_TRUE(std::holds_alternative(result.at(1))); - EXPECT_AS_STRING(std::get(result.at(1)).pointer, ""); - EXPECT_SYMBOL(std::get(result.at(1)).symbol); - EXPECT_EQ(std::get(result.at(1)).members.size(), 1); - EXPECT_EQ(std::get(result.at(1)).members.at(0).first, "anything"); - EXPECT_FALSE(std::get(result.at(1)).members.at(0).second.required); - EXPECT_FALSE(std::get(result.at(1)).members.at(0).second.immutable); + EXPECT_SYMBOL(std::get(result.at(0)).symbol, "anything"); + + EXPECT_TRUE(std::holds_alternative(result.at(1))); + EXPECT_AS_STRING(std::get(result.at(1)).pointer, ""); + EXPECT_SYMBOL(std::get(result.at(1)).symbol); + EXPECT_EQ(std::get(result.at(1)).members.size(), 1); + EXPECT_EQ(std::get(result.at(1)).members.at(0).first, + "anything"); + EXPECT_FALSE( + std::get(result.at(1)).members.at(0).second.required); + EXPECT_FALSE( + std::get(result.at(1)).members.at(0).second.immutable); EXPECT_AS_STRING( - std::get(result.at(1)).members.at(0).second.pointer, + std::get(result.at(1)).members.at(0).second.pointer, "/properties/anything"); - EXPECT_SYMBOL(std::get(result.at(1)).members.at(0).second.symbol, - "anything"); + EXPECT_SYMBOL( + std::get(result.at(1)).members.at(0).second.symbol, + "anything"); EXPECT_TRUE(std::holds_alternative( - std::get(result.at(1)).additional)); - EXPECT_TRUE(std::get(std::get(result.at(1)).additional)); + std::get(result.at(1)).additional)); + EXPECT_TRUE( + std::get(std::get(result.at(1)).additional)); } -TEST(IR_2020_12, boolean_false_schema) { +TEST(Codegen_2020_12, boolean_false_schema) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", @@ -1110,26 +1193,31 @@ TEST(IR_2020_12, boolean_false_schema) { EXPECT_EQ(result.size(), 2); EXPECT_IR_IMPOSSIBLE(result, 0, "/properties/nothing"); - EXPECT_SYMBOL(std::get(result.at(0)).symbol, "nothing"); - - EXPECT_TRUE(std::holds_alternative(result.at(1))); - EXPECT_AS_STRING(std::get(result.at(1)).pointer, ""); - EXPECT_SYMBOL(std::get(result.at(1)).symbol); - EXPECT_EQ(std::get(result.at(1)).members.size(), 1); - EXPECT_EQ(std::get(result.at(1)).members.at(0).first, "nothing"); - EXPECT_FALSE(std::get(result.at(1)).members.at(0).second.required); - EXPECT_FALSE(std::get(result.at(1)).members.at(0).second.immutable); + EXPECT_SYMBOL(std::get(result.at(0)).symbol, "nothing"); + + EXPECT_TRUE(std::holds_alternative(result.at(1))); + EXPECT_AS_STRING(std::get(result.at(1)).pointer, ""); + EXPECT_SYMBOL(std::get(result.at(1)).symbol); + EXPECT_EQ(std::get(result.at(1)).members.size(), 1); + EXPECT_EQ(std::get(result.at(1)).members.at(0).first, + "nothing"); + EXPECT_FALSE( + std::get(result.at(1)).members.at(0).second.required); + EXPECT_FALSE( + std::get(result.at(1)).members.at(0).second.immutable); EXPECT_AS_STRING( - std::get(result.at(1)).members.at(0).second.pointer, + std::get(result.at(1)).members.at(0).second.pointer, "/properties/nothing"); - EXPECT_SYMBOL(std::get(result.at(1)).members.at(0).second.symbol, - "nothing"); + EXPECT_SYMBOL( + std::get(result.at(1)).members.at(0).second.symbol, + "nothing"); EXPECT_TRUE(std::holds_alternative( - std::get(result.at(1)).additional)); - EXPECT_TRUE(std::get(std::get(result.at(1)).additional)); + std::get(result.at(1)).additional)); + EXPECT_TRUE( + std::get(std::get(result.at(1)).additional)); } -TEST(IR_2020_12, object_with_pattern_properties_prefix) { +TEST(Codegen_2020_12, object_with_pattern_properties_prefix) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", @@ -1150,12 +1238,12 @@ TEST(IR_2020_12, object_with_pattern_properties_prefix) { EXPECT_EQ(result.size(), 3); EXPECT_IR_SCALAR(result, 0, String, "/properties/name"); - EXPECT_SYMBOL(std::get(result.at(0)).symbol, "name"); + EXPECT_SYMBOL(std::get(result.at(0)).symbol, "name"); EXPECT_IR_SCALAR(result, 1, String, "/patternProperties/^x-"); - EXPECT_TRUE(std::holds_alternative(result.at(2))); - const auto &object{std::get(result.at(2))}; + EXPECT_TRUE(std::holds_alternative(result.at(2))); + const auto &object{std::get(result.at(2))}; EXPECT_AS_STRING(object.pointer, ""); EXPECT_EQ(object.members.size(), 1); EXPECT_EQ(object.members.at(0).first, "name"); @@ -1168,7 +1256,7 @@ TEST(IR_2020_12, object_with_pattern_properties_prefix) { EXPECT_TRUE(std::get(object.additional)); } -TEST(IR_2020_12, object_with_multiple_pattern_properties) { +TEST(Codegen_2020_12, object_with_multiple_pattern_properties) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", @@ -1185,14 +1273,14 @@ TEST(IR_2020_12, object_with_multiple_pattern_properties) { using namespace sourcemeta::blaze; ASSERT_FALSE(result.empty()); - EXPECT_TRUE(std::holds_alternative(result.back())); - const auto &object{std::get(result.back())}; + EXPECT_TRUE(std::holds_alternative(result.back())); + const auto &object{std::get(result.back())}; EXPECT_EQ(object.pattern.size(), 2); EXPECT_EQ(object.pattern.at(0).prefix, "x-"); EXPECT_EQ(object.pattern.at(1).prefix, "data-"); } -TEST(IR_2020_12, object_with_pattern_properties_and_additional_false) { +TEST(Codegen_2020_12, object_with_pattern_properties_and_additional_false) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", @@ -1212,8 +1300,8 @@ TEST(IR_2020_12, object_with_pattern_properties_and_additional_false) { using namespace sourcemeta::blaze; ASSERT_FALSE(result.empty()); - EXPECT_TRUE(std::holds_alternative(result.back())); - const auto &object{std::get(result.back())}; + EXPECT_TRUE(std::holds_alternative(result.back())); + const auto &object{std::get(result.back())}; EXPECT_EQ(object.members.size(), 1); EXPECT_EQ(object.pattern.size(), 1); EXPECT_EQ(object.pattern.at(0).prefix, "x-"); @@ -1221,7 +1309,7 @@ TEST(IR_2020_12, object_with_pattern_properties_and_additional_false) { EXPECT_FALSE(std::get(object.additional)); } -TEST(IR_2020_12, object_with_non_prefix_pattern_properties) { +TEST(Codegen_2020_12, object_with_non_prefix_pattern_properties) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", @@ -1237,13 +1325,13 @@ TEST(IR_2020_12, object_with_non_prefix_pattern_properties) { using namespace sourcemeta::blaze; ASSERT_FALSE(result.empty()); - EXPECT_TRUE(std::holds_alternative(result.back())); - const auto &object{std::get(result.back())}; + EXPECT_TRUE(std::holds_alternative(result.back())); + const auto &object{std::get(result.back())}; EXPECT_EQ(object.pattern.size(), 1); EXPECT_FALSE(object.pattern.at(0).prefix.has_value()); } -TEST(IR_2020_12, object_with_mixed_prefix_and_non_prefix_patterns) { +TEST(Codegen_2020_12, object_with_mixed_prefix_and_non_prefix_patterns) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", @@ -1260,15 +1348,15 @@ TEST(IR_2020_12, object_with_mixed_prefix_and_non_prefix_patterns) { using namespace sourcemeta::blaze; ASSERT_FALSE(result.empty()); - EXPECT_TRUE(std::holds_alternative(result.back())); - const auto &object{std::get(result.back())}; + EXPECT_TRUE(std::holds_alternative(result.back())); + const auto &object{std::get(result.back())}; EXPECT_EQ(object.pattern.size(), 2); EXPECT_TRUE(object.pattern.at(0).prefix.has_value()); EXPECT_EQ(object.pattern.at(0).prefix.value(), "x-"); EXPECT_FALSE(object.pattern.at(1).prefix.has_value()); } -TEST(IR_2020_12, dynamic_ref_single_anchor) { +TEST(Codegen_2020_12, dynamic_ref_single_anchor) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "array", @@ -1292,7 +1380,7 @@ TEST(IR_2020_12, dynamic_ref_single_anchor) { EXPECT_IR_SCALAR(result, 1, String, "/$defs/foo"); } -TEST(IR_2020_12, dynamic_ref_multiple_anchors) { +TEST(Codegen_2020_12, dynamic_ref_multiple_anchors) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://example.com/root", @@ -1325,16 +1413,16 @@ TEST(IR_2020_12, dynamic_ref_multiple_anchors) { ASSERT_EQ(result.size(), 6); EXPECT_IR_REFERENCE(result, 0, "/allOf/0", "/$defs/list"); EXPECT_IR_SCALAR(result, 1, String, "/$defs/stringItem"); - EXPECT_TRUE(std::holds_alternative(result.at(2))); - EXPECT_AS_STRING(std::get(result.at(2)).pointer, + EXPECT_TRUE(std::holds_alternative(result.at(2))); + EXPECT_AS_STRING(std::get(result.at(2)).pointer, "/$defs/list/items"); - EXPECT_EQ(std::get(result.at(2)).values.size(), 2); + EXPECT_EQ(std::get(result.at(2)).values.size(), 2); EXPECT_IR_SCALAR(result, 3, Number, "/$defs/list/$defs/defaultItem"); EXPECT_IR_ARRAY(result, 4, "/$defs/list", "/$defs/list/items"); EXPECT_IR_REFERENCE(result, 5, "", "/allOf/0"); } -TEST(IR_2020_12, dynamic_anchor_on_typed_schema) { +TEST(Codegen_2020_12, dynamic_anchor_on_typed_schema) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "$dynamicAnchor": "item", @@ -1351,7 +1439,7 @@ TEST(IR_2020_12, dynamic_anchor_on_typed_schema) { EXPECT_IR_SCALAR(result, 0, String, ""); } -TEST(IR_2020_12, allof_two_objects) { +TEST(Codegen_2020_12, allof_two_objects) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "allOf": [ @@ -1380,7 +1468,7 @@ TEST(IR_2020_12, allof_two_objects) { EXPECT_IR_INTERSECTION(result, 6, "", 2); } -TEST(IR_2020_12, allof_ref_and_object) { +TEST(Codegen_2020_12, allof_ref_and_object) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "allOf": [ @@ -1410,7 +1498,7 @@ TEST(IR_2020_12, allof_ref_and_object) { EXPECT_IR_INTERSECTION(result, result.size() - 1, "", 2); } -TEST(IR_2020_12, allof_single_element) { +TEST(Codegen_2020_12, allof_single_element) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "allOf": [ @@ -1429,7 +1517,7 @@ TEST(IR_2020_12, allof_single_element) { EXPECT_IR_SCALAR(result, 0, String, ""); } -TEST(IR_2020_12, allof_three_branches) { +TEST(Codegen_2020_12, allof_three_branches) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "allOf": [ @@ -1460,7 +1548,7 @@ TEST(IR_2020_12, allof_three_branches) { EXPECT_IR_INTERSECTION(result, result.size() - 1, "", 3); } -TEST(IR_2020_12, allof_with_defs) { +TEST(Codegen_2020_12, allof_with_defs) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "$defs": { @@ -1494,7 +1582,7 @@ TEST(IR_2020_12, allof_with_defs) { EXPECT_IR_REFERENCE(result, 1, "/allOf/0", "/$defs/Named"); } -TEST(IR_2020_12, if_then_else_distinct_object_branches) { +TEST(Codegen_2020_12, if_then_else_distinct_object_branches) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "if": { @@ -1523,7 +1611,7 @@ TEST(IR_2020_12, if_then_else_distinct_object_branches) { EXPECT_IR_CONDITIONAL(result, result.size() - 1, "", "/if", "/then", "/else"); } -TEST(IR_2020_12, if_then_else_implicit_else) { +TEST(Codegen_2020_12, if_then_else_implicit_else) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "if": { "type": "string" }, @@ -1539,7 +1627,7 @@ TEST(IR_2020_12, if_then_else_implicit_else) { EXPECT_IR_CONDITIONAL(result, result.size() - 1, "", "/if", "/then", "/else"); } -TEST(IR_2020_12, if_then_else_with_type_sibling) { +TEST(Codegen_2020_12, if_then_else_with_type_sibling) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "string", @@ -1559,7 +1647,7 @@ TEST(IR_2020_12, if_then_else_with_type_sibling) { EXPECT_IR_INTERSECTION(result, result.size() - 1, "", 2); } -TEST(IR_2020_12, if_then_else_with_ref_branches) { +TEST(Codegen_2020_12, if_then_else_with_ref_branches) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "$defs": { @@ -1597,7 +1685,7 @@ TEST(IR_2020_12, if_then_else_with_ref_branches) { EXPECT_IR_CONDITIONAL(result, 10, "", "/if", "/then", "/else"); } -TEST(IR_2020_12, if_then_else_nested_in_object_property) { +TEST(Codegen_2020_12, if_then_else_nested_in_object_property) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", diff --git a/test/codegen/codegen_symbol_test.cc b/test/codegen/codegen_symbol_test.cc index 3ed10a8fe..c55d8b4ea 100644 --- a/test/codegen/codegen_symbol_test.cc +++ b/test/codegen/codegen_symbol_test.cc @@ -4,7 +4,7 @@ #include -TEST(IR_Symbol, nested_additional_properties_items) { +TEST(Codegen_symbol, nested_additional_properties_items) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", @@ -39,7 +39,7 @@ TEST(IR_Symbol, nested_additional_properties_items) { EXPECT_EQ(result.at(2), "items"); } -TEST(IR_Symbol, inside_defs) { +TEST(Codegen_symbol, inside_defs) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "$defs": { @@ -70,7 +70,7 @@ TEST(IR_Symbol, inside_defs) { EXPECT_EQ(result.at(1), "name"); } -TEST(IR_Symbol, property_named_properties) { +TEST(Codegen_symbol, property_named_properties) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", @@ -96,7 +96,7 @@ TEST(IR_Symbol, property_named_properties) { EXPECT_EQ(result.at(0), "properties"); } -TEST(IR_Symbol, anyof_child) { +TEST(Codegen_symbol, anyof_child) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "anyOf": [ diff --git a/test/codegen/codegen_test.cc b/test/codegen/codegen_test.cc index d1ce489d6..8f7d3a5b0 100644 --- a/test/codegen/codegen_test.cc +++ b/test/codegen/codegen_test.cc @@ -3,7 +3,7 @@ #include #include -TEST(IR, unsupported_dialect_draft3) { +TEST(Codegen, unsupported_dialect_draft3) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "http://json-schema.org/draft-03/schema#", "type": "string" @@ -16,7 +16,7 @@ TEST(IR, unsupported_dialect_draft3) { sourcemeta::core::SchemaVocabularyError); } -TEST(IR, unsupported_keyword_error_not) { +TEST(Codegen, unsupported_keyword_error_not) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "not": { "type": "string" } @@ -26,10 +26,10 @@ TEST(IR, unsupported_keyword_error_not) { sourcemeta::core::schema_walker, sourcemeta::core::schema_resolver, sourcemeta::blaze::default_compiler), - sourcemeta::blaze::UnsupportedKeywordError); + sourcemeta::blaze::CodegenUnsupportedKeywordError); } -TEST(IR, unsupported_keyword_value_error_type_not_string) { +TEST(Codegen, unsupported_keyword_value_error_type_not_string) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": 123 @@ -39,10 +39,10 @@ TEST(IR, unsupported_keyword_value_error_type_not_string) { sourcemeta::core::schema_walker, sourcemeta::core::schema_resolver, sourcemeta::blaze::default_compiler), - sourcemeta::blaze::UnsupportedKeywordValueError); + sourcemeta::blaze::CodegenUnsupportedKeywordValueError); } -TEST(IR, unsupported_keyword_value_error_unknown_type) { +TEST(Codegen, unsupported_keyword_value_error_unknown_type) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "foo" @@ -52,5 +52,5 @@ TEST(IR, unsupported_keyword_value_error_unknown_type) { sourcemeta::core::schema_walker, sourcemeta::core::schema_resolver, sourcemeta::blaze::default_compiler), - sourcemeta::blaze::UnsupportedKeywordValueError); + sourcemeta::blaze::CodegenUnsupportedKeywordValueError); } diff --git a/test/codegen/codegen_test_utils.h b/test/codegen/codegen_test_utils.h index b3ff08265..24e9d95af 100644 --- a/test/codegen/codegen_test_utils.h +++ b/test/codegen/codegen_test_utils.h @@ -10,89 +10,98 @@ EXPECT_EQ(actual, (std::vector{__VA_ARGS__})) #define EXPECT_IR_SCALAR(result, index, scalar_type, expected_pointer) \ - EXPECT_TRUE( \ - std::holds_alternative(result.at(index))) \ - << "Expected IRScalar at index " << index; \ + EXPECT_TRUE(std::holds_alternative( \ + result.at(index))) \ + << "Expected CodegenIRScalar at index " << index; \ EXPECT_AS_STRING( \ - std::get(result.at(index)).pointer, \ + std::get(result.at(index)).pointer, \ expected_pointer); \ - EXPECT_EQ(std::get(result.at(index)).value, \ - sourcemeta::blaze::IRScalarType::scalar_type) + EXPECT_EQ( \ + std::get(result.at(index)).value, \ + sourcemeta::blaze::CodegenIRScalarType::scalar_type) #define EXPECT_IR_IMPOSSIBLE(result, index, expected_pointer) \ - EXPECT_TRUE(std::holds_alternative( \ + EXPECT_TRUE(std::holds_alternative( \ result.at(index))) \ - << "Expected IRImpossible at index " << index; \ + << "Expected CodegenIRImpossible at index " << index; \ EXPECT_AS_STRING( \ - std::get(result.at(index)).pointer, \ + std::get(result.at(index)) \ + .pointer, \ expected_pointer) #define EXPECT_IR_ANY(result, index, expected_pointer) \ - EXPECT_TRUE( \ - std::holds_alternative(result.at(index))) \ - << "Expected IRAny at index " << index; \ + EXPECT_TRUE(std::holds_alternative( \ + result.at(index))) \ + << "Expected CodegenIRAny at index " << index; \ EXPECT_AS_STRING( \ - std::get(result.at(index)).pointer, \ + std::get(result.at(index)).pointer, \ expected_pointer) #define EXPECT_IR_ARRAY(result, index, expected_pointer, \ expected_items_pointer) \ - EXPECT_TRUE( \ - std::holds_alternative(result.at(index))) \ - << "Expected IRArray at index " << index; \ + EXPECT_TRUE(std::holds_alternative( \ + result.at(index))) \ + << "Expected CodegenIRArray at index " << index; \ EXPECT_AS_STRING( \ - std::get(result.at(index)).pointer, \ + std::get(result.at(index)).pointer, \ expected_pointer); \ - EXPECT_TRUE(std::get(result.at(index)) \ + EXPECT_TRUE(std::get(result.at(index)) \ .items.has_value()) \ - << "Expected IRArray items to have a value"; \ + << "Expected CodegenIRArray items to have a value"; \ EXPECT_AS_STRING( \ - std::get(result.at(index)).items->pointer, \ + std::get(result.at(index)) \ + .items->pointer, \ expected_items_pointer) #define EXPECT_IR_INTERSECTION(result, index, expected_pointer, \ expected_count) \ - EXPECT_TRUE(std::holds_alternative( \ - result.at(index))) \ - << "Expected IRIntersection at index " << index; \ + EXPECT_TRUE( \ + std::holds_alternative( \ + result.at(index))) \ + << "Expected CodegenIRIntersection at index " << index; \ EXPECT_AS_STRING( \ - std::get(result.at(index)).pointer, \ + std::get(result.at(index)) \ + .pointer, \ expected_pointer); \ - EXPECT_EQ(std::get(result.at(index)) \ - .values.size(), \ - expected_count) + EXPECT_EQ( \ + std::get(result.at(index)) \ + .values.size(), \ + expected_count) #define EXPECT_IR_CONDITIONAL(result, index, expected_pointer, expected_if, \ expected_then, expected_else) \ - EXPECT_TRUE(std::holds_alternative( \ + EXPECT_TRUE(std::holds_alternative( \ result.at(index))) \ - << "Expected IRConditional at index " << index; \ + << "Expected CodegenIRConditional at index " << index; \ EXPECT_AS_STRING( \ - std::get(result.at(index)).pointer, \ + std::get(result.at(index)) \ + .pointer, \ expected_pointer); \ EXPECT_AS_STRING( \ - std::get(result.at(index)) \ + std::get(result.at(index)) \ .condition.pointer, \ expected_if); \ EXPECT_AS_STRING( \ - std::get(result.at(index)) \ + std::get(result.at(index)) \ .consequent.pointer, \ expected_then); \ EXPECT_AS_STRING( \ - std::get(result.at(index)) \ + std::get(result.at(index)) \ .alternative.pointer, \ expected_else) #define EXPECT_IR_REFERENCE(result, index, expected_pointer, \ expected_target_pointer) \ - EXPECT_TRUE(std::holds_alternative( \ + EXPECT_TRUE(std::holds_alternative( \ result.at(index))) \ - << "Expected IRReference at index " << index; \ + << "Expected CodegenIRReference at index " << index; \ EXPECT_AS_STRING( \ - std::get(result.at(index)).pointer, \ + std::get(result.at(index)) \ + .pointer, \ expected_pointer); \ - EXPECT_AS_STRING(std::get(result.at(index)) \ - .target.pointer, \ - expected_target_pointer) + EXPECT_AS_STRING( \ + std::get(result.at(index)) \ + .target.pointer, \ + expected_target_pointer) #endif diff --git a/test/codegen/e2e/typescript/CMakeLists.txt b/test/codegen/e2e/typescript/CMakeLists.txt index 54c3c446e..4a73ad29b 100644 --- a/test/codegen/e2e/typescript/CMakeLists.txt +++ b/test/codegen/e2e/typescript/CMakeLists.txt @@ -23,7 +23,7 @@ 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}" + add_test(NAME "blaze.codegen.e2e.typescript.tsc.${DIALECT_NAME}.${CASE_NAME}" COMMAND "${TSC_BIN}" --strict --noEmit "${CASE_DIRECTORY}/expected.d.ts" "${CASE_DIRECTORY}/test.ts") diff --git a/test/codegen/e2e/typescript/e2e.cc b/test/codegen/e2e/typescript/e2e.cc index cada8f1dd..8a2fceb42 100644 --- a/test/codegen/e2e/typescript/e2e.cc +++ b/test/codegen/e2e/typescript/e2e.cc @@ -69,8 +69,8 @@ auto main(int argc, char **argv) -> int { const auto case_name{case_entry.path().filename().string()}; const auto test_name{dialect_name + "/" + case_name}; - testing::RegisterTest("TypeScriptE2E", test_name.c_str(), nullptr, - nullptr, __FILE__, __LINE__, + testing::RegisterTest("Codegen_e2e_typescript", test_name.c_str(), + nullptr, nullptr, __FILE__, __LINE__, [=]() -> TypeScriptE2ETest * { return new TypeScriptE2ETest(case_entry.path()); }); From fd8f810de1220689b5778d2c14400a1f8375ea89 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Tue, 21 Apr 2026 10:54:04 -0400 Subject: [PATCH 3/5] No CLI Signed-off-by: Juan Cruz Viotti --- .github/workflows/ci.yml | 1 - Makefile | 3 --- package-lock.json | 26 ------------------- package.json | 1 - .../include/sourcemeta/blaze/codegen.h | 2 +- test/codegen/CMakeLists.txt | 7 ----- test/codegen/e2e/typescript/CMakeLists.txt | 3 --- 7 files changed, 1 insertion(+), 42 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4e46b0470..b1ed2ef53 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -127,7 +127,6 @@ jobs: -DCMAKE_COMPILE_WARNING_AS_ERROR:BOOL=ON ${{ matrix.platform.options }} - run: cmake --build ./build --config Release --target clang_format_test - - run: cmake --build ./build --config Release --target jsonschema_metaschema - run: cmake --build ./build --config Release --parallel 4 - run: > cmake --install ./build --prefix ./build/dist --config Release --verbose diff --git a/Makefile b/Makefile index 18ace8ed5..796037f85 100644 --- a/Makefile +++ b/Makefile @@ -43,9 +43,6 @@ test: .always benchmark: .always $(CMAKE) --build ./build --config $(PRESET) --target benchmark_all -lint: .always - $(CMAKE) --build ./build --config $(PRESET) --target jsonschema_metaschema - doxygen: .always $(CMAKE) --build ./build --config $(PRESET) --target doxygen diff --git a/package-lock.json b/package-lock.json index 761699c81..b2c332930 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,35 +8,9 @@ "name": "@sourcemeta/blaze", "version": "0.0.1", "devDependencies": { - "@sourcemeta/jsonschema": "^14.0.3", "typescript": "^5.9.3" } }, - "node_modules/@sourcemeta/jsonschema": { - "version": "14.0.3", - "resolved": "https://registry.npmjs.org/@sourcemeta/jsonschema/-/jsonschema-14.0.3.tgz", - "integrity": "sha512-5hUX6Avayk9Q0ma5k55CM7uxqVxTQZ7gcYrRXDXLBRz4efzr8cpck4zLn/5P1EupiOz3cgTY//alOI1W0LVM1A==", - "cpu": [ - "x64", - "arm64" - ], - "dev": true, - "license": "AGPL-3.0", - "os": [ - "darwin", - "linux", - "win32" - ], - "bin": { - "jsonschema": "npm/cli.js" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sourcemeta" - } - }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", diff --git a/package.json b/package.json index e2acefeea..a2dea343e 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,6 @@ "version": "0.0.1", "private": true, "devDependencies": { - "@sourcemeta/jsonschema": "^14.0.3", "typescript": "^5.9.3" } } diff --git a/src/codegen/include/sourcemeta/blaze/codegen.h b/src/codegen/include/sourcemeta/blaze/codegen.h index ff15f6812..db14db1c2 100644 --- a/src/codegen/include/sourcemeta/blaze/codegen.h +++ b/src/codegen/include/sourcemeta/blaze/codegen.h @@ -26,7 +26,7 @@ #include // std::vector /// @defgroup codegen Codegen -/// @brief The codegen JSON Schema code generation package +/// @brief A code generation utility built on top of Blaze namespace sourcemeta::blaze { diff --git a/test/codegen/CMakeLists.txt b/test/codegen/CMakeLists.txt index ed1d355b7..7054c7554 100644 --- a/test/codegen/CMakeLists.txt +++ b/test/codegen/CMakeLists.txt @@ -9,10 +9,3 @@ target_link_libraries(sourcemeta_blaze_codegen_unit if(BLAZE_CODEGEN) add_subdirectory(e2e/typescript) endif() - -if(E2E_SCHEMAS) - add_custom_target(jsonschema_metaschema - COMMAND "${PROJECT_SOURCE_DIR}/node_modules/.bin/jsonschema" - metaschema ${E2E_SCHEMAS} - VERBATIM) -endif() diff --git a/test/codegen/e2e/typescript/CMakeLists.txt b/test/codegen/e2e/typescript/CMakeLists.txt index 4a73ad29b..39841b0e6 100644 --- a/test/codegen/e2e/typescript/CMakeLists.txt +++ b/test/codegen/e2e/typescript/CMakeLists.txt @@ -13,7 +13,6 @@ else() set(TSC_BIN "${PROJECT_SOURCE_DIR}/node_modules/.bin/tsc") endif() -set(E2E_SCHEMAS "") file(GLOB TYPESCRIPT_DIALECT_DIRECTORIES "${CMAKE_CURRENT_SOURCE_DIR}/*") foreach(DIALECT_DIRECTORY ${TYPESCRIPT_DIALECT_DIRECTORIES}) if(IS_DIRECTORY ${DIALECT_DIRECTORY}) @@ -22,7 +21,6 @@ foreach(DIALECT_DIRECTORY ${TYPESCRIPT_DIALECT_DIRECTORIES}) foreach(CASE_DIRECTORY ${TYPESCRIPT_CASE_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 "blaze.codegen.e2e.typescript.tsc.${DIALECT_NAME}.${CASE_NAME}" COMMAND "${TSC_BIN}" --strict --noEmit "${CASE_DIRECTORY}/expected.d.ts" @@ -31,4 +29,3 @@ foreach(DIALECT_DIRECTORY ${TYPESCRIPT_DIALECT_DIRECTORIES}) endforeach() endif() endforeach() -set(E2E_SCHEMAS "${E2E_SCHEMAS}" PARENT_SCOPE) From 626b2aea302b8ae11572795914e579c09b3020fc Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Tue, 21 Apr 2026 10:56:04 -0400 Subject: [PATCH 4/5] No Node.js Signed-off-by: Juan Cruz Viotti --- .github/workflows/ci.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b1ed2ef53..c049866bd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -97,9 +97,6 @@ jobs: env: HOMEBREW_NO_ANALYTICS: 1 HOMEBREW_NO_AUTO_UPDATE: 1 - - uses: actions/setup-node@v4 - with: - node-version: 'latest' - run: npm ci - run: cmake --version - name: Configure Blaze (static) From 4ab2c85729d997721eec39094c58397d07d9692a Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Tue, 21 Apr 2026 11:35:15 -0400 Subject: [PATCH 5/5] Fix Signed-off-by: Juan Cruz Viotti --- .gitattributes | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.gitattributes b/.gitattributes index 5cc910b25..f461d4695 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1 @@ -* text=auto -*.cc text eol=lf -*.h text eol=lf -*.json text eol=lf -*.ts text eol=lf /vendor/** linguist-generated=true