@monarcode/effetch 0.0.0-stage → 0.1.0-alpha.1
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/CHANGELOG.md +56 -0
- package/LICENSE +21 -0
- package/README.md +613 -2
- package/dist/Client.d.ts +367 -0
- package/dist/Client.js +110 -0
- package/dist/Errors.d.ts +224 -0
- package/dist/Errors.js +151 -0
- package/dist/Options.d.ts +462 -0
- package/dist/Path.d.ts +72 -0
- package/dist/Plugin.d.ts +553 -0
- package/dist/Plugin.js +7 -0
- package/dist/index.d.ts +60 -0
- package/dist/index.js +5 -0
- package/dist/internal/body.js +70 -0
- package/dist/internal/object.js +6 -0
- package/dist/internal/path.js +23 -0
- package/dist/internal/pipeline.js +29 -0
- package/dist/internal/plugins.js +92 -0
- package/dist/internal/query.js +22 -0
- package/dist/internal/request.js +33 -0
- package/dist/internal/resolve.js +23 -0
- package/dist/internal/response.js +73 -0
- package/dist/internal/url.js +24 -0
- package/dist/plugins/bearer.d.ts +152 -0
- package/dist/plugins/bearer.js +63 -0
- package/dist/plugins/index.d.ts +32 -0
- package/dist/plugins/index.js +6 -0
- package/dist/plugins/logger.d.ts +66 -0
- package/dist/plugins/logger.js +67 -0
- package/dist/plugins/retry.d.ts +94 -0
- package/dist/plugins/retry.js +74 -0
- package/dist/plugins/timeout.d.ts +108 -0
- package/dist/plugins/timeout.js +84 -0
- package/package.json +85 -3
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 {};
|