data-parser.js 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. import {DataType} from './data-type.js';
  2. import {Service} from '@e22m4u/js-service';
  3. import {DataValidator} from './data-validator.js';
  4. import {InvalidArgumentError} from '@e22m4u/js-format';
  5. import {validateDataSchema} from './validate-data-schema.js';
  6. import {DataSchemaResolver} from './data-schema-resolver.js';
  7. import {DataSchemaRegistry} from './data-schema-registry.js';
  8. import {
  9. arrayTypeParser,
  10. stringTypeParser,
  11. numberTypeParser,
  12. objectTypeParser,
  13. booleanTypeParser,
  14. } from './data-parsers/index.js';
  15. /**
  16. * Data parser.
  17. */
  18. export class DataParser extends Service {
  19. /**
  20. * Parsers.
  21. *
  22. * @type {Function[]}
  23. */
  24. _parsers = [
  25. stringTypeParser,
  26. booleanTypeParser,
  27. numberTypeParser,
  28. arrayTypeParser,
  29. objectTypeParser,
  30. ];
  31. /**
  32. * Get parsers.
  33. *
  34. * @returns {Function[]}
  35. */
  36. getParsers() {
  37. return [...this._parsers];
  38. }
  39. /**
  40. * Set parsers.
  41. *
  42. * @param {Function[]} list
  43. * @returns {this}
  44. */
  45. setParsers(list) {
  46. if (!Array.isArray(list)) {
  47. throw new InvalidArgumentError(
  48. 'Data parsers must be an Array, but %v was given.',
  49. list,
  50. );
  51. }
  52. list.forEach(parser => {
  53. if (typeof parser !== 'function') {
  54. throw new InvalidArgumentError(
  55. 'Data parser must be a Function, but %v was given.',
  56. parser,
  57. );
  58. }
  59. });
  60. this._parsers = [...list];
  61. return this;
  62. }
  63. /**
  64. * Define schema.
  65. *
  66. * @param {object} schemaDef
  67. * @returns {this}
  68. */
  69. defineSchema(schemaDef) {
  70. this.getService(DataSchemaRegistry).defineSchema(schemaDef);
  71. return this;
  72. }
  73. /**
  74. * Has schema.
  75. *
  76. * @param {string} schemaName
  77. * @returns {boolean}
  78. */
  79. hasSchema(schemaName) {
  80. return this.getService(DataSchemaRegistry).hasSchema(schemaName);
  81. }
  82. /**
  83. * Get schema.
  84. *
  85. * @param {string} schemaName
  86. * @returns {object}
  87. */
  88. getSchema(schemaName) {
  89. return this.getService(DataSchemaRegistry).getSchema(schemaName);
  90. }
  91. /**
  92. * Parse.
  93. *
  94. * @param {*} value
  95. * @param {object|Function|string} schema
  96. * @param {object} [options]
  97. * @returns {*}
  98. */
  99. parse(value, schema, options) {
  100. if (options !== undefined) {
  101. if (
  102. options === null ||
  103. typeof options !== 'object' ||
  104. Array.isArray(options)
  105. ) {
  106. throw new InvalidArgumentError(
  107. 'Parsing options must be an Object, but %v was given.',
  108. options,
  109. );
  110. }
  111. if (options.sourcePath !== undefined) {
  112. if (!options.sourcePath || typeof options.sourcePath !== 'string') {
  113. throw new InvalidArgumentError(
  114. 'Option "sourcePath" must be a non-empty String, but %v was given.',
  115. options.sourcePath,
  116. );
  117. }
  118. }
  119. if (options.shallowMode !== undefined) {
  120. if (typeof options.shallowMode !== 'boolean') {
  121. throw new InvalidArgumentError(
  122. 'Option "shallowMode" must be a Boolean, but %v was given.',
  123. options.shallowMode,
  124. );
  125. }
  126. }
  127. if (options.noParsingErrors !== undefined) {
  128. if (typeof options.noParsingErrors !== 'boolean') {
  129. throw new InvalidArgumentError(
  130. 'Option "noParsingErrors" must be a Boolean, but %v was given.',
  131. options.noParsingErrors,
  132. );
  133. }
  134. }
  135. }
  136. const sourcePath = (options && options.sourcePath) || undefined;
  137. const shallowMode = Boolean(options && options.shallowMode);
  138. const noParsingErrors = Boolean(options && options.noParsingErrors);
  139. // поверхностная проверка схемы
  140. // (режим shallowMode)
  141. validateDataSchema(schema, true);
  142. // если схема данных не является объектом,
  143. // то выполняется извлечение схемы данных
  144. const schemaResolver = this.getService(DataSchemaResolver);
  145. if (typeof schema !== 'object') {
  146. schema = schemaResolver.resolve(schema);
  147. }
  148. // передача значения через каждую функцию
  149. // преобразования в соответствующем порядке
  150. value = this._parsers.reduce((input, parser) => {
  151. return parser(input, schema, options, this.container);
  152. }, value);
  153. // если значение является массивом или объектом,
  154. // то выполняется обработка вложенных элементов
  155. if (!shallowMode) {
  156. // если значение является массивом, то выполняется
  157. // преобразование каждого элемента по схеме, указанной
  158. // в опции items
  159. if (Array.isArray(value) && schema.items !== undefined) {
  160. // чтобы избежать изменения оригинального массива,
  161. // выполняется его поверхностное копирование
  162. value = [...value];
  163. value.forEach((item, index) => {
  164. const itemPath = (sourcePath || 'array') + `[${index}]`;
  165. const itemOptions = {...options, sourcePath: itemPath};
  166. value[index] = this.parse(item, schema.items, itemOptions);
  167. });
  168. }
  169. // если значение является объектом, то выполняется
  170. // рекурсивный обход каждого свойства
  171. else if (
  172. value !== null &&
  173. typeof value === 'object' &&
  174. schema.properties !== undefined
  175. ) {
  176. let propsSchema = schema.properties;
  177. // чтобы избежать изменения оригинального объекта,
  178. // выполняется его поверхностное копирование
  179. value = {...value};
  180. // если схема свойств не является объектом,
  181. // то выполняется извлечение схемы данных
  182. if (typeof propsSchema !== 'object') {
  183. const resolvedSchema = schemaResolver.resolve(propsSchema);
  184. // если извлеченная схема не является
  185. // схемой объекта, то выбрасывается ошибка
  186. if (resolvedSchema.type !== DataType.OBJECT) {
  187. throw new InvalidArgumentError(
  188. 'Unable to get the "properties" option ' +
  189. 'from the data schema of %v type.',
  190. resolvedSchema.type || DataType.ANY,
  191. );
  192. }
  193. propsSchema = resolvedSchema.properties || {};
  194. }
  195. Object.keys(propsSchema).forEach(propName => {
  196. const propSchema = propsSchema[propName];
  197. // если схема свойства не определена,
  198. // то преобразование пропускается
  199. if (propSchema === undefined) {
  200. return;
  201. }
  202. const propValue = value[propName];
  203. const propPath = sourcePath ? sourcePath + `.${propName}` : propName;
  204. const propOptions = {...options, sourcePath: propPath};
  205. const newPropValue = this.parse(propValue, propSchema, propOptions);
  206. // исходный объект может не иметь ключа данного свойства,
  207. // и чтобы избежать его добавления, выполняется проверка
  208. // на отличие старого и нового значения, таким образом,
  209. // значение undefined не будет присвоено свойству,
  210. // которого нет (новый ключ не будет добавлен)
  211. if (value[propName] !== newPropValue) {
  212. value[propName] = newPropValue;
  213. }
  214. });
  215. }
  216. }
  217. // если допускается выброс ошибок, то результирующее
  218. // значение проверяется согласно схеме данных
  219. if (!noParsingErrors) {
  220. const validator = this.getService(DataValidator);
  221. validator.validate(value, schema, {shallowMode: true});
  222. }
  223. return value;
  224. }
  225. }