diff --git a/packages/effect-http-node/examples/example.ts b/packages/effect-http-node/examples/example.ts index 328a0390d..49433299d 100644 --- a/packages/effect-http-node/examples/example.ts +++ b/packages/effect-http-node/examples/example.ts @@ -26,9 +26,17 @@ const getLesnekEndpoint = Api.get("getLesnek", "/lesnek").pipe( Api.setRequestQuery(Lesnek), Api.setSecurity(Security.bearer({ name: "myAwesomeBearerAuth", bearerFormat: "JWT" })) ) -const getMilanEndpoint = Api.get("getMilan", "/milan").pipe(Api.setResponseBody(Schema.String)) -const testEndpoint = Api.get("test", "/test").pipe(Api.setResponseBody(Standa), Api.setRequestQuery(Lesnek)) -const standaEndpoint = Api.post("standa", "/standa").pipe(Api.setResponseBody(Standa), Api.setRequestBody(Standa)) +const getMilanEndpoint = Api.get("getMilan", "/milan").pipe( + Api.setResponseBody(Schema.String) +) +const testEndpoint = Api.get("test", "/test").pipe( + Api.setResponseBody(Standa), + Api.setRequestQuery(Lesnek) +) +const standaEndpoint = Api.post("standa", "/standa").pipe( + Api.setResponseBody(Standa), + Api.setRequestBody(Standa) +) const handleMilanEndpoint = Api.post("handleMilan", "/petr").pipe( Api.setResponseBody(HumanSchema), Api.setRequestBody(HumanSchema) @@ -39,35 +47,38 @@ const callStandaEndpoint = Api.put("callStanda", "/api/zdar").pipe( ) const api = Api.make({ title: "My awesome pets API", version: "1.0.0" }).pipe( - Api.addEndpoint(getLesnekEndpoint), - Api.addEndpoint(getMilanEndpoint), - Api.addEndpoint(testEndpoint), - Api.addEndpoint(standaEndpoint), - Api.addEndpoint(handleMilanEndpoint), - Api.addEndpoint(callStandaEndpoint) + Api.addEndpoints( + getLesnekEndpoint, + getMilanEndpoint, + testEndpoint, + standaEndpoint, + handleMilanEndpoint, + callStandaEndpoint + ) ) -const getLesnekHandler = Handler.make(getLesnekEndpoint, ({ query }) => - Effect.succeed(`hello ${query.name}`).pipe( - Effect.tap(() => Effect.logDebug("hello world")) - )) -const handleMilanHandler = Handler.make(handleMilanEndpoint, ({ body }) => - Effect.map(StuffService, ({ value }) => ({ - ...body, - randomValue: body.height + value - }))) +const getLesnekHandler = Handler.make( + getLesnekEndpoint, + ({ query }) => Effect.tap(Effect.succeed(`hello ${query.name}`), () => Effect.logDebug("hello world")) +) +const handleMilanHandler = Handler.make( + handleMilanEndpoint, + ({ body }) => Effect.map(StuffService, ({ value }) => ({ ...body, randomValue: body.height + value })) +) const getMilanHandler = Handler.make(getMilanEndpoint, () => Effect.succeed("Milan")) const testHandler = Handler.make(testEndpoint, ({ query }) => Effect.succeed(query)) const standaHandler = Handler.make(standaEndpoint, ({ body }) => Effect.succeed(body)) const callStandaHandler = Handler.make(callStandaEndpoint, ({ body }) => Effect.succeed(body.zdar)) const app = RouterBuilder.make(api, { parseOptions: { errors: "all" } }).pipe( - RouterBuilder.handle(getLesnekHandler), - RouterBuilder.handle(handleMilanHandler), - RouterBuilder.handle(getMilanHandler), - RouterBuilder.handle(testHandler), - RouterBuilder.handle(standaHandler), - RouterBuilder.handle(callStandaHandler), + RouterBuilder.handle( + getLesnekHandler, + handleMilanHandler, + getMilanHandler, + testHandler, + standaHandler, + callStandaHandler + ), RouterBuilder.build ) diff --git a/packages/effect-http/dtslint/RouterBuilder.ts b/packages/effect-http/dtslint/RouterBuilder.ts new file mode 100644 index 000000000..f7d76c48b --- /dev/null +++ b/packages/effect-http/dtslint/RouterBuilder.ts @@ -0,0 +1,41 @@ +import type { Schema } from "@effect/schema" +import { Effect } from "effect" +import { Api, Handler, RouterBuilder } from "effect-http" + +declare const bodySchema: Schema.Schema + +const getArticleEndpoint = Api.get("getArticle", "/article").pipe( + Api.setResponseBody(bodySchema) +) +const getBookEndpoint = Api.get("getBook", "/book") + +const api = Api.make().pipe( + Api.addEndpoint(getArticleEndpoint), + Api.addEndpoint(getBookEndpoint) +) + +declare const getArticleHandler: Handler.Handler +declare const getBookHandler: Handler.Handler + +// $ExpectType RouterBuilder +RouterBuilder.make(api).pipe( + RouterBuilder.handle(getArticleHandler, getBookHandler) +) + +const handler1 = Handler.make(getArticleEndpoint, () => Effect.succeed("article")) +const handler2 = Handler.make(getBookEndpoint, () => Effect.void) + +// $ExpectType RouterBuilder +RouterBuilder.make(api).pipe( + RouterBuilder.handle(handler1, handler2) +) + +const api2 = Api.make().pipe( + Api.addEndpoint(getBookEndpoint) +) + +// $ExpectType Default +RouterBuilder.make(api2).pipe( + RouterBuilder.handle(handler2), + RouterBuilder.build +) diff --git a/packages/effect-http/src/Api.ts b/packages/effect-http/src/Api.ts index aeeeb2f4d..0982a85b2 100644 --- a/packages/effect-http/src/Api.ts +++ b/packages/effect-http/src/Api.ts @@ -124,6 +124,14 @@ export const addEndpoint: ( endpoint: E2 ) => (api: Api) => Api = internal.addEndpoint +/** + * @category combining + * @since 1.0.0 + */ +export const addEndpoints: >( + ...endpoint: Endpoints +) => (api: Api) => Api = internal.addEndpoints + /** * @category combining * @since 1.0.0 diff --git a/packages/effect-http/src/ApiEndpoint.ts b/packages/effect-http/src/ApiEndpoint.ts index 5d767cf6d..833787c65 100644 --- a/packages/effect-http/src/ApiEndpoint.ts +++ b/packages/effect-http/src/ApiEndpoint.ts @@ -89,6 +89,19 @@ export declare namespace ApiEndpoint { */ export type Any = ApiEndpoint + /** + * Any endpoint with `Request = Request.Any` and `Response = Response.Any`. + * + * @category models + * @since 1.0.0 + */ + export type Unknown = ApiEndpoint< + AnyId, + ApiRequest.ApiRequest.Unknown, + ApiResponse.ApiResponse.Unknown, + Security.Security.Unknown + > + /** * Default endpoint spec. * diff --git a/packages/effect-http/src/ApiRequest.ts b/packages/effect-http/src/ApiRequest.ts index 3bf5c0406..0d85442fe 100644 --- a/packages/effect-http/src/ApiRequest.ts +++ b/packages/effect-http/src/ApiRequest.ts @@ -53,6 +53,14 @@ export declare namespace ApiRequest { */ export type Any = ApiRequest + /** + * Any request with all `Body`, `Path`, `Query` and `Headers` set to `any`. + * + * @category models + * @since 1.0.0 + */ + export type Unknown = ApiRequest + /** * Default request. * diff --git a/packages/effect-http/src/ApiResponse.ts b/packages/effect-http/src/ApiResponse.ts index 7f0c4dbb7..3410a4b73 100644 --- a/packages/effect-http/src/ApiResponse.ts +++ b/packages/effect-http/src/ApiResponse.ts @@ -63,6 +63,14 @@ export declare namespace ApiResponse { */ export type Any = ApiResponse + /** + * Any request with all `Body`, `Path`, `Query` and `Headers` set to `Schema.Schema.Any`. + * + * @category models + * @since 1.0.0 + */ + export type Unknown = ApiResponse + /** * Default response. * diff --git a/packages/effect-http/src/Handler.ts b/packages/effect-http/src/Handler.ts index 5aad82b88..fbee58f65 100644 --- a/packages/effect-http/src/Handler.ts +++ b/packages/effect-http/src/Handler.ts @@ -32,7 +32,7 @@ export type TypeId = typeof TypeId * @category models * @since 1.0.0 */ -export interface Handler +export interface Handler extends Handler.Variance, Pipeable.Pipeable {} @@ -78,6 +78,30 @@ export declare namespace Handler { */ export type Any = Handler + /** + * @category models + * @since 1.0.0 + */ + export type Unknown = Handler + + /** + * @category models + * @since 1.0.0 + */ + export type Endpoint = [H] extends [Handler] ? A : never + + /** + * @category models + * @since 1.0.0 + */ + export type Error = [H] extends [Handler] ? E : never + + /** + * @category models + * @since 1.0.0 + */ + export type Context = [H] extends [Handler] ? C : never + /** * @category models * @since 1.0.0 @@ -170,3 +194,11 @@ export const getRoute: ( export const getEndpoint: ( handler: Handler ) => A = internal.getEndpoint + +/** + * @category refinements + * @since 1.0.0 + */ +export const isHandler: ( + u: unknown +) => u is Handler = internal.isHandler diff --git a/packages/effect-http/src/RouterBuilder.ts b/packages/effect-http/src/RouterBuilder.ts index 5d75d30b2..08dcea31a 100644 --- a/packages/effect-http/src/RouterBuilder.ts +++ b/packages/effect-http/src/RouterBuilder.ts @@ -133,13 +133,14 @@ export const handleRaw: < * @since 1.0.0 */ export const handle: { - ( - handler: Handler.Handler - ): (builder: RouterBuilder) => RouterBuilder< - Exclude, - E1 | Exclude, - | Exclude - | ApiEndpoint.ApiEndpoint.Context + >( + ...handler: H + ): (builder: RouterBuilder) => RouterBuilder< + Exclude>, + E1 | Exclude, HttpError.HttpError>, + | Exclude, HttpRouter.RouteContext | HttpServerRequest.HttpServerRequest> + | Handler.Handler.Context + | ApiEndpoint.ApiEndpoint.Context> > >( diff --git a/packages/effect-http/src/Security.ts b/packages/effect-http/src/Security.ts index 435d5107a..dc7d6c422 100644 --- a/packages/effect-http/src/Security.ts +++ b/packages/effect-http/src/Security.ts @@ -54,6 +54,12 @@ export declare namespace Security { */ export type Any = Security + /** + * @category models + * @since 1.0.0 + */ + export type Unknown = Security + /** * @category models * @since 1.0.0 diff --git a/packages/effect-http/src/internal/api.ts b/packages/effect-http/src/internal/api.ts index 36b1ab6bc..7893a0666 100644 --- a/packages/effect-http/src/internal/api.ts +++ b/packages/effect-http/src/internal/api.ts @@ -1,3 +1,4 @@ +import { Iterable } from "effect" import * as Array from "effect/Array" import * as Pipeable from "effect/Pipeable" import type * as Api from "../Api.js" @@ -62,6 +63,12 @@ export const addEndpoint = return new ApiImpl([...groupsWithoutDefault, newDefaultGroup], api.options) as any } +/** @internal */ +export const addEndpoints = + >(...endpoints: A2) => + (api: Api.Api): Api.Api => + Iterable.reduce(endpoints, api as Api.Api, (api, endpoint) => addEndpoint(endpoint)(api)) + /** @internal */ export const addGroup = ( group: ApiGroup.ApiGroup diff --git a/packages/effect-http/src/internal/handler.ts b/packages/effect-http/src/internal/handler.ts index c373a0338..d849d292a 100644 --- a/packages/effect-http/src/internal/handler.ts +++ b/packages/effect-http/src/internal/handler.ts @@ -118,3 +118,7 @@ export const getRoute = ( export const getEndpoint = ( handler: Handler.Handler ): A => (handler as HandlerImpl).endpoint + +/** @internal */ +export const isHandler = (u: unknown): u is Handler.Handler.Unknown => + typeof u === "object" && u !== null && TypeId in u diff --git a/packages/effect-http/src/internal/router-builder.ts b/packages/effect-http/src/internal/router-builder.ts index e919b2988..acaa9e223 100644 --- a/packages/effect-http/src/internal/router-builder.ts +++ b/packages/effect-http/src/internal/router-builder.ts @@ -3,6 +3,7 @@ import * as HttpRouter from "@effect/platform/HttpRouter" import type * as HttpServerRequest from "@effect/platform/HttpServerRequest" import * as Effect from "effect/Effect" import { dual } from "effect/Function" +import * as Iterable from "effect/Iterable" import * as Pipeable from "effect/Pipeable" import type * as Scope from "effect/Scope" @@ -113,22 +114,38 @@ const getRemainingEndpoint = < /** @internal */ export const handle = (...args: Array) => (builder: RouterBuilder.RouterBuilder.Any): any => { - if (args.length === 2) { - const [id, handler] = args - const endpoint = getRemainingEndpoint(builder, id) - return handle(Handler.make(endpoint, handler))(builder) + if (Handler.isHandler(args[0])) { + return (args as ReadonlyArray).reduce((builder, handler) => { + const remainingEndpoints = removeRemainingEndpoint( + builder, + ApiEndpoint.getId(Handler.getEndpoint(handler)) + ) + const router = addRoute(builder.router, Handler.getRoute(handler)) + + return new RouterBuilderImpl(remainingEndpoints, builder.api, router, builder.options) + }, builder) } - const handler = args[0] as Handler.Handler.Any - const remainingEndpoints = removeRemainingEndpoint( - builder, - ApiEndpoint.getId(Handler.getEndpoint(handler)) - ) - const router = addRoute(builder.router, Handler.getRoute(handler)) - - return new RouterBuilderImpl(remainingEndpoints, builder.api, router, builder.options) + const [id, handler] = args + const endpoint = getRemainingEndpoint(builder, id) + return handle(Handler.make(endpoint, handler))(builder) } +/** @internal */ +export const handleMany = + (handlers: Iterable) => + (builder: RouterBuilder.RouterBuilder) => + Iterable.reduce( + handlers, + builder as RouterBuilder.RouterBuilder< + Exclude>, + E1 | Exclude, HttpError.HttpError>, + | Exclude, HttpRouter.RouteContext | HttpServerRequest.HttpServerRequest> + | ApiEndpoint.ApiEndpoint.Context> + >, + (builder, handler) => handle(handler)(builder) + ) + /** @internal */ export const handler = >( api: A,