这是indexloc提供的服务,不要输入任何密码
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/two-humans-rescue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hey-api/openapi-ts': patch
---

fix(parser): handle `patternProperties` in OpenAPI 3.1
7 changes: 7 additions & 0 deletions packages/openapi-ts-tests/main/test/3.1.x.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ describe(`OpenAPI ${version}`, () => {
};

const scenarios = [
{
config: createConfig({
input: 'pattern-properties.json',
output: 'pattern-properties',
}),
description: 'handles pattern properties',
},
{
config: createConfig({
input: 'additional-properties-false.json',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// This file is auto-generated by @hey-api/openapi-ts
export * from './types.gen';
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// This file is auto-generated by @hey-api/openapi-ts

export type PatternPropertiesTest = {
id?: string;
metadata?: MetadataObject;
};

export type MetadataObject = {
name?: string;
description?: string;
[key: string]: Array<string> | string | {
value?: string;
enabled?: boolean;
} | undefined;
};

export type NestedPatternObject = {
base?: string;
[key: string]: {
[key: string]: string;
} | string | undefined;
};

export type UnionPatternObject = {
type?: 'user' | 'admin' | 'guest';
[key: string]: ({
[key: string]: unknown;
} & {
level?: number;
}) | (string | number) | ('user' | 'admin' | 'guest') | undefined;
};

export type PatternPropertiesResponse = {
success?: boolean;
data?: MetadataObject;
};

export type PostPatternTestData = {
body: PatternPropertiesTest;
path?: never;
query?: never;
url: '/pattern-test';
};

export type PostPatternTestResponses = {
/**
* Success
*/
200: PatternPropertiesResponse;
};

export type PostPatternTestResponse = PostPatternTestResponses[keyof PostPatternTestResponses];

export type ClientOptions = {
baseUrl: `${string}://${string}` | (string & {});
};
163 changes: 163 additions & 0 deletions packages/openapi-ts-tests/specs/3.1.x/pattern-properties.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
{
"openapi": "3.1.0",
"info": {
"title": "OpenAPI 3.1.0 pattern properties example",
"version": "1"
},
"paths": {
"/pattern-test": {
"post": {
"summary": "Test pattern properties",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PatternPropertiesTest"
}
}
}
},
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PatternPropertiesResponse"
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"PatternPropertiesTest": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"metadata": {
"$ref": "#/components/schemas/MetadataObject"
}
},
"additionalProperties": false
},
"MetadataObject": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"description": {
"type": "string"
}
},
"patternProperties": {
"^meta_": {
"type": "string",
"description": "Any property starting with 'meta_' must be a string"
},
"^config_": {
"type": "object",
"properties": {
"value": {
"type": "string"
},
"enabled": {
"type": "boolean"
}
},
"additionalProperties": false
},
"^tag_[a-zA-Z0-9_]+$": {
"type": "string",
"description": "Tag properties must match pattern and be strings"
},
"^[0-9]+_item$": {
"type": "array",
"items": {
"type": "string"
},
"description": "Numbered item properties must be arrays of strings"
}
},
"additionalProperties": false
},
"NestedPatternObject": {
"type": "object",
"properties": {
"base": {
"type": "string"
}
},
"patternProperties": {
"^nested_": {
"type": "object",
"patternProperties": {
"^sub_": {
"type": "string"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
"UnionPatternObject": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["user", "admin", "guest"]
}
},
"patternProperties": {
"^user_": {
"oneOf": [
{
"type": "string"
},
{
"type": "number"
}
]
},
"^admin_": {
"allOf": [
{
"type": "object"
},
{
"properties": {
"level": {
"type": "number",
"minimum": 1,
"maximum": 10
}
}
}
]
}
},
"additionalProperties": false
},
"PatternPropertiesResponse": {
"type": "object",
"properties": {
"success": {
"type": "boolean"
},
"data": {
"$ref": "#/components/schemas/MetadataObject"
}
},
"additionalProperties": false
}
}
}
}
6 changes: 6 additions & 0 deletions packages/openapi-ts/src/ir/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,10 +174,16 @@ interface IRSchemaObject
* @default 'or'
*/
logicalOperator?: 'and' | 'or';
/**
* When type is `object`, `patternProperties` can be used to define a schema
* for properties that match a specific regex pattern.
*/
patternProperties?: Record<string, IRSchemaObject>;
/**
* When type is `object`, `properties` will contain a map of its properties.
*/
properties?: Record<string, IRSchemaObject>;

/**
* The names of `properties` can be validated against a schema, irrespective
* of their values. This can be useful if you don't want to enforce specific
Expand Down
22 changes: 21 additions & 1 deletion packages/openapi-ts/src/openApi/3.1.x/parser/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,9 @@ const parseObject = ({
const isEmptyObjectInAllOf =
state.inAllOf &&
schema.additionalProperties === false &&
(!schema.properties || Object.keys(schema.properties).length === 0);
(!schema.properties || Object.keys(schema.properties).length === 0) &&
(!schema.patternProperties ||
Object.keys(schema.patternProperties).length === 0);

if (!isEmptyObjectInAllOf) {
irSchema.additionalProperties = {
Expand All @@ -311,6 +313,24 @@ const parseObject = ({
irSchema.additionalProperties = irAdditionalPropertiesSchema;
}

if (schema.patternProperties) {
const patternProperties: Record<string, IR.SchemaObject> = {};

for (const pattern in schema.patternProperties) {
const patternSchema = schema.patternProperties[pattern]!;
const irPatternSchema = schemaToIrSchema({
context,
schema: patternSchema,
state,
});
patternProperties[pattern] = irPatternSchema;
}

if (Object.keys(patternProperties).length) {
irSchema.patternProperties = patternProperties;
}
}

if (schema.propertyNames) {
irSchema.propertyNames = schemaToIrSchema({
context,
Expand Down
69 changes: 49 additions & 20 deletions packages/openapi-ts/src/plugins/@hey-api/typescript/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,14 +246,41 @@ const objectTypeToIdentifier = ({
}
}

if (
schema.additionalProperties &&
(schema.additionalProperties.type !== 'never' || !indexPropertyItems.length)
) {
if (schema.additionalProperties.type === 'never') {
indexPropertyItems = [schema.additionalProperties];
} else {
indexPropertyItems.unshift(schema.additionalProperties);
// include pattern value schemas into the index union
if (schema.patternProperties) {
for (const pattern in schema.patternProperties) {
const ir = schema.patternProperties[pattern]!;
indexPropertyItems.unshift(ir);
}
}

const hasPatterns =
!!schema.patternProperties &&
Object.keys(schema.patternProperties).length > 0;

const addPropsRaw = schema.additionalProperties;
const addPropsObj =
addPropsRaw !== false && addPropsRaw
? (addPropsRaw as IR.SchemaObject)
: undefined;
const shouldCreateIndex =
hasPatterns ||
(!!addPropsObj &&
(addPropsObj.type !== 'never' || !indexPropertyItems.length));

if (shouldCreateIndex) {
// only inject additionalProperties when it’s not "never"
const addProps = addPropsObj;
if (addProps && addProps.type !== 'never') {
indexPropertyItems.unshift(addProps);
} else if (
!hasPatterns &&
!indexPropertyItems.length &&
addProps &&
addProps.type === 'never'
) {
// keep "never" only when there are NO patterns and NO explicit properties
indexPropertyItems = [addProps];
}

if (hasOptionalProperties) {
Expand All @@ -265,18 +292,20 @@ const objectTypeToIdentifier = ({
indexProperty = {
isRequired: !schema.propertyNames,
name: 'key',
type: schemaToType({
onRef,
plugin,
schema:
indexPropertyItems.length === 1
? indexPropertyItems[0]!
: {
items: indexPropertyItems,
logicalOperator: 'or',
},
state,
}),
type:
indexPropertyItems.length === 1
? schemaToType({
onRef,
plugin,
schema: indexPropertyItems[0]!,
state,
})
: schemaToType({
onRef,
plugin,
schema: { items: indexPropertyItems, logicalOperator: 'or' },
state,
}),
};

if (schema.propertyNames?.$ref) {
Expand Down
Loading