Closed a-s-o closed 9 years ago
Hi, tcomb-validation is a low level library so emitted messages are targetting developers but since validation errors contain the whole validation infos:
you could build a custom error message system. For example
import t from 'tcomb-validation';
const Email = t.subtype(t.Str, x => x.indexOf('@') !== -1, 'Email');
const LongPassword = t.subtype(t.Str, x => x.length > 10, 'LongPassword');
const FutureDate = t.subtype(t.Dat, x => x.getTime() > new Date().getTime(), 'FutureDate');
const MyType = t.struct({
email: Email,
password: LongPassword,
date: FutureDate
});
// a simple message system based on a hash: Type name -> message
function getMessages(errors, messages) {
return errors.map(error => {
return messages[error.expected.meta.name];
});
}
// the message system in action
const messages = {
Email: 'Invalid email',
LongPassword: 'Short passwords are not allowed; please use at least 10 characters',
FutureDate: 'Date must be in the future'
};
const result = t.validate({
email: 'badEmail',
password: 'password',
date: new Date(1973, 10, 30)
}, MyType);
console.log(JSON.stringify(getMessages(result.errors, messages), null, 2));
output:
[
"Invalid email",
"Short passwords are not allowed; please use at least 10 characters",
"Date must be in the future"
]
related (but old) issue:
https://github.com/gcanti/tcomb-validation/issues/5#issuecomment-62228216
Wow, thanks for writing that out. I did take a look at #5 but was confused. The above is much clearer.
So as I understand the ValidationResult
output from t.validate just provides a destructured array of errors and does not preserve the structure of the initial type? Is there any way to preserve that?
i.e. let's say the type is defined as in your example above, we get back the following result from validation:
const MyType = t.struct({
email: Email,
password: LongPassword,
start: FutureDate,
end: t.all([AfterStart, FutureDate]) // => made up combinator for specifying multiple types
});
const result = t.validate({
email: 'badEmail',
password: 'password',
start: new Date(2020, 10, 30),
end: new Date(2015, 10, 30)
}, MyType);
Given the above, the result.errors
could be as follows (basically preserving the original data structure):
{
email: [EmailError],
password: [PasswordError],
start: null, // => no error for this field
end: [AfterStartError]
}
Okay, as I write that out, I understand what you are saying that all the errors have paths, we should be able to build the output structure ourselves. I will take a stab at that. If I come up with something useful, I will post back here.
Thanks again. Your response was very helpful.
Can I also ask my other question again? Will any such validation system built using tcomb get disabled in production when NODE_ENV === 'production'? Is there a way to prevent that?
I will post back here.
Yes please, I'd be glad to see a good message system on top of tcomb-validation. I think the most difficult problem is how to handle subtypes (and perhaps unions):
Can I also ask my other question again? Will any such validation system built using tcomb get disabled in production when NODE_ENV === 'production'? Is there a way to prevent that?
Oh sorry, I forgot the other question. Validations will be preserved. Every tcomb type is a function T
such that
T(value)
contains an assert and throws if value
is badT.is(x)
returning true
if x
is an instance of T
the asserts will be stripped out in production, though the function T.is
will be preserved.
In short
// development
const a = t.Str(1); // throws
console.log(t.Str.is(1)); // => false
// production
const a = t.Str(1); // => a = 1
console.log(t.Str.is(1)); // => false
Note. asserts shouldn't ever be used as a validation system but just for type checking.
Since tcomb-validation's internals use extensively such is
functions (example https://github.com/gcanti/tcomb-validation/blob/master/index.js#L79) validations are active also in production.
Thanks for the explanation Giulio. I really appreciate your thorough explanations.
I have started changing over some of my validations to tcomb. Once I develop some understanding of the lib, I will start figuring out what the best api for messages may be. I may ask for your help again.
Closing the issue for now.
Hi @gcanti,
This is what I came up with for displaying messages using tcomb. I propose implementing two functions. First is a T.isnt
function on each type or subtype which takes an optional message as argument and returns a inverted predicate, which instead of returning a boolean, returns the original message.
The second function is a combinator t.invalidate
which accepts and array of functions and returns the first error or empty string if validation passed.
Your input is appreciated.
Usage:
var checkEmail = t.invalidate([
t.Str.isnt('Please provide an email address'),
t.ShortStr.isnt('Email appears incomplete'),
t.Email.isnt('That is not a valid email address')
]);
var checkPassword = t.invalidate([
t.Str.isnt('Password not provided'),
t.ShortStr.isnt('Password is too short. Min 5 chars'),
t.SecurePass.isnt('Password must contain a uppercase letter, lowercase letter and number')
]);
// elsewhere in a submit handler
function onsubmit (formdata) {
const emailError = checkEmail(formdata.email);
const passError = checkPassword(formdata.password);
if (emailError || passError) {
// Display the errors
} else {
// Submit the form
}
}
Implementation:
// f: [list(Func)] -> Func
t.invalidate = function (preds) {
const len = preds.length;
return function (value) {
let fn;
let i = 0;
while (t.Func.is(fn = preds[i++])) {
const err = fn.call(this, value);
if (err) return err;
}
return '';
}
};
// Helper to quickly create inverted predicates
// f: [T] -> Func
function invert (type) {
// f: [Str] -> Func
return function (msg) {
// f: [Any] -> maybe(Str)
return function (value) {
return type.is(value) ? null : msg;
};
};
}
// Define isnt methods on the types as follows
t.Str.isnt = invert(t.Str);
t.ShortStr.isnt = invert(t.ShortStr);
t.SecurePass.isnt = invert(t.SecurePass);
t.Email.isnt = invert(t.Email);
Just fixing mistakes above, and realized that there is no need to have T.isnt
on each type. The isnt
fn can just be a combinator to be used as follows:
t.invalidate([
t.isnt(t.Str, 'Password is required'),
t.isnt(t.LongStr, 'Password is too short'),
t.isnt(t.SecurePass, 'Passwords must contains an uppercase, lowercase, and number')
])
Hi @a-s-o The following issue on tcomb may interest you (there's also an implementation of a message system):
https://github.com/gcanti/tcomb/issues/111
I think that introducing a new intersection
combinator, in addition to being interesting theoretically, can help in designing a general message system.
Oh, wow. That intersection combinator is almost exactly the same. I'll look forward to what becomes of it in v2.2. Thanks.
Hello, I am using
tcomb-validation
but cannot figure out how to use it to display custom error messages. I am currently using my own code to validate form models as follows and would appreciate some direction as to how I can switch to usingtcomb-validation
which is otherwise great for typechecking.Here is an example of what I am currently doing:
I can use the above functions as follows to display custom error messages:
Basically validatePassword returns the first error message that it encounters.
Another example:
Is something like this possible with
tcomb-validation
? I couldn't figure it out from the documentation so far.Somewhat related, how do tcomb-validation and tcomb-form keep types from being disable in production mode? I though that the type checking gets disable when NODE_ENV === 'production'. I like that but in case of forms, one would want to keep the checking enabled.
Thanks for your help and a providing a great tool.