Last active
March 29, 2020 20:36
-
-
Save Dremora/565fc13e5c28623c5291d8d2f98317f3 to your computer and use it in GitHub Desktop.
typescript-json-decoder
This file contains hidden or 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
import { Decoder, array, decode, lazy, object, oneOf, string } from "./typescript-json-decoder"; | |
type TreeEntry = string | { name: string; contents: TreeRoot }; | |
type TreeRoot = TreeEntry[]; | |
function getTreeEntryDecoder(): Decoder<TreeEntry> { | |
return oneOf<TreeEntry>( | |
string, | |
object({ contents: lazy(getTreeDecoder), name: string }) | |
); | |
} | |
function getTreeDecoder(): Decoder<TreeRoot> { | |
return array(lazy(getTreeEntryDecoder)); | |
} | |
const treeDecoder = getTreeDecoder(); | |
// This returns a value | |
decode(treeDecoder, [ | |
"file1", | |
"file2", | |
{ | |
contents: [ | |
"file1", | |
"file2", | |
{ contents: ["file1", "file2"], name: "folder2" }, | |
], | |
name: "folder1", | |
}, | |
]); | |
// This throws an exception | |
decode(treeDecoder, [ | |
"file1", | |
"file2", | |
42, | |
{ contents: ["file1", "file2"], name: "folder1" }, | |
]); | |
This file contains hidden or 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
export type JSONValue = | |
| string | |
| number | |
| null | |
| JSONValue[] | |
| { [key: string]: JSONValue }; | |
class Ok<T> { | |
constructor(readonly value: T) {} | |
} | |
class Err { | |
constructor(readonly error: string) {} | |
} | |
const ok = <T>(value: T) => new Ok(value); | |
const err = (error: string) => new Err(error); | |
export type Result<T> = Ok<T> | Err; | |
export type Decoder<T> = (value: JSONValue) => Result<T>; | |
export const decode = <T>(decoder: Decoder<T>, value: JSONValue): T => { | |
const decoded = decoder(value); | |
if (decoded instanceof Err) { | |
throw decoded.error; | |
} else { | |
return decoded.value; | |
} | |
}; | |
export const number = (value: JSONValue): Result<number> => { | |
if (typeof value === "number") { | |
return ok(value); | |
} else { | |
return err(`${value} is not a number`); | |
} | |
}; | |
export const string = (value: JSONValue): Result<string> => { | |
if (typeof value === "string") { | |
return ok(value); | |
} else { | |
return err(`${value} is not a string`); | |
} | |
}; | |
export const optional = <T>(decoder: Decoder<T>) => ( | |
value: JSONValue | |
): Result<T | undefined> => { | |
if (typeof value === "undefined" || value === null) { | |
return ok(undefined); | |
} else { | |
return decoder(value); | |
} | |
}; | |
export const exact = <T extends JSONValue>(v: T) => ( | |
value: JSONValue | |
): Result<T> => (v === value ? ok(v) : err(`${value} is not ${v}`)); | |
export const oneOf = <T>(...decoders: Decoder<T>[]) => ( | |
value: JSONValue | |
): Result<T> => { | |
for (const decoder of decoders) { | |
const decoded = decoder(value); | |
if (decoded instanceof Ok) { | |
return decoded; | |
} | |
} | |
return err(`Can't decode ${value}`); | |
}; | |
export const array = <T>(decoder: Decoder<T>) => ( | |
value: JSONValue | |
): Result<T[]> => { | |
if (!Array.isArray(value)) { | |
return err(`${value} is not array`); | |
} | |
const decoded: T[] = []; | |
for (const member of value) { | |
const decodedMember = decoder(member); | |
if (decodedMember instanceof Err) { | |
return decodedMember; | |
} else { | |
decoded.push(decodedMember.value); | |
} | |
} | |
return ok(decoded); | |
}; | |
export const lazy = <T>(getDecoder: () => Decoder<T>) => ( | |
value: JSONValue | |
): Result<T> => getDecoder()(value); | |
export type DecoderObject<T> = { [K in keyof Required<T>]: Decoder<T[K]> }; | |
export const dictionary = <T>(decoder: Decoder<T>) => ( | |
value: JSONValue | |
): Result<{ [key: string]: T }> => { | |
if (typeof value !== "object" || value === null || Array.isArray(value)) { | |
return err(`${value} is not an object`); | |
} else { | |
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore | |
// @ts-ignore | |
const decodedValue: { [key: string]: T } = {}; | |
for (const key in value) { | |
const decoded = decoder(value[key]); | |
if (decoded instanceof Err) { | |
return decoded; | |
} else if (typeof decoded.value !== "undefined") { | |
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore | |
// @ts-ignore | |
decodedValue[key] = decoded.value; | |
} | |
} | |
return ok(decodedValue); | |
} | |
}; | |
export const object = < | |
T extends { | |
[key: string]: unknown; | |
} | |
>( | |
decoders: DecoderObject<T> | |
) => (value: JSONValue): Result<T> => { | |
if (typeof value !== "object" || value === null || Array.isArray(value)) { | |
return err(`${value} is not an object`); | |
} else { | |
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore | |
// @ts-ignore | |
const decodedValue: T = {}; | |
for (const key in decoders) { | |
const decoded = decoders[key](value[key]); | |
if (decoded instanceof Err) { | |
return decoded; | |
} else if (typeof decoded.value !== "undefined") { | |
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore | |
// @ts-ignore | |
decodedValue[key] = decoded.value; | |
} | |
} | |
return ok(decodedValue); | |
} | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment