@prover-coder-ai/openapi-effect 1.0.19 → 1.0.21
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 +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +3 -4
- package/src/index.ts +16 -11
- package/src/shell/api-client/create-client-middleware.ts +179 -0
- package/src/shell/api-client/create-client-response.ts +115 -0
- package/src/shell/api-client/create-client-runtime-helpers.ts +142 -0
- package/src/shell/api-client/create-client-runtime-types.ts +97 -0
- package/src/shell/api-client/create-client-runtime.ts +316 -0
- package/src/shell/api-client/create-client-types.ts +278 -286
- package/src/shell/api-client/create-client.ts +89 -496
- package/src/shell/api-client/index.ts +28 -1
- package/src/shell/api-client/openapi-compat-path.ts +82 -0
- package/src/shell/api-client/openapi-compat-request.ts +153 -0
- package/src/shell/api-client/openapi-compat-serializers.ts +277 -0
- package/src/shell/api-client/openapi-compat-utils.ts +10 -0
- package/src/shell/api-client/openapi-compat-value-guards.ts +9 -0
|
@@ -1,517 +1,110 @@
|
|
|
1
|
-
|
|
2
|
-
// WHY: Ensure path/method → operation → request types are all linked
|
|
3
|
-
// QUOTE(ТЗ): "path + method определяют operation, и из неё выводятся request/response types"
|
|
4
|
-
// REF: PR#3 blocking review sections 3.2, 3.3
|
|
5
|
-
// SOURCE: n/a
|
|
6
|
-
// PURITY: SHELL
|
|
7
|
-
// EFFECT: Creates Effect-based API client
|
|
8
|
-
// INVARIANT: All operations are type-safe from path → operation → request → response
|
|
9
|
-
// COMPLEXITY: O(1) client creation
|
|
1
|
+
import type { MediaType } from "openapi-typescript-helpers"
|
|
10
2
|
|
|
11
|
-
import
|
|
12
|
-
import {
|
|
13
|
-
import
|
|
14
|
-
|
|
15
|
-
import { asDispatchersFor, asStrictApiClient, asStrictRequestInit, type Dispatcher } from "../../core/axioms.js"
|
|
16
|
-
import type {
|
|
17
|
-
ClientEffect,
|
|
18
|
-
ClientOptions,
|
|
19
|
-
DispatchersFor,
|
|
20
|
-
DispatchersForMethod,
|
|
21
|
-
StrictApiClientWithDispatchers
|
|
22
|
-
} from "./create-client-types.js"
|
|
23
|
-
import type { StrictRequestInit } from "./strict-client.js"
|
|
24
|
-
import { createUniversalDispatcher, executeRequest } from "./strict-client.js"
|
|
3
|
+
import { asStrictApiClient } from "../../core/axioms.js"
|
|
4
|
+
import type { RuntimeClient, RuntimeFetchOptions } from "./create-client-runtime-types.js"
|
|
5
|
+
import { createRuntimeClient } from "./create-client-runtime.js"
|
|
6
|
+
import type { Client, ClientEffect, ClientOptions, DispatchersFor, PathBasedClient } from "./create-client-types.js"
|
|
25
7
|
|
|
26
8
|
export type {
|
|
9
|
+
Client,
|
|
27
10
|
ClientEffect,
|
|
11
|
+
ClientForPath,
|
|
12
|
+
ClientMethod,
|
|
28
13
|
ClientOptions,
|
|
14
|
+
ClientPathsWithMethod,
|
|
15
|
+
ClientRequestMethod,
|
|
29
16
|
DispatchersFor,
|
|
17
|
+
FetchOptions,
|
|
18
|
+
FetchResponse,
|
|
19
|
+
HeadersOptions,
|
|
20
|
+
MethodResponse,
|
|
21
|
+
Middleware,
|
|
22
|
+
MiddlewareCallbackParams,
|
|
23
|
+
ParseAs,
|
|
24
|
+
PathBasedClient,
|
|
25
|
+
QuerySerializer,
|
|
26
|
+
QuerySerializerOptions,
|
|
27
|
+
RequestBodyOption,
|
|
28
|
+
RequestOptions,
|
|
30
29
|
StrictApiClient,
|
|
31
30
|
StrictApiClientWithDispatchers
|
|
32
31
|
} from "./create-client-types.js"
|
|
33
|
-
export { createUniversalDispatcher } from "./strict-client.js"
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Primitive value type for path/query parameters
|
|
37
|
-
*
|
|
38
|
-
* @pure true - type alias only
|
|
39
|
-
*/
|
|
40
|
-
type ParamValue = string | number | boolean
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Query parameter value - can be primitive or array of primitives
|
|
44
|
-
*
|
|
45
|
-
* @pure true - type alias only
|
|
46
|
-
*/
|
|
47
|
-
type QueryValue = ParamValue | ReadonlyArray<ParamValue>
|
|
48
|
-
|
|
49
|
-
// CHANGE: Add default dispatcher registry for auto-dispatching createClient
|
|
50
|
-
// WHY: Allow createClient(options) without explicitly passing dispatcher map
|
|
51
|
-
// QUOTE(ТЗ): "const apiClient = createClient<Paths>(clientOptions)"
|
|
52
|
-
// REF: user-msg-4
|
|
53
|
-
// SOURCE: n/a
|
|
54
|
-
// FORMAT THEOREM: ∀ call: defaultDispatchers = dispatchersByPath ⇒ createClient uses dispatcher(path, method)
|
|
55
|
-
// PURITY: SHELL
|
|
56
|
-
// EFFECT: none
|
|
57
|
-
// INVARIANT: defaultDispatchers is set before createClient use
|
|
58
|
-
// COMPLEXITY: O(1)
|
|
59
|
-
let defaultDispatchers: DispatchersFor<object> | undefined
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Register default dispatcher map used by createClient(options)
|
|
63
|
-
*
|
|
64
|
-
* @pure false - mutates module-level registry
|
|
65
|
-
* @invariant defaultDispatchers set exactly once per app boot
|
|
66
|
-
*/
|
|
67
|
-
export const registerDefaultDispatchers = <Paths extends object>(
|
|
68
|
-
dispatchers: DispatchersFor<Paths>
|
|
69
|
-
): void => {
|
|
70
|
-
defaultDispatchers = dispatchers
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Resolve default dispatcher map or fail fast
|
|
75
|
-
*
|
|
76
|
-
* @pure false - reads module-level registry
|
|
77
|
-
* @invariant defaultDispatchers must be set for auto-dispatching client
|
|
78
|
-
*/
|
|
79
|
-
const resolveDefaultDispatchers = <Paths extends object>(): DispatchersFor<Paths> => {
|
|
80
|
-
if (defaultDispatchers === undefined) {
|
|
81
|
-
throw new Error("Default dispatchers are not registered. Import generated dispatchers module.")
|
|
82
|
-
}
|
|
83
|
-
return asDispatchersFor<DispatchersFor<Paths>>(defaultDispatchers)
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const applyPathParams = (path: string, params?: Record<string, ParamValue>): string => {
|
|
87
|
-
if (params === undefined) {
|
|
88
|
-
return path
|
|
89
|
-
}
|
|
90
32
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
33
|
+
export {
|
|
34
|
+
createFinalURL,
|
|
35
|
+
createQuerySerializer,
|
|
36
|
+
defaultBodySerializer,
|
|
37
|
+
defaultPathSerializer,
|
|
38
|
+
mergeHeaders,
|
|
39
|
+
removeTrailingSlash,
|
|
40
|
+
serializeArrayParam,
|
|
41
|
+
serializeObjectParam,
|
|
42
|
+
serializePrimitiveParam
|
|
43
|
+
} from "./openapi-compat-utils.js"
|
|
44
|
+
|
|
45
|
+
export const createClient = <Paths extends object, Media extends MediaType = MediaType>(
|
|
46
|
+
clientOptions?: ClientOptions
|
|
47
|
+
): Client<Paths, Media> => asStrictApiClient<Client<Paths, Media>>(createRuntimeClient(clientOptions))
|
|
48
|
+
|
|
49
|
+
class PathCallForwarder {
|
|
50
|
+
constructor(
|
|
51
|
+
private readonly client: RuntimeClient,
|
|
52
|
+
private readonly url: string
|
|
53
|
+
) {}
|
|
54
|
+
|
|
55
|
+
private readonly call = (
|
|
56
|
+
method: "GET" | "PUT" | "POST" | "DELETE" | "OPTIONS" | "HEAD" | "PATCH" | "TRACE",
|
|
57
|
+
init?: RuntimeFetchOptions
|
|
58
|
+
) => this.client[method](this.url, init)
|
|
59
|
+
|
|
60
|
+
public readonly GET = (init?: RuntimeFetchOptions) => this.call("GET", init)
|
|
61
|
+
public readonly PUT = (init?: RuntimeFetchOptions) => this.call("PUT", init)
|
|
62
|
+
public readonly POST = (init?: RuntimeFetchOptions) => this.call("POST", init)
|
|
63
|
+
public readonly DELETE = (init?: RuntimeFetchOptions) => this.call("DELETE", init)
|
|
64
|
+
public readonly OPTIONS = (init?: RuntimeFetchOptions) => this.call("OPTIONS", init)
|
|
65
|
+
public readonly HEAD = (init?: RuntimeFetchOptions) => this.call("HEAD", init)
|
|
66
|
+
public readonly PATCH = (init?: RuntimeFetchOptions) => this.call("PATCH", init)
|
|
67
|
+
public readonly TRACE = (init?: RuntimeFetchOptions) => this.call("TRACE", init)
|
|
96
68
|
}
|
|
97
69
|
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
70
|
+
export const wrapAsPathBasedClient = <
|
|
71
|
+
Paths extends Record<string | number, unknown>,
|
|
72
|
+
Media extends MediaType = MediaType
|
|
73
|
+
>(
|
|
74
|
+
client: Client<Paths, Media>
|
|
75
|
+
): PathBasedClient<Paths, Media> => {
|
|
76
|
+
const cache = new Map<string, object>()
|
|
77
|
+
const target = asStrictApiClient<PathBasedClient<Paths, Media>>({})
|
|
78
|
+
|
|
79
|
+
return new Proxy(target, {
|
|
80
|
+
get: (_target, property) => {
|
|
81
|
+
if (typeof property !== "string") {
|
|
82
|
+
return
|
|
108
83
|
}
|
|
109
|
-
continue
|
|
110
|
-
}
|
|
111
|
-
searchParams.set(key, String(value))
|
|
112
|
-
}
|
|
113
|
-
return searchParams.toString()
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const appendQueryString = (url: string, queryString: string): string => {
|
|
117
|
-
if (queryString.length === 0) {
|
|
118
|
-
return url
|
|
119
|
-
}
|
|
120
|
-
return url.includes("?") ? url + "&" + queryString : url + "?" + queryString
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const withBaseUrl = (baseUrl: string | undefined, url: string): string => {
|
|
124
|
-
// If baseUrl is not provided, keep a relative URL (browser-friendly)
|
|
125
|
-
if (baseUrl === undefined || baseUrl === "") {
|
|
126
|
-
return url
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Construct full URL
|
|
130
|
-
return new URL(url, baseUrl).toString()
|
|
131
|
-
}
|
|
132
84
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
* @param path - Path template with placeholders
|
|
138
|
-
* @param params - Path parameters to substitute
|
|
139
|
-
* @param query - Query parameters to append
|
|
140
|
-
* @returns Fully constructed URL
|
|
141
|
-
*
|
|
142
|
-
* @pure true
|
|
143
|
-
* @complexity O(n + m) where n = |params|, m = |query|
|
|
144
|
-
*/
|
|
145
|
-
const buildUrl = (
|
|
146
|
-
baseUrl: string | undefined,
|
|
147
|
-
path: string,
|
|
148
|
-
params?: Record<string, ParamValue>,
|
|
149
|
-
query?: Record<string, QueryValue>
|
|
150
|
-
): string => {
|
|
151
|
-
const urlWithParams = applyPathParams(path, params)
|
|
152
|
-
const queryString = buildQueryString(query)
|
|
153
|
-
const urlWithQuery = appendQueryString(urlWithParams, queryString)
|
|
154
|
-
return withBaseUrl(baseUrl, urlWithQuery)
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Check if body is already a BodyInit type (not a plain object needing serialization)
|
|
159
|
-
*
|
|
160
|
-
* @pure true
|
|
161
|
-
*/
|
|
162
|
-
const isBodyInit = (body: BodyInit | object): body is BodyInit =>
|
|
163
|
-
typeof body === "string"
|
|
164
|
-
|| body instanceof Blob
|
|
165
|
-
|| body instanceof ArrayBuffer
|
|
166
|
-
|| body instanceof ReadableStream
|
|
167
|
-
|| body instanceof FormData
|
|
168
|
-
|| body instanceof URLSearchParams
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Serialize body to BodyInit - passes through BodyInit types, JSON-stringifies objects
|
|
172
|
-
*
|
|
173
|
-
* @pure true
|
|
174
|
-
* @returns BodyInit or undefined, with consistent return path
|
|
175
|
-
*/
|
|
176
|
-
const serializeBody = (body: BodyInit | object | undefined): BodyInit | undefined => {
|
|
177
|
-
// Early return for undefined
|
|
178
|
-
if (body === undefined) {
|
|
179
|
-
return body
|
|
180
|
-
}
|
|
181
|
-
// Pass through existing BodyInit types
|
|
182
|
-
if (isBodyInit(body)) {
|
|
183
|
-
return body
|
|
184
|
-
}
|
|
185
|
-
// Plain object - serialize to JSON string (which is a valid BodyInit)
|
|
186
|
-
const serialized: BodyInit = JSON.stringify(body)
|
|
187
|
-
return serialized
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Check if body requires JSON Content-Type header
|
|
192
|
-
*
|
|
193
|
-
* @pure true
|
|
194
|
-
*/
|
|
195
|
-
const needsJsonContentType = (body: BodyInit | object | undefined): boolean =>
|
|
196
|
-
body !== undefined
|
|
197
|
-
&& typeof body !== "string"
|
|
198
|
-
&& !(body instanceof Blob)
|
|
199
|
-
&& !(body instanceof FormData)
|
|
200
|
-
|
|
201
|
-
const toHeadersFromRecord = (
|
|
202
|
-
headersInit: Record<
|
|
203
|
-
string,
|
|
204
|
-
| string
|
|
205
|
-
| number
|
|
206
|
-
| boolean
|
|
207
|
-
| ReadonlyArray<string | number | boolean>
|
|
208
|
-
| null
|
|
209
|
-
| undefined
|
|
210
|
-
>
|
|
211
|
-
): Headers => {
|
|
212
|
-
const headers = new Headers()
|
|
85
|
+
const cached = cache.get(property)
|
|
86
|
+
if (cached !== undefined) {
|
|
87
|
+
return cached
|
|
88
|
+
}
|
|
213
89
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
90
|
+
const forwarder = new PathCallForwarder(asStrictApiClient<RuntimeClient>(client), property)
|
|
91
|
+
cache.set(property, forwarder)
|
|
92
|
+
return forwarder
|
|
217
93
|
}
|
|
218
|
-
if (Array.isArray(value)) {
|
|
219
|
-
headers.set(key, value.map(String).join(","))
|
|
220
|
-
continue
|
|
221
|
-
}
|
|
222
|
-
headers.set(key, String(value))
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
return headers
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* Merge headers from client options and request options
|
|
230
|
-
*
|
|
231
|
-
* @pure true
|
|
232
|
-
* @complexity O(n) where n = number of headers
|
|
233
|
-
*/
|
|
234
|
-
const toHeaders = (headersInit: ClientOptions["headers"] | undefined): Headers => {
|
|
235
|
-
if (headersInit === undefined) {
|
|
236
|
-
return new Headers()
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
if (headersInit instanceof Headers) {
|
|
240
|
-
return new Headers(headersInit)
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
if (Array.isArray(headersInit)) {
|
|
244
|
-
return new Headers(headersInit)
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
return toHeadersFromRecord(headersInit)
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
const mergeHeaders = (
|
|
251
|
-
clientHeaders: ClientOptions["headers"] | undefined,
|
|
252
|
-
requestHeaders: ClientOptions["headers"] | undefined
|
|
253
|
-
): Headers => {
|
|
254
|
-
const headers = toHeaders(clientHeaders)
|
|
255
|
-
const optHeaders = toHeaders(requestHeaders)
|
|
256
|
-
for (const [key, value] of optHeaders.entries()) {
|
|
257
|
-
headers.set(key, value)
|
|
258
|
-
}
|
|
259
|
-
return headers
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* Request options type for method handlers
|
|
264
|
-
*
|
|
265
|
-
* @pure true - type alias only
|
|
266
|
-
*/
|
|
267
|
-
type MethodHandlerOptions = {
|
|
268
|
-
params?: Record<string, ParamValue> | undefined
|
|
269
|
-
query?: Record<string, QueryValue> | undefined
|
|
270
|
-
body?: BodyInit | object | undefined
|
|
271
|
-
headers?: ClientOptions["headers"] | undefined
|
|
272
|
-
signal?: AbortSignal | undefined
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/**
|
|
276
|
-
* Create HTTP method handler with full type constraints
|
|
277
|
-
*
|
|
278
|
-
* @param method - HTTP method
|
|
279
|
-
* @param clientOptions - Client configuration
|
|
280
|
-
* @returns Method handler function
|
|
281
|
-
*
|
|
282
|
-
* @pure false - creates function that performs HTTP requests
|
|
283
|
-
* @complexity O(1) handler creation
|
|
284
|
-
*/
|
|
285
|
-
const createMethodHandler = (
|
|
286
|
-
method: HttpMethod,
|
|
287
|
-
clientOptions: ClientOptions
|
|
288
|
-
) =>
|
|
289
|
-
<Responses>(
|
|
290
|
-
path: string,
|
|
291
|
-
dispatcher: Dispatcher<Responses>,
|
|
292
|
-
options?: MethodHandlerOptions
|
|
293
|
-
) => {
|
|
294
|
-
const url = buildUrl(clientOptions.baseUrl, path, options?.params, options?.query)
|
|
295
|
-
const headers = mergeHeaders(clientOptions.headers, options?.headers)
|
|
296
|
-
const body = serializeBody(options?.body)
|
|
297
|
-
|
|
298
|
-
if (needsJsonContentType(options?.body)) {
|
|
299
|
-
headers.set("Content-Type", "application/json")
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
const config: StrictRequestInit<Responses> = asStrictRequestInit({
|
|
303
|
-
method,
|
|
304
|
-
url,
|
|
305
|
-
dispatcher,
|
|
306
|
-
headers,
|
|
307
|
-
body,
|
|
308
|
-
signal: options?.signal
|
|
309
94
|
})
|
|
310
|
-
|
|
311
|
-
return executeRequest(config)
|
|
312
95
|
}
|
|
313
96
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
const createMethodHandlerWithDispatchers = <Paths extends object, Method extends HttpMethod>(
|
|
321
|
-
method: Method,
|
|
322
|
-
clientOptions: ClientOptions,
|
|
323
|
-
dispatchers: DispatchersForMethod<Paths, Method>
|
|
324
|
-
) =>
|
|
325
|
-
<Path extends keyof DispatchersForMethod<Paths, Method> & string>(
|
|
326
|
-
path: Path,
|
|
327
|
-
options?: MethodHandlerOptions
|
|
328
|
-
) =>
|
|
329
|
-
createMethodHandler(method, clientOptions)(
|
|
330
|
-
path,
|
|
331
|
-
dispatchers[path][method],
|
|
332
|
-
options
|
|
333
|
-
)
|
|
334
|
-
|
|
335
|
-
// CHANGE: Create method handler that infers dispatcher from map
|
|
336
|
-
// WHY: Allow per-call API without passing dispatcher parameter
|
|
337
|
-
// QUOTE(ТЗ): "Зачем передавать что либо в GET"
|
|
338
|
-
// REF: user-msg-1
|
|
339
|
-
// SOURCE: n/a
|
|
340
|
-
// FORMAT THEOREM: ∀ path ∈ PathsForMethod<Paths, method>: dispatchers[path][method] = Dispatcher<ResponsesFor<Op>>
|
|
341
|
-
// PURITY: SHELL
|
|
342
|
-
// EFFECT: Effect<ApiSuccess<Responses>, ApiFailure<Responses>, HttpClient>
|
|
343
|
-
// INVARIANT: Dispatcher lookup is total for all operations in Paths
|
|
344
|
-
// COMPLEXITY: O(1) runtime + O(1) dispatcher lookup
|
|
345
|
-
/**
|
|
346
|
-
* Create type-safe Effect-based API client
|
|
347
|
-
*
|
|
348
|
-
* The client enforces:
|
|
349
|
-
* 1. Method availability: GET only on paths with `get`, POST only on paths with `post`
|
|
350
|
-
* 2. Dispatcher correlation: must match operation's responses
|
|
351
|
-
* 3. Request options: params/query/body typed from operation
|
|
352
|
-
*
|
|
353
|
-
* @typeParam Paths - OpenAPI paths type from openapi-typescript
|
|
354
|
-
* @param options - Client configuration
|
|
355
|
-
* @returns API client with typed methods for all operations
|
|
356
|
-
*
|
|
357
|
-
* @pure false - creates client that performs HTTP requests
|
|
358
|
-
* @effect Client methods return Effect<Success, Failure, HttpClient>
|
|
359
|
-
* @invariant ∀ path, method: path ∈ PathsForMethod<Paths, method>
|
|
360
|
-
* @complexity O(1) client creation
|
|
361
|
-
*
|
|
362
|
-
* @example
|
|
363
|
-
* ```typescript
|
|
364
|
-
* import createClient from "openapi-effect"
|
|
365
|
-
* import type { Paths } from "./generated/schema"
|
|
366
|
-
* import "./generated/dispatchers-by-path" // registers default dispatchers
|
|
367
|
-
*
|
|
368
|
-
* const client = createClient<Paths>({
|
|
369
|
-
* baseUrl: "https://api.example.com",
|
|
370
|
-
* credentials: "include"
|
|
371
|
-
* })
|
|
372
|
-
*
|
|
373
|
-
* // Type-safe call - dispatcher inferred from path+method
|
|
374
|
-
* const result = yield* client.GET("/pets/{petId}", {
|
|
375
|
-
* params: { petId: "123" } // Required because getPet has path params
|
|
376
|
-
* })
|
|
377
|
-
*
|
|
378
|
-
* // Compile error: "/pets/{petId}" has no "put" method
|
|
379
|
-
* // client.PUT("/pets/{petId}", ...) // Type error!
|
|
380
|
-
* ```
|
|
381
|
-
*/
|
|
382
|
-
export const createClient = <Paths extends object>(
|
|
383
|
-
options: ClientOptions,
|
|
384
|
-
dispatchers?: DispatchersFor<Paths>
|
|
385
|
-
): StrictApiClientWithDispatchers<Paths> => {
|
|
386
|
-
const resolvedDispatchers = dispatchers ?? resolveDefaultDispatchers<Paths>()
|
|
387
|
-
|
|
388
|
-
return asStrictApiClient<StrictApiClientWithDispatchers<Paths>>({
|
|
389
|
-
GET: createMethodHandlerWithDispatchers("get", options, resolvedDispatchers),
|
|
390
|
-
POST: createMethodHandlerWithDispatchers("post", options, resolvedDispatchers),
|
|
391
|
-
PUT: createMethodHandlerWithDispatchers("put", options, resolvedDispatchers),
|
|
392
|
-
DELETE: createMethodHandlerWithDispatchers("delete", options, resolvedDispatchers),
|
|
393
|
-
PATCH: createMethodHandlerWithDispatchers("patch", options, resolvedDispatchers),
|
|
394
|
-
HEAD: createMethodHandlerWithDispatchers("head", options, resolvedDispatchers),
|
|
395
|
-
OPTIONS: createMethodHandlerWithDispatchers("options", options, resolvedDispatchers)
|
|
396
|
-
})
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
// CHANGE: Add createMethodHandlerWithUniversalDispatcher for zero-boilerplate client
|
|
400
|
-
// WHY: Enable createClientEffect<Paths>(options) without code generation or dispatcher registry
|
|
401
|
-
// QUOTE(ТЗ): "Я не хочу создавать какие-то дополнительные модули"
|
|
402
|
-
// REF: issue-5
|
|
403
|
-
// SOURCE: n/a
|
|
404
|
-
// FORMAT THEOREM: ∀ path, method: universalDispatcher handles response classification generically
|
|
405
|
-
// PURITY: SHELL
|
|
406
|
-
// EFFECT: Effect<ApiSuccess<Responses>, ApiFailure<Responses>, HttpClient>
|
|
407
|
-
// INVARIANT: 2xx → success channel, non-2xx → error channel
|
|
408
|
-
// COMPLEXITY: O(1) handler creation + O(1) universal dispatcher creation per call
|
|
409
|
-
const createMethodHandlerWithUniversalDispatcher = (
|
|
410
|
-
method: HttpMethod,
|
|
411
|
-
clientOptions: ClientOptions
|
|
412
|
-
) =>
|
|
413
|
-
(
|
|
414
|
-
path: string,
|
|
415
|
-
options?: MethodHandlerOptions
|
|
416
|
-
) =>
|
|
417
|
-
createMethodHandler(method, clientOptions)(
|
|
418
|
-
path,
|
|
419
|
-
createUniversalDispatcher(),
|
|
420
|
-
options
|
|
421
|
-
)
|
|
422
|
-
|
|
423
|
-
type HttpErrorTag = { readonly _tag: "HttpError" }
|
|
97
|
+
export const createPathBasedClient = <
|
|
98
|
+
Paths extends Record<string | number, unknown>,
|
|
99
|
+
Media extends MediaType = MediaType
|
|
100
|
+
>(
|
|
101
|
+
clientOptions?: ClientOptions
|
|
102
|
+
): PathBasedClient<Paths, Media> => wrapAsPathBasedClient(createClient<Paths, Media>(clientOptions))
|
|
424
103
|
|
|
425
|
-
const isHttpErrorValue = (error: unknown): error is HttpErrorTag =>
|
|
426
|
-
typeof error === "object"
|
|
427
|
-
&& error !== null
|
|
428
|
-
&& "_tag" in error
|
|
429
|
-
&& Reflect.get(error, "_tag") === "HttpError"
|
|
430
|
-
|
|
431
|
-
const exposeHttpErrorsAsValues = <A, E>(
|
|
432
|
-
request: Effect.Effect<A, E, HttpClient.HttpClient>
|
|
433
|
-
): Effect.Effect<
|
|
434
|
-
A | Extract<E, HttpErrorTag>,
|
|
435
|
-
Exclude<E, Extract<E, HttpErrorTag>>,
|
|
436
|
-
HttpClient.HttpClient
|
|
437
|
-
> =>
|
|
438
|
-
request.pipe(
|
|
439
|
-
Effect.catchIf(
|
|
440
|
-
(error): error is Extract<E, HttpErrorTag> => isHttpErrorValue(error),
|
|
441
|
-
(error) => Effect.succeed(error)
|
|
442
|
-
)
|
|
443
|
-
)
|
|
444
|
-
|
|
445
|
-
const createMethodHandlerWithUniversalDispatcherValue = (
|
|
446
|
-
method: HttpMethod,
|
|
447
|
-
clientOptions: ClientOptions
|
|
448
|
-
) =>
|
|
449
|
-
(
|
|
450
|
-
path: string,
|
|
451
|
-
options?: MethodHandlerOptions
|
|
452
|
-
) =>
|
|
453
|
-
exposeHttpErrorsAsValues(
|
|
454
|
-
createMethodHandlerWithUniversalDispatcher(method, clientOptions)(path, options)
|
|
455
|
-
)
|
|
456
|
-
|
|
457
|
-
// CHANGE: Add createClientEffect — zero-boilerplate Effect-based API client
|
|
458
|
-
// WHY: Enable the user's desired DSL without any generated code or dispatcher setup
|
|
459
|
-
// QUOTE(ТЗ): "const apiClientEffect = createClientEffect<Paths>(clientOptions); apiClientEffect.POST('/api/auth/login', { body: credentials })"
|
|
460
|
-
// REF: issue-5
|
|
461
|
-
// SOURCE: n/a
|
|
462
|
-
// FORMAT THEOREM: ∀ Paths, options: createClientEffect<Paths>(options) → ClientEffect<Paths>
|
|
463
|
-
// PURITY: SHELL
|
|
464
|
-
// EFFECT: Client methods return Effect<ApiSuccess | HttpError, BoundaryError, HttpClient>
|
|
465
|
-
// INVARIANT: ∀ path, method: path ∈ PathsForMethod<Paths, method> (compile-time) ∧ response classified by status range (runtime)
|
|
466
|
-
// COMPLEXITY: O(1) client creation
|
|
467
|
-
/**
|
|
468
|
-
* Create type-safe Effect-based API client with zero boilerplate
|
|
469
|
-
*
|
|
470
|
-
* Uses a universal dispatcher and exposes HTTP statuses as values:
|
|
471
|
-
* - 2xx → success value (ApiSuccess)
|
|
472
|
-
* - non-2xx schema statuses → success value (HttpError with _tag)
|
|
473
|
-
* - boundary/protocol failures stay in error channel
|
|
474
|
-
* - JSON parsed automatically for application/json content types
|
|
475
|
-
*
|
|
476
|
-
* **No code generation needed.** No dispatcher registry needed.
|
|
477
|
-
* Just pass your OpenAPI Paths type and client options.
|
|
478
|
-
*
|
|
479
|
-
* @typeParam Paths - OpenAPI paths type from openapi-typescript
|
|
480
|
-
* @param options - Client configuration (baseUrl, credentials, headers, etc.)
|
|
481
|
-
* @returns API client with typed methods for all operations
|
|
482
|
-
*
|
|
483
|
-
* @pure false - creates client that performs HTTP requests
|
|
484
|
-
* @effect Client methods return Effect<Success, Failure, HttpClient>
|
|
485
|
-
* @invariant ∀ path, method: path ∈ PathsForMethod<Paths, method>
|
|
486
|
-
* @complexity O(1) client creation
|
|
487
|
-
*
|
|
488
|
-
* @example
|
|
489
|
-
* ```typescript
|
|
490
|
-
* import { createClientEffect, type ClientOptions } from "openapi-effect"
|
|
491
|
-
* import type { paths } from "./openapi.d.ts"
|
|
492
|
-
*
|
|
493
|
-
* const clientOptions: ClientOptions = {
|
|
494
|
-
* baseUrl: "https://petstore.example.com",
|
|
495
|
-
* credentials: "include"
|
|
496
|
-
* }
|
|
497
|
-
* const apiClientEffect = createClientEffect<paths>(clientOptions)
|
|
498
|
-
*
|
|
499
|
-
* // Type-safe call — path, method, and body all enforced at compile time
|
|
500
|
-
* const result = yield* apiClientEffect.POST("/api/auth/login", {
|
|
501
|
-
* body: { email: "user@example.com", password: "secret" }
|
|
502
|
-
* })
|
|
503
|
-
* ```
|
|
504
|
-
*/
|
|
505
104
|
export const createClientEffect = <Paths extends object>(
|
|
506
|
-
|
|
507
|
-
): ClientEffect<Paths> =>
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
DELETE: createMethodHandlerWithUniversalDispatcherValue("delete", options),
|
|
513
|
-
PATCH: createMethodHandlerWithUniversalDispatcherValue("patch", options),
|
|
514
|
-
HEAD: createMethodHandlerWithUniversalDispatcherValue("head", options),
|
|
515
|
-
OPTIONS: createMethodHandlerWithUniversalDispatcherValue("options", options)
|
|
516
|
-
})
|
|
517
|
-
}
|
|
105
|
+
clientOptions?: ClientOptions
|
|
106
|
+
): ClientEffect<Paths> => createClient<Paths>(clientOptions)
|
|
107
|
+
|
|
108
|
+
export const registerDefaultDispatchers = <Paths extends object>(
|
|
109
|
+
_dispatchers: DispatchersFor<Paths>
|
|
110
|
+
): void => {}
|
|
@@ -28,10 +28,37 @@ export {
|
|
|
28
28
|
|
|
29
29
|
// High-level client creation API
|
|
30
30
|
export type {
|
|
31
|
+
Client,
|
|
31
32
|
ClientEffect,
|
|
33
|
+
ClientForPath,
|
|
32
34
|
ClientOptions,
|
|
33
35
|
DispatchersFor,
|
|
36
|
+
FetchOptions,
|
|
37
|
+
FetchResponse,
|
|
38
|
+
HeadersOptions,
|
|
39
|
+
Middleware,
|
|
40
|
+
ParseAs,
|
|
41
|
+
PathBasedClient,
|
|
42
|
+
QuerySerializer,
|
|
43
|
+
QuerySerializerOptions,
|
|
44
|
+
RequestBodyOption,
|
|
45
|
+
RequestOptions as FetchRequestOptions,
|
|
34
46
|
StrictApiClient,
|
|
35
47
|
StrictApiClientWithDispatchers
|
|
36
48
|
} from "./create-client.js"
|
|
37
|
-
export {
|
|
49
|
+
export {
|
|
50
|
+
createClient,
|
|
51
|
+
createClientEffect,
|
|
52
|
+
createFinalURL,
|
|
53
|
+
createPathBasedClient,
|
|
54
|
+
createQuerySerializer,
|
|
55
|
+
defaultBodySerializer,
|
|
56
|
+
defaultPathSerializer,
|
|
57
|
+
mergeHeaders,
|
|
58
|
+
registerDefaultDispatchers,
|
|
59
|
+
removeTrailingSlash,
|
|
60
|
+
serializeArrayParam,
|
|
61
|
+
serializeObjectParam,
|
|
62
|
+
serializePrimitiveParam,
|
|
63
|
+
wrapAsPathBasedClient
|
|
64
|
+
} from "./create-client.js"
|