@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.
@@ -1,4 +1,4 @@
1
- import { Schema } from "effect";
1
+ import { Record as Rec, Schema } from "effect";
2
2
  import { OpenApiEncoding } from "./encoding.js";
3
3
  import { OpenApiExample } from "./example.js";
4
4
  import { openApiObject } from "./object.js";
@@ -9,6 +9,6 @@ export const OpenApiMediaType = openApiObject({
9
9
  example: Schema.optionalKey(Schema.Unknown),
10
10
  examples: Schema.optionalKey(Schema.Record(Schema.String, withReference(OpenApiExample))),
11
11
  encoding: Schema.optionalKey(Schema.Record(Schema.String, OpenApiEncoding)),
12
- }).check(Schema.makeFilter((value) => Object.hasOwn(value, "example") && value.examples !== undefined
12
+ }).check(Schema.makeFilter((value) => Rec.has(value, "example") && value.examples !== undefined
13
13
  ? "Media type example and examples are mutually exclusive"
14
14
  : undefined));
@@ -1,4 +1,4 @@
1
- import { Schema } from "effect";
1
+ import { Array as Arr, Formatter, Option, pipe, Record as Rec, Schema, String as Str, } from "effect";
2
2
  /** Preserve extensions and unrecognized fields while validating every declared field. */
3
3
  export function openApiObject(fields) {
4
4
  return Schema.StructWithRest(Schema.Struct(fields), [
@@ -10,8 +10,5 @@ export function openApiMap(key, value) {
10
10
  const isKey = Schema.is(key);
11
11
  return Schema.StructWithRest(Schema.Record(key, value), [
12
12
  Schema.Record(Schema.String, Schema.Unknown),
13
- ]).check(Schema.makeFilter((entries) => {
14
- const invalid = Object.keys(entries).find((name) => !name.startsWith("x-") && !isKey(name));
15
- return invalid === undefined ? undefined : `Unexpected key ${JSON.stringify(invalid)}`;
16
- }));
13
+ ]).check(Schema.makeFilter((entries) => pipe(Rec.keys(entries), Arr.findFirst((name) => !Str.startsWith("x-")(name) && !isKey(name)), Option.map((invalid) => `Unexpected key ${Formatter.formatJson(invalid)}`), Option.getOrUndefined)));
17
14
  }
@@ -1,4 +1,4 @@
1
- import { Schema } from "effect";
1
+ import { Record as Rec, Schema } from "effect";
2
2
  import { OpenApiHeader } from "./header.js";
3
3
  import { openApiObject } from "./object.js";
4
4
  export const OpenApiParameter = openApiObject({
@@ -23,10 +23,10 @@ export const OpenApiParameter = openApiObject({
23
23
  if ((value.schema === undefined) === (value.content === undefined)) {
24
24
  return "A parameter must have exactly one of schema or content";
25
25
  }
26
- if (value.content !== undefined && Object.keys(value.content).length !== 1) {
26
+ if (value.content !== undefined && Rec.size(value.content) !== 1) {
27
27
  return "Parameter content must contain exactly one media type";
28
28
  }
29
- if (Object.hasOwn(value, "example") && value.examples !== undefined) {
29
+ if (Rec.has(value, "example") && value.examples !== undefined) {
30
30
  return "Parameter example and examples are mutually exclusive";
31
31
  }
32
32
  return undefined;
@@ -1,7 +1,7 @@
1
- import { Schema } from "effect";
1
+ import { Array as Arr, Record as Rec, Schema, String as Str } from "effect";
2
2
  import { openApiMap } from "./object.js";
3
3
  import { withReference } from "./reference.js";
4
4
  import { OpenApiResponse } from "./response.js";
5
- export const OpenApiResponses = openApiMap(Schema.String.check(Schema.isPattern(/^(?:default|[1-5](?:\d{2}|XX))$/)), withReference(OpenApiResponse)).check(Schema.makeFilter((value) => Object.keys(value).some((key) => !key.startsWith("x-"))
5
+ export const OpenApiResponses = openApiMap(Schema.String.check(Schema.isPattern(/^(?:default|[1-5](?:\d{2}|XX))$/)), withReference(OpenApiResponse)).check(Schema.makeFilter((value) => Arr.some(Rec.keys(value), (key) => !Str.startsWith("x-")(key))
6
6
  ? undefined
7
7
  : "Responses must contain at least one response"));
@@ -1,4 +1,4 @@
1
- import { JsonSchema, Predicate } from "effect";
1
+ import { Array as Arr, JsonSchema, Predicate, Record as Rec, String as Str } from "effect";
2
2
  import { OpenApiHttpMethod } from "../schemas/3.1/http-method.js";
3
3
  function asRecord(value) {
4
4
  return Predicate.isObject(value) ? value : undefined;
@@ -22,11 +22,11 @@ const ARRAY_KEYS = ["allOf", "anyOf", "oneOf", "prefixItems"];
22
22
  const MAP_KEYS = ["properties", "patternProperties", "$defs", "definitions", "dependentSchemas"];
23
23
  export function isOpenApi3(document) {
24
24
  const record = asRecord(document);
25
- return typeof record?.openapi === "string" && record.openapi.startsWith("3.");
25
+ return typeof record?.openapi === "string" && Str.startsWith("3.")(record.openapi);
26
26
  }
27
27
  export function isOpenApi3_0(document) {
28
28
  const record = asRecord(document);
29
- return typeof record?.openapi === "string" && record.openapi.startsWith("3.0");
29
+ return typeof record?.openapi === "string" && Str.startsWith("3.0")(record.openapi);
30
30
  }
31
31
  export function upgradeOpenApi3_0(document) {
32
32
  if (!isOpenApi3_0(document))
@@ -48,18 +48,18 @@ export function schemaSlots(document, includeComponentSchemas = true) {
48
48
  collectHeaderMap(components.headers, slots);
49
49
  const pathItems = asRecord(components.pathItems);
50
50
  if (pathItems) {
51
- for (const value of Object.values(pathItems))
51
+ for (const value of Rec.values(pathItems))
52
52
  collectPathItem(value, slots);
53
53
  }
54
54
  }
55
55
  const paths = asRecord(document.paths);
56
56
  if (paths) {
57
- for (const value of Object.values(paths))
57
+ for (const value of Rec.values(paths))
58
58
  collectPathItem(value, slots);
59
59
  }
60
60
  const webhooks = asRecord(document.webhooks);
61
61
  if (webhooks) {
62
- for (const value of Object.values(webhooks))
62
+ for (const value of Rec.values(webhooks))
63
63
  collectPathItem(value, slots);
64
64
  }
65
65
  return slots;
@@ -73,35 +73,35 @@ function collectSchemaMap(value, slots) {
73
73
  const map = asRecord(value);
74
74
  if (!map)
75
75
  return;
76
- for (const key of Object.keys(map))
76
+ for (const key of Rec.keys(map))
77
77
  slots.push({ parent: map, key });
78
78
  }
79
79
  function collectParameterMap(value, slots) {
80
80
  const map = asRecord(value);
81
81
  if (!map)
82
82
  return;
83
- for (const parameter of Object.values(map))
83
+ for (const parameter of Rec.values(map))
84
84
  collectParameter(parameter, slots);
85
85
  }
86
86
  function collectResponseMap(value, slots) {
87
87
  const map = asRecord(value);
88
88
  if (!map)
89
89
  return;
90
- for (const response of Object.values(map))
90
+ for (const response of Rec.values(map))
91
91
  collectResponse(response, slots);
92
92
  }
93
93
  function collectRequestBodyMap(value, slots) {
94
94
  const map = asRecord(value);
95
95
  if (!map)
96
96
  return;
97
- for (const body of Object.values(map))
97
+ for (const body of Rec.values(map))
98
98
  collectRequestBody(body, slots);
99
99
  }
100
100
  function collectHeaderMap(value, slots) {
101
101
  const map = asRecord(value);
102
102
  if (!map)
103
103
  return;
104
- for (const header of Object.values(map))
104
+ for (const header of Rec.values(map))
105
105
  collectHeader(header, slots);
106
106
  }
107
107
  function collectPathItem(value, slots) {
@@ -109,7 +109,7 @@ function collectPathItem(value, slots) {
109
109
  if (!record || typeof record.$ref === "string")
110
110
  return;
111
111
  collectParameterList(record.parameters, slots);
112
- for (const [key, child] of Object.entries(record)) {
112
+ for (const [key, child] of Rec.toEntries(record)) {
113
113
  if (METHODS.has(key))
114
114
  collectOperation(child, slots);
115
115
  }
@@ -122,22 +122,22 @@ function collectOperation(value, slots) {
122
122
  collectRequestBody(record.requestBody, slots);
123
123
  const responses = asRecord(record.responses);
124
124
  if (responses) {
125
- for (const response of Object.values(responses))
125
+ for (const response of Rec.values(responses))
126
126
  collectResponse(response, slots);
127
127
  }
128
128
  const callbacks = asRecord(record.callbacks);
129
129
  if (callbacks) {
130
- for (const callback of Object.values(callbacks)) {
130
+ for (const callback of Rec.values(callbacks)) {
131
131
  const paths = asRecord(callback);
132
132
  if (!paths || typeof paths.$ref === "string")
133
133
  continue;
134
- for (const pathItem of Object.values(paths))
134
+ for (const pathItem of Rec.values(paths))
135
135
  collectPathItem(pathItem, slots);
136
136
  }
137
137
  }
138
138
  }
139
139
  function collectParameterList(value, slots) {
140
- if (!Array.isArray(value))
140
+ if (!Arr.isArray(value))
141
141
  return;
142
142
  for (const parameter of value)
143
143
  collectParameter(parameter, slots);
@@ -175,7 +175,7 @@ function collectContent(value, slots) {
175
175
  const content = asRecord(value);
176
176
  if (!content)
177
177
  return;
178
- for (const media of Object.values(content)) {
178
+ for (const media of Rec.values(content)) {
179
179
  const mediaRecord = asRecord(media);
180
180
  if (mediaRecord && "schema" in mediaRecord)
181
181
  slots.push({ parent: mediaRecord, key: "schema" });
@@ -188,25 +188,26 @@ function upgradeSchema(value) {
188
188
  for (const key of SINGLE_KEYS) {
189
189
  if (!(key in record))
190
190
  continue;
191
- if (key === "items" && Array.isArray(record.items)) {
192
- record.items = record.items.map((item) => upgradeSchema(item));
191
+ if (key === "items" && Arr.isArray(record.items)) {
192
+ record.items = Arr.map(record.items, (item) => upgradeSchema(item));
193
193
  }
194
194
  else {
195
195
  record[key] = upgradeSchema(record[key]);
196
196
  }
197
197
  }
198
198
  for (const key of ARRAY_KEYS) {
199
- if (Array.isArray(record[key]))
200
- record[key] = record[key].map((item) => upgradeSchema(item));
199
+ const items = record[key];
200
+ if (Arr.isArray(items))
201
+ record[key] = Arr.map(items, (item) => upgradeSchema(item));
201
202
  }
202
203
  for (const key of MAP_KEYS) {
203
204
  const map = asRecord(record[key]);
204
205
  if (!map)
205
206
  continue;
206
- for (const child of Object.keys(map))
207
+ for (const child of Rec.keys(map))
207
208
  map[child] = upgradeSchema(map[child]);
208
209
  }
209
- if (Array.isArray(record.items)) {
210
+ if (Arr.isArray(record.items)) {
210
211
  record.prefixItems = record.items;
211
212
  if ("additionalItems" in record) {
212
213
  record.items = record.additionalItems;
@@ -216,7 +217,7 @@ function upgradeSchema(value) {
216
217
  delete record.items;
217
218
  }
218
219
  }
219
- if (typeof record.$ref === "string" && Object.keys(record).some((key) => key !== "$ref")) {
220
+ if (typeof record.$ref === "string" && Arr.some(Rec.keys(record), (key) => key !== "$ref")) {
220
221
  const nullable = record.nullable === true;
221
222
  const readOnly = record.readOnly === true;
222
223
  const writeOnly = record.writeOnly === true;
@@ -227,7 +228,9 @@ function upgradeSchema(value) {
227
228
  if (writeOnly)
228
229
  delete siblings.writeOnly;
229
230
  // OpenAPI 3.0 ignores the siblings of a $ref; 3.1 applies them, so they move into allOf
230
- const target = Object.keys(siblings).length > 0 ? { allOf: [{ $ref }, siblings] } : { $ref };
231
+ const target = !Rec.isEmptyRecord(siblings)
232
+ ? { allOf: [{ $ref }, siblings] }
233
+ : { $ref };
231
234
  const lifted = nullable ? orNull(target) : target;
232
235
  if (readOnly)
233
236
  lifted.readOnly = true;
@@ -255,13 +258,13 @@ const ANNOTATION_KEYS = new Set([
255
258
  function orNull(schema) {
256
259
  const outer = {};
257
260
  const inner = {};
258
- for (const [key, value] of Object.entries(schema)) {
259
- if (ANNOTATION_KEYS.has(key) || key.startsWith("x-"))
261
+ for (const [key, value] of Rec.toEntries(schema)) {
262
+ if (ANNOTATION_KEYS.has(key) || Str.startsWith("x-")(key))
260
263
  outer[key] = value;
261
264
  else
262
265
  inner[key] = value;
263
266
  }
264
- return Object.keys(inner).length === 0 ? schema : { ...outer, anyOf: [inner, { type: "null" }] };
267
+ return Rec.isEmptyRecord(inner) ? schema : { ...outer, anyOf: [inner, { type: "null" }] };
265
268
  }
266
269
  function applyOpenApi30Keywords(schema) {
267
270
  const nullable = schema.nullable === true;
@@ -289,8 +292,8 @@ function applyOpenApi30Keywords(schema) {
289
292
  }
290
293
  if ("example" in schema) {
291
294
  const converted = JsonSchema.fromSchemaOpenApi3_0({ example: schema.example }).schema;
292
- const existing = Array.isArray(schema.examples) ? schema.examples : [];
293
- const convertedExamples = Array.isArray(converted.examples) ? converted.examples : [];
295
+ const existing = Arr.isArray(schema.examples) ? schema.examples : [];
296
+ const convertedExamples = Arr.isArray(converted.examples) ? converted.examples : [];
294
297
  schema.examples = [...convertedExamples, ...existing];
295
298
  delete schema.example;
296
299
  }
@@ -302,7 +305,7 @@ function applyOpenApi30Keywords(schema) {
302
305
  if (typeof schema.type === "string" && !composed) {
303
306
  const converted = JsonSchema.fromSchemaOpenApi3_0({ type: schema.type, nullable: true }).schema;
304
307
  schema.type = converted.type;
305
- if (Array.isArray(schema.enum) && !schema.enum.includes(null))
308
+ if (Arr.isArray(schema.enum) && !schema.enum.includes(null))
306
309
  schema.enum = [...schema.enum, null];
307
310
  return schema;
308
311
  }
package/dist/url.d.ts CHANGED
@@ -24,6 +24,6 @@ export declare const resolveUrlSafety: (href: string) => Effect.Effect<{
24
24
  addresses?: undefined;
25
25
  } | {
26
26
  unsafe: boolean;
27
- addresses: import("dns").LookupAddress[];
27
+ addresses: import("node:dns").LookupAddress[];
28
28
  }, ResolverFailure, never>;
29
29
  export declare function isUnsafeHostname(hostname: string): boolean;
package/dist/url.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as NodePath from "@effect/platform-node/NodePath";
2
2
  import { fileURLToPath } from "node:url";
3
- import { Effect, Path } from "effect";
3
+ import { Array as Arr, Effect, Option, Path, pipe, String as Str } from "effect";
4
4
  import { ResolverFailure, nodeErrorCode } from "./errors.js";
5
5
  const unsafeDomainSuffixes = [
6
6
  ".localhost",
@@ -12,14 +12,10 @@ const unsafeDomainSuffixes = [
12
12
  ".lan",
13
13
  ];
14
14
  export function stripHash(value) {
15
- const index = value.indexOf("#");
16
- return index === -1 ? value : value.slice(0, index);
15
+ return pipe(value, Str.indexOf("#"), Option.match({ onNone: () => value, onSome: (index) => Str.slice(0, index)(value) }));
17
16
  }
18
17
  export function getHash(value) {
19
- const index = value.indexOf("#");
20
- if (index === -1)
21
- return "#";
22
- return value.slice(index) || "#";
18
+ return pipe(value, Str.indexOf("#"), Option.match({ onNone: () => "#", onSome: (index) => Str.slice(index)(value) || "#" }));
23
19
  }
24
20
  export function isHttp(value) {
25
21
  return /^https?:\/\//i.test(value);
@@ -27,7 +23,7 @@ export function isHttp(value) {
27
23
  export function isFileSystemPath(value) {
28
24
  if (!value)
29
25
  return false;
30
- if (value.startsWith("file:"))
26
+ if (Str.startsWith("file:")(value))
31
27
  return true;
32
28
  return !/^[a-z][a-z0-9+.-]*:/i.test(value);
33
29
  }
@@ -60,29 +56,31 @@ export function resolveAgainst(from, to) {
60
56
  }
61
57
  export function toFileSystemPath(ref) {
62
58
  const withoutHash = stripHash(ref);
63
- if (withoutHash.startsWith("file:"))
59
+ if (Str.startsWith("file:")(withoutHash))
64
60
  return fileURLToPath(withoutHash);
65
61
  return withoutHash;
66
62
  }
67
63
  export function extensionOf(ref) {
68
- const pathname = stripHash(ref).split("?")[0] ?? "";
69
- const base = pathname.slice(Math.max(pathname.lastIndexOf("/"), pathname.lastIndexOf("\\")) + 1);
70
- const dot = base.lastIndexOf(".");
71
- return dot >= 0 ? base.slice(dot).toLowerCase() : "";
64
+ const pathname = pipe(stripHash(ref), Str.split("?"), Arr.headNonEmpty);
65
+ const base = pipe(pathname, Str.split(/[/\\]/), Arr.lastNonEmpty);
66
+ return pipe(base, Str.lastIndexOf("."), Option.match({
67
+ onNone: () => "",
68
+ onSome: (dot) => pipe(base, Str.slice(dot), Str.toLowerCase),
69
+ }));
72
70
  }
73
71
  export function isUnsafeUrl(value) {
74
- const normalized = value.trim().toLowerCase();
72
+ const normalized = pipe(value, Str.trim, Str.toLowerCase);
75
73
  if (!normalized)
76
74
  return true;
77
- if (normalized.startsWith("javascript:") ||
78
- normalized.startsWith("vbscript:") ||
79
- normalized.startsWith("data:")) {
75
+ if (Str.startsWith("javascript:")(normalized) ||
76
+ Str.startsWith("vbscript:")(normalized) ||
77
+ Str.startsWith("data:")(normalized)) {
80
78
  return true;
81
79
  }
82
- if (normalized.startsWith("file:"))
80
+ if (Str.startsWith("file:")(normalized))
83
81
  return true;
84
82
  try {
85
- const url = new URL(normalized.startsWith("//") ? `http:${normalized}` : normalized);
83
+ const url = new URL(Str.startsWith("//")(normalized) ? `http:${normalized}` : normalized);
86
84
  if (isUnsafeHostname(url.hostname))
87
85
  return true;
88
86
  if (url.port && isInternalPort(Number(url.port)))
@@ -97,14 +95,14 @@ export function isAllowedOrigin(href, allowedOrigins) {
97
95
  const origin = httpOrigin(href);
98
96
  if (!origin || !allowedOrigins?.length)
99
97
  return false;
100
- return allowedOrigins.some((allowed) => httpOrigin(allowed) === origin);
98
+ return Arr.some(allowedOrigins, (allowed) => httpOrigin(allowed) === origin);
101
99
  }
102
100
  export const resolveUrlSafety = Effect.fnUntraced(function* (href) {
103
101
  if (isUnsafeUrl(href))
104
102
  return { unsafe: true };
105
103
  if (typeof process === "undefined" || !process.versions?.node)
106
104
  return { unsafe: false };
107
- const parsed = URL.parse(href.startsWith("//") ? `http:${href}` : href);
105
+ const parsed = URL.parse(Str.startsWith("//")(href) ? `http:${href}` : href);
108
106
  if (parsed === null)
109
107
  return { unsafe: true };
110
108
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
@@ -123,7 +121,7 @@ export const resolveUrlSafety = Effect.fnUntraced(function* (href) {
123
121
  try: () => dns.lookup(normalizeHostname(parsed.hostname), { all: true, verbatim: true }),
124
122
  catch: onError,
125
123
  });
126
- if (addresses.length === 0 || addresses.some(({ address }) => isUnsafeHostname(address))) {
124
+ if (addresses.length === 0 || Arr.some(addresses, ({ address }) => isUnsafeHostname(address))) {
127
125
  return { unsafe: true };
128
126
  }
129
127
  return { unsafe: false, addresses };
@@ -133,7 +131,7 @@ export function isUnsafeHostname(hostname) {
133
131
  if (!normalized)
134
132
  return true;
135
133
  if (normalized === "localhost" ||
136
- unsafeDomainSuffixes.some((suffix) => normalized.endsWith(suffix)))
134
+ Arr.some(unsafeDomainSuffixes, (suffix) => Str.endsWith(suffix)(normalized)))
137
135
  return true;
138
136
  const ipv4 = parseIPv4Address(normalized);
139
137
  if (ipv4)
@@ -148,19 +146,19 @@ function ioCode(cause) {
148
146
  return ioErrorCode ? { ioErrorCode } : undefined;
149
147
  }
150
148
  function normalizeHostname(hostname) {
151
- let normalized = hostname.trim().toLowerCase();
152
- if (normalized.startsWith("[") && normalized.endsWith("]"))
153
- normalized = normalized.slice(1, -1);
154
- while (normalized.endsWith("."))
155
- normalized = normalized.slice(0, -1);
149
+ let normalized = pipe(hostname, Str.trim, Str.toLowerCase);
150
+ if (Str.startsWith("[")(normalized) && Str.endsWith("]")(normalized))
151
+ normalized = Str.slice(1, -1)(normalized);
152
+ while (Str.endsWith(".")(normalized))
153
+ normalized = Str.slice(0, -1)(normalized);
156
154
  return normalized;
157
155
  }
158
156
  function parseIPv4Address(ip) {
159
- const parts = ip.split(".");
157
+ const parts = Str.split(ip, ".");
160
158
  if (parts.length !== 4)
161
159
  return undefined;
162
- const octets = parts.map((part) => (/^\d+$/.test(part) ? Number(part) : Number.NaN));
163
- if (octets.some((octet) => !Number.isInteger(octet) || octet < 0 || octet > 255))
160
+ const octets = Arr.map(parts, (part) => (/^\d+$/.test(part) ? Number(part) : Number.NaN));
161
+ if (Arr.some(octets, (octet) => !Number.isInteger(octet) || octet < 0 || octet > 255))
164
162
  return undefined;
165
163
  return octets;
166
164
  }
@@ -184,23 +182,24 @@ function isUnsafeIPv4Address(octets) {
184
182
  (a === 255 && b === 255 && c === 255 && d === 255));
185
183
  }
186
184
  function parseIPv6Address(ip) {
187
- if (!ip.includes(":"))
185
+ const separator = Str.lastIndexOf(":")(ip);
186
+ if (Option.isNone(separator))
188
187
  return undefined;
189
188
  let normalized = ip;
190
- const lastSeparator = normalized.lastIndexOf(":");
191
- const possibleIPv4 = normalized.slice(lastSeparator + 1);
192
- if (possibleIPv4.includes(".")) {
189
+ const lastSeparator = separator.value;
190
+ const possibleIPv4 = Str.slice(lastSeparator + 1)(normalized);
191
+ if (Str.includes(".")(possibleIPv4)) {
193
192
  const ipv4 = parseIPv4Address(possibleIPv4);
194
193
  if (!ipv4)
195
194
  return undefined;
196
195
  const firstGroup = ipv4[0] * 256 + ipv4[1];
197
196
  const secondGroup = ipv4[2] * 256 + ipv4[3];
198
- normalized = `${normalized.slice(0, lastSeparator + 1)}${firstGroup.toString(16)}:${secondGroup.toString(16)}`;
197
+ normalized = `${Str.slice(0, lastSeparator + 1)(normalized)}${firstGroup.toString(16)}:${secondGroup.toString(16)}`;
199
198
  }
200
- const halves = normalized.split("::");
199
+ const halves = Str.split(normalized, "::");
201
200
  if (halves.length > 2)
202
201
  return undefined;
203
- const head = parseIPv6Groups(halves[0] ?? "");
202
+ const head = parseIPv6Groups(Arr.headNonEmpty(halves));
204
203
  const tail = halves.length === 2 ? parseIPv6Groups(halves[1] ?? "") : [];
205
204
  if (!head || !tail)
206
205
  return undefined;
@@ -214,18 +213,16 @@ function parseIPv6Address(ip) {
214
213
  function parseIPv6Groups(groups) {
215
214
  if (!groups)
216
215
  return [];
217
- const parsed = groups
218
- .split(":")
219
- .map((group) => (/^[\da-f]{1,4}$/i.test(group) ? Number.parseInt(group, 16) : Number.NaN));
220
- if (parsed.some((group) => !Number.isInteger(group) || group < 0 || group > 0xffff))
216
+ const parsed = pipe(groups, Str.split(":"), Arr.map((group) => (/^[\da-f]{1,4}$/i.test(group) ? Number.parseInt(group, 16) : Number.NaN)));
217
+ if (Arr.some(parsed, (group) => !Number.isInteger(group) || group < 0 || group > 0xffff))
221
218
  return undefined;
222
219
  return parsed;
223
220
  }
224
221
  function isUnsafeIPv6Address(groups) {
225
222
  if (groups.length !== 8)
226
223
  return false;
227
- const unspecified = groups.every((group) => group === 0);
228
- const loopback = groups.slice(0, 7).every((group) => group === 0) && groups[7] === 1;
224
+ const unspecified = Arr.every(groups, (group) => group === 0);
225
+ const loopback = Arr.every(groups.slice(0, 7), (group) => group === 0) && groups[7] === 1;
229
226
  const uniqueLocal = (groups[0] & 0xfe00) === 0xfc00;
230
227
  const linkLocal = (groups[0] & 0xffc0) === 0xfe80;
231
228
  const multicast = (groups[0] & 0xff00) === 0xff00;
@@ -235,7 +232,7 @@ function isUnsafeIPv6Address(groups) {
235
232
  return mapped ? isUnsafeIPv4Address(mapped) : false;
236
233
  }
237
234
  function mappedIPv4(groups) {
238
- const firstFive = groups.slice(0, 5).every((group) => group === 0);
235
+ const firstFive = Arr.every(groups.slice(0, 5), (group) => group === 0);
239
236
  const firstSix = firstFive && groups[5] === 0;
240
237
  const v4Mapped = firstFive && groups[5] === 0xffff;
241
238
  if (!firstSix && !v4Mapped)