@shipworthy/openapi-parser 0.0.2 → 1.0.0
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/README.md +6 -6
- package/dist/errors.d.ts +42 -14
- package/dist/errors.js +3 -3
- package/dist/hoist.js +48 -43
- package/dist/http.js +6 -9
- package/dist/load.js +18 -18
- package/dist/loader.js +7 -7
- package/dist/parsers.js +7 -18
- package/dist/pipeline.js +48 -46
- package/dist/pointer.d.ts +3 -0
- package/dist/pointer.js +26 -22
- package/dist/run.js +18 -18
- package/dist/schemas/3.1/callback.js +2 -2
- package/dist/schemas/3.1/example.js +2 -2
- package/dist/schemas/3.1/header.js +3 -3
- package/dist/schemas/3.1/media-type.js +2 -2
- package/dist/schemas/3.1/object.js +2 -5
- package/dist/schemas/3.1/parameter.js +3 -3
- package/dist/schemas/3.1/responses.js +2 -2
- package/dist/upgraders/3.0-to-3.1.js +34 -31
- package/dist/url.d.ts +1 -1
- package/dist/url.js +42 -45
- package/dist/yaml.js +50 -46
- package/package.json +7 -6
package/dist/parsers.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Effect, Predicate } from "effect";
|
|
1
|
+
import { Effect, Predicate, Schema, String as Str } from "effect";
|
|
2
2
|
import { ParseError, ParserFailure } from "./errors.js";
|
|
3
3
|
import { promisePlugin } from "./plugins.js";
|
|
4
4
|
import { extensionOf } from "./url.js";
|
|
@@ -7,19 +7,18 @@ function decodeData(data) {
|
|
|
7
7
|
if (data === undefined)
|
|
8
8
|
return "";
|
|
9
9
|
const text = typeof data === "string" ? data : new TextDecoder().decode(data);
|
|
10
|
-
return
|
|
10
|
+
return Str.replace(/^\uFEFF/, "")(text);
|
|
11
11
|
}
|
|
12
|
+
const decodeJsonText = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Unknown));
|
|
12
13
|
function parseJsonText(text) {
|
|
13
|
-
|
|
14
|
-
return null;
|
|
15
|
-
return JSON.parse(text);
|
|
14
|
+
return Str.isEmpty(Str.trim(text)) ? Effect.succeed(null) : decodeJsonText(text);
|
|
16
15
|
}
|
|
17
16
|
export const parseText = Effect.fnUntraced(function* (source) {
|
|
18
17
|
const text = decodeData(source);
|
|
19
|
-
const first =
|
|
18
|
+
const first = Str.trimStart(text)[0];
|
|
20
19
|
const yaml = () => Effect.try({ try: () => parseYamlDocument(text), catch: parseFailure });
|
|
21
20
|
if (first === "{" || first === "[") {
|
|
22
|
-
return yield*
|
|
21
|
+
return yield* parseJsonText(text).pipe(Effect.catch(yaml));
|
|
23
22
|
}
|
|
24
23
|
return yield* yaml();
|
|
25
24
|
});
|
|
@@ -45,20 +44,10 @@ function jsonParser() {
|
|
|
45
44
|
order: 100,
|
|
46
45
|
can: (file) => extensionOf(file.url) === ".json" || extensionOf(file.url) === "",
|
|
47
46
|
run(file) {
|
|
48
|
-
return Effect.
|
|
49
|
-
try: () => parseJson(file),
|
|
50
|
-
catch: (cause) => new ParserFailure({
|
|
51
|
-
message: cause instanceof Error ? cause.message : "Parser Error",
|
|
52
|
-
source: file.url,
|
|
53
|
-
cause,
|
|
54
|
-
}),
|
|
55
|
-
});
|
|
47
|
+
return parseJsonText(decodeData(file.data)).pipe(Effect.mapError((cause) => new ParserFailure({ message: cause.message, source: file.url, cause })));
|
|
56
48
|
},
|
|
57
49
|
};
|
|
58
50
|
}
|
|
59
|
-
function parseJson(file) {
|
|
60
|
-
return parseJsonText(decodeData(file.data));
|
|
61
|
-
}
|
|
62
51
|
function yamlParser() {
|
|
63
52
|
return {
|
|
64
53
|
order: 200,
|
package/dist/pipeline.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { JsonPointer, JsonSchema, Predicate } from "effect";
|
|
1
|
+
import { Array as Arr, Formatter, JsonPointer, JsonSchema, Predicate, Record as Rec, String as Str, } from "effect";
|
|
2
2
|
import { getAt, joinPointer } from "./pointer.js";
|
|
3
3
|
import { componentSchemas, schemaSlots } from "./upgraders/3.0-to-3.1.js";
|
|
4
4
|
import { bundleFailure, circularAliasFailure, invalidPointerFailure, missingPointerFailure, } from "./errors.js";
|
|
@@ -37,7 +37,7 @@ export function runSchemaPipeline(document, mode, hooks) {
|
|
|
37
37
|
}
|
|
38
38
|
const others = schemaSlots(document, false);
|
|
39
39
|
const slotSchemas = [];
|
|
40
|
-
const booleanSlots =
|
|
40
|
+
const booleanSlots = Arr.map(others, (slot) => {
|
|
41
41
|
const value = slot.parent[slot.key];
|
|
42
42
|
return typeof value === "boolean" ? value : undefined;
|
|
43
43
|
});
|
|
@@ -48,10 +48,10 @@ export function runSchemaPipeline(document, mode, hooks) {
|
|
|
48
48
|
slotSchemas.push(normalized.schema);
|
|
49
49
|
}
|
|
50
50
|
const recursive = recursiveNames(definitions);
|
|
51
|
-
const rootDefs = new Set(
|
|
51
|
+
const rootDefs = new Set(Rec.keys(asRecord(document.$defs) ?? {}));
|
|
52
52
|
const aliases = new Map();
|
|
53
53
|
const resolving = new Set();
|
|
54
|
-
for (const name of
|
|
54
|
+
for (const name of Rec.keys(definitions)) {
|
|
55
55
|
definitions[name] = resolveValue(definitions[name], definitions, rootDefs, recursive, aliases, resolving, mode, hooks, `#/$defs/${name}`);
|
|
56
56
|
}
|
|
57
57
|
for (let index = 0; index < slotSchemas.length; index++) {
|
|
@@ -61,9 +61,9 @@ export function runSchemaPipeline(document, mode, hooks) {
|
|
|
61
61
|
slot.parent[slot.key] = slotSchemas[index];
|
|
62
62
|
}
|
|
63
63
|
const parked = [];
|
|
64
|
-
for (const [name, schema] of
|
|
64
|
+
for (const [name, schema] of Rec.toEntries(definitions))
|
|
65
65
|
park(schema, "definition", name, [], parked);
|
|
66
|
-
|
|
66
|
+
Arr.forEach(slotSchemas, (schema, index) => park(schema, "schema", String(index), [], parked));
|
|
67
67
|
const schemas = (slotSchemas.length > 0 ? slotSchemas : [{}]);
|
|
68
68
|
const converted = JsonSchema.toMultiDocumentOpenApi3_1({
|
|
69
69
|
dialect: "draft-2020-12",
|
|
@@ -79,7 +79,7 @@ export function runSchemaPipeline(document, mode, hooks) {
|
|
|
79
79
|
if (value !== undefined)
|
|
80
80
|
slot.parent[slot.key] = value;
|
|
81
81
|
}
|
|
82
|
-
|
|
82
|
+
Arr.forEach(others, (slot, index) => {
|
|
83
83
|
slot.parent[slot.key] = booleanSlots[index] ?? converted.schemas[index];
|
|
84
84
|
});
|
|
85
85
|
rewriteLeftoverDefinitionRefs(document);
|
|
@@ -100,20 +100,20 @@ function normalizeSchema(value) {
|
|
|
100
100
|
if (typeof record.$id === "string")
|
|
101
101
|
return normalized;
|
|
102
102
|
const extracted = asRecord(normalized.definitions);
|
|
103
|
-
if (!extracted ||
|
|
103
|
+
if (!extracted || Rec.isEmptyRecord(extracted))
|
|
104
104
|
return normalized;
|
|
105
105
|
const schema = asRecord(normalized.schema) ?? {};
|
|
106
106
|
schema.$defs = { ...asRecord(schema.$defs), ...extracted };
|
|
107
107
|
return { dialect: normalized.dialect, schema, definitions: {} };
|
|
108
108
|
}
|
|
109
109
|
function mergeDefinitions(pool, extra, owner, order) {
|
|
110
|
-
for (const [name, schema] of
|
|
111
|
-
if (!
|
|
110
|
+
for (const [name, schema] of Rec.toEntries(extra)) {
|
|
111
|
+
if (!Rec.has(pool, name)) {
|
|
112
112
|
pool[name] = schema;
|
|
113
113
|
rememberDefinition(order, name);
|
|
114
114
|
continue;
|
|
115
115
|
}
|
|
116
|
-
if (
|
|
116
|
+
if (Formatter.formatJson(pool[name]) === Formatter.formatJson(schema))
|
|
117
117
|
continue;
|
|
118
118
|
const unique = uniqueName(pool, name);
|
|
119
119
|
pool[unique] = schema;
|
|
@@ -132,18 +132,18 @@ function orderDefinitions(definitions, order) {
|
|
|
132
132
|
return definitions;
|
|
133
133
|
const output = {};
|
|
134
134
|
for (const name of order) {
|
|
135
|
-
if (
|
|
135
|
+
if (Rec.has(source, name))
|
|
136
136
|
output[name] = source[name];
|
|
137
137
|
}
|
|
138
|
-
for (const name of
|
|
139
|
-
if (!
|
|
138
|
+
for (const name of Rec.keys(source)) {
|
|
139
|
+
if (!Rec.has(output, name))
|
|
140
140
|
output[name] = source[name];
|
|
141
141
|
}
|
|
142
142
|
return output;
|
|
143
143
|
}
|
|
144
144
|
function uniqueName(pool, name) {
|
|
145
145
|
let suffix = 2;
|
|
146
|
-
while (
|
|
146
|
+
while (Rec.has(pool, `${name}_${suffix}`))
|
|
147
147
|
suffix += 1;
|
|
148
148
|
return `${name}_${suffix}`;
|
|
149
149
|
}
|
|
@@ -153,12 +153,12 @@ function rewriteDefName(schema, from, to) {
|
|
|
153
153
|
return;
|
|
154
154
|
if (record.$ref === `#/$defs/${from}`)
|
|
155
155
|
record.$ref = `#/$defs/${to}`;
|
|
156
|
-
for (const value of
|
|
156
|
+
for (const value of Rec.values(record))
|
|
157
157
|
rewriteDefName(value, from, to);
|
|
158
158
|
}
|
|
159
159
|
function recursiveNames(definitions) {
|
|
160
|
-
const names =
|
|
161
|
-
const dependencies = new Map(
|
|
160
|
+
const names = Rec.keys(definitions);
|
|
161
|
+
const dependencies = new Map(Arr.map(names, (name) => [name, collectDefRefs(definitions[name])]));
|
|
162
162
|
const recursive = new Set();
|
|
163
163
|
const state = new Map();
|
|
164
164
|
const stack = [];
|
|
@@ -167,7 +167,7 @@ function recursiveNames(definitions) {
|
|
|
167
167
|
if (current === 1) {
|
|
168
168
|
const start = stack.indexOf(name);
|
|
169
169
|
const cycle = stack.slice(start);
|
|
170
|
-
if (
|
|
170
|
+
if (Arr.every(cycle, (item) => isPureRef(definitions[item]))) {
|
|
171
171
|
throw circularAliasFailure(name);
|
|
172
172
|
}
|
|
173
173
|
for (const item of cycle)
|
|
@@ -190,27 +190,28 @@ function recursiveNames(definitions) {
|
|
|
190
190
|
return recursive;
|
|
191
191
|
}
|
|
192
192
|
function collectDefRefs(value, found = new Set(), seen = new Set()) {
|
|
193
|
-
if (
|
|
193
|
+
if (!Predicate.isObjectOrArray(value) || seen.has(value))
|
|
194
194
|
return found;
|
|
195
195
|
seen.add(value);
|
|
196
|
-
if (
|
|
196
|
+
if (Arr.isArray(value)) {
|
|
197
197
|
for (const item of value)
|
|
198
198
|
collectDefRefs(item, found, seen);
|
|
199
199
|
return found;
|
|
200
200
|
}
|
|
201
|
-
const
|
|
201
|
+
const record = value;
|
|
202
|
+
const ref = record.$ref;
|
|
202
203
|
if (typeof ref === "string") {
|
|
203
204
|
const key = referenceKey(ref);
|
|
204
205
|
if (key)
|
|
205
206
|
found.add(key);
|
|
206
207
|
}
|
|
207
|
-
for (const child of
|
|
208
|
+
for (const child of Rec.values(record))
|
|
208
209
|
collectDefRefs(child, found, seen);
|
|
209
210
|
return found;
|
|
210
211
|
}
|
|
211
212
|
const rootDefRef = (name) => `#/root-defs/${JsonPointer.escapeToken(name)}`;
|
|
212
213
|
function resolveValue(value, definitions, rootDefs, recursive, aliases, resolving, mode, hooks, pointer) {
|
|
213
|
-
if (
|
|
214
|
+
if (Arr.isArray(value)) {
|
|
214
215
|
for (let index = 0; index < value.length; index++) {
|
|
215
216
|
value[index] = resolveValue(value[index], definitions, rootDefs, recursive, aliases, resolving, mode, hooks, joinPointer(pointer, String(index)));
|
|
216
217
|
}
|
|
@@ -221,7 +222,7 @@ function resolveValue(value, definitions, rootDefs, recursive, aliases, resolvin
|
|
|
221
222
|
return value;
|
|
222
223
|
if (typeof record.$ref === "string") {
|
|
223
224
|
const key = referenceKey(record.$ref);
|
|
224
|
-
if (key && !
|
|
225
|
+
if (key && !Rec.has(definitions, key)) {
|
|
225
226
|
if (rootDefs.has(key)) {
|
|
226
227
|
record.$ref = rootDefRef(key);
|
|
227
228
|
}
|
|
@@ -245,7 +246,7 @@ function resolveValue(value, definitions, rootDefs, recursive, aliases, resolvin
|
|
|
245
246
|
resolving.delete(final);
|
|
246
247
|
const siblings = { ...record };
|
|
247
248
|
delete siblings.$ref;
|
|
248
|
-
const result =
|
|
249
|
+
const result = Rec.isEmptyRecord(siblings)
|
|
249
250
|
? resolved
|
|
250
251
|
: {
|
|
251
252
|
allOf: [
|
|
@@ -258,7 +259,7 @@ function resolveValue(value, definitions, rootDefs, recursive, aliases, resolvin
|
|
|
258
259
|
}
|
|
259
260
|
}
|
|
260
261
|
}
|
|
261
|
-
for (const [key, child] of
|
|
262
|
+
for (const [key, child] of Rec.toEntries(record)) {
|
|
262
263
|
record[key] = resolveValue(child, definitions, rootDefs, recursive, aliases, resolving, mode, hooks, joinPointer(pointer, key));
|
|
263
264
|
}
|
|
264
265
|
return record;
|
|
@@ -274,7 +275,7 @@ function finalName(name, definitions, aliases) {
|
|
|
274
275
|
const schema = definitions[name];
|
|
275
276
|
if (isPureRef(schema)) {
|
|
276
277
|
const key = referenceKey(schema.$ref);
|
|
277
|
-
if (key &&
|
|
278
|
+
if (key && Rec.has(definitions, key)) {
|
|
278
279
|
const resolved = finalName(key, definitions, aliases);
|
|
279
280
|
aliases.set(name, resolved);
|
|
280
281
|
return resolved;
|
|
@@ -285,10 +286,10 @@ function finalName(name, definitions, aliases) {
|
|
|
285
286
|
}
|
|
286
287
|
function isPureRef(value) {
|
|
287
288
|
const record = asRecord(value);
|
|
288
|
-
return Boolean(record && typeof record.$ref === "string" &&
|
|
289
|
+
return Boolean(record && typeof record.$ref === "string" && Rec.size(record) === 1);
|
|
289
290
|
}
|
|
290
291
|
function referenceKey(ref) {
|
|
291
|
-
if (!
|
|
292
|
+
if (!Str.startsWith("#")(ref))
|
|
292
293
|
return undefined;
|
|
293
294
|
const tokens = JsonPointer.parseUriFragment(ref);
|
|
294
295
|
if (tokens === undefined)
|
|
@@ -300,7 +301,7 @@ function park(value, scope, id, pointer, saved) {
|
|
|
300
301
|
if (!record)
|
|
301
302
|
return;
|
|
302
303
|
for (const key of ANNOTATIONS) {
|
|
303
|
-
if (!
|
|
304
|
+
if (!Rec.has(record, key))
|
|
304
305
|
continue;
|
|
305
306
|
saved.push({ scope, id, pointer: [...pointer], key, value: record[key] });
|
|
306
307
|
delete record[key];
|
|
@@ -308,15 +309,16 @@ function park(value, scope, id, pointer, saved) {
|
|
|
308
309
|
for (const key of NESTED_SINGLE)
|
|
309
310
|
park(record[key], scope, id, [...pointer, key], saved);
|
|
310
311
|
for (const key of NESTED_ARRAY) {
|
|
311
|
-
|
|
312
|
+
const items = record[key];
|
|
313
|
+
if (!Arr.isArray(items))
|
|
312
314
|
continue;
|
|
313
|
-
|
|
315
|
+
Arr.forEach(items, (item, index) => park(item, scope, id, [...pointer, key, String(index)], saved));
|
|
314
316
|
}
|
|
315
317
|
for (const key of NESTED_MAP) {
|
|
316
318
|
const map = asRecord(record[key]);
|
|
317
319
|
if (!map)
|
|
318
320
|
continue;
|
|
319
|
-
for (const child of
|
|
321
|
+
for (const child of Rec.keys(map))
|
|
320
322
|
park(map[child], scope, id, [...pointer, key, child], saved);
|
|
321
323
|
}
|
|
322
324
|
}
|
|
@@ -334,7 +336,7 @@ function restore(converted, saved) {
|
|
|
334
336
|
function descend(value, pointer) {
|
|
335
337
|
let current = value;
|
|
336
338
|
for (const token of pointer) {
|
|
337
|
-
if (
|
|
339
|
+
if (Arr.isArray(current)) {
|
|
338
340
|
current = current[Number(token)];
|
|
339
341
|
continue;
|
|
340
342
|
}
|
|
@@ -347,7 +349,7 @@ function descend(value, pointer) {
|
|
|
347
349
|
}
|
|
348
350
|
function restoreRootDefs(document) {
|
|
349
351
|
const visit = (value) => {
|
|
350
|
-
if (
|
|
352
|
+
if (Arr.isArray(value)) {
|
|
351
353
|
for (const item of value)
|
|
352
354
|
visit(item);
|
|
353
355
|
return;
|
|
@@ -355,19 +357,19 @@ function restoreRootDefs(document) {
|
|
|
355
357
|
const record = asRecord(value);
|
|
356
358
|
if (!record)
|
|
357
359
|
return;
|
|
358
|
-
if (typeof record.$ref === "string" &&
|
|
359
|
-
const name =
|
|
360
|
+
if (typeof record.$ref === "string" && Str.startsWith("#/root-defs/")(record.$ref)) {
|
|
361
|
+
const name = Str.slice("#/root-defs/".length)(record.$ref);
|
|
360
362
|
record.$ref = joinPointer("#/$defs", JsonPointer.unescapeToken(name));
|
|
361
363
|
}
|
|
362
|
-
for (const child of
|
|
364
|
+
for (const child of Rec.values(record))
|
|
363
365
|
visit(child);
|
|
364
366
|
};
|
|
365
367
|
visit(document);
|
|
366
368
|
}
|
|
367
369
|
function rewriteLeftoverDefinitionRefs(document) {
|
|
368
|
-
const names = new Set(
|
|
370
|
+
const names = new Set(Rec.keys(asRecord(asRecord(document.components)?.schemas) ?? {}));
|
|
369
371
|
const visit = (value) => {
|
|
370
|
-
if (
|
|
372
|
+
if (Arr.isArray(value)) {
|
|
371
373
|
for (const item of value)
|
|
372
374
|
visit(item);
|
|
373
375
|
return;
|
|
@@ -375,14 +377,14 @@ function rewriteLeftoverDefinitionRefs(document) {
|
|
|
375
377
|
const record = asRecord(value);
|
|
376
378
|
if (!record)
|
|
377
379
|
return;
|
|
378
|
-
if (typeof record.$ref === "string" &&
|
|
380
|
+
if (typeof record.$ref === "string" && Str.startsWith("#/$defs/")(record.$ref)) {
|
|
379
381
|
const tokens = JsonPointer.parseUriFragment(record.$ref);
|
|
380
382
|
const name = tokens?.[0] === "$defs" ? tokens[1] : undefined;
|
|
381
383
|
if (name && names.has(name)) {
|
|
382
384
|
record.$ref = joinPointer("#/components/schemas", [name, ...(tokens?.slice(2) ?? [])]);
|
|
383
385
|
}
|
|
384
386
|
}
|
|
385
|
-
for (const child of
|
|
387
|
+
for (const child of Rec.values(record))
|
|
386
388
|
visit(child);
|
|
387
389
|
};
|
|
388
390
|
visit(document);
|
|
@@ -390,7 +392,7 @@ function rewriteLeftoverDefinitionRefs(document) {
|
|
|
390
392
|
export function inlineRemaining(document, recursive, hooks) {
|
|
391
393
|
const visiting = new Set();
|
|
392
394
|
const walk = (value, parent, key, pointer = "#") => {
|
|
393
|
-
if (
|
|
395
|
+
if (Arr.isArray(value)) {
|
|
394
396
|
for (let index = 0; index < value.length; index++) {
|
|
395
397
|
walk(value[index], value, String(index), joinPointer(pointer, String(index)));
|
|
396
398
|
}
|
|
@@ -399,7 +401,7 @@ export function inlineRemaining(document, recursive, hooks) {
|
|
|
399
401
|
const record = asRecord(value);
|
|
400
402
|
if (!record)
|
|
401
403
|
return;
|
|
402
|
-
if (typeof record.$ref === "string" &&
|
|
404
|
+
if (typeof record.$ref === "string" && Str.startsWith("#")(record.$ref) && parent && key) {
|
|
403
405
|
const ref = record.$ref;
|
|
404
406
|
const name = componentSchemaName(ref);
|
|
405
407
|
if (name && recursive.has(name)) {
|
|
@@ -419,7 +421,7 @@ export function inlineRemaining(document, recursive, hooks) {
|
|
|
419
421
|
hooks?.onDereference?.(pointer, copy, parent, key);
|
|
420
422
|
return;
|
|
421
423
|
}
|
|
422
|
-
for (const [childKey, child] of
|
|
424
|
+
for (const [childKey, child] of Rec.toEntries(record)) {
|
|
423
425
|
walk(child, record, childKey, joinPointer(pointer, childKey));
|
|
424
426
|
}
|
|
425
427
|
};
|
package/dist/pointer.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { Option } from "effect";
|
|
1
2
|
export declare function parsePointer(pointer: string, source?: string): string[];
|
|
2
3
|
/** Join pointer tokens without percent-encoding, so `$` stays literal. */
|
|
3
4
|
export declare function joinPointer(base: string, tokens: string | readonly string[]): string;
|
|
5
|
+
/** Reads the member or array element that a JSON Pointer token names (RFC 6901). */
|
|
6
|
+
export declare function childAt(value: unknown, token: string): Option.Option<unknown>;
|
|
4
7
|
export declare function getAt(root: unknown, pointer: string, source?: string): unknown;
|
|
5
8
|
export declare function setAt(root: object, pointer: string, value: unknown): void;
|
|
6
9
|
/** Copy enumerable data, including cycles, and drop non-enumerable parser symbols. */
|
package/dist/pointer.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { JsonPointer } from "effect";
|
|
1
|
+
import { Array as Arr, JsonPointer, Option, pipe, Predicate, Record as Rec, String as Str, } from "effect";
|
|
2
2
|
import { invalidPointerFailure, missingPointerFailure } from "./errors.js";
|
|
3
3
|
const unsafeTokens = new Set(["__proto__", "constructor", "prototype"]);
|
|
4
4
|
export function parsePointer(pointer, source = pointer) {
|
|
5
|
-
const hash = pointer.
|
|
5
|
+
const hash = pipe(pointer, Str.indexOf("#"), Option.match({ onNone: () => pointer, onSome: (index) => Str.slice(index)(pointer) }));
|
|
6
6
|
if (hash === "" || hash === "#")
|
|
7
7
|
return [];
|
|
8
|
-
const fragment =
|
|
8
|
+
const fragment = Str.startsWith("#")(hash) ? hash : `#${hash}`;
|
|
9
9
|
const tokens = JsonPointer.parseUriFragment(fragment) ?? splitPointer(fragment);
|
|
10
10
|
if (tokens === undefined)
|
|
11
11
|
throw invalidPointerFailure(fragment, source);
|
|
@@ -13,46 +13,49 @@ export function parsePointer(pointer, source = pointer) {
|
|
|
13
13
|
}
|
|
14
14
|
/** Effect's URI-fragment parser rejects spaces. JSON Pointer tokens may contain them. */
|
|
15
15
|
function splitPointer(fragment) {
|
|
16
|
-
const body =
|
|
16
|
+
const body = Str.startsWith("#")(fragment) ? Str.slice(1)(fragment) : fragment;
|
|
17
17
|
if (body === "")
|
|
18
18
|
return [];
|
|
19
|
-
if (!
|
|
19
|
+
if (!Str.startsWith("/")(body))
|
|
20
20
|
return undefined;
|
|
21
|
-
return body
|
|
22
|
-
.slice(1)
|
|
23
|
-
.split("/")
|
|
24
|
-
.map((token) => token.replace(/~1/g, "/").replace(/~0/g, "~"));
|
|
21
|
+
return pipe(body, Str.slice(1), Str.split("/"), Arr.map((token) => pipe(token, Str.replace(/~1/g, "/"), Str.replace(/~0/g, "~"))));
|
|
25
22
|
}
|
|
26
23
|
/** Join pointer tokens without percent-encoding, so `$` stays literal. */
|
|
27
24
|
export function joinPointer(base, tokens) {
|
|
28
25
|
const parts = typeof tokens === "string" ? [tokens] : tokens;
|
|
29
|
-
let pointer =
|
|
26
|
+
let pointer = Str.includes("#")(base) ? base : `${base}#`;
|
|
30
27
|
for (const token of parts)
|
|
31
28
|
pointer += `/${JsonPointer.escapeToken(token)}`;
|
|
32
29
|
return pointer;
|
|
33
30
|
}
|
|
31
|
+
const arrayIndex = /^(?:0|[1-9]\d*)$/;
|
|
32
|
+
/** Reads the member or array element that a JSON Pointer token names (RFC 6901). */
|
|
33
|
+
export function childAt(value, token) {
|
|
34
|
+
if (Arr.isArray(value)) {
|
|
35
|
+
return arrayIndex.test(token) ? Arr.get(value, Number(token)) : Option.none();
|
|
36
|
+
}
|
|
37
|
+
return Predicate.isObject(value) ? Rec.get(value, token) : Option.none();
|
|
38
|
+
}
|
|
34
39
|
export function getAt(root, pointer, source = pointer) {
|
|
35
40
|
const tokens = parsePointer(pointer, source);
|
|
36
41
|
let current = root;
|
|
37
42
|
for (const token of tokens) {
|
|
38
|
-
|
|
43
|
+
const child = childAt(current, token);
|
|
44
|
+
if (Option.isNone(child))
|
|
39
45
|
throw missingPointerFailure(token, source, pointer);
|
|
40
|
-
|
|
41
|
-
if (!Object.hasOwn(current, token))
|
|
42
|
-
throw missingPointerFailure(token, source, pointer);
|
|
43
|
-
current = current[token];
|
|
46
|
+
current = child.value;
|
|
44
47
|
}
|
|
45
48
|
return current;
|
|
46
49
|
}
|
|
47
50
|
export function setAt(root, pointer, value) {
|
|
48
51
|
const tokens = parsePointer(pointer, pointer);
|
|
49
|
-
if (tokens.length === 0 ||
|
|
52
|
+
if (tokens.length === 0 || Arr.some(tokens, (token) => unsafeTokens.has(token)))
|
|
50
53
|
return;
|
|
51
54
|
let current = root;
|
|
52
55
|
for (let index = 0; index < tokens.length - 1; index++) {
|
|
53
56
|
const token = tokens[index];
|
|
54
57
|
const next = current[token];
|
|
55
|
-
if (
|
|
58
|
+
if (!Predicate.isObject(next))
|
|
56
59
|
current[token] = {};
|
|
57
60
|
current = current[token];
|
|
58
61
|
}
|
|
@@ -60,21 +63,22 @@ export function setAt(root, pointer, value) {
|
|
|
60
63
|
}
|
|
61
64
|
/** Copy enumerable data, including cycles, and drop non-enumerable parser symbols. */
|
|
62
65
|
export function cloneData(value, seen = new Map()) {
|
|
63
|
-
if (
|
|
66
|
+
if (!Predicate.isObjectOrArray(value))
|
|
64
67
|
return value;
|
|
65
68
|
const cached = seen.get(value);
|
|
66
69
|
if (cached !== undefined)
|
|
67
70
|
return cached;
|
|
68
|
-
if (
|
|
71
|
+
if (Arr.isArray(value)) {
|
|
69
72
|
const copy = [];
|
|
70
73
|
seen.set(value, copy);
|
|
71
74
|
for (const item of value)
|
|
72
75
|
copy.push(cloneData(item, seen));
|
|
73
76
|
return copy;
|
|
74
77
|
}
|
|
78
|
+
const source = value;
|
|
75
79
|
const copy = {};
|
|
76
|
-
seen.set(
|
|
77
|
-
for (const key of
|
|
78
|
-
copy[key] = cloneData(
|
|
80
|
+
seen.set(source, copy);
|
|
81
|
+
for (const key of Rec.keys(source))
|
|
82
|
+
copy[key] = cloneData(source[key], seen);
|
|
79
83
|
return copy;
|
|
80
84
|
}
|
package/dist/run.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { Array, Cache, Effect, Predicate, Ref, Schema } from "effect";
|
|
1
|
+
import { Array, Cache, Effect, Option, Predicate, Record as Rec, Ref, Schema, String as Str, } from "effect";
|
|
2
2
|
import { ParserFailure, groupFailure, isLeafFailure, missingPointerFailure, syncFailure, } from "./errors.js";
|
|
3
3
|
import { hoistExternalRefs } from "./hoist.js";
|
|
4
4
|
import { loadDocument, makeContext } from "./loader.js";
|
|
5
5
|
import { Options } from "./plugins.js";
|
|
6
|
-
import { cloneData, joinPointer, parsePointer } from "./pointer.js";
|
|
6
|
+
import { childAt, cloneData, joinPointer, parsePointer } from "./pointer.js";
|
|
7
7
|
import { inlineRemaining, runSchemaPipeline } from "./pipeline.js";
|
|
8
8
|
import { isOpenApi3, upgradeOpenApi3_0 } from "./upgraders/3.0-to-3.1.js";
|
|
9
9
|
import { getHash, isHttp, normalizeRef, resolveAgainst, stripHash } from "./url.js";
|
|
@@ -112,7 +112,7 @@ function isBundledRef(record) {
|
|
|
112
112
|
const ref = record.$ref;
|
|
113
113
|
if (typeof ref !== "string" || !isHttp(ref))
|
|
114
114
|
return true;
|
|
115
|
-
return
|
|
115
|
+
return Array.every(Rec.keys(record), (key) => referenceKeys.has(key));
|
|
116
116
|
}
|
|
117
117
|
function* externalRefs(node, kept) {
|
|
118
118
|
const seen = new Set();
|
|
@@ -128,14 +128,14 @@ function* externalRefs(node, kept) {
|
|
|
128
128
|
}
|
|
129
129
|
const record = current;
|
|
130
130
|
if (typeof record.$ref === "string" &&
|
|
131
|
-
!
|
|
131
|
+
!Str.startsWith("#")(record.$ref) &&
|
|
132
132
|
!kept.has(record.$ref) &&
|
|
133
133
|
isBundledRef(record)) {
|
|
134
134
|
yield record.$ref;
|
|
135
135
|
}
|
|
136
136
|
if (typeof record.externalValue === "string" && !kept.has(record.externalValue))
|
|
137
137
|
yield record.externalValue;
|
|
138
|
-
stack.push(...
|
|
138
|
+
stack.push(...Rec.values(record));
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
141
|
function absolutize(node, base, ids, keep, kept, seen = new Set()) {
|
|
@@ -153,7 +153,7 @@ function absolutize(node, base, ids, keep, kept, seen = new Set()) {
|
|
|
153
153
|
next = resolveAgainst(base, record.$id);
|
|
154
154
|
ids.set(stripHash(next), record);
|
|
155
155
|
}
|
|
156
|
-
if (typeof record.$ref === "string" && !
|
|
156
|
+
if (typeof record.$ref === "string" && !Str.startsWith("#")(record.$ref)) {
|
|
157
157
|
if (keep(record.$ref))
|
|
158
158
|
kept.add(record.$ref);
|
|
159
159
|
else
|
|
@@ -164,8 +164,8 @@ function absolutize(node, base, ids, keep, kept, seen = new Set()) {
|
|
|
164
164
|
? discriminator.mapping
|
|
165
165
|
: undefined;
|
|
166
166
|
if (mapping) {
|
|
167
|
-
for (const [key, value] of
|
|
168
|
-
if (typeof value !== "string" ||
|
|
167
|
+
for (const [key, value] of Rec.toEntries(mapping)) {
|
|
168
|
+
if (typeof value !== "string" || Str.startsWith("#")(value))
|
|
169
169
|
continue;
|
|
170
170
|
if (keep(value))
|
|
171
171
|
kept.add(value);
|
|
@@ -179,7 +179,7 @@ function absolutize(node, base, ids, keep, kept, seen = new Set()) {
|
|
|
179
179
|
else
|
|
180
180
|
record.externalValue = resolveAgainst(base, record.externalValue);
|
|
181
181
|
}
|
|
182
|
-
for (const [key, value] of
|
|
182
|
+
for (const [key, value] of Rec.toEntries(record)) {
|
|
183
183
|
if (key === "$ref" || key === "$id")
|
|
184
184
|
continue;
|
|
185
185
|
absolutize(value, next, ids, keep, kept, seen);
|
|
@@ -193,7 +193,7 @@ function rewriteIdRefs(root, ids) {
|
|
|
193
193
|
pointers.set(url, pointer);
|
|
194
194
|
}
|
|
195
195
|
walkRecords(root, (record) => {
|
|
196
|
-
if (typeof record.$ref !== "string" ||
|
|
196
|
+
if (typeof record.$ref !== "string" || Str.startsWith("#")(record.$ref))
|
|
197
197
|
return;
|
|
198
198
|
const url = stripHash(record.$ref);
|
|
199
199
|
const pointer = pointers.get(url);
|
|
@@ -225,8 +225,8 @@ function rewriteMappings(root, placed, ids) {
|
|
|
225
225
|
: undefined;
|
|
226
226
|
if (!mapping)
|
|
227
227
|
return;
|
|
228
|
-
for (const [key, value] of
|
|
229
|
-
if (typeof value !== "string" ||
|
|
228
|
+
for (const [key, value] of Rec.toEntries(mapping)) {
|
|
229
|
+
if (typeof value !== "string" || Str.startsWith("#")(value))
|
|
230
230
|
continue;
|
|
231
231
|
const url = stripHash(value);
|
|
232
232
|
const hash = getHash(value);
|
|
@@ -242,7 +242,7 @@ function rewriteMappings(root, placed, ids) {
|
|
|
242
242
|
function findPointer(root, target, pointer = "#", seen = new Set()) {
|
|
243
243
|
if (root === target)
|
|
244
244
|
return pointer;
|
|
245
|
-
if (
|
|
245
|
+
if (!Predicate.isObjectOrArray(root) || seen.has(root))
|
|
246
246
|
return undefined;
|
|
247
247
|
seen.add(root);
|
|
248
248
|
if (Array.isArray(root)) {
|
|
@@ -253,7 +253,7 @@ function findPointer(root, target, pointer = "#", seen = new Set()) {
|
|
|
253
253
|
}
|
|
254
254
|
return undefined;
|
|
255
255
|
}
|
|
256
|
-
for (const [key, value] of
|
|
256
|
+
for (const [key, value] of Rec.toEntries(root)) {
|
|
257
257
|
const found = findPointer(value, target, joinPointer(pointer, key), seen);
|
|
258
258
|
if (found)
|
|
259
259
|
return found;
|
|
@@ -271,7 +271,7 @@ function walkRecords(node, visit, seen = new Set()) {
|
|
|
271
271
|
}
|
|
272
272
|
const record = node;
|
|
273
273
|
visit(record);
|
|
274
|
-
for (const value of
|
|
274
|
+
for (const value of Rec.values(record))
|
|
275
275
|
walkRecords(value, visit, seen);
|
|
276
276
|
}
|
|
277
277
|
const inlineExternalValues = Effect.fnUntraced(function* (root, cache, keep, kept, options, failures, recorded) {
|
|
@@ -304,10 +304,10 @@ function valueAt(document, hash) {
|
|
|
304
304
|
const tokens = parsePointer(hash);
|
|
305
305
|
let current = document;
|
|
306
306
|
for (const token of tokens) {
|
|
307
|
-
|
|
307
|
+
const child = childAt(current, token);
|
|
308
|
+
if (Option.isNone(child))
|
|
308
309
|
throw missingPointerFailure(token, hash);
|
|
309
|
-
|
|
310
|
-
current = current[token];
|
|
310
|
+
current = child.value;
|
|
311
311
|
}
|
|
312
312
|
return current;
|
|
313
313
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Schema } from "effect";
|
|
1
|
+
import { Schema, String as Str } from "effect";
|
|
2
2
|
import { openApiMap } from "./object.js";
|
|
3
3
|
import { OpenApiPathItem } from "./path-item.js";
|
|
4
|
-
export const OpenApiCallback = openApiMap(Schema.String.check(Schema.makeFilter((key) => !
|
|
4
|
+
export const OpenApiCallback = openApiMap(Schema.String.check(Schema.makeFilter((key) => !Str.startsWith("x-")(key))), Schema.suspend(() => OpenApiPathItem));
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { Schema } from "effect";
|
|
1
|
+
import { Record as Rec, Schema } from "effect";
|
|
2
2
|
import { openApiObject } from "./object.js";
|
|
3
3
|
export const OpenApiExample = openApiObject({
|
|
4
4
|
summary: Schema.optionalKey(Schema.String),
|
|
5
5
|
description: Schema.optionalKey(Schema.String),
|
|
6
6
|
value: Schema.optionalKey(Schema.Unknown),
|
|
7
7
|
externalValue: Schema.optionalKey(Schema.String),
|
|
8
|
-
}).check(Schema.makeFilter((value) =>
|
|
8
|
+
}).check(Schema.makeFilter((value) => Rec.has(value, "value") && value.externalValue !== undefined
|
|
9
9
|
? "Example value and externalValue are mutually exclusive"
|
|
10
10
|
: undefined));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Schema } from "effect";
|
|
1
|
+
import { Record as Rec, Schema } from "effect";
|
|
2
2
|
import { OpenApiExample } from "./example.js";
|
|
3
3
|
import { OpenApiMediaType } from "./media-type.js";
|
|
4
4
|
import { openApiObject } from "./object.js";
|
|
@@ -19,10 +19,10 @@ export const OpenApiHeader = openApiObject(fields).check(Schema.makeFilter((valu
|
|
|
19
19
|
if ((value.schema === undefined) === (value.content === undefined)) {
|
|
20
20
|
return "A header must have exactly one of schema or content";
|
|
21
21
|
}
|
|
22
|
-
if (value.content !== undefined &&
|
|
22
|
+
if (value.content !== undefined && Rec.size(value.content) !== 1) {
|
|
23
23
|
return "Header content must contain exactly one media type";
|
|
24
24
|
}
|
|
25
|
-
if (
|
|
25
|
+
if (Rec.has(value, "example") && value.examples !== undefined) {
|
|
26
26
|
return "Header example and examples are mutually exclusive";
|
|
27
27
|
}
|
|
28
28
|
return undefined;
|