-
Notifications
You must be signed in to change notification settings - Fork 55
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(form-models): reimplement models from classes to pure objects
- Loading branch information
Showing
7 changed files
with
1,952 additions
and
3,770 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]>>>; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
Oops, something went wrong.