Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add HttpApi modules to /platform #3495

Merged
merged 59 commits into from
Aug 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
d7f7108
add Api modules to /platform
tim-smart Aug 21, 2024
ed8b427
errors
tim-smart Aug 22, 2024
919bb4d
run codegen
tim-smart Aug 22, 2024
2d7bf1e
middleware
tim-smart Aug 22, 2024
072f289
fix makeHandlers
tim-smart Aug 22, 2024
ca5c476
fix error status codes
tim-smart Aug 22, 2024
c0361f8
wip
tim-smart Aug 22, 2024
1d9ebf8
middleware context
tim-smart Aug 22, 2024
44f21d4
annotations & OpenApi
tim-smart Aug 23, 2024
371f511
security middleware
tim-smart Aug 23, 2024
c1d08a3
middlewareSecurityVoid
tim-smart Aug 23, 2024
f859f23
refactor layer middleware
tim-smart Aug 23, 2024
6a502e5
don't export Proto
tim-smart Aug 23, 2024
1ca2546
add ApiGroup.annotateEndpoints*
tim-smart Aug 23, 2024
2da6d33
start ApiClient
tim-smart Aug 23, 2024
7844aca
client wip
tim-smart Aug 23, 2024
1ee320b
don't refail ParseError
tim-smart Aug 23, 2024
5d13a63
use Redacted for ApiSecurity
tim-smart Aug 23, 2024
bf8f5a8
add OpenApi types
tim-smart Aug 24, 2024
98fc511
fix example
Aug 24, 2024
42b75cd
wip client types
tim-smart Aug 24, 2024
e0241ad
Simplify types
tim-smart Aug 24, 2024
18fc85d
OpenApi generation
tim-smart Aug 24, 2024
f7d8fa5
prefix operationId
tim-smart Aug 24, 2024
9729921
allow operationId to be overriden
tim-smart Aug 24, 2024
6ee91cb
move api to context
tim-smart Aug 25, 2024
958fca9
move ApiReflection into Api module
tim-smart Aug 25, 2024
e984ed3
add support for empty response errors
tim-smart Aug 25, 2024
972624d
openapi wip
tim-smart Aug 25, 2024
d5b7a4a
add Api.Service to ApiBuilder.serve
tim-smart Aug 25, 2024
52c3dbd
OpenApi property descriptions
tim-smart Aug 25, 2024
3ad8e16
makeProperty description fallback
tim-smart Aug 25, 2024
357daec
add swagger layer
tim-smart Aug 26, 2024
9425e0b
fix bearer auth
tim-smart Aug 26, 2024
d5695e5
fix import
tim-smart Aug 26, 2024
8d0202d
fix lint
tim-smart Aug 26, 2024
c494ef6
fix security example
tim-smart Aug 26, 2024
94bf411
improve OpenApi extraction
tim-smart Aug 26, 2024
8cce698
multipart requests
tim-smart Aug 26, 2024
d955f76
fix import
tim-smart Aug 26, 2024
d331195
add some jsdoc comments
tim-smart Aug 26, 2024
f109bcc
ApiEndpoint.addError
tim-smart Aug 26, 2024
027a032
add set prefix to apis
tim-smart Aug 26, 2024
cc80fde
remove Declaration from ApiSchema.isVoid
tim-smart Aug 26, 2024
810f5fb
send full response from handlers
tim-smart Aug 27, 2024
cf6c1de
support cookie auth
tim-smart Aug 27, 2024
2ac26fe
fix ApiSecurity annotations
tim-smart Aug 27, 2024
48bd329
secure cookies by default
tim-smart Aug 27, 2024
6bc4fa0
rename modules
tim-smart Aug 27, 2024
0df127e
fix docgen
tim-smart Aug 27, 2024
1092acc
make HttpApi & HttpApiGroup class extendable
tim-smart Aug 28, 2024
6ca395c
add headers support
tim-smart Aug 28, 2024
dde2259
docs & tests
tim-smart Aug 28, 2024
b20a408
README tweaks
tim-smart Aug 29, 2024
4fde203
add success encoding type support
tim-smart Aug 29, 2024
b9d7175
document withEncoding
tim-smart Aug 29, 2024
509d91e
add changeset
tim-smart Aug 29, 2024
002f244
fix typo
tim-smart Aug 29, 2024
11118f9
add encoding helpers
Aug 30, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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