incur 0.3.22 → 0.3.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Openapi.d.ts.map +1 -1
- package/dist/Openapi.js +2 -2
- package/dist/Openapi.js.map +1 -1
- package/dist/internal/dereference.d.ts +12 -0
- package/dist/internal/dereference.d.ts.map +1 -0
- package/dist/internal/dereference.js +71 -0
- package/dist/internal/dereference.js.map +1 -0
- package/package.json +1 -2
- package/src/Openapi.ts +2 -2
- package/src/internal/dereference.test.ts +695 -0
- package/src/internal/dereference.ts +75 -0
package/dist/Openapi.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Openapi.d.ts","sourceRoot":"","sources":["../src/Openapi.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"Openapi.d.ts","sourceRoot":"","sources":["../src/Openapi.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAKvB,4HAA4H;AAC5H,MAAM,MAAM,WAAW,GAAG;IAAE,KAAK,CAAC,EAAE,EAAE,GAAG,SAAS,CAAA;CAAE,CAAA;AAyBpD,uBAAuB;AACvB,KAAK,YAAY,GAAG,CAAC,GAAG,EAAE,OAAO,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;AAElE,+EAA+E;AAC/E,KAAK,gBAAgB,GAAG;IACtB,IAAI,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,SAAS,CAAA;IACnC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAChC,OAAO,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,SAAS,CAAA;IACtC,GAAG,EAAE,CAAC,OAAO,EAAE,GAAG,KAAK,GAAG,CAAA;CAC3B,CAAA;AAED,0FAA0F;AAC1F,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,WAAW,EACjB,KAAK,EAAE,YAAY,EACnB,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAAO,GAC9C,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAiExC"}
|
package/dist/Openapi.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { dereference } from '@readme/openapi-parser';
|
|
2
1
|
import { z } from 'zod';
|
|
3
2
|
import * as Fetch from './Fetch.js';
|
|
3
|
+
import { dereference } from './internal/dereference.js';
|
|
4
4
|
/** Generates incur command entries from an OpenAPI spec. Resolves all `$ref` pointers. */
|
|
5
5
|
export async function generateCommands(spec, fetch, options = {}) {
|
|
6
|
-
const resolved =
|
|
6
|
+
const resolved = dereference(structuredClone(spec));
|
|
7
7
|
const commands = new Map();
|
|
8
8
|
const paths = (resolved.paths ?? {});
|
|
9
9
|
for (const [path, methods] of Object.entries(paths)) {
|
package/dist/Openapi.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Openapi.js","sourceRoot":"","sources":["../src/Openapi.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"Openapi.js","sourceRoot":"","sources":["../src/Openapi.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dereferences all local `$ref` pointers in a JSON object (e.g. `{"$ref": "#/components/schemas/User"}`),
|
|
3
|
+
* replacing them inline with the resolved values. Only handles local (`#/...`) references.
|
|
4
|
+
*
|
|
5
|
+
* Handles circular references by caching a mutable placeholder before recursing.
|
|
6
|
+
*
|
|
7
|
+
* Minimal reimplementation of the dereferencing behavior from `@apidevtools/json-schema-ref-parser`
|
|
8
|
+
* (https://github.com/APIDevTools/json-schema-ref-parser). Only supports in-memory, local-pointer
|
|
9
|
+
* resolution — no file/URL resolution, no `$id` scoping.
|
|
10
|
+
*/
|
|
11
|
+
export declare function dereference<value>(root: value): value;
|
|
12
|
+
//# sourceMappingURL=dereference.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dereference.d.ts","sourceRoot":"","sources":["../../src/internal/dereference.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,GAAG,KAAK,CAGrD"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dereferences all local `$ref` pointers in a JSON object (e.g. `{"$ref": "#/components/schemas/User"}`),
|
|
3
|
+
* replacing them inline with the resolved values. Only handles local (`#/...`) references.
|
|
4
|
+
*
|
|
5
|
+
* Handles circular references by caching a mutable placeholder before recursing.
|
|
6
|
+
*
|
|
7
|
+
* Minimal reimplementation of the dereferencing behavior from `@apidevtools/json-schema-ref-parser`
|
|
8
|
+
* (https://github.com/APIDevTools/json-schema-ref-parser). Only supports in-memory, local-pointer
|
|
9
|
+
* resolution — no file/URL resolution, no `$id` scoping.
|
|
10
|
+
*/
|
|
11
|
+
export function dereference(root) {
|
|
12
|
+
const cache = new Map();
|
|
13
|
+
return walk(root, root, cache);
|
|
14
|
+
}
|
|
15
|
+
function walk(node, root, cache) {
|
|
16
|
+
if (Array.isArray(node))
|
|
17
|
+
return node.map((item) => walk(item, root, cache));
|
|
18
|
+
if (typeof node !== 'object' || node === null)
|
|
19
|
+
return node;
|
|
20
|
+
const obj = node;
|
|
21
|
+
// Resolve $ref pointer
|
|
22
|
+
if (typeof obj.$ref === 'string' && obj.$ref.startsWith('#')) {
|
|
23
|
+
const ref = obj.$ref;
|
|
24
|
+
if (cache.has(ref))
|
|
25
|
+
return cache.get(ref);
|
|
26
|
+
const resolved = resolvePointer(root, ref);
|
|
27
|
+
// Non-object targets (primitives, arrays) can't be circular — resolve directly
|
|
28
|
+
if (typeof resolved !== 'object' || resolved === null || Array.isArray(resolved)) {
|
|
29
|
+
const dereferenced = walk(resolved, root, cache);
|
|
30
|
+
cache.set(ref, dereferenced);
|
|
31
|
+
return dereferenced;
|
|
32
|
+
}
|
|
33
|
+
// Use a mutable placeholder so circular refs resolve to the same object.
|
|
34
|
+
// If the walked result is not a plain object (e.g. chained ref to primitive/array),
|
|
35
|
+
// skip the placeholder and cache directly.
|
|
36
|
+
const placeholder = {};
|
|
37
|
+
cache.set(ref, placeholder);
|
|
38
|
+
const dereferenced = walk(resolved, root, cache);
|
|
39
|
+
if (typeof dereferenced !== 'object' || dereferenced === null || Array.isArray(dereferenced)) {
|
|
40
|
+
cache.set(ref, dereferenced);
|
|
41
|
+
return dereferenced;
|
|
42
|
+
}
|
|
43
|
+
Object.assign(placeholder, dereferenced);
|
|
44
|
+
return placeholder;
|
|
45
|
+
}
|
|
46
|
+
const result = {};
|
|
47
|
+
for (const key of Object.keys(obj))
|
|
48
|
+
result[key] = walk(obj[key], root, cache);
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
/** Resolves a JSON Pointer (e.g. `#/components/schemas/User`) against a root object. */
|
|
52
|
+
function resolvePointer(root, pointer) {
|
|
53
|
+
// "#" or "#/" → root
|
|
54
|
+
const fragment = pointer.slice(1);
|
|
55
|
+
if (fragment === '' || fragment === '/')
|
|
56
|
+
return root;
|
|
57
|
+
const parts = fragment
|
|
58
|
+
.slice(1)
|
|
59
|
+
.split('/')
|
|
60
|
+
.map((p) => p.replace(/~1/g, '/').replace(/~0/g, '~'));
|
|
61
|
+
let current = root;
|
|
62
|
+
for (const part of parts) {
|
|
63
|
+
if (typeof current !== 'object' || current === null)
|
|
64
|
+
throw new Error(`Cannot resolve $ref "${pointer}": path segment "${part}" not found`);
|
|
65
|
+
current = current[part];
|
|
66
|
+
if (current === undefined)
|
|
67
|
+
throw new Error(`Cannot resolve $ref "${pointer}": "${part}" not found`);
|
|
68
|
+
}
|
|
69
|
+
return current;
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=dereference.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dereference.js","sourceRoot":"","sources":["../../src/internal/dereference.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "incur",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.23",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"@cfworker/json-schema": "^4.1.1",
|
|
18
18
|
"@modelcontextprotocol/server": "^2.0.0-alpha.2",
|
|
19
|
-
"@readme/openapi-parser": "^6.0.0",
|
|
20
19
|
"@toon-format/toon": "^2.1.0",
|
|
21
20
|
"tokenx": "^1.3.0",
|
|
22
21
|
"yaml": "^2.8.2",
|
package/src/Openapi.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { dereference } from '@readme/openapi-parser'
|
|
2
1
|
import { z } from 'zod'
|
|
3
2
|
|
|
4
3
|
import * as Fetch from './Fetch.js'
|
|
4
|
+
import { dereference } from './internal/dereference.js'
|
|
5
5
|
|
|
6
6
|
/** A minimal OpenAPI 3.x spec shape. Accepts both hand-written specs and generated ones (e.g. from `@hono/zod-openapi`). */
|
|
7
7
|
export type OpenAPISpec = { paths?: {} | undefined }
|
|
@@ -46,7 +46,7 @@ export async function generateCommands(
|
|
|
46
46
|
fetch: FetchHandler,
|
|
47
47
|
options: { basePath?: string | undefined } = {},
|
|
48
48
|
): Promise<Map<string, GeneratedCommand>> {
|
|
49
|
-
const resolved =
|
|
49
|
+
const resolved = dereference(structuredClone(spec)) as OpenAPISpec
|
|
50
50
|
const commands = new Map<string, GeneratedCommand>()
|
|
51
51
|
const paths = (resolved.paths ?? {}) as Record<string, Record<string, unknown>>
|
|
52
52
|
|
|
@@ -0,0 +1,695 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { dereference } from './dereference.js'
|
|
4
|
+
|
|
5
|
+
describe('dereference', () => {
|
|
6
|
+
test('resolves basic $ref', () => {
|
|
7
|
+
const spec = {
|
|
8
|
+
paths: {
|
|
9
|
+
'/users': {
|
|
10
|
+
get: {
|
|
11
|
+
responses: {
|
|
12
|
+
'200': {
|
|
13
|
+
content: {
|
|
14
|
+
'application/json': {
|
|
15
|
+
schema: { $ref: '#/components/schemas/User' },
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
components: {
|
|
24
|
+
schemas: {
|
|
25
|
+
User: {
|
|
26
|
+
type: 'object',
|
|
27
|
+
properties: { name: { type: 'string' } },
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
}
|
|
32
|
+
const result = dereference(spec) as any
|
|
33
|
+
expect(result.paths['/users'].get.responses['200'].content['application/json'].schema).toEqual({
|
|
34
|
+
type: 'object',
|
|
35
|
+
properties: { name: { type: 'string' } },
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('resolves nested $ref (ref target contains another ref)', () => {
|
|
40
|
+
const spec = {
|
|
41
|
+
components: {
|
|
42
|
+
schemas: {
|
|
43
|
+
Name: { type: 'string' },
|
|
44
|
+
User: {
|
|
45
|
+
type: 'object',
|
|
46
|
+
properties: { name: { $ref: '#/components/schemas/Name' } },
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
root: { $ref: '#/components/schemas/User' },
|
|
51
|
+
}
|
|
52
|
+
const result = dereference(spec) as any
|
|
53
|
+
expect(result.root).toEqual({
|
|
54
|
+
type: 'object',
|
|
55
|
+
properties: { name: { type: 'string' } },
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('handles circular $ref without infinite loop', () => {
|
|
60
|
+
const spec = {
|
|
61
|
+
components: {
|
|
62
|
+
schemas: {
|
|
63
|
+
Node: {
|
|
64
|
+
type: 'object',
|
|
65
|
+
properties: {
|
|
66
|
+
value: { type: 'string' },
|
|
67
|
+
child: { $ref: '#/components/schemas/Node' },
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
root: { $ref: '#/components/schemas/Node' },
|
|
73
|
+
}
|
|
74
|
+
const result = dereference(spec) as any
|
|
75
|
+
// Should resolve without hanging
|
|
76
|
+
expect(result.root.type).toBe('object')
|
|
77
|
+
expect(result.root.properties.value).toEqual({ type: 'string' })
|
|
78
|
+
// Circular ref should point back to the same resolved object
|
|
79
|
+
expect(result.root.properties.child).toBe(result.root)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test('resolves multiple refs to same target (shares identity)', () => {
|
|
83
|
+
const spec = {
|
|
84
|
+
components: { schemas: { Id: { type: 'number' } } },
|
|
85
|
+
a: { $ref: '#/components/schemas/Id' },
|
|
86
|
+
b: { $ref: '#/components/schemas/Id' },
|
|
87
|
+
}
|
|
88
|
+
const result = dereference(spec) as any
|
|
89
|
+
expect(result.a).toEqual({ type: 'number' })
|
|
90
|
+
expect(result.a).toBe(result.b)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test('resolves $ref in arrays', () => {
|
|
94
|
+
const spec = {
|
|
95
|
+
components: { schemas: { Tag: { type: 'string' } } },
|
|
96
|
+
items: [{ $ref: '#/components/schemas/Tag' }, { $ref: '#/components/schemas/Tag' }],
|
|
97
|
+
}
|
|
98
|
+
const result = dereference(spec) as any
|
|
99
|
+
expect(result.items[0]).toEqual({ type: 'string' })
|
|
100
|
+
expect(result.items[1]).toEqual({ type: 'string' })
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test('handles deeply nested path', () => {
|
|
104
|
+
const spec = {
|
|
105
|
+
a: { b: { c: { d: { value: 42 } } } },
|
|
106
|
+
ref: { $ref: '#/a/b/c/d' },
|
|
107
|
+
}
|
|
108
|
+
const result = dereference(spec) as any
|
|
109
|
+
expect(result.ref).toEqual({ value: 42 })
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test('handles JSON Pointer escaping (~0 for ~, ~1 for /)', () => {
|
|
113
|
+
const spec = {
|
|
114
|
+
'a/b': { 'c~d': { value: 'escaped' } },
|
|
115
|
+
ref: { $ref: '#/a~1b/c~0d' },
|
|
116
|
+
}
|
|
117
|
+
const result = dereference(spec) as any
|
|
118
|
+
expect(result.ref).toEqual({ value: 'escaped' })
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
test('throws on unresolvable $ref', () => {
|
|
122
|
+
const spec = { ref: { $ref: '#/does/not/exist' } }
|
|
123
|
+
expect(() => dereference(spec)).toThrow('Cannot resolve $ref')
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
test('passes through primitives unchanged', () => {
|
|
127
|
+
expect(dereference('hello')).toBe('hello')
|
|
128
|
+
expect(dereference(42)).toBe(42)
|
|
129
|
+
expect(dereference(null)).toBe(null)
|
|
130
|
+
expect(dereference(true)).toBe(true)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
test('does not mutate original object', () => {
|
|
134
|
+
const spec = {
|
|
135
|
+
components: { schemas: { User: { type: 'object' } } },
|
|
136
|
+
ref: { $ref: '#/components/schemas/User' },
|
|
137
|
+
}
|
|
138
|
+
const original = JSON.stringify(spec)
|
|
139
|
+
dereference(spec)
|
|
140
|
+
expect(JSON.stringify(spec)).toBe(original)
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
test('resolves $ref: "#" to root', () => {
|
|
144
|
+
const spec = { type: 'object', self: { $ref: '#' } }
|
|
145
|
+
const result = dereference(spec) as any
|
|
146
|
+
expect(result.self.type).toBe('object')
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
test('realistic OpenAPI spec with shared parameter and request body refs', () => {
|
|
150
|
+
const spec = {
|
|
151
|
+
openapi: '3.0.0',
|
|
152
|
+
info: { title: 'Test', version: '1.0.0' },
|
|
153
|
+
paths: {
|
|
154
|
+
'/users/{id}': {
|
|
155
|
+
get: {
|
|
156
|
+
operationId: 'getUser',
|
|
157
|
+
parameters: [{ $ref: '#/components/parameters/UserId' }],
|
|
158
|
+
responses: {
|
|
159
|
+
'200': {
|
|
160
|
+
content: {
|
|
161
|
+
'application/json': {
|
|
162
|
+
schema: { $ref: '#/components/schemas/User' },
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
put: {
|
|
169
|
+
operationId: 'updateUser',
|
|
170
|
+
parameters: [{ $ref: '#/components/parameters/UserId' }],
|
|
171
|
+
requestBody: {
|
|
172
|
+
content: {
|
|
173
|
+
'application/json': {
|
|
174
|
+
schema: { $ref: '#/components/schemas/UserInput' },
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
components: {
|
|
182
|
+
parameters: {
|
|
183
|
+
UserId: {
|
|
184
|
+
name: 'id',
|
|
185
|
+
in: 'path',
|
|
186
|
+
required: true,
|
|
187
|
+
schema: { type: 'number' },
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
schemas: {
|
|
191
|
+
User: {
|
|
192
|
+
type: 'object',
|
|
193
|
+
properties: {
|
|
194
|
+
id: { type: 'number' },
|
|
195
|
+
name: { type: 'string' },
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
UserInput: {
|
|
199
|
+
type: 'object',
|
|
200
|
+
properties: {
|
|
201
|
+
name: { type: 'string' },
|
|
202
|
+
},
|
|
203
|
+
required: ['name'],
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
}
|
|
208
|
+
const result = dereference(spec) as any
|
|
209
|
+
const getParams = result.paths['/users/{id}'].get.parameters
|
|
210
|
+
expect(getParams[0].name).toBe('id')
|
|
211
|
+
expect(getParams[0].in).toBe('path')
|
|
212
|
+
// Both GET and PUT share the same resolved parameter
|
|
213
|
+
const putParams = result.paths['/users/{id}'].put.parameters
|
|
214
|
+
expect(putParams[0]).toBe(getParams[0])
|
|
215
|
+
// Request body schema resolved
|
|
216
|
+
const bodySchema =
|
|
217
|
+
result.paths['/users/{id}'].put.requestBody.content['application/json'].schema
|
|
218
|
+
expect(bodySchema.properties.name).toEqual({ type: 'string' })
|
|
219
|
+
expect(bodySchema.required).toEqual(['name'])
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
test('mutual circular refs', () => {
|
|
223
|
+
const spec = {
|
|
224
|
+
components: {
|
|
225
|
+
schemas: {
|
|
226
|
+
A: {
|
|
227
|
+
type: 'object',
|
|
228
|
+
properties: { b: { $ref: '#/components/schemas/B' } },
|
|
229
|
+
},
|
|
230
|
+
B: {
|
|
231
|
+
type: 'object',
|
|
232
|
+
properties: { a: { $ref: '#/components/schemas/A' } },
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
root: { $ref: '#/components/schemas/A' },
|
|
237
|
+
}
|
|
238
|
+
const result = dereference(spec) as any
|
|
239
|
+
expect(result.root.type).toBe('object')
|
|
240
|
+
expect(result.root.properties.b.type).toBe('object')
|
|
241
|
+
expect(result.root.properties.b.properties.a).toBe(result.root)
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
test('$ref target is a primitive (string)', () => {
|
|
245
|
+
const spec = {
|
|
246
|
+
components: { values: { name: 'Alice' } },
|
|
247
|
+
ref: { $ref: '#/components/values/name' },
|
|
248
|
+
}
|
|
249
|
+
const result = dereference(spec) as any
|
|
250
|
+
expect(result.ref).toBe('Alice')
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
test('$ref target is a primitive (number)', () => {
|
|
254
|
+
const spec = {
|
|
255
|
+
components: { values: { count: 42 } },
|
|
256
|
+
ref: { $ref: '#/components/values/count' },
|
|
257
|
+
}
|
|
258
|
+
const result = dereference(spec) as any
|
|
259
|
+
expect(result.ref).toBe(42)
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
test('$ref target is null', () => {
|
|
263
|
+
const spec = {
|
|
264
|
+
components: { values: { empty: null } },
|
|
265
|
+
ref: { $ref: '#/components/values/empty' },
|
|
266
|
+
}
|
|
267
|
+
const result = dereference(spec) as any
|
|
268
|
+
expect(result.ref).toBe(null)
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
test('$ref target is an array', () => {
|
|
272
|
+
const spec = {
|
|
273
|
+
components: { values: { tags: ['a', 'b', 'c'] } },
|
|
274
|
+
ref: { $ref: '#/components/values/tags' },
|
|
275
|
+
}
|
|
276
|
+
const result = dereference(spec) as any
|
|
277
|
+
expect(result.ref).toEqual(['a', 'b', 'c'])
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
test('$ref to array element by index', () => {
|
|
281
|
+
const spec = {
|
|
282
|
+
items: [{ name: 'first' }, { name: 'second' }],
|
|
283
|
+
ref: { $ref: '#/items/1' },
|
|
284
|
+
}
|
|
285
|
+
const result = dereference(spec) as any
|
|
286
|
+
expect(result.ref).toEqual({ name: 'second' })
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
test('chain of refs (A -> B -> C)', () => {
|
|
290
|
+
const spec = {
|
|
291
|
+
a: { $ref: '#/b' },
|
|
292
|
+
b: { $ref: '#/c' },
|
|
293
|
+
c: { value: 'end' },
|
|
294
|
+
}
|
|
295
|
+
const result = dereference(spec) as any
|
|
296
|
+
expect(result.a).toEqual({ value: 'end' })
|
|
297
|
+
expect(result.b).toEqual({ value: 'end' })
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
test('triple circular (A -> B -> C -> A)', () => {
|
|
301
|
+
const spec = {
|
|
302
|
+
components: {
|
|
303
|
+
schemas: {
|
|
304
|
+
A: { type: 'A', next: { $ref: '#/components/schemas/B' } },
|
|
305
|
+
B: { type: 'B', next: { $ref: '#/components/schemas/C' } },
|
|
306
|
+
C: { type: 'C', next: { $ref: '#/components/schemas/A' } },
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
root: { $ref: '#/components/schemas/A' },
|
|
310
|
+
}
|
|
311
|
+
const result = dereference(spec) as any
|
|
312
|
+
expect(result.root.type).toBe('A')
|
|
313
|
+
expect(result.root.next.type).toBe('B')
|
|
314
|
+
expect(result.root.next.next.type).toBe('C')
|
|
315
|
+
expect(result.root.next.next.next).toBe(result.root)
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
test('$ref with sibling properties (OpenAPI 3.1 style)', () => {
|
|
319
|
+
const spec = {
|
|
320
|
+
components: {
|
|
321
|
+
schemas: {
|
|
322
|
+
User: { type: 'object', properties: { name: { type: 'string' } } },
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
ref: {
|
|
326
|
+
$ref: '#/components/schemas/User',
|
|
327
|
+
description: 'A user object',
|
|
328
|
+
},
|
|
329
|
+
}
|
|
330
|
+
const result = dereference(spec) as any
|
|
331
|
+
// siblings are dropped (ref replaces the whole node)
|
|
332
|
+
expect(result.ref.type).toBe('object')
|
|
333
|
+
expect(result.ref.description).toBeUndefined()
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
test('root is an array', () => {
|
|
337
|
+
const root = [{ a: 1 }, { b: 2 }]
|
|
338
|
+
const result = dereference(root) as any
|
|
339
|
+
expect(result).toEqual([{ a: 1 }, { b: 2 }])
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
test('empty object', () => {
|
|
343
|
+
expect(dereference({})).toEqual({})
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
test('$ref "#/" resolves to root', () => {
|
|
347
|
+
const spec = { type: 'root', self: { $ref: '#/' } }
|
|
348
|
+
const result = dereference(spec) as any
|
|
349
|
+
expect(result.self.type).toBe('root')
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
test('falsy primitive targets (false, 0, empty string)', () => {
|
|
353
|
+
const spec = {
|
|
354
|
+
vals: { a: false, b: 0, c: '' },
|
|
355
|
+
refA: { $ref: '#/vals/a' },
|
|
356
|
+
refB: { $ref: '#/vals/b' },
|
|
357
|
+
refC: { $ref: '#/vals/c' },
|
|
358
|
+
}
|
|
359
|
+
const result = dereference(spec) as any
|
|
360
|
+
expect(result.refA).toBe(false)
|
|
361
|
+
expect(result.refB).toBe(0)
|
|
362
|
+
expect(result.refC).toBe('')
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
test('$ref target is an empty array', () => {
|
|
366
|
+
const spec = {
|
|
367
|
+
vals: { empty: [] as unknown[] },
|
|
368
|
+
ref: { $ref: '#/vals/empty' },
|
|
369
|
+
}
|
|
370
|
+
const result = dereference(spec) as any
|
|
371
|
+
expect(result.ref).toEqual([])
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
test('$ref target is an empty object', () => {
|
|
375
|
+
const spec = {
|
|
376
|
+
vals: { empty: {} },
|
|
377
|
+
ref: { $ref: '#/vals/empty' },
|
|
378
|
+
}
|
|
379
|
+
const result = dereference(spec) as any
|
|
380
|
+
expect(result.ref).toEqual({})
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
test('chained ref to primitive (A -> B -> string)', () => {
|
|
384
|
+
const spec = {
|
|
385
|
+
vals: { greeting: 'hello' },
|
|
386
|
+
b: { $ref: '#/vals/greeting' },
|
|
387
|
+
a: { $ref: '#/b' },
|
|
388
|
+
}
|
|
389
|
+
const result = dereference(spec) as any
|
|
390
|
+
expect(result.a).toBe('hello')
|
|
391
|
+
expect(result.b).toBe('hello')
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
test('$ref with non-string value is treated as normal object', () => {
|
|
395
|
+
const spec = { obj: { $ref: 123, other: 'value' } }
|
|
396
|
+
const result = dereference(spec) as any
|
|
397
|
+
expect(result.obj).toEqual({ $ref: 123, other: 'value' })
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
test('$ref that does not start with # is left as-is', () => {
|
|
401
|
+
const spec = { obj: { $ref: 'http://example.com/schema.json' } }
|
|
402
|
+
const result = dereference(spec) as any
|
|
403
|
+
expect(result.obj).toEqual({ $ref: 'http://example.com/schema.json' })
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
test('$ref inside array inside a $ref target', () => {
|
|
407
|
+
const spec = {
|
|
408
|
+
components: {
|
|
409
|
+
schemas: {
|
|
410
|
+
Tag: { type: 'string' },
|
|
411
|
+
User: {
|
|
412
|
+
type: 'object',
|
|
413
|
+
properties: {
|
|
414
|
+
tags: {
|
|
415
|
+
type: 'array',
|
|
416
|
+
items: { $ref: '#/components/schemas/Tag' },
|
|
417
|
+
},
|
|
418
|
+
},
|
|
419
|
+
},
|
|
420
|
+
},
|
|
421
|
+
},
|
|
422
|
+
root: { $ref: '#/components/schemas/User' },
|
|
423
|
+
}
|
|
424
|
+
const result = dereference(spec) as any
|
|
425
|
+
expect(result.root.properties.tags.items).toEqual({ type: 'string' })
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
test('allOf/oneOf/anyOf with $ref items', () => {
|
|
429
|
+
const spec = {
|
|
430
|
+
components: {
|
|
431
|
+
schemas: {
|
|
432
|
+
Name: { type: 'string' },
|
|
433
|
+
Age: { type: 'number' },
|
|
434
|
+
},
|
|
435
|
+
},
|
|
436
|
+
root: {
|
|
437
|
+
allOf: [
|
|
438
|
+
{ $ref: '#/components/schemas/Name' },
|
|
439
|
+
{ $ref: '#/components/schemas/Age' },
|
|
440
|
+
],
|
|
441
|
+
},
|
|
442
|
+
}
|
|
443
|
+
const result = dereference(spec) as any
|
|
444
|
+
expect(result.root.allOf[0]).toEqual({ type: 'string' })
|
|
445
|
+
expect(result.root.allOf[1]).toEqual({ type: 'number' })
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
test('$ref inside deeply nested arrays', () => {
|
|
449
|
+
const spec = {
|
|
450
|
+
vals: { x: { value: 1 } },
|
|
451
|
+
nested: [[{ $ref: '#/vals/x' }]],
|
|
452
|
+
}
|
|
453
|
+
const result = dereference(spec) as any
|
|
454
|
+
expect(result.nested[0][0]).toEqual({ value: 1 })
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
test('circular ref inside an array (items ref self)', () => {
|
|
458
|
+
const spec = {
|
|
459
|
+
components: {
|
|
460
|
+
schemas: {
|
|
461
|
+
Tree: {
|
|
462
|
+
type: 'object',
|
|
463
|
+
properties: {
|
|
464
|
+
children: {
|
|
465
|
+
type: 'array',
|
|
466
|
+
items: { $ref: '#/components/schemas/Tree' },
|
|
467
|
+
},
|
|
468
|
+
},
|
|
469
|
+
},
|
|
470
|
+
},
|
|
471
|
+
},
|
|
472
|
+
root: { $ref: '#/components/schemas/Tree' },
|
|
473
|
+
}
|
|
474
|
+
const result = dereference(spec) as any
|
|
475
|
+
expect(result.root.type).toBe('object')
|
|
476
|
+
expect(result.root.properties.children.items).toBe(result.root)
|
|
477
|
+
})
|
|
478
|
+
|
|
479
|
+
test('$ref target is a boolean true', () => {
|
|
480
|
+
const spec = {
|
|
481
|
+
vals: { flag: true },
|
|
482
|
+
ref: { $ref: '#/vals/flag' },
|
|
483
|
+
}
|
|
484
|
+
const result = dereference(spec) as any
|
|
485
|
+
expect(result.ref).toBe(true)
|
|
486
|
+
})
|
|
487
|
+
|
|
488
|
+
test('same $ref used in different subtrees resolves identically', () => {
|
|
489
|
+
const spec = {
|
|
490
|
+
components: { schemas: { S: { type: 'object' } } },
|
|
491
|
+
tree: {
|
|
492
|
+
left: { schema: { $ref: '#/components/schemas/S' } },
|
|
493
|
+
right: { schema: { $ref: '#/components/schemas/S' } },
|
|
494
|
+
},
|
|
495
|
+
}
|
|
496
|
+
const result = dereference(spec) as any
|
|
497
|
+
expect(result.tree.left.schema).toBe(result.tree.right.schema)
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
test('ref target with array value containing refs', () => {
|
|
501
|
+
const spec = {
|
|
502
|
+
components: {
|
|
503
|
+
schemas: { Tag: { type: 'string' } },
|
|
504
|
+
lists: {
|
|
505
|
+
tags: [{ $ref: '#/components/schemas/Tag' }, { literal: true }],
|
|
506
|
+
},
|
|
507
|
+
},
|
|
508
|
+
ref: { $ref: '#/components/lists/tags' },
|
|
509
|
+
}
|
|
510
|
+
const result = dereference(spec) as any
|
|
511
|
+
expect(result.ref[0]).toEqual({ type: 'string' })
|
|
512
|
+
expect(result.ref[1]).toEqual({ literal: true })
|
|
513
|
+
})
|
|
514
|
+
|
|
515
|
+
test('root object is itself a $ref (self-referential)', () => {
|
|
516
|
+
const spec = { $ref: '#', type: 'object' }
|
|
517
|
+
const result = dereference(spec) as any
|
|
518
|
+
// $ref takes precedence, siblings (type) are dropped per OpenAPI 3.0.
|
|
519
|
+
// Circular self-ref resolves without infinite loop.
|
|
520
|
+
expect(result).toBeDefined()
|
|
521
|
+
expect(result.type).toBeUndefined()
|
|
522
|
+
})
|
|
523
|
+
|
|
524
|
+
test('non-local $ref is preserved (not resolved)', () => {
|
|
525
|
+
const spec = {
|
|
526
|
+
a: { $ref: 'https://example.com/schema.json#/Foo' },
|
|
527
|
+
b: { $ref: './other.yaml#/Bar' },
|
|
528
|
+
c: { $ref: 'relative.json' },
|
|
529
|
+
}
|
|
530
|
+
const result = dereference(spec) as any
|
|
531
|
+
expect(result.a.$ref).toBe('https://example.com/schema.json#/Foo')
|
|
532
|
+
expect(result.b.$ref).toBe('./other.yaml#/Bar')
|
|
533
|
+
expect(result.c.$ref).toBe('relative.json')
|
|
534
|
+
})
|
|
535
|
+
|
|
536
|
+
test('$ref target contains a non-local $ref (preserved after deref)', () => {
|
|
537
|
+
const spec = {
|
|
538
|
+
components: {
|
|
539
|
+
schemas: {
|
|
540
|
+
External: { type: 'object', nested: { $ref: 'https://example.com/other.json' } },
|
|
541
|
+
},
|
|
542
|
+
},
|
|
543
|
+
root: { $ref: '#/components/schemas/External' },
|
|
544
|
+
}
|
|
545
|
+
const result = dereference(spec) as any
|
|
546
|
+
expect(result.root.type).toBe('object')
|
|
547
|
+
expect(result.root.nested.$ref).toBe('https://example.com/other.json')
|
|
548
|
+
})
|
|
549
|
+
|
|
550
|
+
test('forward reference (A uses B, B defined after A)', () => {
|
|
551
|
+
const spec = {
|
|
552
|
+
components: {
|
|
553
|
+
schemas: {
|
|
554
|
+
A: { type: 'object', child: { $ref: '#/components/schemas/B' } },
|
|
555
|
+
B: { type: 'string' },
|
|
556
|
+
},
|
|
557
|
+
},
|
|
558
|
+
root: { $ref: '#/components/schemas/A' },
|
|
559
|
+
}
|
|
560
|
+
const result = dereference(spec) as any
|
|
561
|
+
expect(result.root.child).toEqual({ type: 'string' })
|
|
562
|
+
})
|
|
563
|
+
|
|
564
|
+
test('deep chain of refs (A -> B -> C -> D -> E -> value)', () => {
|
|
565
|
+
const spec = {
|
|
566
|
+
a: { $ref: '#/b' },
|
|
567
|
+
b: { $ref: '#/c' },
|
|
568
|
+
c: { $ref: '#/d' },
|
|
569
|
+
d: { $ref: '#/e' },
|
|
570
|
+
e: { value: 'deep' },
|
|
571
|
+
}
|
|
572
|
+
const result = dereference(spec) as any
|
|
573
|
+
expect(result.a).toEqual({ value: 'deep' })
|
|
574
|
+
})
|
|
575
|
+
|
|
576
|
+
test('deep chain of refs to array', () => {
|
|
577
|
+
const spec = {
|
|
578
|
+
a: { $ref: '#/b' },
|
|
579
|
+
b: { $ref: '#/c' },
|
|
580
|
+
c: [1, 2, 3],
|
|
581
|
+
}
|
|
582
|
+
const result = dereference(spec) as any
|
|
583
|
+
expect(result.a).toEqual([1, 2, 3])
|
|
584
|
+
})
|
|
585
|
+
|
|
586
|
+
test('combined ~0 and ~1 escaping in same pointer segment', () => {
|
|
587
|
+
const spec = {
|
|
588
|
+
'a~/b': { value: 'complex' },
|
|
589
|
+
ref: { $ref: '#/a~0~1b' },
|
|
590
|
+
}
|
|
591
|
+
const result = dereference(spec) as any
|
|
592
|
+
expect(result.ref).toEqual({ value: 'complex' })
|
|
593
|
+
})
|
|
594
|
+
|
|
595
|
+
test('$ref to nested value inside a ref target', () => {
|
|
596
|
+
const spec = {
|
|
597
|
+
components: {
|
|
598
|
+
schemas: {
|
|
599
|
+
User: {
|
|
600
|
+
type: 'object',
|
|
601
|
+
properties: { name: { type: 'string', maxLength: 100 } },
|
|
602
|
+
},
|
|
603
|
+
},
|
|
604
|
+
},
|
|
605
|
+
nameSchema: { $ref: '#/components/schemas/User/properties/name' },
|
|
606
|
+
}
|
|
607
|
+
const result = dereference(spec) as any
|
|
608
|
+
expect(result.nameSchema).toEqual({ type: 'string', maxLength: 100 })
|
|
609
|
+
})
|
|
610
|
+
|
|
611
|
+
test('multiple independent circular cycles', () => {
|
|
612
|
+
const spec = {
|
|
613
|
+
components: {
|
|
614
|
+
schemas: {
|
|
615
|
+
X: { type: 'X', self: { $ref: '#/components/schemas/X' } },
|
|
616
|
+
Y: { type: 'Y', self: { $ref: '#/components/schemas/Y' } },
|
|
617
|
+
},
|
|
618
|
+
},
|
|
619
|
+
refX: { $ref: '#/components/schemas/X' },
|
|
620
|
+
refY: { $ref: '#/components/schemas/Y' },
|
|
621
|
+
}
|
|
622
|
+
const result = dereference(spec) as any
|
|
623
|
+
expect(result.refX.type).toBe('X')
|
|
624
|
+
expect(result.refX.self).toBe(result.refX)
|
|
625
|
+
expect(result.refY.type).toBe('Y')
|
|
626
|
+
expect(result.refY.self).toBe(result.refY)
|
|
627
|
+
// X and Y are distinct
|
|
628
|
+
expect(result.refX).not.toBe(result.refY)
|
|
629
|
+
})
|
|
630
|
+
|
|
631
|
+
test('object with constructor/toString keys (no prototype issues)', () => {
|
|
632
|
+
const spec = {
|
|
633
|
+
vals: { constructor: { value: 1 }, toString: { value: 2 } },
|
|
634
|
+
a: { $ref: '#/vals/constructor' },
|
|
635
|
+
b: { $ref: '#/vals/toString' },
|
|
636
|
+
}
|
|
637
|
+
const result = dereference(spec) as any
|
|
638
|
+
expect(result.a).toEqual({ value: 1 })
|
|
639
|
+
expect(result.b).toEqual({ value: 2 })
|
|
640
|
+
})
|
|
641
|
+
|
|
642
|
+
test('ref to boolean nested inside object', () => {
|
|
643
|
+
const spec = {
|
|
644
|
+
config: { features: { enabled: true, disabled: false } },
|
|
645
|
+
a: { $ref: '#/config/features/enabled' },
|
|
646
|
+
b: { $ref: '#/config/features/disabled' },
|
|
647
|
+
}
|
|
648
|
+
const result = dereference(spec) as any
|
|
649
|
+
expect(result.a).toBe(true)
|
|
650
|
+
expect(result.b).toBe(false)
|
|
651
|
+
})
|
|
652
|
+
|
|
653
|
+
test('array of $refs to different types', () => {
|
|
654
|
+
const spec = {
|
|
655
|
+
vals: { str: 'hello', num: 42, obj: { x: 1 }, arr: [1, 2] },
|
|
656
|
+
refs: [
|
|
657
|
+
{ $ref: '#/vals/str' },
|
|
658
|
+
{ $ref: '#/vals/num' },
|
|
659
|
+
{ $ref: '#/vals/obj' },
|
|
660
|
+
{ $ref: '#/vals/arr' },
|
|
661
|
+
],
|
|
662
|
+
}
|
|
663
|
+
const result = dereference(spec) as any
|
|
664
|
+
expect(result.refs[0]).toBe('hello')
|
|
665
|
+
expect(result.refs[1]).toBe(42)
|
|
666
|
+
expect(result.refs[2]).toEqual({ x: 1 })
|
|
667
|
+
expect(result.refs[3]).toEqual([1, 2])
|
|
668
|
+
})
|
|
669
|
+
|
|
670
|
+
test('circular ref where first encounter is NOT via $ref', () => {
|
|
671
|
+
// Schema defines Node inline (not behind a $ref), but Node's child uses $ref
|
|
672
|
+
const spec = {
|
|
673
|
+
components: {
|
|
674
|
+
schemas: {
|
|
675
|
+
Node: {
|
|
676
|
+
type: 'object',
|
|
677
|
+
properties: {
|
|
678
|
+
child: { $ref: '#/components/schemas/Node' },
|
|
679
|
+
},
|
|
680
|
+
},
|
|
681
|
+
},
|
|
682
|
+
},
|
|
683
|
+
// Access Node directly through the tree walk, not via $ref
|
|
684
|
+
direct: {
|
|
685
|
+
schema: {
|
|
686
|
+
type: 'wrapper',
|
|
687
|
+
inner: { $ref: '#/components/schemas/Node' },
|
|
688
|
+
},
|
|
689
|
+
},
|
|
690
|
+
}
|
|
691
|
+
const result = dereference(spec) as any
|
|
692
|
+
expect(result.direct.schema.inner.type).toBe('object')
|
|
693
|
+
expect(result.direct.schema.inner.properties.child).toBe(result.direct.schema.inner)
|
|
694
|
+
})
|
|
695
|
+
})
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dereferences all local `$ref` pointers in a JSON object (e.g. `{"$ref": "#/components/schemas/User"}`),
|
|
3
|
+
* replacing them inline with the resolved values. Only handles local (`#/...`) references.
|
|
4
|
+
*
|
|
5
|
+
* Handles circular references by caching a mutable placeholder before recursing.
|
|
6
|
+
*
|
|
7
|
+
* Minimal reimplementation of the dereferencing behavior from `@apidevtools/json-schema-ref-parser`
|
|
8
|
+
* (https://github.com/APIDevTools/json-schema-ref-parser). Only supports in-memory, local-pointer
|
|
9
|
+
* resolution — no file/URL resolution, no `$id` scoping.
|
|
10
|
+
*/
|
|
11
|
+
export function dereference<value>(root: value): value {
|
|
12
|
+
const cache = new Map<string, unknown>()
|
|
13
|
+
return walk(root, root, cache) as value
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function walk(node: unknown, root: unknown, cache: Map<string, unknown>): unknown {
|
|
17
|
+
if (Array.isArray(node)) return node.map((item) => walk(item, root, cache))
|
|
18
|
+
|
|
19
|
+
if (typeof node !== 'object' || node === null) return node
|
|
20
|
+
|
|
21
|
+
const obj = node as Record<string, unknown>
|
|
22
|
+
|
|
23
|
+
// Resolve $ref pointer
|
|
24
|
+
if (typeof obj.$ref === 'string' && obj.$ref.startsWith('#')) {
|
|
25
|
+
const ref = obj.$ref
|
|
26
|
+
if (cache.has(ref)) return cache.get(ref)
|
|
27
|
+
|
|
28
|
+
const resolved = resolvePointer(root, ref)
|
|
29
|
+
|
|
30
|
+
// Non-object targets (primitives, arrays) can't be circular — resolve directly
|
|
31
|
+
if (typeof resolved !== 'object' || resolved === null || Array.isArray(resolved)) {
|
|
32
|
+
const dereferenced = walk(resolved, root, cache)
|
|
33
|
+
cache.set(ref, dereferenced)
|
|
34
|
+
return dereferenced
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Use a mutable placeholder so circular refs resolve to the same object.
|
|
38
|
+
// If the walked result is not a plain object (e.g. chained ref to primitive/array),
|
|
39
|
+
// skip the placeholder and cache directly.
|
|
40
|
+
const placeholder: Record<string, unknown> = {}
|
|
41
|
+
cache.set(ref, placeholder)
|
|
42
|
+
const dereferenced = walk(resolved, root, cache)
|
|
43
|
+
if (typeof dereferenced !== 'object' || dereferenced === null || Array.isArray(dereferenced)) {
|
|
44
|
+
cache.set(ref, dereferenced)
|
|
45
|
+
return dereferenced
|
|
46
|
+
}
|
|
47
|
+
Object.assign(placeholder, dereferenced)
|
|
48
|
+
return placeholder
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const result: Record<string, unknown> = {}
|
|
52
|
+
for (const key of Object.keys(obj)) result[key] = walk(obj[key], root, cache)
|
|
53
|
+
return result
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Resolves a JSON Pointer (e.g. `#/components/schemas/User`) against a root object. */
|
|
57
|
+
function resolvePointer(root: unknown, pointer: string): unknown {
|
|
58
|
+
// "#" or "#/" → root
|
|
59
|
+
const fragment = pointer.slice(1)
|
|
60
|
+
if (fragment === '' || fragment === '/') return root
|
|
61
|
+
|
|
62
|
+
const parts = fragment
|
|
63
|
+
.slice(1)
|
|
64
|
+
.split('/')
|
|
65
|
+
.map((p) => p.replace(/~1/g, '/').replace(/~0/g, '~'))
|
|
66
|
+
|
|
67
|
+
let current: unknown = root
|
|
68
|
+
for (const part of parts) {
|
|
69
|
+
if (typeof current !== 'object' || current === null)
|
|
70
|
+
throw new Error(`Cannot resolve $ref "${pointer}": path segment "${part}" not found`)
|
|
71
|
+
current = (current as Record<string, unknown>)[part]
|
|
72
|
+
if (current === undefined) throw new Error(`Cannot resolve $ref "${pointer}": "${part}" not found`)
|
|
73
|
+
}
|
|
74
|
+
return current
|
|
75
|
+
}
|