Created
February 8, 2021 17:06
-
-
Save JSuder-xx/8d27ca5a70960daeb2cb18cec709ad80 to your computer and use it in GitHub Desktop.
A micro JSON decoder API in TypeScript.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** Micro Api for building decoders. */ | |
module DecoderApi { | |
type Unknown = unknown; | |
type ErrorMessage = { kind: "error"; error: string } | |
export const errorMessage = (error: string): ErrorMessage => ({ kind: "error", error }); | |
export type DecodeResult<value> = value | ErrorMessage | |
export const isErrorMessage = <value extends unknown>(result: DecodeResult<value>): result is ErrorMessage => | |
!!result && | |
typeof result === "object" && | |
(result as any).kind === "error"; | |
export const isSuccess = <value extends unknown>(result: DecodeResult<value>): result is value => !isErrorMessage(result); | |
export type Decoder<refined> = (val: Unknown) => DecodeResult<refined>; | |
export type TypeOfDecoder<refiner> = refiner extends Decoder<infer refined> ? refined : never; | |
export type DecoderMap = { [propertyName: string]: Decoder<any> } | |
export type ObjectFromDecoderMap<map extends { [propertyName: string]: Decoder<any> }> = { | |
[propertyName in keyof map]: TypeOfDecoder<map[propertyName]>; | |
} | |
export const decodeString: Decoder<string> = (val: Unknown) => typeof val === "string" ? val : errorMessage(`Expected a string but got ${typeof val}`); | |
export const decodeNumber: Decoder<number> = (val: Unknown) => typeof val === "number" ? val : errorMessage(`Expected a number but got ${typeof val}`); | |
export const decodeBoolean: Decoder<boolean> = (val: Unknown) => typeof val === "boolean" ? val : errorMessage(`Expected a boolean but got ${typeof val}`); | |
export const decodeArray: Decoder<any[]> = (val: Unknown) => !!val && typeof (val as any).push === "function" ? (val as any[]) : errorMessage(`Expected an array`); | |
/** Create a predicate which tests for an object. */ | |
export const createObjectDecoder = <decoderMap extends DecoderMap>(decoderMap: decoderMap): Decoder<ObjectFromDecoderMap<decoderMap>> => | |
(obj: any) => { | |
if (obj === null) return errorMessage(`Expected object; got null.`); | |
if (typeof obj !== "object") return errorMessage(`Expected object; got ${typeof obj}`); | |
for(const propertyName in decoderMap) { | |
const decoder = decoderMap[propertyName]; | |
const result = decoder(obj[propertyName]); | |
if (isErrorMessage(result)) | |
return errorMessage(`For property '${propertyName}': ${result.error}`); | |
} | |
return obj; | |
} | |
export const createArrayDecoder = <Item extends Unknown>(itemDecoder: Decoder<Item>): Decoder<Item[]> => | |
(val: any) => { | |
const arrayResult = decodeArray(val); | |
if (isErrorMessage(arrayResult)) return arrayResult; | |
for(let idx = 0; idx < arrayResult.length; idx++) { | |
const result = itemDecoder(arrayResult[idx]); | |
if (isErrorMessage(result)) | |
return errorMessage(`Array index ${idx}: ${result.error}`); | |
} | |
return val; | |
} | |
/** If you trust it will be a homogenous array then use this for performance. */ | |
export const createHomogeneousDecoder = <Item extends Unknown>(itemDecoder: Decoder<Item>): Decoder<Item[]> => | |
(val: any) => { | |
const arrayResult = decodeArray(val); | |
if (isErrorMessage(arrayResult)) return arrayResult; | |
if (arrayResult.length === 0) return arrayResult; | |
const firstItemResult = itemDecoder(arrayResult[0]); | |
return isErrorMessage(firstItemResult) | |
? errorMessage(`For array: ${firstItemResult.error}`) | |
: val; | |
} | |
export const decodeValue = <value extends unknown>(value: value): Decoder<value> => (val: any) => (val === value) ? val : errorMessage(`Expecting ${value}`); | |
export const decodeNull = decodeValue(null); | |
export const decodeValues = <value extends unknown>(values: readonly value[]): Decoder<value> => | |
(val: any) => | |
values.indexOf(val) >= 0 | |
? val | |
: errorMessage(`Actual ${val + ""}; Expecting: ${values.map(it => it + "").join(", ")}`); | |
export const decodeOr = <value1 extends unknown, value2 extends unknown>(value1: Decoder<value1>, value2: Decoder<value2>): Decoder<value1 | value2> => | |
(val: any) => { | |
const value1Result = value1(val); | |
if (isSuccess(value1Result)) return value1Result; | |
const value2Result = value2(val); | |
return isSuccess(value2Result) | |
? value2Result | |
: errorMessage(`Failed to decode OR: ${value1Result.error}, ${value2Result.error}`); | |
} | |
export const decodeOr3 = <value1 extends unknown, value2 extends unknown, value3 extends unknown>( | |
value1: Decoder<value1>, | |
value2: Decoder<value2>, | |
value3: Decoder<value3> | |
): Decoder<value1 | value2 | value3> => | |
(val: any) => { | |
const value1Result = value1(val); | |
if (isSuccess(value1Result)) return value1Result; | |
const value2Result = value2(val); | |
if (isSuccess(value2Result)) return value2Result; | |
const value3Result = value3(val); | |
return isSuccess(value3Result) | |
? value3Result | |
: errorMessage(`Failed to decode OR: ${value1Result.error}, ${value2Result.error}, ${value3Result.error}`); | |
} | |
} | |
module Example { | |
const decodeGender = DecoderApi.decodeValues(["male", "female", "unknown"] as const); | |
const decodePerson = DecoderApi.createObjectDecoder({ | |
firstName: DecoderApi.decodeString, | |
gender: decodeGender, | |
lastName: DecoderApi.decodeString, | |
ageInYears: DecoderApi.decodeNumber, | |
isCool: DecoderApi.decodeBoolean | |
}) | |
// decodeNumber the use of TypeOfTypePredicate to get ahold of the computed type. | |
type Person = DecoderApi.TypeOfDecoder<typeof decodePerson>; | |
const bob: Person = { | |
firstName: "Bob", | |
lastName: "Smith", | |
ageInYears: 30, | |
gender: "male", | |
isCool: true | |
} | |
const decodedPerson = decodePerson({ | |
firstName: "John", | |
lastName: "Suder", | |
gender: "male", | |
ageInYears: 30, | |
isCool: false | |
}); | |
if (DecoderApi.isSuccess(decodedPerson)) console.log(decodedPerson) | |
else console.error(decodedPerson.error); | |
const decodeCompany = DecoderApi.createObjectDecoder({ | |
ceo: decodePerson, | |
coo: decodePerson, | |
cto: decodePerson, | |
workers: DecoderApi.createArrayDecoder(decodePerson) | |
}) | |
type Company = DecoderApi.TypeOfDecoder<typeof decodeCompany>; | |
const decodedCompany = decodeCompany({ | |
ceo: decodedPerson, | |
coo: decodedPerson, | |
cto: decodedPerson, | |
workers: [decodedPerson, 1] | |
}); | |
if (DecoderApi.isSuccess(decodedCompany)) console.log(decodedCompany) | |
else console.error(decodedCompany.error); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment