fork of hey-api/openapi-ts because I need some additional things

Merge pull request #2523 from hey-api/feature/openapi-ts-contribution

feat: implement OpenAPI 3.1 `patternProperties` feature

authored by

Lubos and committed by
GitHub
c84763b6 995f1075

+309 -21
+5
.changeset/two-humans-rescue.md
··· 1 + --- 2 + '@hey-api/openapi-ts': patch 3 + --- 4 + 5 + fix(parser): handle `patternProperties` in OpenAPI 3.1
+7
packages/openapi-ts-tests/main/test/3.1.x.test.ts
··· 46 46 const scenarios = [ 47 47 { 48 48 config: createConfig({ 49 + input: 'pattern-properties.json', 50 + output: 'pattern-properties', 51 + }), 52 + description: 'handles pattern properties', 53 + }, 54 + { 55 + config: createConfig({ 49 56 input: 'additional-properties-false.json', 50 57 output: 'additional-properties-false', 51 58 }),
+2
packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/pattern-properties/index.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + export * from './types.gen';
+56
packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/pattern-properties/types.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + export type PatternPropertiesTest = { 4 + id?: string; 5 + metadata?: MetadataObject; 6 + }; 7 + 8 + export type MetadataObject = { 9 + name?: string; 10 + description?: string; 11 + [key: string]: Array<string> | string | { 12 + value?: string; 13 + enabled?: boolean; 14 + } | undefined; 15 + }; 16 + 17 + export type NestedPatternObject = { 18 + base?: string; 19 + [key: string]: { 20 + [key: string]: string; 21 + } | string | undefined; 22 + }; 23 + 24 + export type UnionPatternObject = { 25 + type?: 'user' | 'admin' | 'guest'; 26 + [key: string]: ({ 27 + [key: string]: unknown; 28 + } & { 29 + level?: number; 30 + }) | (string | number) | ('user' | 'admin' | 'guest') | undefined; 31 + }; 32 + 33 + export type PatternPropertiesResponse = { 34 + success?: boolean; 35 + data?: MetadataObject; 36 + }; 37 + 38 + export type PostPatternTestData = { 39 + body: PatternPropertiesTest; 40 + path?: never; 41 + query?: never; 42 + url: '/pattern-test'; 43 + }; 44 + 45 + export type PostPatternTestResponses = { 46 + /** 47 + * Success 48 + */ 49 + 200: PatternPropertiesResponse; 50 + }; 51 + 52 + export type PostPatternTestResponse = PostPatternTestResponses[keyof PostPatternTestResponses]; 53 + 54 + export type ClientOptions = { 55 + baseUrl: `${string}://${string}` | (string & {}); 56 + };
+163
packages/openapi-ts-tests/specs/3.1.x/pattern-properties.json
··· 1 + { 2 + "openapi": "3.1.0", 3 + "info": { 4 + "title": "OpenAPI 3.1.0 pattern properties example", 5 + "version": "1" 6 + }, 7 + "paths": { 8 + "/pattern-test": { 9 + "post": { 10 + "summary": "Test pattern properties", 11 + "requestBody": { 12 + "required": true, 13 + "content": { 14 + "application/json": { 15 + "schema": { 16 + "$ref": "#/components/schemas/PatternPropertiesTest" 17 + } 18 + } 19 + } 20 + }, 21 + "responses": { 22 + "200": { 23 + "description": "Success", 24 + "content": { 25 + "application/json": { 26 + "schema": { 27 + "$ref": "#/components/schemas/PatternPropertiesResponse" 28 + } 29 + } 30 + } 31 + } 32 + } 33 + } 34 + } 35 + }, 36 + "components": { 37 + "schemas": { 38 + "PatternPropertiesTest": { 39 + "type": "object", 40 + "properties": { 41 + "id": { 42 + "type": "string" 43 + }, 44 + "metadata": { 45 + "$ref": "#/components/schemas/MetadataObject" 46 + } 47 + }, 48 + "additionalProperties": false 49 + }, 50 + "MetadataObject": { 51 + "type": "object", 52 + "properties": { 53 + "name": { 54 + "type": "string" 55 + }, 56 + "description": { 57 + "type": "string" 58 + } 59 + }, 60 + "patternProperties": { 61 + "^meta_": { 62 + "type": "string", 63 + "description": "Any property starting with 'meta_' must be a string" 64 + }, 65 + "^config_": { 66 + "type": "object", 67 + "properties": { 68 + "value": { 69 + "type": "string" 70 + }, 71 + "enabled": { 72 + "type": "boolean" 73 + } 74 + }, 75 + "additionalProperties": false 76 + }, 77 + "^tag_[a-zA-Z0-9_]+$": { 78 + "type": "string", 79 + "description": "Tag properties must match pattern and be strings" 80 + }, 81 + "^[0-9]+_item$": { 82 + "type": "array", 83 + "items": { 84 + "type": "string" 85 + }, 86 + "description": "Numbered item properties must be arrays of strings" 87 + } 88 + }, 89 + "additionalProperties": false 90 + }, 91 + "NestedPatternObject": { 92 + "type": "object", 93 + "properties": { 94 + "base": { 95 + "type": "string" 96 + } 97 + }, 98 + "patternProperties": { 99 + "^nested_": { 100 + "type": "object", 101 + "patternProperties": { 102 + "^sub_": { 103 + "type": "string" 104 + } 105 + }, 106 + "additionalProperties": false 107 + } 108 + }, 109 + "additionalProperties": false 110 + }, 111 + "UnionPatternObject": { 112 + "type": "object", 113 + "properties": { 114 + "type": { 115 + "type": "string", 116 + "enum": ["user", "admin", "guest"] 117 + } 118 + }, 119 + "patternProperties": { 120 + "^user_": { 121 + "oneOf": [ 122 + { 123 + "type": "string" 124 + }, 125 + { 126 + "type": "number" 127 + } 128 + ] 129 + }, 130 + "^admin_": { 131 + "allOf": [ 132 + { 133 + "type": "object" 134 + }, 135 + { 136 + "properties": { 137 + "level": { 138 + "type": "number", 139 + "minimum": 1, 140 + "maximum": 10 141 + } 142 + } 143 + } 144 + ] 145 + } 146 + }, 147 + "additionalProperties": false 148 + }, 149 + "PatternPropertiesResponse": { 150 + "type": "object", 151 + "properties": { 152 + "success": { 153 + "type": "boolean" 154 + }, 155 + "data": { 156 + "$ref": "#/components/schemas/MetadataObject" 157 + } 158 + }, 159 + "additionalProperties": false 160 + } 161 + } 162 + } 163 + }
+6
packages/openapi-ts/src/ir/types.d.ts
··· 256 256 */ 257 257 logicalOperator?: 'and' | 'or'; 258 258 /** 259 + * When type is `object`, `patternProperties` can be used to define a schema 260 + * for properties that match a specific regex pattern. 261 + */ 262 + patternProperties?: Record<string, IRSchemaObject>; 263 + /** 259 264 * When type is `object`, `properties` will contain a map of its properties. 260 265 */ 261 266 properties?: Record<string, IRSchemaObject>; 267 + 262 268 /** 263 269 * The names of `properties` can be validated against a schema, irrespective 264 270 * of their values. This can be useful if you don't want to enforce specific
+21 -1
packages/openapi-ts/src/openApi/3.1.x/parser/schema.ts
··· 295 295 const isEmptyObjectInAllOf = 296 296 state.inAllOf && 297 297 schema.additionalProperties === false && 298 - (!schema.properties || Object.keys(schema.properties).length === 0); 298 + (!schema.properties || Object.keys(schema.properties).length === 0) && 299 + (!schema.patternProperties || 300 + Object.keys(schema.patternProperties).length === 0); 299 301 300 302 if (!isEmptyObjectInAllOf) { 301 303 irSchema.additionalProperties = { ··· 309 311 state, 310 312 }); 311 313 irSchema.additionalProperties = irAdditionalPropertiesSchema; 314 + } 315 + 316 + if (schema.patternProperties) { 317 + const patternProperties: Record<string, IR.SchemaObject> = {}; 318 + 319 + for (const pattern in schema.patternProperties) { 320 + const patternSchema = schema.patternProperties[pattern]!; 321 + const irPatternSchema = schemaToIrSchema({ 322 + context, 323 + schema: patternSchema, 324 + state, 325 + }); 326 + patternProperties[pattern] = irPatternSchema; 327 + } 328 + 329 + if (Object.keys(patternProperties).length) { 330 + irSchema.patternProperties = patternProperties; 331 + } 312 332 } 313 333 314 334 if (schema.propertyNames) {
+49 -20
packages/openapi-ts/src/plugins/@hey-api/typescript/plugin.ts
··· 246 246 } 247 247 } 248 248 249 - if ( 250 - schema.additionalProperties && 251 - (schema.additionalProperties.type !== 'never' || !indexPropertyItems.length) 252 - ) { 253 - if (schema.additionalProperties.type === 'never') { 254 - indexPropertyItems = [schema.additionalProperties]; 255 - } else { 256 - indexPropertyItems.unshift(schema.additionalProperties); 249 + // include pattern value schemas into the index union 250 + if (schema.patternProperties) { 251 + for (const pattern in schema.patternProperties) { 252 + const ir = schema.patternProperties[pattern]!; 253 + indexPropertyItems.unshift(ir); 254 + } 255 + } 256 + 257 + const hasPatterns = 258 + !!schema.patternProperties && 259 + Object.keys(schema.patternProperties).length > 0; 260 + 261 + const addPropsRaw = schema.additionalProperties; 262 + const addPropsObj = 263 + addPropsRaw !== false && addPropsRaw 264 + ? (addPropsRaw as IR.SchemaObject) 265 + : undefined; 266 + const shouldCreateIndex = 267 + hasPatterns || 268 + (!!addPropsObj && 269 + (addPropsObj.type !== 'never' || !indexPropertyItems.length)); 270 + 271 + if (shouldCreateIndex) { 272 + // only inject additionalProperties when it’s not "never" 273 + const addProps = addPropsObj; 274 + if (addProps && addProps.type !== 'never') { 275 + indexPropertyItems.unshift(addProps); 276 + } else if ( 277 + !hasPatterns && 278 + !indexPropertyItems.length && 279 + addProps && 280 + addProps.type === 'never' 281 + ) { 282 + // keep "never" only when there are NO patterns and NO explicit properties 283 + indexPropertyItems = [addProps]; 257 284 } 258 285 259 286 if (hasOptionalProperties) { ··· 265 292 indexProperty = { 266 293 isRequired: !schema.propertyNames, 267 294 name: 'key', 268 - type: schemaToType({ 269 - onRef, 270 - plugin, 271 - schema: 272 - indexPropertyItems.length === 1 273 - ? indexPropertyItems[0]! 274 - : { 275 - items: indexPropertyItems, 276 - logicalOperator: 'or', 277 - }, 278 - state, 279 - }), 295 + type: 296 + indexPropertyItems.length === 1 297 + ? schemaToType({ 298 + onRef, 299 + plugin, 300 + schema: indexPropertyItems[0]!, 301 + state, 302 + }) 303 + : schemaToType({ 304 + onRef, 305 + plugin, 306 + schema: { items: indexPropertyItems, logicalOperator: 'or' }, 307 + state, 308 + }), 280 309 }; 281 310 282 311 if (schema.propertyNames?.$ref) {