From 8d8f11435ab16c6cdfe0d32a1db36a383f9b1746 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Sun, 15 Sep 2019 15:18:27 -0400 Subject: [PATCH] Added a separate factory function for non-class components --- README.md | 20 +++-- src/BabyIoC.test.ts | 186 ++++++++++++++++++++++---------------------- src/index.ts | 29 ++++--- tslint.json | 9 --- 4 files changed, 128 insertions(+), 116 deletions(-) diff --git a/README.md b/README.md index 5806dd4..86a4042 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ BabyIoC is the smallest IoC container you'll ever see _(under 50 lines of code!) It's also got the fewest toys - it's only targeted for use by [GameStartr](https://github.com/FullScreenShenanigans/GameStartr). Key tenants: -* All `@components` are members of the container class instance. +* All `@component`s are members of the container class instance. * Components are stored as lazily evaluated getters: circular dependencies are fine! * Use TypeScript. @@ -37,6 +37,8 @@ Components receive the instance of the container as a single constructor paramet They can use it to reference other components. ```typescript +import { component } from "babyioc"; + class DependencyA { } class DependencyB { @@ -63,7 +65,11 @@ Your components don't have to be direct classes with dependencies. Pass functions that take in your container as an argument. The values returned by those functions are used as the component value. +Use `factory` instead of `component` for these. + ```typescript +import { factory } from "babyioc"; + class DependencyA { public constructor( public readonly member: string, @@ -73,7 +79,7 @@ class DependencyA { const createDependencyA = () => new DependencyA("value"); class Container { - @component(createDependencyA) + @factory(createDependencyA) public readonly dependencyA: DependencyA; } @@ -83,6 +89,8 @@ const { dependencyA } = new Container(); These factory functions have access to all the values on the container, including computed getters. ```typescript +import { factory } from "babyioc"; + class DependencyA { public constructor( public readonly memberA: string, @@ -100,10 +108,10 @@ const createDependencyA = () => new DependencyA("valueA"); const createDependencyB = (instance: Container) => new DependencyB(dependencyA, container.valueC); class Container { - @component(createDependencyA) + @factory(createDependencyA) public readonly dependencyA: DependencyA; - @component(createDependencyB) + @factory(createDependencyB) public readonly dependencyB: DependencyB; public readonly valueC = "valueC"; @@ -116,13 +124,15 @@ const { dependencyA, dependencyB } = new Container(); ## Technical Details -Marking a member as a `@component` creates a double-layer getter on the class prototype. +Marking a member with `@component` or `@factory` creates a double-layer getter on the class prototype. The prototype will have a getter defined that writes a getter on the calling object. Both getters return a new instance of the component. For example, with this component: ```typescript +import { component } from "babyioc"; + class Dependency { } class Container { diff --git a/src/BabyIoC.test.ts b/src/BabyIoC.test.ts index c580c68..94cbc95 100644 --- a/src/BabyIoC.test.ts +++ b/src/BabyIoC.test.ts @@ -1,8 +1,8 @@ import { expect } from "chai"; -import { component } from "./index"; +import { component, factory } from "./index"; -// tslint:disable completed-docs no-use-before-declare +// tslint:disable completed-docs max-classes-per-file no-parameter-properties describe("container", () => { it("resolves a component dependency", () => { @@ -78,96 +78,6 @@ describe("container", () => { expect(dependencyB).to.be.instanceOf(DependencyB); }); - it("creates a component using a factory", () => { - // Arrange - class Dependency { - public constructor( - public readonly member: string, - ) { } - } - const memberValue = "memberValue"; - const createDependency = () => new Dependency(memberValue); - - class Container { - @component(createDependency) - public readonly dependency: Dependency; - } - - // Act - const { dependency } = new Container(); - - // Assert - expect(dependency.member).to.be.equal(memberValue); - }); - - it("creates different components using factories and their naming classes", () => { - // Arrange - class DependencyA { - public constructor( - public readonly memberA: string, - ) { } - } - class DependencyB { - public constructor( - public readonly memberB: string, - ) { } - } - const memberValueA = "memberValueA"; - const memberValueB = "memberValueB"; - const createDependencyA = () => new DependencyA(memberValueA); - const createDependencyB = () => new DependencyB(memberValueB); - - class Container { - @component(createDependencyA) - public readonly dependencyA: DependencyA; - - @component(createDependencyB) - public readonly dependencyB: DependencyB; - } - - // Act - const { dependencyA, dependencyB } = new Container(); - - // Assert - expect(dependencyA.memberA).to.be.equal(memberValueA); - expect(dependencyB.memberB).to.be.equal(memberValueB); - }); - - it("passes the container after creating getters to factories", () => { - // Arrange - class DependencyA { - public constructor( - public readonly memberA: string, - ) { } - } - class DependencyB { - public constructor( - public readonly referenceA: DependencyA, - public readonly valueC: string, - ) { } - } - const memberValueA = "memberValueA"; - const createDependencyA = () => new DependencyA(memberValueA); - const createDependencyB = (instance: Container) => new DependencyB(dependencyA, instance.valueC); - - class Container { - @component(createDependencyA) - public readonly dependencyA: DependencyA; - - @component(createDependencyB) - public readonly dependencyB: DependencyB; - - public readonly valueC: string; - } - - // Act - const { dependencyA, dependencyB } = new Container(); - - // Assert - expect(dependencyA.memberA).to.be.equal(memberValueA); - expect(dependencyB.referenceA).to.be.equal(dependencyA); - }); - it("allows access to created components in class constructors", () => { // Arrange class Dependency { } @@ -250,3 +160,95 @@ describe("container", () => { expect(grandChild).to.be.instanceOf(GrandChild); }); }); + +describe("factory", () => { + it("creates a component using a factory", () => { + // Arrange + class Dependency { + public constructor( + public readonly member: string, + ) { } + } + const memberValue = "memberValue"; + const createDependency = () => new Dependency(memberValue); + + class Container { + @factory(createDependency) + public readonly dependency: Dependency; + } + + // Act + const { dependency } = new Container(); + + // Assert + expect(dependency.member).to.be.equal(memberValue); + }); + + it("creates different components using factories and their naming classes", () => { + // Arrange + class DependencyA { + public constructor( + public readonly memberA: string, + ) { } + } + class DependencyB { + public constructor( + public readonly memberB: string, + ) { } + } + const memberValueA = "memberValueA"; + const memberValueB = "memberValueB"; + const createDependencyA = () => new DependencyA(memberValueA); + const createDependencyB = () => new DependencyB(memberValueB); + + class Container { + @factory(createDependencyA) + public readonly dependencyA: DependencyA; + + @factory(createDependencyB) + public readonly dependencyB: DependencyB; + } + + // Act + const { dependencyA, dependencyB } = new Container(); + + // Assert + expect(dependencyA.memberA).to.be.equal(memberValueA); + expect(dependencyB.memberB).to.be.equal(memberValueB); + }); + + it("passes the container after creating getters to factories", () => { + // Arrange + class DependencyA { + public constructor( + public readonly memberA: string, + ) { } + } + class DependencyB { + public constructor( + public readonly referenceA: DependencyA, + public readonly valueC: string, + ) { } + } + const memberValueA = "memberValueA"; + const createDependencyA = () => new DependencyA(memberValueA); + const createDependencyB = (instance: Container) => new DependencyB(dependencyA, instance.valueC); + + class Container { + @factory(createDependencyA) + public readonly dependencyA: DependencyA; + + @factory(createDependencyB) + public readonly dependencyB: DependencyB; + + public readonly valueC: string; + } + + // Act + const { dependencyA, dependencyB } = new Container(); + + // Assert + expect(dependencyA.memberA).to.be.equal(memberValueA); + expect(dependencyB.referenceA).to.be.equal(dependencyA); + }); +}); diff --git a/src/index.ts b/src/index.ts index 3a569a5..878b818 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,29 +1,30 @@ -export type IClassWithArg = new(arg: TContainer) => TInstance; +export type IClassWithArg = new (arg: TContainer) => TInstance; -export type IClassWithoutArgs = new() => TInstance; +export type IClassWithoutArgs = new () => TInstance; export type IComponentFunction = (container: TContainer) => TInstance; -export type IComponentClassOrFunction = +export type IComponentClass = | IClassWithArg | IClassWithoutArgs - | IComponentFunction -; + ; /** - * Adds a member component to a parent container. + * Decorates a caching getter on a class prototype. * - * @param componentFunction Class or function that creates the component. + * @param factory Method used once within the getter to create an instance member. */ -export const component = (componentFunction: IComponentClassOrFunction) => +export const factory = ( + factory: IComponentFunction, +) => (parentPrototype: TContainer, memberName: string) => { Object.defineProperty(parentPrototype, memberName, { configurable: true, get(this: TContainer): TInstance { - const value: TInstance = new (componentFunction as IClassWithArg)(this); + const value: TInstance = factory(this); Object.defineProperty(this, memberName, { - configurable: true, + configurable: false, get: () => value, }); @@ -31,3 +32,11 @@ export const component = (componentFunction: I }, }); }; + +/** + * Decorates a member component class on a class prototype. + * + * @param componentClass Class to be initialized for the member instance. + */ +export const component = (componentClass: IComponentClass) => + factory((container: TContainer) => new componentClass(container)); diff --git a/tslint.json b/tslint.json index 1804d39..59247a1 100644 --- a/tslint.json +++ b/tslint.json @@ -4,14 +4,5 @@ "exclude": [ "./node_modules/**/*" ] - }, - "rules": { - "ban-types": false, - "completed-docs": false, - "max-classes-per-file": [true, "exclude-class-expressions"], - "no-non-null-assertion": false, - "no-parameter-properties": false, - "only-arrow-functions": false, - "no-shadowed-variable": false } }