@shipworthy/openapi-parser 0.0.1 → 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 CHANGED
@@ -4,6 +4,10 @@ Load an OpenAPI 3.0 or 3.1 document from a file path, a URL, a JSON or YAML stri
4
4
 
5
5
  The API is Effect-native. Every entry point is an `Effect` or an Effect `Schema` codec, so you need [`effect`](https://www.npmjs.com/package/effect) v4 to run it.
6
6
 
7
+ Bundling preserves OpenAPI 3.1 schema keywords alongside `$ref`, including descriptions,
8
+ constraints, and extensions. It rewrites external references to internal references without
9
+ introducing `allOf` wrappers for their sibling keywords.
10
+
7
11
  ## Installation
8
12
 
9
13
  ```sh
@@ -12,12 +16,11 @@ bun add @shipworthy/openapi-parser effect @effect/platform-node
12
16
 
13
17
  ### Requirements
14
18
 
15
- | Requirement | Details |
16
- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
17
- | `effect` | `^4.0.0`, a peer dependency. Install it yourself so your application and the parser share one copy to run the returned effects and to use the schemas. |
18
- | Platform | `@effect/platform-node` `^4.0.0` 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. |
19
- | Module format | ESM only (`"type": "module"`). Ships compiled JavaScript with `.d.ts` declarations; no TypeScript loader or bundler is needed. |
20
- | Peer deps | `effect` `^4.0.0`. |
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. |
21
24
 
22
25
  ## Quick start
23
26
 
@@ -354,7 +357,7 @@ None of these settings can be configured through the public API.
354
357
  - Load files with `URL` or `file://` strings. Bare strings are parsed as JSON or YAML text.
355
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`.
356
359
  - Every error is a `Schema.SchemaError`. Read `error.message`.
357
- - Requires `effect` `^4.0.0` (peer dependency), ESM, and Node.js `>=22.19.0` or Bun. Ships compiled JavaScript. It does not run in browsers.
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.
358
361
  - Next step in the toolchain: pass the result to `decode` from `@shipworthy/openapi-transformer` to get a `ParsedService` for code generators.
359
362
 
360
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.includes("#") ? source.slice(source.indexOf("#")) : 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 ${JSON.stringify(definition)} is a circular alias.`,
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 (Array.isArray(current.node)) {
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
- !record.$ref.startsWith("#") &&
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 = Object.keys(record);
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 (Array.isArray(value)) {
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" && record.$ref.startsWith("#"))
130
+ if (typeof record.$ref === "string" && Str.startsWith("#")(record.$ref))
131
131
  record.$ref = `${fileUrl}${record.$ref}`;
132
- for (const child of Object.values(record))
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 (Array.isArray(value)) {
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
- record.$ref.startsWith("#") &&
150
- Object.keys(record).length === 1) {
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
- Object.keys(target).length === 1 &&
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 Object.values(record))
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 (Object.keys(siblings).length === 0) {
186
+ if (Rec.isEmptyRecord(siblings)) {
187
187
  found.parent[found.key] = value;
188
188
  }
189
189
  else if (oas3DefaultRoot(found.pointer) === "#/components/schemas" ||
190
- found.pointer.includes("/schema")) {
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) && !file.includes("://"))
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 + hash.slice(1), root, buckets);
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 = pathFromRoot.split("/");
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
- rootTokens.every((token, index) => pointer[index] === token));
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
- rootTokens.every((token, index) => pointerTokens_[index] === token));
292
+ Arr.every(rootTokens, (token, index) => pointerTokens_[index] === token));
293
293
  }
294
294
  function pointerTokens(pointer) {
295
- if (!pointer.startsWith("#"))
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 Object.entries(map)) {
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 = Object.keys(record).filter((name) => name !== "$ref" && name !== "summary" && name !== "description");
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 (Array.isArray(current)) {
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" && !record.$ref.startsWith("#")) {
347
- const token = getHash(record.$ref).split("/").at(-1);
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(...Object.values(record));
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 = `${root.slice(1)}/`;
360
- const actualPath = pointer.split(fragment).slice(1);
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 (asRecord(value)) {
385
- for (const key of Object.keys(value))
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
- const pretty = key.replace(/(?:\.|[\\/]+)([a-z])?/g, (_match, letter, index) => {
424
- if (letter === undefined)
425
- return index === 0 ? "" : "_";
426
- return `_${letter.toUpperCase()}`;
427
- });
428
- return pretty;
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 (file.includes("://")) {
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 slash = Math.max(pathname.lastIndexOf("/"), pathname.lastIndexOf("\\"));
441
- const base = slash >= 0 ? pathname.slice(slash + 1) : pathname;
442
- const dot = base.lastIndexOf(".");
443
- return dot > 0 ? base.slice(0, dot) : base;
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 ${redirects.join(" \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 = addresses.filter(({ family }) => requestedFamily === 0 || family === requestedFamily);
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: Object.fromEntries(headers.entries()) };
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: message.startsWith("Error downloading")
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 (source.startsWith("http://") ||
41
- source.startsWith("https://") ||
42
- source.startsWith("file://")) {
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 = source.trim();
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
- value.startsWith("./") ||
74
- value.startsWith("../") ||
75
- value.startsWith(".\\") ||
76
- value.startsWith("..\\"))
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).toLowerCase()) &&
79
- !value.includes(": ") &&
80
- !value.startsWith("{") &&
81
- !value.startsWith("["));
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 = source.trim();
87
- if (!path.isAbsolute(value) && !cwd.startsWith("file:")) {
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) => (url.href.endsWith("/") ? url.href : `${url.href}/`)), Effect.mapError(parseFailure));
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((text) => text.trim()));
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 (Array.isArray(value))
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.filter((plugin) => plugin.can(file));
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(resolvers.map((plugin) => plugin.run(file)));
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.filter((plugin) => plugin.can(parsedFile));
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(selected.map((plugin) => plugin.run(parsedFile)));
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 (filePath.endsWith("/") || filePath.endsWith("\\"))
54
- filePath = filePath.slice(0, -1);
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,