@prover-coder-ai/openapi-effect 1.0.19
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/README.md +36 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/main.js +2 -0
- package/dist/main.js.map +1 -0
- package/package.json +58 -0
- package/src/app/main.ts +18 -0
- package/src/app/program.ts +33 -0
- package/src/core/api/openapi.d.ts +445 -0
- package/src/core/api-client/index.ts +32 -0
- package/src/core/api-client/strict-types.ts +349 -0
- package/src/core/axioms.ts +143 -0
- package/src/core/greeting.ts +22 -0
- package/src/generated/decoders.ts +318 -0
- package/src/generated/dispatch.ts +172 -0
- package/src/generated/dispatchers-by-path.ts +40 -0
- package/src/generated/index.ts +9 -0
- package/src/index.ts +42 -0
- package/src/shell/api-client/create-client-types.ts +310 -0
- package/src/shell/api-client/create-client.ts +517 -0
- package/src/shell/api-client/index.ts +37 -0
- package/src/shell/api-client/strict-client.ts +471 -0
- package/src/shell/cli.ts +22 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
// CHANGE: Define core type-level operations for extracting OpenAPI types
|
|
2
|
+
// WHY: Enable compile-time type safety without runtime overhead through pure type transformations
|
|
3
|
+
// QUOTE(ТЗ): "Success / HttpError являются коррелированными суммами (status → точный тип body) строго из OpenAPI типов"
|
|
4
|
+
// REF: issue-2, section 3.1, 4.1-4.3
|
|
5
|
+
// SOURCE: n/a
|
|
6
|
+
// FORMAT THEOREM: ∀ Op ∈ Operations: ResponseVariant<Op> = Success<Op> ⊎ Failure<Op>
|
|
7
|
+
// PURITY: CORE
|
|
8
|
+
// INVARIANT: All types computed at compile time, no runtime operations
|
|
9
|
+
// COMPLEXITY: O(1) compile-time / O(0) runtime
|
|
10
|
+
|
|
11
|
+
import type { HttpMethod, PathsWithMethod } from "openapi-typescript-helpers"
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Extract all paths that support a given HTTP method
|
|
15
|
+
*
|
|
16
|
+
* @pure true - compile-time only
|
|
17
|
+
* @invariant Result ⊆ paths
|
|
18
|
+
*/
|
|
19
|
+
export type PathsForMethod<
|
|
20
|
+
Paths extends object,
|
|
21
|
+
Method extends HttpMethod
|
|
22
|
+
> = PathsWithMethod<Paths, Method>
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Extract operation definition for a path and method
|
|
26
|
+
*
|
|
27
|
+
* @pure true - compile-time only
|
|
28
|
+
* @invariant ∀ path ∈ Paths, method ∈ Methods: Operation<Paths, path, method> = Paths[path][method]
|
|
29
|
+
*/
|
|
30
|
+
export type OperationFor<
|
|
31
|
+
Paths extends object,
|
|
32
|
+
Path extends keyof Paths,
|
|
33
|
+
Method extends HttpMethod
|
|
34
|
+
> = Method extends keyof Paths[Path] ? Paths[Path][Method] : never
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Extract all response definitions from an operation
|
|
38
|
+
*
|
|
39
|
+
* @pure true - compile-time only
|
|
40
|
+
*/
|
|
41
|
+
export type ResponsesFor<Op> = Op extends { responses: infer R } ? R : never
|
|
42
|
+
|
|
43
|
+
// ============================================================================
|
|
44
|
+
// Request-side typing (path/method → params/query/body)
|
|
45
|
+
// ============================================================================
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Extract path parameters from operation
|
|
49
|
+
*
|
|
50
|
+
* @pure true - compile-time only
|
|
51
|
+
* @invariant Returns path params type or undefined if none
|
|
52
|
+
*/
|
|
53
|
+
export type PathParamsFor<Op> = Op extends { parameters: { path: infer P } }
|
|
54
|
+
? P extends Record<string, infer V> ? Record<string, V>
|
|
55
|
+
: never
|
|
56
|
+
: undefined
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Extract query parameters from operation
|
|
60
|
+
*
|
|
61
|
+
* @pure true - compile-time only
|
|
62
|
+
* @invariant Returns query params type or undefined if none
|
|
63
|
+
*/
|
|
64
|
+
export type QueryParamsFor<Op> = Op extends { parameters: { query?: infer Q } } ? Q
|
|
65
|
+
: undefined
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Extract request body type from operation
|
|
69
|
+
*
|
|
70
|
+
* @pure true - compile-time only
|
|
71
|
+
* @invariant Returns body type or undefined if no requestBody
|
|
72
|
+
*/
|
|
73
|
+
export type RequestBodyFor<Op> = Op extends { requestBody: { content: infer C } }
|
|
74
|
+
? C extends { "application/json": infer J } ? J
|
|
75
|
+
: C extends { [key: string]: infer V } ? V
|
|
76
|
+
: never
|
|
77
|
+
: undefined
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Check if path params are required
|
|
81
|
+
*
|
|
82
|
+
* @pure true - compile-time only
|
|
83
|
+
*/
|
|
84
|
+
|
|
85
|
+
export type HasRequiredPathParams<Op> = Op extends { parameters: { path: infer P } }
|
|
86
|
+
? P extends Record<PropertyKey, string | number | boolean> ? keyof P extends never ? false : true
|
|
87
|
+
: false
|
|
88
|
+
: false
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Check if request body is required
|
|
92
|
+
*
|
|
93
|
+
* @pure true - compile-time only
|
|
94
|
+
*/
|
|
95
|
+
export type HasRequiredBody<Op> = Op extends { requestBody: infer RB } ? RB extends { content: object } ? true
|
|
96
|
+
: false
|
|
97
|
+
: false
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Build request options type from operation with all constraints
|
|
101
|
+
* - params: required if path has required parameters
|
|
102
|
+
* - query: optional, typed from operation
|
|
103
|
+
* - body: required if operation has requestBody (accepts typed object OR string)
|
|
104
|
+
*
|
|
105
|
+
* For request body:
|
|
106
|
+
* - Users can pass either the typed object (preferred, for type safety)
|
|
107
|
+
* - Or a pre-stringified JSON string with headers (for backwards compatibility)
|
|
108
|
+
*
|
|
109
|
+
* @pure true - compile-time only
|
|
110
|
+
* @invariant Options type is fully derived from operation definition
|
|
111
|
+
*/
|
|
112
|
+
export type RequestOptionsFor<Op> =
|
|
113
|
+
& (HasRequiredPathParams<Op> extends true ? { readonly params: PathParamsFor<Op> }
|
|
114
|
+
: { readonly params?: PathParamsFor<Op> })
|
|
115
|
+
& (HasRequiredBody<Op> extends true ? { readonly body: RequestBodyFor<Op> | BodyInit }
|
|
116
|
+
: { readonly body?: RequestBodyFor<Op> | BodyInit })
|
|
117
|
+
& { readonly query?: QueryParamsFor<Op> }
|
|
118
|
+
& { readonly headers?: HeadersInit }
|
|
119
|
+
& { readonly signal?: AbortSignal }
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Extract status codes from responses
|
|
123
|
+
*
|
|
124
|
+
* @pure true - compile-time only
|
|
125
|
+
* @invariant Result = { s | s ∈ keys(Responses) }
|
|
126
|
+
*/
|
|
127
|
+
export type StatusCodes<Responses> = keyof Responses & (number | string)
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Extract content types for a specific status code
|
|
131
|
+
*
|
|
132
|
+
* @pure true - compile-time only
|
|
133
|
+
*/
|
|
134
|
+
export type ContentTypesFor<
|
|
135
|
+
Responses,
|
|
136
|
+
Status extends StatusCodes<Responses>
|
|
137
|
+
> = Status extends keyof Responses ? Responses[Status] extends { content: infer C } ? keyof C & string
|
|
138
|
+
: "none"
|
|
139
|
+
: never
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Extract body type for a specific status and content-type
|
|
143
|
+
*
|
|
144
|
+
* @pure true - compile-time only
|
|
145
|
+
* @invariant Strict correlation: Body type depends on both status and content-type
|
|
146
|
+
*/
|
|
147
|
+
export type BodyFor<
|
|
148
|
+
Responses,
|
|
149
|
+
Status extends StatusCodes<Responses>,
|
|
150
|
+
ContentType extends ContentTypesFor<Responses, Status>
|
|
151
|
+
> = Status extends keyof Responses
|
|
152
|
+
? Responses[Status] extends { content: infer C } ? ContentType extends keyof C ? C[ContentType]
|
|
153
|
+
: never
|
|
154
|
+
: ContentType extends "none" ? undefined
|
|
155
|
+
: never
|
|
156
|
+
: never
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Build a correlated success response variant (status + contentType + body)
|
|
160
|
+
* Used for 2xx responses that go to the success channel.
|
|
161
|
+
*
|
|
162
|
+
* @pure true - compile-time only
|
|
163
|
+
* @invariant ∀ variant: variant.body = BodyFor<Responses, variant.status, variant.contentType>
|
|
164
|
+
*/
|
|
165
|
+
export type ResponseVariant<
|
|
166
|
+
Responses,
|
|
167
|
+
Status extends StatusCodes<Responses>,
|
|
168
|
+
ContentType extends ContentTypesFor<Responses, Status>
|
|
169
|
+
> = {
|
|
170
|
+
readonly status: Status
|
|
171
|
+
readonly contentType: ContentType
|
|
172
|
+
readonly body: BodyFor<Responses, Status, ContentType>
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Build a correlated HTTP error response variant (status + contentType + body + _tag)
|
|
177
|
+
* Used for non-2xx responses (4xx, 5xx) that go to the error channel.
|
|
178
|
+
*
|
|
179
|
+
* The `_tag: "HttpError"` discriminator allows distinguishing HTTP errors from BoundaryErrors.
|
|
180
|
+
*
|
|
181
|
+
* @pure true - compile-time only
|
|
182
|
+
* @invariant ∀ variant: variant.body = BodyFor<Responses, variant.status, variant.contentType>
|
|
183
|
+
*/
|
|
184
|
+
export type HttpErrorResponseVariant<
|
|
185
|
+
Responses,
|
|
186
|
+
Status extends StatusCodes<Responses>,
|
|
187
|
+
ContentType extends ContentTypesFor<Responses, Status>
|
|
188
|
+
> = {
|
|
189
|
+
readonly _tag: "HttpError"
|
|
190
|
+
readonly status: Status
|
|
191
|
+
readonly contentType: ContentType
|
|
192
|
+
readonly body: BodyFor<Responses, Status, ContentType>
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Build all response variants for given responses
|
|
197
|
+
*
|
|
198
|
+
* @pure true - compile-time only
|
|
199
|
+
*/
|
|
200
|
+
type AllResponseVariants<Responses> = StatusCodes<Responses> extends infer Status
|
|
201
|
+
? Status extends StatusCodes<Responses>
|
|
202
|
+
? ContentTypesFor<Responses, Status> extends infer CT
|
|
203
|
+
? CT extends ContentTypesFor<Responses, Status> ? ResponseVariant<Responses, Status, CT>
|
|
204
|
+
: never
|
|
205
|
+
: never
|
|
206
|
+
: never
|
|
207
|
+
: never
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Generic 2xx status detection without hardcoding
|
|
211
|
+
* Uses template literal type to check if status string starts with "2"
|
|
212
|
+
*
|
|
213
|
+
* Works with any 2xx status including non-standard ones like 250.
|
|
214
|
+
*
|
|
215
|
+
* @pure true - compile-time only
|
|
216
|
+
* @invariant Is2xx<S> = true ⟺ 200 ≤ S < 300
|
|
217
|
+
*/
|
|
218
|
+
export type Is2xx<S extends string | number> = `${S}` extends `2${string}` ? true : false
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Filter response variants to success statuses (2xx)
|
|
222
|
+
* Uses generic Is2xx instead of hardcoded status list.
|
|
223
|
+
*
|
|
224
|
+
* @pure true - compile-time only
|
|
225
|
+
* @invariant ∀ v ∈ SuccessVariants: Is2xx<v.status> = true
|
|
226
|
+
*/
|
|
227
|
+
export type SuccessVariants<Responses> = AllResponseVariants<Responses> extends infer V
|
|
228
|
+
? V extends ResponseVariant<Responses, infer S, infer CT> ? Is2xx<S> extends true ? ResponseVariant<Responses, S, CT>
|
|
229
|
+
: never
|
|
230
|
+
: never
|
|
231
|
+
: never
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Filter response variants to error statuses (non-2xx from schema)
|
|
235
|
+
* Returns HttpErrorResponseVariant with `_tag: "HttpError"` for discrimination.
|
|
236
|
+
* Uses generic Is2xx instead of hardcoded status list.
|
|
237
|
+
*
|
|
238
|
+
* @pure true - compile-time only
|
|
239
|
+
* @invariant ∀ v ∈ HttpErrorVariants: Is2xx<v.status> = false ∧ v.status ∈ Schema ∧ v._tag = "HttpError"
|
|
240
|
+
*/
|
|
241
|
+
export type HttpErrorVariants<Responses> = AllResponseVariants<Responses> extends infer V
|
|
242
|
+
? V extends ResponseVariant<Responses, infer S, infer CT> ? Is2xx<S> extends true ? never
|
|
243
|
+
: HttpErrorResponseVariant<Responses, S, CT>
|
|
244
|
+
: never
|
|
245
|
+
: never
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Boundary errors - always present regardless of schema
|
|
249
|
+
*
|
|
250
|
+
* @pure true - compile-time only
|
|
251
|
+
* @invariant These errors represent protocol/parsing failures, not business logic
|
|
252
|
+
*/
|
|
253
|
+
export type TransportError = {
|
|
254
|
+
readonly _tag: "TransportError"
|
|
255
|
+
readonly error: Error
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export type UnexpectedStatus = {
|
|
259
|
+
readonly _tag: "UnexpectedStatus"
|
|
260
|
+
readonly status: number
|
|
261
|
+
readonly body: string
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export type UnexpectedContentType = {
|
|
265
|
+
readonly _tag: "UnexpectedContentType"
|
|
266
|
+
readonly status: number
|
|
267
|
+
readonly expected: ReadonlyArray<string>
|
|
268
|
+
readonly actual: string | undefined
|
|
269
|
+
readonly body: string
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export type ParseError = {
|
|
273
|
+
readonly _tag: "ParseError"
|
|
274
|
+
readonly status: number
|
|
275
|
+
readonly contentType: string
|
|
276
|
+
readonly error: Error
|
|
277
|
+
readonly body: string
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export type DecodeError = {
|
|
281
|
+
readonly _tag: "DecodeError"
|
|
282
|
+
readonly status: number
|
|
283
|
+
readonly contentType: string
|
|
284
|
+
readonly error: Error
|
|
285
|
+
readonly body: string
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export type BoundaryError =
|
|
289
|
+
| TransportError
|
|
290
|
+
| UnexpectedStatus
|
|
291
|
+
| UnexpectedContentType
|
|
292
|
+
| ParseError
|
|
293
|
+
| DecodeError
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Success type for an operation (2xx statuses only)
|
|
297
|
+
*
|
|
298
|
+
* Goes to the **success channel** of Effect.
|
|
299
|
+
* Developers receive this directly without needing to handle errors.
|
|
300
|
+
*
|
|
301
|
+
* @pure true - compile-time only
|
|
302
|
+
* @invariant ∀ v ∈ ApiSuccess: v.status ∈ [200..299]
|
|
303
|
+
*/
|
|
304
|
+
export type ApiSuccess<Responses> = SuccessVariants<Responses>
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* HTTP error responses from schema (non-2xx statuses like 400, 404, 500)
|
|
308
|
+
*
|
|
309
|
+
* Goes to the **error channel** of Effect, forcing explicit handling.
|
|
310
|
+
* These are business-level errors defined in the OpenAPI schema.
|
|
311
|
+
*
|
|
312
|
+
* @pure true - compile-time only
|
|
313
|
+
* @invariant ∀ v ∈ HttpError: v.status ∉ [200..299] ∧ v.status ∈ Schema
|
|
314
|
+
*/
|
|
315
|
+
export type HttpError<Responses> = HttpErrorVariants<Responses>
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Complete failure type for API operations
|
|
319
|
+
*
|
|
320
|
+
* Includes both schema-defined HTTP errors (4xx, 5xx) and boundary errors.
|
|
321
|
+
* All failures go to the **error channel** of Effect, forcing explicit handling.
|
|
322
|
+
*
|
|
323
|
+
* @pure true - compile-time only
|
|
324
|
+
* @invariant ApiFailure = HttpError ⊎ BoundaryError
|
|
325
|
+
*
|
|
326
|
+
* BREAKING CHANGE: Previously, HTTP errors (404, 500) were in success channel.
|
|
327
|
+
* Now they are in error channel, requiring explicit handling with Effect.catchTag
|
|
328
|
+
* or Effect.match pattern.
|
|
329
|
+
*/
|
|
330
|
+
export type ApiFailure<Responses> = HttpError<Responses> | BoundaryError
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* @deprecated Use ApiSuccess<Responses> for success channel
|
|
334
|
+
* and ApiFailure<Responses> for error channel instead.
|
|
335
|
+
*
|
|
336
|
+
* ApiResponse mixed success and error statuses in one type.
|
|
337
|
+
* New API separates them into proper Effect channels.
|
|
338
|
+
*/
|
|
339
|
+
export type ApiResponse<Responses> = SuccessVariants<Responses> | HttpErrorVariants<Responses>
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Helper to ensure exhaustive pattern matching
|
|
343
|
+
*
|
|
344
|
+
* @pure true
|
|
345
|
+
* @throws Compile-time error if called with non-never type
|
|
346
|
+
*/
|
|
347
|
+
export const assertNever = (x: never): never => {
|
|
348
|
+
throw new Error(`Unexpected value: ${JSON.stringify(x)}`)
|
|
349
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// CHANGE: Create axioms module for type-safe cast operations
|
|
2
|
+
// WHY: Centralize all type assertions in a single auditable location per CLAUDE.md
|
|
3
|
+
// QUOTE(ТЗ): "as: запрещён в обычном коде; допускается ТОЛЬКО в одном аксиоматическом модуле"
|
|
4
|
+
// REF: issue-2, section 3.1
|
|
5
|
+
// SOURCE: n/a
|
|
6
|
+
// FORMAT THEOREM: ∀ cast ∈ Axioms: cast(x) → typed(x) ∨ runtime_validated(x)
|
|
7
|
+
// PURITY: CORE
|
|
8
|
+
// EFFECT: none - pure type-level operations
|
|
9
|
+
// INVARIANT: All casts auditable in single file
|
|
10
|
+
// COMPLEXITY: O(1)
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* JSON value type - result of JSON.parse()
|
|
14
|
+
* This is the fundamental type for all parsed JSON values
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Cast function for dispatcher factory
|
|
18
|
+
* AXIOM: Dispatcher factory receives valid classify function
|
|
19
|
+
*
|
|
20
|
+
* This enables generated dispatchers to work with heterogeneous Effect unions.
|
|
21
|
+
* The cast is safe because:
|
|
22
|
+
* 1. The classify function is generated from OpenAPI schema
|
|
23
|
+
* 2. All status/content-type combinations are exhaustively covered
|
|
24
|
+
* 3. The returned Effect conforms to Dispatcher signature
|
|
25
|
+
*
|
|
26
|
+
* @pure true
|
|
27
|
+
*/
|
|
28
|
+
import type { Effect } from "effect"
|
|
29
|
+
import type { ApiFailure, ApiSuccess, TransportError } from "./api-client/strict-types.js"
|
|
30
|
+
|
|
31
|
+
export type Json =
|
|
32
|
+
| null
|
|
33
|
+
| boolean
|
|
34
|
+
| number
|
|
35
|
+
| string
|
|
36
|
+
| ReadonlyArray<Json>
|
|
37
|
+
| { readonly [k: string]: Json }
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Cast parsed JSON value to typed Json
|
|
41
|
+
* AXIOM: JSON.parse returns a valid Json value
|
|
42
|
+
*
|
|
43
|
+
* @precondition value is result of JSON.parse on valid JSON string
|
|
44
|
+
* @postcondition result conforms to Json type
|
|
45
|
+
* @pure true
|
|
46
|
+
*/
|
|
47
|
+
export const asJson = (value: unknown): Json => value as Json
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Cast a value to a specific type with const assertion
|
|
51
|
+
* Used for creating literal typed objects in generated code
|
|
52
|
+
*
|
|
53
|
+
* @pure true
|
|
54
|
+
*/
|
|
55
|
+
export const asConst = <T>(value: T): T => value
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Create a typed RawResponse from raw values
|
|
59
|
+
* AXIOM: HTTP response structure is known at runtime
|
|
60
|
+
*
|
|
61
|
+
* @pure true
|
|
62
|
+
*/
|
|
63
|
+
export type RawResponse = {
|
|
64
|
+
readonly status: number
|
|
65
|
+
readonly headers: Headers
|
|
66
|
+
readonly text: string
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const asRawResponse = (value: {
|
|
70
|
+
status: number
|
|
71
|
+
headers: Headers
|
|
72
|
+
text: string
|
|
73
|
+
}): RawResponse => value as RawResponse
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Dispatcher classifies response and applies decoder
|
|
77
|
+
*
|
|
78
|
+
* NEW DESIGN (Effect-native):
|
|
79
|
+
* - Success channel: `ApiSuccess<Responses>` (2xx responses only)
|
|
80
|
+
* - Error channel: `ApiFailure<Responses>` (non-2xx schema errors + boundary errors)
|
|
81
|
+
*
|
|
82
|
+
* This forces developers to explicitly handle HTTP errors (404, 500, etc.)
|
|
83
|
+
* using Effect.catchTag, Effect.match, or similar patterns.
|
|
84
|
+
*
|
|
85
|
+
* @pure false - applies decoders
|
|
86
|
+
* @effect Effect<ApiSuccess, HttpError | BoundaryError, never>
|
|
87
|
+
* @invariant Must handle all statuses and content-types from schema
|
|
88
|
+
*/
|
|
89
|
+
export type Dispatcher<Responses> = (
|
|
90
|
+
response: RawResponse
|
|
91
|
+
) => Effect.Effect<
|
|
92
|
+
ApiSuccess<Responses>,
|
|
93
|
+
Exclude<ApiFailure<Responses>, TransportError>
|
|
94
|
+
>
|
|
95
|
+
|
|
96
|
+
export const asDispatcher = <Responses>(
|
|
97
|
+
fn: (response: RawResponse) => Effect.Effect<unknown, unknown>
|
|
98
|
+
): Dispatcher<Responses> => fn as Dispatcher<Responses>
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Cast for StrictRequestInit config object
|
|
102
|
+
* AXIOM: Config object has correct structure when all properties assigned
|
|
103
|
+
*
|
|
104
|
+
* @pure true
|
|
105
|
+
*/
|
|
106
|
+
export const asStrictRequestInit = <T>(config: object): T => config as T
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Classifier function type for dispatcher creation
|
|
110
|
+
* AXIOM: Classify function returns Effect with heterogeneous union types
|
|
111
|
+
*
|
|
112
|
+
* This type uses `unknown` to allow the classify function to return
|
|
113
|
+
* heterogeneous Effect unions from switch statements. The actual types
|
|
114
|
+
* are enforced by the generated dispatcher code.
|
|
115
|
+
*
|
|
116
|
+
* @pure true
|
|
117
|
+
*/
|
|
118
|
+
export type ClassifyFn = (
|
|
119
|
+
status: number,
|
|
120
|
+
contentType: string | undefined,
|
|
121
|
+
text: string
|
|
122
|
+
) => Effect.Effect<unknown, unknown>
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Cast internal client implementation to typed StrictApiClient
|
|
126
|
+
* AXIOM: Client implementation correctly implements all method constraints
|
|
127
|
+
*
|
|
128
|
+
* This cast is safe because:
|
|
129
|
+
* 1. StrictApiClient type enforces path/method constraints at call sites
|
|
130
|
+
* 2. The runtime implementation correctly builds requests for any path/method
|
|
131
|
+
* 3. Type checking happens at the call site, not in the implementation
|
|
132
|
+
*
|
|
133
|
+
* @pure true
|
|
134
|
+
*/
|
|
135
|
+
export const asStrictApiClient = <T>(client: object): T => client as T
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Cast default dispatchers registry to specific schema type
|
|
139
|
+
* AXIOM: Default dispatcher registry was registered for the current Paths type
|
|
140
|
+
*
|
|
141
|
+
* @pure true
|
|
142
|
+
*/
|
|
143
|
+
export const asDispatchersFor = <T>(value: unknown): T => value as T
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Match } from "effect"
|
|
2
|
+
|
|
3
|
+
export type GreetingVariant =
|
|
4
|
+
| { readonly kind: "effect" }
|
|
5
|
+
| { readonly kind: "named"; readonly name: string }
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Formats a greeting message without side effects.
|
|
9
|
+
*
|
|
10
|
+
* @param variant - Non-empty, classified name information.
|
|
11
|
+
* @returns Greeting text composed deterministically.
|
|
12
|
+
*
|
|
13
|
+
* @pure true
|
|
14
|
+
* @invariant variant.kind === "named" ⇒ variant.name.length > 0
|
|
15
|
+
* @complexity O(1) time / O(1) space
|
|
16
|
+
*/
|
|
17
|
+
export const formatGreeting = (variant: GreetingVariant): string =>
|
|
18
|
+
Match.value(variant).pipe(
|
|
19
|
+
Match.when({ kind: "effect" }, () => "Hello from Effect!"),
|
|
20
|
+
Match.when({ kind: "named" }, ({ name }) => `Hello, ${name}!`),
|
|
21
|
+
Match.exhaustive
|
|
22
|
+
)
|