Skip to content

Instantly share code, notes, and snippets.

@422404
Created June 13, 2018 21:05
Show Gist options
  • Select an option

  • Save 422404/91c2220197035d678cf1df7eceae4993 to your computer and use it in GitHub Desktop.

Select an option

Save 422404/91c2220197035d678cf1df7eceae4993 to your computer and use it in GitHub Desktop.

Revisions

  1. 422404 created this gist Jun 13, 2018.
    251 changes: 251 additions & 0 deletions dependencyInjection.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,251 @@
    /*********************************** Typing ***************************************/

    /**
    * Signature of a constructible class
    */
    interface IClass {
    /**
    * Used to express the constructability of a class
    */
    new(...args: any[]): {};
    }

    /**
    * Signature of a service factory class
    */
    interface IServiceFactory extends IClass {
    /**
    * Construct and return the instance of the class constructed by the factory
    * @param {any} args Factories are free to require any args
    * @returns {Object} Newly created instance
    */
    getInstance: (...args: any[]) => Object;
    }

    /**
    * Signature of a registered service
    * Handful for IntelliSence
    */
    interface IService extends IClass {
    /**
    * Service identifier
    * Not really optionnal, just because classes are modified at runtime
    * @type {UID}
    */
    $$uid?: UID,

    /**
    * Must be equal to "service"
    * Not really optionnal, just because classes are modified at runtime
    * @type {string}
    */
    $$type?: 'service'
    }

    /**
    * Config that ndicate which factory to use to create the service instance the
    * consumer depends on
    */
    type IServiceFactoryConfig = { factory: IServiceFactory, args?: any[] }

    /**
    * Config that indicate the service instance or factory to use to create
    * the service instance the consumer depends on
    */
    type IServiceInjectionConfig = IService | IServiceFactoryConfig

    /**
    * A unique identifier
    */
    type UID = number

    /*********************************** Private **************************************/

    /**
    * Injects arguments into factory "getInstance" method
    * @param {any} args Arguments to inject, only services in this case
    * @param {IServiceFactory} factoryClass Factory's class
    * @returns {IServiceFactory} The factory class with injected args into "getInstance" method
    */
    function injectArgsIntoFactory(args: any[], factoryClass: IServiceFactory): IServiceFactory {
    let getInstanceFn: Function = factoryClass.getInstance;
    if (!getInstanceFn) {
    throw 'The given class is not a factory !';
    }
    factoryClass.getInstance = getInstanceFn.bind(factoryClass, ...args);

    return factoryClass;
    }

    /**
    * Injects arguments into a constructor
    * @param {any} args Arguments to inject into the constructor, only services in this case
    * @param {IClass} baseClass Class to inject the args into
    * @returns {IClass} The new class with the args injected into its constructor
    */
    function injectArgsIntoConstructor(args: Object[], baseClass: IClass): IClass {
    return class extends baseClass {
    constructor(..._args: any[]) {
    super(...args, ..._args);
    }
    };
    }

    /**
    * Collects the services instances required by the config
    * @param {IServiceInjectionConfig[]} dependanciesConfig Config containing the services
    * or factories to use to retrieve the services instances
    * @returns {Object[]} The services instances
    */
    function collectServices(dependanciesConfig: IServiceInjectionConfig[]): Object[] {
    return dependanciesConfig.map(service => constructServiceOrDelegateToFactory(service));
    }

    /**
    * Constructs a service from the config
    * @param {IServiceInjectionConfig} dependencyConfig Service class or service factory config
    * @returns {Object} The service instance
    */
    function constructServiceOrDelegateToFactory(dependencyConfig: IServiceInjectionConfig): Object {
    let serviceInstance: Object;

    if (typeof dependencyConfig === 'object') {
    serviceInstance = dependencyConfig.factory.getInstance.apply(null,
    dependencyConfig.args !== undefined ? dependencyConfig.args : []
    );
    } else if (typeof dependencyConfig === 'function') {
    if (!dependencyConfig.$$uid || !dependencyConfig.$$type || dependencyConfig.$$type != 'service') {
    throw 'Service not registered !';
    }
    let serviceCtor: IClass = <IClass>ServiceRegistry.getService(dependencyConfig.$$uid);
    serviceInstance = new serviceCtor();
    } else {
    throw 'Bad config !';
    }

    return serviceInstance;
    }

    /**
    * Represents a registered service
    * @class
    */
    class RegisteredService {
    /**
    * @constructor
    * @param {UID} uid Unique identifier of the registered service
    * @param {IClass} klass Service's class
    */
    constructor(public uid: UID, public klass: IClass) {
    }
    }

    /**
    * Registry of services
    * @class
    */
    class ServiceRegistry {
    /**
    * The registered services
    * @static
    * @type {RegisteredService[]}
    */
    private static services: RegisteredService[] = [];

    /**
    * Current uid to be used
    * @static
    * @type {UID}
    */
    private static currentUID: UID = 0;

    /**
    * Generates a new uid and returns it
    * @static
    * @returns {UID} The new uid
    */
    static generateUID() {
    return ++ServiceRegistry.currentUID;
    }

    /**
    * Registers a service
    * @param {IClass} service Service's class
    * @returns {IClass} The new class with $$uid and $$type properties added
    */
    static registerService(service: IClass): IClass {
    let uid = ServiceRegistry.generateUID();
    (<any>service).$$uid = uid;
    (<any>service).$$type = 'service';
    ServiceRegistry.services.push(new RegisteredService(uid, service));

    return service;
    }

    /**
    * Returns the class of a registered service
    * @param {UID} uid The unique id of the service
    * @returns {IClass} The registered service's class
    */
    static getService(uid: UID): IClass {
    let service: RegisteredService|undefined = ServiceRegistry.services.find(service =>
    service.uid == uid
    );

    if (!service) {
    throw 'Unregistered service !';
    }

    return service.klass;
    }
    };

    /*********************************** Public ***************************************/

    /**
    * Service decorator
    * Used to register a class as a service
    */
    export function Service(): Function {
    return function (constructor: IClass): IClass {
    return ServiceRegistry.registerService(constructor);
    };
    }

    /**
    * Service consumer decorator
    * Performs dependancies (services) injection into a class constructor
    * @param {IServiceInjectionConfig[]} needs Array of services or services factories config
    * Array items can be either services classes (i.e: UserService) or objects thats specifies
    * the factory to use and its optionnal arguments (i.e: { factory : UserServiceFactory [, args: [some arg, another]] })
    */
    export function ServicesConsumer(needs: IServiceInjectionConfig[]): Function {
    if (!needs) {
    throw 'To few arguments given !';
    }

    return function (constructor: IClass): IClass {
    let services: Object[] = collectServices(needs);
    return injectArgsIntoConstructor(services, constructor);
    };
    }

    /**
    * Service factory decorator
    * Indicates that a static class is a service factory (do not register it) and
    * performs dependancies injection into the "getInstance" method used to construct
    * the provided service
    * @param needs Array of services or services factories config
    * Array items can be either services classes (i.e: UserService) or objects thats specifies
    * the factory to use and its optionnal arguments (i.e: { factory : UserServiceFactory [, args: [some arg, another]] })
    */
    export function ServiceFactory(needs?: IServiceInjectionConfig[]): Function {
    if (!needs) {
    return;
    }

    return function (constructor: IServiceFactory): IServiceFactory {
    let services: Object[] = collectServices(needs);
    return injectArgsIntoFactory(services, constructor);
    }
    }
    183 changes: 183 additions & 0 deletions index.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,183 @@
    import { Service, ServicesConsumer, ServiceFactory } from './dependencyInjection';

    /**
    * Whether or not the user is logged
    * (such authentication)
    */
    const AUTHENTICATED = false;

    /**
    * Base language of the strings
    */
    const LANG = 'fr';

    /**
    * Base strings in English and their translation in French and Spanish
    */
    const strings: Object = {
    '<anonymous>': {
    'fr': '<anonyme>',
    'es': '<anónimo>'
    },
    'Johnatan': null,
    'Hello': {
    'fr': 'Salut',
    'es': 'Hola'
    }
    };

    /**
    * Internationalization service
    * @class
    */
    @Service()
    class I18nService {
    private baseLang: string = 'en';

    /**
    * @constructor
    * @param {string} [lang] Language to translate the text into
    * @param {Object} [strings] Object that contains base strings and their translations
    */
    constructor(private lang?: string, private strings?: any) {}

    /**
    * Translate a text into other languages
    * @param text Text to translate
    * @returns {string} translated text or base text if no translations
    */
    $(text: string): string {
    if (!this.lang
    || !this.strings
    || this.lang == this.baseLang
    || !this.strings[text]
    || !this.strings[text][this.lang]) {

    return text;
    }
    return this.strings[text][this.lang];
    }
    }

    /**
    * Basic authentication service for the purpose of testing
    * @class
    */
    @Service()
    class AuthService {
    /**
    * Returns whether or not the user is authenticated
    * @
    */
    isAuthenticated(): boolean {
    return AUTHENTICATED;
    }
    }

    /**
    * Constructs I18nService instance with more control
    * @class
    */
    @ServiceFactory()
    class I18nServiceFactory {
    /**
    * Creates an instance of I18nService
    * @static
    * @param {string} lang Language used to translate text
    * @returns {Object} I18nService instance
    */
    static getInstance(lang: string): Object {
    return new I18nService(lang, strings);
    }
    }


    /**
    * Constructs UserService instance with more control
    * @class
    */
    @ServiceFactory([
    AuthService,
    { factory: I18nServiceFactory, args: [LANG] }
    ])
    class UserServiceFactory {
    /**
    * Creates an instance of UserService
    * @static
    * @param {AuthService} auth Some authentification service
    * @param {I18nService} i18n I18n service dependency used to print internationalized text
    */
    static getInstance(auth: AuthService, i18n: I18nService): Object {
    return new UserService(i18n, auth.isAuthenticated() ? i18n.$('Johnatan') : null);
    }
    }

    /**
    * Cool service sayin' "Hello"
    * @class
    */
    @Service()
    @ServicesConsumer([
    { factory: I18nServiceFactory, args: [LANG] }
    ])
    class HelloService {
    /**
    * @constructor
    * @param {I18nService} i18n I18n service dependency used to print internationalized text
    */
    constructor(private i18n: I18nService) {}

    /**
    * Say hello !
    * @returns {string} the "Hello" string translated or not
    */
    sayHello(): string {
    return this.i18n.$('Hello');
    }
    };

    /**
    * Service providing informations about an user (logged or not)
    * @class
    */
    @Service()
    class UserService {
    /**
    * @constructor
    * @param {I18nService} i18n I18n service dependency used to print internationalized text
    * @param {string} [username] name of the logged in user
    */
    constructor(private i18n: I18nService, private username?: string) {}

    /**
    * Shows the current user's name
    * @returns {string} current user's name
    */
    showUser(): string {
    return this.username ? this.username : this.i18n.$('<anonymous>');
    }
    }

    /**
    * Some component using dependency injection
    * @class
    */
    @ServicesConsumer([
    HelloService,
    { factory: UserServiceFactory }
    ])
    class MyComponent {
    /**
    * @constructor
    * @param {HelloService} [hello] Some hello service dependency
    * Just for the test it's made optionnal as we construct the component by hand
    * @param {User} [user] Some user service dependency
    * Just for the test it's made optionnal as we construct the component by hand
    */
    constructor(hello?: HelloService, user?: UserService) {
    console.log(hello.sayHello(), user.showUser(), '!');
    }
    }

    // Should be delegated to a component builder
    let component: MyComponent = new MyComponent();
    1 change: 1 addition & 0 deletions readme.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1 @@
    Don't forget to enable **experimentalDecorators** in tsconfig.json !