Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
32 changes: 32 additions & 0 deletions src/ir/ir_default_compiler.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<IRType> 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,
Expand Down Expand Up @@ -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);
Expand Down
25 changes: 25 additions & 0 deletions test/e2e/typescript/2020-12/oneof_union/expected.d.ts
Original file line number Diff line number Diff line change
@@ -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;
}
3 changes: 3 additions & 0 deletions test/e2e/typescript/2020-12/oneof_union/options.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"defaultPrefix": "OneOfTest"
}
21 changes: 21 additions & 0 deletions test/e2e/typescript/2020-12/oneof_union/schema.json
Original file line number Diff line number Diff line change
@@ -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
}
53 changes: 53 additions & 0 deletions test/e2e/typescript/2020-12/oneof_union/test.ts
Original file line number Diff line number Diff line change
@@ -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"
};
64 changes: 64 additions & 0 deletions test/ir/ir_2020_12_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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<IRUnion>(result.at(2)));
EXPECT_AS_STRING(std::get<IRUnion>(result.at(2)).pointer, "");
EXPECT_EQ(std::get<IRUnion>(result.at(2)).values.size(), 2);
EXPECT_AS_STRING(std::get<IRUnion>(result.at(2)).values.at(0).pointer,
"/oneOf/0");
EXPECT_AS_STRING(std::get<IRUnion>(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<IRUnion>(result.at(3)));
EXPECT_AS_STRING(std::get<IRUnion>(result.at(3)).pointer, "");
EXPECT_EQ(std::get<IRUnion>(result.at(3)).values.size(), 3);
EXPECT_AS_STRING(std::get<IRUnion>(result.at(3)).values.at(0).pointer,
"/oneOf/0");
EXPECT_AS_STRING(std::get<IRUnion>(result.at(3)).values.at(1).pointer,
"/oneOf/1");
EXPECT_AS_STRING(std::get<IRUnion>(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",
Expand Down