From 9ff16c346d6f13caabd4910a7d920c1c11eced18 Mon Sep 17 00:00:00 2001 From: Simon Guo Date: Tue, 9 Apr 2024 15:56:28 +0800 Subject: [PATCH] feat: add support for `label` method (#77) * feat: add support for `label` method * test: update tests and docs --- README.md | 66 ++++++++++++++++------ src/MixedType.ts | 17 +++++- test/MixedTypeSpec.js | 125 +++++++++++++++++++++++++++++++++--------- 3 files changed, 163 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index 41d5f79..e7e5d9a 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ Schema for data modeling & validation - [`when(condition: (schemaSpec: SchemaDeclaration) => Type)`](#whencondition-schemaspec-schemadeclarationdatatype-errormsgtype--type) - [`check(value: ValueType, data?: DataType):CheckResult`](#checkvalue-valuetype-data-datatypecheckresult) - [`checkAsync(value: ValueType, data?: DataType):Promise`](#checkasyncvalue-valuetype-data-datatypepromisecheckresult) + - [`label(label: string)`](#labellabel-string) - [StringType(errorMessage?: string)](#stringtypeerrormessage-string) - [`isEmail(errorMessage?: string)`](#isemailerrormessage-string) - [`isURL(errorMessage?: string)`](#isurlerrormessage-string) @@ -445,37 +446,49 @@ MixedType().addAsyncRule((value, data) => { #### `when(condition: (schemaSpec: SchemaDeclaration) => Type)` -Define data verification rules based on conditions. +Conditional validation, the return value is a new type. -```js +```ts const model = SchemaModel({ - age: NumberType().min(18, 'error'), - contact: MixedType().when(schema => { - const checkResult = schema.age.check(); - return checkResult.hasError - ? StringType().isRequired('Please provide contact information') - : StringType(); + option: StringType().isOneOf(['a', 'b', 'other']), + other: StringType().when(schema => { + const { value } = schema.option; + return value === 'other' ? StringType().isRequired('Other required') : StringType(); }) }); /** -{ - age: { hasError: false }, - contact: { hasError: false } +{ + option: { hasError: false }, + other: { hasError: false } } */ -model.check({ age: 18, contact: '' }); +model.check({ option: 'a', other: '' }); /* { - age: { hasError: true, errorMessage: 'error' }, - contact: { - hasError: true, - errorMessage: 'Please provide contact information' - } + option: { hasError: false }, + other: { hasError: true, errorMessage: 'Other required' } } */ -model.check({ age: 17, contact: '' }); +model.check({ option: 'other', other: '' }); +``` + +Check whether a field passes the validation to determine the validation rules of another field. + +```js +const model = SchemaModel({ + password: StringType().isRequired('Password required'), + confirmPassword: StringType().when(schema => { + const { hasError } = schema.password.check(); + return hasError + ? StringType() + : StringType().addRule( + value => value === schema.password.value, + 'The passwords are inconsistent twice' + ); + }) +}); ``` #### `check(value: ValueType, data?: DataType):CheckResult` @@ -515,6 +528,23 @@ type.checkAsync(1).then(checkResult => { }); ``` +#### `label(label: string)` + +Overrides the key name in error messages. + +```js +MixedType().label('Username'); +``` + +Eg: + +```js +SchemaModel({ + first_name: StringType().label('First name'), + age: NumberType().label('Age') +}); +``` + ### StringType(errorMessage?: string) Define a string type. Supports all the same methods as [MixedType](#mixedtype). diff --git a/src/MixedType.ts b/src/MixedType.ts index d559d5c..9d146cf 100644 --- a/src/MixedType.ts +++ b/src/MixedType.ts @@ -24,6 +24,7 @@ export class MixedType[] = []; protected priorityRules: RuleType[] = []; + protected fieldLabel?: string; schemaSpec: SchemaDeclaration; value: any; @@ -43,7 +44,9 @@ export class MixedType() { diff --git a/test/MixedTypeSpec.js b/test/MixedTypeSpec.js index 84fd684..78af3f5 100644 --- a/test/MixedTypeSpec.js +++ b/test/MixedTypeSpec.js @@ -1,7 +1,8 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ -const chai = require('chai'); -const schema = require('../src'); +import chai, { expect } from 'chai'; +import * as schema from '../src'; + chai.should(); + const { StringType, SchemaModel, NumberType, ArrayType, MixedType } = schema; describe('#MixedType', () => { @@ -364,42 +365,103 @@ describe('#MixedType', () => { const checkResult1 = model.check({ field1: 20, field2: 2 }); - checkResult1.field1.hasError.should.equal(false); - checkResult1.field2.hasError.should.equal(false); + expect(checkResult1).to.deep.equal({ + field1: { hasError: false }, + field2: { hasError: false } + }); const checkResult2 = model.check({ field1: 1, field2: 1 }); - checkResult2.field1.hasError.should.equal(true); - checkResult2.field2.hasError.should.equal(true); - checkResult2.field2.errorMessage.should.equal('error1'); + expect(checkResult2).to.deep.equal({ + field1: { hasError: true, errorMessage: 'field1 must be greater than or equal to 10' }, + field2: { hasError: true, errorMessage: 'error1' } + }); const checkResult3 = model.check({ field1: 10, field2: 1 }); - checkResult3.field1.hasError.should.equal(false); - checkResult3.field2.hasError.should.equal(true); - checkResult3.field2.errorMessage.should.equal('error2'); + expect(checkResult3).to.deep.equal({ + field1: { hasError: false }, + field2: { hasError: true, errorMessage: 'error2' } + }); const checkResult4 = model.checkForField('field2', { field1: 20, field2: 1 }); checkResult4.errorMessage.should.equal('error2'); + expect(checkResult4).to.deep.equal({ hasError: true, errorMessage: 'error2' }); + const checkResult5 = model.checkForField('field2', { field1: 9, field2: 1 }); - checkResult5.errorMessage.should.equal('error1'); + + expect(checkResult5).to.deep.equal({ hasError: true, errorMessage: 'error1' }); }); - it('Should be high priority even if it is empty', () => { + it('Should type be changed by condition', () => { const model = SchemaModel({ - age: NumberType().min(18, 'error1'), - contact: StringType().when(schema => { - const checkResult = schema.age.check(); - return checkResult.hasError ? StringType().isRequired('error2') : StringType(); + option: StringType().isOneOf(['a', 'b', 'other']), + other: StringType().when(schema => { + const { value } = schema.option; + return value === 'other' ? StringType().isRequired('Other required') : StringType(); }) }); - const checkResult = model.check({ age: 17, contact: '' }); + const checkResult = model.check({ option: 'a', other: '' }); + + expect(checkResult).to.deep.equal({ + option: { hasError: false }, + other: { hasError: false } + }); - checkResult.age.hasError.should.equal(true); - checkResult.contact.hasError.should.equal(true); - checkResult.contact.errorMessage.should.equal('error2'); + const checkResult2 = model.check({ option: 'other', other: '' }); + + expect(checkResult2).to.deep.equal({ + option: { hasError: false }, + other: { hasError: true, errorMessage: 'Other required' } + }); + }); + + it('Should type be changed by condition', () => { + const model = SchemaModel({ + password: StringType().isRequired('Password required'), + confirmPassword: StringType().when(schema => { + const { hasError } = schema.password.check(); + return hasError + ? StringType() + : StringType() + .addRule( + value => value === schema.password.value, + 'The passwords are inconsistent twice' + ) + .isRequired() + .label('Confirm password'); + }) + }); + + const checkResult = model.check({ password: '', confirmPassword: '123' }); + + expect(checkResult).to.deep.equal({ + password: { hasError: true, errorMessage: 'Password required' }, + confirmPassword: { hasError: false } + }); + + const checkResult2 = model.check({ password: '123', confirmPassword: '123' }); + + expect(checkResult2).to.deep.equal({ + password: { hasError: false }, + confirmPassword: { hasError: false } + }); + + const checkResult3 = model.check({ password: '123', confirmPassword: '1234' }); + + expect(checkResult3).to.deep.equal({ + password: { hasError: false }, + confirmPassword: { hasError: true, errorMessage: 'The passwords are inconsistent twice' } + }); + + const checkResult4 = model.check({ password: '123', confirmPassword: '' }); + + expect(checkResult4).to.deep.equal({ + password: { hasError: false }, + confirmPassword: { hasError: true, errorMessage: 'Confirm password is a required field' } + }); }); it('should error when an async rule is executed by the sync validator', () => { @@ -450,14 +512,27 @@ describe('#MixedType', () => { .isRequired('error2'); setTimeout(() => { try { - chai.expect(called).to.eq(false); - chai.expect(type.check('').hasError).to.eq(true); - chai.expect(type.check('1').hasError).to.eq(true); - chai.expect(type.check(1).hasError).to.eq(false); + expect(called).to.eq(false); + expect(type.check('').hasError).to.eq(true); + expect(type.check('1').hasError).to.eq(true); + expect(type.check(1).hasError).to.eq(false); + done(); } catch (e) { done(e); } }, 100); }); + + it('Should use label to override the field name in the error message', () => { + const schema = SchemaModel({ + first_name: StringType().label('First Name').isRequired(), + age: NumberType().label('Age').isRequired() + }); + + expect(schema.check({})).to.deep.equal({ + first_name: { hasError: true, errorMessage: 'First Name is a required field' }, + age: { hasError: true, errorMessage: 'Age is a required field' } + }); + }); });