Last active
October 25, 2024 08:26
-
-
Save tim-smart/d0390d7ba8714ce1084f95919675e04e to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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