square / lgtm

Simple object validation for JavaScript.
Other
370 stars 40 forks source link

Composing chained validations from multiple validators #15

Closed sarus closed 9 years ago

sarus commented 9 years ago

I'm using LGTM to validate REST API inputs and found myself repeating the same validations over and over. For example, all my GET endpoints need to validate LIMIT and OFFSET so I have something like this:

var getUsersValidator = LGTM.validator()

    // Validate limit
    .validates('limit')
    .optional()
    .integer(validationJsonError.serialize({
        detail:'The "limit" value must be an integer',
        errorAttribute: 'limit'
    }))
    .minValue(0, validationJsonError.serialize({
            detail:'The minimum value for the "limit" is 0',
            errorAttribute: 'limit'
        }))

    // Validate 'offset'
    .validates('offset')
    .optional()
    .integer(validationJsonError.serialize({
                detail:'The "offset" value must be an integer',
                errorAttribute: 'offset'
            }))
    .minValue(0, validationJsonError.serialize({
                detail: 'The minimum value for the offset is 0',
                errorAttribute: 'offset'
            }))

// Additional validations
.build();

var getPostsValidator =  LGTM.validator()
// Repeat above for limit and offset

Note that instead of returning string messages I return a JSON API error object (http://jsonapi.org/format/#errors). I know in all the examples you return a string but returning the object and then formatting all the JSON API errors into a single JSON API compliant response works really well (might be worth mentioning in the docs that you can return a custom object).

What I'd like to be able to do is compose together different validators to run together as a single compound validation or be able to easily construct a new compound validator from primitive validators (a validator that validates one attribute or property). I'm having a hard time figuring out a clean way to do this.

I basically want to encapsulate the validation logic for limit and the validation logic for offset and then use those as primitives to create a new more complex validator (there are other things like sort and order as well). This is made up but something like this:

var limit = LGTM.validator()    
    .validates('limit')
    .optional()
    .integer(validationJsonError.serialize({
        detail:'The "limit" value must be an integer',
        errorAttribute: 'limit'
    }))
    .minValue(0, validationJsonError.serialize({
            detail:'The minimum value for the "limit" is 0',
            errorAttribute: 'limit'
        }));

var offset = LGTM.validator()
    .validates('offset')
    .optional()
    .integer(validationJsonError.serialize({
                detail:'The "offset" value must be an integer',
                errorAttribute: 'offset'
            }))
    .minValue(0, validationJsonError.serialize({
                detail: 'The minimum value for the offset is 0',
                errorAttribute: 'offset'
            }));

var user = LGTM.validator()
    .maxLength(25)
   .required();

var posts = LGTM.validator()
   .maxLength(200)
   .required();

// My new validators
var getUserValidator = LGTM.build([limit, offset, user]);
var getPostsValidator = LGTM.build([limit, offset, posts]);

Are there some best practices on how I can achieve this? Thanks!

sarus commented 9 years ago

So I accomplished this by wrapping LGTM in my own object where I can chain together commonly used parameter validations such as limit, offset, sort etc. to create custom validators.

Looks something like this:

function Validator(){
    this.lgtmValidator = LGTM.validator();
}

Validator.prototype.enabledOnly = function(){
    this.lgtmValidator.validates('enabledOnly')
        .optional()
        .boolean('Must be a boolean');

    return this;
};

Validator.prototype.includes = function(){
    this.lgtmValidator.validates('includes')
        .optional()
        .string('Must be a string')
        .includes(['id', 'username', 'full-name', 'email', 'is-admin', 'enabled', 'last-login'], 
'The "includes" value must be "id", "username", "full_name", "email", or "is_admin"'
        );
    return this;
};

Validator.prototype.build = function(){
    return this.lgtmValidator.build();
}

module.exports = Validator;

Then usage is:

var getUsersValidator = new Validator()
    .enabledOnly()
    .includes()
    .build();

Thanks!

eventualbuddha commented 9 years ago

Your approach seems like a reasonable one. I'm not opposed to adding something in core if it doesn't interfere with the existing flexibility to do what you ended up with. Perhaps changing the behavior of .build() to return distinct objects rather than simply returning the same one over and over again?