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

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #2153 from hey-api/feat/object-property-names

fix(parser): handle propertyNames keyword

authored by

Lubos and committed by
GitHub
60deb25f ced57a77

+165 -50
+5
.changeset/selfish-vans-heal.md
··· 1 + --- 2 + '@hey-api/openapi-ts': patch 3 + --- 4 + 5 + fix(parser): handle `propertyNames` keyword
+8
packages/openapi-ts-tests/test/3.1.x.test.ts
··· 510 510 }, 511 511 { 512 512 config: createConfig({ 513 + input: 'object-property-names.yaml', 514 + output: 'object-property-names', 515 + }), 516 + description: 517 + 'sets correct index signature type on object with property names', 518 + }, 519 + { 520 + config: createConfig({ 513 521 input: 'operation-204.json', 514 522 output: 'operation-204', 515 523 }),
+2
packages/openapi-ts-tests/test/__snapshots__/3.1.x/object-property-names/index.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + export * from './types.gen';
+11
packages/openapi-ts-tests/test/__snapshots__/3.1.x/object-property-names/types.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + export type Foo = 'foo' | 'bar'; 4 + 5 + export type Bar = { 6 + [key in Foo]?: string; 7 + }; 8 + 9 + export type ClientOptions = { 10 + baseUrl: `${string}://${string}` | (string & {}); 11 + };
+11 -6
packages/openapi-ts-tests/test/openapi-ts.config.ts
··· 51 51 // 'invalid', 52 52 // 'servers-entry.yaml', 53 53 // ), 54 - path: path.resolve(__dirname, 'spec', '3.1.x', 'type-format.yaml'), 54 + path: path.resolve( 55 + __dirname, 56 + 'spec', 57 + '3.1.x', 58 + 'object-property-names.yaml', 59 + ), 55 60 // path: 'http://localhost:4000/', 56 61 // path: 'https://get.heyapi.dev/', 57 62 // path: 'https://get.heyapi.dev/hey-api/backend?branch=main&version=1.0.0', ··· 114 119 // operationId: false, 115 120 // responseStyle: 'data', 116 121 // throwOnError: true, 117 - transformer: '@hey-api/transformers', 122 + // transformer: '@hey-api/transformers', 118 123 // transformer: true, 119 - validator: 'zod', 124 + // validator: 'zod', 120 125 }, 121 126 { 122 127 // bigInt: true, ··· 140 145 }, 141 146 { 142 147 exportFromIndex: true, 143 - name: '@tanstack/react-query', 148 + // name: '@tanstack/react-query', 144 149 }, 145 150 { 146 151 // comments: false, 147 152 // exportFromIndex: true, 148 - name: 'valibot', 153 + // name: 'valibot', 149 154 }, 150 155 { 151 156 // comments: false, 152 157 // exportFromIndex: true, 153 - name: 'zod', 158 + // name: 'zod', 154 159 }, 155 160 ], 156 161 // useOptions: false,
+17
packages/openapi-ts-tests/test/spec/3.1.x/object-property-names.yaml
··· 1 + openapi: 3.1.1 2 + info: 3 + title: OpenAPI 3.1.1 object property names example 4 + version: 1 5 + components: 6 + schemas: 7 + Foo: 8 + enum: 9 + - foo 10 + - bar 11 + type: string 12 + Bar: 13 + additionalProperties: 14 + type: string 15 + propertyNames: 16 + $ref: '#/components/schemas/Foo' 17 + type: object
+82 -42
packages/openapi-ts/src/compiler/typedef.ts
··· 3 3 import { validTypescriptIdentifierRegExp } from '../utils/regexp'; 4 4 import { 5 5 createKeywordTypeNode, 6 + createMappedTypeNode, 6 7 createParameterDeclaration, 7 8 createStringLiteral, 8 9 createTypeNode, 10 + createTypeParameterDeclaration, 9 11 createTypeReferenceNode, 10 12 } from './types'; 11 13 import { ··· 51 53 * @returns ts.TypeLiteralNode | ts.TypeUnionNode 52 54 */ 53 55 export const createTypeInterfaceNode = ({ 56 + indexKey, 54 57 indexProperty, 55 58 isNullable, 56 59 properties, 57 60 useLegacyResolution, 58 61 }: { 59 62 /** 63 + * Adds an index key type. 64 + * 65 + * @example 66 + * ```ts 67 + * type IndexKey = { 68 + * [key in Foo]: string 69 + * } 70 + * ``` 71 + */ 72 + indexKey?: string; 73 + /** 60 74 * Adds an index signature if defined. 75 + * 61 76 * @example 62 77 * ```ts 63 78 * type IndexProperty = { ··· 72 87 }) => { 73 88 const propertyTypes: Array<ts.TypeNode> = []; 74 89 75 - const members: Array<ts.TypeElement> = properties.map((property) => { 76 - const modifiers: readonly ts.Modifier[] | undefined = property.isReadOnly 77 - ? [createModifier({ keyword: 'readonly' })] 78 - : undefined; 90 + const members: Array<ts.TypeElement | ts.MappedTypeNode> = properties.map( 91 + (property) => { 92 + const modifiers: readonly ts.Modifier[] | undefined = property.isReadOnly 93 + ? [createModifier({ keyword: 'readonly' })] 94 + : undefined; 79 95 80 - const questionToken: ts.QuestionToken | undefined = 81 - property.isRequired !== false 82 - ? undefined 83 - : ts.factory.createToken(ts.SyntaxKind.QuestionToken); 96 + const questionToken: ts.QuestionToken | undefined = 97 + property.isRequired !== false 98 + ? undefined 99 + : ts.factory.createToken(ts.SyntaxKind.QuestionToken); 84 100 85 - const type: ts.TypeNode | undefined = createTypeNode(property.type); 86 - propertyTypes.push(type); 101 + const type: ts.TypeNode | undefined = createTypeNode(property.type); 102 + propertyTypes.push(type); 87 103 88 - const signature = ts.factory.createPropertySignature( 89 - modifiers, 90 - useLegacyResolution || 91 - (typeof property.name === 'string' && 92 - property.name.match(validTypescriptIdentifierRegExp)) || 93 - (typeof property.name !== 'string' && ts.isPropertyName(property.name)) 94 - ? property.name 95 - : createStringLiteral({ text: property.name }), 96 - questionToken, 97 - type, 98 - ); 104 + const signature = ts.factory.createPropertySignature( 105 + modifiers, 106 + useLegacyResolution || 107 + (typeof property.name === 'string' && 108 + property.name.match(validTypescriptIdentifierRegExp)) || 109 + (typeof property.name !== 'string' && 110 + ts.isPropertyName(property.name)) 111 + ? property.name 112 + : createStringLiteral({ text: property.name }), 113 + questionToken, 114 + type, 115 + ); 99 116 100 - addLeadingComments({ 101 - comments: property.comment, 102 - node: signature, 103 - }); 117 + addLeadingComments({ 118 + comments: property.comment, 119 + node: signature, 120 + }); 104 121 105 - return signature; 106 - }); 122 + return signature; 123 + }, 124 + ); 125 + 126 + let isIndexMapped = false; 107 127 108 128 if (indexProperty) { 109 - const modifiers: readonly ts.Modifier[] | undefined = 110 - indexProperty.isReadOnly 111 - ? [createModifier({ keyword: 'readonly' })] 112 - : undefined; 113 - const indexSignature = ts.factory.createIndexSignature( 114 - modifiers, 115 - [ 116 - createParameterDeclaration({ 129 + if (!properties.length && indexKey) { 130 + const indexSignature = createMappedTypeNode({ 131 + questionToken: ts.factory.createToken(ts.SyntaxKind.QuestionToken), 132 + type: createKeywordTypeNode({ keyword: 'string' }), 133 + typeParameter: createTypeParameterDeclaration({ 134 + constraint: createTypeReferenceNode({ typeName: indexKey }), 117 135 name: createIdentifier({ text: String(indexProperty.name) }), 118 - type: createKeywordTypeNode({ keyword: 'string' }), 119 136 }), 120 - ], 121 - createTypeNode(indexProperty.type), 122 - ); 123 - members.push(indexSignature); 137 + }); 138 + members.push(indexSignature); 139 + isIndexMapped = true; 140 + } else { 141 + const modifiers: ReadonlyArray<ts.Modifier> | undefined = 142 + indexProperty.isReadOnly 143 + ? [createModifier({ keyword: 'readonly' })] 144 + : undefined; 145 + const indexSignature = ts.factory.createIndexSignature( 146 + modifiers, 147 + [ 148 + createParameterDeclaration({ 149 + name: createIdentifier({ text: String(indexProperty.name) }), 150 + type: createKeywordTypeNode({ keyword: 'string' }), 151 + }), 152 + ], 153 + createTypeNode(indexProperty.type), 154 + ); 155 + members.push(indexSignature); 156 + } 124 157 } 125 158 126 - const node = ts.factory.createTypeLiteralNode(members); 127 - return maybeNullable({ isNullable, node }); 159 + const node = isIndexMapped 160 + ? members[0]! 161 + : // @ts-expect-error 162 + ts.factory.createTypeLiteralNode(members); 163 + return maybeNullable({ 164 + isNullable, 165 + // @ts-expect-error 166 + node, 167 + }); 128 168 }; 129 169 130 170 /**
+7
packages/openapi-ts/src/ir/types.d.ts
··· 178 178 */ 179 179 properties?: Record<string, IRSchemaObject>; 180 180 /** 181 + * The names of `properties` can be validated against a schema, irrespective 182 + * of their values. This can be useful if you don't want to enforce specific 183 + * properties, but you want to make sure that the names of those properties 184 + * follow a specific convention. 185 + */ 186 + propertyNames?: IRSchemaObject; 187 + /** 181 188 * Each schema eventually resolves into `type`. 182 189 */ 183 190 type?:
+4
packages/openapi-ts/src/openApi/3.1.x/parser/graph.ts
··· 80 80 collectSchemaDependencies(item, dependencies); 81 81 } 82 82 } 83 + 84 + if (schema.propertyNames && typeof schema.propertyNames === 'object') { 85 + collectSchemaDependencies(schema.propertyNames, dependencies); 86 + } 83 87 }; 84 88 85 89 export const createGraph = ({
+8
packages/openapi-ts/src/openApi/3.1.x/parser/schema.ts
··· 324 324 irSchema.additionalProperties = irAdditionalPropertiesSchema; 325 325 } 326 326 327 + if (schema.propertyNames) { 328 + irSchema.propertyNames = schemaToIrSchema({ 329 + context, 330 + schema: schema.propertyNames, 331 + state, 332 + }); 333 + } 334 + 327 335 if (schema.required) { 328 336 irSchema.required = schema.required; 329 337 }
+10 -2
packages/openapi-ts/src/plugins/@hey-api/typescript/plugin.ts
··· 5 5 import { operationResponsesMap } from '../../../ir/operation'; 6 6 import { deduplicateSchema } from '../../../ir/schema'; 7 7 import type { IR } from '../../../ir/types'; 8 - import { irRef, isRefOpenApiComponent } from '../../../utils/ref'; 8 + import { irRef, isRefOpenApiComponent, refToName } from '../../../utils/ref'; 9 9 import { numberRegExp } from '../../../utils/regexp'; 10 10 import { stringCase } from '../../../utils/stringCase'; 11 11 import { fieldName } from '../../shared/utils/case'; ··· 562 562 state: State | undefined; 563 563 }): ts.TypeNode | undefined => { 564 564 // TODO: parser - handle constants 565 + let indexKey: string | undefined; 565 566 let indexProperty: Property | undefined; 566 567 const schemaProperties: Array<Property> = []; 567 568 let indexPropertyItems: Array<IR.SchemaObject> = []; ··· 627 628 } 628 629 629 630 indexProperty = { 630 - isRequired: true, 631 + isRequired: !schema.propertyNames, 631 632 name: 'key', 632 633 type: schemaToType({ 633 634 context, ··· 643 644 state, 644 645 }), 645 646 }; 647 + 648 + if (schema.propertyNames) { 649 + if (schema.propertyNames.$ref) { 650 + indexKey = refToName(schema.propertyNames.$ref); 651 + } 652 + } 646 653 } 647 654 648 655 if (hasSkippedProperties && !schemaProperties.length && !indexProperty) { ··· 650 657 } 651 658 652 659 return compiler.typeInterfaceNode({ 660 + indexKey, 653 661 indexProperty, 654 662 properties: schemaProperties, 655 663 useLegacyResolution: false,