Skip to content

Commit

Permalink
feat(form-models): reimplement models from classes to pure objects
Browse files Browse the repository at this point in the history
  • Loading branch information
Lodin committed Jun 12, 2024
1 parent 3428e3f commit c150f74
Show file tree
Hide file tree
Showing 7 changed files with 1,952 additions and 3,770 deletions.
5,260 changes: 1,678 additions & 3,582 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion packages/ts/form-models/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"build:copy": "cd src_ && copyfiles **/*.d.ts ..",
"lint": "eslint src_ test",
"lint:fix": "eslint src_ test --fix",
"test": "karma start ../../../karma.config.cjs --port 9878",
"test": "mocha test/**/*.spec.ts --config ../../../.mocharc.cjs",
"test:coverage": "npm run test -- --coverage",
"test:watch": "npm run test -- --watch",
"typecheck": "tsc --noEmit"
Expand Down Expand Up @@ -58,6 +58,7 @@
"@types/chai": "^4.3.6",
"@types/chai-as-promised": "^7.1.8",
"@types/chai-dom": "^1.11.1",
"@types/chai-like": "^1.1.3",
"@types/mocha": "^10.0.2",
"@types/react": "^18.2.23",
"@types/sinon": "^10.0.17",
Expand Down
159 changes: 62 additions & 97 deletions packages/ts/form-models/src/builders.ts
Original file line number Diff line number Diff line change
@@ -1,135 +1,100 @@
import type { ExtendedModel, Model, ModelMetadata, ModelOwner, EmptyRecord, ModelConstructor } from './model.js';
import {
$defaultValue,
$key,
$meta,
$name,
$owner,
type EmptyRecord,
type ExtendedModel,
type Model,
type ModelMetadata,
} from './model.js';

export type ModelBuilderPropertyOptions = Readonly<{
meta?: ModelMetadata;
}>;

export interface ModelBuilder<T, C extends object = EmptyRecord> {
build(): ModelConstructor<T, C>;
define<K extends keyof any, V>(key: K, value: V): ModelBuilder<T, C & Record<K, V>>;
name(name: string): this;
}
const $base = Symbol();
const $properties = Symbol();

export class CoreModelBuilder<T, C extends object = EmptyRecord> implements ModelBuilder<T, C> {
static from<T>(base: ModelConstructor, defaultValueProvider?: () => T): CoreModelBuilder<T> {
export class CoreModelBuilder<T, C extends object = EmptyRecord> {
static from<T>(base: ExtendedModel, defaultValueProvider?: () => T): CoreModelBuilder<T> {
return new CoreModelBuilder(base, defaultValueProvider);
}

readonly #base: ModelConstructor;
readonly #statics: Record<keyof any, PropertyDescriptor> = {};
protected readonly [$base]: ExtendedModel;
protected readonly [$properties]: Record<keyof any, PropertyDescriptor> = {};

private constructor(base: ModelConstructor, defaultValueProvider?: () => T) {
this.#base = base;
protected constructor(base: ExtendedModel, defaultValueProvider?: () => T) {
this[$base] = base;

if (defaultValueProvider) {
this.#statics.defaultValue = {
enumerable: true,
get() {
return defaultValueProvider();
},
this[$properties].defaultValue = {
get: defaultValueProvider,
};
}
}

define<K extends keyof any, V>(key: K, value: V): ModelBuilder<T, C & Record<K, V>> {
this.#statics[key] = {
enumerable: true,
get() {
return value;
},
};
meta(value: ModelMetadata): this {
this.define($meta, value);
return this;
}

return this as ModelBuilder<T, C & Record<K, V>>;
define<K extends symbol, V>(key: K, value: V): CoreModelBuilder<T, C & Record<K, V>> {
this[$properties][key] = { value };
return this as CoreModelBuilder<T, C & Record<K, V>>;
}

name(name: string): this {
this.define('name', name);
this.define($name, name);
return this;
}

build(): ModelConstructor<T, C> {
const self = this;

const ctr = class extends self.#base {};

Object.defineProperties(ctr, this.#statics);

return ctr as any;
build(): ExtendedModel<T, C> {
return Object.create(this[$base], this[$properties]);
}
}

export class ObjectModelBuilder<T, U, C extends object = EmptyRecord> implements ModelBuilder<T, C> {
static from<T, U = object>(base: ModelConstructor): ObjectModelBuilder<T, U> {
return new ObjectModelBuilder(base);
export class ObjectModelBuilder<
T extends object,
U extends object = object,
C extends object = EmptyRecord,
> extends CoreModelBuilder<T, C> {
static extend<T extends object, U extends object = object>(base: ExtendedModel): ObjectModelBuilder<T, U> {
return new ObjectModelBuilder<T, U>(base);
}

readonly #base: ModelConstructor;
readonly #initializers: Array<(self: ExtendedModel<T>) => void> = [];
readonly #properties: Record<keyof any, PropertyDescriptor> = {};
readonly #propertyModels: Array<readonly [keyof any, ModelConstructor]> = [];
readonly #statics: Record<keyof any, PropertyDescriptor> = {};

private constructor(base: ModelConstructor) {
this.#base = base;

this.#statics.defaultValue = {
enumerable: true,
get: () => Object.fromEntries(this.#propertyModels.map(([key, model]) => [key, model.defaultValue] as const)),
};
protected constructor(base: ExtendedModel) {
super(
base,
() =>
Object.fromEntries(
Object.entries(this[$properties]).map(
([key, descriptor]) => [key, (descriptor.value as Model)[$defaultValue]] as const,
),
) as T,
);
}

define<K extends keyof any, V>(key: K, value: V): ObjectModelBuilder<T, U, C & Record<K, V>> {
this.#statics[key] = {
enumerable: true,
get() {
return value;
},
};
declare ['build']: () => U extends T ? ExtendedModel<T, C> : never;
declare ['define']: <K extends symbol, V>(key: K, value: V) => ObjectModelBuilder<T, U, C & Readonly<Record<K, V>>>;
declare ['name']: (name: string) => this;
declare ['meta']: (value: ModelMetadata) => this;

return this as ObjectModelBuilder<T, U, C & Record<K, V>>;
}

name(name: string): this {
this.define('name', name);
return this;
}

property<K extends keyof any, N>(
property<K extends keyof T>(
key: K,
model: ModelConstructor<N>,
model: ExtendedModel<T[K]>,
options?: ModelBuilderPropertyOptions,
): ObjectModelBuilder<T, U, C & Record<K, N>> {
const registry = new WeakMap<Model, Model>();

this.#propertyModels.push([key, model] as const);

this.#initializers.push((self) => {
registry.set(self, new model(key, self, options?.meta));
});

this.#properties[key] = {
): ObjectModelBuilder<T, Readonly<Record<K, T[K]>> & U, C> {
this[$properties][key] = {
enumerable: true,
get(this: Model<U>) {
return registry.get(this);
},
value: ObjectModelBuilder.extend(model)
.define($key, key)
.define($owner, this)
.define($meta, options?.meta)
.build(),
};

return this as ObjectModelBuilder<T, U, C & Record<K, N>>;
}

build(): U extends T ? ModelConstructor<T, C> : never {
const self = this;

const ctr = class extends self.#base {
constructor(key: keyof any, owner: Model | ModelOwner, meta?: ModelMetadata) {
super(key, owner, meta);
self.#initializers.forEach((initializer) => initializer(this as ExtendedModel<T>));
}
};

Object.defineProperties(ctr.prototype, this.#properties);
Object.defineProperties(ctr, this.#statics);

return ctr as any;
return this as ObjectModelBuilder<T, U, C & Readonly<Record<K, T[K]>>>;
}
}
16 changes: 8 additions & 8 deletions packages/ts/form-models/src/core.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,35 @@
import { CoreModelBuilder } from './builders.js';
import { type Enum, Model } from './model.js';
import { $enum, $itemModel, type Enum, Model } from './model.js';

export const PrimitiveModel = CoreModelBuilder.from(Model, (): unknown => undefined)
.name('primitive')
.build();

export const StringModel = CoreModelBuilder.from(PrimitiveModel, (): string => '')
export const StringModel = CoreModelBuilder.from(PrimitiveModel, () => '')
.name('string')
.build();

export const NumberModel = CoreModelBuilder.from(PrimitiveModel, (): number => 0)
export const NumberModel = CoreModelBuilder.from(PrimitiveModel, () => 0)
.name('number')
.build();

export const BooleanModel = CoreModelBuilder.from(PrimitiveModel, (): boolean => false)
export const BooleanModel = CoreModelBuilder.from(PrimitiveModel, () => false)
.name('boolean')
.build();

export const ArrayModel = CoreModelBuilder.from(Model, (): unknown[] => [])
.name('Array')
.define('itemModel', Model)
.define($itemModel, Model)
.build();

export const ObjectModel = CoreModelBuilder.from(Model, (): object => ({}))
export const ObjectModel = CoreModelBuilder.from(Model, () => ({}))
.name('Object')
.build();

export const EnumModel = CoreModelBuilder.from(
Model,
(): (typeof Enum)[keyof typeof Enum] => Object.values(EnumModel.enum)[0],
(): (typeof Enum)[keyof typeof Enum] => Object.values(EnumModel[$enum])[0],
)
.name('Enum')
.define('enum', {} as typeof Enum)
.define($enum, {} as typeof Enum)
.build();
61 changes: 38 additions & 23 deletions packages/ts/form-models/src/m.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,43 @@
import { CoreModelBuilder } from './builders.js';
import { ArrayModel, EnumModel } from './core.js';
import { type Enum, Model, type ModelConstructor } from './model.js';
import { CoreModelBuilder, ObjectModelBuilder } from './builders.js';
import { ArrayModel, EnumModel, ObjectModel } from './core.js';
import {
type EmptyRecord,
type Enum,
Model,
$members,
$enum,
$itemModel,
$optional,
$name,
type ExtendedModel,
$defaultValue,
} from './model.js';

export const m = {
optional<T>(base: ModelConstructor): ModelConstructor<T | undefined> {
return CoreModelBuilder.from<T | undefined>(base).define('optional', true).build();
},
export class m<T, U, C extends object = EmptyRecord> extends ObjectModelBuilder<T, U, C> {
static optional<T>(base: ExtendedModel): ExtendedModel<T | undefined> {
return CoreModelBuilder.from<T | undefined>(base).define($optional, true).build();
}

array<T, C extends object>(
itemModel: ModelConstructor<T, C>,
): ModelConstructor<T[], Readonly<{ itemModel: ModelConstructor<T, C> }>> {
return CoreModelBuilder.from<T[]>(ArrayModel).define('itemModel', itemModel).build();
},
static array<T, C extends object>(
itemModel: ExtendedModel<T, C>,
): ExtendedModel<T[], Readonly<{ [$itemModel]: ExtendedModel<T, C> }>> {
return CoreModelBuilder.from<T[]>(ArrayModel).define($itemModel, itemModel).build();
}

enum<T extends typeof Enum>(obj: T, name: string): ModelConstructor<T[keyof T], Readonly<{ enum: T }>> {
return CoreModelBuilder.from<T[keyof T]>(EnumModel).define('enum', obj).name(name).build();
},
static enum<T extends typeof Enum>(obj: T, name: string): ExtendedModel<T[keyof T], Readonly<{ [$enum]: T }>> {
return CoreModelBuilder.from<T[keyof T]>(EnumModel).define($enum, obj).name(name).build();
}

union<TT extends unknown[]>(
...members: ReadonlyArray<ModelConstructor<TT[number]>>
): ModelConstructor<TT[number], Readonly<{ members: ReadonlyArray<ModelConstructor<TT[number]>> }>> {
return CoreModelBuilder.from(Model, () => members[0].defaultValue)
.name(members.map((model) => model.name).join(' | '))
.define('members', members)
static union<TT extends unknown[]>(
...members: ReadonlyArray<ExtendedModel<TT[number]>>
): ExtendedModel<TT[number], Readonly<{ [$members]: ReadonlyArray<ExtendedModel<TT[number]>> }>> {
return CoreModelBuilder.from(Model, () => members[0][$defaultValue])
.name(members.map((model) => model[$name]).join(' | '))
.define($members, members)
.build();
},
};
}

static object<T, U = object>(name: string): ObjectModelBuilder<T, U> {
return ObjectModelBuilder.extend<T, U>(ObjectModel).name(name);
}
}
Loading

0 comments on commit c150f74

Please sign in to comment.