Skip to content

Commit 33e1790

Browse files
authored
Support patternProperties general case as a fallback (#22)
Signed-off-by: Juan Cruz Viotti <jv@jviotti.com>
1 parent 4d047ac commit 33e1790

File tree

30 files changed

+392
-23
lines changed

30 files changed

+392
-23
lines changed

README.markdown

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ to use a JSON Schema validator at runtime to enforce remaining constraints.
3232
| Applicator (2020-12) | `items` | Yes |
3333
| Applicator (2020-12) | `prefixItems` | Yes |
3434
| Applicator (2020-12) | `anyOf` | Yes |
35-
| Applicator (2020-12) | `patternProperties` | **PARTIAL** (Prefix patterns only) |
35+
| Applicator (2020-12) | `patternProperties` | **PARTIAL GIVEN LANGUAGE LIMITATIONS** |
3636
| Applicator (2020-12) | `propertyNames` | Ignored |
3737
| Applicator (2020-12) | `dependentSchemas` | Pending |
3838
| Applicator (2020-12) | `contains` | Ignored |

src/generator/typescript.cc

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
#include <sourcemeta/codegen/generator.h>
22

3-
#include <iomanip> // std::hex, std::setfill, std::setw
4-
#include <sstream> // std::ostringstream
3+
#include <algorithm> // std::ranges::any_of
4+
#include <iomanip> // std::hex, std::setfill, std::setw
5+
#include <sstream> // std::ostringstream
56

67
namespace {
78

@@ -137,7 +138,12 @@ auto TypeScript::operator()(const IRObject &entry) -> void {
137138
}
138139

139140
for (const auto &pattern_property : entry.pattern) {
140-
this->output << " [key: `" << pattern_property.prefix << "${string}`]: "
141+
if (!pattern_property.prefix.has_value()) {
142+
continue;
143+
}
144+
145+
this->output << " [key: `" << pattern_property.prefix.value()
146+
<< "${string}`]: "
141147
<< mangle(this->prefix, pattern_property.pointer,
142148
pattern_property.symbol, this->cache);
143149

@@ -146,11 +152,11 @@ auto TypeScript::operator()(const IRObject &entry) -> void {
146152
// is a sub-prefix of another (i.e. "x-data-" starts with "x-"),
147153
// intersect the types so the constraint is satisfied
148154
for (const auto &other : entry.pattern) {
149-
if (&other == &pattern_property) {
155+
if (&other == &pattern_property || !other.prefix.has_value()) {
150156
continue;
151157
}
152158

153-
if (pattern_property.prefix.starts_with(other.prefix)) {
159+
if (pattern_property.prefix.value().starts_with(other.prefix.value())) {
154160
this->output << " & "
155161
<< mangle(this->prefix, other.pointer, other.symbol,
156162
this->cache);
@@ -160,9 +166,14 @@ auto TypeScript::operator()(const IRObject &entry) -> void {
160166
this->output << ";\n";
161167
}
162168

169+
const auto has_non_prefix_pattern{
170+
std::ranges::any_of(entry.pattern, [](const auto &pattern_property) {
171+
return !pattern_property.prefix.has_value();
172+
})};
173+
163174
if (allows_any_additional) {
164175
this->output << " [key: string]: unknown | undefined;\n";
165-
} else if (has_typed_additional) {
176+
} else if (has_typed_additional || has_non_prefix_pattern) {
166177
// TypeScript index signatures must be a supertype of all property value
167178
// types. We use a union of all member types plus the additional properties
168179
// type plus undefined (for optional properties).
@@ -186,11 +197,14 @@ auto TypeScript::operator()(const IRObject &entry) -> void {
186197
<< " |\n";
187198
}
188199

189-
const auto &additional_type{std::get<IRType>(entry.additional)};
190-
this->output << " "
191-
<< mangle(this->prefix, additional_type.pointer,
192-
additional_type.symbol, this->cache)
193-
<< " |\n";
200+
if (has_typed_additional) {
201+
const auto &additional_type{std::get<IRType>(entry.additional)};
202+
this->output << " "
203+
<< mangle(this->prefix, additional_type.pointer,
204+
additional_type.symbol, this->cache)
205+
<< " |\n";
206+
}
207+
194208
this->output << " undefined;\n";
195209
}
196210

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

6565
/// @ingroup ir
6666
struct IRObjectPatternProperty : IRType {
67-
std::string prefix;
67+
std::optional<std::string> prefix;
6868
};
6969

7070
/// @ingroup ir

src/ir/ir_default_compiler.h

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -146,19 +146,18 @@ auto handle_object(const sourcemeta::core::JSON &schema,
146146
frame.traverse(sourcemeta::core::to_weak_pointer(pattern_pointer))};
147147
assert(pattern_location.has_value());
148148

149+
std::optional<std::string> prefix{std::nullopt};
149150
const auto regex{sourcemeta::core::to_regex(entry.first)};
150-
if (!regex.has_value() ||
151-
!std::holds_alternative<sourcemeta::core::RegexTypePrefix>(
151+
if (regex.has_value() &&
152+
std::holds_alternative<sourcemeta::core::RegexTypePrefix>(
152153
regex.value())) {
153-
throw UnsupportedKeywordValueError(
154-
schema, location.pointer, "patternProperties",
155-
"Only prefix patterns are supported");
154+
prefix = std::get<sourcemeta::core::RegexTypePrefix>(regex.value());
156155
}
157156

158157
pattern.push_back(IRObjectPatternProperty{
159158
{.pointer = std::move(pattern_pointer),
160159
.symbol = symbol(frame, pattern_location.value().get())},
161-
std::get<sourcemeta::core::RegexTypePrefix>(regex.value())});
160+
std::move(prefix)});
162161
}
163162
}
164163

test/e2e/typescript/2020-12/pattern_properties_additional_false/test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,9 @@ const test4: StrictExt = {
2222
// @ts-expect-error
2323
other: "value"
2424
};
25+
26+
// Edge case: key is exactly the prefix with empty suffix
27+
const test5: StrictExt = {
28+
name: "hello",
29+
"x-": "value"
30+
};
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
export type EmptyPatternName = string;
2+
3+
export type EmptyPattern = number;
4+
5+
export type EmptyPatternAdditionalProperties = never;
6+
7+
export interface _EmptyPattern {
8+
"name"?: EmptyPatternName;
9+
[key: string]:
10+
// As a notable limitation, TypeScript requires index signatures
11+
// to also include the types of all of its properties, so we must
12+
// match a superset of what JSON Schema allows
13+
EmptyPatternName |
14+
EmptyPattern |
15+
undefined;
16+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"defaultPrefix": "EmptyPattern"
3+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"$schema": "https://json-schema.org/draft/2020-12/schema",
3+
"type": "object",
4+
"properties": {
5+
"name": { "type": "string" }
6+
},
7+
"patternProperties": {
8+
"": { "type": "integer" }
9+
},
10+
"additionalProperties": false
11+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { _EmptyPattern } from "./expected";
2+
3+
// Empty regex matches everything, so all keys go through [key: string]
4+
// union (string | number | undefined)
5+
const test1: _EmptyPattern = {
6+
name: "hello",
7+
anything: 42
8+
};
9+
10+
const test2: _EmptyPattern = {
11+
name: "hello",
12+
anything: "also valid"
13+
};
14+
15+
// Boolean is not in the union
16+
const test3: _EmptyPattern = {
17+
name: "hello",
18+
// @ts-expect-error
19+
flag: true
20+
};
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
export type HybridName = string;
2+
3+
export type HybridAge = number;
4+
5+
export type HybridX = string;
6+
7+
export type Hybrid_09_id = number;
8+
9+
export type HybridAdditionalProperties = boolean;
10+
11+
export interface Hybrid {
12+
"name": HybridName;
13+
"age"?: HybridAge;
14+
[key: `x-${string}`]: HybridX;
15+
[key: string]:
16+
// As a notable limitation, TypeScript requires index signatures
17+
// to also include the types of all of its properties, so we must
18+
// match a superset of what JSON Schema allows
19+
HybridName |
20+
HybridAge |
21+
HybridX |
22+
Hybrid_09_id |
23+
HybridAdditionalProperties |
24+
undefined;
25+
}

0 commit comments

Comments
 (0)