mikeerickson / validatorjs

A data validation library in JavaScript for the browser and Node.js, inspired by Laravel's Validator.
https://www.npmjs.com/package/validatorjs
MIT License
1.77k stars 280 forks source link

Support arrays in nested rules #200

Open vict-shevchenko opened 7 years ago

vict-shevchenko commented 7 years ago

Hi, thanks a lot for an awesome library.

I suppose I have found an inconstancy, here what it is:

var values      = { persons: [ { firstName: 'Viktor' } ] };

var rulesFlat   = { 'persons[0].firstName': 'required' }; // or { 'persons.0.firstName': 'required' }

var rulesNested = { persons: [ { firstName: 'required' } ] }

var v1 = new Validator(values, rulesFlat); // ok
var v2 = new Validator(values, rulesNested) // throw Exception TypeError: ruleString.indexOf is not a function

In my opinion both of them should work. If this is a bug, I can try to make a pull request. Thanks.

vict-shevchenko commented 7 years ago

Hi, I have forked a repo and started some debugging in order to fix this. But came to pretty strange results.

The problem is in support of Arrays in rules. Consider a situations

var values      = { emails: ['test@test.io'] };
var rules      = { emails: ['email'] };

var v1 = new Validator(values, rules);
v.passes() // true. 

But this true is incorrect true, because validation happened on not on string 'test@test.io' rather on Array ['test@test.io']. And RegExp /email regExp/.test(['test@test.io']) returns true. So inputValue that was aimed to be validates was incorrect.

That cause situations

var values      = { emails: ['test@test.io', 'whatever'] };
var rules      = { emails: ['email', 'required'] };

var v1 = new Validator(values, rules);
v.passes() // false. 

And this is incorrect now.

Despite things are ok if we use flat rules object, like:

var rules = {emails[0]: 'email', emails[1]: 'required' }

What I have found for now, is that if we have in rules

_flattenObject function is responsible for that. And I suppose all nested data structures should be flattened.

This may be a massive influence on a lib, taking to account that now it relays on wrong behavior.

Let me know if I am on a right direction. I may have not got the reason why Arrays are not flattened.

Thanks @skaterdav85 , @mikeerickson , @LKay

iamdtang commented 7 years ago

Do you know how Laravel handles an array of emails? If they don't, maybe you can just create a custom rule.

vict-shevchenko commented 7 years ago

So, I did some more investigation on that topic, and it happens that currently there is a similar limitation. Both libraries support "alternative initialization using an array instead pipe" and this makes rules having nested Arrays impossible to use.

var values = { email: 'test@test.io' };
var rules = { email: ['required', 'email'] };  // email in value is not an Array

So in this scenario we dont have one to one match between rules and values data format. Like I expected to have. So any array inrulesobject is counted ad rules list, not asvalues` structure mirroring.

To conclude, if we need to validate only first item of array, we need to write flat rule name.

var values = { emails: ['test@test.io'] };
var rules = { 'emails[0]': 'required|email' }; // or {'email[0]': ['required', 'email']}

This item really confused me for some time. Don`t you mind if I add this to README in PR?

Cephyric-gh commented 2 years ago

Sorry to dredge up a very old issue, but I somewhat recently needed the same thing that is asked for in this ticket. I ended up running my own slightly modified copy of _flattenObject against my ruleset before it is passed into the validator so that the plugin itself didn't need to do any parsing, and it correctly flattens out the nested array object and correctly attaches the nested rules to each key within the sub-object.

(my codebase is written in typescript, so ignore the castings against the variables and it functions as javascript)

function FlattenObject(obj: Record<string, any>): Record<string, any> {
    const flattened: Record<string, any> = {};

    function recurse(current: Record<string, any>, property?: any) {
        if (!property && Object.getOwnPropertyNames(current).length === 0) {
            return;
        }

        if (Object(current) !== current || Array.isArray(current)) {
            if (Array.isArray(current) && current[0] instanceof Object && property) { // <-- Additional check when we know it may be an array to see if the first item is an object, and assume that it is reading nested rules if so
                flattened[property] = ['array'];
                recurse(current[0], `${property}.*`); // <-- recurse down into the sub-object to attach the ruleset to each part of the array with the appropriate "key.*.subKey" structure
            } else {
                flattened[property] = current;
            }
        } else {
            let isEmpty = true;

            for (const p in current) {
                if (Object.prototype.hasOwnProperty.call(current, p)) {
                    isEmpty = false;
                    recurse(current[p], property ? `${property}.${p}` : p);
                }

                if (isEmpty) {
                    flattened[property] = {};
                }
            }
        }
    }

    if (obj) {
        recurse(obj);
    }

    return flattened;
}

This has been working for me for a little bit now, so if it's wanted I can open a PR to adjust the flatten process to take nested array rules into account