@alterior/annotations
Version:
Create and interact with Typescript metadata decorators
957 lines (813 loc) • 36.1 kB
text/typescript
/// <reference types="reflect-metadata" />
/**
* @alterior/annotations
* A class library for handling Typescript metadata decorators via "annotation" classes
*
* (C) 2017-2019 William Lahti
*
*/
import { NotSupportedError } from '@alterior/common';
/**
* Represents an annotation which could be stored in the standard annotation lists
* on a class.
*/
export interface IAnnotation {
$metadataName? : string;
}
// These are the properties on a class where annotation metadata is deposited
// when annotation decorators are executed. Note that these are intended to
// be compatible with Angular 6's model
export const ANNOTATIONS_KEY = '__annotations__';
export const CONSTRUCTOR_PARAMETERS_ANNOTATIONS_KEY = '__parameters__';
export const PROPERTY_ANNOTATIONS_KEY = '__prop__metadata__';
export const METHOD_PARAMETER_ANNOTATIONS_KEY = '__parameter__metadata__';
/**
* Represents an Annotation subclass from the perspective of using it to
* construct itself by passing an options object.
*/
interface AnnotationConstructor<AnnoT extends Annotation, TS extends any[]> {
new (...args : TS) : AnnoT;
getMetadataName();
}
export type AnnotationClassDecorator<TS extends any[]> = (...args: TS) => ((target: any) => void);
export type AnnotationPropertyDecorator<TS extends any[]> = (...args: TS) => ((target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) => void);
export type AnnotationMethodDecorator<TS extends any[]> = (...args: TS) => ((target: any, propertyKey: string | symbol) => void);
export type AnnotationParameterDecorator<TS extends any[]> = (...args: TS) => ((target: any, propertyKey: string | symbol, index: number) => void);
// (...args: TS): (target, ...args) => void;
type UnionToIntersection<U> =
(U extends any ? (x: U)=>void : never) extends ((x: infer I)=>void) ? I : never
type DecoratorTypeUnionForValidTargets<Targets> =
Targets extends 'class' ? ClassDecorator
: Targets extends 'method' ? MethodDecorator
: Targets extends 'property' ? PropertyDecorator
: Targets extends 'parameter' ? ParameterDecorator
: never;
;
type DecoratorTypeForValidTargets<Targets> = UnionToIntersection<DecoratorTypeUnionForValidTargets<Targets>>;
/**
* Represents a decorator which accepts an Annotation's options object.
*/
export type AnnotationDecorator<TS extends any[]> = (...args: TS) =>
ClassDecorator &
PropertyDecorator &
MethodDecorator &
ParameterDecorator
;
export interface DecoratorSite {
type : 'class' | 'method' | 'property' | 'parameter';
target : any;
propertyKey? : string;
propertyDescriptor? : PropertyDescriptor;
index? : number;
}
export type AnnotationDecoratorTarget = 'class' | 'property' | 'method' | 'parameter';
export interface AnnotationDecoratorOptions<AnnoT, TS extends any[] = []> {
factory? : (target : DecoratorSite, ...args : TS) => AnnoT | void;
validTargets? : AnnotationDecoratorTarget[];
allowMultiple? : boolean;
}
/**
* Thrown when a caller attempts to decorate an annotation target when the
* annotation does not support that target.
*/
export class AnnotationTargetError extends NotSupportedError {
constructor(annotationClass, invalidType: string, supportedTypes: string[], message?: string) {
super(message || `You cannot decorate a ${invalidType} with annotation ${annotationClass.name}. Valid targets: ${supportedTypes.join(', ')}`);
this._invalidType = invalidType;
this._annotationClass = annotationClass;
this._supportedTypes = supportedTypes;
}
private _invalidType : string;
private _annotationClass : Function;
private _supportedTypes : string[];
get invalidType() : string {
return this._invalidType;
}
get supportedTypes(): string[] {
return this._supportedTypes;
}
get annotationClass(): Function {
return this._annotationClass;
}
}
/**
* Create a decorator suitable for use along with an Annotation class.
* This is the core of the Annotation.decorator() method.
*
* @param ctor
* @param options
*/
function makeDecorator<AnnoT extends Annotation, TS extends any[]>(
ctor : AnnotationConstructor<AnnoT, TS>,
options? : AnnotationDecoratorOptions<AnnoT, TS>
): AnnotationDecorator<TS>
{
if (!ctor)
throw new Error(`Cannot create decorator: Passed class reference was undefined/null: This can happen due to circular dependencies.`);
let factory : (target : DecoratorSite, ...args : TS) => AnnoT | void = null;
let validTargets : string[] = null;
let allowMultiple = false;
if (options) {
if (options.factory)
factory = options.factory;
if (options.validTargets)
validTargets = options.validTargets as any;
if (options.allowMultiple)
allowMultiple = options.allowMultiple;
}
if (!factory)
factory = (target, ...args) => new ctor(...args);
if (!validTargets)
validTargets = ['class', 'method', 'property', 'parameter'];
return (...decoratorArgs : TS) => {
return (target, ...args) => {
// Note that checking the length is not enough, because for properties
// two arguments are passed, but the property descriptor is `undefined`.
// So we make sure that we have a valid property descriptor (args[1])
if (args.length === 2 && args[1] !== undefined) {
if (typeof args[1] === 'number') {
// Parameter decorator on a method or a constructor (methodName will be undefined)
let methodName : string = args[0];
let index : number = args[1];
if (!validTargets.includes('parameter'))
throw new AnnotationTargetError(ctor, 'parameter', validTargets);
if (!allowMultiple) {
let existingParamDecs = Annotations.getParameterAnnotations(target, methodName, true);
let existingParamAnnots = existingParamDecs[index] || [];
if (existingParamAnnots.find(x => x.$metadataName === ctor['$metadataName']))
throw new Error(`Annotation ${ctor.name} can only be applied to an element once.`);
}
if (methodName) {
let annotation = factory({
type: 'parameter',
target,
propertyKey: methodName,
index
}, ...decoratorArgs);
if (!annotation)
return;
annotation.applyToParameter(target, methodName, index);
} else {
let annotation = factory({
type: 'parameter',
target,
index
}, ...decoratorArgs);
if (!annotation)
return;
annotation.applyToConstructorParameter(target, index);
}
} else {
// Method decorator
let methodName : string = args[0];
let descriptor : PropertyDescriptor = args[1];
if (!validTargets.includes('method'))
throw new AnnotationTargetError(ctor, 'method', validTargets);
if (!allowMultiple) {
let existingAnnots = Annotations.getMethodAnnotations(target, methodName, true);
if (existingAnnots.find(x => x.$metadataName === ctor['$metadataName']))
throw new Error(`Annotation ${ctor.name} can only be applied to an element once.`);
}
let annotation = factory({
type: 'method',
target,
propertyKey: methodName,
propertyDescriptor: descriptor
}, ...decoratorArgs);
if (!annotation)
return;
annotation.applyToMethod(target, methodName);
}
} else if (args.length >= 1) {
// Property decorator
let propertyKey : string = args[0];
if (!validTargets.includes('property'))
throw new AnnotationTargetError(ctor, 'property', validTargets);
if (!allowMultiple) {
let existingAnnots = Annotations.getPropertyAnnotations(target, propertyKey, true);
if (existingAnnots.find(x => x.$metadataName === ctor['$metadataName']))
throw new Error(`Annotation ${ctor.name} can only be applied to an element once.`);
}
let annotation = factory({
type: 'property',
target,
propertyKey
}, ...decoratorArgs);
if (!annotation)
return;
annotation.applyToProperty(target, propertyKey);
} else if (args.length === 0) {
// Class decorator
if (!validTargets.includes('class'))
throw new AnnotationTargetError(ctor, 'class', validTargets);
if (!allowMultiple) {
let existingAnnots = Annotations.getClassAnnotations(target);
if (existingAnnots.find(x => x.$metadataName === ctor['$metadataName']))
throw new Error(`Annotation ${ctor.name} can only be applied to an element once.`);
}
let annotation = factory({
type: 'class',
target
}, ...decoratorArgs);
if (!annotation)
return;
annotation.applyToClass(target);
} else {
// Invalid, or future decorator types we don't support yet.
throw new Error(`Encountered unknown decorator invocation with ${args.length + 1} parameters.`);
}
};
}
}
export function MetadataName(name : string) {
return target => Object.defineProperty(target, '$metadataName', { value: name });
}
export interface MutatorDefinition {
invoke: (site : DecoratorSite) => void;
options?: AnnotationDecoratorOptions<void>;
}
/**
* Represents a metadata annotation which can be applied to classes,
* constructor parameters, methods, properties, or method parameters
* via decorators.
*
* Custom annotations are defined as subclasses of this class.
* By convention, all custom annotation classes should have a name
* which ends in "Annotation" such as "NameAnnotation".
*
* To create a new annotation create a subclass of `Annotation`
* with a constructor that takes the parameters you are interested in
* storing, and save the appropriate information onto fields of the
* new instance. For your convenience, Annotation provides a default
* constructor which takes a map object and applies its properties onto
* the current instance, but you may replace it with a constructor that
* takes any arguments you wish.
*
* You may wish to add type safety to the default constructor parameter.
* To do so, override the constructor and define it:
*
```
class XYZ extends Annotation {
constructor(
options : MyOptions
) {
super(options);
}
}
```
*
* Annotations are applied by using decorators.
* When you define a custom annotation, you must also define a
* custom decorator:
*
```
const Name =
NameAnnotation.decorator();
```
* You can then use that decorator:
```
@Name()
class ABC {
// ...
}
```
*
*/
export class Annotation implements IAnnotation {
constructor(
props? : any
) {
this.$metadataName = this.constructor['$metadataName'];
if (!this.$metadataName || !this.$metadataName.includes(':')) {
throw new Error(
`You must specify a metadata name for this annotation in the form of `
+ ` 'mynamespace:myproperty'. You specified: '${this.$metadataName || '<none>'}'`
);
}
Object.assign(this, props || {});
}
readonly $metadataName : string;
toString() {
return `@${this.constructor.name}`;
}
static getMetadataName(): string {
if (!this['$metadataName'])
throw new Error(`Annotation subclass ${this.name} must have `);
return this['$metadataName'];
}
/**
* Construct a decorator suitable for attaching annotations of the called type
* onto classes, constructor parameters, methods, properties, and parameters.
* Must be called while referencing the subclass of Annotation you wish to construct
* the decorator for. E.g., for FooAnnotation, call FooAnnotation.decorator().
*
* @param this The Annotation subclass for which the decorator is being constructed
* @param options Allows for specifying options which will modify the behavior of the decorator.
* See the DecoratorOptions documentation for more information.
*/
public static decorator<
T extends Annotation,
TS extends any[],
U extends AnnotationDecoratorTarget
>(
this: AnnotationConstructor<T, TS>,
options? : Exclude<AnnotationDecoratorOptions<T, TS>, 'validTargets'> & { validTargets: U[] }
): (...args: TS) => DecoratorTypeForValidTargets<U>;
public static decorator<T extends Annotation, TS extends any[]>(this: AnnotationConstructor<T, TS>, options?: AnnotationDecoratorOptions<T, TS>): AnnotationDecorator<TS>;
public static decorator<T extends Annotation, TS extends any[]>(this: AnnotationConstructor<T, TS>, options?: AnnotationDecoratorOptions<T, TS>): AnnotationDecorator<TS> {
if ((this as any) === Annotation) {
if (!options || !options.factory) {
throw new Error(`When calling Annotation.decorator() to create a mutator, you must specify a factory (or use Mutator.decorator())`);
}
}
return makeDecorator(this, options);
}
/**
* Clone this annotation instance into a new one. This is not a deep copy.
*/
public clone(): this {
return Annotations.clone(this);
}
/**
* Apply this annotation to a given target.
* @param target
*/
public applyToClass(target : any): this {
return Annotations.applyToClass(this, target);
}
/**
* Apply this annotation instance to the given property.
* @param target
* @param name
*/
public applyToProperty(target : any, name : string): this {
return Annotations.applyToProperty(this, target, name);
}
/**
* Apply this annotation instance to the given method.
* @param target
* @param name
*/
public applyToMethod(target : any, name : string): this {
return Annotations.applyToMethod(this, target, name);
}
/**
* Apply this annotation instance to the given method parameter.
* @param target
* @param name
* @param index
*/
public applyToParameter(target : any, name : string, index : number): this {
return Annotations.applyToParameter(this, target, name, index);
}
/**
* Apply this annotation instance to the given constructor parameter.
* @param target
* @param name
* @param index
*/
public applyToConstructorParameter(target : any, index : number): this {
return Annotations.applyToConstructorParameter(this, target, index);
}
/**
* Filter the given list of annotations for the ones which match this annotation class
* based on matching $metadataName.
*
* @param this
* @param annotations
*/
public static filter<T extends Annotation, TS extends any[]>(
this : AnnotationConstructor<T, TS>,
annotations : IAnnotation[]
) : T[] {
return annotations.filter(
x => x.$metadataName === this.getMetadataName()
) as T[];
}
/**
* Get all instances of this annotation class attached to the given class.
* If called on a subclass of Annotation, it returns only annotations that match
* that subclass.
* @param this
* @param type The class to check
*/
public static getAllForClass<T extends Annotation, TS extends any[]>(
this : AnnotationConstructor<T, TS>,
type : any
): T[] {
return (Annotations.getClassAnnotations(type) as T[])
.filter(x => x.$metadataName === this.getMetadataName())
;
}
/**
* Get a single instance of this annotation class attached to the given class.
* If called on a subclass of Annotation, it returns only annotations that match
* that subclass.
*
* @param this
* @param type
*/
public static getForClass<T extends Annotation, TS extends any[]>(
this : AnnotationConstructor<T, TS>,
type : any
): T {
return (this as any).getAllForClass(type)[0];
}
/**
* Get all instances of this annotation class attached to the given method.
* If called on a subclass of Annotation, it returns only annotations that match
* that subclass.
*
* @param this
* @param type The class where the method is defined
* @param methodName The name of the method to check
*/
public static getAllForMethod<T extends Annotation, TS extends any[]>(
this : AnnotationConstructor<T, TS>,
type : any,
methodName : string
): T[] {
return (Annotations.getMethodAnnotations(type, methodName) as T[])
.filter(x => x.$metadataName === this.getMetadataName())
;
}
/**
* Get one instance of this annotation class attached to the given method.
* If called on a subclass of Annotation, it returns only annotations that match
* that subclass.
*
* @param this
* @param type The class where the method is defined
* @param methodName The name of the method to check
*/
public static getForMethod<T extends Annotation, TS extends any[]>(
this : AnnotationConstructor<T, TS>,
type : any,
methodName : string
): T {
return (this as any).getAllForMethod(type, methodName)[0];
}
/**
* Get all instances of this annotation class attached to the given property.
* If called on a subclass of Annotation, it returns only annotations that match
* that subclass.
*
* @param this
* @param type The class where the property is defined
* @param propertyName The name of the property to check
*/
public static getAllForProperty<T extends Annotation, TS extends any[]>(
this : AnnotationConstructor<T, TS>,
type : any,
propertyName : string
): T[] {
return (Annotations.getPropertyAnnotations(type, propertyName) as T[])
.filter(x => x.$metadataName === this.getMetadataName())
;
}
/**
* Get one instance of this annotation class attached to the given property.
* If called on a subclass of Annotation, it returns only annotations that match
* that subclass.
*
* @param this
* @param type The class where the property is defined
* @param propertyName The name of the property to check
*/
public static getForProperty<T extends Annotation, TS extends any[]>(
this : AnnotationConstructor<T, TS>,
type : any,
propertyName : string
): T {
return (this as any).getAllForProperty(type, propertyName)[0];
}
/**
* Get all instances of this annotation class attached to the parameters of the given method.
* If called on a subclass of Annotation, it returns only annotations that match
* that subclass.
*
* @param this
* @param type The class where the method is defined
* @param methodName The name of the method where parameter annotations should be checked for
*/
public static getAllForParameters<T extends Annotation, TS extends any[]>(
this : AnnotationConstructor<T, TS>,
type : any,
methodName : string
): T[][] {
return (Annotations.getParameterAnnotations(type, methodName) as T[][])
.map(set => (set || []).filter(x => (this as any) === Annotation ? true : (x.$metadataName === this.getMetadataName())))
;
}
/**
* Get all instances of this annotation class attached to the parameters of the constructor
* for the given class.
* If called on a subclass of Annotation, it returns only annotations that match
* that subclass.
*
* @param this
* @param type The class where constructor parameter annotations should be checked for
*/
public static getAllForConstructorParameters<T extends Annotation, TS extends any[]>(
this : AnnotationConstructor<T, TS>,
type : any
): T[][] {
let finalSet = new Array(<any>type.length).fill(undefined);
let annotations = (Annotations.getConstructorParameterAnnotations(type) as T[][])
.map(set => (set || []).filter(x => (this as any) === Annotation ? true : (x.$metadataName === this.getMetadataName())))
;
for (let i = 0, max = annotations.length; i < max; ++i)
finalSet[i] = annotations[i];
return finalSet;
}
}
/**
* A helper class for managing annotations
*/
export class Annotations {
/**
* Copy the annotations defined for one class onto another.
* @param from The class to copy annotations from
* @param to The class to copy annotations to
*/
public static copyClassAnnotations(from : Function, to : Function) {
let annotations = Annotations.getClassAnnotations(from);
annotations.forEach(x => Annotations.applyToClass(x, to));
}
/**
* Apply this annotation to a given target.
* @param target
*/
public static applyToClass<T extends IAnnotation>(annotation : T, target : any): T {
let list = this.getOrCreateListForClass(target);
let clone = this.clone(annotation);
list.push(clone);
if (Reflect.getOwnMetadata) {
let reflectedAnnotations = Reflect.getOwnMetadata('annotations', target) || [];
reflectedAnnotations.push({ toString() { return `${clone.$metadataName}`; }, annotation: clone });
Reflect.defineMetadata('annotations', reflectedAnnotations, target);
}
return clone;
}
/**
* Apply this annotation instance to the given property.
* @param target
* @param name
*/
public static applyToProperty<T extends IAnnotation>(annotation : T, target : any, name : string): T {
let list = this.getOrCreateListForProperty(target, name);
let clone = this.clone(annotation);
list.push(clone);
if (Reflect.getOwnMetadata) {
let reflectedAnnotations = Reflect.getOwnMetadata('propMetadata', target, name) || [];
reflectedAnnotations.push({ toString() { return `${clone.$metadataName}`; }, annotation: clone });
Reflect.defineMetadata('propMetadata', reflectedAnnotations, target, name);
}
return clone;
}
/**
* Apply this annotation instance to the given method.
* @param target
* @param name
*/
public static applyToMethod<T extends IAnnotation>(annotation : T, target : any, name : string): T {
let list = this.getOrCreateListForMethod(target, name);
let clone = Annotations.clone(annotation);
list.push(clone);
if (Reflect.getOwnMetadata && target.constructor) {
const meta = Reflect.getOwnMetadata('propMetadata', target.constructor) || {};
meta[name] = (meta.hasOwnProperty(name) && meta[name]) || [];
meta[name].unshift({ toString() { return `${clone.$metadataName}`; }, annotation: clone });
Reflect.defineMetadata('propMetadata', meta, target.constructor);
}
return clone;
}
/**
* Apply this annotation instance to the given method parameter.
* @param target
* @param name
* @param index
*/
public static applyToParameter<T extends IAnnotation>(annotation : T, target : any, name : string, index : number): T {
let list = this.getOrCreateListForMethodParameters(target, name);
while (list.length < index)
list.push(null);
let paramList = list[index] || [];
let clone = this.clone(annotation);
paramList.push(clone);
list[index] = paramList;
return clone;
}
/**
* Apply this annotation instance to the given constructor parameter.
* @param target
* @param name
* @param index
*/
public static applyToConstructorParameter<T extends IAnnotation>(annotation : T, target : any, index : number): T {
let list = this.getOrCreateListForConstructorParameters(target);
while (list.length < index)
list.push(null);
let paramList = list[index] || [];
let clone = this.clone(annotation);
paramList.push(clone);
list[index] = paramList;
if (Reflect.getOwnMetadata) {
let parameterList = Reflect.getOwnMetadata('parameters', target) || [];
while (parameterList.length < index)
parameterList.push(null);
let parameterAnnotes = parameterList[index] || [];
parameterAnnotes.push(clone);
parameterList[index] = parameterAnnotes;
Reflect.defineMetadata('parameters', parameterList, target);
}
return clone;
}
/**
* Clone the given Annotation instance into a new instance. This is not
* a deep copy.
*
* @param annotation
*/
public static clone<T extends IAnnotation>(annotation : T): T {
if (!annotation)
return annotation;
return Object.assign(Object.create(Object.getPrototypeOf(annotation)), annotation);
}
/**
* Get all annotations (including from Angular and other compatible
* frameworks).
*
* @param target The target to fetch annotations for
*/
public static getClassAnnotations(target : any): IAnnotation[] {
return (this.getListForClass(target) || [])
.map(x => this.clone(x));
}
/**
* Get all annotations (including from Angular and other compatible
* frameworks).
*
* @param target The target to fetch annotations for
*/
public static getMethodAnnotations(target : any, methodName : string, isStatic : boolean = false): IAnnotation[] {
return (this.getListForMethod(isStatic ? target : target.prototype, methodName) || [])
.map(x => this.clone(x));
}
/**
* Get all annotations (including from Angular and other compatible
* frameworks).
*
* @param target The target to fetch annotations for
*/
public static getPropertyAnnotations(target : any, methodName : string, isStatic : boolean = false): IAnnotation[] {
return (this.getListForProperty(isStatic ? target : target.prototype, methodName) || [])
.map(x => this.clone(x));
}
/**
* Get the annotations defined on the parameters of the given method of the given
* class.
*
* @param type
* @param methodName
* @param isStatic Whether `type` itself (isStatic = true), or `type.prototype` (isStatic = false) should be the target.
* Note that passing true may indicate that the passed `type` is already the prototype of a class.
*/
public static getParameterAnnotations(type : any, methodName : string, isStatic : boolean = false): IAnnotation[][] {
return (this.getListForMethodParameters(isStatic ? type : type.prototype, methodName) || [])
.map(set => set ? set.map(anno => this.clone(anno)) : []);
}
/**
* Get the annotations defined on the parameters of the given method of the given
* class.
*
* @param type
* @param methodName
*/
public static getConstructorParameterAnnotations(type : any): IAnnotation[][] {
return (this.getListForConstructorParameters(type) || [])
.map(set => set ? set.map(anno => this.clone(anno)) : []);
}
/**
* Get a list of annotations for the given class.
* @param target
*/
private static getListForClass(target : Object): IAnnotation[] {
if (!target)
return [];
let combinedSet = [];
let superclass = Object.getPrototypeOf(target);
if (superclass && superclass !== Function)
combinedSet = combinedSet.concat(this.getListForClass(superclass));
if (target.hasOwnProperty(ANNOTATIONS_KEY))
combinedSet = combinedSet.concat(target[ANNOTATIONS_KEY] || []);
return combinedSet;
}
/**
* Get a list of own annotations for the given class, or create that list.
* @param target
*/
private static getOrCreateListForClass(target : Object): IAnnotation[] {
if (!target.hasOwnProperty(ANNOTATIONS_KEY))
Object.defineProperty(target, ANNOTATIONS_KEY, { enumerable: false, value: [] });
return target[ANNOTATIONS_KEY];
}
/**
* Gets a map of the annotations defined on all properties of the given class/function. To get the annotations of instance fields,
* make sure to use `Class.prototype`, otherwise static annotations are returned.
*/
public static getMapForClassProperties(target : Object, mapToPopulate? : Record<string,IAnnotation[]>): Record<string,IAnnotation[]> {
let combinedSet = mapToPopulate || {};
if (!target || target === Function)
return combinedSet;
this.getMapForClassProperties(Object.getPrototypeOf(target), combinedSet);
if (target.hasOwnProperty(PROPERTY_ANNOTATIONS_KEY)) {
let ownMap : Record<string,IAnnotation[]> = target[PROPERTY_ANNOTATIONS_KEY] || {};
for (let key of Object.keys(ownMap))
combinedSet[key] = (combinedSet[key] || []).concat(ownMap[key]);
}
return combinedSet;
}
private static getOrCreateMapForClassProperties(target : Object): Record<string,IAnnotation[]> {
if (!target.hasOwnProperty(PROPERTY_ANNOTATIONS_KEY))
Object.defineProperty(target, PROPERTY_ANNOTATIONS_KEY, { enumerable: false, value: [] });
return target[PROPERTY_ANNOTATIONS_KEY];
}
private static getListForProperty(target : any, propertyKey : string): IAnnotation[] {
let map = this.getMapForClassProperties(target);
if (!map)
return null;
return map[propertyKey];
}
private static getOrCreateListForProperty(target : any, propertyKey : string): IAnnotation[] {
let map = this.getOrCreateMapForClassProperties(target);
if (!map[propertyKey])
map[propertyKey] = [];
return map[propertyKey];
}
private static getOrCreateListForMethod(target : any, methodName : string): IAnnotation[] {
return this.getOrCreateListForProperty(target, methodName);
}
private static getListForMethod(target : any, methodName : string): IAnnotation[] {
return this.getListForProperty(target, methodName);
}
/**
* Get a map of the annotations defined on all parameters of all methods of the given class/function.
* To get instance methods, make sure to pass `Class.prototype`, otherwise the results are for static fields.
*/
public static getMapForMethodParameters(target : Object, mapToPopulate? : Record<string,IAnnotation[][]>): Record<string,IAnnotation[][]> {
let combinedMap = mapToPopulate || {};
if (!target || target === Function)
return combinedMap;
// superclass/prototype
this.getMapForMethodParameters(Object.getPrototypeOf(target), combinedMap);
if (target.hasOwnProperty(METHOD_PARAMETER_ANNOTATIONS_KEY)) {
let ownMap : Record<string,IAnnotation[][]> = target[METHOD_PARAMETER_ANNOTATIONS_KEY] || {};
for (let methodName of Object.keys(ownMap)) {
let parameters = ownMap[methodName];
let combinedMethodMap = combinedMap[methodName] || [];
for (let i = 0, max = parameters.length; i < max; ++i) {
combinedMethodMap[i] = (combinedMethodMap[i] || []).concat(parameters[i] || []);
}
combinedMap[methodName] = combinedMethodMap;
}
}
return combinedMap;
}
private static getOrCreateMapForMethodParameters(target : Object): Record<string, IAnnotation[][]> {
if (!target.hasOwnProperty(METHOD_PARAMETER_ANNOTATIONS_KEY))
Object.defineProperty(target, METHOD_PARAMETER_ANNOTATIONS_KEY, { enumerable: false, value: {} });
return target[METHOD_PARAMETER_ANNOTATIONS_KEY];
}
private static getListForMethodParameters(target : any, methodName : string): IAnnotation[][] {
let map = this.getMapForMethodParameters(target);
if (!map)
return null;
return map[methodName];
}
private static getOrCreateListForMethodParameters(target : any, methodName : string): IAnnotation[][] {
let map = this.getOrCreateMapForMethodParameters(target);
if (!map[methodName])
map[methodName] = [];
return map[methodName];
}
private static getOrCreateListForConstructorParameters(target : any): IAnnotation[][] {
if (!target[CONSTRUCTOR_PARAMETERS_ANNOTATIONS_KEY])
Object.defineProperty(target, CONSTRUCTOR_PARAMETERS_ANNOTATIONS_KEY, { enumerable: false, value: [] });
return target[CONSTRUCTOR_PARAMETERS_ANNOTATIONS_KEY];
}
private static getListForConstructorParameters(target : any): IAnnotation[][] {
return target[CONSTRUCTOR_PARAMETERS_ANNOTATIONS_KEY];
}
}
/**
* An annotation for attaching a label to a programmatic element.
* Can be queried with LabelAnnotation.getForClass() for example.
*/
export class LabelAnnotation extends Annotation {
constructor(readonly text : string) {
super();
}
}
export const Label = LabelAnnotation.decorator();