Browse Source

chore: adds DebuggableService class

e22m4u 3 months ago
parent
commit
cce8925859

+ 56 - 3
dist/cjs/index.cjs

@@ -2,6 +2,7 @@ var __defProp = Object.defineProperty;
 var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
 var __getOwnPropNames = Object.getOwnPropertyNames;
 var __hasOwnProp = Object.prototype.hasOwnProperty;
+var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
 var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
 var __export = (target, all) => {
   for (var name in all)
@@ -16,20 +17,27 @@ var __copyProps = (to, from, except, desc) => {
   return to;
 };
 var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
+var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
 
 // src/index.js
 var index_exports = {};
 __export(index_exports, {
   DEFAULT_OFFSET_STEP_SPACES: () => DEFAULT_OFFSET_STEP_SPACES,
+  DebuggableService: () => DebuggableService,
   INSPECT_OPTIONS: () => INSPECT_OPTIONS,
   createColorizedDump: () => createColorizedDump,
   createDebugger: () => createDebugger
 });
 module.exports = __toCommonJS(index_exports);
 
-// src/create-debugger.js
-var import_js_format = require("@e22m4u/js-format");
-var import_js_format2 = require("@e22m4u/js-format");
+// src/services/debuggable-service.js
+var import_js_service = require("@e22m4u/js-service");
+
+// src/utils/to-camel-case.js
+function toCamelCase(input) {
+  return input.replace(/(^\w|[A-Z]|\b\w)/g, (c) => c.toUpperCase()).replace(/\W+/g, "").replace(/(^\w)/g, (c) => c.toLowerCase());
+}
+__name(toCamelCase, "toCamelCase");
 
 // src/utils/is-non-array-object.js
 function isNonArrayObject(input) {
@@ -55,6 +63,50 @@ function generateRandomHex(length = 4) {
 }
 __name(generateRandomHex, "generateRandomHex");
 
+// src/services/debuggable-service.js
+var _DebuggableService = class _DebuggableService extends import_js_service.Service {
+  /**
+   * Debug.
+   *
+   * @type {Function}
+   */
+  debug;
+  /**
+   * Возвращает функцию-отладчик с сегментом пространства имен
+   * указанного в параметре метода.
+   *
+   * @param {Function} method
+   * @returns {Function}
+   */
+  getDebuggerFor(method) {
+    return this.debug.withHash().withNs(method.name);
+  }
+  /**
+   * Constructor.
+   *
+   * @param {object|undefined} container
+   */
+  constructor(container) {
+    super(container);
+    const serviceName = toCamelCase(this.constructor.name);
+    this.debug = createDebugger(serviceName);
+    const debug = this.debug.withNs("constructor").withHash();
+    debug(_DebuggableService.INSTANTIATION_MESSAGE);
+  }
+};
+__name(_DebuggableService, "DebuggableService");
+/**
+ * Instantiation message;
+ *
+ * @type {string}
+ */
+__publicField(_DebuggableService, "INSTANTIATION_MESSAGE", "Instantiated.");
+var DebuggableService = _DebuggableService;
+
+// src/create-debugger.js
+var import_js_format = require("@e22m4u/js-format");
+var import_js_format2 = require("@e22m4u/js-format");
+
 // src/create-colorized-dump.js
 var import_util = require("util");
 var INSPECT_OPTIONS = {
@@ -329,6 +381,7 @@ __name(createDebugger, "createDebugger");
 // Annotate the CommonJS export names for ESM import in node:
 0 && (module.exports = {
   DEFAULT_OFFSET_STEP_SPACES,
+  DebuggableService,
   INSPECT_OPTIONS,
   createColorizedDump,
   createDebugger

+ 2 - 1
package.json

@@ -38,7 +38,8 @@
     "prepare": "husky"
   },
   "dependencies": {
-    "@e22m4u/js-format": "~0.1.8"
+    "@e22m4u/js-format": "~0.1.8",
+    "@e22m4u/js-service": "~0.3.3"
   },
   "devDependencies": {
     "@commitlint/cli": "~19.8.1",

+ 1 - 4
src/create-debugger.spec.js

@@ -1,12 +1,9 @@
 import {expect} from 'chai';
 import {createSpy} from '@e22m4u/js-spy';
+import {stripAnsi} from './utils/strip-ansi.js';
 import {createDebugger} from './create-debugger.js';
 import {DEFAULT_OFFSET_STEP_SPACES} from './create-debugger.js';
 
-// вспомогательная функция для удаления ANSI escape-кодов (цветов)
-// eslint-disable-next-line no-control-regex
-const stripAnsi = str => str.replace(/\x1b\[[0-9;]*m/g, '');
-
 describe('createDebugger', function () {
   let consoleLogSpy;
   let originalDebugEnv;

+ 1 - 0
src/index.js

@@ -1,2 +1,3 @@
+export * from './services/index.js';
 export * from './create-debugger.js';
 export * from './create-colorized-dump.js';

+ 30 - 0
src/services/debuggable-service.d.ts

@@ -0,0 +1,30 @@
+import {Callable} from '../types.js';
+import {Service} from '@e22m4u/js-service';
+import {Debugger} from '../create-debugger.js';
+import {ServiceContainer} from '@e22m4u/js-service';
+
+/**
+ * Debuggable Service.
+ */
+export class DebuggableService extends Service {
+  /**
+   * Debug.
+   */
+  debug: Debugger;
+
+  /**
+   * Возвращает функцию-отладчик с сегментом пространства имен
+   * указанного в параметре метода.
+   *
+   * @param method
+   * @protected
+   */
+  protected getDebuggerFor(method: Callable): Debugger;
+
+  /**
+   * Constructor.
+   *
+   * @param container
+   */
+  constructor(container?: ServiceContainer);
+}

+ 46 - 0
src/services/debuggable-service.js

@@ -0,0 +1,46 @@
+import {Service} from '@e22m4u/js-service';
+import {toCamelCase} from '../utils/index.js';
+import {createDebugger} from '@e22m4u/js-debug';
+
+/**
+ * Debuggable Service.
+ */
+export class DebuggableService extends Service {
+  /**
+   * Instantiation message;
+   *
+   * @type {string}
+   */
+  static INSTANTIATION_MESSAGE = 'Instantiated.';
+
+  /**
+   * Debug.
+   *
+   * @type {Function}
+   */
+  debug;
+
+  /**
+   * Возвращает функцию-отладчик с сегментом пространства имен
+   * указанного в параметре метода.
+   *
+   * @param {Function} method
+   * @returns {Function}
+   */
+  getDebuggerFor(method) {
+    return this.debug.withHash().withNs(method.name);
+  }
+
+  /**
+   * Constructor.
+   *
+   * @param {object|undefined} container
+   */
+  constructor(container) {
+    super(container);
+    const serviceName = toCamelCase(this.constructor.name);
+    this.debug = createDebugger(serviceName);
+    const debug = this.debug.withNs('constructor').withHash();
+    debug(DebuggableService.INSTANTIATION_MESSAGE);
+  }
+}

+ 60 - 0
src/services/debuggable-service.spec.js

@@ -0,0 +1,60 @@
+import {expect} from 'chai';
+import {createSpy} from '@e22m4u/js-spy';
+import {Service} from '@e22m4u/js-service';
+import {escapeRegexp, stripAnsi} from '../utils/index.js';
+import {DebuggableService} from './debuggable-service.js';
+
+describe('DebuggableService', function () {
+  let consoleLogSpy;
+  let originalDebugEnv;
+  let originalDebuggerNamespaceEnv;
+
+  beforeEach(function () {
+    // шпионим за console.log перед каждым тестом
+    consoleLogSpy = createSpy(console, 'log');
+    // сохраняем исходные переменные окружения
+    originalDebugEnv = process.env.DEBUG;
+    originalDebuggerNamespaceEnv = process.env.DEBUGGER_NAMESPACE;
+    // сбрасываем переменные перед тестом
+    delete process.env.DEBUG;
+    delete process.env.DEBUGGER_NAMESPACE;
+  });
+
+  afterEach(function () {
+    // восстанавливаем console.log
+    consoleLogSpy.restore();
+    // восстанавливаем переменные окружения
+    if (originalDebugEnv === undefined) {
+      delete process.env.DEBUG;
+    } else {
+      process.env.DEBUG = originalDebugEnv;
+    }
+    if (originalDebuggerNamespaceEnv === undefined) {
+      delete process.env.DEBUGGER_NAMESPACE;
+    } else {
+      process.env.DEBUGGER_NAMESPACE = originalDebuggerNamespaceEnv;
+    }
+  });
+
+  it('has the debug method', function () {
+    const res = new DebuggableService();
+    expect(typeof res.debug).to.be.eq('function');
+  });
+
+  describe('constructor', function () {
+    it('extends the Service class', function () {
+      const res = new DebuggableService();
+      expect(res).to.be.instanceof(Service);
+    });
+
+    it('should output the specific message when instantiated', function () {
+      process.env.DEBUG = '*';
+      new DebuggableService();
+      expect(consoleLogSpy.callCount).to.equal(1);
+      const msg = escapeRegexp(DebuggableService.INSTANTIATION_MESSAGE);
+      expect(stripAnsi(consoleLogSpy.getCall(0).args[0])).to.match(
+        new RegExp(`debuggableService:constructor:[a-f0-9]{4} ${msg}`),
+      );
+    });
+  });
+});

+ 1 - 0
src/services/index.d.ts

@@ -0,0 +1 @@
+export * from './debuggable-service.js';

+ 1 - 0
src/services/index.js

@@ -0,0 +1 @@
+export * from './debuggable-service.js';

+ 5 - 0
src/types.d.ts

@@ -0,0 +1,5 @@
+/**
+ * A function type without class and constructor.
+ */
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export type Callable<T = unknown> = (...args: any[]) => T;

+ 1 - 0
src/types.js

@@ -0,0 +1 @@
+export {};

+ 10 - 0
src/utils/escape-regexp.js

@@ -0,0 +1,10 @@
+/**
+ * Экранирует специальные символы в строке для использования в регулярном выражении.
+ *
+ * @param {string} str
+ * @returns {string}
+ */
+export function escapeRegexp(str) {
+  // $& означает всю совпавшую строку.
+  return String(str).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+}

+ 81 - 0
src/utils/escape-regexp.spec.js

@@ -0,0 +1,81 @@
+import {expect} from 'chai';
+import {escapeRegexp} from './escape-regexp.js';
+
+describe('escapeRegexp', function () {
+  it('should not change a string with no special characters', function () {
+    // проверяем, что обычная строка без спецсимволов не изменяется
+    const input = 'hello world';
+    const expected = 'hello world';
+    expect(escapeRegexp(input)).to.equal(expected);
+  });
+
+  it('should escape all special regex characters', function () {
+    // проверяем, что все специальные символы для RegExp корректно экранируются
+    const input = '.*+?^${}()|[]\\';
+    const expected = '\\.\\*\\+\\?\\^\\$\\{\\}\\(\\)\\|\\[\\]\\\\';
+    expect(escapeRegexp(input)).to.equal(expected);
+  });
+
+  it('should escape a string containing a URL', function () {
+    // проверяем экранирование в строке, которая является URL-адресом
+    const input = 'http://example.com?query=a+b';
+    const expected = 'http://example\\.com\\?query=a\\+b';
+    expect(escapeRegexp(input)).to.equal(expected);
+  });
+
+  it('should escape characters within a mixed string', function () {
+    // проверяем, что символы экранируются правильно внутри обычной строки
+    const input = 'a (very) important string [_v2.0_]';
+    const expected = 'a \\(very\\) important string \\[_v2\\.0_\\]';
+    expect(escapeRegexp(input)).to.equal(expected);
+  });
+
+  it('should correctly escape backslashes', function () {
+    // отдельно проверяем правильное экранирование обратных слэшей
+    const input = 'C:\\Users\\Test';
+    const expected = 'C:\\\\Users\\\\Test';
+    expect(escapeRegexp(input)).to.equal(expected);
+  });
+
+  it('should handle an empty string', function () {
+    // проверяем, что пустая строка обрабатывается корректно
+    const input = '';
+    const expected = '';
+    expect(escapeRegexp(input)).to.equal(expected);
+  });
+
+  it('should convert non-string input to a string and escape it', function () {
+    // тест с числом, которое содержит специальный для RegExp символ '.'
+    const inputNumber = 123.45;
+    const expectedNumber = '123\\.45';
+    expect(escapeRegexp(inputNumber)).to.equal(expectedNumber);
+
+    // тест с null.
+    const inputNull = null;
+    const expectedNull = 'null';
+    expect(escapeRegexp(inputNull)).to.equal(expectedNull);
+
+    // тест с undefined.
+    const inputUndefined = undefined;
+    const expectedUndefined = 'undefined';
+    expect(escapeRegexp(inputUndefined)).to.equal(expectedUndefined);
+  });
+
+  it('should correctly create a usable RegExp object after escaping', function () {
+    // проверяем, что после экранирования мы можем
+    // создать рабочее регулярное выражение
+    const dangerousString = 'search(v1.0)';
+    const escapedString = escapeRegexp(dangerousString);
+    const regex = new RegExp(escapedString);
+
+    // убедимся, что экранированная строка имеет ожидаемый вид
+    expect(escapedString).to.equal('search\\(v1\\.0\\)');
+
+    // созданный RegExp должен находить точное совпадение с исходной строкой
+    expect(regex.test('search(v1.0)')).to.be.true;
+
+    // и он не должен находить совпадения там, где символы могут
+    // быть неверно интерпретированы как операторы
+    expect(regex.test('search(v1a0)')).to.be.false;
+  });
+});

+ 3 - 0
src/utils/index.js

@@ -1,2 +1,5 @@
+export * from './strip-ansi.js';
+export * from './escape-regexp.js';
+export * from './to-camel-case.js';
 export * from './is-non-array-object.js';
 export * from './generate-random-hex.js';

+ 3 - 0
src/utils/strip-ansi.js

@@ -0,0 +1,3 @@
+// вспомогательная функция для удаления ANSI escape-кодов (цветов)
+// eslint-disable-next-line no-control-regex
+export const stripAnsi = str => str.replace(/\x1b\[[0-9;]*m/g, '');

+ 41 - 0
src/utils/strip-ansi.spec.js

@@ -0,0 +1,41 @@
+import {expect} from 'chai';
+import {stripAnsi} from './strip-ansi.js';
+
+describe('stripAnsi', function () {
+  it('should remove simple ANSI escape codes', function () {
+    const coloredString = '\u001b[31mHello, world\u001b[39m';
+    const result = stripAnsi(coloredString);
+    expect(result).to.equal('Hello, world');
+  });
+
+  it('should handle strings without any ANSI codes', function () {
+    const normalString = 'This is a normal string.';
+    const result = stripAnsi(normalString);
+    expect(result).to.equal('This is a normal string.');
+  });
+
+  it('should handle an empty string', function () {
+    const emptyString = '';
+    const result = stripAnsi(emptyString);
+    expect(result).to.equal('');
+  });
+
+  it('should remove multiple ANSI codes from a string', function () {
+    const multiColoredString =
+      '\u001b[31mRed\u001b[39m and \u001b[34mblue\u001b[39m';
+    const result = stripAnsi(multiColoredString);
+    expect(result).to.equal('Red and blue');
+  });
+
+  it('should remove complex ANSI codes (with multiple parameters)', function () {
+    const complexString = '\u001b[1;31mBold red text\u001b[0m';
+    const result = stripAnsi(complexString);
+    expect(result).to.equal('Bold red text');
+  });
+
+  it('should correctly handle a string containing only ANSI codes', function () {
+    const onlyAnsi = '\u001b[31m\u001b[39m\u001b[1m';
+    const result = stripAnsi(onlyAnsi);
+    expect(result).to.equal('');
+  });
+});

+ 11 - 0
src/utils/to-camel-case.js

@@ -0,0 +1,11 @@
+/**
+ * To camel case.
+ *
+ * @param input
+ */
+export function toCamelCase(input) {
+  return input
+    .replace(/(^\w|[A-Z]|\b\w)/g, c => c.toUpperCase())
+    .replace(/\W+/g, '')
+    .replace(/(^\w)/g, c => c.toLowerCase());
+}

+ 11 - 0
src/utils/to-camel-case.spec.js

@@ -0,0 +1,11 @@
+import {expect} from 'chai';
+import {toCamelCase} from './to-camel-case.js';
+
+describe('toCamelCase', function () {
+  it('returns a camelCase string', function () {
+    expect(toCamelCase('TestString')).to.be.eq('testString');
+    expect(toCamelCase('test-string')).to.be.eq('testString');
+    expect(toCamelCase('test string')).to.be.eq('testString');
+    expect(toCamelCase('Test string')).to.be.eq('testString');
+  });
+});