-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(exception): add exception filter implemention (#209)
* feat(exception): add exception filter implemention * feat(exception): error message * chore(exception): error message typo * test(exception): add invalid filter case * feat(exception): exception filterr default duplicated check
- Loading branch information
1 parent
f7e615b
commit 274cd6f
Showing
22 changed files
with
417 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export const EXCEPTION_FILTER_METADATA_KEY = 'exception_filter_meta'; | ||
export const EXCEPTION_FILTER_MAP_INJECT_ID = Symbol.for('exception_filter_map'); | ||
export const EXCEPTION_FILTER_DEFAULT_SYMBOL = Symbol.for('exception_filter_default'); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { Constructable, Injectable } from '@artus/injection'; | ||
import { HOOK_FILE_LOADER } from '../constant'; | ||
import { EXCEPTION_FILTER_DEFAULT_SYMBOL, EXCEPTION_FILTER_METADATA_KEY } from './constant'; | ||
|
||
export const Catch = (targetErr?: string|Constructable<Error>): ClassDecorator => { | ||
return (target: Function) => { | ||
Reflect.defineMetadata(EXCEPTION_FILTER_METADATA_KEY, { | ||
targetErr: targetErr ?? EXCEPTION_FILTER_DEFAULT_SYMBOL, | ||
}, target); | ||
Reflect.defineMetadata(HOOK_FILE_LOADER, { loader: 'exception_filter' }, target); | ||
Injectable()(target); | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,5 @@ | ||
export * from './constant'; | ||
export * from './decorator'; | ||
export * from './impl'; | ||
export * from './types'; | ||
export * from './utils'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,14 @@ | ||
import { Constructable } from '@artus/injection'; | ||
import { ArtusStdError } from './impl'; | ||
|
||
export interface ExceptionItem { | ||
desc: string | Record<string, string>; | ||
detailUrl?: string; | ||
} | ||
} | ||
|
||
export type ExceptionIdentifier = string|symbol|Constructable<Error>; | ||
export type ExceptionFilterMapType = Map<ExceptionIdentifier, Constructable<ExceptionFilterType>>; | ||
|
||
export interface ExceptionFilterType { | ||
catch(err: Error|ArtusStdError): void | Promise<void>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
|
||
import { Constructable, Container } from '@artus/injection'; | ||
import { EXCEPTION_FILTER_DEFAULT_SYMBOL, EXCEPTION_FILTER_MAP_INJECT_ID } from './constant'; | ||
import { ArtusStdError } from './impl'; | ||
import { ExceptionFilterMapType, ExceptionFilterType } from './types'; | ||
|
||
export const matchExceptionFilter = (err: Error, container: Container): ExceptionFilterType | null => { | ||
const filterMap: ExceptionFilterMapType = container.get(EXCEPTION_FILTER_MAP_INJECT_ID, { | ||
noThrow: true, | ||
}); | ||
if (!filterMap) { | ||
return null; | ||
} | ||
let targetFilterClazz: Constructable<ExceptionFilterType>; | ||
// handle ArtusStdError with code simply | ||
if (err instanceof ArtusStdError) { | ||
targetFilterClazz = filterMap.get(err.code); | ||
} | ||
if (!targetFilterClazz) { | ||
// handler other Exception by Clazz | ||
for (const errorClazz of filterMap.keys()) { | ||
if (typeof errorClazz === 'string' || typeof errorClazz === 'symbol') { | ||
continue; | ||
} | ||
if (err instanceof errorClazz) { | ||
targetFilterClazz = filterMap.get(errorClazz); | ||
break; | ||
} | ||
} | ||
} | ||
if (!targetFilterClazz && filterMap.has(EXCEPTION_FILTER_DEFAULT_SYMBOL)) { | ||
// handle default ExceptionFilter | ||
targetFilterClazz = filterMap.get(EXCEPTION_FILTER_DEFAULT_SYMBOL); | ||
} | ||
|
||
// return the instance of exception filter | ||
return targetFilterClazz ? container.get(targetFilterClazz) : null; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import { DefineLoader } from '../decorator'; | ||
import { ManifestItem } from '../types'; | ||
import ModuleLoader from './module'; | ||
import { ArtusStdError, EXCEPTION_FILTER_DEFAULT_SYMBOL, EXCEPTION_FILTER_MAP_INJECT_ID, EXCEPTION_FILTER_METADATA_KEY } from '../../exception'; | ||
import { Constructable } from '@artus/injection'; | ||
import { ExceptionFilterMapType, ExceptionFilterType, ExceptionIdentifier } from '../../exception/types'; | ||
|
||
@DefineLoader('exception_filter') | ||
class ExceptionFilterLoader extends ModuleLoader { | ||
async load(item: ManifestItem) { | ||
// Get or Init Exception Filter Map | ||
let filterMap: ExceptionFilterMapType = this.container.get(EXCEPTION_FILTER_MAP_INJECT_ID, { | ||
noThrow: true, | ||
}); | ||
if (!filterMap) { | ||
filterMap = new Map(); | ||
this.container.set({ | ||
id: EXCEPTION_FILTER_MAP_INJECT_ID, | ||
value: filterMap, | ||
}); | ||
} | ||
|
||
const clazzList = await super.load(item) as Constructable<ExceptionFilterType>[]; | ||
for (let i = 0; i < clazzList.length; i++) { | ||
const filterClazz = clazzList[i]; | ||
const filterMeta: { | ||
targetErr: ExceptionIdentifier | ||
} = Reflect.getOwnMetadata(EXCEPTION_FILTER_METADATA_KEY, filterClazz); | ||
|
||
if (!filterMeta) { | ||
throw new Error(`invalid ExceptionFilter ${filterClazz.name}`); | ||
} | ||
|
||
let { targetErr } = filterMeta; | ||
if (filterMap.has(targetErr)) { | ||
// check duplicated | ||
if (targetErr === EXCEPTION_FILTER_DEFAULT_SYMBOL) { | ||
throw new Error('the Default ExceptionFilter is duplicated'); | ||
} | ||
let targetErrName = targetErr; | ||
if (typeof targetErr !== 'string' && typeof targetErr !== 'symbol') { | ||
targetErrName = targetErr.name || targetErr; | ||
} | ||
throw new Error(`the ExceptionFilter for ${String(targetErrName)} is duplicated`); | ||
} | ||
|
||
if ( | ||
typeof targetErr !== 'string' && typeof targetErr !== 'symbol' && // Decorate with a error class | ||
Object.prototype.isPrototypeOf.call(ArtusStdError.prototype, targetErr.prototype) && // the class extends ArtusStdError | ||
typeof targetErr['code'] === 'string' // Have static property `code` defined by string | ||
) { | ||
// Custom Exception Class use Error Code for simplied match | ||
targetErr = targetErr['code'] as string; | ||
} | ||
|
||
filterMap.set(targetErr, filterClazz); | ||
} | ||
return clazzList; | ||
} | ||
} | ||
|
||
export default ExceptionFilterLoader; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
import 'reflect-metadata'; | ||
import { ArtusApplication, ArtusStdError, Trigger } from '../src'; | ||
import { Input } from '@artus/pipeline'; | ||
|
||
describe('test/exception_filter.test.ts', () => { | ||
it('a standard exception catch logic with no filter', async () => { | ||
try { | ||
const app = new ArtusApplication(); | ||
const trigger = app.container.get(Trigger); | ||
trigger.use(() => { | ||
throw new ArtusStdError('TEST'); | ||
}); | ||
const ctx = await trigger.initContext(); | ||
try { | ||
await trigger.startPipeline(ctx); | ||
} catch (error) { | ||
expect(error).toBeInstanceOf(ArtusStdError); | ||
} | ||
} catch (error) { | ||
throw error; | ||
} | ||
}); | ||
it('exception should pass their filter', async () => { | ||
try { | ||
const { | ||
main, | ||
} = await import('./fixtures/exception_filter/bootstrap'); | ||
|
||
const app = await main(); | ||
const trigger = app.container.get(Trigger); | ||
const mockSet: Set<string> = app.container.get('mock_exception_set'); | ||
for (const [inputTarget, exceptedVal] of [ | ||
['default', 'Error'], | ||
['custom', 'TestCustomError'], | ||
['wrapped', 'APP:WRAPPED_ERROR'], | ||
['APP:TEST_ERROR', 'APP:TEST_ERROR'], | ||
]) { | ||
const input = new Input(); | ||
input.params = { | ||
target: inputTarget, | ||
}; | ||
const ctx = await trigger.initContext(input); | ||
try { | ||
await trigger.startPipeline(ctx); | ||
} catch (error) {} | ||
expect(mockSet.has(exceptedVal)).toBeTruthy(); | ||
} | ||
} catch (error) { | ||
throw error; | ||
} | ||
}); | ||
it('should throw error then filter is invalid', async () => { | ||
try { | ||
const { | ||
main, | ||
} = await import('./fixtures/exception_invalid_filter/bootstrap'); | ||
|
||
expect(() => main()).rejects.toThrow(new Error(`invalid ExceptionFilter TestInvalidFilter`)); | ||
} catch (error) { | ||
throw error; | ||
} | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
import { Context } from '@artus/pipeline'; | ||
import path from 'path'; | ||
import { ArtusApplication, ArtusStdError, Trigger } from '../../../src'; | ||
import { TestCustomError, TestWrappedError } from './error'; | ||
|
||
async function main() { | ||
const app = new ArtusApplication(); | ||
app.container.set({ | ||
id: 'mock_exception_set', | ||
value: new Set(), | ||
}); | ||
await app.load({ | ||
items: [ | ||
{ | ||
path: path.resolve(__dirname, './filter'), | ||
extname: '.ts', | ||
filename: 'filter.ts', | ||
loader: 'exception_filter', | ||
loaderState: { | ||
exportNames: [ | ||
'TestDefaultExceptionHandler', | ||
'TestAppCodeExceptionHandler', | ||
'TestWrappedExceptionHandler', | ||
'TestCustomExceptionHandler', | ||
], | ||
}, | ||
source: 'app', | ||
}, | ||
{ | ||
path: path.resolve(__dirname, '../../../exception.json'), | ||
extname: '.json', | ||
filename: 'exception.json', | ||
loader: 'exception', | ||
source: 'app', | ||
}, | ||
{ | ||
path: path.resolve(__dirname, './exception.json'), | ||
extname: '.json', | ||
filename: 'exception.json', | ||
loader: 'exception', | ||
source: 'app', | ||
}, | ||
], | ||
}); | ||
const trigger = app.container.get(Trigger); | ||
trigger.use((ctx: Context) => { | ||
const target = ctx.input.params.target; | ||
switch(target) { | ||
case 'default': | ||
throw new Error('default error'); | ||
case 'custom': | ||
throw new TestCustomError(); | ||
case 'wrapped': | ||
throw new TestWrappedError(); | ||
default: | ||
throw new ArtusStdError(target); | ||
} | ||
}); | ||
await app.run(); | ||
return app; | ||
} | ||
|
||
export { | ||
main, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import { ArtusStdError } from '../../../src'; | ||
|
||
export class TestWrappedError extends ArtusStdError { | ||
static code = 'APP:WRAPPED_ERROR'; | ||
name = 'TestWrappedError'; | ||
|
||
constructor() { | ||
super(TestWrappedError.code); | ||
} | ||
} | ||
|
||
export class TestCustomError extends Error { | ||
name = 'TestCustomError'; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
{ | ||
"APP:TEST_ERROR": { | ||
"desc": "这是一个测试用的错误", | ||
"detailUrl": "https://github.com/artusjs" | ||
}, | ||
"APP:WRAPPED_ERROR": { | ||
"desc": "这个错误将会被自定义类包装" | ||
} | ||
} |
Oops, something went wrong.