Skip to content

Instantly share code, notes, and snippets.

@tim-smart
Last active October 25, 2024 08:26
Show Gist options
  • Save tim-smart/d0390d7ba8714ce1084f95919675e04e to your computer and use it in GitHub Desktop.
Save tim-smart/d0390d7ba8714ce1084f95919675e04e to your computer and use it in GitHub Desktop.
import {
HttpApi,
HttpApiBuilder,
HttpApiEndpoint,
HttpApiGroup,
HttpApiMiddleware,
HttpApiSwagger,
HttpServerRequest
} from "@effect/platform"
import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
import { Context, Effect, flow, Layer, Option, ParseResult, Schema, SchemaAST } from "effect"
import { createServer } from "http"
// ApiVersion + middleware definition
class ApiVersion extends Context.Tag("ApiVersion")<
ApiVersion,
Option.Option<number>
>() {}
class ApiVersionParser extends HttpApiMiddleware.Tag<ApiVersionParser>()("ApiVersionParser", {
provides: ApiVersion
}) {}
const ApiVersionParserLive = Layer.succeed(
ApiVersionParser,
Effect.map(HttpServerRequest.HttpServerRequest, (request) => {
const version = Number(request.headers["version"])
return Number.isNaN(version) ? Option.none() : Option.some(version)
})
)
// Helper for creating versioned schemas
const Versioned = <Schemas extends Record<number, Schema.Schema.Any>>(
schemas: Schemas
): Schema.Schema<
keyof Schemas extends infer Version ? Version extends keyof Schemas ? {
readonly version: Version
readonly value: Schema.Schema.Type<Schemas[Version]>
} :
never :
never,
Schema.Schema.Encoded<Schemas[keyof Schemas]>,
Schema.Schema.Context<Schemas[keyof Schemas]>
> => {
const entries = Object.entries(schemas)
const versions = entries.map(([version]) => Number(version))
const maxVersion = Math.max(...versions)
const getVersion = Effect.map(
Effect.serviceOption(ApiVersion),
flow(
Option.flatten,
Option.match({
onNone: () => maxVersion,
onSome: (version) => schemas[version] ? version : maxVersion
})
)
)
const Union = Schema.Union(...entries.map(([version, schema]) =>
Schema.encodedSchema(schema).annotations({
...Option.match(SchemaAST.getIdentifierAnnotation(schema.ast), {
onNone: () => undefined,
onSome: (identifier) => ({ identifier: `${identifier}V${version}` })
}),
title: Option.match(SchemaAST.getTitleAnnotation(schema.ast), {
onNone: () => `V${version}`,
onSome: (title) => `${title} (V${version})`
})
})
))
const VersionUnion = Schema.Union(...entries.map(([version, schema]) =>
Schema.Struct({
version: Schema.tag(Number(version)),
value: schema
})
))
return Schema.transformOrFail(Union, VersionUnion, {
decode: (payload) => Effect.map(getVersion, (version) => ({ version, value: payload })),
encode: (payload) => ParseResult.succeed(payload.value)
}) as any
}
// Usage
const V1Payload = Schema.Struct({
foo: Schema.String
})
const V2Payload = Schema.Struct({
foo: Schema.NumberFromString
})
const Payload = Versioned({
1: V1Payload,
2: V2Payload
})
class Group extends HttpApiGroup.make("group")
.add(HttpApiEndpoint.post("get", "/").setPayload(Payload))
{}
class Api extends HttpApi.empty
.add(Group)
.middleware(ApiVersionParser)
{}
const GroupLive = HttpApiBuilder.group(
Api,
"group",
(handlers) => handlers.handle("get", ({ payload }) => Effect.log(payload))
)
const ApiLive = HttpApiBuilder.api(Api).pipe(
Layer.provide(GroupLive),
Layer.provide(ApiVersionParserLive)
)
HttpApiBuilder.serve().pipe(
Layer.provide(HttpApiSwagger.layer()),
Layer.provide(ApiLive),
Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })),
Layer.launch,
NodeRuntime.runMain
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment