@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prover-coder-ai/openapi-effect",
3
- "version": "1.0.19",
3
+ "version": "1.0.21",
4
4
  "description": "Drop-in replacement for openapi-fetch with an opt-in Effect API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -35,11 +35,10 @@
35
35
  },
36
36
  "homepage": "https://github.com/ProverCoderAI/effect-template#readme",
37
37
  "dependencies": {
38
- "@effect/platform": "^0.94.4",
38
+ "@effect/platform": "^0.94.5",
39
39
  "@effect/platform-node": "^0.104.1",
40
40
  "@effect/schema": "^0.75.5",
41
- "effect": "^3.19.16",
42
- "openapi-fetch": "^0.15.2"
41
+ "effect": "^3.19.17"
43
42
  },
44
43
  "scripts": {
45
44
  "build": "vite build",
package/src/index.ts CHANGED
@@ -1,23 +1,16 @@
1
- // CHANGE: Make openapi-effect a drop-in replacement for openapi-fetch (Promise API), with an opt-in Effect API.
2
- // WHY: Consumer projects must be able to swap openapi-fetch -> openapi-effect with near-zero code changes.
3
- // QUOTE(ТЗ): "openapi-effect должен почти 1 в 1 заменяться с openapi-fetch" / "Просто добавлять effect поведение"
1
+ // CHANGE: Expose Effect-only public API
2
+ // WHY: Enforce Effect-first paradigm and remove Promise-based client surface
4
3
  // SOURCE: n/a
5
4
  // PURITY: SHELL (re-exports)
6
5
  // COMPLEXITY: O(1)
7
6
 
8
- // Promise-based client (openapi-fetch compatible)
9
- export { default } from "openapi-fetch"
10
- export { default as createClient } from "openapi-fetch"
11
- export * from "openapi-fetch"
12
-
13
- // Effect-based client (opt-in)
14
7
  export * as FetchHttpClient from "@effect/platform/FetchHttpClient"
15
8
 
16
- // Strict Effect client (advanced)
17
9
  export type * from "./core/api-client/index.js"
18
10
  export { assertNever } from "./core/api-client/index.js"
19
11
 
20
12
  export type {
13
+ ClientOptions,
21
14
  DispatchersFor,
22
15
  StrictApiClient,
23
16
  StrictApiClientWithDispatchers
@@ -26,17 +19,29 @@ export type {
26
19
  export type { Decoder, Dispatcher, RawResponse, StrictClient, StrictRequestInit } from "./shell/api-client/index.js"
27
20
 
28
21
  export {
29
- createClient as createClientStrict,
22
+ createClient,
30
23
  createClientEffect,
31
24
  createDispatcher,
25
+ createFinalURL,
26
+ createPathBasedClient,
27
+ createQuerySerializer,
32
28
  createStrictClient,
33
29
  createUniversalDispatcher,
30
+ defaultBodySerializer,
31
+ defaultPathSerializer,
34
32
  executeRequest,
33
+ mergeHeaders,
35
34
  parseJSON,
36
35
  registerDefaultDispatchers,
36
+ removeTrailingSlash,
37
+ serializeArrayParam,
38
+ serializeObjectParam,
39
+ serializePrimitiveParam,
37
40
  unexpectedContentType,
38
41
  unexpectedStatus
39
42
  } from "./shell/api-client/index.js"
40
43
 
44
+ export { createClient as default } from "./shell/api-client/index.js"
45
+
41
46
  // Generated dispatchers (auto-generated from OpenAPI schema)
42
47
  export * from "./generated/index.js"
@@ -0,0 +1,179 @@
1
+ import { Effect } from "effect"
2
+
3
+ import { toError } from "./create-client-response.js"
4
+ import type { AsyncValue, MergedOptions, Middleware, MiddlewareRequestParams, Thenable } from "./create-client-types.js"
5
+
6
+ const isThenable = <T>(value: unknown): value is Thenable<T> => (
7
+ typeof value === "object"
8
+ && value !== null
9
+ && "then" in value
10
+ && typeof Reflect.get(value, "then") === "function"
11
+ )
12
+
13
+ export const toPromiseEffect = <T>(value: AsyncValue<T>): Effect.Effect<T, Error> => (
14
+ isThenable(value)
15
+ ? Effect.async<T, Error>((resume) => {
16
+ value.then(
17
+ (result) => {
18
+ resume(Effect.succeed(result))
19
+ },
20
+ (error) => {
21
+ resume(Effect.fail(toError(error)))
22
+ }
23
+ )
24
+ })
25
+ : Effect.succeed(value)
26
+ )
27
+
28
+ export type MiddlewareContext = {
29
+ schemaPath: string
30
+ params: MiddlewareRequestParams
31
+ options: MergedOptions
32
+ id: string
33
+ middleware: Array<Middleware>
34
+ }
35
+
36
+ const reverseMiddleware = (middleware: Array<Middleware>): Array<Middleware> => {
37
+ const output: Array<Middleware> = []
38
+
39
+ for (let index = middleware.length - 1; index >= 0; index -= 1) {
40
+ const item = middleware[index]
41
+ if (item !== undefined) {
42
+ output.push(item)
43
+ }
44
+ }
45
+
46
+ return output
47
+ }
48
+
49
+ type RequestMiddlewareResult = {
50
+ request: Request
51
+ response?: Response
52
+ }
53
+
54
+ const createMiddlewareParams = (
55
+ request: Request,
56
+ context: MiddlewareContext
57
+ ): {
58
+ request: Request
59
+ schemaPath: string
60
+ params: MiddlewareRequestParams
61
+ options: MergedOptions
62
+ id: string
63
+ } => ({
64
+ request,
65
+ schemaPath: context.schemaPath,
66
+ params: context.params,
67
+ options: context.options,
68
+ id: context.id
69
+ })
70
+
71
+ export const applyRequestMiddleware = (
72
+ request: Request,
73
+ context: MiddlewareContext
74
+ ): Effect.Effect<RequestMiddlewareResult, Error> =>
75
+ Effect.gen(function*() {
76
+ let nextRequest = request
77
+
78
+ for (const item of context.middleware) {
79
+ if (typeof item.onRequest !== "function") {
80
+ continue
81
+ }
82
+
83
+ const result = yield* toPromiseEffect(item.onRequest(createMiddlewareParams(nextRequest, context)))
84
+
85
+ if (result === undefined) {
86
+ continue
87
+ }
88
+
89
+ if (result instanceof Request) {
90
+ nextRequest = result
91
+ continue
92
+ }
93
+
94
+ if (result instanceof Response) {
95
+ return { request: nextRequest, response: result }
96
+ }
97
+
98
+ return yield* Effect.fail(
99
+ new Error("onRequest: must return new Request() or Response() when modifying the request")
100
+ )
101
+ }
102
+
103
+ return { request: nextRequest }
104
+ })
105
+
106
+ export const applyResponseMiddleware = (
107
+ request: Request,
108
+ response: Response,
109
+ context: MiddlewareContext
110
+ ): Effect.Effect<Response, Error> =>
111
+ Effect.gen(function*() {
112
+ let nextResponse = response
113
+
114
+ for (const item of reverseMiddleware(context.middleware)) {
115
+ if (typeof item.onResponse !== "function") {
116
+ continue
117
+ }
118
+
119
+ const result = yield* toPromiseEffect(item.onResponse({
120
+ ...createMiddlewareParams(request, context),
121
+ response: nextResponse
122
+ }))
123
+
124
+ if (result === undefined) {
125
+ continue
126
+ }
127
+
128
+ if (!(result instanceof Response)) {
129
+ return yield* Effect.fail(
130
+ new Error("onResponse: must return new Response() when modifying the response")
131
+ )
132
+ }
133
+
134
+ nextResponse = result
135
+ }
136
+
137
+ return nextResponse
138
+ })
139
+
140
+ const normalizeErrorResult = (
141
+ result: Response | Error | undefined
142
+ ): Effect.Effect<Response | Error | undefined, Error> => {
143
+ if (result === undefined || result instanceof Response || result instanceof Error) {
144
+ return Effect.succeed(result)
145
+ }
146
+
147
+ return Effect.fail(new Error("onError: must return new Response() or instance of Error"))
148
+ }
149
+
150
+ export const applyErrorMiddleware = (
151
+ request: Request,
152
+ fetchError: Error,
153
+ context: MiddlewareContext
154
+ ): Effect.Effect<Response, Error> =>
155
+ Effect.gen(function*() {
156
+ let nextError: Error = fetchError
157
+
158
+ for (const item of reverseMiddleware(context.middleware)) {
159
+ if (typeof item.onError !== "function") {
160
+ continue
161
+ }
162
+
163
+ const rawResult = yield* toPromiseEffect(item.onError({
164
+ ...createMiddlewareParams(request, context),
165
+ error: nextError
166
+ }))
167
+
168
+ const result = yield* normalizeErrorResult(rawResult)
169
+ if (result instanceof Response) {
170
+ return result
171
+ }
172
+
173
+ if (result instanceof Error) {
174
+ nextError = result
175
+ }
176
+ }
177
+
178
+ return yield* Effect.fail(nextError)
179
+ })
@@ -0,0 +1,115 @@
1
+ import { Effect } from "effect"
2
+
3
+ import { asJson } from "../../core/axioms.js"
4
+ import type { ParseAs } from "./create-client-types.js"
5
+
6
+ type RuntimeFetchResponse = {
7
+ data?: unknown
8
+ error?: unknown
9
+ response: Response
10
+ }
11
+
12
+ export const toError = (error: unknown): Error => (
13
+ error instanceof Error ? error : new Error(String(error))
14
+ )
15
+
16
+ const parseJsonText = (rawText: string): Effect.Effect<unknown, Error> => (
17
+ rawText.length === 0
18
+ ? Effect.void
19
+ : Effect.try({
20
+ try: () => asJson(JSON.parse(rawText)),
21
+ catch: toError
22
+ })
23
+ )
24
+
25
+ const readResponseText = (response: Response): Effect.Effect<string, Error> => (
26
+ Effect.tryPromise({
27
+ try: () => response.text(),
28
+ catch: toError
29
+ })
30
+ )
31
+
32
+ const parseSuccessData = (
33
+ response: Response,
34
+ parseAs: ParseAs,
35
+ contentLength: string | null
36
+ ): Effect.Effect<unknown, Error> => {
37
+ if (parseAs === "stream") {
38
+ return Effect.succeed(response.body)
39
+ }
40
+
41
+ if (parseAs === "text") {
42
+ return Effect.tryPromise({ try: () => response.text(), catch: toError })
43
+ }
44
+
45
+ if (parseAs === "blob") {
46
+ return Effect.tryPromise({ try: () => response.blob(), catch: toError })
47
+ }
48
+
49
+ if (parseAs === "arrayBuffer") {
50
+ return Effect.tryPromise({ try: () => response.arrayBuffer(), catch: toError })
51
+ }
52
+
53
+ if (contentLength === null) {
54
+ return readResponseText(response).pipe(
55
+ Effect.flatMap((rawText) => parseJsonText(rawText))
56
+ )
57
+ }
58
+
59
+ return Effect.tryPromise({ try: () => response.json(), catch: toError })
60
+ }
61
+
62
+ const parseErrorData = (response: Response): Effect.Effect<unknown, Error> => (
63
+ readResponseText(response).pipe(
64
+ Effect.flatMap((rawText) =>
65
+ Effect.match(
66
+ Effect.try({
67
+ try: () => asJson(JSON.parse(rawText)),
68
+ catch: toError
69
+ }),
70
+ {
71
+ onFailure: () => rawText,
72
+ onSuccess: (parsed) => parsed
73
+ }
74
+ )
75
+ )
76
+ )
77
+ )
78
+
79
+ const hasChunkedTransferEncoding = (response: Response): boolean => (
80
+ response.headers.get("Transfer-Encoding")?.includes("chunked") === true
81
+ )
82
+
83
+ const isEmptyResponse = (
84
+ request: Request,
85
+ response: Response,
86
+ contentLength: string | null
87
+ ): boolean => (
88
+ response.status === 204
89
+ || request.method === "HEAD"
90
+ || (contentLength === "0" && !hasChunkedTransferEncoding(response))
91
+ )
92
+
93
+ export const createResponseEnvelope = (
94
+ request: Request,
95
+ response: Response,
96
+ parseAs: ParseAs
97
+ ): Effect.Effect<RuntimeFetchResponse, Error> => {
98
+ const contentLength = response.headers.get("Content-Length")
99
+
100
+ if (isEmptyResponse(request, response, contentLength)) {
101
+ return response.ok
102
+ ? Effect.succeed({ data: undefined, response })
103
+ : Effect.succeed({ error: undefined, response })
104
+ }
105
+
106
+ if (response.ok) {
107
+ return parseSuccessData(response, parseAs, contentLength).pipe(
108
+ Effect.map((data) => ({ data, response }))
109
+ )
110
+ }
111
+
112
+ return parseErrorData(response).pipe(
113
+ Effect.map((error) => ({ error, response }))
114
+ )
115
+ }
@@ -0,0 +1,142 @@
1
+ import { Effect } from "effect"
2
+
3
+ import { asStrictApiClient } from "../../core/axioms.js"
4
+ import { toError } from "./create-client-response.js"
5
+ import type { FetchWithRequestInitExt, HeaderRecord } from "./create-client-runtime-types.js"
6
+ import type {
7
+ BodySerializer,
8
+ ClientOptions,
9
+ MergedOptions,
10
+ MiddlewareRequestParams,
11
+ ParseAs,
12
+ PathSerializer,
13
+ QuerySerializer,
14
+ QuerySerializerOptions
15
+ } from "./create-client-types.js"
16
+ import { createQuerySerializer } from "./openapi-compat-utils.js"
17
+
18
+ export const supportsRequestInitExt = (): boolean => (
19
+ typeof process === "object"
20
+ && Number.parseInt(process.versions.node.slice(0, 2), 10) >= 18
21
+ && typeof process.versions["undici"] === "string"
22
+ )
23
+
24
+ export const randomID = (): string => (
25
+ globalThis.crypto.randomUUID().replaceAll("-", "").slice(0, 9)
26
+ )
27
+
28
+ const isQuerySerializerOptions = (
29
+ value: QuerySerializer<unknown> | QuerySerializerOptions | undefined
30
+ ): value is QuerySerializerOptions => (
31
+ value !== undefined && typeof value === "object"
32
+ )
33
+
34
+ export const resolveQuerySerializer = (
35
+ globalQuerySerializer: ClientOptions["querySerializer"],
36
+ requestQuerySerializer: QuerySerializer<unknown> | QuerySerializerOptions | undefined
37
+ ): QuerySerializer<unknown> => {
38
+ let serializer = typeof globalQuerySerializer === "function"
39
+ ? globalQuerySerializer
40
+ : createQuerySerializer(globalQuerySerializer)
41
+
42
+ if (requestQuerySerializer) {
43
+ serializer = typeof requestQuerySerializer === "function"
44
+ ? requestQuerySerializer
45
+ : createQuerySerializer({
46
+ ...(isQuerySerializerOptions(globalQuerySerializer) ? globalQuerySerializer : {}),
47
+ ...requestQuerySerializer
48
+ })
49
+ }
50
+
51
+ return serializer
52
+ }
53
+
54
+ const isHeaderPrimitive = (value: unknown): value is string | number | boolean => (
55
+ typeof value === "string" || typeof value === "number" || typeof value === "boolean"
56
+ )
57
+
58
+ export const toHeaderOverrides = (headers: MiddlewareRequestParams["header"]): HeaderRecord => {
59
+ if (headers === undefined) {
60
+ return {}
61
+ }
62
+
63
+ const normalized: HeaderRecord = {}
64
+ for (const [key, rawValue] of Object.entries(headers)) {
65
+ if (rawValue === undefined || rawValue === null || isHeaderPrimitive(rawValue)) {
66
+ normalized[key] = rawValue
67
+ continue
68
+ }
69
+
70
+ if (Array.isArray(rawValue)) {
71
+ normalized[key] = rawValue.filter((item) => isHeaderPrimitive(item))
72
+ }
73
+ }
74
+
75
+ return normalized
76
+ }
77
+
78
+ const isBodyInit = (value: BodyInit | object): value is BodyInit => (
79
+ typeof value === "string"
80
+ || value instanceof Blob
81
+ || value instanceof URLSearchParams
82
+ || value instanceof ArrayBuffer
83
+ || value instanceof FormData
84
+ || value instanceof ReadableStream
85
+ )
86
+
87
+ export type SerializedBody =
88
+ | { hasBody: false }
89
+ | { hasBody: true; value: BodyInit }
90
+
91
+ export const serializeBody = (
92
+ body: BodyInit | object | undefined,
93
+ serializer: BodySerializer<unknown>,
94
+ headers: Headers
95
+ ): SerializedBody => {
96
+ if (body === undefined) {
97
+ return { hasBody: false }
98
+ }
99
+
100
+ if (isBodyInit(body)) {
101
+ return { hasBody: true, value: body }
102
+ }
103
+
104
+ return { hasBody: true, value: serializer(body, headers) }
105
+ }
106
+
107
+ export const setCustomRequestFields = (request: Request, init: Record<string, unknown>): void => {
108
+ for (const key in init) {
109
+ if (!(key in request)) {
110
+ Reflect.set(request, key, init[key])
111
+ }
112
+ }
113
+ }
114
+
115
+ export const invokeFetch = (
116
+ fetch: NonNullable<ClientOptions["fetch"]>,
117
+ request: Request,
118
+ requestInitExt?: Record<string, unknown>
119
+ ): Effect.Effect<Response, Error> => {
120
+ const fetchWithExt = asStrictApiClient<FetchWithRequestInitExt>(fetch)
121
+ return Effect.tryPromise({
122
+ try: () => fetchWithExt(request, requestInitExt),
123
+ catch: toError
124
+ })
125
+ }
126
+
127
+ export const createMergedOptions = (options: {
128
+ baseUrl: string
129
+ parseAs: ParseAs
130
+ querySerializer: QuerySerializer<unknown>
131
+ bodySerializer: BodySerializer<unknown>
132
+ pathSerializer: PathSerializer
133
+ fetch: NonNullable<ClientOptions["fetch"]>
134
+ }): MergedOptions =>
135
+ Object.freeze<MergedOptions>({
136
+ baseUrl: options.baseUrl,
137
+ parseAs: options.parseAs,
138
+ querySerializer: options.querySerializer,
139
+ bodySerializer: options.bodySerializer,
140
+ pathSerializer: options.pathSerializer,
141
+ fetch: asStrictApiClient<typeof globalThis.fetch>(options.fetch)
142
+ })
@@ -0,0 +1,97 @@
1
+ import type { Effect } from "effect"
2
+
3
+ import type { MiddlewareContext } from "./create-client-middleware.js"
4
+ import type {
5
+ BodySerializer,
6
+ ClientOptions,
7
+ HeadersOptions,
8
+ Middleware,
9
+ MiddlewareRequestParams,
10
+ ParseAs,
11
+ PathSerializer,
12
+ QuerySerializer,
13
+ QuerySerializerOptions
14
+ } from "./create-client-types.js"
15
+
16
+ export type RuntimeFetchResponse = {
17
+ data?: unknown
18
+ error?: unknown
19
+ response: Response
20
+ }
21
+
22
+ export type RuntimeFetchOptions = Omit<RequestInit, "body" | "headers" | "method"> & {
23
+ baseUrl?: string
24
+ fetch?: NonNullable<ClientOptions["fetch"]>
25
+ Request?: ClientOptions["Request"]
26
+ headers?: HeadersOptions
27
+ params?: MiddlewareRequestParams
28
+ parseAs?: ParseAs
29
+ querySerializer?: QuerySerializer<unknown> | QuerySerializerOptions
30
+ pathSerializer?: PathSerializer
31
+ bodySerializer?: BodySerializer<unknown>
32
+ body?: BodyInit | object
33
+ middleware?: Array<Middleware>
34
+ method?: string
35
+ [key: string]: unknown
36
+ }
37
+
38
+ export type RuntimeClient = {
39
+ request: (method: string, url: string, init?: RuntimeFetchOptions) => Effect.Effect<RuntimeFetchResponse, Error>
40
+ GET: (url: string, init?: RuntimeFetchOptions) => Effect.Effect<RuntimeFetchResponse, Error>
41
+ PUT: (url: string, init?: RuntimeFetchOptions) => Effect.Effect<RuntimeFetchResponse, Error>
42
+ POST: (url: string, init?: RuntimeFetchOptions) => Effect.Effect<RuntimeFetchResponse, Error>
43
+ DELETE: (url: string, init?: RuntimeFetchOptions) => Effect.Effect<RuntimeFetchResponse, Error>
44
+ OPTIONS: (url: string, init?: RuntimeFetchOptions) => Effect.Effect<RuntimeFetchResponse, Error>
45
+ HEAD: (url: string, init?: RuntimeFetchOptions) => Effect.Effect<RuntimeFetchResponse, Error>
46
+ PATCH: (url: string, init?: RuntimeFetchOptions) => Effect.Effect<RuntimeFetchResponse, Error>
47
+ TRACE: (url: string, init?: RuntimeFetchOptions) => Effect.Effect<RuntimeFetchResponse, Error>
48
+ use: (...middleware: Array<Middleware>) => void
49
+ eject: (...middleware: Array<Middleware>) => void
50
+ }
51
+
52
+ export type HeaderValue =
53
+ | string
54
+ | number
55
+ | boolean
56
+ | Array<string | number | boolean>
57
+ | null
58
+ | undefined
59
+
60
+ export type HeaderRecord = Record<string, HeaderValue>
61
+
62
+ export type BaseRuntimeConfig = {
63
+ Request: typeof Request
64
+ baseUrl: string
65
+ bodySerializer: BodySerializer<unknown> | undefined
66
+ fetch: NonNullable<ClientOptions["fetch"]>
67
+ pathSerializer: PathSerializer | undefined
68
+ headers: HeadersOptions | undefined
69
+ querySerializer: QuerySerializer<unknown> | QuerySerializerOptions | undefined
70
+ requestInitExt: Record<string, unknown> | undefined
71
+ baseOptions: Omit<
72
+ ClientOptions,
73
+ | "Request"
74
+ | "baseUrl"
75
+ | "bodySerializer"
76
+ | "fetch"
77
+ | "headers"
78
+ | "querySerializer"
79
+ | "pathSerializer"
80
+ | "requestInitExt"
81
+ >
82
+ globalMiddlewares: Array<Middleware>
83
+ }
84
+
85
+ export type PreparedRequest = {
86
+ request: Request
87
+ fetch: NonNullable<ClientOptions["fetch"]>
88
+ parseAs: ParseAs
89
+ context: MiddlewareContext
90
+ middleware: Array<Middleware>
91
+ requestInitExt: Record<string, unknown> | undefined
92
+ }
93
+
94
+ export type FetchWithRequestInitExt = (
95
+ input: Request,
96
+ requestInitExt?: Record<string, unknown>
97
+ ) => ReturnType<typeof globalThis.fetch>