@monarcode/effetch 0.0.0-stage → 0.1.0-alpha.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/Errors.js ADDED
@@ -0,0 +1,151 @@
1
+ import * as Headers from "effect/http/Headers";
2
+ import * as Schema from "effect/Schema";
3
+
4
+ //#region src/Errors.ts
5
+ /**
6
+ * The request never produced a response: DNS failure, connection refused,
7
+ * CORS rejection, the device is offline, and so on.
8
+ *
9
+ * `cause` is the underlying error from the platform's HTTP client.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * import * as Effect from "effect/Effect";
14
+ * import * as Fetch from "@monarcode/effetch";
15
+ *
16
+ * declare const call: Effect.Effect<string, Fetch.FetchError>;
17
+ *
18
+ * const program = call.pipe(
19
+ * Effect.catchTag("NetworkError", (error) =>
20
+ * Effect.succeed(`Offline? ${error.method} ${error.url}`),
21
+ * ),
22
+ * );
23
+ * ```
24
+ *
25
+ * @since 0.1.0
26
+ */
27
+ var NetworkError = class extends Schema.TaggedError()("NetworkError", {
28
+ method: Schema.String,
29
+ url: Schema.String,
30
+ cause: Schema.Unknown
31
+ }) {
32
+ /** `"<method> <url>: network error"`. */
33
+ get message() {
34
+ return `${this.method} ${this.url}: network error`;
35
+ }
36
+ };
37
+ /**
38
+ * The server answered with a non-2xx status.
39
+ *
40
+ * Only 2xx counts as success, as in `fetch`'s `response.ok`. 3xx is an
41
+ * error too: `fetch` follows redirects itself, so a 3xx only reaches you
42
+ * with `redirect: "manual"` or a conditional request answered with
43
+ * `304 Not Modified`. Catch it by `status` if you expect one.
44
+ *
45
+ * - `url` is the request URL without its query string, so secrets in query
46
+ * parameters stay out of logs.
47
+ * - `headers` are the response headers. They're Effect `Headers`, so
48
+ * sensitive ones (`set-cookie`, `authorization`, …) print as
49
+ * `<redacted>` when the error is logged or serialised.
50
+ * - `text` is always the raw response body (`""` if it couldn't be read).
51
+ * - `body` is the body decoded with the call's (or client's) `error` schema,
52
+ * or `undefined` if it didn't match. Without an `error` schema, `body` is
53
+ * the parsed JSON when `text` is valid JSON, else `undefined`. An empty
54
+ * body is always `undefined`. Reading `body` never fails the call.
55
+ *
56
+ * Use {@link HttpError.With} for the type of an `HttpError` whose `body` was
57
+ * decoded with a known schema.
58
+ *
59
+ * @example
60
+ * ```ts
61
+ * import * as Effect from "effect/Effect";
62
+ * import * as Fetch from "@monarcode/effetch";
63
+ *
64
+ * declare const call: Effect.Effect<
65
+ * string,
66
+ * Fetch.HttpError.With<{ readonly code: string }>
67
+ * >;
68
+ *
69
+ * const program = call.pipe(
70
+ * Effect.catchTag("HttpError", (error) =>
71
+ * error.status === 404
72
+ * ? Effect.succeed("not found")
73
+ * : Effect.fail(error.body?.code ?? `HTTP ${error.status}`),
74
+ * ),
75
+ * );
76
+ * ```
77
+ *
78
+ * @since 0.1.0
79
+ */
80
+ var HttpError = class extends Schema.TaggedError()("HttpError", {
81
+ method: Schema.String,
82
+ url: Schema.String,
83
+ status: Schema.Number,
84
+ headers: Schema.declare(Headers.isHeaders),
85
+ text: Schema.String,
86
+ body: Schema.Unknown
87
+ }) {
88
+ /** `"<method> <url>: HTTP <status>"`. */
89
+ get message() {
90
+ return `${this.method} ${this.url}: HTTP ${this.status}`;
91
+ }
92
+ };
93
+ /**
94
+ * A 2xx response body didn't match the call's `output` schema, wasn't valid
95
+ * JSON, or couldn't be read.
96
+ *
97
+ * `issue` is what went wrong and `message` is a readable summary of it:
98
+ *
99
+ * - the schema issue when the body doesn't match `output`,
100
+ * - the `SyntaxError` when the body isn't valid JSON,
101
+ * - the `HttpClientError` when the body couldn't be read.
102
+ *
103
+ * An empty body (204, HEAD, …) is decoded as JSON `null`, so it only
104
+ * passes schemas that accept nothing, such as `Schema.Void`.
105
+ *
106
+ * `url` is the request URL without its query string.
107
+ *
108
+ * @example
109
+ * ```ts
110
+ * import * as Effect from "effect/Effect";
111
+ * import * as Fetch from "@monarcode/effetch";
112
+ *
113
+ * declare const call: Effect.Effect<string, Fetch.FetchError>;
114
+ *
115
+ * const program = call.pipe(
116
+ * Effect.catchTag("DecodeError", (error) =>
117
+ * Effect.succeed(`Unexpected response from ${error.url}: ${error.message}`),
118
+ * ),
119
+ * );
120
+ * ```
121
+ *
122
+ * @since 0.1.0
123
+ */
124
+ var DecodeError = class extends Schema.TaggedError()("DecodeError", {
125
+ method: Schema.String,
126
+ url: Schema.String,
127
+ status: Schema.Number,
128
+ issue: Schema.Unknown,
129
+ message: Schema.String
130
+ }) {};
131
+ /**
132
+ * Returns `true` if `u` is a {@link NetworkError}, {@link HttpError} or
133
+ * {@link DecodeError}.
134
+ *
135
+ * @example
136
+ * ```ts
137
+ * import * as Fetch from "@monarcode/effetch";
138
+ *
139
+ * const report = (error: unknown) => {
140
+ * if (Fetch.isFetchError(error)) {
141
+ * console.error(error._tag, error.url);
142
+ * }
143
+ * };
144
+ * ```
145
+ *
146
+ * @since 0.1.0
147
+ */
148
+ const isFetchError = (u) => u instanceof NetworkError || u instanceof HttpError || u instanceof DecodeError;
149
+
150
+ //#endregion
151
+ export { DecodeError, HttpError, NetworkError, isFetchError };
@@ -0,0 +1,462 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as Headers from "effect/http/Headers";
3
+ import * as HttpClientResponse from "effect/http/HttpClientResponse";
4
+ import * as HttpMethod from "effect/http/HttpMethod";
5
+ import * as UrlParams from "effect/http/UrlParams";
6
+ import * as Schema from "effect/Schema";
7
+
8
+ import { DecodeError, HttpError, NetworkError } from "./Errors.js";
9
+ import { PathParamNames, PathParamValue, PathParams } from "./Path.js";
10
+ import { Plugin } from "./Plugin.js";
11
+ //#region src/Options.d.ts
12
+ /**
13
+ * A single query value. effetch drops `null` and `undefined` (Effect's own
14
+ * `UrlParams` would send `null` as the string `"null"`); other values are
15
+ * converted with `String()`.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * import type * as Fetch from "@monarcode/effetch";
20
+ *
21
+ * const page: Fetch.QueryValue = 2;
22
+ * ```
23
+ *
24
+ * @since 0.1.0
25
+ */
26
+ export type QueryValue = UrlParams.Coercible;
27
+ /**
28
+ * The `query` option: an object whose entries become URL query parameters.
29
+ * Arrays repeat the key (`tags=a&tags=b`); `null` and `undefined` entries
30
+ * and array items are dropped.
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * import type * as Fetch from "@monarcode/effetch";
35
+ *
36
+ * const query: Fetch.QueryInput = {
37
+ * page: 1,
38
+ * tags: ["a", "b"],
39
+ * search: undefined, // dropped
40
+ * };
41
+ * // → ?page=1&tags=a&tags=b
42
+ * ```
43
+ *
44
+ * @since 0.1.0
45
+ */
46
+ export type QueryInput = {
47
+ readonly [key: string]: QueryValue | ReadonlyArray<QueryValue>;
48
+ };
49
+ /**
50
+ * Any Effect schema accepted by the `output`, `error` and `input` options.
51
+ *
52
+ * @example
53
+ * ```ts
54
+ * import * as Schema from "effect/Schema";
55
+ * import type * as Fetch from "@monarcode/effetch";
56
+ *
57
+ * const Todo: Fetch.AnySchema = Schema.Struct({ id: Schema.Number });
58
+ * ```
59
+ *
60
+ * @since 0.1.0
61
+ */
62
+ export type AnySchema = Schema.Constraint;
63
+ /**
64
+ * The `params` option for path `P`: required (with exactly the path's
65
+ * parameter names) when the path has `:params`, forbidden otherwise.
66
+ *
67
+ * When `P` is plain `string` the names can't be known, so `params` is an
68
+ * optional record.
69
+ *
70
+ * @example
71
+ * ```ts
72
+ * import type * as Fetch from "@monarcode/effetch";
73
+ *
74
+ * const withParams: Fetch.ParamsOption<"/todos/:id"> = { params: { id: 1 } };
75
+ * const without: Fetch.ParamsOption<"/todos"> = {};
76
+ * ```
77
+ *
78
+ * @since 0.1.0
79
+ */
80
+ export type ParamsOption<P extends string> = string extends P
81
+ ? {
82
+ readonly params?:
83
+ | {
84
+ readonly [name: string]: PathParamValue;
85
+ }
86
+ | undefined;
87
+ }
88
+ : [PathParamNames<P>] extends [never]
89
+ ? {
90
+ readonly params?: undefined;
91
+ }
92
+ : {
93
+ readonly params: PathParams<P>;
94
+ };
95
+ /**
96
+ * Options shared by every method.
97
+ *
98
+ * - `O`: the `output` schema, or `undefined` for the raw response.
99
+ * - `EB`: the `error` schema for non-2xx bodies.
100
+ * - `Ps`: the client's plugins, for the `plugins` override map.
101
+ *
102
+ * @example
103
+ * ```ts
104
+ * import * as Schema from "effect/Schema";
105
+ * import type * as Fetch from "@monarcode/effetch";
106
+ *
107
+ * const Todo = Schema.Struct({ id: Schema.Number });
108
+ *
109
+ * const options: Fetch.BaseOptions<typeof Todo, Schema.Unknown, []> = {
110
+ * query: { expand: "owner" },
111
+ * headers: { "x-trace": "1" },
112
+ * output: Todo,
113
+ * };
114
+ * ```
115
+ *
116
+ * @since 0.1.0
117
+ */
118
+ export interface BaseOptions<
119
+ O extends AnySchema | undefined,
120
+ EB extends AnySchema,
121
+ Ps extends ReadonlyArray<Plugin.Any>,
122
+ > {
123
+ /** Query parameters. Arrays repeat the key; `null`/`undefined` are dropped. */
124
+ readonly query?: QueryInput | undefined;
125
+ /** Headers for this call, merged over the client's headers. */
126
+ readonly headers?: Headers.Input | undefined;
127
+ /**
128
+ * Decodes the 2xx JSON body with this schema. Without it, the call
129
+ * succeeds with the raw `HttpClientResponse`.
130
+ *
131
+ * With an `output`, the request carries `Accept: application/json`,
132
+ * unless an `accept` header is set on the call or the client.
133
+ *
134
+ * An empty body (204, HEAD, …) is decoded as JSON `null`: use
135
+ * `Schema.Void` for endpoints that return nothing. Non-2xx responses,
136
+ * 3xx included, fail with an `HttpError` before this runs.
137
+ */
138
+ readonly output?: O;
139
+ /** Decodes non-2xx bodies into `HttpError.body`. Overrides the client's. */
140
+ readonly error?: EB | undefined;
141
+ /**
142
+ * Per-request plugin overrides, by plugin name: `false` skips the
143
+ * plugin's hooks and `wrap` for this call, and a partial of its options
144
+ * is merged over its `defaults`.
145
+ */
146
+ readonly plugins?: Plugin.Overrides<Ps> | undefined;
147
+ }
148
+ /**
149
+ * The `body` and `input` options of methods that send a body.
150
+ *
151
+ * With an `input` schema, `body` is typed as the schema's type and encoded
152
+ * with it before being sent as JSON. Without one (`I` is `Schema.Unknown`),
153
+ * `body` is sent according to its runtime type: `HttpBody`, strings,
154
+ * `ArrayBuffer` and typed arrays, `Blob`/`File`, `FormData` and
155
+ * `URLSearchParams` as-is, anything else as JSON.
156
+ *
157
+ * A body that doesn't match `input`, or can't be serialised as JSON, is a
158
+ * defect (the call dies), not a typed failure.
159
+ *
160
+ * @example
161
+ * ```ts
162
+ * import * as Schema from "effect/Schema";
163
+ * import type * as Fetch from "@monarcode/effetch";
164
+ *
165
+ * const NewTodo = Schema.Struct({ title: Schema.String });
166
+ *
167
+ * const options: Fetch.BodyOptions<typeof NewTodo> = {
168
+ * input: NewTodo,
169
+ * body: { title: "Write docs" },
170
+ * };
171
+ * ```
172
+ *
173
+ * @since 0.1.0
174
+ */
175
+ export interface BodyOptions<I extends AnySchema> {
176
+ /** Encodes `body` with this schema, then sends it as JSON. */
177
+ readonly input?: I | undefined;
178
+ /** The request body. Typed by `input` when given. */
179
+ readonly body?: I["Type"] | undefined;
180
+ }
181
+ /**
182
+ * Options of methods that don't send a body: `get`, `head`, `options`.
183
+ *
184
+ * @example
185
+ * ```ts
186
+ * import type * as Schema from "effect/Schema";
187
+ * import type * as Fetch from "@monarcode/effetch";
188
+ *
189
+ * const options: Fetch.NoBodyRequestOptions<
190
+ * "/todos/:id", undefined, Schema.Unknown, []
191
+ * > = { params: { id: 1 } };
192
+ * ```
193
+ *
194
+ * @since 0.1.0
195
+ */
196
+ export type NoBodyRequestOptions<
197
+ P extends string,
198
+ O extends AnySchema | undefined,
199
+ EB extends AnySchema,
200
+ Ps extends ReadonlyArray<Plugin.Any>,
201
+ > = ParamsOption<P> &
202
+ BaseOptions<O, EB, Ps> & {
203
+ readonly body?: undefined;
204
+ readonly input?: undefined;
205
+ };
206
+ /**
207
+ * Options of methods that send a body: `post`, `put`, `patch`, `del` and
208
+ * `request`.
209
+ *
210
+ * @example
211
+ * ```ts
212
+ * import type * as Schema from "effect/Schema";
213
+ * import type * as Fetch from "@monarcode/effetch";
214
+ *
215
+ * const options: Fetch.RequestOptions<
216
+ * "/todos/:id", undefined, Schema.Unknown, Schema.Unknown, []
217
+ * > = { params: { id: 1 }, body: { done: true } };
218
+ * ```
219
+ *
220
+ * @since 0.1.0
221
+ */
222
+ export type RequestOptions<
223
+ P extends string,
224
+ O extends AnySchema | undefined,
225
+ EB extends AnySchema,
226
+ I extends AnySchema,
227
+ Ps extends ReadonlyArray<Plugin.Any>,
228
+ > = ParamsOption<P> & BaseOptions<O, EB, Ps> & BodyOptions<I>;
229
+ /**
230
+ * The rest arguments after the path: the options object `T` is required
231
+ * when path `P` has parameters, optional otherwise.
232
+ *
233
+ * @example
234
+ * ```ts
235
+ * import type * as Fetch from "@monarcode/effetch";
236
+ *
237
+ * type WithParams = Fetch.Args<"/todos/:id", { params: { id: number } }>;
238
+ * // ^? [options: { params: { id: number } }]
239
+ * type WithoutParams = Fetch.Args<"/todos", {}>;
240
+ * // ^? [options?: {}]
241
+ * ```
242
+ *
243
+ * @since 0.1.0
244
+ */
245
+ export type Args<P extends string, T> = [PathParamNames<P>] extends [never]
246
+ ? [options?: T]
247
+ : [options: T];
248
+ /**
249
+ * What a call succeeds with: the `output` schema's type, or the raw
250
+ * `HttpClientResponse` without one. A conditional `output` gives the union.
251
+ *
252
+ * @example
253
+ * ```ts
254
+ * import * as Schema from "effect/Schema";
255
+ * import type * as Fetch from "@monarcode/effetch";
256
+ *
257
+ * type Todo = Fetch.Success<Schema.Struct<{ id: Schema.Number }>>;
258
+ * // ^? { readonly id: number }
259
+ * ```
260
+ *
261
+ * @since 0.1.0
262
+ */
263
+ export type Success<O extends AnySchema | undefined> = O extends AnySchema
264
+ ? O["Type"]
265
+ : HttpClientResponse.HttpClientResponse;
266
+ /**
267
+ * What a call can fail with: `NetworkError`, `HttpError` (with `body`
268
+ * typed by the `error` schema), `DecodeError` when there's an `output`
269
+ * schema, and the plugins' errors.
270
+ *
271
+ * @example
272
+ * ```ts
273
+ * import * as Schema from "effect/Schema";
274
+ * import type * as Fetch from "@monarcode/effetch";
275
+ *
276
+ * type E = Fetch.CallError<
277
+ * Schema.Struct<{ id: Schema.Number }>,
278
+ * Schema.Struct<{ code: Schema.String }>,
279
+ * []
280
+ * >;
281
+ * // ^? NetworkError | HttpError.With<{ readonly code: string }> | DecodeError
282
+ * ```
283
+ *
284
+ * @since 0.1.0
285
+ */
286
+ export type CallError<
287
+ O extends AnySchema | undefined,
288
+ EB extends AnySchema,
289
+ Ps extends ReadonlyArray<Plugin.Any>,
290
+ > =
291
+ | NetworkError
292
+ | HttpError.With<EB["Type"]>
293
+ | (O extends AnySchema ? DecodeError : never)
294
+ | Plugin.Errors<Ps>;
295
+ /**
296
+ * The services a call requires: those needed to decode `output` and
297
+ * `error` and to encode `input`.
298
+ *
299
+ * @example
300
+ * ```ts
301
+ * import * as Schema from "effect/Schema";
302
+ * import type * as Fetch from "@monarcode/effetch";
303
+ *
304
+ * type R = Fetch.CallServices<
305
+ * Schema.Struct<{ id: Schema.Number }>,
306
+ * Schema.Unknown,
307
+ * Schema.Unknown
308
+ * >;
309
+ * // ^? never
310
+ * ```
311
+ *
312
+ * @since 0.1.0
313
+ */
314
+ export type CallServices<
315
+ O extends AnySchema | undefined,
316
+ EB extends AnySchema,
317
+ I extends AnySchema,
318
+ > =
319
+ | (O extends AnySchema ? O["DecodingServices"] : never)
320
+ | EB["DecodingServices"]
321
+ | I["EncodingServices"];
322
+ /**
323
+ * The Effect a call returns: it succeeds with {@link Success}, fails with
324
+ * {@link CallError} and requires {@link CallServices}.
325
+ *
326
+ * - `O`: the `output` schema, or `undefined` for the raw response.
327
+ * - `EB`: the `error` schema for non-2xx bodies.
328
+ * - `I`: the `input` schema (`Schema.Unknown` when the call has none).
329
+ * - `Ps`: the client's plugins.
330
+ *
331
+ * @example
332
+ * ```ts
333
+ * import * as Schema from "effect/Schema";
334
+ * import type * as Fetch from "@monarcode/effetch";
335
+ *
336
+ * type Call = Fetch.CallEffect<
337
+ * Schema.Struct<{ id: Schema.Number }>,
338
+ * Schema.Unknown,
339
+ * Schema.Unknown,
340
+ * []
341
+ * >;
342
+ * // ^? Effect<{ readonly id: number }, NetworkError | HttpError.With<unknown> | DecodeError, never>
343
+ * ```
344
+ *
345
+ * @since 0.1.0
346
+ */
347
+ export type CallEffect<
348
+ O extends AnySchema | undefined,
349
+ EB extends AnySchema,
350
+ I extends AnySchema,
351
+ Ps extends ReadonlyArray<Plugin.Any>,
352
+ > = Effect.Effect<Success<O>, CallError<O, EB, Ps>, CallServices<O, EB, I>>;
353
+ /**
354
+ * The signature of `get`, `head` and `options`: no `body` or `input`.
355
+ *
356
+ * - `Ps`: the client's plugins.
357
+ * - `DefaultEB`: the client's `error` schema, used when a call has none.
358
+ *
359
+ * @example
360
+ * ```ts
361
+ * import * as Schema from "effect/Schema";
362
+ * import type * as Fetch from "@monarcode/effetch";
363
+ *
364
+ * declare const get: Fetch.NoBodyMethod<[], Schema.Unknown>;
365
+ *
366
+ * const Todo = Schema.Struct({ id: Schema.Number });
367
+ * const todo = get("/todos/:id", { params: { id: 1 }, output: Todo });
368
+ * ```
369
+ *
370
+ * @since 0.1.0
371
+ */
372
+ export interface NoBodyMethod<
373
+ Ps extends ReadonlyArray<Plugin.Any>,
374
+ DefaultEB extends AnySchema,
375
+ > {
376
+ <
377
+ P extends string,
378
+ O extends AnySchema | undefined = undefined,
379
+ EB extends AnySchema = DefaultEB,
380
+ >(
381
+ path: P,
382
+ ...args: Args<P, NoBodyRequestOptions<P, O, EB, Ps>>
383
+ ): CallEffect<O, EB, Schema.Unknown, Ps>;
384
+ }
385
+ /**
386
+ * The signature of `post`, `put`, `patch` and `del`: `body` allowed,
387
+ * typed by `input` when given.
388
+ *
389
+ * - `Ps`: the client's plugins.
390
+ * - `DefaultEB`: the client's `error` schema, used when a call has none.
391
+ *
392
+ * @example
393
+ * ```ts
394
+ * import * as Schema from "effect/Schema";
395
+ * import type * as Fetch from "@monarcode/effetch";
396
+ *
397
+ * declare const patch: Fetch.BodyMethod<[], Schema.Unknown>;
398
+ *
399
+ * const Todo = Schema.Struct({ id: Schema.Number, done: Schema.Boolean });
400
+ * const updated = patch("/todos/:id", {
401
+ * params: { id: 1 },
402
+ * body: { done: true },
403
+ * output: Todo,
404
+ * });
405
+ * ```
406
+ *
407
+ * @since 0.1.0
408
+ */
409
+ export interface BodyMethod<
410
+ Ps extends ReadonlyArray<Plugin.Any>,
411
+ DefaultEB extends AnySchema,
412
+ > {
413
+ <
414
+ P extends string,
415
+ O extends AnySchema | undefined = undefined,
416
+ EB extends AnySchema = DefaultEB,
417
+ I extends AnySchema = Schema.Unknown,
418
+ >(
419
+ path: P,
420
+ ...args: Args<P, RequestOptions<P, O, EB, I, Ps>>
421
+ ): CallEffect<O, EB, I, Ps>;
422
+ }
423
+ /**
424
+ * The signature of `request`: any HTTP method, `body` allowed, typed by
425
+ * `input` when given.
426
+ *
427
+ * - `Ps`: the client's plugins.
428
+ * - `DefaultEB`: the client's `error` schema, used when a call has none.
429
+ *
430
+ * @example
431
+ * ```ts
432
+ * import * as Schema from "effect/Schema";
433
+ * import type * as Fetch from "@monarcode/effetch";
434
+ *
435
+ * declare const request: Fetch.RequestMethod<[], Schema.Unknown>;
436
+ *
437
+ * const Todo = Schema.Struct({ id: Schema.Number, done: Schema.Boolean });
438
+ * const updated = request("PATCH", "/todos/:id", {
439
+ * params: { id: 1 },
440
+ * body: { done: true },
441
+ * output: Todo,
442
+ * });
443
+ * ```
444
+ *
445
+ * @since 0.1.0
446
+ */
447
+ export interface RequestMethod<
448
+ Ps extends ReadonlyArray<Plugin.Any>,
449
+ DefaultEB extends AnySchema,
450
+ > {
451
+ <
452
+ P extends string,
453
+ O extends AnySchema | undefined = undefined,
454
+ EB extends AnySchema = DefaultEB,
455
+ I extends AnySchema = Schema.Unknown,
456
+ >(
457
+ method: HttpMethod.HttpMethod,
458
+ path: P,
459
+ ...args: Args<P, RequestOptions<P, O, EB, I, Ps>>
460
+ ): CallEffect<O, EB, I, Ps>;
461
+ }
462
+ //#endregion
package/dist/Path.d.ts ADDED
@@ -0,0 +1,72 @@
1
+ //#region src/Path.d.ts
2
+ /**
3
+ * The parameter name in one path segment `S` (`":id"` gives `"id"`), or
4
+ * `never`.
5
+ */
6
+ type SegmentParam<S extends string> = S extends `:${infer N}`
7
+ ? N extends ""
8
+ ? never
9
+ : N
10
+ : never;
11
+ /**
12
+ * The names of the path parameters in `P`, as a union of string literals.
13
+ *
14
+ * A parameter is a **whole path segment** that starts with `:`. Segments are
15
+ * split on `/`, so `/files/:name.json` has one parameter called `name.json`,
16
+ * and `https://host:3000/users/:id` has one parameter called `id` (`host:3000`
17
+ * doesn't start with `:`). A lone `:` is not a parameter.
18
+ *
19
+ * Each segment is checked on its own, so template literal paths keep their
20
+ * parameters: in `` `/orgs/${string}/users/:id` `` the parameter is `id`.
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * import type * as Fetch from "@monarcode/effetch";
25
+ *
26
+ * type Names = Fetch.PathParamNames<"/users/:id/posts/:postId">;
27
+ * // ^? "id" | "postId"
28
+ *
29
+ * type None = Fetch.PathParamNames<"/users">;
30
+ * // ^? never
31
+ * ```
32
+ *
33
+ * @since 0.1.0
34
+ */
35
+ export type PathParamNames<P extends string> =
36
+ P extends `${infer Head}/${infer Tail}`
37
+ ? SegmentParam<Head> | PathParamNames<Tail>
38
+ : SegmentParam<P>;
39
+ /**
40
+ * A value accepted for a path parameter. It's converted with `String()` and
41
+ * then `encodeURIComponent`.
42
+ *
43
+ * @example
44
+ * ```ts
45
+ * import type * as Fetch from "@monarcode/effetch";
46
+ *
47
+ * const id: Fetch.PathParamValue = 42;
48
+ * ```
49
+ *
50
+ * @since 0.1.0
51
+ */
52
+ export type PathParamValue = string | number;
53
+ /**
54
+ * The `params` object for path `P`: one required key per parameter name.
55
+ *
56
+ * @example
57
+ * ```ts
58
+ * import type * as Fetch from "@monarcode/effetch";
59
+ *
60
+ * const params: Fetch.PathParams<"/users/:id/posts/:postId"> = {
61
+ * id: 1,
62
+ * postId: "intro",
63
+ * };
64
+ * ```
65
+ *
66
+ * @since 0.1.0
67
+ */
68
+ export type PathParams<P extends string> = {
69
+ readonly [K in PathParamNames<P>]: PathParamValue;
70
+ };
71
+ //#endregion
72
+ export {};