Skip to content

Commit

Permalink
HttpApi second revision (#3794)
Browse files Browse the repository at this point in the history
  • Loading branch information
tim-smart committed Oct 21, 2024
1 parent dac760e commit 87f4d95
Show file tree
Hide file tree
Showing 22 changed files with 3,139 additions and 2,667 deletions.
16 changes: 16 additions & 0 deletions .changeset/fair-cheetahs-learn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
"@effect/platform": minor
---

HttpApi second revision

- `HttpApi`, `HttpApiGroup` & `HttpApiEndpoint` now use a chainable api instead
of a pipeable api.
- `HttpApiMiddleware` module has been added, with a updated way of defining
security middleware.
- You can now add multiple success schemas
- A url search parameter schema has been added
- Error schemas now support `HttpApiSchema` encoding apis
- `toWebHandler` has been simplified

For more information, see the [README](https://github.com/Effect-TS/effect/blob/main/packages/platform/README.md#http-api).
5 changes: 5 additions & 0 deletions .changeset/four-monkeys-arrive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@effect/experimental": patch
---

add identifiers to VariantSchema variants
5 changes: 4 additions & 1 deletion packages/experimental/src/VariantSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -506,7 +506,10 @@ export const make = <
}
for (const variant of options.variants) {
Object.defineProperty(Base, variant, {
value: extract(variantStruct, variant)
value: extract(variantStruct, variant).annotations({
identifier: `${identifier}.${variant}`,
title: `${identifier}.${variant}`
})
})
}
return Base
Expand Down
218 changes: 129 additions & 89 deletions packages/platform-node/examples/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ import {
HttpApiClient,
HttpApiEndpoint,
HttpApiGroup,
HttpApiMiddleware,
HttpApiSchema,
HttpApiSecurity,
HttpApiSwagger,
HttpClient,
HttpClientRequest,
HttpMiddleware,
HttpServer,
OpenApi
Expand All @@ -27,59 +30,63 @@ 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)}` }))
)
export class Authentication extends HttpApiMiddleware.Tag<Authentication>()("Authentication", {
failure: Unauthorized,
provides: CurrentUser,
security: {
bearer: HttpApiSecurity.bearer
}
}) {}

class UsersApi extends HttpApiGroup.make("users").pipe(
HttpApiGroup.add(
HttpApiEndpoint.get("findById", "/:id").pipe(
HttpApiEndpoint.setPath(Schema.Struct({
class UsersApi extends HttpApiGroup.make("users")
.add(
HttpApiEndpoint.get("findById", "/:id")
.setPath(Schema.Struct({
id: Schema.NumberFromString
})),
HttpApiEndpoint.setSuccess(User),
HttpApiEndpoint.setHeaders(Schema.Struct({
}))
.addSuccess(User)
.setHeaders(Schema.Struct({
page: Schema.NumberFromString.pipe(
Schema.optionalWith({ default: () => 1 })
)
})),
HttpApiEndpoint.addError(Schema.String.pipe(
}))
.addError(Schema.String.pipe(
HttpApiSchema.asEmpty({ status: 413, decode: () => "boom" })
))
)
),
HttpApiGroup.add(
HttpApiEndpoint.post("create", "/").pipe(
HttpApiEndpoint.setPayload(HttpApiSchema.Multipart(Schema.Struct({
)
.add(
HttpApiEndpoint.post("create", "/")
.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({
})))
.addSuccess(User)
)
.add(
HttpApiEndpoint.get("me", "/me")
.addSuccess(User)
)
.middleware(Authentication)
.prefix("/users")
.annotateContext(OpenApi.annotations({
title: "Users API",
description: "API for managing users"
}))
{}

class TopLevelApi extends HttpApiGroup.make("topLevel", { topLevel: true })
.add(
HttpApiEndpoint.get("csv", "/csv")
.addSuccess(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(
)
.add(
HttpApiEndpoint.get("binary", "/binary")
.addSuccess(HttpApiSchema.Uint8Array())
)
.add(
HttpApiEndpoint.get("urlParams", "/url-params")
.addSuccess(
Schema.Struct({
id: Schema.NumberFromString,
name: Schema.String
Expand All @@ -89,53 +96,82 @@ class UsersApi extends HttpApiGroup.make("users").pipe(
})
)
)
)
),
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"
),
)
.annotateContext(OpenApi.annotations({
title: "Top Level API",
description: "API for top level endpoints"
}))
{}

class MyApi extends HttpApi.empty
.add(UsersApi)
.add(TopLevelApi)
{}

// ------------------------------------------------
// implementation
// ------------------------------------------------

const AuthenticationLive = Layer.succeed(
Authentication,
Authentication.of({
bearer: (token) =>
Effect.succeed(
new User({
id: _.path.id,
name: `John Doe (${_.headers.page})`
id: 1000,
name: `Authenticated with ${Redacted.value(token)}`
})
)),
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 UsersLive = HttpApiBuilder.group(
MyApi,
"users",
(handlers) =>
handlers
.handle("create", (_) => Effect.succeed(new User({ ..._.payload, id: 123 })))
.handle("findById", (_) =>
Effect.as(
HttpApiBuilder.securitySetCookie(
HttpApiSecurity.apiKey({
in: "cookie",
key: "token"
}),
"secret123"
),
new User({
id: _.path.id,
name: `John Doe (${_.headers.page})`
})
))
.handle("me", (_) => CurrentUser)
).pipe(
Layer.provide(AuthenticationLive)
)

const TopLevelLive = HttpApiBuilder.group(
MyApi,
"topLevel",
(handlers) =>
handlers
.handle("csv", (_) => Effect.succeed("id,name\n1,John"))
.handle("urlParams", (_) =>
Effect.succeed({
id: 123,
name: "John"
}))
.handle("binary", (_) => Effect.succeed(new Uint8Array([1, 2, 3, 4, 5])))
)

const ApiLive = HttpApiBuilder.api(MyApi).pipe(
Layer.provide(UsersLive)
Layer.provide([UsersLive, TopLevelLive])
)

// ------------------------------------------------
// server
// ------------------------------------------------

HttpApiBuilder.serve(HttpMiddleware.logger).pipe(
Layer.provide(HttpApiSwagger.layer()),
Layer.provide(HttpApiBuilder.middlewareOpenApi()),
Expand All @@ -150,26 +186,30 @@ HttpApiBuilder.serve(HttpMiddleware.logger).pipe(
Effect.gen(function*() {
yield* Effect.sleep(2000)
const client = yield* HttpApiClient.make(MyApi, {
baseUrl: "http://localhost:3000"
baseUrl: "http://localhost:3000",
transformClient: HttpClient.mapRequest(HttpClientRequest.bearerToken("token"))
})

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

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

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

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

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

const binary = yield* client.users.binary()
const binary = yield* client.binary()
console.log("binary", binary)
}).pipe(
Effect.provide(FetchHttpClient.layer),
Expand Down
Loading

0 comments on commit 87f4d95

Please sign in to comment.