68 lines
2.4 KiB
TypeScript
68 lines
2.4 KiB
TypeScript
import { Config, Context, Effect, Layer } from "effect"
|
|
|
|
type ConfigMap = Record<string, Config.Config<unknown>>
|
|
|
|
/**
|
|
* The service shape inferred from an object of Effect `Config` definitions.
|
|
*/
|
|
export type Shape<Fields extends ConfigMap> = {
|
|
readonly [Key in keyof Fields]: Config.Success<Fields[Key]>
|
|
}
|
|
|
|
/**
|
|
* A Context service class with generated layers for config-backed services.
|
|
*/
|
|
export type ServiceClass<Self, Id extends string, Service> = Context.ServiceClass<Self, Id, Service> & {
|
|
/** Provide already-parsed config, useful in tests. */
|
|
readonly layer: (input: Service) => Layer.Layer<Self>
|
|
/** Parse config once from the active Effect ConfigProvider and provide the service. */
|
|
readonly defaultLayer: Layer.Layer<Self, Config.ConfigError>
|
|
}
|
|
|
|
/**
|
|
* Create a Context service whose implementation is derived from Effect `Config`.
|
|
*
|
|
* This keeps Effect `Config` as the source of truth for env names, defaults, and
|
|
* validation while generating a typed service plus convenient production/test
|
|
* layers.
|
|
*
|
|
* ```ts
|
|
* class ServerAuthConfig extends ConfigService.Service<ServerAuthConfig>()(
|
|
* "@opencode/ServerAuthConfig",
|
|
* {
|
|
* password: Config.string("OPENCODE_SERVER_PASSWORD").pipe(Config.option),
|
|
* username: Config.string("OPENCODE_SERVER_USERNAME").pipe(Config.withDefault("opencode")),
|
|
* },
|
|
* ) {}
|
|
*
|
|
* const live = ServerAuthConfig.defaultLayer
|
|
* const test = ServerAuthConfig.layer({ password: Option.some("secret"), username: "kit" })
|
|
* ```
|
|
*/
|
|
export const Service =
|
|
<Self>() =>
|
|
<const Id extends string, const Fields extends ConfigMap>(id: Id, fields: Fields) => {
|
|
class ConfigTag extends Context.Service<Self, Shape<Fields>>()(id) {
|
|
static layer(input: Shape<Fields>) {
|
|
return Layer.succeed(this, this.of(input))
|
|
}
|
|
|
|
static get defaultLayer() {
|
|
return Layer.effect(
|
|
this,
|
|
Config.all(fields)
|
|
.asEffect()
|
|
.pipe(
|
|
// oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- Config.all preserves the field shape, but its conditional return type also supports iterable inputs.
|
|
Effect.map((config) => this.of(config as Shape<Fields>)),
|
|
),
|
|
)
|
|
}
|
|
}
|
|
|
|
// oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- The generated class carries typed static helpers.
|
|
return ConfigTag as ServiceClass<Self, Id, Shape<Fields>>
|
|
}
|
|
|
|
export * as ConfigService from "./config-service"
|