parent
c6d3032be8
commit
f8f5fc4e7c
@ -0,0 +1,236 @@ |
|||||||
|
import {DateTime} from "luxon"; |
||||||
|
|
||||||
|
export interface Serializer<DeserializedT, SerializedT> { |
||||||
|
checkType(data: unknown): data is SerializedT |
||||||
|
hydrate(data: SerializedT): DeserializedT |
||||||
|
dehydrate(data: DeserializedT): SerializedT |
||||||
|
} |
||||||
|
|
||||||
|
export interface LocatedError { |
||||||
|
readonly location: string |
||||||
|
readonly error: Error|LocatedError |
||||||
|
} |
||||||
|
|
||||||
|
export function isLocatedError(object: unknown): object is LocatedError { |
||||||
|
return typeof object === "object" && object !== null && object.hasOwnProperty("location") && object.hasOwnProperty("error") |
||||||
|
} |
||||||
|
|
||||||
|
export function wrapErrorWithLocation(location: string, object: unknown): LocatedError { |
||||||
|
if (typeof object === "string") { |
||||||
|
return { |
||||||
|
location, |
||||||
|
error: Error(object) |
||||||
|
} |
||||||
|
} else if (object instanceof Error || isLocatedError(object)) { |
||||||
|
return { |
||||||
|
location, |
||||||
|
error: object |
||||||
|
} |
||||||
|
} else { |
||||||
|
return { |
||||||
|
location, |
||||||
|
error: Error(`Unknown error: ${object}`) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export class SelfSerializer<T> implements Serializer<T, T> { |
||||||
|
readonly checkType: (data: unknown) => data is T; |
||||||
|
|
||||||
|
constructor(checkType: (data: unknown) => data is T) { |
||||||
|
this.checkType = checkType |
||||||
|
} |
||||||
|
|
||||||
|
dehydrate(data: T): T { |
||||||
|
return data; |
||||||
|
} |
||||||
|
|
||||||
|
hydrate(data: T): T { |
||||||
|
return data; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const StringSerializer = new SelfSerializer((data): data is string => typeof data === "string") |
||||||
|
export const DateTimeSerializer: Serializer<DateTime, string> = { |
||||||
|
checkType(data: unknown): data is string { |
||||||
|
return typeof data === "string" |
||||||
|
}, |
||||||
|
dehydrate(data: DateTime): string { |
||||||
|
return data.toISO(); |
||||||
|
}, |
||||||
|
hydrate(data: string): DateTime { |
||||||
|
return DateTime.fromISO(data); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export class EnumSerializer<T extends string|number> implements Serializer<T, T> { |
||||||
|
readonly values: readonly T[] |
||||||
|
|
||||||
|
constructor(values: readonly T[]) { |
||||||
|
this.values = values |
||||||
|
} |
||||||
|
|
||||||
|
checkType(data: unknown): data is T { |
||||||
|
if (!(this.values as unknown[]).includes(data)) { |
||||||
|
throw Error("not a valid value for the enum") |
||||||
|
} |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
dehydrate(data: T): T { |
||||||
|
return data; |
||||||
|
} |
||||||
|
|
||||||
|
hydrate(data: T): T { |
||||||
|
return data; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export class NullableSerializer<DeserializedT, SerializedT> implements Serializer<DeserializedT | null, SerializedT | null> { |
||||||
|
readonly inner: Serializer<DeserializedT, SerializedT> |
||||||
|
constructor(inner: Serializer<DeserializedT, SerializedT>) { |
||||||
|
this.inner = inner |
||||||
|
} |
||||||
|
|
||||||
|
checkType(data: unknown): data is SerializedT | null { |
||||||
|
return data === null || this.inner.checkType(data) |
||||||
|
} |
||||||
|
|
||||||
|
dehydrate(data: DeserializedT | null): SerializedT | null { |
||||||
|
if (data === null) { |
||||||
|
return null |
||||||
|
} |
||||||
|
return this.inner.dehydrate(data); |
||||||
|
} |
||||||
|
|
||||||
|
hydrate(data: SerializedT | null): DeserializedT | null { |
||||||
|
if (data === null) { |
||||||
|
return null |
||||||
|
} |
||||||
|
return this.inner.hydrate(data); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export class OptionalSerializer<DeserializedT, SerializedT> implements Serializer<DeserializedT | undefined, SerializedT | undefined> { |
||||||
|
readonly inner: Serializer<DeserializedT, SerializedT> |
||||||
|
constructor(inner: Serializer<DeserializedT, SerializedT>) { |
||||||
|
this.inner = inner |
||||||
|
} |
||||||
|
|
||||||
|
checkType(data: unknown): data is SerializedT | undefined { |
||||||
|
return typeof data === "undefined" || this.inner.checkType(data) |
||||||
|
} |
||||||
|
|
||||||
|
dehydrate(data: DeserializedT | undefined): SerializedT | undefined { |
||||||
|
if (typeof data === "undefined") { |
||||||
|
return data |
||||||
|
} |
||||||
|
return this.inner.dehydrate(data); |
||||||
|
} |
||||||
|
|
||||||
|
hydrate(data: SerializedT | undefined): DeserializedT | undefined { |
||||||
|
if (typeof data === "undefined") { |
||||||
|
return data |
||||||
|
} |
||||||
|
return this.inner.hydrate(data); |
||||||
|
} |
||||||
|
} |
||||||
|
export const OptionalStringSerializer = new OptionalSerializer(StringSerializer) |
||||||
|
export const OptionalDateTimeSerializer = new OptionalSerializer(DateTimeSerializer) |
||||||
|
|
||||||
|
export type KeySerializers<DeserializedT extends {[key in KeysT]?: unknown}, SerializedT extends {[key in KeysT]?: unknown}, KeysT extends string|number> = { |
||||||
|
[key in KeysT]: Serializer<DeserializedT[key], SerializedT[key]> |
||||||
|
} |
||||||
|
|
||||||
|
export class ObjectSerializer<DeserializedT extends {[key in KeysT]?: unknown}, SerializedT extends {[key in KeysT]?: unknown}, KeysT extends string|number> implements Serializer<DeserializedT, SerializedT> { |
||||||
|
readonly inner: KeySerializers<DeserializedT, SerializedT, KeysT> |
||||||
|
readonly keys: KeysT[] |
||||||
|
constructor(inner: KeySerializers<DeserializedT, SerializedT, KeysT>, keys: KeysT[]) { |
||||||
|
this.inner = inner |
||||||
|
this.keys = keys |
||||||
|
} |
||||||
|
|
||||||
|
checkType(data: unknown): data is SerializedT { |
||||||
|
if (typeof data !== "object" || data === null) { |
||||||
|
throw Error("expected an object") |
||||||
|
} |
||||||
|
const map: {[key in KeysT]?: unknown} = data |
||||||
|
for (const key of this.keys) { |
||||||
|
try { |
||||||
|
if (!this.inner[key].checkType(map[key])) { |
||||||
|
// noinspection ExceptionCaughtLocallyJS
|
||||||
|
throw Error("checkType returned false") |
||||||
|
} |
||||||
|
} catch (e) { |
||||||
|
throw wrapErrorWithLocation(`at key ${key}`, e) |
||||||
|
} |
||||||
|
} |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
dehydrate(data: DeserializedT): SerializedT { |
||||||
|
const result: Partial<SerializedT> = {} |
||||||
|
for (const key of this.keys) { |
||||||
|
try { |
||||||
|
result[key] = this.inner[key].dehydrate(data[key]) |
||||||
|
} catch (e) { |
||||||
|
throw wrapErrorWithLocation(`at key ${key}`, e) |
||||||
|
} |
||||||
|
} |
||||||
|
return result as SerializedT |
||||||
|
} |
||||||
|
|
||||||
|
hydrate(data: SerializedT): DeserializedT { |
||||||
|
const result: Partial<DeserializedT> = {} |
||||||
|
for (const key of this.keys) { |
||||||
|
try { |
||||||
|
result[key] = this.inner[key].hydrate(data[key]) |
||||||
|
} catch (e) { |
||||||
|
throw wrapErrorWithLocation(`at key ${key}`, e) |
||||||
|
} |
||||||
|
} |
||||||
|
return result as DeserializedT |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export class ArraySerializer<DeserializedT, SerializedT> implements Serializer<DeserializedT[], SerializedT[]> { |
||||||
|
readonly inner: Serializer<DeserializedT, SerializedT> |
||||||
|
constructor(inner: Serializer<DeserializedT, SerializedT>) { |
||||||
|
this.inner = inner |
||||||
|
} |
||||||
|
|
||||||
|
checkType(data: unknown): data is SerializedT[] { |
||||||
|
return Array.isArray(data) && data.every((value, index) => { |
||||||
|
try { |
||||||
|
if (!this.inner.checkType(value)) { |
||||||
|
// noinspection ExceptionCaughtLocallyJS
|
||||||
|
throw Error("checkType returned false") |
||||||
|
} |
||||||
|
} catch (e) { |
||||||
|
throw wrapErrorWithLocation(`at index ${index}`, e) |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
dehydrate(data: DeserializedT[]): SerializedT[] { |
||||||
|
return data.map((value, index) => { |
||||||
|
try { |
||||||
|
return this.inner.dehydrate(value) |
||||||
|
} catch (e) { |
||||||
|
throw wrapErrorWithLocation(`at index ${index}`, e) |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
hydrate(data: SerializedT[]): DeserializedT[] { |
||||||
|
return data.map((value, index) => { |
||||||
|
try { |
||||||
|
return this.inner.hydrate(value) |
||||||
|
} catch (e) { |
||||||
|
throw wrapErrorWithLocation(`at index ${index}`, e) |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
export const ArrayStringSerializer = new ArraySerializer(StringSerializer) |
||||||
|
export const OptionalArrayStringSerializer = new OptionalSerializer(ArrayStringSerializer) |
Loading…
Reference in new issue