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

refactor: makes strict mode by default

e22m4u 2 недель назад
Родитель
Сommit
157a11fce0
6 измененных файлов с 144 добавлено и 96 удалено
  1. 9 9
      README.md
  2. 8 8
      dist/cjs/index.cjs
  3. 4 4
      src/data-projector.spec.js
  4. 1 1
      src/project-data.d.ts
  5. 17 14
      src/project-data.js
  6. 105 60
      src/project-data.spec.js

+ 9 - 9
README.md

@@ -8,7 +8,7 @@ JavaScript модуль для создания проекции данных н
 - [Схема проекции](#схема-проекции)
 - [Использование](#использование)
   - [Применение схемы](#применение-схемы)
-  - [Строгий режим](#строгий-режим)
+  - [Неуказанные поля](#неуказанные-поля)
   - [Область проекции](#область-проекции)
   - [Фабричные функции](#фабричные-функции)
   - [Именованные схемы](#именованные-схемы)
@@ -110,14 +110,13 @@ const schema = {
 const data = {
   name: 'Fedor',       // допускается, явное правило
   password: 'pass123', // исключается, явное правило
-  extra: 10,           // допускается в режиме по умолчанию
+  extra: 10,           // исключается в режиме по умолчанию
 };
 
 const result = projectData(data, schema);
 console.log(result);
 // {
-//   name: 'Fedor',
-//   extra: 10
+//   name: 'Fedor'
 // }
 ```
 
@@ -180,9 +179,9 @@ console.log(result);
 // }
 ```
 
-### Строгий режим
+### Неуказанные поля
 
-Исключение полей не указанных в схеме проекции.
+Допуск полей не указанных в схеме проекции.
 
 ```js
 import {projectData} from '@e22m4u/js-data-projector';
@@ -195,15 +194,16 @@ const schema = {
 const data = {
   name: 'Fedor',       // допускается, явное правило
   password: 'pass123', // исключается, явное правило
-  extra: 10,           // исключается в строгом режиме
+  extra: 10,           // допускается в режиме "keepUnknown"
 };
 
 const result = projectData(data, schema, {
-  strict: true, // <= строгий режим
+  keepUnknown: true, // <= допуск неуказанных полей
 });
 console.log(result);
 // {
-//   name: 'Fedor'
+//   name: 'Fedor',
+//   extra: 10
 // }
 ```
 

+ 8 - 8
dist/cjs/index.cjs

@@ -133,10 +133,10 @@ function projectData(data, schema, options) {
         options
       );
     }
-    if (options.strict !== void 0 && typeof options.strict !== "boolean") {
+    if (options.keepUnknown !== void 0 && typeof options.keepUnknown !== "boolean") {
       throw new import_js_format2.InvalidArgumentError(
-        'Projection option "strict" must be a Boolean, but %v was given.',
-        options.strict
+        'Projection option "keepUnknown" must be a Boolean, but %v was given.',
+        options.keepUnknown
       );
     }
     if (options.scope !== void 0 && (options.scope === "" || typeof options.scope !== "string")) {
@@ -194,15 +194,15 @@ function projectData(data, schema, options) {
     return data.map((item) => projectData(item, schema, options));
   }
   const result = {};
-  const strict = Boolean(options && options.strict);
+  const keepUnknown = Boolean(options && options.keepUnknown);
   const scope = options && options.scope || void 0;
-  const propNames = Object.keys(strict ? schema : data);
+  const propNames = Object.keys(keepUnknown ? data : schema);
   propNames.forEach((propName) => {
     if (!(propName in data)) {
       return;
     }
     const propOptions = schema[propName];
-    if (_shouldSelect(propOptions, strict, scope)) {
+    if (_shouldSelect(propOptions, keepUnknown, scope)) {
       const value = data[propName];
       if (propOptions && typeof propOptions === "object" && propOptions.schema) {
         result[propName] = projectData(value, propOptions.schema, options);
@@ -214,7 +214,7 @@ function projectData(data, schema, options) {
   return result;
 }
 __name(projectData, "projectData");
-function _shouldSelect(propOptions, strict, scope) {
+function _shouldSelect(propOptions, keepUnknown, scope) {
   if (typeof propOptions === "boolean") {
     return propOptions;
   }
@@ -233,7 +233,7 @@ function _shouldSelect(propOptions, strict, scope) {
     }
     return true;
   }
-  return !strict;
+  return Boolean(keepUnknown);
 }
 __name(_shouldSelect, "_shouldSelect");
 

+ 4 - 4
src/data-projector.spec.js

@@ -50,16 +50,16 @@ describe('DataProjector', function () {
       const S = new DataProjector();
       S.defineSchema({name: 'mySchema', schema: {foo: true, bar: false}});
       const res = S.project({foo: 10, bar: 20, baz: 30}, 'mySchema');
-      expect(res).to.be.eql({foo: 10, baz: 30});
+      expect(res).to.be.eql({foo: 10});
     });
 
-    it('should exclude unknown properties in the strict mode', function () {
+    it('should include unknown properties in the keep unknown mode', function () {
       const S = new DataProjector();
       S.defineSchema({name: 'mySchema', schema: {foo: true, bar: false}});
       const res = S.project({foo: 10, bar: 20, baz: 30}, 'mySchema', {
-        strict: true,
+        keepUnknown: true,
       });
-      expect(res).to.be.eql({foo: 10});
+      expect(res).to.be.eql({foo: 10, baz: 30});
     });
 
     it('should allow override the "nameResolver" option', function () {

+ 1 - 1
src/project-data.d.ts

@@ -11,7 +11,7 @@ export type ProjectionSchemaNameResolver = (
  * Project data options.
  */
 export type ProjectDataOptions = {
-  strict?: boolean;
+  keepUnknown?: boolean;
   scope?: string;
   nameResolver?: ProjectionSchemaNameResolver;
   factoryArgs?: unknown[];

+ 17 - 14
src/project-data.js

@@ -18,11 +18,14 @@ export function projectData(data, schema, options) {
         options,
       );
     }
-    // options.strict
-    if (options.strict !== undefined && typeof options.strict !== 'boolean') {
+    // options.keepUnknown
+    if (
+      options.keepUnknown !== undefined &&
+      typeof options.keepUnknown !== 'boolean'
+    ) {
       throw new InvalidArgumentError(
-        'Projection option "strict" must be a Boolean, but %v was given.',
-        options.strict,
+        'Projection option "keepUnknown" must be a Boolean, but %v was given.',
+        options.keepUnknown,
       );
     }
     // options.scope
@@ -126,12 +129,12 @@ export function projectData(data, schema, options) {
   // если данные являются объектом,
   // то проекция создается согласно схеме
   const result = {};
-  const strict = Boolean(options && options.strict);
+  const keepUnknown = Boolean(options && options.keepUnknown);
   const scope = (options && options.scope) || undefined;
-  // в обычном режиме итерация выполняется по ключам исходного
-  // объекта, а в строгом режиме по ключам, описанным в схеме
-  // (исключая ключи прототипа Object.keys(x))
-  const propNames = Object.keys(strict ? schema : data);
+  // в обычном режиме итерация выполняется по ключам схемы,
+  // но при активном параметре "keepUnknown" итерация выполняется
+  // по ключам исходного объекта (исключая ключи прототипа)
+  const propNames = Object.keys(keepUnknown ? data : schema);
   propNames.forEach(propName => {
     // если свойство отсутствует в исходных
     // данных, то свойство игнорируется
@@ -141,7 +144,7 @@ export function projectData(data, schema, options) {
     const propOptions = schema[propName];
     // проверка доступности свойства для данной
     // области проекции (если определена)
-    if (_shouldSelect(propOptions, strict, scope)) {
+    if (_shouldSelect(propOptions, keepUnknown, scope)) {
       const value = data[propName];
       // если определена вложенная схема,
       // то проекция применяется рекурсивно
@@ -170,14 +173,14 @@ export function projectData(data, schema, options) {
  * Приоритет:
  *   1. Правило для области.
  *   2. Общее правило.
- *   4. Режим проекции.
+ *   3. Режим "keepUnknown".
  *
  * @param {object|boolean} propOptions
- * @param {boolean|undefined} strict
+ * @param {boolean|undefined} keepUnknown
  * @param {string|undefined} scope
  * @returns {boolean}
  */
-function _shouldSelect(propOptions, strict, scope) {
+function _shouldSelect(propOptions, keepUnknown, scope) {
   // если настройки свойства являются логическим значением,
   // то значение используется как индикатор видимости
   if (typeof propOptions === 'boolean') {
@@ -231,5 +234,5 @@ function _shouldSelect(propOptions, strict, scope) {
   }
   // если правила видимости не определены
   // то результат будет зависеть от режима
-  return !strict;
+  return Boolean(keepUnknown);
 }

+ 105 - 60
src/project-data.spec.js

@@ -19,11 +19,11 @@ describe('projectData', function () {
     throwable(undefined)();
   });
 
-  it('should require the "strict" option to be a boolean', function () {
-    const throwable = v => () => projectData(10, {}, {strict: v});
+  it('should require the "keepUnknown" option to be a boolean', function () {
+    const throwable = v => () => projectData(10, {}, {keepUnknown: v});
     const error = s =>
       format(
-        'Projection option "strict" must be a Boolean, but %s was given.',
+        'Projection option "keepUnknown" must be a Boolean, but %s was given.',
         s,
       );
     expect(throwable('str')).to.throw(error('"str"'));
@@ -350,57 +350,118 @@ describe('projectData', function () {
     expect(projectData(null, {})).to.be.eq(null);
   });
 
-  it('should project an array items', function () {
-    const list = [{foo: 10, bar: 20, baz: 30}, {qux: 30}];
-    const expectedList = [{foo: 10, baz: 30}, {qux: 30}];
-    const res = projectData(list, {foo: true, bar: false});
-    expect(res).to.be.eql(expectedList);
+  it('should include a property defined with an empty object', function () {
+    const schema = {foo: {}, bar: {}};
+    const data = {foo: 10, baz: 30};
+    const res = projectData(data, schema);
+    expect(res).to.be.eql({foo: 10});
   });
 
-  it('should project an array items in the strict mode', function () {
-    const list = [{foo: 10, bar: 20, baz: 30}, {qux: 30}];
-    const expectedList = [{foo: 10}, {}];
-    const res = projectData(list, {foo: true, bar: false}, {strict: true});
-    expect(res).to.be.eql(expectedList);
+  it('should exclude a property when the logical rule is false', function () {
+    const schema = {foo: false, bar: false};
+    const data = {foo: 10, bar: 20};
+    const res = projectData(data, schema);
+    expect(res).to.be.eql({});
   });
 
-  it('should exclude unknown properties when the strict mode is enabled', function () {
-    const res = projectData(
-      {foo: 10, bar: 20, baz: 30},
-      {foo: true, bar: false},
-      {strict: true},
-    );
-    expect(res).to.be.eql({foo: 10});
+  it('should include a property when the logical rule is true', function () {
+    const schema = {foo: true, bar: true};
+    const data = {foo: 10, bar: 20};
+    const res = projectData(data, schema);
+    expect(res).to.be.eql({foo: 10, bar: 20});
   });
 
-  it('should include a property defined with an empty object in the strict mode', function () {
-    const schema = {foo: {}, bar: {}};
-    const data = {foo: 10, baz: 30};
-    const res = projectData(data, schema, {strict: true});
+  it('should exclude a property when the "select" option is false', function () {
+    const schema = {foo: true, bar: {select: false}};
+    const data = {foo: 10, bar: 20};
+    const res = projectData(data, schema);
     expect(res).to.be.eql({foo: 10});
   });
 
-  it('should include a property with a nested schema in the strict mode', function () {
+  it('should include a property when the "select" option is true', function () {
+    const schema = {foo: true, bar: {select: true}};
+    const data = {foo: 10, bar: 20};
+    const res = projectData(data, schema);
+    expect(res).to.be.eql({foo: 10, bar: 20});
+  });
+
+  it('should include a property with a nested schema', function () {
     const schema = {user: {schema: {id: true, name: false}}};
-    const data = {user: {id: 1, name: 'John Doe'}, timestamp: 12345};
-    const res = projectData(data, schema, {strict: true});
+    const data = {user: {id: 1, name: 'John Doe'}};
+    const res = projectData(data, schema);
     expect(res).to.be.eql({user: {id: 1}});
   });
 
-  it('should exclude a property when the "select" option is false in the strict mode', function () {
-    const schema = {foo: {}, bar: {select: false}};
+  it('should exclude properties not defined in a given schema', function () {
+    const res = projectData({foo: 10, bar: 20}, {});
+    expect(res).to.be.eql({});
+  });
+
+  it('should project an array items by a given schema', function () {
+    const list = [
+      {foo: 10, bar: 20, baz: 30},
+      {bar: 20, qux: 30},
+    ];
+    const expectedList = [{foo: 10}, {}];
+    const res = projectData(list, {foo: true, bar: false});
+    expect(res).to.be.eql(expectedList);
+  });
+
+  it('should include a property defined with an empty object in the keep unknown mode', function () {
+    const schema = {foo: {}, bar: {}};
     const data = {foo: 10, bar: 20};
-    const res = projectData(data, schema, {strict: true});
+    const res = projectData(data, schema, {keepUnknown: true});
+    expect(res).to.be.eql({foo: 10, bar: 20});
+  });
+
+  it('should exclude a property when the logical rule is false in the keep unknown mode', function () {
+    const schema = {foo: false, bar: false};
+    const data = {foo: 10, bar: 20};
+    const res = projectData(data, schema, {keepUnknown: true});
+    expect(res).to.be.eql({});
+  });
+
+  it('should include a property when the logical rule is true in the keep unknown mode', function () {
+    const schema = {foo: true, bar: true};
+    const data = {foo: 10, bar: 20};
+    const res = projectData(data, schema, {keepUnknown: true});
+    expect(res).to.be.eql({foo: 10, bar: 20});
+  });
+
+  it('should exclude a property when the "select" option is false in the keep unknown mode', function () {
+    const schema = {foo: true, bar: {select: false}};
+    const data = {foo: 10, bar: 20};
+    const res = projectData(data, schema, {keepUnknown: true});
     expect(res).to.be.eql({foo: 10});
   });
 
-  it('should include a property when the "select" option is true in the strict mode', function () {
-    const schema = {foo: {}, bar: {select: true}};
+  it('should include a property when the "select" option is true in the keep unknown mode', function () {
+    const schema = {foo: true, bar: {select: true}};
+    const data = {foo: 10, bar: 20};
+    const res = projectData(data, schema, {keepUnknown: true});
+    expect(res).to.be.eql({foo: 10, bar: 20});
+  });
+
+  it('should include a property with a nested schema in the keep unknown mode', function () {
+    const schema = {user: {schema: {id: true, name: false}}};
+    const data = {user: {id: 1, name: 'John Doe'}};
+    const res = projectData(data, schema, {keepUnknown: true});
+    expect(res).to.be.eql({user: {id: 1}});
+  });
+
+  it('should include unknown properties in the keep unknown mode', function () {
     const data = {foo: 10, bar: 20};
-    const res = projectData(data, schema, {strict: true});
+    const res = projectData(data, {}, {keepUnknown: true});
     expect(res).to.be.eql({foo: 10, bar: 20});
   });
 
+  it('should project an array items in the keep unknown mode', function () {
+    const list = [{foo: 10, bar: 20, baz: 30}, {qux: 30}];
+    const expectedList = [{foo: 10, baz: 30}, {qux: 30}];
+    const res = projectData(list, {foo: true, bar: false}, {keepUnknown: true});
+    expect(res).to.be.eql(expectedList);
+  });
+
   it('should ignore prototype properties', function () {
     const data = Object.create({baz: 30});
     data.foo = 10;
@@ -410,19 +471,6 @@ describe('projectData', function () {
     expect(res).to.be.eql({foo: 10});
   });
 
-  it('should project the property by a boolean rule', function () {
-    const res = projectData({foo: 10, bar: 20}, {foo: true, bar: false});
-    expect(res).to.be.eql({foo: 10});
-  });
-
-  it('should project the property by the select option', function () {
-    const res = projectData(
-      {foo: 10, bar: 20},
-      {foo: {select: true}, bar: {select: false}},
-    );
-    expect(res).to.be.eql({foo: 10});
-  });
-
   it('should ignore scope options when no active scope is provided', function () {
     const schema = {
       foo: {select: true, scopes: {input: false}},
@@ -432,7 +480,7 @@ describe('projectData', function () {
     expect(res).to.be.eql({foo: 10});
   });
 
-  it('should project the active scope by the boolean rule', function () {
+  it('should project the active scope by a boolean rule', function () {
     const schema = {
       foo: {scopes: {input: true}},
       bar: {scopes: {input: false}},
@@ -450,7 +498,7 @@ describe('projectData', function () {
     expect(res).to.be.eql({foo: 10});
   });
 
-  it('should prioritize the scope rule over the general options', function () {
+  it('should prioritize the scope rule over common rules', function () {
     const schema = {
       foo: {select: false, scopes: {input: true}},
       bar: {select: true, scopes: {input: false}},
@@ -468,34 +516,31 @@ describe('projectData', function () {
     expect(res).to.be.eql({foo: 10});
   });
 
-  it('should include a property in the strict mode if no rule for the active scope is specified', function () {
+  it('should include a property in the keep unknown mode if no rule for the active scope is specified', function () {
     const schema = {
       foo: {scopes: {input: true}},
       bar: {scopes: {input: false}},
       baz: {scopes: {output: true}},
     };
     const data = {foo: 10, bar: 20, baz: 30, qux: 40};
-    const res = projectData(data, schema, {strict: true, scope: 'input'});
-    expect(res).to.be.eql({foo: 10, baz: 30});
+    const res = projectData(data, schema, {scope: 'input', keepUnknown: true});
+    expect(res).to.be.eql({foo: 10, baz: 30, qux: 40});
   });
 
-  it('should prioritize the scope options over the general options in the strict mode', function () {
+  it('should prioritize scope options over common options in the keep unknown mode', function () {
     const schema = {
       foo: {select: false, scopes: {input: true}},
       bar: {select: false, scopes: {input: {select: true}}},
     };
     const data = {foo: 10, bar: 20, baz: 30};
-    const res = projectData(data, schema, {strict: true, scope: 'input'});
-    expect(res).to.be.eql({foo: 10, bar: 20});
+    const res = projectData(data, schema, {scope: 'input', keepUnknown: true});
+    expect(res).to.be.eql({foo: 10, bar: 20, baz: 30});
   });
 
-  it('should project the nested object by the given schema', function () {
-    const schema = {
-      foo: true,
-      bar: {schema: {baz: true, qux: false}},
-    };
+  it('should project a nested object by a given schema', function () {
+    const schema = {foo: true, bar: {schema: {baz: true, qux: false}}};
     const data = {foo: 10, bar: {baz: 20, qux: 30, buz: 40}};
     const res = projectData(data, schema, {scope: 'input'});
-    expect(res).to.be.eql({foo: 10, bar: {baz: 20, buz: 40}});
+    expect(res).to.be.eql({foo: 10, bar: {baz: 20}});
   });
 });