@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/README.md
CHANGED
|
@@ -16,11 +16,11 @@ bun add @shipworthy/openapi-parser effect @effect/platform-node
|
|
|
16
16
|
|
|
17
17
|
### Requirements
|
|
18
18
|
|
|
19
|
-
| Requirement | Details
|
|
20
|
-
| ------------- |
|
|
21
|
-
| `effect` |
|
|
22
|
-
| Platform | `@effect/platform-node`
|
|
23
|
-
| Module format | ESM only (`"type": "module"`). Ships compiled JavaScript with `.d.ts` declarations; no TypeScript loader or bundler is needed.
|
|
19
|
+
| Requirement | Details |
|
|
20
|
+
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
21
|
+
| `effect` | `4.1.4`, a regular dependency. Add a compatible version to your own `package.json` so your application and the parser resolve to one copy to run the returned effects and to use the schemas. |
|
|
22
|
+
| Platform | `@effect/platform-node` `4.1.4` is a regular dependency. Install it yourself only if your code imports it, as the examples here do for `NodeServices`. The parser imports `node:stream`, `node:url`, `node:path`, `node:dns/promises`, `node:net`, and the `Buffer` global, so it needs Node.js `>=22.19.0` or Bun. It does not run in browsers. |
|
|
23
|
+
| Module format | ESM only (`"type": "module"`). Ships compiled JavaScript with `.d.ts` declarations; no TypeScript loader or bundler is needed. |
|
|
24
24
|
|
|
25
25
|
## Quick start
|
|
26
26
|
|
|
@@ -357,7 +357,7 @@ None of these settings can be configured through the public API.
|
|
|
357
357
|
- Load files with `URL` or `file://` strings. Bare strings are parsed as JSON or YAML text.
|
|
358
358
|
- Input can be OpenAPI 3.0 or 3.1. Output is always bundled and upgraded OpenAPI 3.1 (`openapi: "3.1.0"` when upgraded from 3.0), with external refs moved into `components`.
|
|
359
359
|
- Every error is a `Schema.SchemaError`. Read `error.message`.
|
|
360
|
-
- Depends on `effect`
|
|
360
|
+
- Depends on `effect` `4.1.4`. ESM only, and Node.js `>=22.19.0` or Bun. Ships compiled JavaScript. It does not run in browsers.
|
|
361
361
|
- Next step in the toolchain: pass the result to `decode` from `@shipworthy/openapi-transformer` to get a `ParsedService` for code generators.
|
|
362
362
|
|
|
363
363
|
## License
|
package/dist/errors.d.ts
CHANGED
|
@@ -3,7 +3,9 @@ declare const ParserFailure_base: Schema.Class<ParserFailure, Schema.TaggedStruc
|
|
|
3
3
|
readonly message: Schema.String;
|
|
4
4
|
readonly source: Schema.String;
|
|
5
5
|
readonly cause: Schema.optionalKey<Schema.Defect>;
|
|
6
|
-
}>, import("effect/Cause").YieldableError
|
|
6
|
+
}>, import("effect/Cause").YieldableError & {
|
|
7
|
+
readonly brand: unique symbol;
|
|
8
|
+
}>;
|
|
7
9
|
export declare class ParserFailure extends ParserFailure_base {
|
|
8
10
|
}
|
|
9
11
|
declare const ResolverFailure_base: Schema.Class<ResolverFailure, Schema.TaggedStruct<"ResolverFailure", {
|
|
@@ -11,19 +13,25 @@ declare const ResolverFailure_base: Schema.Class<ResolverFailure, Schema.TaggedS
|
|
|
11
13
|
readonly source: Schema.String;
|
|
12
14
|
readonly ioErrorCode: Schema.optionalKey<Schema.String>;
|
|
13
15
|
readonly cause: Schema.optionalKey<Schema.Defect>;
|
|
14
|
-
}>, import("effect/Cause").YieldableError
|
|
16
|
+
}>, import("effect/Cause").YieldableError & {
|
|
17
|
+
readonly brand: unique symbol;
|
|
18
|
+
}>;
|
|
15
19
|
export declare class ResolverFailure extends ResolverFailure_base {
|
|
16
20
|
}
|
|
17
21
|
declare const UnmatchedParserFailure_base: Schema.Class<UnmatchedParserFailure, Schema.TaggedStruct<"UnmatchedParserFailure", {
|
|
18
22
|
message: Schema.String;
|
|
19
23
|
source: Schema.String;
|
|
20
|
-
}>, import("effect/Cause").YieldableError
|
|
24
|
+
}>, import("effect/Cause").YieldableError & {
|
|
25
|
+
readonly brand: unique symbol;
|
|
26
|
+
}>;
|
|
21
27
|
export declare class UnmatchedParserFailure extends UnmatchedParserFailure_base {
|
|
22
28
|
}
|
|
23
29
|
declare const UnmatchedResolverFailure_base: Schema.Class<UnmatchedResolverFailure, Schema.TaggedStruct<"UnmatchedResolverFailure", {
|
|
24
30
|
message: Schema.String;
|
|
25
31
|
source: Schema.String;
|
|
26
|
-
}>, import("effect/Cause").YieldableError
|
|
32
|
+
}>, import("effect/Cause").YieldableError & {
|
|
33
|
+
readonly brand: unique symbol;
|
|
34
|
+
}>;
|
|
27
35
|
export declare class UnmatchedResolverFailure extends UnmatchedResolverFailure_base {
|
|
28
36
|
}
|
|
29
37
|
declare const MissingPointerFailure_base: Schema.Class<MissingPointerFailure, Schema.TaggedStruct<"MissingPointerFailure", {
|
|
@@ -33,31 +41,41 @@ declare const MissingPointerFailure_base: Schema.Class<MissingPointerFailure, Sc
|
|
|
33
41
|
readonly targetRef: Schema.String;
|
|
34
42
|
readonly targetFound: Schema.String;
|
|
35
43
|
readonly parentPath: Schema.String;
|
|
36
|
-
}>, import("effect/Cause").YieldableError
|
|
44
|
+
}>, import("effect/Cause").YieldableError & {
|
|
45
|
+
readonly brand: unique symbol;
|
|
46
|
+
}>;
|
|
37
47
|
export declare class MissingPointerFailure extends MissingPointerFailure_base {
|
|
38
48
|
}
|
|
39
49
|
declare const InvalidPointerFailure_base: Schema.Class<InvalidPointerFailure, Schema.TaggedStruct<"InvalidPointerFailure", {
|
|
40
50
|
readonly message: Schema.String;
|
|
41
51
|
readonly source: Schema.String;
|
|
42
52
|
readonly pointer: Schema.String;
|
|
43
|
-
}>, import("effect/Cause").YieldableError
|
|
53
|
+
}>, import("effect/Cause").YieldableError & {
|
|
54
|
+
readonly brand: unique symbol;
|
|
55
|
+
}>;
|
|
44
56
|
export declare class InvalidPointerFailure extends InvalidPointerFailure_base {
|
|
45
57
|
}
|
|
46
58
|
declare const CircularAliasFailure_base: Schema.Class<CircularAliasFailure, Schema.TaggedStruct<"CircularAliasFailure", {
|
|
47
59
|
readonly message: Schema.String;
|
|
48
60
|
readonly definition: Schema.String;
|
|
49
|
-
}>, import("effect/Cause").YieldableError
|
|
61
|
+
}>, import("effect/Cause").YieldableError & {
|
|
62
|
+
readonly brand: unique symbol;
|
|
63
|
+
}>;
|
|
50
64
|
export declare class CircularAliasFailure extends CircularAliasFailure_base {
|
|
51
65
|
}
|
|
52
66
|
declare const BundleFailure_base: Schema.Class<BundleFailure, Schema.TaggedStruct<"BundleFailure", {
|
|
53
67
|
readonly message: Schema.String;
|
|
54
|
-
}>, import("effect/Cause").YieldableError
|
|
68
|
+
}>, import("effect/Cause").YieldableError & {
|
|
69
|
+
readonly brand: unique symbol;
|
|
70
|
+
}>;
|
|
55
71
|
export declare class BundleFailure extends BundleFailure_base {
|
|
56
72
|
}
|
|
57
73
|
declare const TimeoutFailure_base: Schema.Class<TimeoutFailure, Schema.TaggedStruct<"TimeoutFailure", {
|
|
58
74
|
readonly message: Schema.String;
|
|
59
75
|
readonly timeout: Schema.Finite;
|
|
60
|
-
}>, import("effect/Cause").YieldableError
|
|
76
|
+
}>, import("effect/Cause").YieldableError & {
|
|
77
|
+
readonly brand: unique symbol;
|
|
78
|
+
}>;
|
|
61
79
|
export declare class TimeoutFailure extends TimeoutFailure_base {
|
|
62
80
|
}
|
|
63
81
|
declare const leafFailure: Schema.Union<readonly [typeof ParserFailure, typeof ResolverFailure, typeof UnmatchedParserFailure, typeof UnmatchedResolverFailure, typeof MissingPointerFailure, typeof InvalidPointerFailure, typeof CircularAliasFailure, typeof BundleFailure, typeof TimeoutFailure]>;
|
|
@@ -65,7 +83,9 @@ declare const GroupFailure_base: Schema.Class<GroupFailure, Schema.TaggedStruct<
|
|
|
65
83
|
readonly source: Schema.String;
|
|
66
84
|
readonly message: Schema.String;
|
|
67
85
|
readonly errors: Schema.$Array<Schema.Union<readonly [typeof ParserFailure, typeof ResolverFailure, typeof UnmatchedParserFailure, typeof UnmatchedResolverFailure, typeof MissingPointerFailure, typeof InvalidPointerFailure, typeof CircularAliasFailure, typeof BundleFailure, typeof TimeoutFailure]>>;
|
|
68
|
-
}>, import("effect/Cause").YieldableError
|
|
86
|
+
}>, import("effect/Cause").YieldableError & {
|
|
87
|
+
readonly brand: unique symbol;
|
|
88
|
+
}>;
|
|
69
89
|
export declare class GroupFailure extends GroupFailure_base {
|
|
70
90
|
}
|
|
71
91
|
export type LeafFailure = typeof leafFailure.Type;
|
|
@@ -82,23 +102,31 @@ export declare function groupFailure(source: string, errors: ReadonlyArray<LeafF
|
|
|
82
102
|
export declare function syncFailure(cause: unknown): TaggedFailure;
|
|
83
103
|
declare const EmptySource_base: Schema.Class<EmptySource, Schema.TaggedStruct<"EmptySource", {
|
|
84
104
|
readonly message: Schema.String;
|
|
85
|
-
}>, import("effect/Cause").YieldableError
|
|
105
|
+
}>, import("effect/Cause").YieldableError & {
|
|
106
|
+
readonly brand: unique symbol;
|
|
107
|
+
}>;
|
|
86
108
|
export declare class EmptySource extends EmptySource_base {
|
|
87
109
|
}
|
|
88
110
|
declare const InvalidSource_base: Schema.Class<InvalidSource, Schema.TaggedStruct<"InvalidSource", {
|
|
89
111
|
readonly message: Schema.String;
|
|
90
|
-
}>, import("effect/Cause").YieldableError
|
|
112
|
+
}>, import("effect/Cause").YieldableError & {
|
|
113
|
+
readonly brand: unique symbol;
|
|
114
|
+
}>;
|
|
91
115
|
export declare class InvalidSource extends InvalidSource_base {
|
|
92
116
|
}
|
|
93
117
|
declare const UnsupportedVersion_base: Schema.Class<UnsupportedVersion, Schema.TaggedStruct<"UnsupportedVersion", {
|
|
94
118
|
readonly message: Schema.String;
|
|
95
|
-
}>, import("effect/Cause").YieldableError
|
|
119
|
+
}>, import("effect/Cause").YieldableError & {
|
|
120
|
+
readonly brand: unique symbol;
|
|
121
|
+
}>;
|
|
96
122
|
export declare class UnsupportedVersion extends UnsupportedVersion_base {
|
|
97
123
|
}
|
|
98
124
|
declare const ParseError_base: Schema.Class<ParseError, Schema.TaggedStruct<"ParseError", {
|
|
99
125
|
readonly message: Schema.String;
|
|
100
126
|
readonly cause: Schema.Defect;
|
|
101
|
-
}>, import("effect/Cause").YieldableError
|
|
127
|
+
}>, import("effect/Cause").YieldableError & {
|
|
128
|
+
readonly brand: unique symbol;
|
|
129
|
+
}>;
|
|
102
130
|
export declare class ParseError extends ParseError_base {
|
|
103
131
|
}
|
|
104
132
|
export type LoadError = EmptySource | InvalidSource | UnsupportedVersion | ParseError;
|
package/dist/errors.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Schema } from "effect";
|
|
1
|
+
import { Formatter, Option, pipe, Schema, String as Str } from "effect";
|
|
2
2
|
const failureFields = {
|
|
3
3
|
message: Schema.String,
|
|
4
4
|
source: Schema.String,
|
|
@@ -97,7 +97,7 @@ function errorCode(cause) {
|
|
|
97
97
|
return undefined;
|
|
98
98
|
}
|
|
99
99
|
export function missingPointerFailure(token, source, targetRef = "", targetFound = "", parentPath = "") {
|
|
100
|
-
const hash = source.
|
|
100
|
+
const hash = pipe(source, Str.indexOf("#"), Option.match({ onNone: () => source, onSome: (index) => Str.slice(index)(source) }));
|
|
101
101
|
return new MissingPointerFailure({
|
|
102
102
|
message: `Missing $ref pointer "${hash}". Token "${token}" does not exist.`,
|
|
103
103
|
source,
|
|
@@ -116,7 +116,7 @@ export function invalidPointerFailure(pointer, source = pointer) {
|
|
|
116
116
|
}
|
|
117
117
|
export function circularAliasFailure(definition) {
|
|
118
118
|
return new CircularAliasFailure({
|
|
119
|
-
message: `Definition ${
|
|
119
|
+
message: `Definition ${Formatter.formatJson(definition)} is a circular alias.`,
|
|
120
120
|
definition,
|
|
121
121
|
});
|
|
122
122
|
}
|
package/dist/hoist.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Predicate, Schema } from "effect";
|
|
1
|
+
import { Array as Arr, Option, pipe, Predicate, Record as Rec, Schema, String as Str, } from "effect";
|
|
2
2
|
import { MissingPointerFailure, bundleFailure } from "./errors.js";
|
|
3
3
|
import { cloneData, getAt, joinPointer, parsePointer, setAt } from "./pointer.js";
|
|
4
4
|
import { getHash, isFileSystemPath, isHttp, stripHash } from "./url.js";
|
|
@@ -17,7 +17,7 @@ export function findExternalRef(root, keep, abandoned = new Set()) {
|
|
|
17
17
|
if (seen.has(current.node))
|
|
18
18
|
continue;
|
|
19
19
|
seen.add(current.node);
|
|
20
|
-
if (
|
|
20
|
+
if (Arr.isArray(current.node)) {
|
|
21
21
|
for (let index = current.node.length - 1; index >= 0; index--) {
|
|
22
22
|
stack.push({
|
|
23
23
|
node: current.node[index],
|
|
@@ -32,7 +32,7 @@ export function findExternalRef(root, keep, abandoned = new Set()) {
|
|
|
32
32
|
if (current.parent &&
|
|
33
33
|
current.key &&
|
|
34
34
|
typeof record.$ref === "string" &&
|
|
35
|
-
!
|
|
35
|
+
!Str.startsWith("#")(record.$ref) &&
|
|
36
36
|
!keep(record.$ref) &&
|
|
37
37
|
!abandoned.has(current.pointer)) {
|
|
38
38
|
return {
|
|
@@ -42,7 +42,7 @@ export function findExternalRef(root, keep, abandoned = new Set()) {
|
|
|
42
42
|
pointer: current.pointer,
|
|
43
43
|
};
|
|
44
44
|
}
|
|
45
|
-
const keys =
|
|
45
|
+
const keys = Rec.keys(record);
|
|
46
46
|
for (let index = keys.length - 1; index >= 0; index--) {
|
|
47
47
|
const key = keys[index];
|
|
48
48
|
stack.push({
|
|
@@ -121,15 +121,15 @@ function rebaseInternalRefs(value, fileUrl, seen = new Set()) {
|
|
|
121
121
|
if (typeof value !== "object" || value === null || seen.has(value))
|
|
122
122
|
return;
|
|
123
123
|
seen.add(value);
|
|
124
|
-
if (
|
|
124
|
+
if (Arr.isArray(value)) {
|
|
125
125
|
for (const item of value)
|
|
126
126
|
rebaseInternalRefs(item, fileUrl, seen);
|
|
127
127
|
return;
|
|
128
128
|
}
|
|
129
129
|
const record = value;
|
|
130
|
-
if (typeof record.$ref === "string" &&
|
|
130
|
+
if (typeof record.$ref === "string" && Str.startsWith("#")(record.$ref))
|
|
131
131
|
record.$ref = `${fileUrl}${record.$ref}`;
|
|
132
|
-
for (const child of
|
|
132
|
+
for (const child of Rec.values(record))
|
|
133
133
|
rebaseInternalRefs(child, fileUrl, seen);
|
|
134
134
|
}
|
|
135
135
|
function collapseRefChains(root) {
|
|
@@ -139,20 +139,20 @@ function collapseRefChains(root) {
|
|
|
139
139
|
if (typeof value !== "object" || value === null || seen.has(value))
|
|
140
140
|
return;
|
|
141
141
|
seen.add(value);
|
|
142
|
-
if (
|
|
142
|
+
if (Arr.isArray(value)) {
|
|
143
143
|
for (const item of value)
|
|
144
144
|
visit(item, seen);
|
|
145
145
|
return;
|
|
146
146
|
}
|
|
147
147
|
const record = value;
|
|
148
148
|
if (typeof record.$ref === "string" &&
|
|
149
|
-
|
|
150
|
-
|
|
149
|
+
Str.startsWith("#")(record.$ref) &&
|
|
150
|
+
Rec.size(record) === 1) {
|
|
151
151
|
try {
|
|
152
152
|
const target = asRecord(getAt(root, record.$ref, record.$ref));
|
|
153
153
|
if (target &&
|
|
154
154
|
typeof target.$ref === "string" &&
|
|
155
|
-
|
|
155
|
+
Rec.size(target) === 1 &&
|
|
156
156
|
target.$ref !== record.$ref) {
|
|
157
157
|
record.$ref = target.$ref;
|
|
158
158
|
changed = true;
|
|
@@ -163,7 +163,7 @@ function collapseRefChains(root) {
|
|
|
163
163
|
throw error;
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
|
-
for (const child of
|
|
166
|
+
for (const child of Rec.values(record))
|
|
167
167
|
visit(child, seen);
|
|
168
168
|
};
|
|
169
169
|
visit(root);
|
|
@@ -183,11 +183,11 @@ function inlineRef(found, value, hooks) {
|
|
|
183
183
|
const current = asRecord(found.parent[found.key]);
|
|
184
184
|
const siblings = { ...current };
|
|
185
185
|
delete siblings.$ref;
|
|
186
|
-
if (
|
|
186
|
+
if (Rec.isEmptyRecord(siblings)) {
|
|
187
187
|
found.parent[found.key] = value;
|
|
188
188
|
}
|
|
189
189
|
else if (oas3DefaultRoot(found.pointer) === "#/components/schemas" ||
|
|
190
|
-
|
|
190
|
+
Str.includes("/schema")(found.pointer)) {
|
|
191
191
|
found.parent[found.key] = { allOf: [value, siblings] };
|
|
192
192
|
}
|
|
193
193
|
else if (asRecord(value)) {
|
|
@@ -204,7 +204,7 @@ function generateKey(schema, file, hash, pathFromRoot, buckets, childReservesBas
|
|
|
204
204
|
const root = oas3DefaultRoot(pathFromRoot);
|
|
205
205
|
if (!root)
|
|
206
206
|
return null;
|
|
207
|
-
if (!isFileSystemPath(file) && !isHttp(file) && !
|
|
207
|
+
if (!isFileSystemPath(file) && !isHttp(file) && !Str.includes("://")(file))
|
|
208
208
|
return null;
|
|
209
209
|
if (hash === "#" && isUnderDirectRoot(pathFromRoot, root))
|
|
210
210
|
return null;
|
|
@@ -221,7 +221,7 @@ function generateKey(schema, file, hash, pathFromRoot, buckets, childReservesBas
|
|
|
221
221
|
const filePointer = keyForFile(schema, file, root, buckets);
|
|
222
222
|
if (filePointer == null)
|
|
223
223
|
return null;
|
|
224
|
-
return keyForPointer(schema, filePointer +
|
|
224
|
+
return keyForPointer(schema, filePointer + Str.slice(1)(hash), root, buckets);
|
|
225
225
|
}
|
|
226
226
|
return keyForFile(schema, file, root, buckets, hash === "#" && childReservesBase);
|
|
227
227
|
}
|
|
@@ -229,7 +229,7 @@ function isOpenApiDocument(schema) {
|
|
|
229
229
|
return typeof schema.openapi === "string";
|
|
230
230
|
}
|
|
231
231
|
export function oas3DefaultRoot(pathFromRoot) {
|
|
232
|
-
const path =
|
|
232
|
+
const path = Str.split(pathFromRoot, "/");
|
|
233
233
|
if (canReferenceParameter(path))
|
|
234
234
|
return "#/components/parameters";
|
|
235
235
|
if (canReferenceResponse(path))
|
|
@@ -283,16 +283,16 @@ function isUnderDirectRoot(pathFromRoot, root) {
|
|
|
283
283
|
const pointer = pointerTokens(pathFromRoot);
|
|
284
284
|
const rootTokens = pointerTokens(root);
|
|
285
285
|
return (pointer.length === rootTokens.length + 1 &&
|
|
286
|
-
|
|
286
|
+
Arr.every(rootTokens, (token, index) => pointer[index] === token));
|
|
287
287
|
}
|
|
288
288
|
function isInRoot(pointer, root) {
|
|
289
289
|
const pointerTokens_ = pointerTokens(pointer);
|
|
290
290
|
const rootTokens = pointerTokens(root);
|
|
291
291
|
return (pointerTokens_.length > rootTokens.length &&
|
|
292
|
-
|
|
292
|
+
Arr.every(rootTokens, (token, index) => pointerTokens_[index] === token));
|
|
293
293
|
}
|
|
294
294
|
function pointerTokens(pointer) {
|
|
295
|
-
if (!
|
|
295
|
+
if (!Str.startsWith("#")(pointer))
|
|
296
296
|
return [];
|
|
297
297
|
try {
|
|
298
298
|
return parsePointer(pointer);
|
|
@@ -305,11 +305,11 @@ function existingComponentPointer(schema, file, hash, root) {
|
|
|
305
305
|
const map = asRecord(valueAtSafe(schema, root));
|
|
306
306
|
if (!map)
|
|
307
307
|
return undefined;
|
|
308
|
-
for (const [key, child] of
|
|
308
|
+
for (const [key, child] of Rec.toEntries(map)) {
|
|
309
309
|
const record = asRecord(child);
|
|
310
310
|
if (!record || typeof record.$ref !== "string")
|
|
311
311
|
continue;
|
|
312
|
-
const extras =
|
|
312
|
+
const extras = Arr.filter(Rec.keys(record), (name) => name !== "$ref" && name !== "summary" && name !== "description");
|
|
313
313
|
if (extras.length > 0)
|
|
314
314
|
continue;
|
|
315
315
|
if (stripHash(record.$ref) !== file || getHash(record.$ref) !== hash)
|
|
@@ -338,17 +338,17 @@ function childReservesName(value, base) {
|
|
|
338
338
|
if (typeof current !== "object" || current === null || seen.has(current))
|
|
339
339
|
continue;
|
|
340
340
|
seen.add(current);
|
|
341
|
-
if (
|
|
341
|
+
if (Arr.isArray(current)) {
|
|
342
342
|
stack.push(...current);
|
|
343
343
|
continue;
|
|
344
344
|
}
|
|
345
345
|
const record = current;
|
|
346
|
-
if (typeof record.$ref === "string" && !
|
|
347
|
-
const token = getHash(record.$ref).split("/").
|
|
346
|
+
if (typeof record.$ref === "string" && !Str.startsWith("#")(record.$ref)) {
|
|
347
|
+
const token = pipe(getHash(record.$ref), Str.split("/"), Arr.lastNonEmpty);
|
|
348
348
|
if (token === base)
|
|
349
349
|
return true;
|
|
350
350
|
}
|
|
351
|
-
stack.push(...
|
|
351
|
+
stack.push(...Rec.values(record));
|
|
352
352
|
}
|
|
353
353
|
return false;
|
|
354
354
|
}
|
|
@@ -356,9 +356,8 @@ function keyForPointer(schema, pointer, root, buckets) {
|
|
|
356
356
|
const names = bucket(schema, root, buckets);
|
|
357
357
|
if (names.generated.has(pointer))
|
|
358
358
|
return names.generated.get(pointer) ?? null;
|
|
359
|
-
const fragment = `${
|
|
360
|
-
const
|
|
361
|
-
const base = prettify(actualPath.join("/"));
|
|
359
|
+
const fragment = `${Str.slice(1)(root)}/`;
|
|
360
|
+
const base = prettify(pipe(pointer, Str.split(fragment), Arr.drop(1), Arr.join("/")));
|
|
362
361
|
if (!base)
|
|
363
362
|
return register(schema, root, pointer, null, buckets);
|
|
364
363
|
return register(schema, root, pointer, suggest(names.taken, base), buckets);
|
|
@@ -380,9 +379,9 @@ function bucket(schema, root, buckets) {
|
|
|
380
379
|
if (existing)
|
|
381
380
|
return existing;
|
|
382
381
|
const taken = new Set();
|
|
383
|
-
const value = valueAtSafe(schema, root);
|
|
384
|
-
if (
|
|
385
|
-
for (const key of
|
|
382
|
+
const value = asRecord(valueAtSafe(schema, root));
|
|
383
|
+
if (value) {
|
|
384
|
+
for (const key of Rec.keys(value))
|
|
386
385
|
taken.add(key);
|
|
387
386
|
}
|
|
388
387
|
const created = { taken, generated: new Map() };
|
|
@@ -419,17 +418,25 @@ function matchingComponent(_schema, _root, _value, _source, _componentSource) {
|
|
|
419
418
|
// schema. DigitalOcean repeats one enum as apps_domain_phase and app_alert_phase.
|
|
420
419
|
return undefined;
|
|
421
420
|
}
|
|
421
|
+
/**
|
|
422
|
+
* Replaces each `.` or run of slashes with `_`, uppercasing a following lowercase
|
|
423
|
+
* letter. A leading separator without a following letter is dropped.
|
|
424
|
+
*/
|
|
422
425
|
function prettify(key) {
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
426
|
+
let pretty = "";
|
|
427
|
+
let last = 0;
|
|
428
|
+
for (const match of Str.matchAll(/(?:\.|[\\/]+)([a-z])?/g)(key)) {
|
|
429
|
+
const index = match.index ?? last;
|
|
430
|
+
const letter = match[1];
|
|
431
|
+
const replacement = letter === undefined ? (index === 0 ? "" : "_") : `_${Str.toUpperCase(letter)}`;
|
|
432
|
+
pretty += `${Str.slice(last, index)(key)}${replacement}`;
|
|
433
|
+
last = index + match[0].length;
|
|
434
|
+
}
|
|
435
|
+
return `${pretty}${Str.slice(last)(key)}`;
|
|
429
436
|
}
|
|
430
437
|
function fileBaseName(file) {
|
|
431
438
|
let pathname = file;
|
|
432
|
-
if (
|
|
439
|
+
if (Str.includes("://")(file)) {
|
|
433
440
|
try {
|
|
434
441
|
pathname = decodeURIComponent(new URL(file).pathname);
|
|
435
442
|
}
|
|
@@ -437,8 +444,6 @@ function fileBaseName(file) {
|
|
|
437
444
|
pathname = file;
|
|
438
445
|
}
|
|
439
446
|
}
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
const dot = base.lastIndexOf(".");
|
|
443
|
-
return dot > 0 ? base.slice(0, dot) : base;
|
|
447
|
+
const base = pipe(pathname, Str.split(/[/\\]/), Arr.lastNonEmpty);
|
|
448
|
+
return pipe(base, Str.lastIndexOf("."), Option.filter((dot) => dot > 0), Option.match({ onNone: () => base, onSome: (dot) => Str.slice(0, dot)(base) }));
|
|
444
449
|
}
|
package/dist/http.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as NodeHttpClient from "@effect/platform-node/NodeHttpClient";
|
|
2
2
|
import { Agent } from "@effect/platform-node/Undici";
|
|
3
|
-
import { Cause, Effect, Schedule } from "effect";
|
|
3
|
+
import { Array as Arr, Cause, Effect, pipe, Record as Rec, Schedule, String as Str, } from "effect";
|
|
4
4
|
import { HttpClient } from "effect/http";
|
|
5
5
|
import { ResolverFailure, nodeErrorCode, timeoutFailure } from "./errors.js";
|
|
6
6
|
import { httpOrigin, isAllowedOrigin, isHttp, isUnsafeUrl, resolveUrlSafety, } from "./url.js";
|
|
@@ -94,7 +94,7 @@ const receive = Effect.fnUntraced(function* (client, href, http, redirects) {
|
|
|
94
94
|
if (response.status >= 300) {
|
|
95
95
|
if (http.redirects !== undefined && redirects.length > http.redirects) {
|
|
96
96
|
return yield* new ResolverFailure({
|
|
97
|
-
message: `Error downloading ${redirects[0]}. \nToo many redirects: \n ${
|
|
97
|
+
message: `Error downloading ${redirects[0]}. \nToo many redirects: \n ${Arr.join(redirects, " \n ")}`,
|
|
98
98
|
source: href,
|
|
99
99
|
ioErrorCode: String(response.status),
|
|
100
100
|
});
|
|
@@ -119,7 +119,7 @@ function pinnedLookup(href, addresses) {
|
|
|
119
119
|
return (hostname, options, callback) => {
|
|
120
120
|
const normalizedHostname = normalizeLookupHostname(hostname);
|
|
121
121
|
const requestedFamily = options.family === "IPv4" ? 4 : options.family === "IPv6" ? 6 : options.family || 0;
|
|
122
|
-
const candidates =
|
|
122
|
+
const candidates = Arr.filter(addresses, ({ family }) => requestedFamily === 0 || family === requestedFamily);
|
|
123
123
|
if (normalizedHostname !== expectedHostname || candidates.length === 0) {
|
|
124
124
|
const error = new Error(`No validated address is available for ${hostname}`);
|
|
125
125
|
error.code = "ENOTFOUND";
|
|
@@ -135,10 +135,7 @@ function pinnedLookup(href, addresses) {
|
|
|
135
135
|
};
|
|
136
136
|
}
|
|
137
137
|
function normalizeLookupHostname(hostname) {
|
|
138
|
-
return hostname
|
|
139
|
-
.replace(/^\[|\]$/g, "")
|
|
140
|
-
.replace(/\.+$/, "")
|
|
141
|
-
.toLowerCase();
|
|
138
|
+
return pipe(hostname, Str.replace(/^\[|\]$/g, ""), Str.replace(/\.+$/, ""), Str.toLowerCase);
|
|
142
139
|
}
|
|
143
140
|
function withoutSensitiveHeaders(http) {
|
|
144
141
|
if (!http.headers)
|
|
@@ -147,13 +144,13 @@ function withoutSensitiveHeaders(http) {
|
|
|
147
144
|
headers.delete("authorization");
|
|
148
145
|
headers.delete("proxy-authorization");
|
|
149
146
|
headers.delete("cookie");
|
|
150
|
-
return { ...http, headers:
|
|
147
|
+
return { ...http, headers: Rec.fromEntries(headers.entries()) };
|
|
151
148
|
}
|
|
152
149
|
function resolverFailure(href, cause) {
|
|
153
150
|
const message = cause instanceof Error ? cause.message : String(cause);
|
|
154
151
|
const ioErrorCode = nodeErrorCode(cause);
|
|
155
152
|
return new ResolverFailure({
|
|
156
|
-
message:
|
|
153
|
+
message: Str.startsWith("Error downloading")(message)
|
|
157
154
|
? message
|
|
158
155
|
: `Error downloading ${href}: ${message}`,
|
|
159
156
|
source: href,
|
package/dist/load.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// oxlint-disable-next-line effecttsgo/node-builtin-import -- Detect the public Node Readable input before adapting it with NodeStream.
|
|
2
2
|
import { Readable } from "node:stream";
|
|
3
3
|
import * as NodeStream from "@effect/platform-node/NodeStream";
|
|
4
|
-
import { Effect, Path, Predicate, Schema, Stream } from "effect";
|
|
4
|
+
import { Array as Arr, Effect, Path, Predicate, Schema, Stream, String as Str, } from "effect";
|
|
5
5
|
import { decodeDocument } from "./document.js";
|
|
6
6
|
import { EmptySource, InvalidSource, ParseError } from "./errors.js";
|
|
7
7
|
import { loadDocument as loadResolvedDocument, makeContext } from "./loader.js";
|
|
@@ -37,9 +37,9 @@ const parseSource = Effect.fnUntraced(function* (source, absoluteRef) {
|
|
|
37
37
|
if (source instanceof Buffer)
|
|
38
38
|
return yield* parseSource(source.toString("utf8"), absoluteRef);
|
|
39
39
|
if (typeof source === "string") {
|
|
40
|
-
if (
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
if (Str.startsWith("http://")(source) ||
|
|
41
|
+
Str.startsWith("https://")(source) ||
|
|
42
|
+
Str.startsWith("file://")(source)) {
|
|
43
43
|
const url = yield* Effect.try({
|
|
44
44
|
try: () => new URL(source),
|
|
45
45
|
catch: (cause) => new InvalidSource({
|
|
@@ -66,25 +66,25 @@ const parseSource = Effect.fnUntraced(function* (source, absoluteRef) {
|
|
|
66
66
|
* matches: one-line YAML needs `key: value` or `{...}`, and JSON starts with `{`.
|
|
67
67
|
*/
|
|
68
68
|
function isFilePath(source, path) {
|
|
69
|
-
const value =
|
|
69
|
+
const value = Str.trim(source);
|
|
70
70
|
if (value === "" || /[\r\n]/.test(value))
|
|
71
71
|
return false;
|
|
72
72
|
if (path.isAbsolute(value) ||
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
73
|
+
Str.startsWith("./")(value) ||
|
|
74
|
+
Str.startsWith("../")(value) ||
|
|
75
|
+
Str.startsWith(".\\")(value) ||
|
|
76
|
+
Str.startsWith("..\\")(value))
|
|
77
77
|
return true;
|
|
78
|
-
return ([".json", ".yaml", ".yml"].includes(path.extname(value)
|
|
79
|
-
!
|
|
80
|
-
!
|
|
81
|
-
!
|
|
78
|
+
return ([".json", ".yaml", ".yml"].includes(Str.toLowerCase(path.extname(value))) &&
|
|
79
|
+
!Str.includes(": ")(value) &&
|
|
80
|
+
!Str.startsWith("{")(value) &&
|
|
81
|
+
!Str.startsWith("[")(value));
|
|
82
82
|
}
|
|
83
83
|
/** Resolve a path string against the `cwd` directory URL. */
|
|
84
84
|
const fileUrl = Effect.fnUntraced(function* (source, cwd) {
|
|
85
85
|
const path = yield* Path.Path;
|
|
86
|
-
const value =
|
|
87
|
-
if (!path.isAbsolute(value) && !
|
|
86
|
+
const value = Str.trim(source);
|
|
87
|
+
if (!path.isAbsolute(value) && !Str.startsWith("file:")(cwd)) {
|
|
88
88
|
return yield* Effect.try({ try: () => new URL(value, cwd), catch: parseFailure });
|
|
89
89
|
}
|
|
90
90
|
const directory = path.isAbsolute(value) ? "" : yield* filePath(new URL(cwd));
|
|
@@ -95,7 +95,7 @@ const resolveCwd = Effect.fnUntraced(function* (path, cwd) {
|
|
|
95
95
|
return cwd.href;
|
|
96
96
|
}
|
|
97
97
|
const directory = typeof cwd === "string" ? path.resolve(cwd) : path.resolve(".");
|
|
98
|
-
return yield* path.toFileUrl(directory).pipe(Effect.map((url) => (
|
|
98
|
+
return yield* path.toFileUrl(directory).pipe(Effect.map((url) => (Str.endsWith("/")(url.href) ? url.href : `${url.href}/`)), Effect.mapError(parseFailure));
|
|
99
99
|
});
|
|
100
100
|
const filePath = Effect.fnUntraced(function* (url) {
|
|
101
101
|
const path = yield* Path.Path;
|
|
@@ -105,7 +105,7 @@ const readReadable = Effect.fnUntraced(function* (source) {
|
|
|
105
105
|
return yield* NodeStream.fromReadable({
|
|
106
106
|
evaluate: () => source,
|
|
107
107
|
onError: parseFailure,
|
|
108
|
-
}).pipe(Stream.decodeText, Stream.mkString, Effect.map(
|
|
108
|
+
}).pipe(Stream.decodeText, Stream.mkString, Effect.map(Str.trim));
|
|
109
109
|
});
|
|
110
110
|
const documentObject = Schema.Record(Schema.String, Schema.Unknown);
|
|
111
111
|
const requireDocument = Effect.fnUntraced(function* (value) {
|
|
@@ -122,7 +122,7 @@ function parseFailure(cause) {
|
|
|
122
122
|
function valueKind(value) {
|
|
123
123
|
if (value === null)
|
|
124
124
|
return "null";
|
|
125
|
-
if (
|
|
125
|
+
if (Arr.isArray(value))
|
|
126
126
|
return "Array";
|
|
127
127
|
return typeof value;
|
|
128
128
|
}
|
package/dist/loader.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Effect, FileSystem } from "effect";
|
|
1
|
+
import { Array as Arr, Effect, FileSystem, String as Str } from "effect";
|
|
2
2
|
import { ParserFailure, ResolverFailure, UnmatchedParserFailure, UnmatchedResolverFailure, nodeErrorCode, } from "./errors.js";
|
|
3
3
|
import { httpResolver } from "./http.js";
|
|
4
4
|
import { parserPlugins, parseText } from "./parsers.js";
|
|
@@ -13,19 +13,19 @@ export function makeContext(rootUrl, options, policy = "reference") {
|
|
|
13
13
|
}
|
|
14
14
|
export const loadDocument = Effect.fnUntraced(function* (url, context) {
|
|
15
15
|
const file = fileInfo(url);
|
|
16
|
-
const resolvers = context.resolvers
|
|
16
|
+
const resolvers = Arr.filter(context.resolvers, (plugin) => plugin.can(file));
|
|
17
17
|
if (resolvers.length === 0) {
|
|
18
18
|
return yield* new UnmatchedResolverFailure({
|
|
19
19
|
message: `Could not find resolver for "${url}"`,
|
|
20
20
|
source: url,
|
|
21
21
|
});
|
|
22
22
|
}
|
|
23
|
-
const bytes = yield* Effect.firstSuccessOf(
|
|
23
|
+
const bytes = yield* Effect.firstSuccessOf(Arr.map(resolvers, (plugin) => plugin.run(file)));
|
|
24
24
|
if (context.policy === "root") {
|
|
25
25
|
return yield* parseText(bytes).pipe(Effect.mapError((cause) => new ParserFailure({ message: cause.message, source: url, cause })));
|
|
26
26
|
}
|
|
27
27
|
const parsedFile = { ...file, data: bytes };
|
|
28
|
-
const parsers = context.parsers
|
|
28
|
+
const parsers = Arr.filter(context.parsers, (plugin) => plugin.can(parsedFile));
|
|
29
29
|
const selected = parsers.length > 0 ? parsers : context.parsers;
|
|
30
30
|
if (selected.length === 0) {
|
|
31
31
|
return yield* new UnmatchedParserFailure({
|
|
@@ -33,7 +33,7 @@ export const loadDocument = Effect.fnUntraced(function* (url, context) {
|
|
|
33
33
|
source: url,
|
|
34
34
|
});
|
|
35
35
|
}
|
|
36
|
-
const parsed = yield* Effect.firstSuccessOf(
|
|
36
|
+
const parsed = yield* Effect.firstSuccessOf(Arr.map(selected, (plugin) => plugin.run(parsedFile)));
|
|
37
37
|
return parsed;
|
|
38
38
|
});
|
|
39
39
|
function resolversFor(rootUrl, options, policy) {
|
|
@@ -50,8 +50,8 @@ function resolversFor(rootUrl, options, policy) {
|
|
|
50
50
|
const readResolvedFile = Effect.fnUntraced(function* (file) {
|
|
51
51
|
const fs = yield* FileSystem.FileSystem;
|
|
52
52
|
let filePath = toFileSystemPath(file.url);
|
|
53
|
-
if (
|
|
54
|
-
filePath =
|
|
53
|
+
if (Str.endsWith("/")(filePath) || Str.endsWith("\\")(filePath))
|
|
54
|
+
filePath = Str.slice(0, -1)(filePath);
|
|
55
55
|
return yield* fs.readFile(filePath).pipe(Effect.mapError((error) => new ResolverFailure({
|
|
56
56
|
message: `Error opening file ${filePath}: ${error.message}`,
|
|
57
57
|
source: filePath,
|