Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add decorator feature and tests (#71)
* Add decorator feature and tests * Avoid warning * Remove unwanted change
- Loading branch information
Showing
6 changed files
with
234 additions
and
2 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 |
---|---|---|
|
@@ -43,5 +43,7 @@ | |
}, | ||
"testEnvironment": "node" | ||
}, | ||
"dependencies": {} | ||
"dependencies": { | ||
"reflect-metadata": "^0.1.12" | ||
} | ||
} |
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,125 @@ | ||
import { check, checked, String } from '.'; | ||
|
||
describe('Decorators', () => { | ||
describe('@checked', () => { | ||
describe('parameter length', () => { | ||
it('works', () => { | ||
expect(() => { | ||
class Class { | ||
@checked(String) | ||
static method(s: string) { | ||
return s; | ||
} | ||
} | ||
return Class; | ||
}).not.toThrow(); | ||
expect(() => { | ||
class Class { | ||
@checked(String, String) | ||
static method(s: string) { | ||
return s; | ||
} | ||
} | ||
return Class; | ||
}).toThrow(); | ||
expect(() => { | ||
class Class { | ||
@checked() | ||
static method(s: string) { | ||
return s; | ||
} | ||
} | ||
return Class; | ||
}).toThrow(); | ||
expect(() => { | ||
class Class { | ||
@checked(String, String) | ||
static method(s: string = 'initial', t: string = 'initial') { | ||
return { s, t }; | ||
} | ||
} | ||
return Class; | ||
}).not.toThrow(); | ||
}); | ||
}); | ||
describe('parameter check', () => { | ||
it('works', () => { | ||
class Class { | ||
@checked(String) | ||
static method1(s: string) { | ||
return s; | ||
} | ||
@checked(String.withConstraint(s => /^world$/.test(s))) | ||
static method2(s: string) { | ||
return s; | ||
} | ||
} | ||
expect(() => { | ||
Class.method1('hello'); | ||
}).not.toThrow(); | ||
expect(() => { | ||
Class.method2('hello'); | ||
}).toThrow(/Failed constraint check/); | ||
expect(() => { | ||
Class.method2('world'); | ||
}).not.toThrow(); | ||
}); | ||
}); | ||
}); | ||
describe('@check', () => { | ||
describe('parameter length', () => { | ||
it('works', () => { | ||
expect(() => { | ||
class Class { | ||
@checked(String) | ||
static method(@check s: string) { | ||
return s; | ||
} | ||
} | ||
return Class; | ||
}).not.toThrow(); | ||
expect(() => { | ||
class Class { | ||
@checked(String, String) | ||
static method(@check s: string, t: string) { | ||
return { s, t }; | ||
} | ||
} | ||
return Class; | ||
}).toThrow(); | ||
expect(() => { | ||
class Class { | ||
@checked(String) | ||
static method(@check s: string, t: string) { | ||
return { s, t }; | ||
} | ||
} | ||
return Class; | ||
}).not.toThrow(); | ||
}); | ||
}); | ||
describe('parameter check', () => { | ||
it('works', () => { | ||
class Class { | ||
@checked(String) | ||
static method1(@check s: string) { | ||
return s; | ||
} | ||
@checked(String.withConstraint(s => /^world$/.test(s))) | ||
static method2(s: string, @check t: string) { | ||
return { s, t }; | ||
} | ||
} | ||
expect(() => { | ||
Class.method1('hello'); | ||
}).not.toThrow(); | ||
expect(() => { | ||
Class.method2('world', 'hello'); | ||
}).toThrow(/Failed constraint check/); | ||
expect(() => { | ||
Class.method2('hello', 'world'); | ||
}).not.toThrow(); | ||
}); | ||
}); | ||
}); | ||
}); |
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,98 @@ | ||
import { Runtype, ValidationError } from './runtype'; | ||
|
||
import 'reflect-metadata'; | ||
|
||
const CHECKED_PARAMETER_INDICES = Symbol.for('runtypes:checked-parameter-indices'); | ||
|
||
/** | ||
* A parameter decorator. Explicitly mark the parameter as checked on every method call in combination with `@checked` method decorator. The number of `@check` params must be the same as the number of provided runtypes into `@checked`.\ | ||
* Usage: | ||
* ```ts | ||
* @checked(Runtype1, Runtype3) | ||
* method(@check p1: Runtype1, p2: number, @check p3: Runtype3) { ... } | ||
* ``` | ||
*/ | ||
export function check(target: any, propertyKey: string | symbol, parameterIndex: number) { | ||
const existingValidParameterIndices: number[] = | ||
Reflect.getOwnMetadata(CHECKED_PARAMETER_INDICES, target, propertyKey) || []; | ||
existingValidParameterIndices.push(parameterIndex); | ||
Reflect.defineMetadata( | ||
CHECKED_PARAMETER_INDICES, | ||
existingValidParameterIndices, | ||
target, | ||
propertyKey, | ||
); | ||
} | ||
|
||
/** | ||
* A method decorator. Takes runtypes as arguments which correspond to the ones of the actual method. | ||
* | ||
* Usually, the number of provided runtypes must be _**the same as**_ or _**less than**_ the actual parameters. | ||
* | ||
* If you explicitly mark which parameter shall be checked using `@check` parameter decorator, the number of `@check` parameters must be _**the same as**_ the runtypes provided into `@checked`. | ||
* | ||
* Usage: | ||
* ```ts | ||
* @checked(Runtype1, Runtype2) | ||
* method1(param1: Static1, param2: Static2, param3: any) { | ||
* ... | ||
* } | ||
* | ||
* @checked(Runtype1, Runtype3) | ||
* method2(@check param1: Static1, param2: any, @check param3: Static3) { | ||
* ... | ||
* } | ||
* ``` | ||
*/ | ||
export function checked(...runtypes: Runtype[]) { | ||
if (runtypes.length === 0) { | ||
throw new Error('No runtype provided to `@checked`. Please remove the decorator.'); | ||
} | ||
return (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) => { | ||
const method: Function = descriptor.value!; | ||
const className = target.name || target.constructor.name; | ||
const methodId = | ||
className + | ||
(target.name ? '' : '.prototype') + | ||
(typeof propertyKey === 'string' ? `["${propertyKey}"]` : `[${String(propertyKey)}]`); | ||
const validParameterIndices: number[] | undefined = Reflect.getOwnMetadata( | ||
CHECKED_PARAMETER_INDICES, | ||
target, | ||
propertyKey, | ||
); | ||
if (validParameterIndices) { | ||
// if used with `@check` parameter decorator | ||
if (runtypes.length === validParameterIndices.length) { | ||
descriptor.value = function(...args: any[]) { | ||
runtypes.forEach((type, typeIndex) => { | ||
const parameterIndex = validParameterIndices[typeIndex]; | ||
try { | ||
type.check(args[parameterIndex]); | ||
} catch (err) { | ||
throw new ValidationError(`${methodId}, argument #${parameterIndex}: ${err.message}`); | ||
} | ||
}); | ||
|
||
return method.apply(this, args); | ||
}; | ||
} else { | ||
throw new Error('Number of `@checked` runtypes and @valid parameters not matched.'); | ||
} | ||
} else { | ||
// if used without `@check` parameter decorator | ||
if (runtypes.length <= method.length) { | ||
descriptor.value = function(...args: any[]) { | ||
runtypes.forEach((type, typeIndex) => { | ||
try { | ||
type.check(args[typeIndex]); | ||
} catch (err) { | ||
throw new ValidationError(`${methodId}, argument #${typeIndex}: ${err.message}`); | ||
} | ||
}); | ||
}; | ||
} else { | ||
throw new Error('Number of `@checked` runtypes exceeds actual parameter length.'); | ||
} | ||
} | ||
}; | ||
} |
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