fork of hey-api/openapi-ts because I need some additional things
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}