Skip to content

Commit 7121329

Browse files
authored
[codex] trim package to openapi-effect (#22)
* feat(api-client): infer createClientEffect output channels * chore(api-client): trim package to openapi-effect
1 parent c2e1985 commit 7121329

32 files changed

Lines changed: 96 additions & 3781 deletions

packages/app/eslint.config.mts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import eslint from '@eslint/js';
44
import { defineConfig } from 'eslint/config';
55
import tseslint from 'typescript-eslint';
66
import vitest from "@vitest/eslint-plugin";
7-
import suggestMembers from "@prover-coder-ai/eslint-plugin-suggest-members";
87
import sonarjs from "eslint-plugin-sonarjs";
98
import unicorn from "eslint-plugin-unicorn";
109
import * as effectEslint from "@effect/eslint-plugin";
@@ -37,7 +36,6 @@ export default defineConfig(
3736
eslint.configs.recommended,
3837
tseslint.configs.strictTypeChecked,
3938
effectEslint.configs.dprint,
40-
suggestMembers.configs.recommended,
4139
eslintCommentsConfigs.recommended,
4240
{
4341
name: "analyzers",
Lines changed: 33 additions & 181 deletions
Original file line numberDiff line numberDiff line change
@@ -1,203 +1,55 @@
11
// 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
33
// QUOTE(ТЗ): "Приёмка по смыслу: после catchTags(...) тип ошибки становится never"
44
// REF: PR#3 blocking review from skulidropek
55
// SOURCE: n/a
66
// PURITY: SHELL
7-
// EFFECT: Effect<void, never, HttpClient> - all errors handled
7+
// EFFECT: Effect<void, never, never> - all errors handled
88

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"
1412
import type { Paths } from "../tests/fixtures/petstore.openapi.js"
1513

16-
/**
17-
* Client configuration
18-
*/
1914
const clientOptions: ClientOptions = {
2015
baseUrl: "https://petstore.example.com",
2116
credentials: "include"
2217
}
2318

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
2823
// 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>
3025
// PURITY: SHELL
3126
// 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
3328
// 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)
12430

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 } }
14433
}).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`)),
15037
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+
})
20153
)
20254

203-
void Effect.runPromise(program)
55+
export const strictErrorHandlingProgram: Effect.Effect<void> = listPetsProgram

0 commit comments

Comments
 (0)