Skip to content

Commit

Permalink
Migration of src/utils/mixin.ts + tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Guillaume Robin committed Jul 7, 2024
1 parent 96eaad1 commit b312e4c
Show file tree
Hide file tree
Showing 2 changed files with 170 additions and 0 deletions.
29 changes: 29 additions & 0 deletions src/utils/mixin.ts
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);
};
}
141 changes: 141 additions & 0 deletions tests/utils/mixin_test.ts
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");
});
});

0 comments on commit b312e4c

Please sign in to comment.