@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/pipeline.js
ADDED
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
import { JsonPointer, JsonSchema, Predicate } from "effect";
|
|
2
|
+
import { getAt, joinPointer } from "./pointer.js";
|
|
3
|
+
import { componentSchemas, schemaSlots } from "./upgraders/3.0-to-3.1.js";
|
|
4
|
+
import { bundleFailure, circularAliasFailure, invalidPointerFailure, missingPointerFailure, } from "./errors.js";
|
|
5
|
+
const ANNOTATIONS = ["example", "discriminator", "xml", "externalDocs", "nullable"];
|
|
6
|
+
function asRecord(value) {
|
|
7
|
+
return Predicate.isObject(value) ? value : undefined;
|
|
8
|
+
}
|
|
9
|
+
const NESTED_SINGLE = [
|
|
10
|
+
"additionalProperties",
|
|
11
|
+
"unevaluatedProperties",
|
|
12
|
+
"items",
|
|
13
|
+
"contains",
|
|
14
|
+
"not",
|
|
15
|
+
"if",
|
|
16
|
+
"then",
|
|
17
|
+
"else",
|
|
18
|
+
"propertyNames",
|
|
19
|
+
"unevaluatedItems",
|
|
20
|
+
"contentSchema",
|
|
21
|
+
];
|
|
22
|
+
const NESTED_ARRAY = ["allOf", "anyOf", "oneOf", "prefixItems"];
|
|
23
|
+
const NESTED_MAP = ["properties", "patternProperties", "$defs", "dependentSchemas"];
|
|
24
|
+
export function runSchemaPipeline(document, mode, hooks) {
|
|
25
|
+
const definitions = {};
|
|
26
|
+
const definitionOrder = [];
|
|
27
|
+
const booleanDefinitions = new Map();
|
|
28
|
+
for (const slot of componentSchemas(document)) {
|
|
29
|
+
const value = slot.parent[slot.key];
|
|
30
|
+
if (typeof value === "boolean") {
|
|
31
|
+
booleanDefinitions.set(slot.key, value);
|
|
32
|
+
}
|
|
33
|
+
const normalized = normalizeSchema(slot.parent[slot.key]);
|
|
34
|
+
definitions[slot.key] = normalized.schema;
|
|
35
|
+
rememberDefinition(definitionOrder, slot.key);
|
|
36
|
+
mergeDefinitions(definitions, normalized.definitions, normalized.schema, definitionOrder);
|
|
37
|
+
}
|
|
38
|
+
const others = schemaSlots(document, false);
|
|
39
|
+
const slotSchemas = [];
|
|
40
|
+
const booleanSlots = others.map((slot) => {
|
|
41
|
+
const value = slot.parent[slot.key];
|
|
42
|
+
return typeof value === "boolean" ? value : undefined;
|
|
43
|
+
});
|
|
44
|
+
for (const slot of others) {
|
|
45
|
+
const normalized = normalizeSchema(slot.parent[slot.key]);
|
|
46
|
+
mergeDefinitions(definitions, normalized.definitions, normalized.schema, definitionOrder);
|
|
47
|
+
slot.parent[slot.key] = normalized.schema;
|
|
48
|
+
slotSchemas.push(normalized.schema);
|
|
49
|
+
}
|
|
50
|
+
const recursive = recursiveNames(definitions);
|
|
51
|
+
const rootDefs = new Set(Object.keys(asRecord(document.$defs) ?? {}));
|
|
52
|
+
const aliases = new Map();
|
|
53
|
+
const resolving = new Set();
|
|
54
|
+
for (const name of Object.keys(definitions)) {
|
|
55
|
+
definitions[name] = resolveValue(definitions[name], definitions, rootDefs, recursive, aliases, resolving, mode, hooks, `#/$defs/${name}`);
|
|
56
|
+
}
|
|
57
|
+
for (let index = 0; index < slotSchemas.length; index++) {
|
|
58
|
+
slotSchemas[index] = resolveValue(slotSchemas[index], definitions, rootDefs, recursive, aliases, resolving, mode, hooks, "#");
|
|
59
|
+
const slot = others[index];
|
|
60
|
+
if (slot)
|
|
61
|
+
slot.parent[slot.key] = slotSchemas[index];
|
|
62
|
+
}
|
|
63
|
+
const parked = [];
|
|
64
|
+
for (const [name, schema] of Object.entries(definitions))
|
|
65
|
+
park(schema, "definition", name, [], parked);
|
|
66
|
+
slotSchemas.forEach((schema, index) => park(schema, "schema", String(index), [], parked));
|
|
67
|
+
const schemas = (slotSchemas.length > 0 ? slotSchemas : [{}]);
|
|
68
|
+
const converted = JsonSchema.toMultiDocumentOpenApi3_1({
|
|
69
|
+
dialect: "draft-2020-12",
|
|
70
|
+
schemas,
|
|
71
|
+
definitions,
|
|
72
|
+
});
|
|
73
|
+
restore(converted, parked);
|
|
74
|
+
const components = asRecord(document.components) ?? {};
|
|
75
|
+
document.components = components;
|
|
76
|
+
components.schemas = orderDefinitions(converted.definitions, definitionOrder);
|
|
77
|
+
for (const slot of componentSchemas(document)) {
|
|
78
|
+
const value = booleanDefinitions.get(slot.key);
|
|
79
|
+
if (value !== undefined)
|
|
80
|
+
slot.parent[slot.key] = value;
|
|
81
|
+
}
|
|
82
|
+
others.forEach((slot, index) => {
|
|
83
|
+
slot.parent[slot.key] = booleanSlots[index] ?? converted.schemas[index];
|
|
84
|
+
});
|
|
85
|
+
rewriteLeftoverDefinitionRefs(document);
|
|
86
|
+
restoreRootDefs(document);
|
|
87
|
+
if (mode === "dereference")
|
|
88
|
+
inlineRemaining(document, recursive, hooks);
|
|
89
|
+
}
|
|
90
|
+
function normalizeSchema(value) {
|
|
91
|
+
if (typeof value === "boolean") {
|
|
92
|
+
return { dialect: "draft-2020-12", schema: value ? {} : { not: {} }, definitions: {} };
|
|
93
|
+
}
|
|
94
|
+
const record = asRecord(value);
|
|
95
|
+
if (!record)
|
|
96
|
+
throw bundleFailure("A schema position must contain an object or a boolean");
|
|
97
|
+
const normalized = JsonSchema.fromSchemaOpenApi3_1(liftSiblings(record));
|
|
98
|
+
// A schema $id is its own resource, so its $defs join the shared pool.
|
|
99
|
+
// Nested $defs on a schema without $id stay on that schema.
|
|
100
|
+
if (typeof record.$id === "string")
|
|
101
|
+
return normalized;
|
|
102
|
+
const extracted = asRecord(normalized.definitions);
|
|
103
|
+
if (!extracted || Object.keys(extracted).length === 0)
|
|
104
|
+
return normalized;
|
|
105
|
+
const schema = asRecord(normalized.schema) ?? {};
|
|
106
|
+
schema.$defs = { ...asRecord(schema.$defs), ...extracted };
|
|
107
|
+
return { dialect: normalized.dialect, schema, definitions: {} };
|
|
108
|
+
}
|
|
109
|
+
function mergeDefinitions(pool, extra, owner, order) {
|
|
110
|
+
for (const [name, schema] of Object.entries(extra)) {
|
|
111
|
+
if (!Object.hasOwn(pool, name)) {
|
|
112
|
+
pool[name] = schema;
|
|
113
|
+
rememberDefinition(order, name);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (JSON.stringify(pool[name]) === JSON.stringify(schema))
|
|
117
|
+
continue;
|
|
118
|
+
const unique = uniqueName(pool, name);
|
|
119
|
+
pool[unique] = schema;
|
|
120
|
+
rememberDefinition(order, unique);
|
|
121
|
+
rewriteDefName(owner, name, unique);
|
|
122
|
+
rewriteDefName(schema, name, unique);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function rememberDefinition(order, name) {
|
|
126
|
+
if (!order.includes(name))
|
|
127
|
+
order.push(name);
|
|
128
|
+
}
|
|
129
|
+
function orderDefinitions(definitions, order) {
|
|
130
|
+
const source = asRecord(definitions);
|
|
131
|
+
if (!source)
|
|
132
|
+
return definitions;
|
|
133
|
+
const output = {};
|
|
134
|
+
for (const name of order) {
|
|
135
|
+
if (Object.hasOwn(source, name))
|
|
136
|
+
output[name] = source[name];
|
|
137
|
+
}
|
|
138
|
+
for (const name of Object.keys(source)) {
|
|
139
|
+
if (!Object.hasOwn(output, name))
|
|
140
|
+
output[name] = source[name];
|
|
141
|
+
}
|
|
142
|
+
return output;
|
|
143
|
+
}
|
|
144
|
+
function uniqueName(pool, name) {
|
|
145
|
+
let suffix = 2;
|
|
146
|
+
while (Object.hasOwn(pool, `${name}_${suffix}`))
|
|
147
|
+
suffix += 1;
|
|
148
|
+
return `${name}_${suffix}`;
|
|
149
|
+
}
|
|
150
|
+
function rewriteDefName(schema, from, to) {
|
|
151
|
+
const record = asRecord(schema);
|
|
152
|
+
if (!record)
|
|
153
|
+
return;
|
|
154
|
+
if (record.$ref === `#/$defs/${from}`)
|
|
155
|
+
record.$ref = `#/$defs/${to}`;
|
|
156
|
+
for (const value of Object.values(record))
|
|
157
|
+
rewriteDefName(value, from, to);
|
|
158
|
+
}
|
|
159
|
+
function liftSiblings(value) {
|
|
160
|
+
if (Array.isArray(value))
|
|
161
|
+
return value.map((item) => liftSiblings(item));
|
|
162
|
+
const record = asRecord(value);
|
|
163
|
+
if (!record)
|
|
164
|
+
return value;
|
|
165
|
+
const lifted = {};
|
|
166
|
+
for (const [key, child] of Object.entries(record))
|
|
167
|
+
lifted[key] = liftSiblings(child);
|
|
168
|
+
if (typeof lifted.$ref !== "string")
|
|
169
|
+
return lifted;
|
|
170
|
+
const siblings = { ...lifted };
|
|
171
|
+
delete siblings.$ref;
|
|
172
|
+
// readOnly/writeOnly stay beside $ref so generators can wrap the reference itself.
|
|
173
|
+
const flags = {};
|
|
174
|
+
for (const key of ["readOnly", "writeOnly"]) {
|
|
175
|
+
if (siblings[key] === true) {
|
|
176
|
+
flags[key] = true;
|
|
177
|
+
delete siblings[key];
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (Object.keys(siblings).length === 0)
|
|
181
|
+
return { $ref: lifted.$ref, ...flags };
|
|
182
|
+
return { allOf: [{ $ref: lifted.$ref }, siblings], ...flags };
|
|
183
|
+
}
|
|
184
|
+
function recursiveNames(definitions) {
|
|
185
|
+
const names = Object.keys(definitions);
|
|
186
|
+
const dependencies = new Map(names.map((name) => [name, collectDefRefs(definitions[name])]));
|
|
187
|
+
const recursive = new Set();
|
|
188
|
+
const state = new Map();
|
|
189
|
+
const stack = [];
|
|
190
|
+
const visit = (name) => {
|
|
191
|
+
const current = state.get(name) ?? 0;
|
|
192
|
+
if (current === 1) {
|
|
193
|
+
const start = stack.indexOf(name);
|
|
194
|
+
const cycle = stack.slice(start);
|
|
195
|
+
if (cycle.every((item) => isPureRef(definitions[item]))) {
|
|
196
|
+
throw circularAliasFailure(name);
|
|
197
|
+
}
|
|
198
|
+
for (const item of cycle)
|
|
199
|
+
recursive.add(item);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (current === 2)
|
|
203
|
+
return;
|
|
204
|
+
state.set(name, 1);
|
|
205
|
+
stack.push(name);
|
|
206
|
+
for (const dependency of dependencies.get(name) ?? []) {
|
|
207
|
+
if (dependencies.has(dependency))
|
|
208
|
+
visit(dependency);
|
|
209
|
+
}
|
|
210
|
+
stack.pop();
|
|
211
|
+
state.set(name, 2);
|
|
212
|
+
};
|
|
213
|
+
for (const name of names)
|
|
214
|
+
visit(name);
|
|
215
|
+
return recursive;
|
|
216
|
+
}
|
|
217
|
+
function collectDefRefs(value, found = new Set(), seen = new Set()) {
|
|
218
|
+
if (typeof value !== "object" || value === null || seen.has(value))
|
|
219
|
+
return found;
|
|
220
|
+
seen.add(value);
|
|
221
|
+
if (Array.isArray(value)) {
|
|
222
|
+
for (const item of value)
|
|
223
|
+
collectDefRefs(item, found, seen);
|
|
224
|
+
return found;
|
|
225
|
+
}
|
|
226
|
+
const ref = value.$ref;
|
|
227
|
+
if (typeof ref === "string") {
|
|
228
|
+
const key = referenceKey(ref);
|
|
229
|
+
if (key)
|
|
230
|
+
found.add(key);
|
|
231
|
+
}
|
|
232
|
+
for (const child of Object.values(value))
|
|
233
|
+
collectDefRefs(child, found, seen);
|
|
234
|
+
return found;
|
|
235
|
+
}
|
|
236
|
+
const rootDefRef = (name) => `#/root-defs/${JsonPointer.escapeToken(name)}`;
|
|
237
|
+
function resolveValue(value, definitions, rootDefs, recursive, aliases, resolving, mode, hooks, pointer) {
|
|
238
|
+
if (Array.isArray(value)) {
|
|
239
|
+
for (let index = 0; index < value.length; index++) {
|
|
240
|
+
value[index] = resolveValue(value[index], definitions, rootDefs, recursive, aliases, resolving, mode, hooks, joinPointer(pointer, String(index)));
|
|
241
|
+
}
|
|
242
|
+
return value;
|
|
243
|
+
}
|
|
244
|
+
const record = asRecord(value);
|
|
245
|
+
if (!record)
|
|
246
|
+
return value;
|
|
247
|
+
if (typeof record.$ref === "string") {
|
|
248
|
+
const key = referenceKey(record.$ref);
|
|
249
|
+
if (!key)
|
|
250
|
+
return record;
|
|
251
|
+
if (!Object.hasOwn(definitions, key)) {
|
|
252
|
+
if (rootDefs.has(key)) {
|
|
253
|
+
record.$ref = rootDefRef(key);
|
|
254
|
+
return record;
|
|
255
|
+
}
|
|
256
|
+
throw missingPointerFailure(key, record.$ref, record.$ref);
|
|
257
|
+
}
|
|
258
|
+
const final = finalName(key, definitions, aliases);
|
|
259
|
+
const ref = joinPointer("#/$defs", final);
|
|
260
|
+
if (recursive.has(final) || mode === "bundle") {
|
|
261
|
+
if (recursive.has(final))
|
|
262
|
+
hooks?.onCircular?.(pointer);
|
|
263
|
+
record.$ref = ref;
|
|
264
|
+
return record;
|
|
265
|
+
}
|
|
266
|
+
if (resolving.has(final))
|
|
267
|
+
throw circularAliasFailure(final);
|
|
268
|
+
resolving.add(final);
|
|
269
|
+
const resolved = resolveValue(structuredClone(definitions[final]), definitions, rootDefs, recursive, aliases, resolving, mode, hooks, ref);
|
|
270
|
+
resolving.delete(final);
|
|
271
|
+
hooks?.onDereference?.(pointer, resolved, record, "$ref");
|
|
272
|
+
return resolved;
|
|
273
|
+
}
|
|
274
|
+
for (const [key, child] of Object.entries(record)) {
|
|
275
|
+
record[key] = resolveValue(child, definitions, rootDefs, recursive, aliases, resolving, mode, hooks, joinPointer(pointer, key));
|
|
276
|
+
}
|
|
277
|
+
return record;
|
|
278
|
+
}
|
|
279
|
+
function finalName(name, definitions, aliases) {
|
|
280
|
+
if (aliases.has(name)) {
|
|
281
|
+
const cached = aliases.get(name);
|
|
282
|
+
if (cached == null)
|
|
283
|
+
throw circularAliasFailure(name);
|
|
284
|
+
return cached;
|
|
285
|
+
}
|
|
286
|
+
aliases.set(name, null);
|
|
287
|
+
const schema = definitions[name];
|
|
288
|
+
if (isPureRef(schema)) {
|
|
289
|
+
const key = referenceKey(schema.$ref);
|
|
290
|
+
if (key && Object.hasOwn(definitions, key)) {
|
|
291
|
+
const resolved = finalName(key, definitions, aliases);
|
|
292
|
+
aliases.set(name, resolved);
|
|
293
|
+
return resolved;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
aliases.set(name, name);
|
|
297
|
+
return name;
|
|
298
|
+
}
|
|
299
|
+
function isPureRef(value) {
|
|
300
|
+
const record = asRecord(value);
|
|
301
|
+
return Boolean(record && typeof record.$ref === "string" && Object.keys(record).length === 1);
|
|
302
|
+
}
|
|
303
|
+
function referenceKey(ref) {
|
|
304
|
+
if (!ref.startsWith("#"))
|
|
305
|
+
return undefined;
|
|
306
|
+
const tokens = JsonPointer.parseUriFragment(ref);
|
|
307
|
+
if (tokens === undefined)
|
|
308
|
+
throw invalidPointerFailure(ref, ref);
|
|
309
|
+
return tokens.length === 2 && tokens[0] === "$defs" ? tokens[1] : undefined;
|
|
310
|
+
}
|
|
311
|
+
function park(value, scope, id, pointer, saved) {
|
|
312
|
+
const record = asRecord(value);
|
|
313
|
+
if (!record)
|
|
314
|
+
return;
|
|
315
|
+
for (const key of ANNOTATIONS) {
|
|
316
|
+
if (!Object.hasOwn(record, key))
|
|
317
|
+
continue;
|
|
318
|
+
saved.push({ scope, id, pointer: [...pointer], key, value: record[key] });
|
|
319
|
+
delete record[key];
|
|
320
|
+
}
|
|
321
|
+
for (const key of NESTED_SINGLE)
|
|
322
|
+
park(record[key], scope, id, [...pointer, key], saved);
|
|
323
|
+
for (const key of NESTED_ARRAY) {
|
|
324
|
+
if (!Array.isArray(record[key]))
|
|
325
|
+
continue;
|
|
326
|
+
record[key].forEach((item, index) => park(item, scope, id, [...pointer, key, String(index)], saved));
|
|
327
|
+
}
|
|
328
|
+
for (const key of NESTED_MAP) {
|
|
329
|
+
const map = asRecord(record[key]);
|
|
330
|
+
if (!map)
|
|
331
|
+
continue;
|
|
332
|
+
for (const child of Object.keys(map))
|
|
333
|
+
park(map[child], scope, id, [...pointer, key, child], saved);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
function restore(converted, saved) {
|
|
337
|
+
for (const annotation of saved) {
|
|
338
|
+
const schema = annotation.scope === "definition"
|
|
339
|
+
? converted.definitions[annotation.id]
|
|
340
|
+
: converted.schemas[Number(annotation.id)];
|
|
341
|
+
const target = annotation.pointer.length === 0 ? schema : descend(schema, annotation.pointer);
|
|
342
|
+
const record = asRecord(target);
|
|
343
|
+
if (record)
|
|
344
|
+
record[annotation.key] = annotation.value;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
function descend(value, pointer) {
|
|
348
|
+
let current = value;
|
|
349
|
+
for (const token of pointer) {
|
|
350
|
+
if (Array.isArray(current)) {
|
|
351
|
+
current = current[Number(token)];
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
const record = asRecord(current);
|
|
355
|
+
if (!record)
|
|
356
|
+
return undefined;
|
|
357
|
+
current = record[token];
|
|
358
|
+
}
|
|
359
|
+
return current;
|
|
360
|
+
}
|
|
361
|
+
function restoreRootDefs(document) {
|
|
362
|
+
const visit = (value) => {
|
|
363
|
+
if (Array.isArray(value)) {
|
|
364
|
+
for (const item of value)
|
|
365
|
+
visit(item);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
const record = asRecord(value);
|
|
369
|
+
if (!record)
|
|
370
|
+
return;
|
|
371
|
+
if (typeof record.$ref === "string" && record.$ref.startsWith("#/root-defs/")) {
|
|
372
|
+
const name = record.$ref.slice("#/root-defs/".length);
|
|
373
|
+
record.$ref = joinPointer("#/$defs", JsonPointer.unescapeToken(name));
|
|
374
|
+
}
|
|
375
|
+
for (const child of Object.values(record))
|
|
376
|
+
visit(child);
|
|
377
|
+
};
|
|
378
|
+
visit(document);
|
|
379
|
+
}
|
|
380
|
+
function rewriteLeftoverDefinitionRefs(document) {
|
|
381
|
+
const names = new Set(Object.keys(asRecord(asRecord(document.components)?.schemas) ?? {}));
|
|
382
|
+
const visit = (value) => {
|
|
383
|
+
if (Array.isArray(value)) {
|
|
384
|
+
for (const item of value)
|
|
385
|
+
visit(item);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
const record = asRecord(value);
|
|
389
|
+
if (!record)
|
|
390
|
+
return;
|
|
391
|
+
if (typeof record.$ref === "string" && record.$ref.startsWith("#/$defs/")) {
|
|
392
|
+
const tokens = JsonPointer.parseUriFragment(record.$ref);
|
|
393
|
+
const name = tokens?.[0] === "$defs" ? tokens[1] : undefined;
|
|
394
|
+
if (name && names.has(name)) {
|
|
395
|
+
record.$ref = joinPointer("#/components/schemas", [name, ...(tokens?.slice(2) ?? [])]);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
for (const child of Object.values(record))
|
|
399
|
+
visit(child);
|
|
400
|
+
};
|
|
401
|
+
visit(document);
|
|
402
|
+
}
|
|
403
|
+
export function inlineRemaining(document, recursive, hooks) {
|
|
404
|
+
const visiting = new Set();
|
|
405
|
+
const walk = (value, parent, key, pointer = "#") => {
|
|
406
|
+
if (Array.isArray(value)) {
|
|
407
|
+
for (let index = 0; index < value.length; index++) {
|
|
408
|
+
walk(value[index], value, String(index), joinPointer(pointer, String(index)));
|
|
409
|
+
}
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
const record = asRecord(value);
|
|
413
|
+
if (!record)
|
|
414
|
+
return;
|
|
415
|
+
if (typeof record.$ref === "string" && record.$ref.startsWith("#") && parent && key) {
|
|
416
|
+
const ref = record.$ref;
|
|
417
|
+
const name = componentSchemaName(ref);
|
|
418
|
+
if (name && recursive.has(name)) {
|
|
419
|
+
hooks?.onCircular?.(pointer);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
if (visiting.has(ref)) {
|
|
423
|
+
hooks?.onCircular?.(pointer);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
const target = getAt(document, ref, ref);
|
|
427
|
+
visiting.add(ref);
|
|
428
|
+
const copy = structuredClone(target);
|
|
429
|
+
walk(copy, undefined, undefined, ref);
|
|
430
|
+
visiting.delete(ref);
|
|
431
|
+
parent[key] = copy;
|
|
432
|
+
hooks?.onDereference?.(pointer, copy, parent, key);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
for (const [childKey, child] of Object.entries(record)) {
|
|
436
|
+
walk(child, record, childKey, joinPointer(pointer, childKey));
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
walk(document);
|
|
440
|
+
}
|
|
441
|
+
function componentSchemaName(ref) {
|
|
442
|
+
const tokens = JsonPointer.parseUriFragment(ref);
|
|
443
|
+
if (!tokens || tokens[0] !== "components" || tokens[1] !== "schemas" || tokens.length !== 3)
|
|
444
|
+
return undefined;
|
|
445
|
+
return tokens[2];
|
|
446
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { Effect, type FileSystem, Schema } from "effect";
|
|
2
|
+
import type { HttpClient } from "effect/http";
|
|
3
|
+
import type { TaggedFailure } from "./errors.ts";
|
|
4
|
+
export interface FileInfo {
|
|
5
|
+
url: string;
|
|
6
|
+
extension: string;
|
|
7
|
+
data?: Uint8Array | string;
|
|
8
|
+
}
|
|
9
|
+
declare const httpResolverOptions: Schema.Struct<{
|
|
10
|
+
readonly headers: Schema.optional<Schema.Union<readonly [Schema.$Record<Schema.String, Schema.String>, Schema.Null]>>;
|
|
11
|
+
readonly timeout: Schema.optional<Schema.Finite>;
|
|
12
|
+
readonly redirects: Schema.optional<Schema.Finite>;
|
|
13
|
+
readonly withCredentials: Schema.optional<Schema.Boolean>;
|
|
14
|
+
readonly safeUrlResolver: Schema.optional<Schema.Boolean>;
|
|
15
|
+
readonly allowedOrigins: Schema.optional<Schema.$Array<Schema.String>>;
|
|
16
|
+
}>;
|
|
17
|
+
export type HttpResolverOptions = typeof httpResolverOptions.Type;
|
|
18
|
+
declare const resolverPluginShape: Schema.Struct<{
|
|
19
|
+
readonly order: Schema.optional<Schema.Finite>;
|
|
20
|
+
readonly canRead: Schema.declare<(file: FileInfo) => boolean, (file: FileInfo) => boolean>;
|
|
21
|
+
readonly read: Schema.declare<(file: FileInfo) => Uint8Array | string | Promise<Uint8Array | string>, (file: FileInfo) => Uint8Array | string | Promise<Uint8Array | string>>;
|
|
22
|
+
}>;
|
|
23
|
+
export type ResolverPlugin = typeof resolverPluginShape.Type;
|
|
24
|
+
declare const parserPluginShape: Schema.Struct<{
|
|
25
|
+
readonly order: Schema.optional<Schema.Finite>;
|
|
26
|
+
readonly canParse: Schema.declare<(file: FileInfo) => boolean, (file: FileInfo) => boolean>;
|
|
27
|
+
readonly parse: Schema.declare<(file: FileInfo) => unknown, (file: FileInfo) => unknown>;
|
|
28
|
+
}>;
|
|
29
|
+
export type ParserPlugin = typeof parserPluginShape.Type;
|
|
30
|
+
declare const bundleHooksShape: Schema.Struct<{
|
|
31
|
+
readonly onBundle: Schema.optional<Schema.declare<(ref: string, value: unknown, parent: object, key: string) => void, (ref: string, value: unknown, parent: object, key: string) => void>>;
|
|
32
|
+
}>;
|
|
33
|
+
export type BundleHooks = typeof bundleHooksShape.Type;
|
|
34
|
+
declare const dereferenceHooksShape: Schema.Struct<{
|
|
35
|
+
readonly onDereference: Schema.optional<Schema.declare<(path: string, value: unknown, parent: object, key: string | number) => void, (path: string, value: unknown, parent: object, key: string | number) => void>>;
|
|
36
|
+
readonly onCircular: Schema.optional<Schema.declare<(path: string) => void, (path: string) => void>>;
|
|
37
|
+
}>;
|
|
38
|
+
export type DereferenceHooks = typeof dereferenceHooksShape.Type;
|
|
39
|
+
export declare const Options: Schema.Struct<{
|
|
40
|
+
readonly continueOnError: Schema.optional<Schema.Boolean>;
|
|
41
|
+
readonly keepUrlRefs: Schema.optional<Schema.Boolean>;
|
|
42
|
+
readonly resolve: Schema.optional<Schema.Struct<{
|
|
43
|
+
readonly file: Schema.optional<Schema.Boolean>;
|
|
44
|
+
readonly http: Schema.optional<Schema.Union<readonly [Schema.Boolean, Schema.Struct<{
|
|
45
|
+
readonly headers: Schema.optional<Schema.Union<readonly [Schema.$Record<Schema.String, Schema.String>, Schema.Null]>>;
|
|
46
|
+
readonly timeout: Schema.optional<Schema.Finite>;
|
|
47
|
+
readonly redirects: Schema.optional<Schema.Finite>;
|
|
48
|
+
readonly withCredentials: Schema.optional<Schema.Boolean>;
|
|
49
|
+
readonly safeUrlResolver: Schema.optional<Schema.Boolean>;
|
|
50
|
+
readonly allowedOrigins: Schema.optional<Schema.$Array<Schema.String>>;
|
|
51
|
+
}>]>>;
|
|
52
|
+
}>>;
|
|
53
|
+
readonly parse: Schema.optional<Schema.Struct<{
|
|
54
|
+
readonly json: Schema.optional<Schema.Boolean>;
|
|
55
|
+
readonly yaml: Schema.optional<Schema.Boolean>;
|
|
56
|
+
}>>;
|
|
57
|
+
readonly resolvers: Schema.optional<Schema.$Array<Schema.declare<{
|
|
58
|
+
readonly order?: number | undefined;
|
|
59
|
+
readonly canRead: (file: FileInfo) => boolean;
|
|
60
|
+
readonly read: (file: FileInfo) => Uint8Array | string | Promise<Uint8Array | string>;
|
|
61
|
+
}, {
|
|
62
|
+
readonly order?: number | undefined;
|
|
63
|
+
readonly canRead: (file: FileInfo) => boolean;
|
|
64
|
+
readonly read: (file: FileInfo) => Uint8Array | string | Promise<Uint8Array | string>;
|
|
65
|
+
}>>>;
|
|
66
|
+
readonly parsers: Schema.optional<Schema.$Array<Schema.declare<{
|
|
67
|
+
readonly order?: number | undefined;
|
|
68
|
+
readonly canParse: (file: FileInfo) => boolean;
|
|
69
|
+
readonly parse: (file: FileInfo) => unknown;
|
|
70
|
+
}, {
|
|
71
|
+
readonly order?: number | undefined;
|
|
72
|
+
readonly canParse: (file: FileInfo) => boolean;
|
|
73
|
+
readonly parse: (file: FileInfo) => unknown;
|
|
74
|
+
}>>>;
|
|
75
|
+
readonly bundle: Schema.optional<Schema.declare<{
|
|
76
|
+
readonly onBundle?: ((ref: string, value: unknown, parent: object, key: string) => void) | undefined;
|
|
77
|
+
}, {
|
|
78
|
+
readonly onBundle?: ((ref: string, value: unknown, parent: object, key: string) => void) | undefined;
|
|
79
|
+
}>>;
|
|
80
|
+
readonly dereference: Schema.optional<Schema.declare<{
|
|
81
|
+
readonly onDereference?: ((path: string, value: unknown, parent: object, key: string | number) => void) | undefined;
|
|
82
|
+
readonly onCircular?: ((path: string) => void) | undefined;
|
|
83
|
+
}, {
|
|
84
|
+
readonly onDereference?: ((path: string, value: unknown, parent: object, key: string | number) => void) | undefined;
|
|
85
|
+
readonly onCircular?: ((path: string) => void) | undefined;
|
|
86
|
+
}>>;
|
|
87
|
+
}>;
|
|
88
|
+
export type Options = typeof Options.Type;
|
|
89
|
+
export interface EffectPlugin<A> {
|
|
90
|
+
order: number;
|
|
91
|
+
can: (file: FileInfo) => boolean;
|
|
92
|
+
run: (file: FileInfo) => Effect.Effect<A, TaggedFailure, FileSystem.FileSystem | HttpClient.HttpClient>;
|
|
93
|
+
}
|
|
94
|
+
export declare function promisePlugin<A>(order: number, can: (file: FileInfo) => boolean, run: (file: FileInfo) => A | Promise<A>, onError: (cause: unknown, file: FileInfo) => TaggedFailure): EffectPlugin<A>;
|
|
95
|
+
export {};
|
package/dist/plugins.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Effect, Predicate, Schema } from "effect";
|
|
2
|
+
function callback() {
|
|
3
|
+
return Schema.declare((value) => Predicate.isFunction(value));
|
|
4
|
+
}
|
|
5
|
+
const httpResolverOptions = Schema.Struct({
|
|
6
|
+
headers: Schema.optional(Schema.Union([Schema.Record(Schema.String, Schema.String), Schema.Null])),
|
|
7
|
+
timeout: Schema.optional(Schema.Finite),
|
|
8
|
+
redirects: Schema.optional(Schema.Finite),
|
|
9
|
+
withCredentials: Schema.optional(Schema.Boolean),
|
|
10
|
+
safeUrlResolver: Schema.optional(Schema.Boolean),
|
|
11
|
+
allowedOrigins: Schema.optional(Schema.Array(Schema.String)),
|
|
12
|
+
});
|
|
13
|
+
const resolverPluginShape = Schema.Struct({
|
|
14
|
+
order: Schema.optional(Schema.Finite),
|
|
15
|
+
canRead: callback(),
|
|
16
|
+
read: callback(),
|
|
17
|
+
});
|
|
18
|
+
const resolverPlugin = Schema.declare(Schema.is(resolverPluginShape));
|
|
19
|
+
const parserPluginShape = Schema.Struct({
|
|
20
|
+
order: Schema.optional(Schema.Finite),
|
|
21
|
+
canParse: callback(),
|
|
22
|
+
parse: callback(),
|
|
23
|
+
});
|
|
24
|
+
const parserPlugin = Schema.declare(Schema.is(parserPluginShape));
|
|
25
|
+
const bundleHooksShape = Schema.Struct({
|
|
26
|
+
onBundle: Schema.optional(callback()),
|
|
27
|
+
});
|
|
28
|
+
const bundleHooks = Schema.declare(Schema.is(bundleHooksShape));
|
|
29
|
+
const dereferenceHooksShape = Schema.Struct({
|
|
30
|
+
onDereference: Schema.optional(callback()),
|
|
31
|
+
onCircular: Schema.optional(callback()),
|
|
32
|
+
});
|
|
33
|
+
const dereferenceHooks = Schema.declare(Schema.is(dereferenceHooksShape));
|
|
34
|
+
export const Options = Schema.Struct({
|
|
35
|
+
continueOnError: Schema.optional(Schema.Boolean),
|
|
36
|
+
keepUrlRefs: Schema.optional(Schema.Boolean),
|
|
37
|
+
resolve: Schema.optional(Schema.Struct({
|
|
38
|
+
file: Schema.optional(Schema.Boolean),
|
|
39
|
+
http: Schema.optional(Schema.Union([Schema.Boolean, httpResolverOptions])),
|
|
40
|
+
})),
|
|
41
|
+
parse: Schema.optional(Schema.Struct({
|
|
42
|
+
json: Schema.optional(Schema.Boolean),
|
|
43
|
+
yaml: Schema.optional(Schema.Boolean),
|
|
44
|
+
})),
|
|
45
|
+
resolvers: Schema.optional(Schema.Array(resolverPlugin)),
|
|
46
|
+
parsers: Schema.optional(Schema.Array(parserPlugin)),
|
|
47
|
+
bundle: Schema.optional(bundleHooks),
|
|
48
|
+
dereference: Schema.optional(dereferenceHooks),
|
|
49
|
+
});
|
|
50
|
+
export function promisePlugin(order, can, run, onError) {
|
|
51
|
+
return {
|
|
52
|
+
order,
|
|
53
|
+
can,
|
|
54
|
+
run(file) {
|
|
55
|
+
return Effect.tryPromise({
|
|
56
|
+
try: () => Promise.resolve(run(file)),
|
|
57
|
+
catch: (cause) => onError(cause, file),
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function parsePointer(pointer: string, source?: string): string[];
|
|
2
|
+
/** Join pointer tokens without percent-encoding, so `$` stays literal. */
|
|
3
|
+
export declare function joinPointer(base: string, tokens: string | readonly string[]): string;
|
|
4
|
+
export declare function getAt(root: unknown, pointer: string, source?: string): unknown;
|
|
5
|
+
export declare function setAt(root: object, pointer: string, value: unknown): void;
|
|
6
|
+
/** Copy enumerable data, including cycles, and drop non-enumerable parser symbols. */
|
|
7
|
+
export declare function cloneData(value: unknown, seen?: Map<object, unknown>): unknown;
|
package/dist/pointer.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { JsonPointer } from "effect";
|
|
2
|
+
import { invalidPointerFailure, missingPointerFailure } from "./errors.js";
|
|
3
|
+
const unsafeTokens = new Set(["__proto__", "constructor", "prototype"]);
|
|
4
|
+
export function parsePointer(pointer, source = pointer) {
|
|
5
|
+
const hash = pointer.includes("#") ? pointer.slice(pointer.indexOf("#")) : pointer;
|
|
6
|
+
if (hash === "" || hash === "#")
|
|
7
|
+
return [];
|
|
8
|
+
const fragment = hash.startsWith("#") ? hash : `#${hash}`;
|
|
9
|
+
const tokens = JsonPointer.parseUriFragment(fragment) ?? splitPointer(fragment);
|
|
10
|
+
if (tokens === undefined)
|
|
11
|
+
throw invalidPointerFailure(fragment, source);
|
|
12
|
+
return [...tokens];
|
|
13
|
+
}
|
|
14
|
+
/** Effect's URI-fragment parser rejects spaces. JSON Pointer tokens may contain them. */
|
|
15
|
+
function splitPointer(fragment) {
|
|
16
|
+
const body = fragment.startsWith("#") ? fragment.slice(1) : fragment;
|
|
17
|
+
if (body === "")
|
|
18
|
+
return [];
|
|
19
|
+
if (!body.startsWith("/"))
|
|
20
|
+
return undefined;
|
|
21
|
+
return body
|
|
22
|
+
.slice(1)
|
|
23
|
+
.split("/")
|
|
24
|
+
.map((token) => token.replace(/~1/g, "/").replace(/~0/g, "~"));
|
|
25
|
+
}
|
|
26
|
+
/** Join pointer tokens without percent-encoding, so `$` stays literal. */
|
|
27
|
+
export function joinPointer(base, tokens) {
|
|
28
|
+
const parts = typeof tokens === "string" ? [tokens] : tokens;
|
|
29
|
+
let pointer = base.includes("#") ? base : `${base}#`;
|
|
30
|
+
for (const token of parts)
|
|
31
|
+
pointer += `/${JsonPointer.escapeToken(token)}`;
|
|
32
|
+
return pointer;
|
|
33
|
+
}
|
|
34
|
+
export function getAt(root, pointer, source = pointer) {
|
|
35
|
+
const tokens = parsePointer(pointer, source);
|
|
36
|
+
let current = root;
|
|
37
|
+
for (const token of tokens) {
|
|
38
|
+
if (current === null || typeof current !== "object") {
|
|
39
|
+
throw missingPointerFailure(token, source, pointer);
|
|
40
|
+
}
|
|
41
|
+
if (!Object.hasOwn(current, token))
|
|
42
|
+
throw missingPointerFailure(token, source, pointer);
|
|
43
|
+
current = current[token];
|
|
44
|
+
}
|
|
45
|
+
return current;
|
|
46
|
+
}
|
|
47
|
+
export function setAt(root, pointer, value) {
|
|
48
|
+
const tokens = parsePointer(pointer, pointer);
|
|
49
|
+
if (tokens.length === 0 || tokens.some((token) => unsafeTokens.has(token)))
|
|
50
|
+
return;
|
|
51
|
+
let current = root;
|
|
52
|
+
for (let index = 0; index < tokens.length - 1; index++) {
|
|
53
|
+
const token = tokens[index];
|
|
54
|
+
const next = current[token];
|
|
55
|
+
if (next === null || typeof next !== "object" || Array.isArray(next))
|
|
56
|
+
current[token] = {};
|
|
57
|
+
current = current[token];
|
|
58
|
+
}
|
|
59
|
+
current[tokens[tokens.length - 1]] = value;
|
|
60
|
+
}
|
|
61
|
+
/** Copy enumerable data, including cycles, and drop non-enumerable parser symbols. */
|
|
62
|
+
export function cloneData(value, seen = new Map()) {
|
|
63
|
+
if (typeof value !== "object" || value === null)
|
|
64
|
+
return value;
|
|
65
|
+
const cached = seen.get(value);
|
|
66
|
+
if (cached !== undefined)
|
|
67
|
+
return cached;
|
|
68
|
+
if (Array.isArray(value)) {
|
|
69
|
+
const copy = [];
|
|
70
|
+
seen.set(value, copy);
|
|
71
|
+
for (const item of value)
|
|
72
|
+
copy.push(cloneData(item, seen));
|
|
73
|
+
return copy;
|
|
74
|
+
}
|
|
75
|
+
const copy = {};
|
|
76
|
+
seen.set(value, copy);
|
|
77
|
+
for (const key of Object.keys(value))
|
|
78
|
+
copy[key] = cloneData(value[key], seen);
|
|
79
|
+
return copy;
|
|
80
|
+
}
|