fork of hey-api/openapi-ts because I need some additional things
at feat/skip-token 142 lines 4.3 kB view raw
1/** 2 * After these structural segments, the next segment has a known role. 3 * This is what makes a property literally named "properties" safe — 4 * it occupies the name position, never the structural position. 5 */ 6const STRUCTURAL_ROLE: Record<string, 'name' | 'index'> = { 7 items: 'index', 8 patternProperties: 'name', 9 properties: 'name', 10}; 11 12/** 13 * These structural segments have no following name/index — 14 * they are the terminal structural node. Append a suffix 15 * to disambiguate from the parent. 16 */ 17const STRUCTURAL_SUFFIX: Record<string, string> = { 18 additionalProperties: 'Value', 19}; 20 21type RootContextConfig = { 22 /** How many consecutive semantic segments follow before structural walking begins */ 23 names: number; 24 /** How many leading segments to skip (the root keyword + any category segment) */ 25 skip: number; 26}; 27 28/** 29 * Root context configuration. 30 */ 31const ROOT_CONTEXT: Record<string | number, RootContextConfig> = { 32 components: { names: 1, skip: 2 }, // components/schemas/{name} 33 definitions: { names: 1, skip: 1 }, // definitions/{name} 34 paths: { names: 2, skip: 1 }, // paths/{path}/{method} 35 webhooks: { names: 2, skip: 1 }, // webhooks/{name}/{method} 36}; 37 38/** 39 * Sanitizes a path segment for use in a derived name. 40 * 41 * Handles API path segments like `/api/v1/users/{id}` → `ApiV1UsersId`. 42 */ 43function sanitizeSegment(segment: string | number): string { 44 const str = String(segment); 45 if (str.startsWith('/')) { 46 return str 47 .split('/') 48 .filter(Boolean) 49 .map((part) => { 50 const clean = part.replace(/[{}]/g, ''); 51 return clean.charAt(0).toUpperCase() + clean.slice(1); 52 }) 53 .join(''); 54 } 55 return str; 56} 57 58export interface PathToNameOptions { 59 /** 60 * When provided, replaces the root semantic segments with this anchor. 61 * Structural suffixes are still derived from path. 62 */ 63 anchor?: string; 64} 65 66/** 67 * Derives a composite name from a path. 68 * 69 * Examples: 70 * .../User → 'User' 71 * .../User/properties/address → 'UserAddress' 72 * .../User/properties/properties → 'UserProperties' 73 * .../User/properties/address/properties/city → 'UserAddressCity' 74 * .../Pet/additionalProperties → 'PetValue' 75 * .../Order/properties/items/items/0 → 'OrderItems' 76 * paths//event/get/properties/query → 'EventGetQuery' 77 * 78 * With anchor: 79 * paths//event/get/properties/query, { anchor: 'event.subscribe' } 80 * → 'event.subscribe-Query' 81 */ 82export function pathToName( 83 path: ReadonlyArray<string | number>, 84 options?: PathToNameOptions, 85): string { 86 const names: Array<string> = []; 87 let index = 0; 88 89 const rootContext = ROOT_CONTEXT[path[0]!]; 90 if (rootContext) { 91 index = rootContext.skip; 92 93 if (options?.anchor) { 94 // Use anchor as base name, skip past root semantic segments 95 names.push(options.anchor); 96 index += rootContext.names; 97 } else { 98 // Collect consecutive semantic name segments 99 for (let n = 0; n < rootContext.names && index < path.length; n++) { 100 names.push(sanitizeSegment(path[index]!)); 101 index++; 102 } 103 } 104 } else { 105 // Unknown root 106 if (options?.anchor) { 107 names.push(options.anchor); 108 index++; 109 } else if (index < path.length) { 110 names.push(sanitizeSegment(path[index]!)); 111 index++; 112 } 113 } 114 115 while (index < path.length) { 116 const segment = String(path[index]); 117 118 const role = STRUCTURAL_ROLE[segment]; 119 if (role === 'name') { 120 // Next segment is a semantic name — collect it 121 index++; 122 if (index < path.length) { 123 names.push(sanitizeSegment(path[index]!)); 124 } 125 } else if (role === 'index') { 126 // Next segment is a numeric index — skip it 127 index++; 128 if (index < path.length && typeof path[index] === 'number') { 129 index++; 130 } 131 continue; 132 } else if (STRUCTURAL_SUFFIX[segment]) { 133 names.push(STRUCTURAL_SUFFIX[segment]); 134 } 135 136 index++; 137 } 138 139 // refs using unicode characters become encoded, didn't investigate why 140 // but the suspicion is this comes from `@hey-api/json-schema-ref-parser` 141 return decodeURI(names.join('-')); 142}