|
1 | 1 | // CHANGE: Strict example demonstrating forced E=never error handling |
2 | | -// WHY: Prove that after catchTags with Match.exhaustive, the error channel becomes 'never' |
| 2 | +// WHY: Prove that after catchTags with Match.exhaustive, the error channel becomes never |
3 | 3 | // QUOTE(ТЗ): "Приёмка по смыслу: после catchTags(...) тип ошибки становится never" |
4 | 4 | // REF: PR#3 blocking review from skulidropek |
5 | 5 | // SOURCE: n/a |
6 | 6 | // PURITY: SHELL |
7 | | -// EFFECT: Effect<void, never, HttpClient> - all errors handled |
| 7 | +// EFFECT: Effect<void, never, never> - all errors handled |
8 | 8 |
|
9 | | -import * as FetchHttpClient from "@effect/platform/FetchHttpClient" |
10 | | -import type * as HttpClient from "@effect/platform/HttpClient" |
11 | | -import { Cause, Console, Effect, Match } from "effect" |
12 | | -import "../src/generated/dispatchers-by-path.js" |
13 | | -import { type ClientOptions, createClient } from "../src/shell/api-client/create-client.js" |
| 9 | +import { Console, Effect, Match } from "effect" |
| 10 | + |
| 11 | +import { type ClientOptions, createClientEffect } from "../src/index.js" |
14 | 12 | import type { Paths } from "../tests/fixtures/petstore.openapi.js" |
15 | 13 |
|
16 | | -/** |
17 | | - * Client configuration |
18 | | - */ |
19 | 14 | const clientOptions: ClientOptions = { |
20 | 15 | baseUrl: "https://petstore.example.com", |
21 | 16 | credentials: "include" |
22 | 17 | } |
23 | 18 |
|
24 | | -// CHANGE: Use default dispatcher registry (registered by generated module) |
25 | | -// WHY: Call createClient(options) without passing dispatcher map |
26 | | -// QUOTE(ТЗ): "const apiClient = createClient<Paths>(clientOptions)" |
27 | | -// REF: user-msg-4 |
| 19 | +// CHANGE: Use createClientEffect with openapi-fetch-compatible inputs |
| 20 | +// WHY: Output channels are inferred from createClientEffect<Paths>() without per-call schema |
| 21 | +// QUOTE(ТЗ): "input должен быть 1 в 1" |
| 22 | +// REF: user-msg-openapi-effect-input-compat |
28 | 23 | // SOURCE: n/a |
29 | | -// FORMAT THEOREM: ∀ op ∈ Operations: createClient(options) uses registered dispatchers |
| 24 | +// FORMAT THEOREM: ∀ path, method: responses(Paths[path][method]) -> Effect<S, E, never> |
30 | 25 | // PURITY: SHELL |
31 | 26 | // EFFECT: none |
32 | | -// INVARIANT: default dispatchers registered before client creation |
| 27 | +// INVARIANT: Path and method select the OpenAPI operation that determines success and failure channels |
33 | 28 | // COMPLEXITY: O(1) |
34 | | -const apiClient = createClient<Paths>(clientOptions) |
35 | | - |
36 | | -// ============================================================================= |
37 | | -// STRICT EXAMPLE 1: getPet - handles 404, 500 + all boundary errors |
38 | | -// ============================================================================= |
39 | | - |
40 | | -/** |
41 | | - * CRITICAL: This program has E=never - all errors are explicitly handled! |
42 | | - * |
43 | | - * The reviewer requires: |
44 | | - * 1. Only Match.exhaustive (no Match.orElse) |
45 | | - * 2. All _tag variants handled via catchTags |
46 | | - * 3. After catchTags, type becomes Effect<void, never, HttpClient> |
47 | | - * |
48 | | - * Schema: getPet has responses 200 (success), 404 (error), 500 (error) |
49 | | - * Error channel: HttpError<404 | 500> | BoundaryError |
50 | | - * |
51 | | - * @invariant After catchTags, E = never |
52 | | - * @effect Effect<void, never, HttpClient> |
53 | | - */ |
54 | | -export const getPetStrictProgram: Effect.Effect<void, never, HttpClient.HttpClient> = Effect.gen(function*() { |
55 | | - yield* Console.log("=== getPet: Strict Error Handling ===") |
56 | | - |
57 | | - // Execute request - yields only on 200 |
58 | | - yield* apiClient.GET("/pets/{petId}", { params: { petId: "123" } }).pipe( |
59 | | - Effect.tap((result) => Console.log(`Got pet: ${result.body.name}`)) |
60 | | - ) |
61 | | -}).pipe( |
62 | | - // Handle HttpError with EXHAUSTIVE matching (no orElse!) |
63 | | - Effect.catchTag("HttpError", (error) => |
64 | | - Match.value(error.status).pipe( |
65 | | - Match.when(404, () => Console.log(`Not found: ${JSON.stringify(error.body)}`)), |
66 | | - Match.when(500, () => Console.log(`Server error: ${JSON.stringify(error.body)}`)), |
67 | | - // CRITICAL: Match.exhaustive - forces handling ALL schema statuses |
68 | | - // If a new status (e.g., 401) is added to schema, this will fail typecheck |
69 | | - Match.exhaustive |
70 | | - )), |
71 | | - // Handle ALL boundary errors |
72 | | - Effect.catchTag("TransportError", (e) => Console.log(`Transport error: ${e.error.message}`)), |
73 | | - Effect.catchTag("UnexpectedStatus", (e) => Console.log(`Unexpected status: ${e.status}`)), |
74 | | - Effect.catchTag("UnexpectedContentType", (e) => Console.log(`Unexpected content-type: ${e.actual ?? "unknown"}`)), |
75 | | - Effect.catchTag("ParseError", (e) => Console.log(`Parse error: ${e.error.message}`)), |
76 | | - Effect.catchTag("DecodeError", (e) => Console.log(`Decode error: ${e.error.message}`)) |
77 | | -) |
78 | | - |
79 | | -// ============================================================================= |
80 | | -// STRICT EXAMPLE 2: createPet - handles 400, 500 + all boundary errors |
81 | | -// ============================================================================= |
82 | | - |
83 | | -/** |
84 | | - * createPet strict handler |
85 | | - * |
86 | | - * Schema: createPet has responses 201 (success), 400 (error), 500 (error) |
87 | | - * Error channel: HttpError<400 | 500> | BoundaryError |
88 | | - * |
89 | | - * @invariant After catchTags, E = never |
90 | | - * @effect Effect<void, never, HttpClient> |
91 | | - */ |
92 | | -export const createPetStrictProgram: Effect.Effect<void, never, HttpClient.HttpClient> = Effect.gen(function*() { |
93 | | - yield* Console.log("=== createPet: Strict Error Handling ===") |
94 | | - |
95 | | - yield* apiClient.POST( |
96 | | - "/pets", |
97 | | - { |
98 | | - // Body can be typed object - client will auto-stringify and set Content-Type |
99 | | - body: { name: "Fluffy", tag: "cat" } |
100 | | - } |
101 | | - ).pipe( |
102 | | - Effect.tap((result) => Console.log(`Created pet: ${result.body.id}`)) |
103 | | - ) |
104 | | -}).pipe( |
105 | | - // Handle HttpError with EXHAUSTIVE matching |
106 | | - Effect.catchTag("HttpError", (error) => |
107 | | - Match.value(error.status).pipe( |
108 | | - Match.when(400, () => Console.log(`Validation error: ${JSON.stringify(error.body)}`)), |
109 | | - Match.when(500, () => Console.log(`Server error: ${JSON.stringify(error.body)}`)), |
110 | | - // Match.exhaustive forces handling 400 AND 500 |
111 | | - Match.exhaustive |
112 | | - )), |
113 | | - // Handle ALL boundary errors |
114 | | - Effect.catchTag("TransportError", (e) => Console.log(`Transport error: ${e.error.message}`)), |
115 | | - Effect.catchTag("UnexpectedStatus", (e) => Console.log(`Unexpected status: ${e.status}`)), |
116 | | - Effect.catchTag("UnexpectedContentType", (e) => Console.log(`Unexpected content-type: ${e.actual ?? "unknown"}`)), |
117 | | - Effect.catchTag("ParseError", (e) => Console.log(`Parse error: ${e.error.message}`)), |
118 | | - Effect.catchTag("DecodeError", (e) => Console.log(`Decode error: ${e.error.message}`)) |
119 | | -) |
120 | | - |
121 | | -// ============================================================================= |
122 | | -// STRICT EXAMPLE 3: listPets - handles 500 + all boundary errors |
123 | | -// ============================================================================= |
| 29 | +const apiClient = createClientEffect<Paths>(clientOptions) |
124 | 30 |
|
125 | | -/** |
126 | | - * listPets strict handler |
127 | | - * |
128 | | - * Schema: listPets has responses 200 (success), 500 (error) |
129 | | - * Error channel: HttpError<500> | BoundaryError |
130 | | - * |
131 | | - * @invariant After catchTags, E = never |
132 | | - * @effect Effect<void, never, HttpClient> |
133 | | - */ |
134 | | -export const listPetsStrictProgram: Effect.Effect<void, never, HttpClient.HttpClient> = Effect.gen(function*() { |
135 | | - yield* Console.log("=== listPets: Strict Error Handling ===") |
136 | | - |
137 | | - yield* apiClient.GET("/pets", { query: { limit: 10 } }).pipe( |
138 | | - Effect.tap((result) => Console.log(`Got ${result.body.length} pets`)) |
139 | | - ) |
140 | | - |
141 | | - const pets = yield* apiClient.GET("/pets", { query: { limit: 10 } }) |
142 | | - |
143 | | - yield* Console.log(`Got ${pets.body.length} pets`) |
| 31 | +const listPetsProgram = apiClient.GET("/pets", { |
| 32 | + params: { query: { limit: 10 } } |
144 | 33 | }).pipe( |
145 | | - // Handle HttpError with EXHAUSTIVE matching |
146 | | - Effect.catchTag("HttpError", (error) => |
147 | | - Match.value(error.status).pipe( |
148 | | - Match.when(500, () => Console.log(`Server error: ${JSON.stringify(error.body)}`)), |
149 | | - // Match.exhaustive - only 500 needs handling for listPets |
| 34 | + Effect.flatMap((success) => |
| 35 | + Match.value(success).pipe( |
| 36 | + Match.when({ status: 200 }, ({ body }) => Console.log(`Got ${body.length} pets`)), |
150 | 37 | Match.exhaustive |
151 | | - )), |
152 | | - // Handle ALL boundary errors |
153 | | - Effect.catchTag("TransportError", (e) => Console.log(`Transport error: ${e.error.message}`)), |
154 | | - Effect.catchTag("UnexpectedStatus", (e) => Console.log(`Unexpected status: ${e.status}`)), |
155 | | - Effect.catchTag("UnexpectedContentType", (e) => Console.log(`Unexpected content-type: ${e.actual ?? "unknown"}`)), |
156 | | - Effect.catchTag("ParseError", (e) => Console.log(`Parse error: ${e.error.message}`)), |
157 | | - Effect.catchTag("DecodeError", (e) => Console.log(`Decode error: ${e.error.message}`)) |
158 | | -) |
159 | | - |
160 | | -// ============================================================================= |
161 | | -// MAIN: Run all strict programs |
162 | | -// ============================================================================= |
163 | | - |
164 | | -/** |
165 | | - * Main program combines all strict examples |
166 | | - * Type annotation proves E=never: Effect<void, never, HttpClient> |
167 | | - */ |
168 | | -const mainProgram: Effect.Effect<void, never, HttpClient.HttpClient> = Effect.gen(function*() { |
169 | | - yield* Console.log("========================================") |
170 | | - yield* Console.log(" Strict Error Handling Examples") |
171 | | - yield* Console.log(" (All have E=never)") |
172 | | - yield* Console.log("========================================\n") |
173 | | - |
174 | | - // All these programs have E=never - errors fully handled |
175 | | - yield* getPetStrictProgram |
176 | | - yield* Console.log("") |
177 | | - |
178 | | - yield* createPetStrictProgram |
179 | | - yield* Console.log("") |
180 | | - |
181 | | - yield* listPetsStrictProgram |
182 | | - |
183 | | - yield* Console.log("\n========================================") |
184 | | - yield* Console.log(" All errors handled - E=never verified!") |
185 | | - yield* Console.log("========================================") |
186 | | -}) |
187 | | - |
188 | | -// CHANGE: Remove async/await entrypoint and handle defects in Effect |
189 | | -// WHY: Lint rules forbid async/await and floating promises; defects are handled in Effect channel |
190 | | -// QUOTE(ТЗ): "Запрещён async/await — используй Effect.gen / Effect.tryPromise." |
191 | | -// REF: user-msg-2 |
192 | | -// SOURCE: n/a |
193 | | -// FORMAT THEOREM: For all exits, mainProgram E=never implies failure(exit) -> defect(exit) |
194 | | -// PURITY: SHELL |
195 | | -// EFFECT: Effect<void, never, HttpClient> -> Promise<void> via Effect.runPromise |
196 | | -// INVARIANT: Typed error channel remains never |
197 | | -// COMPLEXITY: O(1) |
198 | | -const program = mainProgram.pipe( |
199 | | - Effect.provide(FetchHttpClient.layer), |
200 | | - Effect.catchAllCause((cause) => Console.error(`Unexpected defect: ${Cause.pretty(cause)}`)) |
| 38 | + ) |
| 39 | + ), |
| 40 | + Effect.catchTags({ |
| 41 | + HttpError: (error) => |
| 42 | + Match.value(error.status).pipe( |
| 43 | + Match.when(500, () => Console.log(`Server error: ${error.body.message}`)), |
| 44 | + Match.exhaustive |
| 45 | + ), |
| 46 | + TransportError: ({ error }) => Console.log(`Transport error: ${error.message}`), |
| 47 | + UnexpectedStatus: ({ body, status }) => Console.log(`Unexpected status ${status}: ${body}`), |
| 48 | + UnexpectedContentType: ({ actual, expected }) => |
| 49 | + Console.log(`Unexpected content type ${actual ?? "unknown"}; expected ${expected.join(", ")}`), |
| 50 | + ParseError: ({ error }) => Console.log(`Parse error: ${error.message}`), |
| 51 | + DecodeError: ({ error }) => Console.log(`Decode error: ${error.message}`) |
| 52 | + }) |
201 | 53 | ) |
202 | 54 |
|
203 | | -void Effect.runPromise(program) |
| 55 | +export const strictErrorHandlingProgram: Effect.Effect<void> = listPetsProgram |
0 commit comments