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.

Add patch.input and shorthand patch() function support

Co-authored-by: mrlubos <12529395+mrlubos@users.noreply.github.com>

+487 -116
+156 -116
packages/shared/src/config/parser/patch.ts
··· 1 1 import type { MaybeFunc } from '@hey-api/types'; 2 2 3 3 import type { 4 + OpenApi, 4 5 OpenApiMetaObject, 5 6 OpenApiOperationObject, 6 7 OpenApiParameterObject, ··· 9 10 OpenApiSchemaObject, 10 11 } from '../../openApi/types'; 11 12 12 - export type Patch = { 13 - /** 14 - * Patch the OpenAPI meta object in place. Useful for modifying general metadata such as title, description, version, or custom fields before further processing. 15 - * 16 - * @param meta The OpenAPI meta object for the current version. 17 - */ 18 - meta?: ( 19 - meta: OpenApiMetaObject.V2_0_X | OpenApiMetaObject.V3_0_X | OpenApiMetaObject.V3_1_X, 20 - ) => void; 21 - /** 22 - * Patch OpenAPI operations in place. The key is the operation method and operation path, and the function receives the operation object to modify directly. 23 - * 24 - * @example 25 - * operations: { 26 - * 'GET /foo': (operation) => { 27 - * operation.responses['200'].description = 'foo'; 28 - * } 29 - * } 30 - */ 31 - operations?: Record< 32 - string, 33 - ( 34 - operation: 35 - | OpenApiOperationObject.V2_0_X 36 - | OpenApiOperationObject.V3_0_X 37 - | OpenApiOperationObject.V3_1_X, 38 - ) => void 39 - >; 40 - /** 41 - * Patch OpenAPI parameters in place. The key is the parameter name, and the function receives the parameter object to modify directly. 42 - * 43 - * @example 44 - * parameters: { 45 - * limit: (parameter) => { 46 - * parameter.schema.type = 'integer'; 47 - * } 48 - * } 49 - */ 50 - parameters?: Record< 51 - string, 52 - (parameter: OpenApiParameterObject.V3_0_X | OpenApiParameterObject.V3_1_X) => void 53 - >; 54 - /** 55 - * Patch OpenAPI request bodies in place. The key is the request body name, and the function receives the request body object to modify directly. 56 - * 57 - * @example 58 - * requestBodies: { 59 - * CreateUserRequest: (requestBody) => { 60 - * requestBody.required = true; 61 - * } 62 - * } 63 - */ 64 - requestBodies?: Record< 65 - string, 66 - (requestBody: OpenApiRequestBodyObject.V3_0_X | OpenApiRequestBodyObject.V3_1_X) => void 67 - >; 68 - /** 69 - * Patch OpenAPI responses in place. The key is the response name, and the function receives the response object to modify directly. 70 - * 71 - * @example 72 - * responses: { 73 - * NotFound: (response) => { 74 - * response.description = 'Resource not found.'; 75 - * } 76 - * } 77 - */ 78 - responses?: Record< 79 - string, 80 - (response: OpenApiResponseObject.V3_0_X | OpenApiResponseObject.V3_1_X) => void 81 - >; 82 - /** 83 - * Each function receives the schema object to be modified in place. Common 84 - * use cases include fixing incorrect data types, removing unwanted 85 - * properties, adding missing fields, or standardizing date/time formats. 86 - * 87 - * @example 88 - * ```js 89 - * schemas: { 90 - * Foo: (schema) => { 91 - * // convert date-time format to timestamp 92 - * delete schema.properties.updatedAt.format; 93 - * schema.properties.updatedAt.type = 'number'; 94 - * }, 95 - * Bar: (schema) => { 96 - * // add missing property 97 - * schema.properties.metadata = { 98 - * additionalProperties: true, 99 - * type: 'object', 100 - * }; 101 - * schema.required = ['metadata']; 102 - * }, 103 - * Baz: (schema) => { 104 - * // remove property 105 - * delete schema.properties.internalField; 106 - * } 107 - * } 108 - * ``` 109 - */ 110 - schemas?: Record< 111 - string, 112 - ( 113 - schema: OpenApiSchemaObject.V2_0_X | OpenApiSchemaObject.V3_0_X | OpenApiSchemaObject.V3_1_X, 114 - ) => void 115 - >; 116 - /** 117 - * Patch the OpenAPI version string. The function receives the current version and should return the new version string. 118 - * Useful for normalizing or overriding the version value before further processing. 119 - * 120 - * @param version The current OpenAPI version string. 121 - * @returns The new version string to use. 122 - * 123 - * @example 124 - * version: (version) => version.replace(/^v/, '') 125 - */ 126 - version?: MaybeFunc<(version: string) => string>; 127 - }; 13 + export type Patch = 14 + | ((spec: OpenApi.V2_0_X | OpenApi.V3_0_X | OpenApi.V3_1_X) => void) 15 + | { 16 + /** 17 + * Patch the raw OpenAPI spec object in place. Called before all other 18 + * patch callbacks. Useful for bulk/structural transformations such as 19 + * adding new component definitions or modifying many operations at once. 20 + * 21 + * @param spec The OpenAPI spec object for the current version. 22 + * 23 + * @example 24 + * ```ts 25 + * input: (spec) => { 26 + * // Create new component parameters 27 + * if (!spec.components) spec.components = {}; 28 + * if (!spec.components.parameters) spec.components.parameters = {}; 29 + * spec.components.parameters.MyParam = { 30 + * in: 'query', 31 + * name: 'myParam', 32 + * schema: { type: 'string' } 33 + * }; 34 + * 35 + * // Inject parameters into operations 36 + * for (const [path, pathItem] of Object.entries(spec.paths ?? {})) { 37 + * if (pathItem?.get) { 38 + * if (!Array.isArray(pathItem.get.parameters)) { 39 + * pathItem.get.parameters = []; 40 + * } 41 + * pathItem.get.parameters.push({ 42 + * $ref: '#/components/parameters/MyParam' 43 + * }); 44 + * } 45 + * } 46 + * } 47 + * ``` 48 + */ 49 + input?: (spec: OpenApi.V2_0_X | OpenApi.V3_0_X | OpenApi.V3_1_X) => void; 50 + /** 51 + * Patch the OpenAPI meta object in place. Useful for modifying general metadata such as title, description, version, or custom fields before further processing. 52 + * 53 + * @param meta The OpenAPI meta object for the current version. 54 + */ 55 + meta?: ( 56 + meta: OpenApiMetaObject.V2_0_X | OpenApiMetaObject.V3_0_X | OpenApiMetaObject.V3_1_X, 57 + ) => void; 58 + /** 59 + * Patch OpenAPI operations in place. The key is the operation method and operation path, and the function receives the operation object to modify directly. 60 + * 61 + * @example 62 + * operations: { 63 + * 'GET /foo': (operation) => { 64 + * operation.responses['200'].description = 'foo'; 65 + * } 66 + * } 67 + */ 68 + operations?: Record< 69 + string, 70 + ( 71 + operation: 72 + | OpenApiOperationObject.V2_0_X 73 + | OpenApiOperationObject.V3_0_X 74 + | OpenApiOperationObject.V3_1_X, 75 + ) => void 76 + >; 77 + /** 78 + * Patch OpenAPI parameters in place. The key is the parameter name, and the function receives the parameter object to modify directly. 79 + * 80 + * @example 81 + * parameters: { 82 + * limit: (parameter) => { 83 + * parameter.schema.type = 'integer'; 84 + * } 85 + * } 86 + */ 87 + parameters?: Record< 88 + string, 89 + (parameter: OpenApiParameterObject.V3_0_X | OpenApiParameterObject.V3_1_X) => void 90 + >; 91 + /** 92 + * Patch OpenAPI request bodies in place. The key is the request body name, and the function receives the request body object to modify directly. 93 + * 94 + * @example 95 + * requestBodies: { 96 + * CreateUserRequest: (requestBody) => { 97 + * requestBody.required = true; 98 + * } 99 + * } 100 + */ 101 + requestBodies?: Record< 102 + string, 103 + (requestBody: OpenApiRequestBodyObject.V3_0_X | OpenApiRequestBodyObject.V3_1_X) => void 104 + >; 105 + /** 106 + * Patch OpenAPI responses in place. The key is the response name, and the function receives the response object to modify directly. 107 + * 108 + * @example 109 + * responses: { 110 + * NotFound: (response) => { 111 + * response.description = 'Resource not found.'; 112 + * } 113 + * } 114 + */ 115 + responses?: Record< 116 + string, 117 + (response: OpenApiResponseObject.V3_0_X | OpenApiResponseObject.V3_1_X) => void 118 + >; 119 + /** 120 + * Each function receives the schema object to be modified in place. Common 121 + * use cases include fixing incorrect data types, removing unwanted 122 + * properties, adding missing fields, or standardizing date/time formats. 123 + * 124 + * @example 125 + * ```js 126 + * schemas: { 127 + * Foo: (schema) => { 128 + * // convert date-time format to timestamp 129 + * delete schema.properties.updatedAt.format; 130 + * schema.properties.updatedAt.type = 'number'; 131 + * }, 132 + * Bar: (schema) => { 133 + * // add missing property 134 + * schema.properties.metadata = { 135 + * additionalProperties: true, 136 + * type: 'object', 137 + * }; 138 + * schema.required = ['metadata']; 139 + * }, 140 + * Baz: (schema) => { 141 + * // remove property 142 + * delete schema.properties.internalField; 143 + * } 144 + * } 145 + * ``` 146 + */ 147 + schemas?: Record< 148 + string, 149 + ( 150 + schema: 151 + | OpenApiSchemaObject.V2_0_X 152 + | OpenApiSchemaObject.V3_0_X 153 + | OpenApiSchemaObject.V3_1_X, 154 + ) => void 155 + >; 156 + /** 157 + * Patch the OpenAPI version string. The function receives the current version and should return the new version string. 158 + * Useful for normalizing or overriding the version value before further processing. 159 + * 160 + * @param version The current OpenAPI version string. 161 + * @returns The new version string to use. 162 + * 163 + * @example 164 + * version: (version) => version.replace(/^v/, '') 165 + */ 166 + version?: MaybeFunc<(version: string) => string>; 167 + };
+320
packages/shared/src/openApi/shared/utils/__tests__/patch.test.ts
··· 19 19 }; 20 20 21 21 describe('patchOpenApiSpec', () => { 22 + describe('patch.input', () => { 23 + describe('OpenAPI v3', () => { 24 + it('calls patch.input function before other patches', () => { 25 + const inputFn = vi.fn(); 26 + const metaFn = vi.fn(); 27 + const spec: OpenApi.V3_1_X = { 28 + ...specMetadataV3, 29 + }; 30 + 31 + patchOpenApiSpec({ 32 + patchOptions: { 33 + input: inputFn, 34 + meta: metaFn, 35 + }, 36 + spec, 37 + }); 38 + 39 + // Both should be called 40 + expect(inputFn).toHaveBeenCalledOnce(); 41 + expect(inputFn).toHaveBeenCalledWith(spec); 42 + expect(metaFn).toHaveBeenCalledOnce(); 43 + }); 44 + 45 + it('allows bulk creation of component parameters', () => { 46 + const spec: OpenApi.V3_1_X = { 47 + ...specMetadataV3, 48 + paths: {}, 49 + }; 50 + 51 + patchOpenApiSpec({ 52 + patchOptions: { 53 + input: (spec) => { 54 + if ('openapi' in spec) { 55 + if (!spec.components) spec.components = {}; 56 + if (!spec.components.parameters) spec.components.parameters = {}; 57 + spec.components.parameters.MyParam = { 58 + in: 'query', 59 + name: 'myParam', 60 + schema: { type: 'string' }, 61 + } as any; 62 + } 63 + }, 64 + }, 65 + spec, 66 + }); 67 + 68 + expect(spec.components?.parameters?.MyParam).toEqual({ 69 + in: 'query', 70 + name: 'myParam', 71 + schema: { type: 'string' }, 72 + }); 73 + }); 74 + 75 + it('allows injecting parameters into multiple operations', () => { 76 + const spec: OpenApi.V3_1_X = { 77 + ...specMetadataV3, 78 + components: { 79 + parameters: { 80 + SharedParam: { 81 + in: 'query', 82 + name: 'shared', 83 + schema: { type: 'string' }, 84 + } as any, 85 + }, 86 + }, 87 + paths: { 88 + '/bar': { 89 + get: { 90 + responses: {}, 91 + }, 92 + }, 93 + '/baz': { 94 + post: { 95 + responses: {}, 96 + }, 97 + }, 98 + '/foo': { 99 + get: { 100 + responses: {}, 101 + }, 102 + }, 103 + } as any, 104 + }; 105 + 106 + patchOpenApiSpec({ 107 + patchOptions: { 108 + input: (spec) => { 109 + // Inject parameter into all GET operations 110 + for (const [, pathItem] of Object.entries(spec.paths ?? {})) { 111 + if (pathItem?.get) { 112 + if (!Array.isArray(pathItem.get.parameters)) { 113 + pathItem.get.parameters = []; 114 + } 115 + (pathItem.get.parameters as any[]).push({ 116 + $ref: '#/components/parameters/SharedParam', 117 + }); 118 + } 119 + } 120 + }, 121 + }, 122 + spec, 123 + }); 124 + 125 + expect((spec.paths as any)['/foo'].get.parameters).toEqual([ 126 + { $ref: '#/components/parameters/SharedParam' }, 127 + ]); 128 + expect((spec.paths as any)['/bar'].get.parameters).toEqual([ 129 + { $ref: '#/components/parameters/SharedParam' }, 130 + ]); 131 + expect((spec.paths as any)['/baz'].post.parameters).toBeUndefined(); 132 + }); 133 + 134 + it('allows complex Redfish-like transformations', () => { 135 + const spec: OpenApi.V3_1_X = { 136 + ...specMetadataV3, 137 + paths: { 138 + '/other/path': { 139 + get: { 140 + responses: {}, 141 + }, 142 + }, 143 + '/redfish/v1/Chassis': { 144 + get: { 145 + responses: {}, 146 + }, 147 + }, 148 + '/redfish/v1/Systems': { 149 + get: { 150 + responses: {}, 151 + }, 152 + }, 153 + } as any, 154 + }; 155 + 156 + const QUERY_PARAMS = [ 157 + { description: 'Expand related resources.', key: '$expand' }, 158 + { description: 'Select subset.', key: '$select' }, 159 + ]; 160 + 161 + patchOpenApiSpec({ 162 + patchOptions: { 163 + input: (spec) => { 164 + if (!('openapi' in spec)) return; 165 + 166 + // 1. Create component parameters 167 + if (!spec.components) spec.components = {}; 168 + if (!spec.components.parameters) spec.components.parameters = {}; 169 + 170 + for (const param of QUERY_PARAMS) { 171 + (spec.components.parameters as any)[`Redfish_${param.key}`] = { 172 + description: param.description, 173 + in: 'query', 174 + name: param.key, 175 + required: false, 176 + schema: { type: 'string' }, 177 + }; 178 + } 179 + 180 + // 2. Inject into Redfish paths 181 + for (const [path, pathItem] of Object.entries(spec.paths ?? {})) { 182 + if (!path.startsWith('/redfish/v1')) continue; 183 + const getOp = pathItem?.get; 184 + if (!getOp) continue; 185 + 186 + if (!Array.isArray(getOp.parameters)) getOp.parameters = []; 187 + for (const param of QUERY_PARAMS) { 188 + (getOp.parameters as any[]).push({ 189 + $ref: `#/components/parameters/Redfish_${param.key}`, 190 + }); 191 + } 192 + } 193 + }, 194 + }, 195 + spec, 196 + }); 197 + 198 + // Verify component parameters were created 199 + expect(spec.components?.parameters).toHaveProperty('Redfish_$expand'); 200 + expect(spec.components?.parameters).toHaveProperty('Redfish_$select'); 201 + 202 + // Verify they were injected into Redfish paths 203 + expect((spec.paths as any)['/redfish/v1/Systems'].get.parameters).toHaveLength(2); 204 + expect((spec.paths as any)['/redfish/v1/Chassis'].get.parameters).toHaveLength(2); 205 + 206 + // Verify they were NOT injected into non-Redfish paths 207 + expect((spec.paths as any)['/other/path'].get.parameters).toBeUndefined(); 208 + }); 209 + }); 210 + 211 + describe('OpenAPI v2', () => { 212 + it('calls patch.input function for v2 specs', () => { 213 + const inputFn = vi.fn(); 214 + const spec: OpenApi.V2_0_X = { 215 + ...specMetadataV2, 216 + }; 217 + 218 + patchOpenApiSpec({ 219 + patchOptions: { 220 + input: inputFn, 221 + }, 222 + spec, 223 + }); 224 + 225 + expect(inputFn).toHaveBeenCalledOnce(); 226 + expect(inputFn).toHaveBeenCalledWith(spec); 227 + }); 228 + 229 + it('allows adding definitions in v2 specs', () => { 230 + const spec: OpenApi.V2_0_X = { 231 + ...specMetadataV2, 232 + }; 233 + 234 + patchOpenApiSpec({ 235 + patchOptions: { 236 + input: (spec) => { 237 + if ('swagger' in spec) { 238 + if (!spec.definitions) spec.definitions = {}; 239 + spec.definitions.NewSchema = { 240 + properties: { 241 + id: { type: 'string' }, 242 + }, 243 + type: 'object', 244 + } as any; 245 + } 246 + }, 247 + }, 248 + spec, 249 + }); 250 + 251 + expect(spec.definitions?.NewSchema).toEqual({ 252 + properties: { 253 + id: { type: 'string' }, 254 + }, 255 + type: 'object', 256 + }); 257 + }); 258 + }); 259 + }); 260 + 261 + describe('shorthand patch function', () => { 262 + describe('OpenAPI v3', () => { 263 + it('calls shorthand patch function', () => { 264 + const patchFn = vi.fn(); 265 + const spec: OpenApi.V3_1_X = { 266 + ...specMetadataV3, 267 + }; 268 + 269 + patchOpenApiSpec({ 270 + patchOptions: patchFn, 271 + spec, 272 + }); 273 + 274 + expect(patchFn).toHaveBeenCalledOnce(); 275 + expect(patchFn).toHaveBeenCalledWith(spec); 276 + }); 277 + 278 + it('allows modifications through shorthand function', () => { 279 + const spec: OpenApi.V3_1_X = { 280 + ...specMetadataV3, 281 + }; 282 + 283 + patchOpenApiSpec({ 284 + patchOptions: (spec) => { 285 + spec.info.title = 'Modified Title'; 286 + }, 287 + spec, 288 + }); 289 + 290 + expect(spec.info.title).toBe('Modified Title'); 291 + }); 292 + 293 + it('shorthand function prevents other patches from running', () => { 294 + const spec: OpenApi.V3_1_X = { 295 + ...specMetadataV3, 296 + }; 297 + 298 + patchOpenApiSpec({ 299 + patchOptions: (spec) => { 300 + spec.info.title = 'Shorthand Title'; 301 + }, 302 + spec, 303 + }); 304 + 305 + expect(spec.info.title).toBe('Shorthand Title'); 306 + }); 307 + }); 308 + 309 + describe('OpenAPI v2', () => { 310 + it('calls shorthand patch function for v2 specs', () => { 311 + const patchFn = vi.fn(); 312 + const spec: OpenApi.V2_0_X = { 313 + ...specMetadataV2, 314 + }; 315 + 316 + patchOpenApiSpec({ 317 + patchOptions: patchFn, 318 + spec, 319 + }); 320 + 321 + expect(patchFn).toHaveBeenCalledOnce(); 322 + expect(patchFn).toHaveBeenCalledWith(spec); 323 + }); 324 + 325 + it('allows modifications through shorthand function in v2', () => { 326 + const spec: OpenApi.V2_0_X = { 327 + ...specMetadataV2, 328 + }; 329 + 330 + patchOpenApiSpec({ 331 + patchOptions: (spec) => { 332 + spec.info.title = 'Modified V2 Title'; 333 + }, 334 + spec, 335 + }); 336 + 337 + expect(spec.info.title).toBe('Modified V2 Title'); 338 + }); 339 + }); 340 + }); 341 + 22 342 describe('edge cases', () => { 23 343 it('does not modify spec', () => { 24 344 const spec: OpenApi.V3_1_X = {
+11
packages/shared/src/openApi/shared/utils/patch.ts
··· 14 14 15 15 const spec = _spec as OpenApi.V2_0_X | OpenApi.V3_0_X | OpenApi.V3_1_X; 16 16 17 + // Handle shorthand function syntax: patch: (spec) => { ... } 18 + if (typeof patchOptions === 'function') { 19 + patchOptions(spec); 20 + return; 21 + } 22 + 23 + // Handle patch.input callback 24 + if (patchOptions.input) { 25 + patchOptions.input(spec); 26 + } 27 + 17 28 if ('swagger' in spec) { 18 29 if (patchOptions.version && spec.swagger) { 19 30 spec.swagger = (