@bunny.net/api-client-effect-v4-runtime 0.0.0-stage → 1.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/CHANGELOG.md +10 -0
- package/LICENSE +21 -0
- package/README.md +85 -2
- package/dist/mod.d.ts +22 -0
- package/dist/mod.js +23 -0
- package/dist/mod.js.map +1 -0
- package/dist/src/credentials.d.ts +26 -0
- package/dist/src/credentials.js +27 -0
- package/dist/src/credentials.js.map +1 -0
- package/dist/src/errors.d.ts +64 -0
- package/dist/src/errors.js +35 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/operation.d.ts +99 -0
- package/dist/src/operation.js +302 -0
- package/dist/src/operation.js.map +1 -0
- package/dist/src/testing.d.ts +78 -0
- package/dist/src/testing.js +280 -0
- package/dist/src/testing.js.map +1 -0
- package/package.json +43 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# @bunny.net/api-client-effect-v4-runtime
|
|
2
|
+
|
|
3
|
+
## 1.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 1baeec8: `BunnyCredentials.fromEnv()` without a name now reads `BUNNYNET_API_KEY`. The
|
|
8
|
+
previous default, `BUNNY_API_KEY`, is still read when `BUNNYNET_API_KEY` is
|
|
9
|
+
unset, and is deprecated: the fallback goes away in the next major. A name
|
|
10
|
+
passed explicitly is read as before.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 bunny.net
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,86 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
|
+
<a href="https://bunny.net">
|
|
3
|
+
<img src="https://github.com/BunnyWay/bunnynet-typescript/blob/main/asset/bunny.png?raw=true" width="500" height="auto" alt="Bunny"/>
|
|
4
|
+
</a>
|
|
5
|
+
</div>
|
|
2
6
|
|
|
3
|
-
|
|
7
|
+
# Bunny API client runtime for Effect 4
|
|
8
|
+
|
|
9
|
+
This package contains `@bunny.net/api-client-effect-v4-runtime`, what every
|
|
10
|
+
`@bunny.net/api-client-effect-v4-*` client shares: the `BunnyCredentials`
|
|
11
|
+
service, the error types and the operation executor. You get it with any client;
|
|
12
|
+
import it directly for the credential layers and to match on errors. It is the
|
|
13
|
+
Effect 4 flavour; on Effect 3, use `@bunny.net/api-client-effect-runtime` and
|
|
14
|
+
the `@bunny.net/api-client-effect-*` clients instead.
|
|
15
|
+
|
|
16
|
+
## 🥕 Usage
|
|
17
|
+
|
|
18
|
+
It comes as a peer dependency of each client, so one copy serves every Bunny
|
|
19
|
+
client in an app and their service tags and error classes are the same objects:
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
npm install @bunny.net/api-client-effect-v4-core @bunny.net/api-client-effect-v4-runtime effect
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Handling a documented error
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import { Effect, Layer } from "effect";
|
|
29
|
+
import { FetchHttpClient } from "effect/http";
|
|
30
|
+
import { BunnyCore } from "@bunny.net/api-client-effect-v4-core";
|
|
31
|
+
import { BunnyCredentials } from "@bunny.net/api-client-effect-v4-runtime";
|
|
32
|
+
|
|
33
|
+
const program = Effect.gen(function* () {
|
|
34
|
+
const core = yield* BunnyCore;
|
|
35
|
+
const zone = yield* core.pullZonePublicIndex({ path: { id: 123 } });
|
|
36
|
+
return zone.value.Name;
|
|
37
|
+
}).pipe(
|
|
38
|
+
Effect.catchTag(
|
|
39
|
+
"BunnyResponseError",
|
|
40
|
+
(error) =>
|
|
41
|
+
Effect.succeed(`pull zone 123 answered HTTP ${error.response.status}`),
|
|
42
|
+
),
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const Live = BunnyCore.Default.pipe(
|
|
46
|
+
Layer.provide(BunnyCredentials.layer(process.env.BUNNYNET_API_KEY!)),
|
|
47
|
+
Layer.provide(FetchHttpClient.layer),
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
console.log(await Effect.runPromise(program.pipe(Effect.provide(Live))));
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`BunnyCredentials.layer(key)` holds one key, as a string or an existing
|
|
54
|
+
`Redacted`; `BunnyCredentials.fromEnv(name)` reads it from configuration,
|
|
55
|
+
`BUNNYNET_API_KEY` by default, falling back to the deprecated `BUNNY_API_KEY`
|
|
56
|
+
when it is unset. The key is kept `Redacted`, so it never shows up through
|
|
57
|
+
`toString`, `JSON.stringify` or inspection, and it is revealed at one place
|
|
58
|
+
only: when the auth header is set on the outgoing request.
|
|
59
|
+
|
|
60
|
+
The errors are all `Data.TaggedError`, so `Effect.catchTag` narrows them:
|
|
61
|
+
|
|
62
|
+
- `BunnyInputError`: the input was rejected by the operation's schema and
|
|
63
|
+
nothing was sent.
|
|
64
|
+
- `BunnyResponseError`: a documented error status, with `response` holding the
|
|
65
|
+
status and the body decoded by that status's schema.
|
|
66
|
+
- `BunnyDecodeError`: a documented status whose body the schema rejects. This is
|
|
67
|
+
the signal that the live API drifted from its spec.
|
|
68
|
+
- `BunnyUnexpectedStatusError`: a status the spec doesn't document, with the raw
|
|
69
|
+
body attached.
|
|
70
|
+
|
|
71
|
+
Transport failures are Effect's own `HttpClientError` from `effect/http`: one
|
|
72
|
+
class, tagged `"HttpClientError"`, whose `reason` says what happened
|
|
73
|
+
(`TransportError`, `StatusCodeError`, …). Coming from the Effect 3 client,
|
|
74
|
+
`Effect.catchTag("RequestError", …)` becomes
|
|
75
|
+
`Effect.catchTag("HttpClientError", …)` with a match on `error.reason._tag`.
|
|
76
|
+
Generated clients contain no transport code: retries, timeouts, tracing and
|
|
77
|
+
request rewriting are whatever your `HttpClient` layer does, and every operation
|
|
78
|
+
runs inside a span named after its operation id.
|
|
79
|
+
|
|
80
|
+
The `/testing` subpath has `stubHttpClient()`, `fetchHttpClient()` and
|
|
81
|
+
`serviceContract()`, the behaviours every generated client is checked against.
|
|
82
|
+
|
|
83
|
+
## 🔧 Contributing
|
|
84
|
+
|
|
85
|
+
The development setup, the commands and how releases are made are described in
|
|
86
|
+
[docs/human_development.md](https://github.com/BunnyWay/bunnynet-typescript/blob/main/docs/human_development.md).
|
package/dist/mod.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @bunny.net/api-client-effect-v4-runtime
|
|
3
|
+
*
|
|
4
|
+
* The shared runtime every generated Bunny.net Effect 4 client depends on:
|
|
5
|
+
*
|
|
6
|
+
* - `BunnyCredentials` — the service that carries the API key (a
|
|
7
|
+
* `Redacted` string). Provide it with `BunnyCredentials.layer(key)` or
|
|
8
|
+
* `BunnyCredentials.fromEnv()`.
|
|
9
|
+
* - The error taxonomy — tagged errors on the Effect error channel
|
|
10
|
+
* (`BunnyInputError`, `BunnyResponseError`, `BunnyDecodeError`,
|
|
11
|
+
* `BunnyUnexpectedStatusError`); transport failures surface as
|
|
12
|
+
* Effect's own `HttpClientError` from `effect/http`.
|
|
13
|
+
* - `executeOperation` — runs one generated operation descriptor
|
|
14
|
+
* against the `HttpClient` from context. Generated clients never
|
|
15
|
+
* touch `fetch`: whichever `HttpClient` layer the caller provides
|
|
16
|
+
* (`FetchHttpClient.layer`, `NodeHttpClient.layer`, a test stub) is
|
|
17
|
+
* what sends the request, and retry / timeout / tracing are the
|
|
18
|
+
* caller's `HttpClient` combinators, not generated code.
|
|
19
|
+
*/
|
|
20
|
+
export * from "./src/credentials.js";
|
|
21
|
+
export * from "./src/errors.js";
|
|
22
|
+
export * from "./src/operation.js";
|
package/dist/mod.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @bunny.net/api-client-effect-v4-runtime
|
|
3
|
+
*
|
|
4
|
+
* The shared runtime every generated Bunny.net Effect 4 client depends on:
|
|
5
|
+
*
|
|
6
|
+
* - `BunnyCredentials` — the service that carries the API key (a
|
|
7
|
+
* `Redacted` string). Provide it with `BunnyCredentials.layer(key)` or
|
|
8
|
+
* `BunnyCredentials.fromEnv()`.
|
|
9
|
+
* - The error taxonomy — tagged errors on the Effect error channel
|
|
10
|
+
* (`BunnyInputError`, `BunnyResponseError`, `BunnyDecodeError`,
|
|
11
|
+
* `BunnyUnexpectedStatusError`); transport failures surface as
|
|
12
|
+
* Effect's own `HttpClientError` from `effect/http`.
|
|
13
|
+
* - `executeOperation` — runs one generated operation descriptor
|
|
14
|
+
* against the `HttpClient` from context. Generated clients never
|
|
15
|
+
* touch `fetch`: whichever `HttpClient` layer the caller provides
|
|
16
|
+
* (`FetchHttpClient.layer`, `NodeHttpClient.layer`, a test stub) is
|
|
17
|
+
* what sends the request, and retry / timeout / tracing are the
|
|
18
|
+
* caller's `HttpClient` combinators, not generated code.
|
|
19
|
+
*/
|
|
20
|
+
export * from "./src/credentials.js";
|
|
21
|
+
export * from "./src/errors.js";
|
|
22
|
+
export * from "./src/operation.js";
|
|
23
|
+
//# sourceMappingURL=mod.js.map
|
package/dist/mod.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mod.js","sourceRoot":"","sources":["../mod.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,cAAc,sBAAsB,CAAC;AACrC,cAAc,iBAAiB,CAAC;AAChC,cAAc,oBAAoB,CAAC","sourcesContent":["/**\n * @bunny.net/api-client-effect-v4-runtime\n *\n * The shared runtime every generated Bunny.net Effect 4 client depends on:\n *\n * - `BunnyCredentials` — the service that carries the API key (a\n * `Redacted` string). Provide it with `BunnyCredentials.layer(key)` or\n * `BunnyCredentials.fromEnv()`.\n * - The error taxonomy — tagged errors on the Effect error channel\n * (`BunnyInputError`, `BunnyResponseError`, `BunnyDecodeError`,\n * `BunnyUnexpectedStatusError`); transport failures surface as\n * Effect's own `HttpClientError` from `effect/http`.\n * - `executeOperation` — runs one generated operation descriptor\n * against the `HttpClient` from context. Generated clients never\n * touch `fetch`: whichever `HttpClient` layer the caller provides\n * (`FetchHttpClient.layer`, `NodeHttpClient.layer`, a test stub) is\n * what sends the request, and retry / timeout / tracing are the\n * caller's `HttpClient` combinators, not generated code.\n */\nexport * from \"./src/credentials.ts\";\nexport * from \"./src/errors.ts\";\nexport * from \"./src/operation.ts\";\n"]}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Config, Context, Layer, Redacted } from "effect";
|
|
2
|
+
/** What a Bunny.net client needs to authenticate: the API key (or, for the Edge Storage API, the zone password). */
|
|
3
|
+
export interface BunnyCredentialsShape {
|
|
4
|
+
readonly accessKey: Redacted.Redacted<string>;
|
|
5
|
+
}
|
|
6
|
+
declare const BunnyCredentials_base: Context.ServiceClass<BunnyCredentials, "@bunny.net/api-client-effect-v4-runtime/BunnyCredentials", BunnyCredentialsShape>;
|
|
7
|
+
/**
|
|
8
|
+
* The credential service every generated client reads from context.
|
|
9
|
+
*
|
|
10
|
+
* The key is held as `Redacted` so it never appears through
|
|
11
|
+
* `toString`/`JSON.stringify`/inspection; it is revealed at exactly one
|
|
12
|
+
* place, when the auth header is set on the outgoing request.
|
|
13
|
+
*/
|
|
14
|
+
export declare class BunnyCredentials extends BunnyCredentials_base {
|
|
15
|
+
/** A layer holding one key. Accepts a raw string (wrapped immediately) or an existing `Redacted`. */
|
|
16
|
+
static readonly layer: (accessKey: string | Redacted.Redacted<string>) => Layer.Layer<BunnyCredentials>;
|
|
17
|
+
/**
|
|
18
|
+
* A layer reading the key from configuration (the environment by default).
|
|
19
|
+
*
|
|
20
|
+
* With a name, it reads that name only. Without one, it reads
|
|
21
|
+
* `BUNNYNET_API_KEY`, then the deprecated `BUNNY_API_KEY` when that is
|
|
22
|
+
* unset; the fallback goes away in the next major.
|
|
23
|
+
*/
|
|
24
|
+
static readonly fromEnv: (name?: string) => Layer.Layer<BunnyCredentials, Config.ConfigError>;
|
|
25
|
+
}
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Config, Context, Effect, Layer, Redacted } from "effect";
|
|
2
|
+
/**
|
|
3
|
+
* The credential service every generated client reads from context.
|
|
4
|
+
*
|
|
5
|
+
* The key is held as `Redacted` so it never appears through
|
|
6
|
+
* `toString`/`JSON.stringify`/inspection; it is revealed at exactly one
|
|
7
|
+
* place, when the auth header is set on the outgoing request.
|
|
8
|
+
*/
|
|
9
|
+
export class BunnyCredentials extends Context.Service()("@bunny.net/api-client-effect-v4-runtime/BunnyCredentials") {
|
|
10
|
+
/** A layer holding one key. Accepts a raw string (wrapped immediately) or an existing `Redacted`. */
|
|
11
|
+
static layer = (accessKey) => Layer.succeed(BunnyCredentials, {
|
|
12
|
+
accessKey: Redacted.isRedacted(accessKey)
|
|
13
|
+
? accessKey
|
|
14
|
+
: Redacted.make(accessKey),
|
|
15
|
+
});
|
|
16
|
+
/**
|
|
17
|
+
* A layer reading the key from configuration (the environment by default).
|
|
18
|
+
*
|
|
19
|
+
* With a name, it reads that name only. Without one, it reads
|
|
20
|
+
* `BUNNYNET_API_KEY`, then the deprecated `BUNNY_API_KEY` when that is
|
|
21
|
+
* unset; the fallback goes away in the next major.
|
|
22
|
+
*/
|
|
23
|
+
static fromEnv = (name) => Layer.effect(BunnyCredentials, (name === undefined
|
|
24
|
+
? Config.Redacted("BUNNYNET_API_KEY").pipe(Config.orElse(() => Config.Redacted("BUNNY_API_KEY")))
|
|
25
|
+
: Config.Redacted(name)).pipe(Effect.map((accessKey) => ({ accessKey }))));
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=credentials.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credentials.js","sourceRoot":"","sources":["../../src/credentials.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAOlE;;;;;;GAMG;AACH,MAAM,OAAO,gBACX,SAAQ,OAAO,CAAC,OAAO,EAA2C,CAChE,0DAA0D,CAC3D;IACD,qGAAqG;IACrG,MAAM,CAAU,KAAK,GAAG,CACtB,SAA6C,EACd,EAAE,CACjC,KAAK,CAAC,OAAO,CAAC,gBAAgB,EAAE;QAC9B,SAAS,EAAE,QAAQ,CAAC,UAAU,CAAC,SAAS,CAAC;YACvC,CAAC,CAAC,SAAS;YACX,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC;KAC7B,CAAC,CAAC;IAEL;;;;;;OAMG;IACH,MAAM,CAAU,OAAO,GAAG,CACxB,IAAa,EACsC,EAAE,CACrD,KAAK,CAAC,MAAM,CACV,gBAAgB,EAChB,CAAC,IAAI,KAAK,SAAS;QACjB,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,kBAAkB,CAAC,CAAC,IAAI,CACxC,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAC,CACtD;QACD,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAC3B,MAAM,CAAC,GAAG,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAC3C,CACJ,CAAC","sourcesContent":["import { Config, Context, Effect, Layer, Redacted } from \"effect\";\n\n/** What a Bunny.net client needs to authenticate: the API key (or, for the Edge Storage API, the zone password). */\nexport interface BunnyCredentialsShape {\n readonly accessKey: Redacted.Redacted<string>;\n}\n\n/**\n * The credential service every generated client reads from context.\n *\n * The key is held as `Redacted` so it never appears through\n * `toString`/`JSON.stringify`/inspection; it is revealed at exactly one\n * place, when the auth header is set on the outgoing request.\n */\nexport class BunnyCredentials\n extends Context.Service<BunnyCredentials, BunnyCredentialsShape>()(\n \"@bunny.net/api-client-effect-v4-runtime/BunnyCredentials\",\n ) {\n /** A layer holding one key. Accepts a raw string (wrapped immediately) or an existing `Redacted`. */\n static readonly layer = (\n accessKey: string | Redacted.Redacted<string>,\n ): Layer.Layer<BunnyCredentials> =>\n Layer.succeed(BunnyCredentials, {\n accessKey: Redacted.isRedacted(accessKey)\n ? accessKey\n : Redacted.make(accessKey),\n });\n\n /**\n * A layer reading the key from configuration (the environment by default).\n *\n * With a name, it reads that name only. Without one, it reads\n * `BUNNYNET_API_KEY`, then the deprecated `BUNNY_API_KEY` when that is\n * unset; the fallback goes away in the next major.\n */\n static readonly fromEnv = (\n name?: string,\n ): Layer.Layer<BunnyCredentials, Config.ConfigError> =>\n Layer.effect(\n BunnyCredentials,\n (name === undefined\n ? Config.Redacted(\"BUNNYNET_API_KEY\").pipe(\n Config.orElse(() => Config.Redacted(\"BUNNY_API_KEY\")),\n )\n : Config.Redacted(name)).pipe(\n Effect.map((accessKey) => ({ accessKey })),\n ),\n );\n}\n"]}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { type Schema } from "effect";
|
|
2
|
+
import type { HttpClientError } from "effect/http";
|
|
3
|
+
declare const BunnyInputError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
4
|
+
readonly _tag: "BunnyInputError";
|
|
5
|
+
} & Readonly<A>;
|
|
6
|
+
/** The caller's input did not satisfy the operation's input schema. */
|
|
7
|
+
export declare class BunnyInputError extends BunnyInputError_base<{
|
|
8
|
+
readonly operation: string;
|
|
9
|
+
readonly cause: Schema.SchemaError;
|
|
10
|
+
}> {
|
|
11
|
+
get message(): string;
|
|
12
|
+
}
|
|
13
|
+
declare const BunnyResponseError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
14
|
+
readonly _tag: "BunnyResponseError";
|
|
15
|
+
} & Readonly<A>;
|
|
16
|
+
/**
|
|
17
|
+
* The API answered with a status the spec documents as an error. `response`
|
|
18
|
+
* is `{ status, value }` with `value` decoded by that status's schema, so
|
|
19
|
+
* the error channel is as typed as the success channel.
|
|
20
|
+
*/
|
|
21
|
+
export declare class BunnyResponseError<R = {
|
|
22
|
+
readonly status: number;
|
|
23
|
+
readonly value: unknown;
|
|
24
|
+
}> extends BunnyResponseError_base<{
|
|
25
|
+
readonly operation: string;
|
|
26
|
+
readonly response: R;
|
|
27
|
+
}> {
|
|
28
|
+
get message(): string;
|
|
29
|
+
}
|
|
30
|
+
declare const BunnyDecodeError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
31
|
+
readonly _tag: "BunnyDecodeError";
|
|
32
|
+
} & Readonly<A>;
|
|
33
|
+
/**
|
|
34
|
+
* The API answered with a documented status but a body the schema rejects.
|
|
35
|
+
* This is the spec-drift signal: the fix belongs in a `patches.ts` next to
|
|
36
|
+
* the surface's barrel, not in the caller.
|
|
37
|
+
*/
|
|
38
|
+
export declare class BunnyDecodeError extends BunnyDecodeError_base<{
|
|
39
|
+
readonly operation: string;
|
|
40
|
+
readonly status: number;
|
|
41
|
+
readonly cause: Schema.SchemaError;
|
|
42
|
+
}> {
|
|
43
|
+
get message(): string;
|
|
44
|
+
}
|
|
45
|
+
declare const BunnyUnexpectedStatusError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
46
|
+
readonly _tag: "BunnyUnexpectedStatusError";
|
|
47
|
+
} & Readonly<A>;
|
|
48
|
+
/** The API answered with a status the spec does not document at all. `body` is the raw payload (JSON-parsed when possible). */
|
|
49
|
+
export declare class BunnyUnexpectedStatusError extends BunnyUnexpectedStatusError_base<{
|
|
50
|
+
readonly operation: string;
|
|
51
|
+
readonly status: number;
|
|
52
|
+
readonly body: unknown;
|
|
53
|
+
}> {
|
|
54
|
+
get message(): string;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Everything an operation can fail with besides its own typed
|
|
58
|
+
* `BunnyResponseError`. `HttpClientError` covers transport failures
|
|
59
|
+
* (connection, timeout, malformed body) and comes straight from Effect's
|
|
60
|
+
* `effect/http`: one class, tagged `"HttpClientError"`, whose `reason`
|
|
61
|
+
* (`TransportError`, `StatusCodeError`, …) says what happened.
|
|
62
|
+
*/
|
|
63
|
+
export type BunnyClientError = BunnyInputError | BunnyDecodeError | BunnyUnexpectedStatusError | HttpClientError.HttpClientError;
|
|
64
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Data } from "effect";
|
|
2
|
+
/** The caller's input did not satisfy the operation's input schema. */
|
|
3
|
+
export class BunnyInputError extends Data.TaggedError("BunnyInputError") {
|
|
4
|
+
get message() {
|
|
5
|
+
return `${this.operation}: invalid input — ${this.cause.message}`;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The API answered with a status the spec documents as an error. `response`
|
|
10
|
+
* is `{ status, value }` with `value` decoded by that status's schema, so
|
|
11
|
+
* the error channel is as typed as the success channel.
|
|
12
|
+
*/
|
|
13
|
+
export class BunnyResponseError extends Data.TaggedError("BunnyResponseError") {
|
|
14
|
+
get message() {
|
|
15
|
+
const status = this.response.status;
|
|
16
|
+
return `${this.operation}: HTTP ${status ?? "?"}`;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The API answered with a documented status but a body the schema rejects.
|
|
21
|
+
* This is the spec-drift signal: the fix belongs in a `patches.ts` next to
|
|
22
|
+
* the surface's barrel, not in the caller.
|
|
23
|
+
*/
|
|
24
|
+
export class BunnyDecodeError extends Data.TaggedError("BunnyDecodeError") {
|
|
25
|
+
get message() {
|
|
26
|
+
return `${this.operation}: HTTP ${this.status} body does not match the spec — ${this.cause.message}`;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** The API answered with a status the spec does not document at all. `body` is the raw payload (JSON-parsed when possible). */
|
|
30
|
+
export class BunnyUnexpectedStatusError extends Data.TaggedError("BunnyUnexpectedStatusError") {
|
|
31
|
+
get message() {
|
|
32
|
+
return `${this.operation}: unexpected HTTP ${this.status}`;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAe,MAAM,QAAQ,CAAC;AAG3C,uEAAuE;AACvE,MAAM,OAAO,eAAgB,SAAQ,IAAI,CAAC,WAAW,CAAC,iBAAiB,CAGrE;IACA,IAAa,OAAO;QAClB,OAAO,GAAG,IAAI,CAAC,SAAS,qBAAqB,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC;IACpE,CAAC;CACF;AAED;;;;GAIG;AACH,MAAM,OAAO,kBAEX,SAAQ,IAAI,CAAC,WAAW,CAAC,oBAAoB,CAG7C;IACA,IAAa,OAAO;QAClB,MAAM,MAAM,GAAI,IAAI,CAAC,QAAgC,CAAC,MAAM,CAAC;QAC7D,OAAO,GAAG,IAAI,CAAC,SAAS,UAAU,MAAM,IAAI,GAAG,EAAE,CAAC;IACpD,CAAC;CACF;AAED;;;;GAIG;AACH,MAAM,OAAO,gBAAiB,SAAQ,IAAI,CAAC,WAAW,CAAC,kBAAkB,CAIvE;IACA,IAAa,OAAO;QAClB,OAAO,GAAG,IAAI,CAAC,SAAS,UAAU,IAAI,CAAC,MAAM,mCAAmC,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC;IACvG,CAAC;CACF;AAED,+HAA+H;AAC/H,MAAM,OAAO,0BACX,SAAQ,IAAI,CAAC,WAAW,CAAC,4BAA4B,CAInD;IACF,IAAa,OAAO;QAClB,OAAO,GAAG,IAAI,CAAC,SAAS,qBAAqB,IAAI,CAAC,MAAM,EAAE,CAAC;IAC7D,CAAC;CACF","sourcesContent":["import { Data, type Schema } from \"effect\";\nimport type { HttpClientError } from \"effect/http\";\n\n/** The caller's input did not satisfy the operation's input schema. */\nexport class BunnyInputError extends Data.TaggedError(\"BunnyInputError\")<{\n readonly operation: string;\n readonly cause: Schema.SchemaError;\n}> {\n override get message(): string {\n return `${this.operation}: invalid input — ${this.cause.message}`;\n }\n}\n\n/**\n * The API answered with a status the spec documents as an error. `response`\n * is `{ status, value }` with `value` decoded by that status's schema, so\n * the error channel is as typed as the success channel.\n */\nexport class BunnyResponseError<\n R = { readonly status: number; readonly value: unknown },\n> extends Data.TaggedError(\"BunnyResponseError\")<{\n readonly operation: string;\n readonly response: R;\n}> {\n override get message(): string {\n const status = (this.response as { status?: number }).status;\n return `${this.operation}: HTTP ${status ?? \"?\"}`;\n }\n}\n\n/**\n * The API answered with a documented status but a body the schema rejects.\n * This is the spec-drift signal: the fix belongs in a `patches.ts` next to\n * the surface's barrel, not in the caller.\n */\nexport class BunnyDecodeError extends Data.TaggedError(\"BunnyDecodeError\")<{\n readonly operation: string;\n readonly status: number;\n readonly cause: Schema.SchemaError;\n}> {\n override get message(): string {\n return `${this.operation}: HTTP ${this.status} body does not match the spec — ${this.cause.message}`;\n }\n}\n\n/** The API answered with a status the spec does not document at all. `body` is the raw payload (JSON-parsed when possible). */\nexport class BunnyUnexpectedStatusError\n extends Data.TaggedError(\"BunnyUnexpectedStatusError\")<{\n readonly operation: string;\n readonly status: number;\n readonly body: unknown;\n }> {\n override get message(): string {\n return `${this.operation}: unexpected HTTP ${this.status}`;\n }\n}\n\n/**\n * Everything an operation can fail with besides its own typed\n * `BunnyResponseError`. `HttpClientError` covers transport failures\n * (connection, timeout, malformed body) and comes straight from Effect's\n * `effect/http`: one class, tagged `\"HttpClientError\"`, whose `reason`\n * (`TransportError`, `StatusCodeError`, …) says what happened.\n */\nexport type BunnyClientError =\n | BunnyInputError\n | BunnyDecodeError\n | BunnyUnexpectedStatusError\n | HttpClientError.HttpClientError;\n"]}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import { type HttpClient } from "effect/http";
|
|
3
|
+
import type { BunnyCredentialsShape } from "./credentials.js";
|
|
4
|
+
import { type BunnyClientError, BunnyResponseError } from "./errors.js";
|
|
5
|
+
/** How a response body is read before (or instead of) schema decoding. `stream` is read fully, like `binary`. */
|
|
6
|
+
export type ResponseKind = "json" | "text" | "empty" | "binary" | "stream";
|
|
7
|
+
/**
|
|
8
|
+
* Any schema with no decoding and no encoding services: `Codec` is covariant
|
|
9
|
+
* in its type parameters, so every concrete schema is assignable here, and
|
|
10
|
+
* the `never` services pin every operation to `R = never`, whatever schema the
|
|
11
|
+
* generator emits (`Schema.Top` would widen the services to `unknown`).
|
|
12
|
+
*/
|
|
13
|
+
export type ServiceFreeCodec = Schema.Codec<unknown, unknown, never, never>;
|
|
14
|
+
export interface ResponseEntry {
|
|
15
|
+
readonly schema: ServiceFreeCodec;
|
|
16
|
+
readonly kind: ResponseKind;
|
|
17
|
+
}
|
|
18
|
+
/** Status code (or `"default"`) → how to read + decode that response. */
|
|
19
|
+
export type ResponseSpec = Record<string, ResponseEntry>;
|
|
20
|
+
export type ResponseValue<T extends ResponseEntry> = T["kind"] extends "stream" | "binary" ? ArrayBuffer : T["schema"]["Type"];
|
|
21
|
+
/** The `{ status, value }` union an operation resolves (or fails) with, one member per documented status. */
|
|
22
|
+
export type ResponseUnion<T extends ResponseSpec> = {
|
|
23
|
+
[K in keyof T]: K extends "default" ? {
|
|
24
|
+
readonly status: number;
|
|
25
|
+
readonly value: ResponseValue<T[K]>;
|
|
26
|
+
} : K extends `${infer N extends number}` ? {
|
|
27
|
+
readonly status: N;
|
|
28
|
+
readonly value: ResponseValue<T[K]>;
|
|
29
|
+
} : K extends number ? {
|
|
30
|
+
readonly status: K;
|
|
31
|
+
readonly value: ResponseValue<T[K]>;
|
|
32
|
+
} : {
|
|
33
|
+
readonly status: number;
|
|
34
|
+
readonly value: ResponseValue<T[K]>;
|
|
35
|
+
};
|
|
36
|
+
}[keyof T];
|
|
37
|
+
/** The typed failure for an operation's documented error statuses (`never` when the spec documents none). */
|
|
38
|
+
export type OperationFailure<E extends ResponseSpec | undefined> = E extends ResponseSpec ? BunnyResponseError<ResponseUnion<E>> : never;
|
|
39
|
+
export type SecurityScheme = {
|
|
40
|
+
readonly type: "http";
|
|
41
|
+
readonly scheme: "bearer" | "basic";
|
|
42
|
+
readonly description?: string;
|
|
43
|
+
} | {
|
|
44
|
+
readonly type: "apiKey";
|
|
45
|
+
readonly in: "header" | "query" | "cookie";
|
|
46
|
+
readonly name: string;
|
|
47
|
+
readonly description?: string;
|
|
48
|
+
} | {
|
|
49
|
+
readonly type: "oauth2";
|
|
50
|
+
readonly flows: unknown;
|
|
51
|
+
readonly description?: string;
|
|
52
|
+
} | {
|
|
53
|
+
readonly type: "openIdConnect";
|
|
54
|
+
readonly openIdConnectUrl: string;
|
|
55
|
+
readonly description?: string;
|
|
56
|
+
};
|
|
57
|
+
export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS";
|
|
58
|
+
export interface OperationDescriptor<S extends ResponseSpec, E extends ResponseSpec | undefined = undefined> {
|
|
59
|
+
/** Operation id, used in error messages and the tracing span name. */
|
|
60
|
+
readonly name: string;
|
|
61
|
+
readonly method: HttpMethod;
|
|
62
|
+
/** Path template with `{param}` placeholders. */
|
|
63
|
+
readonly path: string;
|
|
64
|
+
/** The spec's server URL for this operation; `ClientOptions.baseUrl` overrides it. */
|
|
65
|
+
readonly baseUrl?: string;
|
|
66
|
+
/** How the credential is sent. Omit for unauthenticated operations. */
|
|
67
|
+
readonly security?: SecurityScheme;
|
|
68
|
+
/** `{ path?, query?, headers?, body? }` schema; omit when the operation takes no input. Its encoded side is checked against `DecodedInput`. */
|
|
69
|
+
readonly input?: Schema.Codec<unknown, DecodedInput, never, never>;
|
|
70
|
+
/** Request body content type; defaults to JSON. */
|
|
71
|
+
readonly contentType?: string;
|
|
72
|
+
readonly success: S;
|
|
73
|
+
readonly error?: E;
|
|
74
|
+
}
|
|
75
|
+
/** Per-client options every generated `layer(options)` accepts. */
|
|
76
|
+
export interface ClientOptions {
|
|
77
|
+
/** Overrides the spec's server URL — e.g. to point a client at a staging host. */
|
|
78
|
+
readonly baseUrl?: string;
|
|
79
|
+
}
|
|
80
|
+
/** What a generated service captures once at layer construction. */
|
|
81
|
+
export interface ClientContext {
|
|
82
|
+
readonly http: HttpClient.HttpClient;
|
|
83
|
+
readonly credentials: BunnyCredentialsShape;
|
|
84
|
+
readonly options: ClientOptions;
|
|
85
|
+
}
|
|
86
|
+
/** The encoded form of an operation's input: what the generated input schema must encode to. */
|
|
87
|
+
export type DecodedInput = {
|
|
88
|
+
readonly path?: Record<string, unknown>;
|
|
89
|
+
readonly query?: Record<string, unknown>;
|
|
90
|
+
readonly headers?: Record<string, unknown>;
|
|
91
|
+
readonly body?: unknown;
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Run one operation: decode the input, build the request, send it through
|
|
95
|
+
* the caller's `HttpClient`, and decode the response by status. Resolves
|
|
96
|
+
* with `{ status, value }` for documented success statuses, fails with a
|
|
97
|
+
* typed `BunnyResponseError` for documented error statuses.
|
|
98
|
+
*/
|
|
99
|
+
export declare const executeOperation: <S extends ResponseSpec, E extends ResponseSpec | undefined>(ctx: ClientContext, op: OperationDescriptor<S, E>, input: unknown) => Effect.Effect<ResponseUnion<S>, BunnyClientError | OperationFailure<E>, never>;
|