Skip to content

Commit ae9fb58

Browse files
committed
feat(ioc): allow deep plain object as param
1 parent 1d24f05 commit ae9fb58

5 files changed

Lines changed: 112 additions & 24 deletions

File tree

.changeset/hot-points-help.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@dreamkit/ioc": minor
3+
---
4+
5+
Allow deep plain object as param

packages/ioc/src/context.ts

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
} from "./registry.js";
1818
import { ensureSync } from "./utils/async.js";
1919
import { iocKind, KindMap } from "./utils/kind.js";
20+
import { isPlainObject } from "./utils/object.js";
2021
import { capitalize } from "./utils/string.js";
2122
import type { AbstractConstructor, Constructor } from "./utils/ts.js";
2223
import { is } from "@dreamkit/kind";
@@ -323,7 +324,7 @@ export class IocContext {
323324
if (!onResolveIocObject || onResolveIocObject(input)) {
324325
return {
325326
paramOptions: { context, parent: input },
326-
params: normalizeIocParams(iocObject.$ioc.params),
327+
params: iocObject.$ioc.params,
327328
create,
328329
};
329330
}
@@ -340,22 +341,45 @@ export class IocContext {
340341
? this.resolveAsync(input, options)
341342
: this.resolve(input, options);
342343
}
343-
resolveParams<T extends IocParamsUserConfig>(input: T): IocParams<T> {
344-
const config = normalizeIocParams(input);
344+
resolveParams<T extends IocParamsUserConfig>(
345+
input: T,
346+
options: {
347+
parent?: unknown;
348+
context?: IocContext;
349+
} = {},
350+
): IocParams<T> {
351+
const config = normalizeIocParams(input, false);
345352
const params: Record<string, any> = {};
346353
for (const name in config) {
347-
params[name] = ensureSync(this.createParam(config[name]));
354+
const value = config[name];
355+
if (isPlainObject(value)) {
356+
params[name] = this.resolveParams(value, options);
357+
} else {
358+
params[name] = ensureSync(this.createParam(value, options));
359+
}
348360
}
349361
return params as any;
350362
}
351363

352364
async resolveAsyncParams<T extends IocParamsUserConfig>(
353365
input: T,
366+
options: {
367+
parent?: unknown;
368+
context?: IocContext;
369+
} = {},
354370
): Promise<IocParams<T>> {
355-
const config = normalizeIocParams(input);
371+
const config = normalizeIocParams(input, false);
356372
const params: Record<string, any> = {};
357373
for (const name in config) {
358-
params[name] = await this.createParam(config[name]);
374+
const value = config[name];
375+
if (isPlainObject(value)) {
376+
params[name] = await this.resolveAsyncParams(value, options);
377+
} else {
378+
params[name] = await this.createParam(value, {
379+
...options,
380+
async: true,
381+
});
382+
}
359383
}
360384
return params as any;
361385
}
@@ -400,12 +424,7 @@ export class IocContext {
400424
const object = this.tryParseIocObject(input, options);
401425

402426
if (object) {
403-
const params: Record<string, any> = {};
404-
for (const name in object.params) {
405-
params[name] = ensureSync(
406-
this.createParam(object.params[name], object.paramOptions),
407-
);
408-
}
427+
const params = this.resolveParams(object.params, object.paramOptions);
409428
return object.create(params);
410429
}
411430
}
@@ -451,13 +470,10 @@ export class IocContext {
451470
const object = this.tryParseIocObject(input, options);
452471

453472
if (object) {
454-
const params: Record<string, any> = {};
455-
for (const name in object.params) {
456-
params[name] = await this.createParam(object.params[name], {
457-
...object.paramOptions,
458-
async: true,
459-
});
460-
}
473+
const params = await this.resolveAsyncParams(
474+
object.params,
475+
object.paramOptions,
476+
);
461477
return object.create(params);
462478
}
463479
}

packages/ioc/src/params.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { IocFunc } from "./func.js";
22
import type { IocRegistryData, IocRegistryKey } from "./registry.js";
33
import { iocKind } from "./utils/kind.js";
4+
import { isPlainObject } from "./utils/object.js";
45
import { uncapitalize } from "./utils/string.js";
56
import type {
67
AbstractConstructor,
@@ -18,7 +19,9 @@ export type IocParamValue = ParamValue & {
1819
};
1920

2021
export type IocParamsConfig = Record<string, IocParamBuilder>;
21-
export type IocParamsUserConfig = Record<string, IocParamBuilder | ParamValue>;
22+
export type IocParamsUserConfig = {
23+
[name: string]: IocParamBuilder | ParamValue | IocParamsUserConfig;
24+
};
2225

2326
export type IocParamConfigurable<V extends ParamValue> = V extends IocParamValue
2427
? {
@@ -131,7 +134,9 @@ export type IocParam<V> = V extends NumberConstructor
131134
? V extends { bind: IocBind }
132135
? ReturnType<V["bind"]>
133136
: (...args: Parameters<V>) => ReturnType<V>
134-
: V;
137+
: V extends Record<string, any>
138+
? IocParams<V>
139+
: V;
135140

136141
export type IocBind<S = any, R = any> = {
137142
(input: S): R;
@@ -180,16 +185,27 @@ export function iocParam<V extends ParamValue>(
180185
): IocParamBuilder<{ value: V }> {
181186
return new IocParamBuilder({ value });
182187
}
183-
188+
export function normalizeIocParams(input: IocParamsUserConfig): IocParamsConfig;
189+
export function normalizeIocParams(
190+
input: IocParamsUserConfig,
191+
deep: false,
192+
): {
193+
[name: string]: IocParamBuilder | IocParamsUserConfig;
194+
};
184195
export function normalizeIocParams(
185196
input: IocParamsUserConfig,
197+
deep?: boolean,
186198
): IocParamsConfig {
187-
const params: IocParamsConfig = {};
199+
const params: any = {};
188200
for (const key in input) {
189201
const value = input[key];
190202
params[uncapitalize(key)] = kindOf(value, IocParamBuilder)
191203
? value
192-
: iocParam(value);
204+
: isPlainObject(value)
205+
? deep
206+
? normalizeIocParams(value)
207+
: value
208+
: iocParam(value);
193209
}
194210
return params;
195211
}

packages/ioc/src/utils/object.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export function isPlainObject(
2+
value: unknown,
3+
): value is Record<string, unknown> {
4+
if (!value || typeof value !== "object") return false;
5+
const proto = Object.getPrototypeOf(value);
6+
return !proto || !Object.getPrototypeOf(proto);
7+
}

packages/ioc/test/class.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { createIocClass, IocClass } from "../src/class.js";
22
import { context } from "../src/context.js";
3+
import { IocFunc } from "../src/func.js";
34
import { iocParam } from "../src/params.js";
45
import { kind } from "@dreamkit/kind";
56
import { describe, expect, expectTypeOf, it } from "vitest";
@@ -187,6 +188,49 @@ describe("IocClass", () => {
187188
external: 2,
188189
});
189190
});
191+
it("with deep plain object", () => {
192+
class Handler {
193+
read() {
194+
return 1;
195+
}
196+
fetch() {
197+
return 2;
198+
}
199+
}
200+
class Token {
201+
constructor(readonly value: string) {}
202+
}
203+
204+
const api = {
205+
fs: {
206+
Token,
207+
read: IocFunc({ Handler })(function () {
208+
return this.handler.read();
209+
}),
210+
},
211+
network: {
212+
http: {
213+
Token: iocParam(Token).optional(),
214+
fetch: IocFunc({ Handler })(function () {
215+
return this.handler.fetch();
216+
}),
217+
},
218+
},
219+
};
220+
221+
class App extends IocClass({ api }) {}
222+
const app = context
223+
.fork()
224+
.register(new Handler())
225+
.register(new Token("secret"))
226+
.resolve(App);
227+
// @ts-expect-error
228+
app.api.network.http.token.value;
229+
expect(app.api.fs.token.value).toBe("secret");
230+
expect(app.api.network.http.token?.value).toBe("secret");
231+
expect(app.api.fs.read()).toBe(1);
232+
expect(app.api.network.http.fetch()).toBe(2);
233+
});
190234
});
191235

192236
describe("createIocClass", () => {

0 commit comments

Comments
 (0)