Add validation to text inputs (#299)
* Add validation to text inputs (#56) * [CI Skip] 0.24.0-beta.3 skip-checks: true * Change imports * Refactor ValidatedInput.tsx to stop cloning elements * Fix linter * Fix lint Co-authored-by: Github Actions <action@github.com>
This commit is contained in:
parent
d20f6e1a38
commit
1af44b4264
@ -32,6 +32,16 @@ describe('Create Account', () => {
|
||||
|
||||
const check = (input: ReactWrapper): unknown => input.simulate('change', { target: { checked: true } });
|
||||
|
||||
const type = async (input: ReactWrapper, value: string): Promise<void> => {
|
||||
input.simulate('change', { target: { value } });
|
||||
await act(flushAllPromises);
|
||||
wrapper.update();
|
||||
};
|
||||
|
||||
const enterName = (name: string): Promise<void> => type(wrapper.find('input'), name);
|
||||
const password = (password: string) => (): Promise<void> => type(wrapper.find('input[type="password"]').first(), password);
|
||||
const repeat = (password: string) => (): Promise<void> => type(wrapper.find('input[type="password"]').last(), password);
|
||||
|
||||
beforeEach(async () => {
|
||||
onActionStub = jest.fn();
|
||||
jest.spyOn(messaging, 'createSeed').mockResolvedValue(exampleAccount);
|
||||
@ -67,11 +77,10 @@ describe('Create Account', () => {
|
||||
});
|
||||
|
||||
describe('Phase 2', () => {
|
||||
const type = (input: ReactWrapper, value: string): unknown => input.simulate('change', { target: { value } });
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
check(wrapper.find('input[type="checkbox"]'));
|
||||
wrapper.find('button').simulate('click');
|
||||
await act(flushAllPromises);
|
||||
});
|
||||
|
||||
it('only account name input is visible at first', () => {
|
||||
@ -81,55 +90,96 @@ describe('Create Account', () => {
|
||||
expect(wrapper.find(Button)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('after typing less than 3 characters into name input, password input is not visible', () => {
|
||||
type(wrapper.find('input'), 'ab');
|
||||
it('input should not be highlighted as error until first interaction', () => {
|
||||
expect(wrapper.find(Input).prop('withError')).toBe(false);
|
||||
});
|
||||
|
||||
it('after typing less than 3 characters into name input, password input is not visible', async () => {
|
||||
await enterName('ab');
|
||||
expect(wrapper.find(Input).prop('withError')).toBe(true);
|
||||
expect(wrapper.find('ErrorMessage').text()).toBe('Account name is too short');
|
||||
expect(wrapper.find(InputWithLabel).find('[data-input-password]')).toHaveLength(0);
|
||||
expect(wrapper.find(InputWithLabel).find('[data-input-repeat-password]')).toHaveLength(0);
|
||||
expect(wrapper.find(Button)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('after typing 3 characters into name input, first password input is visible', () => {
|
||||
type(wrapper.find('input'), 'abc');
|
||||
it('input should keep showing error when something has been typed but then erased', async () => {
|
||||
await enterName('ab');
|
||||
await enterName('');
|
||||
expect(wrapper.find(Input).prop('withError')).toBe(true);
|
||||
});
|
||||
|
||||
it('after typing 3 characters into name input, first password input is visible', async () => {
|
||||
await enterName('abc');
|
||||
expect(wrapper.find(Input).first().prop('withError')).toBe(false);
|
||||
expect(wrapper.find(InputWithLabel).find('[data-input-password]').find(Input)).toHaveLength(1);
|
||||
expect(wrapper.find(InputWithLabel).find('[data-input-repeat-password]')).toHaveLength(0);
|
||||
expect(wrapper.find(Button)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('password shorter than 6 characters should be not valid', () => {
|
||||
type(wrapper.find('input'), 'abc');
|
||||
type(wrapper.find('input[type="password"]'), 'abcde');
|
||||
it('password shorter than 6 characters should be not valid', async () => {
|
||||
await enterName('abc').then(password('abcde'));
|
||||
expect(wrapper.find(InputWithLabel).find('[data-input-password]').find(Input).prop('withError')).toBe(true);
|
||||
expect(wrapper.find('ErrorMessage').text()).toBe('Password is too short');
|
||||
expect(wrapper.find(InputWithLabel).find('[data-input-password]').find(Input)).toHaveLength(1);
|
||||
expect(wrapper.find(InputWithLabel).find('[data-input-repeat-password]')).toHaveLength(0);
|
||||
expect(wrapper.find(Button)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('submit button is not visible until both passwords are equal', () => {
|
||||
type(wrapper.find('input'), 'abc');
|
||||
type(wrapper.find('input[type="password"]').first(), 'abcdef');
|
||||
type(wrapper.find('input[type="password"]').last(), 'abcdeg');
|
||||
it('submit button is not visible until both passwords are equal', async () => {
|
||||
await enterName('abc').then(password('abcdef')).then(repeat('abcdeg'));
|
||||
expect(wrapper.find('ErrorMessage').text()).toBe('Passwords do not match');
|
||||
expect(wrapper.find(InputWithLabel).find('[data-input-repeat-password]').find(Input).prop('withError')).toBe(true);
|
||||
expect(wrapper.find(Button)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('submit button is visible when both passwords are equal', () => {
|
||||
type(wrapper.find('input'), 'abc');
|
||||
type(wrapper.find('input[type="password"]').first(), 'abcdef');
|
||||
type(wrapper.find('input[type="password"]').last(), 'abcdef');
|
||||
it('submit button is visible when both passwords are equal', async () => {
|
||||
await enterName('abc').then(password('abcdef')).then(repeat('abcdef'));
|
||||
expect(wrapper.find('ErrorMessage')).toHaveLength(0);
|
||||
expect(wrapper.find(InputWithLabel).find('[data-input-repeat-password]').find(Input).prop('withError')).toBe(false);
|
||||
expect(wrapper.find(Button)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('saves account with provided name and password', async () => {
|
||||
type(wrapper.find('input'), 'abc');
|
||||
type(wrapper.find('input[type="password"]').first(), 'abcdef');
|
||||
type(wrapper.find('input[type="password"]').last(), 'abcdef');
|
||||
await enterName('abc').then(password('abcdef')).then(repeat('abcdef'));
|
||||
wrapper.find(Button).find('button').simulate('click');
|
||||
await act(flushAllPromises);
|
||||
|
||||
expect(messaging.createAccountSuri).toBeCalledWith('abc', 'abcdef', exampleAccount.seed);
|
||||
await flushAllPromises();
|
||||
expect(onActionStub).toBeCalledWith('/');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Both passwords are equal, but then', () => {
|
||||
beforeEach(async () => {
|
||||
check(wrapper.find('input[type="checkbox"]'));
|
||||
wrapper.find('button').simulate('click');
|
||||
await act(flushAllPromises);
|
||||
await enterName('abc').then(password('abcdef')).then(repeat('abcdef'));
|
||||
});
|
||||
|
||||
it('first password input is cleared - second one and button get hidden', async () => {
|
||||
await type(wrapper.find('input[type="password"]').first(), '');
|
||||
expect(wrapper.find(InputWithLabel).find('[data-input-repeat-password]')).toHaveLength(0);
|
||||
expect(wrapper.find(Button)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('first password changes - button is not visible', async () => {
|
||||
await type(wrapper.find('input[type="password"]').first(), 'aaaaaa');
|
||||
expect(wrapper.find('ErrorMessage').text()).toBe('Passwords do not match');
|
||||
expect(wrapper.find(Button)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('first password changes, then second changes too - button is visible', async () => {
|
||||
await type(wrapper.find('input[type="password"]').first(), 'aaaaaa');
|
||||
await type(wrapper.find('input[type="password"]').last(), 'aaaaaa');
|
||||
expect(wrapper.find(Button)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('second password changes, then first changes too - button is visible', async () => {
|
||||
await type(wrapper.find('input[type="password"]').last(), 'aaaaaa');
|
||||
await type(wrapper.find('input[type="password"]').first(), 'aaaaaa');
|
||||
expect(wrapper.find(Button)).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -2,35 +2,54 @@
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import React, { useContext, useState } from 'react';
|
||||
|
||||
import { ActionContext, Address, Button, ButtonArea, Header, TextAreaWithLabel, VerticalSpace } from '../components';
|
||||
import { createAccountSuri, validateSeed } from '../messaging';
|
||||
import { Name, Password } from '../partials';
|
||||
import React, { useCallback, useContext, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { ActionContext, Address, Button, ButtonArea, Header, TextAreaWithLabel, ValidatedInput, VerticalSpace } from '../components';
|
||||
import { createAccountSuri, validateSeed } from '../messaging';
|
||||
import { Name, Password } from '../partials';
|
||||
import { allOf, isNotShorterThan, Result } from '../validators';
|
||||
|
||||
type Props = {};
|
||||
|
||||
const validate = async (suri: string): Promise<Result<string>> => {
|
||||
try {
|
||||
await validateSeed(suri);
|
||||
|
||||
return Result.ok(suri);
|
||||
} catch (err) {
|
||||
return Result.error(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
const isSeedValid = allOf(
|
||||
isNotShorterThan(1, 'Seed is empty'),
|
||||
validate
|
||||
);
|
||||
|
||||
export default function Import (): React.ReactElement<Props> {
|
||||
const onAction = useContext(ActionContext);
|
||||
const [account, setAccount] = useState<null | { address: string; suri: string }>(null);
|
||||
const [name, setName] = useState<string | null>(null);
|
||||
const [password, setPassword] = useState<string | null>(null);
|
||||
|
||||
const _onChangeSeed = (suri: string): Promise<void> =>
|
||||
validateSeed(suri)
|
||||
.then(setAccount)
|
||||
.catch((): void => setAccount(null));
|
||||
const _onChangeSeed = useCallback(async (suri: string | null): Promise<void> => {
|
||||
if (suri) {
|
||||
setAccount(await validateSeed(suri));
|
||||
} else {
|
||||
setAccount(null);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// FIXME Duplicated between here and Create.tsx
|
||||
const _onCreate = (): void => {
|
||||
const _onCreate = useCallback((): void => {
|
||||
// this should always be the case
|
||||
if (name && password && account) {
|
||||
createAccountSuri(name, password, account.suri)
|
||||
.then((): void => onAction('/'))
|
||||
.catch((error: Error) => console.error(error));
|
||||
}
|
||||
};
|
||||
}, [account, name, onAction, password]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -38,12 +57,14 @@ export default function Import (): React.ReactElement<Props> {
|
||||
showBackArrow
|
||||
text='Import account'
|
||||
/>
|
||||
<SeedInput
|
||||
isError={!account}
|
||||
<ValidatedInput
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
component={SeedInput}
|
||||
isFocused
|
||||
label='existing 12 or 24-word mnemonic seed'
|
||||
onChange={_onChangeSeed}
|
||||
onValidatedChange={_onChangeSeed}
|
||||
rowsCount={2}
|
||||
validator={isSeedValid}
|
||||
/>
|
||||
{account && <Name onChange={setName} />}
|
||||
{account && name && <Password onChange={setPassword} />}
|
||||
|
||||
64
packages/extension-ui/src/components/ValidatedInput.tsx
Normal file
64
packages/extension-ui/src/components/ValidatedInput.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
// Copyright 2019 @polkadot/extension-ui authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
import styled from 'styled-components';
|
||||
import { Result, Validator } from '../validators';
|
||||
|
||||
interface BasicProps {
|
||||
isError?: boolean;
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
}
|
||||
type Props<T extends BasicProps> = T & {
|
||||
className?: string;
|
||||
validator: Validator<string>;
|
||||
component: React.ComponentType<T>;
|
||||
onValidatedChange: (value: string | null) => void;
|
||||
}
|
||||
|
||||
function ValidatedInput<T extends object> ({ className, component: Input, onValidatedChange, validator, ...props }: Props<T>): React.ReactElement<Props<T>> {
|
||||
const [value, setValue] = useState('');
|
||||
const [wasMounted, setWasMounted] = useState(false);
|
||||
const [validationResult, setValidationResult] = useState<Result<string>>(Result.ok(''));
|
||||
|
||||
useEffect(() => {
|
||||
if (!wasMounted) {
|
||||
setWasMounted(true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
(async (): Promise<void> => {
|
||||
const result = await validator(value);
|
||||
|
||||
setValidationResult(result);
|
||||
onValidatedChange(Result.isOk(result) ? value : null);
|
||||
})();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [value, validator, onValidatedChange]);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<Input {...props as T}
|
||||
isError={Result.isError(validationResult)}
|
||||
onChange={setValue}
|
||||
value={value} />
|
||||
{Result.isError(validationResult) && <ErrorMessage>{validationResult.error.errorDescription}</ErrorMessage>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const ErrorMessage = styled.span`
|
||||
display: block;
|
||||
margin-top: -10px;
|
||||
font-size: ${({ theme }): string => theme.labelFontSize};
|
||||
line-height: ${({ theme }): string => theme.labelLineHeight};
|
||||
color: ${({ theme }): string => theme.errorColor};
|
||||
`;
|
||||
|
||||
ErrorMessage.displayName = 'ErrorMessage';
|
||||
|
||||
export default ValidatedInput;
|
||||
@ -27,6 +27,7 @@ export { default as Svg } from './Svg';
|
||||
export { default as Table } from './Table';
|
||||
export { default as TextAreaWithLabel } from './TextAreaWithLabel';
|
||||
export { default as Title } from './Title';
|
||||
export { default as ValidatedInput } from './ValidatedInput';
|
||||
export { default as VerticalSpace } from './VerticalSpace';
|
||||
export { default as View } from './View';
|
||||
export { default as Warning } from './Warning';
|
||||
|
||||
@ -3,8 +3,9 @@
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { InputWithLabel } from '@polkadot/extension-ui/components';
|
||||
import { validateDerivationPath } from '@polkadot/extension-ui/messaging';
|
||||
import { InputWithLabel, ValidatedInput } from '../components';
|
||||
import { Result } from '../validators';
|
||||
|
||||
interface Props {
|
||||
onChange: (derivedAccount: { address: string; suri: string } | null) => void;
|
||||
@ -14,33 +15,31 @@ interface Props {
|
||||
|
||||
function DerivationPath ({ onChange, parentAddress, parentPassword }: Props): React.ReactElement<Props> {
|
||||
const [path, setPath] = useState<string>('');
|
||||
const [isValid, setValid] = useState(false);
|
||||
|
||||
const _onChange = useCallback(
|
||||
async (newPath: string) => {
|
||||
setPath(newPath);
|
||||
const isPathValid = useCallback(async (path: string): Promise<Result<string>> => {
|
||||
try {
|
||||
await validateDerivationPath(parentAddress, path, parentPassword);
|
||||
|
||||
try {
|
||||
onChange(await validateDerivationPath(parentAddress, newPath, parentPassword));
|
||||
setValid(true);
|
||||
} catch (err) {
|
||||
onChange(null);
|
||||
setValid(false);
|
||||
}
|
||||
},
|
||||
[parentAddress, onChange, parentPassword]
|
||||
);
|
||||
return Result.ok(path);
|
||||
} catch (error) {
|
||||
return Result.error('Invalid derivation path');
|
||||
}
|
||||
}, [parentAddress, parentPassword]);
|
||||
|
||||
const _onChange = useCallback(async (newPath: string | null) => {
|
||||
newPath && setPath(newPath);
|
||||
onChange(newPath ? await validateDerivationPath(parentAddress, newPath, parentPassword) : null);
|
||||
}, [parentAddress, onChange, parentPassword]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<InputWithLabel
|
||||
isError={!isValid}
|
||||
label='Derivation path'
|
||||
onChange={_onChange}
|
||||
placeholder='//hard/soft'
|
||||
value={path}
|
||||
/>
|
||||
</div>
|
||||
<ValidatedInput
|
||||
component={InputWithLabel}
|
||||
label='Derivation path'
|
||||
onValidatedChange={_onChange}
|
||||
placeholder='//hard/soft'
|
||||
validator={isPathValid}
|
||||
value={path}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -2,50 +2,39 @@
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import React, { useContext } from 'react';
|
||||
|
||||
import { AccountContext, InputWithLabel } from '../components';
|
||||
import { AccountContext, InputWithLabel, ValidatedInput } from '../components';
|
||||
import { isNotShorterThan } from '../validators';
|
||||
|
||||
interface Props {
|
||||
address?: string;
|
||||
className?: string;
|
||||
defaultValue?: string | null;
|
||||
isFocused?: boolean;
|
||||
label?: string;
|
||||
onBlur?: () => void;
|
||||
onChange: (name: string | null) => void;
|
||||
}
|
||||
|
||||
const MIN_LENGTH = 3;
|
||||
const isNameValid = isNotShorterThan(3, 'Account name is too short');
|
||||
|
||||
export default function Name ({ address, className, defaultValue, isFocused, label = 'A descriptive name for your account', onBlur, onChange }: Props): React.ReactElement<Props> {
|
||||
export default function Name ({ address, className, isFocused, label = 'A descriptive name for your account', onBlur, onChange }: Props): React.ReactElement<Props> {
|
||||
const accounts = useContext(AccountContext);
|
||||
const [name, setName] = useState('');
|
||||
const account = accounts.find((account): boolean => account.address === address);
|
||||
const startValue = (account && account.name) || defaultValue;
|
||||
const isError = !name && startValue
|
||||
? false
|
||||
: (name.length < MIN_LENGTH);
|
||||
|
||||
useEffect((): void => {
|
||||
onChange(
|
||||
name && (name.length >= MIN_LENGTH)
|
||||
? name
|
||||
: null
|
||||
);
|
||||
}, [name, onChange]);
|
||||
const startValue = account && account.name;
|
||||
|
||||
return (
|
||||
<InputWithLabel
|
||||
<ValidatedInput
|
||||
className={className}
|
||||
component={InputWithLabel}
|
||||
data-input-name
|
||||
defaultValue={startValue}
|
||||
isError={isError}
|
||||
isFocused={isFocused}
|
||||
label={label}
|
||||
onBlur={onBlur}
|
||||
onChange={setName}
|
||||
onValidatedChange={onChange}
|
||||
type='text'
|
||||
validator={isNameValid}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -2,9 +2,10 @@
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
import { InputWithLabel } from '../components';
|
||||
import { InputWithLabel, ValidatedInput } from '../components';
|
||||
import { allOf, isNotShorterThan, isSameAs, Validator } from '../validators';
|
||||
|
||||
interface Props {
|
||||
isFocussed?: boolean;
|
||||
@ -12,36 +13,39 @@ interface Props {
|
||||
}
|
||||
|
||||
const MIN_LENGTH = 6;
|
||||
const isFirstPasswordValid = isNotShorterThan(MIN_LENGTH, 'Password is too short');
|
||||
const isSecondPasswordValid = (firstPassword: string): Validator<string> => allOf(
|
||||
isNotShorterThan(MIN_LENGTH, 'Password is too short'),
|
||||
isSameAs(firstPassword, 'Passwords do not match')
|
||||
);
|
||||
|
||||
export default function Password ({ isFocussed, onChange }: Props): React.ReactElement<Props> {
|
||||
const [pass1, setPass1] = useState('');
|
||||
const [pass2, setPass2] = useState('');
|
||||
const [pass1, setPass1] = useState<string | null>(null);
|
||||
const [pass2, setPass2] = useState<string | null>(null);
|
||||
|
||||
useEffect((): void => {
|
||||
onChange(
|
||||
(pass1 && pass2 && (pass1.length >= MIN_LENGTH) && (pass1 === pass2))
|
||||
? pass1
|
||||
: null
|
||||
);
|
||||
onChange(pass1 && pass2 ? pass1 : null);
|
||||
}, [onChange, pass1, pass2]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<InputWithLabel
|
||||
<ValidatedInput
|
||||
component={InputWithLabel}
|
||||
data-input-password
|
||||
isError={pass1.length < MIN_LENGTH}
|
||||
isFocused={isFocussed}
|
||||
label='A new password for this account'
|
||||
onChange={setPass1}
|
||||
onValidatedChange={setPass1}
|
||||
type='password'
|
||||
validator={isFirstPasswordValid}
|
||||
/>
|
||||
{(pass1.length >= MIN_LENGTH) && (
|
||||
<InputWithLabel
|
||||
{pass1 && (
|
||||
<ValidatedInput
|
||||
component={InputWithLabel}
|
||||
data-input-repeat-password
|
||||
isError={pass1 !== pass2}
|
||||
label='Repeat password for verification'
|
||||
onChange={setPass2}
|
||||
onValidatedChange={setPass2}
|
||||
type='password'
|
||||
validator={isSecondPasswordValid(pass1)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
50
packages/extension-ui/src/validators.ts
Normal file
50
packages/extension-ui/src/validators.ts
Normal file
@ -0,0 +1,50 @@
|
||||
// Copyright 2019 @polkadot/extension-ui authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
interface InputError {
|
||||
errorDescription: string;
|
||||
}
|
||||
|
||||
export type Result<T> = { error: InputError } | { ok: T }
|
||||
export const Result = {
|
||||
error: <T>(errorDescription: string): Result<T> => ({ error: { errorDescription } }),
|
||||
isError<T> (value: Result<T>): value is ({ error: InputError }) {
|
||||
return Object.hasOwnProperty.call(value, 'error');
|
||||
},
|
||||
isOk<T> (value: Result<T>): value is ({ ok: T }) {
|
||||
return Object.hasOwnProperty.call(value, 'ok');
|
||||
},
|
||||
ok: <T>(ok: T): Result<T> => ({ ok })
|
||||
};
|
||||
|
||||
export declare type Validator<T> = (value: T) => Result<T> | Promise<Result<T>>;
|
||||
|
||||
export const allOf = <T>(...validators: Validator<T>[]): Validator<T> => async (value: T): Promise<Result<T>> => {
|
||||
for (const validator of validators) {
|
||||
const validationResult = await validator(value);
|
||||
|
||||
if (Result.isError(validationResult)) {
|
||||
return validationResult;
|
||||
}
|
||||
}
|
||||
|
||||
return Result.ok(value);
|
||||
};
|
||||
|
||||
export const isNotShorterThan = (minLength: number, errorText: string): Validator<string> =>
|
||||
(value: string): Result<string> => {
|
||||
if (value.length < minLength) {
|
||||
return Result.error(errorText);
|
||||
}
|
||||
|
||||
return Result.ok(value);
|
||||
};
|
||||
|
||||
export const isSameAs = <T>(expectedValue: T, errorText: string): Validator<T> => (value: T): Result<T> => {
|
||||
if (value !== expectedValue) {
|
||||
return Result.error(errorText);
|
||||
}
|
||||
|
||||
return Result.ok(value);
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user