We can now finalize the Validatable piece of our data modeling library.
This lesson is part of a series. Be sure to watch the previous lessons to fully understand what is going on!
All right. We're finally ready to add the final functionality to our validation library. If you recall, we have our model, which has certain validations described on it, we have a min and a max length, and then we have an instance of our class.
When we call validate on that class without passing in the name of a field, we'll get errors added to the errors hash here, so we'll expect it to contain, "Must be at least five characters," and then when we call person.validate, it will either return true or false with whether or not the person is a valid instance of the person class. Let's get started here.
We need to head back into our validatable module here. We need to add this __validate because, remember, this is going to be an instance method, so this is going to be a function. It's going to take a field named validate, potentially, so what this is going to do is it will return _validations.validate, and we'll set the context to this, which is the instance of the class that we want to validate, and we'll pass the field name as well.
Then we need to create another private variable, validations validate, and this will be a function that takes an instance to validate and a field name, potentially, so we're going to need to get the fields to validate, so we'll say, "Get fields to validate." We'll pass it the field name, so here we'll create "Get fields to validate."
It takes the field name, so if we're passed the field name, and there are validations for the field name, then we're just going to return validations for the field name, so that's going to be the array of validations for that particular field. Otherwise we want to return an array of all of the validations, so we're just going to return _chainValidations, and we're going to map those, so this is a function.
Remember, the validation, or the array of validations, is the value, and the key would be the name of the field, but we don't need the key here. What we actually just want to do is return the validations themselves and then we're going to flatten that, so instead of having an array of arrays, we'll just have a single array containing each of the validations, and then we'll just call .value to get the unwrapped version because we chained it up here, so that will return all of the fields that we need to validate.
Then we want to loop through each of the fields to validate, and we want to curry validate field, which we're going to right in a minute, with the instance, so this will get passed each of the validations, and each of those validations will be curried with this instance here, so we'll write this function, validate field, and it will take an instance, and it will take a validation.
Then we'll say if validation.validate instance is false, instance.errors.add. We'll use this add method that we added to the errorable hash here, so we'll say validation.field and validation.message, so we want to add this message to this field if it's not valid. Otherwise we want to say instanceErrors.clear, validation.field, validation.message, so we'll make sure we remove that message as long as we pass this validation.
Then, finally, we want to describe whether or not this is valid for this particular field, so we'll say return instance.errors.countFor, which we have to define still, so we'll get a count for this name and we'll say if that equals zero then this is a valid instance for this field name, and if we don't pass a field name, we'll want this to just say whether or not it's valid in general, so we have to return to errorable.
We'll define another private variable for errors called countFor. It's a function that takes a field name, so if is undefined, field name will just return. We have down here, remember, errors.count, so we'll just return the count for all of the errors.
Otherwise we're going to want to return the number of errors for this field name, so if errors field name exists then we'll return errors field name.length, so the length of the array. Otherwise it doesn't exist so we''ll return zero, and we need to head back here and actually return that.
We're actually still getting an error here, and that's because back in validatable, we actually have to do, say, this.constructor.validation, so we want to get the validations for this particular class and call validate, and that will make our test pass here.
Next, even though we'd like people to register custom validators, I do want them to be able to define ad hoc validators, so we're going to add age here, which is going to define an arbitrary validator, which is just a function that returns if you convert age or the input to a number that it's greater than or equal to 21.
We're going to want to come down here and we'll add this expectation, so we'll say it adds custom validations, so person.age is 21. We'll validate age, and expect it to be true, and then when we call age is 20, validate age, and we'll expect that to be false.
If we recall back in our BCvalidatable.field implementation, if we didn't find a validator because it wasn't registered, we're going to create a new validator, and we're going to pass it the options, and we're going to pass it the validation name, so we're going to want to come back to our validator and we're going to want our validator now to accept the name of a validation here.
We're going to say if validation function.validator, and the reason we're saying that is because in our spec, the way we've defined this was that legal was defining an anonymous validator on this validator key here, so we're going to want to use that validator key here, and we'll say return new anonymous validator, and we're going to pass in the validation function and the name.
Then here we'll create a function for an anonymous validator that takes a validation function and a name attribute. There we go, so if is function, as long as our validation function.validator is a function, and actually what I want to do here is rename these options, because that's actually what these are.
Remember, validation function.validator is an option on the set of options that we passed in, so this is actually more descriptive of what we have, so that'll make more sense when we do this, so we'll say if options.message, then we want options.validator.message to equal options.message.
Because, remember, we want our validation function to have that message assigned to it, so we'll just pull it off of the rest of the options and we'll return new validator, passing in options.validator and the name here.
Now that we have that, since our validation function is actually unnamed, we'll set the default name here to name, and that way we'll grab the name legal, because this is an unnamed anonymous function, so that will allow us to register a named validator using name here.
Here we're failing still, and that's actually just because we made a few silly errors earlier. I have age here nested under as a validation of name, which it isn't. It's another field, so actually age should be its own field, and that should do it for age. Then if we come down here, and we define age and a default age here, this will make sure that our age validation passes and our tests pass.
Next I just want to add some declarative tests. These should pass based on everything we've written so far, but this will just make sure that we explain that we're not going to add errors when we have a valid instance and that we'll remove previously invalid fields, so if we have an invalid field, and then we make that field valid, that the count for errors will be zero at that point.
Finally, I just want to add some semantic sugar-type methods, so we'll say sets $valid is a property of the instance, so calling $valid will run all of the validators, and if they return true then this will be true, and invalid will just be the inverse of that.
We'll head back to validatable because these are going to need to be added to our instance, so we'll say object.defineProperty this __$valid, because it's an instance property, and it's just going to be a getter, so every time we get it, it will return this.constructor.validations.validate this, and we don't need to pass in a field name because it will run all of the validations, so that makes one of our tests pass.
Then we want to say invalid, and we'll return the opposite of that, so that will make all of our tests pass, so now we've completely implemented the entire validatable library, and at the bottom here, the very end of what we've done, we've really laid out the foundation, and it kind of serves as documentation for this portion of the library.
This is the way that we expect this part of the library to be used. Someone will declare validations on a model. Each of those validations will be added to that model, and then for each instance that we have for that, whenever we call validate, we will add errors to that instances errors hash.
Each of those errors keys will be an array containing the errors that instance currently has. We'll be able to get true/false for a field or for the entire instance as to whether or not that instance or field is valid. We'll have a count of the errors exposed, and then we'll have these $valid and $invalid helpers to assist us along.
Offscreen I'm going to also add a number of built-in helpers that you can take a look at to see further how we can work with our validates library, or validatable library, and that concludes this portion of building the base class.