@shipworthy/openapi-parser 0.0.0-stage → 0.0.1
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/LICENSE +21 -0
- package/README.md +361 -2
- package/dist/bundle.d.ts +973 -0
- package/dist/bundle.js +11 -0
- package/dist/codec.d.ts +2188 -0
- package/dist/codec.js +42 -0
- package/dist/document.d.ts +9 -0
- package/dist/document.js +28 -0
- package/dist/errors.d.ts +105 -0
- package/dist/errors.js +162 -0
- package/dist/hoist.d.ts +10 -0
- package/dist/hoist.js +444 -0
- package/dist/http.d.ts +2 -0
- package/dist/http.js +163 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/load.d.ts +17 -0
- package/dist/load.js +128 -0
- package/dist/loader.d.ts +11 -0
- package/dist/loader.js +89 -0
- package/dist/parsers.d.ts +5 -0
- package/dist/parsers.js +90 -0
- package/dist/pipeline.d.ts +3 -0
- package/dist/pipeline.js +446 -0
- package/dist/plugins.d.ts +95 -0
- package/dist/plugins.js +61 -0
- package/dist/pointer.d.ts +7 -0
- package/dist/pointer.js +80 -0
- package/dist/run.d.ts +17 -0
- package/dist/run.js +313 -0
- package/dist/schemas/3.1/callback.d.ts +205 -0
- package/dist/schemas/3.1/callback.js +4 -0
- package/dist/schemas/3.1/components.d.ts +801 -0
- package/dist/schemas/3.1/components.js +25 -0
- package/dist/schemas/3.1/contact.d.ts +7 -0
- package/dist/schemas/3.1/contact.js +7 -0
- package/dist/schemas/3.1/document.d.ts +1093 -0
- package/dist/schemas/3.1/document.js +25 -0
- package/dist/schemas/3.1/encoding.d.ts +14 -0
- package/dist/schemas/3.1/encoding.js +11 -0
- package/dist/schemas/3.1/example.d.ts +8 -0
- package/dist/schemas/3.1/example.js +10 -0
- package/dist/schemas/3.1/external-docs.d.ts +6 -0
- package/dist/schemas/3.1/external-docs.js +6 -0
- package/dist/schemas/3.1/header.d.ts +194 -0
- package/dist/schemas/3.1/header.js +29 -0
- package/dist/schemas/3.1/http-method.d.ts +4 -0
- package/dist/schemas/3.1/http-method.js +12 -0
- package/dist/schemas/3.1/index.d.ts +29 -0
- package/dist/schemas/3.1/index.js +29 -0
- package/dist/schemas/3.1/info.d.ts +19 -0
- package/dist/schemas/3.1/info.js +13 -0
- package/dist/schemas/3.1/license.d.ts +7 -0
- package/dist/schemas/3.1/license.js +9 -0
- package/dist/schemas/3.1/link.d.ts +18 -0
- package/dist/schemas/3.1/link.js +13 -0
- package/dist/schemas/3.1/media-type.d.ts +27 -0
- package/dist/schemas/3.1/media-type.js +14 -0
- package/dist/schemas/3.1/oauth-flow.d.ts +8 -0
- package/dist/schemas/3.1/oauth-flow.js +8 -0
- package/dist/schemas/3.1/oauth-flows.d.ts +28 -0
- package/dist/schemas/3.1/oauth-flows.js +13 -0
- package/dist/schemas/3.1/object.d.ts +5 -0
- package/dist/schemas/3.1/object.js +17 -0
- package/dist/schemas/3.1/operation.d.ts +1040 -0
- package/dist/schemas/3.1/operation.js +25 -0
- package/dist/schemas/3.1/parameter.d.ts +101 -0
- package/dist/schemas/3.1/parameter.js +33 -0
- package/dist/schemas/3.1/path-item.d.ts +127 -0
- package/dist/schemas/3.1/path-item.js +22 -0
- package/dist/schemas/3.1/paths.d.ts +126 -0
- package/dist/schemas/3.1/paths.js +4 -0
- package/dist/schemas/3.1/reference.d.ts +13 -0
- package/dist/schemas/3.1/reference.js +11 -0
- package/dist/schemas/3.1/request-body.d.ts +31 -0
- package/dist/schemas/3.1/request-body.js +8 -0
- package/dist/schemas/3.1/response.d.ts +149 -0
- package/dist/schemas/3.1/response.js +12 -0
- package/dist/schemas/3.1/responses.d.ts +152 -0
- package/dist/schemas/3.1/responses.js +7 -0
- package/dist/schemas/3.1/schema.d.ts +4 -0
- package/dist/schemas/3.1/schema.js +6 -0
- package/dist/schemas/3.1/security-requirement.d.ts +3 -0
- package/dist/schemas/3.1/security-requirement.js +2 -0
- package/dist/schemas/3.1/security-scheme.d.ts +49 -0
- package/dist/schemas/3.1/security-scheme.js +25 -0
- package/dist/schemas/3.1/server-variable.d.ts +7 -0
- package/dist/schemas/3.1/server-variable.js +7 -0
- package/dist/schemas/3.1/server.d.ts +11 -0
- package/dist/schemas/3.1/server.js +8 -0
- package/dist/schemas/3.1/tag.d.ts +10 -0
- package/dist/schemas/3.1/tag.js +8 -0
- package/dist/upgraders/3.0-to-3.1.d.ts +9 -0
- package/dist/upgraders/3.0-to-3.1.js +310 -0
- package/dist/url.d.ts +29 -0
- package/dist/url.js +255 -0
- package/dist/yaml.d.ts +6 -0
- package/dist/yaml.js +235 -0
- package/package.json +50 -4
package/dist/hoist.js
ADDED
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
import { Predicate, Schema } from "effect";
|
|
2
|
+
import { MissingPointerFailure, bundleFailure } from "./errors.js";
|
|
3
|
+
import { cloneData, getAt, joinPointer, parsePointer, setAt } from "./pointer.js";
|
|
4
|
+
import { getHash, isFileSystemPath, isHttp, stripHash } from "./url.js";
|
|
5
|
+
const MAX_ATTEMPTS = 1000;
|
|
6
|
+
function asRecord(value) {
|
|
7
|
+
return Predicate.isObject(value) ? value : undefined;
|
|
8
|
+
}
|
|
9
|
+
const MAX_PASSES = 100_000;
|
|
10
|
+
export function findExternalRef(root, keep, abandoned = new Set()) {
|
|
11
|
+
const stack = [{ node: root, pointer: "#" }];
|
|
12
|
+
const seen = new Set();
|
|
13
|
+
while (stack.length > 0) {
|
|
14
|
+
const current = stack.pop();
|
|
15
|
+
if (typeof current.node !== "object" || current.node === null)
|
|
16
|
+
continue;
|
|
17
|
+
if (seen.has(current.node))
|
|
18
|
+
continue;
|
|
19
|
+
seen.add(current.node);
|
|
20
|
+
if (Array.isArray(current.node)) {
|
|
21
|
+
for (let index = current.node.length - 1; index >= 0; index--) {
|
|
22
|
+
stack.push({
|
|
23
|
+
node: current.node[index],
|
|
24
|
+
pointer: joinPointer(current.pointer, String(index)),
|
|
25
|
+
parent: current.node,
|
|
26
|
+
key: String(index),
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const record = current.node;
|
|
32
|
+
if (current.parent &&
|
|
33
|
+
current.key &&
|
|
34
|
+
typeof record.$ref === "string" &&
|
|
35
|
+
!record.$ref.startsWith("#") &&
|
|
36
|
+
!keep(record.$ref) &&
|
|
37
|
+
!abandoned.has(current.pointer)) {
|
|
38
|
+
return {
|
|
39
|
+
parent: current.parent,
|
|
40
|
+
key: current.key,
|
|
41
|
+
ref: record.$ref,
|
|
42
|
+
pointer: current.pointer,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const keys = Object.keys(record);
|
|
46
|
+
for (let index = keys.length - 1; index >= 0; index--) {
|
|
47
|
+
const key = keys[index];
|
|
48
|
+
stack.push({
|
|
49
|
+
node: record[key],
|
|
50
|
+
pointer: joinPointer(current.pointer, key),
|
|
51
|
+
parent: record,
|
|
52
|
+
key,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
export function hoistExternalRefs(root, readValue, hooks, keep) {
|
|
59
|
+
const placed = new Map();
|
|
60
|
+
const componentSource = new Map();
|
|
61
|
+
const buckets = new Map();
|
|
62
|
+
const abandoned = new Set();
|
|
63
|
+
for (let pass = 0; pass < MAX_PASSES; pass++) {
|
|
64
|
+
const found = findExternalRef(root, keep, abandoned);
|
|
65
|
+
if (!found) {
|
|
66
|
+
collapseRefChains(root);
|
|
67
|
+
return placed;
|
|
68
|
+
}
|
|
69
|
+
const url = stripHash(found.ref);
|
|
70
|
+
const hash = getHash(found.ref);
|
|
71
|
+
const placeKey = `${url}${hash}`;
|
|
72
|
+
if (readValue(url) === undefined) {
|
|
73
|
+
abandoned.add(found.pointer);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
// Some documents put `$ref` under `properties`, so the map itself is the reference.
|
|
77
|
+
// Rewriting that to a component `$ref` leaves a string where a schema object is required.
|
|
78
|
+
if (found.key === "properties" || found.key === "patternProperties") {
|
|
79
|
+
const inlined = cloneData(valueAt(readValue(url), hash, found.ref));
|
|
80
|
+
rebaseInternalRefs(inlined, url);
|
|
81
|
+
inlineRef(found, inlined, hooks);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const existing = placed.get(placeKey);
|
|
85
|
+
if (existing) {
|
|
86
|
+
rewriteRef(found, existing, hooks);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const value = cloneData(valueAt(readValue(url), hash, found.ref));
|
|
90
|
+
rebaseInternalRefs(value, url);
|
|
91
|
+
const rootName = oas3DefaultRoot(found.pointer);
|
|
92
|
+
const identical = rootName
|
|
93
|
+
? matchingComponent(root, rootName, value, url, componentSource)
|
|
94
|
+
: undefined;
|
|
95
|
+
if (identical) {
|
|
96
|
+
placed.set(placeKey, identical);
|
|
97
|
+
rewriteRef(found, identical, hooks);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const reserveName = rootName === "#/components/schemas" && childReservesName(value, fileBaseName(url));
|
|
101
|
+
const target = generateKey(root, url, hash, found.pointer, buckets, reserveName);
|
|
102
|
+
if (target) {
|
|
103
|
+
setAt(root, target, value);
|
|
104
|
+
placed.set(placeKey, target);
|
|
105
|
+
componentSource.set(target, url);
|
|
106
|
+
rewriteRef(found, target, hooks);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
inlineRef(found, value, hooks);
|
|
110
|
+
placed.set(placeKey, found.pointer);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
throw bundleFailure("Could not finish bundling external references.");
|
|
114
|
+
}
|
|
115
|
+
function valueAt(document, hash, ref) {
|
|
116
|
+
if (hash === "#")
|
|
117
|
+
return document;
|
|
118
|
+
return getAt(document, hash, ref);
|
|
119
|
+
}
|
|
120
|
+
function rebaseInternalRefs(value, fileUrl, seen = new Set()) {
|
|
121
|
+
if (typeof value !== "object" || value === null || seen.has(value))
|
|
122
|
+
return;
|
|
123
|
+
seen.add(value);
|
|
124
|
+
if (Array.isArray(value)) {
|
|
125
|
+
for (const item of value)
|
|
126
|
+
rebaseInternalRefs(item, fileUrl, seen);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const record = value;
|
|
130
|
+
if (typeof record.$ref === "string" && record.$ref.startsWith("#"))
|
|
131
|
+
record.$ref = `${fileUrl}${record.$ref}`;
|
|
132
|
+
for (const child of Object.values(record))
|
|
133
|
+
rebaseInternalRefs(child, fileUrl, seen);
|
|
134
|
+
}
|
|
135
|
+
function collapseRefChains(root) {
|
|
136
|
+
for (let pass = 0; pass < 20; pass++) {
|
|
137
|
+
let changed = false;
|
|
138
|
+
const visit = (value, seen = new Set()) => {
|
|
139
|
+
if (typeof value !== "object" || value === null || seen.has(value))
|
|
140
|
+
return;
|
|
141
|
+
seen.add(value);
|
|
142
|
+
if (Array.isArray(value)) {
|
|
143
|
+
for (const item of value)
|
|
144
|
+
visit(item, seen);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const record = value;
|
|
148
|
+
if (typeof record.$ref === "string" &&
|
|
149
|
+
record.$ref.startsWith("#") &&
|
|
150
|
+
Object.keys(record).length === 1) {
|
|
151
|
+
try {
|
|
152
|
+
const target = asRecord(getAt(root, record.$ref, record.$ref));
|
|
153
|
+
if (target &&
|
|
154
|
+
typeof target.$ref === "string" &&
|
|
155
|
+
Object.keys(target).length === 1 &&
|
|
156
|
+
target.$ref !== record.$ref) {
|
|
157
|
+
record.$ref = target.$ref;
|
|
158
|
+
changed = true;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
if (!Schema.is(MissingPointerFailure)(error))
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
for (const child of Object.values(record))
|
|
167
|
+
visit(child, seen);
|
|
168
|
+
};
|
|
169
|
+
visit(root);
|
|
170
|
+
if (!changed)
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function rewriteRef(found, pointer, hooks) {
|
|
175
|
+
const current = found.parent[found.key];
|
|
176
|
+
const record = asRecord(current);
|
|
177
|
+
if (!record)
|
|
178
|
+
return;
|
|
179
|
+
record.$ref = pointer;
|
|
180
|
+
hooks?.onBundle?.(pointer, record, found.parent, found.key);
|
|
181
|
+
}
|
|
182
|
+
function inlineRef(found, value, hooks) {
|
|
183
|
+
const current = asRecord(found.parent[found.key]);
|
|
184
|
+
const siblings = { ...current };
|
|
185
|
+
delete siblings.$ref;
|
|
186
|
+
if (Object.keys(siblings).length === 0) {
|
|
187
|
+
found.parent[found.key] = value;
|
|
188
|
+
}
|
|
189
|
+
else if (oas3DefaultRoot(found.pointer) === "#/components/schemas" ||
|
|
190
|
+
found.pointer.includes("/schema")) {
|
|
191
|
+
found.parent[found.key] = { allOf: [value, siblings] };
|
|
192
|
+
}
|
|
193
|
+
else if (asRecord(value)) {
|
|
194
|
+
found.parent[found.key] = { ...asRecord(value), ...siblings };
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
found.parent[found.key] = value;
|
|
198
|
+
}
|
|
199
|
+
hooks?.onBundle?.(found.ref, found.parent[found.key], found.parent, found.key);
|
|
200
|
+
}
|
|
201
|
+
function generateKey(schema, file, hash, pathFromRoot, buckets, childReservesBase = false) {
|
|
202
|
+
if (!isOpenApiDocument(schema))
|
|
203
|
+
return null;
|
|
204
|
+
const root = oas3DefaultRoot(pathFromRoot);
|
|
205
|
+
if (!root)
|
|
206
|
+
return null;
|
|
207
|
+
if (!isFileSystemPath(file) && !isHttp(file) && !file.includes("://"))
|
|
208
|
+
return null;
|
|
209
|
+
if (hash === "#" && isUnderDirectRoot(pathFromRoot, root))
|
|
210
|
+
return null;
|
|
211
|
+
const existing = existingComponentPointer(schema, file, hash, root);
|
|
212
|
+
if (existing && existing !== pathFromRoot)
|
|
213
|
+
return existing;
|
|
214
|
+
if (hash !== "#") {
|
|
215
|
+
if (isUnderDirectRoot(hash, root) || !isInRoot(hash, root)) {
|
|
216
|
+
const token = pointerTokens(hash).at(-1);
|
|
217
|
+
if (!token)
|
|
218
|
+
return null;
|
|
219
|
+
return register(schema, root, `${file}${hash}`, suggest(bucket(schema, root, buckets).taken, token, fileBaseName(file)), buckets);
|
|
220
|
+
}
|
|
221
|
+
const filePointer = keyForFile(schema, file, root, buckets);
|
|
222
|
+
if (filePointer == null)
|
|
223
|
+
return null;
|
|
224
|
+
return keyForPointer(schema, filePointer + hash.slice(1), root, buckets);
|
|
225
|
+
}
|
|
226
|
+
return keyForFile(schema, file, root, buckets, hash === "#" && childReservesBase);
|
|
227
|
+
}
|
|
228
|
+
function isOpenApiDocument(schema) {
|
|
229
|
+
return typeof schema.openapi === "string";
|
|
230
|
+
}
|
|
231
|
+
export function oas3DefaultRoot(pathFromRoot) {
|
|
232
|
+
const path = pathFromRoot.split("/");
|
|
233
|
+
if (canReferenceParameter(path))
|
|
234
|
+
return "#/components/parameters";
|
|
235
|
+
if (canReferenceResponse(path))
|
|
236
|
+
return "#/components/responses";
|
|
237
|
+
if (canReferenceRequestBody(path))
|
|
238
|
+
return "#/components/requestBodies";
|
|
239
|
+
if (canReferenceHeader(path))
|
|
240
|
+
return "#/components/headers";
|
|
241
|
+
if (canReferenceExample(path))
|
|
242
|
+
return "#/components/examples";
|
|
243
|
+
if (canReferenceSecurityScheme(path))
|
|
244
|
+
return "#/components/securitySchemes";
|
|
245
|
+
if (canReferenceLink(path))
|
|
246
|
+
return "#/components/links";
|
|
247
|
+
if (canReferenceCallback(path))
|
|
248
|
+
return "#/components/callbacks";
|
|
249
|
+
if (canReferenceSchema(path))
|
|
250
|
+
return "#/components/schemas";
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
function canReferenceSchema(path) {
|
|
254
|
+
return path.includes("schema") || (path[1] === "components" && path[2] === "schemas");
|
|
255
|
+
}
|
|
256
|
+
function canReferenceParameter(path) {
|
|
257
|
+
return path.at(-2) === "parameters";
|
|
258
|
+
}
|
|
259
|
+
function canReferenceResponse(path) {
|
|
260
|
+
return path.at(-2) === "responses";
|
|
261
|
+
}
|
|
262
|
+
function canReferenceRequestBody(path) {
|
|
263
|
+
return path.at(-1) === "requestBody";
|
|
264
|
+
}
|
|
265
|
+
function canReferenceHeader(path) {
|
|
266
|
+
return path.at(-2) === "headers";
|
|
267
|
+
}
|
|
268
|
+
function canReferenceExample(path) {
|
|
269
|
+
return path.at(-2) === "examples";
|
|
270
|
+
}
|
|
271
|
+
function canReferenceSecurityScheme(path) {
|
|
272
|
+
return path[1] === "components" && path[2] === "securitySchemes";
|
|
273
|
+
}
|
|
274
|
+
function canReferenceLink(path) {
|
|
275
|
+
return ((path[1] === "components" && path[2] === "links") ||
|
|
276
|
+
(path[1] === "paths" && path.includes("links")));
|
|
277
|
+
}
|
|
278
|
+
function canReferenceCallback(path) {
|
|
279
|
+
return ((path[1] === "components" && path[2] === "callbacks") ||
|
|
280
|
+
(path[1] === "paths" && path.includes("callbacks")));
|
|
281
|
+
}
|
|
282
|
+
function isUnderDirectRoot(pathFromRoot, root) {
|
|
283
|
+
const pointer = pointerTokens(pathFromRoot);
|
|
284
|
+
const rootTokens = pointerTokens(root);
|
|
285
|
+
return (pointer.length === rootTokens.length + 1 &&
|
|
286
|
+
rootTokens.every((token, index) => pointer[index] === token));
|
|
287
|
+
}
|
|
288
|
+
function isInRoot(pointer, root) {
|
|
289
|
+
const pointerTokens_ = pointerTokens(pointer);
|
|
290
|
+
const rootTokens = pointerTokens(root);
|
|
291
|
+
return (pointerTokens_.length > rootTokens.length &&
|
|
292
|
+
rootTokens.every((token, index) => pointerTokens_[index] === token));
|
|
293
|
+
}
|
|
294
|
+
function pointerTokens(pointer) {
|
|
295
|
+
if (!pointer.startsWith("#"))
|
|
296
|
+
return [];
|
|
297
|
+
try {
|
|
298
|
+
return parsePointer(pointer);
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
return [];
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
function existingComponentPointer(schema, file, hash, root) {
|
|
305
|
+
const map = asRecord(valueAtSafe(schema, root));
|
|
306
|
+
if (!map)
|
|
307
|
+
return undefined;
|
|
308
|
+
for (const [key, child] of Object.entries(map)) {
|
|
309
|
+
const record = asRecord(child);
|
|
310
|
+
if (!record || typeof record.$ref !== "string")
|
|
311
|
+
continue;
|
|
312
|
+
const extras = Object.keys(record).filter((name) => name !== "$ref" && name !== "summary" && name !== "description");
|
|
313
|
+
if (extras.length > 0)
|
|
314
|
+
continue;
|
|
315
|
+
if (stripHash(record.$ref) !== file || getHash(record.$ref) !== hash)
|
|
316
|
+
continue;
|
|
317
|
+
return joinPointer(root, key);
|
|
318
|
+
}
|
|
319
|
+
return undefined;
|
|
320
|
+
}
|
|
321
|
+
function keyForFile(schema, file, root, buckets, childReservesBase = false) {
|
|
322
|
+
const names = bucket(schema, root, buckets);
|
|
323
|
+
if (names.generated.has(file))
|
|
324
|
+
return names.generated.get(file) ?? null;
|
|
325
|
+
const base = prettify(fileBaseName(file));
|
|
326
|
+
if (!base)
|
|
327
|
+
return register(schema, root, file, null, buckets);
|
|
328
|
+
const taken = childReservesBase ? new Set(names.taken).add(base) : names.taken;
|
|
329
|
+
return register(schema, root, file, suggest(taken, base), buckets);
|
|
330
|
+
}
|
|
331
|
+
function childReservesName(value, base) {
|
|
332
|
+
if (!base)
|
|
333
|
+
return false;
|
|
334
|
+
const stack = [value];
|
|
335
|
+
const seen = new Set();
|
|
336
|
+
while (stack.length > 0) {
|
|
337
|
+
const current = stack.pop();
|
|
338
|
+
if (typeof current !== "object" || current === null || seen.has(current))
|
|
339
|
+
continue;
|
|
340
|
+
seen.add(current);
|
|
341
|
+
if (Array.isArray(current)) {
|
|
342
|
+
stack.push(...current);
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
const record = current;
|
|
346
|
+
if (typeof record.$ref === "string" && !record.$ref.startsWith("#")) {
|
|
347
|
+
const token = getHash(record.$ref).split("/").at(-1);
|
|
348
|
+
if (token === base)
|
|
349
|
+
return true;
|
|
350
|
+
}
|
|
351
|
+
stack.push(...Object.values(record));
|
|
352
|
+
}
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
function keyForPointer(schema, pointer, root, buckets) {
|
|
356
|
+
const names = bucket(schema, root, buckets);
|
|
357
|
+
if (names.generated.has(pointer))
|
|
358
|
+
return names.generated.get(pointer) ?? null;
|
|
359
|
+
const fragment = `${root.slice(1)}/`;
|
|
360
|
+
const actualPath = pointer.split(fragment).slice(1);
|
|
361
|
+
const base = prettify(actualPath.join("/"));
|
|
362
|
+
if (!base)
|
|
363
|
+
return register(schema, root, pointer, null, buckets);
|
|
364
|
+
return register(schema, root, pointer, suggest(names.taken, base), buckets);
|
|
365
|
+
}
|
|
366
|
+
function register(schema, root, id, key, buckets) {
|
|
367
|
+
const names = bucket(schema, root, buckets);
|
|
368
|
+
if (key === null) {
|
|
369
|
+
names.generated.set(id, null);
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
names.taken.add(key);
|
|
373
|
+
const pointer = joinPointer(root, key);
|
|
374
|
+
names.generated.set(id, pointer);
|
|
375
|
+
return pointer;
|
|
376
|
+
}
|
|
377
|
+
function bucket(schema, root, buckets) {
|
|
378
|
+
const id = `${root}`;
|
|
379
|
+
const existing = buckets.get(id);
|
|
380
|
+
if (existing)
|
|
381
|
+
return existing;
|
|
382
|
+
const taken = new Set();
|
|
383
|
+
const value = valueAtSafe(schema, root);
|
|
384
|
+
if (asRecord(value)) {
|
|
385
|
+
for (const key of Object.keys(value))
|
|
386
|
+
taken.add(key);
|
|
387
|
+
}
|
|
388
|
+
const created = { taken, generated: new Map() };
|
|
389
|
+
buckets.set(id, created);
|
|
390
|
+
return created;
|
|
391
|
+
}
|
|
392
|
+
function valueAtSafe(root, pointer) {
|
|
393
|
+
try {
|
|
394
|
+
return getAt(root, pointer);
|
|
395
|
+
}
|
|
396
|
+
catch (error) {
|
|
397
|
+
if (Schema.is(MissingPointerFailure)(error))
|
|
398
|
+
return undefined;
|
|
399
|
+
throw error;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
function suggest(taken, key, fileBase) {
|
|
403
|
+
if (!taken.has(key))
|
|
404
|
+
return key;
|
|
405
|
+
if (fileBase && fileBase !== key) {
|
|
406
|
+
const prefixed = `${fileBase}_${key}`;
|
|
407
|
+
if (!taken.has(prefixed))
|
|
408
|
+
return prefixed;
|
|
409
|
+
}
|
|
410
|
+
for (let attempt = 2; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
411
|
+
const candidate = `${key}-${attempt}`;
|
|
412
|
+
if (!taken.has(candidate))
|
|
413
|
+
return candidate;
|
|
414
|
+
}
|
|
415
|
+
throw bundleFailure(`Could not generate a unique component name for ${key}.`);
|
|
416
|
+
}
|
|
417
|
+
function matchingComponent(_schema, _root, _value, _source, _componentSource) {
|
|
418
|
+
// Each external file keeps its own component, even when two files contain the same
|
|
419
|
+
// schema. DigitalOcean repeats one enum as apps_domain_phase and app_alert_phase.
|
|
420
|
+
return undefined;
|
|
421
|
+
}
|
|
422
|
+
function prettify(key) {
|
|
423
|
+
const pretty = key.replace(/(?:\.|[\\/]+)([a-z])?/g, (_match, letter, index) => {
|
|
424
|
+
if (letter === undefined)
|
|
425
|
+
return index === 0 ? "" : "_";
|
|
426
|
+
return `_${letter.toUpperCase()}`;
|
|
427
|
+
});
|
|
428
|
+
return pretty;
|
|
429
|
+
}
|
|
430
|
+
function fileBaseName(file) {
|
|
431
|
+
let pathname = file;
|
|
432
|
+
if (file.includes("://")) {
|
|
433
|
+
try {
|
|
434
|
+
pathname = decodeURIComponent(new URL(file).pathname);
|
|
435
|
+
}
|
|
436
|
+
catch {
|
|
437
|
+
pathname = file;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
const slash = Math.max(pathname.lastIndexOf("/"), pathname.lastIndexOf("\\"));
|
|
441
|
+
const base = slash >= 0 ? pathname.slice(slash + 1) : pathname;
|
|
442
|
+
const dot = base.lastIndexOf(".");
|
|
443
|
+
return dot > 0 ? base.slice(0, dot) : base;
|
|
444
|
+
}
|
package/dist/http.d.ts
ADDED
package/dist/http.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import * as NodeHttpClient from "@effect/platform-node/NodeHttpClient";
|
|
2
|
+
import { Agent } from "@effect/platform-node/Undici";
|
|
3
|
+
import { Cause, Effect, Schedule } from "effect";
|
|
4
|
+
import { HttpClient } from "effect/http";
|
|
5
|
+
import { ResolverFailure, nodeErrorCode, timeoutFailure } from "./errors.js";
|
|
6
|
+
import { httpOrigin, isAllowedOrigin, isHttp, isUnsafeUrl, resolveUrlSafety, } from "./url.js";
|
|
7
|
+
export function httpResolver(rootUrl, options, policy = "reference") {
|
|
8
|
+
if (policy === "root") {
|
|
9
|
+
return {
|
|
10
|
+
order: 200,
|
|
11
|
+
can: (file) => isHttp(file.url),
|
|
12
|
+
run: (file) => Effect.flatMap(HttpClient.HttpClient, (client) => client
|
|
13
|
+
.pipe(HttpClient.filterStatusOk)
|
|
14
|
+
.get(file.url)
|
|
15
|
+
.pipe(Effect.flatMap((response) => response.arrayBuffer), Effect.map((buffer) => new Uint8Array(buffer)), Effect.mapError((cause) => resolverFailure(file.url, cause)))),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
if (options.resolve?.http === false)
|
|
19
|
+
return undefined;
|
|
20
|
+
const http = httpSettings(rootUrl, options);
|
|
21
|
+
return {
|
|
22
|
+
order: 200,
|
|
23
|
+
can: (file) => isHttp(file.url) &&
|
|
24
|
+
(http.safeUrlResolver === false ||
|
|
25
|
+
isAllowedOrigin(file.url, http.allowedOrigins) ||
|
|
26
|
+
!isUnsafeUrl(file.url)),
|
|
27
|
+
run: (file) => download(file.url, http),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function httpSettings(rootUrl, options) {
|
|
31
|
+
const user = typeof options.resolve?.http === "object" ? options.resolve.http : {};
|
|
32
|
+
const origin = httpOrigin(rootUrl);
|
|
33
|
+
const allowed = [...(user.allowedOrigins ?? [])];
|
|
34
|
+
if (origin && !allowed.includes(origin))
|
|
35
|
+
allowed.push(origin);
|
|
36
|
+
return {
|
|
37
|
+
headers: null,
|
|
38
|
+
timeout: 60_000,
|
|
39
|
+
redirects: 5,
|
|
40
|
+
withCredentials: false,
|
|
41
|
+
safeUrlResolver: true,
|
|
42
|
+
...user,
|
|
43
|
+
allowedOrigins: allowed,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const download = Effect.fnUntraced(function* (href, http, redirects = []) {
|
|
47
|
+
const nextRedirects = [...redirects, href];
|
|
48
|
+
const requestedPrivate = isAllowedOrigin(href, http.allowedOrigins) && isUnsafeUrl(href);
|
|
49
|
+
let addresses;
|
|
50
|
+
if (http.safeUrlResolver !== false && !requestedPrivate) {
|
|
51
|
+
const safety = yield* resolveUrlSafety(href);
|
|
52
|
+
if (safety.unsafe) {
|
|
53
|
+
return yield* new ResolverFailure({
|
|
54
|
+
message: `Error downloading ${href}: Unsafe URL blocked by safeUrlResolver: ${href}`,
|
|
55
|
+
source: href,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
addresses = safety.addresses;
|
|
59
|
+
}
|
|
60
|
+
const hop = addresses && addresses.length > 0
|
|
61
|
+
? yield* Effect.scoped(pinnedHop(href, http, addresses, nextRedirects))
|
|
62
|
+
: yield* receive(yield* HttpClient.HttpClient, href, http, nextRedirects);
|
|
63
|
+
if (hop.kind === "redirect")
|
|
64
|
+
return yield* download(hop.href, hop.http, nextRedirects);
|
|
65
|
+
return hop.bytes;
|
|
66
|
+
});
|
|
67
|
+
const pinnedHop = Effect.fnUntraced(function* (href, http, addresses, redirects) {
|
|
68
|
+
const dispatcher = yield* Effect.acquireRelease(Effect.sync(() => new Agent({
|
|
69
|
+
connect: { lookup: pinnedLookup(href, addresses) },
|
|
70
|
+
autoSelectFamily: true,
|
|
71
|
+
})), (agent) => Effect.promise(() => agent.destroy()));
|
|
72
|
+
const client = yield* NodeHttpClient.makeUndici.pipe(Effect.provideService(NodeHttpClient.Dispatcher, dispatcher));
|
|
73
|
+
return yield* receive(client, href, http, redirects);
|
|
74
|
+
});
|
|
75
|
+
const receive = Effect.fnUntraced(function* (client, href, http, redirects) {
|
|
76
|
+
const request = client
|
|
77
|
+
.pipe(HttpClient.retryTransient({
|
|
78
|
+
schedule: Schedule.exponential("100 millis"),
|
|
79
|
+
times: 3,
|
|
80
|
+
}))
|
|
81
|
+
.get(href, { headers: http.headers || undefined })
|
|
82
|
+
.pipe(Effect.mapError((cause) => resolverFailure(href, cause)));
|
|
83
|
+
const timeout = http.timeout;
|
|
84
|
+
const response = yield* typeof timeout === "number" && timeout > 0
|
|
85
|
+
? request.pipe(Effect.timeout(timeout), Effect.catchIf(Cause.isTimeoutError, () => Effect.fail(timeoutFailure(timeout))))
|
|
86
|
+
: request;
|
|
87
|
+
if (response.status >= 400) {
|
|
88
|
+
return yield* new ResolverFailure({
|
|
89
|
+
message: `Error downloading ${href}: HTTP ERROR ${response.status}`,
|
|
90
|
+
source: href,
|
|
91
|
+
ioErrorCode: String(response.status),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
if (response.status >= 300) {
|
|
95
|
+
if (http.redirects !== undefined && redirects.length > http.redirects) {
|
|
96
|
+
return yield* new ResolverFailure({
|
|
97
|
+
message: `Error downloading ${redirects[0]}. \nToo many redirects: \n ${redirects.join(" \n ")}`,
|
|
98
|
+
source: href,
|
|
99
|
+
ioErrorCode: String(response.status),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
const location = response.headers.location;
|
|
103
|
+
if (!location) {
|
|
104
|
+
return yield* new ResolverFailure({
|
|
105
|
+
message: `Error downloading ${href}: HTTP ${response.status} redirect with no location header`,
|
|
106
|
+
source: href,
|
|
107
|
+
ioErrorCode: String(response.status),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
const redirectTo = new URL(location, href).href;
|
|
111
|
+
const redirectOptions = new URL(redirectTo).origin === new URL(href).origin ? http : withoutSensitiveHeaders(http);
|
|
112
|
+
return { kind: "redirect", href: redirectTo, http: redirectOptions };
|
|
113
|
+
}
|
|
114
|
+
const buffer = yield* response.arrayBuffer.pipe(Effect.mapError((cause) => resolverFailure(href, cause)));
|
|
115
|
+
return { kind: "body", bytes: new Uint8Array(buffer) };
|
|
116
|
+
});
|
|
117
|
+
function pinnedLookup(href, addresses) {
|
|
118
|
+
const expectedHostname = normalizeLookupHostname(new URL(href).hostname);
|
|
119
|
+
return (hostname, options, callback) => {
|
|
120
|
+
const normalizedHostname = normalizeLookupHostname(hostname);
|
|
121
|
+
const requestedFamily = options.family === "IPv4" ? 4 : options.family === "IPv6" ? 6 : options.family || 0;
|
|
122
|
+
const candidates = addresses.filter(({ family }) => requestedFamily === 0 || family === requestedFamily);
|
|
123
|
+
if (normalizedHostname !== expectedHostname || candidates.length === 0) {
|
|
124
|
+
const error = new Error(`No validated address is available for ${hostname}`);
|
|
125
|
+
error.code = "ENOTFOUND";
|
|
126
|
+
callback(error, "", 0);
|
|
127
|
+
}
|
|
128
|
+
else if (options.all) {
|
|
129
|
+
callback(null, candidates);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
const selected = candidates[0];
|
|
133
|
+
callback(null, selected.address, selected.family);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function normalizeLookupHostname(hostname) {
|
|
138
|
+
return hostname
|
|
139
|
+
.replace(/^\[|\]$/g, "")
|
|
140
|
+
.replace(/\.+$/, "")
|
|
141
|
+
.toLowerCase();
|
|
142
|
+
}
|
|
143
|
+
function withoutSensitiveHeaders(http) {
|
|
144
|
+
if (!http.headers)
|
|
145
|
+
return http;
|
|
146
|
+
const headers = new Headers(http.headers);
|
|
147
|
+
headers.delete("authorization");
|
|
148
|
+
headers.delete("proxy-authorization");
|
|
149
|
+
headers.delete("cookie");
|
|
150
|
+
return { ...http, headers: Object.fromEntries(headers.entries()) };
|
|
151
|
+
}
|
|
152
|
+
function resolverFailure(href, cause) {
|
|
153
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
154
|
+
const ioErrorCode = nodeErrorCode(cause);
|
|
155
|
+
return new ResolverFailure({
|
|
156
|
+
message: message.startsWith("Error downloading")
|
|
157
|
+
? message
|
|
158
|
+
: `Error downloading ${href}: ${message}`,
|
|
159
|
+
source: href,
|
|
160
|
+
cause,
|
|
161
|
+
...(ioErrorCode ? { ioErrorCode } : {}),
|
|
162
|
+
});
|
|
163
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/dist/load.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Effect, type FileSystem, Path, Schema } from "effect";
|
|
2
|
+
import type { HttpClient } from "effect/http";
|
|
3
|
+
import { type OpenAPIDocument } from "./document.ts";
|
|
4
|
+
import { type LoadError } from "./errors.ts";
|
|
5
|
+
declare const loadOptions: Schema.Struct<{
|
|
6
|
+
readonly cwd: Schema.optional<Schema.Union<readonly [Schema.String, Schema.URL]>>;
|
|
7
|
+
}>;
|
|
8
|
+
export type LoadOptions = typeof loadOptions.Type;
|
|
9
|
+
interface LoadedDocument {
|
|
10
|
+
readonly document: OpenAPIDocument;
|
|
11
|
+
readonly absoluteRef: string;
|
|
12
|
+
}
|
|
13
|
+
type LoadServices = FileSystem.FileSystem | Path.Path | HttpClient.HttpClient;
|
|
14
|
+
export declare const loadDocument: (source: unknown, options?: {
|
|
15
|
+
readonly cwd?: string | URL | undefined;
|
|
16
|
+
} | undefined) => Effect.Effect<LoadedDocument, LoadError, LoadServices>;
|
|
17
|
+
export {};
|