@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/yaml.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Array as Arr, pipe, Predicate, Record as Rec, String as Str } from "effect";
|
|
1
2
|
import * as Yaml from "effect/encoding/Yaml";
|
|
2
3
|
const blockScalar = /^[|>](?:[1-9]?[+-]?|[+-]?[1-9]?)$/;
|
|
3
4
|
/**
|
|
@@ -5,8 +6,8 @@ const blockScalar = /^[|>](?:[1-9]?[+-]?|[+-]?[1-9]?)$/;
|
|
|
5
6
|
* OpenAPI fixtures in this repo use both, so expand those forms before parsing.
|
|
6
7
|
*/
|
|
7
8
|
export function parseYamlDocument(source) {
|
|
8
|
-
const text =
|
|
9
|
-
if (text
|
|
9
|
+
const text = Str.startsWith("\uFEFF")(source) ? Str.slice(1)(source) : source;
|
|
10
|
+
if (isBlank(text))
|
|
10
11
|
return null;
|
|
11
12
|
return applyMergeKeys(Yaml.parse(expandCompactNestedSequences(deepenShallowBlockScalars(relaxIndentTabs(text)))));
|
|
12
13
|
}
|
|
@@ -16,12 +17,12 @@ export function parseYamlDocument(source) {
|
|
|
16
17
|
* drop the explicit indicator so auto-detection keeps the scalar text.
|
|
17
18
|
*/
|
|
18
19
|
function deepenShallowBlockScalars(source) {
|
|
19
|
-
const lines = source.replace(/\r\n?/g, "\n").split("\n");
|
|
20
|
+
const lines = pipe(source, Str.replace(/\r\n?/g, "\n"), Str.split("\n"));
|
|
20
21
|
const output = [];
|
|
21
22
|
for (let index = 0; index < lines.length; index++) {
|
|
22
23
|
const line = lines[index] ?? "";
|
|
23
24
|
const indent = leadingSpaces(line);
|
|
24
|
-
const text =
|
|
25
|
+
const text = pipe(line, Str.slice(indent), stripComment, Str.trimEnd);
|
|
25
26
|
if (!isExplicitBlockScalar(text) || !hasShallowContent(lines, index, indent)) {
|
|
26
27
|
output.push(line);
|
|
27
28
|
continue;
|
|
@@ -30,49 +31,49 @@ function deepenShallowBlockScalars(source) {
|
|
|
30
31
|
index++;
|
|
31
32
|
while (index < lines.length) {
|
|
32
33
|
const next = lines[index] ?? "";
|
|
33
|
-
if (next
|
|
34
|
+
if (!isBlank(next) && leadingSpaces(next) < indent)
|
|
34
35
|
break;
|
|
35
|
-
output.push(next
|
|
36
|
+
output.push(isBlank(next) ? next : ` ${next}`);
|
|
36
37
|
index++;
|
|
37
38
|
}
|
|
38
39
|
index--;
|
|
39
40
|
}
|
|
40
|
-
return
|
|
41
|
+
return Arr.join(output, "\n");
|
|
41
42
|
}
|
|
42
43
|
function isExplicitBlockScalar(text) {
|
|
43
44
|
return /^[|>](?:[+-]?[1-9]|[1-9][+-])$/.test(scalarToken(text));
|
|
44
45
|
}
|
|
45
46
|
function scalarToken(text) {
|
|
46
47
|
const separator = mappingSeparator(text);
|
|
47
|
-
const token = separator === -1 ? text : text
|
|
48
|
-
if (token === "-" ||
|
|
49
|
-
const item = token.slice(1).trimStart
|
|
48
|
+
const token = separator === -1 ? text : afterIndex(text, separator);
|
|
49
|
+
if (token === "-" || Str.startsWith("- ")(token)) {
|
|
50
|
+
const item = pipe(token, Str.slice(1), Str.trimStart);
|
|
50
51
|
const nested = mappingSeparator(item);
|
|
51
|
-
return nested === -1 ? item : item
|
|
52
|
+
return nested === -1 ? item : afterIndex(item, nested);
|
|
52
53
|
}
|
|
53
54
|
return token;
|
|
54
55
|
}
|
|
55
56
|
function hasShallowContent(lines, header, indent) {
|
|
56
57
|
for (let index = header + 1; index < lines.length; index++) {
|
|
57
58
|
const line = lines[index] ?? "";
|
|
58
|
-
if (line
|
|
59
|
+
if (isBlank(line))
|
|
59
60
|
continue;
|
|
60
61
|
return leadingSpaces(line) <= indent;
|
|
61
62
|
}
|
|
62
63
|
return false;
|
|
63
64
|
}
|
|
64
65
|
function dropExplicitIndent(line) {
|
|
65
|
-
return
|
|
66
|
+
return Str.replace(/([|>])(\+|-)?[1-9](\+|-)?(\s*(?:#.*)?)?$/, "$1$2$3$4")(line);
|
|
66
67
|
}
|
|
67
|
-
/**
|
|
68
|
+
/**
|
|
69
|
+
* Effect rejects a tab after leading spaces, including inside block scalars.
|
|
70
|
+
* Replace each tab in a line's leading run of spaces then tabs with two spaces.
|
|
71
|
+
*/
|
|
68
72
|
function relaxIndentTabs(source) {
|
|
69
|
-
return
|
|
73
|
+
return Str.replace(/(?<=^ *\t*)\t/gm, " ")(source);
|
|
70
74
|
}
|
|
71
75
|
export function expandCompactNestedSequences(source) {
|
|
72
|
-
const lines = source
|
|
73
|
-
.replace(/^\uFEFF/, "")
|
|
74
|
-
.replace(/\r\n?/g, "\n")
|
|
75
|
-
.split("\n");
|
|
76
|
+
const lines = pipe(source, Str.replace(/^\uFEFF/, ""), Str.replace(/\r\n?/g, "\n"), Str.split("\n"));
|
|
76
77
|
const output = [];
|
|
77
78
|
let blockIndent = -1;
|
|
78
79
|
for (const line of lines) {
|
|
@@ -80,32 +81,32 @@ export function expandCompactNestedSequences(source) {
|
|
|
80
81
|
while (true) {
|
|
81
82
|
const indent = leadingSpaces(raw);
|
|
82
83
|
if (blockIndent >= 0) {
|
|
83
|
-
if (raw
|
|
84
|
+
if (!isBlank(raw) && indent <= blockIndent)
|
|
84
85
|
blockIndent = -1;
|
|
85
86
|
else
|
|
86
87
|
break;
|
|
87
88
|
}
|
|
88
|
-
const text =
|
|
89
|
+
const text = pipe(raw, Str.slice(indent), stripComment, Str.trimEnd);
|
|
89
90
|
const compact = compactRest(raw, indent);
|
|
90
91
|
if (compact === undefined) {
|
|
91
92
|
if (isBlockScalar(text))
|
|
92
93
|
blockIndent = indent;
|
|
93
94
|
break;
|
|
94
95
|
}
|
|
95
|
-
output.push(`${
|
|
96
|
-
raw = `${
|
|
96
|
+
output.push(`${Str.repeat(indent)(" ")}-`);
|
|
97
|
+
raw = `${Str.repeat(indent + 2)(" ")}${compact}`;
|
|
97
98
|
}
|
|
98
99
|
output.push(raw);
|
|
99
100
|
}
|
|
100
|
-
return
|
|
101
|
+
return Arr.join(output, "\n");
|
|
101
102
|
}
|
|
102
103
|
function compactRest(raw, indent) {
|
|
103
|
-
const body =
|
|
104
|
-
if (!
|
|
104
|
+
const body = Str.slice(indent)(raw);
|
|
105
|
+
if (!Str.startsWith("- ")(body))
|
|
105
106
|
return undefined;
|
|
106
|
-
const rest =
|
|
107
|
-
const text = stripComment(rest)
|
|
108
|
-
if (text === "-" ||
|
|
107
|
+
const rest = Str.slice(2)(body);
|
|
108
|
+
const text = Str.trimEnd(stripComment(rest));
|
|
109
|
+
if (text === "-" || Str.startsWith("- ")(text))
|
|
109
110
|
return rest;
|
|
110
111
|
return undefined;
|
|
111
112
|
}
|
|
@@ -114,60 +115,60 @@ function isBlockScalar(text) {
|
|
|
114
115
|
return true;
|
|
115
116
|
const separator = mappingSeparator(text);
|
|
116
117
|
if (separator !== -1)
|
|
117
|
-
return blockScalar.test(text
|
|
118
|
-
if (text === "-" ||
|
|
119
|
-
const item = text.slice(1).trimStart
|
|
118
|
+
return blockScalar.test(afterIndex(text, separator));
|
|
119
|
+
if (text === "-" || Str.startsWith("- ")(text)) {
|
|
120
|
+
const item = pipe(text, Str.slice(1), Str.trimStart);
|
|
120
121
|
const nested = mappingSeparator(item);
|
|
121
122
|
if (nested === -1)
|
|
122
123
|
return blockScalar.test(item);
|
|
123
|
-
return blockScalar.test(item
|
|
124
|
+
return blockScalar.test(afterIndex(item, nested));
|
|
124
125
|
}
|
|
125
126
|
return false;
|
|
126
127
|
}
|
|
127
128
|
function applyMergeKeys(value, seen = new Set()) {
|
|
128
|
-
if (
|
|
129
|
+
if (!Predicate.isObjectOrArray(value) || seen.has(value))
|
|
129
130
|
return value;
|
|
130
131
|
seen.add(value);
|
|
131
|
-
if (
|
|
132
|
+
if (Arr.isArray(value)) {
|
|
132
133
|
for (let index = 0; index < value.length; index++)
|
|
133
134
|
value[index] = applyMergeKeys(value[index], seen);
|
|
134
135
|
return value;
|
|
135
136
|
}
|
|
136
137
|
const record = value;
|
|
137
|
-
for (const key of
|
|
138
|
+
for (const key of Rec.keys(record))
|
|
138
139
|
record[key] = applyMergeKeys(record[key], seen);
|
|
139
|
-
if (!
|
|
140
|
+
if (!Arr.some(Rec.keys(record), isMergeKey))
|
|
140
141
|
return record;
|
|
141
142
|
const next = {};
|
|
142
|
-
for (const key of
|
|
143
|
+
for (const key of Rec.keys(record)) {
|
|
143
144
|
if (!isMergeKey(key)) {
|
|
144
145
|
next[key] = record[key];
|
|
145
146
|
continue;
|
|
146
147
|
}
|
|
147
148
|
const source = record[key];
|
|
148
|
-
const entries =
|
|
149
|
+
const entries = Arr.isArray(source) ? source : [source];
|
|
149
150
|
for (const entry of entries) {
|
|
150
|
-
if (
|
|
151
|
+
if (!Predicate.isObject(entry))
|
|
151
152
|
continue;
|
|
152
|
-
for (const [mergedKey, mergedValue] of
|
|
153
|
-
if (isMergeKey(mergedKey) ||
|
|
153
|
+
for (const [mergedKey, mergedValue] of Rec.toEntries(entry)) {
|
|
154
|
+
if (isMergeKey(mergedKey) || Rec.has(next, mergedKey))
|
|
154
155
|
continue;
|
|
155
156
|
next[mergedKey] = mergedValue;
|
|
156
157
|
}
|
|
157
158
|
}
|
|
158
159
|
}
|
|
159
|
-
for (const key of
|
|
160
|
+
for (const key of Rec.keys(record))
|
|
160
161
|
delete record[key];
|
|
161
162
|
Object.assign(record, next);
|
|
162
163
|
return record;
|
|
163
164
|
}
|
|
164
165
|
function isMergeKey(key) {
|
|
165
|
-
const text =
|
|
166
|
+
const text = Str.trim(key);
|
|
166
167
|
return text === "<<" || /^!!\S+\s+<<$/.test(text) || /^!<[^>]+>\s+<<$/.test(text);
|
|
167
168
|
}
|
|
168
169
|
function leadingSpaces(raw) {
|
|
169
170
|
let index = 0;
|
|
170
|
-
while (raw
|
|
171
|
+
while (raw[index] === " ")
|
|
171
172
|
index++;
|
|
172
173
|
return index;
|
|
173
174
|
}
|
|
@@ -230,6 +231,9 @@ const findIndicator = (input, indicator) => {
|
|
|
230
231
|
};
|
|
231
232
|
const stripComment = (input) => {
|
|
232
233
|
const index = findIndicator(input, "#");
|
|
233
|
-
return index === -1 ? input : input.slice(0, index).trimEnd
|
|
234
|
+
return index === -1 ? input : pipe(input, Str.slice(0, index), Str.trimEnd);
|
|
234
235
|
};
|
|
235
236
|
const mappingSeparator = (input) => findIndicator(input, ":");
|
|
237
|
+
/** The trimmed text after `index`. */
|
|
238
|
+
const afterIndex = (input, index) => pipe(input, Str.slice(index + 1), Str.trim);
|
|
239
|
+
const isBlank = (input) => Str.isEmpty(Str.trim(input));
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipworthy/openapi-parser",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"files": [
|
|
5
5
|
"dist"
|
|
6
6
|
],
|
|
7
7
|
"dependencies": {
|
|
8
|
-
"@effect/platform-node": "
|
|
9
|
-
"effect": "
|
|
8
|
+
"@effect/platform-node": "4.1.4",
|
|
9
|
+
"@effect/platform-node-shared": "4.1.4",
|
|
10
|
+
"effect": "4.1.4"
|
|
10
11
|
},
|
|
11
12
|
"description": "Parse, bundle, and upgrade OpenAPI documents with Effect schemas.",
|
|
12
13
|
"license": "MIT",
|
|
@@ -32,11 +33,11 @@
|
|
|
32
33
|
"build": "rm -rf dist && tsc -p tsconfig.build.json"
|
|
33
34
|
},
|
|
34
35
|
"devDependencies": {
|
|
35
|
-
"@effect/vitest": "4.
|
|
36
|
+
"@effect/vitest": "4.1.4",
|
|
36
37
|
"@shipworthy/typescript-config": "0.0.1",
|
|
37
|
-
"@types/node": "^
|
|
38
|
+
"@types/node": "^26.6.6",
|
|
38
39
|
"typescript": "^7.0.2",
|
|
39
|
-
"vitest": "5.0
|
|
40
|
+
"vitest": "5.1.0"
|
|
40
41
|
},
|
|
41
42
|
"engines": {
|
|
42
43
|
"node": ">=22.19.0"
|