Skip to content

Commit 0f22be2

Browse files
hontasPontus Lundin
andauthored
Feat: Add support for pattern properties (#2288)
* add support for pattern properties * add tests * fix fmt issue in test file * add changeset * update after feedback * fix lint issues * apply suggestions --------- Co-authored-by: Pontus Lundin <[email protected]>
1 parent 6ed0634 commit 0f22be2

File tree

4 files changed

+143
-27
lines changed

4 files changed

+143
-27
lines changed

.changeset/eleven-shoes-mate.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"openapi-typescript": minor
3+
---
4+
5+
Add support for patternProperties

packages/openapi-typescript/src/transform/schema-object.ts

Lines changed: 48 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,7 @@ function transformSchemaObjectCore(schemaObject: SchemaObject, options: Transfor
443443
if (
444444
("properties" in schemaObject && schemaObject.properties && Object.keys(schemaObject.properties).length) ||
445445
("additionalProperties" in schemaObject && schemaObject.additionalProperties) ||
446+
("patternProperties" in schemaObject && schemaObject.patternProperties) ||
446447
("$defs" in schemaObject && schemaObject.$defs)
447448
) {
448449
// properties
@@ -542,34 +543,54 @@ function transformSchemaObjectCore(schemaObject: SchemaObject, options: Transfor
542543
);
543544
}
544545

545-
// additionalProperties
546-
if (schemaObject.additionalProperties || options.ctx.additionalProperties) {
547-
const hasExplicitAdditionalProperties =
548-
typeof schemaObject.additionalProperties === "object" && Object.keys(schemaObject.additionalProperties).length;
549-
const addlType = hasExplicitAdditionalProperties
550-
? transformSchemaObject(schemaObject.additionalProperties as SchemaObject, options)
551-
: UNKNOWN;
552-
return tsIntersection([
553-
...(coreObjectType.length ? [ts.factory.createTypeLiteralNode(coreObjectType)] : []),
554-
ts.factory.createTypeLiteralNode([
555-
ts.factory.createIndexSignature(
556-
/* modifiers */ tsModifiers({
557-
readonly: options.ctx.immutable,
558-
}),
559-
/* parameters */ [
560-
ts.factory.createParameterDeclaration(
561-
/* modifiers */ undefined,
562-
/* dotDotDotToken */ undefined,
563-
/* name */ ts.factory.createIdentifier("key"),
564-
/* questionToken */ undefined,
565-
/* type */ STRING,
566-
),
567-
],
568-
/* type */ addlType,
569-
),
570-
]),
571-
]);
546+
// additionalProperties / patternProperties
547+
const hasExplicitAdditionalProperties =
548+
typeof schemaObject.additionalProperties === "object" && Object.keys(schemaObject.additionalProperties).length;
549+
const hasImplicitAdditionalProperties =
550+
schemaObject.additionalProperties === true ||
551+
(typeof schemaObject.additionalProperties === "object" &&
552+
Object.keys(schemaObject.additionalProperties).length === 0);
553+
const hasExplicitPatternProperties =
554+
typeof schemaObject.patternProperties === "object" && Object.keys(schemaObject.patternProperties).length;
555+
const stringIndexTypes = [];
556+
if (hasExplicitAdditionalProperties) {
557+
stringIndexTypes.push(transformSchemaObject(schemaObject.additionalProperties as SchemaObject, options));
572558
}
559+
if (hasImplicitAdditionalProperties || (!schemaObject.additionalProperties && options.ctx.additionalProperties)) {
560+
stringIndexTypes.push(UNKNOWN);
561+
}
562+
if (hasExplicitPatternProperties) {
563+
for (const [_, v] of getEntries(schemaObject.patternProperties ?? {}, options.ctx)) {
564+
stringIndexTypes.push(transformSchemaObject(v, options));
565+
}
566+
}
567+
568+
if (stringIndexTypes.length === 0) {
569+
return coreObjectType.length ? ts.factory.createTypeLiteralNode(coreObjectType) : undefined;
570+
}
571+
572+
const stringIndexType = tsUnion(stringIndexTypes);
573+
574+
return tsIntersection([
575+
...(coreObjectType.length ? [ts.factory.createTypeLiteralNode(coreObjectType)] : []),
576+
ts.factory.createTypeLiteralNode([
577+
ts.factory.createIndexSignature(
578+
/* modifiers */ tsModifiers({
579+
readonly: options.ctx.immutable,
580+
}),
581+
/* parameters */ [
582+
ts.factory.createParameterDeclaration(
583+
/* modifiers */ undefined,
584+
/* dotDotDotToken */ undefined,
585+
/* name */ ts.factory.createIdentifier("key"),
586+
/* questionToken */ undefined,
587+
/* type */ STRING,
588+
),
589+
],
590+
/* type */ stringIndexType,
591+
),
592+
]),
593+
]);
573594
}
574595

575596
return coreObjectType.length ? ts.factory.createTypeLiteralNode(coreObjectType) : undefined;

packages/openapi-typescript/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,7 @@ export interface ObjectSubtype {
502502
type: "object" | ["object", "null"];
503503
properties?: { [name: string]: SchemaObject | ReferenceObject };
504504
additionalProperties?: boolean | Record<string, never> | SchemaObject | ReferenceObject;
505+
patternProperties?: Record<string, SchemaObject | ReferenceObject>;
505506
required?: string[];
506507
allOf?: (SchemaObject | ReferenceObject)[];
507508
anyOf?: (SchemaObject | ReferenceObject)[];

packages/openapi-typescript/test/transform/schema-object/object.test.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,95 @@ describe("transformSchemaObject > object", () => {
9999
// options: DEFAULT_OPTIONS,
100100
},
101101
],
102+
[
103+
"patternProperties > empty object",
104+
{
105+
given: { type: "object", patternProperties: {} },
106+
want: "Record<string, never>",
107+
},
108+
],
109+
[
110+
"patternProperties > empty object with options.additionalProperties=true",
111+
{
112+
given: { type: "object", patternProperties: {} },
113+
want: `{
114+
[key: string]: unknown;
115+
}`,
116+
options: {
117+
...DEFAULT_OPTIONS,
118+
ctx: { ...DEFAULT_CTX, additionalProperties: true },
119+
},
120+
},
121+
],
122+
[
123+
"patternProperties > basic",
124+
{
125+
given: { type: "object", patternProperties: { "^a": { type: "string" } } },
126+
want: `{
127+
[key: string]: string;
128+
}`,
129+
},
130+
],
131+
[
132+
"patternProperties > enum",
133+
{
134+
given: { type: "object", patternProperties: { "^a": { type: "string", enum: ["a", "b", "c"] } } },
135+
want: `{
136+
[key: string]: "a" | "b" | "c";
137+
}`,
138+
},
139+
],
140+
[
141+
"patternProperties > multiple patterns",
142+
{
143+
given: { type: "object", patternProperties: { "^a": { type: "string" }, "^b": { type: "number" } } },
144+
want: `{
145+
[key: string]: string | number;
146+
}`,
147+
},
148+
],
149+
[
150+
"patternProperties > additional=true and patterns",
151+
{
152+
given: {
153+
type: "object",
154+
additionalProperties: true,
155+
patternProperties: { "^a": { type: "string" } },
156+
},
157+
want: `{
158+
[key: string]: unknown | string;
159+
}`,
160+
},
161+
],
162+
[
163+
"patternProperties > additional and patterns",
164+
{
165+
given: {
166+
type: "object",
167+
additionalProperties: { type: "number" },
168+
patternProperties: { "^a": { type: "string" } },
169+
},
170+
want: `{
171+
[key: string]: number | string;
172+
}`,
173+
},
174+
],
175+
[
176+
"patternProperties > patterns with options.additionalProperties=true",
177+
{
178+
given: {
179+
type: "object",
180+
patternProperties: { "^a": { type: "string" } },
181+
},
182+
want: `{
183+
[key: string]: unknown | string;
184+
}`,
185+
options: {
186+
...DEFAULT_OPTIONS,
187+
ctx: { ...DEFAULT_CTX, additionalProperties: true },
188+
},
189+
},
190+
],
102191
[
103192
"nullable",
104193
{

0 commit comments

Comments
 (0)