UNPKG

veffect

Version:

powerful TypeScript validation library built on the robust foundation of Effect combining exceptional type safety, high performance, and developer experience. Taking inspiration from Effect's functional principles, VEffect delivers a balanced approach tha

391 lines (364 loc) 11.5 kB
/** * This module provides a data structure called `Context` that can be used for dependency injection in effectful * programs. It is essentially a table mapping `Tag`s to their implementations (called `Service`s), and can be used to * manage dependencies in a type-safe way. The `Context` data structure is essentially a way of providing access to a set * of related services that can be passed around as a single unit. This module provides functions to create, modify, and * query the contents of a `Context`, as well as a number of utility types for working with tags and services. * * @since 2.0.0 */ import type { Equal } from "./Equal.js" import type { Inspectable } from "./Inspectable.js" import * as internal from "./internal/context.js" import type { Option } from "./Option.js" import type { Pipeable } from "./Pipeable.js" import type * as Types from "./Types.js" import type * as Unify from "./Unify.js" const TagTypeId: unique symbol = internal.TagTypeId /** * @since 2.0.0 * @category symbol */ export type TagTypeId = typeof TagTypeId /** * @since 2.0.0 * @category models */ export interface Tag<in out Id, in out Value> extends Pipeable, Inspectable { readonly _tag: "Tag" readonly _op: "Tag" readonly [TagTypeId]: { readonly _Service: Types.Invariant<Value> readonly _Identifier: Types.Invariant<Id> } of(self: Value): Value context(self: Value): Context<Id> readonly stack?: string | undefined readonly key: string [Unify.typeSymbol]?: unknown [Unify.unifySymbol]?: TagUnify<this> [Unify.ignoreSymbol]?: TagUnifyIgnore } /** * @since 2.0.0 * @category models */ export interface TagClassShape<Id, Shape> { readonly [TagTypeId]: TagTypeId readonly Type: Shape readonly Id: Id } /** * @since 2.0.0 * @category models */ export interface TagClass<Self, Id, Type> extends Tag<Self, Type> { new(_: never): TagClassShape<Id, Type> } /** * @category models * @since 2.0.0 */ export interface TagUnify<A extends { [Unify.typeSymbol]?: any }> { Tag?: () => A[Unify.typeSymbol] extends Tag<infer I0, infer S0> | infer _ ? Tag<I0, S0> : never } /** * @category models * @since 2.0.0 */ export interface TagUnifyIgnore {} /** * @since 2.0.0 */ export declare namespace Tag { /** * @since 2.0.0 */ export type Service<T extends Tag<any, any> | TagClassShape<any, any>> = T extends Tag<any, infer A> ? A : T extends TagClassShape<any, infer A> ? A : never /** * @since 2.0.0 */ export type Identifier<T extends Tag<any, any> | TagClassShape<any, any>> = T extends Tag<infer A, any> ? A : T extends TagClassShape<infer A, any> ? A : never } /** * Creates a new `Tag` instance with an optional key parameter. * * @param key - A key that will be used to compare tags. * * @example * import * as Context from "effect/Context" * * assert.strictEqual(Context.GenericTag("PORT").key === Context.GenericTag("PORT").key, true) * * @since 2.0.0 * @category constructors */ export const GenericTag: <Identifier, Service = Identifier>(key: string) => Tag<Identifier, Service> = internal.makeGenericTag const TypeId: unique symbol = internal.TypeId as TypeId /** * @since 2.0.0 * @category symbol */ export type TypeId = typeof TypeId /** * @since 2.0.0 * @category models */ export type ValidTagsById<R> = R extends infer S ? Tag<S, any> : never /** * @since 2.0.0 * @category models */ export interface Context<in Services> extends Equal, Pipeable, Inspectable { readonly [TypeId]: { readonly _Services: Types.Contravariant<Services> } readonly unsafeMap: Map<string, any> } /** * @since 2.0.0 * @category constructors */ export const unsafeMake: <Services>(unsafeMap: Map<string, any>) => Context<Services> = internal.makeContext /** * Checks if the provided argument is a `Context`. * * @param input - The value to be checked if it is a `Context`. * * @example * import * as Context from "effect/Context" * * assert.strictEqual(Context.isContext(Context.empty()), true) * * @since 2.0.0 * @category guards */ export const isContext: (input: unknown) => input is Context<never> = internal.isContext /** * Checks if the provided argument is a `Tag`. * * @param input - The value to be checked if it is a `Tag`. * * @example * import * as Context from "effect/Context" * * assert.strictEqual(Context.isTag(Context.GenericTag("Tag")), true) * * @since 2.0.0 * @category guards */ export const isTag: (input: unknown) => input is Tag<any, any> = internal.isTag /** * Returns an empty `Context`. * * @example * import * as Context from "effect/Context" * * assert.strictEqual(Context.isContext(Context.empty()), true) * * @since 2.0.0 * @category constructors */ export const empty: () => Context<never> = internal.empty /** * Creates a new `Context` with a single service associated to the tag. * * @example * import * as Context from "effect/Context" * * const Port = Context.GenericTag<{ PORT: number }>("Port") * * const Services = Context.make(Port, { PORT: 8080 }) * * assert.deepStrictEqual(Context.get(Services, Port), { PORT: 8080 }) * * @since 2.0.0 * @category constructors */ export const make: <T extends Tag<any, any>>(tag: T, service: Tag.Service<T>) => Context<Tag.Identifier<T>> = internal.make /** * Adds a service to a given `Context`. * * @example * import * as Context from "effect/Context" * import { pipe } from "effect/Function" * * const Port = Context.GenericTag<{ PORT: number }>("Port") * const Timeout = Context.GenericTag<{ TIMEOUT: number }>("Timeout") * * const someContext = Context.make(Port, { PORT: 8080 }) * * const Services = pipe( * someContext, * Context.add(Timeout, { TIMEOUT: 5000 }) * ) * * assert.deepStrictEqual(Context.get(Services, Port), { PORT: 8080 }) * assert.deepStrictEqual(Context.get(Services, Timeout), { TIMEOUT: 5000 }) * * @since 2.0.0 */ export const add: { <T extends Tag<any, any>>( tag: T, service: Tag.Service<T> ): <Services>(self: Context<Services>) => Context<Services | Tag.Identifier<T>> <Services, T extends Tag<any, any>>( self: Context<Services>, tag: T, service: Tag.Service<T> ): Context<Services | Tag.Identifier<T>> } = internal.add /** * Get a service from the context that corresponds to the given tag. * * @param self - The `Context` to search for the service. * @param tag - The `Tag` of the service to retrieve. * * @example * import * as Context from "effect/Context" * import { pipe } from "effect/Function" * * const Port = Context.GenericTag<{ PORT: number }>("Port") * const Timeout = Context.GenericTag<{ TIMEOUT: number }>("Timeout") * * const Services = pipe( * Context.make(Port, { PORT: 8080 }), * Context.add(Timeout, { TIMEOUT: 5000 }) * ) * * assert.deepStrictEqual(Context.get(Services, Timeout), { TIMEOUT: 5000 }) * * @since 2.0.0 * @category getters */ export const get: { <Services, T extends ValidTagsById<Services>>(tag: T): (self: Context<Services>) => Tag.Service<T> <Services, T extends ValidTagsById<Services>>(self: Context<Services>, tag: T): Tag.Service<T> } = internal.get /** * Get a service from the context that corresponds to the given tag. * This function is unsafe because if the tag is not present in the context, a runtime error will be thrown. * * For a safer version see {@link getOption}. * * @param self - The `Context` to search for the service. * @param tag - The `Tag` of the service to retrieve. * * @example * import * as Context from "effect/Context" * * const Port = Context.GenericTag<{ PORT: number }>("Port") * const Timeout = Context.GenericTag<{ TIMEOUT: number }>("Timeout") * * const Services = Context.make(Port, { PORT: 8080 }) * * assert.deepStrictEqual(Context.unsafeGet(Services, Port), { PORT: 8080 }) * assert.throws(() => Context.unsafeGet(Services, Timeout)) * * @since 2.0.0 * @category unsafe */ export const unsafeGet: { <S, I>(tag: Tag<I, S>): <Services>(self: Context<Services>) => S <Services, S, I>(self: Context<Services>, tag: Tag<I, S>): S } = internal.unsafeGet /** * Get the value associated with the specified tag from the context wrapped in an `Option` object. If the tag is not * found, the `Option` object will be `None`. * * @param self - The `Context` to search for the service. * @param tag - The `Tag` of the service to retrieve. * * @example * import * as Context from "effect/Context" * import * as O from "effect/Option" * * const Port = Context.GenericTag<{ PORT: number }>("Port") * const Timeout = Context.GenericTag<{ TIMEOUT: number }>("Timeout") * * const Services = Context.make(Port, { PORT: 8080 }) * * assert.deepStrictEqual(Context.getOption(Services, Port), O.some({ PORT: 8080 })) * assert.deepStrictEqual(Context.getOption(Services, Timeout), O.none()) * * @since 2.0.0 * @category getters */ export const getOption: { <S, I>(tag: Tag<I, S>): <Services>(self: Context<Services>) => Option<S> <Services, S, I>(self: Context<Services>, tag: Tag<I, S>): Option<S> } = internal.getOption /** * Merges two `Context`s, returning a new `Context` containing the services of both. * * @param self - The first `Context` to merge. * @param that - The second `Context` to merge. * * @example * import * as Context from "effect/Context" * * const Port = Context.GenericTag<{ PORT: number }>("Port") * const Timeout = Context.GenericTag<{ TIMEOUT: number }>("Timeout") * * const firstContext = Context.make(Port, { PORT: 8080 }) * const secondContext = Context.make(Timeout, { TIMEOUT: 5000 }) * * const Services = Context.merge(firstContext, secondContext) * * assert.deepStrictEqual(Context.get(Services, Port), { PORT: 8080 }) * assert.deepStrictEqual(Context.get(Services, Timeout), { TIMEOUT: 5000 }) * * @since 2.0.0 */ export const merge: { <R1>(that: Context<R1>): <Services>(self: Context<Services>) => Context<R1 | Services> <Services, R1>(self: Context<Services>, that: Context<R1>): Context<Services | R1> } = internal.merge /** * Returns a new `Context` that contains only the specified services. * * @param self - The `Context` to prune services from. * @param tags - The list of `Tag`s to be included in the new `Context`. * * @example * import * as Context from "effect/Context" * import { pipe } from "effect/Function" * import * as O from "effect/Option" * * const Port = Context.GenericTag<{ PORT: number }>("Port") * const Timeout = Context.GenericTag<{ TIMEOUT: number }>("Timeout") * * const someContext = pipe( * Context.make(Port, { PORT: 8080 }), * Context.add(Timeout, { TIMEOUT: 5000 }) * ) * * const Services = pipe(someContext, Context.pick(Port)) * * assert.deepStrictEqual(Context.getOption(Services, Port), O.some({ PORT: 8080 })) * assert.deepStrictEqual(Context.getOption(Services, Timeout), O.none()) * * @since 2.0.0 */ export const pick: <Services, S extends Array<ValidTagsById<Services>>>( ...tags: S ) => (self: Context<Services>) => Context<{ [k in keyof S]: Tag.Identifier<S[k]> }[number]> = internal.pick /** * @since 2.0.0 */ export const omit: <Services, S extends Array<ValidTagsById<Services>>>( ...tags: S ) => (self: Context<Services>) => Context<Exclude<Services, { [k in keyof S]: Tag.Identifier<S[k]> }[keyof S]>> = internal.omit /** * @since 2.0.0 * @category constructors */ export const Tag: <const Id extends string>(id: Id) => <Self, Shape>() => TagClass<Self, Id, Shape> = internal.Tag