Skip to content

Commit 49b4b6c

Browse files
authored
Compile oneOf as a type union as a good enough approximation (#9)
Signed-off-by: Juan Cruz Viotti <jv@jviotti.com>
1 parent edb5a44 commit 49b4b6c

7 files changed

Lines changed: 199 additions & 1 deletion

File tree

README.markdown

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ to use a JSON Schema validator at runtime to enforce remaining constraints.
3737
| Applicator (2020-12) | `dependentSchemas` | Pending |
3838
| Applicator (2020-12) | `contains` | Ignored |
3939
| Applicator (2020-12) | `allOf` | Pending |
40-
| Applicator (2020-12) | `oneOf` | **CANNOT SUPPORT** |
40+
| Applicator (2020-12) | `oneOf` | **PARTIAL GIVEN LANGUAGE LIMITATIONS** |
4141
| Applicator (2020-12) | `not` | **CANNOT SUPPORT** |
4242
| Applicator (2020-12) | `if` | Pending |
4343
| Applicator (2020-12) | `then` | Pending |

src/ir/ir_default_compiler.h

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,33 @@ auto handle_anyof(const sourcemeta::core::JSON &schema,
285285
std::move(branches)};
286286
}
287287

288+
auto handle_oneof(const sourcemeta::core::JSON &schema,
289+
const sourcemeta::core::SchemaFrame &,
290+
const sourcemeta::core::SchemaFrame::Location &location,
291+
const sourcemeta::core::Vocabularies &,
292+
const sourcemeta::core::SchemaResolver &,
293+
const sourcemeta::core::JSON &subschema) -> IREntity {
294+
ONLY_WHITELIST_KEYWORDS(
295+
schema, subschema, location.pointer,
296+
{"$schema", "$id", "$anchor", "$defs", "$vocabulary", "oneOf"});
297+
298+
const auto &one_of{subschema.at("oneOf")};
299+
assert(one_of.is_array());
300+
assert(one_of.size() >= 2);
301+
302+
std::vector<IRType> branches;
303+
for (std::size_t index = 0; index < one_of.size(); ++index) {
304+
auto branch_pointer{sourcemeta::core::to_pointer(location.pointer)};
305+
branch_pointer.push_back("oneOf");
306+
branch_pointer.push_back(index);
307+
308+
branches.push_back({.pointer = std::move(branch_pointer)});
309+
}
310+
311+
return IRUnion{{.pointer = sourcemeta::core::to_pointer(location.pointer)},
312+
std::move(branches)};
313+
}
314+
288315
auto handle_ref(const sourcemeta::core::JSON &schema,
289316
const sourcemeta::core::SchemaFrame &frame,
290317
const sourcemeta::core::SchemaFrame::Location &location,
@@ -391,6 +418,11 @@ auto default_compiler(const sourcemeta::core::JSON &schema,
391418
} else if (subschema.defines("anyOf")) {
392419
return handle_anyof(schema, frame, location, vocabularies, resolver,
393420
subschema);
421+
// This is usually a good enough approximation. We usually can't check that
422+
// the other types DO NOT match, but that is in a way a validation concern
423+
} else if (subschema.defines("oneOf")) {
424+
return handle_oneof(schema, frame, location, vocabularies, resolver,
425+
subschema);
394426
} else if (subschema.defines("$ref")) {
395427
return handle_ref(schema, frame, location, vocabularies, resolver,
396428
subschema);
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
export type OneOfTest_Properties_Value_OneOf_ZIndex2 = boolean;
2+
3+
export type OneOfTest_Properties_Value_OneOf_ZIndex1 = number;
4+
5+
export type OneOfTest_Properties_Value_OneOf_ZIndex0 = string;
6+
7+
export type OneOfTest_Properties_Value =
8+
OneOfTest_Properties_Value_OneOf_ZIndex0 |
9+
OneOfTest_Properties_Value_OneOf_ZIndex1 |
10+
OneOfTest_Properties_Value_OneOf_ZIndex2;
11+
12+
export type OneOfTest_Properties_Status_OneOf_ZIndex1 = "completed" | "cancelled";
13+
14+
export type OneOfTest_Properties_Status_OneOf_ZIndex0 = "pending" | "active";
15+
16+
export type OneOfTest_Properties_Status =
17+
OneOfTest_Properties_Status_OneOf_ZIndex0 |
18+
OneOfTest_Properties_Status_OneOf_ZIndex1;
19+
20+
export type OneOfTest_AdditionalProperties = never;
21+
22+
export interface OneOfTest {
23+
"value": OneOfTest_Properties_Value;
24+
"status"?: OneOfTest_Properties_Status;
25+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"defaultPrefix": "OneOfTest"
3+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"$schema": "https://json-schema.org/draft/2020-12/schema",
3+
"type": "object",
4+
"properties": {
5+
"value": {
6+
"oneOf": [
7+
{ "type": "string" },
8+
{ "type": "integer" },
9+
{ "type": "boolean" }
10+
]
11+
},
12+
"status": {
13+
"oneOf": [
14+
{ "enum": [ "pending", "active" ] },
15+
{ "enum": [ "completed", "cancelled" ] }
16+
]
17+
}
18+
},
19+
"required": [ "value" ],
20+
"additionalProperties": false
21+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { OneOfTest } from "./expected";
2+
3+
// Valid: value as string
4+
const withString: OneOfTest = {
5+
value: "hello"
6+
};
7+
8+
// Valid: value as integer
9+
const withInteger: OneOfTest = {
10+
value: 42
11+
};
12+
13+
// Valid: value as boolean
14+
const withBoolean: OneOfTest = {
15+
value: true
16+
};
17+
18+
// Valid: with status from first enum
19+
const withPendingStatus: OneOfTest = {
20+
value: "test",
21+
status: "pending"
22+
};
23+
24+
// Valid: with status from second enum
25+
const withCompletedStatus: OneOfTest = {
26+
value: 123,
27+
status: "completed"
28+
};
29+
30+
// Invalid: value cannot be null
31+
const invalidNull: OneOfTest = {
32+
// @ts-expect-error
33+
value: null
34+
};
35+
36+
// Invalid: value cannot be object
37+
const invalidObject: OneOfTest = {
38+
// @ts-expect-error
39+
value: { foo: "bar" }
40+
};
41+
42+
// Invalid: status must be one of the allowed values
43+
const invalidStatus: OneOfTest = {
44+
value: "test",
45+
// @ts-expect-error
46+
status: "unknown"
47+
};
48+
49+
// Invalid: missing required value
50+
// @ts-expect-error
51+
const missingValue: OneOfTest = {
52+
status: "active"
53+
};

test/ir/ir_2020_12_test.cc

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,70 @@ TEST(IR_2020_12, anyof_three_branches) {
635635
"/anyOf/2");
636636
}
637637

638+
TEST(IR_2020_12, oneof_two_branches) {
639+
const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({
640+
"$schema": "https://json-schema.org/draft/2020-12/schema",
641+
"oneOf": [
642+
{ "type": "string" },
643+
{ "type": "integer" }
644+
]
645+
})JSON")};
646+
647+
const auto result{
648+
sourcemeta::codegen::compile(schema, sourcemeta::core::schema_walker,
649+
sourcemeta::core::schema_resolver,
650+
sourcemeta::codegen::default_compiler)};
651+
652+
using namespace sourcemeta::codegen;
653+
654+
EXPECT_EQ(result.size(), 3);
655+
656+
EXPECT_IR_SCALAR(result, 0, Integer, "/oneOf/1");
657+
EXPECT_IR_SCALAR(result, 1, String, "/oneOf/0");
658+
659+
EXPECT_TRUE(std::holds_alternative<IRUnion>(result.at(2)));
660+
EXPECT_AS_STRING(std::get<IRUnion>(result.at(2)).pointer, "");
661+
EXPECT_EQ(std::get<IRUnion>(result.at(2)).values.size(), 2);
662+
EXPECT_AS_STRING(std::get<IRUnion>(result.at(2)).values.at(0).pointer,
663+
"/oneOf/0");
664+
EXPECT_AS_STRING(std::get<IRUnion>(result.at(2)).values.at(1).pointer,
665+
"/oneOf/1");
666+
}
667+
668+
TEST(IR_2020_12, oneof_three_branches) {
669+
const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({
670+
"$schema": "https://json-schema.org/draft/2020-12/schema",
671+
"oneOf": [
672+
{ "type": "string" },
673+
{ "type": "integer" },
674+
{ "type": "boolean" }
675+
]
676+
})JSON")};
677+
678+
const auto result{
679+
sourcemeta::codegen::compile(schema, sourcemeta::core::schema_walker,
680+
sourcemeta::core::schema_resolver,
681+
sourcemeta::codegen::default_compiler)};
682+
683+
using namespace sourcemeta::codegen;
684+
685+
EXPECT_EQ(result.size(), 4);
686+
687+
EXPECT_IR_SCALAR(result, 0, Boolean, "/oneOf/2");
688+
EXPECT_IR_SCALAR(result, 1, Integer, "/oneOf/1");
689+
EXPECT_IR_SCALAR(result, 2, String, "/oneOf/0");
690+
691+
EXPECT_TRUE(std::holds_alternative<IRUnion>(result.at(3)));
692+
EXPECT_AS_STRING(std::get<IRUnion>(result.at(3)).pointer, "");
693+
EXPECT_EQ(std::get<IRUnion>(result.at(3)).values.size(), 3);
694+
EXPECT_AS_STRING(std::get<IRUnion>(result.at(3)).values.at(0).pointer,
695+
"/oneOf/0");
696+
EXPECT_AS_STRING(std::get<IRUnion>(result.at(3)).values.at(1).pointer,
697+
"/oneOf/1");
698+
EXPECT_AS_STRING(std::get<IRUnion>(result.at(3)).values.at(2).pointer,
699+
"/oneOf/2");
700+
}
701+
638702
TEST(IR_2020_12, ref_recursive_to_root) {
639703
const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({
640704
"$schema": "https://json-schema.org/draft/2020-12/schema",

0 commit comments

Comments
 (0)