Mari's guided journal software.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

236 lines
7.4 KiB

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)