diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000..692b0f6 Binary files /dev/null and b/bun.lockb differ diff --git a/package.json b/package.json index e2d658f..4f0ff3b 100644 --- a/package.json +++ b/package.json @@ -28,9 +28,13 @@ "clean:node": "rm -rf node_modules" }, "dependencies": { - "type-fest": "^4.9.0" + "effect": "^2.0.0-next.62", + "lodash-es": "^4.17.21", + "type-fest": "^4.9.0", + "zod": "^3.22.4" }, "devDependencies": { + "@types/lodash-es": "^4.17.12", "bun-types": "latest", "npm-check-updates": "^16.14.12", "npm-sort": "^0.0.4", diff --git a/src/index.ts b/src/index.ts index 8351eca..e69de29 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,195 +0,0 @@ -import { AbstractClass, AbstractConstructor, Opaque, UnionToIntersection } from "type-fest" - - -/** - * Represents the static members of a class. - * @template C - The class type. - */ -export type StaticMembers = { - [Key in keyof C as Key extends "prototype" ? never : Key]: C[Key] -} - - -/** - * Represents a trait that can be applied to a class. - * @template C - The abstract class type. - */ -export type Trait< - C extends AbstractClass -> = Opaque< - TraitApplier, - "thilatrait/Trait" -> - -/** - * Represents the function signature for applying a trait to a parent class. - * @template C - The abstract class type. - */ -export type TraitApplier< - C extends AbstractClass -> = - (Parent: AbstractConstructor) => C - -/** - * Unwraps the type of the class from a given trait. - * @template T - The trait type. - */ -export type UnwrapTraitC = - T extends Trait - ? C - : never - - -/** - * Creates a trait using the provided trait applier function. - * @template C - The abstract class type. - * @param applier - The trait applier function. - * @returns A trait. - * @example - * Creates a trait: - * ```ts - * const Permissible = trait(Parent => { - * abstract class Permissible extends Parent { - * static readonly defaultPermissions: string[] = [] - * permissions: string[] = [] - * - * // Constructor is optional - * // If you wish to use it, make sure it takes any[] as an args array and passes it to the super call. This is necessary for inheritance to work properly. - * // Trait constructors cannot have typed arguments of their own, they only serve to run logic during object instantiation. - * constructor(...args: any[]) { - * super(...args) - * } - * } - * - * return Permissible - * }) - * ``` - * Creates a generic trait: - * ```ts - * const Identifiable = () => - * trait(Parent => { - * abstract class Identifiable extends Parent { - * abstract readonly id: ID - * - * equals(el: Identifiable) { - * return this.id === el.id - * } - * - * // Optional - * constructor(...args: any[]) { - * super(...args) - * } - * } - * - * return Identifiable - * }) - * ``` - * Creates a subtrait: - * ```ts - * const ImplementsIdentifiable = (defaultID: ID) => - * trait(Parent => { - * abstract class ImplementsIdentifiable extends extendsAndExpresses( - * Parent, - * [Identifiable()], - * ) { - * id: ID = defaultID - * - * // Optional - * constructor(...args: any[]) { - * super(...args) - * } - * } - * - * return ImplementsIdentifiable - * }) - * ``` - */ -export function trait< - C extends AbstractClass ->( - applier: TraitApplier -) { - return applier as Trait -} - - -/** - * Extends a class with the given traits and expresses their combined functionality. - * @template C - The abstract class type. - * @template Traits - An array of traits. - * @param extend - The class to extend. - * @param traits - An array of traits to apply. - * @returns A new class type expressing the combined functionality of the base class and traits. - * @example - * Extends a superclass and applies traits: - * ```ts - * class User extends extendsAndExpresses(Entity, [Identifiable(), Permissible]) { - * readonly id: bigint - * - * constructor(id: bigint) { - * super() - * this.id = id - * } - * } - * ``` - */ -export function extendsAndExpresses< - C extends AbstractClass, - Traits extends readonly Trait[], ->( - extend: C, - traits: Traits, -) { - return traits.reduce( - (previous, trait) => trait(previous), - extend, - ) as ( - AbstractClass< - InstanceType & - UnionToIntersection< - InstanceType< - UnwrapTraitC< - Traits[number] - > - > - >, - - ConstructorParameters - > & - - StaticMembers & - StaticMembers< - UnionToIntersection< - UnwrapTraitC< - Traits[number] - > - > - > - ) -} - -/** - * Expresses the combined functionality of multiple traits. - * @template Traits - An array of trait. - * @param traits - An array of trait to apply. - * @returns A new class type expressing the combined functionality of the traits. - * @example - * Applies traits to a class: - * ```ts - * class User extends expresses(Identifiable(), Permissible) { - * readonly id: bigint - * - * constructor(id: bigint) { - * super() - * this.id = id - * } - * } - * ``` - */ -export function expresses< - Traits extends readonly Trait[], ->( - ...traits: Traits -) { - return extendsAndExpresses(Object, traits) -} diff --git a/src/legacy/tests.ts b/src/legacy/tests.ts deleted file mode 100644 index e86528d..0000000 --- a/src/legacy/tests.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { AbstractClass } from "type-fest" -import { expresses } from "./trait" - - -function inspectClass(class_: AbstractClass) { - Object.getOwnPropertyNames(class_).forEach(name => { - console.log( - "[static]", - name, - Object.getOwnPropertyDescriptor(class_, name) - ) - }) - - Object.getOwnPropertyNames(class_.prototype).forEach(name => { - console.log( - "[prototype]", - name, - Object.getOwnPropertyDescriptor(class_.prototype, name) - ) - }) -} - - -abstract class Identified { - abstract id: ID - - equals(el: Identified) { - return this.id === el.id - } - - // initializer() { - // console.log("Identified initializer") - // } -} - -class ImplementsIdentifiable extends Identified { - id!: ID -} - - -abstract class Permissible { - static readonly defaultPermissions: string[] = [] - permissions: string[] = [] - // permissions!: string[] - - constructor() { - console.log("Permissible constructor") - } - - initializer() { - console.log("Permissible initializer") - this.permissions = [] - } -} - - -class User extends expresses( - Identified as typeof Identified, - // Identified, - Permissible, -) { - readonly id: bigint - - constructor(id: bigint) { - super() - this.id = id - } -} - -const user1 = new User(BigInt(1)) -const user2 = new User(BigInt(2)) - -console.log(user1) -console.log(user1.equals(user2)) diff --git a/src/legacy/trait.ts b/src/legacy/trait.ts deleted file mode 100644 index 10cc87b..0000000 --- a/src/legacy/trait.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { AbstractClass, Class, UnionToIntersection } from "type-fest" -import { StaticMembers, copyProperties, getInheritanceHierarchy } from "./util/class" - - -/** - * Represents a trait that can be used to define common behavior - * for classes and abstract classes. - * @typeParam T - The type of the trait. - */ -export type Trait = - AbstractClass - - -/** - * Creates a link class that expresses the given traits. - * @param traits - An array of traits to be expressed by the link class. - * @returns A dynamically created class that expresses the given traits. - * @typeParam Traits - An array of traits that the link class expresses. - */ -export function expresses< - Traits extends readonly Trait[] ->( - ...traits: Traits -) { - return makeLinkClass(traits) -} - - -/** - * Creates a link class that extends a base class and expresses the given traits. - * @param extend - The base class or abstract class to extend. - * @param traits - An array of traits to be expressed by the link class. - * @returns A dynamically created class that extends the given base class and expresses the given traits. - * @typeParam C - The type of the base class to extend. - * @typeParam Traits - An array of traits that the link class expresses. - */ -export function extendsAndExpresses< - C extends Class - | AbstractClass, - Traits extends readonly Trait[], ->( - extend: C, - ...traits: Traits -) { - return makeLinkClass(traits, extend) -} - - -/** - * Creates a link class that expresses the given traits and optionally extends a base class. - * @param traits - An array of traits to be expressed by the link class. - * @param extend - The base class or abstract class to extend (optional). - * @returns A dynamically created class that expresses the given traits and extends the base class. - * @typeParam Traits - An array of traits that the link class expresses. - * @typeParam C - The type of the base class to extend (optional). - */ -export function makeLinkClass< - Traits extends readonly Trait[], - C extends Class - | AbstractClass - | undefined = undefined, ->( - traits: Traits, - extend?: C, -) { - const class_ = extend - ? class extends extend { - constructor(...args: any[]) { - super(...args) - - traits.forEach(trait => { - trait.prototype.initializer?.call(this) - }) - } - } - : class { - constructor() { - traits.forEach(trait => { - trait.prototype.initializer?.call(this) - }) - } - } - - traits.forEach(trait => { - getInheritanceHierarchy(trait).forEach(current => { - copyProperties( - current, - class_, - ["name", "length"], - ["constructor"], - ) - }) - }) - - return class_ as unknown as ( - (C extends Class | AbstractClass - ? ( - AbstractClass< - InstanceType & - UnionToIntersection< - InstanceType< - Traits[number] - > - >, - - ConstructorParameters - > & - - StaticMembers - ) - : Trait< - UnionToIntersection< - InstanceType< - Traits[number] - > - > - > - ) & - - StaticMembers< - UnionToIntersection< - Traits[number] - > - > - ) -} diff --git a/src/legacy/util/class.ts b/src/legacy/util/class.ts deleted file mode 100644 index 518c960..0000000 --- a/src/legacy/util/class.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { AbstractClass } from "type-fest" - - -/** - * Represents the static members of a class. - * - * @template C - The type of the class for which static members are extracted. - * @typeparam The static members of the class. - */ -export type StaticMembers = Pick - - -/** - * Flattens the inheritance hierarchy of a given class by copying all properties - * from its superclass chain into a single object. - * - * @template C - The type of the class to be flattened, extending AbstractClass. - * @param C - The class to be flattened. - * @returns A new class with properties flattened from the entire inheritance hierarchy. - */ -export function flattenClass< - C extends AbstractClass ->(class_: C) { - const flattenedClass = class {} as unknown as C - - getInheritanceHierarchy(class_).forEach(current => { - copyProperties(current, flattenedClass) - }) - - copyProperty(class_, flattenedClass, "name") - copyProperty(class_.prototype, flattenedClass.prototype, "constructor") - - return flattenedClass -} - - -/** - * Retrieves the inheritance hierarchy of a given class, including itself. - * - * @param class_ - The class for which the inheritance hierarchy is retrieved. - * @returns An array representing the inheritance hierarchy, ordered from the furthest in the hierarchy to `class_` itself. - */ -export function getInheritanceHierarchy( - class_: AbstractClass -): AbstractClass[] { - const parent = Object.getPrototypeOf(class_) - - return isClass(parent) - ? [...getInheritanceHierarchy(parent), class_] - : [class_] -} - - -/** - * Checks if a given element appears to be a class based on its string representation. - * - * @param el - The element to check for being a class. - * @returns `true` if the element is likely a class; otherwise, `false`. - */ -export function isClass(el: { toString: () => string }) { - return Boolean(el.toString().match(/^class(?: [.\S]+)?(?: extends [.\S]+)? {[\s\S]*}$/)) -} - - -/** - * Copies properties from one class to another, including static and prototype properties. - * - * @param from - The source class to copy properties from. - * @param to - The destination class to copy properties to. - */ -export function copyProperties( - from: AbstractClass, - to: AbstractClass, - ignoreKeys: string[] = [], - ignorePrototypeKeys: string[] = [], -) { - Object.getOwnPropertyNames(from).forEach(name => { - if (name === "prototype" - || ignoreKeys.find(v => v === name) - ) - return - - // console.log(from, to, name, Object.getOwnPropertyDescriptor(from, name)) - - copyProperty(from, to, name) - }) - - Object.getOwnPropertyNames(from.prototype).forEach(name => { - if (ignorePrototypeKeys.find(v => v === name)) - return - - // console.log(from, to, name, Object.getOwnPropertyDescriptor(from, name)) - - copyProperty(from.prototype, to.prototype, name) - }) -} - - -export function copyProperty( - from: unknown, - to: unknown, - name: string, -) { - Object.defineProperty( - to, - name, - Object.getOwnPropertyDescriptor(from, name) || Object.create(null), - ) -} diff --git a/src/tests.ts b/src/tests.ts deleted file mode 100644 index bb49d1f..0000000 --- a/src/tests.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { expresses, extendsAndExpresses, trait } from "." - - -const Identifiable = () => - trait(Parent => { - abstract class Identifiable extends Parent { - abstract readonly id: ID - - equals(el: Identifiable) { - return this.id === el.id - } - - constructor(...args: any[]) { - super(...args) - console.log("Identified constructor") - } - } - - return Identifiable - }) - -const ImplementsIdentifiable = (defaultID: ID) => - trait(Parent => { - abstract class ImplementsIdentifiable extends extendsAndExpresses( - Parent, - [Identifiable()], - ) { - id: ID = defaultID - - constructor(...args: any[]) { - super(...args) - console.log("ImplementsIdentifiable constructor") - } - } - - return ImplementsIdentifiable - }) - - -const Permissible = trait(Parent => { - abstract class Permissible extends Parent { - static readonly defaultPermissions: string[] = [] - permissions: string[] = [] - - constructor(...args: any[]) { - super(...args) - console.log("Permissible constructor") - } - } - - return Permissible -}) - - -const UserProto = expresses( - // Identifiable(), - ImplementsIdentifiable(0n), - Permissible, -) - -class User extends UserProto { - constructor(id: bigint) { - super() - this.id = id - } -} - -const user1 = new User(1n) -console.log(user1) -console.log(user1.equals(user1)) diff --git a/src/util.ts b/src/util.ts new file mode 100644 index 0000000..7ab75c7 --- /dev/null +++ b/src/util.ts @@ -0,0 +1,73 @@ +import { Effect, pipe } from "effect" +import { mapValues } from "lodash-es" +import { z } from "zod" + + +/** + * Removes default values from a ZodObject schema and returns a new schema. + * + * @param schema - The ZodObject schema to process. + * @returns A new ZodObject schema with default values removed. + */ +export const zodObjectRemoveDefaults = < + T extends z.ZodRawShape, + UnknownKeys extends z.UnknownKeysParam, + Catchall extends z.ZodTypeAny, + Output extends {}, + Input extends {}, +>( + schema: z.ZodObject< + T, + UnknownKeys, + Catchall, + Output, + Input + > +) => + schema.extend(zodShapeRemoveDefaults(schema.shape)) + +/** + * Removes default values from a ZodObject shape and returns a new shape. + * + * @param shape - The ZodObject shape to process. + * @returns A new shape with default values removed. + */ +export const zodShapeRemoveDefaults = < + Shape extends z.ZodRawShape +>( + shape: Shape +): { + [K in keyof Shape]: + Shape[K] extends z.ZodDefault + ? T + : Shape[K] +} => + mapValues(shape, el => + el instanceof z.ZodDefault + ? el.removeDefault() + : el + ) + + +/** + * Parses a value using a ZodType schema wrapped in an Effect monad. + * + * @param schema - The ZodType schema to use for parsing. + * @param args - The arguments to pass to the `safeParseAsync` method of the schema. + * @returns An Effect monad representing the parsing result. + */ +export const parseZodTypeEffect = < + Output, + Input, +>( + schema: z.ZodType, + ...args: Parameters +) => pipe( + Effect.promise(() => schema.safeParseAsync(...args)), + + Effect.flatMap(response => + response.success + ? Effect.succeed(response.data) + : Effect.fail(response.error) + ), +)