diff --git a/README.markdown b/README.markdown index 85f3471..b348b79 100644 --- a/README.markdown +++ b/README.markdown @@ -37,7 +37,7 @@ to use a JSON Schema validator at runtime to enforce remaining constraints. | Applicator (2020-12) | `dependentSchemas` | Pending | | Applicator (2020-12) | `contains` | Ignored | | Applicator (2020-12) | `allOf` | Pending | -| Applicator (2020-12) | `oneOf` | **CANNOT SUPPORT** | +| Applicator (2020-12) | `oneOf` | **PARTIAL GIVEN LANGUAGE LIMITATIONS** | | Applicator (2020-12) | `not` | **CANNOT SUPPORT** | | Applicator (2020-12) | `if` | Pending | | Applicator (2020-12) | `then` | Pending | diff --git a/src/ir/ir_default_compiler.h b/src/ir/ir_default_compiler.h index b69d97b..0716384 100644 --- a/src/ir/ir_default_compiler.h +++ b/src/ir/ir_default_compiler.h @@ -285,6 +285,33 @@ auto handle_anyof(const sourcemeta::core::JSON &schema, std::move(branches)}; } +auto handle_oneof(const sourcemeta::core::JSON &schema, + const sourcemeta::core::SchemaFrame &, + const sourcemeta::core::SchemaFrame::Location &location, + const sourcemeta::core::Vocabularies &, + const sourcemeta::core::SchemaResolver &, + const sourcemeta::core::JSON &subschema) -> IREntity { + ONLY_WHITELIST_KEYWORDS( + schema, subschema, location.pointer, + {"$schema", "$id", "$anchor", "$defs", "$vocabulary", "oneOf"}); + + 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); + + branches.push_back({.pointer = std::move(branch_pointer)}); + } + + return IRUnion{{.pointer = sourcemeta::core::to_pointer(location.pointer)}, + std::move(branches)}; +} + auto handle_ref(const sourcemeta::core::JSON &schema, const sourcemeta::core::SchemaFrame &frame, const sourcemeta::core::SchemaFrame::Location &location, @@ -391,6 +418,11 @@ auto default_compiler(const sourcemeta::core::JSON &schema, } 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("$ref")) { return handle_ref(schema, frame, location, vocabularies, resolver, subschema); diff --git a/test/e2e/typescript/2020-12/oneof_union/expected.d.ts b/test/e2e/typescript/2020-12/oneof_union/expected.d.ts new file mode 100644 index 0000000..5d0798c --- /dev/null +++ b/test/e2e/typescript/2020-12/oneof_union/expected.d.ts @@ -0,0 +1,25 @@ +export type OneOfTest_Properties_Value_OneOf_ZIndex2 = boolean; + +export type OneOfTest_Properties_Value_OneOf_ZIndex1 = number; + +export type OneOfTest_Properties_Value_OneOf_ZIndex0 = string; + +export type OneOfTest_Properties_Value = + OneOfTest_Properties_Value_OneOf_ZIndex0 | + OneOfTest_Properties_Value_OneOf_ZIndex1 | + OneOfTest_Properties_Value_OneOf_ZIndex2; + +export type OneOfTest_Properties_Status_OneOf_ZIndex1 = "completed" | "cancelled"; + +export type OneOfTest_Properties_Status_OneOf_ZIndex0 = "pending" | "active"; + +export type OneOfTest_Properties_Status = + OneOfTest_Properties_Status_OneOf_ZIndex0 | + OneOfTest_Properties_Status_OneOf_ZIndex1; + +export type OneOfTest_AdditionalProperties = never; + +export interface OneOfTest { + "value": OneOfTest_Properties_Value; + "status"?: OneOfTest_Properties_Status; +} diff --git a/test/e2e/typescript/2020-12/oneof_union/options.json b/test/e2e/typescript/2020-12/oneof_union/options.json new file mode 100644 index 0000000..343a43e --- /dev/null +++ b/test/e2e/typescript/2020-12/oneof_union/options.json @@ -0,0 +1,3 @@ +{ + "defaultPrefix": "OneOfTest" +} diff --git a/test/e2e/typescript/2020-12/oneof_union/schema.json b/test/e2e/typescript/2020-12/oneof_union/schema.json new file mode 100644 index 0000000..0223b06 --- /dev/null +++ b/test/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/e2e/typescript/2020-12/oneof_union/test.ts b/test/e2e/typescript/2020-12/oneof_union/test.ts new file mode 100644 index 0000000..ae77941 --- /dev/null +++ b/test/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/ir/ir_2020_12_test.cc b/test/ir/ir_2020_12_test.cc index 3e19e03..435c24f 100644 --- a/test/ir/ir_2020_12_test.cc +++ b/test/ir/ir_2020_12_test.cc @@ -635,6 +635,70 @@ TEST(IR_2020_12, anyof_three_branches) { "/anyOf/2"); } +TEST(IR_2020_12, oneof_two_branches) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "oneOf": [ + { "type": "string" }, + { "type": "integer" } + ] + })JSON")}; + + const auto result{ + sourcemeta::codegen::compile(schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, + sourcemeta::codegen::default_compiler)}; + + using namespace sourcemeta::codegen; + + EXPECT_EQ(result.size(), 3); + + EXPECT_IR_SCALAR(result, 0, Integer, "/oneOf/1"); + EXPECT_IR_SCALAR(result, 1, String, "/oneOf/0"); + + EXPECT_TRUE(std::holds_alternative(result.at(2))); + EXPECT_AS_STRING(std::get(result.at(2)).pointer, ""); + EXPECT_EQ(std::get(result.at(2)).values.size(), 2); + EXPECT_AS_STRING(std::get(result.at(2)).values.at(0).pointer, + "/oneOf/0"); + EXPECT_AS_STRING(std::get(result.at(2)).values.at(1).pointer, + "/oneOf/1"); +} + +TEST(IR_2020_12, oneof_three_branches) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "oneOf": [ + { "type": "string" }, + { "type": "integer" }, + { "type": "boolean" } + ] + })JSON")}; + + const auto result{ + sourcemeta::codegen::compile(schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, + sourcemeta::codegen::default_compiler)}; + + using namespace sourcemeta::codegen; + + EXPECT_EQ(result.size(), 4); + + EXPECT_IR_SCALAR(result, 0, Boolean, "/oneOf/2"); + EXPECT_IR_SCALAR(result, 1, Integer, "/oneOf/1"); + EXPECT_IR_SCALAR(result, 2, String, "/oneOf/0"); + + EXPECT_TRUE(std::holds_alternative(result.at(3))); + EXPECT_AS_STRING(std::get(result.at(3)).pointer, ""); + EXPECT_EQ(std::get(result.at(3)).values.size(), 3); + EXPECT_AS_STRING(std::get(result.at(3)).values.at(0).pointer, + "/oneOf/0"); + EXPECT_AS_STRING(std::get(result.at(3)).values.at(1).pointer, + "/oneOf/1"); + EXPECT_AS_STRING(std::get(result.at(3)).values.at(2).pointer, + "/oneOf/2"); +} + TEST(IR_2020_12, ref_recursive_to_root) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema",