@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.
@@ -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
+ )