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; } }