diff --git a/.gitignore b/.gitignore index 4afb8ba..e984930 100644 --- a/.gitignore +++ b/.gitignore @@ -38,4 +38,5 @@ Thumbs.db dist/**/* # ignore yarn.lock -yarn.lock \ No newline at end of file +yarn.lock +/release \ No newline at end of file diff --git a/src/datatypes/Condition.ts b/src/datatypes/Condition.ts index 8f6298f..6007a84 100644 --- a/src/datatypes/Condition.ts +++ b/src/datatypes/Condition.ts @@ -1,4 +1,4 @@ -import {schema} from "../schemata/SchemaData.js"; +import {EnumSerializer, OptionalSerializer} from "../schemata/Serialization.js"; export enum Condition { CRITICAL = "Critical", @@ -11,12 +11,5 @@ export enum Condition { } export const CONDITIONS = [Condition.CRITICAL, Condition.POOR, Condition.SOSO, Condition.GOOD, Condition.EXCELLENT, Condition.UNSURE, Condition.NUMB] -export type ConditionJTD = typeof ConditionJTD -export const ConditionJTD = schema({ - schema: { - enum: CONDITIONS - }, - key: "condition", - references: [], -}) - +export const ConditionSerializer = new EnumSerializer(CONDITIONS) +export const OptionalConditionSerializer = new OptionalSerializer(ConditionSerializer) \ No newline at end of file diff --git a/src/datatypes/Entry.ts b/src/datatypes/Entry.ts index 9f76265..4068b9f 100644 --- a/src/datatypes/Entry.ts +++ b/src/datatypes/Entry.ts @@ -1,12 +1,11 @@ -import {Condition, ConditionJTD} from "./Condition.js"; -import {GuidedEmpathy, GuidedEmpathyJTD} from "./GuidedEmpathy.js"; -import {Suicidality, SuicidalityJTD} from "./Suicidality.js"; -import {schema} from "../schemata/SchemaData.js"; -import {SleepRecord, SleepRecordListJTD} from "./SleepRecord.js"; +import {Condition, OptionalConditionSerializer} from "./Condition.js"; +import {GuidedEmpathy} from "./GuidedEmpathy.js"; +import {OptionalSuicidalitySerializer, Suicidality} from "./Suicidality.js"; +import {SleepRecord} from "./SleepRecord.js"; +import {DateTime} from "luxon"; +import {DateTimeSerializer, ObjectSerializer, OptionalStringSerializer} from "../schemata/Serialization.js"; -export interface Entry { - readonly startedAt: Date - readonly finishedAt: Date +export interface BaseEntry { readonly condition?: Condition readonly summary?: string readonly journalEntry?: string @@ -20,24 +19,24 @@ export interface Entry { readonly suicidality?: Suicidality // readonly recoveries?: readonly Recovery[] } -export type EntryJTD = typeof EntryJTD -export const EntryJTD = schema({ - schema: { - properties: { - startedAt: { type: "timestamp" }, - finishedAt: { type: "timestamp" }, - }, - optionalProperties: { - condition: ConditionJTD.reference, - summary: { type: "string" }, - journalEntry: { type: "string" }, - guidedEmpathy: { elements: GuidedEmpathyJTD.reference }, - suicidality: SuicidalityJTD.reference, - sleepRecords: SleepRecordListJTD.reference, - } - }, - typeHint: null as Entry|null, - key: "entry", - references: [ConditionJTD, GuidedEmpathyJTD, SuicidalityJTD, SleepRecordListJTD, ...SleepRecordListJTD.requiredReferences, ...GuidedEmpathyJTD.requiredReferences] -}) +export interface Entry extends BaseEntry { + readonly startedAt: DateTime + readonly finishedAt: DateTime +} + +export interface SerializedEntry extends BaseEntry { + readonly startedAt: string + readonly finishedAt: string +} + +export const EntrySerializer = new ObjectSerializer({ + startedAt: DateTimeSerializer, + finishedAt: DateTimeSerializer, + condition: OptionalConditionSerializer, + summary: OptionalStringSerializer, + journalEntry: OptionalStringSerializer, + guidedEmpathy: OptionalArrayGuidedEmpathySerializer, + sleepRecords: OptionalArraySleepRecordSerializer, + suicidality: OptionalSuicidalitySerializer, +}, ["startedAt", "finishedAt", "condition", "summary", "journalEntry", "guidedEmpathy", "sleepRecords", "suicidality"]) \ No newline at end of file diff --git a/src/datatypes/GuidedEmpathy.ts b/src/datatypes/GuidedEmpathy.ts index a8cc105..371f0e0 100644 --- a/src/datatypes/GuidedEmpathy.ts +++ b/src/datatypes/GuidedEmpathy.ts @@ -8,8 +8,13 @@ import { personaVerb } from "./Persona.js"; import chalk from "chalk"; -import {schema} from "../schemata/SchemaData.js"; import capitalize from "capitalize"; +import { + ArraySerializer, + ObjectSerializer, + OptionalArrayStringSerializer, + OptionalSerializer +} from "../schemata/Serialization.js"; export interface GuidedEmpathy { readonly feelings?: readonly string[] @@ -17,20 +22,14 @@ export interface GuidedEmpathy { readonly events?: readonly string[] readonly requests?: readonly string[] } -export type GuidedEmpathyJTD = typeof GuidedEmpathyJTD -export const GuidedEmpathyJTD = schema({ - schema: { - optionalProperties: { - feelings: { elements: { type: "string" } }, - needs: { elements: { type: "string" } }, - events: { elements: { type: "string" } }, - requests: { elements: { type: "string" } }, - } - }, - typeHint: null as GuidedEmpathy|null, - key: "guidedEmpathy", - references: [], -}) +export const GuidedEmpathySerializer = new ObjectSerializer({ + feelings: OptionalArrayStringSerializer, + needs: OptionalArrayStringSerializer, + events: OptionalArrayStringSerializer, + requests: OptionalArrayStringSerializer, +}, ["feelings", "needs", "events", "requests"]) +export const ArrayGuidedEmpathySerializer = new ArraySerializer(GuidedEmpathySerializer) +export const OptionalArrayGuidedEmpathySerializer = new OptionalSerializer(ArrayGuidedEmpathySerializer) export function isPopulatedGuidedEmpathy(empathy: GuidedEmpathy | undefined): boolean { return !!empathy && (isPopulatedArray(empathy.feelings) diff --git a/src/datatypes/Suicidality.ts b/src/datatypes/Suicidality.ts index cae7505..a790bf4 100644 --- a/src/datatypes/Suicidality.ts +++ b/src/datatypes/Suicidality.ts @@ -1,4 +1,6 @@ import {schema} from "../schemata/SchemaData.js"; +import {EnumSerializer, OptionalSerializer} from "../schemata/Serialization.js"; +import {Condition, CONDITIONS} from "./Condition.js"; export enum Suicidality { NONE = "None", @@ -12,12 +14,5 @@ export enum Suicidality { } export const SUICIDALITIES: Suicidality[] = [Suicidality.NONE, Suicidality.PASSIVE, Suicidality.INTRUSIVE, Suicidality.ACTIVE, Suicidality.RESIGNED, Suicidality.PLANNING, Suicidality.PLANNED, Suicidality.DANGER] -export type SuicidalityJTD = typeof SuicidalityJTD -export const SuicidalityJTD = schema({ - schema: { - enum: SUICIDALITIES - }, - key: "suicidality", - references: [], -}) - +export const SuicidalitySerializer = new EnumSerializer(SUICIDALITIES) +export const OptionalSuicidalitySerializer = new OptionalSerializer(SuicidalitySerializer) diff --git a/src/schemata/Serialization.ts b/src/schemata/Serialization.ts new file mode 100644 index 0000000..416206b --- /dev/null +++ b/src/schemata/Serialization.ts @@ -0,0 +1,236 @@ +import {DateTime} from "luxon"; + +export interface Serializer { + 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 implements Serializer { + 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 = { + 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 implements Serializer { + 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 implements Serializer { + readonly inner: Serializer + constructor(inner: Serializer) { + 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 implements Serializer { + readonly inner: Serializer + constructor(inner: Serializer) { + 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 = { + [key in KeysT]: Serializer +} + +export class ObjectSerializer implements Serializer { + readonly inner: KeySerializers + readonly keys: KeysT[] + constructor(inner: KeySerializers, 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 = {} + 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 = {} + 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 implements Serializer { + readonly inner: Serializer + constructor(inner: Serializer) { + 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) \ No newline at end of file