e22m4u 3 недель назад
Сommit
d5fd2cd003
89 измененных файлов с 5317 добавлено и 0 удалено
  1. 9 0
      .c8rc
  2. 5 0
      .commitlintrc
  3. 13 0
      .editorconfig
  4. 18 0
      .gitignore
  5. 1 0
      .husky/commit-msg
  6. 6 0
      .husky/pre-commit
  7. 4 0
      .mocharc.json
  8. 7 0
      .prettierrc
  9. 21 0
      LICENSE
  10. 303 0
      README.md
  11. 16 0
      build-cjs.js
  12. 40 0
      eslint.config.js
  13. 66 0
      package.json
  14. 74 0
      src/data-parser.d.ts
  15. 233 0
      src/data-parser.js
  16. 648 0
      src/data-parser.spec.js
  17. 6 0
      src/data-parsers/array-type-parser.d.ts
  18. 56 0
      src/data-parsers/array-type-parser.js
  19. 98 0
      src/data-parsers/array-type-parser.spec.js
  20. 6 0
      src/data-parsers/boolean-type-parser.d.ts
  21. 57 0
      src/data-parsers/boolean-type-parser.js
  22. 138 0
      src/data-parsers/boolean-type-parser.spec.js
  23. 5 0
      src/data-parsers/index.d.ts
  24. 5 0
      src/data-parsers/index.js
  25. 6 0
      src/data-parsers/number-type-parser.d.ts
  26. 52 0
      src/data-parsers/number-type-parser.js
  27. 138 0
      src/data-parsers/number-type-parser.spec.js
  28. 6 0
      src/data-parsers/object-type-parser.d.ts
  29. 60 0
      src/data-parsers/object-type-parser.js
  30. 98 0
      src/data-parsers/object-type-parser.spec.js
  31. 6 0
      src/data-parsers/string-type-parser.d.ts
  32. 47 0
      src/data-parsers/string-type-parser.js
  33. 97 0
      src/data-parsers/string-type-parser.spec.js
  34. 9 0
      src/data-schema-definition.d.ts
  35. 37 0
      src/data-schema-registry.d.ts
  36. 77 0
      src/data-schema-registry.js
  37. 72 0
      src/data-schema-registry.spec.js
  38. 14 0
      src/data-schema-resolver.d.ts
  39. 65 0
      src/data-schema-resolver.js
  40. 136 0
      src/data-schema-resolver.spec.js
  41. 36 0
      src/data-schema.d.ts
  42. 29 0
      src/data-type.d.ts
  43. 34 0
      src/data-type.js
  44. 53 0
      src/data-type.spec.js
  45. 72 0
      src/data-validator.d.ts
  46. 205 0
      src/data-validator.js
  47. 629 0
      src/data-validator.spec.js
  48. 6 0
      src/data-validators/array-type-validator.d.ts
  49. 47 0
      src/data-validators/array-type-validator.js
  50. 55 0
      src/data-validators/array-type-validator.spec.js
  51. 6 0
      src/data-validators/boolean-type-validator.d.ts
  52. 47 0
      src/data-validators/boolean-type-validator.js
  53. 55 0
      src/data-validators/boolean-type-validator.spec.js
  54. 6 0
      src/data-validators/index.d.ts
  55. 6 0
      src/data-validators/index.js
  56. 6 0
      src/data-validators/number-type-validator.d.ts
  57. 47 0
      src/data-validators/number-type-validator.js
  58. 55 0
      src/data-validators/number-type-validator.spec.js
  59. 6 0
      src/data-validators/object-type-validator.d.ts
  60. 47 0
      src/data-validators/object-type-validator.js
  61. 55 0
      src/data-validators/object-type-validator.spec.js
  62. 6 0
      src/data-validators/required-value-validator.d.ts
  63. 42 0
      src/data-validators/required-value-validator.js
  64. 46 0
      src/data-validators/required-value-validator.spec.js
  65. 6 0
      src/data-validators/string-type-validator.d.ts
  66. 47 0
      src/data-validators/string-type-validator.js
  67. 54 0
      src/data-validators/string-type-validator.spec.js
  68. 31 0
      src/errors/data-parsing-error.d.ts
  69. 54 0
      src/errors/data-parsing-error.js
  70. 26 0
      src/errors/data-parsing-error.spec.js
  71. 6 0
      src/errors/data-validation-error.d.ts
  72. 6 0
      src/errors/data-validation-error.js
  73. 17 0
      src/errors/data-validation-error.spec.js
  74. 2 0
      src/errors/index.d.ts
  75. 2 0
      src/errors/index.js
  76. 12 0
      src/index.d.ts
  77. 10 0
      src/index.js
  78. 1 0
      src/utils/index.d.ts
  79. 1 0
      src/utils/index.js
  80. 6 0
      src/utils/to-pascal-case.d.ts
  81. 24 0
      src/utils/to-pascal-case.js
  82. 15 0
      src/utils/to-pascal-case.spec.js
  83. 10 0
      src/validate-data-schema-definition.d.ts
  84. 36 0
      src/validate-data-schema-definition.js
  85. 117 0
      src/validate-data-schema-definition.spec.js
  86. 14 0
      src/validate-data-schema.d.ts
  87. 148 0
      src/validate-data-schema.js
  88. 307 0
      src/validate-data-schema.spec.js
  89. 14 0
      tsconfig.json

+ 9 - 0
.c8rc

@@ -0,0 +1,9 @@
+{
+  "all": true,
+  "include": [
+    "src/**/*.js"
+  ],
+  "exclude": [
+    "src/**/*.spec.js"
+  ]
+}

+ 5 - 0
.commitlintrc

@@ -0,0 +1,5 @@
+{
+  "extends": [
+    "@commitlint/config-conventional"
+  ]
+}

+ 13 - 0
.editorconfig

@@ -0,0 +1,13 @@
+# EditorConfig is awesome: https://EditorConfig.org
+
+# top-most EditorConfig file
+root = true
+
+# Unix-style newlines with a newline ending every file
+[*]
+end_of_line = lf
+insert_final_newline = true
+charset = utf-8
+indent_style = space
+indent_size = 2
+max_line_length = 80

+ 18 - 0
.gitignore

@@ -0,0 +1,18 @@
+# OS
+.DS_Store
+
+# Editor
+.idea
+.vscode
+
+# Npm
+node_modules
+npm-debug.log
+package-lock.json
+
+# Yarn
+.yarn/
+.yarn*
+
+# c8
+coverage

+ 1 - 0
.husky/commit-msg

@@ -0,0 +1 @@
+npx --no -- commitlint --edit $1

+ 6 - 0
.husky/pre-commit

@@ -0,0 +1,6 @@
+npm run lint:fix
+npm run format
+npm run test
+npm run build:cjs
+
+git add -A

+ 4 - 0
.mocharc.json

@@ -0,0 +1,4 @@
+{
+  "extension": ["js"],
+  "spec": "src/**/*.spec.js"
+}

+ 7 - 0
.prettierrc

@@ -0,0 +1,7 @@
+{
+  "bracketSpacing": false,
+  "singleQuote": true,
+  "printWidth": 80,
+  "trailingComma": "all",
+  "arrowParens": "avoid"
+}

+ 21 - 0
LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023-2025 Mikhail Evstropov <e22m4u@yandex.ru>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 303 - 0
README.md

@@ -0,0 +1,303 @@
+## @e22m4u/js-data-schema
+
+JavaScript модуль для работы со схемой данных.
+
+## Содержание
+
+- [Установка](#установка)
+- [Схема данных](#схема-данных)
+- [Использование](#использование)
+  - [Проверка данных](#проверка-данных)
+  - [Парсинг данных](#парсинг-данных)
+- [Пустые значения](#пустые-значения)
+- [Тесты](#тесты)
+- [Лицензия](#лицензия)
+
+## Установка
+
+```bash
+npm install @e22m4u/js-data-schema
+```
+
+Модуль поддерживает ESM и CommonJS стандарты.
+
+*ESM*
+
+```js
+import {DataValidator} from '@e22m4u/js-data-schema';
+```
+
+*CommonJS*
+
+```js
+const {DataValidator} = require('@e22m4u/js-data-schema');
+```
+
+### Схема данных
+
+Структура:
+
+```ts
+{
+  type?: DataType;
+  items?: DataSchemaObject | DataSchemaFactory | string;
+  properties?: DataSchemaProperties | DataSchemaFactory | string;
+  required?: boolean;
+}
+```
+
+#### type
+
+Параметр `type` определяет тип допустимых значений.
+
+| константа          | значение    |
+|--------------------|-------------|
+| `DataType.ANY`     | `"any"`     |
+| `DataType.STRING`  | `"string"`  |
+| `DataType.NUMBER`  | `"number"`  |
+| `DataType.BOOLEAN` | `"boolean"` |
+| `DataType.ARRAY`   | `"array"`   |
+| `DataType.OBJECT`  | `"object"`  |
+
+
+#### items
+
+Параметр `items` позволяет указать схему для элементов массива. Параметр можно
+использовать, только если явно указан тип `array` текущей схемы.
+
+```js
+{
+  type: DataType.ARRAY,
+  items: {
+    type: DataType.NUMBER
+  }
+}
+// [1, 2, 3]
+```
+
+#### properties
+
+Параметр `properties` позволяет указать схемы для каждого свойства объекта.
+Параметр можно использовать, только если явно указан тип `object` текущей схемы.
+
+```js
+{
+  type: DataType.OBJECT,
+  properties: {
+    name: {
+      type: DataType.STRING
+    },
+    age: {
+      type: DataType.NUMBER
+    }
+  }
+}
+// {
+//   name: "John",
+//   age: 32
+// }
+```
+
+#### required
+
+Параметр `required` позволяет запретить [пустые значения](#пустые-значения).
+
+```js
+{
+  type: DataType.STRING,
+  required: true
+}
+// запрещает '', undefined и null
+// см. раздел «Пустые значения»
+```
+
+## Использование
+
+Ниже приводятся примеры использования данного модуля.
+
+### Проверка данных
+
+Проверка примитивных значений.
+
+```js
+import {DataValidator, DataType} from '@e22m4u/js-data-schema';
+
+const schema = {
+  type: DataType.STRING
+};
+
+const validator = new DataValidator();
+validator.validate('str', schema); // OK
+validator.validate(undefined, schema); // OK (пустое значение)
+
+validator.validate(10, schema); // error: DataValidationError
+validator.validate(true, schema); // error: DataValidationError
+```
+
+Проверка обязательного значения.
+
+```js
+import {DataValidator, DataType} from '@e22m4u/js-data-schema';
+
+const schema = {
+  type: DataType.NUMBER,
+  required: true, // <= исключает 0, undefined и null (для типа number)
+};
+
+const validator = new DataValidator();
+validator.validate(10, schema); // OK
+
+validator.validate('str', schema); // error: DataValidationError
+validator.validate(0, schema); // error: DataValidationError
+validator.validate(true, schema); // error: DataValidationError
+```
+
+Проверка элементов массива.
+
+```js
+import {DataValidator, DataType} from '@e22m4u/js-data-schema';
+
+const schema = {
+  type: DataType.ARRAY,
+  items: {
+    type: DataType.NUMBER,
+  },
+};
+
+const validator = new DataValidator();
+validator.validate([1, 2, 3], schema); // OK
+validator.validate([], schema); // OK
+
+validator.validate('str', schema); // error: DataValidationError
+validator.validate(['a', 'b'], schema); // error: DataValidationError
+validator.validate({foo: 'bar'}, schema); // error: DataValidationError
+```
+
+Проверка свойств объекта.
+
+```js
+import {DataValidator, DataType} from '@e22m4u/js-data-schema';
+
+const schema = {
+  type: DataType.OBJECT,
+  properties: {
+    foo: {
+      type: DataType.STRING,
+    },
+    bar: {
+      type: DataType.NUMBER,
+    },
+  },
+};
+
+const validator = new DataValidator();
+validator.validate({foo: 'str', bar: 10}, schema); // OK
+validator.validate({}, schema); // OK
+
+validator.validate({foo: true}, schema); // error: DataValidationError
+validator.validate([1, 2, 3], schema); // error: DataValidationError
+```
+
+### Парсинг данных
+
+Приведение строки к числу.
+
+```js
+import {DataParser, DataType} from '@e22m4u/js-data-schema';
+
+const parser = new DataParser();
+
+const schema = {
+  type: DataType.NUMBER
+};
+
+const result = parser.parse('10', schema);
+console.log(result); // 10
+
+// строка не является числом
+parser.parse('10abc', schema); // error: DataParsingError
+```
+
+Разбор JSON строки согласно схеме массива.
+
+```js
+import {DataParser, DataType} from '@e22m4u/js-data-schema';
+
+const parser = new DataParser();
+
+const schema = {
+  type: DataType.ARRAY,
+  items: {
+    type: DataType.NUMBER, // тип элементов
+  },
+};
+
+const result = parser.parse('[1, 2, 3]', schema);
+console.log(result);
+// [1, 2, 3]
+
+// элементы массива не соответствуют схеме
+parser.parse('["str", true]', schema); // error: DataParsingError
+```
+
+Разбор JSON строки согласно схеме объекта.
+
+```js
+import {DataParser, DataType} from '@e22m4u/js-data-schema';
+
+const parser = new DataParser();
+
+const schema = {
+  type: DataType.OBJECT,
+  properties: {
+    foo: {
+      type: DataType.STRING,
+    },
+    bar: {
+      type: DataType.NUMBER,
+    },
+  },
+};
+
+const result = parser.parse(
+  '{"foo": "str", "bar": 10}',
+  schema,
+);
+console.log(result);
+// {
+//   "foo": "str",
+//   "bar": 10
+// }
+
+// значения полей не соответствуют схеме
+parser.parse('{"foo": true, "bar": "str"}', schema); // error: DataParsingError
+```
+
+## Пустые значения
+
+Разные типы данных имеют свои наборы пустых значений. Эти наборы используются
+для определения наличия полезной нагрузки в значении свойства. Например,
+параметр `required` исключает пустые значения при проверке данных. 
+
+| тип         | пустые значения           |
+|-------------|---------------------------|
+| `'any'`     | `undefined`, `null`       |
+| `'string'`  | `undefined`, `null`, `''` |
+| `'number'`  | `undefined`, `null`, `0`  |
+| `'boolean'` | `undefined`, `null`       |
+| `'array'`   | `undefined`, `null`, `[]` |
+| `'object'`  | `undefined`, `null`, `{}` |
+
+Управление этими наборами осуществляется через специальный сервис, который предоставляет модуль
+[@e22m4u/js-empty-values](https://www.npmjs.com/package/@e22m4u/js-empty-values)
+(не требует установки).
+
+## Тесты
+
+```bash
+npm run test
+```
+
+## Лицензия
+
+MIT

+ 16 - 0
build-cjs.js

@@ -0,0 +1,16 @@
+import * as esbuild from 'esbuild';
+import packageJson from './package.json' with {type: 'json'};
+
+await esbuild.build({
+  entryPoints: ['src/index.js'],
+  outfile: 'dist/cjs/index.cjs',
+  format: 'cjs',
+  platform: 'node',
+  target: ['node12'],
+  bundle: true,
+  keepNames: true,
+  external: [
+    ...Object.keys(packageJson.peerDependencies || {}),
+    ...Object.keys(packageJson.dependencies || {}),
+  ],
+});

+ 40 - 0
eslint.config.js

@@ -0,0 +1,40 @@
+import globals from 'globals';
+import eslintJs from '@eslint/js';
+import eslintJsdocPlugin from 'eslint-plugin-jsdoc';
+import eslintMochaPlugin from 'eslint-plugin-mocha';
+import eslintImportPlugin from 'eslint-plugin-import';
+import eslintPrettierConfig from 'eslint-config-prettier';
+import eslintChaiExpectPlugin from 'eslint-plugin-chai-expect';
+
+export default [{
+  languageOptions: {
+    globals: {
+      ...globals.node,
+      ...globals.es2021,
+      ...globals.mocha,
+    },
+  },
+  plugins: {
+    'jsdoc': eslintJsdocPlugin,
+    'mocha': eslintMochaPlugin,
+    'import': eslintImportPlugin,
+    'chai-expect': eslintChaiExpectPlugin,
+  },
+  rules: {
+    ...eslintJs.configs.recommended.rules,
+    ...eslintPrettierConfig.rules,
+    ...eslintImportPlugin.flatConfigs.recommended.rules,
+    ...eslintMochaPlugin.configs.recommended.rules,
+    ...eslintChaiExpectPlugin.configs['recommended-flat'].rules,
+    ...eslintJsdocPlugin.configs['flat/recommended-error'].rules,
+    'no-duplicate-imports': 'error',
+    'import/export': 0,
+    'jsdoc/reject-any-type': 0,
+    'jsdoc/reject-function-type': 0,
+    'jsdoc/require-param-description': 0,
+    'jsdoc/require-returns-description': 0,
+    'jsdoc/require-property-description': 0,
+    'jsdoc/tag-lines': ['error', 'any', {startLines: 1}],
+  },
+  files: ['src/**/*.js'],
+}];

+ 66 - 0
package.json

@@ -0,0 +1,66 @@
+{
+  "name": "@e22m4u/js-data-schema",
+  "version": "0.0.0",
+  "description": "JavaScript модуль для работы со схемой данных",
+  "author": "Mikhail Evstropov <e22m4u@yandex.ru>",
+  "license": "MIT",
+  "keywords": [
+    "data",
+    "schema",
+    "parsing",
+    "validation"
+  ],
+  "homepage": "https://gitrepos.ru/e22m4u/js-data-schema",
+  "repository": {
+    "type": "git",
+    "url": "git+https://gitrepos.ru/e22m4u/js-data-schema.git"
+  },
+  "type": "module",
+  "types": "./src/index.d.ts",
+  "module": "./src/index.js",
+  "main": "./dist/cjs/index.cjs",
+  "exports": {
+    "types": "./src/index.d.ts",
+    "import": "./src/index.js",
+    "require": "./dist/cjs/index.cjs"
+  },
+  "engines": {
+    "node": ">=12"
+  },
+  "scripts": {
+    "lint": "tsc && eslint ./src",
+    "lint:fix": "tsc && eslint ./src --fix",
+    "format": "prettier --write \"./src/**/*.js\"",
+    "test": "npm run lint && c8 --reporter=text-summary mocha --bail",
+    "test:coverage": "npm run lint && c8 --reporter=text mocha --bail",
+    "build:cjs": "rimraf ./dist/cjs && node build-cjs.js",
+    "prepare": "husky"
+  },
+  "dependencies": {
+    "@e22m4u/js-empty-values": "~0.2.1",
+    "@e22m4u/js-format": "~0.3.2",
+    "@e22m4u/js-service": "~0.5.1"
+  },
+  "devDependencies": {
+    "@commitlint/cli": "~20.2.0",
+    "@commitlint/config-conventional": "~20.2.0",
+    "@eslint/js": "~9.39.2",
+    "@types/chai": "~5.2.3",
+    "@types/mocha": "~10.0.10",
+    "c8": "~10.1.3",
+    "chai": "~6.2.1",
+    "esbuild": "~0.27.1",
+    "eslint": "~9.39.2",
+    "eslint-config-prettier": "~10.1.8",
+    "eslint-plugin-chai-expect": "~3.1.0",
+    "eslint-plugin-import": "~2.32.0",
+    "eslint-plugin-jsdoc": "~61.5.0",
+    "eslint-plugin-mocha": "~11.2.0",
+    "globals": "~16.5.0",
+    "husky": "~9.1.7",
+    "mocha": "~11.7.5",
+    "prettier": "~3.7.4",
+    "rimraf": "~6.1.2",
+    "typescript": "~5.9.3"
+  }
+}

+ 74 - 0
src/data-parser.d.ts

@@ -0,0 +1,74 @@
+import {Service, ServiceContainer} from '@e22m4u/js-service';
+import {DataSchema, DataSchemaObject} from './data-schema.js';
+import {DataSchemaDefinition} from './data-schema-definition.js';
+
+/**
+ * Data parsing option.
+ */
+export type DataParsingOptions = {
+  sourcePath?: string;
+  shallowMode?: boolean;
+  noParsingErrors?: boolean;
+};
+
+/**
+ * Data parsing function.
+ */
+export type DataParsingFunction = (
+  value: unknown,
+  schema: DataSchemaObject,
+  options: DataParsingOptions | undefined,
+  container: ServiceContainer,
+) => unknown;
+
+/**
+ * Data parser.
+ */
+export declare class DataParser extends Service {
+  /**
+   * Get parsers.
+   */
+  getParsers(): DataParsingFunction[];
+
+  /**
+   * Set parsers.
+   *
+   * @param list
+   */
+  setParsers(list: DataParsingFunction[]): this;
+
+
+  /**
+   * Define schema.
+   * 
+   * @param schemaDef 
+   */
+  defineSchema(schemaDef: DataSchemaDefinition): this;
+
+  /**
+   * Has schema.
+   * 
+   * @param schemaName 
+   */
+  hasSchema(schemaName: string): boolean;
+
+  /**
+   * Get schema.
+   * 
+   * @param schemaName 
+   */
+  getSchema(schemaName: string): DataSchema;
+
+  /**
+   * Parse.
+   *
+   * @param value
+   * @param schema
+   * @param options
+   */
+  parse<T = any>(
+    value: unknown,
+    schema: DataSchema,
+    options?: DataParsingOptions,
+  ): T;
+}

+ 233 - 0
src/data-parser.js

@@ -0,0 +1,233 @@
+import {DataType} from './data-type.js';
+import {Service} from '@e22m4u/js-service';
+import {DataValidator} from './data-validator.js';
+import {InvalidArgumentError} from '@e22m4u/js-format';
+import {validateDataSchema} from './validate-data-schema.js';
+import {DataSchemaResolver} from './data-schema-resolver.js';
+import {DataSchemaRegistry} from './data-schema-registry.js';
+
+import {
+  arrayTypeParser,
+  stringTypeParser,
+  numberTypeParser,
+  objectTypeParser,
+  booleanTypeParser,
+} from './data-parsers/index.js';
+
+/**
+ * Data parser.
+ */
+export class DataParser extends Service {
+  /**
+   * Parsers.
+   *
+   * @type {Function[]}
+   */
+  _parsers = [
+    stringTypeParser,
+    booleanTypeParser,
+    numberTypeParser,
+    arrayTypeParser,
+    objectTypeParser,
+  ];
+
+  /**
+   * Get parsers.
+   *
+   * @returns {Function[]}
+   */
+  getParsers() {
+    return [...this._parsers];
+  }
+
+  /**
+   * Set parsers.
+   *
+   * @param {Function[]} list
+   * @returns {this}
+   */
+  setParsers(list) {
+    if (!Array.isArray(list)) {
+      throw new InvalidArgumentError(
+        'Data parsers must be an Array, but %v was given.',
+        list,
+      );
+    }
+    list.forEach(parser => {
+      if (typeof parser !== 'function') {
+        throw new InvalidArgumentError(
+          'Data parser must be a Function, but %v was given.',
+          parser,
+        );
+      }
+    });
+    this._parsers = [...list];
+    return this;
+  }
+
+  /**
+   * Define schema.
+   * 
+   * @param {object} schemaDef 
+   * @returns {this}
+   */
+  defineSchema(schemaDef) {
+    this.getService(DataSchemaRegistry).defineSchema(schemaDef);
+    return this;
+  }
+
+  /**
+   * Has schema.
+   * 
+   * @param {string} schemaName 
+   * @returns {boolean}
+   */
+  hasSchema(schemaName) {
+    return this.getService(DataSchemaRegistry).hasSchema(schemaName);
+  }
+
+  /**
+   * Get schema.
+   * 
+   * @param {string} schemaName 
+   * @returns {object}
+   */
+  getSchema(schemaName) {
+    return this.getService(DataSchemaRegistry).getSchema(schemaName);
+  }
+
+  /**
+   * Parse.
+   *
+   * @param {*} value
+   * @param {object|Function|string} schema
+   * @param {object} [options]
+   * @returns {*}
+   */
+  parse(value, schema, options) {
+    if (options !== undefined) {
+      if (
+        options === null ||
+        typeof options !== 'object' ||
+        Array.isArray(options)
+      ) {
+        throw new InvalidArgumentError(
+          'Parsing options must be an Object, but %v was given.',
+          options,
+        );
+      }
+      if (options.sourcePath !== undefined) {
+        if (!options.sourcePath || typeof options.sourcePath !== 'string') {
+          throw new InvalidArgumentError(
+            'Option "sourcePath" must be a non-empty String, but %v was given.',
+            options.sourcePath,
+          );
+        }
+      }
+      if (options.shallowMode !== undefined) {
+        if (typeof options.shallowMode !== 'boolean') {
+          throw new InvalidArgumentError(
+            'Option "shallowMode" must be a Boolean, but %v was given.',
+            options.shallowMode,
+          );
+        }
+      }
+      if (options.noParsingErrors !== undefined) {
+        if (typeof options.noParsingErrors !== 'boolean') {
+          throw new InvalidArgumentError(
+            'Option "noParsingErrors" must be a Boolean, but %v was given.',
+            options.noParsingErrors,
+          );
+        }
+      }
+    }
+    const sourcePath = (options && options.sourcePath) || undefined;
+    const shallowMode = Boolean(options && options.shallowMode);
+    const noParsingErrors = Boolean(options && options.noParsingErrors);
+    // поверхностная проверка схемы
+    // (режим shallowMode)
+    validateDataSchema(schema, true);
+    // если схема данных не является объектом,
+    // то выполняется извлечение схемы данных
+    const schemaResolver = this.getService(DataSchemaResolver);
+    if (typeof schema !== 'object') {
+      schema = schemaResolver.resolve(schema);
+    }
+    // передача значения через каждую функцию
+    // преобразования в соответствующем порядке
+    value = this._parsers.reduce((input, parser) => {
+      return parser(input, schema, options, this.container);
+    }, value);
+    // если значение является массивом или объектом,
+    // то выполняется обработка вложенных элементов
+    if (!shallowMode) {
+      // если значение является массивом, то выполняется
+      // преобразование каждого элемента по схеме, указанной
+      // в опции items
+      if (Array.isArray(value) && schema.items !== undefined) {
+        // чтобы избежать изменения оригинального массива,
+        // выполняется его поверхностное копирование
+        value = [...value];
+        value.forEach((item, index) => {
+          const itemPath = (sourcePath || 'array') + `[${index}]`;
+          const itemOptions = {...options, sourcePath: itemPath};
+          value[index] = this.parse(item, schema.items, itemOptions);
+        });
+      }
+      // если значение является объектом, то выполняется
+      // рекурсивный обход каждого свойства
+      else if (
+        value !== null &&
+        typeof value === 'object' &&
+        schema.properties !== undefined
+      ) {
+        let propsSchema = schema.properties;
+        // чтобы избежать изменения оригинального объекта,
+        // выполняется его поверхностное копирование
+        value = {...value};
+        // если схема свойств не является объектом,
+        // то выполняется извлечение схемы данных
+        if (typeof propsSchema !== 'object') {
+          const resolvedSchema = schemaResolver.resolve(propsSchema);
+          // если извлеченная схема не является
+          // схемой объекта, то выбрасывается ошибка
+          if (resolvedSchema.type !== DataType.OBJECT) {
+            throw new InvalidArgumentError(
+              'Unable to get the "properties" option ' +
+                'from the data schema of %v type.',
+              resolvedSchema.type || DataType.ANY,
+            );
+          }
+          propsSchema = resolvedSchema.properties || {};
+        }
+        Object.keys(propsSchema).forEach(propName => {
+          const propSchema = propsSchema[propName];
+          // если схема свойства не определена,
+          // то преобразование пропускается
+          if (propSchema === undefined) {
+            return;
+          }
+          const propValue = value[propName];
+          const propPath = sourcePath ? sourcePath + `.${propName}` : propName;
+          const propOptions = {...options, sourcePath: propPath};
+          const newPropValue = this.parse(propValue, propSchema, propOptions);
+          // исходный объект может не иметь ключа данного свойства,
+          // и чтобы избежать его добавления, выполняется проверка
+          // на отличие старого и нового значения, таким образом,
+          // значение undefined не будет присвоено свойству,
+          // которого нет (новый ключ не будет добавлен)
+          if (value[propName] !== newPropValue) {
+            value[propName] = newPropValue;
+          }
+        });
+      }
+    }
+    // если допускается выброс ошибок, то результирующее
+    // значение проверяется согласно схеме данных
+    if (!noParsingErrors) {
+      const validator = this.getService(DataValidator);
+      validator.validate(value, schema, {shallowMode: true});
+    }
+    return value;
+  }
+}

+ 648 - 0
src/data-parser.spec.js

@@ -0,0 +1,648 @@
+import {expect} from 'chai';
+import {DataType} from './data-type.js';
+import {format} from '@e22m4u/js-format';
+import {DataParser} from './data-parser.js';
+import {DataSchemaRegistry} from './data-schema-registry.js';
+
+import {
+  arrayTypeParser,
+  numberTypeParser,
+  objectTypeParser,
+  stringTypeParser,
+  booleanTypeParser,
+} from './data-parsers/index.js';
+
+describe('DataParser', function () {
+  describe('getParsers', function () {
+    it('should return a default parser list', function () {
+      const S = new DataParser();
+      const res = S.getParsers();
+      expect(res).to.be.eql([
+        stringTypeParser,
+        booleanTypeParser,
+        numberTypeParser,
+        arrayTypeParser,
+        objectTypeParser,
+      ]);
+    });
+
+    it('should return a modified parser list', function () {
+      const S = new DataParser();
+      const parser1 = () => undefined;
+      const parser2 = () => undefined;
+      S.setParsers([parser1, parser2]);
+      const res = S.getParsers();
+      expect(res).to.be.eql([parser1, parser2]);
+    });
+  });
+
+  describe('setParsers', function () {
+    it('should require a given value to be an array', function () {
+      const S = new DataParser();
+      const throwable = v => () => S.setParsers(v);
+      const error = s =>
+        format('Data parsers must be an Array, but %s was given.', s);
+      expect(throwable('str')).to.throw(error('"str"'));
+      expect(throwable('')).to.throw(error('""'));
+      expect(throwable(10)).to.throw(error('10'));
+      expect(throwable(0)).to.throw(error('0'));
+      expect(throwable(true)).to.throw(error('true'));
+      expect(throwable(false)).to.throw(error('false'));
+      expect(throwable({})).to.throw(error('Object'));
+      expect(throwable(null)).to.throw(error('null'));
+      expect(throwable(undefined)).to.throw(error('undefined'));
+      throwable([])();
+    });
+
+    it('should require given parsers to be a function', function () {
+      const S = new DataParser();
+      const throwable = v => () => S.setParsers([v]);
+      const error = s =>
+        format('Data parser must be a Function, but %s was given.', s);
+      expect(throwable('str')).to.throw(error('"str"'));
+      expect(throwable('')).to.throw(error('""'));
+      expect(throwable(10)).to.throw(error('10'));
+      expect(throwable(0)).to.throw(error('0'));
+      expect(throwable(true)).to.throw(error('true'));
+      expect(throwable(false)).to.throw(error('false'));
+      expect(throwable([])).to.throw(error('Array'));
+      expect(throwable({})).to.throw(error('Object'));
+      expect(throwable(null)).to.throw(error('null'));
+      expect(throwable(undefined)).to.throw(error('undefined'));
+      throwable(() => undefined)();
+    });
+
+    it('should set the parsers list', function () {
+      const S = new DataParser();
+      const parser1 = () => undefined;
+      const parser2 = () => undefined;
+      S.setParsers([parser1, parser2]);
+      const res1 = S.getParsers();
+      expect(res1).to.be.eql([parser1, parser2]);
+    });
+
+    it('should able to clean the parser list', function () {
+      const S = new DataParser();
+      const parser1 = () => undefined;
+      const parser2 = () => undefined;
+      S.setParsers([parser1, parser2]);
+      const res1 = S.getParsers();
+      expect(res1).to.be.eql([parser1, parser2]);
+      S.setParsers([]);
+      const res2 = S.getParsers();
+      expect(res2).to.be.eql([]);
+    });
+  });
+
+  describe('defineSchema', function() {
+    it('should pass a given schema definition to the registry', function() {
+      const S = new DataParser();
+      const registry = S.getService(DataSchemaRegistry);
+      const schemaDef = {name: 'mySchema', schema: {}};
+      S.defineSchema(schemaDef);
+      const res = registry.getDefinition(schemaDef.name);
+      expect(res).to.be.eql(schemaDef);
+    });
+
+    it('should return a current instance', function() {
+      const S = new DataParser();
+      const schemaDef = {name: 'mySchema', schema: {}};
+      const res = S.defineSchema(schemaDef);
+      expect(res).to.be.eq(S);
+    });
+  });
+
+  describe('hasSchema', function() {
+    it('should return true if a given name is registered', function() {
+      const S = new DataParser();
+      const schemaDef = {name: 'mySchema', schema: {}};
+      expect(S.hasSchema(schemaDef.name)).to.be.false;
+      S.defineSchema(schemaDef);
+      expect(S.hasSchema(schemaDef.name)).to.be.true;
+    });
+  });
+  
+  describe('getSchema', function() {
+    it('should return a register schema for a given name', function() {
+      const S = new DataParser();
+      const schemaDef = {name: 'mySchema', schema: {}};
+      S.defineSchema(schemaDef);
+      const res = S.getSchema(schemaDef.name);
+      expect(res).to.be.eql(schemaDef.schema);
+    });
+
+    it('should throw an error if a given name is not registered', function() {
+      const S = new DataParser();
+      const throwable = () => S.getSchema('mySchema');
+      expect(throwable).to.throw('Data schema "mySchema" is not found.');
+    });
+  });
+
+  describe('parse', function () {
+    it('should require the "options" argument to be an object', function () {
+      const S = new DataParser();
+      const throwable = v => () => S.parse(10, {}, v);
+      const error = s =>
+        format('Parsing options must be an Object, but %s was given.', s);
+      expect(throwable('str')).to.throw(error('"str"'));
+      expect(throwable('')).to.throw(error('""'));
+      expect(throwable(10)).to.throw(error('10'));
+      expect(throwable(0)).to.throw(error('0'));
+      expect(throwable(true)).to.throw(error('true'));
+      expect(throwable(false)).to.throw(error('false'));
+      expect(throwable([])).to.throw(error('Array'));
+      expect(throwable(null)).to.throw(error('null'));
+      expect(throwable(() => undefined)).to.throw(error('Function'));
+      throwable({})();
+      throwable(undefined)();
+    });
+
+    it('should require the "sourcePath" argument to be a non-empty string', function () {
+      const S = new DataParser();
+      const throwable = v => () => S.parse(10, {}, {sourcePath: v});
+      const error = s =>
+        format(
+          'Option "sourcePath" must be a non-empty String, but %s was given.',
+          s,
+        );
+      expect(throwable('')).to.throw(error('""'));
+      expect(throwable(10)).to.throw(error('10'));
+      expect(throwable(0)).to.throw(error('0'));
+      expect(throwable(true)).to.throw(error('true'));
+      expect(throwable(false)).to.throw(error('false'));
+      expect(throwable([])).to.throw(error('Array'));
+      expect(throwable({})).to.throw(error('Object'));
+      expect(throwable(null)).to.throw(error('null'));
+      expect(throwable(() => undefined)).to.throw(error('Function'));
+      throwable('str')();
+      throwable(undefined)();
+    });
+
+    it('should require the "shallowMode" argument to be a boolean', function () {
+      const S = new DataParser();
+      const throwable = v => () => S.parse(10, {}, {shallowMode: v});
+      const error = s =>
+        format('Option "shallowMode" must be a Boolean, but %s was given.', s);
+      expect(throwable('str')).to.throw(error('"str"'));
+      expect(throwable('')).to.throw(error('""'));
+      expect(throwable(10)).to.throw(error('10'));
+      expect(throwable(0)).to.throw(error('0'));
+      expect(throwable([])).to.throw(error('Array'));
+      expect(throwable({})).to.throw(error('Object'));
+      expect(throwable(null)).to.throw(error('null'));
+      expect(throwable(() => undefined)).to.throw(error('Function'));
+      throwable(true)();
+      throwable(false)();
+      throwable(undefined)();
+    });
+
+    it('should require the "noParsingErrors" argument to be a boolean', function () {
+      const S = new DataParser();
+      const throwable = v => () => S.parse(10, {}, {noParsingErrors: v});
+      const error = s =>
+        format(
+          'Option "noParsingErrors" must be a Boolean, but %s was given.',
+          s,
+        );
+      expect(throwable('str')).to.throw(error('"str"'));
+      expect(throwable('')).to.throw(error('""'));
+      expect(throwable(10)).to.throw(error('10'));
+      expect(throwable(0)).to.throw(error('0'));
+      expect(throwable([])).to.throw(error('Array'));
+      expect(throwable({})).to.throw(error('Object'));
+      expect(throwable(null)).to.throw(error('null'));
+      expect(throwable(() => undefined)).to.throw(error('Function'));
+      throwable(true)();
+      throwable(false)();
+      throwable(undefined)();
+    });
+
+    it('should validate a given schema in the shallow mode', function () {
+      const S = new DataParser();
+      const throwable = () => S.parse(10, {required: 10});
+      expect(throwable).to.throw(
+        'Schema option "required" must be a Boolean, but 10 was given.',
+      );
+      S.parse([], {type: DataType.ARRAY, items: {type: 10}});
+    });
+
+    it('should resolve the data schema from a given factory', function () {
+      const S = new DataParser();
+      S.setParsers([stringTypeParser]);
+      const res = S.parse(10, () => ({type: DataType.STRING}));
+      expect(res).to.be.eq('10');
+    });
+
+    it('should resolve the data schema from a schema name', function () {
+      const S = new DataParser();
+      S.setParsers([stringTypeParser]);
+      S.getService(DataSchemaRegistry).defineSchema({
+        name: 'mySchema',
+        schema: {type: DataType.STRING},
+      });
+      const res = S.parse(10, 'mySchema');
+      expect(res).to.be.eq('10');
+    });
+
+    it('should resolve a schema name from a given factory', function () {
+      const S = new DataParser();
+      S.setParsers([stringTypeParser]);
+      S.getService(DataSchemaRegistry).defineSchema({
+        name: 'mySchema',
+        schema: {type: DataType.STRING},
+      });
+      const res = S.parse(10, () => 'mySchema');
+      expect(res).to.be.eq('10');
+    });
+
+    it('should resolve a schema factory from a named schema', function () {
+      const S = new DataParser();
+      S.setParsers([stringTypeParser]);
+      S.getService(DataSchemaRegistry).defineSchema({
+        name: 'mySchema',
+        schema: () => ({type: DataType.STRING}),
+      });
+      const res = S.parse(10, 'mySchema');
+      expect(res).to.be.eq('10');
+    });
+
+    it('should pass specific arguments to data parsers', function () {
+      const S = new DataParser();
+      const value = 10;
+      const schema = {type: DataType.NUMBER};
+      const options = {sourcePath: 'mySource'};
+      let invoked = 0;
+      const parser = (...args) => {
+        invoked++;
+        expect(args).to.be.eql([value, schema, options, S.container]);
+        return args[0];
+      };
+      S.setParsers([parser]);
+      const res = S.parse(value, schema, options);
+      expect(res).to.be.eq(value);
+      expect(invoked).to.be.eq(1);
+    });
+
+    it('should propagate an error from the data parser', function () {
+      const S = new DataParser();
+      const parser = () => {
+        throw new Error('Caught!');
+      };
+      S.setParsers([parser]);
+      const throwable = () => S.parse(10, {});
+      expect(throwable).to.throw('Caught!');
+    });
+
+    it('should apply parsers sequentially to a given value', function () {
+      const S = new DataParser();
+      const value = 'a';
+      S.setParsers([v => v + 'b', v => v + 'c']);
+      const res = S.parse(value, {});
+      expect(res).to.be.eq('abc');
+    });
+
+    it('should not parse array items in the shallow mode even if the items schema is provided', function () {
+      const S = new DataParser();
+      const value = [1, 2, 3];
+      const schema = {
+        type: DataType.ARRAY,
+        items: {type: DataType.STRING},
+      };
+      S.setParsers([stringTypeParser]);
+      const res = S.parse(value, schema, {shallowMode: true});
+      expect(res).to.be.eql(value);
+    });
+
+    it('should not parse array items when the items schema is not provided', function () {
+      const S = new DataParser();
+      const value = [1, 2, 3];
+      const schema = {type: DataType.ARRAY};
+      let invoked = 0;
+      const parser = (...args) => {
+        invoked++;
+        expect(args[0]).to.be.eql(value);
+        return args[0];
+      };
+      S.setParsers([parser]);
+      const res = S.parse(value, schema);
+      expect(res).to.be.eql(value);
+      expect(invoked).to.be.eq(1);
+    });
+
+    it('should parse array items when the array schema is provided', function () {
+      const S = new DataParser();
+      const value = [1, 2, 3];
+      const schema = {
+        type: DataType.ARRAY,
+        items: {type: DataType.STRING},
+      };
+      const expectedCalls = [
+        [value, schema, undefined, S.container],
+        [1, schema.items, {sourcePath: 'array[0]'}, S.container],
+        [2, schema.items, {sourcePath: 'array[1]'}, S.container],
+        [3, schema.items, {sourcePath: 'array[2]'}, S.container],
+      ];
+      const calls = [];
+      const parser = (...args) => {
+        calls.push(args);
+        return Array.isArray(args[0]) ? args[0] : String(args[0]);
+      };
+      S.setParsers([parser]);
+      const res = S.parse(value, schema);
+      expect(res).to.be.eql(['1', '2', '3']);
+      expect(expectedCalls).to.be.eql(calls);
+    });
+
+    it('should add an array index to a provided source path', function () {
+      const S = new DataParser();
+      const value = [1, 2, 3];
+      const schema = {
+        type: DataType.ARRAY,
+        items: {type: DataType.STRING},
+      };
+      const options = {sourcePath: 'mySource'};
+      const expectedCalls = [
+        [value, schema, options, S.container],
+        [1, schema.items, {sourcePath: 'mySource[0]'}, S.container],
+        [2, schema.items, {sourcePath: 'mySource[1]'}, S.container],
+        [3, schema.items, {sourcePath: 'mySource[2]'}, S.container],
+      ];
+      const calls = [];
+      const parser = (...args) => {
+        calls.push(args);
+        return Array.isArray(args[0]) ? args[0] : String(args[0]);
+      };
+      S.setParsers([parser]);
+      const res = S.parse(value, schema, options);
+      expect(res).to.be.eql(['1', '2', '3']);
+      expect(expectedCalls).to.be.eql(calls);
+    });
+
+    it('should resolve a schema factory from the "items" option', function () {
+      const S = new DataParser();
+      S.setParsers([stringTypeParser]);
+      const factory = () => ({type: DataType.STRING});
+      const schema = {type: DataType.ARRAY, items: factory};
+      const res = S.parse([10], schema);
+      expect(res).to.be.eql(['10']);
+    });
+
+    it('should resolve a schema name from the "items" option', function () {
+      const S = new DataParser();
+      S.setParsers([stringTypeParser]);
+      const schemaA = {
+        type: DataType.ARRAY,
+        items: 'schemaB',
+      };
+      S.getService(DataSchemaRegistry).defineSchema({
+        name: 'schemaB',
+        schema: {type: DataType.STRING},
+      });
+      const res = S.parse([10], schemaA);
+      expect(res).to.be.eql(['10']);
+    });
+
+    it('should not parse object properties in the shallow mode even if the properties schema is provided', function () {
+      const S = new DataParser();
+      const value = {p1: 1, p2: 2, p3: 3};
+      const schema = {
+        type: DataType.OBJECT,
+        properties: {
+          p1: {type: DataType.STRING},
+          p2: {type: DataType.STRING},
+          p3: {type: DataType.STRING},
+        },
+      };
+      S.setParsers([stringTypeParser]);
+      const res = S.parse(value, schema, {shallowMode: true});
+      expect(res).to.be.eql(value);
+    });
+
+    it('should not parse object properties when the properties schema is not provided', function () {
+      const S = new DataParser();
+      const value = {p1: 1, p2: 2, p3: 3};
+      const schema = {type: DataType.OBJECT};
+      let invoked = 0;
+      const parser = (...args) => {
+        invoked++;
+        expect(args).to.be.eql([value, schema, undefined, S.container]);
+        return args[0];
+      };
+      S.setParsers([parser]);
+      const res = S.parse(value, schema);
+      expect(res).to.be.eql(value);
+      expect(invoked).to.be.eq(1);
+    });
+
+    it('should parse object properties when the properties schema is provided', function () {
+      const S = new DataParser();
+      const value = {p1: 1, p2: 2, p3: 3};
+      const schema = {
+        type: DataType.OBJECT,
+        properties: {
+          p1: {type: DataType.STRING},
+          p2: {type: DataType.STRING},
+          p3: {type: DataType.STRING},
+        },
+      };
+      const expectedCalls = [
+        [value, schema, undefined, S.container],
+        [1, schema.properties.p1, {sourcePath: 'p1'}, S.container],
+        [2, schema.properties.p2, {sourcePath: 'p2'}, S.container],
+        [3, schema.properties.p3, {sourcePath: 'p3'}, S.container],
+      ];
+      const calls = [];
+      const parser = (...args) => {
+        calls.push(args);
+        return typeof args[0] === 'object' ? args[0] : String(args[0]);
+      };
+      S.setParsers([parser]);
+      const res = S.parse(value, schema);
+      expect(res).to.be.eql({p1: '1', p2: '2', p3: '3'});
+      expect(expectedCalls).to.be.eql(calls);
+    });
+
+    it('should apply parsers sequentially to a given object and its properties', function () {
+      const S = new DataParser();
+      const value = {p1: 'a', p2: 'b', p3: 'c'};
+      const schema = {
+        type: DataType.OBJECT,
+        properties: {
+          p1: {type: DataType.STRING},
+          p2: {type: DataType.STRING},
+          p3: {type: DataType.STRING},
+        },
+      };
+      const parser = suffix => value => {
+        return typeof value === 'string' ? value + suffix : value;
+      };
+      S.setParsers([parser('b'), parser('c')]);
+      const res = S.parse(value, schema);
+      expect(res).to.be.eql({p1: 'abc', p2: 'bbc', p3: 'cbc'});
+    });
+
+    it('should not parse object properties without a specified schema', function () {
+      const S = new DataParser();
+      const value = {p1: 1, p2: 2, p3: 3};
+      const schema = {
+        type: DataType.OBJECT,
+        properties: {
+          p1: {type: DataType.NUMBER},
+        },
+      };
+      const expectedCalls = [
+        [value, schema, undefined, S.container],
+        [value.p1, schema.properties.p1, {sourcePath: 'p1'}, S.container],
+      ];
+      const calls = [];
+      const parser = (...args) => {
+        calls.push(args);
+        return args[0];
+      };
+      S.setParsers([parser]);
+      const res = S.parse(value, schema);
+      expect(res).to.be.eql(value);
+      expect(expectedCalls).to.be.eql(calls);
+    });
+
+    it('should add property name to a provided source path', function () {
+      const S = new DataParser();
+      const value = {p1: 1, p2: 2, p3: 3};
+      const schema = {
+        type: DataType.OBJECT,
+        properties: {
+          p1: {type: DataType.STRING},
+          p2: {type: DataType.STRING},
+          p3: {type: DataType.STRING},
+        },
+      };
+      const options = {sourcePath: 'mySource'};
+      const expectedCalls = [
+        [value, schema, options, S.container],
+        [1, schema.properties.p1, {sourcePath: 'mySource.p1'}, S.container],
+        [2, schema.properties.p2, {sourcePath: 'mySource.p2'}, S.container],
+        [3, schema.properties.p3, {sourcePath: 'mySource.p3'}, S.container],
+      ];
+      const calls = [];
+      const parser = (...args) => {
+        calls.push(args);
+        return typeof args[0] === 'object' ? args[0] : String(args[0]);
+      };
+      S.setParsers([parser]);
+      const res = S.parse(value, schema, options);
+      expect(res).to.be.eql({p1: '1', p2: '2', p3: '3'});
+      expect(expectedCalls).to.be.eql(calls);
+    });
+
+    it('should resolve a schema factory from the "properties" option', function () {
+      const S = new DataParser();
+      S.setParsers([stringTypeParser]);
+      const factory = () => ({
+        type: DataType.OBJECT,
+        properties: {
+          prop: {type: DataType.STRING},
+        },
+      });
+      const schema = {
+        type: DataType.OBJECT,
+        properties: factory,
+      };
+      const res = S.parse({prop: 10}, schema);
+      expect(res).to.be.eql({prop: '10'});
+    });
+
+    it('should resolve a schema name from the "properties" option', function () {
+      const S = new DataParser();
+      S.setParsers([stringTypeParser]);
+      const schemaA = {
+        type: DataType.OBJECT,
+        properties: 'schemaB',
+      };
+      S.getService(DataSchemaRegistry).defineSchema({
+        name: 'schemaB',
+        schema: {
+          type: DataType.OBJECT,
+          properties: {
+            prop: {type: DataType.STRING},
+          },
+        },
+      });
+      const res = S.parse({prop: 10}, schemaA);
+      expect(res).to.be.eql({prop: '10'});
+    });
+
+    it('should throw an error if a properties schema from the schema factory is a non-array schema', function () {
+      const S = new DataParser();
+      const factory = () => ({type: DataType.BOOLEAN});
+      const schema = {
+        type: DataType.OBJECT,
+        properties: factory,
+      };
+      const throwable = () => S.parse({prop: 10}, schema);
+      expect(throwable).to.throw(
+        'Unable to get the "properties" option ' +
+          'from the data schema of "boolean" type.',
+      );
+    });
+
+    it('should throw an error if a properties schema from the schema name is a non-array schema', function () {
+      const S = new DataParser();
+      const schemaA = {
+        type: DataType.OBJECT,
+        properties: 'schemaB',
+      };
+      S.getService(DataSchemaRegistry).defineSchema({
+        name: 'schemaB',
+        schema: {type: DataType.BOOLEAN},
+      });
+      const throwable = () => S.parse({prop: 10}, schemaA);
+      expect(throwable).to.throw(
+        'Unable to get the "properties" option ' +
+          'from the data schema of "boolean" type.',
+      );
+    });
+
+    it('should resolve a schema factory from the property schema', function () {
+      const S = new DataParser();
+      S.setParsers([stringTypeParser]);
+      const factory = () => ({type: DataType.STRING});
+      const schema = {
+        type: DataType.OBJECT,
+        properties: {prop: factory},
+      };
+      const res = S.parse({prop: 10}, schema);
+      expect(res).to.be.eql({prop: '10'});
+    });
+
+    it('should resolve a schema name from the property schema', function () {
+      const S = new DataParser();
+      S.setParsers([stringTypeParser]);
+      const schemaA = {
+        type: DataType.OBJECT,
+        properties: {prop: 'schemaB'},
+      };
+      S.getService(DataSchemaRegistry).defineSchema({
+        name: 'schemaB',
+        schema: {type: DataType.STRING},
+      });
+      const res = S.parse({prop: 10}, schemaA);
+      expect(res).to.be.eql({prop: '10'});
+    });
+
+    it('should not set undefined value to a non existent property', function () {
+      const S = new DataParser();
+      const parser = value => value;
+      S.setParsers([parser]);
+      const value = {p1: 'str'};
+      const schema = {
+        type: DataType.OBJECT,
+        properties: {
+          p1: {type: DataType.ANY},
+          p2: {type: DataType.ANY},
+        },
+      };
+      const res = S.parse(value, schema);
+      expect(res).to.be.eql(value);
+    });
+  });
+});

+ 6 - 0
src/data-parsers/array-type-parser.d.ts

@@ -0,0 +1,6 @@
+import {DataParsingFunction} from "../data-parser.js";
+
+/**
+ * Array type parser.
+ */
+export declare const arrayTypeParser: DataParsingFunction;

+ 56 - 0
src/data-parsers/array-type-parser.js

@@ -0,0 +1,56 @@
+import {DataType} from '../data-type.js';
+import {ServiceContainer} from '@e22m4u/js-service';
+import {DataParsingError} from '../errors/index.js';
+import {EmptyValuesService} from '@e22m4u/js-empty-values';
+
+/**
+ * Array type parser.
+ *
+ * @param {*} value
+ * @param {object} schema
+ * @param {object|undefined} options
+ * @param {ServiceContainer} container
+ * @returns {*}
+ */
+export function arrayTypeParser(value, schema, options, container) {
+  // если тип не соответствует массиву,
+  // то преобразование пропускается
+  if (schema.type !== DataType.ARRAY) {
+    return value;
+  }
+  // если значение является массивом,
+  // то преобразование пропускается
+  if (Array.isArray(value)) {
+    return value;
+  }
+  // если значение является строкой,
+  // то выполняется попытка разбора JSON
+  if (typeof value === 'string') {
+    value = value.trim();
+    let newValue;
+    try {
+      newValue = JSON.parse(value);
+    } catch {
+      //
+    }
+    if (Array.isArray(newValue)) {
+      return newValue;
+    }
+  }
+  // если значение является пустым,
+  // то преобразование пропускается
+  const dataType = schema.type || DataType.ANY;
+  const emptyValues = container.get(EmptyValuesService);
+  if (emptyValues.isEmptyByType(dataType, value)) {
+    return value;
+  }
+  // если преобразовать значение не удалось,
+  // то выбрасывается ошибка
+  if (!options || !options.noParsingErrors) {
+    const sourcePath = options && options.sourcePath;
+    throw new DataParsingError(value, dataType, sourcePath);
+  }
+  // если установлен флаг бесшумной работы,
+  // то значение возвращается без изменений
+  return value;
+}

+ 98 - 0
src/data-parsers/array-type-parser.spec.js

@@ -0,0 +1,98 @@
+import {expect} from 'chai';
+import {DataParsingError} from '../errors/index.js';
+import {ServiceContainer} from '@e22m4u/js-service';
+import {arrayTypeParser} from './array-type-parser.js';
+import {DATA_TYPE_LIST, DataType} from '../data-type.js';
+import {EmptyValuesService} from '@e22m4u/js-empty-values';
+
+describe('arrayTypeParser', function () {
+  it('should return a value as is when a non-array schema is given', function () {
+    const container = new ServiceContainer();
+    const dataTypes = DATA_TYPE_LIST.filter(v => v !== DataType.ARRAY);
+    const values = ['str', '', 10, 0, true, false, [], {}, undefined, null];
+    const parse = type => {
+      values.forEach(value => {
+        const res = arrayTypeParser(value, {type}, undefined, container);
+        expect(res).to.be.eql(value);
+      });
+    };
+    dataTypes.forEach(parse);
+  });
+
+  it('should return an array value as is when the array schema is given', function () {
+    const container = new ServiceContainer();
+    const value = [];
+    const res = arrayTypeParser(
+      value,
+      {type: DataType.ARRAY},
+      undefined,
+      container,
+    );
+    expect(res).to.be.eq(value);
+  });
+
+  it('should parse a json string when the array schema is given', function () {
+    const container = new ServiceContainer();
+    const value = '[1, 2, 3]';
+    const res = arrayTypeParser(
+      value,
+      {type: DataType.ARRAY},
+      undefined,
+      container,
+    );
+    expect(res).to.be.eql([1, 2, 3]);
+  });
+
+  it('should return an empty value as is when the array schema is given', function () {
+    const container = new ServiceContainer();
+    const emptyValues = container.get(EmptyValuesService);
+    emptyValues.setEmptyValuesOf(DataType.ARRAY, [undefined]);
+    const res = arrayTypeParser(
+      undefined,
+      {type: DataType.ARRAY},
+      undefined,
+      container,
+    );
+    expect(res).to.be.undefined;
+  });
+
+  it('should throw an error for a non-array value when the array schema is given', function () {
+    const container = new ServiceContainer();
+    const values = ['str', '', 10, 0, true, false, {}];
+    const dataType = DataType.ARRAY;
+    const parse = value => {
+      const throwable = () =>
+        arrayTypeParser(value, {type: dataType}, undefined, container);
+      expect(throwable).to.throw(DataParsingError);
+    };
+    values.forEach(parse);
+  });
+
+  it('should pass correct arguments to the DataParsingError', function () {
+    const container = new ServiceContainer();
+    const value = Symbol();
+    const dataType = DataType.ARRAY;
+    const sourcePath = 'aSource';
+    let error;
+    try {
+      arrayTypeParser(value, {type: dataType}, {sourcePath}, container);
+    } catch (e) {
+      error = e;
+    }
+    expect(error.value).to.be.eq(value);
+    expect(error.targetType).to.be.eq(dataType);
+    expect(error.sourcePath).to.be.eq(sourcePath);
+  });
+
+  it('should ignore parsing errors when the "noParsingErrors" option is true', function () {
+    const container = new ServiceContainer();
+    const value = Symbol();
+    const res = arrayTypeParser(
+      value,
+      {type: DataType.ARRAY},
+      {noParsingErrors: true},
+      container,
+    );
+    expect(res).to.be.eq(value);
+  });
+});

+ 6 - 0
src/data-parsers/boolean-type-parser.d.ts

@@ -0,0 +1,6 @@
+import {DataParsingFunction} from "../data-parser.js";
+
+/**
+ * Boolean type parser.
+ */
+export declare const booleanTypeParser: DataParsingFunction;

+ 57 - 0
src/data-parsers/boolean-type-parser.js

@@ -0,0 +1,57 @@
+import {DataType} from '../data-type.js';
+import {ServiceContainer} from '@e22m4u/js-service';
+import {DataParsingError} from '../errors/index.js';
+import {EmptyValuesService} from '@e22m4u/js-empty-values';
+
+/**
+ * Boolean type parser.
+ *
+ * @param {*} value
+ * @param {object} schema
+ * @param {object|undefined} options
+ * @param {ServiceContainer} container
+ * @returns {*}
+ */
+export function booleanTypeParser(value, schema, options, container) {
+  // если тип не соответствует логическому
+  // значению, то преобразование пропускается
+  if (schema.type !== DataType.BOOLEAN) {
+    return value;
+  }
+  // если значение является логическим,
+  // то преобразование пропускается
+  if (typeof value === 'boolean') {
+    return value;
+  }
+  // если значение является строкой,
+  // то выполняется попытка преобразования
+  if (typeof value === 'string') {
+    value = value.trim();
+    if (value === '1') return true;
+    if (value === '0') return false;
+    if (value === 'true') return true;
+    if (value === 'false') return false;
+  }
+  // если значение является числом,
+  // то выполняется попытка преобразования
+  else if (typeof value === 'number') {
+    if (value === 1) return true;
+    if (value === 0) return false;
+  }
+  // если значение является пустым,
+  // то преобразование пропускается
+  const dataType = schema.type || DataType.ANY;
+  const emptyValues = container.get(EmptyValuesService);
+  if (emptyValues.isEmptyByType(dataType, value)) {
+    return value;
+  }
+  // если преобразовать значение не удалось,
+  // то выбрасывается ошибка
+  if (!options || !options.noParsingErrors) {
+    const sourcePath = options && options.sourcePath;
+    throw new DataParsingError(value, dataType, sourcePath);
+  }
+  // если установлен флаг бесшумной работы,
+  // то значение возвращается без изменений
+  return value;
+}

+ 138 - 0
src/data-parsers/boolean-type-parser.spec.js

@@ -0,0 +1,138 @@
+import {expect} from 'chai';
+import {ServiceContainer} from '@e22m4u/js-service';
+import {DataParsingError} from '../errors/index.js';
+import {DATA_TYPE_LIST, DataType} from '../data-type.js';
+import {booleanTypeParser} from './boolean-type-parser.js';
+import {EmptyValuesService} from '@e22m4u/js-empty-values';
+
+describe('booleanTypeParser', function () {
+  it('should return a value as is when a non-boolean schema is given', function () {
+    const container = new ServiceContainer();
+    const dataTypes = DATA_TYPE_LIST.filter(v => v !== DataType.BOOLEAN);
+    const values = ['str', '', 10, 0, true, false, [], {}, undefined, null];
+    const parse = type => {
+      values.forEach(value => {
+        const res = booleanTypeParser(value, {type}, undefined, container);
+        expect(res).to.be.eql(value);
+      });
+    };
+    dataTypes.forEach(parse);
+  });
+
+  it('should return a boolean value as is when the boolean schema is given', function () {
+    const container = new ServiceContainer();
+    const value1 = true;
+    const res1 = booleanTypeParser(
+      value1,
+      {type: DataType.BOOLEAN},
+      undefined,
+      container,
+    );
+    const value2 = true;
+    expect(res1).to.be.eq(value1);
+    const res2 = booleanTypeParser(
+      value2,
+      {type: DataType.BOOLEAN},
+      undefined,
+      container,
+    );
+    expect(res2).to.be.eq(value2);
+  });
+
+  it('should convert a string value to a boolean when the boolean schema is given', function () {
+    const container = new ServiceContainer();
+    const resMap = [
+      ['1', true],
+      ['0', false],
+      ['true', true],
+      ['false', false],
+    ];
+    resMap.forEach(([value, expected]) => {
+      const res = booleanTypeParser(
+        value,
+        {type: DataType.BOOLEAN},
+        undefined,
+        container,
+      );
+      expect(expected).to.be.eq(res);
+    });
+  });
+
+  it('should trim a given string before parsing as a boolean', function () {
+    const container = new ServiceContainer();
+    const res = booleanTypeParser(
+      ' true ',
+      {type: DataType.BOOLEAN},
+      undefined,
+      container,
+    );
+    expect(res).to.be.true;
+  });
+
+  it('should convert a number value to a boolean when the boolean schema is given', function () {
+    const container = new ServiceContainer();
+    const resMap = [[1, true], [0, false]];
+    resMap.forEach(([value, expected]) => {
+      const res = booleanTypeParser(
+        value,
+        {type: DataType.BOOLEAN},
+        undefined,
+        container,
+      );
+      expect(expected).to.be.eq(res);
+    });
+  });
+
+  it('should return an empty value as is when the boolean schema is given', function () {
+    const container = new ServiceContainer();
+    const emptyValues = container.get(EmptyValuesService);
+    emptyValues.setEmptyValuesOf(DataType.BOOLEAN, [undefined]);
+    const res = booleanTypeParser(
+      undefined,
+      {type: DataType.BOOLEAN},
+      undefined,
+      container,
+    );
+    expect(res).to.be.undefined;
+  });
+
+  it('should throw an error for a non-boolean value when the boolean schema is given', function () {
+    const container = new ServiceContainer();
+    const values = ['str', '', 10, -10, [], {}];
+    const dataType = DataType.BOOLEAN;
+    const parse = value => {
+      const throwable = () =>
+        booleanTypeParser(value, {type: dataType}, undefined, container);
+      expect(throwable).to.throw(DataParsingError);
+    };
+    values.forEach(parse);
+  });
+
+  it('should pass correct arguments to the DataParsingError', function () {
+    const container = new ServiceContainer();
+    const value = Symbol();
+    const dataType = DataType.BOOLEAN;
+    const sourcePath = 'aSource';
+    let error;
+    try {
+      booleanTypeParser(value, {type: dataType}, {sourcePath}, container);
+    } catch (e) {
+      error = e;
+    }
+    expect(error.value).to.be.eq(value);
+    expect(error.targetType).to.be.eq(dataType);
+    expect(error.sourcePath).to.be.eq(sourcePath);
+  });
+
+  it('should ignore parsing errors when the "noParsingErrors" option is true', function () {
+    const container = new ServiceContainer();
+    const value = Symbol();
+    const res = booleanTypeParser(
+      value,
+      {type: DataType.BOOLEAN},
+      {noParsingErrors: true},
+      container,
+    );
+    expect(res).to.be.eq(value);
+  });
+});

+ 5 - 0
src/data-parsers/index.d.ts

@@ -0,0 +1,5 @@
+export * from './array-type-parser.js'
+export * from './string-type-parser.js'
+export * from './number-type-parser.js'
+export * from './object-type-parser.js'
+export * from './boolean-type-parser.js'

+ 5 - 0
src/data-parsers/index.js

@@ -0,0 +1,5 @@
+export * from './array-type-parser.js'
+export * from './string-type-parser.js'
+export * from './number-type-parser.js'
+export * from './object-type-parser.js'
+export * from './boolean-type-parser.js'

+ 6 - 0
src/data-parsers/number-type-parser.d.ts

@@ -0,0 +1,6 @@
+import {DataParsingFunction} from "../data-parser.js";
+
+/**
+ * Number type parser.
+ */
+export declare const numberTypeParser: DataParsingFunction;

+ 52 - 0
src/data-parsers/number-type-parser.js

@@ -0,0 +1,52 @@
+import {DataType} from '../data-type.js';
+import {ServiceContainer} from '@e22m4u/js-service';
+import {DataParsingError} from '../errors/index.js';
+import {EmptyValuesService} from '@e22m4u/js-empty-values';
+
+/**
+ * Number type parser.
+ *
+ * @param {*} value
+ * @param {object} schema
+ * @param {object|undefined} options
+ * @param {ServiceContainer} container
+ * @returns {*}
+ */
+export function numberTypeParser(value, schema, options, container) {
+  // если тип не соответствует числу,
+  // то преобразование пропускается
+  if (schema.type !== DataType.NUMBER) {
+    return value;
+  }
+  // если значение является числом,
+  // то преобразование пропускается
+  if (typeof value === 'number') {
+    return value;
+  }
+  // если значение является не пусто строкой,
+  // то выполняется попытка преобразования
+  if (value && typeof value === 'string') {
+    if (value.length <= 20) {
+      const newValue = Number(value.trim());
+      if (!isNaN(newValue)) {
+        return newValue;
+      }
+    }
+  }
+  // если значение является пустым,
+  // то преобразование пропускается
+  const dataType = schema.type || DataType.ANY;
+  const emptyValues = container.get(EmptyValuesService);
+  if (emptyValues.isEmptyByType(dataType, value)) {
+    return value;
+  }
+  // если преобразовать значение не удалось,
+  // то выбрасывается ошибка
+  if (!options || !options.noParsingErrors) {
+    const sourcePath = options && options.sourcePath;
+    throw new DataParsingError(value, dataType, sourcePath);
+  }
+  // если установлен флаг бесшумной работы,
+  // то значение возвращается без изменений
+  return value;
+}

+ 138 - 0
src/data-parsers/number-type-parser.spec.js

@@ -0,0 +1,138 @@
+import {expect} from 'chai';
+import {ServiceContainer} from '@e22m4u/js-service';
+import {DataParsingError} from '../errors/index.js';
+import {numberTypeParser} from './number-type-parser.js';
+import {DATA_TYPE_LIST, DataType} from '../data-type.js';
+import {EmptyValuesService} from '@e22m4u/js-empty-values';
+
+describe('numberTypeParser', function () {
+  it('should return a value as is when a non-number schema is given', function () {
+    const container = new ServiceContainer();
+    const dataTypes = DATA_TYPE_LIST.filter(v => v !== DataType.NUMBER);
+    const values = ['str', '', 10, 0, true, false, [], {}, undefined, null];
+    const parse = type => {
+      values.forEach(value => {
+        const res = numberTypeParser(value, {type}, undefined, container);
+        expect(res).to.be.eql(value);
+      });
+    };
+    dataTypes.forEach(parse);
+  });
+
+  it('should return a number value as is when the number schema is given', function () {
+    const container = new ServiceContainer();
+    const value = 10;
+    const res = numberTypeParser(
+      value,
+      {type: DataType.NUMBER},
+      undefined,
+      container,
+    );
+    expect(res).to.be.eq(value);
+  });
+
+  it('should parse a digits string when the number schema is given', function () {
+    const container = new ServiceContainer();
+    const res = numberTypeParser(
+      '10',
+      {type: DataType.NUMBER},
+      undefined,
+      container,
+    );
+    expect(res).to.be.eq(10);
+  });
+
+  it('should parse a float string when the number schema is given', function () {
+    const container = new ServiceContainer();
+    const res = numberTypeParser(
+      '10.5',
+      {type: DataType.NUMBER},
+      undefined,
+      container,
+    );
+    expect(res).to.be.eq(10.5);
+  });
+
+  it('should trim a given string before parsing as a number', function () {
+    const container = new ServiceContainer();
+    const res = numberTypeParser(
+      ' 10 ',
+      {type: DataType.NUMBER},
+      undefined,
+      container,
+    );
+    expect(res).to.be.eq(10);
+  });
+
+  it('should throw an error when digits string is too long', function () {
+    const container = new ServiceContainer();
+    const throwable = () =>
+      numberTypeParser(
+        '100000000000000000000',
+        {type: DataType.NUMBER},
+        undefined,
+        container,
+      );
+    expect(throwable).to.throw(DataParsingError);
+  });
+
+  it('should throw an error for an empty string', function () {
+    const container = new ServiceContainer();
+    const throwable = () =>
+      numberTypeParser('', {type: DataType.NUMBER}, undefined, container);
+    expect(throwable).to.throw(DataParsingError);
+  });
+
+  it('should return an empty value as is when the number schema is given', function () {
+    const container = new ServiceContainer();
+    const emptyValues = container.get(EmptyValuesService);
+    emptyValues.setEmptyValuesOf(DataType.NUMBER, [undefined]);
+    const res = numberTypeParser(
+      undefined,
+      {type: DataType.NUMBER},
+      undefined,
+      container,
+    );
+    expect(res).to.be.undefined;
+  });
+
+  it('should throw an error for a non-number value when a number schema is given', function () {
+    const container = new ServiceContainer();
+    const values = ['str', true, false, [], {}];
+    const dataType = DataType.NUMBER;
+    const parse = value => {
+      const throwable = () =>
+        numberTypeParser(value, {type: dataType}, undefined, container);
+      expect(throwable).to.throw(DataParsingError);
+    };
+    values.forEach(parse);
+  });
+
+  it('should pass correct arguments to the DataParsingError', function () {
+    const container = new ServiceContainer();
+    const value = Symbol();
+    const dataType = DataType.NUMBER;
+    const sourcePath = 'aSource';
+    let error;
+    try {
+      numberTypeParser(value, {type: dataType}, {sourcePath}, container);
+    } catch (e) {
+      error = e;
+    }
+    expect(error.value).to.be.eq(value);
+    expect(error.targetType).to.be.eq(dataType);
+    expect(error.sourcePath).to.be.eq(sourcePath);
+  });
+
+  it('should ignore parsing errors when the "noParsingErrors" option is true', function () {
+    const container = new ServiceContainer();
+    const value = Symbol();
+    const res = numberTypeParser(
+      value,
+      {type: DataType.NUMBER},
+      {noParsingErrors: true},
+      container,
+    );
+    expect(res).to.be.eq(value);
+  });
+});

+ 6 - 0
src/data-parsers/object-type-parser.d.ts

@@ -0,0 +1,6 @@
+import {DataParsingFunction} from "../data-parser.js";
+
+/**
+ * Object type parser.
+ */
+export declare const objectTypeParser: DataParsingFunction;

+ 60 - 0
src/data-parsers/object-type-parser.js

@@ -0,0 +1,60 @@
+import {DataType} from '../data-type.js';
+import {ServiceContainer} from '@e22m4u/js-service';
+import {DataParsingError} from '../errors/index.js';
+import {EmptyValuesService} from '@e22m4u/js-empty-values';
+
+/**
+ * Object type parser.
+ *
+ * @param {*} value
+ * @param {object} schema
+ * @param {options|undefined} options
+ * @param {ServiceContainer} container
+ * @returns {*}
+ */
+export function objectTypeParser(value, schema, options, container) {
+  // если тип не соответствует объекту,
+  // то проверка пропускается
+  if (schema.type !== DataType.OBJECT) {
+    return value;
+  }
+  // если значение является объектом,
+  // то проверка пропускается
+  if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
+    return value;
+  }
+  // если значение является строкой,
+  // то выполняется попытка преобразования
+  if (typeof value === 'string') {
+    value = value.trim();
+    let newValue;
+    try {
+      newValue = JSON.parse(value);
+    } catch {
+      //
+    }
+    if (
+      newValue !== null &&
+      typeof newValue === 'object' &&
+      !Array.isArray(newValue)
+    ) {
+      return newValue;
+    }
+  }
+  // если значение является пустым,
+  // то преобразование пропускается
+  const dataType = schema.type || DataType.ANY;
+  const emptyValues = container.get(EmptyValuesService);
+  if (emptyValues.isEmptyByType(dataType, value)) {
+    return value;
+  }
+  // если преобразовать значение не удалось,
+  // то выбрасывается ошибка
+  if (!options || !options.noParsingErrors) {
+    const sourcePath = options && options.sourcePath;
+    throw new DataParsingError(value, dataType, sourcePath);
+  }
+  // если установлен флаг бесшумной работы,
+  // то значение возвращается без изменений
+  return value;
+}

+ 98 - 0
src/data-parsers/object-type-parser.spec.js

@@ -0,0 +1,98 @@
+import {expect} from 'chai';
+import {ServiceContainer} from '@e22m4u/js-service';
+import {DataParsingError} from '../errors/index.js';
+import {objectTypeParser} from './object-type-parser.js';
+import {DATA_TYPE_LIST, DataType} from '../data-type.js';
+import {EmptyValuesService} from '@e22m4u/js-empty-values';
+
+describe('objectTypeParser', function () {
+  it('should return a value as is when a non-object schema is given', function () {
+    const container = new ServiceContainer();
+    const dataTypes = DATA_TYPE_LIST.filter(v => v !== DataType.OBJECT);
+    const values = ['str', '', 10, 0, true, false, [], {}, undefined, null];
+    const parse = type => {
+      values.forEach(value => {
+        const res = objectTypeParser(value, {type}, undefined, container);
+        expect(res).to.be.eql(value);
+      });
+    };
+    dataTypes.forEach(parse);
+  });
+
+  it('should return an object value as is when the object schema is given', function () {
+    const container = new ServiceContainer();
+    const value = {};
+    const res = objectTypeParser(
+      value,
+      {type: DataType.OBJECT},
+      undefined,
+      container,
+    );
+    expect(res).to.be.eq(value);
+  });
+
+  it('should parse a json string when the object schema is given', function () {
+    const container = new ServiceContainer();
+    const value = '{"foo": "bar"}';
+    const res = objectTypeParser(
+      value,
+      {type: DataType.OBJECT},
+      undefined,
+      container,
+    );
+    expect(res).to.be.eql({foo: 'bar'});
+  });
+
+  it('should return an empty value as is when the object schema is given', function () {
+    const container = new ServiceContainer();
+    const emptyValues = container.get(EmptyValuesService);
+    emptyValues.setEmptyValuesOf(DataType.OBJECT, [undefined]);
+    const res = objectTypeParser(
+      undefined,
+      {type: DataType.OBJECT},
+      undefined,
+      container,
+    );
+    expect(res).to.be.undefined;
+  });
+
+  it('should throw an error for a non-object value when the object schema is given', function () {
+    const container = new ServiceContainer();
+    const values = ['str', '', 10, 0, true, false, []];
+    const dataType = DataType.OBJECT;
+    const parse = value => {
+      const throwable = () =>
+        objectTypeParser(value, {type: dataType}, undefined, container);
+      expect(throwable).to.throw(DataParsingError);
+    };
+    values.forEach(parse);
+  });
+
+  it('should pass correct arguments to the DataParsingError', function () {
+    const container = new ServiceContainer();
+    const value = Symbol();
+    const dataType = DataType.OBJECT;
+    const sourcePath = 'aSource';
+    let error;
+    try {
+      objectTypeParser(value, {type: dataType}, {sourcePath}, container);
+    } catch (e) {
+      error = e;
+    }
+    expect(error.value).to.be.eq(value);
+    expect(error.targetType).to.be.eq(dataType);
+    expect(error.sourcePath).to.be.eq(sourcePath);
+  });
+
+  it('should ignore parsing errors when the "noParsingErrors" option is true', function () {
+    const container = new ServiceContainer();
+    const value = Symbol();
+    const res = objectTypeParser(
+      value,
+      {type: DataType.OBJECT},
+      {noParsingErrors: true},
+      container,
+    );
+    expect(res).to.be.eq(value);
+  });
+});

+ 6 - 0
src/data-parsers/string-type-parser.d.ts

@@ -0,0 +1,6 @@
+import {DataParsingFunction} from "../data-parser.js";
+
+/**
+ * String type parser.
+ */
+export declare const stringTypeParser: DataParsingFunction;

+ 47 - 0
src/data-parsers/string-type-parser.js

@@ -0,0 +1,47 @@
+import {DataType} from '../data-type.js';
+import {ServiceContainer} from '@e22m4u/js-service';
+import {DataParsingError} from '../errors/index.js';
+import {EmptyValuesService} from '@e22m4u/js-empty-values';
+
+/**
+ * String type parser.
+ *
+ * @param {*} value
+ * @param {object} schema
+ * @param {object|undefined} options
+ * @param {ServiceContainer} container
+ * @returns {*}
+ */
+export function stringTypeParser(value, schema, options, container) {
+  // если тип не соответствует строке,
+  // то преобразование пропускается
+  if (schema.type !== DataType.STRING) {
+    return value;
+  }
+  // если значение является строкой,
+  // то преобразование пропускается
+  if (typeof value === 'string') {
+    return value;
+  }
+  // если значение является числом,
+  // то значение приводится к строке
+  if (typeof value === 'number') {
+    return String(value);
+  }
+  // если значение является пустым,
+  // то преобразование пропускается
+  const dataType = schema.type || DataType.ANY;
+  const emptyValues = container.get(EmptyValuesService);
+  if (emptyValues.isEmptyByType(dataType, value)) {
+    return value;
+  }
+  // если преобразовать значение не удалось,
+  // то выбрасывается ошибка
+  if (!options || !options.noParsingErrors) {
+    const sourcePath = options && options.sourcePath;
+    throw new DataParsingError(value, dataType, sourcePath);
+  }
+  // если установлен флаг бесшумной работы,
+  // то значение возвращается без изменений
+  return value;
+}

+ 97 - 0
src/data-parsers/string-type-parser.spec.js

@@ -0,0 +1,97 @@
+import {expect} from 'chai';
+import {ServiceContainer} from '@e22m4u/js-service';
+import {DataParsingError} from '../errors/index.js';
+import {stringTypeParser} from './string-type-parser.js';
+import {DATA_TYPE_LIST, DataType} from '../data-type.js';
+import {EmptyValuesService} from '@e22m4u/js-empty-values';
+
+describe('stringTypeParser', function () {
+  it('should return a value as is when a non-string schema is given', function () {
+    const container = new ServiceContainer();
+    const dataTypes = DATA_TYPE_LIST.filter(v => v !== DataType.STRING);
+    const values = ['str', '', 10, 0, true, false, [], {}, undefined, null];
+    const parse = type => {
+      values.forEach(value => {
+        const res = stringTypeParser(value, {type}, undefined, container);
+        expect(res).to.be.eql(value);
+      });
+    };
+    dataTypes.forEach(parse);
+  });
+
+  it('should return a string value as is when the string schema is given', function () {
+    const container = new ServiceContainer();
+    const value = 'str';
+    const res = stringTypeParser(
+      value,
+      {type: DataType.STRING},
+      undefined,
+      container,
+    );
+    expect(res).to.be.eq(value);
+  });
+
+  it('should convert a number value to a string when the string schema is given', function () {
+    const container = new ServiceContainer();
+    const res = stringTypeParser(
+      10,
+      {type: DataType.STRING},
+      undefined,
+      container,
+    );
+    expect(res).to.be.eq('10');
+  });
+
+  it('should return an empty value as is when the string schema is given', function () {
+    const container = new ServiceContainer();
+    const emptyValues = container.get(EmptyValuesService);
+    emptyValues.setEmptyValuesOf(DataType.STRING, [undefined]);
+    const res = stringTypeParser(
+      undefined,
+      {type: DataType.STRING},
+      undefined,
+      container,
+    );
+    expect(res).to.be.undefined;
+  });
+
+  it('should throw an error for a non-string value when the string schema is given', function () {
+    const container = new ServiceContainer();
+    const values = [true, false, [], {}];
+    const dataType = DataType.STRING;
+    const parse = value => {
+      const throwable = () =>
+        stringTypeParser(value, {type: dataType}, undefined, container);
+      expect(throwable).to.throw(DataParsingError);
+    };
+    values.forEach(parse);
+  });
+
+  it('should pass correct arguments to the DataParsingError', function () {
+    const container = new ServiceContainer();
+    const value = Symbol();
+    const dataType = DataType.STRING;
+    const sourcePath = 'aSource';
+    let error;
+    try {
+      stringTypeParser(value, {type: dataType}, {sourcePath}, container);
+    } catch (e) {
+      error = e;
+    }
+    expect(error.value).to.be.eq(value);
+    expect(error.targetType).to.be.eq(dataType);
+    expect(error.sourcePath).to.be.eq(sourcePath);
+  });
+
+  it('should ignore parsing errors when the "noParsingErrors" option is true', function () {
+    const container = new ServiceContainer();
+    const value = Symbol();
+    const res = stringTypeParser(
+      value,
+      {type: DataType.STRING},
+      {noParsingErrors: true},
+      container,
+    );
+    expect(res).to.be.eq(value);
+  });
+});

+ 9 - 0
src/data-schema-definition.d.ts

@@ -0,0 +1,9 @@
+import {DataSchema} from "./data-schema.js";
+
+/**
+ * Data schema definition.
+ */
+export type DataSchemaDefinition = {
+  name: string;
+  schema: DataSchema;
+}

+ 37 - 0
src/data-schema-registry.d.ts

@@ -0,0 +1,37 @@
+import {Service} from '@e22m4u/js-service';
+import {DataSchema} from './data-schema.js';
+import {DataSchemaDefinition} from './data-schema-definition.js';
+
+/**
+ * Data schema registry.
+ */
+export declare class DataSchemaRegistry extends Service {
+
+  /**
+   * Define schema.
+   *
+   * @param schemaDef
+   */
+  defineSchema(schemaDef: DataSchemaDefinition): this;
+
+  /**
+   * Has schema.
+   *
+   * @param schemaName
+   */
+  hasSchema(schemaName: string): boolean;
+
+  /**
+   * Get schema.
+   *
+   * @param schemaName
+   */
+  getSchema(schemaName: string): DataSchema;
+
+  /**
+   * Get definition.
+   *
+   * @param schemaName
+   */
+  getDefinition(schemaName: string): DataSchemaDefinition;
+}

+ 77 - 0
src/data-schema-registry.js

@@ -0,0 +1,77 @@
+import {Service} from '@e22m4u/js-service';
+import {InvalidArgumentError} from '@e22m4u/js-format';
+import {validateDataSchemaDefinition} from './validate-data-schema-definition.js';
+
+/**
+ * Data schema registry.
+ */
+export class DataSchemaRegistry extends Service {
+  /**
+   * Definitions.
+   *
+   * @type {Map<string, object>}
+   */
+  _definitions = new Map();
+
+  /**
+   * Define schema.
+   *
+   * @param {object} schemaDef
+   * @returns {this}
+   */
+  defineSchema(schemaDef) {
+    validateDataSchemaDefinition(schemaDef);
+    if (this._definitions.has(schemaDef.name)) {
+      throw new InvalidArgumentError(
+        'Data schema %v is already registered.',
+        schemaDef.name,
+      );
+    }
+    this._definitions.set(schemaDef.name, schemaDef);
+    return this;
+  }
+
+  /**
+   * Has schema.
+   *
+   * @param {string} schemaName
+   * @returns {boolean}
+   */
+  hasSchema(schemaName) {
+    return this._definitions.has(schemaName);
+  }
+
+  /**
+   * Get schema.
+   *
+   * @param {string} schemaName
+   * @returns {object}
+   */
+  getSchema(schemaName) {
+    const schemaDef = this._definitions.get(schemaName);
+    if (!schemaDef) {
+      throw new InvalidArgumentError(
+        'Data schema %v is not found.',
+        schemaName,
+      );
+    }
+    return schemaDef.schema;
+  }
+
+  /**
+   * Get definition.
+   *
+   * @param {string} schemaName
+   * @returns {object}
+   */
+  getDefinition(schemaName) {
+    const schemaDef = this._definitions.get(schemaName);
+    if (!schemaDef) {
+      throw new InvalidArgumentError(
+        'Schema definition %v is not found.',
+        schemaName,
+      );
+    }
+    return schemaDef;
+  }
+}

+ 72 - 0
src/data-schema-registry.spec.js

@@ -0,0 +1,72 @@
+import {expect} from "chai";
+import {DataSchemaRegistry} from "./data-schema-registry.js";
+
+describe('DataSchemaRegistry', function() {
+  describe('defineSchema', function() {
+    it('should throw an error if the schema name is already registered', function() {
+      const S = new DataSchemaRegistry();
+      const schemaDef = {name: 'mySchema', schema: {}};
+      S.defineSchema(schemaDef);
+      const throwable = () => S.defineSchema(schemaDef);
+      expect(throwable).to.throw(
+        'Data schema "mySchema" is already registered.',
+      );
+    });
+
+    it('should register the given definition', function() {
+      const S = new DataSchemaRegistry();
+      const schemaDef = {name: 'mySchema', schema: {}};
+      S.defineSchema(schemaDef);
+      const res = S.getDefinition(schemaDef.name);
+      expect(res).to.be.eql(schemaDef);
+    });
+
+    it('should return the current instance', function() {
+      const S = new DataSchemaRegistry();
+      const res = S.defineSchema({name: 'mySchema', schema: {}});
+      expect(res).to.be.eq(S);
+    });
+  });
+
+  describe('hasSchema', function() {
+    it('should return true if the given name is registered', function() {
+      const S = new DataSchemaRegistry();
+      const schemaDef = {name: 'mySchema', schema: {}};
+      expect(S.hasSchema(schemaDef.name)).to.be.false;
+      S.defineSchema(schemaDef);
+      expect(S.hasSchema(schemaDef.name)).to.be.true;
+    });
+  });
+
+  describe('getSchema', function() {
+    it('should throw an error if the given name is not registered', function() {
+      const S = new DataSchemaRegistry();
+      const throwable = () => S.getSchema('mySchema');
+      expect(throwable).to.throw('Data schema "mySchema" is not found.');
+    });
+
+    it('should return the schema part of a registered definition for a given name', function() {
+      const S = new DataSchemaRegistry();
+      const schemaDef = {name: 'mySchema', schema: {}};
+      S.defineSchema(schemaDef);
+      const res = S.getSchema(schemaDef.name);
+      expect(res).to.be.eql(schemaDef.schema);
+    });
+  });
+
+  describe('getDefinition', function() {
+    it('should throw an error if the given name is not registered', function() {
+      const S = new DataSchemaRegistry();
+      const throwable = () => S.getDefinition('mySchema');
+      expect(throwable).to.throw('Schema definition "mySchema" is not found.');
+    });
+    
+    it('should return the registered definition for a given name', function() {
+      const S = new DataSchemaRegistry();
+      const schemaDef = {name: 'mySchema', schema: {}};
+      S.defineSchema(schemaDef);
+      const res = S.getDefinition(schemaDef.name);
+      expect(res).to.be.eql(schemaDef);
+    });
+  });
+});

+ 14 - 0
src/data-schema-resolver.d.ts

@@ -0,0 +1,14 @@
+import {Service} from '@e22m4u/js-service';
+import {DataSchema, DataSchemaObject} from './data-schema.js';
+
+/**
+ * Data schema resolver.
+ */
+export declare class DataSchemaResolver extends Service {
+  /**
+   * Resolve schema.
+   *
+   * @param schema
+   */
+  resolve(schema: DataSchema): DataSchemaObject;
+}

+ 65 - 0
src/data-schema-resolver.js

@@ -0,0 +1,65 @@
+import {Service} from '@e22m4u/js-service';
+import {InvalidArgumentError} from '@e22m4u/js-format';
+import {validateDataSchema} from './validate-data-schema.js';
+import {DataSchemaRegistry} from './data-schema-registry.js';
+
+/**
+ * Data schema resolver.
+ */
+export class DataSchemaResolver extends Service {
+  /**
+   * Resolve schema.
+   *
+   * @param {object|Function|string} schema
+   * @returns {object}
+   */
+  resolve(schema) {
+    // если схема является фабрикой,
+    // то извлекается фабричное значение
+    if (typeof schema === 'function') {
+      schema = schema(this.container);
+      // если фабричное значение не является объектом
+      // или не пустой строкой, то выбрасывается ошибка
+      if (
+        !schema ||
+        (typeof schema !== 'object' && typeof schema !== 'string') ||
+        Array.isArray(schema)
+      ) {
+        throw new InvalidArgumentError(
+          'Schema factory must return an Object ' +
+            'or a non-empty String, but %v was given.',
+          schema,
+        );
+      }
+    }
+    // если схема является не пустой строкой,
+    // то выполняется поиск именованной схемы
+    if (schema && typeof schema === 'string') {
+      schema = this.getService(DataSchemaRegistry).getSchema(schema);
+      // если итоговая схема не является объектом,
+      // то выбрасывается ошибка
+      if (
+        !schema ||
+        (typeof schema !== 'object' &&
+          typeof schema !== 'function' &&
+          typeof schema !== 'string') ||
+        Array.isArray(schema)
+      ) {
+        throw new InvalidArgumentError(
+          'Named schema must be an Object, a Function ' +
+            'or a non-empty String, but %v was given.',
+          schema,
+        );
+      }
+      // если реестр вернул фабрику или строку,
+      // то полученное значение передается в рекурсию
+      if (typeof schema === 'string' || typeof schema === 'function') {
+        return this.resolve(schema);
+      }
+    }
+    // проверка схемы без вложений
+    // (режим shallowMode)
+    validateDataSchema(schema, true);
+    return schema;
+  }
+}

+ 136 - 0
src/data-schema-resolver.spec.js

@@ -0,0 +1,136 @@
+import {expect} from 'chai';
+import {format} from '@e22m4u/js-format';
+import {DATA_TYPE_LIST, DataType} from './data-type.js';
+import {DataSchemaResolver} from './data-schema-resolver.js';
+import {DataSchemaRegistry} from './data-schema-registry.js';
+
+describe('DataSchemaResolver', function () {
+  describe('resolve', function () {
+    it('should resolve a factory value as the schema object', function () {
+      const S = new DataSchemaResolver();
+      let invoked = 0;
+      const schema = {type: DataType.ANY};
+      const factory = container => {
+        expect(container).to.be.eql(S.container);
+        invoked++;
+        return schema;
+      };
+      expect(S.resolve(factory)).to.be.eq(schema);
+      expect(invoked).to.be.eq(1);
+    });
+
+    it('should require a factory value to be an object or a non-empty string', function() {
+      const S = new DataSchemaResolver();
+      const registry = S.getService(DataSchemaRegistry);
+      registry.defineSchema({name: 'mySchema', schema: {}});
+      const throwable = v => () => S.resolve(() => v);
+      const error = s =>
+        format(
+          'Schema factory must return an Object ' +
+            'or a non-empty String, but %s was given.',
+          s,
+        );
+      expect(throwable('')).to.throw(error('""'));
+      expect(throwable(10)).to.throw(error('10'));
+      expect(throwable(0)).to.throw(error('0'));
+      expect(throwable([])).to.throw(error('Array'));
+      expect(throwable(null)).to.throw(error('null'));
+      expect(throwable(undefined)).to.throw(error('undefined'));
+      expect(throwable(() => undefined)).to.throw(error('Function'));
+      throwable('mySchema')();
+      throwable({})();
+    });
+
+    it('should resolve a given name as the registered schema', function () {
+      const S = new DataSchemaResolver();
+      const schema = {type: DataType.ANY};
+      const registry = S.getService(DataSchemaRegistry);
+      registry.defineSchema({name: 'mySchema', schema});
+      expect(S.resolve('mySchema')).to.be.eq(schema);
+    });
+
+    it('should require the schema registry return a valid value', function() {
+      const S = new DataSchemaResolver();
+      const registry = S.getService(DataSchemaRegistry);
+      const throwable = v => () => {
+        registry.getSchema = name => {
+          if (name === 'correctSchema') {
+            return {type: DataType.ANY};
+          }
+          expect(name).to.be.eq('dummySchema');
+          return v;
+        };
+        S.resolve('dummySchema');
+      }
+      const error = s => format(
+        'Named schema must be an Object, a Function ' +
+          'or a non-empty String, but %s was given.',
+        s,
+      );
+      expect(throwable('')).to.throw(error('""'));
+      expect(throwable(10)).to.throw(error('10'));
+      expect(throwable(0)).to.throw(error('0'));
+      expect(throwable(true)).to.throw(error('true'));
+      expect(throwable(false)).to.throw(error('false'));
+      expect(throwable([])).to.throw(error('Array'));
+      expect(throwable(null)).to.throw(error('null'));
+      expect(throwable(undefined)).to.throw(error('undefined'));
+      throwable('correctSchema')();
+      throwable({})();
+      throwable(() => ({}))();
+    });
+
+    it('should be able to resolve a name chain', function() {
+      const S = new DataSchemaResolver();
+      const schema = {type: DataType.ANY};
+      const registry = S.getService(DataSchemaRegistry);
+      registry.defineSchema({name: 'schema1', schema: 'schema2'});
+      registry.defineSchema({name: 'schema2', schema});
+      expect(S.resolve('schema1')).to.be.eq(schema);
+    });
+
+    it('should validate a factory value as the schema object', function () {
+      const S = new DataSchemaResolver();
+      const throwable = () => S.resolve(() => ({type: 10}));
+      expect(throwable).to.throw(
+        format(
+          'Schema option "type" must be one of values: %l, but 10 was given.',
+          DATA_TYPE_LIST,
+        ),
+      );
+    });
+
+    it('should resolve the schema name from the schema factory', function () {
+      const S = new DataSchemaResolver();
+      let invoked = 0;
+      const schemaDef = {name: 'mySchema', schema: {type: DataType.ANY}};
+      const registry = S.getService(DataSchemaRegistry);
+      registry.defineSchema(schemaDef);
+      const factory = container => {
+        expect(container).to.be.eql(S.container);
+        invoked++;
+        return schemaDef.name;
+      };
+      expect(S.resolve(factory)).to.be.eq(schemaDef.schema);
+      expect(invoked).to.be.eq(1);
+    });
+
+    it('should return a given schema as is', function() {
+      const S = new DataSchemaResolver();
+      const schema = {type: DataType.STRING};
+      const res = S.resolve(schema);
+      expect(res).to.be.eq(schema);
+    });
+
+    it('should validate options of a given schema', function () {
+      const S = new DataSchemaResolver();
+      const throwable = () => S.resolve({type: 10});
+      expect(throwable).to.throw(
+        format(
+          'Schema option "type" must be one of values: %l, but 10 was given.',
+          DATA_TYPE_LIST,
+        ),
+      );
+    });
+  });
+});

+ 36 - 0
src/data-schema.d.ts

@@ -0,0 +1,36 @@
+import {ServiceContainer} from '@e22m4u/js-service';
+import {DataType} from './data-type.js';
+
+/**
+ * Data schema.
+ */
+export type DataSchema = DataSchemaObject | DataSchemaFactory | DataSchemaName;
+
+/**
+ * Data schema object.
+ */
+export type DataSchemaObject = {
+  type?: DataType;
+  items?: DataSchema;
+  properties?: DataSchemaProperties | DataSchemaFactory | DataSchemaName;
+  required?: boolean;
+};
+
+/**
+ * Data schema factory.
+ */
+export type DataSchemaFactory = (
+  container: ServiceContainer,
+) => DataSchemaObject | DataSchemaName;
+
+/**
+ * Data schema name.
+ */
+export type DataSchemaName = string;
+
+/**
+ * Data schema properties.
+ */
+export type DataSchemaProperties = {
+  [property: string]: DataSchema | undefined;
+};

+ 29 - 0
src/data-type.d.ts

@@ -0,0 +1,29 @@
+/**
+ * Data type.
+ */
+export declare const DataType: {
+  ANY: 'any',
+  STRING: 'string',
+  NUMBER: 'number',
+  BOOLEAN: 'boolean',
+  ARRAY: 'array',
+  OBJECT: 'object',
+};
+
+/**
+ * Data type.
+ */
+export type DataType = typeof DataType[keyof typeof DataType];
+
+
+/**
+ * Data type list.
+ */
+export declare const DATA_TYPE_LIST: DataType[];
+
+/**
+ * Get data type from value.
+ *
+ * @param value
+ */
+export declare function getDataTypeFromValue(value: unknown): DataType;

+ 34 - 0
src/data-type.js

@@ -0,0 +1,34 @@
+/**
+ * Data type.
+ */
+export const DataType = {
+  ANY: 'any',
+  STRING: 'string',
+  NUMBER: 'number',
+  BOOLEAN: 'boolean',
+  ARRAY: 'array',
+  OBJECT: 'object',
+};
+
+
+/**
+ * Data type list.
+ */
+export const DATA_TYPE_LIST = Object.values(DataType);
+
+/**
+ * Get data type from value.
+ *
+ * @param {*} value
+ * @returns {DataType}
+ */
+export function getDataTypeFromValue(value) {
+  if (value == null) return DataType.ANY;
+  const baseType = typeof value;
+  if (baseType === 'string') return DataType.STRING;
+  if (baseType === 'number') return DataType.NUMBER;
+  if (baseType === 'boolean') return DataType.BOOLEAN;
+  if (Array.isArray(value)) return DataType.ARRAY;
+  if (baseType === 'object') return DataType.OBJECT;
+  return DataType.ANY;
+}

+ 53 - 0
src/data-type.spec.js

@@ -0,0 +1,53 @@
+import {expect} from 'chai';
+import {DataType, getDataTypeFromValue} from './data-type.js';
+
+describe('getDataTypeFromValue', function () {
+  it('returns DataType.ANY for undefined and null values', function () {
+    const res1 = getDataTypeFromValue(undefined);
+    const res2 = getDataTypeFromValue(null);
+    expect(res1).to.be.eq(DataType.ANY);
+    expect(res2).to.be.eq(DataType.ANY);
+  });
+
+  it('returns DataType.STRING for a string value', function () {
+    const res1 = getDataTypeFromValue('value');
+    const res2 = getDataTypeFromValue('');
+    expect(res1).to.be.eq(DataType.STRING);
+    expect(res2).to.be.eq(DataType.STRING);
+  });
+
+  it('returns DataType.NUMBER for an integer value', function () {
+    const res1 = getDataTypeFromValue(10);
+    const res2 = getDataTypeFromValue(-10);
+    expect(res1).to.be.eq(DataType.NUMBER);
+    expect(res2).to.be.eq(DataType.NUMBER);
+  });
+
+  it('returns DataType.NUMBER for a float value', function () {
+    const res1 = getDataTypeFromValue(10.5);
+    const res2 = getDataTypeFromValue(-10.5);
+    expect(res1).to.be.eq(DataType.NUMBER);
+    expect(res2).to.be.eq(DataType.NUMBER);
+  });
+
+  it('returns DataType.BOOLEAN for a boolean value', function () {
+    const res1 = getDataTypeFromValue(true);
+    const res2 = getDataTypeFromValue(false);
+    expect(res1).to.be.eq(DataType.BOOLEAN);
+    expect(res2).to.be.eq(DataType.BOOLEAN);
+  });
+
+  it('returns DataType.ARRAY for an array value', function () {
+    const res1 = getDataTypeFromValue([1, 2, 3]);
+    const res2 = getDataTypeFromValue([]);
+    expect(res1).to.be.eq(DataType.ARRAY);
+    expect(res2).to.be.eq(DataType.ARRAY);
+  });
+
+  it('returns DataType.OBJECT for an object value', function () {
+    const res1 = getDataTypeFromValue({foo: 'bar'});
+    const res2 = getDataTypeFromValue({});
+    expect(res1).to.be.eq(DataType.OBJECT);
+    expect(res2).to.be.eq(DataType.OBJECT);
+  });
+});

+ 72 - 0
src/data-validator.d.ts

@@ -0,0 +1,72 @@
+import {Service, ServiceContainer} from '@e22m4u/js-service';
+import {DataSchema, DataSchemaObject} from './data-schema.js';
+import {DataSchemaDefinition} from './data-schema-definition.js';
+
+/**
+ * Data validation options.
+ */
+export type DataValidationOptions = {
+  sourcePath?: string;
+  shallowMode?: boolean;
+};
+
+/**
+ * Data validation function.
+ */
+export type DataValidationFunction = (
+  value: unknown,
+  schema: DataSchemaObject,
+  options: DataValidationOptions | undefined,
+  container: ServiceContainer,
+) => void;
+
+/**
+ * Data validator.
+ */
+export declare class DataValidator extends Service {
+  /**
+   * Get validators.
+   */
+  getValidators(): DataValidationFunction[];
+
+  /**
+   * Set validators.
+   *
+   * @param list
+   */
+  setValidators(list: DataValidationFunction[]): this;
+
+  /**
+   * Define schema.
+   * 
+   * @param schemaDef 
+   */
+  defineSchema(schemaDef: DataSchemaDefinition): this;
+
+  /**
+   * Has schema.
+   * 
+   * @param schemaName 
+   */
+  hasSchema(schemaName: string): boolean;
+
+  /**
+   * Get schema.
+   * 
+   * @param schemaName 
+   */
+  getSchema(schemaName: string): DataSchema;
+
+  /**
+   * Validate.
+   *
+   * @param value
+   * @param schema
+   * @param options
+   */
+  validate(
+    value: unknown,
+    schema: DataSchema,
+    options?: DataValidationOptions,
+  ): void;
+}

+ 205 - 0
src/data-validator.js

@@ -0,0 +1,205 @@
+import {DataType} from './data-type.js';
+import {Service} from '@e22m4u/js-service';
+import {InvalidArgumentError} from '@e22m4u/js-format';
+import {DataSchemaResolver} from './data-schema-resolver.js';
+import {validateDataSchema} from './validate-data-schema.js';
+import {DataSchemaRegistry} from './data-schema-registry.js';
+
+import {
+  arrayTypeValidator,
+  objectTypeValidator,
+  stringTypeValidator,
+  numberTypeValidator,
+  booleanTypeValidator,
+  requiredValueValidator,
+} from './data-validators/index.js';
+
+/**
+ * Data validator.
+ */
+export class DataValidator extends Service {
+  /**
+   * Validators.
+   *
+   * @type {Function[]}
+   */
+  _validators = [
+    stringTypeValidator,
+    booleanTypeValidator,
+    numberTypeValidator,
+    objectTypeValidator,
+    arrayTypeValidator,
+    requiredValueValidator,
+  ];
+
+  /**
+   * Get validators.
+   *
+   * @returns {Function[]}
+   */
+  getValidators() {
+    return [...this._validators];
+  }
+
+  /**
+   * Set validators.
+   *
+   * @param {Function[]} list
+   * @returns {this}
+   */
+  setValidators(list) {
+    if (!Array.isArray(list)) {
+      throw new InvalidArgumentError(
+        'Data validators must be an Array, but %v was given.',
+        list,
+      );
+    }
+    list.forEach(validator => {
+      if (typeof validator !== 'function') {
+        throw new InvalidArgumentError(
+          'Data validator must be a Function, but %v was given.',
+          validator,
+        );
+      }
+    });
+    this._validators = [...list];
+    return this;
+  }
+
+  /**
+   * Define schema.
+   * 
+   * @param {object} schemaDef 
+   * @returns {this}
+   */
+  defineSchema(schemaDef) {
+    this.getService(DataSchemaRegistry).defineSchema(schemaDef);
+    return this;
+  }
+
+  /**
+   * Has schema.
+   * 
+   * @param {string} schemaName 
+   * @returns {boolean}
+   */
+  hasSchema(schemaName) {
+    return this.getService(DataSchemaRegistry).hasSchema(schemaName);
+  }
+
+  /**
+   * Get schema.
+   * 
+   * @param {string} schemaName 
+   * @returns {object}
+   */
+  getSchema(schemaName) {
+    return this.getService(DataSchemaRegistry).getSchema(schemaName);
+  }
+
+  /**
+   * Validate.
+   *
+   * @param {*} value
+   * @param {object|Function|string} schema
+   * @param {object} [options]
+   */
+  validate(value, schema, options) {
+    if (options !== undefined) {
+      if (
+        options === null ||
+        typeof options !== 'object' ||
+        Array.isArray(options)
+      ) {
+        throw new InvalidArgumentError(
+          'Validation options must be an Object, but %v was given.',
+          options,
+        );
+      }
+      if (options.sourcePath !== undefined) {
+        if (!options.sourcePath || typeof options.sourcePath !== 'string') {
+          throw new InvalidArgumentError(
+            'Option "sourcePath" must be a non-empty String, but %v was given.',
+            options.sourcePath,
+          );
+        }
+      }
+      if (options.shallowMode !== undefined) {
+        if (typeof options.shallowMode !== 'boolean') {
+          throw new InvalidArgumentError(
+            'Option "shallowMode" must be a Boolean, but %v was given.',
+            options.shallowMode,
+          );
+        }
+      }
+    }
+    const sourcePath = (options && options.sourcePath) || undefined;
+    const shallowMode = Boolean(options && options.shallowMode);
+    // поверхностная проверка схемы
+    // (режим shallowMode)
+    validateDataSchema(schema, true);
+    // если схема данных не является объектом,
+    // то выполняется извлечение схемы данных
+    const schemaResolver = this.getService(DataSchemaResolver);
+    if (typeof schema !== 'object') {
+      schema = schemaResolver.resolve(schema);
+    }
+    // проверка значения валидаторами
+    // данного экземпляра согласно схеме
+    this._validators.forEach(validate => {
+      validate(value, schema, options, this.container);
+    });
+    // если активирован поверхностный режим проверки,
+    // то обработка вложенных элементов пропускается
+    if (shallowMode) {
+      return;
+    }
+    // если значение является массивом, то проверяется
+    // каждый элемент по схеме, указанной в опции items
+    if (Array.isArray(value) && schema.items !== undefined) {
+      value.forEach((item, index) => {
+        const itemPath = (sourcePath || 'array') + `[${index}]`;
+        const itemOptions = {...options, sourcePath: itemPath};
+        this.validate(item, schema.items, itemOptions);
+      });
+    }
+    // если значение является объектом, то проверяется
+    // значение каждого свойства по схеме опции properties
+    else if (
+      value !== null &&
+      typeof value === 'object' &&
+      schema.properties !== undefined
+    ) {
+      let propsSchema = schema.properties;
+      // если схема свойств не является объектом,
+      // то выполняется извлечение схемы данных
+      if (typeof propsSchema !== 'object') {
+        const resolvedSchema = schemaResolver.resolve(propsSchema);
+        // если извлеченная схема не является
+        // схемой объекта, то выбрасывается ошибка
+        if (resolvedSchema.type !== DataType.OBJECT) {
+          throw new InvalidArgumentError(
+            'Unable to get the "properties" option ' +
+              'from the data schema of %v type.',
+            resolvedSchema.type || DataType.ANY,
+          );
+        }
+        propsSchema = resolvedSchema.properties || {};
+      }
+      Object.keys(propsSchema).forEach(propName => {
+        const propSchema = propsSchema[propName];
+        // если схема текущего свойства не определена,
+        // то свойство пропускается
+        if (propSchema === undefined) {
+          return;
+        }
+        // если схема свойства определена,
+        // то выполняется проверка значения
+        const propValue = value[propName];
+        const propPath = sourcePath ? sourcePath + `.${propName}` : propName;
+        const propOptions = {...options, sourcePath: propPath};
+        this.validate(propValue, propSchema, propOptions);
+      });
+    }
+  }
+}

+ 629 - 0
src/data-validator.spec.js

@@ -0,0 +1,629 @@
+import {expect} from 'chai';
+import {DataType} from './data-type.js';
+import {format} from '@e22m4u/js-format';
+import {DataValidator} from './data-validator.js';
+import {DataValidationError} from './errors/index.js';
+import {DataSchemaRegistry} from './data-schema-registry.js';
+
+import {
+  arrayTypeValidator,
+  objectTypeValidator,
+  stringTypeValidator,
+  numberTypeValidator,
+  booleanTypeValidator,
+  requiredValueValidator,
+} from './data-validators/index.js';
+
+describe('DataValidator', function () {
+  describe('getValidators', function () {
+    it('should return a default validator list', function () {
+      const S = new DataValidator();
+      const res = S.getValidators();
+      expect(res).to.be.eql([
+        stringTypeValidator,
+        booleanTypeValidator,
+        numberTypeValidator,
+        objectTypeValidator,
+        arrayTypeValidator,
+        requiredValueValidator,
+      ]);
+    });
+
+    it('should return a modified validator list', function () {
+      const S = new DataValidator();
+      const validator1 = () => undefined;
+      const validator2 = () => undefined;
+      S.setValidators([validator1, validator2]);
+      const res = S.getValidators();
+      expect(res).to.be.eql([validator1, validator2]);
+    });
+  });
+
+  describe('setValidators', function () {
+    it('should require a given value to be an array', function () {
+      const S = new DataValidator();
+      const throwable = v => () => S.setValidators(v);
+      const error = s =>
+        format('Data validators must be an Array, but %s was given.', s);
+      expect(throwable('str')).to.throw(error('"str"'));
+      expect(throwable('')).to.throw(error('""'));
+      expect(throwable(10)).to.throw(error('10'));
+      expect(throwable(0)).to.throw(error('0'));
+      expect(throwable(true)).to.throw(error('true'));
+      expect(throwable(false)).to.throw(error('false'));
+      expect(throwable({})).to.throw(error('Object'));
+      expect(throwable(null)).to.throw(error('null'));
+      expect(throwable(undefined)).to.throw(error('undefined'));
+      throwable([])();
+    });
+
+    it('should require given validators to be a function', function () {
+      const S = new DataValidator();
+      const throwable = v => () => S.setValidators([v]);
+      const error = s =>
+        format('Data validator must be a Function, but %s was given.', s);
+      expect(throwable('str')).to.throw(error('"str"'));
+      expect(throwable('')).to.throw(error('""'));
+      expect(throwable(10)).to.throw(error('10'));
+      expect(throwable(0)).to.throw(error('0'));
+      expect(throwable(true)).to.throw(error('true'));
+      expect(throwable(false)).to.throw(error('false'));
+      expect(throwable([])).to.throw(error('Array'));
+      expect(throwable({})).to.throw(error('Object'));
+      expect(throwable(null)).to.throw(error('null'));
+      expect(throwable(undefined)).to.throw(error('undefined'));
+      throwable(() => undefined)();
+    });
+
+    it('should set the validator list', function () {
+      const S = new DataValidator();
+      const validator1 = () => undefined;
+      const validator2 = () => undefined;
+      S.setValidators([validator1, validator2]);
+      const res1 = S.getValidators();
+      expect(res1).to.be.eql([validator1, validator2]);
+    });
+
+    it('should able to clean the validator list', function () {
+      const S = new DataValidator();
+      const validator1 = () => undefined;
+      const validator2 = () => undefined;
+      S.setValidators([validator1, validator2]);
+      const res1 = S.getValidators();
+      expect(res1).to.be.eql([validator1, validator2]);
+      S.setValidators([]);
+      const res2 = S.getValidators();
+      expect(res2).to.be.eql([]);
+    });
+  });
+
+  describe('defineSchema', function() {
+    it('should pass a given schema definition to the registry', function() {
+      const S = new DataValidator();
+      const registry = S.getService(DataSchemaRegistry);
+      const schemaDef = {name: 'mySchema', schema: {}};
+      S.defineSchema(schemaDef);
+      const res = registry.getDefinition(schemaDef.name);
+      expect(res).to.be.eql(schemaDef);
+    });
+
+    it('should return a current instance', function() {
+      const S = new DataValidator();
+      const schemaDef = {name: 'mySchema', schema: {}};
+      const res = S.defineSchema(schemaDef);
+      expect(res).to.be.eq(S);
+    });
+  });
+
+  describe('hasSchema', function() {
+    it('should return true if a given name is registered', function() {
+      const S = new DataValidator();
+      const schemaDef = {name: 'mySchema', schema: {}};
+      expect(S.hasSchema(schemaDef.name)).to.be.false;
+      S.defineSchema(schemaDef);
+      expect(S.hasSchema(schemaDef.name)).to.be.true;
+    });
+  });
+
+  describe('getSchema', function() {
+    it('should return a register schema for a given name', function() {
+      const S = new DataValidator();
+      const schemaDef = {name: 'mySchema', schema: {}};
+      S.defineSchema(schemaDef);
+      const res = S.getSchema(schemaDef.name);
+      expect(res).to.be.eql(schemaDef.schema);
+    });
+
+    it('should throw an error if a given name is not registered', function() {
+      const S = new DataValidator();
+      const throwable = () => S.getSchema('mySchema');
+      expect(throwable).to.throw('Data schema "mySchema" is not found.');
+    });
+  });
+
+  describe('validate', function () {
+    it('should require the "options" argument to be an object', function () {
+      const S = new DataValidator();
+      const throwable = v => () => S.validate(10, {}, v);
+      const error = s =>
+        format('Validation options must be an Object, but %s was given.', s);
+      expect(throwable('str')).to.throw(error('"str"'));
+      expect(throwable('')).to.throw(error('""'));
+      expect(throwable(10)).to.throw(error('10'));
+      expect(throwable(0)).to.throw(error('0'));
+      expect(throwable(true)).to.throw(error('true'));
+      expect(throwable(false)).to.throw(error('false'));
+      expect(throwable([])).to.throw(error('Array'));
+      expect(throwable(null)).to.throw(error('null'));
+      expect(throwable(() => undefined)).to.throw(error('Function'));
+      throwable({})();
+      throwable(undefined)();
+    });
+
+    it('should require the "sourcePath" argument to be a non-empty string', function () {
+      const S = new DataValidator();
+      const throwable = v => () => S.validate(10, {}, {sourcePath: v});
+      const error = s =>
+        format(
+          'Option "sourcePath" must be a non-empty String, but %s was given.',
+          s,
+        );
+      expect(throwable('')).to.throw(error('""'));
+      expect(throwable(10)).to.throw(error('10'));
+      expect(throwable(0)).to.throw(error('0'));
+      expect(throwable(true)).to.throw(error('true'));
+      expect(throwable(false)).to.throw(error('false'));
+      expect(throwable([])).to.throw(error('Array'));
+      expect(throwable({})).to.throw(error('Object'));
+      expect(throwable(null)).to.throw(error('null'));
+      expect(throwable(() => undefined)).to.throw(error('Function'));
+      throwable('str')();
+      throwable(undefined)();
+    });
+
+    it('should require the "shallowMode" argument to be a boolean', function () {
+      const S = new DataValidator();
+      const throwable = v => () => S.validate(10, {}, {shallowMode: v});
+      const error = s =>
+        format('Option "shallowMode" must be a Boolean, but %s was given.', s);
+      expect(throwable('str')).to.throw(error('"str"'));
+      expect(throwable('')).to.throw(error('""'));
+      expect(throwable(10)).to.throw(error('10'));
+      expect(throwable(0)).to.throw(error('0'));
+      expect(throwable([])).to.throw(error('Array'));
+      expect(throwable({})).to.throw(error('Object'));
+      expect(throwable(null)).to.throw(error('null'));
+      expect(throwable(() => undefined)).to.throw(error('Function'));
+      throwable(true)();
+      throwable(false)();
+      throwable(undefined)();
+    });
+
+    it('should validate a given schema in the shallow mode', function () {
+      const S = new DataValidator();
+      const throwable = () => S.validate({}, {required: 10});
+      expect(throwable).to.throw(
+        'Schema option "required" must be a Boolean, but 10 was given.',
+      );
+      S.validate([], {type: DataType.ARRAY, items: {type: 10}});
+    });
+
+    it('should resolve the data schema from a given factory', function () {
+      const S = new DataValidator();
+      S.setValidators([stringTypeValidator]);
+      const throwable = () => S.validate(10, () => ({type: DataType.STRING}));
+      expect(throwable).to.throw(DataValidationError);
+    });
+
+    it('should resolve the data schema from a schema name', function () {
+      const S = new DataValidator();
+      S.setValidators([stringTypeValidator]);
+      S.getService(DataSchemaRegistry).defineSchema({
+        name: 'mySchema',
+        schema: {type: DataType.STRING},
+      });
+      const throwable = () => S.validate(10, 'mySchema');
+      expect(throwable).to.throw(DataValidationError);
+    });
+
+    it('should resolve a schema name from a given factory', function () {
+      const S = new DataValidator();
+      S.setValidators([stringTypeValidator]);
+      S.getService(DataSchemaRegistry).defineSchema({
+        name: 'mySchema',
+        schema: {type: DataType.STRING},
+      });
+      const throwable = () => S.validate(10, () => 'mySchema');
+      expect(throwable).to.throw(DataValidationError);
+    });
+
+    it('should resolve a schema factory from a named schema', function () {
+      const S = new DataValidator();
+      S.setValidators([stringTypeValidator]);
+      S.getService(DataSchemaRegistry).defineSchema({
+        name: 'mySchema',
+        schema: () => ({type: DataType.STRING}),
+      });
+      const throwable = () => S.validate(10, 'mySchema');
+      expect(throwable).to.throw(DataValidationError);
+    });
+
+    it('should pass specific arguments to data validators', function () {
+      const S = new DataValidator();
+      const value = 10;
+      const schema = {type: DataType.NUMBER};
+      const options = {sourcePath: 'aSource'};
+      let invoked = 0;
+      const validator = (...args) => {
+        invoked++;
+        expect(args).to.be.eql([value, schema, options, S.container]);
+      };
+      S.setValidators([validator]);
+      S.validate(value, schema, options);
+      expect(invoked).to.be.eq(1);
+    });
+
+    it('should propagate an error from the data validator', function () {
+      const S = new DataValidator();
+      const validator = () => {
+        throw new Error('Caught!');
+      };
+      S.setValidators([validator]);
+      const throwable = () => S.validate(10, {});
+      expect(throwable).to.throw('Caught!');
+    });
+
+    it('should apply parsers sequentially to a given value', function () {
+      const S = new DataValidator();
+      const value = [1, 2, 3];
+      const schema = {
+        type: DataType.ARRAY,
+        items: {type: DataType.NUMBER},
+      };
+      const expectedCalls = [
+        ['A', value, schema, undefined, S.container],
+        ['B', value, schema, undefined, S.container],
+        ['A', 1, schema.items, {sourcePath: 'array[0]'}, S.container],
+        ['B', 1, schema.items, {sourcePath: 'array[0]'}, S.container],
+        ['A', 2, schema.items, {sourcePath: 'array[1]'}, S.container],
+        ['B', 2, schema.items, {sourcePath: 'array[1]'}, S.container],
+        ['A', 3, schema.items, {sourcePath: 'array[2]'}, S.container],
+        ['B', 3, schema.items, {sourcePath: 'array[2]'}, S.container],
+      ];
+      const calls = [];
+      const validator = name => {
+        return (...args) => {
+          calls.push([name, ...args]);
+        };
+      };
+      S.setValidators([validator('A'), validator('B')]);
+      S.validate(value, schema);
+      expect(expectedCalls).to.be.eql(calls);
+    });
+
+    it('should ignore array items in the shallow mode even if the items schema is provided', function () {
+      const S = new DataValidator();
+      const value = [1, 2, 3];
+      const schema = {
+        type: DataType.ARRAY,
+        items: {type: DataType.STRING},
+      };
+      S.setValidators([stringTypeValidator]);
+      S.validate(value, schema, {shallowMode: true});
+    });
+
+    it('should ignore array items when the items schema is not provided', function () {
+      const S = new DataValidator();
+      const value = [1, 2, 3];
+      const schema = {type: DataType.ARRAY};
+      let invoked = 0;
+      const validator = (...args) => {
+        invoked++;
+        expect(args[0]).to.be.eql(value);
+      };
+      S.setValidators([validator]);
+      S.validate(value, schema);
+      expect(invoked).to.be.eq(1);
+    });
+
+    it('should validate array items when the items schema is provided', function () {
+      const S = new DataValidator();
+      const value = [1, 2, 3];
+      const schema = {
+        type: DataType.ARRAY,
+        items: {type: DataType.NUMBER},
+      };
+      const expectedCalls = [
+        [value, schema, undefined, S.container],
+        [1, schema.items, {sourcePath: 'array[0]'}, S.container],
+        [2, schema.items, {sourcePath: 'array[1]'}, S.container],
+        [3, schema.items, {sourcePath: 'array[2]'}, S.container],
+      ];
+      const calls = [];
+      const validator = (...args) => {
+        calls.push(args);
+      };
+      S.setValidators([validator]);
+      S.validate(value, schema);
+      expect(expectedCalls).to.be.eql(calls);
+    });
+
+    it('should add an array index to a provided source path', function () {
+      const S = new DataValidator();
+      const value = [1, 2, 3];
+      const schema = {
+        type: DataType.ARRAY,
+        items: {type: DataType.NUMBER},
+      };
+      const options = {sourcePath: 'mySource'};
+      const expectedCalls = [
+        [value, schema, options, S.container],
+        [1, schema.items, {sourcePath: 'mySource[0]'}, S.container],
+        [2, schema.items, {sourcePath: 'mySource[1]'}, S.container],
+        [3, schema.items, {sourcePath: 'mySource[2]'}, S.container],
+      ];
+      const calls = [];
+      const validator = (...args) => {
+        calls.push(args);
+      };
+      S.setValidators([validator]);
+      S.validate(value, schema, options);
+      expect(expectedCalls).to.be.eql(calls);
+    });
+
+    it('should resolve a schema factory from the "items" option', function () {
+      const S = new DataValidator();
+      S.setValidators([stringTypeValidator]);
+      const factory = () => ({type: DataType.STRING});
+      const schema = {type: DataType.ARRAY, items: factory};
+      const throwable = () => S.validate([10], schema);
+      expect(throwable).to.throw(DataValidationError);
+    });
+
+    it('should resolve a schema name from the "items" option', function () {
+      const S = new DataValidator();
+      S.setValidators([stringTypeValidator]);
+      const schemaA = {
+        type: DataType.ARRAY,
+        items: 'schemaB',
+      };
+      S.getService(DataSchemaRegistry).defineSchema({
+        name: 'schemaB',
+        schema: {type: DataType.STRING},
+      });
+      const throwable = () => S.validate([10], schemaA);
+      expect(throwable).to.throw(DataValidationError);
+    });
+
+    it('should ignore object properties in the shallow mode even if the properties schema is provided', function () {
+      const S = new DataValidator();
+      const value = {p1: 1, p2: 2, p3: 3};
+      const schema = {
+        type: DataType.OBJECT,
+        properties: {
+          p1: {type: DataType.STRING},
+          p2: {type: DataType.STRING},
+          p3: {type: DataType.STRING},
+        },
+      };
+      S.setValidators([stringTypeValidator]);
+      S.validate(value, schema, {shallowMode: true});
+    });
+
+    it('should ignore object properties when the properties schema is not provided', function () {
+      const S = new DataValidator();
+      const value = {p1: 1, p2: 2, p3: 3};
+      const schema = {type: DataType.OBJECT};
+      let invoked = 0;
+      const validator = (...args) => {
+        invoked++;
+        expect(args).to.be.eql([value, schema, undefined, S.container]);
+      };
+      S.setValidators([validator]);
+      S.validate(value, schema);
+      expect(invoked).to.be.eq(1);
+    });
+
+    it('should validate object properties when the properties schema is provided', function () {
+      const S = new DataValidator();
+      const value = {p1: 1, p2: 2, p3: 3};
+      const schema = {
+        type: DataType.OBJECT,
+        properties: {
+          p1: {type: DataType.NUMBER},
+          p2: {type: DataType.NUMBER},
+          p3: {type: DataType.NUMBER},
+        },
+      };
+      const expectedCalls = [
+        [value, schema, undefined, S.container],
+        [1, schema.properties.p1, {sourcePath: 'p1'}, S.container],
+        [2, schema.properties.p2, {sourcePath: 'p2'}, S.container],
+        [3, schema.properties.p3, {sourcePath: 'p3'}, S.container],
+      ];
+      const calls = [];
+      const validator = (...args) => {
+        calls.push(args);
+      };
+      S.setValidators([validator]);
+      S.validate(value, schema);
+      expect(expectedCalls).to.be.eql(calls);
+    });
+
+    it('should apply validators sequentially to a given object and its properties', function () {
+      const S = new DataValidator();
+      const value = {p1: 1, p2: 2, p3: 3};
+      const schema = {
+        type: DataType.OBJECT,
+        properties: {
+          p1: {type: DataType.NUMBER},
+          p2: {type: DataType.NUMBER},
+          p3: {type: DataType.NUMBER},
+        },
+      };
+      const expectedCalls = [
+        ['A', value, schema, undefined, S.container],
+        ['B', value, schema, undefined, S.container],
+        ['A', 1, schema.properties.p1, {sourcePath: 'p1'}, S.container],
+        ['B', 1, schema.properties.p1, {sourcePath: 'p1'}, S.container],
+        ['A', 2, schema.properties.p2, {sourcePath: 'p2'}, S.container],
+        ['B', 2, schema.properties.p2, {sourcePath: 'p2'}, S.container],
+        ['A', 3, schema.properties.p3, {sourcePath: 'p3'}, S.container],
+        ['B', 3, schema.properties.p3, {sourcePath: 'p3'}, S.container],
+      ];
+      const calls = [];
+      const validator = name => {
+        return (...args) => {
+          calls.push([name, ...args]);
+        };
+      };
+      S.setValidators([validator('A'), validator('B')]);
+      S.validate(value, schema);
+      expect(expectedCalls).to.be.eql(calls);
+    });
+
+    it('should ignore object properties without a specified schema', function () {
+      const S = new DataValidator();
+      const value = {p1: 1, p2: 2, p3: 3};
+      const schema = {
+        type: DataType.OBJECT,
+        properties: {
+          p1: {type: DataType.NUMBER},
+        },
+      };
+      const expectedCalls = [
+        [value, schema, undefined, S.container],
+        [value.p1, schema.properties.p1, {sourcePath: 'p1'}, S.container],
+      ];
+      const calls = [];
+      const validator = (...args) => {
+        calls.push(args);
+      };
+      S.setValidators([validator]);
+      S.validate(value, schema);
+      expect(expectedCalls).to.be.eql(calls);
+    });
+
+    it('should add property name to a provided source path', function () {
+      const S = new DataValidator();
+      const value = {p1: 1, p2: 2, p3: 3};
+      const schema = {
+        type: DataType.OBJECT,
+        properties: {
+          p1: {type: DataType.NUMBER},
+          p2: {type: DataType.NUMBER},
+          p3: {type: DataType.NUMBER},
+        },
+      };
+      const options = {sourcePath: 'mySource'};
+      const expectedCalls = [
+        [value, schema, options, S.container],
+        [1, schema.properties.p1, {sourcePath: 'mySource.p1'}, S.container],
+        [2, schema.properties.p2, {sourcePath: 'mySource.p2'}, S.container],
+        [3, schema.properties.p3, {sourcePath: 'mySource.p3'}, S.container],
+      ];
+      const calls = [];
+      const validator = (...args) => {
+        calls.push(args);
+      };
+      S.setValidators([validator]);
+      S.validate(value, schema, options);
+      expect(expectedCalls).to.be.eql(calls);
+    });
+
+    it('should resolve a schema factory from the "properties" option', function () {
+      const S = new DataValidator();
+      S.setValidators([stringTypeValidator]);
+      const factory = () => ({
+        type: DataType.OBJECT,
+        properties: {
+          prop: {type: DataType.STRING},
+        },
+      });
+      const schema = {
+        type: DataType.OBJECT,
+        properties: factory,
+      };
+      const throwable = () => S.validate({prop: 10}, schema);
+      expect(throwable).to.throw(DataValidationError);
+    });
+
+    it('should resolve a schema name from the "properties" option', function () {
+      const S = new DataValidator();
+      S.setValidators([stringTypeValidator]);
+      const schemaA = {
+        type: DataType.OBJECT,
+        properties: 'schemaB',
+      };
+      S.getService(DataSchemaRegistry).defineSchema({
+        name: 'schemaB',
+        schema: {
+          type: DataType.OBJECT,
+          properties: {
+            prop: {type: DataType.STRING},
+          },
+        },
+      });
+      const throwable = () => S.validate({prop: 10}, schemaA);
+      expect(throwable).to.throw(DataValidationError);
+    });
+
+    it('should throw an error if a properties schema from the schema factory is a non-array schema', function () {
+      const S = new DataValidator();
+      const factory = () => ({type: DataType.BOOLEAN});
+      const schema = {
+        type: DataType.OBJECT,
+        properties: factory,
+      };
+      const throwable = () => S.validate({prop: 10}, schema);
+      expect(throwable).to.throw(
+        'Unable to get the "properties" option ' +
+          'from the data schema of "boolean" type.',
+      );
+    });
+
+    it('should throw an error if a properties schema from the schema name is a non-array schema', function () {
+      const S = new DataValidator();
+      const schemaA = {
+        type: DataType.OBJECT,
+        properties: 'schemaB',
+      };
+      S.getService(DataSchemaRegistry).defineSchema({
+        name: 'schemaB',
+        schema: {type: DataType.BOOLEAN},
+      });
+      const throwable = () => S.validate({prop: 10}, schemaA);
+      expect(throwable).to.throw(
+        'Unable to get the "properties" option ' +
+          'from the data schema of "boolean" type.',
+      );
+    });
+
+    it('should resolve a schema factory from the property schema', function () {
+      const S = new DataValidator();
+      S.setValidators([stringTypeValidator]);
+      const factory = () => ({type: DataType.STRING});
+      const schema = {
+        type: DataType.OBJECT,
+        properties: {prop: factory},
+      };
+      const throwable = () => S.validate({prop: 10}, schema);
+      expect(throwable).to.throw(DataValidationError);
+    });
+
+    it('should resolve a schema name from the property schema', function () {
+      const S = new DataValidator();
+      S.setValidators([stringTypeValidator]);
+      const schemaA = {
+        type: DataType.OBJECT,
+        properties: {prop: 'schemaB'},
+      };
+      S.getService(DataSchemaRegistry).defineSchema({
+        name: 'schemaB',
+        schema: {type: DataType.STRING},
+      });
+      const throwable = () => S.validate({prop: 10}, schemaA);
+      expect(throwable).to.throw(DataValidationError);
+    });
+  });
+});

+ 6 - 0
src/data-validators/array-type-validator.d.ts

@@ -0,0 +1,6 @@
+import {DataValidationFunction} from "../data-validator.js";
+
+/**
+ * Array type validator.
+ */
+export declare const arrayTypeValidator: DataValidationFunction;

+ 47 - 0
src/data-validators/array-type-validator.js

@@ -0,0 +1,47 @@
+import {DataType} from '../data-type.js';
+import {ServiceContainer} from '@e22m4u/js-service';
+import {DataValidationError} from '../errors/index.js';
+import {EmptyValuesService} from '@e22m4u/js-empty-values';
+
+/**
+ * Array type validator.
+ *
+ * @param {*} value
+ * @param {object} schema
+ * @param {object|undefined} options
+ * @param {ServiceContainer} container
+ */
+export function arrayTypeValidator(value, schema, options, container) {
+  // если тип не соответствует массиву,
+  // то проверка пропускается
+  if (schema.type !== DataType.ARRAY) {
+    return;
+  }
+  // если значение является пустым,
+  // то проверка пропускается
+  const emptyValues = container.get(EmptyValuesService);
+  const dataType = schema.type || DataType.ANY;
+  if (emptyValues.isEmptyByType(dataType, value)) {
+    return;
+  }
+  // если значение является массивом,
+  // то проверка успешно завершается
+  if (Array.isArray(value)) {
+    return;
+  }
+  // если значение не является массивом,
+  // то выбрасывается ошибка
+  const sourcePath = options && options.sourcePath;
+  if (sourcePath) {
+    throw new DataValidationError(
+      'Value of %v must be an Array, but %v was given.',
+      sourcePath,
+      value,
+    );
+  } else {
+    throw new DataValidationError(
+      'Value must be an Array, but %v was given.',
+      value,
+    );
+  }
+}

+ 55 - 0
src/data-validators/array-type-validator.spec.js

@@ -0,0 +1,55 @@
+import {expect} from 'chai';
+import {format} from '@e22m4u/js-format';
+import {DataType} from '../data-type.js';
+import {ServiceContainer} from '@e22m4u/js-service';
+import {EmptyValuesService} from '@e22m4u/js-empty-values';
+import {arrayTypeValidator} from './array-type-validator.js';
+
+const validate = arrayTypeValidator;
+
+describe('arrayTypeValidator', function () {
+  it('should not validate non-array types', function () {
+    const cont = new ServiceContainer();
+    const value = Symbol('invalidValue');
+    validate(value, {}, undefined, cont);
+    validate(value, {type: DataType.ANY}, undefined, cont);
+    validate(value, {type: DataType.STRING}, undefined, cont);
+    validate(value, {type: DataType.NUMBER}, undefined, cont);
+    validate(value, {type: DataType.BOOLEAN}, undefined, cont);
+    validate(value, {type: DataType.OBJECT}, undefined, cont);
+  });
+
+  it('should not validate empty values', function () {
+    const cont = new ServiceContainer();
+    const emptyValues = cont.get(EmptyValuesService);
+    emptyValues.setEmptyValuesOf(DataType.ARRAY, [[]]);
+    validate([], {type: DataType.ARRAY}, undefined, cont);
+  });
+
+  it('should throw an error for an invalid value', function () {
+    const cont = new ServiceContainer();
+    const schema = {type: DataType.ARRAY};
+    const throwable = v => () => validate(v, schema, undefined, cont);
+    const error = s => format('Value must be an Array, but %s was given.', s);
+    expect(throwable('str')).to.throw(error('"str"'));
+    expect(throwable('')).to.throw(error('""'));
+    expect(throwable(10)).to.throw(error('10'));
+    expect(throwable(0)).to.throw(error('0'));
+    expect(throwable(true)).to.throw(error('true'));
+    expect(throwable(false)).to.throw(error('false'));
+    expect(throwable({p: 1})).to.throw(error('Object'));
+    expect(throwable({})).to.throw(error('Object'));
+    expect(throwable(() => undefined)).to.throw(error('Function'));
+    throwable([1])();
+    throwable([])();
+  });
+
+  it('should add the source path to error message', function () {
+    const cont = new ServiceContainer();
+    const schema = {type: DataType.ARRAY};
+    const options = {sourcePath: 'array[0]'};
+    const throwable = () => validate('str', schema, options, cont);
+    const error = 'Value of "array[0]" must be an Array, but "str" was given.';
+    expect(throwable).to.throw(error);
+  });
+});

+ 6 - 0
src/data-validators/boolean-type-validator.d.ts

@@ -0,0 +1,6 @@
+import {DataValidationFunction} from "../data-validator.js";
+
+/**
+ * Boolean type validator.
+ */
+export declare const booleanTypeValidator: DataValidationFunction;

+ 47 - 0
src/data-validators/boolean-type-validator.js

@@ -0,0 +1,47 @@
+import {DataType} from '../data-type.js';
+import {ServiceContainer} from '@e22m4u/js-service';
+import {DataValidationError} from '../errors/index.js';
+import {EmptyValuesService} from '@e22m4u/js-empty-values';
+
+/**
+ * Boolean type validator.
+ *
+ * @param {*} value
+ * @param {object} schema
+ * @param {object|undefined} options
+ * @param {ServiceContainer} container
+ */
+export function booleanTypeValidator(value, schema, options, container) {
+  // если тип не соответствует логическому
+  // значению, то проверка пропускается
+  if (schema.type !== DataType.BOOLEAN) {
+    return;
+  }
+  // если значение является пустым,
+  // то проверка пропускается
+  const emptyValues = container.get(EmptyValuesService);
+  const dataType = schema.type || DataType.ANY;
+  if (emptyValues.isEmptyByType(dataType, value)) {
+    return;
+  }
+  // если значение является логическим,
+  // то проверка успешно завершается
+  if (typeof value === 'boolean') {
+    return;
+  }
+  // если значение не является логическим,
+  // то выбрасывается ошибка
+  const sourcePath = options && options.sourcePath;
+  if (sourcePath) {
+    throw new DataValidationError(
+      'Value of %v must be a Boolean, but %v was given.',
+      sourcePath,
+      value,
+    );
+  } else {
+    throw new DataValidationError(
+      'Value must be a Boolean, but %v was given.',
+      value,
+    );
+  }
+}

+ 55 - 0
src/data-validators/boolean-type-validator.spec.js

@@ -0,0 +1,55 @@
+import {expect} from 'chai';
+import {format} from '@e22m4u/js-format';
+import {DataType} from '../data-type.js';
+import {ServiceContainer} from '@e22m4u/js-service';
+import {EmptyValuesService} from '@e22m4u/js-empty-values';
+import {booleanTypeValidator} from './boolean-type-validator.js';
+
+const validate = booleanTypeValidator;
+
+describe('booleanTypeValidator', function () {
+  it('should not validate non-boolean types', function () {
+    const cont = new ServiceContainer();
+    const value = Symbol('invalidValue');
+    validate(value, {}, undefined, cont);
+    validate(value, {type: DataType.ANY}, undefined, cont);
+    validate(value, {type: DataType.STRING}, undefined, cont);
+    validate(value, {type: DataType.NUMBER}, undefined, cont);
+    validate(value, {type: DataType.ARRAY}, undefined, cont);
+    validate(value, {type: DataType.OBJECT}, undefined, cont);
+  });
+
+  it('should not validate empty values', function () {
+    const cont = new ServiceContainer();
+    const emptyValues = cont.get(EmptyValuesService);
+    emptyValues.setEmptyValuesOf(DataType.BOOLEAN, [false]);
+    validate(false, {type: DataType.BOOLEAN}, undefined, cont);
+  });
+
+  it('should throw an error for an invalid value', function () {
+    const cont = new ServiceContainer();
+    const schema = {type: DataType.BOOLEAN};
+    const throwable = v => () => validate(v, schema, undefined, cont);
+    const error = s => format('Value must be a Boolean, but %s was given.', s);
+    expect(throwable('str')).to.throw(error('"str"'));
+    expect(throwable('')).to.throw(error('""'));
+    expect(throwable(10)).to.throw(error('10'));
+    expect(throwable(0)).to.throw(error('0'));
+    expect(throwable([1])).to.throw(error('Array'));
+    expect(throwable([])).to.throw(error('Array'));
+    expect(throwable({p: 1})).to.throw(error('Object'));
+    expect(throwable({})).to.throw(error('Object'));
+    expect(throwable(() => undefined)).to.throw(error('Function'));
+    throwable(true)();
+    throwable(false)();
+  });
+
+  it('should add the source path to error message', function () {
+    const cont = new ServiceContainer();
+    const schema = {type: DataType.BOOLEAN};
+    const options = {sourcePath: 'array[0]'};
+    const throwable = () => validate('str', schema, options, cont);
+    const error = 'Value of "array[0]" must be a Boolean, but "str" was given.';
+    expect(throwable).to.throw(error);
+  });
+});

+ 6 - 0
src/data-validators/index.d.ts

@@ -0,0 +1,6 @@
+export * from './array-type-validator.js';
+export * from './object-type-validator.js';
+export * from './string-type-validator.js';
+export * from './number-type-validator.js';
+export * from './boolean-type-validator.js';
+export * from './required-value-validator.js';

+ 6 - 0
src/data-validators/index.js

@@ -0,0 +1,6 @@
+export * from './array-type-validator.js';
+export * from './object-type-validator.js';
+export * from './string-type-validator.js';
+export * from './number-type-validator.js';
+export * from './boolean-type-validator.js';
+export * from './required-value-validator.js';

+ 6 - 0
src/data-validators/number-type-validator.d.ts

@@ -0,0 +1,6 @@
+import {DataValidationFunction} from "../data-validator.js";
+
+/**
+ * Number type validator.
+ */
+export declare const numberTypeValidator: DataValidationFunction;

+ 47 - 0
src/data-validators/number-type-validator.js

@@ -0,0 +1,47 @@
+import {DataType} from '../data-type.js';
+import {ServiceContainer} from '@e22m4u/js-service';
+import {DataValidationError} from '../errors/index.js';
+import {EmptyValuesService} from '@e22m4u/js-empty-values';
+
+/**
+ * Number type validator.
+ *
+ * @param {*} value
+ * @param {object} schema
+ * @param {object|undefined} options
+ * @param {ServiceContainer} container
+ */
+export function numberTypeValidator(value, schema, options, container) {
+  // если тип не соответствует числу,
+  // то проверка пропускается
+  if (schema.type !== DataType.NUMBER) {
+    return;
+  }
+  // если значение является пустым,
+  // то проверка пропускается
+  const emptyValues = container.get(EmptyValuesService);
+  const dataType = schema.type || DataType.ANY;
+  if (emptyValues.isEmptyByType(dataType, value)) {
+    return;
+  }
+  // если значение является числом,
+  // то проверка успешно завершается
+  if (typeof value === 'number') {
+    return;
+  }
+  // если значение не является числом,
+  // то выбрасывается ошибка
+  const sourcePath = options && options.sourcePath;
+  if (sourcePath) {
+    throw new DataValidationError(
+      'Value of %v must be a Number, but %v was given.',
+      sourcePath,
+      value,
+    );
+  } else {
+    throw new DataValidationError(
+      'Value must be a Number, but %v was given.',
+      value,
+    );
+  }
+}

+ 55 - 0
src/data-validators/number-type-validator.spec.js

@@ -0,0 +1,55 @@
+import {expect} from 'chai';
+import {format} from '@e22m4u/js-format';
+import {DataType} from '../data-type.js';
+import {ServiceContainer} from '@e22m4u/js-service';
+import {EmptyValuesService} from '@e22m4u/js-empty-values';
+import {numberTypeValidator} from './number-type-validator.js';
+
+const validate = numberTypeValidator;
+
+describe('numberTypeValidator', function () {
+  it('should not validate non-number types', function () {
+    const cont = new ServiceContainer();
+    const value = Symbol('invalidValue');
+    validate(value, {}, undefined, cont);
+    validate(value, {type: DataType.ANY}, undefined, cont);
+    validate(value, {type: DataType.STRING}, undefined, cont);
+    validate(value, {type: DataType.BOOLEAN}, undefined, cont);
+    validate(value, {type: DataType.ARRAY}, undefined, cont);
+    validate(value, {type: DataType.OBJECT}, undefined, cont);
+  });
+
+  it('should not validate empty values', function () {
+    const cont = new ServiceContainer();
+    const emptyValues = cont.get(EmptyValuesService);
+    emptyValues.setEmptyValuesOf(DataType.NUMBER, [0]);
+    validate(0, {type: DataType.NUMBER}, undefined, cont);
+  });
+
+  it('should throw an error for an invalid value', function () {
+    const cont = new ServiceContainer();
+    const schema = {type: DataType.NUMBER};
+    const throwable = v => () => validate(v, schema, undefined, cont);
+    const error = s => format('Value must be a Number, but %s was given.', s);
+    expect(throwable('str')).to.throw(error('"str"'));
+    expect(throwable('')).to.throw(error('""'));
+    expect(throwable(true)).to.throw(error('true'));
+    expect(throwable(false)).to.throw(error('false'));
+    expect(throwable([1])).to.throw(error('Array'));
+    expect(throwable([])).to.throw(error('Array'));
+    expect(throwable({p: 1})).to.throw(error('Object'));
+    expect(throwable({})).to.throw(error('Object'));
+    expect(throwable(() => undefined)).to.throw(error('Function'));
+    throwable(10)();
+    throwable(0)();
+  });
+
+  it('should add the source path to error message', function () {
+    const cont = new ServiceContainer();
+    const schema = {type: DataType.NUMBER};
+    const options = {sourcePath: 'array[0]'};
+    const throwable = () => validate('str', schema, options, cont);
+    const error = 'Value of "array[0]" must be a Number, but "str" was given.';
+    expect(throwable).to.throw(error);
+  });
+});

+ 6 - 0
src/data-validators/object-type-validator.d.ts

@@ -0,0 +1,6 @@
+import {DataValidationFunction} from "../data-validator.js";
+
+/**
+ * Object type validator.
+ */
+export declare const objectTypeValidator: DataValidationFunction;

+ 47 - 0
src/data-validators/object-type-validator.js

@@ -0,0 +1,47 @@
+import {DataType} from '../data-type.js';
+import {ServiceContainer} from '@e22m4u/js-service';
+import {DataValidationError} from '../errors/index.js';
+import {EmptyValuesService} from '@e22m4u/js-empty-values';
+
+/**
+ * Object type validator.
+ *
+ * @param {*} value
+ * @param {object} schema
+ * @param {object|undefined} options
+ * @param {ServiceContainer} container
+ */
+export function objectTypeValidator(value, schema, options, container) {
+  // если тип не соответствует объекту,
+  // то проверка пропускается
+  if (schema.type !== DataType.OBJECT) {
+    return;
+  }
+  // если значение является пустым,
+  // то проверка пропускается
+  const emptyValues = container.get(EmptyValuesService);
+  const dataType = schema.type || DataType.ANY;
+  if (emptyValues.isEmptyByType(dataType, value)) {
+    return;
+  }
+  // если значение является объектом,
+  // то проверка успешно завершается
+  if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
+    return;
+  }
+  // если значение не является объектом,
+  // то выбрасывается ошибка
+  const sourcePath = options && options.sourcePath;
+  if (sourcePath) {
+    throw new DataValidationError(
+      'Value of %v must be an Object, but %v was given.',
+      sourcePath,
+      value,
+    );
+  } else {
+    throw new DataValidationError(
+      'Value must be an Object, but %v was given.',
+      value,
+    );
+  }
+}

+ 55 - 0
src/data-validators/object-type-validator.spec.js

@@ -0,0 +1,55 @@
+import {expect} from 'chai';
+import {format} from '@e22m4u/js-format';
+import {DataType} from '../data-type.js';
+import {ServiceContainer} from '@e22m4u/js-service';
+import {EmptyValuesService} from '@e22m4u/js-empty-values';
+import {objectTypeValidator} from './object-type-validator.js';
+
+const validate = objectTypeValidator;
+
+describe('objectTypeValidator', function () {
+  it('should not validate non-object types', function () {
+    const cont = new ServiceContainer();
+    const value = Symbol('invalidValue');
+    validate(value, {}, undefined, cont);
+    validate(value, {type: DataType.ANY}, undefined, cont);
+    validate(value, {type: DataType.STRING}, undefined, cont);
+    validate(value, {type: DataType.NUMBER}, undefined, cont);
+    validate(value, {type: DataType.BOOLEAN}, undefined, cont);
+    validate(value, {type: DataType.ARRAY}, undefined, cont);
+  });
+
+  it('should not validate empty values', function () {
+    const cont = new ServiceContainer();
+    const emptyValues = cont.get(EmptyValuesService);
+    emptyValues.setEmptyValuesOf(DataType.OBJECT, [{}]);
+    validate({}, {type: DataType.OBJECT}, undefined, cont);
+  });
+
+  it('should throw an error for an invalid value', function () {
+    const cont = new ServiceContainer();
+    const schema = {type: DataType.OBJECT};
+    const throwable = v => () => validate(v, schema, undefined, cont);
+    const error = s => format('Value must be an Object, but %s was given.', s);
+    expect(throwable('str')).to.throw(error('"str"'));
+    expect(throwable('')).to.throw(error('""'));
+    expect(throwable(10)).to.throw(error('10'));
+    expect(throwable(0)).to.throw(error('0'));
+    expect(throwable(true)).to.throw(error('true'));
+    expect(throwable(false)).to.throw(error('false'));
+    expect(throwable([1])).to.throw(error('Array'));
+    expect(throwable([])).to.throw(error('Array'));
+    expect(throwable(() => undefined)).to.throw(error('Function'));
+    throwable({p: 1})();
+    throwable({})();
+  });
+
+  it('should add the source path to error message', function () {
+    const cont = new ServiceContainer();
+    const schema = {type: DataType.OBJECT};
+    const options = {sourcePath: 'array[0]'};
+    const throwable = () => validate('str', schema, options, cont);
+    const error = 'Value of "array[0]" must be an Object, but "str" was given.';
+    expect(throwable).to.throw(error);
+  });
+});

+ 6 - 0
src/data-validators/required-value-validator.d.ts

@@ -0,0 +1,6 @@
+import {DataValidationFunction} from "../data-validator.js";
+
+/**
+ * Required value validator.
+ */
+export declare const requiredValueValidator: DataValidationFunction;

+ 42 - 0
src/data-validators/required-value-validator.js

@@ -0,0 +1,42 @@
+import {DataType} from '../data-type.js';
+import {ServiceContainer} from '@e22m4u/js-service';
+import {DataValidationError} from '../errors/index.js';
+import {EmptyValuesService} from '@e22m4u/js-empty-values';
+
+/**
+ * Required value validator.
+ *
+ * @param {*} value
+ * @param {object} schema
+ * @param {object|undefined} options
+ * @param {ServiceContainer} container
+ */
+export function requiredValueValidator(value, schema, options, container) {
+  // если значение не является обязательным,
+  // то проверка пропускается
+  if (schema.required !== true) {
+    return;
+  }
+  // если значение не является пустым,
+  // то проверка успешно завершается
+  const emptyValues = container.get(EmptyValuesService);
+  const dataType = schema.type || DataType.ANY;
+  if (!emptyValues.isEmptyByType(dataType, value)) {
+    return;
+  }
+  // если значение является пустым,
+  // то выбрасывается ошибка
+  const sourcePath = options && options.sourcePath;
+  if (sourcePath) {
+    throw new DataValidationError(
+      'Value of %v is required, but %v was given.',
+      sourcePath,
+      value,
+    );
+  } else {
+    throw new DataValidationError(
+      'Value is required, but %v was given.',
+      value,
+    );
+  }
+}

+ 46 - 0
src/data-validators/required-value-validator.spec.js

@@ -0,0 +1,46 @@
+import {expect} from 'chai';
+import {DataType} from '../data-type.js';
+import {ServiceContainer} from '@e22m4u/js-service';
+import {EmptyValuesService} from '@e22m4u/js-empty-values';
+import {requiredValueValidator} from './required-value-validator.js';
+
+const validate = requiredValueValidator;
+
+describe('requiredValueValidator', function () {
+  it('should skip not required value', function () {
+    const cont = new ServiceContainer();
+    const emptyValues = cont.get(EmptyValuesService);
+    emptyValues.setEmptyValuesOf(DataType.ANY, ['none']);
+    validate('none', {}, undefined, cont);
+    validate('none', {required: false}, undefined, cont);
+  });
+
+  it('should throw an error when required value is empty', function () {
+    const cont = new ServiceContainer();
+    const emptyValues = cont.get(EmptyValuesService);
+    emptyValues.setEmptyValuesOf(DataType.ANY, ['none']);
+    const schema = {required: true};
+    const throwable = () => validate('none', schema, undefined, cont);
+    expect(throwable).to.throw('Value is required, but "none" was given.');
+  });
+
+  it('should throw an error when required value is empty when the type is specified', function () {
+    const cont = new ServiceContainer();
+    const emptyValues = cont.get(EmptyValuesService);
+    emptyValues.setEmptyValuesOf(DataType.STRING, ['none']);
+    const schema = {type: DataType.STRING, required: true};
+    const throwable = () => validate('none', schema, undefined, cont);
+    expect(throwable).to.throw('Value is required, but "none" was given.');
+  });
+
+  it('should add the source path to error message', function () {
+    const cont = new ServiceContainer();
+    const emptyValues = cont.get(EmptyValuesService);
+    emptyValues.setEmptyValuesOf(DataType.ANY, ['none']);
+    const schema = {required: true};
+    const options = {sourcePath: 'array[0]'};
+    const throwable = () => validate('none', schema, options, cont);
+    const error = 'Value of "array[0]" is required, but "none" was given.';
+    expect(throwable).to.throw(error);
+  });
+});

+ 6 - 0
src/data-validators/string-type-validator.d.ts

@@ -0,0 +1,6 @@
+import {DataValidationFunction} from "../data-validator.js";
+
+/**
+ * String type validator.
+ */
+export declare const stringTypeValidator: DataValidationFunction;

+ 47 - 0
src/data-validators/string-type-validator.js

@@ -0,0 +1,47 @@
+import {DataType} from '../data-type.js';
+import {ServiceContainer} from '@e22m4u/js-service';
+import {DataValidationError} from '../errors/index.js';
+import {EmptyValuesService} from '@e22m4u/js-empty-values';
+
+/**
+ * String type validator.
+ *
+ * @param {*} value
+ * @param {object} schema
+ * @param {object|undefined} options
+ * @param {ServiceContainer} container
+ */
+export function stringTypeValidator(value, schema, options, container) {
+  // если тип не соответствует строке,
+  // то проверка пропускается
+  if (schema.type !== DataType.STRING) {
+    return;
+  }
+  // если значение является пустым,
+  // то проверка пропускается
+  const emptyValues = container.get(EmptyValuesService);
+  const dataType = schema.type || DataType.ANY;
+  if (emptyValues.isEmptyByType(dataType, value)) {
+    return;
+  }
+  // если значение является строкой,
+  // то проверка успешно завершается
+  if (typeof value === 'string') {
+    return;
+  }
+  // если значение не является строкой,
+  // то выбрасывается ошибка
+  const sourcePath = options && options.sourcePath;
+  if (sourcePath) {
+    throw new DataValidationError(
+      'Value of %v must be a String, but %v was given.',
+      sourcePath,
+      value,
+    );
+  } else {
+    throw new DataValidationError(
+      'Value must be a String, but %v was given.',
+      value,
+    );
+  }
+}

+ 54 - 0
src/data-validators/string-type-validator.spec.js

@@ -0,0 +1,54 @@
+import {expect} from 'chai';
+import {format} from '@e22m4u/js-format';
+import {DataType} from '../data-type.js';
+import {ServiceContainer} from '@e22m4u/js-service';
+import {EmptyValuesService} from '@e22m4u/js-empty-values';
+import {stringTypeValidator} from './string-type-validator.js';
+
+const validate = stringTypeValidator;
+
+describe('stringTypeValidator', function () {
+  it('should not validate non-string types', function () {
+    const cont = new ServiceContainer();
+    const value = Symbol('invalidValue');
+    validate(value, {}, undefined, cont);
+    validate(value, {type: DataType.ANY}, undefined, cont);
+    validate(value, {type: DataType.NUMBER}, undefined, cont);
+    validate(value, {type: DataType.BOOLEAN}, undefined, cont);
+    validate(value, {type: DataType.ARRAY}, undefined, cont);
+    validate(value, {type: DataType.OBJECT}, undefined, cont);
+  });
+
+  it('should not validate empty values', function () {
+    const cont = new ServiceContainer();
+    const emptyValues = cont.get(EmptyValuesService);
+    emptyValues.setEmptyValuesOf(DataType.STRING, ['']);
+    validate('', {type: DataType.STRING}, undefined, cont);
+  });
+
+  it('should throw an error for an invalid value', function () {
+    const cont = new ServiceContainer();
+    const schema = {type: DataType.STRING};
+    const throwable = v => () => validate(v, schema, undefined, cont);
+    const error = s => format('Value must be a String, but %s was given.', s);
+    expect(throwable(10)).to.throw(error('10'));
+    expect(throwable(0)).to.throw(error('0'));
+    expect(throwable(true)).to.throw(error('true'));
+    expect(throwable(false)).to.throw(error('false'));
+    expect(throwable([1])).to.throw(error('Array'));
+    expect(throwable([])).to.throw(error('Array'));
+    expect(throwable({p: 1})).to.throw(error('Object'));
+    expect(throwable({})).to.throw(error('Object'));
+    expect(throwable(() => undefined)).to.throw(error('Function'));
+    throwable('str')();
+  });
+
+  it('should add the source path to error message', function () {
+    const cont = new ServiceContainer();
+    const schema = {type: DataType.STRING};
+    const options = {sourcePath: 'array[0]'};
+    const throwable = () => validate(10, schema, options, cont);
+    const error = 'Value of "array[0]" must be a String, but 10 was given.';
+    expect(throwable).to.throw(error);
+  });
+});

+ 31 - 0
src/errors/data-parsing-error.d.ts

@@ -0,0 +1,31 @@
+import {DataType} from '../data-type.js';
+import {InvalidArgumentError} from '@e22m4u/js-format';
+
+/**
+ * Data parsing error.
+ */
+export declare class DataParsingError extends InvalidArgumentError {
+  /**
+   * Value.
+   */
+  readonly value?: unknown;
+
+  /**
+   * Target type.
+   */
+  readonly targetType: DataType;
+
+  /**
+   * Source path.
+   */
+  readonly sourcePath?: string;
+
+  /**
+   * Constructor.
+   *
+   * @param value
+   * @param targetType
+   * @param sourcePath
+   */
+  constructor(value: unknown, targetType: DataType, sourcePath?: string);
+}

+ 54 - 0
src/errors/data-parsing-error.js

@@ -0,0 +1,54 @@
+import {toPascalCase} from '../utils/to-pascal-case.js';
+import {format, InvalidArgumentError} from '@e22m4u/js-format';
+
+/**
+ * Data parsing error.
+ */
+export class DataParsingError extends InvalidArgumentError {
+  /**
+   * Value.
+   *
+   * @type {*}
+   */
+  value;
+
+  /**
+   * Target type.
+   *
+   * @type {string}
+   */
+  targetType;
+
+  /**
+   * Source path.
+   *
+   * @type {string|undefined}
+   */
+  sourcePath;
+
+  /**
+   * Constructor.
+   *
+   * @param {*} value
+   * @param {string} targetType
+   * @param {string} [sourcePath]
+   */
+  constructor(value, targetType, sourcePath) {
+    const targetTypePc = toPascalCase(targetType);
+    let message = '';
+    if (sourcePath) {
+      message = format(
+        'Unable to parse %v from %v as %s.',
+        value,
+        sourcePath,
+        targetTypePc,
+      );
+    } else {
+      message = format('Unable to parse %v as %s.', value, targetTypePc);
+    }
+    super(message);
+    this.value = value;
+    this.targetType = targetType;
+    this.sourcePath = sourcePath;
+  }
+}

+ 26 - 0
src/errors/data-parsing-error.spec.js

@@ -0,0 +1,26 @@
+import {expect} from 'chai';
+import {DataType} from '../data-type.js';
+import {DataParsingError} from './data-parsing-error.js';
+
+describe('DataParsingError', function () {
+  describe('constructor', function () {
+    it('should set given arguments to instance properties', function() {
+      const error = new DataParsingError(10, DataType.STRING, 'aSource');
+      expect(error.value).to.be.eq(10);
+      expect(error.targetType).to.be.eq(DataType.STRING);
+      expect(error.sourcePath).to.be.eq('aSource');
+    });
+
+    it('sets error message', function () {
+      const error = new DataParsingError(10, DataType.STRING);
+      expect(error.message).to.be.eq('Unable to parse 10 as String.');
+    });
+
+    it('sets error message with source type', function () {
+      const error = new DataParsingError(10, DataType.STRING, 'query.id');
+      expect(error.message).to.be.eq(
+        'Unable to parse 10 from "query.id" as String.',
+      );
+    });
+  });
+});

+ 6 - 0
src/errors/data-validation-error.d.ts

@@ -0,0 +1,6 @@
+import {InvalidArgumentError} from "@e22m4u/js-format";
+
+/**
+ * Data validation error.
+ */
+export declare class DataValidationError extends InvalidArgumentError {}

+ 6 - 0
src/errors/data-validation-error.js

@@ -0,0 +1,6 @@
+import {InvalidArgumentError} from "@e22m4u/js-format";
+
+/**
+ * Data validation error.
+ */
+export class DataValidationError extends InvalidArgumentError {}

+ 17 - 0
src/errors/data-validation-error.spec.js

@@ -0,0 +1,17 @@
+import {expect} from 'chai';
+import {Errorf} from '@e22m4u/js-format';
+import {DataValidationError} from './data-validation-error.js';
+
+describe('DataValidationError', function () {
+  describe('constructor', function () {
+    it('extends the Errorf class', function () {
+      const error = new DataValidationError('My error');
+      expect(error).to.be.instanceof(Errorf);
+    });
+
+    it('interpolates given message', function () {
+      const error = new DataValidationError('The value is %v.', 10);
+      expect(error.message).to.be.eq('The value is 10.');
+    });
+  });
+});

+ 2 - 0
src/errors/index.d.ts

@@ -0,0 +1,2 @@
+export * from './data-parsing-error.js';
+export * from './data-validation-error.js';

+ 2 - 0
src/errors/index.js

@@ -0,0 +1,2 @@
+export * from './data-parsing-error.js';
+export * from './data-validation-error.js';

+ 12 - 0
src/index.d.ts

@@ -0,0 +1,12 @@
+export * from './data-type.js';
+export * from './data-parser.js';
+export * from './data-schema.js';
+export * from './errors/index.js';
+export * from './data-validator.js';
+export * from './data-parsers/index.js';
+export * from './validate-data-schema.js';
+export * from './data-schema-registry.js';
+export * from './data-schema-resolver.js';
+export * from './data-validators/index.js';
+export * from './data-schema-definition.js';
+export * from './validate-data-schema-definition.js';

+ 10 - 0
src/index.js

@@ -0,0 +1,10 @@
+export * from './data-type.js';
+export * from './data-parser.js';
+export * from './errors/index.js';
+export * from './data-validator.js';
+export * from './data-parsers/index.js';
+export * from './validate-data-schema.js';
+export * from './data-schema-registry.js';
+export * from './data-schema-resolver.js';
+export * from './data-validators/index.js';
+export * from './validate-data-schema-definition.js';

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

@@ -0,0 +1 @@
+export * from './to-pascal-case.js';

+ 1 - 0
src/utils/index.js

@@ -0,0 +1 @@
+export * from './to-pascal-case.js';

+ 6 - 0
src/utils/to-pascal-case.d.ts

@@ -0,0 +1,6 @@
+/**
+ * To pascal case.
+ *
+ * @param input
+ */
+export declare function toPascalCase(input: string): string;

+ 24 - 0
src/utils/to-pascal-case.js

@@ -0,0 +1,24 @@
+/**
+ * To pascal case.
+ *
+ * @param {string} input
+ * @returns {string}
+ */
+export function toPascalCase(input) {
+  if (!input) return '';
+  return (
+    input
+      // splits camelCase words into separate words
+      .replace(/([a-z])([A-Z])/g, '$1 $2')
+      // splits numbers and words
+      .replace(/([0-9])([a-zA-Z])/g, '$1 $2')
+      // replaces dashes, underscores, and special characters with spaces
+      .replace(/[-_]+|[^\p{L}\p{N}]/gu, ' ')
+      // converts the entire string to lowercase
+      .toLowerCase()
+      // capitalizes the first letter of each word
+      .replace(/(?:^|\s)(\p{L})/gu, (_, letter) => letter.toUpperCase())
+      // removes all spaces
+      .replace(/\s+/g, '')
+  );
+}

+ 15 - 0
src/utils/to-pascal-case.spec.js

@@ -0,0 +1,15 @@
+import {expect} from 'chai';
+import {toPascalCase} from './to-pascal-case.js';
+
+describe('toPascalCase', function () {
+  it('returns a PascalCase string', function () {
+    expect(toPascalCase('hello world')).to.be.eq('HelloWorld');
+    expect(toPascalCase('snake_case')).to.be.eq('SnakeCase');
+    expect(toPascalCase('kebab-case')).to.be.eq('KebabCase');
+    expect(toPascalCase('alreadyCamel')).to.be.eq('AlreadyCamel');
+    expect(toPascalCase('AlreadyPascal')).to.be.eq('AlreadyPascal');
+    expect(toPascalCase(' single word ')).to.be.eq('SingleWord');
+    expect(toPascalCase('')).to.be.eq('');
+    expect(toPascalCase('1number')).to.be.eq('1Number');
+  });
+});

+ 10 - 0
src/validate-data-schema-definition.d.ts

@@ -0,0 +1,10 @@
+import {DataSchemaDefinition} from './data-schema-definition.js';
+
+/**
+ * Validate data schema definition.
+ *
+ * @param schemaDef
+ */
+export function validateDataSchemaDefinition(
+  schemaDef: DataSchemaDefinition,
+): void;

+ 36 - 0
src/validate-data-schema-definition.js

@@ -0,0 +1,36 @@
+import {InvalidArgumentError} from '@e22m4u/js-format';
+import {validateDataSchema} from './validate-data-schema.js';
+
+/**
+ * Validate data schema definition.
+ * 
+ * @param {object} schemaDef
+ */
+export function validateDataSchemaDefinition(schemaDef) {
+  if (!schemaDef || typeof schemaDef !== 'object' || Array.isArray(schemaDef)) {
+    throw new InvalidArgumentError(
+      'Schema definition must be an Object, but %v was given.',
+      schemaDef,
+    );
+  }
+  if (!schemaDef.name || typeof schemaDef.name !== 'string') {
+    throw new InvalidArgumentError(
+      'Definition option "name" must be a non-empty String, but %v was given.',
+      schemaDef.name,
+    );
+  }
+  if (
+    !schemaDef.schema ||
+    (typeof schemaDef.schema !== 'object' &&
+      typeof schemaDef.schema !== 'function' &&
+      typeof schemaDef.schema !== 'string') ||
+    Array.isArray(schemaDef.schema)
+  ) {
+    throw new InvalidArgumentError(
+      'Definition option "schema" must be an Object, a Function ' +
+        'or a non-empty String, but %v was given.',
+      schemaDef.schema,
+    );
+  }
+  validateDataSchema(schemaDef.schema);
+}

+ 117 - 0
src/validate-data-schema-definition.spec.js

@@ -0,0 +1,117 @@
+import {expect} from 'chai';
+import {format} from '@e22m4u/js-format';
+import {DATA_TYPE_LIST, DataType} from './data-type.js';
+import {validateDataSchemaDefinition} from './validate-data-schema-definition.js';
+
+describe('validateDataSchemaDefinition', function () {
+  it('should require the "schemaDef" argument to be an object', function () {
+    const throwable = v => () => validateDataSchemaDefinition(v);
+    const error = s =>
+      format('Schema definition must be an Object, but %s was given.', s);
+    expect(throwable('str')).to.throw(error('"str"'));
+    expect(throwable('')).to.throw(error('""'));
+    expect(throwable(10)).to.throw(error('10'));
+    expect(throwable(0)).to.throw(error('0'));
+    expect(throwable(true)).to.throw(error('true'));
+    expect(throwable(false)).to.throw(error('false'));
+    expect(throwable([])).to.throw(error('Array'));
+    expect(throwable(undefined)).to.throw(error('undefined'));
+    expect(throwable(null)).to.throw(error('null'));
+    expect(throwable(() => undefined)).to.throw(error('Function'));
+    throwable({name: 'mySchema', schema: {}})();
+  });
+
+  it('should require the "name" option to be a non-empty string', function () {
+    const throwable = v => () =>
+      validateDataSchemaDefinition({name: v, schema: {}});
+    const error = s =>
+      format(
+        'Definition option "name" must be ' +
+          'a non-empty String, but %s was given.',
+        s,
+      );
+    expect(throwable('')).to.throw(error('""'));
+    expect(throwable(10)).to.throw(error('10'));
+    expect(throwable(0)).to.throw(error('0'));
+    expect(throwable(true)).to.throw(error('true'));
+    expect(throwable(false)).to.throw(error('false'));
+    expect(throwable([])).to.throw(error('Array'));
+    expect(throwable({})).to.throw(error('Object'));
+    expect(throwable(undefined)).to.throw(error('undefined'));
+    expect(throwable(null)).to.throw(error('null'));
+    expect(throwable(() => undefined)).to.throw(error('Function'));
+    throwable('str')();
+  });
+
+  it('should require the "schema" option to be a valid value', function () {
+    const throwable = v => () =>
+      validateDataSchemaDefinition({name: 'mySchema', schema: v});
+    const error = s =>
+      format(
+        'Definition option "schema" must be an Object, a Function ' +
+          'or a non-empty String, but %s was given.',
+        s,
+      );
+    expect(throwable('')).to.throw(error('""'));
+    expect(throwable(10)).to.throw(error('10'));
+    expect(throwable(0)).to.throw(error('0'));
+    expect(throwable(true)).to.throw(error('true'));
+    expect(throwable(false)).to.throw(error('false'));
+    expect(throwable([])).to.throw(error('Array'));
+    expect(throwable(undefined)).to.throw(error('undefined'));
+    expect(throwable(null)).to.throw(error('null'));
+    throwable('str')();
+    throwable({})();
+    throwable(() => ({}))();
+  });
+
+  it('should validate options of the data schema', function () {
+    const throwable = () =>
+      validateDataSchemaDefinition({
+        name: 'mySchema',
+        schema: {type: 10},
+      });
+    const error = format(
+      'Schema option "type" must be one of values: %l, but %v was given.',
+      DATA_TYPE_LIST,
+      10,
+    );
+    expect(throwable).to.throw(error);
+  });
+
+  it('should validate options of the items schema', function () {
+    const throwable = () =>
+      validateDataSchemaDefinition({
+        name: 'mySchema',
+        schema: {
+          type: DataType.ARRAY,
+          items: {type: 10},
+        },
+      });
+    const error = format(
+      'Schema option "type" must be one of values: %l, but %v was given.',
+      DATA_TYPE_LIST,
+      10,
+    );
+    expect(throwable).to.throw(error);
+  });
+
+  it('should validate options of the property schema', function () {
+    const throwable = () =>
+      validateDataSchemaDefinition({
+        name: 'mySchema',
+        schema: {
+          type: DataType.OBJECT,
+          properties: {
+            foo: {type: 10},
+          },
+        },
+      });
+    const error = format(
+      'Schema option "type" must be one of values: %l, but %v was given.',
+      DATA_TYPE_LIST,
+      10,
+    );
+    expect(throwable).to.throw(error);
+  });
+});

+ 14 - 0
src/validate-data-schema.d.ts

@@ -0,0 +1,14 @@
+import {DataSchema} from './data-schema.js';
+
+/**
+ * Validate data schema.
+ *
+ * @param schema
+ * @param shallowMode
+ * @param validatedSchemas
+ */
+export function validateDataSchema(
+  schema: DataSchema,
+  shallowMode?: boolean,
+  validatedSchemas?: Set<unknown>,
+): void;

+ 148 - 0
src/validate-data-schema.js

@@ -0,0 +1,148 @@
+import {InvalidArgumentError} from '@e22m4u/js-format';
+import {DATA_TYPE_LIST, DataType} from './data-type.js';
+
+/**
+ * Validate data schema.
+ *
+ * @param {object|Function|string} schema
+ * @param {boolean} [shallowMode]
+ * @param {Set} [validatedSchemas]
+ */
+export function validateDataSchema(
+  schema,
+  shallowMode = false,
+  validatedSchemas = new Set(),
+) {
+  if (typeof shallowMode !== 'boolean') {
+    throw new InvalidArgumentError(
+      'Argument "shallowMode" must be a Boolean, but %v was given.',
+      shallowMode,
+    );
+  }
+  if (!(validatedSchemas instanceof Set)) {
+    throw new InvalidArgumentError(
+      'Argument "validatedSchemas" must be ' +
+        'an instance of Set, but %v was given.',
+      validatedSchemas,
+    );
+  }
+  // если схема уже была проверена в предыдущих
+  // итерациях, то проверка пропускается
+  if (validatedSchemas.has(schema)) {
+    return;
+  }
+  // schema
+  if (
+    !schema ||
+    (typeof schema !== 'object' &&
+      typeof schema !== 'function' &&
+      typeof schema !== 'string') ||
+    Array.isArray(schema)
+  ) {
+    throw new InvalidArgumentError(
+      'Data schema must be an Object, a Function ' +
+        'or a non-empty String, but %v was given.',
+      schema,
+    );
+  }
+  if (typeof schema !== 'object') {
+    return;
+  }
+  // для исключения бесконечного цикла,
+  // текущая схема добавляется в историю
+  validatedSchemas.add(schema);
+  // schema.type
+  if (schema.type !== undefined) {
+    if (!schema.type || !DATA_TYPE_LIST.includes(schema.type)) {
+      throw new InvalidArgumentError(
+        'Schema option "type" must be one of values: %l, but %v was given.',
+        DATA_TYPE_LIST,
+        schema.type,
+      );
+    }
+  }
+  // schema.items
+  if (schema.items !== undefined) {
+    if (
+      !schema.items ||
+      (typeof schema.items !== 'object' &&
+        typeof schema.items !== 'function' &&
+        typeof schema.items !== 'string') ||
+      Array.isArray(schema.items)
+    ) {
+      throw new InvalidArgumentError(
+        'Schema option "items" must be an Object, a Function ' +
+          'or a non-empty String, but %v was given.',
+        schema.items,
+      );
+    }
+    // если тип не является массивом,
+    // то выбрасывается ошибка
+    if (schema.type !== DataType.ARRAY) {
+      throw new InvalidArgumentError(
+        'Schema option "items" is only allowed ' +
+          'for the "array" type, but %v was given.',
+        schema.type,
+      );
+    }
+    if (!shallowMode && typeof schema.items === 'object') {
+      validateDataSchema(schema.items, shallowMode, validatedSchemas);
+    }
+  }
+  // schema.properties
+  if (schema.properties !== undefined) {
+    if (
+      !schema.properties ||
+      (typeof schema.properties !== 'object' &&
+        typeof schema.properties !== 'function' &&
+        typeof schema.properties !== 'string') ||
+      Array.isArray(schema.properties)
+    ) {
+      throw new InvalidArgumentError(
+        'Schema option "properties" must be an Object, a Function ' +
+          'or a non-empty String, but %v was given.',
+        schema.properties,
+      );
+    }
+    // если тип не является объектом,
+    // то выбрасывается ошибка
+    if (schema.type !== DataType.OBJECT) {
+      throw new InvalidArgumentError(
+        'Schema option "properties" is only allowed ' +
+          'for the "object" type, but %v was given.',
+        schema.type,
+      );
+    }
+    if (typeof schema.properties === 'object') {
+      // schema.properties[k]
+      Object.values(schema.properties).forEach(propSchema => {
+        if (propSchema === undefined) {
+          return;
+        }
+        if (
+          !propSchema ||
+          (typeof propSchema !== 'object' &&
+            typeof propSchema !== 'function' &&
+            typeof propSchema !== 'string') ||
+          Array.isArray(propSchema)
+        ) {
+          throw new InvalidArgumentError(
+            'Property schema must be an Object, a Function ' +
+              'or a non-empty String, but %v was given.',
+            propSchema,
+          );
+        }
+        if (!shallowMode && typeof propSchema === 'object') {
+          validateDataSchema(propSchema, shallowMode, validatedSchemas);
+        }
+      });
+    }
+  }
+  // schema.required
+  if (schema.required !== undefined && typeof schema.required !== 'boolean') {
+    throw new InvalidArgumentError(
+      'Schema option "required" must be a Boolean, but %v was given.',
+      schema.required,
+    );
+  }
+}

+ 307 - 0
src/validate-data-schema.spec.js

@@ -0,0 +1,307 @@
+import {expect} from 'chai';
+import {format} from '@e22m4u/js-format';
+import {DATA_TYPE_LIST, DataType} from './data-type.js';
+import {validateDataSchema} from './validate-data-schema.js';
+
+describe('validateDataSchema', function () {
+  it('should require the "shallowMode" argument to be a Boolean', function () {
+    const throwable = v => () => validateDataSchema({}, v);
+    const error = s =>
+      format(
+        'Argument "shallowMode" must be a Boolean, but %s was given.',
+        s,
+      );
+    expect(throwable('str')).to.throw(error('"str"'));
+    expect(throwable('')).to.throw(error('""'));
+    expect(throwable(10)).to.throw(error('10'));
+    expect(throwable(0)).to.throw(error('0'));
+    expect(throwable([])).to.throw(error('Array'));
+    expect(throwable({})).to.throw(error('Object'));
+    expect(throwable(null)).to.throw(error('null'));
+    expect(throwable(() => undefined)).to.throw(error('Function'));
+    throwable(true)();
+    throwable(false)();
+    throwable(undefined)();
+  });
+  
+  it('should require the "validatedSchemas" argument to be a Set instance', function () {
+    const throwable = v => () => validateDataSchema({}, false, v);
+    const error = s =>
+      format(
+        'Argument "validatedSchemas" must be ' +
+          'an instance of Set, but %s was given.',
+        s,
+      );
+    expect(throwable('str')).to.throw(error('"str"'));
+    expect(throwable('')).to.throw(error('""'));
+    expect(throwable(10)).to.throw(error('10'));
+    expect(throwable(0)).to.throw(error('0'));
+    expect(throwable(true)).to.throw(error('true'));
+    expect(throwable(false)).to.throw(error('false'));
+    expect(throwable([])).to.throw(error('Array'));
+    expect(throwable({})).to.throw(error('Object'));
+    expect(throwable(null)).to.throw(error('null'));
+    expect(throwable(() => undefined)).to.throw(error('Function'));
+    throwable(new Set())();
+    throwable(undefined)();
+  });
+
+  it('should skip validation if the given schema is already validated', function() {
+    const invalidSchema = {type: 'invalid'};
+    const validatedSchemas = new Set([invalidSchema]);
+    validateDataSchema(invalidSchema, undefined, validatedSchemas);
+  });
+
+  it('should require the "schema" argument to be a valid value', function () {
+    const throwable = v => () => validateDataSchema(v);
+    const error = s =>
+      format(
+        'Data schema must be an Object, a Function ' +
+          'or a non-empty String, but %s was given.',
+        s,
+      );
+    expect(throwable('')).to.throw(error('""'));
+    expect(throwable(10)).to.throw(error('10'));
+    expect(throwable(0)).to.throw(error('0'));
+    expect(throwable(true)).to.throw(error('true'));
+    expect(throwable(false)).to.throw(error('false'));
+    expect(throwable([])).to.throw(error('Array'));
+    expect(throwable(undefined)).to.throw(error('undefined'));
+    expect(throwable(null)).to.throw(error('null'));
+    throwable('str')();
+    throwable({})();
+    throwable(() => ({}))();
+  });
+
+  it('should add the given schema to the validated schema set', function() {
+    const schema = {type: DataType.ANY};
+    const validatedSchemas = new Set();
+    validateDataSchema(schema, undefined, validatedSchemas);
+    expect(validatedSchemas.has(schema)).to.be.true;
+  });
+
+  it('should require the "type" option to be a data type', function () {
+    const throwable = v => () => validateDataSchema({type: v});
+    const error = s =>
+      format(
+        'Schema option "type" must be one of values: %l, but %s was given.',
+        DATA_TYPE_LIST,
+        s,
+      );
+    expect(throwable('str')).to.throw(error('"str"'));
+    expect(throwable('')).to.throw(error('""'));
+    expect(throwable(10)).to.throw(error('10'));
+    expect(throwable(0)).to.throw(error('0'));
+    expect(throwable(true)).to.throw(error('true'));
+    expect(throwable(false)).to.throw(error('false'));
+    expect(throwable([])).to.throw(error('Array'));
+    expect(throwable(null)).to.throw(error('null'));
+    expect(throwable(() => undefined)).to.throw(error('Function'));
+    DATA_TYPE_LIST.forEach(type => validateDataSchema({type}));
+    validateDataSchema({type: undefined});
+  });
+
+  it('should require the "items" option to be a valid value', function () {
+    const throwable = v => () =>
+      validateDataSchema({type: DataType.ARRAY, items: v});
+    const error = s =>
+      format(
+        'Schema option "items" must be an Object, a Function ' +
+          'or a non-empty String, but %s was given.',
+        s,
+      );
+    expect(throwable('')).to.throw(error('""'));
+    expect(throwable(10)).to.throw(error('10'));
+    expect(throwable(0)).to.throw(error('0'));
+    expect(throwable(true)).to.throw(error('true'));
+    expect(throwable(false)).to.throw(error('false'));
+    expect(throwable([])).to.throw(error('Array'));
+    expect(throwable(null)).to.throw(error('null'));
+    throwable('str')();
+    throwable({})();
+    throwable(() => ({}))();
+    throwable(undefined)();
+  });
+
+  it('should throw an error if the "items" option is provided for non-array schema', function () {
+    const throwable = v => () => validateDataSchema({type: v, items: {}});
+    const error = s =>
+      format(
+        'Schema option "items" is only allowed ' +
+          'for the "array" type, but %s was given.',
+        s,
+      );
+    expect(throwable(undefined)).to.throw(error('undefined'));
+    expect(throwable(DataType.ANY)).to.throw(error('"any"'));
+    expect(throwable(DataType.STRING)).to.throw(error('"string"'));
+    expect(throwable(DataType.NUMBER)).to.throw(error('"number"'));
+    expect(throwable(DataType.BOOLEAN)).to.throw(error('"boolean"'));
+    expect(throwable(DataType.OBJECT)).to.throw(error('"object"'));
+    validateDataSchema({type: DataType.ARRAY, items: {}});
+  });
+
+  it('should validate options of the items schema', function () {
+    const throwable = () =>
+      validateDataSchema({type: DataType.ARRAY, items: {type: 10}});
+    expect(throwable).to.throw(
+      format(
+        'Schema option "type" must be one of values: %l, but %v was given.',
+        DATA_TYPE_LIST,
+        10,
+      ),
+    );
+  });
+
+  it('should skip options validation of the items schema in the shallow mode', function () {
+    const throwable1 = () => validateDataSchema({type: 10}, true);
+    expect(throwable1).to.throw(
+      format(
+        'Schema option "type" must be one of values: %l, but %v was given.',
+        DATA_TYPE_LIST,
+        10,
+      ),
+    );
+    validateDataSchema({type: DataType.ARRAY, items: {type: 10}}, true);
+  });
+
+  it('should require the "properties" option to be a valid value', function () {
+    const throwable = v => () =>
+      validateDataSchema({type: DataType.OBJECT, properties: v});
+    const error = s =>
+      format(
+        'Schema option "properties" must be an Object, a Function ' +
+          'or a non-empty String, but %s was given.',
+        s,
+      );
+    expect(throwable('')).to.throw(error('""'));
+    expect(throwable(10)).to.throw(error('10'));
+    expect(throwable(0)).to.throw(error('0'));
+    expect(throwable(true)).to.throw(error('true'));
+    expect(throwable(false)).to.throw(error('false'));
+    expect(throwable([])).to.throw(error('Array'));
+    expect(throwable(null)).to.throw(error('null'));
+    throwable('str')();
+    throwable({})();
+    throwable(() => ({}))();
+    throwable(undefined)();
+  });
+
+  it('should throw an error if the "properties" option is provided for non-object schema', function () {
+    const throwable = v => () => validateDataSchema({type: v, properties: {}});
+    const error = s =>
+      format(
+        'Schema option "properties" is only allowed ' +
+          'for the "object" type, but %s was given.',
+        s,
+      );
+    expect(throwable(undefined)).to.throw(error('undefined'));
+    expect(throwable(DataType.ANY)).to.throw(error('"any"'));
+    expect(throwable(DataType.STRING)).to.throw(error('"string"'));
+    expect(throwable(DataType.NUMBER)).to.throw(error('"number"'));
+    expect(throwable(DataType.BOOLEAN)).to.throw(error('"boolean"'));
+    expect(throwable(DataType.ARRAY)).to.throw(error('"array"'));
+    validateDataSchema({type: DataType.OBJECT, properties: {}});
+  });
+
+  it('should require the property schema to be a valid value', function () {
+    const throwable = v => () =>
+      validateDataSchema({type: DataType.OBJECT, properties: {foo: v}});
+    const error = s =>
+      format(
+        'Property schema must be an Object, a Function ' +
+          'or a non-empty String, but %s was given.',
+        s,
+      );
+    expect(throwable('')).to.throw(error('""'));
+    expect(throwable(10)).to.throw(error('10'));
+    expect(throwable(0)).to.throw(error('0'));
+    expect(throwable(true)).to.throw(error('true'));
+    expect(throwable(false)).to.throw(error('false'));
+    expect(throwable([])).to.throw(error('Array'));
+    expect(throwable(null)).to.throw(error('null'));
+    throwable('str')();
+    throwable({})();
+    throwable(() => ({}))();
+    throwable(undefined)();
+  });
+
+  it('should validate options of the property schema', function () {
+    const throwable = () =>
+      validateDataSchema({
+        type: DataType.OBJECT,
+        properties: {foo: {type: 10}},
+      });
+    expect(throwable).to.throw(
+      format(
+        'Schema option "type" must be one of values: %l, but %v was given.',
+        DATA_TYPE_LIST,
+        10,
+      ),
+    );
+  });
+
+  it('should skip property schema validation in the shallow mode', function () {
+    const throwable1 = () =>
+      validateDataSchema({type: DataType.OBJECT, properties: 10}, true);
+    expect(throwable1).to.throw(
+      'Schema option "properties" must be an Object, a Function ' +
+        'or a non-empty String, but 10 was given.',
+    );
+    validateDataSchema(
+      {type: DataType.OBJECT, properties: {foo: {type: 10}}},
+      true,
+    );
+  });
+
+  it('should require the "required" option to be a boolean', function () {
+    const throwable = v => () => validateDataSchema({required: v});
+    const error = s =>
+      format(
+        'Schema option "required" must be a Boolean, but %s was given.',
+        s,
+      );
+    expect(throwable('str')).to.throw(error('"str"'));
+    expect(throwable('')).to.throw(error('""'));
+    expect(throwable(10)).to.throw(error('10'));
+    expect(throwable(0)).to.throw(error('0'));
+    expect(throwable([])).to.throw(error('Array'));
+    expect(throwable({})).to.throw(error('Object'));
+    expect(throwable(null)).to.throw(error('null'));
+    expect(throwable(() => undefined)).to.throw(error('Function'));
+    throwable(true)();
+    throwable(false)();
+    throwable(undefined)();
+  });
+
+  it('should allow circular schema validation', function() {
+    const schemaA = () => ({
+      type: DataType.OBJECT,
+      properties: {
+        foo: schemaB(),
+      }
+    });
+    const schemaB = () => ({
+      type: DataType.OBJECT,
+      properties: {
+        bar: schemaA(),
+      }
+    });
+    validateDataSchema(schemaA);
+  });
+
+  it('should allow circular schema validation in the shallow mode', function() {
+    const schemaA = () => ({
+      type: DataType.OBJECT,
+      properties: {
+        foo: schemaB(),
+      }
+    });
+    const schemaB = () => ({
+      type: DataType.OBJECT,
+      properties: {
+        bar: schemaA(),
+      }
+    });
+    validateDataSchema(schemaA, true);
+  });
+});

+ 14 - 0
tsconfig.json

@@ -0,0 +1,14 @@
+{
+  "compilerOptions": {
+    "strict": true,
+    "target": "es2022",
+    "module": "NodeNext",
+    "moduleResolution": "NodeNext",
+    "noEmit": true,
+    "allowJs": true
+  },
+  "include": [
+    "./src/**/*.ts",
+    "./src/**/*.js"
+  ]
+}