From ca0850d7ef6c49634ee52044011699c8416d77d2 Mon Sep 17 00:00:00 2001 From: Scott Wei Date: Mon, 30 Sep 2024 15:19:09 +0800 Subject: [PATCH] Add the behaviors API. --- packages/@ot-doc/model/src/behavior/hkt.ts | 0 .../@ot-doc/model/src/behavior/operation.ts | 85 -------------- .../@ot-doc/model/src/behavior/variable.ts | 2 - .../@ot-doc/model/src/behaviors/behavior.ts | 84 ++++++++++++++ .../@ot-doc/model/src/behaviors/editable.ts | 105 ++++++++++++++++++ packages/@ot-doc/model/src/behaviors/eq.ts | 32 ++++++ packages/@ot-doc/model/src/behaviors/hkt.ts | 20 ++++ packages/@ot-doc/model/src/behaviors/maybe.ts | 6 + .../@ot-doc/model/src/behaviors/operation.ts | 49 ++++++++ .../@ot-doc/model/src/behaviors/preset.ts | 17 +++ .../@ot-doc/model/src/behaviors/readable.ts | 79 +++++++++++++ .../@ot-doc/model/src/behaviors/signatured.ts | 21 ++++ .../@ot-doc/model/src/behaviors/struct.ts | 40 +++++++ .../@ot-doc/model/src/behaviors/variables.ts | 8 ++ packages/@ot-doc/model/src/data/index.ts | 0 15 files changed, 461 insertions(+), 87 deletions(-) delete mode 100644 packages/@ot-doc/model/src/behavior/hkt.ts delete mode 100644 packages/@ot-doc/model/src/behavior/operation.ts delete mode 100644 packages/@ot-doc/model/src/behavior/variable.ts create mode 100644 packages/@ot-doc/model/src/behaviors/behavior.ts create mode 100644 packages/@ot-doc/model/src/behaviors/editable.ts create mode 100644 packages/@ot-doc/model/src/behaviors/eq.ts create mode 100644 packages/@ot-doc/model/src/behaviors/hkt.ts create mode 100644 packages/@ot-doc/model/src/behaviors/maybe.ts create mode 100644 packages/@ot-doc/model/src/behaviors/operation.ts create mode 100644 packages/@ot-doc/model/src/behaviors/preset.ts create mode 100644 packages/@ot-doc/model/src/behaviors/readable.ts create mode 100644 packages/@ot-doc/model/src/behaviors/signatured.ts create mode 100644 packages/@ot-doc/model/src/behaviors/struct.ts create mode 100644 packages/@ot-doc/model/src/behaviors/variables.ts delete mode 100644 packages/@ot-doc/model/src/data/index.ts diff --git a/packages/@ot-doc/model/src/behavior/hkt.ts b/packages/@ot-doc/model/src/behavior/hkt.ts deleted file mode 100644 index e69de29..0000000 diff --git a/packages/@ot-doc/model/src/behavior/operation.ts b/packages/@ot-doc/model/src/behavior/operation.ts deleted file mode 100644 index e629a13..0000000 --- a/packages/@ot-doc/model/src/behavior/operation.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { $Var } from "./variable"; - -declare const symOp: unique symbol; -export type $Op = { [symOp]: typeof symOp }; - -export type Prim = string | number | boolean; - -/** - * Update a primitive value - * @param o the old value - * @param n to value - */ - -export type PrimOp = { - o?: T; - n?: T; - t: number; -}; - -/** - * Update a segment in an array, could be deletion or insertion - * @param i index - * @param a array of values - */ - -export type ArrayOplet = { i: number; a: T[] }; - -/** - * Update array - * @param d deletions - * @param i insertions - */ -export type ArrayOp = { d: ArrayOplet[]; i: ArrayOplet[] }; - -/** - * Update a dict - */ -export type DictOp = Record> - -/** - * Struct op - */ -export type ObjectOp = Partial<{ [K in keyof T]: Op }>; - -export type Op = T extends $Var - ? $Op - : T extends string - ? PrimOp - : T extends number - ? PrimOp - : T extends boolean - ? PrimOp - : T extends Array - ? ArrayOp - : T extends object - ? ObjectOp - : never; - -export type Updater = (v: T) => Op; - -export type GeneralUpdater = T extends Prim - ? Updater - : T extends Array - ? Updater - : T extends object - ? - | Updater - | Partial<{ - [K in keyof T]: Updater; - }> - : never; - -export const updater = (g: GeneralUpdater): Updater => { - if (typeof g === 'function') return g; - const gStt = g as Partial<{ [K in keyof T]: Updater }>; - return (stt) => Object.keys(g).reduce((m, key) => { - const keyG = key as keyof typeof gStt; - const keyOp = key as keyof Op & string; - const f = gStt[keyG]; - if (f) { - (m as any)[keyOp] = updater(f)((stt as any)[key]); - } - return m; - }, {} as Op); -} diff --git a/packages/@ot-doc/model/src/behavior/variable.ts b/packages/@ot-doc/model/src/behavior/variable.ts deleted file mode 100644 index 8b45dec..0000000 --- a/packages/@ot-doc/model/src/behavior/variable.ts +++ /dev/null @@ -1,2 +0,0 @@ -declare const symVar: unique symbol; -export type $Var = { [symVar]: typeof symVar }; diff --git a/packages/@ot-doc/model/src/behaviors/behavior.ts b/packages/@ot-doc/model/src/behaviors/behavior.ts new file mode 100644 index 0000000..08e9b19 --- /dev/null +++ b/packages/@ot-doc/model/src/behaviors/behavior.ts @@ -0,0 +1,84 @@ +import { $ } from './hkt'; +import { $Struct, AnyDict, Dict } from './struct'; + +/** + * Behavior + */ +export type Behavior = { + $string: $; + $number: $; + $boolean: $; + $array: (elm: $) => $; + $dict: (val: $) => $>; + $struct: (struct: $Struct) => $; +}; + +/** + * Behavior Definition + */ +export type BehaviorDef< + Type extends AnyDict, + Base extends AnyDict = AnyDict, +> = { + $string: (u: $) => $; + $number: (u: $) => $; + $boolean: (u: $) => $; + $array: (u: $) => (elm: $) => $; + $dict: ( + u: $>, + ) => (val: $) => $>; + $struct: ( + u: $, + ) => (stt: $Struct) => $; +}; + +const define = ( + { $string, $number, $boolean, $array, $dict, $struct }: Behavior, + def: BehaviorDef, +) => + ({ + $string: Object.assign({}, $string, def.$string($string)), + $number: Object.assign({}, $number, def.$number($number)), + $boolean: Object.assign({}, $boolean, def.$boolean($boolean)), + $array: (elm: $) => { + const bhv = $array(elm); + return Object.assign({}, bhv, def.$array(bhv)(elm)); + }, + $dict: (val: $) => { + const bhv = $dict(val); + return Object.assign({}, bhv, def.$dict(bhv)(val)); + }, + $struct: (stt: $Struct) => { + const bhv = $struct(stt); + return Object.assign({}, bhv, def.$struct(bhv)(stt)); + }, + }) as Behavior; + +type Builder = { + mixin: (def: BehaviorDef) => Builder; + build: () => Behavior; +}; + +const builder = (bhv: Behavior): Builder => ({ + mixin: (def) => builder(define(bhv, def)), + build: () => bhv, +}); + +export const behavior = (bhv: Behavior) => + ({ + $string: () => bhv.$string, + $number: () => bhv.$number, + $boolean: () => bhv.$boolean, + $array: () => bhv.$array, + $dict: () => bhv.$dict, + $struct: () => bhv.$struct, + }) as BehaviorDef; + +export const BehaviorBuilder = builder({ + $string: {}, + $number: {}, + $boolean: {}, + $array: () => ({}), + $dict: () => ({}), + $struct: () => ({}), +}); diff --git a/packages/@ot-doc/model/src/behaviors/editable.ts b/packages/@ot-doc/model/src/behaviors/editable.ts new file mode 100644 index 0000000..735d4aa --- /dev/null +++ b/packages/@ot-doc/model/src/behaviors/editable.ts @@ -0,0 +1,105 @@ +import { Maybe, just, nothing } from "./maybe"; +import { BehaviorDef } from "./behavior"; +import { $Var } from "./variables"; +import { Action, ObjectOp, Op, Prim } from "./operation"; +import { $Struct, reduceDict, reduceStruct } from "./struct"; +import { Eq } from "./eq"; +import { Preset } from "./preset"; + +export type Result = { value: T, op: Op }; + +export type Update = (action: Action) => (a: T) => Maybe>; +export type Editable = { update: Update }; + +const withUpdate = (update: Update): Editable => ({ update }); +const constant = (value: T) => () => value; + +const withUpdatePrim = ({ preset }: Preset): Editable => + withUpdate((action) => (v) => { + const op = action(v); + const { o = preset, n = preset } = op; + return o === v ? just({ value: n as T, op }) : nothing(); + }); + +const editable: BehaviorDef = { + $string: withUpdatePrim, + $number: withUpdatePrim, + $boolean: withUpdatePrim, + $array: + ({ eq }) => + () => + withUpdate((updater) => (arrOld) => { + const op = updater(arrOld); + const { i: ins, d: del } = op; + if (del.length === 0 && ins.length === 0) + return just({ value: arrOld, op }); + const arrNew = [...arrOld]; + for (const { i: idx, a: arr } of del) { + if ( + idx > arrNew.length || + idx < 0 || + !eq(arr)(arrNew.slice(idx, idx + arr.length)) + ) + return nothing(); + arrNew.splice(idx, arr.length); + } + for (const { i: idx, a: arr } of ins) { + if (idx > arrNew.length || idx < 0) return nothing(); + arrNew.splice(idx, 0, ...arr); + } + return just({ value: arrNew, op }); + }), + $dict: + () => + ({ update, preset, eq }) => + withUpdate((action) => (dictOld) => { + const op = action(dictOld); + return reduceDict( + op, + (m, opVal, key) => { + if (m.$ === 'Nothing' || !opVal || typeof key !== 'string') + return m; + const valOld = dictOld[key] ?? preset; + const mResult = update(constant(opVal))(valOld); + if (mResult.$ === 'Nothing') return nothing(); + const { value } = mResult.v; + if (!eq(dictOld[key])(value)) { + if (m.v.value === dictOld) { + m.v.value = { ...dictOld }; + } + if (eq(preset)(value)) { + delete m.v.value[key]; + } else { + m.v.value[key] = value; + } + } + return m; + }, + just({ value: dictOld, op }) + ); + }), + $struct: () => (sttDoc: $Struct) => + withUpdate((updater: Action) => (sttOld: S): Maybe> => { + const op = updater(sttOld); + const opObj = op as ObjectOp; + return reduceStruct( + sttOld, + (m: Maybe>, valOld: S[K], key: K) => { + if (m.$ === 'Nothing' || !opObj[key]) return m; + const mResult = sttDoc[key].update(constant(opObj[key]))(valOld); + if (mResult.$ === 'Nothing') return nothing(); + const { value } = mResult.v; + if (!sttDoc[key].eq(valOld)(value)) { + if (m.v.value === sttOld) { + m.v.value = { ...sttOld }; + } + m.v.value[key] = value; + } + return m; + }, + just({ value: sttOld, op }) + ); + }), +}; + +export default editable; diff --git a/packages/@ot-doc/model/src/behaviors/eq.ts b/packages/@ot-doc/model/src/behaviors/eq.ts new file mode 100644 index 0000000..403fe99 --- /dev/null +++ b/packages/@ot-doc/model/src/behaviors/eq.ts @@ -0,0 +1,32 @@ +import { $Var } from "./hkt"; +import { behavior } from "./behavior"; + +export type Relation = (a: T) => (b: T) => boolean; +export type Eq = { eq: Relation }; + +const withEq = (f: Relation = () => () => false): Eq => ({ + eq: (a) => (b) => a === b || f(a)(b), +}); + +export const eq = behavior({ + $string: withEq(), + $number: withEq(), + $boolean: withEq(), + $array: ({ eq }) => + withEq((a) => (b) => { + if (a.length !== b.length) return false; + for (let i = 0; i < a.length; i += 1) if (!eq(a[i])(b[i])) return false; + return true; + }), + $dict: ({ eq }) => + withEq((a) => (b) => { + for (const key in a) if (!(key in b) || !eq(a[key])(b[key])) return false; + for (const key in b) if (!(key in a)) return false; + return true; + }), + $struct: (stt) => + withEq((a) => (b) => { + for (const key in stt) if (!stt[key].eq(a[key])(b[key])) return false; + return true; + }), +}); diff --git a/packages/@ot-doc/model/src/behaviors/hkt.ts b/packages/@ot-doc/model/src/behaviors/hkt.ts new file mode 100644 index 0000000..71ec893 --- /dev/null +++ b/packages/@ot-doc/model/src/behaviors/hkt.ts @@ -0,0 +1,20 @@ +import { Op } from "./operation"; +import { $OpVar, $Var } from "./variables"; + + +// Type application (substitutes type variables with types) +export type $ = T extends $Var + ? S + : T extends $OpVar + ? Op + : T extends undefined | null | boolean | string | number + ? T + : T extends Array + ? $[] + : T extends (...args: infer Args) => infer R + ? (...x: { [K in keyof Args]: $ }) => $ + : T extends object + ? { [K in keyof T]: $ } + : T; + +export type { $Var }; diff --git a/packages/@ot-doc/model/src/behaviors/maybe.ts b/packages/@ot-doc/model/src/behaviors/maybe.ts new file mode 100644 index 0000000..29061a3 --- /dev/null +++ b/packages/@ot-doc/model/src/behaviors/maybe.ts @@ -0,0 +1,6 @@ +// $: Constructor type +// v: the value +export type Maybe = { $: 'Nothing' } | { $: 'Just'; v: T }; + +export const nothing = (): Maybe => ({ $: 'Nothing' }); +export const just = (v: T): Maybe => ({ $: 'Just', v }); diff --git a/packages/@ot-doc/model/src/behaviors/operation.ts b/packages/@ot-doc/model/src/behaviors/operation.ts new file mode 100644 index 0000000..37aff42 --- /dev/null +++ b/packages/@ot-doc/model/src/behaviors/operation.ts @@ -0,0 +1,49 @@ +import { $Var, $OpVar } from "./variables"; + +export type Prim = string | number | boolean; + +/** + * Update a primitive value + * @param o the old value + * @param n to value + */ +export type PrimOp = { + o?: T; + n?: T; + t: number; +}; + +/** + * Update a segment in an array, could be deletion or insertion + * @param i index + * @param a array of values + */ +export type ArrayOplet = { i: number; a: T[] }; + +/** + * Update array + * @param d deletions + * @param i insertions + */ +export type ArrayOp = { d: ArrayOplet[]; i: ArrayOplet[] }; + +/** + * Struct op + */ +export type ObjectOp = Partial<{ [K in keyof T]: Op }>; + +export type Op = T extends $Var + ? $OpVar + : T extends string + ? PrimOp + : T extends number + ? PrimOp + : T extends boolean + ? PrimOp + : T extends Array + ? ArrayOp + : T extends object + ? ObjectOp + : never; + +export type Action = (v: T) => Op; \ No newline at end of file diff --git a/packages/@ot-doc/model/src/behaviors/preset.ts b/packages/@ot-doc/model/src/behaviors/preset.ts new file mode 100644 index 0000000..bfb95dd --- /dev/null +++ b/packages/@ot-doc/model/src/behaviors/preset.ts @@ -0,0 +1,17 @@ +import { behavior } from './behavior'; +import { mapStruct } from './struct'; +import { $Var } from './variables'; + +export type Preset = { preset: T }; + +const withPreset = (preset: T): Preset => ({ preset }); + +export const preset = behavior({ + $string: withPreset(''), + $number: withPreset(0), + $boolean: withPreset(false), + $array: () => withPreset([]), + $dict: () => withPreset({}), + $struct: (stt) => withPreset(mapStruct(stt, ({ preset }) => preset)), +}); + diff --git a/packages/@ot-doc/model/src/behaviors/readable.ts b/packages/@ot-doc/model/src/behaviors/readable.ts new file mode 100644 index 0000000..15ef3aa --- /dev/null +++ b/packages/@ot-doc/model/src/behaviors/readable.ts @@ -0,0 +1,79 @@ +import { BehaviorDef } from './behavior'; +import { $Var } from './hkt'; +import { $Struct, Dict, mapDict, mapStruct } from './struct'; +import { Preset } from './preset'; +import { Signatured } from './signatured'; + +export type Readable = ( + raise: (path: string) => (message: string) => void, +) => (u: unknown) => T; + +export type Read = { + read: Readable; +}; + +// eslint-disable-next-line @typescript-eslint/no-empty-function +export const noop = () => {}; + +export const readData = + ({ read }: Read) => + (u: unknown, onError: (message: string) => void = noop) => + read(path => message => onError(`${message}, at $${path}`))(u); + +const withRead = (read: Readable): Read => ({ read }); + +const withReadPrim = ({ + preset, + signature, +}: Preset & Signatured): Read => + withRead((raise) => (u) => { + if (typeof u === typeof preset) { + return u as T; + } + raise('')(`requires ${signature}`); + return preset; + }); + +const readable: BehaviorDef = { + $string: withReadPrim, + $number: withReadPrim, + $boolean: withReadPrim, + $array: + ({ preset, signature }) => + ({ read }) => + withRead((raise) => (u) => { + if (!Array.isArray(u)) { + raise('')(`requires ${signature}`); + return preset; + } + return u.map((e, i) => read((path) => raise(`[${i}]${path}`))(e)); + }), + $dict: + ({ preset, signature }) => + ({ read }) => + withRead((raise) => (u) => { + if (typeof u !== 'object' || !u) { + raise('')(`requires ${signature}`); + return preset; + } + return mapDict(u as Dict, (v, key) => + read((path) => raise(`.${key}${path}`))(v), + ); + }), + $struct: + ({ preset, signature }) => + (stt) => + withRead((raise) => (u) => { + if (typeof u !== 'object' || !u) { + raise('')(`requires ${signature}`); + return preset; + } + return mapStruct(stt, ({ read }, key) => + read((path) => raise(`.${key as string}${path}`))( + (u as $Struct)[key], + ), + ); + }), +}; + +export default readable; \ No newline at end of file diff --git a/packages/@ot-doc/model/src/behaviors/signatured.ts b/packages/@ot-doc/model/src/behaviors/signatured.ts new file mode 100644 index 0000000..6f907e6 --- /dev/null +++ b/packages/@ot-doc/model/src/behaviors/signatured.ts @@ -0,0 +1,21 @@ +import { behavior } from './behavior'; + +export type Signatured = { + signature: string; +}; + +const withSignature = (signature: string): Signatured => ({ signature }); + +export const signatured = behavior({ + $string: withSignature('string'), + $number: withSignature('number'), + $boolean: withSignature('boolean'), + $array: ({ signature }) => withSignature(`Array<${signature}>`), + $dict: ({ signature }) => withSignature(`Dict<${signature}>`), + $struct: (stt) => + withSignature( + `{ ${Object.keys(stt) + .map((key) => `${key}: ${stt[key].signature}`) + .join('; ')} }` + ), +}); diff --git a/packages/@ot-doc/model/src/behaviors/struct.ts b/packages/@ot-doc/model/src/behaviors/struct.ts new file mode 100644 index 0000000..74b4576 --- /dev/null +++ b/packages/@ot-doc/model/src/behaviors/struct.ts @@ -0,0 +1,40 @@ +import { $ } from './hkt'; + +export type Dict = Record; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type AnyDict = Dict; +export type $Struct = { [K in keyof S]: $ }; + +export const reduceStruct = ( + t: T, + f: (u: U, v: T[K], key: K) => U, + u: U +): U => + Object.keys(t).reduce( + (m: U, k: K) => f(m, t[k], k), + u as U + ); + +export const mapStruct = ( + t: T, + f: (v: T[K], key: K) => $, +): $Struct => + reduceStruct( + t, + (m, v, k) => { + m[k] = f(v, k); + return m; + }, + {} as $Struct, + ); + +export const reduceDict = reduceStruct as ( + dict: Dict, + f: (u: U, v: T, key: string) => U, + u: U, +) => U; + +export const mapDict = mapStruct as ( + dict: Dict, + f: (t: T, key: string) => V, +) => Dict; \ No newline at end of file diff --git a/packages/@ot-doc/model/src/behaviors/variables.ts b/packages/@ot-doc/model/src/behaviors/variables.ts new file mode 100644 index 0000000..1f807b2 --- /dev/null +++ b/packages/@ot-doc/model/src/behaviors/variables.ts @@ -0,0 +1,8 @@ +declare const symVar: unique symbol; +export type $Var = { [symVar]: typeof symVar }; + +declare const symOp: unique symbol; +export type $OpVar = { [symOp]: typeof symOp }; + +declare const symAct: unique symbol; +export type $ActVar = { [symAct]: typeof symAct }; \ No newline at end of file diff --git a/packages/@ot-doc/model/src/data/index.ts b/packages/@ot-doc/model/src/data/index.ts deleted file mode 100644 index e69de29..0000000