@typed/fp
Version:
Data Structures and Resources for fp-ts
900 lines (810 loc) • 20.5 kB
text/typescript
/**
* Decoder is a data structure for representing runtime representations of your types.
* @since 0.9.4
*/
import { parseISO } from 'date-fns'
import * as App from 'fp-ts/Applicative'
import * as Ap from 'fp-ts/Apply'
import * as Ch from 'fp-ts/Chain'
import * as Ei from 'fp-ts/Either'
import * as F from 'fp-ts/Functor'
import { IO } from 'fp-ts/IO'
import { Json } from 'fp-ts/Json'
import { Monad2 } from 'fp-ts/Monad'
import * as N from 'fp-ts/number'
import { Pointed2 } from 'fp-ts/Pointed'
import { not, Predicate } from 'fp-ts/Predicate'
import * as RA from 'fp-ts/ReadonlyArray'
import { concatW } from 'fp-ts/ReadonlyNonEmptyArray'
import { Refinement } from 'fp-ts/Refinement'
import * as S from 'fp-ts/string'
import * as DE from './DecodeError'
import { leaf } from './DecodeError'
import { pipe } from './function'
import { memoize, UndoPartial } from './internal'
import { Literal, Schemable2C, WithRefine2C, WithUnion2C } from './Schemable'
import * as St from './struct'
import { make } from './struct'
import * as T from './These'
/**
* @category Model
* @since 0.9.4
*/
export interface Decoder<I, O> {
readonly decode: (input: I) => T.These<DE.DecodeErrors, O>
}
/**
* @category Type-level
* @since 0.9.4
*/
export type InputOf<A> = [A] extends [Decoder<infer I, any>] ? I : never
/**
* @category Type-level
* @since 0.9.4
*/
export type OutputOf<A> = [A] extends [Decoder<any, infer O>] ? O : never
/**
* @category Constructor
* @since 0.9.4
*/
export function fromRefinement<I, O extends I>(
refinement: Refinement<I, O>,
expected: string,
): Decoder<I, O> {
return {
decode: (i) => (refinement(i) ? T.right(i) : T.left([DE.leaf(i, expected)])),
}
}
/**
* @category Combinator
* @since 0.9.4
*/
export const compose =
<A, O>(second: Decoder<A, O>) =>
<I>(first: Decoder<I, A>): Decoder<I, O> => {
const { chain } = T.getChain(DE.getSemigroup())
return {
decode: (i) => pipe(i, first.decode, chain(second.decode)),
}
}
/**
* @category Constructor
* @since 0.9.4
*/
export const string = fromRefinement((x: unknown): x is string => typeof x === 'string', 'string')
/**
* @category Constructor
* @since 0.9.4
*/
export const number = fromRefinement((x: unknown): x is number => typeof x === 'number', 'number')
/**
* @category Constructor
* @since 0.9.4
*/
export const boolean = fromRefinement(
(x: unknown): x is boolean => typeof x === 'boolean',
'boolean',
)
/**
* @category Decoder
* @since 0.9.5
*/
export const dateFromISOString: Decoder<string, Date> = {
decode: (i) => {
const date = parseISO(i)
const time = date.getTime()
if (Number.isNaN(time)) {
return T.left([leaf(i, `dateFromISOString`)])
}
return T.right(date)
},
}
/**
* @category Instance
* @since 0.9.5
*/
export const WithRefine: WithRefine2C<URI, unknown> = {
refine: (refinment, id) => (from) => pipe(from, compose(fromRefinement(refinment, id))),
}
/**
* @category Combinator
* @since 0.9.5
*/
export const refine = WithRefine.refine
/**
* @category Constructor
* @since 0.9.4
*/
export const union =
<I, O1>(second: Decoder<I, O1>) =>
<O2>(first: Decoder<I, O2>): Decoder<I, O1 | O2> => {
const { concat } = DE.getSemigroup()
return {
decode: (i) =>
pipe(
i,
first.decode,
T.mapLeft((errors) => [DE.member(0, errors)] as const),
T.matchW(
(e1) =>
pipe(
i,
second.decode,
T.mapLeft((errors) => [DE.member(1, errors)] as const),
T.matchW(
(e2) => pipe(e1, concat(e2), T.left),
T.right,
(e2, a) => T.both(pipe(e1, concat(e2)), a),
),
),
T.right,
(e1, o1) =>
pipe(
i,
second.decode,
T.mapLeft((errors) => [DE.member(1, errors)] as const),
T.matchW(
(e2) => T.both(pipe(e1, concat(e2)), o1),
T.right,
(e2) => T.both(pipe(e1, concat(e2)), o1),
),
),
),
) as T.These<DE.DecodeErrors, O1 | O2>,
}
}
/**
* @category Refinement
* @since 0.9.6
*/
export const isDate = (x: unknown): x is Date => x instanceof Date
/**
* @category Constructor
* @since 0.9.6
*/
export const date = pipe(string, refine(isDate, 'Date'), union(fromRefinement(isDate, 'Date')))
/**
* @category Constructor
* @since 0.9.6
*/
export const sum =
<T extends string>(tag: T) =>
<A>(
members: { [K in keyof A]: Decoder<unknown, A[K] & Record<T, K>> },
): Decoder<unknown, A[keyof A]> => {
return {
decode: (i) =>
pipe(
i,
struct(St.make(tag, literal(...Object.keys(members)))).decode,
T.matchW(
T.left,
(a) => members[a[tag] as keyof A].decode(a),
([first, ...rest], a) =>
pipe(
members[a[tag] as keyof A].decode(a),
T.mapLeft((e) => [first, ...rest, ...e]),
),
),
),
}
}
/**
* @category Constructor
* @since 0.9.6
*/
export const literal = <A extends readonly Literal[]>(
...literals: A
): Decoder<unknown, A[number]> =>
fromRefinement((x): x is A[number] => literals.includes(x as any), literals.join(' | '))
/**
* @category Combinator
* @since 0.9.4
*/
export const nullable = union(fromRefinement((x): x is null => x === null, 'null'))
/**
* @category Combinator
* @since 0.9.4
*/
export const optional = union(fromRefinement((x): x is undefined => x === undefined, 'undefined'))
/**
* @category Constructor
* @since 0.9.4
*/
export const unknownArray = fromRefinement<unknown, ReadonlyArray<unknown>>(
Array.isArray,
'Array<unknown>',
)
/**
* @category Constructor
* @since 0.9.4
*/
export const unknownRecord = fromRefinement<unknown, { readonly [key: string]: unknown }>(
(x): x is { readonly [key: string]: unknown } =>
!!x && !Array.isArray(x) && typeof x === 'object',
'Record<string, unknown>',
)
/**
* @category Constructor
* @since 0.9.4
*/
export const fromArray = <O>(
member: Decoder<unknown, O>,
): Decoder<readonly unknown[], readonly O[]> => {
const { concat } = T.getSemigroup(DE.getSemigroup(), RA.getSemigroup<O>())
return {
decode: (inputs) => {
const array = inputs.map((input, index) =>
pipe(
input,
member.decode,
T.mapLeft((e): DE.DecodeErrors => [DE.index(index, e)] as const),
T.map((o): readonly O[] => [o]),
),
)
return array.reduce((acc, x) => pipe(x, concat(acc)), T.right([]))
},
}
}
/**
* @category Constructor
* @since 0.9.4
*/
export const array = <O>(member: Decoder<unknown, O>) =>
pipe(unknownArray, compose(fromArray(member)))
/**
* @category Constructor
* @since 0.9.4
*/
export const fromStruct = <A extends { readonly [key: string]: Decoder<unknown, any> }>(
properties: A,
): Decoder<
Readonly<Record<string, unknown>>,
Partial<{ readonly [K in keyof A]: OutputOf<A[K]> }>
> => {
type O = { readonly [K in keyof A]: OutputOf<A[K]> }
const { concat } = T.getSemigroup(DE.getSemigroup(), St.getAssignSemigroup<any>())
return {
decode: (i) => {
const expectedKeys = Object.keys(properties)
const remainingKeys = expectedKeys.filter((k) => k in i)
const struct = remainingKeys.map((k) =>
pipe(
properties[k].decode(i[k]),
T.mapLeft((e): DE.DecodeErrors => [DE.key(k, e)] as const),
T.map((o: O[keyof O]) => make(k, o)),
),
)
const result = struct.reduce((acc, x) => pipe(x, concat(acc)), T.right({}))
return result as T.These<DE.DecodeErrors, O>
},
}
}
/**
* @category Constructor
* @since 0.9.4
*/
export function missingKeys<A extends { readonly [key: string]: Decoder<unknown, any> }>(
properties: A,
): Decoder<
Readonly<Record<string, unknown>>,
Partial<{ readonly [K in keyof A]: OutputOf<A[K]> }>
> {
type O = Partial<{ readonly [K in keyof A]: OutputOf<A[K]> }>
const diff = RA.difference(S.Eq)
return {
decode: (i) => {
const expectedKeys = Object.keys(properties)
const actualKeys = Object.keys(i)
const missingKeys = pipe(expectedKeys, diff(actualKeys))
const result = RA.isNonEmpty(missingKeys)
? T.both([DE.missingKeys([missingKeys[0], ...missingKeys.slice(1)])] as const, i as O)
: T.right(i as O)
return result
},
}
}
/**
* @category Constructor
* @since 0.9.4
*/
export function unexpectedKeys<A extends { readonly [key: string]: Decoder<unknown, any> }>(
properties: A,
): Decoder<
Readonly<Record<string, unknown>>,
Partial<{ readonly [K in keyof A]: OutputOf<A[K]> }>
> {
type O = { readonly [K in keyof A]: OutputOf<A[K]> }
const diff = RA.difference(S.Eq)
return {
decode: (i) => {
const expectedKeys = Object.keys(properties)
const actualKeys = Object.keys(i)
const unexpectedKeys = pipe(actualKeys, diff(expectedKeys))
const result = RA.isNonEmpty(unexpectedKeys)
? T.both(
[DE.unexpectedKeys([unexpectedKeys[0], ...unexpectedKeys.slice(1)])] as const,
i as O,
)
: T.right(i as O)
return result
},
}
}
/**
* @category Constructor
* @since 0.9.4
*/
export function struct<A extends { readonly [key: string]: Decoder<unknown, any> }>(
properties: A,
): Decoder<unknown, Partial<{ readonly [K in keyof A]: OutputOf<A[K]> }>> {
return pipe(
unknownRecord,
compose(missingKeys(properties)),
compose(unexpectedKeys(properties)),
compose(fromStruct(properties)),
)
}
/**
* @category Constructor
* @since 0.9.4
*/
export function fromRecord<A>(
decoder: Decoder<unknown, A>,
): Decoder<Readonly<Record<string, unknown>>, Readonly<Record<string, A>>> {
const { concat } = T.getSemigroup(DE.getSemigroup(), St.getAssignSemigroup<any>())
return {
decode: (i) => {
const results = Object.entries(i).map(([key, value]) =>
pipe(
value,
decoder.decode,
T.mapLeft((errors): DE.DecodeErrors => [DE.key(key, errors)]),
T.map((b) => ({ [key]: b })),
),
)
return results.reduce((acc, x) => pipe(x, concat(acc)), T.of(Object.create(null)))
},
}
}
/**
* @category Constructor
* @since 0.9.6
*/
export const record = <O>(codomain: Decoder<unknown, O>) =>
pipe(unknownRecord, compose(fromRecord(codomain)))
/**
* @category Constructor
* @since 0.9.4
*/
export function fromTuple<A extends readonly unknown[]>(
...components: { readonly [K in keyof A]: Decoder<unknown, A[K]> }
): Decoder<readonly unknown[], A> {
const { concat } = T.getSemigroup(DE.getSemigroup(), RA.getSemigroup<unknown>())
return {
decode: (input) => {
const tuple = components.map((d, i) =>
pipe(
d.decode(input[i]),
T.mapLeft((errors): DE.DecodeErrors => [DE.index(i, errors)]),
T.map((o): readonly unknown[] => [o]),
),
)
const result = tuple.reduce((acc, x) => pipe(x, concat(acc)), T.right([]))
return result as T.These<DE.DecodeErrors, A>
},
}
}
/**
* @category Constructor
* @since 0.9.4
*/
export function missingIndexes<A extends readonly unknown[]>(
...components: { readonly [K in keyof A]: Decoder<unknown, A[K]> }
): Decoder<readonly unknown[], readonly unknown[]> {
const diff = RA.difference(N.Eq)
return {
decode: (i) => {
const expectedKeys = Object.keys(components).map(parseFloat)
const actualKeys = Object.keys(i).map(parseFloat)
const missingKeys = pipe(expectedKeys, diff(actualKeys))
const result = RA.isNonEmpty(missingKeys)
? T.both([DE.missingIndexes([missingKeys[0], ...missingKeys.slice(1)])] as const, i)
: T.right(i)
return result
},
}
}
/**
* @category Constructor
* @since 0.9.4
*/
export function unexpectedIndexes<A extends readonly unknown[]>(
...components: { readonly [K in keyof A]: Decoder<unknown, A[K]> }
): Decoder<readonly unknown[], readonly unknown[]> {
const diff = RA.difference(S.Eq)
return {
decode: (i) => {
const expectedKeys = Object.keys(components)
const actualKeys = Object.keys(i)
const unexpectedKeys = pipe(actualKeys, diff(expectedKeys))
const result = RA.isNonEmpty(unexpectedKeys)
? T.both([DE.unexpectedKeys([unexpectedKeys[0], ...unexpectedKeys.slice(1)])] as const, i)
: T.right(i)
return result
},
}
}
/**
* @category Constructor
* @since 0.9.4
*/
export function tuple<A extends readonly unknown[]>(
...components: { readonly [K in keyof A]: Decoder<unknown, A[K]> }
): Decoder<unknown, A> {
return pipe(
unknownArray,
compose(missingIndexes(...components)),
compose(unexpectedIndexes(...components)),
compose(fromTuple<A>(...components)),
)
}
/**
* @category Combinator
* @since 0.9.6
*/
export const intersect =
<A, B>(second: Decoder<A, B>) =>
<C, D>(first: Decoder<C, D>): Decoder<A & C, B & D> => {
const { concat } = T.getSemigroup(DE.getSemigroup(), St.getAssignSemigroup<any>())
return {
decode: (i) => concat(second.decode(i))(first.decode(i)),
}
}
/**
* @category Constructor
* @since 0.9.6
*/
export const lazy = <I, O>(id: string, f: () => Decoder<I, O>): Decoder<I, O> => {
const get = memoize((_: void) => f())
return {
decode: (i) =>
pipe(
get().decode(i),
T.mapLeft((errors) => [DE.lazy(id, errors)]),
),
}
}
/**
* @category URI
* @since 0.9.4
*/
export const URI = '@typed/fp/Decoder'
/**
* @category URI
* @since 0.9.4
*/
export type URI = typeof URI
declare module 'fp-ts/HKT' {
export interface URItoKind2<E, A> {
[URI]: Decoder<E, A>
}
}
/**
* @category Constructor
* @since 0.9.4
*/
export const of = <A>(value: A): Decoder<unknown, A> => ({
decode: () => T.right(value),
})
/**
* @category Combinator
* @since 0.9.4
*/
export const map =
<A, B>(f: (value: A) => B) =>
<I>(decoder: Decoder<I, A>): Decoder<I, B> => ({
decode: (i) => pipe(i, decoder.decode, T.map(f)),
})
/**
* @category Combinator
* @since 0.9.4
*/
export function condemnWhen(predicate: Predicate<DE.DecodeError>) {
return <I, A>(decoder: Decoder<I, A>): Decoder<I, A> => ({
decode: (i) =>
pipe(
i,
decoder.decode,
T.matchW(T.left, T.right, (errors, a): T.These<DE.DecodeErrors, A> => {
const { left: absolved, right: condemned } = pipe(
errors,
RA.map(Ei.fromPredicate(predicate)),
RA.separate,
)
return RA.isNonEmpty(condemned)
? T.left(condemned)
: RA.isNonEmpty(absolved)
? T.both(absolved, a)
: T.right(a)
}),
),
})
}
/**
* @category Combinator
* @since 0.9.4
*/
export const condemn = condemnWhen(() => true)
/**
* @category Combinator
* @since 0.9.4
*/
export function absolveWhen(predicate: Predicate<DE.DecodeError>) {
return <I, A>(decoder: Decoder<I, A>): Decoder<I, A> => ({
decode: (i) =>
pipe(
i,
decoder.decode,
T.matchW(T.left, T.right, (errors, a): T.These<DE.DecodeErrors, A> => {
const { left: condemned, right: absolved } = pipe(
errors,
RA.map(Ei.fromPredicate(not(predicate))),
RA.separate,
)
return RA.isNonEmpty(absolved)
? T.right(a)
: RA.isNonEmpty(condemned)
? T.both(condemned, a)
: T.right(a)
}),
),
})
}
/**
* @category Combinator
* @since 0.9.4
*/
export const absolve = absolveWhen(() => true)
/**
* @category Combinator
* @since 0.9.4
*/
export const condemnUnexpectedKeys = condemnWhen((d) => d._tag === 'UnexpectedKeys')
/**
* @category Combinator
* @since 0.9.4
*/
export const condemmMissingKeys = condemnWhen((d) => d._tag === 'MissingKeys') as <I, A>(
decoder: Decoder<I, A>,
) => Decoder<I, UndoPartial<A>>
/**
* @category Combinator
* @since 0.9.4
*/
export const strict = condemnWhen(
(d) => d._tag === 'UnexpectedKeys' || d._tag === 'MissingKeys',
) as <I, A>(decoder: Decoder<I, A>) => Decoder<I, UndoPartial<A>>
/**
* @category Instance
* @since 0.9.4
*/
export const Pointed: Pointed2<URI> = {
of,
}
/**
* @category Instance
* @since 0.9.4
*/
export const Functor: F.Functor2<URI> = {
map,
}
/**
* @category Combinator
* @since 0.9.4
*/
export const bindTo = F.bindTo(Functor)
/**
* @category Combinator
* @since 0.9.4
*/
export const flap = F.flap(Functor)
/**
* @category Combinator
* @since 0.9.4
*/
export const tupled = F.tupled(Functor)
/**
* @category Constructor
* @since 0.9.4
*/
export const fromIO = <A>(io: IO<A>): Decoder<unknown, {}> => ({
decode: () => T.right(io()),
})
/**
* @category Combinator
* @since 0.9.4
*/
export const chain =
<A, I, B>(f: (a: A) => Decoder<I, B>) =>
(decoder: Decoder<I, A>): Decoder<I, B> => ({
decode: (i) =>
pipe(
i,
decoder.decode,
T.matchW(
T.left,
(a) => f(a).decode(i),
(errors, a) => pipe(i, f(a).decode, T.mapLeft(concatW(errors))),
),
),
})
/**
* @category Constructor
* @since 0.9.4
*/
export const Chain: Ch.Chain2<URI> = {
map,
chain,
}
/**
* @category Combinator
* @since 0.9.4
*/
export const ap = Ch.ap(Chain)
/**
* @category Combinator
* @since 0.9.4
*/
export const chainFirst = Ch.chainFirst(Chain)
/**
* @category Combinator
* @since 0.9.4
*/
export const bind = Ch.bind(Chain)
/**
* @category Instance
* @since 0.9.4
*/
export const Apply: Ap.Apply2<URI> = {
map,
ap,
}
/**
* @category Combinator
* @since 0.9.4
*/
export const apFirst = Ap.apFirst(Apply)
/**
* @category Combinator
* @since 0.9.4
*/
export const apS = Ap.apS(Apply)
/**
* @category Combinator
* @since 0.9.4
*/
export const apSecond = Ap.apSecond(Apply)
/**
* @category Combinator
* @since 0.9.4
*/
export const apT = Ap.apT(Apply)
/**
* @category Typeclass Constructor
* @since 0.9.4
*/
export const getApplySemigroup = Ap.getApplySemigroup(Apply)
/**
* @category Instance
* @since 0.9.4
*/
export const Applicative: App.Applicative2<URI> = {
of,
...Apply,
}
/**
* @category Combinator
* @since 0.9.4
*/
export const getApplicativeMonoid = App.getApplicativeMonoid(Applicative)
/**
* @category Constructor
* @since 0.9.4
*/
export const Do: Decoder<unknown, {}> = fromIO(() => Object.create(null))
/**
* @category Instance
* @since 0.9.4
*/
export const Monad: Monad2<URI> = {
...Pointed,
...Chain,
}
/**
* @category Instance
* @since 0.9.5
*/
export const Schemable: Schemable2C<URI, unknown> = {
URI,
literal,
string,
number,
boolean,
date,
nullable,
optional,
struct: struct as Schemable2C<URI, unknown>['struct'],
record,
array,
tuple: tuple as Schemable2C<URI, unknown>['tuple'],
intersect,
sum,
lazy,
branded: ((d) => d) as Schemable2C<URI, unknown>['branded'],
unknownArray,
unknownRecord,
}
/**
* @category Instance
* @since 0.9.5
*/
export const WithUnion: WithUnion2C<URI, unknown> = {
union,
}
/**
* @category Decoder
* @since 0.9.5
*/
export const jsonParseFromString: Decoder<string, Json> = {
decode: (i) => {
try {
return T.right(JSON.parse(i) as Json)
} catch (e) {
return T.left([DE.leaf(i, `Json`)])
}
},
}
/**
* @category Decoder
* @since 0.9.5
*/
export const jsonParse = pipe(string, compose(jsonParseFromString))
/**
* Throw if not a valid decoder. Absolves optional errors
* @category Interpreter
* @since 0.9.5
*/
export const assert =
<I, O>(decoder: Decoder<I, O>) =>
(i: I): O =>
pipe(
i,
decoder.decode,
T.absolve,
Ei.matchW(
(errors) => {
throw new Error(DE.drawErrors(errors))
},
(o) => o,
),
)
/**
* Throw if not a valid decoder. Condemns optional errors
* @category Interpreter
* @since 0.9.5
*/
export const assertStrict =
<I, O>(decoder: Decoder<I, O>) =>
(i: I): Required<O> =>
pipe(
i,
decoder.decode,
T.condemn,
Ei.matchW(
(errors) => {
throw new Error(DE.drawErrors(errors))
},
(o) => o as Required<O>,
),
)