@alterior/annotations
Version:
Create and interact with Typescript metadata decorators
707 lines • 28.7 kB
JavaScript
"use strict";
/// <reference types="reflect-metadata" />
Object.defineProperty(exports, "__esModule", { value: true });
exports.Label = exports.LabelAnnotation = exports.Annotations = exports.Annotation = exports.AnnotationTargetError = exports.METHOD_PARAMETER_ANNOTATIONS_KEY = exports.PROPERTY_ANNOTATIONS_KEY = exports.CONSTRUCTOR_PARAMETERS_ANNOTATIONS_KEY = exports.ANNOTATIONS_KEY = void 0;
exports.MetadataName = MetadataName;
const tslib_1 = require("tslib");
/**
* @alterior/annotations
* A class library for handling Typescript metadata decorators via "annotation" classes
*
* (C) 2017-2019 William Lahti
*
*/
const common_1 = require("@alterior/common");
// 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
exports.ANNOTATIONS_KEY = '__annotations__';
exports.CONSTRUCTOR_PARAMETERS_ANNOTATIONS_KEY = '__parameters__';
exports.PROPERTY_ANNOTATIONS_KEY = '__prop__metadata__';
exports.METHOD_PARAMETER_ANNOTATIONS_KEY = '__parameter__metadata__';
;
/**
* Thrown when a caller attempts to decorate an annotation target when the
* annotation does not support that target.
*/
class AnnotationTargetError extends common_1.NotSupportedError {
constructor(annotationClass, invalidType, supportedTypes, message) {
super(message || `You cannot decorate a ${invalidType} with annotation ${annotationClass.name}. Valid targets: ${supportedTypes.join(', ')}`);
this._invalidType = invalidType;
this._annotationClass = annotationClass;
this._supportedTypes = supportedTypes;
}
get invalidType() {
return this._invalidType;
}
get supportedTypes() {
return this._supportedTypes;
}
get annotationClass() {
return this._annotationClass;
}
}
exports.AnnotationTargetError = AnnotationTargetError;
/**
* 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(ctor, options) {
if (!ctor)
throw new Error(`Cannot create decorator: Passed class reference was undefined/null: This can happen due to circular dependencies.`);
let factory = null;
let validTargets = null;
let allowMultiple = false;
if (options) {
if (options.factory)
factory = options.factory;
if (options.validTargets)
validTargets = options.validTargets;
if (options.allowMultiple)
allowMultiple = options.allowMultiple;
}
if (!factory)
factory = (target, ...args) => new ctor(...args);
if (!validTargets)
validTargets = ['class', 'method', 'property', 'parameter'];
return (...decoratorArgs) => {
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 = args[0];
let index = 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 = args[0];
let descriptor = 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 = 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.`);
}
};
};
}
function MetadataName(name) {
return target => Object.defineProperty(target, '$metadataName', { value: name });
}
/**
* 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 {
// ...
}
```
*
*/
class Annotation {
constructor(props) {
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 || {});
}
toString() {
return `@${this.constructor.name}`;
}
static getMetadataName() {
if (!this['$metadataName'])
throw new Error(`Annotation subclass ${this.name} must have `);
return this['$metadataName'];
}
static decorator(options) {
if (this === 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.
*/
clone() {
return Annotations.clone(this);
}
/**
* Apply this annotation to a given target.
* @param target
*/
applyToClass(target) {
return Annotations.applyToClass(this, target);
}
/**
* Apply this annotation instance to the given property.
* @param target
* @param name
*/
applyToProperty(target, name) {
return Annotations.applyToProperty(this, target, name);
}
/**
* Apply this annotation instance to the given method.
* @param target
* @param name
*/
applyToMethod(target, name) {
return Annotations.applyToMethod(this, target, name);
}
/**
* Apply this annotation instance to the given method parameter.
* @param target
* @param name
* @param index
*/
applyToParameter(target, name, index) {
return Annotations.applyToParameter(this, target, name, index);
}
/**
* Apply this annotation instance to the given constructor parameter.
* @param target
* @param name
* @param index
*/
applyToConstructorParameter(target, index) {
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
*/
static filter(annotations) {
return annotations.filter(x => x.$metadataName === this.getMetadataName());
}
/**
* 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
*/
static getAllForClass(type) {
return Annotations.getClassAnnotations(type)
.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
*/
static getForClass(type) {
return this.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
*/
static getAllForMethod(type, methodName) {
return Annotations.getMethodAnnotations(type, methodName)
.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
*/
static getForMethod(type, methodName) {
return this.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
*/
static getAllForProperty(type, propertyName) {
return Annotations.getPropertyAnnotations(type, propertyName)
.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
*/
static getForProperty(type, propertyName) {
return this.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
*/
static getAllForParameters(type, methodName) {
return Annotations.getParameterAnnotations(type, methodName)
.map(set => (set || []).filter(x => this === 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
*/
static getAllForConstructorParameters(type) {
let finalSet = new Array(type.length).fill(undefined);
let annotations = Annotations.getConstructorParameterAnnotations(type)
.map(set => (set || []).filter(x => this === Annotation ? true : (x.$metadataName === this.getMetadataName())));
for (let i = 0, max = annotations.length; i < max; ++i)
finalSet[i] = annotations[i];
return finalSet;
}
}
exports.Annotation = Annotation;
/**
* A helper class for managing annotations
*/
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
*/
static copyClassAnnotations(from, to) {
let annotations = Annotations.getClassAnnotations(from);
annotations.forEach(x => Annotations.applyToClass(x, to));
}
/**
* Apply this annotation to a given target.
* @param target
*/
static applyToClass(annotation, target) {
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
*/
static applyToProperty(annotation, target, name) {
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
*/
static applyToMethod(annotation, target, name) {
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
*/
static applyToParameter(annotation, target, name, index) {
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
*/
static applyToConstructorParameter(annotation, target, index) {
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
*/
static clone(annotation) {
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
*/
static getClassAnnotations(target) {
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
*/
static getMethodAnnotations(target, methodName, isStatic = false) {
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
*/
static getPropertyAnnotations(target, methodName, isStatic = false) {
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.
*/
static getParameterAnnotations(type, methodName, isStatic = false) {
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
*/
static getConstructorParameterAnnotations(type) {
return (this.getListForConstructorParameters(type) || [])
.map(set => set ? set.map(anno => this.clone(anno)) : []);
}
/**
* Get a list of annotations for the given class.
* @param target
*/
static getListForClass(target) {
if (!target)
return [];
let combinedSet = [];
let superclass = Object.getPrototypeOf(target);
if (superclass && superclass !== Function)
combinedSet = combinedSet.concat(this.getListForClass(superclass));
if (target.hasOwnProperty(exports.ANNOTATIONS_KEY))
combinedSet = combinedSet.concat(target[exports.ANNOTATIONS_KEY] || []);
return combinedSet;
}
/**
* Get a list of own annotations for the given class, or create that list.
* @param target
*/
static getOrCreateListForClass(target) {
if (!target.hasOwnProperty(exports.ANNOTATIONS_KEY))
Object.defineProperty(target, exports.ANNOTATIONS_KEY, { enumerable: false, value: [] });
return target[exports.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.
*/
static getMapForClassProperties(target, mapToPopulate) {
let combinedSet = mapToPopulate || {};
if (!target || target === Function)
return combinedSet;
this.getMapForClassProperties(Object.getPrototypeOf(target), combinedSet);
if (target.hasOwnProperty(exports.PROPERTY_ANNOTATIONS_KEY)) {
let ownMap = target[exports.PROPERTY_ANNOTATIONS_KEY] || {};
for (let key of Object.keys(ownMap))
combinedSet[key] = (combinedSet[key] || []).concat(ownMap[key]);
}
return combinedSet;
}
static getOrCreateMapForClassProperties(target) {
if (!target.hasOwnProperty(exports.PROPERTY_ANNOTATIONS_KEY))
Object.defineProperty(target, exports.PROPERTY_ANNOTATIONS_KEY, { enumerable: false, value: [] });
return target[exports.PROPERTY_ANNOTATIONS_KEY];
}
static getListForProperty(target, propertyKey) {
let map = this.getMapForClassProperties(target);
if (!map)
return null;
return map[propertyKey];
}
static getOrCreateListForProperty(target, propertyKey) {
let map = this.getOrCreateMapForClassProperties(target);
if (!map[propertyKey])
map[propertyKey] = [];
return map[propertyKey];
}
static getOrCreateListForMethod(target, methodName) {
return this.getOrCreateListForProperty(target, methodName);
}
static getListForMethod(target, methodName) {
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.
*/
static getMapForMethodParameters(target, mapToPopulate) {
let combinedMap = mapToPopulate || {};
if (!target || target === Function)
return combinedMap;
// superclass/prototype
this.getMapForMethodParameters(Object.getPrototypeOf(target), combinedMap);
if (target.hasOwnProperty(exports.METHOD_PARAMETER_ANNOTATIONS_KEY)) {
let ownMap = target[exports.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;
}
static getOrCreateMapForMethodParameters(target) {
if (!target.hasOwnProperty(exports.METHOD_PARAMETER_ANNOTATIONS_KEY))
Object.defineProperty(target, exports.METHOD_PARAMETER_ANNOTATIONS_KEY, { enumerable: false, value: {} });
return target[exports.METHOD_PARAMETER_ANNOTATIONS_KEY];
}
static getListForMethodParameters(target, methodName) {
let map = this.getMapForMethodParameters(target);
if (!map)
return null;
return map[methodName];
}
static getOrCreateListForMethodParameters(target, methodName) {
let map = this.getOrCreateMapForMethodParameters(target);
if (!map[methodName])
map[methodName] = [];
return map[methodName];
}
static getOrCreateListForConstructorParameters(target) {
if (!target[exports.CONSTRUCTOR_PARAMETERS_ANNOTATIONS_KEY])
Object.defineProperty(target, exports.CONSTRUCTOR_PARAMETERS_ANNOTATIONS_KEY, { enumerable: false, value: [] });
return target[exports.CONSTRUCTOR_PARAMETERS_ANNOTATIONS_KEY];
}
static getListForConstructorParameters(target) {
return target[exports.CONSTRUCTOR_PARAMETERS_ANNOTATIONS_KEY];
}
}
exports.Annotations = Annotations;
/**
* An annotation for attaching a label to a programmatic element.
* Can be queried with LabelAnnotation.getForClass() for example.
*/
let LabelAnnotation = class LabelAnnotation extends Annotation {
constructor(text) {
super();
this.text = text;
}
};
exports.LabelAnnotation = LabelAnnotation;
exports.LabelAnnotation = LabelAnnotation = tslib_1.__decorate([
MetadataName('alterior:Label'),
tslib_1.__metadata("design:paramtypes", [String])
], LabelAnnotation);
exports.Label = LabelAnnotation.decorator();
//# sourceMappingURL=annotations.js.map