fork of hey-api/openapi-ts because I need some additional things
1const jsonPointerSlash = /~1/g;
2const jsonPointerTilde = /~0/g;
3
4/**
5 * Returns the reusable component name from `$ref`.
6 */
7export function refToName($ref: string): string {
8 const path = jsonPointerToPath($ref);
9 const name = path[path.length - 1]!;
10 // refs using unicode characters become encoded, didn't investigate why
11 // but the suspicion is this comes from `@hey-api/json-schema-ref-parser`
12 return decodeURI(name);
13}
14
15/**
16 * Encodes a path segment for use in a JSON Pointer (RFC 6901).
17 *
18 * - Replaces all '~' with '~0'.
19 * - Replaces all '/' with '~1'.
20 *
21 * This ensures that path segments containing these characters are safely
22 * represented in JSON Pointer strings.
23 *
24 * @param segment - The path segment (string or number) to encode.
25 * @returns The encoded segment as a string.
26 */
27export function encodeJsonPointerSegment(segment: string | number): string {
28 return String(segment).replace(/~/g, '~0').replace(/\//g, '~1');
29}
30
31/**
32 * Converts a JSON Pointer string (RFC 6901) to an array of path segments.
33 *
34 * - Removes the leading '#' if present.
35 * - Splits the pointer on '/'.
36 * - Decodes '~1' to '/' and '~0' to '~' in each segment.
37 * - Returns an empty array for the root pointer ('#' or '').
38 *
39 * @param pointer - The JSON Pointer string to convert (e.g., '#/components/schemas/Foo').
40 * @returns An array of decoded path segments.
41 */
42export function jsonPointerToPath(pointer: string): ReadonlyArray<string> {
43 let clean = pointer.trim();
44 if (clean.startsWith('#')) {
45 clean = clean.slice(1);
46 }
47 if (clean.startsWith('/')) {
48 clean = clean.slice(1);
49 }
50 if (!clean) {
51 return [];
52 }
53 return clean
54 .split('/')
55 .map((part) => part.replace(jsonPointerSlash, '/').replace(jsonPointerTilde, '~'));
56}
57
58/**
59 * Normalizes a JSON Pointer string to a canonical form.
60 *
61 * - Ensures the pointer starts with '#'.
62 * - Removes trailing slashes (except for root).
63 * - Collapses multiple consecutive slashes into one.
64 * - Trims whitespace from the input.
65 *
66 * @param pointer - The JSON Pointer string to normalize.
67 * @returns The normalized JSON Pointer string.
68 */
69export function normalizeJsonPointer(pointer: string): string {
70 let normalized = pointer.trim();
71 if (!normalized.startsWith('#')) {
72 normalized = `#${normalized}`;
73 }
74 // Remove trailing slashes (except for root)
75 if (normalized.length > 1 && normalized.endsWith('/')) {
76 normalized = normalized.slice(0, -1);
77 }
78 // Collapse multiple slashes
79 normalized = normalized.replace(/\/+/g, '/');
80 return normalized;
81}
82
83/**
84 * Encode path as JSON Pointer (RFC 6901).
85 *
86 * @param path
87 * @returns
88 */
89export function pathToJsonPointer(path: ReadonlyArray<string | number>): string {
90 const segments = path.map(encodeJsonPointerSegment).join('/');
91 return '#' + (segments ? `/${segments}` : '');
92}
93
94/**
95 * Checks if a $ref or path points to a top-level component (not a deep path reference).
96 *
97 * Top-level component references:
98 * - OpenAPI 3.x: #/components/{type}/{name} (3 segments)
99 * - OpenAPI 2.0: #/definitions/{name} (2 segments)
100 *
101 * Deep path references (4+ segments for 3.x, 3+ for 2.0) should be inlined
102 * because they don't have corresponding registered symbols.
103 *
104 * @param refOrPath - The $ref string or path array to check
105 * @returns true if the ref points to a top-level component, false otherwise
106 */
107export function isTopLevelComponent(refOrPath: string | ReadonlyArray<string | number>): boolean {
108 const path = refOrPath instanceof Array ? refOrPath : jsonPointerToPath(refOrPath);
109
110 // OpenAPI 3.x: #/components/{type}/{name} = 3 segments
111 if (path[0] === 'components') {
112 return path.length === 3;
113 }
114
115 // OpenAPI 2.0: #/definitions/{name} = 2 segments
116 if (path[0] === 'definitions') {
117 return path.length === 2;
118 }
119
120 return false;
121}
122
123export function resolveRef<T>({ $ref, spec }: { $ref: string; spec: Record<string, any> }): T {
124 // refs using unicode characters become encoded, didn't investigate why
125 // but the suspicion is this comes from `@hey-api/json-schema-ref-parser`
126 const path = jsonPointerToPath(decodeURI($ref));
127
128 let current = spec;
129
130 for (const part of path) {
131 const segment = part as keyof typeof current;
132 if (current[segment] === undefined) {
133 throw new Error(`Reference not found: ${$ref}`);
134 }
135 current = current[segment];
136 }
137
138 return current as T;
139}