-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Validated beginners doc #1903
Validated beginners doc #1903
Changes from 1 commit
ef08641
650c537
12f911d
32588fe
78b9504
8172251
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,7 +27,7 @@ Signature of the structure is as follows: | |
|
||
```scala | ||
sealed abstract class Validated[+E, +A] extends Product with Serializable { | ||
// Implementation elided | ||
// Implementation elided | ||
} | ||
``` | ||
|
||
|
@@ -123,23 +123,28 @@ Well, yes, but the error reporting part will have the downside of showing only t | |
Let's look in detail this part: | ||
|
||
```tut:silent:fail | ||
for { | ||
validatedUserName <- validateUserName(username) | ||
validatedPassword <- validatePassword(password) | ||
validatedFirstName <- validateFirstName(firstName) | ||
validatedLastName <- validateLastName(lastName) | ||
validatedAge <- validateAge(age) | ||
} | ||
yield RegistrationData(validatedUserName, validatedPassword, validatedFirstName, validatedLastName, validatedAge) | ||
} | ||
for { | ||
validatedUserName <- validateUserName(username) | ||
validatedPassword <- validatePassword(password) | ||
validatedFirstName <- validateFirstName(firstName) | ||
validatedLastName <- validateLastName(lastName) | ||
validatedAge <- validateAge(age) | ||
} | ||
yield RegistrationData(validatedUserName, validatedPassword, validatedFirstName, validatedLastName, validatedAge) | ||
``` | ||
|
||
A for-comprehension is _fail-fast_. If some of the evaluations in the `for` block fails for some reason, the `yield` statement will not complete. In our case, if that happens we won't be getting the accumulated list of errors. | ||
|
||
If we run our code: | ||
|
||
```tut:book | ||
FormValidator.validateForm("fakeUs3rname", "password", "John", "Doe", 15) | ||
FormValidator.validateForm( | ||
username = "fakeUs3rname", | ||
password = "password", | ||
firstName = "John", | ||
lastName = "Doe", | ||
age = 15 | ||
) | ||
``` | ||
|
||
We should have gotten another `DomainValidation` object denoting the invalid age. | ||
|
@@ -153,40 +158,38 @@ import cats.data._ | |
import cats.data.Validated._ | ||
import cats.implicits._ | ||
|
||
def validateUserName(userName: String): Validated[DomainValidation, String] = { | ||
if (userName.matches("^[a-zA-Z0-9]+$")) Valid(userName) else Invalid(UsernameHasSpecialCharacters) | ||
} | ||
def validateUserName(userName: String): Validated[DomainValidation, String] = { | ||
if (userName.matches("^[a-zA-Z0-9]+$")) Valid(userName) else Invalid(UsernameHasSpecialCharacters) | ||
} | ||
|
||
def validatePassword(password: String): Validated[DomainValidation, String] = { | ||
if (password.matches("(?=^.{10,}$)((?=.*\\d)|(?=.*\\W+))(?![.\\n])(?=.*[A-Z])(?=.*[a-z]).*$")) Valid(password) | ||
else Invalid(PasswordDoesNotMeetCriteria) | ||
} | ||
def validatePassword(password: String): Validated[DomainValidation, String] = { | ||
if (password.matches("(?=^.{10,}$)((?=.*\\d)|(?=.*\\W+))(?![.\\n])(?=.*[A-Z])(?=.*[a-z]).*$")) Valid(password) | ||
else Invalid(PasswordDoesNotMeetCriteria) | ||
} | ||
|
||
def validateFirstName(firstName: String): Validated[DomainValidation, String] = { | ||
if (firstName.matches("^[a-zA-Z]+$")) Valid(firstName) else Invalid(FirstNameHasSpecialCharacters) | ||
def validateFirstName(firstName: String): Validated[DomainValidation, String] = { | ||
if (firstName.matches("^[a-zA-Z]+$")) Valid(firstName) else Invalid(FirstNameHasSpecialCharacters) | ||
} | ||
|
||
def validateLastName(lastName: String): Validated[DomainValidation, String] = { | ||
if (lastName.matches("^[a-zA-Z]+$")) Valid(lastName) else Invalid(LastNameHasSpecialCharacters) | ||
def validateLastName(lastName: String): Validated[DomainValidation, String] = { | ||
if (lastName.matches("^[a-zA-Z]+$")) Valid(lastName) else Invalid(LastNameHasSpecialCharacters) | ||
} | ||
|
||
def validateAge(age: Int): Validated[DomainValidation, Int] = { | ||
if (age >= 18 && age <= 75) Valid(age) else Invalid(AgeIsInvalid) | ||
def validateAge(age: Int): Validated[DomainValidation, Int] = { | ||
if (age >= 18 && age <= 75) Valid(age) else Invalid(AgeIsInvalid) | ||
} | ||
``` | ||
```tut:book:fail | ||
def validateForm(username: String, password: String, firstName: String, lastName: String, age: Int): Validated[DomainValidation, RegistrationData] = { | ||
|
||
for { | ||
validatedUserName <- validateUserName(username) | ||
validatedPassword <- validatePassword(password) | ||
validatedFirstName <- validateFirstName(firstName) | ||
validatedLastName <- validateLastName(lastName) | ||
validatedAge <- validateAge(age) | ||
} | ||
yield RegistrationData(validatedUserName, validatedPassword, validatedFirstName, validatedLastName, validatedAge) | ||
def validateForm(username: String, password: String, firstName: String, lastName: String, age: Int): Validated[DomainValidation, RegistrationData] = { | ||
for { | ||
validatedUserName <- validateUserName(username) | ||
validatedPassword <- validatePassword(password) | ||
validatedFirstName <- validateFirstName(firstName) | ||
validatedLastName <- validateLastName(lastName) | ||
validatedAge <- validateAge(age) | ||
} | ||
|
||
yield RegistrationData(validatedUserName, validatedPassword, validatedFirstName, validatedLastName, validatedAge) | ||
} | ||
``` | ||
|
||
Looks similar to the first version. What we've done here was to use `Validated` instead of `Either`. Please note that our `Right` is now a `Valid` and `Left` is an `Invalid`. | ||
|
@@ -256,47 +259,97 @@ For example: | |
|
||
```tut:book | ||
FormValidatorNel.validateForm( | ||
username = "Joe", | ||
password = "Passw0r$1234", | ||
firstName = "John", | ||
lastName = "Doe", | ||
age = 21 | ||
) | ||
username = "Joe", | ||
password = "Passw0r$1234", | ||
firstName = "John", | ||
lastName = "Doe", | ||
age = 21 | ||
) | ||
|
||
FormValidatorNel.validateForm( | ||
username = "Joe%%%", | ||
password = "password", | ||
firstName = "John", | ||
lastName = "Doe", | ||
age = 21 | ||
) | ||
username = "Joe%%%", | ||
password = "password", | ||
firstName = "John", | ||
lastName = "Doe", | ||
age = 21 | ||
) | ||
``` | ||
|
||
Sweet success! Now you can take your validation process to the next level! | ||
|
||
### Coming from `Either`-based validation | ||
### A short detour | ||
|
||
Typically, you'll see that `Validated` will be accompanied by a `NonEmptyList` when it comes to accumulation. The thing here is that you can define your own accumulative data structure and you're not limited to the aforementioned construction. | ||
|
||
For doing this, you have to provide a `Semigroup` instance. `NonEmptyList`, by definition has its own `Semigroup`. For those who don't know what a `Semigroup` is, let's see a simple example. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is really great, thank you! However, you didn't really include an example? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It was more about how |
||
|
||
#### Accumulative Structures | ||
|
||
According to [Wikipedia](https://en.wikipedia.org/wiki/Semigroup): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of linking to wikipedia, I think the link to cats documentation should be enough :) What do you think? :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sounds good! I'll fix it. |
||
|
||
> A semigroup is an algebraic structure consisting of a set together with an associative binary operation. | ||
|
||
You can find more about how `Semigroup` works in cats [here](../typeclasses/semigroup.html). | ||
|
||
Let's take a look at `ap` method of `Validated`: | ||
|
||
```tut:silent:fail | ||
/** | ||
* From Apply: | ||
* if both the function and this value are Valid, apply the function | ||
*/ | ||
def ap[EE >: E, B](f: Validated[EE, A => B])(implicit EE: Semigroup[EE]): Validated[EE, B] = | ||
(this, f) match { | ||
// ... | ||
case (Invalid(e1), Invalid(e2)) => Invalid(EE.combine(e2, e1)) | ||
// ... | ||
} | ||
} | ||
``` | ||
|
||
We've omitted the complete implementation because our focus here is the case in where you need to append (that's the function of this method) two failures. Note the `implicit EE: Semigroup[EE]` parameter and the usage of its `.combine` operation. In the case of `NonEmptyList`, we're talking about a `List`, with certain properties that allow us to _combine_ (append) more than one element to it. That's because, apart from the fact that it is a `List`, it also has an instance of `Semigroup`, telling it how to operate with the accumulation. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I find this section a bit confusing. Maybe something like :
I don't think we need the part about Something like : NonEmptyList.one("error 1") |+| NonEmptyList("error 2", "error 3")
"error 1".invalidNel[Int] |+| "error 2".invalidNel
("error 1".invalidNel[Int], "error 2".invalidNel[Int]).mapN(_ + _) |
||
|
||
As we've said before: if you need another data type for processing the failures, you can use it, providing an instance of a `Semigroup` with the `.combine` logic. | ||
|
||
### Going back and forth | ||
|
||
cats offer you a nice set of combinators to transform your `Validated` based approach to an `Either` one and vice-versa. | ||
Please note that, if you're using an `Either`-based approach as seen in our first example and you choose to convert it to a `Validated` one, you're constrained to the fail-fast nature of `Either`, but you're gaining a broader set of features with `Validated`. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What do you mean with
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the section in where I have doubts about it. I was trying to express that, coming from With your previous comments, I have an idea for simplifying this section (or even delete it). Let me work on it and I'll reach you out again :) |
||
|
||
#### From `Validated` to `Either` | ||
|
||
To do this, simply use `.toEither` combinator: | ||
|
||
```tut:book | ||
FormValidatorNel.validateForm( | ||
username = "Joe", | ||
password = "Passw0r$1234", | ||
firstName = "John", | ||
lastName = "Doe", | ||
age = 21 | ||
).toEither | ||
``` | ||
|
||
cats offer you a nice set of combinators to transform your `Either` based approach to a `Validated` one and vice-versa. | ||
Please note that, if you're using an `Either`-based approach as seen in our first example, you're constrained to the fail-fast nature of `Either`, but you're gaining a broader set of features with `Validated`. | ||
#### From `Either` to `Validated` | ||
|
||
To do this, you'll need to use either `.toValidated` or `.toValidatedNel`. Let's see an example: | ||
To do this, you'll need to use either `.toValidated` or `.toValidatedNel`: | ||
|
||
```tut:book | ||
FormValidator.validateForm( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure how good this example really is, the fail-fast behaviour of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was thinking the same. I've done this little example for making use of |
||
username = "MrJohnDoe$$", | ||
password = "password", | ||
firstName = "John", | ||
lastName = "Doe", | ||
age = 31 | ||
).toValidated | ||
username = "MrJohnDoe$$", | ||
password = "password", | ||
firstName = "John", | ||
lastName = "Doe", | ||
age = 31 | ||
).toValidated | ||
|
||
FormValidator.validateForm( | ||
username = "MrJohnDoe$$", | ||
password = "password", | ||
firstName = "John", | ||
lastName = "Doe", | ||
age = 31 | ||
).toValidatedNel | ||
username = "MrJohnDoe$$", | ||
password = "password", | ||
firstName = "John", | ||
lastName = "Doe", | ||
age = 31 | ||
).toValidatedNel | ||
``` | ||
|
||
The difference between the previous examples is that `.toValidated` gives you an `Invalid` instance in case of failure. Meanwhile, `.toValidatedNel` will give you a `NonEmptyList` with the possible failures. Don't forget about the caveat with `Either`-based approaches, mentioned before. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe reuse the
Either
methods here and calltoValidated
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good suggestion. I'll take a look at it!