88// INVARIANT: All operations are type-safe from path → operation → request → response
99// COMPLEXITY: O(1) client creation
1010
11+ import type * as HttpClient from "@effect/platform/HttpClient"
12+ import { Effect } from "effect"
1113import type { HttpMethod } from "openapi-typescript-helpers"
1214
1315import { asDispatchersFor , asStrictApiClient , asStrictRequestInit , type Dispatcher } from "../../core/axioms.js"
1416import type {
17+ ClientEffect ,
1518 ClientOptions ,
1619 DispatchersFor ,
1720 DispatchersForMethod ,
@@ -21,6 +24,7 @@ import type { StrictRequestInit } from "./strict-client.js"
2124import { createUniversalDispatcher , executeRequest } from "./strict-client.js"
2225
2326export type {
27+ ClientEffect ,
2428 ClientOptions ,
2529 DispatchersFor ,
2630 StrictApiClient ,
@@ -349,22 +353,57 @@ const createMethodHandlerWithUniversalDispatcher = (
349353 options
350354 )
351355
356+ type HttpErrorTag = { readonly _tag : "HttpError" }
357+
358+ const isHttpErrorValue = ( error : unknown ) : error is HttpErrorTag =>
359+ typeof error === "object"
360+ && error !== null
361+ && "_tag" in error
362+ && Reflect . get ( error , "_tag" ) === "HttpError"
363+
364+ const exposeHttpErrorsAsValues = < A , E > (
365+ request : Effect . Effect < A , E , HttpClient . HttpClient >
366+ ) : Effect . Effect <
367+ A | Extract < E , HttpErrorTag > ,
368+ Exclude < E , Extract < E , HttpErrorTag > > ,
369+ HttpClient . HttpClient
370+ > =>
371+ request . pipe (
372+ Effect . catchIf (
373+ ( error ) : error is Extract < E , HttpErrorTag > => isHttpErrorValue ( error ) ,
374+ ( error ) => Effect . succeed ( error )
375+ )
376+ )
377+
378+ const createMethodHandlerWithUniversalDispatcherValue = (
379+ method : HttpMethod ,
380+ clientOptions : ClientOptions
381+ ) =>
382+ (
383+ path : string ,
384+ options ?: MethodHandlerOptions
385+ ) =>
386+ exposeHttpErrorsAsValues (
387+ createMethodHandlerWithUniversalDispatcher ( method , clientOptions ) ( path , options )
388+ )
389+
352390// CHANGE: Add createClientEffect — zero-boilerplate Effect-based API client
353391// WHY: Enable the user's desired DSL without any generated code or dispatcher setup
354392// QUOTE(ТЗ): "const apiClientEffect = createClientEffect<Paths>(clientOptions); apiClientEffect.POST('/api/auth/login', { body: credentials })"
355393// REF: issue-5
356394// SOURCE: n/a
357- // FORMAT THEOREM: ∀ Paths, options: createClientEffect<Paths>(options) → StrictApiClientWithDispatchers <Paths>
395+ // FORMAT THEOREM: ∀ Paths, options: createClientEffect<Paths>(options) → ClientEffect <Paths>
358396// PURITY: SHELL
359- // EFFECT: Client methods return Effect<ApiSuccess, ApiFailure , HttpClient>
397+ // EFFECT: Client methods return Effect<ApiSuccess | HttpError, BoundaryError , HttpClient>
360398// INVARIANT: ∀ path, method: path ∈ PathsForMethod<Paths, method> (compile-time) ∧ response classified by status range (runtime)
361399// COMPLEXITY: O(1) client creation
362400/**
363401 * Create type-safe Effect-based API client with zero boilerplate
364402 *
365- * Uses a universal dispatcher that classifies responses by HTTP status range:
366- * - 2xx → success channel (ApiSuccess)
367- * - non-2xx → error channel (HttpError)
403+ * Uses a universal dispatcher and exposes HTTP statuses as values:
404+ * - 2xx → success value (ApiSuccess)
405+ * - non-2xx schema statuses → success value (HttpError with _tag)
406+ * - boundary/protocol failures stay in error channel
368407 * - JSON parsed automatically for application/json content types
369408 *
370409 * **No code generation needed.** No dispatcher registry needed.
@@ -398,14 +437,14 @@ const createMethodHandlerWithUniversalDispatcher = (
398437 */
399438export const createClientEffect = < Paths extends object > (
400439 options : ClientOptions
401- ) : StrictApiClientWithDispatchers < Paths > => {
402- return asStrictApiClient < StrictApiClientWithDispatchers < Paths > > ( {
403- GET : createMethodHandlerWithUniversalDispatcher ( "get" , options ) ,
404- POST : createMethodHandlerWithUniversalDispatcher ( "post" , options ) ,
405- PUT : createMethodHandlerWithUniversalDispatcher ( "put" , options ) ,
406- DELETE : createMethodHandlerWithUniversalDispatcher ( "delete" , options ) ,
407- PATCH : createMethodHandlerWithUniversalDispatcher ( "patch" , options ) ,
408- HEAD : createMethodHandlerWithUniversalDispatcher ( "head" , options ) ,
409- OPTIONS : createMethodHandlerWithUniversalDispatcher ( "options" , options )
440+ ) : ClientEffect < Paths > => {
441+ return asStrictApiClient < ClientEffect < Paths > > ( {
442+ GET : createMethodHandlerWithUniversalDispatcherValue ( "get" , options ) ,
443+ POST : createMethodHandlerWithUniversalDispatcherValue ( "post" , options ) ,
444+ PUT : createMethodHandlerWithUniversalDispatcherValue ( "put" , options ) ,
445+ DELETE : createMethodHandlerWithUniversalDispatcherValue ( "delete" , options ) ,
446+ PATCH : createMethodHandlerWithUniversalDispatcherValue ( "patch" , options ) ,
447+ HEAD : createMethodHandlerWithUniversalDispatcherValue ( "head" , options ) ,
448+ OPTIONS : createMethodHandlerWithUniversalDispatcherValue ( "options" , options )
410449 } )
411450}
0 commit comments