on master help sign in
fix: clean up validator logic, so fresh so clean
commite82c62856bd0c2324fc1f8b24c64d1f866038f2b authorMatt Condon (shrugs) <mattgcondon@gmail.com> dateTue 6 Aug 2019, 11:18 (7 years ago) parent6526dd7
8 files changed+108−287
src/form/validators.js+39−27 src/lib/validators.js+56−247 src/views/Activate/ActivateCode.js+2−2 src/views/Activate/PassportVerify.js+2−2 src/views/Admin/Reticket/ReticketVerify.js+2−2 src/views/CreateGalaxy.js+1−1 src/views/IssueChild.js+1−1 src/views/Login/Ticket.js+5−5
src/form/validators.js+39−27
@@ -2,8 +2,7 @@ import { some } from 'lodash';
22
33import {
44 validateNotEmpty,
5− validateTicket,
6− kDefaultValidator,
5+ validatePatq,
76 validateMnemonic,
87 validatePoint,
98 validateMaximumPatpByteLength,
@@ -13,19 +12,21 @@ import {
1312 validateEthereumAddress,
1413 validateGreaterThan,
1514 validateEmail,
15+ validateNotNullAddress,
1616} from 'lib/validators';
17−import { compose } from 'lib/lib';
1817
19−const buildValidator = (
20− validators = [],
21− fn = () => undefined
22−) => async value => {
23− return (
24− compose(
25− ...validators,
26− kDefaultValidator
27− )(value).error || (await fn(value))
28− );
18+// iterate over validators, exiting early if there's an error
19+const buildValidator = (validators = []) => async value => {
20+ for (const validator of validators) {
21+ try {
22+ const error = await validator(value);
23+ if (error) {
24+ return error;
25+ }
26+ } catch (error) {
27+ return error.message;
28+ }
29+ }
2930};
3031
3132// error object has errors if some of its fields are
@@ -34,10 +35,10 @@ const buildValidator = (
3435export const hasErrors = iter =>
3536 some(iter, v => (Array.isArray(v) ? hasErrors(v) : v !== undefined));
3637
37−export const buildTicketValidator = (validators = []) =>
38− buildValidator([...validators, validateTicket, validateNotEmpty]);
38+export const buildPatqValidator = (validators = []) =>
39+ buildValidator([validateNotEmpty, validatePatq, ...validators]);
3940export const buildMnemonicValidator = () =>
40− buildValidator([validateMnemonic, validateNotEmpty]);
41+ buildValidator([validateNotEmpty, validateMnemonic]);
4142export const buildCheckboxValidator = mustBe =>
4243 buildValidator([
4344 validateOneOf(mustBe !== undefined ? [mustBe] : [true, false]),
@@ -45,34 +46,40 @@ export const buildCheckboxValidator = mustBe =>
4546export const buildPassphraseValidator = () => buildValidator([]);
4647// TODO: validate hdpath format
4748export const buildHdPathValidator = () => buildValidator([validateNotEmpty]);
48−export const buildPointValidator = (size = 4, validate) =>
49− buildValidator(
50− [validatePoint, validateMaximumPatpByteLength(size), validateNotEmpty],
51− validate
52− );
49+export const buildPointValidator = (size = 4, validators = []) =>
50+ buildValidator([
51+ validateNotEmpty,
52+ validateMaximumPatpByteLength(size),
53+ validatePoint,
54+ ...validators,
55+ ]);
5356export const buildSelectValidator = options =>
5457 buildValidator([validateOneOf(options.map(option => option.value))]);
5558export const buildHexValidator = length =>
5659 buildValidator([
57− validateHexLength(length),
58− validateHexString,
5960 validateNotEmpty,
61+ validateHexString,
62+ validateHexLength(length),
6063 ]);
6164export const buildUploadValidator = () => buildValidator([validateNotEmpty]);
6265export const buildAddressValidator = () =>
6366 buildValidator([
64− validateEthereumAddress,
65− validateHexLength(40),
66− validateHexString,
6767 validateNotEmpty,
68+ validateHexString,
69+ validateHexLength(40),
70+ validateNotNullAddress,
71+ validateEthereumAddress,
6872 ]);
6973export const buildNumberValidator = (min = 0) =>
7074 buildValidator([validateGreaterThan(min)]);
7175export const buildEmailValidator = () =>
72− buildValidator([validateEmail, validateNotEmpty]);
76+ buildValidator([validateNotEmpty, validateEmail]);
7377
7478// the default form validator just returns field-level validations
7579const kDefaultFormValidator = (values, errors) => errors;
80+
81+// the form validator is the composition of all of the field validators
82+// plus an additional form validator function
7683export const composeValidator = (
7784 fieldValidators = {},
7885 formValidator = kDefaultFormValidator
@@ -83,6 +90,7 @@ export const composeValidator = (
8390 fieldValidators[name](value)
8491 );
8592
93+ // async reduce errors per-field into an errors object
8694 const fieldLevelValidator = async values => {
8795 const errors = await Promise.all(
8896 names.map((name, i) => fieldLevelValidators[i](values[name]))
@@ -97,8 +105,12 @@ export const composeValidator = (
97105 );
98106 };
99107
108+ // final-form `validate` function
100109 return async values => {
110+ // ask for field-level errors
101111 const errors = await fieldLevelValidator(values);
112+ // pass the current values and their validity to the form-level validator
113+ // that can implement conditional logic and more complex validations
102114 return await formValidator(values, errors);
103115 };
104116};
src/lib/validators.js+56−247
@@ -1,6 +1,6 @@
11import * as bip39 from 'bip39';
22import * as ob from 'urbit-ob';
3−import { identity, includes } from 'lodash';
3+import { includes } from 'lodash';
44
55import { isValidAddress, ETH_ZERO_ADDR, ETH_ZERO_ADDR_SHORT } from './wallet';
66import patp2dec from './patp2dec';
@@ -14,274 +14,83 @@ import { MIN_GALAXY, MAX_GALAXY } from './constants';
1414// via: https://emailregex.com/
1515const emailRegExp = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
1616
17−// is this 64 hex chars?
18−const plain64CharHexValue = /[0-9A-Fa-f]{64}/;
19−
2017// is this only hex values with a 0x prefix?
2118const isHexString = /0x[0-9A-Fa-f]/;
2219
23−// Wraps single validation functions in a controlled and predictable way.
24−export const simpleValidatorWrapper = ({
25− prevMessage,
26− transform = identity,
27− validator,
28− error,
29−}) => {
30− // If a previous validation has already failed, skip this validation and
31− // return the prev message to the next stage in the validation function chain.
32− // Failed validations should drop all the way down the chain and drop out of
33− // the output.
34− if (!prevMessage.pass) {
35− return prevMessage;
36− }
37−
38− // Run the validator and return the result.
39− const pass = validator(prevMessage.data);
40− const data = pass ? transform(prevMessage.data) : prevMessage.data;
41−
42− return newMessage(data, pass, !pass && error);
43−};
44−
45−// Validation message
46−// {
47−// data: ...
48−// pass: ...
49−// error: ...
50−// }
51−// Creates a new validation message in a uniform way.
52−const newMessage = (data, pass, error) => ({
53− // The input data
54− data,
55− // Has the data passed validation?
56− pass,
57− // If data has failed a validator, the error message goes here.
58− error,
59−});
60−
61−// A validator that always passes.
62−export const kDefaultValidator = data => newMessage(data, true, null);
63−
6420// Validates a bip39 mnemonic
65−export const validateMnemonic = m =>
66− simpleValidatorWrapper({
67− prevMessage: m,
68− validator: d => bip39.validateMnemonic(d),
69− error: 'This is not a valid mnemonic.',
70− });
21+export const validateMnemonic = v =>
22+ !bip39.validateMnemonic(v) && 'This is not a valid mnemonic.';
7123
7224// Checks an empty field
73−export const validateNotEmpty = m =>
74− simpleValidatorWrapper({
75− prevMessage: m,
76− validator: d => {
77− try {
78− return d.length > 0;
79− } catch {
80− return false;
81− }
82− },
83− error: 'This field is required.',
84− });
25+export const validateNotEmpty = v =>
26+ v.length === 0 && 'This field is required.';
8527
8628// Checks if a patp is a valid galaxy
87−export const validateGalaxy = m =>
88− simpleValidatorWrapper({
89− prevMessage: m,
90− validator: d => {
91− let point;
92− try {
93− point = patp2dec(d);
94− return point >= MIN_GALAXY && point <= MAX_GALAXY;
95− } catch (e) {
96− return false;
97− }
98− },
99− error: 'This is not a valid galaxy.',
100− });
101−
102−export const validatePoint = m =>
103− simpleValidatorWrapper({
104− prevMessage: m,
105− validator: d => {
106− try {
107− return ob.isValidPatp(d);
108− } catch (e) {
109− return false;
110− }
111− },
112− error: 'This is not a valid point.',
113− });
114−
115−export const validateTicket = m =>
116− simpleValidatorWrapper({
117− prevMessage: m,
118− validator: d => {
119− try {
120− return ob.isValidPatq(d);
121− } catch (e) {
122− return false;
123− }
124− },
125− error: 'This is not a valid ticket.',
126− });
127−
128−export const validateShard = m =>
129− simpleValidatorWrapper({
130− prevMessage: m,
131− validator: d => {
132− try {
133− return ob.isValidPatq(d);
134− } catch (e) {
135− return false;
136− }
137− },
138− error: 'This is not a valid shard.',
139− });
140−
141−export const validateOneOf = (options = []) => m =>
142− simpleValidatorWrapper({
143− prevMessage: m,
144− validator: d => includes(options, d),
145− error: `Is not a valid option.`,
146− });
147−
148−export const validateHexString = m =>
149− simpleValidatorWrapper({
150− prevMessage: m,
151− validator: d => isHexString.test(d),
152− error: 'This is not a valid hex string.',
153− });
154−
155−// @deprecate
156−export const validateNetworkKey = m =>
157− simpleValidatorWrapper({
158− prevMessage: m,
159− validator: d => plain64CharHexValue.test(d),
160− error: 'This is not a valid network key.',
161− });
29+export const validateGalaxy = v => {
30+ try {
31+ const point = patp2dec(v);
32+ const isValidGalaxy = point >= MIN_GALAXY && point <= MAX_GALAXY;
33+ if (!isValidGalaxy) {
34+ throw new Error();
35+ }
36+ } catch {
37+ return 'This is not a valid galaxy.';
38+ }
39+};
16240
163−// @deprecate
164−export const validateNetworkSeed = m =>
165− simpleValidatorWrapper({
166− prevMessage: m,
167− validator: d => plain64CharHexValue.test(d),
168− error: 'This is not a valid network seed.',
169− });
41+export const validatePoint = v => {
42+ try {
43+ if (!ob.isValidPatp(v)) {
44+ throw new Error();
45+ }
46+ } catch {
47+ return 'This is not a valid point.';
48+ }
49+};
17050
171−// Checks if a string is a valid ethereum address
172−export const validateEthereumAddress = m =>
173− simpleValidatorWrapper({
174− prevMessage: m,
175− validator: d => isValidAddress(d),
176− error: 'This is not a valid Ethereum address.',
177− });
51+export const validatePatq = v => {
52+ try {
53+ if (!ob.isValidPatq(v)) {
54+ throw new Error();
55+ }
56+ } catch {
57+ return 'This is not a valid ticket.';
58+ }
59+};
17860
179−export const validateEmail = m =>
180− simpleValidatorWrapper({
181− prevMessage: m,
182− validator: d => emailRegExp.test(d),
183− error: 'This is not a valid email address.',
184− });
61+export const validateOneOf = (options = []) => v =>
62+ !includes(options, v) && 'Is not a valid option.';
18563
186−export const validateInt = m =>
187− simpleValidatorWrapper({
188− prevMessage: m,
189− transform: d => parseInt(d, 10),
190− validator: d => {
191− try {
192− parseInt(d, 10);
193− return;
194− } catch {
195− return false;
196− }
197− },
198− error: 'This is not a valid number.',
199− });
64+export const validateHexString = v =>
65+ !isHexString.test(v) && 'This is not a valid hex string.';
20066
201−export const validateExactly = (value, error) => m =>
202− simpleValidatorWrapper({
203− prevMessage: m,
204− validator: d => d === value,
205− error,
206− });
67+export const validateEthereumAddress = v =>
68+ !isValidAddress(v) && 'This is not a valid Ethereum address.';
20769
208−export const validateNotAny = (values = []) => m =>
209− simpleValidatorWrapper({
210− prevMessage: m,
211− validator: d => !values.includes(d),
212− error: `Cannot be ${m.data}.`,
213− });
70+export const validateEmail = v =>
71+ !emailRegExp.test(v) && 'This is not a valid email address.';
21472
215−export const validateLength = l => m =>
216− simpleValidatorWrapper({
217− prevMessage: m,
218− validator: d => {
219− try {
220− return d.length === l;
221− } catch {
222− return false;
223− }
224− },
225− error: `Must be exactly ${l} characters.`,
226− });
73+export const validateExactly = (value, error) => v => v !== value && error;
22774
228−export const validateHexLength = l => m =>
229− simpleValidatorWrapper({
230− prevMessage: m,
231− validator: d => {
232− try {
233− return d.length === l + 2;
234− } catch {
235− return false;
236− }
237− },
238− error: `Must be exactly ${l} hex characters.`,
239− });
75+export const validateNotAny = (values = []) => v =>
76+ values.includes(v) && `Cannot be ${v}.`;
24077
241−export const validateMaximumLength = l => m =>
242− simpleValidatorWrapper({
243− prevMessage: m,
244− validator: d => {
245− try {
246− return d.length <= l;
247− } catch {
248− return false;
249− }
250− },
251− error: `Must be ${l} characters or fewer.`,
252− });
78+export const validateLength = l => v =>
79+ v.length !== l && `Must be exactly ${l} characters.`;
25380
254−export const validateGreaterThan = l => m =>
255− simpleValidatorWrapper({
256− prevMessage: m,
257− validator: d => d > l,
258− error: `Must be at least ${l}.`,
259− });
81+export const validateHexLength = l => v =>
82+ v.length !== l + 2 && `Must be exactly ${l} hex characters.`;
26083
261−export const validateInSet = (set, error) => m =>
262− simpleValidatorWrapper({
263− prevMessage: m,
264− validator: d => set.has(d),
265− error,
266− });
84+export const validateMaximumLength = l => v =>
85+ v.length > l && `Must be ${l} characters or fewer.`;
26786
268−export const validateNameInNumberSet = (set, error) => m =>
269− simpleValidatorWrapper({
270− prevMessage: m,
271− validator: p => {
272− const num = patp2dec(p);
273− return set.has(num);
274− },
275− error,
276− });
87+export const validateGreaterThan = l => v =>
88+ !(v > l) && `Must be at least ${l}.`;
27789
278−export const validatePatpByteLength = byteLength => {
279− return validateLength(patpStringLength(byteLength));
280−};
90+export const validateInSet = (set, error) => v => !set.has(v) && error;
28191
282−export const validateMaximumPatpByteLength = byteLength => {
283− return validateMaximumLength(patpStringLength(byteLength));
284−};
92+export const validateMaximumPatpByteLength = byteLength =>
93+ validateMaximumLength(patpStringLength(byteLength));
28594
28695export const validateNotNullAddress = validateNotAny([
28796 ETH_ZERO_ADDR,
src/views/Activate/ActivateCode.js+2−2
@@ -25,7 +25,7 @@ import useHasDisclaimed from 'lib/useHasDisclaimed';
2525import BridgeForm from 'form/BridgeForm';
2626import SubmitButton from 'form/SubmitButton';
2727import { TicketInput } from 'form/Inputs';
28−import { composeValidator, buildTicketValidator } from 'form/validators';
28+import { composeValidator, buildPatqValidator } from 'form/validators';
2929import FormError from 'form/FormError';
3030import { FORM_ERROR } from 'final-form';
3131
@@ -56,7 +56,7 @@ export default function ActivateCode() {
5656 }, [names, push, hasDisclaimed]);
5757
5858 const validate = useMemo(
59− () => composeValidator({ ticket: buildTicketValidator() }),
59+ () => composeValidator({ ticket: buildPatqValidator() }),
6060 []
6161 );
6262
src/views/Activate/PassportVerify.js+2−2
@@ -8,7 +8,7 @@ import { isDevelopment } from 'lib/flags';
88
99import SubmitButton from 'form/SubmitButton';
1010import { TicketInput } from 'form/Inputs';
11−import { composeValidator, buildTicketValidator } from 'form/validators';
11+import { composeValidator, buildPatqValidator } from 'form/validators';
1212import BridgeForm from 'form/BridgeForm';
1313
1414import { useActivateFlow } from './ActivateFlow';
@@ -26,7 +26,7 @@ export default function PassportVerify({ className }) {
2626 const validate = useMemo(
2727 () =>
2828 composeValidator({
29− ticket: buildTicketValidator([
29+ ticket: buildPatqValidator([
3030 validateExactly(ticket, 'Does not match expected master ticket.'),
3131 ]),
3232 }),
src/views/Admin/Reticket/ReticketVerify.js+2−2
@@ -7,7 +7,7 @@ import { isDevelopment } from 'lib/flags';
77
88import BridgeForm from 'form/BridgeForm';
99import { TicketInput } from 'form/Inputs';
10−import { composeValidator, buildTicketValidator } from 'form/validators';
10+import { composeValidator, buildPatqValidator } from 'form/validators';
1111import SubmitButton from 'form/SubmitButton';
1212import FormError from 'form/FormError';
1313
@@ -20,7 +20,7 @@ export default function ReticketVerify({ newWallet }) {
2020 const validate = useMemo(
2121 () =>
2222 composeValidator({
23− ticket: buildTicketValidator([
23+ ticket: buildPatqValidator([
2424 validateExactly(ticket, 'Does not match expected master ticket.'),
2525 ]),
2626 }),
src/views/CreateGalaxy.js+1−1
@@ -77,7 +77,7 @@ export default function CreateGalaxy() {
7777 const validate = useMemo(
7878 () =>
7979 composeValidator({
80− galaxyName: buildPointValidator(1, validateGalaxy),
80+ galaxyName: buildPointValidator(1, [validateGalaxy]),
8181 owner: buildAddressValidator(),
8282 }),
8383 [validateGalaxy]
src/views/IssueChild.js+1−1
@@ -96,7 +96,7 @@ export default function IssueChild() {
9696 const validate = useMemo(
9797 () =>
9898 composeValidator({
99− point: buildPointValidator(4, validatePoint),
99+ point: buildPointValidator(4, [validatePoint]),
100100 owner: buildAddressValidator(),
101101 }),
102102 [validatePoint]
src/views/Login/Ticket.js+5−5
@@ -23,7 +23,7 @@ import { TicketInput, PassphraseInput, PointInput } from 'form/Inputs';
2323import {
2424 composeValidator,
2525 buildCheckboxValidator,
26− buildTicketValidator,
26+ buildPatqValidator,
2727 buildPassphraseValidator,
2828 buildPointValidator,
2929} from 'form/validators';
@@ -112,10 +112,10 @@ export default function Ticket({ className, goHome }) {
112112 usePassphrase: buildCheckboxValidator(),
113113 useShards: buildCheckboxValidator(),
114114 point: buildPointValidator(4),
115− ticket: buildTicketValidator(),
116− shard1: buildTicketValidator(),
117− shard2: buildTicketValidator(),
118− shard3: buildTicketValidator(),
115+ ticket: buildPatqValidator(),
116+ shard1: buildPatqValidator(),
117+ shard2: buildPatqValidator(),
118+ shard3: buildPatqValidator(),
119119 passphrase: buildPassphraseValidator(),
120120 },
121121 validateForm