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

feat: adds default option to schema

e22m4u 3 недель назад
Родитель
Сommit
53f6dff7ab

+ 14 - 0
README.md

@@ -45,6 +45,7 @@ const {DataValidator} = require('@e22m4u/js-data-schema');
 - `items` схема элементов массива, фабрика или имя схемы;
 - `properties` схема свойств объекта, фабрика или имя схемы;
 - `required` исключает *пустые значения*;
+- `default` значение по умолчанию или фабрика;
 
 #### type
 
@@ -151,6 +152,19 @@ const {DataValidator} = require('@e22m4u/js-data-schema');
 // "foo", "bar" ...
 ```
 
+#### default
+
+Значение по умолчанию можно указать в параметре `default`. Указанное значение
+будет использовано, если исходное значение оказалось *пустым* при разборе
+входящих данных.
+
+```js
+{
+  type: DataType.STRING, // тип данных
+  default: 'N/A'         // значение по умолчанию
+}
+```
+
 ## Использование
 
 Ниже приводятся примеры использования различных схем.

+ 36 - 3
dist/cjs/index.cjs

@@ -33,6 +33,7 @@ __export(index_exports, {
   arrayTypeValidator: () => arrayTypeValidator,
   booleanTypeParser: () => booleanTypeParser,
   booleanTypeValidator: () => booleanTypeValidator,
+  defaultValueSetter: () => defaultValueSetter,
   getDataTypeFromValue: () => getDataTypeFromValue,
   numberTypeParser: () => numberTypeParser,
   numberTypeValidator: () => numberTypeValidator,
@@ -69,7 +70,7 @@ function getDataTypeFromValue(value) {
 __name(getDataTypeFromValue, "getDataTypeFromValue");
 
 // src/data-parser.js
-var import_js_service15 = require("@e22m4u/js-service");
+var import_js_service16 = require("@e22m4u/js-service");
 
 // src/data-validator.js
 var import_js_service9 = require("@e22m4u/js-service");
@@ -872,8 +873,30 @@ function booleanTypeParser(value, schema, options, container) {
 }
 __name(booleanTypeParser, "booleanTypeParser");
 
+// src/data-parsers/default-value-setter.js
+var import_js_service15 = require("@e22m4u/js-service");
+var import_js_empty_values12 = require("@e22m4u/js-empty-values");
+function defaultValueSetter(value, schema, options, container) {
+  if (options && options.noDefaultValues) {
+    return value;
+  }
+  if (schema.default === void 0) {
+    return value;
+  }
+  const emptyValues = container.get(import_js_empty_values12.EmptyValuesService);
+  const dataType = schema.type || DataType.ANY;
+  if (!emptyValues.isEmptyOf(dataType, value)) {
+    return value;
+  }
+  if (typeof schema.default === "function") {
+    return schema.default(container);
+  }
+  return structuredClone(schema.default);
+}
+__name(defaultValueSetter, "defaultValueSetter");
+
 // src/data-parser.js
-var _DataParser = class _DataParser extends import_js_service15.Service {
+var _DataParser = class _DataParser extends import_js_service16.Service {
   /**
    * Parsers.
    *
@@ -884,7 +907,8 @@ var _DataParser = class _DataParser extends import_js_service15.Service {
     booleanTypeParser,
     numberTypeParser,
     arrayTypeParser,
-    objectTypeParser
+    objectTypeParser,
+    defaultValueSetter
   ];
   /**
    * Get parsers.
@@ -978,6 +1002,14 @@ var _DataParser = class _DataParser extends import_js_service15.Service {
           );
         }
       }
+      if (options.noDefaultValues !== void 0) {
+        if (typeof options.noDefaultValues !== "boolean") {
+          throw new import_js_format8.InvalidArgumentError(
+            'Option "noDefaultValues" must be a Boolean, but %v was given.',
+            options.noDefaultValues
+          );
+        }
+      }
       if (options.noParsingErrors !== void 0) {
         if (typeof options.noParsingErrors !== "boolean") {
           throw new import_js_format8.InvalidArgumentError(
@@ -1057,6 +1089,7 @@ var DataParser = _DataParser;
   arrayTypeValidator,
   booleanTypeParser,
   booleanTypeValidator,
+  defaultValueSetter,
   getDataTypeFromValue,
   numberTypeParser,
   numberTypeValidator,

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

@@ -8,6 +8,7 @@ import {DataSchemaDefinition} from './data-schema-definition.js';
 export type DataParsingOptions = {
   sourcePath?: string;
   shallowMode?: boolean;
+  noDefaultValues?: boolean;
   noParsingErrors?: boolean;
 };
 

+ 10 - 0
src/data-parser.js

@@ -12,6 +12,7 @@ import {
   numberTypeParser,
   objectTypeParser,
   booleanTypeParser,
+  defaultValueSetter,
 } from './data-parsers/index.js';
 
 /**
@@ -29,6 +30,7 @@ export class DataParser extends Service {
     numberTypeParser,
     arrayTypeParser,
     objectTypeParser,
+    defaultValueSetter,
   ];
 
   /**
@@ -132,6 +134,14 @@ export class DataParser extends Service {
           );
         }
       }
+      if (options.noDefaultValues !== undefined) {
+        if (typeof options.noDefaultValues !== 'boolean') {
+          throw new InvalidArgumentError(
+            'Option "noDefaultValues" must be a Boolean, but %v was given.',
+            options.noDefaultValues,
+          );
+        }
+      }
       if (options.noParsingErrors !== undefined) {
         if (typeof options.noParsingErrors !== 'boolean') {
           throw new InvalidArgumentError(

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

@@ -10,6 +10,7 @@ import {
   objectTypeParser,
   stringTypeParser,
   booleanTypeParser,
+  defaultValueSetter,
 } from './data-parsers/index.js';
 
 describe('DataParser', function () {
@@ -23,6 +24,7 @@ describe('DataParser', function () {
         numberTypeParser,
         arrayTypeParser,
         objectTypeParser,
+        defaultValueSetter,
       ]);
     });
 
@@ -196,6 +198,27 @@ describe('DataParser', function () {
       throwable(undefined)();
     });
 
+    it('should require the "noDefaultValues" argument to be a boolean', function () {
+      const S = new DataParser();
+      const throwable = v => () => S.parse(10, {}, {noDefaultValues: v});
+      const error = s =>
+        format(
+          'Option "noDefaultValues" 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});

+ 6 - 0
src/data-parsers/default-value-setter.d.ts

@@ -0,0 +1,6 @@
+import {DataParsingFunction} from "../data-parser.js";
+
+/**
+ * Default value setter.
+ */
+export declare const defaultValueSetter: DataParsingFunction;

+ 40 - 0
src/data-parsers/default-value-setter.js

@@ -0,0 +1,40 @@
+import {DataType} from '../data-type.js';
+import {ServiceContainer} from '@e22m4u/js-service';
+import {EmptyValuesService} from '@e22m4u/js-empty-values';
+
+/**
+ * Default value setter.
+ *
+ * @param {*} value
+ * @param {object} schema
+ * @param {object|undefined} options
+ * @param {ServiceContainer} container
+ * @returns {*}
+ */
+export function defaultValueSetter(value, schema, options, container) {
+  // если применение значений по умолчанию
+  // отключено, то операция пропускается
+  if (options && options.noDefaultValues) {
+    return value;
+  }
+  // если схема не имеет значения по умолчанию,
+  // то операция пропускается
+  if (schema.default === undefined) {
+    return value;
+  }
+  // если значение не является пустым,
+  // то операция пропускается
+  const emptyValues = container.get(EmptyValuesService);
+  const dataType = schema.type || DataType.ANY;
+  if (!emptyValues.isEmptyOf(dataType, value)) {
+    return value;
+  }
+  // если значение является фабрикой,
+  // то возвращается фабричное значение
+  if (typeof schema.default === 'function') {
+    return schema.default(container);
+  }
+  // в противном случае, возвращается
+  // копия значения по умолчанию
+  return structuredClone(schema.default);
+}

+ 107 - 0
src/data-parsers/default-value-setter.spec.js

@@ -0,0 +1,107 @@
+import {expect} from 'chai';
+import {DataType} from '../data-type.js';
+import {ServiceContainer} from '@e22m4u/js-service';
+import {EmptyValuesService} from '@e22m4u/js-empty-values';
+import {defaultValueSetter} from './default-value-setter.js';
+
+describe('defaultValueSetter', function () {
+  it('should return an empty value as is when default values is disabled', function () {
+    const container = new ServiceContainer();
+    const emptyValues = container.get(EmptyValuesService);
+    emptyValues.setEmptyValuesOf(DataType.ANY, ['none']);
+    const res = defaultValueSetter(
+      'none',
+      {type: DataType.ANY, default: 10},
+      {noDefaultValues: true},
+      container,
+    );
+    expect(res).to.be.eq('none');
+  });
+
+  it('should return an empty value as is when no default value is specified', function () {
+    const container = new ServiceContainer();
+    const emptyValues = container.get(EmptyValuesService);
+    emptyValues.setEmptyValuesOf(DataType.ANY, ['none']);
+    const res = defaultValueSetter(
+      'none',
+      {type: DataType.ANY},
+      undefined,
+      container,
+    );
+    expect(res).to.be.eq('none');
+  });
+
+  it('should return a non-empty value as is', function () {
+    const container = new ServiceContainer();
+    const emptyValues = container.get(EmptyValuesService);
+    emptyValues.setEmptyValuesOf(DataType.ANY, ['none']);
+    const res = defaultValueSetter(
+      'non-empty',
+      {type: DataType.ANY},
+      undefined,
+      container,
+    );
+    expect(res).to.be.eq('non-empty');
+  });
+
+  it('should return a non-empty value as is even if the "default" option is specified', function () {
+    const container = new ServiceContainer();
+    const emptyValues = container.get(EmptyValuesService);
+    emptyValues.setEmptyValuesOf(DataType.ANY, ['none']);
+    const res = defaultValueSetter(
+      'non-empty',
+      {type: DataType.ANY, default: 10},
+      undefined,
+      container,
+    );
+    expect(res).to.be.eq('non-empty');
+  });
+
+  it('should resolve a factory value from the "default" option', function () {
+    const container = new ServiceContainer();
+    const emptyValues = container.get(EmptyValuesService);
+    emptyValues.setEmptyValuesOf(DataType.ANY, ['none']);
+    let invoked = 0;
+    const factory = (...args) => {
+      invoked++;
+      expect(args).to.be.eql([container]);
+      return 10;
+    };
+    const res = defaultValueSetter(
+      'none',
+      {type: DataType.ANY, default: factory},
+      undefined,
+      container,
+    );
+    expect(res).to.be.eq(10);
+    expect(invoked).to.be.eq(1);
+  });
+
+  it('should return a default value when a given value is empty', function () {
+    const container = new ServiceContainer();
+    const emptyValues = container.get(EmptyValuesService);
+    emptyValues.setEmptyValuesOf(DataType.ANY, ['none']);
+    const res = defaultValueSetter(
+      'none',
+      {type: DataType.ANY, default: 10},
+      undefined,
+      container,
+    );
+    expect(res).to.be.eq(10);
+  });
+
+  it('should return a clone of a default value instead of its reference', function () {
+    const container = new ServiceContainer();
+    const emptyValues = container.get(EmptyValuesService);
+    emptyValues.setEmptyValuesOf(DataType.ANY, ['none']);
+    const defaultValue = {foo: 'bar'};
+    const res = defaultValueSetter(
+      'none',
+      {type: DataType.ANY, default: defaultValue},
+      undefined,
+      container,
+    );
+    expect(res).to.be.eql(defaultValue);
+    expect(res).to.be.not.eq(defaultValue);
+  });
+});

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

@@ -1,5 +1,6 @@
-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'
+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';
+export * from './default-value-setter.js';

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

@@ -3,3 +3,4 @@ export * from './string-type-parser.js';
 export * from './number-type-parser.js';
 export * from './object-type-parser.js';
 export * from './boolean-type-parser.js';
+export * from './default-value-setter.js';

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

@@ -14,6 +14,7 @@ export type DataSchemaObject = {
   items?: DataSchema;
   properties?: DataSchemaProperties | DataSchemaFactory | DataSchemaName;
   required?: boolean;
+  default?: unknown;
 };
 
 /**