Skip to content

Commit 7bfa3e8

Browse files
committed
feat: introduce createDependency for deps without abstractions
1 parent 8ea5abc commit 7bfa3e8

File tree

7 files changed

+330
-6
lines changed

7 files changed

+330
-6
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
node_modules
22
dist
33
.idea
4+
.claude

__tests__/createDependency.test.ts

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
import { describe, test, expect, beforeEach } from "vitest";
2+
import { Container, Abstraction, createDependency } from "../src/index.js";
3+
4+
describe("createDependency", () => {
5+
let container: Container;
6+
7+
beforeEach(() => {
8+
container = new Container();
9+
});
10+
11+
test("should create a dependency-only class without abstraction", () => {
12+
// Create an abstraction for a simple service
13+
const ConfigAbstraction = new Abstraction<{ getValue(): string }>("Config");
14+
15+
class ConfigImpl {
16+
getValue(): string {
17+
return "config value";
18+
}
19+
}
20+
21+
const ConfigImplementation = ConfigAbstraction.createImplementation({
22+
implementation: ConfigImpl,
23+
dependencies: []
24+
});
25+
26+
// Create a local dependency class that doesn't need an abstraction
27+
class LocalService {
28+
constructor(private config: { getValue(): string }) {}
29+
30+
getConfigValue(): string {
31+
return this.config.getValue();
32+
}
33+
}
34+
35+
const LocalServiceDep = createDependency({
36+
implementation: LocalService,
37+
dependencies: [ConfigAbstraction]
38+
});
39+
40+
// Now use the local service in another abstraction
41+
const AppAbstraction = new Abstraction<{ run(): string }>("App");
42+
43+
class AppImpl {
44+
constructor(private localService: LocalService) {}
45+
46+
run(): string {
47+
return this.localService.getConfigValue();
48+
}
49+
}
50+
51+
const AppImplementation = AppAbstraction.createImplementation({
52+
implementation: AppImpl,
53+
dependencies: [LocalServiceDep]
54+
});
55+
56+
// Register and resolve
57+
container.register(ConfigImplementation);
58+
container.register(AppImplementation);
59+
60+
const app = container.resolve(AppAbstraction);
61+
expect(app.run()).toBe("config value");
62+
});
63+
64+
test("should resolve dependencies transitively through dependency-only classes", () => {
65+
// Create a chain: ServiceA -> LocalClass -> ServiceB
66+
const ServiceBAbstraction = new Abstraction<{ getValue(): number }>("ServiceB");
67+
68+
class ServiceBImpl {
69+
getValue(): number {
70+
return 42;
71+
}
72+
}
73+
74+
const ServiceBImplementation = ServiceBAbstraction.createImplementation({
75+
implementation: ServiceBImpl,
76+
dependencies: []
77+
});
78+
79+
// Local class without abstraction
80+
class LocalMiddleware {
81+
constructor(private serviceB: { getValue(): number }) {}
82+
83+
process(): number {
84+
return this.serviceB.getValue() * 2;
85+
}
86+
}
87+
88+
const LocalMiddlewareDep = createDependency({
89+
implementation: LocalMiddleware,
90+
dependencies: [ServiceBAbstraction]
91+
});
92+
93+
// Another service that depends on the local class
94+
const ServiceAAbstraction = new Abstraction<{ compute(): number }>("ServiceA");
95+
96+
class ServiceAImpl {
97+
constructor(private middleware: LocalMiddleware) {}
98+
99+
compute(): number {
100+
return this.middleware.process() + 10;
101+
}
102+
}
103+
104+
const ServiceAImplementation = ServiceAAbstraction.createImplementation({
105+
implementation: ServiceAImpl,
106+
dependencies: [LocalMiddlewareDep]
107+
});
108+
109+
container.register(ServiceBImplementation);
110+
container.register(ServiceAImplementation);
111+
112+
const serviceA = container.resolve(ServiceAAbstraction);
113+
expect(serviceA.compute()).toBe(94); // (42 * 2) + 10
114+
});
115+
116+
test("should work with multiple dependency-only classes", () => {
117+
const DataAbstraction = new Abstraction<{ getData(): string }>("Data");
118+
119+
class DataImpl {
120+
getData(): string {
121+
return "data";
122+
}
123+
}
124+
125+
const DataImplementation = DataAbstraction.createImplementation({
126+
implementation: DataImpl,
127+
dependencies: []
128+
});
129+
130+
// First local class
131+
class LocalParser {
132+
constructor(private data: { getData(): string }) {}
133+
134+
parse(): string {
135+
return `parsed: ${this.data.getData()}`;
136+
}
137+
}
138+
139+
const LocalParserDep = createDependency({
140+
implementation: LocalParser,
141+
dependencies: [DataAbstraction]
142+
});
143+
144+
// Second local class
145+
class LocalFormatter {
146+
constructor(private data: { getData(): string }) {}
147+
148+
format(): string {
149+
return `formatted: ${this.data.getData()}`;
150+
}
151+
}
152+
153+
const LocalFormatterDep = createDependency({
154+
implementation: LocalFormatter,
155+
dependencies: [DataAbstraction]
156+
});
157+
158+
// Service that uses both local classes
159+
const ProcessorAbstraction = new Abstraction<{ process(): string }>("Processor");
160+
161+
class ProcessorImpl {
162+
constructor(
163+
private parser: LocalParser,
164+
private formatter: LocalFormatter
165+
) {}
166+
167+
process(): string {
168+
return `${this.parser.parse()} | ${this.formatter.format()}`;
169+
}
170+
}
171+
172+
const ProcessorImplementation = ProcessorAbstraction.createImplementation({
173+
implementation: ProcessorImpl,
174+
dependencies: [LocalParserDep, LocalFormatterDep]
175+
});
176+
177+
container.register(DataImplementation);
178+
container.register(ProcessorImplementation);
179+
180+
const processor = container.resolve(ProcessorAbstraction);
181+
expect(processor.process()).toBe("parsed: data | formatted: data");
182+
});
183+
184+
test("should throw error if dependency-only class is used without metadata", () => {
185+
// Create a class without using createDependency
186+
class InvalidClass {
187+
constructor(private value: string) {}
188+
}
189+
190+
const ServiceAbstraction = new Abstraction<{ test(): void }>("Service");
191+
192+
class ServiceImpl {
193+
constructor(private invalid: InvalidClass) {}
194+
195+
test(): void {}
196+
}
197+
198+
const ServiceImplementation = ServiceAbstraction.createImplementation({
199+
implementation: ServiceImpl,
200+
dependencies: [InvalidClass] // Using class without createDependency
201+
});
202+
203+
container.register(ServiceImplementation);
204+
205+
expect(() => container.resolve(ServiceAbstraction)).toThrow(
206+
"InvalidClass does not have dependency metadata"
207+
);
208+
});
209+
210+
test("should support nested dependency-only classes", () => {
211+
const CoreAbstraction = new Abstraction<{ getCore(): string }>("Core");
212+
213+
class CoreImpl {
214+
getCore(): string {
215+
return "core";
216+
}
217+
}
218+
219+
const CoreImplementation = CoreAbstraction.createImplementation({
220+
implementation: CoreImpl,
221+
dependencies: []
222+
});
223+
224+
// Level 1 local class
225+
class Level1 {
226+
constructor(private core: { getCore(): string }) {}
227+
228+
getLevel1(): string {
229+
return `L1(${this.core.getCore()})`;
230+
}
231+
}
232+
233+
const Level1Dep = createDependency({
234+
implementation: Level1,
235+
dependencies: [CoreAbstraction]
236+
});
237+
238+
// Level 2 local class depends on Level 1
239+
class Level2 {
240+
constructor(private level1: Level1) {}
241+
242+
getLevel2(): string {
243+
return `L2(${this.level1.getLevel1()})`;
244+
}
245+
}
246+
247+
const Level2Dep = createDependency({
248+
implementation: Level2,
249+
dependencies: [Level1Dep]
250+
});
251+
252+
// Top level service depends on Level 2
253+
const TopAbstraction = new Abstraction<{ getTop(): string }>("Top");
254+
255+
class TopImpl {
256+
constructor(private level2: Level2) {}
257+
258+
getTop(): string {
259+
return `Top(${this.level2.getLevel2()})`;
260+
}
261+
}
262+
263+
const TopImplementation = TopAbstraction.createImplementation({
264+
implementation: TopImpl,
265+
dependencies: [Level2Dep]
266+
});
267+
268+
container.register(CoreImplementation);
269+
container.register(TopImplementation);
270+
271+
const top = container.resolve(TopAbstraction);
272+
expect(top.getTop()).toBe("Top(L2(L1(core)))");
273+
});
274+
});

src/Container.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,10 +137,15 @@ export class Container {
137137
}
138138

139139
private resolveInternal<T>(
140-
abstraction: Abstraction<T>,
140+
abstraction: Abstraction<T> | Constructor<T>,
141141
resolutionStack: Map<symbol, boolean>,
142142
options: DependencyOptions
143143
): T {
144+
// Handle constructor classes (dependency-only)
145+
if (typeof abstraction === "function") {
146+
return this.resolveDependencyOnly(abstraction, resolutionStack);
147+
}
148+
144149
if (resolutionStack.has(abstraction.token) && !options.multiple) {
145150
throw new Error(`Circular dependency detected for ${abstraction.toString()}`);
146151
}
@@ -295,6 +300,27 @@ export class Container {
295300

296301
return result;
297302
}
303+
304+
private resolveDependencyOnly<T>(
305+
constructor: Constructor<T>,
306+
resolutionStack: Map<symbol, boolean>
307+
): T {
308+
const metadata = new Metadata(constructor);
309+
const dependencies = metadata.getDependencies();
310+
311+
if (!dependencies) {
312+
throw new Error(
313+
`${constructor.name} does not have dependency metadata. Use createDependency to define dependencies.`
314+
);
315+
}
316+
317+
const resolvedDeps = dependencies.map(dep => {
318+
const [abstractionDep, depOptions] = Array.isArray(dep) ? dep : [dep, {}];
319+
return this.resolveInternal(abstractionDep, new Map(resolutionStack), depOptions);
320+
});
321+
322+
return new constructor(...resolvedDeps);
323+
}
298324
}
299325

300326
class RegistrationBuilder<T> {

src/Metadata.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ export const KEYS = {
55
ABSTRACTION: "wby:abstraction",
66
DEPENDENCIES: "wby:dependencies",
77
IS_DECORATOR: "wby:isDecorator",
8-
IS_COMPOSITE: "wby:isComposite"
8+
IS_COMPOSITE: "wby:isComposite",
9+
IS_DEPENDENCY: "wby:isDependency"
910
};
1011

1112
export class Metadata<T extends Constructor> {

src/createDependency.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import type { Constructor, Dependencies } from "./types.js";
2+
import { Metadata } from "./Metadata.js";
3+
4+
export function createDependency<I extends Constructor>(params: {
5+
implementation: I;
6+
dependencies: Dependencies<I>;
7+
}): I {
8+
const metadata = new Metadata(params.implementation);
9+
metadata.setDependencies(params.dependencies);
10+
metadata.setAttribute("IS_DEPENDENCY", true);
11+
12+
return params.implementation;
13+
}

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export { Abstraction } from "./Abstraction.js";
33
export { createImplementation } from "./createImplementation.js";
44
export { createDecorator } from "./createDecorator.js";
55
export { createComposite } from "./createComposite.js";
6+
export { createDependency } from "./createDependency.js";
67
export { Metadata } from "./Metadata.js";
78
export * from "./isDecorator.js";
89
export * from "./isComposite.js";

src/types.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ export interface DependencyOptions {
1010
optional?: boolean;
1111
}
1212

13-
export type Dependency = Abstraction<any> | [Abstraction<any>, DependencyOptions];
13+
export type Dependency =
14+
| Abstraction<any>
15+
| [Abstraction<any>, DependencyOptions]
16+
| Constructor<any>
17+
| [Constructor<any>, DependencyOptions];
1418

1519
export interface Registration<T = any> {
1620
implementation: Constructor<T>;
@@ -58,11 +62,15 @@ export type MapDependencies<T extends [...any]> = {
5862
: // Requires a single implementation.
5963
T[K] extends IsOptionalValue<T[K]>
6064
? // Support shorthand and long form.
61-
[Abstraction<T[K]>, OptionalTrue & Partial<MultipleFalse>]
65+
| [Abstraction<T[K]>, OptionalTrue & Partial<MultipleFalse>]
66+
| [Constructor<T[K]>, OptionalTrue & Partial<MultipleFalse>]
6267
: // Support shorthand and long form.
63-
| [Abstraction<T[K]>, MultipleFalse & Partial<OptionalFalse>]
68+
| [Abstraction<T[K]>, MultipleFalse & Partial<OptionalFalse>]
6469
| [Abstraction<T[K]>]
65-
| Abstraction<T[K]>;
70+
| Abstraction<T[K]>
71+
| [Constructor<T[K]>, MultipleFalse & Partial<OptionalFalse>]
72+
| [Constructor<T[K]>]
73+
| Constructor<T[K]>;
6674
};
6775

6876
export type Dependencies<T> = T extends Constructor

0 commit comments

Comments
 (0)