-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Migration of src/utils/mixin.ts + tests
- Loading branch information
Guillaume Robin
committed
Jul 7, 2024
1 parent
96eaad1
commit b312e4c
Showing
2 changed files
with
170 additions
and
0 deletions.
There are no files selected for viewing
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 |
---|---|---|
@@ -0,0 +1,29 @@ | ||
// deno-lint-ignore-file no-explicit-any | ||
import { flatten } from "./object.ts"; | ||
|
||
export type MixinFunc< | ||
T extends new (...args: any[]) => any, | ||
> = (Class: T) => T; | ||
|
||
// TODO: rework typing to avoid casting output | ||
export function mixin<T extends new (...args: any[]) => any>( | ||
...params: [T, ...MixinFunc<T>[]] | ||
) { | ||
const args = flatten(params); | ||
const mixins = args.slice(1) as MixinFunc<T>[]; | ||
|
||
return mixins.reduce((Class: T, mixinFunc: MixinFunc<T>) => { | ||
return mixinFunc(Class); | ||
}, args[0] as T); | ||
} | ||
|
||
// TODO: rework typing to avoid casting output | ||
export function compose<T extends new (...args: any[]) => any>( | ||
...args: MixinFunc<T>[] | ||
): (arg0: T) => T { | ||
const mixins = flatten(args); | ||
|
||
return function (Class: T) { | ||
return mixin<T>(Class, ...mixins); | ||
}; | ||
} |
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 |
---|---|---|
@@ -0,0 +1,141 @@ | ||
// deno-lint-ignore-file no-explicit-any | ||
import { assertEquals } from "https://deno.land/[email protected]/assert/mod.ts"; | ||
import { compose, mixin } from "../../src/utils/mixin.ts"; | ||
|
||
// Example mixin functions | ||
function withLogger<T extends new (...args: any[]) => any>(Class: T): T { | ||
return class extends Class { | ||
log(message: string) { | ||
return `[LOG] ${message}`; | ||
} | ||
}; | ||
} | ||
|
||
function withTimestamp<T extends new (...args: any[]) => any>(Class: T): T { | ||
return class extends Class { | ||
getTimestamp(): string { | ||
return new Date().toISOString(); | ||
} | ||
}; | ||
} | ||
|
||
// Base class | ||
class MyBaseClass { | ||
constructor(public name: string) {} | ||
} | ||
|
||
Deno.test("mixin", async (t) => { | ||
await t.step("combines Logger with MyBaseClass", () => { | ||
const MixedClass = mixin(MyBaseClass, withLogger); | ||
const instance = new MixedClass("TestInstance"); | ||
|
||
assertEquals(instance.name, "TestInstance"); | ||
// TODO: how to avoid as any? | ||
assertEquals((instance as any).log("Hello"), "[LOG] Hello"); | ||
}); | ||
|
||
await t.step("combines Timestamp with MyBaseClass", () => { | ||
const MixedClass = mixin(MyBaseClass, withTimestamp); | ||
const instance = new MixedClass("TestInstance"); | ||
|
||
assertEquals(instance.name, "TestInstance"); | ||
assertEquals(typeof (instance as any).getTimestamp(), "string"); | ||
}); | ||
|
||
await t.step( | ||
"combines Logger and Timestamp mixins with MyBaseClass", | ||
() => { | ||
const MixedClass = mixin(MyBaseClass, withLogger, withTimestamp); | ||
const instance = new MixedClass("TestInstance"); | ||
|
||
assertEquals(instance.name, "TestInstance"); | ||
assertEquals((instance as any).log("Hello"), "[LOG] Hello"); | ||
assertEquals(typeof (instance as any).getTimestamp(), "string"); | ||
}, | ||
); | ||
|
||
// Additional edge cases | ||
await t.step("with no mixins returns the base class unchanged", () => { | ||
const MixedClass = mixin(MyBaseClass); | ||
const instance = new MixedClass("TestInstance"); | ||
|
||
assertEquals(instance.name, "TestInstance"); | ||
}); | ||
|
||
await t.step( | ||
"with multiple instances of the same results in correct behavior", | ||
() => { | ||
const MixedClass = mixin( | ||
MyBaseClass, | ||
withLogger, | ||
withLogger, | ||
withLogger, | ||
); | ||
const instance = new MixedClass("TestInstance"); | ||
|
||
assertEquals(instance.name, "TestInstance"); | ||
assertEquals((instance as any).log("Hello"), "[LOG] Hello"); | ||
}, | ||
); | ||
}); | ||
|
||
Deno.test("compose", async (t) => { | ||
await t.step("with Logger mixin", () => { | ||
const ComposedClass = compose(withLogger)(MyBaseClass); | ||
const instance = new ComposedClass("TestInstance"); | ||
|
||
assertEquals(instance.name, "TestInstance"); | ||
// TODO: how to avoid as any? | ||
assertEquals((instance as any).log("Hello"), "[LOG] Hello"); | ||
}); | ||
|
||
await t.step("with Timestamp mixin", () => { | ||
const ComposedClass = compose(withTimestamp)(MyBaseClass); | ||
const instance = new ComposedClass("TestInstance"); | ||
|
||
assertEquals(instance.name, "TestInstance"); | ||
assertEquals(typeof (instance as any).getTimestamp(), "string"); | ||
}); | ||
|
||
await t.step("with Logger and Timestamp mixins", () => { | ||
const ComposedClass = compose(withLogger, withTimestamp)(MyBaseClass); | ||
const instance = new ComposedClass("TestInstance"); | ||
|
||
assertEquals(instance.name, "TestInstance"); | ||
assertEquals((instance as any).log("Hello"), "[LOG] Hello"); | ||
assertEquals(typeof (instance as any).getTimestamp(), "string"); | ||
}); | ||
|
||
await t.step("with multiple instances of Logger mixin", () => { | ||
const ComposedClass = compose(withLogger, withLogger, withLogger)( | ||
MyBaseClass, | ||
); | ||
const instance = new ComposedClass("TestInstance"); | ||
|
||
assertEquals(instance.name, "TestInstance"); | ||
assertEquals((instance as any).log("Hello"), "[LOG] Hello"); | ||
}); | ||
|
||
await t.step("with no mixins", () => { | ||
const ComposedClass = compose()(MyBaseClass); | ||
const instance = new ComposedClass("TestInstance"); | ||
|
||
assertEquals(instance.name, "TestInstance"); | ||
}); | ||
|
||
await t.step("with function as a mixin", () => { | ||
// TODO: I didn't understand how to properly type this test | ||
const ComposedClass = compose((Class: typeof MyBaseClass) => { | ||
return class extends Class { | ||
additionalMethod() { | ||
return "Additional method"; | ||
} | ||
}; | ||
})(MyBaseClass as any); | ||
|
||
const instance = new ComposedClass("TestInstance"); | ||
|
||
assertEquals(instance.name, "TestInstance"); | ||
assertEquals(instance.additionalMethod(), "Additional method"); | ||
}); | ||
}); |