@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,471 @@
|
|
|
1
|
+
// CHANGE: Implement Effect-based HTTP client with Effect-native error handling
|
|
2
|
+
// WHY: Force explicit handling of HTTP errors (4xx, 5xx) via Effect error channel
|
|
3
|
+
// QUOTE(ТЗ): "каждый запрос возвращает Effect<Success, Failure, never>; Failure включает все инварианты протокола и схемы"
|
|
4
|
+
// REF: issue-2, section 2, 4, 5.1
|
|
5
|
+
// SOURCE: n/a
|
|
6
|
+
// FORMAT THEOREM: ∀ req ∈ Requests: execute(req) → Effect<ApiSuccess, ApiFailure, R>
|
|
7
|
+
// PURITY: SHELL
|
|
8
|
+
// EFFECT: Effect<ApiSuccess<Op>, ApiFailure<Op>, HttpClient.HttpClient>
|
|
9
|
+
// INVARIANT: 2xx → success channel, non-2xx → error channel (forced handling)
|
|
10
|
+
// COMPLEXITY: O(1) per request / O(n) for body size
|
|
11
|
+
|
|
12
|
+
import * as HttpBody from "@effect/platform/HttpBody"
|
|
13
|
+
import * as HttpClient from "@effect/platform/HttpClient"
|
|
14
|
+
import * as HttpClientRequest from "@effect/platform/HttpClientRequest"
|
|
15
|
+
import { Effect } from "effect"
|
|
16
|
+
import type { HttpMethod } from "openapi-typescript-helpers"
|
|
17
|
+
|
|
18
|
+
import type {
|
|
19
|
+
ApiFailure,
|
|
20
|
+
ApiSuccess,
|
|
21
|
+
DecodeError,
|
|
22
|
+
OperationFor,
|
|
23
|
+
ParseError,
|
|
24
|
+
ResponsesFor,
|
|
25
|
+
TransportError,
|
|
26
|
+
UnexpectedContentType,
|
|
27
|
+
UnexpectedStatus
|
|
28
|
+
} from "../../core/api-client/strict-types.js"
|
|
29
|
+
import {
|
|
30
|
+
asDispatcher,
|
|
31
|
+
asJson,
|
|
32
|
+
asRawResponse,
|
|
33
|
+
asStrictRequestInit,
|
|
34
|
+
type ClassifyFn,
|
|
35
|
+
type Dispatcher,
|
|
36
|
+
type Json,
|
|
37
|
+
type RawResponse
|
|
38
|
+
} from "../../core/axioms.js"
|
|
39
|
+
|
|
40
|
+
// Re-export Dispatcher type for consumers
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Decoder for response body
|
|
44
|
+
*
|
|
45
|
+
* @pure false - may perform validation
|
|
46
|
+
* @effect Effect<T, DecodeError, never>
|
|
47
|
+
*/
|
|
48
|
+
export type Decoder<T> = (
|
|
49
|
+
status: number,
|
|
50
|
+
contentType: string,
|
|
51
|
+
body: string
|
|
52
|
+
) => Effect.Effect<T, DecodeError>
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Configuration for a strict API client request
|
|
56
|
+
*/
|
|
57
|
+
export type StrictRequestInit<Responses> = {
|
|
58
|
+
readonly method: HttpMethod
|
|
59
|
+
readonly url: string
|
|
60
|
+
readonly dispatcher: Dispatcher<Responses>
|
|
61
|
+
readonly headers?: HeadersInit
|
|
62
|
+
readonly body?: BodyInit
|
|
63
|
+
readonly signal?: AbortSignal
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Execute HTTP request with Effect-native error handling
|
|
68
|
+
*
|
|
69
|
+
* @param config - Request configuration with dispatcher
|
|
70
|
+
* @returns Effect with success (2xx) and failures (non-2xx + boundary errors)
|
|
71
|
+
*
|
|
72
|
+
* **Effect Channel Design:**
|
|
73
|
+
* - Success channel: `ApiSuccess<Responses>` - 2xx responses only
|
|
74
|
+
* - Error channel: `ApiFailure<Responses>` - HTTP errors (4xx, 5xx) + boundary errors
|
|
75
|
+
*
|
|
76
|
+
* This forces developers to explicitly handle HTTP errors using:
|
|
77
|
+
* - `Effect.catchTag` for specific error types
|
|
78
|
+
* - `Effect.match` for exhaustive handling
|
|
79
|
+
* - `Effect.catchAll` for generic error handling
|
|
80
|
+
*
|
|
81
|
+
* @pure false - performs HTTP request
|
|
82
|
+
* @effect Effect<ApiSuccess<Responses>, ApiFailure<Responses>, HttpClient.HttpClient>
|
|
83
|
+
* @invariant 2xx → success channel, non-2xx → error channel
|
|
84
|
+
* @precondition config.dispatcher handles all schema statuses
|
|
85
|
+
* @postcondition ∀ response: success(2xx) ∨ httpError(non-2xx) ∨ boundaryError
|
|
86
|
+
* @complexity O(1) + O(|body|) for text extraction
|
|
87
|
+
*/
|
|
88
|
+
export const executeRequest = <Responses>(
|
|
89
|
+
config: StrictRequestInit<Responses>
|
|
90
|
+
): Effect.Effect<ApiSuccess<Responses>, ApiFailure<Responses>, HttpClient.HttpClient> =>
|
|
91
|
+
Effect.gen(function*() {
|
|
92
|
+
// STEP 1: Get HTTP client from context
|
|
93
|
+
const client = yield* HttpClient.HttpClient
|
|
94
|
+
|
|
95
|
+
// STEP 2: Build request based on method
|
|
96
|
+
const request = buildRequest(config)
|
|
97
|
+
|
|
98
|
+
// STEP 3: Execute request with error mapping
|
|
99
|
+
const rawResponse = yield* Effect.mapError(
|
|
100
|
+
Effect.gen(function*() {
|
|
101
|
+
const response = yield* client.execute(request)
|
|
102
|
+
const text = yield* response.text
|
|
103
|
+
return asRawResponse({
|
|
104
|
+
status: response.status,
|
|
105
|
+
headers: toNativeHeaders(response.headers),
|
|
106
|
+
text
|
|
107
|
+
})
|
|
108
|
+
}),
|
|
109
|
+
(error): TransportError => ({
|
|
110
|
+
_tag: "TransportError",
|
|
111
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
112
|
+
})
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
// STEP 4: Delegate classification to dispatcher (handles status/content-type/decode)
|
|
116
|
+
return yield* config.dispatcher(rawResponse)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Build HTTP request from config
|
|
121
|
+
*
|
|
122
|
+
* @pure true
|
|
123
|
+
*/
|
|
124
|
+
const buildRequest = <Responses>(config: StrictRequestInit<Responses>): HttpClientRequest.HttpClientRequest => {
|
|
125
|
+
const methodMap: Record<string, (url: string) => HttpClientRequest.HttpClientRequest> = {
|
|
126
|
+
get: HttpClientRequest.get,
|
|
127
|
+
post: HttpClientRequest.post,
|
|
128
|
+
put: HttpClientRequest.put,
|
|
129
|
+
patch: HttpClientRequest.patch,
|
|
130
|
+
delete: HttpClientRequest.del,
|
|
131
|
+
head: HttpClientRequest.head,
|
|
132
|
+
options: HttpClientRequest.options
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const createRequest = methodMap[config.method] ?? HttpClientRequest.get
|
|
136
|
+
let request = createRequest(config.url)
|
|
137
|
+
|
|
138
|
+
// Add headers if provided
|
|
139
|
+
if (config.headers !== undefined) {
|
|
140
|
+
const headers = toRecordHeaders(config.headers)
|
|
141
|
+
request = HttpClientRequest.setHeaders(request, headers)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Add body if provided
|
|
145
|
+
if (config.body !== undefined) {
|
|
146
|
+
const bodyText = typeof config.body === "string" ? config.body : JSON.stringify(config.body)
|
|
147
|
+
request = HttpClientRequest.setBody(request, HttpBody.text(bodyText))
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return request
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Convert Headers to Record<string, string>
|
|
155
|
+
*
|
|
156
|
+
* @pure true
|
|
157
|
+
*/
|
|
158
|
+
const toRecordHeaders = (headers: HeadersInit): Record<string, string> => {
|
|
159
|
+
if (headers instanceof Headers) {
|
|
160
|
+
const result: Record<string, string> = {}
|
|
161
|
+
for (const [key, value] of headers.entries()) {
|
|
162
|
+
result[key] = value
|
|
163
|
+
}
|
|
164
|
+
return result
|
|
165
|
+
}
|
|
166
|
+
if (Array.isArray(headers)) {
|
|
167
|
+
const result: Record<string, string> = {}
|
|
168
|
+
for (const headerPair of headers) {
|
|
169
|
+
const [headerKey, headerValue] = headerPair
|
|
170
|
+
result[headerKey] = headerValue
|
|
171
|
+
}
|
|
172
|
+
return result
|
|
173
|
+
}
|
|
174
|
+
return headers
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Convert @effect/platform Headers to native Headers
|
|
179
|
+
*
|
|
180
|
+
* @pure true
|
|
181
|
+
*/
|
|
182
|
+
const toNativeHeaders = (platformHeaders: { readonly [key: string]: string }): Headers => {
|
|
183
|
+
const headers = new Headers()
|
|
184
|
+
for (const [key, value] of Object.entries(platformHeaders)) {
|
|
185
|
+
headers.set(key, value)
|
|
186
|
+
}
|
|
187
|
+
return headers
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Helper to create dispatcher from switch-based classifier
|
|
192
|
+
*
|
|
193
|
+
* This function uses a permissive type signature to allow generated code
|
|
194
|
+
* to work with any response variant without requiring exact type matching.
|
|
195
|
+
* The classify function can return any Effect with union types for success/error.
|
|
196
|
+
*
|
|
197
|
+
* NOTE: Uses axioms module for type casts to allow heterogeneous Effect
|
|
198
|
+
* unions from switch statements. The returned Dispatcher is properly typed.
|
|
199
|
+
*
|
|
200
|
+
* @pure true - returns pure function
|
|
201
|
+
* @complexity O(1)
|
|
202
|
+
*/
|
|
203
|
+
|
|
204
|
+
export const createDispatcher = <Responses>(
|
|
205
|
+
classify: ClassifyFn
|
|
206
|
+
): Dispatcher<Responses> => {
|
|
207
|
+
return asDispatcher<Responses>((response: RawResponse) => {
|
|
208
|
+
const contentType = response.headers.get("content-type") ?? undefined
|
|
209
|
+
return classify(response.status, contentType, response.text)
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Helper to parse JSON with error handling
|
|
215
|
+
*
|
|
216
|
+
* @pure false - performs parsing
|
|
217
|
+
* @effect Effect<Json, ParseError, never>
|
|
218
|
+
*/
|
|
219
|
+
export const parseJSON = (
|
|
220
|
+
status: number,
|
|
221
|
+
contentType: string,
|
|
222
|
+
text: string
|
|
223
|
+
): Effect.Effect<Json, ParseError> =>
|
|
224
|
+
Effect.try({
|
|
225
|
+
try: () => asJson(JSON.parse(text)),
|
|
226
|
+
catch: (error): ParseError => ({
|
|
227
|
+
_tag: "ParseError",
|
|
228
|
+
status,
|
|
229
|
+
contentType,
|
|
230
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
231
|
+
body: text
|
|
232
|
+
})
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Helper to create UnexpectedStatus error
|
|
237
|
+
*
|
|
238
|
+
* @pure true
|
|
239
|
+
*/
|
|
240
|
+
export const unexpectedStatus = (status: number, body: string): UnexpectedStatus => ({
|
|
241
|
+
_tag: "UnexpectedStatus",
|
|
242
|
+
status,
|
|
243
|
+
body
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Helper to create UnexpectedContentType error
|
|
248
|
+
*
|
|
249
|
+
* @pure true
|
|
250
|
+
*/
|
|
251
|
+
export const unexpectedContentType = (
|
|
252
|
+
status: number,
|
|
253
|
+
expected: ReadonlyArray<string>,
|
|
254
|
+
actual: string | undefined,
|
|
255
|
+
body: string
|
|
256
|
+
): UnexpectedContentType => ({
|
|
257
|
+
_tag: "UnexpectedContentType",
|
|
258
|
+
status,
|
|
259
|
+
expected,
|
|
260
|
+
actual,
|
|
261
|
+
body
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Generic client interface for any OpenAPI schema with Effect-native error handling
|
|
266
|
+
*
|
|
267
|
+
* **Effect Channel Design:**
|
|
268
|
+
* - Success channel: `ApiSuccess<Op>` - 2xx responses
|
|
269
|
+
* - Error channel: `ApiFailure<Op>` - HTTP errors (4xx, 5xx) + boundary errors
|
|
270
|
+
*
|
|
271
|
+
* @pure false - performs HTTP requests
|
|
272
|
+
* @effect Effect<ApiSuccess<Op>, ApiFailure<Op>, HttpClient.HttpClient>
|
|
273
|
+
*/
|
|
274
|
+
export type StrictClient<Paths extends object> = {
|
|
275
|
+
readonly GET: <Path extends keyof Paths>(
|
|
276
|
+
path: Path,
|
|
277
|
+
options: RequestOptions<Paths, Path, "get">
|
|
278
|
+
) => Effect.Effect<
|
|
279
|
+
ApiSuccess<ResponsesFor<OperationFor<Paths, Path, "get">>>,
|
|
280
|
+
ApiFailure<ResponsesFor<OperationFor<Paths, Path, "get">>>,
|
|
281
|
+
HttpClient.HttpClient
|
|
282
|
+
>
|
|
283
|
+
|
|
284
|
+
readonly POST: <Path extends keyof Paths>(
|
|
285
|
+
path: Path,
|
|
286
|
+
options: RequestOptions<Paths, Path, "post">
|
|
287
|
+
) => Effect.Effect<
|
|
288
|
+
ApiSuccess<ResponsesFor<OperationFor<Paths, Path, "post">>>,
|
|
289
|
+
ApiFailure<ResponsesFor<OperationFor<Paths, Path, "post">>>,
|
|
290
|
+
HttpClient.HttpClient
|
|
291
|
+
>
|
|
292
|
+
|
|
293
|
+
readonly PUT: <Path extends keyof Paths>(
|
|
294
|
+
path: Path,
|
|
295
|
+
options: RequestOptions<Paths, Path, "put">
|
|
296
|
+
) => Effect.Effect<
|
|
297
|
+
ApiSuccess<ResponsesFor<OperationFor<Paths, Path, "put">>>,
|
|
298
|
+
ApiFailure<ResponsesFor<OperationFor<Paths, Path, "put">>>,
|
|
299
|
+
HttpClient.HttpClient
|
|
300
|
+
>
|
|
301
|
+
|
|
302
|
+
readonly PATCH: <Path extends keyof Paths>(
|
|
303
|
+
path: Path,
|
|
304
|
+
options: RequestOptions<Paths, Path, "patch">
|
|
305
|
+
) => Effect.Effect<
|
|
306
|
+
ApiSuccess<ResponsesFor<OperationFor<Paths, Path, "patch">>>,
|
|
307
|
+
ApiFailure<ResponsesFor<OperationFor<Paths, Path, "patch">>>,
|
|
308
|
+
HttpClient.HttpClient
|
|
309
|
+
>
|
|
310
|
+
|
|
311
|
+
readonly DELETE: <Path extends keyof Paths>(
|
|
312
|
+
path: Path,
|
|
313
|
+
options: RequestOptions<Paths, Path, "delete">
|
|
314
|
+
) => Effect.Effect<
|
|
315
|
+
ApiSuccess<ResponsesFor<OperationFor<Paths, Path, "delete">>>,
|
|
316
|
+
ApiFailure<ResponsesFor<OperationFor<Paths, Path, "delete">>>,
|
|
317
|
+
HttpClient.HttpClient
|
|
318
|
+
>
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Request options for a specific operation
|
|
323
|
+
*/
|
|
324
|
+
export type RequestOptions<
|
|
325
|
+
Paths extends object,
|
|
326
|
+
Path extends keyof Paths,
|
|
327
|
+
Method extends HttpMethod
|
|
328
|
+
> = {
|
|
329
|
+
readonly dispatcher: Dispatcher<ResponsesFor<OperationFor<Paths, Path, Method>>>
|
|
330
|
+
readonly baseUrl: string
|
|
331
|
+
readonly params?: Record<string, string | number>
|
|
332
|
+
readonly query?: Record<string, string | number>
|
|
333
|
+
readonly headers?: HeadersInit
|
|
334
|
+
readonly body?: BodyInit
|
|
335
|
+
readonly signal?: AbortSignal
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Create a strict client for an OpenAPI schema
|
|
340
|
+
*
|
|
341
|
+
* @pure true - returns pure client object
|
|
342
|
+
* @complexity O(1)
|
|
343
|
+
*/
|
|
344
|
+
export const createStrictClient = <Paths extends object>(): StrictClient<
|
|
345
|
+
Paths
|
|
346
|
+
> => {
|
|
347
|
+
const makeRequest = <Path extends keyof Paths, Method extends HttpMethod>(
|
|
348
|
+
method: Method,
|
|
349
|
+
path: Path,
|
|
350
|
+
options: RequestOptions<Paths, Path, Method>
|
|
351
|
+
) => {
|
|
352
|
+
let url = `${options.baseUrl}${String(path)}`
|
|
353
|
+
|
|
354
|
+
// Replace path parameters
|
|
355
|
+
if (options.params !== undefined) {
|
|
356
|
+
for (const [key, value] of Object.entries(options.params)) {
|
|
357
|
+
url = url.replace(`{${key}}`, encodeURIComponent(String(value)))
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Add query parameters
|
|
362
|
+
if (options.query !== undefined) {
|
|
363
|
+
const params = new URLSearchParams()
|
|
364
|
+
for (const [key, value] of Object.entries(options.query)) {
|
|
365
|
+
params.append(key, String(value))
|
|
366
|
+
}
|
|
367
|
+
url = `${url}?${params.toString()}`
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Build config object, only including optional properties if they are defined
|
|
371
|
+
// This satisfies exactOptionalPropertyTypes constraint
|
|
372
|
+
const config = asStrictRequestInit<StrictRequestInit<ResponsesFor<OperationFor<Paths, Path, Method>>>>({
|
|
373
|
+
method,
|
|
374
|
+
url,
|
|
375
|
+
dispatcher: options.dispatcher,
|
|
376
|
+
...(options.headers !== undefined && { headers: options.headers }),
|
|
377
|
+
...(options.body !== undefined && { body: options.body }),
|
|
378
|
+
...(options.signal !== undefined && { signal: options.signal })
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
return executeRequest(config)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return {
|
|
385
|
+
GET: (path, options) => makeRequest("get", path, options),
|
|
386
|
+
POST: (path, options) => makeRequest("post", path, options),
|
|
387
|
+
PUT: (path, options) => makeRequest("put", path, options),
|
|
388
|
+
PATCH: (path, options) => makeRequest("patch", path, options),
|
|
389
|
+
DELETE: (path, options) => makeRequest("delete", path, options)
|
|
390
|
+
} satisfies StrictClient<Paths>
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// CHANGE: Add universal dispatcher that handles any OpenAPI responses generically
|
|
394
|
+
// WHY: Enable createClient<Paths>(options) without code generation or manual dispatcher wiring
|
|
395
|
+
// QUOTE(ТЗ): "Я не хочу создавать какие-то дополнительные модули"
|
|
396
|
+
// REF: issue-5
|
|
397
|
+
// SOURCE: n/a
|
|
398
|
+
// FORMAT THEOREM: ∀ status, ct: universalDispatcher(status, ct, text) → success(2xx) ∨ httpError(non-2xx) ∨ boundaryError
|
|
399
|
+
// PURITY: SHELL
|
|
400
|
+
// EFFECT: Effect<ApiSuccess<Responses>, Exclude<ApiFailure<Responses>, TransportError>, never>
|
|
401
|
+
// INVARIANT: 2xx → success channel, non-2xx → error channel, no-content → body: undefined
|
|
402
|
+
// COMPLEXITY: O(1) per dispatch + O(|text|) for JSON parsing
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Create a universal dispatcher that handles any OpenAPI response generically
|
|
406
|
+
*
|
|
407
|
+
* The universal dispatcher classifies responses by status code range:
|
|
408
|
+
* - 2xx → success channel (ApiSuccess)
|
|
409
|
+
* - non-2xx → error channel (HttpError)
|
|
410
|
+
*
|
|
411
|
+
* For JSON content types, it parses the body. For no-content responses (empty body),
|
|
412
|
+
* it returns undefined body with contentType "none".
|
|
413
|
+
*
|
|
414
|
+
* This enables using createClient<Paths>(options) without generating
|
|
415
|
+
* per-operation dispatchers, fulfilling the zero-boilerplate DSL requirement.
|
|
416
|
+
*
|
|
417
|
+
* @pure true - returns pure dispatcher function
|
|
418
|
+
* @complexity O(1) creation + O(|body|) per dispatch
|
|
419
|
+
*/
|
|
420
|
+
export const createUniversalDispatcher = <Responses>(): Dispatcher<Responses> => {
|
|
421
|
+
return asDispatcher<Responses>((response: RawResponse) => {
|
|
422
|
+
const contentType = response.headers.get("content-type") ?? undefined
|
|
423
|
+
const is2xx = response.status >= 200 && response.status < 300
|
|
424
|
+
|
|
425
|
+
// No-content response (empty body or 204)
|
|
426
|
+
if (response.text === "" || response.status === 204) {
|
|
427
|
+
const variant = {
|
|
428
|
+
status: response.status,
|
|
429
|
+
contentType: "none" as const,
|
|
430
|
+
body: undefined
|
|
431
|
+
} as const
|
|
432
|
+
|
|
433
|
+
return is2xx
|
|
434
|
+
? Effect.succeed(variant)
|
|
435
|
+
: Effect.fail({
|
|
436
|
+
_tag: "HttpError" as const,
|
|
437
|
+
...variant
|
|
438
|
+
})
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// JSON content type
|
|
442
|
+
if (contentType?.includes("application/json")) {
|
|
443
|
+
return Effect.gen(function*() {
|
|
444
|
+
const parsed = yield* parseJSON(response.status, "application/json", response.text)
|
|
445
|
+
const variant = {
|
|
446
|
+
status: response.status,
|
|
447
|
+
contentType: "application/json" as const,
|
|
448
|
+
body: parsed
|
|
449
|
+
} as const
|
|
450
|
+
|
|
451
|
+
if (is2xx) {
|
|
452
|
+
return variant
|
|
453
|
+
}
|
|
454
|
+
return yield* Effect.fail({
|
|
455
|
+
_tag: "HttpError" as const,
|
|
456
|
+
...variant
|
|
457
|
+
})
|
|
458
|
+
})
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Unknown content type
|
|
462
|
+
return Effect.fail(unexpectedContentType(
|
|
463
|
+
response.status,
|
|
464
|
+
["application/json"],
|
|
465
|
+
contentType,
|
|
466
|
+
response.text
|
|
467
|
+
))
|
|
468
|
+
})
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
export { type Dispatcher, type RawResponse } from "../../core/axioms.js"
|
package/src/shell/cli.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as S from "@effect/schema/Schema"
|
|
2
|
+
import { Effect, pipe } from "effect"
|
|
3
|
+
|
|
4
|
+
import type { GreetingVariant } from "../core/greeting.js"
|
|
5
|
+
|
|
6
|
+
const cliSchema = S.Struct({
|
|
7
|
+
name: S.optionalWith(S.NonEmptyString, { default: () => "Effect" })
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
type CliInput = S.Schema.Type<typeof cliSchema>
|
|
11
|
+
|
|
12
|
+
const toVariant = (input: CliInput): GreetingVariant =>
|
|
13
|
+
input.name.toLowerCase() === "effect"
|
|
14
|
+
? { kind: "effect" }
|
|
15
|
+
: { kind: "named", name: input.name }
|
|
16
|
+
|
|
17
|
+
export const readGreetingVariant = pipe(
|
|
18
|
+
Effect.sync(() => process.argv.slice(2)),
|
|
19
|
+
Effect.map((args) => args.length > 0 && args[0] !== undefined ? { name: args[0] } : {}),
|
|
20
|
+
Effect.flatMap(S.decodeUnknown(cliSchema)),
|
|
21
|
+
Effect.map((input) => toVariant(input))
|
|
22
|
+
)
|