Skip to content

Commit 0b02797

Browse files
committed
Support prefix patternProperties
Signed-off-by: Juan Cruz Viotti <jv@jviotti.com>
1 parent ed22a81 commit 0b02797

File tree

37 files changed

+585
-5
lines changed

37 files changed

+585
-5
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` | **CANNOT SUPPORT** |
35+
| Applicator (2020-12) | `patternProperties` | **PARTIAL** (Prefix patterns only) |
3636
| Applicator (2020-12) | `propertyNames` | Ignored |
3737
| Applicator (2020-12) | `dependentSchemas` | Pending |
3838
| Applicator (2020-12) | `contains` | Ignored |

src/generator/typescript.cc

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ auto TypeScript::operator()(const IRObject &entry) -> void {
101101
std::holds_alternative<bool>(entry.additional) &&
102102
std::get<bool>(entry.additional)};
103103

104-
if (has_typed_additional && entry.members.empty()) {
104+
if (has_typed_additional && entry.members.empty() && entry.pattern.empty()) {
105105
const auto &additional_type{std::get<IRType>(entry.additional)};
106106
this->output << "export type " << type_name << " = Record<string, "
107107
<< mangle(this->prefix, additional_type.pointer,
@@ -110,7 +110,7 @@ auto TypeScript::operator()(const IRObject &entry) -> void {
110110
return;
111111
}
112112

113-
if (allows_any_additional && entry.members.empty()) {
113+
if (allows_any_additional && entry.members.empty() && entry.pattern.empty()) {
114114
this->output << "export type " << type_name
115115
<< " = Record<string, unknown>;\n";
116116
return;
@@ -136,6 +136,30 @@ auto TypeScript::operator()(const IRObject &entry) -> void {
136136
<< ";\n";
137137
}
138138

139+
for (const auto &pattern_property : entry.pattern) {
140+
this->output << " [key: `" << pattern_property.prefix << "${string}`]: "
141+
<< mangle(this->prefix, pattern_property.pointer,
142+
pattern_property.symbol, this->cache);
143+
144+
// TypeScript requires that a more specific index signature type is
145+
// assignable to any less specific one that overlaps it. When a prefix
146+
// is a sub-prefix of another (i.e. "x-data-" starts with "x-"),
147+
// intersect the types so the constraint is satisfied
148+
for (const auto &other : entry.pattern) {
149+
if (&other == &pattern_property) {
150+
continue;
151+
}
152+
153+
if (pattern_property.prefix.starts_with(other.prefix)) {
154+
this->output << " & "
155+
<< mangle(this->prefix, other.pointer, other.symbol,
156+
this->cache);
157+
}
158+
}
159+
160+
this->output << ";\n";
161+
}
162+
139163
if (allows_any_additional) {
140164
this->output << " [key: string]: unknown | undefined;\n";
141165
} else if (has_typed_additional) {
@@ -155,6 +179,13 @@ auto TypeScript::operator()(const IRObject &entry) -> void {
155179
<< " |\n";
156180
}
157181

182+
for (const auto &pattern_property : entry.pattern) {
183+
this->output << " "
184+
<< mangle(this->prefix, pattern_property.pointer,
185+
pattern_property.symbol, this->cache)
186+
<< " |\n";
187+
}
188+
158189
const auto &additional_type{std::get<IRType>(entry.additional)};
159190
this->output << " "
160191
<< mangle(this->prefix, additional_type.pointer,

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,17 @@ struct IRObjectValue : IRType {
6262
bool immutable;
6363
};
6464

65+
/// @ingroup ir
66+
struct IRObjectPatternProperty : IRType {
67+
std::string prefix;
68+
};
69+
6570
/// @ingroup ir
6671
struct IRObject : IRType {
6772
// To preserve the user's ordering
6873
std::vector<std::pair<sourcemeta::core::JSON::String, IRObjectValue>> members;
6974
std::variant<bool, IRType> additional;
75+
std::vector<IRObjectPatternProperty> pattern;
7076
};
7177

7278
/// @ingroup ir

src/ir/ir_default_compiler.h

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include <sourcemeta/codegen/ir.h>
55

66
#include <sourcemeta/core/jsonschema.h>
7+
#include <sourcemeta/core/regex.h>
78

89
#include <cassert> // assert
910
#include <string_view> // std::string_view
@@ -77,7 +78,7 @@ auto handle_object(const sourcemeta::core::JSON &schema,
7778
// other properties. Therefore, we whitelist this, but we consider it to
7879
// be the responsability of the validator
7980
"additionalProperties", "minProperties", "maxProperties",
80-
"propertyNames"});
81+
"propertyNames", "patternProperties"});
8182

8283
std::vector<std::pair<sourcemeta::core::JSON::String, IRObjectValue>> members;
8384

@@ -133,10 +134,39 @@ auto handle_object(const sourcemeta::core::JSON &schema,
133134
}
134135
}
135136

137+
std::vector<IRObjectPatternProperty> pattern;
138+
if (subschema.defines("patternProperties")) {
139+
const auto &pattern_props{subschema.at("patternProperties")};
140+
for (const auto &entry : pattern_props.as_object()) {
141+
auto pattern_pointer{sourcemeta::core::to_pointer(location.pointer)};
142+
pattern_pointer.push_back("patternProperties");
143+
pattern_pointer.push_back(entry.first);
144+
145+
const auto pattern_location{
146+
frame.traverse(sourcemeta::core::to_weak_pointer(pattern_pointer))};
147+
assert(pattern_location.has_value());
148+
149+
const auto regex{sourcemeta::core::to_regex(entry.first)};
150+
if (!regex.has_value() ||
151+
!std::holds_alternative<sourcemeta::core::RegexTypePrefix>(
152+
regex.value())) {
153+
throw UnsupportedKeywordValueError(
154+
schema, location.pointer, "patternProperties",
155+
"Only prefix patterns are supported");
156+
}
157+
158+
pattern.push_back(IRObjectPatternProperty{
159+
{.pointer = std::move(pattern_pointer),
160+
.symbol = symbol(frame, pattern_location.value().get())},
161+
std::get<sourcemeta::core::RegexTypePrefix>(regex.value())});
162+
}
163+
}
164+
136165
return IRObject{{.pointer = sourcemeta::core::to_pointer(location.pointer),
137166
.symbol = symbol(frame, location)},
138167
std::move(members),
139-
std::move(additional)};
168+
std::move(additional),
169+
std::move(pattern)};
140170
}
141171

142172
auto handle_integer(const sourcemeta::core::JSON &schema,
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export type StrictExtName = string;
2+
3+
export type StrictExtX = string;
4+
5+
export type StrictExtAdditionalProperties = never;
6+
7+
export interface StrictExt {
8+
"name"?: StrictExtName;
9+
[key: `x-${string}`]: StrictExtX;
10+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"defaultPrefix": "StrictExt"
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+
"^x-": { "type": "string" }
9+
},
10+
"additionalProperties": false
11+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { StrictExt } from "./expected";
2+
3+
const test1: StrictExt = {
4+
name: "hello"
5+
};
6+
7+
const test2: StrictExt = {
8+
name: "hello",
9+
"x-custom": "value"
10+
};
11+
12+
// Wrong type for pattern property
13+
const test3: StrictExt = {
14+
name: "hello",
15+
// @ts-expect-error
16+
"x-custom": 42
17+
};
18+
19+
// Non-matching key should be rejected (additionalProperties: false)
20+
const test4: StrictExt = {
21+
name: "hello",
22+
// @ts-expect-error
23+
other: "value"
24+
};
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export type OpenExtName = string;
2+
3+
export type OpenExtX = string;
4+
5+
export type OpenExtAdditionalProperties = unknown;
6+
7+
export interface OpenExt {
8+
"name"?: OpenExtName;
9+
[key: `x-${string}`]: OpenExtX;
10+
[key: string]: unknown | undefined;
11+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"defaultPrefix": "OpenExt"
3+
}

0 commit comments

Comments
 (0)