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 >() {} class ApiVersionParser extends HttpApiMiddleware.Tag()("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: Schemas ): Schema.Schema< keyof Schemas extends infer Version ? Version extends keyof Schemas ? { readonly version: Version readonly value: Schema.Schema.Type } : never : never, Schema.Schema.Encoded, Schema.Schema.Context > => { 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 )