Skip to content

Commit

Permalink
feat: test
Browse files Browse the repository at this point in the history
  • Loading branch information
nailiable committed Nov 1, 2024
1 parent f820373 commit 1f99166
Show file tree
Hide file tree
Showing 26 changed files with 615 additions and 51 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
node_modules
dist
coverage
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"cSpell.words": [
"antfu",
"Autowired",
"bumpp",
"Containerable",
"Naily",
Expand Down
1 change: 1 addition & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ export default antfu({
'antfu/curly': 'off',
'antfu/if-newline': 'off',
'ts/method-signature-style': 'off',
'ts/no-wrapper-object-types': 'off',
},
})
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@
"build:ioc": "pnpm -F @nailyjs/ioc build",
"lint": "eslint .",
"postinstall": "npx simple-git-hooks",
"test": "vitest"
"test": "vitest --ui --coverage"
},
"devDependencies": {
"@antfu/eslint-config": "^3.7.3",
"@swc/core": "^1.7.42",
"@types/js-yaml": "^4.0.9",
"@types/node": "^22.7.5",
"@vitest/ui": "2.1.4",
"bumpp": "^9.7.1",
"eslint": "^9.12.0",
"js-yaml": "^4.1.0",
Expand All @@ -27,6 +28,7 @@
"tsup": "^8.3.0",
"tsx": "^4.19.1",
"typescript": "^5.6.3",
"unplugin-swc": "^1.5.1",
"vitest": "^2.1.2"
},
"simple-git-hooks": {
Expand Down
3 changes: 3 additions & 0 deletions packages/ioc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
"publishConfig": {
"access": "public"
},
"files": [
"dist"
],
"scripts": {
"build": "tsup",
"watch": "tsup -w"
Expand Down
1 change: 1 addition & 0 deletions packages/ioc/src/constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ import 'reflect-metadata'
export const InjectableWatermark = '__naily_injectable__'
export const InjectWatermark = '__naily_inject__'
export const FilterWatermark = '__naily_filter__'
export const PostConstructWatermark = '__naily_post_construct__'
5 changes: 4 additions & 1 deletion packages/ioc/src/container.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import type { Saveable } from './protocols'
import type { Class, InjectionToken } from './types'
import { Injectable } from './decorators'
import { ClassWrapper } from './wrappers/class-wrapper'
import { ConstantWrapper } from './wrappers/constant-wrapper'

@Injectable()
export class Container implements Saveable {
private static map = new Map<InjectionToken, ClassWrapper | ConstantWrapper>()

Expand All @@ -22,7 +24,8 @@ export class Container implements Saveable {
return new ConstantWrapper(injectionToken, value)
}

save(wrapper: ClassWrapper | ConstantWrapper): void {
save(wrapper: ClassWrapper | ConstantWrapper): this {
Container.map.set(wrapper.getInjectionToken(), wrapper)
return this
}
}
8 changes: 8 additions & 0 deletions packages/ioc/src/decorators/constant.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { InjectionToken } from '../types'
import { Container } from '../container'

export function Constant<Value>(token: InjectionToken, value: Value): PropertyDecorator & ClassDecorator & MethodDecorator {
return (() => {
new Container().createConstantWrapper(token, value).save()
}) as ClassDecorator & PropertyDecorator
}
4 changes: 4 additions & 0 deletions packages/ioc/src/decorators/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './constant.decorator'
export * from './inject.decorator'
export * from './injectable.decorator'
export * from './post-construct.decorator'
40 changes: 28 additions & 12 deletions packages/ioc/src/decorators/inject.decorator.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,43 @@
import type { InjectOptions } from '../types'
import type { InjectionToken, InjectOptions } from '../types'
import { InjectWatermark } from '../constant'

export function Inject(options: Partial<Omit<InjectOptions, 'parameterIndex' | 'propertyKey'>> = {}): ParameterDecorator & PropertyDecorator {
return ((target, propertyKey, parameterIndex) => {
Reflect.defineMetadata(InjectWatermark, [
...(Reflect.getMetadata(InjectWatermark, typeof parameterIndex === 'number' ? target : target.constructor) || []),
const oldMetadata: InjectOptions[] = Reflect.getMetadata(InjectWatermark, typeof parameterIndex === 'number' ? target : target.constructor) || []
const equal = (injectOptions: InjectOptions): boolean => injectOptions.parameterIndex === parameterIndex && injectOptions.propertyKey === propertyKey
const injectOptions = oldMetadata.find(equal)
if (!injectOptions) return Reflect.defineMetadata(InjectWatermark, [
...oldMetadata,
{
optional: false,
...options,
parameterIndex: typeof parameterIndex === 'number' ? parameterIndex : undefined,
propertyKey: typeof parameterIndex === 'number' ? undefined : propertyKey,
parameterIndex,
propertyKey,
},
] as InjectOptions[], typeof parameterIndex === 'number' ? target.constructor : target)
] as InjectOptions[], typeof parameterIndex === 'number' ? target : target.constructor)

Reflect.defineMetadata(InjectWatermark, oldMetadata.map((injectOptions) => {
return equal(injectOptions)
? {
...injectOptions,
...options,
}
: injectOptions
}), typeof parameterIndex === 'number' ? target : target.constructor)
}) as ParameterDecorator & PropertyDecorator
}

export function Autowired(injectionToken?: InjectionToken, options: Partial<Omit<InjectOptions, 'parameterIndex' | 'propertyKey' | 'injectionToken'>> = {}): ParameterDecorator & PropertyDecorator {
return Inject({ ...options, injectionToken })
}

export function Optional(): ParameterDecorator & PropertyDecorator {
return ((target, propertyKey, parameterIndex) => {
const metadata: InjectOptions[] = Reflect.getMetadata(InjectWatermark, typeof parameterIndex === 'number' ? target : target.constructor) || []
const injectOptions = metadata.find((injectOptions) => {
if (typeof parameterIndex === 'number') return injectOptions.parameterIndex === parameterIndex && injectOptions.propertyKey === propertyKey
return injectOptions.propertyKey === propertyKey
})
if (!injectOptions) return Inject({ optional: true })(target, propertyKey, parameterIndex)
else return Inject({ ...injectOptions, optional: true })(target, propertyKey, parameterIndex)
const injectOptions = metadata.find(injectOptions =>
(injectOptions.parameterIndex === parameterIndex) && (injectOptions.propertyKey === propertyKey),
)

return Inject({ ...(injectOptions || {}), optional: true })(target, propertyKey, parameterIndex)
}) as ParameterDecorator & PropertyDecorator
}
10 changes: 6 additions & 4 deletions packages/ioc/src/decorators/injectable.decorator.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import type { InjectableOptions } from '../types'
import { InjectWatermark } from '../constant'
import type { Class, InjectableOptions } from '../types'
import { InjectableWatermark } from '../constant'
import { Container } from '../container'

export function Injectable(options: Partial<InjectableOptions> = {}): ClassDecorator {
return ((target) => {
Reflect.defineMetadata(InjectWatermark, options || {}, target)
return ((target: Class) => {
Reflect.defineMetadata(InjectableWatermark, options || {}, target)
new Container().createClassWrapper(target).save()
}) as ClassDecorator
}
14 changes: 14 additions & 0 deletions packages/ioc/src/decorators/post-construct.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { CallType, PostConstructMetadata } from '../types'
import { PostConstructWatermark } from '../constant'

export function PostConstruct(callType: CallType = 'parallel'): MethodDecorator {
return ((target: Object, propertyKey: string | symbol) => {
Reflect.defineMetadata(PostConstructWatermark, [
...(Reflect.getMetadata(PostConstructWatermark, target) || []),
{
propertyKey,
callType,
},
] as PostConstructMetadata[], target.constructor)
}) as MethodDecorator
}
1 change: 1 addition & 0 deletions packages/ioc/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './constant'
export * from './container'
export * from './decorators'
export * from './metadata-scanner'
export * from './protocols'
export * from './types'
Expand Down
4 changes: 4 additions & 0 deletions packages/ioc/src/metadata-scanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,8 @@ export class MetadataScanner {
getMethodParamTypes(propertyKey: string | symbol): any[] {
return this.getClassWrapper().getPropertyMetadata('design:paramtypes', propertyKey) || []
}

getPropertyType(propertyKey: string | symbol): any {
return this.getClassWrapper().getPropertyMetadata('design:type', propertyKey, true)
}
}
2 changes: 1 addition & 1 deletion packages/ioc/src/protocols.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { InjectionToken } from './types'
import type { ClassWrapper } from './wrappers/class-wrapper'

export interface Saveable {
save(...args: any[]): void
save(...args: any[]): void | this
}
export interface GetInjectionTokenable {
getInjectionToken(): InjectionToken | undefined
Expand Down
25 changes: 25 additions & 0 deletions packages/ioc/src/task-runner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export class TaskRunner {
async runTasksSequentially(tasks: Array<() => any>): Promise<void> {
for (const task of tasks) {
try {
await task()
}
catch (error) {
return error as any
}
}
}

async runTasksInParallel(tasks: Array<() => any>): Promise<void> {
const taskPromises = tasks.map(async (task) => {
try {
await task()
}
catch (error) {
return error
}
})

await Promise.allSettled(taskPromises)
}
}
5 changes: 5 additions & 0 deletions packages/ioc/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,8 @@ export interface InjectOptions {
parameterIndex: number | undefined
propertyKey: string | symbol | undefined
}
export type CallType = 'parallel' | 'series'
export interface PostConstructMetadata {
propertyKey: string | symbol
callType: CallType
}
53 changes: 45 additions & 8 deletions packages/ioc/src/wrappers/class-wrapper.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import type { ContainerWrapper } from '../protocols'
import type { Class, InjectionToken } from '../types'
import type { Class, InjectionToken, PostConstructMetadata } from '../types'
import { PostConstructWatermark } from '../constant'
import { Container } from '../container'
import { MetadataScanner } from '../metadata-scanner'
import { TaskRunner } from '../task-runner'
import { InjectableFactory } from './injectable-factory'

export class ClassWrapper<Instance = any> implements ContainerWrapper {
Expand Down Expand Up @@ -39,6 +41,19 @@ export class ClassWrapper<Instance = any> implements ContainerWrapper {
return this._injectableFactory
}

private _taskRunner: TaskRunner | null = null
/**
* ### Get task runner.
*
* @description Get task runner for the class.
* @param cache - if false, it will create a new instance of task runner. Default is `true`.
*/
getTaskRunner(cache: boolean = true): TaskRunner {
if (this._taskRunner && cache) return this._taskRunner
this._taskRunner = new TaskRunner()
return this._taskRunner
}

getInjectionToken(): InjectionToken {
return this.getMetadataScanner()
.getInjectableMetadata()
Expand All @@ -48,6 +63,27 @@ export class ClassWrapper<Instance = any> implements ContainerWrapper {
private singletonInstance: null | Instance = null

setSingletonInstance(instance: Instance): void {
const taskRunner = this.getTaskRunner()
const tasks: PostConstructMetadata[] = this.getMetadata(PostConstructWatermark) || []
// 这是全部一起开始执行的并行任务list
const parallelTasks = tasks.filter(({ callType }) => callType === 'parallel')
// 这是上一个任务执行完后下一个任务才会开始的串行任务list
const seriesTasks = tasks.filter(({ callType }) => callType === 'series')

// 串行任务
taskRunner.runTasksSequentially(
seriesTasks
.filter(({ propertyKey }) => typeof (instance as Record<string | symbol, any>)[propertyKey] === 'function')
.map(({ propertyKey }) => () => (instance as Record<string | symbol, any>)[propertyKey]()),
)

// 并行任务
taskRunner.runTasksInParallel(
parallelTasks
.filter(({ propertyKey }) => typeof (instance as Record<string | symbol, any>)[propertyKey] === 'function')
.map(({ propertyKey }) => () => (instance as Record<string | symbol, any>)[propertyKey]()),
)

this.singletonInstance = instance
}

Expand All @@ -67,12 +103,12 @@ export class ClassWrapper<Instance = any> implements ContainerWrapper {
return Reflect.getMetadata(key, this.target)
}

getPropertyMetadata<Key extends string | symbol = string | symbol>(key: 'design:paramtypes', propertyKey: Key): any[] | undefined
getPropertyMetadata<Key extends string | symbol = string | symbol>(key: 'design:type', propertyKey: Key): any | undefined
getPropertyMetadata<Key extends string | symbol = string | symbol>(key: 'design:returntype', propertyKey: Key): any | undefined
getPropertyMetadata<Value = any, Key extends string | symbol = string | symbol>(key: Key, propertyKey: Key): Value | undefined
getPropertyMetadata<Value, Key extends string | symbol = string | symbol>(key: string, propertyKey: Key): Value | undefined {
return Reflect.getMetadata(key, this.target, propertyKey)
getPropertyMetadata<Key extends string | symbol = string | symbol>(key: 'design:paramtypes', propertyKey: Key, prototype?: boolean): any[] | undefined
getPropertyMetadata<Key extends string | symbol = string | symbol>(key: 'design:type', propertyKey: Key, prototype?: boolean): any | undefined
getPropertyMetadata<Key extends string | symbol = string | symbol>(key: 'design:returntype', propertyKey: Key, prototype?: boolean): any | undefined
getPropertyMetadata<Value = any, Key extends string | symbol = string | symbol>(key: Key, propertyKey: Key, prototype?: boolean): Value | undefined
getPropertyMetadata<Value, Key extends string | symbol = string | symbol>(key: string, propertyKey: Key, prototype: boolean = false): Value | undefined {
return Reflect.getMetadata(key, prototype === true ? this.target.prototype : this.target, propertyKey)
}

hasMetadata<Key extends string | symbol = string | symbol>(key: Key): boolean {
Expand All @@ -90,7 +126,8 @@ export class ClassWrapper<Instance = any> implements ContainerWrapper {
return this._classWrapperContainer
}

save(): void {
save(): this {
this.getGlobalContainer().save(this)
return this
}
}
16 changes: 13 additions & 3 deletions packages/ioc/src/wrappers/constant-wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import type { ContainerWrapper } from '../protocols'
import type { InjectionToken } from '../types'
import { Container } from '../container'

export class ConstantWrapper implements ContainerWrapper {
constructor(private readonly token: InjectionToken, private readonly value: any) {}
export class ConstantWrapper<Value = any> implements ContainerWrapper {
constructor(private readonly token: InjectionToken, private value: Value) {}

wrapperType = 'constant' as const

Expand All @@ -15,7 +15,17 @@ export class ConstantWrapper implements ContainerWrapper {
return new Container()
}

save(): void {
getValue(): Value {
return this.value
}

setValue(value: any): this {
this.value = value
return this
}

save(): this {
new Container().save(this)
return this
}
}
Loading

0 comments on commit 1f99166

Please sign in to comment.