Skip to content

Commit 0fc9218

Browse files
authored
Fix additionalProperties handling (#7)
Signed-off-by: Juan Cruz Viotti <jv@jviotti.com>
1 parent 0e9fa86 commit 0fc9218

26 files changed

Lines changed: 439 additions & 605 deletions

File tree

src/generator/typescript.cc

Lines changed: 14 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -91,11 +91,7 @@ auto TypeScript::operator()(const IRObject &entry) const -> void {
9191
return;
9292
}
9393

94-
if (has_additional) {
95-
this->output << "export type " << type_name << " = {\n";
96-
} else {
97-
this->output << "export interface " << type_name << " {\n";
98-
}
94+
this->output << "export interface " << type_name << " {\n";
9995

10096
// We always quote property names for safety. JSON Schema allows any string
10197
// as a property name, but unquoted TypeScript/ECMAScript property names
@@ -115,32 +111,24 @@ auto TypeScript::operator()(const IRObject &entry) const -> void {
115111
}
116112

117113
if (has_additional) {
118-
this->output << "} & {\n";
119-
120-
// While we could do this with the more idiomatic `Record<Exclude<string,
121-
// X>, Y>`, we choose the more verbose manner in order to allow users to
122-
// declare a type called `Record`
123-
this->output << " [K in string as K extends\n";
124-
125-
std::size_t index{0};
114+
// TypeScript index signatures must be a supertype of all property value
115+
// types. We use a union of all member types plus the additional properties
116+
// type plus undefined (for optional properties).
117+
this->output << " [key: string]:\n";
126118
for (const auto &[member_name, member_value] : entry.members) {
127-
const auto is_last{index == entry.members.size() - 1};
128-
this->output << " \"" << escape_string(member_name) << "\"";
129-
if (!is_last) {
130-
this->output << " |";
131-
}
132-
this->output << "\n";
133-
++index;
119+
this->output << " "
120+
<< sourcemeta::core::mangle(member_value.pointer,
121+
this->prefix)
122+
<< " |\n";
134123
}
135-
136-
this->output << " ? never : K]: "
124+
this->output << " "
137125
<< sourcemeta::core::mangle(entry.additional->pointer,
138126
this->prefix)
139-
<< ";\n";
140-
this->output << "};\n";
141-
} else {
142-
this->output << "}\n";
127+
<< " |\n";
128+
this->output << " undefined;\n";
143129
}
130+
131+
this->output << "}\n";
144132
}
145133

146134
auto TypeScript::operator()(const IRImpossible &entry) const -> void {

src/ir/include/sourcemeta/codegen/ir.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ struct IRObjectValue : IRType {
6464
struct IRObject : IRType {
6565
// To preserve the user's ordering
6666
std::vector<std::pair<sourcemeta::core::JSON::String, IRObjectValue>> members;
67-
std::optional<IRObjectValue> additional;
67+
std::optional<IRType> additional;
6868
};
6969

7070
/// @ingroup ir

src/ir/ir_default_compiler.h

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -96,13 +96,16 @@ auto handle_object(const sourcemeta::core::JSON &schema,
9696
members.emplace_back(entry.first, std::move(member_value));
9797
}
9898

99-
std::optional<IRObjectValue> additional{std::nullopt};
99+
std::optional<IRType> additional{std::nullopt};
100100
if (subschema.defines("additionalProperties")) {
101-
auto additional_pointer{sourcemeta::core::to_pointer(location.pointer)};
102-
additional_pointer.push_back("additionalProperties");
103-
104-
additional =
105-
IRObjectValue{{.pointer = std::move(additional_pointer)}, false, false};
101+
const auto &additional_schema{subschema.at("additionalProperties")};
102+
const auto is_false{additional_schema.is_boolean() &&
103+
!additional_schema.to_boolean()};
104+
if (!is_false) {
105+
auto additional_pointer{sourcemeta::core::to_pointer(location.pointer)};
106+
additional_pointer.push_back("additionalProperties");
107+
additional = IRType{.pointer = std::move(additional_pointer)};
108+
}
106109
}
107110

108111
return IRObject{{.pointer = sourcemeta::core::to_pointer(location.pointer)},
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export type StrictPerson_Properties_Name = string;
2+
3+
export type StrictPerson_Properties_Age = number;
4+
5+
export type StrictPerson_AdditionalProperties = never;
6+
7+
export interface StrictPerson {
8+
"name": StrictPerson_Properties_Name;
9+
"age"?: StrictPerson_Properties_Age;
10+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"defaultPrefix": "StrictPerson"
3+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"$schema": "https://json-schema.org/draft/2020-12/schema",
3+
"type": "object",
4+
"properties": {
5+
"name": {
6+
"type": "string"
7+
},
8+
"age": {
9+
"type": "integer"
10+
}
11+
},
12+
"required": [ "name" ],
13+
"additionalProperties": false
14+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { StrictPerson } from "./expected";
2+
3+
// Valid: required name only
4+
const person1: StrictPerson = {
5+
name: "John Doe"
6+
};
7+
8+
// Valid: name and optional age
9+
const person2: StrictPerson = {
10+
name: "Jane Doe",
11+
age: 25
12+
};
13+
14+
// Invalid: name must be string
15+
const person3: StrictPerson = {
16+
// @ts-expect-error
17+
name: 123
18+
};
19+
20+
// Invalid: age must be number
21+
const person4: StrictPerson = {
22+
name: "John",
23+
// @ts-expect-error
24+
age: "twenty"
25+
};
26+
27+
// Invalid: missing required name
28+
// @ts-expect-error
29+
const person5: StrictPerson = {
30+
age: 30
31+
};
32+
33+
// Invalid: extra string property should be rejected
34+
const person6: StrictPerson = {
35+
name: "John",
36+
// @ts-expect-error
37+
nickname: "Johnny"
38+
};
39+
40+
// Invalid: extra number property should be rejected
41+
const person7: StrictPerson = {
42+
name: "John",
43+
// @ts-expect-error
44+
score: 100
45+
};
46+
47+
// Invalid: extra boolean property should be rejected
48+
const person8: StrictPerson = {
49+
name: "John",
50+
// @ts-expect-error
51+
active: true
52+
};
53+
54+
// Invalid: extra null property should be rejected
55+
const person9: StrictPerson = {
56+
name: "John",
57+
// @ts-expect-error
58+
nothing: null
59+
};
60+
61+
// Invalid: extra array property should be rejected
62+
const person10: StrictPerson = {
63+
name: "John",
64+
// @ts-expect-error
65+
tags: [ "a", "b" ]
66+
};
67+
68+
// Invalid: extra object property should be rejected
69+
const person11: StrictPerson = {
70+
name: "John",
71+
// @ts-expect-error
72+
metadata: { foo: "bar" }
73+
};
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
export type FlexibleRecord_Properties_Name = string;
2+
3+
export type FlexibleRecord_Properties_Count = number;
4+
5+
export type FlexibleRecord_AdditionalProperties_AnyOf_ZIndex5 = number;
6+
7+
export type FlexibleRecord_AdditionalProperties_AnyOf_ZIndex4 = string;
8+
9+
export type FlexibleRecord_AdditionalProperties_AnyOf_ZIndex3 = unknown[];
10+
11+
export interface FlexibleRecord_AdditionalProperties_AnyOf_ZIndex2 {
12+
}
13+
14+
export type FlexibleRecord_AdditionalProperties_AnyOf_ZIndex1 = boolean;
15+
16+
export type FlexibleRecord_AdditionalProperties_AnyOf_ZIndex0 = null;
17+
18+
export type FlexibleRecord_AdditionalProperties =
19+
FlexibleRecord_AdditionalProperties_AnyOf_ZIndex0 |
20+
FlexibleRecord_AdditionalProperties_AnyOf_ZIndex1 |
21+
FlexibleRecord_AdditionalProperties_AnyOf_ZIndex2 |
22+
FlexibleRecord_AdditionalProperties_AnyOf_ZIndex3 |
23+
FlexibleRecord_AdditionalProperties_AnyOf_ZIndex4 |
24+
FlexibleRecord_AdditionalProperties_AnyOf_ZIndex5;
25+
26+
export interface FlexibleRecord {
27+
"name": FlexibleRecord_Properties_Name;
28+
"count"?: FlexibleRecord_Properties_Count;
29+
[key: string]:
30+
FlexibleRecord_Properties_Name |
31+
FlexibleRecord_Properties_Count |
32+
FlexibleRecord_AdditionalProperties |
33+
undefined;
34+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"defaultPrefix": "FlexibleRecord"
3+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"$schema": "https://json-schema.org/draft/2020-12/schema",
3+
"type": "object",
4+
"properties": {
5+
"name": {
6+
"type": "string"
7+
},
8+
"count": {
9+
"type": "integer"
10+
}
11+
},
12+
"required": [ "name" ],
13+
"additionalProperties": true
14+
}

0 commit comments

Comments
 (0)