-
Notifications
You must be signed in to change notification settings - Fork 337
Consider to add type converters #50
Comments
I don't really understand the problem here, the The reason I opted for supporting strings per default is that when you read values from a form you get them as strings. |
So input may be |
I like the idea. Version 0.8.0 will ship with a I could add a function that does type conversion for you (automatically when using promises as with the attribute cleaning). |
@ansman let me think about it a bit more. This turns out to be a harder question than I thought. I wrote a form-builder library in Python once, but that was for backend-driven app. My current requirements differ significantly. I promise to write a big post about all this here. |
IntroOk, it's me again. I spent last days thinking and experimenting with validation and the final picture is getting clear to me. JS community desperately needs an environment agnostic data validation library for JS.
It's ridiculous, I mean we have to have a lot of competing solutions today. But, spent a lot of hours on search, I did find only 3 projects worth considering. Joi, TComb-Validation and this one. TComb-Validation, being very interesting on itself, is a very specific beast and I'd like to exlude it from today's rant. So we have Joi and ValidateJS. Joi does not have two crucial components ValidateJS has: custom validators and async validators. Joi devs are not going to support second one at all, and first one are only planned (since 2014...). That being said Joi has 1200 stars against local 120 still... Not less important is that fact, that Joi is very very big, 2.5 Mib uncompressed, against 30 Kb of ValidateJS. So, IMO, Joi as a very overrated library, and ValidateJS is an underrated. Validation is one of the only few areas which suppor code sharing between frontend and backend. We should be eager for such opportunities. They are, above all, one of the topmost marketing benefit of JS. That's why a lot of people, including me, asked Joi authors to reconsider their priorities. But as already said, Joi authors are very rejective to all requests outside of their own view. A bit about myself. I wrote a big form validation and form building library once in Python (https://github.com/ivan-kleshnin/flask-paqforms). It's OOP... crap – I know. But, at lest, I was there and I know a bit of that problems you're facing. And I still think I managed to build my libs better than much more popular (mainstream) WTForms. By objective reasons... but who cares? Let's come back to JS. |
Parse, Validate, FormatThere are a three tightly connected areas that's often called Validation: parsing, validation, formatting. It's becoming obvious we have two layers: HTML and Business. Let's call those two kinds of values form values and business values. But should we couple or decouple these three aspects? Ok, lets make a broad and shallow list of our requirements first:
So why not isolate formatting and parsing in one library and validation in another? That's decoupling and decoupling is good, right? Yes, but there is one very tiny detail everyone seems to forget. Parsing and formatting also can fail! Formatting fails are very simple. They can be caused only by serious data mismatch. Therefore, they should be thinked of as a programmer errors so we can just throw an exception and crash the process. Parsing fail is the same as validation fail. This is super important so I will repeat Parsing fail is the same as validation fail. That's why we can't and shouldn't totally isolate one from another. Validation can be decoupled, but should be concerned of Parsing. Very simple and reliable API is quite self-rendering here. parseString(formValue: String): String
// throws if value is not string, converts '' to undefined
formatString(businessValue: Maybe<String>): String
// throws if value is not string or undefined, converts undefined to ''
parseInteger(formValue: String): Number
// throws if value is not string
formatInteger(businessValue: Maybe<Number>): String
// throws if value is not number or undefined, converts undefined to '' So Validation library should somehow accept a Parse handler. This handler can solve any user-specific task like mentioned "task of splitting a phones". You can provide some sane defaults but you can't just go with some restricted set of predefined types here. That was Joi biggest mistake. Form cases can be very very different. Callback can solve any of them. Formatting is not really participate here. It will be used somewhere in HTML form. A shortened example from real React component: <input type="date"
value={formatDate(form.birthDate)}
onBlur={() => this.validate("birthDate")}
onChange={event => this.handleChange("birthDate", event.currentTarget.value, parseDate(event.currentTarget.value))}
id="birthDate" ref="birthDate"
className="form-control"/>
/> All three aspects of parsing, formatting and validation met here. |
L10NEveryone remembers about localisation of error messages. Unfortunately it seems quite common to forget about localisation of input data. I met this issue in every validation library of any programming language. Date formats are different across different cultures. But the same applies to numeric formats and a lot of other data. That's how I solved this by converters (merged OOP parsers + formatters): class IntConverter:
def parse(self, data, locale='en'):
if type(data) == int:
return data # I would throw in this case now
elif type(data) == float:
return int(round(data)) # I would throw in this case now
elif type(data) == str:
if data:
data = data.strip().replace(" ", "\u00A0") # non-breaking-space (for correct number parsing)
try:
return babel.numbers.parse_number(data, locale=locale)
except Exception:
raise ValueError
else:
return None
elif data is None:
return None
else:
raise TypeError I used the great Babel python library here. In JS I'd like to rely on something similar like http://www.localeplanet.com/ |
OutroImportant question remains: how to wire up parsers and validators. We still can approach this in several ways:
As I stated in the first post, I believe that the first option is the best one. It's the most performance-friendly and declarative of them. Code example for getting tags from textarea, validating them by number of tags (up to 10) and by length of each tag (up to 100). Resulting Array should not contain Code example a-la ValidatorJS: import R from "ramda"
{
tags: {
presence: true,
parser: R.pipe(
R.split("\n"), R.map(parseString), R.filter)
),
type: Array,
maxLength: 10,
item: {
parser: parseString,
type: String,
maxLength: 100
}
}
} Code example a-la Joi: import R from "ramda"
{
tags: {
Joi.Array().of(Joi.String().maxLength(100)).required().parseWith(
R.pipe(
R.split("\n"), R.map(parseString), R.filter)
)
).maxLength(10),
}
} Anyhow, you can seriously cut the codesize of any validation library, decoupling questions of type onto separate lib. Validation will validate only one (or two with One question remains: how to incorporate errors from parsing step into validation layer result. Both |
I agree that validation is complex task and should consist of 3 parts:
I think they are indivisible and should be described in one place. Also some fields may be optional and have default value. Validated object may have deep nesting (see #46). Simplified example of validated object in node.js: var filter = require('validator');
var fromAndTo = {
startTime: {
$default: function() { // value or function returning value
return new Date();
},
$validators: {
presence: true,
datetime: true
}
},
endTime: {
$validators: {
presence: true,
datetime: true
}
},
isTerminal: {
$parsers: filter.toBoolean // function or array of functions
},
address: {
cityId: {
$validators: {
presence: true
}
},
terminalId: {
$validators: {
presence: function(value, attributes, attributeName, attributePath) {
// attributePath is array ['from', 'address', 'terminalId']
// or string 'from.address.terminalId'
var from = attributes[attributePath[0]];
return from.isTerminal;
}
}
},
value: {
$parsers: filter.trim,
$validators: {
presence: function(value, attributes, attributeName, attributePath) {
var from = attributes[attributePath[0]];
return !from.isTerminal;
}
}
}
},
geo: {
$validators: {
presence: function(value, attributes) {
var from = attributes[attributePath[0]];
return !from.isTerminal;
}
},
latitude: {
$validators: {
presence: true,
numericality: {
greaterThanOrEqualTo: -90,
lessThanOrEqualTo: 90
}
}
},
longitude: {
$validators: {
presence: true,
numericality: {
greaterThanOrEqualTo: -180,
lessThanOrEqualTo: 180
}
}
}
},
note: {
$parsers: filter.trim
}
};
var needValidate = {
from: fromAndTo,
to: fromAndTo
}; I like the idea of |
Hi @Jokero! As parsing step can and often includes conversion from localized to universal format and formatting step, contrary, includes conversion from universal to localized format term filtering is really not so great as parsing / formatting. We does not filter "bad data" as people often simplify this process. Things are much more complex. Your API is 3-rd of the possible ways to organize this, yeah.
Well, they can be decoupled into different libs but are indivisible if you talk in the sense of requirements. I agree about other of your detalizations. |
I think this format is justified, but it is necessary to separate levels of abstraction. 1 level. Validate framework. Has config with special fields: 2 level. Filters (parsers and formatters) and validators. Filters can include localization and others. Validate.js has library of basic filters and validators, but user can create customs. User determines which should return custom filters (f.e. convert empty strings to undefined). If someone need special bundle of filters or validators for your application. He can set it in default config for all requests/responses. defaults.$parsers.push(emtyStringToUndefined);
defaults.date.$parsers.push(localToUTF); |
With chaining be ready to meet corner cases like "how to deal with a promise in second element if first and third calls were sync" and so on. I still think function composition is better.
Yeah, yeah – one of such questions. Piping is just not flexible enough.
Why include such stuff in Validate.js? It should be a separate library cause all this parsing functions may be reused in different areas. Like HTML data scraping. |
@ivan-kleshnin, I reread your comments and realized that I initially didn't understand the meaning of You mean:
And I:
|
I'm taking a look at validate.js for parsing the desiredCapabilities object which gets passed into Appium. This is a case of needing some pretty simple js object-validation, but isn't tied to web forms or anything like that (so I need to verify that a property is typeof 'string' and such). The proposal in this thread would probably be the ideal thing for us (we also need the filtering and parsing steps). This also looks like a related project: https://github.com/molnarg/js-schema/ |
@Jokero aren't that diagrams equal just presented in different ways 😃 ? |
Easy! function chainEvaluator(filters, input) {
return filters.reduce(function(previousPromise, currentFn, index, arr) {
return previousPromise.then(function(value) {
return currentFn(value);
})
}, q.when(filters.shift()(input)));
}
var fn1 = function(value) {return value + '-1'};
var fn2 = function(value) {return q.when(value + '-2')}; //return promise
var fn3 = function(value) {return value + '-3'};
var functionsArray = [fn1, fn2, fn3];
chainEvaluator(functionsArray, 'smth').then(function(result) {
console.log(result);
});
Because without basic validators no one will use the bare framework. I mean |
I'm for separate library as a dependency as well. Anyway it's good to see that people are mostly agree here. |
good. @ansman What do you think? |
I don't really understand the problem you are talking about to be honest. The scope of validate.js has been and will always be a fast, simple and easy way of validating a set of attributes against a set of constraints. As it stands today there is a simple form parser, this is only because it's such a commonplace thing to do. I'm all for things such as Promises and Reactive programming which makes things like validation easy: Promise.resolve(document.querySelector("form#signup"))
.then(validate.collectFormValues) // This is the parsing step, could easily be replaced with another lib
.then(function(formValues) { // This is validation, the core of validate.js
return validate.async(formValues, constraints);
})
.then(api.signup)
.then(handleSignupSuccess)
.catch(ValidationError, handleValidationError) // This is formatting
.catch(ServerError, handleServerError)
.catch(handleGenericError); |
I'm closing this due to inactivity but feel free to comment and I'll reopen it. |
I'm researching Joi alternatives and found this lib. I like the crossplatform aim you set for this project (Joi sucks for frontend having 2+Mib). Unfortunately there is little meaning in validation without type coercion. If value is checked to be an integer it should be possible to use this as an integer.
Not validate as string, then apply type convertion manually, then use. It breaks the whole purpose of declarativeness in validation: you'll end up with the second deeply-nested declarative object with the same keys, and different values describing type convertion rules (semantically the same, as you've already said you want an integer)...
The text was updated successfully, but these errors were encountered: