Skip to content

Commit

Permalink
add HttpApi modules to /platform (#3495)
Browse files Browse the repository at this point in the history
Co-authored-by: Tim Smart <[email protected]>
  • Loading branch information
tim-smart and Tim Smart committed Aug 30, 2024
1 parent f114c96 commit 327cd82
Show file tree
Hide file tree
Showing 27 changed files with 6,398 additions and 307 deletions.
10 changes: 10 additions & 0 deletions .changeset/curvy-clouds-battle.md
Original file line number Diff line number Diff line change
@@ -0,0 1,10 @@
---
"@effect/platform": patch
---

add HttpApi modules

The `HttpApi` family of modules provide a declarative way to define HTTP APIs.

For more infomation see the README.md for the /platform package:<br />
https://github.com/Effect-TS/effect/blob/main/packages/platform/README.md
5 changes: 5 additions & 0 deletions .changeset/young-pugs-grow.md
Original file line number Diff line number Diff line change
@@ -0,0 1,5 @@
---
"effect": minor
---

add Context.getOrElse api, for gettings a Tag's value with a fallback
4 changes: 4 additions & 0 deletions .prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 1,4 @@
{
"semi": false,
"trailingComma": "none"
}
13 changes: 13 additions & 0 deletions packages/effect/src/Context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 8,7 @@
* @since 2.0.0
*/
import type { Equal } from "./Equal.js"
import type { LazyArg } from "./Function.js"
import type { Inspectable } from "./Inspectable.js"
import * as internal from "./internal/context.js"
import type { Option } from "./Option.js"
Expand Down Expand Up @@ -263,6 264,18 @@ export const get: {
<Services, T extends ValidTagsById<Services>>(self: Context<Services>, tag: T): Tag.Service<T>
} = internal.get

/**
* Get a service from the context that corresponds to the given tag, or
* use the fallback value.
*
* @since 3.7.0
* @category getters
*/
export const getOrElse: {
<S, I, B>(tag: Tag<I, S>, orElse: LazyArg<B>): <Services>(self: Context<Services>) => S | B
<Services, S, I, B>(self: Context<Services>, tag: Tag<I, S>, orElse: LazyArg<B>): S | B
} = internal.getOrElse

/**
* Get a service from the context that corresponds to the given tag.
* This function is unsafe because if the tag is not present in the context, a runtime error will be thrown.
Expand Down
12 changes: 12 additions & 0 deletions packages/effect/src/internal/context.ts
Original file line number Diff line number Diff line change
@@ -1,5 1,6 @@
import type * as C from "../Context.js"
import * as Equal from "../Equal.js"
import type { LazyArg } from "../Function.js"
import { dual } from "../Function.js"
import * as Hash from "../Hash.js"
import { format, NodeInspectSymbol, toJSON } from "../Inspectable.js"
Expand Down Expand Up @@ -209,6 210,17 @@ export const get: {
<Services, T extends C.ValidTagsById<Services>>(self: C.Context<Services>, tag: T): C.Tag.Service<T>
} = unsafeGet

/** @internal */
export const getOrElse = dual<
<S, I, B>(tag: C.Tag<I, S>, orElse: LazyArg<B>) => <Services>(self: C.Context<Services>) => S | B,
<Services, S, I, B>(self: C.Context<Services>, tag: C.Tag<I, S>, orElse: LazyArg<B>) => S | B
>(3, (self, tag, orElse) => {
if (!self.unsafeMap.has(tag.key)) {
return orElse()
}
return self.unsafeMap.get(tag.key)! as any
})

/** @internal */
export const getOption = dual<
<S, I>(tag: C.Tag<I, S>) => <Services>(self: C.Context<Services>) => O.Option<S>,
Expand Down
178 changes: 178 additions & 0 deletions packages/platform-node/examples/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 1,178 @@
import {
HttpApi,
HttpApiBuilder,
HttpApiClient,
HttpApiEndpoint,
HttpApiGroup,
HttpApiSchema,
HttpApiSecurity,
HttpApiSwagger,
HttpClient,
HttpMiddleware,
HttpServer,
OpenApi
} from "@effect/platform"
import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
import { Schema } from "@effect/schema"
import { Context, Effect, Layer, Redacted } from "effect"
import { createServer } from "node:http"

class User extends Schema.Class<User>("User")({
id: Schema.Number,
name: Schema.String
}) {}

class CurrentUser extends Context.Tag("CurrentUser")<CurrentUser, User>() {}

class Unauthorized extends Schema.TaggedError<Unauthorized>()("Unauthorized", {
message: Schema.String
}, HttpApiSchema.annotations({ status: 401 })) {}

const security = HttpApiSecurity.bearer

const securityMiddleware = HttpApiBuilder.middlewareSecurity(
security,
CurrentUser,
(token) => Effect.succeed(new User({ id: 1000, name: `Authenticated with ${Redacted.value(token)}` }))
)

class UsersApi extends HttpApiGroup.make("users").pipe(
HttpApiGroup.add(
HttpApiEndpoint.get("findById", "/:id").pipe(
HttpApiEndpoint.setPath(Schema.Struct({
id: Schema.NumberFromString
})),
HttpApiEndpoint.setSuccess(User),
HttpApiEndpoint.setHeaders(Schema.Struct({
page: Schema.NumberFromString.pipe(
Schema.optionalWith({ default: () => 1 })
)
})),
HttpApiEndpoint.addError(Schema.String.pipe(
HttpApiSchema.asEmpty({ status: 413, decode: () => "boom" })
))
)
),
HttpApiGroup.add(
HttpApiEndpoint.post("create", "/").pipe(
HttpApiEndpoint.setPayload(HttpApiSchema.Multipart(Schema.Struct({
name: Schema.String
}))),
HttpApiEndpoint.setSuccess(User)
)
),
HttpApiGroup.add(
HttpApiEndpoint.get("me", "/me").pipe(
HttpApiEndpoint.setSuccess(User)
)
),
HttpApiGroup.add(
HttpApiEndpoint.get("csv", "/csv").pipe(
HttpApiEndpoint.setSuccess(HttpApiSchema.Text({
contentType: "text/csv"
}))
)
),
HttpApiGroup.add(
HttpApiEndpoint.get("binary", "/binary").pipe(
HttpApiEndpoint.setSuccess(HttpApiSchema.Uint8Array())
)
),
HttpApiGroup.add(
HttpApiEndpoint.get("urlParams", "/url-params").pipe(
HttpApiEndpoint.setSuccess(
Schema.Struct({
id: Schema.NumberFromString,
name: Schema.String
}).pipe(
HttpApiSchema.withEncoding({
kind: "UrlParams"
})
)
)
)
),
HttpApiGroup.addError(Unauthorized),
HttpApiGroup.prefix("/users"),
OpenApi.annotate({ security })
) {}

class MyApi extends HttpApi.empty.pipe(
HttpApi.addGroup(UsersApi),
OpenApi.annotate({
title: "Users API",
description: "API for managing users"
})
) {}

const UsersLive = HttpApiBuilder.group(MyApi, "users", (handlers) =>
handlers.pipe(
HttpApiBuilder.handle("create", (_) => Effect.succeed(new User({ ..._.payload, id: 123 }))),
HttpApiBuilder.handle("findById", (_) =>
Effect.as(
HttpApiBuilder.securitySetCookie(
HttpApiSecurity.apiKey({
in: "cookie",
key: "token"
}),
"secret123"
),
new User({
id: _.path.id,
name: `John Doe (${_.headers.page})`
})
)),
HttpApiBuilder.handle("me", (_) => CurrentUser),
HttpApiBuilder.handle("csv", (_) => Effect.succeed("id,name\n1,John")),
HttpApiBuilder.handle("urlParams", (_) =>
Effect.succeed({
id: 123,
name: "John"
})),
HttpApiBuilder.handle("binary", (_) => Effect.succeed(new Uint8Array([1, 2, 3, 4, 5]))),
securityMiddleware
))

const ApiLive = HttpApiBuilder.api(MyApi).pipe(
Layer.provide(UsersLive)
)

HttpApiBuilder.serve(HttpMiddleware.logger).pipe(
Layer.provide(HttpApiSwagger.layer()),
Layer.provide(HttpApiBuilder.middlewareOpenApi()),
Layer.provide(ApiLive),
Layer.provide(HttpApiBuilder.middlewareCors()),
HttpServer.withLogAddress,
Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })),
Layer.launch,
NodeRuntime.runMain
)

Effect.gen(function*() {
yield* Effect.sleep(2000)
const client = yield* HttpApiClient.make(MyApi, {
baseUrl: "http://localhost:3000"
})

const data = new FormData()
data.append("name", "John")
console.log("Multipart", yield* client.users.create({ payload: data }))

const user = yield* client.users.findById({
path: { id: 123 },
headers: { page: 10 }
})
console.log("json", user)

const csv = yield* client.users.csv()
console.log("csv", csv)

const urlParams = yield* client.users.urlParams()
console.log("urlParams", urlParams)

const binary = yield* client.users.binary()
console.log("binary", binary)
}).pipe(
Effect.provide(HttpClient.layer),
NodeRuntime.runMain
)
Loading

0 comments on commit 327cd82

Please sign in to comment.