@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/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 text.replace(/^\uFEFF/, "");
10
+ return Str.replace(/^\uFEFF/, "")(text);
11
11
  }
12
+ const decodeJsonText = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Unknown));
12
13
  function parseJsonText(text) {
13
- if (text.trim().length === 0)
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 = text.trimStart()[0];
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* Effect.try({ try: () => parseJsonText(text), catch: parseFailure }).pipe(Effect.catch(yaml));
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.try({
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 = others.map((slot) => {
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(Object.keys(asRecord(document.$defs) ?? {}));
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 Object.keys(definitions)) {
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 Object.entries(definitions))
64
+ for (const [name, schema] of Rec.toEntries(definitions))
65
65
  park(schema, "definition", name, [], parked);
66
- slotSchemas.forEach((schema, index) => park(schema, "schema", String(index), [], parked));
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
- others.forEach((slot, index) => {
82
+ Arr.forEach(others, (slot, index) => {
83
83
  slot.parent[slot.key] = booleanSlots[index] ?? converted.schemas[index];
84
84
  });
85
85
  rewriteLeftoverDefinitionRefs(document);
@@ -94,26 +94,26 @@ function normalizeSchema(value) {
94
94
  const record = asRecord(value);
95
95
  if (!record)
96
96
  throw bundleFailure("A schema position must contain an object or a boolean");
97
- const normalized = JsonSchema.fromSchemaOpenApi3_1(liftSiblings(record));
97
+ const normalized = JsonSchema.fromSchemaOpenApi3_1(record);
98
98
  // A schema $id is its own resource, so its $defs join the shared pool.
99
99
  // Nested $defs on a schema without $id stay on that schema.
100
100
  if (typeof record.$id === "string")
101
101
  return normalized;
102
102
  const extracted = asRecord(normalized.definitions);
103
- if (!extracted || Object.keys(extracted).length === 0)
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 Object.entries(extra)) {
111
- if (!Object.hasOwn(pool, name)) {
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 (JSON.stringify(pool[name]) === JSON.stringify(schema))
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 (Object.hasOwn(source, name))
135
+ if (Rec.has(source, name))
136
136
  output[name] = source[name];
137
137
  }
138
- for (const name of Object.keys(source)) {
139
- if (!Object.hasOwn(output, name))
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 (Object.hasOwn(pool, `${name}_${suffix}`))
146
+ while (Rec.has(pool, `${name}_${suffix}`))
147
147
  suffix += 1;
148
148
  return `${name}_${suffix}`;
149
149
  }
@@ -153,37 +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 Object.values(record))
156
+ for (const value of Rec.values(record))
157
157
  rewriteDefName(value, from, to);
158
158
  }
159
- function liftSiblings(value) {
160
- if (Array.isArray(value))
161
- return value.map((item) => liftSiblings(item));
162
- const record = asRecord(value);
163
- if (!record)
164
- return value;
165
- const lifted = {};
166
- for (const [key, child] of Object.entries(record))
167
- lifted[key] = liftSiblings(child);
168
- if (typeof lifted.$ref !== "string")
169
- return lifted;
170
- const siblings = { ...lifted };
171
- delete siblings.$ref;
172
- // readOnly/writeOnly stay beside $ref so generators can wrap the reference itself.
173
- const flags = {};
174
- for (const key of ["readOnly", "writeOnly"]) {
175
- if (siblings[key] === true) {
176
- flags[key] = true;
177
- delete siblings[key];
178
- }
179
- }
180
- if (Object.keys(siblings).length === 0)
181
- return { $ref: lifted.$ref, ...flags };
182
- return { allOf: [{ $ref: lifted.$ref }, siblings], ...flags };
183
- }
184
159
  function recursiveNames(definitions) {
185
- const names = Object.keys(definitions);
186
- const dependencies = new Map(names.map((name) => [name, collectDefRefs(definitions[name])]));
160
+ const names = Rec.keys(definitions);
161
+ const dependencies = new Map(Arr.map(names, (name) => [name, collectDefRefs(definitions[name])]));
187
162
  const recursive = new Set();
188
163
  const state = new Map();
189
164
  const stack = [];
@@ -192,7 +167,7 @@ function recursiveNames(definitions) {
192
167
  if (current === 1) {
193
168
  const start = stack.indexOf(name);
194
169
  const cycle = stack.slice(start);
195
- if (cycle.every((item) => isPureRef(definitions[item]))) {
170
+ if (Arr.every(cycle, (item) => isPureRef(definitions[item]))) {
196
171
  throw circularAliasFailure(name);
197
172
  }
198
173
  for (const item of cycle)
@@ -215,27 +190,28 @@ function recursiveNames(definitions) {
215
190
  return recursive;
216
191
  }
217
192
  function collectDefRefs(value, found = new Set(), seen = new Set()) {
218
- if (typeof value !== "object" || value === null || seen.has(value))
193
+ if (!Predicate.isObjectOrArray(value) || seen.has(value))
219
194
  return found;
220
195
  seen.add(value);
221
- if (Array.isArray(value)) {
196
+ if (Arr.isArray(value)) {
222
197
  for (const item of value)
223
198
  collectDefRefs(item, found, seen);
224
199
  return found;
225
200
  }
226
- const ref = value.$ref;
201
+ const record = value;
202
+ const ref = record.$ref;
227
203
  if (typeof ref === "string") {
228
204
  const key = referenceKey(ref);
229
205
  if (key)
230
206
  found.add(key);
231
207
  }
232
- for (const child of Object.values(value))
208
+ for (const child of Rec.values(record))
233
209
  collectDefRefs(child, found, seen);
234
210
  return found;
235
211
  }
236
212
  const rootDefRef = (name) => `#/root-defs/${JsonPointer.escapeToken(name)}`;
237
213
  function resolveValue(value, definitions, rootDefs, recursive, aliases, resolving, mode, hooks, pointer) {
238
- if (Array.isArray(value)) {
214
+ if (Arr.isArray(value)) {
239
215
  for (let index = 0; index < value.length; index++) {
240
216
  value[index] = resolveValue(value[index], definitions, rootDefs, recursive, aliases, resolving, mode, hooks, joinPointer(pointer, String(index)));
241
217
  }
@@ -246,32 +222,44 @@ function resolveValue(value, definitions, rootDefs, recursive, aliases, resolvin
246
222
  return value;
247
223
  if (typeof record.$ref === "string") {
248
224
  const key = referenceKey(record.$ref);
249
- if (!key)
250
- return record;
251
- if (!Object.hasOwn(definitions, key)) {
225
+ if (key && !Rec.has(definitions, key)) {
252
226
  if (rootDefs.has(key)) {
253
227
  record.$ref = rootDefRef(key);
254
- return record;
255
228
  }
256
- throw missingPointerFailure(key, record.$ref, record.$ref);
229
+ else {
230
+ throw missingPointerFailure(key, record.$ref, record.$ref);
231
+ }
257
232
  }
258
- const final = finalName(key, definitions, aliases);
259
- const ref = joinPointer("#/$defs", final);
260
- if (recursive.has(final) || mode === "bundle") {
261
- if (recursive.has(final))
262
- hooks?.onCircular?.(pointer);
263
- record.$ref = ref;
264
- return record;
233
+ else if (key) {
234
+ const final = finalName(key, definitions, aliases);
235
+ const ref = joinPointer("#/$defs", final);
236
+ if (recursive.has(final) || mode === "bundle") {
237
+ if (recursive.has(final))
238
+ hooks?.onCircular?.(pointer);
239
+ record.$ref = ref;
240
+ }
241
+ else {
242
+ if (resolving.has(final))
243
+ throw circularAliasFailure(final);
244
+ resolving.add(final);
245
+ const resolved = resolveValue(structuredClone(definitions[final]), definitions, rootDefs, recursive, aliases, resolving, mode, hooks, ref);
246
+ resolving.delete(final);
247
+ const siblings = { ...record };
248
+ delete siblings.$ref;
249
+ const result = Rec.isEmptyRecord(siblings)
250
+ ? resolved
251
+ : {
252
+ allOf: [
253
+ resolved,
254
+ resolveValue(siblings, definitions, rootDefs, recursive, aliases, resolving, mode, hooks, pointer),
255
+ ],
256
+ };
257
+ hooks?.onDereference?.(pointer, result, record, "$ref");
258
+ return result;
259
+ }
265
260
  }
266
- if (resolving.has(final))
267
- throw circularAliasFailure(final);
268
- resolving.add(final);
269
- const resolved = resolveValue(structuredClone(definitions[final]), definitions, rootDefs, recursive, aliases, resolving, mode, hooks, ref);
270
- resolving.delete(final);
271
- hooks?.onDereference?.(pointer, resolved, record, "$ref");
272
- return resolved;
273
261
  }
274
- for (const [key, child] of Object.entries(record)) {
262
+ for (const [key, child] of Rec.toEntries(record)) {
275
263
  record[key] = resolveValue(child, definitions, rootDefs, recursive, aliases, resolving, mode, hooks, joinPointer(pointer, key));
276
264
  }
277
265
  return record;
@@ -287,7 +275,7 @@ function finalName(name, definitions, aliases) {
287
275
  const schema = definitions[name];
288
276
  if (isPureRef(schema)) {
289
277
  const key = referenceKey(schema.$ref);
290
- if (key && Object.hasOwn(definitions, key)) {
278
+ if (key && Rec.has(definitions, key)) {
291
279
  const resolved = finalName(key, definitions, aliases);
292
280
  aliases.set(name, resolved);
293
281
  return resolved;
@@ -298,10 +286,10 @@ function finalName(name, definitions, aliases) {
298
286
  }
299
287
  function isPureRef(value) {
300
288
  const record = asRecord(value);
301
- return Boolean(record && typeof record.$ref === "string" && Object.keys(record).length === 1);
289
+ return Boolean(record && typeof record.$ref === "string" && Rec.size(record) === 1);
302
290
  }
303
291
  function referenceKey(ref) {
304
- if (!ref.startsWith("#"))
292
+ if (!Str.startsWith("#")(ref))
305
293
  return undefined;
306
294
  const tokens = JsonPointer.parseUriFragment(ref);
307
295
  if (tokens === undefined)
@@ -313,7 +301,7 @@ function park(value, scope, id, pointer, saved) {
313
301
  if (!record)
314
302
  return;
315
303
  for (const key of ANNOTATIONS) {
316
- if (!Object.hasOwn(record, key))
304
+ if (!Rec.has(record, key))
317
305
  continue;
318
306
  saved.push({ scope, id, pointer: [...pointer], key, value: record[key] });
319
307
  delete record[key];
@@ -321,15 +309,16 @@ function park(value, scope, id, pointer, saved) {
321
309
  for (const key of NESTED_SINGLE)
322
310
  park(record[key], scope, id, [...pointer, key], saved);
323
311
  for (const key of NESTED_ARRAY) {
324
- if (!Array.isArray(record[key]))
312
+ const items = record[key];
313
+ if (!Arr.isArray(items))
325
314
  continue;
326
- record[key].forEach((item, index) => park(item, scope, id, [...pointer, key, String(index)], saved));
315
+ Arr.forEach(items, (item, index) => park(item, scope, id, [...pointer, key, String(index)], saved));
327
316
  }
328
317
  for (const key of NESTED_MAP) {
329
318
  const map = asRecord(record[key]);
330
319
  if (!map)
331
320
  continue;
332
- for (const child of Object.keys(map))
321
+ for (const child of Rec.keys(map))
333
322
  park(map[child], scope, id, [...pointer, key, child], saved);
334
323
  }
335
324
  }
@@ -347,7 +336,7 @@ function restore(converted, saved) {
347
336
  function descend(value, pointer) {
348
337
  let current = value;
349
338
  for (const token of pointer) {
350
- if (Array.isArray(current)) {
339
+ if (Arr.isArray(current)) {
351
340
  current = current[Number(token)];
352
341
  continue;
353
342
  }
@@ -360,7 +349,7 @@ function descend(value, pointer) {
360
349
  }
361
350
  function restoreRootDefs(document) {
362
351
  const visit = (value) => {
363
- if (Array.isArray(value)) {
352
+ if (Arr.isArray(value)) {
364
353
  for (const item of value)
365
354
  visit(item);
366
355
  return;
@@ -368,19 +357,19 @@ function restoreRootDefs(document) {
368
357
  const record = asRecord(value);
369
358
  if (!record)
370
359
  return;
371
- if (typeof record.$ref === "string" && record.$ref.startsWith("#/root-defs/")) {
372
- const name = record.$ref.slice("#/root-defs/".length);
360
+ if (typeof record.$ref === "string" && Str.startsWith("#/root-defs/")(record.$ref)) {
361
+ const name = Str.slice("#/root-defs/".length)(record.$ref);
373
362
  record.$ref = joinPointer("#/$defs", JsonPointer.unescapeToken(name));
374
363
  }
375
- for (const child of Object.values(record))
364
+ for (const child of Rec.values(record))
376
365
  visit(child);
377
366
  };
378
367
  visit(document);
379
368
  }
380
369
  function rewriteLeftoverDefinitionRefs(document) {
381
- const names = new Set(Object.keys(asRecord(asRecord(document.components)?.schemas) ?? {}));
370
+ const names = new Set(Rec.keys(asRecord(asRecord(document.components)?.schemas) ?? {}));
382
371
  const visit = (value) => {
383
- if (Array.isArray(value)) {
372
+ if (Arr.isArray(value)) {
384
373
  for (const item of value)
385
374
  visit(item);
386
375
  return;
@@ -388,14 +377,14 @@ function rewriteLeftoverDefinitionRefs(document) {
388
377
  const record = asRecord(value);
389
378
  if (!record)
390
379
  return;
391
- if (typeof record.$ref === "string" && record.$ref.startsWith("#/$defs/")) {
380
+ if (typeof record.$ref === "string" && Str.startsWith("#/$defs/")(record.$ref)) {
392
381
  const tokens = JsonPointer.parseUriFragment(record.$ref);
393
382
  const name = tokens?.[0] === "$defs" ? tokens[1] : undefined;
394
383
  if (name && names.has(name)) {
395
384
  record.$ref = joinPointer("#/components/schemas", [name, ...(tokens?.slice(2) ?? [])]);
396
385
  }
397
386
  }
398
- for (const child of Object.values(record))
387
+ for (const child of Rec.values(record))
399
388
  visit(child);
400
389
  };
401
390
  visit(document);
@@ -403,7 +392,7 @@ function rewriteLeftoverDefinitionRefs(document) {
403
392
  export function inlineRemaining(document, recursive, hooks) {
404
393
  const visiting = new Set();
405
394
  const walk = (value, parent, key, pointer = "#") => {
406
- if (Array.isArray(value)) {
395
+ if (Arr.isArray(value)) {
407
396
  for (let index = 0; index < value.length; index++) {
408
397
  walk(value[index], value, String(index), joinPointer(pointer, String(index)));
409
398
  }
@@ -412,7 +401,7 @@ export function inlineRemaining(document, recursive, hooks) {
412
401
  const record = asRecord(value);
413
402
  if (!record)
414
403
  return;
415
- if (typeof record.$ref === "string" && record.$ref.startsWith("#") && parent && key) {
404
+ if (typeof record.$ref === "string" && Str.startsWith("#")(record.$ref) && parent && key) {
416
405
  const ref = record.$ref;
417
406
  const name = componentSchemaName(ref);
418
407
  if (name && recursive.has(name)) {
@@ -432,7 +421,7 @@ export function inlineRemaining(document, recursive, hooks) {
432
421
  hooks?.onDereference?.(pointer, copy, parent, key);
433
422
  return;
434
423
  }
435
- for (const [childKey, child] of Object.entries(record)) {
424
+ for (const [childKey, child] of Rec.toEntries(record)) {
436
425
  walk(child, record, childKey, joinPointer(pointer, childKey));
437
426
  }
438
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.includes("#") ? pointer.slice(pointer.indexOf("#")) : 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 = hash.startsWith("#") ? hash : `#${hash}`;
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 = fragment.startsWith("#") ? fragment.slice(1) : fragment;
16
+ const body = Str.startsWith("#")(fragment) ? Str.slice(1)(fragment) : fragment;
17
17
  if (body === "")
18
18
  return [];
19
- if (!body.startsWith("/"))
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 = base.includes("#") ? base : `${base}#`;
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
- if (current === null || typeof current !== "object") {
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 || tokens.some((token) => unsafeTokens.has(token)))
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 (next === null || typeof next !== "object" || Array.isArray(next))
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 (typeof value !== "object" || value === null)
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 (Array.isArray(value)) {
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(value, copy);
77
- for (const key of Object.keys(value))
78
- copy[key] = cloneData(value[key], seen);
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
  }