It looks over-engineered, complicated and non-intuitive. In 90% of cases, vanilla business logic will be concise enough. I've used validation modules like Meteor's check[0] before, and I think that's a good example of how much surface area a validation library API should have.
Apart from the contrived example you cited, why do we need an abstraction for detailed value checking? Conditional statements are already a thing. Just use modules and readable function names:
// helpers.js
const isNumber = typeof n === "number" && isFinite(n)
const isTestScore = n => isNumber(n) && n >= 0 && n <= 100;
// business-logic.js
const uploadScore = (n, cb) => {
if (!isTestScore(n))
return cb(new Error("invalid test score"));
// super contrived, but let's do it
if (n === 32 || n%2)
return cb(new Error("we don't like oddities and we hate 32"));
// do something with valid test score n
}
IMHO, a validation library should validate types and structure and should know nothing about the specific values of your data. The values are likely relevant to your business logic and should be handled there for finer-grained error handling, logical branching, etc.
>Apart from the contrived example you cited, why do we need an abstraction for detailed value checking?
We don't "need" an abstraction for anything, in the sense that we need oxygen. We _want_ abstractions, and they usually make us more productive, which is something different.
Case in point the above horribly noisy procedural code for something that this library abstracts into neat, purpose specific, calls.
What happens when you want to know why your value failed the test? Easy, just break up your chained v8n call into smaller chunks and handle each one individually -- but wait, now we're back to the same procedural code we started with!
Never fear; the devs implement hooks that let you do something when a certain part of the chain fails. Cool -- but now all of our code is tightly coupled to the structure imposed by the hook syntax.
Okay, better solution: how about an array of "reasons" is returned that tells you why your test failed. Now we've got to tell v8n what reason text we want (if any) for each part of the chain that fails, so we're back to having to declare a bunch of messages somewhere, which is what people already do.
Yeah, we want abstraction, but we want the right abstraction. Premature generalization/abstraction is a common pitfall among even the best developers. This library could be useful for some cases, but I'm not convinced it should be used for everything it can be used for.
>What happens when you want to know why your value failed the test? Easy, just break up your chained v8n call into smaller chunks and handle each one individually
You seem confused. You just need to substitute .check() instead of .test() and you get a ValidationException if your value doesn't validate that tells you exactly which rule failed.
And that's just the two two basic result values (true/false on validation or Exception) that the devs implemented.
Nothing in this kind of design prevents returning any kind of detailed error or array of errors etc if one wants too.
>kay, better solution: how about an array of "reasons" is returned that tells you why your test failed. Now we've got to tell v8n what reason text we want (if any) for each part of the chain that fails, so we're back to having to declare a bunch of messages somewhere
Woooosh. The validation library is not about not having to write our own messages for the user. It's about not writing our own tests when the dozens of built-in ones are just as good. Plus they give structure that's better than a bunch of if/elses.
>Yeah, we want abstraction, but we want the right abstraction. Premature generalization/abstraction is a common pitfall among even the best developers.
Premature generalization/abstraction? You might be seeing this pattern for the first time, but this library follows a very standard pattern for validation libraries, that has been with us, and used in tons of production code, for over 2 decades. Apache Commons did that since ages. Even the fluent interface take on such libs is nothing new.
Check and Match are so well written. JSON schema/ajv also. Validation shouldn’t rely on a module/codebase to work and should be able to be built dynamically or stored in a database without needing eval/hacks. Runtime validation is important, not just dev experience/typescript.
It’s the AppleScript Uncanny Valley: the closer you get to allowing all natural language in your programming language or API, the more likely it is that users will be confused, because they expect something to be possible and it isn’t. This feels similar.
My company migrated everything away from joi in favor of AJV. Joi isn't serialisable and the maintainers are hostile to the idea of adding that capability. There are some 3rd party extensions to add serialization support, but they are not feature-complete. I also find the declarative nature of JSON Schema preferable to joi's semi-imperative API which is occasionally quite difficult to debug.
Why I like AJV or similar options is because you get to declare what you expect with a simple schema and it rarely needs to get complicated.
It's more overhead than simple functional approaches combined with conditional statements, but I think it's more approachable. Otherwise I find it doesn't leave too much of a margin for error and it's reasonably fast.
It's a personal preference, but I prefer it far more than a fluent interface. I actually avoid those now despite really loving them in the past.
+1 for joi. We use it extensively. It really reduces development time for us and it has some nice features like referencing other values in an object and evaluating the whole object at once (instead of fast fail) which is useful for APIs so you can return all the errors at once.
For Ruby the dry gems are pretty nice and help for validation (https://dry-rb.org). There is a dry-validation module, although I found the dry-types and dry-struct gems even nicer for expressing validation directly on my model fields and hash schemes.
return () => value => {
return (
typeof value === type ||
(value === null && type === "null") ||
(Array.isArray(value) && type === "array")
);
};
This means that null and "null" (as a string) are now numbers.
v8n().number().test("null") // true
It also means that an array of numbers is a number... something that is counterintuitive for the user of the library.
In addition to that, most of the time you want to deal with finite numbers. A validation library should have an API that reflects this, but this is not the case for v8n.
`makeTestType("null")()(null)` return true, but `makeTestType("number")()(null)` doesn't. The signature of `makeTestType` is `type: String => () => value: Any => Boolean`.
Great, but this can be done with type-safety too. See this https://github.com/pelotom/runtypes -- it isn't purely meant for validation, but not much is needed to use it for it.
It seems like the main purpose of the exception-based validation function `check` is to provide an explanation for failures. Could this have also been achieved by returning an error object with the same information in it? This is what clojure.spec does: https://clojure.github.io/spec.alpha/clojure.spec.alpha-api....
I've been interested in clojure spec for a while, but I'm stuck in JS land. There's a port called js.spec, and I think it works really well for validation.
[0] https://docs.meteor.com/api/check.html