Skip to content

Commit

Permalink
add Layer based api for creating HttpRouter's (#3059)
Browse files Browse the repository at this point in the history
  • Loading branch information
tim-smart authored Jun 24, 2024
1 parent 428edf1 commit 2e8e252
Show file tree
Hide file tree
Showing 4 changed files with 283 additions and 10 deletions.
50 changes: 50 additions & 0 deletions .changeset/eleven-mayflies-prove.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
---
"@effect/platform": patch
---

add Layer based api for creating HttpRouter's

```ts
import {
HttpMiddleware,
HttpRouter,
HttpServer,
HttpServerResponse,
} from "@effect/platform";
import { BunHttpServer, BunRuntime } from "@effect/platform-bun";
import { Effect, Layer } from "effect";

// create your router Context.Tag
class UserRouter extends HttpRouter.Tag("UserRouter")<UserRouter>() {}

// create routes with the `.use` api.
// There is also `.useScoped`
const GetUsers = UserRouter.use((router) =>
Effect.gen(function* () {
yield* router.get("/", HttpServerResponse.text("got users"));
}),
);

const CreateUser = UserRouter.use((router) =>
Effect.gen(function* () {
yield* router.post("/", HttpServerResponse.text("created user"));
}),
);

const AllRoutes = Layer.mergeAll(GetUsers, CreateUser);

const ServerLive = BunHttpServer.layer({ port: 3000 });

// access the router with the `.router` api, to create your server
const HttpLive = Layer.unwrapEffect(
Effect.gen(function* () {
return HttpServer.serve(yield* UserRouter.router, HttpMiddleware.logger);
}),
).pipe(
Layer.provide(UserRouter.Live),
Layer.provide(AllRoutes),
Layer.provide(ServerLive),
);

BunRuntime.runMain(Layer.launch(HttpLive));
```
31 changes: 31 additions & 0 deletions packages/platform-bun/examples/http-tag-router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { HttpMiddleware, HttpRouter, HttpServer, HttpServerResponse } from "@effect/platform"
import { BunHttpServer, BunRuntime } from "@effect/platform-bun"
import { Effect, Layer } from "effect"

class UserRouter extends HttpRouter.Tag("UserRouter")<UserRouter>() {}

const GetUsers = UserRouter.use((router) =>
Effect.gen(function*() {
yield* router.get("/", HttpServerResponse.text("got users"))
})
)

const CreateUser = UserRouter.use((router) =>
Effect.gen(function*() {
yield* router.post("/", HttpServerResponse.text("created user"))
})
)

const AllRoutes = Layer.mergeAll(GetUsers, CreateUser)

const ServerLive = BunHttpServer.layer({ port: 3000 })

const HttpLive = Layer.unwrapEffect(Effect.gen(function*() {
return HttpServer.serve(yield* UserRouter.router, HttpMiddleware.logger)
})).pipe(
Layer.provide(UserRouter.Live),
Layer.provide(AllRoutes),
Layer.provide(ServerLive)
)

BunRuntime.runMain(Layer.launch(HttpLive))
107 changes: 101 additions & 6 deletions packages/platform/src/HttpRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type * as Chunk from "effect/Chunk"
import type * as Context from "effect/Context"
import type * as Effect from "effect/Effect"
import type { Inspectable } from "effect/Inspectable"
import type * as Layer from "effect/Layer"
import type * as Option from "effect/Option"
import type * as Scope from "effect/Scope"
import type * as App from "./HttpApp.js"
Expand Down Expand Up @@ -54,10 +55,75 @@ export declare namespace HttpRouter {
/**
* @since 1.0.0
*/
export type ExcludeProvided<A> = Exclude<
A,
RouteContext | ServerRequest.HttpServerRequest | ServerRequest.ParsedSearchParams | Scope.Scope
>
export type Provided = RouteContext | ServerRequest.HttpServerRequest | ServerRequest.ParsedSearchParams | Scope.Scope

/**
* @since 1.0.0
*/
export type ExcludeProvided<A> = Exclude<A, Provided>

/**
* @since 1.0.0
*/
export interface Service<E, R> {
readonly router: Effect.Effect<HttpRouter<E, R>>

readonly addRoute: (route: Route<E, R>) => Effect.Effect<void>

readonly all: (
path: PathInput,
handler: Route.Handler<E, R | Provided>,
options?: { readonly uninterruptible?: boolean | undefined } | undefined
) => Effect.Effect<void>
readonly get: (
path: PathInput,
handler: Route.Handler<E, R | Provided>,
options?: { readonly uninterruptible?: boolean | undefined } | undefined
) => Effect.Effect<void>
readonly post: (
path: PathInput,
handler: Route.Handler<E, R | Provided>,
options?: { readonly uninterruptible?: boolean | undefined } | undefined
) => Effect.Effect<void>
readonly put: (
path: PathInput,
handler: Route.Handler<E, R | Provided>,
options?: { readonly uninterruptible?: boolean | undefined } | undefined
) => Effect.Effect<void>
readonly patch: (
path: PathInput,
handler: Route.Handler<E, R | Provided>,
options?: { readonly uninterruptible?: boolean | undefined } | undefined
) => Effect.Effect<void>
readonly del: (
path: PathInput,
handler: Route.Handler<E, R | Provided>,
options?: { readonly uninterruptible?: boolean | undefined } | undefined
) => Effect.Effect<void>
readonly head: (
path: PathInput,
handler: Route.Handler<E, R | Provided>,
options?: { readonly uninterruptible?: boolean | undefined } | undefined
) => Effect.Effect<void>
readonly options: (
path: PathInput,
handler: Route.Handler<E, R | Provided>,
options?: { readonly uninterruptible?: boolean | undefined } | undefined
) => Effect.Effect<void>
}

/**
* @since 1.0.0
*/
export interface TagClass<Self, Name extends string, E, R> extends Context.Tag<Self, Service<E, R>> {
new(_: never): Context.TagClassShape<`@effect/platform/HttpRouter/${Name}`, Service<E, R>>
readonly Live: Layer.Layer<Self>
readonly router: Effect.Effect<HttpRouter<E, R>, never, Self>
readonly use: <XA, XE, XR>(f: (router: Service<E, R>) => Effect.Effect<XA, XE, XR>) => Layer.Layer<never, XE, XR>
readonly useScoped: <XA, XE, XR>(
f: (router: Service<E, R>) => Effect.Effect<XA, XE, XR>
) => Layer.Layer<never, XE, Exclude<XR, Scope.Scope>>
}
}

/**
Expand Down Expand Up @@ -232,8 +298,7 @@ export const makeRoute: <E, R>(
method: Method.HttpMethod,
path: PathInput,
handler: Route.Handler<E, R>,
prefix?: Option.Option<string>,
uninterruptible?: boolean
options?: { readonly prefix?: string | undefined; readonly uninterruptible?: boolean | undefined } | undefined
) => Route<E, HttpRouter.ExcludeProvided<R>> = internal.makeRoute

/**
Expand All @@ -245,6 +310,28 @@ export const prefixAll: {
<E, R>(self: HttpRouter<E, R>, prefix: PathInput): HttpRouter<E, R>
} = internal.prefixAll

/**
* @since 1.0.0
* @category combinators
*/
export const append: {
<R1, E1>(
route: Route<E1, R1>
): <E, R>(
self: HttpRouter<E, R>
) => HttpRouter<
E1 | E,
R | HttpRouter.ExcludeProvided<R1>
>
<E, R, E1, R1>(
self: HttpRouter<E, R>,
route: Route<E1, R1>
): HttpRouter<
E | E1,
R | HttpRouter.ExcludeProvided<R1>
>
} = internal.append

/**
* @since 1.0.0
* @category combinators
Expand Down Expand Up @@ -634,3 +721,11 @@ export const provideServiceEffect: {
| Exclude<HttpRouter.ExcludeProvided<R1>, Context.Tag.Identifier<T>>
>
} = internal.provideServiceEffect

/**
* @since 1.0.0
* @category tags
*/
export const Tag: <const Name extends string>(
id: Name
) => <Self, E = never, R = never>() => HttpRouter.TagClass<Self, Name, E, R> = internal.Tag
105 changes: 101 additions & 4 deletions packages/platform/src/internal/httpRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import * as Effectable from "effect/Effectable"
import * as FiberRef from "effect/FiberRef"
import { dual } from "effect/Function"
import * as Inspectable from "effect/Inspectable"
import * as Layer from "effect/Layer"
import * as Option from "effect/Option"
import * as Predicate from "effect/Predicate"
import * as Tracer from "effect/Tracer"
Expand Down Expand Up @@ -317,17 +318,30 @@ export const makeRoute = <E, R>(
method: Method.HttpMethod,
path: Router.PathInput,
handler: Router.Route.Handler<E, R>,
prefix: Option.Option<string> = Option.none(),
uninterruptible = false
options?: {
readonly prefix?: string | undefined
readonly uninterruptible?: boolean | undefined
} | undefined
): Router.Route<E, Router.HttpRouter.ExcludeProvided<R>> =>
new RouteImpl(
method,
path,
handler,
prefix,
uninterruptible
options?.prefix ? Option.some(options.prefix) : Option.none(),
options?.uninterruptible ?? false
) as any

/** @internal */
export const append = dual<
<R1, E1>(
route: Router.Route<E1, R1>
) => <E, R>(self: Router.HttpRouter<E, R>) => Router.HttpRouter<E | E1, R | Router.HttpRouter.ExcludeProvided<R1>>,
<E, R, E1, R1>(
self: Router.HttpRouter<E, R>,
route: Router.Route<E1, R1>
) => Router.HttpRouter<E | E1, R | Router.HttpRouter.ExcludeProvided<R1>>
>(2, (self, route) => new RouterImpl(Chunk.append(self.routes, route) as any, self.mounts))

/** @internal */
export const concat = dual<
<R1, E1>(
Expand Down Expand Up @@ -645,3 +659,86 @@ export const provideServiceEffect = dual<
Context.Tag.Identifier<T>
>
> => use(self, Effect.provideServiceEffect(tag, effect)) as any)

const makeService = <E, R>(): Router.HttpRouter.Service<E, R> => {
let router = empty as Router.HttpRouter<E, R>
return {
addRoute(route) {
return Effect.sync(() => {
router = append(router, route)
})
},
all(path, handler, options) {
return Effect.sync(() => {
router = all(router, path, handler, options)
})
},
get(path, handler, options) {
return Effect.sync(() => {
router = get(router, path, handler, options)
})
},
post(path, handler, options) {
return Effect.sync(() => {
router = post(router, path, handler, options)
})
},
put(path, handler, options) {
return Effect.sync(() => {
router = put(router, path, handler, options)
})
},
patch(path, handler, options) {
return Effect.sync(() => {
router = patch(router, path, handler, options)
})
},
del(path, handler, options) {
return Effect.sync(() => {
router = del(router, path, handler, options)
})
},
head(path, handler, options) {
return Effect.sync(() => {
router = head(router, path, handler, options)
})
},
options(path, handler, opts) {
return Effect.sync(() => {
router = options(router, path, handler, opts)
})
},
router: Effect.sync(() => router)
}
}

/* @internal */
export const Tag =
<const Name extends string>(id: Name) =>
<Self, E = never, R = never>(): Router.HttpRouter.TagClass<Self, Name, E, R> => {
const Err = globalThis.Error as any
const limit = Err.stackTraceLimit
Err.stackTraceLimit = 2
const creationError = new Err()
Err.stackTraceLimit = limit

function TagClass() {}
Object.setPrototypeOf(TagClass, Object.getPrototypeOf(Context.GenericTag<Self, any>(id)))
TagClass.key = id
Object.defineProperty(TagClass, "stack", {
get() {
return creationError.stack
}
})
TagClass.Live = Layer.sync(TagClass as any, makeService)
TagClass.router = Effect.flatMap(TagClass as any, (_: any) => _.router)
TagClass.use = (f: any) =>
Layer.effectDiscard(Effect.flatMap(TagClass as any, f)).pipe(
Layer.provide(TagClass.Live)
)
TagClass.useScoped = (f: any) =>
Layer.scopedDiscard(Effect.flatMap(TagClass as any, f)).pipe(
Layer.provide(TagClass.Live)
)
return TagClass as any
}

0 comments on commit 2e8e252

Please sign in to comment.