inversify/InversifyJS

View on GitHub
src/annotation/decorator_utils.ts

Summary

Maintainability
A
45 mins
Test Coverage
import * as ERROR_MSGS from '../constants/error_msgs';
import * as METADATA_KEY from '../constants/metadata_keys';
import { interfaces } from '../interfaces/interfaces';
import { getFirstArrayDuplicate } from '../utils/js';

function targetIsConstructorFunction<T = Object>(target: DecoratorTarget<T>): target is ConstructorFunction<T> {
  return (target as ConstructorFunction<T>).prototype !== undefined;
}

type Prototype<T> = {
  [Property in keyof T]:
  T[Property] extends NewableFunction ?
  T[Property] :
  T[Property] | undefined
} & { constructor: NewableFunction }

interface ConstructorFunction<T = Record<string, unknown>> {
  new(...args: unknown[]): T,
  prototype: Prototype<T>
}

export type DecoratorTarget<T = unknown> = ConstructorFunction<T> | Prototype<T>

function _throwIfMethodParameter(parameterName: string | symbol | undefined): void {
  if (parameterName !== undefined) {
    throw new Error(ERROR_MSGS.INVALID_DECORATOR_OPERATION);
  }
}


function tagParameter(
  annotationTarget: DecoratorTarget,
  parameterName: string | symbol | undefined,
  parameterIndex: number,
  metadata: interfaces.MetadataOrMetadataArray
) {
  _throwIfMethodParameter(parameterName);
  _tagParameterOrProperty(METADATA_KEY.TAGGED, annotationTarget as ConstructorFunction, parameterIndex.toString(), metadata);
}

function tagProperty(
  annotationTarget: DecoratorTarget,
  propertyName: string | symbol,
  metadata: interfaces.MetadataOrMetadataArray
) {
  if (targetIsConstructorFunction(annotationTarget)) {
    throw new Error(ERROR_MSGS.INVALID_DECORATOR_OPERATION);
  }
  _tagParameterOrProperty(METADATA_KEY.TAGGED_PROP, annotationTarget.constructor, propertyName, metadata);
}

function _ensureNoMetadataKeyDuplicates(metadata: interfaces.MetadataOrMetadataArray): interfaces.Metadata[] {
  let metadatas: interfaces.Metadata[] = [];
  if (Array.isArray(metadata)) {
    metadatas = metadata;
    const duplicate = getFirstArrayDuplicate(metadatas.map(md => md.key));
    if (duplicate !== undefined) {
      throw new Error(`${ERROR_MSGS.DUPLICATED_METADATA} ${duplicate.toString()}`);
    }
  } else {
    metadatas = [metadata];
  }
  return metadatas;
}

function _tagParameterOrProperty(
  metadataKey: string,
  annotationTarget: NewableFunction,
  key: string | symbol,
  metadata: interfaces.MetadataOrMetadataArray,
) {
  const metadatas: interfaces.Metadata[] = _ensureNoMetadataKeyDuplicates(metadata);

  let paramsOrPropertiesMetadata: Record<string | symbol, interfaces.Metadata[] | undefined> = {};
  // read metadata if available
  if (Reflect.hasOwnMetadata(metadataKey, annotationTarget)) {
    paramsOrPropertiesMetadata = Reflect.getMetadata(metadataKey, annotationTarget);
  }

  let paramOrPropertyMetadata: interfaces.Metadata[] | undefined = paramsOrPropertiesMetadata[key as string];

  if (paramOrPropertyMetadata === undefined) {
    paramOrPropertyMetadata = [];
  } else {
    for (const m of paramOrPropertyMetadata) {
      if (metadatas.some(md => md.key === m.key)) {
        throw new Error(`${ERROR_MSGS.DUPLICATED_METADATA} ${m.key.toString()}`);
      }
    }
  }

  // set metadata
  paramOrPropertyMetadata.push(...metadatas);
  paramsOrPropertiesMetadata[key] = paramOrPropertyMetadata;
  Reflect.defineMetadata(metadataKey, paramsOrPropertiesMetadata, annotationTarget);

}



function createTaggedDecorator(
  metadata: interfaces.MetadataOrMetadataArray,
) {
  return <T>(
    target: DecoratorTarget,
    targetKey?: string | symbol,
    indexOrPropertyDescriptor?: number | TypedPropertyDescriptor<T>,
  ) => {
    if (typeof indexOrPropertyDescriptor === 'number') {
      tagParameter(target, targetKey, indexOrPropertyDescriptor, metadata);
    } else {
      tagProperty(target, targetKey as string | symbol, metadata);
    }
  };
}

function _decorate(
  decorators: (DecoratorTarget | ParameterDecorator | MethodDecorator)[],
  target: object | NewableFunction,
): void {
  Reflect.decorate(decorators as ClassDecorator[], target as NewableFunction);
}

function _param(paramIndex: number, decorator: ParameterDecorator) {
  return function (target: string, key: string) { decorator(target, key, paramIndex); };
}

// Allows VanillaJS developers to use decorators:
// decorate(injectable(), FooBar);
// decorate(targetName('foo', 'bar'), FooBar);
// decorate(named('foo'), FooBar, 0);
// decorate(tagged('bar'), FooBar, 1);
function decorate(
  decorator: (DecoratorTarget | ParameterDecorator | MethodDecorator),
  target: object,
  parameterIndexOrProperty?: number | string): void {

  if (typeof parameterIndexOrProperty === 'number') {
    _decorate([_param(parameterIndexOrProperty, decorator as ParameterDecorator)], target);
  } else if (typeof parameterIndexOrProperty === 'string') {
    Reflect.decorate([decorator as MethodDecorator], target, parameterIndexOrProperty);
  } else {
    _decorate([decorator], target);
  }
}

export { decorate, tagParameter, tagProperty, createTaggedDecorator };