Skip to content

Instantly share code, notes, and snippets.

@SerafimArts
Last active October 24, 2023 22:47
Show Gist options
  • Save SerafimArts/f802c544d8e9d15f5eece1dfb2664929 to your computer and use it in GitHub Desktop.
Save SerafimArts/f802c544d8e9d15f5eece1dfb2664929 to your computer and use it in GitHub Desktop.
JavaScript Annotations example
import Annotation from './Annotation';
import Target from './Target';
@Target("Class")
export default class Entity {
/**
* @type {boolean}
*/
readOnly: boolean = false;
/**
* @type {Function|null}
*/
repository: ?Function = null;
/**
* @constructor
*/
call constructor(args) {
return new Annotation(args, 'repository').delegate(Entity);
}
}
import Entity from './_example_annotation';
import Reader from './Reader';
@Entity({ readOnly: true })
class User {
}
let reader = new Reader(User);
console.log(reader.getClassAnnotations());
// >> Array [ Entity {readOnly: true, repository: null} ]
import Reader from './Reader';
import Target from './Target';
/**
* This is default annotation property for automatic type casting:
* <code>
* @Annotation({ some: any })
* // => will be casts "as is" {some: any}
*
* @Annotation("any")
* // => will be casts to object {DEFAULT_ANNOTATION_PROPERTY: any}
* </code>
*
* @type {string}
*/
const DEFAULT_ANNOTATION_PROPERTY = 'default';
/**
* This is helper class for define annotations. Example:
*
* <code>
* @Target("Class")
* export default class MyAnnotation {
* a = 42;
* b = 23;
*
* call constructor(args) {
* return new Annotation(args).delegate(MyAnnotation);
* }
* }
*
* // Usage:
*
* @MyAnnotation({ a: "new value" })
* class Some {}
*
* // (new Reader(Some)).getClassAnnotations(); // returns [ AnnotationClass { a = "new value", b = 23 } ]
* </code>
*/
export default class Annotation {
/**
* @param ctx
* @param descr
* @return {string}
*/
static getTarget(ctx, descr): string {
if (ctx instanceof Function) {
return Target.TARGET_CLASS;
}
if (typeof descr.value === 'function') {
return Target.TARGET_METHOD;
}
return Target.TARGET_PROPERTY;
}
/**
* @param ctx
* @param name
* @param descr
* @return {string}
*/
static getName(ctx, name, descr): string {
let type = this.getTarget(ctx, descr);
return type === Target.TARGET_CLASS ? ctx.name : name;
}
/**
* @param ctx
* @param descr
* @return {Function}
*/
static getClassContext(ctx, descr): Function {
let type = this.getTarget(ctx, descr);
return type === Target.TARGET_CLASS ? ctx : ctx.constructor;
}
/**
* @param ctx
* @param name
* @param descr
* @return {{target: string, name: string, class: Function}}
*/
static info(ctx, name, descr) {
return {
target: this.getTarget(ctx, name, descr),
name: this.getName(ctx, name, descr),
class: this.getClassContext(ctx, descr)
}
}
/**
* @param ctx
* @param descr
* @return {Reader}
*/
static reader(ctx, descr): Reader {
return new Reader(this.getClassContext(ctx, descr));
}
/**
* @type {{}}
* @private
*/
_args: Object = {};
/**
* @param {Object|*} args
* @param {string} _defaultProperty
*/
constructor(args: Object = {}, _defaultProperty: string = DEFAULT_ANNOTATION_PROPERTY) {
if (typeof args !== 'object') {
args = {
[_defaultProperty]: args
};
}
this._args = args;
}
/**
* @param {Function} targetAnnotation
* @return {Function}
*/
delegate(targetAnnotation: Function): Function {
let annotation = this.constructor._fill(targetAnnotation, this._args);
return function (ctx, name, descr) {
let info = Annotation.info(ctx, name, descr);
let meta = Annotation.reader(ctx, descr);
Target.check(targetAnnotation, info);
switch (info.target) {
case Target.TARGET_CLASS:
meta.addClassAnnotation(annotation);
break;
case Target.TARGET_PROPERTY:
meta.addPropertyAnnotation(info.name, annotation);
break;
case Target.TARGET_METHOD:
meta.addMethodAnnotation(info.name, annotation);
break;
}
return descr;
};
}
/**
* @param _class
* @param args
* @return {Object}
* @private
*/
static _fill(_class: Function, args: Object = {}): Object {
let instance = new _class(args);
for (let key of Object.keys(args)) {
instance[key] = args[key];
}
return instance;
}
}
import Target from "./Target";
/**
* Metadata key for all class annotations
*
* @type {Symbol}
*/
const METADATA_CLASS = Symbol('METADATA_CLASS');
/**
* Metadata key for all method annotations
*
* @type {Symbol}
*/
const METADATA_METHOD = Symbol('METADATA_METHOD');
/**
* Metadata key for all property annotations
*
* @type {Symbol}
*/
const METADATA_PROPERTY = Symbol('METADATA_PROPERTY');
/**
* Default name for read polymorfic structures, like:
*
* <code>
* class_metadata {
* DEFAULT_META_KEY: [ Annotation ]
* }
* property_metadata {
* propertyA: [ Annotation ]
* propertyB: [ Annotation, Annotation ]
* }
* method_metadata {
* methodA: [ Annotation, Annotation ]
* methodB: [ Annotation ]
* }
* </code>
*
* @type {string}
*/
const DEFAULT_META_KEY = 'default';
/**
* This is annotations reader class over Reflect Metadata API
*/
export default class Reader {
/**
* @type {Function}
* @private
*/
_class: Function;
/**
* @param {Function} _class
*/
constructor(_class: Function) {
this._class = _class;
}
/**
* @param type
* @param key
* @private
*/
_boot(type, key = DEFAULT_META_KEY) {
let data = Reflect.getMetadata(type, this._class) || {};
if (typeof data[key] === 'undefined') {
data[key] = [];
Reflect.defineMetadata(type, data, this._class);
}
}
/**
* @param {*} type
* @param {string} key
* @return {Array}
*/
getMetadata(type: any, key: string = DEFAULT_META_KEY): Array {
this._boot(type, key);
let items = Reflect.getMetadata(type, this._class)[key];
// Be sure for items array are immutable
return items.slice(0);
}
/**
* @param {Object} annotation
* @param {*} type
* @param {string} key
* @return {void}
*/
addMetadata(annotation: Object, type: any, key: string = DEFAULT_META_KEY): void {
this._boot(type, key);
let data = Reflect.getMetadata(type, this._class);
let items = data[key];
items.push(annotation);
Reflect.defineMetadata(type, data, this._class);
}
/**
* @return {Array}
*/
getClassAnnotations(): Array {
return this.getMetadata(Target.TARGET_CLASS);
}
/**
* @param {string} name
* @return {Object|null}
*/
getClassAnnotation(name: string): ?Object {
for (let annotation of this.getClassAnnotations()) {
if (annotation.constructor.name === name) {
return annotation;
}
}
return null;
}
/**
* @param {Object} annotation
* @return {Reader}
*/
addClassAnnotation(annotation: Object): Reader {
this.addMetadata(annotation, Target.TARGET_CLASS);
return this;
}
/**
* @param {string} methodName
* @return {Array}
*/
getMethodAnnotations(methodName: string): Array {
return this.getMetadata(Target.TARGET_METHOD, methodName);
}
/**
* @param {string} methodName
* @param {Object} annotation
* @return {Reader}
*/
addMethodAnnotation(methodName: string, annotation: Object): Reader {
this.addMetadata(annotation, Target.TARGET_METHOD, methodName);
return this;
}
/**
* @param {string} propertyName
* @return {Generator}
*/
getPropertyAnnotations(propertyName: string): Array {
return this.getMetadata(Target.TARGET_PROPERTY, propertyName);
}
/**
* @param {string} propertyName
* @param {Object} annotation
* @return {Reader}
*/
addPropertyAnnotation(propertyName: string, annotation: Object): Reader {
this.addMetadata(annotation, Target.TARGET_PROPERTY, propertyName);
return this;
}
}
import Reader from "./Reader";
import Annotation from "./Annotation";
/**
* Throws while annotation define over invalid (or unsupported) structure
*/
export class AnnotationTargetError extends TypeError {}
/**
* This is a polymorfic type for annotations types
*
* @type {string}
*/
type AnnotationTarget = Target.TARGET_CLASS | Target.TARGET_METHOD | Target.TARGET_PROPERTY;
/**
* Target
*/
export default class Target {
/**
* @type {string}
*/
static TARGET_CLASS = 'Class';
/**
* @type {string}
*/
static TARGET_METHOD = 'Method';
/**
* @type {string}
*/
static TARGET_PROPERTY = 'Property';
/**
* @type {Array}
*/
targetings: AnnotationTarget = [];
/**
* @constructor
*/
call constructor(target) {
return function (ctx, name, descr) {
let info = Annotation.info(ctx, name, descr);
if (info.target === Target.TARGET_CLASS) {
let annotation = new Target();
annotation.targetings = target instanceof Array ? target : [target];
new Reader(info.class).addClassAnnotation(annotation);
}
return descr;
};
}
/**
* @param {Function} annotationClass
* @param {{target: string, name: string, class: Function}} imp
*/
static check(annotationClass, imp: Object) {
let annotation = new Reader(annotationClass).getClassAnnotation('Target');
if (annotation) {
for (let targeting of annotation.targetings) {
if (targeting === imp.target) {
return;
}
}
throw new AnnotationTargetError(`${annotation.targetings.join(', ')} target required but ${target} given.`);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment