-
Notifications
You must be signed in to change notification settings - Fork 9
[RMBWEB-2780] Support for structured validation results #87
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
83cd99a
8d68591
f466115
db9f109
227f425
d84c44a
90e4499
78f35e5
b2f88ba
e5afe2a
3e45f64
511d66f
b8fb666
9065ddf
5d56725
2c76ca8
a8cb353
031f8a1
ba4b45e
dbba8f5
a5ec69d
82e505b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,19 +3,13 @@ import * as v2 from 'formstate-x-v2' | |
| import { BaseState } from '../state' | ||
| import * as v3 from '..' | ||
| import Disposable from '../disposable' | ||
| import { isPromiseLike, normalizeError, normalizeRawError } from '../utils' | ||
|
|
||
| interface IV3StateFromV2<T extends v2.ComposibleValidatable<unknown, V>, V> extends v3.IState<V> { | ||
| /** The original ([email protected]) state */ | ||
| $: T | ||
| } | ||
|
|
||
| type ExcludeErrorObject<V> = V extends v3.ErrorObject ? never : V extends Promise<infer R> ? Promise<ExcludeErrorObject<R>> : V | ||
|
|
||
| type Conditional<K> = K extends any ? ExcludeErrorObject<K> : never | ||
|
|
||
| // Omit ErrorObject | ||
| type LegacyValidator<T> = (V: T) => Conditional<ReturnType<v3.Validator<T>>> | ||
|
|
||
| class Upgrader<T extends v2.ComposibleValidatable<unknown, V>, V> extends BaseState implements IV3StateFromV2<T, V> { | ||
| constructor(private stateV2: T) { | ||
| super() | ||
|
|
@@ -50,12 +44,12 @@ class Upgrader<T extends v2.ComposibleValidatable<unknown, V>, V> extends BaseSt | |
| setForV2(this.stateV2, value) | ||
| } | ||
| reset() { this.stateV2.reset() } | ||
| withValidator(...validators: Array<LegacyValidator<V>>) { | ||
| withValidator(...validators: Array<v3.Validator<V>>) { | ||
| if ( | ||
| isV2FieldState(this.stateV2) | ||
| || isV2FormState(this.stateV2) | ||
| ) { | ||
| this.stateV2.validators(...validators) | ||
| this.stateV2.validators(...portV2Validators(...validators)) | ||
| return this | ||
| } | ||
| throwNotSupported() | ||
|
|
@@ -72,6 +66,22 @@ class Upgrader<T extends v2.ComposibleValidatable<unknown, V>, V> extends BaseSt | |
| } | ||
| } | ||
|
|
||
| function portV2Validators<V>(...validators: Array<v3.Validator<V>>): Array<v2.Validator<V>> { | ||
| const normalizeRet = (v: any) => ( | ||
| normalizeError(normalizeRawError(v)) | ||
| ) | ||
| return validators.map(validator => { | ||
| return (value: V) => { | ||
| const returned = validator(value) | ||
| if (isPromiseLike(returned)) { | ||
| return returned.then(normalizeRet) | ||
| } else { | ||
| return normalizeRet(returned) | ||
| } | ||
| } | ||
| }) | ||
| } | ||
|
|
||
| /** Converts [email protected] state to [email protected] state */ | ||
| export function fromV2<T extends v2.ComposibleValidatable<unknown, unknown>>(stateV2: T): IV3StateFromV2<T, T['value']> { | ||
| return new Upgrader(stateV2) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,7 @@ | ||
| import { action, autorun, computed, makeObservable, observable, when } from 'mobx' | ||
| import { ValidationRawError, ValidationError, IState, Validation, ValidateResult, ValidateStatus, Validator } from './types' | ||
| import Disposable from './disposable' | ||
| import { applyValidators, isValid, isPromiseLike, isErrorObject } from './utils' | ||
| import { applyValidators, isPromiseLike, normalizeRawError, normalizeError } from './utils' | ||
|
|
||
| /** Extraction for some basic features of State */ | ||
| export abstract class BaseState extends Disposable implements Pick< | ||
|
|
@@ -63,7 +63,7 @@ export abstract class ValidatableState<V> extends BaseState implements IState<V> | |
| } | ||
|
|
||
| @computed get ownError() { | ||
|
||
| return this.rawError ? (isErrorObject(this.rawError) ? this.rawError.message : this.rawError) : undefined | ||
| return normalizeError(this.rawError) | ||
| } | ||
|
|
||
| @computed get error() { | ||
|
|
@@ -108,7 +108,7 @@ export abstract class ValidatableState<V> extends BaseState implements IState<V> | |
| action('end-validation', () => { | ||
| this.validation = undefined | ||
| this._validateStatus = ValidateStatus.Validated | ||
| this.setError(isValid(result) ? undefined : result) | ||
| this.setError(normalizeRawError(result)) | ||
| })() | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,23 +1,24 @@ | ||
| import { observable } from 'mobx' | ||
| import { asyncResultsAnd, isValid, isArrayLike, isErrorObject } from './utils' | ||
| import { asyncResultsAnd, isValidationPassed, isArrayLike, isErrorObject, normalizeRawError, normalizeError } from './utils' | ||
| import { delayValue as delay } from './testUtils' | ||
| import { ValidationErrorObject } from './types' | ||
|
|
||
| describe('asyncResultsAnd', () => { | ||
| it('should work well with empty results', async () => { | ||
| const result = await asyncResultsAnd([]) | ||
| expect(isValid(result)).toBe(true) | ||
| expect(isValidationPassed(result)).toBe(true) | ||
| }) | ||
|
|
||
| it('should work well with all-passed results', async () => { | ||
| const result = await asyncResultsAnd([delay(null)]) | ||
| expect(isValid(result)).toBe(true) | ||
| expect(isValidationPassed(result)).toBe(true) | ||
|
|
||
| await asyncResultsAnd([ | ||
| delay(null, 30), | ||
| delay(undefined, 10), | ||
| delay(false, 20) | ||
| ]) | ||
| expect(isValid(result)).toBe(true) | ||
| expect(isValidationPassed(result)).toBe(true) | ||
| }) | ||
|
|
||
| it('should work well with unpassed results', async () => { | ||
|
|
@@ -99,5 +100,45 @@ describe('isErrorObject', () => { | |
| expect(isErrorObject({message: 'msg'})).toBe(true) | ||
| expect(isErrorObject({message: 'msg', extra: 'ext' })).toBe(true) | ||
| expect(isErrorObject(new Error('error msg'))).toBe(true) | ||
Luncher marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| class Foo implements ValidationErrorObject { message = 'mewo' } | ||
| expect(isErrorObject(new Foo())).toBe(true) | ||
|
|
||
| class Bar extends Foo {} | ||
| expect(isErrorObject(new Bar())).toBe(true) | ||
| }) | ||
| }) | ||
|
|
||
| describe('normalizeValidationResult', () => { | ||
| it('normalizeRawError should work well', () => { | ||
| expect(normalizeRawError(false)).toBe(undefined) | ||
| expect(normalizeRawError(null)).toBe(undefined) | ||
| expect(normalizeRawError(undefined)).toBe(undefined) | ||
| expect(normalizeRawError('')).toBe(undefined) | ||
| expect(normalizeRawError('foo')).toBe('foo') | ||
| expect(normalizeRawError({ message: 'mewo' })).toEqual({ message: 'mewo' }) | ||
|
|
||
| class Foo implements ValidationErrorObject { message = 'mewo' } | ||
| const foo = new Foo() | ||
| expect(normalizeRawError(foo)).toBe(foo) | ||
|
|
||
| class Bar extends Foo {} | ||
| const bar = new Bar() | ||
| expect(normalizeRawError(bar)).toBe(bar) | ||
| }) | ||
|
|
||
| it('normalizeError should work well', () => { | ||
| expect(normalizeError(undefined)).toBe(undefined) | ||
| expect(normalizeError('')).toBe('') | ||
|
||
| expect(normalizeError('foo')).toBe('foo') | ||
| expect(normalizeError({ message: 'mewo' })).toBe('mewo') | ||
|
|
||
| class Foo implements ValidationErrorObject { message = 'mewo' } | ||
| const foo = new Foo() | ||
| expect(normalizeError(foo)).toBe('mewo') | ||
|
|
||
| class Bar extends Foo {} | ||
| const bar = new Bar() | ||
| expect(normalizeError(bar)).toBe('mewo') | ||
| }) | ||
| }) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,17 +1,36 @@ | ||
| import { isObservableArray, IObservableArray } from 'mobx' | ||
| import { Validator, ValidationResult, ValidatorReturned, ErrorObject } from './types' | ||
| import { Validator, ValidationResult, ValidatorReturned, ValidationError, ValidationErrorObject, ValidationRawError } from './types' | ||
|
|
||
| export function isErrorObject(err: any): err is ErrorObject { | ||
| export function normalizeRawError(err: ValidationResult): ValidationRawError { | ||
| if (isErrorObject(err)) { | ||
| return err | ||
| } | ||
|
|
||
| if (err === false || err === '' || err === null) { | ||
| // TODO: print an alert? | ||
| return undefined | ||
| } | ||
|
|
||
| return err | ||
| } | ||
|
|
||
| export function normalizeError(rawError: ValidationRawError): ValidationError { | ||
| if (isErrorObject(rawError)) { | ||
| return rawError.message | ||
| } | ||
| return rawError | ||
| } | ||
|
|
||
| export function isErrorObject(err: any): err is ValidationErrorObject { | ||
| return err != null && typeof err === 'object' && 'message' in err | ||
|
||
| } | ||
|
|
||
| export function isPromiseLike(arg: any): arg is Promise<any> { | ||
| return arg != null && typeof arg === 'object' && typeof arg.then === 'function' | ||
| } | ||
|
|
||
| /** If validation result is valid */ | ||
| export function isValid(result: ValidationResult): result is '' | null | undefined | false { | ||
| return !result | ||
| export function isValidationPassed(result: ValidationResult): result is undefined { | ||
|
||
| return normalizeRawError(result) === undefined | ||
| } | ||
|
|
||
| export function asyncResultsAnd(asyncResults: Array<Promise<ValidationResult>>): ValidatorReturned { | ||
|
|
@@ -22,7 +41,7 @@ export function asyncResultsAnd(asyncResults: Array<Promise<ValidationResult>>): | |
| let validResultCount = 0 | ||
| asyncResults.forEach(asyncResult => asyncResult.then(result => { | ||
| // return error if any result is invalid | ||
|
||
| if (!isValid(result)) { | ||
| if (!isValidationPassed(result)) { | ||
| resolve(result) | ||
| return | ||
| } | ||
|
|
@@ -56,7 +75,7 @@ export function applyValidators<TValue>(value: TValue, validators: Validator<TVa | |
| } | ||
|
|
||
| // 任一不通过,则不通过 | ||
| if (!isValid(returned)) { | ||
| if (!isValidationPassed(returned)) { | ||
| return returned | ||
| } | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
在实现
DebouncedState的时候,也应该去通过 overriderawError来实现,而不是 overrideownError?如 #87 (comment) 这里所述,
ownError = normalizeError(rawError)这个逻辑对于DebouncedState也应该是成立的?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
没注意这里有个 override,我看怎么改下。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
031f8a1
done