on master help sign in
fix: clean up validator logic, so fresh so clean
e82c628Matt Condon (shrugs)
src/form/validators.js+39−27
@@ -2,8 +2,7 @@ import { some } from 'lodash';
2
2
3import {
3import {
4 validateNotEmpty,
4 validateNotEmpty,
5− validateTicket,
5+ validatePatq,
6− kDefaultValidator,
7 validateMnemonic,
6 validateMnemonic,
8 validatePoint,
7 validatePoint,
9 validateMaximumPatpByteLength,
8 validateMaximumPatpByteLength,
@@ -13,19 +12,21 @@ import {
13 validateEthereumAddress,
12 validateEthereumAddress,
14 validateGreaterThan,
13 validateGreaterThan,
15 validateEmail,
14 validateEmail,
15+ validateNotNullAddress,
16} from 'lib/validators';
16} from 'lib/validators';
17−import { compose } from 'lib/lib';
18
17
18+// iterate over validators, exiting early if there's an error
19−const buildValidator = (
19+const buildValidator = (validators = []) => async value => {
20− validators = [],
20+ for (const validator of validators) {
21− fn = () => undefined
21+ try {
22−) => async value => {
23− return (
24− compose(
25− ...validators,
26− kDefaultValidator
27− )(value).error || (await fn(value))
22+ const error = await validator(value);
28− );
23+ if (error) {
24+ return error;
25+ }
26+ } catch (error) {
27+ return error.message;
28+ }
29+ }
29};
30};
30
31
31// error object has errors if some of its fields are
32// error object has errors if some of its fields are
@@ -34,10 +35,10 @@ const buildValidator = (
34export const hasErrors = iter =>
35export const hasErrors = iter =>
35 some(iter, v => (Array.isArray(v) ? hasErrors(v) : v !== undefined));
36 some(iter, v => (Array.isArray(v) ? hasErrors(v) : v !== undefined));
36
37
37−export const buildTicketValidator = (validators = []) =>
38+export const buildPatqValidator = (validators = []) =>
38− buildValidator([...validators, validateTicket, validateNotEmpty]);
39+ buildValidator([validateNotEmpty, validatePatq, ...validators]);
39export const buildMnemonicValidator = () =>
40export const buildMnemonicValidator = () =>
40− buildValidator([validateMnemonic, validateNotEmpty]);
41+ buildValidator([validateNotEmpty, validateMnemonic]);
41export const buildCheckboxValidator = mustBe =>
42export const buildCheckboxValidator = mustBe =>
42 buildValidator([
43 buildValidator([
43 validateOneOf(mustBe !== undefined ? [mustBe] : [true, false]),
44 validateOneOf(mustBe !== undefined ? [mustBe] : [true, false]),
@@ -45,34 +46,40 @@ export const buildCheckboxValidator = mustBe =>
45export const buildPassphraseValidator = () => buildValidator([]);
46export const buildPassphraseValidator = () => buildValidator([]);
46// TODO: validate hdpath format
47// TODO: validate hdpath format
47export const buildHdPathValidator = () => buildValidator([validateNotEmpty]);
48export const buildHdPathValidator = () => buildValidator([validateNotEmpty]);
48−export const buildPointValidator = (size = 4, validate) =>
49+export const buildPointValidator = (size = 4, validators = []) =>
49− buildValidator(
50+ buildValidator([
50− [validatePoint, validateMaximumPatpByteLength(size), validateNotEmpty],
51+ validateNotEmpty,
51− validate
52+ validateMaximumPatpByteLength(size),
53+ validatePoint,
54+ ...validators,
52− );
55+ ]);
53export const buildSelectValidator = options =>
56export const buildSelectValidator = options =>
54 buildValidator([validateOneOf(options.map(option => option.value))]);
57 buildValidator([validateOneOf(options.map(option => option.value))]);
55export const buildHexValidator = length =>
58export const buildHexValidator = length =>
56 buildValidator([
59 buildValidator([
57− validateHexLength(length),
58− validateHexString,
59 validateNotEmpty,
60 validateNotEmpty,
61+ validateHexString,
62+ validateHexLength(length),
60 ]);
63 ]);
61export const buildUploadValidator = () => buildValidator([validateNotEmpty]);
64export const buildUploadValidator = () => buildValidator([validateNotEmpty]);
62export const buildAddressValidator = () =>
65export const buildAddressValidator = () =>
63 buildValidator([
66 buildValidator([
64− validateEthereumAddress,
65− validateHexLength(40),
66− validateHexString,
67 validateNotEmpty,
67 validateNotEmpty,
68+ validateHexString,
69+ validateHexLength(40),
70+ validateNotNullAddress,
71+ validateEthereumAddress,
68 ]);
72 ]);
69export const buildNumberValidator = (min = 0) =>
73export const buildNumberValidator = (min = 0) =>
70 buildValidator([validateGreaterThan(min)]);
74 buildValidator([validateGreaterThan(min)]);
71export const buildEmailValidator = () =>
75export const buildEmailValidator = () =>
72− buildValidator([validateEmail, validateNotEmpty]);
76+ buildValidator([validateNotEmpty, validateEmail]);
73
77
74// the default form validator just returns field-level validations
78// the default form validator just returns field-level validations
75const kDefaultFormValidator = (values, errors) => errors;
79const 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
76export const composeValidator = (
83export const composeValidator = (
77 fieldValidators = {},
84 fieldValidators = {},
78 formValidator = kDefaultFormValidator
85 formValidator = kDefaultFormValidator
@@ -83,6 +90,7 @@ export const composeValidator = (
83 fieldValidators[name](value)
90 fieldValidators[name](value)
84 );
91 );
85
92
93+ // async reduce errors per-field into an errors object
86 const fieldLevelValidator = async values => {
94 const fieldLevelValidator = async values => {
87 const errors = await Promise.all(
95 const errors = await Promise.all(
88 names.map((name, i) => fieldLevelValidators[i](values[name]))
96 names.map((name, i) => fieldLevelValidators[i](values[name]))
@@ -97,8 +105,12 @@ export const composeValidator = (
97 );
105 );
98 };
106 };
99
107
108+ // final-form `validate` function
100 return async values => {
109 return async values => {
110+ // ask for field-level errors
101 const errors = await fieldLevelValidator(values);
111 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
102 return await formValidator(values, errors);
114 return await formValidator(values, errors);
103 };
115 };
104};
116};
src/lib/validators.js+56−247
@@ -1,6 +1,6 @@
1import * as bip39 from 'bip39';
1import * as bip39 from 'bip39';
2import * as ob from 'urbit-ob';
2import * as ob from 'urbit-ob';
3−import { identity, includes } from 'lodash';
3+import { includes } from 'lodash';
4
4
5import { isValidAddress, ETH_ZERO_ADDR, ETH_ZERO_ADDR_SHORT } from './wallet';
5import { isValidAddress, ETH_ZERO_ADDR, ETH_ZERO_ADDR_SHORT } from './wallet';
6import patp2dec from './patp2dec';
6import patp2dec from './patp2dec';
@@ -14,274 +14,83 @@ import { MIN_GALAXY, MAX_GALAXY } from './constants';
14// via: https://emailregex.com/
14// via: https://emailregex.com/
15const 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,}))$/;
15const 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,}))$/;
16
16
17−// is this 64 hex chars?
18−const plain64CharHexValue = /[0-9A-Fa-f]{64}/;
19−
20// is this only hex values with a 0x prefix?
17// is this only hex values with a 0x prefix?
21const isHexString = /0x[0-9A-Fa-f]/;
18const isHexString = /0x[0-9A-Fa-f]/;
22
19
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−
64// Validates a bip39 mnemonic
20// Validates a bip39 mnemonic
65−export const validateMnemonic = m =>
21+export const validateMnemonic = v =>
66− simpleValidatorWrapper({
67− prevMessage: m,
68− validator: d => bip39.validateMnemonic(d),
69− error: 'This is not a valid mnemonic.',
22+ !bip39.validateMnemonic(v) && 'This is not a valid mnemonic.';
70− });
71
23
72// Checks an empty field
24// Checks an empty field
73−export const validateNotEmpty = m =>
25+export const validateNotEmpty = v =>
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.',
26+ v.length === 0 && 'This field is required.';
84− });
85
27
86// Checks if a patp is a valid galaxy
28// Checks if a patp is a valid galaxy
87−export const validateGalaxy = m =>
29+export const validateGalaxy = v => {
88− simpleValidatorWrapper({
89− prevMessage: m,
90− validator: d => {
91− let point;
92− try {
30+ try {
93− point = patp2dec(d);
31+ const point = patp2dec(v);
94− return point >= MIN_GALAXY && point <= MAX_GALAXY;
32+ const isValidGalaxy = point >= MIN_GALAXY && point <= MAX_GALAXY;
95− } catch (e) {
33+ if (!isValidGalaxy) {
96− return false;
34+ throw new Error();
97− }
35+ }
98− },
36+ } catch {
99− error: 'This is not a valid galaxy.',
37+ return 'This is not a valid galaxy.';
100− });
38+ }
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− }
39+};
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− });
162
40
163−// @deprecate
41+export const validatePoint = v => {
164−export const validateNetworkSeed = m =>
42+ try {
165− simpleValidatorWrapper({
43+ if (!ob.isValidPatp(v)) {
166− prevMessage: m,
44+ throw new Error();
167− validator: d => plain64CharHexValue.test(d),
45+ }
46+ } catch {
168− error: 'This is not a valid network seed.',
47+ return 'This is not a valid point.';
169− });
48+ }
49+};
170
50
171−// Checks if a string is a valid ethereum address
51+export const validatePatq = v => {
172−export const validateEthereumAddress = m =>
52+ try {
173− simpleValidatorWrapper({
53+ if (!ob.isValidPatq(v)) {
174− prevMessage: m,
54+ throw new Error();
175− validator: d => isValidAddress(d),
55+ }
56+ } catch {
176− error: 'This is not a valid Ethereum address.',
57+ return 'This is not a valid ticket.';
177− });
58+ }
59+};
178
60
179−export const validateEmail = m =>
61+export const validateOneOf = (options = []) => v =>
180− simpleValidatorWrapper({
62+ !includes(options, v) && 'Is not a valid option.';
181− prevMessage: m,
182− validator: d => emailRegExp.test(d),
183− error: 'This is not a valid email address.',
184− });
185
63
186−export const validateInt = m =>
64+export const validateHexString = v =>
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.',
65+ !isHexString.test(v) && 'This is not a valid hex string.';
199− });
200
66
201−export const validateExactly = (value, error) => m =>
67+export const validateEthereumAddress = v =>
202− simpleValidatorWrapper({
68+ !isValidAddress(v) && 'This is not a valid Ethereum address.';
203− prevMessage: m,
204− validator: d => d === value,
205− error,
206− });
207
69
208−export const validateNotAny = (values = []) => m =>
70+export const validateEmail = v =>
209− simpleValidatorWrapper({
71+ !emailRegExp.test(v) && 'This is not a valid email address.';
210− prevMessage: m,
211− validator: d => !values.includes(d),
212− error: `Cannot be ${m.data}.`,
213− });
214
72
215−export const validateLength = l => m =>
73+export const validateExactly = (value, error) => v => v !== value && error;
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− });
227
74
228−export const validateHexLength = l => m =>
75+export const validateNotAny = (values = []) => v =>
229− simpleValidatorWrapper({
76+ values.includes(v) && `Cannot be ${v}.`;
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− });
240
77
241−export const validateMaximumLength = l => m =>
78+export const validateLength = l => v =>
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.`,
79+ v.length !== l && `Must be exactly ${l} characters.`;
252− });
253
80
254−export const validateGreaterThan = l => m =>
81+export const validateHexLength = l => v =>
255− simpleValidatorWrapper({
82+ v.length !== l + 2 && `Must be exactly ${l} hex characters.`;
256− prevMessage: m,
257− validator: d => d > l,
258− error: `Must be at least ${l}.`,
259− });
260
83
261−export const validateInSet = (set, error) => m =>
84+export const validateMaximumLength = l => v =>
262− simpleValidatorWrapper({
85+ v.length > l && `Must be ${l} characters or fewer.`;
263− prevMessage: m,
264− validator: d => set.has(d),
265− error,
266− });
267
86
268−export const validateNameInNumberSet = (set, error) => m =>
87+export const validateGreaterThan = l => v =>
269− simpleValidatorWrapper({
88+ !(v > l) && `Must be at least ${l}.`;
270− prevMessage: m,
271− validator: p => {
272− const num = patp2dec(p);
273− return set.has(num);
274− },
275− error,
276− });
277
89
278−export const validatePatpByteLength = byteLength => {
90+export const validateInSet = (set, error) => v => !set.has(v) && error;
279− return validateLength(patpStringLength(byteLength));
280−};
281
91
282−export const validateMaximumPatpByteLength = byteLength => {
92+export const validateMaximumPatpByteLength = byteLength =>
283− return validateMaximumLength(patpStringLength(byteLength));
93+ validateMaximumLength(patpStringLength(byteLength));
284−};
285
94
286export const validateNotNullAddress = validateNotAny([
95export const validateNotNullAddress = validateNotAny([
287 ETH_ZERO_ADDR,
96 ETH_ZERO_ADDR,
src/views/Activate/ActivateCode.js+2−2
@@ -25,7 +25,7 @@ import useHasDisclaimed from 'lib/useHasDisclaimed';
25import BridgeForm from 'form/BridgeForm';
25import BridgeForm from 'form/BridgeForm';
26import SubmitButton from 'form/SubmitButton';
26import SubmitButton from 'form/SubmitButton';
27import { TicketInput } from 'form/Inputs';
27import { TicketInput } from 'form/Inputs';
28−import { composeValidator, buildTicketValidator } from 'form/validators';
28+import { composeValidator, buildPatqValidator } from 'form/validators';
29import FormError from 'form/FormError';
29import FormError from 'form/FormError';
30import { FORM_ERROR } from 'final-form';
30import { FORM_ERROR } from 'final-form';
31
31
@@ -56,7 +56,7 @@ export default function ActivateCode() {
56 }, [names, push, hasDisclaimed]);
56 }, [names, push, hasDisclaimed]);
57
57
58 const validate = useMemo(
58 const validate = useMemo(
59− () => composeValidator({ ticket: buildTicketValidator() }),
59+ () => composeValidator({ ticket: buildPatqValidator() }),
60 []
60 []
61 );
61 );
62
62
src/views/Activate/PassportVerify.js+2−2
@@ -8,7 +8,7 @@ import { isDevelopment } from 'lib/flags';
8
8
9import SubmitButton from 'form/SubmitButton';
9import SubmitButton from 'form/SubmitButton';
10import { TicketInput } from 'form/Inputs';
10import { TicketInput } from 'form/Inputs';
11−import { composeValidator, buildTicketValidator } from 'form/validators';
11+import { composeValidator, buildPatqValidator } from 'form/validators';
12import BridgeForm from 'form/BridgeForm';
12import BridgeForm from 'form/BridgeForm';
13
13
14import { useActivateFlow } from './ActivateFlow';
14import { useActivateFlow } from './ActivateFlow';
@@ -26,7 +26,7 @@ export default function PassportVerify({ className }) {
26 const validate = useMemo(
26 const validate = useMemo(
27 () =>
27 () =>
28 composeValidator({
28 composeValidator({
29− ticket: buildTicketValidator([
29+ ticket: buildPatqValidator([
30 validateExactly(ticket, 'Does not match expected master ticket.'),
30 validateExactly(ticket, 'Does not match expected master ticket.'),
31 ]),
31 ]),
32 }),
32 }),
src/views/Admin/Reticket/ReticketVerify.js+2−2
@@ -7,7 +7,7 @@ import { isDevelopment } from 'lib/flags';
7
7
8import BridgeForm from 'form/BridgeForm';
8import BridgeForm from 'form/BridgeForm';
9import { TicketInput } from 'form/Inputs';
9import { TicketInput } from 'form/Inputs';
10−import { composeValidator, buildTicketValidator } from 'form/validators';
10+import { composeValidator, buildPatqValidator } from 'form/validators';
11import SubmitButton from 'form/SubmitButton';
11import SubmitButton from 'form/SubmitButton';
12import FormError from 'form/FormError';
12import FormError from 'form/FormError';
13
13
@@ -20,7 +20,7 @@ export default function ReticketVerify({ newWallet }) {
20 const validate = useMemo(
20 const validate = useMemo(
21 () =>
21 () =>
22 composeValidator({
22 composeValidator({
23− ticket: buildTicketValidator([
23+ ticket: buildPatqValidator([
24 validateExactly(ticket, 'Does not match expected master ticket.'),
24 validateExactly(ticket, 'Does not match expected master ticket.'),
25 ]),
25 ]),
26 }),
26 }),
src/views/CreateGalaxy.js+1−1
@@ -77,7 +77,7 @@ export default function CreateGalaxy() {
77 const validate = useMemo(
77 const validate = useMemo(
78 () =>
78 () =>
79 composeValidator({
79 composeValidator({
80− galaxyName: buildPointValidator(1, validateGalaxy),
80+ galaxyName: buildPointValidator(1, [validateGalaxy]),
81 owner: buildAddressValidator(),
81 owner: buildAddressValidator(),
82 }),
82 }),
83 [validateGalaxy]
83 [validateGalaxy]
src/views/IssueChild.js+1−1
@@ -96,7 +96,7 @@ export default function IssueChild() {
96 const validate = useMemo(
96 const validate = useMemo(
97 () =>
97 () =>
98 composeValidator({
98 composeValidator({
99− point: buildPointValidator(4, validatePoint),
99+ point: buildPointValidator(4, [validatePoint]),
100 owner: buildAddressValidator(),
100 owner: buildAddressValidator(),
101 }),
101 }),
102 [validatePoint]
102 [validatePoint]
src/views/Login/Ticket.js+5−5
@@ -23,7 +23,7 @@ import { TicketInput, PassphraseInput, PointInput } from 'form/Inputs';
23import {
23import {
24 composeValidator,
24 composeValidator,
25 buildCheckboxValidator,
25 buildCheckboxValidator,
26− buildTicketValidator,
26+ buildPatqValidator,
27 buildPassphraseValidator,
27 buildPassphraseValidator,
28 buildPointValidator,
28 buildPointValidator,
29} from 'form/validators';
29} from 'form/validators';
@@ -112,10 +112,10 @@ export default function Ticket({ className, goHome }) {
112 usePassphrase: buildCheckboxValidator(),
112 usePassphrase: buildCheckboxValidator(),
113 useShards: buildCheckboxValidator(),
113 useShards: buildCheckboxValidator(),
114 point: buildPointValidator(4),
114 point: buildPointValidator(4),
115− ticket: buildTicketValidator(),
115+ ticket: buildPatqValidator(),
116− shard1: buildTicketValidator(),
116+ shard1: buildPatqValidator(),
117− shard2: buildTicketValidator(),
117+ shard2: buildPatqValidator(),
118− shard3: buildTicketValidator(),
118+ shard3: buildPatqValidator(),
119 passphrase: buildPassphraseValidator(),
119 passphrase: buildPassphraseValidator(),
120 },
120 },
121 validateForm
121 validateForm