import * as dateFns from 'date-fns' import { NonEmptyString, UUID } from 'io-ts-types' import { Iso } from 'monocle-ts' import { AnyNewtype, CarrierOf, Newtype, prism, URIOf } from 'newtype-ts' import { AnyTuple, Overwrite } from 'typelevel-ts' import * as E from 'fp-ts/lib/Either' import { pipe } from 'fp-ts/lib/function' import * as O from 'fp-ts/lib/Option' import * as Ord from 'fp-ts/lib/Ord' import { logger } from '@monorail/v2/shared/helpers' /** * Utility interface used to attach a tag & unique symbol to a Newtype's _URI * field. * * NOTE: We are using the representation * `{ _Tag: 'MyTag'; _Symbol: unique symbol }` instead of * `{ MyTag: unique symbol }` in order to facilitate easily overwriting the tag * when making fresh newtypes. See the `SetTag` utility for more info. */ export interface NewtypeURI { readonly _Tag: Tag readonly _Symbol: unique symbol } /** * Utility interface used to attach "phantom params" to a Newtype's _URI field. */ export type PhantomParams = { readonly _PhantomParams: Ps } /** * A union type representing a valid URI for a SimSpaceNewtype that is also a * phantom type. */ export type SimSpacePhantomTypeURI = NewtypeURI & PhantomParams /** * A union type representing a valid type for a SimspaceNewtype's _URI field. * This union type is used to allow or disallow constructing a phantom type by * either providing or not providing a tuple of PhantomParams" (type parameters * that affect the nominal type being constructed but that do not appear on the * underlying runtime type). */ export type SimSpaceNewtypeURI = NewtypeURI | SimSpacePhantomTypeURI /** * A wrapper around newtype-ts' Newtype interface with exta constraints on the * _URI field. */ export interface SimSpaceNewtype extends Newtype {} /** * Utility type used to update an existing SimSpaceNewtype's _Tag field. * This can be used to provide a new identifier while extending an existing * SimSpaceNewtype. */ export type SetTag< Tag extends string, New extends SimSpaceNewtype > = New extends SimSpaceNewtype ? Overwrite< New, { _URI: NewtypeURI & PhantomParams } > // if the newtype is a phantom type, also persist its "phantom params" : { _URI: NewtypeURI } // otherwise, only replace the _URI field /** * A generic phantom type. `A` represents the underlying runtime type being * wrapped, and `B` represents an extra "phantom param" used to tag the nominal * type with extra information. * * Modeled after Haskell's `Const` from `Control.Applicative` and * `Data.Functor.Const`. * * NOTE: `fp-ts` already has a `Const` data type and module, which does not use * newtype-ts. Be careful to avoid collisions. */ export interface Const extends SimSpaceNewtype & PhantomParams<[B]>, A> {} /** * A specialized verson of the `Const` phantom type with its type params * flipped and the "phantom param" being constrained to a type-level literal * string. * * Modeled after Haskell's `Tagged` from `Data.Tagged`. * * NOTE: `io-ts` already has a `Tagged` type, but it is deprecated. Be careful * to avoid collisions. */ export interface Tagged extends SetTag<'Tagged', Const> {} /** * A specialized verson of the `Const` phantom type where the underlying * runtime type is fixed to type `string`. This is a useful phantom type for * providing extra safety by disambiguating between string IDs for different * types. * * Modeled after the `Key` phantom type in our Haskell codebase. * * Note: Some of the generated TypeScript type files already have a * `type Key = string` type alias. Be Careful to avoid collisions. */ export interface Key extends SetTag<'Key', Const> {} /** * A phantom type just like `Key` but the underlying type is an io-ts-types UUID * instead of a string. */ export interface UUIDKey extends SetTag<'UUIDKey', Const> {} const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i /** * Gets a prism that validates UUIDs. * * NOTE: You may not need this if using io-ts Codecs. */ export function getPrismUUID() { return prism>(uuidRegex.test) } /** * A phantom type wrapper for expresing versions which are a part of * versioned entities. * * Modeled after the `Version` phantom type in our Haskell codebase. * * Note: Some of the generated TypeScript type files already have a * `type Version = number` type alias. Be Careful to avoid collisions. */ export interface Version extends SetTag<'Version', Const> {} /** * A specialized verson of the `Const` phantom type where the underlying * runtime type is fixed to type `string`. This is a useful phantom type for * providing extra safety by disambiguating between names for different types. */ export interface Name extends SetTag<'Name', Const> {} /** * A phantom type just like `Name` but the underlying type is an io-ts-types * NonEmptyString instead of a string. */ export interface NonEmptyName extends SetTag<'NonEmptyName', Const> {} /** * A specialized verson of the `Const` phantom type where the underlying * runtime type is fixed to type `string`. This is a useful phantom type for * providing extra safety by disambiguating between descriptions for different * types. */ export interface Description extends SetTag<'Description', Const> {} /** * NaN is a refinement of number */ export interface NaN extends SimSpaceNewtype, number> {} /** * Use this prism to return an option with prismNaN.getOption(someNumber) */ export const prismNaN = prism(Number.isNaN) /** * Infinity is a refinement of number */ export interface Infinity extends SimSpaceNewtype, number> {} export const prismInfinity = prism( x => !Number.isFinite(x) && !Number.isNaN(x), ) /** * Finite is a refinement of number */ export interface Finite extends SimSpaceNewtype, number> {} export const prismFinite = prism(Number.isFinite) /* * IsoDate is a refinement of string */ export interface IsoDate extends SimSpaceNewtype, string> {} export const isoDateToDate = (isoDate: IsoDate): Date => new Date(coerce(isoDate)) export const ordIsoDate: Ord.Ord = pipe( Ord.ordDate, Ord.contramap(isoDateToDate), ) /* * A prism giving you a `getOption` function that returns a `Some` * if the run-time string can is a valid ISO date string or a `None` if it * isn't. */ export const prismIsoDate = prism(str => { const parsedDate = dateFns.parseISO(str) return dateFns.isValid(parsedDate) }) /* * Coerce any Newtype to its underlying runtime type. * * NOTE: This coercion is 100% safe and does not require explicitly providing * the generic type param for the Newtype. */ export type CoerceNewtype = N extends Newtype< unknown, infer A > ? A : never /* * Coerce any Newtype to its underlying runtime type. * * NOTE: This coercion is 100% safe and does not require explicitly providing * the generic type param for the Newtype. */ export const coerce = (n: N): CoerceNewtype => // eslint-disable-next-line @typescript-eslint/no-unsafe-return n as CoerceNewtype /* * Coerce any type-level string literal or Newtype over string to its underlying * string run-time type. * * NOTE: This coercion is 100% safe and does not require explicitly providing * the generic type param for the type-level string literal or Newtype. */ export const coerceToString = >( s: S, ): string => s as string /** * Try to take a param and decode it into a UUID by way of io-ts UUID * branded type. If the param correctly decodes, then take the value * and wrap it in the proper newtype created for the param * * @param {string} param - Any param that can possibly be turned into a UUID * @param {string} paramName - The name of the param being passed in for logging purposes * @param {Iso>} iso - The iso used to wrap the param into a new type for that param */ export const buildKeyNewtypeFromParam = ( paramName: string, iso: Iso>, ) => (param: string): O.Option, string>> => pipe( param, UUID.decode, E.mapLeft(e => logger(({ error }) => error({ message: `The param "${paramName}" could not be decoded into a UUID.`, error: e, }), ), ), O.fromEither, O.map(iso.wrap), )