import { isArray, isBoolean, isNumber, isPlainObject, isString, values } from 'lodash'; import { Seq } from 'wdk-client/Utils/IterableUtils'; /** * Validate and parse JSON strings into TypeScript/JavaScript objects. * * Inspired by Elm's Json.Decode library (https://guide.elm-lang.org/interop/json.html). * * This module exposes: * - primitive decoders (`string`, `number`, `boolean`, etc) * - higher-order decoders for creating more sophisticated decoders (`oneOf`, * `field`, etc) * - a function for using the decoders (`decode`) * * In most cases, a decoder will be used to describe data we expect to receive * from a web service to ensure it conforms to types resident in the client * application. If the data does not conform, an error will be thrown describing * the first encountered problem. */ export type Decoder = (t: any) => Result; // Get underlying type of Decoder (e.g, T of Decoder) export type Unpack = T extends Decoder ? R : never; // Decoder return types // -------------------- export type Ok = { status: 'ok'; value: T; } export type Err = { status: 'err'; value: any; expected: string; context?: string; } export type Result = Ok | Err; export function ok(value: T): Ok { return { status: 'ok', value }; } export function err(value: any, expected: string, context: string = ''): Err { return { status: 'err', value, expected, context}; } // primitives // ---------- export function string(t: any): Result { return isString(t) ? ok(t) : err(t, 'string'); } export function number(t: any): Result { return isNumber(t) ? ok(t) : err(t, 'number'); } export function boolean(t: any): Result { return isBoolean(t) ? ok(t) : err(t, 'boolean'); } export function nullValue(t: any): Result { return t === null ? ok(t) : err(t, 'null'); } export function none(t: any): Result { return t === undefined ? ok(t) : err(t, 'undefined'); } export function unknown(t: unknown): Result { return ok(t); } // higher order decoders // --------------------- // Expect a specific value export function constant(value: T): Decoder; export function constant(value: T) { return function constantGuard(t: any): Result { return t === value ? ok(t) : err(t, JSON.stringify(value)) } } // Expect an object with string keys and values of a specific type export function objectOf(decoder: Decoder) { return function objectOfDecoder(t: any): Result> { if (!isPlainObject(t)) return err(t, 'object'); const e = values(t).map(decoder).find(r => r.status === 'err') as Err; return e == null ? ok(t) : err(e.value, e.expected, `[string]${e.context}`); } } // Expect an array with elements of a specific type export function arrayOf(decoder: Decoder) { return function arrayOfDecoder(t: any): Result> { if (!isArray(t)) return err(t, `Array<${decoder.name}>`); const e = t.map(decoder).find(r => r.status === 'err') as Err; return e == null ? ok(t) : err(e.value, e.expected, `[number]${e.context}`); } } // Expect an object with a specific field whose value is of a specific type. // Additional fields are ok and will be ignored. // `field` decoders can be combined using `combine` to describe more properties // of an object. export function field(fieldName: S, decoder: Decoder) { return function fieldDecoder(t: any): Result<{ [K in S]: T }> { if (!(isPlainObject(t))) return err(t, `object`); const r = decoder(t[fieldName]); return r.status === 'ok' ? ok(t) : err(r.value, r.expected, `.${fieldName}${r.context}`); } } export function record(decoderRecord: { [K in keyof T]: Decoder } ): Decoder { return function decodeRecord(t: any): Result { if (!isPlainObject(t)) return err(t, `object`); for (const key in decoderRecord) { const r = decoderRecord[key](t[key]); if (r.status === 'err') return err(r.value, r.expected, `.${key}${r.context}`); } return ok(t); } } // Expect a value of a specific type, or `undefined` export function optional(decoder: Decoder) { return function optionalGuard(t?: any): Result { return t === undefined ? ok(t) : decoder(t); } } // Combine multiple decoders such that all must return Ok export function combine(decoder1: Decoder): Decoder; export function combine(decoder1: Decoder, decoder2: Decoder): Decoder; export function combine(decoder1: Decoder, decoder2: Decoder, decoder3: Decoder): Decoder; export function combine(decoder1: Decoder, decoder2: Decoder, decoder3: Decoder, decoder4: Decoder): Decoder; export function combine(decoder1: Decoder, decoder2: Decoder, decoder3: Decoder, decoder4: Decoder, decoder5: Decoder

): Decoder; export function combine(decoder1: Decoder, decoder2: Decoder, decoder3: Decoder, decoder4: Decoder, decoder5: Decoder

, decoder6: Decoder): Decoder; export function combine(decoder1: Decoder, decoder2: Decoder, decoder3: Decoder, decoder4: Decoder, decoder5: Decoder

, decoder6: Decoder, decoder7: Decoder): Decoder; export function combine(decoder1: Decoder, decoder2: Decoder, decoder3: Decoder, decoder4: Decoder, decoder5: Decoder

, decoder6: Decoder, decoder7: Decoder, decoder8: Decoder): Decoder; export function combine(decoder1: Decoder, decoder2: Decoder, decoder3: Decoder, decoder4: Decoder, decoder5: Decoder

, decoder6: Decoder, decoder7: Decoder, decoder8: Decoder, decoder9: Decoder): Decoder; export function combine(decoder1: Decoder, decoder2: Decoder, decoder3: Decoder, decoder4: Decoder, decoder5: Decoder

, decoder6: Decoder, decoder7: Decoder, decoder8: Decoder, decoder9: Decoder, decoder10: Decoder): Decoder; export function combine(...decoders: Decoder[]): Decoder; export function combine(...decoders: any[]) { return function combineDecoder(t: any) { return Seq.from(decoders).map(d => d(t)).find(r => r.status === 'err') || ok(t); } } // Combine multiple decoders such that at least one must return Ok export function oneOf(decoder1: Decoder, decoder2: Decoder): Decoder; export function oneOf(decoder1: Decoder, decoder2: Decoder, decoder3: Decoder): Decoder; export function oneOf(decoder1: Decoder, decoder2: Decoder, decoder3: Decoder, decoder4: Decoder): Decoder; export function oneOf(decoder1: Decoder, decoder2: Decoder, decoder3: Decoder, decoder4: Decoder, decoder5: Decoder

): Decoder; export function oneOf(decoder1: Decoder, decoder2: Decoder, decoder3: Decoder, decoder4: Decoder, decoder5: Decoder

, decoder6: Decoder): Decoder; export function oneOf(decoder1: Decoder, decoder2: Decoder, decoder3: Decoder, decoder4: Decoder, decoder5: Decoder

, decoder6: Decoder, decoder7: Decoder): Decoder; export function oneOf(decoder1: Decoder, decoder2: Decoder, decoder3: Decoder, decoder4: Decoder, decoder5: Decoder

, decoder6: Decoder, decoder7: Decoder, decoder8: Decoder): Decoder; export function oneOf(decoder1: Decoder, decoder2: Decoder, decoder3: Decoder, decoder4: Decoder, decoder5: Decoder

, decoder6: Decoder, decoder7: Decoder, decoder8: Decoder, decoder9: Decoder): Decoder; export function oneOf(decoder1: Decoder, decoder2: Decoder, decoder3: Decoder, decoder4: Decoder, decoder5: Decoder

, decoder6: Decoder, decoder7: Decoder, decoder8: Decoder, decoder9: Decoder, decoder10: Decoder): Decoder; export function oneOf(decoder1: Decoder, decoder2: Decoder, decoder3: Decoder, decoder4: Decoder, decoder5: Decoder

, decoder6: Decoder, decoder7: Decoder, decoder8: Decoder, decoder9: Decoder, decoder10: Decoder, decoder11: Decoder): Decoder; export function oneOf(...decoders: Decoder[]): Decoder; export function oneOf(...decoders: any[]) { return function oneOfDecoder(t: any) { const results = Seq.from(decoders).map(d => d(t)); const okResult = results.find(r => r.status === 'ok') if (okResult) return ok(t); // build error message return err( results.map(e => e.value).first(), `${results.map(e => e.expected).join(' | ')}`, results.map(e => e.context).first() ); } } // Combine multiple decoders such that at least one must return Ok export function tuple(decoder1: Decoder, decoder2: Decoder): Decoder<[T, S]>; export function tuple(decoder1: Decoder, decoder2: Decoder, decoder3: Decoder): Decoder<[T, S, R]>; export function tuple(decoder1: Decoder, decoder2: Decoder, decoder3: Decoder, decoder4: Decoder): Decoder<[T, S, R, Q]>; export function tuple(decoder1: Decoder, decoder2: Decoder, decoder3: Decoder, decoder4: Decoder, decoder5: Decoder

): Decoder<[T, S, R, Q, P]>; export function tuple(decoder1: Decoder, decoder2: Decoder, decoder3: Decoder, decoder4: Decoder, decoder5: Decoder

, decoder6: Decoder): Decoder<[T, S, R, Q, P, O]>; export function tuple(decoder1: Decoder, decoder2: Decoder, decoder3: Decoder, decoder4: Decoder, decoder5: Decoder

, decoder6: Decoder, decoder7: Decoder): Decoder<[T, S, R, Q, P, O, N]>; export function tuple(decoder1: Decoder, decoder2: Decoder, decoder3: Decoder, decoder4: Decoder, decoder5: Decoder

, decoder6: Decoder, decoder7: Decoder, decoder8: Decoder): Decoder<[T, S, R, Q, P, O, N, M]>; export function tuple(decoder1: Decoder, decoder2: Decoder, decoder3: Decoder, decoder4: Decoder, decoder5: Decoder

, decoder6: Decoder, decoder7: Decoder, decoder8: Decoder, decoder9: Decoder): Decoder<[T, S, R, Q, P, O, N, M, L]>; export function tuple(decoder1: Decoder, decoder2: Decoder, decoder3: Decoder, decoder4: Decoder, decoder5: Decoder

, decoder6: Decoder, decoder7: Decoder, decoder8: Decoder, decoder9: Decoder, decoder10: Decoder): Decoder<[T, S, R, Q, P, O, N, M, L, K]>; export function tuple(decoder1: Decoder, decoder2: Decoder, decoder3: Decoder, decoder4: Decoder, decoder5: Decoder

, decoder6: Decoder, decoder7: Decoder, decoder8: Decoder, decoder9: Decoder, decoder10: Decoder, decoder11: Decoder): Decoder; export function tuple(...decoders: Decoder[]): Decoder; export function tuple(...decoders: Decoder[]) { return function tupleDecoder(t: any) { if (!isArray(t)) return err(t, 'an array'); const results = decoders.map((d, i) => d(t[i])); return results.find(r => r.status === 'ok') ? ok(t) : err(t, `[ ${decoders.map(d => d.name).join(', ')} ]`) } } // Ensure that a decoder is not evaluated until it is needed. This is useful for // recursive types (like trees). export function lazy(decoderThunk: () => Decoder) { return function lazyGuard(t: any): Result { return decoderThunk()(t); } } // Decoder // ------- // Run `decoder` on `jsonString` export function decode(decoder: Decoder, jsonString: string): T { let t: any; try { t = JSON.parse(jsonString); } catch (error) { throw new Error("Provided JSON is not valid: " + error.message); } const r = decoder(t); if (r.status === 'err') { throw new Error(`Could not decode string: ${standardErrorReport(r)}`); } return r.value; } export function standardErrorReport(errResult: Err) { return `Expected ${errResult.expected}${errResult.context ? (' at _' + errResult.context) : ''}, but got ${JSON.stringify(errResult.value)}.`; } export function decodeOrElse(decoder: Decoder, defaultValue: T, jsonString: string): T { try { return decode(decoder, jsonString); } catch (e) { console.warn('Unable to decode value. Falling back to default.', e); return defaultValue; } }