Last active
October 24, 2023 22:47
-
-
Save SerafimArts/f802c544d8e9d15f5eece1dfb2664929 to your computer and use it in GitHub Desktop.
JavaScript Annotations example
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
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); | |
} | |
} |
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
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} ] |
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
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; | |
} | |
} |
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
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; | |
} | |
} |
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
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