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 #2086 from hey-api/fix/parser-input-filters-parameters-responses

fix(parser): extend input filters to handle reusable parameters and responses

authored by

Lubos and committed by
GitHub
2313cec7 2f5beadd

+887 -62
+5
.changeset/real-fireants-drop.md
··· 1 + --- 2 + '@hey-api/openapi-ts': patch 3 + --- 4 + 5 + fix(parser): add back support for regular expressions in input filters
+5
.changeset/two-files-call.md
··· 1 + --- 2 + '@hey-api/openapi-ts': patch 3 + --- 4 + 5 + fix(parser): extend input filters to handle reusable parameters and responses
+85 -9
docs/openapi-ts/configuration.md
··· 197 197 198 198 ### Operations 199 199 200 - Set `include` to match operations to be included or `exclude` to match operations to be excluded. When both rules match the same operation, `exclude` takes precedence over `include`. 200 + Set `include` to match operations to be included or `exclude` to match operations to be excluded. Both exact keys and regular expressions are supported. When both rules match the same operation, `exclude` takes precedence over `include`. 201 201 202 202 ::: code-group 203 203 ··· 206 206 input: { 207 207 filters: { 208 208 operations: { 209 - include: ['GET /api/v1/foo'], // [!code ++] 209 + include: ['GET /api/v1/foo', '/^[A-Z]+ /api/v1//'], // [!code ++] 210 210 }, 211 211 }, 212 212 path: 'https://get.heyapi.dev/hey-api/backend', ··· 221 221 input: { 222 222 filters: { 223 223 operations: { 224 - exclude: ['GET /api/v1/foo'], // [!code ++] 224 + exclude: ['GET /api/v1/foo', '/^[A-Z]+ /api/v1//'], // [!code ++] 225 225 }, 226 226 }, 227 227 path: 'https://get.heyapi.dev/hey-api/backend', ··· 290 290 291 291 ### Schemas 292 292 293 - Set `include` to match schemas to be included or `exclude` to match schemas to be excluded. When both rules match the same schema, `exclude` takes precedence over `include`. 293 + Set `include` to match schemas to be included or `exclude` to match schemas to be excluded. Both exact keys and regular expressions are supported. When both rules match the same schema, `exclude` takes precedence over `include`. 294 294 295 295 ::: code-group 296 296 ··· 299 299 input: { 300 300 filters: { 301 301 schemas: { 302 - include: ['Foo'], // [!code ++] 302 + include: ['Foo', '/^Bar/'], // [!code ++] 303 303 }, 304 304 }, 305 305 path: 'https://get.heyapi.dev/hey-api/backend', ··· 314 314 input: { 315 315 filters: { 316 316 schemas: { 317 - exclude: ['Foo'], // [!code ++] 317 + exclude: ['Foo', '/^Bar/'], // [!code ++] 318 + }, 319 + }, 320 + path: 'https://get.heyapi.dev/hey-api/backend', 321 + }, 322 + output: 'src/client', 323 + plugins: ['@hey-api/client-fetch'], 324 + }; 325 + ``` 326 + 327 + ::: 328 + 329 + ### Parameters 330 + 331 + Set `include` to match parameters to be included or `exclude` to match parameters to be excluded. Both exact keys and regular expressions are supported. When both rules match the same parameter, `exclude` takes precedence over `include`. 332 + 333 + ::: code-group 334 + 335 + ```js [include] 336 + export default { 337 + input: { 338 + filters: { 339 + parameters: { 340 + include: ['QueryParameter', '/^MyQueryParameter/'], // [!code ++] 341 + }, 342 + }, 343 + path: 'https://get.heyapi.dev/hey-api/backend', 344 + }, 345 + output: 'src/client', 346 + plugins: ['@hey-api/client-fetch'], 347 + }; 348 + ``` 349 + 350 + ```js [exclude] 351 + export default { 352 + input: { 353 + filters: { 354 + parameters: { 355 + exclude: ['QueryParameter', '/^MyQueryParameter/'], // [!code ++] 318 356 }, 319 357 }, 320 358 path: 'https://get.heyapi.dev/hey-api/backend', ··· 328 366 329 367 ### Request Bodies 330 368 331 - Set `include` to match request bodies to be included or `exclude` to match request bodies to be excluded. When both rules match the same request body, `exclude` takes precedence over `include`. 369 + Set `include` to match request bodies to be included or `exclude` to match request bodies to be excluded. Both exact keys and regular expressions are supported. When both rules match the same request body, `exclude` takes precedence over `include`. 332 370 333 371 ::: code-group 334 372 ··· 337 375 input: { 338 376 filters: { 339 377 requestBodies: { 340 - include: ['Payload'], // [!code ++] 378 + include: ['Payload', '/^SpecialPayload/'], // [!code ++] 341 379 }, 342 380 }, 343 381 path: 'https://get.heyapi.dev/hey-api/backend', ··· 352 390 input: { 353 391 filters: { 354 392 requestBodies: { 355 - exclude: ['Payload'], // [!code ++] 393 + exclude: ['Payload', '/^SpecialPayload/'], // [!code ++] 394 + }, 395 + }, 396 + path: 'https://get.heyapi.dev/hey-api/backend', 397 + }, 398 + output: 'src/client', 399 + plugins: ['@hey-api/client-fetch'], 400 + }; 401 + ``` 402 + 403 + ::: 404 + 405 + ### Responses 406 + 407 + Set `include` to match responses to be included or `exclude` to match responses to be excluded. Both exact keys and regular expressions are supported. When both rules match the same response, `exclude` takes precedence over `include`. 408 + 409 + ::: code-group 410 + 411 + ```js [include] 412 + export default { 413 + input: { 414 + filters: { 415 + responses: { 416 + include: ['Foo', '/^Bar/'], // [!code ++] 417 + }, 418 + }, 419 + path: 'https://get.heyapi.dev/hey-api/backend', 420 + }, 421 + output: 'src/client', 422 + plugins: ['@hey-api/client-fetch'], 423 + }; 424 + ``` 425 + 426 + ```js [exclude] 427 + export default { 428 + input: { 429 + filters: { 430 + responses: { 431 + exclude: ['Foo', '/^Bar/'], // [!code ++] 356 432 }, 357 433 }, 358 434 path: 'https://get.heyapi.dev/hey-api/backend',
+10 -7
packages/openapi-ts-tests/test/openapi-ts.config.ts
··· 23 23 // }, 24 24 filters: { 25 25 // deprecated: false, 26 - // operations: { 27 - // include: ['POST /foo'], 28 - // }, 26 + operations: { 27 + include: [ 28 + // 'PUT /foo', 29 + '/^[A-Z]+ /v1//', 30 + ], 31 + }, 29 32 // orphans: false, 30 33 // preserveOrder: true, 31 34 // tags: { ··· 41 44 // openapi: '3.1.0', 42 45 // paths: {}, 43 46 // }, 44 - path: path.resolve(__dirname, 'spec', '3.1.x', 'validators.json'), 47 + path: path.resolve(__dirname, 'spec', '3.1.x', 'parser-filters.yaml'), 45 48 // path: 'http://localhost:4000/', 46 49 // path: 'https://get.heyapi.dev/', 47 50 // path: 'https://get.heyapi.dev/hey-api/backend?branch=main&version=1.0.0', ··· 85 88 // type: 'json', 86 89 }, 87 90 { 88 - asClass: true, 91 + // asClass: true, 89 92 // auth: false, 90 93 client: false, 91 94 // include... ··· 125 128 { 126 129 // comments: false, 127 130 // exportFromIndex: true, 128 - name: 'valibot', 131 + // name: 'valibot', 129 132 }, 130 133 { 131 134 // comments: false, 132 135 // exportFromIndex: true, 133 - name: 'zod', 136 + // name: 'zod', 134 137 }, 135 138 ], 136 139 // useOptions: false,
+12 -2
packages/openapi-ts-tests/test/spec/3.1.x/parser-filters.yaml
··· 3 3 title: OpenAPI 3.1.1 parser filters example 4 4 version: 1 5 5 paths: 6 - /foo: 6 + /v1/foo: 7 7 post: 8 8 deprecated: true 9 9 tags: 10 10 - foo 11 11 - bar 12 + parameters: 13 + - $ref: '#/components/parameters/Foo' 12 14 requestBody: 13 15 $ref: '#/components/requestBodies/Foo' 14 16 responses: ··· 30 32 schema: 31 33 $ref: '#/components/schemas/Baz' 32 34 description: OK 33 - /bar: 35 + /v2/bar: 34 36 post: 35 37 requestBody: 36 38 content: ··· 46 48 $ref: '#/components/schemas/Bar' 47 49 description: OK 48 50 components: 51 + parameters: 52 + Foo: 53 + name: foo 54 + in: query 55 + description: Query parameter 56 + required: false 57 + schema: 58 + type: string 49 59 requestBodies: 50 60 Foo: 51 61 required: true
+2
packages/openapi-ts/src/openApi/2.0.x/parser/filter.ts
··· 17 17 spec, 18 18 }: { 19 19 operations: Set<string>; 20 + parameters: Set<string>; 20 21 preserveOrder: boolean; 21 22 requestBodies: Set<string>; 23 + responses: Set<string>; 22 24 schemas: Set<string>; 23 25 spec: OpenApiV2_0_X; 24 26 }) => {
+1 -1
packages/openapi-ts/src/openApi/2.0.x/parser/index.ts
··· 26 26 export const parseV2_0_X = (context: IR.Context<OpenApiV2_0_X>) => { 27 27 if (hasFilters(context.config.input.filters)) { 28 28 const graph = createGraph(context.spec); 29 - const filters = createFilters(context.config.input.filters); 29 + const filters = createFilters(context.config.input.filters, context.spec); 30 30 const sets = createFilteredDependencies({ filters, graph }); 31 31 filterSpec({ 32 32 ...sets,
+52
packages/openapi-ts/src/openApi/3.0.x/parser/filter.ts
··· 7 7 */ 8 8 export const filterSpec = ({ 9 9 operations, 10 + parameters, 10 11 preserveOrder, 11 12 requestBodies, 13 + responses, 12 14 schemas, 13 15 spec, 14 16 }: { 15 17 operations: Set<string>; 18 + parameters: Set<string>; 16 19 preserveOrder: boolean; 17 20 requestBodies: Set<string>; 21 + responses: Set<string>; 18 22 schemas: Set<string>; 19 23 spec: OpenApiV3_0_X; 20 24 }) => { 21 25 if (spec.components) { 26 + if (spec.components.parameters) { 27 + const filtered: typeof spec.components.parameters = {}; 28 + 29 + if (preserveOrder) { 30 + for (const [name, source] of Object.entries( 31 + spec.components.parameters, 32 + )) { 33 + if (parameters.has(addNamespace('parameter', name))) { 34 + filtered[name] = source; 35 + } 36 + } 37 + } else { 38 + for (const key of parameters) { 39 + const { name } = removeNamespace(key); 40 + const source = spec.components.parameters[name]; 41 + if (source) { 42 + filtered[name] = source; 43 + } 44 + } 45 + } 46 + 47 + spec.components.parameters = filtered; 48 + } 49 + 22 50 if (spec.components.requestBodies) { 23 51 const filtered: typeof spec.components.requestBodies = {}; 24 52 ··· 41 69 } 42 70 43 71 spec.components.requestBodies = filtered; 72 + } 73 + 74 + if (spec.components.responses) { 75 + const filtered: typeof spec.components.responses = {}; 76 + 77 + if (preserveOrder) { 78 + for (const [name, source] of Object.entries( 79 + spec.components.responses, 80 + )) { 81 + if (responses.has(addNamespace('response', name))) { 82 + filtered[name] = source; 83 + } 84 + } 85 + } else { 86 + for (const key of responses) { 87 + const { name } = removeNamespace(key); 88 + const source = spec.components.responses[name]; 89 + if (source) { 90 + filtered[name] = source; 91 + } 92 + } 93 + } 94 + 95 + spec.components.responses = filtered; 44 96 } 45 97 46 98 if (spec.components.schemas) {
+1 -1
packages/openapi-ts/src/openApi/3.0.x/parser/index.ts
··· 25 25 export const parseV3_0_X = (context: IR.Context<OpenApiV3_0_X>) => { 26 26 if (hasFilters(context.config.input.filters)) { 27 27 const graph = createGraph(context.spec); 28 - const filters = createFilters(context.config.input.filters); 28 + const filters = createFilters(context.config.input.filters, context.spec); 29 29 const sets = createFilteredDependencies({ filters, graph }); 30 30 filterSpec({ 31 31 ...sets,
+52
packages/openapi-ts/src/openApi/3.1.x/parser/filter.ts
··· 7 7 */ 8 8 export const filterSpec = ({ 9 9 operations, 10 + parameters, 10 11 preserveOrder, 11 12 requestBodies, 13 + responses, 12 14 schemas, 13 15 spec, 14 16 }: { 15 17 operations: Set<string>; 18 + parameters: Set<string>; 16 19 preserveOrder: boolean; 17 20 requestBodies: Set<string>; 21 + responses: Set<string>; 18 22 schemas: Set<string>; 19 23 spec: OpenApiV3_1_X; 20 24 }) => { 21 25 if (spec.components) { 26 + if (spec.components.parameters) { 27 + const filtered: typeof spec.components.parameters = {}; 28 + 29 + if (preserveOrder) { 30 + for (const [name, source] of Object.entries( 31 + spec.components.parameters, 32 + )) { 33 + if (parameters.has(addNamespace('parameter', name))) { 34 + filtered[name] = source; 35 + } 36 + } 37 + } else { 38 + for (const key of parameters) { 39 + const { name } = removeNamespace(key); 40 + const source = spec.components.parameters[name]; 41 + if (source) { 42 + filtered[name] = source; 43 + } 44 + } 45 + } 46 + 47 + spec.components.parameters = filtered; 48 + } 49 + 22 50 if (spec.components.requestBodies) { 23 51 const filtered: typeof spec.components.requestBodies = {}; 24 52 ··· 41 69 } 42 70 43 71 spec.components.requestBodies = filtered; 72 + } 73 + 74 + if (spec.components.responses) { 75 + const filtered: typeof spec.components.responses = {}; 76 + 77 + if (preserveOrder) { 78 + for (const [name, source] of Object.entries( 79 + spec.components.responses, 80 + )) { 81 + if (responses.has(addNamespace('response', name))) { 82 + filtered[name] = source; 83 + } 84 + } 85 + } else { 86 + for (const key of responses) { 87 + const { name } = removeNamespace(key); 88 + const source = spec.components.responses[name]; 89 + if (source) { 90 + filtered[name] = source; 91 + } 92 + } 93 + } 94 + 95 + spec.components.responses = filtered; 44 96 } 45 97 46 98 if (spec.components.schemas) {
+1 -1
packages/openapi-ts/src/openApi/3.1.x/parser/index.ts
··· 25 25 export const parseV3_1_X = (context: IR.Context<OpenApiV3_1_X>) => { 26 26 if (hasFilters(context.config.input.filters)) { 27 27 const graph = createGraph(context.spec); 28 - const filters = createFilters(context.config.input.filters); 28 + const filters = createFilters(context.config.input.filters, context.spec); 29 29 const sets = createFilteredDependencies({ filters, graph }); 30 30 filterSpec({ 31 31 ...sets,
+531 -30
packages/openapi-ts/src/openApi/shared/utils/filter.ts
··· 1 1 import type { Config } from '../../../types/config'; 2 - import type { Graph } from './graph'; 2 + import type { PathItemObject, PathsObject } from '../../3.1.x/types/spec'; 3 + import type { OpenApi } from '../../types'; 4 + import type { Graph, GraphType } from './graph'; 3 5 import { addNamespace, removeNamespace } from './graph'; 6 + import { httpMethods } from './operation'; 4 7 5 8 type FiltersConfigToState<T> = { 6 9 [K in keyof T]-?: NonNullable<T[K]> extends ReadonlyArray<infer U> ··· 14 17 NonNullable<Config['input']['filters']> 15 18 >; 16 19 17 - export const createFilters = (config: Config['input']['filters']): Filters => { 20 + interface SetAndRegExps { 21 + regexps: Array<RegExp>; 22 + set: Set<string>; 23 + } 24 + 25 + const createFiltersSetAndRegExps = ( 26 + type: GraphType, 27 + filters: ReadonlyArray<string> | undefined, 28 + ): SetAndRegExps => { 29 + const keys: Array<string> = []; 30 + const regexps: Array<RegExp> = []; 31 + if (filters) { 32 + for (const value of filters) { 33 + if (value.startsWith('/') && value.endsWith('/')) { 34 + regexps.push(new RegExp(value.slice(1, value.length - 1))); 35 + } else { 36 + keys.push(addNamespace(type, value)); 37 + } 38 + } 39 + } 40 + return { 41 + regexps, 42 + set: new Set(keys), 43 + }; 44 + }; 45 + 46 + interface CollectFiltersSetFromRegExps { 47 + excludeOperations: SetAndRegExps; 48 + excludeParameters: SetAndRegExps; 49 + excludeRequestBodies: SetAndRegExps; 50 + excludeResponses: SetAndRegExps; 51 + excludeSchemas: SetAndRegExps; 52 + includeOperations: SetAndRegExps; 53 + includeParameters: SetAndRegExps; 54 + includeRequestBodies: SetAndRegExps; 55 + includeResponses: SetAndRegExps; 56 + includeSchemas: SetAndRegExps; 57 + } 58 + 59 + const collectFiltersSetFromRegExpsOpenApiV2 = ({ 60 + excludeOperations, 61 + excludeSchemas, 62 + includeOperations, 63 + includeSchemas, 64 + spec, 65 + }: CollectFiltersSetFromRegExps & { 66 + spec: OpenApi.V2_0_X; 67 + }) => { 68 + if ( 69 + (excludeOperations.regexps.length || includeOperations.regexps.length) && 70 + spec.paths 71 + ) { 72 + for (const entry of Object.entries(spec.paths)) { 73 + const path = entry[0] as keyof PathsObject; 74 + const pathItem = entry[1] as PathItemObject; 75 + for (const method of httpMethods) { 76 + const operation = pathItem[method]; 77 + if (!operation) { 78 + continue; 79 + } 80 + 81 + const key = `${method.toUpperCase()} ${path}`; 82 + if (excludeOperations.regexps.some((regexp) => regexp.test(key))) { 83 + excludeOperations.set.add(addNamespace('operation', key)); 84 + } 85 + if (includeOperations.regexps.some((regexp) => regexp.test(key))) { 86 + includeOperations.set.add(addNamespace('operation', key)); 87 + } 88 + } 89 + } 90 + } 91 + 92 + if (spec.definitions) { 93 + // TODO: add parameters 94 + 95 + if (excludeSchemas.regexps.length || includeSchemas.regexps.length) { 96 + for (const key of Object.keys(spec.definitions)) { 97 + if (excludeSchemas.regexps.some((regexp) => regexp.test(key))) { 98 + excludeSchemas.set.add(addNamespace('schema', key)); 99 + } 100 + if (includeSchemas.regexps.some((regexp) => regexp.test(key))) { 101 + includeSchemas.set.add(addNamespace('schema', key)); 102 + } 103 + } 104 + } 105 + } 106 + }; 107 + 108 + const collectFiltersSetFromRegExpsOpenApiV3 = ({ 109 + excludeOperations, 110 + excludeParameters, 111 + excludeRequestBodies, 112 + excludeResponses, 113 + excludeSchemas, 114 + includeOperations, 115 + includeParameters, 116 + includeRequestBodies, 117 + includeResponses, 118 + includeSchemas, 119 + spec, 120 + }: CollectFiltersSetFromRegExps & { 121 + spec: OpenApi.V3_0_X | OpenApi.V3_1_X; 122 + }) => { 123 + if ( 124 + (excludeOperations.regexps.length || includeOperations.regexps.length) && 125 + spec.paths 126 + ) { 127 + for (const entry of Object.entries(spec.paths)) { 128 + const path = entry[0] as keyof PathsObject; 129 + const pathItem = entry[1] as PathItemObject; 130 + for (const method of httpMethods) { 131 + const operation = pathItem[method]; 132 + if (!operation) { 133 + continue; 134 + } 135 + 136 + const key = `${method.toUpperCase()} ${path}`; 137 + if (excludeOperations.regexps.some((regexp) => regexp.test(key))) { 138 + excludeOperations.set.add(addNamespace('operation', key)); 139 + } 140 + if (includeOperations.regexps.some((regexp) => regexp.test(key))) { 141 + includeOperations.set.add(addNamespace('operation', key)); 142 + } 143 + } 144 + } 145 + } 146 + 147 + if (spec.components) { 148 + if ( 149 + (excludeParameters.regexps.length || includeParameters.regexps.length) && 150 + spec.components.parameters 151 + ) { 152 + for (const key of Object.keys(spec.components.parameters)) { 153 + if (excludeParameters.regexps.some((regexp) => regexp.test(key))) { 154 + excludeParameters.set.add(addNamespace('parameter', key)); 155 + } 156 + if (includeParameters.regexps.some((regexp) => regexp.test(key))) { 157 + includeParameters.set.add(addNamespace('parameter', key)); 158 + } 159 + } 160 + } 161 + 162 + if ( 163 + (excludeRequestBodies.regexps.length || 164 + includeRequestBodies.regexps.length) && 165 + spec.components.requestBodies 166 + ) { 167 + for (const key of Object.keys(spec.components.requestBodies)) { 168 + if (excludeRequestBodies.regexps.some((regexp) => regexp.test(key))) { 169 + excludeRequestBodies.set.add(addNamespace('body', key)); 170 + } 171 + if (includeRequestBodies.regexps.some((regexp) => regexp.test(key))) { 172 + includeRequestBodies.set.add(addNamespace('body', key)); 173 + } 174 + } 175 + } 176 + 177 + if ( 178 + (excludeResponses.regexps.length || includeResponses.regexps.length) && 179 + spec.components.responses 180 + ) { 181 + for (const key of Object.keys(spec.components.responses)) { 182 + if (excludeResponses.regexps.some((regexp) => regexp.test(key))) { 183 + excludeResponses.set.add(addNamespace('response', key)); 184 + } 185 + if (includeResponses.regexps.some((regexp) => regexp.test(key))) { 186 + includeResponses.set.add(addNamespace('response', key)); 187 + } 188 + } 189 + } 190 + 191 + if ( 192 + (excludeSchemas.regexps.length || includeSchemas.regexps.length) && 193 + spec.components.schemas 194 + ) { 195 + for (const key of Object.keys(spec.components.schemas)) { 196 + if (excludeSchemas.regexps.some((regexp) => regexp.test(key))) { 197 + excludeSchemas.set.add(addNamespace('schema', key)); 198 + } 199 + if (includeSchemas.regexps.some((regexp) => regexp.test(key))) { 200 + includeSchemas.set.add(addNamespace('schema', key)); 201 + } 202 + } 203 + } 204 + } 205 + }; 206 + 207 + const collectFiltersSetFromRegExps = ({ 208 + spec, 209 + ...filters 210 + }: CollectFiltersSetFromRegExps & { 211 + spec: OpenApi.V2_0_X | OpenApi.V3_0_X | OpenApi.V3_1_X; 212 + }): void => { 213 + if ('swagger' in spec) { 214 + collectFiltersSetFromRegExpsOpenApiV2({ ...filters, spec }); 215 + } else { 216 + collectFiltersSetFromRegExpsOpenApiV3({ ...filters, spec }); 217 + } 218 + }; 219 + 220 + export const createFilters = ( 221 + config: Config['input']['filters'], 222 + spec: OpenApi.V2_0_X | OpenApi.V3_0_X | OpenApi.V3_1_X, 223 + ): Filters => { 224 + const excludeOperations = createFiltersSetAndRegExps( 225 + 'operation', 226 + config?.operations?.exclude, 227 + ); 228 + const includeOperations = createFiltersSetAndRegExps( 229 + 'operation', 230 + config?.operations?.include, 231 + ); 232 + const excludeParameters = createFiltersSetAndRegExps( 233 + 'parameter', 234 + config?.parameters?.exclude, 235 + ); 236 + const includeParameters = createFiltersSetAndRegExps( 237 + 'parameter', 238 + config?.parameters?.include, 239 + ); 240 + const excludeRequestBodies = createFiltersSetAndRegExps( 241 + 'body', 242 + config?.requestBodies?.exclude, 243 + ); 244 + const includeRequestBodies = createFiltersSetAndRegExps( 245 + 'body', 246 + config?.requestBodies?.include, 247 + ); 248 + const excludeResponses = createFiltersSetAndRegExps( 249 + 'response', 250 + config?.responses?.exclude, 251 + ); 252 + const includeResponses = createFiltersSetAndRegExps( 253 + 'response', 254 + config?.responses?.include, 255 + ); 256 + const excludeSchemas = createFiltersSetAndRegExps( 257 + 'schema', 258 + config?.schemas?.exclude, 259 + ); 260 + const includeSchemas = createFiltersSetAndRegExps( 261 + 'schema', 262 + config?.schemas?.include, 263 + ); 264 + 265 + collectFiltersSetFromRegExps({ 266 + excludeOperations, 267 + excludeParameters, 268 + excludeRequestBodies, 269 + excludeResponses, 270 + excludeSchemas, 271 + includeOperations, 272 + includeParameters, 273 + includeRequestBodies, 274 + includeResponses, 275 + includeSchemas, 276 + spec, 277 + }); 278 + 18 279 const filters: Filters = { 19 280 deprecated: config?.deprecated ?? true, 20 281 operations: { 21 - exclude: new Set( 22 - config?.operations?.exclude?.map((value) => 23 - addNamespace('operation', value), 24 - ), 25 - ), 26 - include: new Set( 27 - config?.operations?.include?.map((value) => 28 - addNamespace('operation', value), 29 - ), 30 - ), 282 + exclude: excludeOperations.set, 283 + include: includeOperations.set, 31 284 }, 32 285 orphans: config?.orphans ?? false, 286 + parameters: { 287 + exclude: excludeParameters.set, 288 + include: includeParameters.set, 289 + }, 33 290 preserveOrder: config?.preserveOrder ?? false, 34 291 requestBodies: { 35 - exclude: new Set( 36 - config?.requestBodies?.exclude?.map((value) => 37 - addNamespace('body', value), 38 - ), 39 - ), 40 - include: new Set( 41 - config?.requestBodies?.include?.map((value) => 42 - addNamespace('body', value), 43 - ), 44 - ), 292 + exclude: excludeRequestBodies.set, 293 + include: includeRequestBodies.set, 294 + }, 295 + responses: { 296 + exclude: excludeResponses.set, 297 + include: includeResponses.set, 45 298 }, 46 299 schemas: { 47 - exclude: new Set( 48 - config?.schemas?.exclude?.map((value) => addNamespace('schema', value)), 49 - ), 50 - include: new Set( 51 - config?.schemas?.include?.map((value) => addNamespace('schema', value)), 52 - ), 300 + exclude: excludeSchemas.set, 301 + include: includeSchemas.set, 53 302 }, 54 303 tags: { 55 304 exclude: new Set(config?.tags?.exclude), ··· 72 321 return Boolean( 73 322 config.operations?.exclude?.length || 74 323 config.operations?.include?.length || 324 + config.parameters?.exclude?.length || 325 + config.parameters?.include?.length || 75 326 config.requestBodies?.exclude?.length || 76 327 config.requestBodies?.include?.length || 328 + config.responses?.exclude?.length || 329 + config.responses?.include?.length || 77 330 config.schemas?.exclude?.length || 78 331 config.schemas?.include?.length || 79 332 config.tags?.exclude?.length || ··· 87 340 const collectOperations = ({ 88 341 filters, 89 342 graph, 343 + parameters, 90 344 requestBodies, 345 + responses, 91 346 schemas, 92 347 }: { 93 348 filters: Filters; 94 349 graph: Graph; 350 + parameters: Set<string>; 95 351 requestBodies: Set<string>; 352 + responses: Set<string>; 96 353 schemas: Set<string>; 97 354 }): { 98 355 operations: Set<string>; ··· 142 399 switch (namespace) { 143 400 case 'body': 144 401 return !requestBodies.has(dependency); 402 + case 'parameter': 403 + return !parameters.has(dependency); 404 + case 'response': 405 + return !responses.has(dependency); 145 406 case 'schema': 146 407 return !schemas.has(dependency); 147 408 default: ··· 158 419 }; 159 420 160 421 /** 161 - * Collect requestBodies that satisfy the include/exclude filters and schema dependencies. 422 + * Collect parameters that satisfy the include/exclude filters and schema dependencies. 423 + */ 424 + const collectParameters = ({ 425 + filters, 426 + graph, 427 + schemas, 428 + }: { 429 + filters: Filters; 430 + graph: Graph; 431 + schemas: Set<string>; 432 + }): { 433 + parameters: Set<string>; 434 + } => { 435 + const finalSet = new Set<string>(); 436 + const initialSet = filters.parameters.include.size 437 + ? filters.parameters.include 438 + : new Set(graph.parameters.keys()); 439 + const stack = [...initialSet]; 440 + while (stack.length) { 441 + const key = stack.pop()!; 442 + 443 + if (filters.parameters.exclude.has(key) || finalSet.has(key)) { 444 + continue; 445 + } 446 + 447 + const node = graph.parameters.get(key); 448 + 449 + if (!node) { 450 + continue; 451 + } 452 + 453 + if (!filters.deprecated && node.deprecated) { 454 + continue; 455 + } 456 + 457 + finalSet.add(key); 458 + 459 + if (!node.dependencies.size) { 460 + continue; 461 + } 462 + 463 + for (const dependency of node.dependencies) { 464 + const { namespace } = removeNamespace(dependency); 465 + switch (namespace) { 466 + case 'body': { 467 + if (filters.requestBodies.exclude.has(dependency)) { 468 + finalSet.delete(key); 469 + } else if (!finalSet.has(dependency)) { 470 + stack.push(dependency); 471 + } 472 + break; 473 + } 474 + case 'schema': { 475 + if (filters.schemas.exclude.has(dependency)) { 476 + finalSet.delete(key); 477 + } else if (!schemas.has(dependency)) { 478 + schemas.add(dependency); 479 + } 480 + break; 481 + } 482 + } 483 + } 484 + } 485 + return { parameters: finalSet }; 486 + }; 487 + 488 + /** 489 + * Collect request bodies that satisfy the include/exclude filters and schema dependencies. 162 490 */ 163 491 const collectRequestBodies = ({ 164 492 filters, ··· 225 553 }; 226 554 227 555 /** 556 + * Collect responses that satisfy the include/exclude filters and schema dependencies. 557 + */ 558 + const collectResponses = ({ 559 + filters, 560 + graph, 561 + schemas, 562 + }: { 563 + filters: Filters; 564 + graph: Graph; 565 + schemas: Set<string>; 566 + }): { 567 + responses: Set<string>; 568 + } => { 569 + const finalSet = new Set<string>(); 570 + const initialSet = filters.responses.include.size 571 + ? filters.responses.include 572 + : new Set(graph.responses.keys()); 573 + const stack = [...initialSet]; 574 + while (stack.length) { 575 + const key = stack.pop()!; 576 + 577 + if (filters.responses.exclude.has(key) || finalSet.has(key)) { 578 + continue; 579 + } 580 + 581 + const node = graph.responses.get(key); 582 + 583 + if (!node) { 584 + continue; 585 + } 586 + 587 + if (!filters.deprecated && node.deprecated) { 588 + continue; 589 + } 590 + 591 + finalSet.add(key); 592 + 593 + if (!node.dependencies.size) { 594 + continue; 595 + } 596 + 597 + for (const dependency of node.dependencies) { 598 + const { namespace } = removeNamespace(dependency); 599 + switch (namespace) { 600 + case 'body': { 601 + if (filters.requestBodies.exclude.has(dependency)) { 602 + finalSet.delete(key); 603 + } else if (!finalSet.has(dependency)) { 604 + stack.push(dependency); 605 + } 606 + break; 607 + } 608 + case 'schema': { 609 + if (filters.schemas.exclude.has(dependency)) { 610 + finalSet.delete(key); 611 + } else if (!schemas.has(dependency)) { 612 + schemas.add(dependency); 613 + } 614 + break; 615 + } 616 + } 617 + } 618 + } 619 + return { responses: finalSet }; 620 + }; 621 + 622 + /** 228 623 * Collect schemas that satisfy the include/exclude filters. 229 624 */ 230 625 const collectSchemas = ({ ··· 283 678 }; 284 679 285 680 /** 681 + * Drop parameters that depend on already excluded parameters. 682 + */ 683 + const dropExcludedParameters = ({ 684 + filters, 685 + graph, 686 + parameters, 687 + }: { 688 + filters: Filters; 689 + graph: Graph; 690 + parameters: Set<string>; 691 + }): void => { 692 + if (!filters.parameters.exclude.size) { 693 + return; 694 + } 695 + 696 + for (const key of parameters) { 697 + const node = graph.parameters.get(key); 698 + 699 + if (!node?.dependencies.size) { 700 + continue; 701 + } 702 + 703 + for (const excludedKey of filters.parameters.exclude) { 704 + if (node.dependencies.has(excludedKey)) { 705 + parameters.delete(key); 706 + break; 707 + } 708 + } 709 + } 710 + }; 711 + 712 + /** 286 713 * Drop request bodies that depend on already excluded request bodies. 287 714 */ 288 715 const dropExcludedRequestBodies = ({ ··· 315 742 }; 316 743 317 744 /** 745 + * Drop responses that depend on already excluded responses. 746 + */ 747 + const dropExcludedResponses = ({ 748 + filters, 749 + graph, 750 + responses, 751 + }: { 752 + filters: Filters; 753 + graph: Graph; 754 + responses: Set<string>; 755 + }): void => { 756 + if (!filters.responses.exclude.size) { 757 + return; 758 + } 759 + 760 + for (const key of responses) { 761 + const node = graph.responses.get(key); 762 + 763 + if (!node?.dependencies.size) { 764 + continue; 765 + } 766 + 767 + for (const excludedKey of filters.responses.exclude) { 768 + if (node.dependencies.has(excludedKey)) { 769 + responses.delete(key); 770 + break; 771 + } 772 + } 773 + } 774 + }; 775 + 776 + /** 318 777 * Drop schemas that depend on already excluded schemas. 319 778 */ 320 779 const dropExcludedSchemas = ({ ··· 348 807 349 808 const dropOrphans = ({ 350 809 operationDependencies, 810 + parameters, 351 811 requestBodies, 812 + responses, 352 813 schemas, 353 814 }: { 354 815 operationDependencies: Set<string>; 816 + parameters: Set<string>; 355 817 requestBodies: Set<string>; 818 + responses: Set<string>; 356 819 schemas: Set<string>; 357 820 }) => { 358 821 for (const key of schemas) { 359 822 if (!operationDependencies.has(key)) { 360 823 schemas.delete(key); 824 + } 825 + } 826 + for (const key of parameters) { 827 + if (!operationDependencies.has(key)) { 828 + parameters.delete(key); 361 829 } 362 830 } 363 831 for (const key of requestBodies) { ··· 365 833 requestBodies.delete(key); 366 834 } 367 835 } 836 + for (const key of responses) { 837 + if (!operationDependencies.has(key)) { 838 + responses.delete(key); 839 + } 840 + } 368 841 }; 369 842 370 843 const collectOperationDependencies = ({ ··· 398 871 dependencies = graph.requestBodies.get(key)?.dependencies; 399 872 } else if (namespace === 'operation') { 400 873 dependencies = graph.operations.get(key)?.dependencies; 874 + } else if (namespace === 'parameter') { 875 + dependencies = graph.parameters.get(key)?.dependencies; 876 + } else if (namespace === 'response') { 877 + dependencies = graph.responses.get(key)?.dependencies; 401 878 } else if (namespace === 'schema') { 402 879 dependencies = graph.schemas.get(key)?.dependencies; 403 880 } ··· 423 900 graph: Graph; 424 901 }): { 425 902 operations: Set<string>; 903 + parameters: Set<string>; 426 904 requestBodies: Set<string>; 905 + responses: Set<string>; 427 906 schemas: Set<string>; 428 907 } => { 429 908 const { schemas } = collectSchemas({ filters, graph }); 909 + const { parameters } = collectParameters({ 910 + filters, 911 + graph, 912 + schemas, 913 + }); 430 914 const { requestBodies } = collectRequestBodies({ 431 915 filters, 432 916 graph, 433 917 schemas, 434 918 }); 919 + const { responses } = collectResponses({ 920 + filters, 921 + graph, 922 + schemas, 923 + }); 435 924 436 925 dropExcludedSchemas({ filters, graph, schemas }); 926 + dropExcludedParameters({ filters, graph, parameters }); 437 927 dropExcludedRequestBodies({ filters, graph, requestBodies }); 928 + dropExcludedResponses({ filters, graph, responses }); 438 929 439 930 // collect operations after dropping components 440 931 const { operations } = collectOperations({ 441 932 filters, 442 933 graph, 934 + parameters, 443 935 requestBodies, 936 + responses, 444 937 schemas, 445 938 }); 446 939 ··· 449 942 graph, 450 943 operations, 451 944 }); 452 - dropOrphans({ operationDependencies, requestBodies, schemas }); 945 + dropOrphans({ 946 + operationDependencies, 947 + parameters, 948 + requestBodies, 949 + responses, 950 + schemas, 951 + }); 453 952 } 454 953 455 954 return { 456 955 operations, 956 + parameters, 457 957 requestBodies, 958 + responses, 458 959 schemas, 459 960 }; 460 961 };
+94 -11
packages/openapi-ts/src/openApi/shared/utils/graph.ts
··· 17 17 tags: Set<string>; 18 18 } 19 19 >; 20 - // TODO: add parameters 20 + parameters: Map< 21 + string, 22 + { 23 + dependencies: Set<string>; 24 + deprecated: boolean; 25 + } 26 + >; 21 27 requestBodies: Map< 22 28 string, 23 29 { ··· 25 31 deprecated: boolean; 26 32 } 27 33 >; 34 + responses: Map< 35 + string, 36 + { 37 + dependencies: Set<string>; 38 + deprecated: boolean; 39 + } 40 + >; 28 41 schemas: Map< 29 42 string, 30 43 { ··· 34 47 >; 35 48 }; 36 49 37 - type Type = 'body' | 'operation' | 'schema' | 'unknown'; 50 + export type GraphType = 51 + | 'body' 52 + | 'operation' 53 + | 'parameter' 54 + | 'response' 55 + | 'schema' 56 + | 'unknown'; 38 57 39 58 /** 40 59 * Converts reference strings from OpenAPI $ref keywords into namespaces. 41 60 * @example '#/components/schemas/Foo' -> 'schema' 42 61 */ 43 - export const stringToNamespace = (value: string): Type => { 62 + export const stringToNamespace = (value: string): GraphType => { 44 63 switch (value) { 64 + case 'parameters': 65 + return 'parameter'; 45 66 case 'requestBodies': 46 67 return 'body'; 68 + case 'responses': 69 + return 'response'; 47 70 case 'definitions': 48 71 case 'schemas': 49 72 return 'schema'; ··· 52 75 } 53 76 }; 54 77 55 - export const addNamespace = (namespace: Type, value: string = ''): string => 56 - `${namespace}/${value}`; 78 + const namespaceNeedle = '/'; 79 + 80 + export const addNamespace = ( 81 + namespace: GraphType, 82 + value: string = '', 83 + ): string => `${namespace}${namespaceNeedle}${value}`; 57 84 58 85 export const removeNamespace = ( 59 86 key: string, 60 87 ): { 61 88 name: string; 62 - namespace: Type; 89 + namespace: GraphType; 63 90 } => { 64 - const [namespace, name] = key.split('/'); 91 + const index = key.indexOf(namespaceNeedle); 92 + const name = key.slice(index + 1); 65 93 return { 66 - name: name!, 67 - namespace: namespace! as Type, 94 + name, 95 + namespace: key.slice(0, index)! as GraphType, 68 96 }; 69 97 }; 70 98 ··· 80 108 const type = parts[parts.length - 2]; 81 109 const name = parts[parts.length - 1]; 82 110 if (type && name) { 83 - dependencies.add(addNamespace(stringToNamespace(type), name)); 111 + const namespace = stringToNamespace(type); 112 + if (namespace === 'unknown') { 113 + console.warn(`unsupported type: ${type}`); 114 + } 115 + dependencies.add(addNamespace(namespace, name)); 84 116 } 85 117 } 86 118 ··· 237 269 } 238 270 } 239 271 240 - // TODO: add parameters 272 + if (spec.components.parameters) { 273 + type Parameter = ExtractedType<typeof spec.components.parameters>; 274 + for (const [key, value] of Object.entries(spec.components.parameters)) { 275 + const parameter = value as Parameter; 276 + const dependencies = new Set<string>(); 277 + if ('$ref' in parameter) { 278 + collectSchemaDependencies(parameter, dependencies); 279 + } else { 280 + if (parameter.schema) { 281 + collectSchemaDependencies(parameter.schema, dependencies); 282 + } 283 + 284 + if (parameter.content) { 285 + for (const media of Object.values(parameter.content)) { 286 + if (media.schema) { 287 + collectSchemaDependencies(media.schema, dependencies); 288 + } 289 + } 290 + } 291 + } 292 + graph.parameters.set(addNamespace('parameter', key), { 293 + dependencies, 294 + deprecated: 295 + 'deprecated' in parameter ? Boolean(parameter.deprecated) : false, 296 + }); 297 + } 298 + } 241 299 242 300 if (spec.components.requestBodies) { 243 301 type RequestBody = ExtractedType<typeof spec.components.requestBodies>; ··· 256 314 } 257 315 } 258 316 graph.requestBodies.set(addNamespace('body', key), { 317 + dependencies, 318 + deprecated: false, 319 + }); 320 + } 321 + } 322 + 323 + if (spec.components.responses) { 324 + type Response = ExtractedType<typeof spec.components.responses>; 325 + for (const [key, value] of Object.entries(spec.components.responses)) { 326 + const response = value as Response; 327 + const dependencies = new Set<string>(); 328 + if ('$ref' in response) { 329 + collectSchemaDependencies(response, dependencies); 330 + } else { 331 + if (response.content) { 332 + for (const media of Object.values(response.content)) { 333 + if (media.schema) { 334 + collectSchemaDependencies(media.schema, dependencies); 335 + } 336 + } 337 + } 338 + } 339 + graph.responses.set(addNamespace('response', key), { 259 340 dependencies, 260 341 deprecated: false, 261 342 }); ··· 333 414 ): Graph => { 334 415 const graph: Graph = { 335 416 operations: new Map(), 417 + parameters: new Map(), 336 418 requestBodies: new Map(), 419 + responses: new Map(), 337 420 schemas: new Map(), 338 421 }; 339 422
+36
packages/openapi-ts/src/types/config.d.ts
··· 79 79 * @default false 80 80 */ 81 81 orphans?: boolean; 82 + parameters?: { 83 + /** 84 + * Prevent parameters matching the `exclude` filters from being processed. 85 + * 86 + * In case of conflicts, `exclude` takes precedence over `include`. 87 + * 88 + * @example ['QueryParam'] 89 + */ 90 + exclude?: ReadonlyArray<string>; 91 + /** 92 + * Process only parameters matching the `include` filters. 93 + * 94 + * In case of conflicts, `exclude` takes precedence over `include`. 95 + * 96 + * @example ['QueryParam'] 97 + */ 98 + include?: ReadonlyArray<string>; 99 + }; 82 100 /** 83 101 * Should we preserve the key order when overwriting your input? This 84 102 * option is disabled by default to improve performance. ··· 97 115 exclude?: ReadonlyArray<string>; 98 116 /** 99 117 * Process only request bodies matching the `include` filters. 118 + * 119 + * In case of conflicts, `exclude` takes precedence over `include`. 120 + * 121 + * @example ['Foo'] 122 + */ 123 + include?: ReadonlyArray<string>; 124 + }; 125 + responses?: { 126 + /** 127 + * Prevent responses matching the `exclude` filters from being processed. 128 + * 129 + * In case of conflicts, `exclude` takes precedence over `include`. 130 + * 131 + * @example ['Foo'] 132 + */ 133 + exclude?: ReadonlyArray<string>; 134 + /** 135 + * Process only responses matching the `include` filters. 100 136 * 101 137 * In case of conflicts, `exclude` takes precedence over `include`. 102 138 *