const isString = (input: unknown): input is string => { return typeof input === 'string' } const isBoolean = (input: unknown): input is boolean => { return typeof input === 'boolean' } const isNumber = (input: unknown): input is number => { return typeof input === 'number' && !isNaN(input) && isFinite(input) } const isInteger = (input: unknown): input is number => { return isNumber(input) && /^(\+|-)?\d+$/.test(input.toString()) } const isArray = (input: unknown): input is Array => { return input instanceof Array } const isObject = (input: unknown): input is Record => { return typeof input === 'object' && input !== null && !isArray(input) } const hasOwnProperty = ( prop: string, obj: Record ): boolean => { return Object.prototype.hasOwnProperty.call(obj, prop) } // E R R O R export type DecodeError = | { type: 'RUNTIME_EXCEPTION'; error: Error } | { type: 'ONE_OF'; errors: Array } | { type: 'OPTIONAL'; error: DecodeError } | { type: 'IN_FIELD'; name: string; error: DecodeError } | { type: 'AT_INDEX'; position: number; error: DecodeError } | { type: 'REQUIRED_FIELD'; name: string; source: Record } | { type: 'REQUIRED_INDEX'; position: number; source: Array } | { type: 'FAILURE'; message: string; source: unknown } | { type: 'EXPECT_STRING'; source: unknown } | { type: 'EXPECT_BOOLEAN'; source: unknown } | { type: 'EXPECT_INT'; source: unknown } | { type: 'EXPECT_FLOAT'; source: unknown } | { type: 'EXPECT_OBJECT'; source: unknown } | { type: 'EXPECT_ARRAY'; source: unknown } | { type: 'EXPECT_EXACT' value: string | number | boolean | null source: unknown } export type DecodeJsonError = | DecodeError | { type: 'INVALID_JSON'; error: SyntaxError; source: string } const InvalidJsonError = ( error: SyntaxError, json: string ): DecodeJsonError => ({ type: 'INVALID_JSON', error, source: json }) const RuntimeExceptionError = (error: Error): DecodeError => ({ type: 'RUNTIME_EXCEPTION', error }) const OneOfError = (errors: Array): DecodeError => ({ type: 'ONE_OF', errors }) const OptionalError = (error: DecodeError): DecodeError => ({ type: 'OPTIONAL', error }) const InFieldError = (name: string, error: DecodeError): DecodeError => ({ type: 'IN_FIELD', name, error }) const AtIndexError = (position: number, error: DecodeError): DecodeError => ({ type: 'AT_INDEX', position, error }) const RequiredFieldError = ( name: string, source: Record ): DecodeError => ({ type: 'REQUIRED_FIELD', name, source }) const RequiredIndexError = ( position: number, source: Array ): DecodeError => ({ type: 'REQUIRED_INDEX', position, source }) const FailureError = (message: string, source: unknown): DecodeError => ({ type: 'FAILURE', message, source }) const ExpectExactError = ( value: string | number | boolean | null, source: unknown ): DecodeError => ({ type: 'EXPECT_EXACT', value, source }) const ExpectStringError = (source: unknown): DecodeError => ({ type: 'EXPECT_STRING', source }) const ExpectBooleanError = (source: unknown): DecodeError => ({ type: 'EXPECT_BOOLEAN', source }) const ExpectIntError = (source: unknown): DecodeError => ({ type: 'EXPECT_INT', source }) const ExpectFloatError = (source: unknown): DecodeError => ({ type: 'EXPECT_FLOAT', source }) const ExpectObjectError = (source: unknown): DecodeError => ({ type: 'EXPECT_OBJECT', source }) const ExpectArrayError = (source: unknown): DecodeError => ({ type: 'EXPECT_ARRAY', source }) // R E S U L T export type DecodeResult = | { error: E; value?: never } | { error?: never; value: T } const Left = (error: E): DecodeResult => ({ error }) const Right = (value: T): DecodeResult => ({ value }) // D E C O D E R export interface Decoder { map(fn: (value: T) => R): Decoder chain(fn: (value: T) => Decoder): Decoder decode(input: unknown): DecodeResult decodeJSON(json: string): DecodeResult } abstract class DecoderImpl implements Decoder { public map(fn: (value: T) => R): Decoder { return new MapDecoder(fn, this) } public chain(fn: (value: T) => Decoder): Decoder { return new ChainDecoder(fn, this) } public decodeJSON(json: string): DecodeResult { try { return this.decode(JSON.parse(json)) } catch (jsonError) { return Left(InvalidJsonError(jsonError, json)) } } public decode(input: unknown): DecodeResult { try { return this.run(input) } catch (unknownError) { return Left(RuntimeExceptionError(unknownError)) } } protected abstract run(input: unknown): DecodeResult } class MapDecoder extends DecoderImpl { public constructor( private readonly fn: (value: T) => R, protected readonly decoder: Decoder ) { super() } protected run(input: unknown): DecodeResult { const result = this.decoder.decode(input) if (result.error != null) { return result } return Right(this.fn(result.value)) } } class ChainDecoder extends DecoderImpl { public constructor( private readonly fn: (value: T) => Decoder, protected readonly decoder: Decoder ) { super() } protected run(input: unknown): DecodeResult { const result = this.decoder.decode(input) if (result.error != null) { return result } return this.fn(result.value).decode(input) } } class PrimitiveDecoder extends DecoderImpl { public constructor( private readonly createError: (source: unknown) => DecodeError, private readonly check: (input: unknown) => input is T ) { super() } protected run(input: unknown): DecodeResult { if (this.check(input)) { return Right(input) } return Left(this.createError(input)) } } class UnknownDecoder extends DecoderImpl { // eslint-disable-next-line class-methods-use-this protected run(input: unknown): DecodeResult { return Right(input) } } class ExactDecoder extends DecoderImpl { public constructor( private readonly expect: string | number | boolean | null, private readonly value: T ) { super() } protected run(input: unknown): DecodeResult { if (input === this.expect) { return Right(this.value) } return Left(ExpectExactError(this.expect, input)) } } class FailDecoder extends DecoderImpl { public constructor(private readonly message: string) { super() } protected run(input: unknown): DecodeResult { return Left(FailureError(this.message, input)) } } class SucceedDecoder extends DecoderImpl { public constructor(private readonly value: T) { super() } protected run(): DecodeResult { return Right(this.value) } } class NullableDecoder extends DecoderImpl { public constructor(private readonly decoder: Decoder) { super() } protected run(input: unknown): DecodeResult { if (input == null) { return Right(null) } const result = this.decoder.decode(input) if (result.error != null) { return Left(OptionalError(result.error)) } return result } } class KeyValueDecoder extends DecoderImpl> { public constructor( private readonly convertKey: (key: string) => DecodeResult, private readonly itemDecoder: Decoder ) { super() } protected run(input: unknown): DecodeResult> { if (!isObject(input)) { return Left(ExpectObjectError(input)) } const acc: Array<[K, T]> = [] for (const key of Object.keys(input)) { const keyResult = this.convertKey(key) if (keyResult.error != null) { return Left(FailureError(keyResult.error, key)) } const itemResult = this.itemDecoder.decode(input[key]) if (itemResult.error != null) { return Left(InFieldError(key, itemResult.error)) } acc.push([keyResult.value, itemResult.value]) } return Right(acc) } } class RecordDecoder extends DecoderImpl> { public constructor(private readonly itemDecoder: Decoder) { super() } protected run(input: unknown): DecodeResult> { if (!isObject(input)) { return Left(ExpectObjectError(input)) } const acc: Record = {} for (const key of Object.keys(input)) { const itemResult = this.itemDecoder.decode(input[key]) if (itemResult.error != null) { return Left(InFieldError(key, itemResult.error)) } acc[key] = itemResult.value } return Right(acc) } } class ShapeDecoder extends DecoderImpl { public constructor( private readonly schema: { [K in keyof T]: Decoder } ) { super() } protected run(input: unknown): DecodeResult { const acc = {} as T for (const key of Object.keys(this.schema)) { const keyResult = this.schema[key as keyof T].decode(input) if (keyResult.error != null) { return keyResult } acc[key as keyof T] = keyResult.value } return Right(acc) } } class ListDecoder extends DecoderImpl> { public constructor(private readonly itemDecoder: Decoder) { super() } protected run(input: unknown): DecodeResult> { if (!isArray(input)) { return Left(ExpectArrayError(input)) } const N = input.length const acc: Array = new Array(N) for (let i = 0; i < input.length; i++) { const itemResult = this.itemDecoder.decode(input[i]) if (itemResult.error != null) { return Left(AtIndexError(i, itemResult.error)) } acc[i] = itemResult.value } return Right(acc) } } class OneOfDecoder extends DecoderImpl { public constructor(private readonly options: Array>) { super() } protected run(input: unknown): DecodeResult { const errors: Array = [] for (const option of this.options) { const optionResult = option.decode(input) if (optionResult.error == null) { return optionResult } errors.push(optionResult.error) } return Left(OneOfError(errors)) } } class RequiredFieldDecoder extends DecoderImpl { public constructor( private readonly name: string, private readonly decoder: Decoder ) { super() } protected fieldNotDefined( input: Record ): DecodeResult { return Left(RequiredFieldError(this.name, input)) } protected run(input: unknown): DecodeResult { if (!isObject(input)) { return Left(ExpectObjectError(input)) } if (!hasOwnProperty(this.name, input)) { return this.fieldNotDefined(input) } const result = this.decoder.decode(input[this.name]) if (result.error != null) { return Left(InFieldError(this.name, result.error)) } return result } } class OptionalFieldDecoder extends RequiredFieldDecoder { // eslint-disable-next-line class-methods-use-this protected fieldNotDefined(): DecodeResult { return Right(null) } } class RequiredIndexDecoder extends DecoderImpl { public constructor( private readonly position: number, private readonly decoder: Decoder ) { super() } protected outOfRange(input: Array): DecodeResult { return Left(RequiredIndexError(this.position, input)) } protected run(input: unknown): DecodeResult { if (!isArray(input)) { return Left(ExpectArrayError(input)) } if (this.position < 0 || this.position >= input.length) { return this.outOfRange(input) } const result = this.decoder.decode(input[this.position]) if (result.error != null) { return Left(AtIndexError(this.position, result.error)) } return result } } class OptionalIndexDecoder extends RequiredIndexDecoder { // eslint-disable-next-line class-methods-use-this protected outOfRange(): DecodeResult { return Right(null) } } export interface DecodeOptional { string: Decoder boolean: Decoder int: Decoder float: Decoder list: MakeList record: MakeRecord keyValue: MakeKeyValue field: MakeField index: MakeIndex } class Optional implements DecodeOptional { public constructor( private readonly createDecoder: ( decoder: Decoder ) => Decoder ) {} private of(decoder: Decoder): Decoder { return this.createDecoder(new NullableDecoder(decoder)) } public get string(): Decoder { return this.of(string) } public get boolean(): Decoder { return this.of(boolean) } public get int(): Decoder { return this.of(int) } public get float(): Decoder { return this.of(float) } public list(itemDecoder: Decoder): Decoder> { return this.of(list(itemDecoder)) } public record(itemDecoder: Decoder): Decoder> { return this.of(record(itemDecoder)) } public keyValue( ...args: | [Decoder] | [(key: string) => DecodeResult, Decoder] ): Decoder> { return this.of(keyValueHelp(args)) } public field(name: string): OptionalDecodePath { return pathHelp(decoder => { return this.of(new OptionalFieldDecoder(name, decoder)) }) } public index(position: number): OptionalDecodePath { return pathHelp(decoder => { return this.of(new OptionalIndexDecoder(position, decoder)) }) } } interface DecodePath { optional: DecodeOptional unknown: Decoder string: Decoder boolean: Decoder int: Decoder float: Decoder list: MakeList record: MakeRecord keyValue: MakeKeyValue tuple: MakeTuple shape: MakeShape exact: MakeExact oneOf: MakeOneOf lazy: MakeLazy field: MakeField index: MakeIndex of(decoder: Decoder): Decoder } export type RequiredDecodePath = DecodePath export type OptionalDecodePath = DecodePath class PathDecoder implements DecodePath { public constructor( protected readonly createDecoder: ( decoder: Decoder ) => Decoder ) {} public of(decoder: Decoder): Decoder { return this.createDecoder(decoder) } public get optional(): DecodeOptional { return new Optional(this.createDecoder) } public get unknown(): Decoder { return this.of(unknown) } public get string(): Decoder { return this.of(string) } public get boolean(): Decoder { return this.of(boolean) } public get int(): Decoder { return this.of(int) } public get float(): Decoder { return this.of(float) } public exact( ...args: | [string | number | boolean | null] | [string | number | boolean | null, T] ): Decoder { return this.of(exactHelp(args)) } public lazy( lazyDecoder: () => Decoder ): Decoder { return this.of(lazy(lazyDecoder)) } public list( itemDecoder: Decoder ): Decoder : null | Array> { return this.of(list(itemDecoder)) } public tuple>( ...schema: [Array>] | Array> ): Decoder { return this.of(tupleHelp(schema)) } public record( itemDecoder: Decoder ): Decoder : null | Record> { return this.of(record(itemDecoder)) } public shape>( schema: { [K in keyof T]: Decoder } ): Decoder { return this.of(shape(schema)) } public keyValue( ...args: | [Decoder] | [(key: string) => DecodeResult, Decoder] ): Decoder< X extends true ? Array<[K | string, T]> : null | Array<[K | string, T]> > { return this.of(keyValueHelp(args)) } public oneOf( ...args: [Array>] | Array> ): Decoder { return this.of(oneOfHelp(args)) } public field( name: string ): X extends true ? RequiredDecodePath : OptionalDecodePath { return pathHelp(decoder => { return this.of(new RequiredFieldDecoder(name, decoder)) }) } public index( position: number ): X extends true ? RequiredDecodePath : OptionalDecodePath { return pathHelp(decoder => { return this.of(new RequiredIndexDecoder(position, decoder)) }) } } // ------------------------- // -- P U B L I C A P I -- // ------------------------- const optional: DecodeOptional = new Optional(decoder => decoder) const unknown: Decoder = new UnknownDecoder() const string: Decoder = new PrimitiveDecoder( ExpectStringError, isString ) const boolean: Decoder = new PrimitiveDecoder( ExpectBooleanError, isBoolean ) const int: Decoder = new PrimitiveDecoder(ExpectIntError, isInteger) const float: Decoder = new PrimitiveDecoder(ExpectFloatError, isNumber) function fail(message: string): Decoder { return new FailDecoder(message) } function succeed(value: T): Decoder { return new SucceedDecoder(value) } // E X A C T interface MakeExact { (value: T): Decoder< X extends true ? T : null | T > (expect: string | number | boolean | null, value: T): Decoder< X extends true ? T : null | T > } const exactHelp = ( args: | [string | number | boolean | null] | [string | number | boolean | null, T] ): Decoder => { if (args.length === 1) { return new ExactDecoder(args[0], args[0]) } return new ExactDecoder(args[0], args[1]) } const exact: MakeExact = ( ...args: | [string | number | boolean | null] | [string | number | boolean | null, T] ) => exactHelp(args) // R E C O R D type MakeRecord = ( itemDecoder: Decoder ) => Decoder : null | Record> const record: MakeRecord = itemDecoder => new RecordDecoder(itemDecoder) // S H A P E type MakeShape = >( schema: { [K in keyof T]: Decoder } ) => Decoder const shape: MakeShape = schema => new ShapeDecoder(schema) // T U P L E type TupleSchema> = T extends [ infer A, ...infer R ] ? [Decoder, ...TupleSchema] : [] interface MakeTuple { (schema: [Decoder, Decoder]): Decoder< X extends true ? [T1, T2] : null | [T1, T2] > (_1: Decoder, _2: Decoder): Decoder< X extends true ? [T1, T2] : null | [T1, T2] > (schema: [Decoder, Decoder, Decoder]): Decoder< X extends true ? [T1, T2, T3] : null | [T1, T2, T3] > (_1: Decoder, _2: Decoder, _3: Decoder): Decoder< X extends true ? [T1, T2, T3] : null | [T1, T2, T3] > ( schema: [Decoder, Decoder, Decoder, Decoder] ): Decoder ( _1: Decoder, _2: Decoder, _3: Decoder, _4: Decoder ): Decoder ( schema: [Decoder, Decoder, Decoder, Decoder, Decoder] ): Decoder< X extends true ? [T1, T2, T3, T4, T5] : null | [T1, T2, T3, T4, T5] > ( _1: Decoder, _2: Decoder, _3: Decoder, _4: Decoder, _5: Decoder ): Decoder< X extends true ? [T1, T2, T3, T4, T5] : null | [T1, T2, T3, T4, T5] > ( schema: [ Decoder, Decoder, Decoder, Decoder, Decoder, Decoder ] ): Decoder< X extends true ? [T1, T2, T3, T4, T5, T6] : null | [T1, T2, T3, T4, T5, T6] > ( _1: Decoder, _2: Decoder, _3: Decoder, _4: Decoder, _5: Decoder, _6: Decoder ): Decoder< X extends true ? [T1, T2, T3, T4, T5, T6] : null | [T1, T2, T3, T4, T5, T6] > >(schema: TupleSchema): Decoder< X extends true ? T : null | T > >(...schema: TupleSchema): Decoder< X extends true ? T : null | T > } const tupleHelp = >( schema: [Array>] | Array> ): Decoder => { const decoders = schema.length === 1 && isArray(schema[0]) ? schema[0] : (schema as Array>) const obj: Record> = {} const N = decoders.length for (let i = 0; i < N; i++) { obj[i] = decoders[i] } return shape(obj).map(rec => { const arr = new Array(N) as T for (let i = 0; i < N; i++) { arr[i] = rec[i] } return arr }) } const tuple: MakeTuple = >( ...schema: [Array>] | Array> ): Decoder => tupleHelp(schema) // L I S T type MakeList = ( itemDecoder: Decoder ) => Decoder : null | Array> const list: MakeList = itemDecoder => new ListDecoder(itemDecoder) // K E Y V A L U E interface MakeKeyValue { (itemDecoder: Decoder): Decoder< X extends true ? Array<[string, T]> : null | Array<[string, T]> > ( convertKey: (key: string) => DecodeResult, itemDecoder: Decoder ): Decoder : null | Array<[K, T]>> } const keyValueHelp = ( args: [Decoder] | [(key: string) => DecodeResult, Decoder] ): Decoder> => { const [convertKey, itemDecoder] = args.length === 1 ? [Right, args[0]] : args return new KeyValueDecoder(convertKey, itemDecoder) } const keyValue: MakeKeyValue = ( ...args: [Decoder] | [(key: string) => DecodeResult, Decoder] ) => { return keyValueHelp(args) } // O N E O F interface MakeOneOf { (options: Array>): Decoder ( first: Decoder, second: Decoder, ...options: Array> ): Decoder } const oneOfHelp = ( args: [Array>] | Array> ): Decoder => { if (args.length === 1 && isArray(args[0])) { return new OneOfDecoder(args[0]) } return new OneOfDecoder(args as Array>) } const oneOf: MakeOneOf = ( ...args: [Array>] | Array> ) => oneOfHelp(args) // L A Z Y type MakeLazy = ( lazyDecoder: () => Decoder ) => Decoder const lazy: MakeLazy = lazyDecoder => succeed(null).chain(lazyDecoder) // F I E L D A N D I N D E X const pathHelp = ( createDecoder: ( decoder: Decoder ) => Decoder ): X extends true ? RequiredDecodePath : OptionalDecodePath => { const pathDecoder = new PathDecoder( (decoder: Decoder): Decoder => { return createDecoder(decoder) } ) return (pathDecoder as unknown) as X extends true ? RequiredDecodePath : OptionalDecodePath } type MakeField = ( fieldName: string ) => X extends true ? RequiredDecodePath : OptionalDecodePath const field: MakeField = name => { return pathHelp(decoder => new RequiredFieldDecoder(name, decoder)) } type MakeIndex = ( elementPosition: number ) => X extends true ? RequiredDecodePath : OptionalDecodePath const index: MakeIndex = position => { return pathHelp(decoder => new RequiredIndexDecoder(position, decoder)) } const Decode = { optional, field, index, unknown, string, boolean, int, float, exact, record, list, keyValue, shape, tuple, oneOf, lazy, fail, succeed } export default Decode