Compare commits

...

33 Commits

Author SHA1 Message Date
8bb153fe54 version: Pre release version bump to v0.1.0-.1 2024-08-24 03:22:58 +02:00
Max P.
794c1e07c3 Merge branch 'main' into dev/v1.0.0 2024-08-24 03:16:29 +02:00
2942e15fcf docs: Reflect changes to changelog 2024-08-24 03:13:56 +02:00
1d94b33542 ci: Add a build option for pre release buils from dev/* branches 2024-08-24 03:13:56 +02:00
08d58b2d41 build: Add new complete prepare deploy command 2024-08-24 03:13:56 +02:00
1a67d3f4e3 docs: Reflect curent changes to the changelog 2024-08-24 02:40:21 +02:00
042d918d00 test: Add tests to reflect the changes in the signature of the decorators
The identify is no optional with fallback to the property key or class
2024-08-24 02:34:21 +02:00
06ae6737fd feat!: Update Inject Decorator for stable decorator api of typescript
Update the function and refactor the typing to reflect the property type
2024-08-24 02:32:58 +02:00
81873f3689 feat!: Update RegisterInstance Decorator for stable decorator api of typescript 2024-08-24 02:21:50 +02:00
4aa12321e8 feat!: Update Register Decorator for stable decorator api of typescript 2024-08-24 02:18:16 +02:00
26b35d1e5b refactor: Add error class for missing identifiers 2024-08-24 02:16:02 +02:00
78564b9a76 refactor!: Disable experimentalDecorators and emitDecoratorMetadata in tsconfig 2024-08-24 02:02:41 +02:00
19f7be1e3d ci: Disable branch validation workflow 2024-08-23 21:34:18 +02:00
Max P.
a490ea980a Revert "feat: Update Identifier documentation in TypeScript"
This reverts commit 6e6a521c1f.
2024-08-23 21:23:56 +02:00
Max P.
d69eacf9be Revert "docs: Add Identifiers Changelog entry"
This reverts commit 099c8dbfa3.
2024-08-23 21:23:56 +02:00
099c8dbfa3 docs: Add Identifiers Changelog entry 2024-08-23 19:24:47 +02:00
6e6a521c1f feat: Update Identifier documentation in TypeScript 2024-08-23 19:24:47 +02:00
ac99f7d306 ci: Fix release naming in workflow 2024-08-23 00:25:57 +02:00
58767b85f7 docs: Reflect Version change in Changelog 2024-08-23 00:23:56 +02:00
5e095d7b09 docs: Push version to 0.1.0 2024-08-23 00:17:14 +02:00
32126c0784 docs: Add changelog entrys 2024-08-23 00:17:14 +02:00
5df69c219e test: Add additional tests for Inject decorator 2024-08-23 00:17:14 +02:00
5bc9aef9ad feat: Add initialization error handling and refactor Inject 2024-08-23 00:17:14 +02:00
ae9f25fe94 feat: Add new Error InitializationError to reflect errors during initialization of a dependency 2024-08-23 00:17:14 +02:00
4a97a46aed docs: Added **ChangeLog** file 2024-08-23 00:17:14 +02:00
e0542214c0 docs: Add Identifiers and Jest Sections to the README.md file 2024-08-23 00:17:14 +02:00
e6e304dfc7 ci: changed version format to prefix the version wth v 2024-08-23 00:17:14 +02:00
75333b3310 Add error handling and constructor checks to Inject
- Import additional exception classes from `src/interfaces/Exceptions`
- Modify `Inject` function to:
  - Accept `init` parameter as a function or `true` for instantiation
  - Throw specific errors: `DependencyResolutionError`, `InjectorError`, `NoInstantiationMethodError`
  - Ensure necessary dependencies are handled properly
  - Define property with `Object.defineProperty` for performance
- Add `hasConstructor` helper function to check if an object has a constructor
2024-08-23 00:17:14 +02:00
ba9ec70c77 Add InjectorError and NoInstantiationMethodError classes
- Added `InjectorError` class for handling injector errors
- Added `NoInstantiationMethodError` class for missing instantiation methods
- Both classes extend `TSinjexError` and provide detailed error messages
2024-08-23 00:17:14 +02:00
6f20913e4a Update DependencyResolutionError to use Identifier type
- Import `Identifier` from `src/types/Identifier`
- Change `identifier` parameter type in `DependencyResolutionError` constructor from `string` to `Identifier`
- Update error message to call `identifier.toString()`
2024-08-23 00:17:14 +02:00
c5ea21356c Update Identifier type to include symbol
- Modified `Identifier` type in `src/types/Identifier.ts` from `string` to `string | symbol`
2024-08-23 00:17:14 +02:00
9a4fdecaac Update warning message in TSinjex class
- Converted `identifier` to string in the deprecation warning.
2024-08-23 00:17:14 +02:00
6f189942f6 Ignore test files in Istanbul coverage reporting
- Added `/* istanbul ignore file */` comment to `Decorators.spec.ts`, `Functions.spec.ts`, `ITSinjex.spec.ts`
2024-08-23 00:17:14 +02:00
15 changed files with 524 additions and 114 deletions

View File

@@ -4,6 +4,7 @@ on:
push: push:
branches: branches:
- main - main
- 'dev/*'
paths: paths:
- 'package.json' - 'package.json'
workflow_dispatch: # Allows manual execution of the workflow. workflow_dispatch: # Allows manual execution of the workflow.
@@ -22,20 +23,14 @@ jobs:
with: with:
node-version: '20.8.0' node-version: '20.8.0'
- name: Install Dependencies - name: Install dependencies, run tests and build
run: npm install run: npm run prepare:deploy
- name: Run Tests
run: npm run test:verbose
- name: Build the Project
run: npm run build:tsc
- name: Get the version - name: Get the version
id: get_version id: get_version
run: | run: |
VERSION=$(npm run version:show | tail -n 1) VERSION=$(npm run version:show | tail -n 1)
echo "VERSION=$VERSION" >> $GITHUB_ENV echo "VERSION=v$VERSION" >> $GITHUB_ENV
shell: bash shell: bash
- name: Get previous release tag - name: Get previous release tag
@@ -77,8 +72,9 @@ jobs:
echo "Generating release notes from ${{ env.PREVIOUS_TAG }} to HEAD..." echo "Generating release notes from ${{ env.PREVIOUS_TAG }} to HEAD..."
repo_url=$(git config --get remote.origin.url) repo_url=$(git config --get remote.origin.url)
notes=$(git log ${{ env.PREVIOUS_TAG }}..HEAD --pretty=format:"- [\`%h\`]($repo_url/commit/%H): %s%n") notes=$(git log ${{ env.PREVIOUS_TAG }}..HEAD --pretty=format:"- [\`%h\`]($repo_url/commit/%H): %s%n")
echo "Release notes:" echo "See [CHANGELOG.md](./CHANGELOG.md) for more details."
echo "$notes" echo "$notes"
echo ""
echo "### Changes in this release" > release_notes.md echo "### Changes in this release" > release_notes.md
echo "$notes" >> release_notes.md echo "$notes" >> release_notes.md
shell: bash shell: bash
@@ -90,17 +86,6 @@ jobs:
git config --local user.email "actions@github.com" git config --local user.email "actions@github.com"
shell: bash shell: bash
# - name: Create temporary branch
# id: create_temp_branch
# if: steps.check_version.outputs.skip_release == 'false'
# run: |
# git checkout --orphan release/v${{ env.VERSION }}
# git reset
# rm -f .gitignore
# git add README.md package.json LICENSE dist/ src/ tsconfig.json
# git commit -m "Prepare files for release ${{ env.VERSION }}"
# shell: bash
- name: Create and push tag - name: Create and push tag
id: create_tag id: create_tag
if: steps.check_version.outputs.skip_release == 'false' if: steps.check_version.outputs.skip_release == 'false'
@@ -111,6 +96,15 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash shell: bash
- name: Set Release Prerelease Flag
id: set_prerelease_flag
run: |
if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
echo "PRE_RELEASE=false" >> $GITHUB_ENV
elif [[ "${{ github.ref }}" == refs/heads/dev/* ]]; then
echo "PRE_RELEASE=true" >> $GITHUB_ENV
fi
- name: Release - name: Release
if: steps.check_version.outputs.skip_release == 'false' if: steps.check_version.outputs.skip_release == 'false'
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
@@ -118,6 +112,6 @@ jobs:
tag_name: ${{ env.VERSION }} tag_name: ${{ env.VERSION }}
name: Release ${{ env.VERSION }} name: Release ${{ env.VERSION }}
body_path: release_notes.md body_path: release_notes.md
prerelease: true prerelease: ${{ env.PRE_RELEASE }}
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -3,7 +3,7 @@ name: Validate Branch Name on Pull Request
on: on:
pull_request: pull_request:
branches: branches:
- main - UNDEFINED
jobs: jobs:
validate-branch-name-on-pull-request: validate-branch-name-on-pull-request:

62
CHANGELOG.md Normal file
View File

@@ -0,0 +1,62 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
- feat: Add new `IdentifierRequiredError` class for missing identifiers.
- feat: Add the option to use the decorators without passing the identifier: In this case, the identifier will be the class name (register) or the property name (inject).
- Add pre release building to release workflow on dev/* branches an version changes.
### Deprecated
### Removed
- feat!: Disable `experimentalDecorators` and `emitDecoratorMetadata` in the `tsconfig.json` file to reflect the change to the stable decorators api.
### Fixed
- feat!: Update `Register`, `RegisterInstance` and `Inject` decorators to reflect the change to the stable decorators api.
- feat!: Update `Inject` Decorator typing to reflect the correct property type.
### Security
## [0.0.14]
### Added
- Added **ChangeLog** file and format it according to [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- Added reference to **Semantic Versioning** in the changelog file. (History will be updated on time).
- Version format is now `v0.0.0` instead of `0.0.0`. Changes to this are also reflected in the workflos.
- Add `Identifiers` and `Jest` Sections to the `README.md` file.
- feat: Add new Error `InitializationError` to reflect errors during initialization of a dependency.
- feat: Add initialization error handling and refactor Inject.
- feat: After injecting a dependency, the lazzy loading getter will be replaced with the dependency itself.
- feat: remove the use of a private property to store the injected dependencies. Now the dependencies are stored in the property itself.
- test: Add tests for the new features.
### Deprecated
- Deprecated the old version format `0.0.0`.
### Removed
### Fixed
### Security
---
[unreleased]: https://github.com/PxaMMaxP/TSinjex/compare/0.0.14...HEAD
[0.0.14]: https://github.com/PxaMMaxP/TSinjex/compare/0.0.13...v0.0.14

View File

@@ -6,8 +6,14 @@
## Configuration ## Configuration
### Identifiers
Strings and symbols are possible for the **identifiers**.
### Jest ### Jest
For the use of TSinjex with Jest, the corresponding source files can be found under `./src` of the TSinjex node_module folder. To use these files, the `moduleNameMapper` must be configured in the Jest configuration file. The following example shows how to configure the Jest configuration file to use the source files of TSinjex.
#### Example jest setup #### Example jest setup
```ts ```ts

View File

@@ -1,6 +1,6 @@
{ {
"name": "ts-injex", "name": "ts-injex",
"version": "0.0.13", "version": "0.1.0-.1",
"description": "Simple boilerplate code free dependency injection system for TypeScript.", "description": "Simple boilerplate code free dependency injection system for TypeScript.",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",
@@ -22,7 +22,8 @@
"docs:fix:coverage": "node scripts/fix-coverage-paths.cjs", "docs:fix:coverage": "node scripts/fix-coverage-paths.cjs",
"docs:generate:badge": "node scripts/generate-badge.cjs", "docs:generate:badge": "node scripts/generate-badge.cjs",
"docs:fix:escape": "node scripts/replace-doc-escaping.cjs", "docs:fix:escape": "node scripts/replace-doc-escaping.cjs",
"version:show": "node -e \"console.log(require('./package.json').version)\"" "version:show": "node -e \"console.log(require('./package.json').version)\"",
"prepare:deploy": "npm install && npm run test:verbose && npm run build:tsc"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

@@ -1,3 +1,4 @@
/* istanbul ignore file */
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import { Inject } from 'src/decorators/Inject'; import { Inject } from 'src/decorators/Inject';
import { DependencyResolutionError } from 'src/interfaces/Exceptions'; import { DependencyResolutionError } from 'src/interfaces/Exceptions';
@@ -66,6 +67,29 @@ export function test_InjectDecorator(
expect(instance.getDependency().value).toBe('test-value-init'); expect(instance.getDependency().value).toBe('test-value-init');
}); });
it('should inject dependency and run initializer without identifier', () => {
container.register('MockDependencyIdentifier', {
value: 'test-value',
});
class TestClass {
@Inject(undefined, (x: string) => {
(x as unknown as { value: string }).value =
'test-value-init';
return x;
})
MockDependencyIdentifier!: any;
public getDependency() {
return this.MockDependencyIdentifier;
}
}
const instance = new TestClass();
expect(instance.getDependency().value).toBe('test-value-init');
});
it('should throw an error when necessary is true and the initializer throws an error', () => { it('should throw an error when necessary is true and the initializer throws an error', () => {
let _error: Error | undefined = undefined; let _error: Error | undefined = undefined;
@@ -77,7 +101,7 @@ export function test_InjectDecorator(
class TestClass { class TestClass {
@Inject( @Inject(
'InitThrowDependencie', 'InitThrowDependencie',
() => { (): any => {
throw new Error('Initializer error'); throw new Error('Initializer error');
}, },
true, true,
@@ -117,6 +141,130 @@ export function test_InjectDecorator(
} }
expect(_error).toBeInstanceOf(DependencyResolutionError); expect(_error).toBeInstanceOf(DependencyResolutionError);
}); });
it('should replace the property with the resolved dependency', () => {
container.register('MockDependencyIdentifier', {
value: 'test-value',
});
class TestClass {
@Inject('MockDependencyIdentifier')
private readonly _dependency!: any;
public getDependency() {
return this._dependency;
}
public isDependencyTypeofFunction() {
return typeof this._dependency === 'function';
}
}
const instance = new TestClass();
expect(instance.getDependency().value).toBe('test-value');
expect(instance.isDependencyTypeofFunction()).toBe(false);
expect(instance.getDependency().value).toBe('test-value');
});
it('should use a empty initializer when none is provided but true', () => {
container.register(
'MockDependencyIdentifier',
class X {
public value: string = 'test-value';
constructor() {}
},
);
class TestClass {
@Inject('MockDependencyIdentifier', true)
private readonly _dependency!: any;
public getDependency() {
return this._dependency;
}
}
const instance = new TestClass();
expect(instance.getDependency().value).toBe('test-value');
});
it('should throw an error when the dependency has no instantiation method', () => {
container.register('MockDependencyIdentifier', {
value: 'test-value',
});
class TestClass {
@Inject('MockDependencyIdentifier', true)
private readonly _dependency!: any;
public getDependency() {
return this._dependency;
}
}
expect(() => {
const instance = new TestClass();
instance.getDependency();
}).toThrow(new RegExp('No instantiation method found for.*'));
});
it('should not throw an error when the dependency has no instantiation method if not necessary', () => {
container.register('MockDependencyIdentifier', {
value: 'test-value',
});
class TestClass {
@Inject('MockDependencyIdentifier', true, false)
private readonly _dependency!: any;
public getDependency() {
return this._dependency;
}
}
expect(() => {
const instance = new TestClass();
instance.getDependency();
}).not.toThrow(new RegExp('No instantiation method found for.*'));
});
it('should throw an error when the dependency cannot be resolved', () => {
container.register('MockDependencyIdentifier', null);
class TestClass {
@Inject('MockDependencyIdentifier', true)
private readonly _dependency!: any;
public getDependency() {
return this._dependency;
}
}
expect(() => {
const instance = new TestClass();
instance.getDependency();
}).toThrow(new RegExp('.*could not be resolved.*'));
});
it('should not throw an error when the dependency cannot be resolved if not necessary', () => {
container.register('MockDependencyIdentifier', null);
class TestClass {
@Inject('MockDependencyIdentifier', true, false)
private readonly _dependency!: any;
public getDependency() {
return this._dependency;
}
}
expect(() => {
const instance = new TestClass();
instance.getDependency();
}).not.toThrow(new RegExp('.*could not be resolved.*'));
});
}); });
} }
@@ -150,6 +298,19 @@ export function test_RegisterDecorator(
TestClass, TestClass,
); );
}); });
it('should register a dependency without an identifier', () => {
@register()
class TestClass {
private readonly _dependency!: any;
public getDependency() {
return this._dependency;
}
}
expect(container.resolve('TestClass')).toBe(TestClass);
});
}); });
} }
@@ -186,6 +347,23 @@ export function test_RegisterInstanceDecorator(
).toBe('instance'); ).toBe('instance');
}); });
it('should register an instance of a dependency with an identifier', () => {
@registerInstance()
class TestClass {
private readonly _dependency!: any;
public getDependency() {
return this._dependency;
}
public mark: string = 'instance';
}
expect(container.resolve<TestClass>('TestClass').mark).toBe(
'instance',
);
});
it('should register an instance of a dependency an run the init function', () => { it('should register an instance of a dependency an run the init function', () => {
@registerInstance( @registerInstance(
'InstanceIdentifier', 'InstanceIdentifier',

View File

@@ -1,3 +1,4 @@
/* istanbul ignore file */
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import { ITSinjex, ITSinjex_ } from 'src/interfaces/ITSinjex'; import { ITSinjex, ITSinjex_ } from 'src/interfaces/ITSinjex';

View File

@@ -1,3 +1,4 @@
/* istanbul ignore file */
import { ITSinjex_, ITSinjex } from '../interfaces/ITSinjex'; import { ITSinjex_, ITSinjex } from '../interfaces/ITSinjex';
/** /**

View File

@@ -114,7 +114,7 @@ export class TSinjex implements ITSinjex {
} }
if (dependency.deprecated) { if (dependency.deprecated) {
console.warn(`Dependency ${identifier} is deprecated`); console.warn(`Dependency ${identifier.toString()} is deprecated`);
// Remove the deprecation warning; it should only be logged once. // Remove the deprecation warning; it should only be logged once.
dependency.deprecated = false; dependency.deprecated = false;

View File

@@ -1,19 +1,31 @@
import {
DependencyResolutionError,
IdentifierRequiredError,
InitializationError,
InjectorError,
NoInstantiationMethodError,
} from 'src/interfaces/Exceptions';
import { TSinjex } from '../classes/TSinjex'; import { TSinjex } from '../classes/TSinjex';
import { Identifier } from '../types/Identifier'; import { Identifier } from '../types/Identifier';
import { InitDelegate } from '../types/InitDelegate'; import { InitDelegate } from '../types/InitDelegate';
/** /**
* A decorator to inject a dependency from a DI (Dependency Injection) container into a class property. * A decorator to inject a dependency from a DI (Dependency Injection) container into a class property.
* @template T The type of the dependency to be injected. * @template TargetType The type of the class to inject the dependency into.
* @template U The type of the property to be injected. * @template DependencyType The type of the dependency to be injected.
* @param identifier The identifier used to resolve the class in the DI container. * @template PropertyType The type of the property to be injected.
* @see {@link Identifier} for more information on identifiers. * @param identifier The {@link Identifier|identifier} used to resolve the dependencie in the DI container or the property name if not provided.
* @param init An optional initializer function to transform the dependency before injection. * @param init An optional initializer {@link InitDelegate|function} to transform the dependency before injection
* @see {@link InitDelegate} for more information on initializer functions. * or true to instantiate the dependency if it has a constructor.
* @param necessary If true, throws an error if the dependency is not found. * @param necessary If true, throws an error if the dependency is not found.
* @returns The resolved dependency or undefined if the dependency is not necessary * @returns The resolved dependency or undefined if the dependency is not necessary
* and not found, or throws an error if the dependency is necessary and not found. * and not found, or throws an error if the dependency is necessary and not found.
* @throws A {@link DependencyResolutionError} if the dependency is not found and necessary. * @throws **Only throws errors if the dependency is necessary.**
* @throws An {@link IdentifierRequiredError} if the identifier is not provided and the class name is not available.
* @throws A {@link DependencyResolutionError} if the dependency is not found.
* @throws A {@link InjectorError} if an error occurs during the injection process.
* @throws A {@link NoInstantiationMethodError} if the dependency does not have a constructor.
* @throws An {@link InitializationError} if an error occurs during the initialization process.
* @example * @example
* ```ts * ```ts
* class MyClass { * class MyClass {
@@ -29,49 +41,128 @@ import { InitDelegate } from '../types/InitDelegate';
* } * }
* ``` * ```
*/ */
export function Inject<T, U>( export function Inject<TargetType, DependencyType, PropertyType>(
identifier: Identifier, identifier?: Identifier,
init?: InitDelegate<T, U>, init?: InitDelegate<DependencyType, PropertyType> | true,
necessary = true, necessary = true,
) { ) {
return function (target: unknown, propertyKey: string | symbol): void { return function (
// Unique symbol to store the private property constructor: undefined,
const privatePropertyKey: unique symbol = Symbol(); context: ClassFieldDecoratorContext<TargetType> & {
// Get the DI container instance name: PropertyType;
const diContainer = TSinjex.getInstance(); },
): void {
const _identifier = identifier ?? context.name;
// Function to evaluate the dependency lazily if (_identifier == null && necessary === true)
// to avoid circular dependencies, not found dependencies, etc. throw new IdentifierRequiredError();
const evaluate = (): T | undefined => {
return diContainer.resolve<T>(identifier, necessary); /**
* Function to evaluate the dependency lazily
* to avoid circular dependencies, not found dependencies, etc.
* @returns The resolved dependency or undefined if the dependency is not found.
*/
const resolve = (): DependencyType | undefined => {
return TSinjex.getInstance().resolve<DependencyType>(
_identifier,
necessary,
);
}; };
// Define the property context.addInitializer(function (this: TargetType) {
Object.defineProperty(target, propertyKey, { Object.defineProperty(this, context.name, {
get() { get() {
// If the property is not defined, evaluate the dependency let instance: DependencyType | PropertyType | undefined;
if (!this.hasOwnProperty(privatePropertyKey)) {
if (init) {
try {
this[privatePropertyKey] = init(evaluate() as T);
} catch (error) {
if (necessary) {
throw error;
}
}
} else {
this[privatePropertyKey] = evaluate();
}
}
return this[privatePropertyKey]; const dependency: DependencyType | undefined = tryAndCatch(
}, () => resolve(),
// Not necessary to set the property necessary,
// set(value: PropertieType) { _identifier,
// this[privatePropertyKey] = value; DependencyResolutionError,
// }, );
enumerable: true,
configurable: false, if (dependency != null) {
const initFunction: (() => PropertyType) | undefined =
typeof init === 'function' && dependency != null
? (): PropertyType => init(dependency)
: init === true && hasConstructor(dependency)
? (): PropertyType =>
new dependency() as PropertyType
: undefined;
if (init == null) instance = dependency;
else if (initFunction != null)
instance = tryAndCatch(
initFunction,
necessary,
_identifier,
InitializationError,
);
else if (necessary)
throw new NoInstantiationMethodError(_identifier);
} else if (necessary)
throw new DependencyResolutionError(_identifier);
/**
* Replace itself with the resolved dependency
* for performance reasons.
*/
Object.defineProperty(this, context.name, {
value: instance,
writable: false,
enumerable: false,
configurable: false,
});
return instance;
},
/**
* Make the property configurable to allow replacing it
*/
configurable: true,
});
}); });
}; };
} }
/**
* Tries to execute a function and catches any errors that occur.
* If the function is necessary and an error occurs, it throws the error
* with the specified error class and identifier.
* @param fn The function to execute.
* @param necessary If true, throws an error if an error occurs.
* @param identifier The identifier of the dependency.
* @param errorClass The error class to throw if an error occurs.
* @returns The result of the function or undefined if an error occurs and the function is not necessary.
*/
function tryAndCatch<ReturnType, ErrorType>(
fn: () => ReturnType,
necessary: boolean,
identifier?: Identifier,
errorClass?: ErrorType,
): ReturnType | undefined {
try {
return fn();
} catch (error) {
if (necessary)
throw new (errorClass != null ? errorClass : error)(
identifier ?? 'not specified',
error,
);
else return undefined;
}
}
/**
* Checks if an object has a constructor.
* @param obj The object to check.
* @returns True if the object has a constructor, false otherwise.
*/
function hasConstructor<T>(obj: T): obj is T & { new (): unknown } {
const _obj = obj as unknown as { prototype?: { constructor?: unknown } };
return (
_obj?.prototype != null &&
typeof _obj.prototype.constructor === 'function'
);
}

View File

@@ -1,14 +1,14 @@
import { IdentifierRequiredError } from 'src/interfaces/Exceptions';
import { TSinjex } from '../classes/TSinjex'; import { TSinjex } from '../classes/TSinjex';
import { Identifier } from '../types/Identifier'; import { Identifier } from '../types/Identifier';
/** /**
* A decorator to register a class in the **TSinjex** DI (Dependency Injection) container. * A decorator to register a class in the **TSinjex** DI (Dependency Injection) container.
* @template TargetType The type of the class to be registered. * @template TargetType The type of the class to be registered.
* @param identifier The identifier used to register the class in the DI container. * @param identifier The {@link Identifier|identifier} used to register the class in the DI container or the class name if not provided.
* @see {@link Identifier} for more information on identifiers. * @param deprecated If true, the dependency is deprecated and a warning is logged only once upon the first resolution of the dependency.
* @param deprecated If true, the dependency is deprecated and a warning
* is logged only once upon the first resolution of the dependency.
* @returns The decorator function to be applied on the class. * @returns The decorator function to be applied on the class.
* @throws An {@link IdentifierRequiredError} if the identifier is not provided and the class name is not available.
* @example * @example
* ```ts * ```ts
* \@Register('MyClassIdentifier') * \@Register('MyClassIdentifier')
@@ -19,12 +19,16 @@ import { Identifier } from '../types/Identifier';
*/ */
export function Register< export function Register<
TargetType extends new (...args: unknown[]) => InstanceType<TargetType>, TargetType extends new (...args: unknown[]) => InstanceType<TargetType>,
>(identifier: Identifier, deprecated?: boolean) { >(identifier?: Identifier, deprecated?: boolean) {
return function (constructor: TargetType, ...args: unknown[]): void { return function (
// Get the instance of the DI container constructor: TargetType,
const diContainer = TSinjex.getInstance(); context: ClassDecoratorContext<TargetType>,
) {
const _identifier = identifier ?? context.name;
// Register the class in the DI container if (_identifier == null) throw new IdentifierRequiredError();
diContainer.register(identifier, constructor, deprecated);
const diContainer = TSinjex.getInstance();
diContainer.register(_identifier, constructor, deprecated);
}; };
} }

View File

@@ -1,3 +1,4 @@
import { IdentifierRequiredError } from 'src/interfaces/Exceptions';
import { TSinjex } from '../classes/TSinjex'; import { TSinjex } from '../classes/TSinjex';
import { Identifier } from '../types/Identifier'; import { Identifier } from '../types/Identifier';
import { InitDelegate } from '../types/InitDelegate'; import { InitDelegate } from '../types/InitDelegate';
@@ -5,12 +6,11 @@ import { InitDelegate } from '../types/InitDelegate';
/** /**
* A decorator to register an instance of a class in the DI (Dependency Injection) container. * A decorator to register an instance of a class in the DI (Dependency Injection) container.
* @template TargetType The type of the class whose instance is to be registered. * @template TargetType The type of the class whose instance is to be registered.
* @param identifier The identifier used to register the instance in the DI container. * @param identifier The {@link Identifier|identifier} used to register the class in the DI container or the class name if not provided.
* @see {@link Identifier} for more information on identifiers. * @param init An optional initializer {@link InitDelegate|function} which get the constructor of the class
* @param init An optional initializer function which get the constructor of the class
* as input and returns an instance of the class. * as input and returns an instance of the class.
* @see {@link InitDelegate} for more information on initializer functions.
* @returns The decorator function to be applied on the class. * @returns The decorator function to be applied on the class.
* @throws An {@link IdentifierRequiredError} if the identifier is not provided and the class name is not available.
* @example * @example
* ```ts * ```ts
* \@RegisterInstance('MyClassInstanceIdentifier', (constructor) => new constructor()) * \@RegisterInstance('MyClassInstanceIdentifier', (constructor) => new constructor())
@@ -22,43 +22,51 @@ import { InitDelegate } from '../types/InitDelegate';
export function RegisterInstance< export function RegisterInstance<
TargetType extends new (..._args: unknown[]) => InstanceType<TargetType>, TargetType extends new (..._args: unknown[]) => InstanceType<TargetType>,
>( >(
identifier: Identifier, identifier?: Identifier,
init?: InitDelegate< init?: InitDelegate<
TargetType & { new (..._args: unknown[]): InstanceType<TargetType> }, TargetType & { new (..._args: unknown[]): InstanceType<TargetType> },
InstanceType<TargetType> InstanceType<TargetType>
>, >,
) { ) {
return function (constructor: TargetType, ...args: unknown[]): void { return function (
// Get the instance of the DI container constructor: TargetType,
context: ClassDecoratorContext<TargetType>,
): void {
const _identifier = identifier ?? context.name;
if (_identifier == null) throw new IdentifierRequiredError();
const diContainer = TSinjex.getInstance(); const diContainer = TSinjex.getInstance();
let instance: InstanceType<TargetType>; let instance: InstanceType<TargetType>;
/**
* Get the instance of the class
* and replace the lazy proxy with the instance
* for performance optimization.
*/
const getAndRegisterInstance = (): void => {
if (instance == null) {
if (init) {
instance = init(constructor);
} else {
instance = new constructor();
}
}
diContainer.register(_identifier, instance);
};
// Create a proxy to instantiate the class when needed (Lazy Initialization) // Create a proxy to instantiate the class when needed (Lazy Initialization)
let lazyProxy: unknown = new Proxy( const lazyProxy: unknown = new Proxy(
{}, {},
{ {
get(target, prop, receiver) { get(_target, prop, _receiver) {
if (instance == null) { getAndRegisterInstance();
if (init) {
instance = init(constructor);
} else {
instance = new constructor(...args);
}
}
lazyProxy = instance;
// Return the requested property of the instance // Return the requested property of the instance
return instance[prop as keyof InstanceType<TargetType>]; return instance[prop as keyof InstanceType<TargetType>];
}, },
set(target, prop, value, receiver) { set(_target, prop, value, _receiver) {
if (instance == null) { getAndRegisterInstance();
if (init) {
instance = init(constructor);
} else {
instance = new constructor(...args);
}
}
lazyProxy = instance;
// Set the requested property of the instance // Set the requested property of the instance
return (instance[prop as keyof InstanceType<TargetType>] = return (instance[prop as keyof InstanceType<TargetType>] =
@@ -67,7 +75,6 @@ export function RegisterInstance<
}, },
); );
// Register the lazy proxy in the DI container diContainer.register(_identifier, lazyProxy);
diContainer.register(identifier, lazyProxy);
}; };
} }

View File

@@ -1,3 +1,4 @@
import { Identifier } from 'src/types/Identifier';
import { ITSinjex } from './ITSinjex'; import { ITSinjex } from './ITSinjex';
/** /**
@@ -14,6 +15,19 @@ export class TSinjexError extends Error {
} }
} }
/**
* Error class for missing identifiers in {@link ITSinjex} methods.
*/
export class IdentifierRequiredError extends TSinjexError {
/**
* Creates a new instance of {@link IdentifierRequiredError}
*/
constructor() {
super('Identifier is required.');
this.name = 'TSinjexIdentifierRequiredError';
}
}
/** /**
* Error class for dependency resolution errors in {@link ITSinjex}. * Error class for dependency resolution errors in {@link ITSinjex}.
* @see {@link ITSinjex.resolve} * @see {@link ITSinjex.resolve}
@@ -23,8 +37,61 @@ export class DependencyResolutionError extends TSinjexError {
* Creates a new instance of {@link DependencyResolutionError} * Creates a new instance of {@link DependencyResolutionError}
* @param identifier **The identifier of the dependency** * @param identifier **The identifier of the dependency**
*/ */
constructor(identifier: string) { constructor(identifier: Identifier) {
super(`Dependency ${identifier} could not be resolved.`); super(`Dependency ${identifier.toString()} could not be resolved.`);
this.name = 'TSinjexResolutionError'; this.name = 'TSinjexResolutionError';
} }
} }
/**
* Error class for Injector errors in {@link ITSinjex}.
* @see {@link ITSinjex.inject}
*/
export class InjectorError extends TSinjexError {
/**
* Creates a new instance of {@link InjectorError}
* @param identifier **The identifier of the dependency**
* @param originalError **The original error that caused the injection error**
*/
constructor(identifier: Identifier, originalError?: Error) {
super(
`Error injecting dependency ${identifier.toString()} with error: "${originalError}"`,
);
this.name = 'TSinjexInjectorError';
}
}
/**
* Error class for missing instantiation methods in {@link ITSinjex}.
* @see {@link ITSinjex.inject}
*/
export class NoInstantiationMethodError extends TSinjexError {
/**
* Creates a new instance of {@link NoInstantiationMethodError}
* @param identifier **The identifier of the dependency**
*/
constructor(identifier: Identifier) {
super(
`No instantiation method found for dependency ${identifier.toString()}.`,
);
this.name = 'TSinjexNoInstantiationMethodError';
}
}
/**
* Error class for errors during the initialization of a dependency in {@link ITSinjex}.
* @see {@link ITSinjex.inject}
*/
export class InitializationError extends TSinjexError {
/**
* Creates a new instance of {@link InitializationError}
* @param identifier **The identifier of the dependency**
* @param originalError **The original error that caused the initialization error**
*/
constructor(identifier: Identifier, originalError?: Error) {
super(
`Error initializing dependency ${identifier.toString()} with error: "${originalError}"`,
);
this.name = 'TSinjexInitializationError';
}
}

View File

@@ -8,4 +8,4 @@
* I.e. a class `ClassA` that implements the interface `IClassA` and is * I.e. a class `ClassA` that implements the interface `IClassA` and is
* registered as a dependent class is registered under the interface name `IClassA`. * registered as a dependent class is registered under the interface name `IClassA`.
*/ */
export type Identifier = string; export type Identifier = string | symbol;

View File

@@ -18,8 +18,6 @@
"importHelpers": true, "importHelpers": true,
"isolatedModules": true, "isolatedModules": true,
"resolveJsonModule": true, "resolveJsonModule": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"strictNullChecks": true, "strictNullChecks": true,
"strictPropertyInitialization": true, "strictPropertyInitialization": true,
"lib": [ "lib": [