e22m4u 1 месяц назад
Родитель
Сommit
1cf57b2b52

+ 2 - 0
README.md

@@ -199,6 +199,7 @@ const myService = container.get(MyService);
 - `add(ctor, ...args)` добавить конструктор в контейнер (ленивая инициализация);
 - `add(ctor, ...args)` добавить конструктор в контейнер (ленивая инициализация);
 - `use(ctor, ...args)` добавить конструктор и сразу создать экземпляр;
 - `use(ctor, ...args)` добавить конструктор и сразу создать экземпляр;
 - `set(ctor, service)` добавить конструктор и связанный с ним готовый экземпляр;
 - `set(ctor, service)` добавить конструктор и связанный с ним готовый экземпляр;
+- `find(predicate, noParent = false)` найти сервис удовлетворяющий условию;
 - `getParent()` получить родительский сервис-контейнер;
 - `getParent()` получить родительский сервис-контейнер;
 - `hasParent()` проверить наличие родительского сервис-контейнера;
 - `hasParent()` проверить наличие родительского сервис-контейнера;
 
 
@@ -272,6 +273,7 @@ console.log(hasService); // true
 - `addService(ctor, ...args)` добавить конструктор в контейнер;
 - `addService(ctor, ...args)` добавить конструктор в контейнер;
 - `useService(ctor, ...args)` добавить конструктор и создать экземпляр;
 - `useService(ctor, ...args)` добавить конструктор и создать экземпляр;
 - `setService(ctor, service)` добавить конструктор и его экземпляр;
 - `setService(ctor, service)` добавить конструктор и его экземпляр;
+- `findService(predicate, noParent = false)` найти сервис удовлетворяющий условию;
 
 
 Сервисом может являться совершенно любой класс. Однако, если это
 Сервисом может являться совершенно любой класс. Однако, если это
 наследник класса `Service`, то такой сервис позволяет инкапсулировать
 наследник класса `Service`, то такой сервис позволяет инкапсулировать

+ 48 - 0
dist/cjs/index.cjs

@@ -209,6 +209,36 @@ var _ServiceContainer = class _ServiceContainer {
     this._services.set(ctor, service);
     this._services.set(ctor, service);
     return this;
     return this;
   }
   }
+  /**
+   * Найти сервис удовлетворяющий условию.
+   *
+   * @param {function(Function, ServiceContainer): boolean} predicate
+   * @param {boolean} noParent
+   * @returns {*}
+   */
+  find(predicate, noParent = false) {
+    if (typeof predicate !== "function") {
+      throw new InvalidArgumentError(
+        "The first argument of ServiceContainer.find must be a function, but %v given.",
+        predicate
+      );
+    }
+    const isRecursive = !noParent;
+    let currentContainer = this;
+    do {
+      for (const ctor of currentContainer._services.keys()) {
+        if (predicate(ctor, currentContainer) === true) {
+          return this.get(ctor);
+        }
+      }
+      if (isRecursive && currentContainer.hasParent()) {
+        currentContainer = currentContainer.getParent();
+      } else {
+        currentContainer = null;
+      }
+    } while (currentContainer);
+    return void 0;
+  }
 };
 };
 __name(_ServiceContainer, "ServiceContainer");
 __name(_ServiceContainer, "ServiceContainer");
 /**
 /**
@@ -307,6 +337,16 @@ var _Service = class _Service {
     this.container.set(ctor, service);
     this.container.set(ctor, service);
     return this;
     return this;
   }
   }
+  /**
+   * Найти сервис удовлетворяющий условию.
+   *
+   * @param {function(Function, ServiceContainer): boolean} predicate
+   * @param {boolean} noParent
+   * @returns {*}
+   */
+  findService(predicate, noParent = false) {
+    return this.container.find(predicate, noParent);
+  }
 };
 };
 __name(_Service, "Service");
 __name(_Service, "Service");
 /**
 /**
@@ -383,6 +423,14 @@ var _DebuggableService = class _DebuggableService extends import_js_debug.Debugg
   get setService() {
   get setService() {
     return this._service.setService;
     return this._service.setService;
   }
   }
+  /**
+   * Найти сервис удовлетворяющий условию.
+   *
+   * @type {Service['findService']}
+   */
+  get findService() {
+    return this._service.findService;
+  }
   /**
   /**
    * Constructor.
    * Constructor.
    *
    *

+ 3 - 3
package.json

@@ -44,7 +44,7 @@
   "devDependencies": {
   "devDependencies": {
     "@commitlint/cli": "~20.1.0",
     "@commitlint/cli": "~20.1.0",
     "@commitlint/config-conventional": "~20.0.0",
     "@commitlint/config-conventional": "~20.0.0",
-    "@e22m4u/js-spy": "~0.0.2",
+    "@e22m4u/js-spy": "~0.0.3",
     "@eslint/js": "~9.38.0",
     "@eslint/js": "~9.38.0",
     "c8": "~10.1.3",
     "c8": "~10.1.3",
     "chai": "~6.2.0",
     "chai": "~6.2.0",
@@ -52,13 +52,13 @@
     "eslint": "~9.38.0",
     "eslint": "~9.38.0",
     "eslint-config-prettier": "~10.1.8",
     "eslint-config-prettier": "~10.1.8",
     "eslint-plugin-chai-expect": "~3.1.0",
     "eslint-plugin-chai-expect": "~3.1.0",
-    "eslint-plugin-jsdoc": "~61.1.9",
+    "eslint-plugin-jsdoc": "~61.1.11",
     "eslint-plugin-mocha": "~11.2.0",
     "eslint-plugin-mocha": "~11.2.0",
     "globals": "~16.4.0",
     "globals": "~16.4.0",
     "husky": "~9.1.7",
     "husky": "~9.1.7",
     "mocha": "~11.7.4",
     "mocha": "~11.7.4",
     "prettier": "~3.6.2",
     "prettier": "~3.6.2",
-    "rimraf": "~6.0.1",
+    "rimraf": "~6.1.0",
     "typescript": "~5.9.3"
     "typescript": "~5.9.3"
   }
   }
 }
 }

+ 12 - 1
src/debuggable-service.d.ts

@@ -2,7 +2,7 @@ import {Service} from './service.js';
 import {Constructor} from './types.js';
 import {Constructor} from './types.js';
 import {Debuggable} from '@e22m4u/js-debug';
 import {Debuggable} from '@e22m4u/js-debug';
 import {DebuggableOptions} from '@e22m4u/js-debug';
 import {DebuggableOptions} from '@e22m4u/js-debug';
-import {ServiceContainer} from './service-container.js';
+import {FindServicePredicate, ServiceContainer} from './service-container.js';
 
 
 /**
 /**
  * Debuggable service.
  * Debuggable service.
@@ -93,4 +93,15 @@ export class DebuggableService extends Debuggable implements Service {
     ctor: Constructor<T>,
     ctor: Constructor<T>,
     service: T,
     service: T,
   ): this;
   ): this;
+
+  /**
+   * Найти сервис удовлетворяющий условию.
+   *
+   * @param predicate
+   * @param noParent
+   */
+  findService<T extends object>(
+    predicate: FindServicePredicate<T>,
+    noParent?: boolean,
+  ): T | undefined;
 }
 }

+ 9 - 0
src/debuggable-service.js

@@ -84,6 +84,15 @@ export class DebuggableService extends Debuggable {
     return this._service.setService;
     return this._service.setService;
   }
   }
 
 
+  /**
+   * Найти сервис удовлетворяющий условию.
+   *
+   * @type {Service['findService']}
+   */
+  get findService() {
+    return this._service.findService;
+  }
+
   /**
   /**
    * Constructor.
    * Constructor.
    *
    *

+ 6 - 0
src/debuggable-service.spec.js

@@ -73,5 +73,11 @@ describe('DebuggableService', function () {
         debuggableService._service.setService,
         debuggableService._service.setService,
       );
       );
     });
     });
+
+    it("should delegate the 'findService' getter to the internal service's findService method", function () {
+      expect(debuggableService.findService).to.eq(
+        debuggableService._service.findService,
+      );
+    });
   });
   });
 });
 });

+ 25 - 23
src/service-container.d.ts

@@ -1,5 +1,13 @@
 import {Constructor} from './types.js';
 import {Constructor} from './types.js';
 
 
+/**
+ * Find service predicate.
+ */
+export type FindServicePredicate<T extends object> = (
+  ctor: Constructor<T>,
+  container: ServiceContainer,
+) => boolean;
+
 /**
 /**
  * Service container.
  * Service container.
  */
  */
@@ -27,10 +35,7 @@ export declare class ServiceContainer {
    * @param ctor
    * @param ctor
    * @param args
    * @param args
    */
    */
-  get<T extends object>(
-    ctor: Constructor<T>,
-    ...args: any[],
-  ): T;
+  get<T extends object>(ctor: Constructor<T>, ...args: any[]): T;
 
 
   /**
   /**
    * Получить существующий или новый экземпляр,
    * Получить существующий или новый экземпляр,
@@ -39,19 +44,14 @@ export declare class ServiceContainer {
    * @param ctor
    * @param ctor
    * @param args
    * @param args
    */
    */
-  getRegistered<T extends object>(
-    ctor: Constructor<T>,
-    ...args: any[],
-  ): T;
+  getRegistered<T extends object>(ctor: Constructor<T>, ...args: any[]): T;
 
 
   /**
   /**
    * Проверить существование конструктора в контейнере.
    * Проверить существование конструктора в контейнере.
    *
    *
    * @param ctor
    * @param ctor
    */
    */
-  has<T extends object>(
-    ctor: Constructor<T>,
-  ): boolean;
+  has<T extends object>(ctor: Constructor<T>): boolean;
 
 
   /**
   /**
    * Добавить конструктор в контейнер.
    * Добавить конструктор в контейнер.
@@ -59,10 +59,7 @@ export declare class ServiceContainer {
    * @param ctor
    * @param ctor
    * @param args
    * @param args
    */
    */
-  add<T extends object>(
-    ctor: Constructor<T>,
-    ...args: any[],
-  ): this;
+  add<T extends object>(ctor: Constructor<T>, ...args: any[]): this;
 
 
   /**
   /**
    * Добавить конструктор и создать экземпляр.
    * Добавить конструктор и создать экземпляр.
@@ -70,10 +67,7 @@ export declare class ServiceContainer {
    * @param ctor
    * @param ctor
    * @param args
    * @param args
    */
    */
-  use<T extends object>(
-    ctor: Constructor<T>,
-    ...args: any[],
-  ): this;
+  use<T extends object>(ctor: Constructor<T>, ...args: any[]): this;
 
 
   /**
   /**
    * Добавить конструктор и связанный экземпляр.
    * Добавить конструктор и связанный экземпляр.
@@ -81,8 +75,16 @@ export declare class ServiceContainer {
    * @param ctor
    * @param ctor
    * @param service
    * @param service
    */
    */
-  set<T extends object>(
-    ctor: Constructor<T>,
-    service: T,
-  ): this;
+  set<T extends object>(ctor: Constructor<T>, service: T): this;
+
+  /**
+   * Найти сервис удовлетворяющий условию.
+   *
+   * @param predicate
+   * @param noParent
+   */
+  find<T extends object>(
+    predicate: FindServicePredicate<T>,
+    noParent?: boolean,
+  ): T | undefined;
 }
 }

+ 32 - 0
src/service-container.js

@@ -238,4 +238,36 @@ export class ServiceContainer {
     this._services.set(ctor, service);
     this._services.set(ctor, service);
     return this;
     return this;
   }
   }
+
+  /**
+   * Найти сервис удовлетворяющий условию.
+   *
+   * @param {function(Function, ServiceContainer): boolean} predicate
+   * @param {boolean} noParent
+   * @returns {*}
+   */
+  find(predicate, noParent = false) {
+    if (typeof predicate !== 'function') {
+      throw new InvalidArgumentError(
+        'The first argument of ServiceContainer.find ' +
+          'must be a function, but %v given.',
+        predicate,
+      );
+    }
+    const isRecursive = !noParent;
+    let currentContainer = this;
+    do {
+      for (const ctor of currentContainer._services.keys()) {
+        if (predicate(ctor, currentContainer) === true) {
+          return this.get(ctor);
+        }
+      }
+      if (isRecursive && currentContainer.hasParent()) {
+        currentContainer = currentContainer.getParent();
+      } else {
+        currentContainer = null;
+      }
+    } while (currentContainer);
+    return undefined;
+  }
 }
 }

+ 98 - 0
src/service-container.spec.js

@@ -1387,4 +1387,102 @@ describe('ServiceContainer', function () {
       });
       });
     });
     });
   });
   });
+
+  describe('find', function () {
+    class ServiceX {
+      static id = 'X';
+    }
+    class ServiceY {
+      static id = 'Y';
+    }
+
+    it('should throw an error if the predicate is not a function', function () {
+      const container = new ServiceContainer();
+      const throwable = v => () => container.find(v);
+      const error = v =>
+        format(
+          'The first argument of ServiceContainer.find ' +
+            'must be a function, but %s given.',
+          v,
+        );
+      expect(throwable('not a function')).to.throw(error('"not a function"'));
+      expect(throwable(123)).to.throw(error('123'));
+    });
+
+    it('should return undefined if no service matches the predicate', function () {
+      const container = new ServiceContainer();
+      container.add(ServiceX);
+      const result = container.find(() => false);
+      expect(result).to.be.undefined;
+    });
+
+    it('should find a service in the current container and return its instance', function () {
+      const container = new ServiceContainer();
+      container.use(ServiceX);
+      const result = container.find(ctor => ctor === ServiceX);
+      expect(result).to.be.instanceof(ServiceX);
+    });
+
+    it('should find a service by a static property and return its instance', function () {
+      const container = new ServiceContainer();
+      container.add(ServiceX);
+      container.add(ServiceY);
+      const result = container.find(ctor => ctor.id === 'Y');
+      expect(result).to.be.instanceof(ServiceY);
+    });
+
+    it('should search in the parent container by default (noParent=false)', function () {
+      const parent = new ServiceContainer();
+      parent.add(ServiceX);
+      const child = new ServiceContainer(parent);
+      child.add(ServiceY);
+      const result = child.find(ctor => ctor.id === 'X');
+      expect(result).to.be.instanceof(ServiceX);
+    });
+
+    it('should not search in the parent container when noParent is true', function () {
+      const parent = new ServiceContainer();
+      parent.add(ServiceX);
+      const child = new ServiceContainer(parent);
+      child.add(ServiceY);
+      const result = child.find(ctor => ctor.id === 'X', true);
+      expect(result).to.be.undefined;
+    });
+
+    it('should stop searching and return the first match (from child)', function () {
+      const parent = new ServiceContainer();
+      parent.use(ServiceX);
+      const child = new ServiceContainer(parent);
+      const childInstance = new ServiceX();
+      child.set(ServiceX, childInstance);
+      const foundContainerSpy = createSpy();
+      const result = child.find((ctor, container) => {
+        if (ctor === ServiceX) {
+          foundContainerSpy(container);
+          return true;
+        }
+        return false;
+      });
+      expect(result).to.equal(childInstance);
+      expect(foundContainerSpy.callCount).to.be.eq(1);
+      expect(foundContainerSpy.getCall(0).args[0]).to.equal(child);
+    });
+
+    it('should pass the correct constructor and container to the predicate', function () {
+      const parent = new ServiceContainer();
+      parent.add(ServiceX);
+      const child = new ServiceContainer(parent);
+      child.add(ServiceY);
+      const predicateSpy = createSpy(() => false);
+      child.find(predicateSpy);
+      const calls = predicateSpy.calls.map(c => ({
+        ctor: c.args[0],
+        container: c.args[1],
+      }));
+      expect(calls).to.have.deep.members([
+        {ctor: ServiceY, container: child},
+        {ctor: ServiceX, container: parent},
+      ]);
+    });
+  });
 });
 });

+ 12 - 1
src/service.d.ts

@@ -1,5 +1,5 @@
 import {Constructor} from './types.js';
 import {Constructor} from './types.js';
-import {ServiceContainer} from './service-container.js';
+import {FindServicePredicate, ServiceContainer} from './service-container.js';
 
 
 /**
 /**
  * Service class name.
  * Service class name.
@@ -91,4 +91,15 @@ export declare class Service {
     ctor: Constructor<T>,
     ctor: Constructor<T>,
     service: T,
     service: T,
   ): this;
   ): this;
+
+  /**
+   * Найти сервис удовлетворяющий условию.
+   *
+   * @param predicate
+   * @param noParent
+   */
+  findService<T extends object>(
+    predicate: FindServicePredicate<T>,
+    noParent?: boolean,
+  ): T | undefined;
 }
 }

+ 11 - 0
src/service.js

@@ -105,4 +105,15 @@ export class Service {
     this.container.set(ctor, service);
     this.container.set(ctor, service);
     return this;
     return this;
   }
   }
+
+  /**
+   * Найти сервис удовлетворяющий условию.
+   *
+   * @param {function(Function, ServiceContainer): boolean} predicate
+   * @param {boolean} noParent
+   * @returns {*}
+   */
+  findService(predicate, noParent = false) {
+    return this.container.find(predicate, noParent);
+  }
 }
 }

+ 17 - 2
src/service.spec.js

@@ -90,7 +90,7 @@ describe('Service', function () {
         expect(ctor).to.be.eq(Date);
         expect(ctor).to.be.eq(Date);
         expect(args).to.be.eql(['foo', 'bar', 'baz']);
         expect(args).to.be.eql(['foo', 'bar', 'baz']);
       };
       };
-      const res = service.addService(Date, 'foo', 'bar', 'baz');
+      const res = service.useService(Date, 'foo', 'bar', 'baz');
       expect(res).to.be.eq(service);
       expect(res).to.be.eq(service);
     });
     });
   });
   });
@@ -103,8 +103,23 @@ describe('Service', function () {
         expect(ctor).to.be.eq(Date);
         expect(ctor).to.be.eq(Date);
         expect(input).to.be.eq(date);
         expect(input).to.be.eq(date);
       };
       };
-      const res = service.addService(Date, date);
+      const res = service.setService(Date, date);
       expect(res).to.be.eq(service);
       expect(res).to.be.eq(service);
     });
     });
   });
   });
+
+  describe('findService', function () {
+    it('should call the container "find" method', function () {
+      const service = new Service();
+      const predicate = () => true;
+      const noParent = true;
+      service.container.find = function (arg1, arg2) {
+        expect(predicate).to.be.eq(arg1);
+        expect(noParent).to.be.eq(arg2);
+        return 'OK';
+      };
+      const res = service.findService(predicate, noParent);
+      expect(res).to.be.eq('OK');
+    });
+  });
 });
 });