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

feat: add parser.transforms.schemas.name configuration and implementation

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

+209
+25
packages/shared/src/config/parser/config.ts
··· 36 36 name: '{{name}}', 37 37 }, 38 38 }, 39 + schemas: { 40 + enabled: false, 41 + name: '{{name}}', 42 + }, 39 43 }, 40 44 validate_EXPERIMENTAL: false, 41 45 }, ··· 136 140 }), 137 141 }, 138 142 value: fields.readWrite, 143 + }), 144 + schemas: valueToObject({ 145 + defaultValue: { 146 + ...(defaultValue.schemas as Extract< 147 + typeof defaultValue.schemas, 148 + Record<string, unknown> 149 + >), 150 + enabled: 151 + fields.schemas !== undefined 152 + ? Boolean(fields.schemas) 153 + : ( 154 + defaultValue.schemas as Extract< 155 + typeof defaultValue.schemas, 156 + Record<string, unknown> 157 + > 158 + ).enabled, 159 + }, 160 + mappers: { 161 + boolean: (enabled: boolean) => ({ enabled }), 162 + }, 163 + value: fields.schemas, 139 164 }), 140 165 }), 141 166 },
+59
packages/shared/src/config/parser/types.ts
··· 175 175 name?: NameTransformer; 176 176 }; 177 177 }; 178 + /** 179 + * Rename schema component keys and automatically update all `$ref` pointers 180 + * throughout the specification. 181 + * 182 + * This is useful for: 183 + * - Stripping version markers from schema names 184 + * - Removing vendor prefixes 185 + * - Converting naming conventions 186 + * - Shortening verbose auto-generated names 187 + * 188 + * @example 189 + * ```ts 190 + * { 191 + * schemas: { 192 + * name: (name) => name.replace(/_v\d+_\d+_\d+_/, '_') 193 + * } 194 + * } 195 + * ``` 196 + * 197 + * @default false 198 + */ 199 + schemas?: 200 + | boolean 201 + | { 202 + /** 203 + * Whether this feature is enabled. 204 + * 205 + * @default false 206 + */ 207 + enabled?: boolean; 208 + /** 209 + * Customize the generated name of schema components. 210 + * When provided, this transformer is called for each schema key 211 + * in `components.schemas` to compute the new name. 212 + * 213 + * If the new name conflicts with an existing schema, the rename 214 + * is skipped for that schema. 215 + * 216 + * @default undefined 217 + */ 218 + name?: NameTransformer; 219 + }; 178 220 }; 179 221 /** 180 222 * **This is an experimental feature.** ··· 279 321 * Configuration for generated response-specific schemas. 280 322 */ 281 323 responses: NamingOptions; 324 + }; 325 + /** 326 + * Rename schema component keys and automatically update all `$ref` pointers 327 + * throughout the specification. 328 + */ 329 + schemas: FeatureToggle & { 330 + /** 331 + * Customize the generated name of schema components. 332 + * When provided, this transformer is called for each schema key 333 + * in `components.schemas` to compute the new name. 334 + * 335 + * If the new name conflicts with an existing schema, the rename 336 + * is skipped for that schema. 337 + * 338 + * @default '{{name}}' 339 + */ 340 + name: NameTransformer; 282 341 }; 283 342 }; 284 343 /**
+9
packages/shared/src/openApi/shared/transforms/index.ts
··· 2 2 import { enumsTransform } from './enums'; 3 3 import { propertiesRequiredByDefaultTransform } from './propertiesRequiredByDefault'; 4 4 import { readWriteTransform } from './readWrite'; 5 + import { schemasTransform } from './schemas'; 5 6 6 7 export const transformOpenApiSpec = ({ context }: { context: Context }) => { 7 8 const { logger } = context; 8 9 const eventTransformOpenApiSpec = logger.timeEvent('transform-openapi-spec'); 10 + 11 + if (context.config.parser.transforms.schemas.enabled) { 12 + schemasTransform({ 13 + config: context.config.parser.transforms.schemas, 14 + spec: context.spec, 15 + }); 16 + } 17 + 9 18 if (context.config.parser.transforms.enums.enabled) { 10 19 enumsTransform({ 11 20 config: context.config.parser.transforms.enums,
+116
packages/shared/src/openApi/shared/transforms/schemas.ts
··· 1 + import type { Parser } from '../../../config/parser/types'; 2 + import { applyNaming } from '../../../utils/naming/naming'; 3 + import { getSchemasObject } from '../utils/transforms'; 4 + import { specToSchemasPointerNamespace } from './utils'; 5 + 6 + type SchemasConfig = Parser['transforms']['schemas']; 7 + 8 + /** 9 + * Recursively walks the entire spec object and replaces all $ref strings 10 + * according to the provided rename mapping. 11 + * 12 + * @param node - Current node being visited 13 + * @param renameMap - Map from old pointer to new pointer 14 + */ 15 + const rewriteRefs = (node: unknown, renameMap: Record<string, string>) => { 16 + if (node instanceof Array) { 17 + node.forEach((item) => rewriteRefs(item, renameMap)); 18 + } else if (node && typeof node === 'object') { 19 + for (const [key, value] of Object.entries(node)) { 20 + if (key === '$ref' && typeof value === 'string' && value in renameMap) { 21 + // Replace the $ref with the new name 22 + (node as Record<string, unknown>)[key] = renameMap[value]; 23 + } else { 24 + rewriteRefs(value, renameMap); 25 + } 26 + } 27 + } 28 + }; 29 + 30 + /** 31 + * Applies the schema name transform to rename schema component keys and 32 + * update all $ref pointers throughout the spec. 33 + * 34 + * This transform: 35 + * 1. Iterates all schema keys in components.schemas (or definitions for Swagger 2.0) 36 + * 2. Applies the name transformer to compute new names 37 + * 3. Handles name collisions (skips rename if new name already exists) 38 + * 4. Renames schema keys in the schemas object 39 + * 5. Updates all $ref pointers throughout the spec to use the new names 40 + * 41 + * @param config - The schemas transform config 42 + * @param spec - The OpenAPI spec object to transform 43 + */ 44 + export const schemasTransform = ({ config, spec }: { config: SchemasConfig; spec: unknown }) => { 45 + const schemasObj = getSchemasObject(spec); 46 + if (!schemasObj) { 47 + return; 48 + } 49 + 50 + const schemasPointerNamespace = specToSchemasPointerNamespace(spec); 51 + if (!schemasPointerNamespace) { 52 + return; 53 + } 54 + 55 + // Build rename map: oldPointer -> newPointer 56 + const renameMap: Record<string, string> = {}; 57 + const newNames = new Set<string>(); 58 + 59 + // First pass: compute all new names and check for collisions 60 + for (const oldName of Object.keys(schemasObj)) { 61 + const newName = applyNaming(oldName, config); 62 + 63 + // Skip if name doesn't change 64 + if (newName === oldName) { 65 + newNames.add(oldName); 66 + continue; 67 + } 68 + 69 + // Skip if new name collides with an existing schema or another renamed schema 70 + if (oldName in schemasObj && newName in schemasObj && oldName !== newName) { 71 + // Collision with existing schema - skip rename 72 + newNames.add(oldName); 73 + continue; 74 + } 75 + 76 + if (newNames.has(newName)) { 77 + // Collision with another renamed schema - skip rename 78 + newNames.add(oldName); 79 + continue; 80 + } 81 + 82 + // Record the rename 83 + renameMap[`${schemasPointerNamespace}${oldName}`] = `${schemasPointerNamespace}${newName}`; 84 + newNames.add(newName); 85 + } 86 + 87 + // Second pass: rename schema keys 88 + // We need to be careful about the order to avoid overwriting 89 + const renamedSchemas: Record<string, unknown> = {}; 90 + const processedOldNames = new Set<string>(); 91 + 92 + for (const [oldPointer, newPointer] of Object.entries(renameMap)) { 93 + const oldName = oldPointer.slice(schemasPointerNamespace.length); 94 + const newName = newPointer.slice(schemasPointerNamespace.length); 95 + 96 + // Store the schema under the new name 97 + renamedSchemas[newName] = schemasObj[oldName]; 98 + processedOldNames.add(oldName); 99 + } 100 + 101 + // Add all schemas that weren't renamed 102 + for (const [name, schema] of Object.entries(schemasObj)) { 103 + if (!processedOldNames.has(name)) { 104 + renamedSchemas[name] = schema; 105 + } 106 + } 107 + 108 + // Replace the entire schemas object with the renamed version 109 + Object.keys(schemasObj).forEach((key) => delete schemasObj[key]); 110 + Object.assign(schemasObj, renamedSchemas); 111 + 112 + // Third pass: rewrite all $ref pointers throughout the spec 113 + if (Object.keys(renameMap).length > 0) { 114 + rewriteRefs(spec, renameMap); 115 + } 116 + };