Skip to content

Commit 2932a55

Browse files
committed
Support patternProperties general case as a fallback
Signed-off-by: Juan Cruz Viotti <jv@jviotti.com>
1 parent 4d047ac commit 2932a55

File tree

25 files changed

+321
-23
lines changed

25 files changed

+321
-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

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+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"defaultPrefix": "Hybrid"
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": { "type": "string" },
6+
"age": { "type": "integer" }
7+
},
8+
"required": [ "name" ],
9+
"patternProperties": {
10+
"^x-": { "type": "string" },
11+
"[0-9]+_id": { "type": "integer" }
12+
},
13+
"additionalProperties": { "type": "boolean" }
14+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { Hybrid } from "./expected";
2+
3+
// All features together
4+
const test1: Hybrid = {
5+
name: "hello",
6+
age: 30,
7+
"x-custom": "extension",
8+
"123_id": 42,
9+
flag: true
10+
};
11+
12+
// Required name only
13+
const test2: Hybrid = {
14+
name: "hello"
15+
};
16+
17+
// Missing required name
18+
// @ts-expect-error
19+
const test3: Hybrid = {
20+
age: 30
21+
};
22+
23+
// Prefix pattern enforced: x- must be string, not number
24+
const test4: Hybrid = {
25+
name: "hello",
26+
// @ts-expect-error
27+
"x-custom": 42
28+
};
29+
30+
// Additional property must be in the union (no arrays)
31+
const test5: Hybrid = {
32+
name: "hello",
33+
// @ts-expect-error
34+
extra: [ 1, 2, 3 ]
35+
};
36+
37+
// Additional property with boolean works (additionalProperties type)
38+
const test6: Hybrid = {
39+
name: "hello",
40+
flag: true
41+
};
42+
43+
// Additional property with number works (in union via member/pattern types)
44+
const test7: Hybrid = {
45+
name: "hello",
46+
extra: 42
47+
};
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export type MixedFallbackX = string;
2+
3+
export type MixedFallback_09 = number;
4+
5+
export interface MixedFallback {
6+
[key: `x-${string}`]: MixedFallbackX;
7+
[key: string]: unknown | undefined;
8+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{ "defaultPrefix": "MixedFallback" }

0 commit comments

Comments
 (0)