diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a7717825e..c049866bd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -97,6 +97,7 @@ jobs: env: HOMEBREW_NO_ANALYTICS: 1 HOMEBREW_NO_AUTO_UPDATE: 1 + - run: npm ci - run: cmake --version - name: Configure Blaze (static) if: matrix.platform.type == 'static' 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..796037f85 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 \ 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..0590b4e17 --- /dev/null +++ b/contrib/typescript.cc @@ -0,0 +1,75 @@ +#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::CodegenUnsupportedKeywordError &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::CodegenUnsupportedKeywordValueError &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::CodegenUnexpectedSchemaError &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..b2c332930 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,29 @@ +{ + "name": "@sourcemeta/blaze", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@sourcemeta/blaze", + "version": "0.0.1", + "devDependencies": { + "typescript": "^5.9.3" + } + }, + "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..a2dea343e --- /dev/null +++ b/package.json @@ -0,0 +1,8 @@ +{ + "name": "@sourcemeta/blaze", + "version": "0.0.1", + "private": true, + "devDependencies": { + "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..03d309415 --- /dev/null +++ b/src/codegen/codegen.cc @@ -0,0 +1,138 @@ +#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) -> CodegenIRResult { + // -------------------------------------------------------------------------- + // (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; + CodegenIRResult 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 CodegenIREntity &left, const CodegenIREntity &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..c92041bb8 --- /dev/null +++ b/src/codegen/codegen_default_compiler.h @@ -0,0 +1,784 @@ +#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::CodegenUnsupportedKeywordError( \ + (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 &) -> CodegenIRImpossible { + return CodegenIRImpossible{ + {.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 &) -> CodegenIRAny { + return CodegenIRAny{ + {.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) -> CodegenIRScalar { + 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 CodegenIRScalar{ + {.pointer = sourcemeta::core::to_pointer(location.pointer), + .symbol = symbol(frame, location)}, + CodegenIRScalarType::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) -> CodegenIRObject { + 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()); + + CodegenIRObjectValue 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 = CodegenIRType{ + .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(CodegenIRObjectPatternProperty{ + {.pointer = std::move(pattern_pointer), + .symbol = symbol(frame, pattern_location.value().get())}, + std::move(prefix)}); + } + } + + 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, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + const sourcemeta::core::Vocabularies &, + const sourcemeta::core::SchemaResolver &, + 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 CodegenIRScalar{ + {.pointer = sourcemeta::core::to_pointer(location.pointer), + .symbol = symbol(frame, location)}, + CodegenIRScalarType::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) -> 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 CodegenIRScalar{ + {.pointer = sourcemeta::core::to_pointer(location.pointer), + .symbol = symbol(frame, location)}, + CodegenIRScalarType::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) -> CodegenIREntity { + 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 = CodegenIRType{ + .pointer = std::move(additional_pointer), + .symbol = symbol(frame, additional_location.value().get())}; + } + + return CodegenIRTuple{ + {.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 = CodegenIRType{ + .pointer = std::move(additional_pointer), + .symbol = symbol(frame, additional_location.value().get())}; + } + + 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}; + 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 = + CodegenIRType{.pointer = std::move(items_pointer), + .symbol = symbol(frame, items_location.value().get())}; + } + + 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, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + const sourcemeta::core::Vocabularies &, + const sourcemeta::core::SchemaResolver &, + const sourcemeta::core::JSON &subschema) -> CodegenIREntity { + 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 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 CodegenIRScalar{ + {.pointer = sourcemeta::core::to_pointer(location.pointer), + .symbol = symbol(frame, location)}, + CodegenIRScalarType::Boolean}; + } + } + + std::vector values{enum_json.as_array().cbegin(), + enum_json.as_array().cend()}; + return CodegenIREnumeration{ + {.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) -> CodegenIREntity { + 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 CodegenIRUnion{ + {.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) -> CodegenIREntity { + 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 CodegenIRUnion{ + {.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) -> CodegenIREntity { + 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 CodegenUnexpectedSchemaError( + schema, location.pointer, "Could not resolve reference destination"); + } + + const auto &target_location{target.value().get()}; + + return CodegenIRReference{ + {.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) + -> CodegenIREntity { + 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 CodegenUnexpectedSchemaError( + schema, location.pointer, "Could not resolve reference destination"); + } + + const auto &target_location{target.value().get()}; + + return CodegenIRReference{ + {.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 CodegenIRUnion{ + {.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) -> CodegenIREntity { + 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 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; + 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 CodegenIRIntersection{ + {.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) -> CodegenIREntity { + 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 CodegenIRConditional{ + {.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) + -> CodegenIREntity { + 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 CodegenUnsupportedKeywordValueError( + 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 CodegenUnsupportedKeywordValueError( + 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 CodegenUnsupportedKeywordError(schema, location.pointer, "not", + "Unsupported keyword in subschema"); + } else { + throw CodegenUnexpectedSchemaError(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..c632372c3 --- /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 CodegenIRScalar &entry) -> void { + this->output << "export type " + << mangle(this->prefix, entry.pointer, entry.symbol, this->cache) + << " = "; + + switch (entry.value) { + case CodegenIRScalarType::String: + this->output << "string"; + break; + case CodegenIRScalarType::Number: + case CodegenIRScalarType::Integer: + this->output << "number"; + break; + case CodegenIRScalarType::Boolean: + this->output << "boolean"; + break; + case CodegenIRScalarType::Null: + this->output << "null"; + break; + } + + this->output << ";\n"; +} + +auto TypeScript::operator()(const CodegenIREnumeration &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 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)}; + 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 CodegenIRImpossible &entry) -> void { + this->output << "export type " + << mangle(this->prefix, entry.pointer, entry.symbol, this->cache) + << " = never;\n"; +} + +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 CodegenIRArray &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 CodegenIRReference &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 CodegenIRTuple &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 CodegenIRUnion &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 CodegenIRIntersection &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 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 + 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..db14db1c2 --- /dev/null +++ b/src/codegen/include/sourcemeta/blaze/codegen.h @@ -0,0 +1,180 @@ +#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 A code generation utility built on top of Blaze + +namespace sourcemeta::blaze { + +/// @ingroup codegen +enum class CodegenIRScalarType : std::uint8_t { + String, + Number, + Integer, + Boolean, + Null +}; + +/// @ingroup codegen +struct CodegenIRType { + sourcemeta::core::Pointer pointer; + std::vector symbol; +}; + +/// @ingroup codegen +struct CodegenIRScalar : CodegenIRType { + CodegenIRScalarType value; +}; + +/// @ingroup codegen +struct CodegenIREnumeration : CodegenIRType { + std::vector values; +}; + +/// @ingroup codegen +struct CodegenIRUnion : CodegenIRType { + std::vector values; +}; + +/// @ingroup codegen +struct CodegenIRIntersection : CodegenIRType { + std::vector values; +}; + +/// @ingroup codegen +struct CodegenIRObjectValue : CodegenIRType { + bool required; + bool immutable; +}; + +/// @ingroup codegen +struct CodegenIRObjectPatternProperty : CodegenIRType { + std::optional prefix; +}; + +/// @ingroup codegen +struct CodegenIRObject : CodegenIRType { + // To preserve the user's ordering + std::vector> + members; + std::variant additional; + std::vector pattern; +}; + +/// @ingroup codegen +struct CodegenIRArray : CodegenIRType { + std::optional items; +}; + +/// @ingroup codegen +struct CodegenIRTuple : CodegenIRType { + std::vector items; + std::optional additional; +}; + +/// @ingroup codegen +struct CodegenIRImpossible : CodegenIRType {}; + +/// @ingroup codegen +struct CodegenIRAny : CodegenIRType {}; + +/// @ingroup codegen +struct CodegenIRConditional : CodegenIRType { + CodegenIRType condition; + CodegenIRType consequent; + CodegenIRType alternative; +}; + +/// @ingroup codegen +struct CodegenIRReference : CodegenIRType { + CodegenIRType target; +}; + +/// @ingroup codegen +using CodegenIREntity = + std::variant; + +/// @ingroup codegen +using CodegenIRResult = 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) + -> CodegenIREntity; + +/// @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 = "") -> CodegenIRResult; + +/// @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 CodegenIRResult &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..bb5fc76f7 --- /dev/null +++ b/src/codegen/include/sourcemeta/blaze/codegen_error.h @@ -0,0 +1,181 @@ +#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 CodegenUnsupportedKeywordError + : public std::exception { +public: + 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} {} + 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_; + } + + [[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 CodegenUnsupportedKeywordValueError + : public std::exception { +public: + 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} {} + 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_; + } + + [[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 CodegenUnexpectedSchemaError + : public std::exception { +public: + CodegenUnexpectedSchemaError(sourcemeta::core::JSON json, + sourcemeta::core::Pointer pointer, + const char *message) + : json_{std::move(json)}, pointer_{std::move(pointer)}, + message_{message} {} + 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_; + } + + [[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..cac0000f1 --- /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 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 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 +// 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..7054c7554 --- /dev/null +++ b/test/codegen/CMakeLists.txt @@ -0,0 +1,11 @@ +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() diff --git a/test/codegen/codegen_2020_12_test.cc b/test/codegen/codegen_2020_12_test.cc new file mode 100644 index 000000000..fa78f28c3 --- /dev/null +++ b/test/codegen/codegen_2020_12_test.cc @@ -0,0 +1,1714 @@ +#include + +#include + +#include "codegen_test_utils.h" + +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" + })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(Codegen_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(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", + "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(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" + })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(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" + })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(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", + "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(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", + "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(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 ] + })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(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 ] + })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(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 ] + })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(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" ] + })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(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 + })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(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" + })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(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 + })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(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 + })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(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" + })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(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", + "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(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", + "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(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", + "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(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", + "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(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", + "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(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", + "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(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", + "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(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", + "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(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": [ + { "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(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": [ + { "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(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": [ + { "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(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": [ + { "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(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", + "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(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", + "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(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", + "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(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", + "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 + // 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_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 + // CodegenIRAny) + EXPECT_TRUE(std::holds_alternative( + std::get(result.at(2)).additional)); + EXPECT_TRUE( + std::get(std::get(result.at(2)).additional)); +} + +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", + "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(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", + "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(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", + "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(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", + "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(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", + "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(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", + "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(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", + "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(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", + "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(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", + "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(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", + "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(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", + "$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(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", + "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(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": [ + { + "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(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": [ + { "$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(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": [ + { "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(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": [ + { + "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(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": { + "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(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": { + "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(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" }, + "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(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", + "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(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": { + "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(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", + "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..c55d8b4ea --- /dev/null +++ b/test/codegen/codegen_symbol_test.cc @@ -0,0 +1,121 @@ +#include + +#include + +#include + +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", + "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(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": { + "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(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", + "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(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": [ + { "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..8f7d3a5b0 --- /dev/null +++ b/test/codegen/codegen_test.cc @@ -0,0 +1,56 @@ +#include + +#include +#include + +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" + })JSON")}; + + EXPECT_THROW(sourcemeta::blaze::compile(schema, + sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, + sourcemeta::blaze::default_compiler), + sourcemeta::core::SchemaVocabularyError); +} + +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" } + })JSON")}; + + EXPECT_THROW(sourcemeta::blaze::compile(schema, + sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, + sourcemeta::blaze::default_compiler), + sourcemeta::blaze::CodegenUnsupportedKeywordError); +} + +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 + })JSON")}; + + EXPECT_THROW(sourcemeta::blaze::compile(schema, + sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, + sourcemeta::blaze::default_compiler), + sourcemeta::blaze::CodegenUnsupportedKeywordValueError); +} + +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" + })JSON")}; + + EXPECT_THROW(sourcemeta::blaze::compile(schema, + sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, + sourcemeta::blaze::default_compiler), + sourcemeta::blaze::CodegenUnsupportedKeywordValueError); +} diff --git a/test/codegen/codegen_test_utils.h b/test/codegen/codegen_test_utils.h new file mode 100644 index 000000000..24e9d95af --- /dev/null +++ b/test/codegen/codegen_test_utils.h @@ -0,0 +1,107 @@ +#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 CodegenIRScalar at index " << index; \ + EXPECT_AS_STRING( \ + std::get(result.at(index)).pointer, \ + expected_pointer); \ + 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( \ + result.at(index))) \ + << "Expected CodegenIRImpossible 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 CodegenIRAny 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 CodegenIRArray 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 CodegenIRArray 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 CodegenIRIntersection 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 CodegenIRConditional 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 CodegenIRReference 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..39841b0e6 --- /dev/null +++ b/test/codegen/e2e/typescript/CMakeLists.txt @@ -0,0 +1,31 @@ +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() + +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) + 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") + endif() + endforeach() + endif() +endforeach() diff --git a/test/codegen/e2e/typescript/e2e.cc b/test/codegen/e2e/typescript/e2e.cc new file mode 100644 index 000000000..8a2fceb42 --- /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("Codegen_e2e_typescript", 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