Skip to content

Commit 9a993a2

Browse files
konardclaude
andcommitted
feat(app): export createClientEffect from package entry point and add CI integration test
- Export createClientEffect and createUniversalDispatcher from src/index.ts so `import { createClientEffect, type ClientOptions } from "openapi-effect"` works - Add integration test verifying the exact user DSL snippet from issue #5: createClientEffect<paths>(options).POST("/api/auth/login", { body: credentials }) - 5 new tests: POST with body, yield* pattern, error handling, GET, 204 no-content Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 14fab61 commit 9a993a2

2 files changed

Lines changed: 187 additions & 1 deletion

File tree

packages/app/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export type {
1414
StrictApiClient,
1515
StrictApiClientWithDispatchers
1616
} from "./shell/api-client/create-client.js"
17-
export { registerDefaultDispatchers } from "./shell/api-client/create-client.js"
17+
export { createClientEffect, registerDefaultDispatchers } from "./shell/api-client/create-client.js"
1818

1919
// Core types (for advanced type manipulation)
2020
// Effect Channel Design:
@@ -48,6 +48,7 @@ export type { Decoder, Dispatcher, RawResponse, StrictClient, StrictRequestInit
4848
export {
4949
createDispatcher,
5050
createStrictClient,
51+
createUniversalDispatcher,
5152
executeRequest,
5253
parseJSON,
5354
unexpectedContentType,
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
// CHANGE: Integration test verifying the exact user DSL snippet from issue #5
2+
// WHY: Ensure the import/usage pattern requested by the user compiles and works end-to-end
3+
// QUOTE(ТЗ): "import { createClientEffect, type ClientOptions } from 'openapi-effect' ... apiClientEffect.POST('/api/auth/login', { body: credentials })"
4+
// REF: issue-5, PR#6 comment from skulidropek
5+
// SOURCE: n/a
6+
// FORMAT THEOREM: ∀ Paths, options: createClientEffect<Paths>(options).POST(path, { body }) → Effect<ApiSuccess, ApiFailure, HttpClient>
7+
// PURITY: SHELL
8+
// EFFECT: Effect<void, never, never>
9+
// INVARIANT: The exact user snippet compiles and produces correct runtime behavior
10+
// COMPLEXITY: O(1) per test
11+
12+
import * as HttpClient from "@effect/platform/HttpClient"
13+
import * as HttpClientResponse from "@effect/platform/HttpClientResponse"
14+
import { Effect, Either, Layer } from "effect"
15+
import { describe, expect, it } from "vitest"
16+
17+
// CHANGE: Import via the package's main entry point (src/index.ts)
18+
// WHY: Verify the user's import pattern `from "openapi-effect"` resolves correctly
19+
// REF: issue-5
20+
import { createClientEffect } from "../../src/index.js"
21+
import type { ClientOptions } from "../../src/index.js"
22+
23+
// CHANGE: Import paths from the auth OpenAPI schema
24+
// WHY: Match the user's pattern `import type { paths } from "./openapi.d.ts"`
25+
// REF: issue-5
26+
import type { paths } from "../../src/core/api/openapi.js"
27+
28+
/**
29+
* Create a mock HttpClient layer that returns a fixed response
30+
*
31+
* @pure true - returns pure layer
32+
*/
33+
const createMockHttpClientLayer = (
34+
status: number,
35+
headers: Record<string, string>,
36+
body: string
37+
): Layer.Layer<HttpClient.HttpClient> =>
38+
Layer.succeed(
39+
HttpClient.HttpClient,
40+
HttpClient.make(
41+
(request) =>
42+
Effect.succeed(
43+
HttpClientResponse.fromWeb(
44+
request,
45+
status === 204 || status === 304
46+
? new Response(null, { status, headers: new Headers(headers) })
47+
: new Response(body, { status, headers: new Headers(headers) })
48+
)
49+
)
50+
)
51+
)
52+
53+
/**
54+
* Test fixtures for integration tests
55+
*
56+
* @pure true - immutable test data factories
57+
*/
58+
const fixtures = {
59+
loginBody: () => ({ email: "user@example.com", password: `test-${Date.now()}` }),
60+
wrongLoginBody: () => ({ email: "user@example.com", password: `wrong-${Date.now()}` })
61+
} as const
62+
63+
// =============================================================================
64+
// SECTION: Exact user snippet integration test (CI/CD check)
65+
// =============================================================================
66+
67+
describe("CI/CD: exact user snippet from issue #5", () => {
68+
// --- The exact code from the user's PR comment ---
69+
const clientOptions: ClientOptions = {
70+
baseUrl: "https://petstore.example.com",
71+
credentials: "include"
72+
}
73+
const apiClientEffect = createClientEffect<paths>(clientOptions)
74+
75+
it("should compile and execute: apiClientEffect.POST('/api/auth/login', { body: credentials })", () =>
76+
Effect.gen(function*() {
77+
const credentials = fixtures.loginBody()
78+
const successBody = JSON.stringify({
79+
id: "550e8400-e29b-41d4-a716-446655440000",
80+
email: "user@example.com",
81+
firstName: "John",
82+
lastName: "Doe",
83+
profileImageUrl: null,
84+
emailVerified: true,
85+
phoneVerified: false
86+
})
87+
88+
// Type-safe — path, method, and body all enforced at compile time
89+
const result = yield* Effect.either(
90+
apiClientEffect.POST("/api/auth/login", {
91+
body: credentials
92+
}).pipe(
93+
Effect.provide(
94+
createMockHttpClientLayer(200, { "content-type": "application/json" }, successBody)
95+
)
96+
)
97+
)
98+
99+
expect(Either.isRight(result)).toBe(true)
100+
if (Either.isRight(result)) {
101+
expect(result.right.status).toBe(200)
102+
expect(result.right.contentType).toBe("application/json")
103+
const body = result.right.body as { id: string; email: string }
104+
expect(body.email).toBe("user@example.com")
105+
}
106+
}).pipe(Effect.runPromise))
107+
108+
it("should compile and execute: yield* apiClientEffect.POST (inside Effect.gen)", () =>
109+
Effect.gen(function*() {
110+
const credentials = fixtures.loginBody()
111+
const successBody = JSON.stringify({
112+
id: "550e8400-e29b-41d4-a716-446655440000",
113+
email: "user@example.com"
114+
})
115+
116+
// This verifies the `yield*` pattern from the user's snippet
117+
const result = yield* apiClientEffect.POST("/api/auth/login", {
118+
body: credentials
119+
}).pipe(
120+
Effect.provide(
121+
createMockHttpClientLayer(200, { "content-type": "application/json" }, successBody)
122+
)
123+
)
124+
125+
expect(result.status).toBe(200)
126+
expect(result.contentType).toBe("application/json")
127+
const body = result.body as { email: string }
128+
expect(body.email).toBe("user@example.com")
129+
}).pipe(Effect.runPromise))
130+
131+
it("should handle error responses via Effect error channel", () =>
132+
Effect.gen(function*() {
133+
const credentials = fixtures.wrongLoginBody()
134+
const errorBody = JSON.stringify({ error: "invalid_credentials" })
135+
136+
const result = yield* Effect.either(
137+
apiClientEffect.POST("/api/auth/login", {
138+
body: credentials
139+
}).pipe(
140+
Effect.provide(
141+
createMockHttpClientLayer(401, { "content-type": "application/json" }, errorBody)
142+
)
143+
)
144+
)
145+
146+
expect(Either.isLeft(result)).toBe(true)
147+
if (Either.isLeft(result)) {
148+
expect(result.left).toMatchObject({
149+
_tag: "HttpError",
150+
status: 401
151+
})
152+
}
153+
}).pipe(Effect.runPromise))
154+
155+
it("should work with GET requests (no body required)", () =>
156+
Effect.gen(function*() {
157+
const profileBody = JSON.stringify({
158+
id: "550e8400-e29b-41d4-a716-446655440000",
159+
email: "user@example.com"
160+
})
161+
162+
const result = yield* apiClientEffect.GET("/api/auth/me").pipe(
163+
Effect.provide(
164+
createMockHttpClientLayer(200, { "content-type": "application/json" }, profileBody)
165+
)
166+
)
167+
168+
expect(result.status).toBe(200)
169+
const body = result.body as { email: string }
170+
expect(body.email).toBe("user@example.com")
171+
}).pipe(Effect.runPromise))
172+
173+
it("should handle 204 no-content responses", () =>
174+
Effect.gen(function*() {
175+
const result = yield* apiClientEffect.POST("/api/auth/logout").pipe(
176+
Effect.provide(
177+
createMockHttpClientLayer(204, {}, "")
178+
)
179+
)
180+
181+
expect(result.status).toBe(204)
182+
expect(result.contentType).toBe("none")
183+
expect(result.body).toBeUndefined()
184+
}).pipe(Effect.runPromise))
185+
})

0 commit comments

Comments
 (0)