@jamx/http 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License Copyright (c) 2026 Joshua Amaju
2
+
3
+ Permission is hereby granted, free
4
+ of charge, to any person obtaining a copy of this software and associated
5
+ documentation files (the "Software"), to deal in the Software without
6
+ restriction, including without limitation the rights to use, copy, modify, merge,
7
+ publish, distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to the
9
+ following conditions:
10
+
11
+ The above copyright notice and this permission notice
12
+ (including the next paragraph) shall be included in all copies or substantial
13
+ portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16
+ ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
18
+ EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
19
+ OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,91 @@
1
+ # `@jamx/http`
2
+
3
+ Composable HTTP helpers built around `fetch`, interceptors, and `Either`-style
4
+ results.
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ pnpm add @jamx/http
10
+ ```
11
+
12
+ ## Quick Start
13
+
14
+ ```ts
15
+ import {
16
+ composeInterceptors,
17
+ createMemoryCacheStore,
18
+ defaultFetch,
19
+ withAuth,
20
+ withCache,
21
+ withHeaders,
22
+ withRetry,
23
+ withTimeout,
24
+ } from "@jamx/http";
25
+
26
+ const cache = createMemoryCacheStore();
27
+
28
+ const handler = composeInterceptors(
29
+ withTimeout(250),
30
+ withHeaders({ accept: "application/json" }),
31
+ withAuth("demo-token"),
32
+ withJsonBody({ tenant: "team-a" }),
33
+ withCache({ store: cache }),
34
+ withRetry({ retries: 1 }),
35
+ )(defaultFetch);
36
+ ```
37
+
38
+ ## Core APIs
39
+
40
+ ```ts
41
+ import {
42
+ createFetchHandler,
43
+ decodeJson,
44
+ defaultContext,
45
+ defaultFetch,
46
+ expectStatus,
47
+ } from "@jamx/http";
48
+
49
+ const customFetch = createFetchHandler(defaultContext);
50
+
51
+ const response = await defaultFetch("https://api.example.com/users/42");
52
+ const user = await decodeJson(expectStatus(response, 200), decodeUser);
53
+ ```
54
+
55
+ - `defaultContext` is a reusable `Context` backed by `globalThis.fetch`.
56
+ - `defaultFetch` is `createFetchHandler(defaultContext)`.
57
+ - `createFetchHandler(...)` is useful when you want to inject a mocked or custom
58
+ fetch implementation.
59
+ - `composeInterceptors(...)` returns an executable handler with a composed result.
60
+
61
+ ## Decoder Helpers
62
+
63
+ Decoder helpers accept either a plain `Response` or an `Either<..., Response>`.
64
+
65
+ ```ts
66
+ import { decodeJson, json, text, validate } from "@jamx/http";
67
+ import { z } from "zod";
68
+
69
+ const rawResponse = await fetch("https://api.example.com/users/42");
70
+ const userSchema = z.object({ id: z.number(), name: z.string() });
71
+
72
+ const bodyText = await text(rawResponse);
73
+ const bodyJson = await json(rawResponse);
74
+ const user = await decodeJson(rawResponse, decodeUser);
75
+ const userWithSchema = await validate(bodyJson, userSchema);
76
+ ```
77
+
78
+ When you already have an `Either`, upstream errors are preserved in the helper
79
+ result type.
80
+
81
+ ## Notes
82
+
83
+ - `withBaseUrl` rebases request paths onto a configured base URL.
84
+ - Put request-shaping interceptors like `withHeaders` and `withAuth` before
85
+ `withCache` so cache keys can include the final request headers.
86
+ - `withRetry` only retries idempotent methods by default. Pass
87
+ `methods: ["POST"]` if you need to opt a write request into replay.
88
+ - `validate(result, schema)` accepts an `Either` plus a Standard Schema
89
+ compatible validator such as Zod.
90
+ - `withTimeout` aborts the underlying request and returns a `TimeoutError`
91
+ when the timeout elapses. `TimeoutError` extends `FetchError`.
@@ -0,0 +1,7 @@
1
+ export * from "./internal/core.js";
2
+ export * from "./internal/either.js";
3
+ export * from "./internal/decoders.js";
4
+ export * from "./internal/interceptors.js";
5
+ export * from "./internal/statuses/statuses.js";
6
+ export * from "./internal/statuses/validators.js";
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC;AACnC,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,iCAAiC,CAAC;AAChD,cAAc,mCAAmC,CAAC"}
package/build/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export * from "./internal/core.js";
2
+ export * from "./internal/either.js";
3
+ export * from "./internal/decoders.js";
4
+ export * from "./internal/interceptors.js";
5
+ export * from "./internal/statuses/statuses.js";
6
+ export * from "./internal/statuses/validators.js";
@@ -0,0 +1,115 @@
1
+ import { type Either } from "./either.js";
2
+ import type { AnyEither } from "./types.js";
3
+ export interface Context {
4
+ fetch: typeof globalThis.fetch;
5
+ }
6
+ /**
7
+ * Default HTTP context backed by the platform `fetch` implementation.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * import { createFetchHandler, defaultContext } from "@jamx/http";
12
+ *
13
+ * const fetcher = createFetchHandler(defaultContext);
14
+ * ```
15
+ */
16
+ export declare const defaultContext: Context;
17
+ export interface Input {
18
+ input: RequestInfo | URL;
19
+ init?: RequestInit;
20
+ }
21
+ export type Result = Either<FetchError, Response>;
22
+ export type Handler = (request: Request) => Promise<Result>;
23
+ export type Next<R extends AnyEither> = (request?: Request) => Promise<R>;
24
+ export interface InterceptorContext<T extends AnyEither> {
25
+ request: Request;
26
+ next: Next<T>;
27
+ }
28
+ export interface Chain extends InterceptorContext<Result> {
29
+ }
30
+ export type Interceptor<AddedResult extends AnyEither = never, DownstreamResult extends AnyEither = Result> = (ctx: InterceptorContext<DownstreamResult>) => Promise<DownstreamResult | AddedResult>;
31
+ type AnyInterceptor = (args: InterceptorContext<any>) => Promise<AnyEither>;
32
+ type InterceptorResult<TInterceptor extends AnyInterceptor> = Awaited<ReturnType<TInterceptor>>;
33
+ /**
34
+ * Type helper for the final `Either` result produced by a composed interceptor chain.
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * import type { ComposeInterceptorsResult, Result } from "@jamx/http";
39
+ *
40
+ * type FinalResult = ComposeInterceptorsResult<[], Result>;
41
+ * ```
42
+ */
43
+ export type ComposeInterceptorsResult<TInterceptors extends readonly AnyInterceptor[], TResult extends AnyEither = Result> = TInterceptors extends readonly [
44
+ infer THead extends AnyInterceptor,
45
+ ...infer TTail extends readonly AnyInterceptor[]
46
+ ] ? InterceptorResult<THead> | ComposeInterceptorsResult<TTail, TResult> : TResult;
47
+ export interface ExecutableHandler<TResult extends AnyEither> {
48
+ (input: RequestInfo | URL): Promise<TResult>;
49
+ (input: RequestInfo | URL, init?: RequestInit): Promise<TResult>;
50
+ }
51
+ /**
52
+ * Base error returned when a request fails before a response is produced.
53
+ */
54
+ export declare class FetchError extends Error {
55
+ name: string;
56
+ constructor(message?: string, cause?: unknown);
57
+ }
58
+ export declare class TimeoutError extends FetchError {
59
+ name: string;
60
+ readonly timeoutMs: number;
61
+ constructor(timeoutMs: number, cause?: unknown);
62
+ }
63
+ /**
64
+ * Creates a `fetch`-backed handler that returns `Either<FetchError, Response>`.
65
+ *
66
+ * @example
67
+ * ```ts
68
+ * import { createFetchHandler, defaultContext } from "@jamx/http";
69
+ *
70
+ * const fetcher = createFetchHandler(defaultContext);
71
+ * const result = await fetcher(new Request("https://example.com"));
72
+ * ```
73
+ */
74
+ export declare const createFetchHandler: (context: Context) => Handler;
75
+ /**
76
+ * Ready-to-use HTTP handler backed by `globalThis.fetch`.
77
+ *
78
+ * @example
79
+ * ```ts
80
+ * import { defaultFetch } from "@jamx/http";
81
+ *
82
+ * const result = await defaultFetch("https://example.com/users");
83
+ * ```
84
+ */
85
+ export declare const defaultFetch: Handler;
86
+ /**
87
+ * Composes one or more interceptors into an executable HTTP handler.
88
+ *
89
+ * @example
90
+ * ```ts
91
+ * import { composeInterceptors, defaultFetch, withAuth, withTimeout } from "@jamx/http";
92
+ *
93
+ * const handler = composeInterceptors(
94
+ * withTimeout(250),
95
+ * withAuth("demo-token"),
96
+ * )(defaultFetch);
97
+ * ```
98
+ */
99
+ export declare const composeInterceptors: <const TInterceptors extends readonly AnyInterceptor[]>(...interceptors: TInterceptors) => <THandler extends Handler>(handler: THandler) => ExecutableHandler<ComposeInterceptorsResult<TInterceptors>>;
100
+ /**
101
+ * Defines an interceptor while preserving its inferred result type.
102
+ *
103
+ * @example
104
+ * ```ts
105
+ * import { defineInterceptor, ok } from "@jamx/http";
106
+ *
107
+ * const interceptor = defineInterceptor(async ({ next }) => {
108
+ * const result = await next();
109
+ * return result.ok ? ok(result.value) : result;
110
+ * });
111
+ * ```
112
+ */
113
+ export declare const defineInterceptor: <TInterceptor extends (args: Chain) => Promise<AnyEither>>(interceptor: TInterceptor) => TInterceptor;
114
+ export {};
115
+ //# sourceMappingURL=core.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../../src/internal/core.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,119 @@
1
+ import { err, ok } from "./either.js";
2
+ /**
3
+ * Default HTTP context backed by the platform `fetch` implementation.
4
+ *
5
+ * @example
6
+ * ```ts
7
+ * import { createFetchHandler, defaultContext } from "@jamx/http";
8
+ *
9
+ * const fetcher = createFetchHandler(defaultContext);
10
+ * ```
11
+ */
12
+ export const defaultContext = {
13
+ fetch: globalThis.fetch,
14
+ };
15
+ /**
16
+ * Base error returned when a request fails before a response is produced.
17
+ */
18
+ export class FetchError extends Error {
19
+ constructor(message = "Fetch request failed.", cause) {
20
+ super(message, { cause });
21
+ this.name = "FetchError";
22
+ }
23
+ }
24
+ export class TimeoutError extends FetchError {
25
+ constructor(timeoutMs, cause) {
26
+ super(`Fetch request timed out after ${timeoutMs}ms.`, cause);
27
+ this.name = "TimeoutError";
28
+ this.timeoutMs = timeoutMs;
29
+ }
30
+ }
31
+ /**
32
+ * Creates a `fetch`-backed handler that returns `Either<FetchError, Response>`.
33
+ *
34
+ * @example
35
+ * ```ts
36
+ * import { createFetchHandler, defaultContext } from "@jamx/http";
37
+ *
38
+ * const fetcher = createFetchHandler(defaultContext);
39
+ * const result = await fetcher(new Request("https://example.com"));
40
+ * ```
41
+ */
42
+ export const createFetchHandler = (context) => {
43
+ return async (request) => {
44
+ try {
45
+ const response = await context.fetch(request);
46
+ return ok(response);
47
+ }
48
+ catch (error) {
49
+ return err(normalizeError(error));
50
+ }
51
+ };
52
+ };
53
+ /**
54
+ * Ready-to-use HTTP handler backed by `globalThis.fetch`.
55
+ *
56
+ * @example
57
+ * ```ts
58
+ * import { defaultFetch } from "@jamx/http";
59
+ *
60
+ * const result = await defaultFetch("https://example.com/users");
61
+ * ```
62
+ */
63
+ export const defaultFetch = createFetchHandler(defaultContext);
64
+ /**
65
+ * Composes one or more interceptors into an executable HTTP handler.
66
+ *
67
+ * @example
68
+ * ```ts
69
+ * import { composeInterceptors, defaultFetch, withAuth, withTimeout } from "@jamx/http";
70
+ *
71
+ * const handler = composeInterceptors(
72
+ * withTimeout(250),
73
+ * withAuth("demo-token"),
74
+ * )(defaultFetch);
75
+ * ```
76
+ */
77
+ export const composeInterceptors = (...interceptors) => (handler) => {
78
+ const execute = async (input, init) => {
79
+ const request = new Request(input, init);
80
+ const dispatch = async (index, currentRequest) => {
81
+ const interceptor = interceptors[index];
82
+ if (!interceptor) {
83
+ return handler(currentRequest);
84
+ }
85
+ return interceptor({
86
+ request: currentRequest,
87
+ next: (nextRequest = currentRequest) => dispatch(index + 1, nextRequest),
88
+ });
89
+ };
90
+ return dispatch(0, request);
91
+ };
92
+ return execute;
93
+ };
94
+ /**
95
+ * Defines an interceptor while preserving its inferred result type.
96
+ *
97
+ * @example
98
+ * ```ts
99
+ * import { defineInterceptor, ok } from "@jamx/http";
100
+ *
101
+ * const interceptor = defineInterceptor(async ({ next }) => {
102
+ * const result = await next();
103
+ * return result.ok ? ok(result.value) : result;
104
+ * });
105
+ * ```
106
+ */
107
+ export const defineInterceptor = (interceptor) => interceptor;
108
+ function normalizeError(error) {
109
+ if (error instanceof FetchError) {
110
+ return error;
111
+ }
112
+ if (error instanceof Error) {
113
+ return new FetchError(error.message, error);
114
+ }
115
+ if (typeof error === "string") {
116
+ return new FetchError(error, error);
117
+ }
118
+ return new FetchError("Fetch request failed.", error);
119
+ }
@@ -0,0 +1,107 @@
1
+ import type { StandardSchemaV1 } from "@standard-schema/spec";
2
+ import { type Either } from "./either.js";
3
+ export interface DecodeError {
4
+ message: string;
5
+ cause?: unknown;
6
+ }
7
+ export type Decoder<TValue, TError extends DecodeError = DecodeError> = (input: unknown) => Either<TError, TValue>;
8
+ /**
9
+ * Defines a decoder while preserving inferred input, output, and error types.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * import { defineDecoder, err, ok } from "@jamx/http";
14
+ *
15
+ * const decodeUser = defineDecoder((input) => {
16
+ * return typeof input === "object" && input !== null
17
+ * ? ok(input)
18
+ * : err({ message: "invalid-user", cause: input });
19
+ * });
20
+ * ```
21
+ */
22
+ export declare const defineDecoder: <TValue, TDecodeError extends DecodeError = DecodeError>(decoder: Decoder<TValue, TDecodeError>) => Decoder<TValue, TDecodeError>;
23
+ /**
24
+ * Error returned when schema validation fails after decoding succeeds.
25
+ */
26
+ export declare class SchemaError extends Error {
27
+ name: string;
28
+ readonly vendor: string;
29
+ readonly issues: ReadonlyArray<StandardSchemaV1.Issue>;
30
+ constructor(vendor: string, issues: ReadonlyArray<StandardSchemaV1.Issue>, cause?: unknown);
31
+ }
32
+ /**
33
+ * Validates a decoded `Either` value with a Standard Schema compatible validator.
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * import { json, validate } from "@jamx/http";
38
+ * import { z } from "zod";
39
+ *
40
+ * const schema = z.object({ id: z.number(), name: z.string() });
41
+ * const parsed = await json(new Response('{"id":1,"name":"Ada"}'));
42
+ * const result = await validate(parsed, schema);
43
+ * ```
44
+ */
45
+ export declare const validate: <TError, TDecodedValue, TSchema extends StandardSchemaV1<TDecodedValue, any>>(result: Either<TError, TDecodedValue>, schema: TSchema) => Promise<Either<TError | SchemaError, StandardSchemaV1.InferOutput<TSchema>>>;
46
+ /**
47
+ * Error returned when a body helper cannot parse a response payload.
48
+ */
49
+ export declare class ParseError extends Error {
50
+ name: string;
51
+ readonly response: Response;
52
+ readonly operation: "json" | "text" | "empty";
53
+ constructor(response: Response, operation: "json" | "text" | "empty", cause?: unknown);
54
+ }
55
+ /**
56
+ * Parses a response body as JSON.
57
+ *
58
+ * @example
59
+ * ```ts
60
+ * import { json } from "@jamx/http";
61
+ *
62
+ * const result = await json(new Response('{"ok":true}'));
63
+ * ```
64
+ */
65
+ export declare function json(response: Response): Promise<Either<ParseError, unknown>>;
66
+ export declare function json<TError>(result: Either<TError, Response>): Promise<Either<TError | ParseError, unknown>>;
67
+ /**
68
+ * Parses a response body as text.
69
+ *
70
+ * @example
71
+ * ```ts
72
+ * import { text } from "@jamx/http";
73
+ *
74
+ * const result = await text(new Response("hello"));
75
+ * ```
76
+ */
77
+ export declare function text(response: Response): Promise<Either<ParseError, string>>;
78
+ export declare function text<TError>(result: Either<TError, Response>): Promise<Either<TError | ParseError, string>>;
79
+ /**
80
+ * Validates that a response body is empty.
81
+ *
82
+ * @example
83
+ * ```ts
84
+ * import { empty } from "@jamx/http";
85
+ *
86
+ * const result = await empty(new Response(null, { status: 204 }));
87
+ * ```
88
+ */
89
+ export declare function empty(response: Response): Promise<Either<ParseError, void>>;
90
+ export declare function empty<TError>(result: Either<TError, Response>): Promise<Either<TError | ParseError, void>>;
91
+ /**
92
+ * Parses a response body as JSON and then decodes the parsed payload.
93
+ *
94
+ * @example
95
+ * ```ts
96
+ * import { decodeJson, err, ok } from "@jamx/http";
97
+ *
98
+ * const result = await decodeJson(new Response('{"id":1}'), (input) =>
99
+ * typeof input === "object" && input !== null
100
+ * ? ok(input)
101
+ * : err({ message: "invalid-json", cause: input }),
102
+ * );
103
+ * ```
104
+ */
105
+ export declare function decodeJson<TValue, TDecodeError extends DecodeError = DecodeError>(response: Response, decoder: Decoder<TValue, TDecodeError>): Promise<Either<ParseError | TDecodeError, TValue>>;
106
+ export declare function decodeJson<TError, TValue, TDecodeError extends DecodeError = DecodeError>(result: Either<TError, Response>, decoder: Decoder<TValue, TDecodeError>): Promise<Either<TError | ParseError | TDecodeError, TValue>>;
107
+ //# sourceMappingURL=decoders.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decoders.d.ts","sourceRoot":"","sources":["../../src/internal/decoders.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,110 @@
1
+ import { chain, err, isErr, ok } from "./either.js";
2
+ /**
3
+ * Defines a decoder while preserving inferred input, output, and error types.
4
+ *
5
+ * @example
6
+ * ```ts
7
+ * import { defineDecoder, err, ok } from "@jamx/http";
8
+ *
9
+ * const decodeUser = defineDecoder((input) => {
10
+ * return typeof input === "object" && input !== null
11
+ * ? ok(input)
12
+ * : err({ message: "invalid-user", cause: input });
13
+ * });
14
+ * ```
15
+ */
16
+ export const defineDecoder = (decoder) => decoder;
17
+ /**
18
+ * Error returned when schema validation fails after decoding succeeds.
19
+ */
20
+ export class SchemaError extends Error {
21
+ constructor(vendor, issues, cause) {
22
+ super(formatSchemaIssues(issues), { cause });
23
+ this.name = "SchemaError";
24
+ this.vendor = vendor;
25
+ this.issues = issues;
26
+ }
27
+ }
28
+ /**
29
+ * Validates a decoded `Either` value with a Standard Schema compatible validator.
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * import { json, validate } from "@jamx/http";
34
+ * import { z } from "zod";
35
+ *
36
+ * const schema = z.object({ id: z.number(), name: z.string() });
37
+ * const parsed = await json(new Response('{"id":1,"name":"Ada"}'));
38
+ * const result = await validate(parsed, schema);
39
+ * ```
40
+ */
41
+ export const validate = (result, schema) => (async () => {
42
+ if (isErr(result)) {
43
+ return result;
44
+ }
45
+ const validated = await schema["~standard"].validate(result.value);
46
+ if (validated.issues) {
47
+ return err(new SchemaError(schema["~standard"].vendor, validated.issues, validated));
48
+ }
49
+ return ok(validated.value);
50
+ })();
51
+ /**
52
+ * Error returned when a body helper cannot parse a response payload.
53
+ */
54
+ export class ParseError extends Error {
55
+ constructor(response, operation, cause) {
56
+ super(`Failed to parse response body as ${operation}.`, { cause });
57
+ this.name = "ParseError";
58
+ this.response = response;
59
+ this.operation = operation;
60
+ }
61
+ }
62
+ export async function json(input) {
63
+ const result = toResponseEither(input);
64
+ if (isErr(result))
65
+ return result;
66
+ try {
67
+ return ok(await result.value.json());
68
+ }
69
+ catch (cause) {
70
+ return err(new ParseError(result.value, "json", cause));
71
+ }
72
+ }
73
+ export async function text(input) {
74
+ const result = toResponseEither(input);
75
+ if (isErr(result))
76
+ return result;
77
+ try {
78
+ return ok(await result.value.text());
79
+ }
80
+ catch (cause) {
81
+ return err(new ParseError(result.value, "text", cause));
82
+ }
83
+ }
84
+ export async function empty(input) {
85
+ const result = toResponseEither(input);
86
+ if (isErr(result))
87
+ return result;
88
+ try {
89
+ const value = await result.value.text();
90
+ return value.length === 0
91
+ ? ok(undefined)
92
+ : err(new ParseError(result.value, "empty"));
93
+ }
94
+ catch (cause) {
95
+ return err(new ParseError(result.value, "empty", cause));
96
+ }
97
+ }
98
+ export async function decodeJson(input, decoder) {
99
+ const parsed = await json(toResponseEither(input));
100
+ return chain(parsed, decoder);
101
+ }
102
+ function toResponseEither(input) {
103
+ return input instanceof Response ? ok(input) : input;
104
+ }
105
+ function formatSchemaIssues(issues) {
106
+ if (issues.length === 0) {
107
+ return "Schema validation failed.";
108
+ }
109
+ return issues.map((issue) => issue.message).join("; ");
110
+ }
@@ -0,0 +1,25 @@
1
+ import { AnyEither } from "./types.js";
2
+ export type Right<T> = {
3
+ ok: true;
4
+ value: T;
5
+ };
6
+ export type Left<T> = {
7
+ ok: false;
8
+ error: T;
9
+ };
10
+ export type Either<E, A> = Right<A> | Left<E>;
11
+ export declare const ok: <T>(value: T) => Right<T>;
12
+ export declare const err: <T>(error: T) => Left<T>;
13
+ export declare function isOk<T extends AnyEither>(_: T): _ is Extract<T, Right<any>>;
14
+ export declare function isErr<T extends AnyEither>(_: T): _ is Extract<T, Left<any>>;
15
+ type ErrorOf<TResult extends AnyEither> = TResult extends Left<infer TError> ? TError : never;
16
+ type ValueOf<TResult extends AnyEither> = TResult extends Right<infer TValue> ? TValue : never;
17
+ export declare function mapOk<TResult extends AnyEither, TValue>(result: TResult, mapper: (value: ValueOf<TResult>) => TValue): Either<ErrorOf<TResult>, TValue>;
18
+ export declare function mapErr<TResult extends AnyEither, TError>(result: TResult, mapper: (error: ErrorOf<TResult>) => TError): Either<TError, ValueOf<TResult>>;
19
+ export declare function chain<TResult extends AnyEither, TNext extends AnyEither>(result: TResult, mapper: (value: ValueOf<TResult>) => TNext): Either<ErrorOf<TResult> | ErrorOf<TNext>, ValueOf<TNext>>;
20
+ export declare function match<TResult extends AnyEither, TOutput>(result: TResult, branches: {
21
+ ok: (value: ValueOf<TResult>) => TOutput;
22
+ err: (error: ErrorOf<TResult>) => TOutput;
23
+ }): TOutput;
24
+ export {};
25
+ //# sourceMappingURL=either.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"either.d.ts","sourceRoot":"","sources":["../../src/internal/either.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEvC,MAAM,MAAM,KAAK,CAAC,CAAC,IAAI;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAE,CAAC;AAE9C,MAAM,MAAM,IAAI,CAAC,CAAC,IAAI;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAE,CAAC;AAE9C,MAAM,MAAM,MAAM,CAAC,CAAC,EAAE,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;AAE9C,eAAO,MAAM,EAAE,GAAI,CAAC,EAAE,OAAO,CAAC,KAAG,KAAK,CAAC,CAAC,CAA0B,CAAC;AAEnE,eAAO,MAAM,GAAG,GAAI,CAAC,EAAE,OAAO,CAAC,KAAG,IAAI,CAAC,CAAC,CAA2B,CAAC;AAEpE,wBAAgB,IAAI,CAAC,CAAC,SAAS,SAAS,EAAE,CAAC,EAAE,CAAC,GAAG,CAAC,IAAI,OAAO,CAAC,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAE3E;AAED,wBAAgB,KAAK,CAAC,CAAC,SAAS,SAAS,EAAE,CAAC,EAAE,CAAC,GAAG,CAAC,IAAI,OAAO,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAE3E;AAED,KAAK,OAAO,CAAC,OAAO,SAAS,SAAS,IACpC,OAAO,SAAS,IAAI,CAAC,MAAM,MAAM,CAAC,GAAG,MAAM,GAAG,KAAK,CAAC;AAEtD,KAAK,OAAO,CAAC,OAAO,SAAS,SAAS,IACpC,OAAO,SAAS,KAAK,CAAC,MAAM,MAAM,CAAC,GAAG,MAAM,GAAG,KAAK,CAAC;AAEvD,wBAAgB,KAAK,CAAC,OAAO,SAAS,SAAS,EAAE,MAAM,EACrD,MAAM,EAAE,OAAO,EACf,MAAM,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,OAAO,CAAC,KAAK,MAAM,GAC1C,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,CAGlC;AAED,wBAAgB,MAAM,CAAC,OAAO,SAAS,SAAS,EAAE,MAAM,EACtD,MAAM,EAAE,OAAO,EACf,MAAM,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,OAAO,CAAC,KAAK,MAAM,GAC1C,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC,CAGlC;AAED,wBAAgB,KAAK,CAAC,OAAO,SAAS,SAAS,EAAE,KAAK,SAAS,SAAS,EACtE,MAAM,EAAE,OAAO,EACf,MAAM,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,OAAO,CAAC,KAAK,KAAK,GACzC,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC,CAG3D;AAED,wBAAgB,KAAK,CAAC,OAAO,SAAS,SAAS,EAAE,OAAO,EACtD,MAAM,EAAE,OAAO,EACf,QAAQ,EAAE;IACR,EAAE,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC;IACzC,GAAG,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC;CAC3C,GACA,OAAO,CAGT"}
@@ -0,0 +1,28 @@
1
+ export const ok = (value) => ({ ok: true, value });
2
+ export const err = (error) => ({ ok: false, error });
3
+ export function isOk(_) {
4
+ return _.ok;
5
+ }
6
+ export function isErr(_) {
7
+ return !_.ok;
8
+ }
9
+ export function mapOk(result, mapper) {
10
+ if (!result.ok)
11
+ return result;
12
+ return ok(mapper(result.value));
13
+ }
14
+ export function mapErr(result, mapper) {
15
+ if (!result.ok)
16
+ return err(mapper(result.error));
17
+ return result;
18
+ }
19
+ export function chain(result, mapper) {
20
+ if (!result.ok)
21
+ return result;
22
+ return mapper(result.value);
23
+ }
24
+ export function match(result, branches) {
25
+ if (!result.ok)
26
+ return branches.err(result.error);
27
+ return branches.ok(result.value);
28
+ }