UNPKG

@alterior/annotations

Version:
707 lines 28.7 kB
"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 @MetadataName()`); 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