Просмотр исходного кода

feat: adds option "keepUnknownProperties" to data parser

e22m4u 2 недель назад
Родитель
Сommit
7096198270

+ 53 - 2
README.md

@@ -273,6 +273,57 @@ parser.parse(undefined, schema); // 0
 parser.parse('N/A', schema);     // DataParsingError
 ```
 
+Если схема объекта содержит параметр `properties`, то свойства исходного
+объекта, не описанные в схеме, исключаются из результата. Итоговый объект
+будет содержать только те свойства, которые были определены явно.
+
+```js
+const schema = {
+  type: DataType.OBJECT,
+  properties: {
+    name: {
+      type: DataType.STRING
+    },
+  },
+};
+
+const data = {
+  name: 'Fedor',
+  isAdmin: true,      // лишнее свойство
+  metadata: 'ignored' // лишнее свойство
+};
+
+const result = parser.parse(data, schema);
+console.log(result);
+// {"name": "Fedor"}
+```
+
+При указании схемы свойств, неизвестные свойства будут отброшены. Чтобы
+изменить это поведение, используется параметр `keepUnknownProperties`,
+который позволяет сохранить все свойства исходного объекта.
+
+```js
+const schema = {
+  type: DataType.OBJECT,
+  properties: {
+    id: {type: DataType.NUMBER},
+  },
+};
+
+const data = {
+  id: 1,
+  role: 'admin',
+};
+
+// по умолчанию лишние свойства удаляются
+parser.parse(data, schema);
+// {id: 1}
+
+// с опцией "keepUnknownProperties" свойства сохраняются
+parser.parse(data, schema, {keepUnknownProperties: true});
+// {id: 1, role: 'admin'}
+```
+
 ## Именованные схемы
 
 Модуль экспортирует сервисы для проверки и парсинга данных. Каждый
@@ -280,7 +331,7 @@ parser.parse('N/A', schema);     // DataParsingError
 методом `defineSchema`.
 
 ```js
-import {DataValidator, DataParser, DataType} from '@e22m4u/js-data-service';
+import {DataValidator, DataParser, DataType} from '@e22m4u/js-data-schema';
 
 const validator = new DataValidator();
 const parser = new DataParser();
@@ -308,7 +359,7 @@ import {
   DataParser,
   DataValidator,
   DataSchemaRegistry,
-} from '@e22m4u/js-data-service';
+} from '@e22m4u/js-data-schema';
 
 const app = new ServiceContainer();
 const registry = app.get(DataSchemaRegistry);

+ 15 - 3
dist/cjs/index.cjs

@@ -1026,10 +1026,21 @@ var DataParser = class extends import_js_service16.Service {
           );
         }
       }
+      if (options.keepUnknownProperties !== void 0) {
+        if (typeof options.keepUnknownProperties !== "boolean") {
+          throw new import_js_format8.InvalidArgumentError(
+            'Option "keepUnknownProperties" must be a Boolean, but %v was given.',
+            options.keepUnknownProperties
+          );
+        }
+      }
     }
     const sourcePath = options && options.sourcePath || void 0;
     const shallowMode = Boolean(options && options.shallowMode);
     const noParsingErrors = Boolean(options && options.noParsingErrors);
+    const keepUnknownProperties = Boolean(
+      options && options.keepUnknownProperties
+    );
     validateDataSchema(schema, true);
     const schemaResolver = this.getService(DataSchemaResolver);
     if (typeof schema !== "object") {
@@ -1048,7 +1059,6 @@ var DataParser = class extends import_js_service16.Service {
         });
       } else if (value !== null && typeof value === "object" && schema.properties !== void 0) {
         let propsSchema = schema.properties;
-        value = { ...value };
         if (typeof propsSchema !== "object") {
           const resolvedSchema = schemaResolver.resolve(propsSchema);
           if (resolvedSchema.type !== DataType.OBJECT) {
@@ -1059,6 +1069,7 @@ var DataParser = class extends import_js_service16.Service {
           }
           propsSchema = resolvedSchema.properties || {};
         }
+        let newValue = keepUnknownProperties ? { ...value } : {};
         Object.keys(propsSchema).forEach((propName) => {
           const propSchema = propsSchema[propName];
           if (propSchema === void 0) {
@@ -1068,10 +1079,11 @@ var DataParser = class extends import_js_service16.Service {
           const propPath = sourcePath ? sourcePath + `.${propName}` : propName;
           const propOptions = { ...options, sourcePath: propPath };
           const newPropValue = this.parse(propValue, propSchema, propOptions);
-          if (value[propName] !== newPropValue) {
-            value[propName] = newPropValue;
+          if (propName in value || value[propName] !== newPropValue) {
+            newValue[propName] = newPropValue;
           }
         });
+        value = newValue;
       }
     }
     if (!noParsingErrors) {

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

@@ -10,6 +10,7 @@ export type DataParsingOptions = {
   shallowMode?: boolean;
   noDefaultValues?: boolean;
   noParsingErrors?: boolean;
+  keepUnknownProperties?: boolean;
 };
 
 /**

+ 26 - 10
src/data-parser.js

@@ -150,10 +150,22 @@ export class DataParser extends Service {
           );
         }
       }
+      if (options.keepUnknownProperties !== undefined) {
+        if (typeof options.keepUnknownProperties !== 'boolean') {
+          throw new InvalidArgumentError(
+            'Option "keepUnknownProperties" must be a Boolean, ' +
+              'but %v was given.',
+            options.keepUnknownProperties,
+          );
+        }
+      }
     }
     const sourcePath = (options && options.sourcePath) || undefined;
     const shallowMode = Boolean(options && options.shallowMode);
     const noParsingErrors = Boolean(options && options.noParsingErrors);
+    const keepUnknownProperties = Boolean(
+      options && options.keepUnknownProperties,
+    );
     // поверхностная проверка схемы
     // (режим shallowMode)
     validateDataSchema(schema, true);
@@ -192,9 +204,6 @@ export class DataParser extends Service {
         schema.properties !== undefined
       ) {
         let propsSchema = schema.properties;
-        // чтобы избежать изменения оригинального объекта,
-        // выполняется его поверхностное копирование
-        value = {...value};
         // если схема свойств не является объектом,
         // то выполняется извлечение схемы данных
         if (typeof propsSchema !== 'object') {
@@ -210,6 +219,9 @@ export class DataParser extends Service {
           }
           propsSchema = resolvedSchema.properties || {};
         }
+        // свойства исходного объекта, отсутствующие
+        // в схеме, будут отброшены
+        let newValue = keepUnknownProperties ? {...value} : {};
         Object.keys(propsSchema).forEach(propName => {
           const propSchema = propsSchema[propName];
           // если схема свойства не определена,
@@ -221,15 +233,19 @@ export class DataParser extends Service {
           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;
+          // исходный объект может не иметь свойства, указанного в схеме,
+          // и чтобы избежать добавления undefined, условием для записи
+          // значения является наличие свойства схемы в исходном объекте
+          // или отличие старого и нового значения данного свойства,
+          // таким образом, значение undefined не будет присвоено
+          // свойству, которого нет (новый ключ не будет добавлен)
+          if (propName in value || value[propName] !== newPropValue) {
+            newValue[propName] = newPropValue;
           }
         });
+        // исходное значение подменяется новым
+        // объектом, обработанным согласно схеме
+        value = newValue;
       }
     }
     // если допускается выброс ошибок, то результирующее

+ 85 - 1
src/data-parser.spec.js

@@ -242,6 +242,27 @@ describe('DataParser', function () {
       throwable(undefined)();
     });
 
+    it('should require the "keepUnknownProperties" argument to be a boolean', function () {
+      const S = new DataParser();
+      const throwable = v => () => S.parse(10, {}, {keepUnknownProperties: v});
+      const error = s =>
+        format(
+          'Option "keepUnknownProperties" 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});
@@ -506,7 +527,23 @@ describe('DataParser', function () {
       expect(res).to.be.eql({p1: 'abc', p2: 'bbc', p3: 'cbc'});
     });
 
-    it('should not parse object properties without a specified schema', function () {
+    it('should not exclude properties when no properties schema is specified', function () {
+      const S = new DataParser();
+      const value = {p1: 1, p2: 2, p3: 3};
+      const schema = {type: DataType.OBJECT};
+      const expectedCalls = [[value, schema, undefined, 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 exclude object properties without a specified schema', function () {
       const S = new DataParser();
       const value = {p1: 1, p2: 2, p3: 3};
       const schema = {
@@ -526,6 +563,53 @@ describe('DataParser', function () {
       };
       S.setParsers([parser]);
       const res = S.parse(value, schema);
+      expect(res).to.be.eql({p1: 1});
+      expect(expectedCalls).to.be.eql(calls);
+    });
+
+    it('should exclude properties when a properties schema is an empty object', function () {
+      const S = new DataParser();
+      const value = {p1: 1, p2: 2, p3: 3};
+      const schema = {
+        type: DataType.OBJECT,
+        properties: {},
+      };
+      const expectedCalls = [[value, schema, undefined, 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({});
+      expect(expectedCalls).to.be.eql(calls);
+    });
+
+    it('should keep unknown properties when the "keepUnknownProperties" is true', function () {
+      const S = new DataParser();
+      const value = {p1: 1, p2: 2, p3: 3};
+      const options = {keepUnknownProperties: true};
+      const schema = {
+        type: DataType.OBJECT,
+        properties: {p1: {type: DataType.NUMBER}},
+      };
+      const expectedCalls = [
+        [value, schema, options, S.container],
+        [
+          value.p1,
+          schema.properties.p1,
+          {...options, sourcePath: 'p1'},
+          S.container,
+        ],
+      ];
+      const calls = [];
+      const parser = (...args) => {
+        calls.push(args);
+        return args[0];
+      };
+      S.setParsers([parser]);
+      const res = S.parse(value, schema, options);
       expect(res).to.be.eql(value);
       expect(expectedCalls).to.be.eql(calls);
     });

+ 1 - 1
src/data-parsers/number-type-parser.js

@@ -23,7 +23,7 @@ export function numberTypeParser(value, schema, options, container) {
   if (typeof value === 'number') {
     return value;
   }
-  // если значение является не пусто строкой,
+  // если значение является не пустой строкой,
   // то выполняется попытка преобразования
   if (value && typeof value === 'string') {
     if (value.length <= 20) {

+ 1 - 1
src/data-validator.spec.js

@@ -273,7 +273,7 @@ describe('DataValidator', function () {
       expect(throwable).to.throw('Caught!');
     });
 
-    it('should apply parsers sequentially to a given value', function () {
+    it('should apply validators sequentially to a given value', function () {
       const S = new DataValidator();
       const value = [1, 2, 3];
       const schema = {

+ 3 - 3
src/errors/data-validation-error.spec.js

@@ -1,12 +1,12 @@
 import {expect} from 'chai';
-import {Errorf} from '@e22m4u/js-format';
+import {InvalidArgumentError} from '@e22m4u/js-format';
 import {DataValidationError} from './data-validation-error.js';
 
 describe('DataValidationError', function () {
   describe('constructor', function () {
-    it('extends the Errorf class', function () {
+    it('extends the InvalidArgumentError class', function () {
       const error = new DataValidationError('My error');
-      expect(error).to.be.instanceof(Errorf);
+      expect(error).to.be.instanceof(InvalidArgumentError);
     });
 
     it('interpolates given message', function () {