Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/arturopala/validator
Simple but versatile Scala validator using Either[Error,Unit] to represent validation status.
https://github.com/arturopala/validator
scala validation
Last synced: about 14 hours ago
JSON representation
Simple but versatile Scala validator using Either[Error,Unit] to represent validation status.
- Host: GitHub
- URL: https://github.com/arturopala/validator
- Owner: arturopala
- License: apache-2.0
- Created: 2021-11-23T23:36:07.000Z (almost 3 years ago)
- Default Branch: main
- Last Pushed: 2023-12-07T11:07:28.000Z (11 months ago)
- Last Synced: 2024-05-09T07:10:48.794Z (6 months ago)
- Topics: scala, validation
- Language: Scala
- Homepage:
- Size: 1.27 MB
- Stars: 6
- Watchers: 2
- Forks: 0
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
[![Build and test](https://github.com/arturopala/validator/actions/workflows/build.yml/badge.svg)](https://github.com/arturopala/validator/actions/workflows/build.yml)
[![Maven Central](https://img.shields.io/maven-central/v/com.github.arturopala/validator_2.13.svg?label=Maven%20Central)](https://search.maven.org/search?q=g:%22com.github.arturopala%22%20AND%20a:%22validator_2.13%22)
[![Scala.js](https://www.scala-js.org/assets/badges/scalajs-1.12.0.svg)](https://www.scala-js.org)
![Code size](https://img.shields.io/github/languages/code-size/arturopala/validator)
![GitHub](https://img.shields.io/github/license/arturopala/validator)
![Lift](https://lift.sonatype.com/api/badge/github.com/arturopala/validator)Validator
===This is a micro-library for Scala
"com.github.arturopala" %% "validator" % "0.22.0"
//> using "com.github.arturopala::validator:0.22.0"
Cross-compiles to Scala versions `2.13.12`, `2.12.18`, `3.3.1`,
and ScalaJS version `1.14.0`, and ScalaNative version `0.4.16`.Motivation
---
Writing validation rules for the complex data structure is an everyday business developer's life. There are multiple ways available in Scala to do the validation but still one of the simplest ways to represent and manipulate validation results is to use the built-in `Either`.This library provides a thin wrapper around `Either` with a simpler API and opinionated type parameters.
Here the validator is represented by the function type alias:
sealed trait Error
type Result = Either[Error, Unit]type Validate[-T] = T => Result
The rest of the API is focused on creating and composing instances of `Validate[T]`.
Scaladoc
---Try in Scastie!
---Simple example
---```scala
import com.github.arturopala.validator.Validator._case class Address(
street: String,
town: String,
postcode: String,
country: String
)
case class PhoneNumber(
prefix: String,
number: String,
description: String
)
case class Contact(
name: String,
address: Either[String, Address],
phoneNumbers: Seq[PhoneNumber] = Seq.empty,
email: Option[String] = None
)object Country {
val codes = Set("en", "de", "fr")
val telephonePrefixes = Set("+44", "+41", "+42")
}val validatePostcode =
checkIsTrue[String](_.matches("""\d{5}"""), "address.postcode.invalid")
// validatePostcode: Validate[String] = com.github.arturopala.validator.Validator$$$Lambda$9449/0x000000080269c840@72e2046cval validateCountry =
checkIsTrue[String](_.isOneOf(Country.codes), "address.country.invalid")
// validateCountry: Validate[String] = com.github.arturopala.validator.Validator$$$Lambda$9449/0x000000080269c840@be576feval validatePhoneNumberPrefix =
checkIsTrue[String](
_.isOneOf(Country.telephonePrefixes),
"address.phone.prefix.invalid"
)
// validatePhoneNumberPrefix: Validate[String] = com.github.arturopala.validator.Validator$$$Lambda$9449/0x000000080269c840@5ff82116val validatePhoneNumberValue =
checkIsTrue[String](_.matches("""\d{7}"""), "address.phone.prefix.invalid")
// validatePhoneNumberValue: Validate[String] = com.github.arturopala.validator.Validator$$$Lambda$9449/0x000000080269c840@7480a87aval validatePhoneNumber =
all[PhoneNumber](
checkProp(_.prefix, validatePhoneNumberPrefix),
checkProp(_.number, validatePhoneNumberValue)
)
// validatePhoneNumber: PhoneNumber => Either[Error, Unit] = com.github.arturopala.validator.Validator$$$Lambda$9451/0x000000080269f040@294801f5val validateEmail =
all[String](
checkIsTrue[String](_.contains("@"), "address.email.invalid")
)
// validateEmail: Validate[String] = com.github.arturopala.validator.Validator$$$Lambda$9451/0x000000080269f040@9bac0a4val validateAddress =
all[Address](
checkIsTrue(_.street.nonEmpty, "address.street.empty"),
checkIsTrue(_.town.nonEmpty, "address.town.empty"),
checkProp(_.postcode, validatePostcode),
checkProp(_.country, validateCountry)
)
// validateAddress: Address => Either[Error, Unit] = com.github.arturopala.validator.Validator$$$Lambda$9451/0x000000080269f040@1754a014val validateContact =
all[Contact](
checkIsTrue(_.name.nonEmpty, "contact.name.empty"),
checkEither(_.address, validateStringNonEmpty("address.manual.empty") , validateAddress),
any(
checkIfSome(_.email, validateEmail, isValidIfNone = false),
all(
checkProp(_.phoneNumbers, validateCollectionNonEmpty("address.phoneNumbers.empty")),
checkEach(_.phoneNumbers, validatePhoneNumber)
)
)
)
// validateContact: Contact => Either[Error, Unit] = com.github.arturopala.validator.Validator$$$Lambda$9451/0x000000080269f040@666619fc// TEST
val c1 = Contact(
name = "Foo Bar",
address = Right(Address(
street = "Sesame Street 1",
town = "Cookieburgh",
country = "en",
postcode = "00001"
)),
phoneNumbers = Seq(
PhoneNumber("+44", "1234567", "ceo"),
PhoneNumber("+41", "7654321", "sales")
)
)
// c1: Contact = Contact(
// name = "Foo Bar",
// address = Right(
// value = Address(
// street = "Sesame Street 1",
// town = "Cookieburgh",
// postcode = "00001",
// country = "en"
// )
// ),
// phoneNumbers = List(
// PhoneNumber(prefix = "+44", number = "1234567", description = "ceo"),
// PhoneNumber(prefix = "+41", number = "7654321", description = "sales")
// ),
// email = None
// )validateContact(c1)
// res0: Either[Error, Unit] = Right(value = ())val c2 = Contact(
name = "",
address =
Right(Address(street = "", town = "", country = "ca", postcode = "foobar")),
phoneNumbers = Seq(
PhoneNumber("+1", "11111111111", "ceo"),
PhoneNumber("+01", "00000000", "sales")
)
)
// c2: Contact = Contact(
// name = "",
// address = Right(
// value = Address(street = "", town = "", postcode = "foobar", country = "ca")
// ),
// phoneNumbers = List(
// PhoneNumber(prefix = "+1", number = "11111111111", description = "ceo"),
// PhoneNumber(prefix = "+01", number = "00000000", description = "sales")
// ),
// email = None
// )validateContact(c2).isValid
// res1: Boolean = false
validateContact(c2).errorsOption
// res2: Option[Seq[String]] = Some(
// value = List(
// "contact.name.empty",
// "address.street.empty",
// "address.town.empty",
// "address.postcode.invalid",
// "address.country.invalid",
// "Expected Some value but got None",
// "address.phone.prefix.invalid"
// )
// )val c3 = Contact(
name = "Alice",
address =
Left("1 Home Av. Daisytown CA"),
email = Some("alice@home")
)
// c3: Contact = Contact(
// name = "Alice",
// address = Left(value = "1 Home Av. Daisytown CA"),
// phoneNumbers = List(),
// email = Some(value = "alice@home")
// )validateContact(c3).isValid
// res3: Boolean = true
validateContact(c3).errorsOption
// res4: Option[Seq[String]] = Noneval c4 = Contact(
name = "",
address =
Left(""),
email = Some("alice.home")
)
// c4: Contact = Contact(
// name = "",
// address = Left(value = ""),
// phoneNumbers = List(),
// email = Some(value = "alice.home")
// )validateContact(c4).isValid
// res5: Boolean = false
validateContact(c4).errorsOption
// res6: Option[Seq[String]] = Some(
// value = List(
// "contact.name.empty",
// "address.manual.empty",
// "address.email.invalid",
// "address.phoneNumbers.empty"
// )
// )
```All batteries included
---```scala
import com.github.arturopala.validator.Validator._case class E(a: Int, b: String, c: Option[Int], d: Seq[Int], e: Either[String,E], f: Option[Seq[Int]], g: Boolean, h: Option[String])
val divisibleByThree = checkIsTrue[Int](_ % 3 == 0, "must be divisible by three")
// divisibleByThree: Validate[Int] = com.github.arturopala.validator.Validator$$$Lambda$9449/0x000000080269c840@5a560ad9val validateE: Validate[E] = any[E](
checkEquals(_.a.toString, _.b, "a must be same as b"),
checkNotEquals(_.a.toString, _.b, "a must be different to b"),
checkFromEither(_.e),
checkIsDefined(_.c, "c must be defined"),
checkIsEmpty(_.c, "c must be not defined"),
checkWith(_.a, divisibleByThree),
checkIfSome(_.c, divisibleByThree, isValidIfNone = true),
all(
checkIfSome(_.c, divisibleByThree, isValidIfNone = false),
checkEach(_.d, divisibleByThree),
checkEachIfNonEmpty(_.d, divisibleByThree),
checkEachIfSome(_.f, divisibleByThree),
),
checkIfAllDefined(Seq(_.c, _.f),"c and f must be all defined"),
checkIfAllEmpty(Seq(_.c, _.f),"c and f must be all empty"),
checkIfAllOrNoneDefined(Seq(_.c, _.f),"c and f must be either all defined or all empty"),
checkIfAtLeastOneIsDefined(Seq(_.c,_.f),"c or f or both must be defined"),
checkIfAtMostOneIsDefined(Seq(_.c,_.f),"none or c or f must be defined"),
checkIfOnlyOneIsDefined(Seq(_.c,_.f),"c or f must be defined"),
checkIfOnlyOneSetIsDefined[E](Seq(Set(_.c,_.f), Set(_.c,_.h)),"only (c and f) or (c and h) must be defined"),
checkIfAllTrue(Seq(_.a.inRange(0,10), _.g),"a must be 0..10 if g is true"),
checkIfAllFalse(Seq(_.a.inRange(0,10), _.g),"a must not be 0..10 if g is false"),
checkIfAtLeastOneIsTrue(Seq(_.a.inRange(0,10), _.g),"a must not be 0..10 or g or both must be true"),
checkIfAtMostOneIsTrue(Seq(_.a.inRange(0,10), _.g),"none or a must not be 0..10 or g must be true"),
checkIfOnlyOneIsTrue(Seq(_.a.inRange(0,10), _.g),"a must not be 0..10 or g must be true"),
checkIfOnlyOneSetIsTrue[E](Seq(Set(_.a.inRange(0,10), _.g), Set(_.g,_.h.isDefined)),"only (g and a must not be 0..10) or (g and h.isDefined) must be true"),
)
// validateE: Validate[E] = com.github.arturopala.validator.Validator$$$Lambda$9457/0x00000008026ab840@bba516d
```Usage
---Create a simple validator:
```scala
import com.github.arturopala.validator.Validator._val validateIsEven: Validate[Int] =
checkIsTrue[Int](_ % 2 == 0, "must be even integer")val validateIsNonEmpty: Validate[String] =
checkIsTrue[String](_.nonEmpty, "must be non-empty string")val validateStringLengthPair: Validate[(String,Int)] =
checkIsTrue[(String,Int)]({case (s,l) => s.length() == l}, "string must be of expected length")
```and run it with the tested value:
```scala
validateIsEven(2).isValid
// res7: Boolean = true
validateIsEven(1).isInvalid
// res8: Boolean = truevalidateIsNonEmpty("").isInvalid
// res9: Boolean = true
validateIsNonEmpty("abc").isValid
// res10: Boolean = truevalidateStringLengthPair(("abc",3)).isValid
// res11: Boolean = true
validateStringLengthPair(("ab",1)).isInvalid
// res12: Boolean = true
```Validators can be combined using different strategies:
```scala
val validateIsPositive: Validate[Int] =
checkIsTrue[Int](_ > 0, "must be positive integer")
// combine using ANY to validate whether all checks pass
val validateIsEvenAndPositive: Validate[Int] =
all(
validateIsEven,
validateIsPositive
)// or use an infix operator &
val evenAndPositive = validateIsEven & validateIsPositive// combine using ANY to validate whether any check passes
val validateIsEvenOrPositive: Validate[Int] =
any(
validateIsEven,
validateIsPositive
)// or use an infix operator |
val evenOrPositive = validateIsEven | validateIsPositive// combine using PRODUCT to validate a pair
val validateIsEvenPositivePair: Validate[(Int,Int)] =
product(
validateIsEven,
validateIsPositive
)// or use an infix operator *
val evenPositivePair = validateIsEven * validateIsPositive```
```scala
evenAndPositive(2).isValid
// res13: Boolean = true
evenAndPositive(1).isInvalid
// res14: Boolean = true
evenAndPositive(-1).isInvalid
// res15: Boolean = true
evenAndPositive(-2).isInvalid
// res16: Boolean = trueevenOrPositive(2).isValid
// res17: Boolean = true
evenOrPositive(1).isValid
// res18: Boolean = true
evenOrPositive(-1).isInvalid
// res19: Boolean = true
evenOrPositive(-2).isValid
// res20: Boolean = trueevenPositivePair((2,1)).isValid
// res21: Boolean = true
evenPositivePair((1,2)).isInvalid
// res22: Boolean = true
```Validate objects using `checkWith`, `checkIfSome`, `checkEach`, `checkEachIfSome`, etc.:
```scala
case class Foo(a: String, b: Option[Int], c: Boolean, d: Seq[String], e: Bar)
case class Bar(f: BigDecimal, h: Option[Seq[Int]])val validateBar: Validate[Bar] = all[Bar](
checkIsTrue(_.f.inRange(0,100),".f must be in range 0..100 inclusive"),
checkEachIfSomeWithErrorPrefix(_.h, validateIsEvenAndPositive, i => s".h[$i] ", isValidIfNone = false)
).withErrorPrefix("[Bar]")val prefix: AnyRef => String = o => s"[${o.getClass.getSimpleName}]"
val validateFoo: Validate[Foo] = all[Foo](
checkWith(_.a, validateIsNonEmpty),
checkIsTrue(_.a.matches("[A-Z]\\d{3,5}"),".a must follow pattern [A-Z]\\d{3,5}"),
checkIfSome[Foo,Int](_.b, evenOrPositive, isValidIfNone = true).withErrorPrefix(".b"),
conditionally(
_.c,
checkEachWithErrorPrefix(_.d, validateIsNonEmpty & checkIsTrue[String](_.lengthMax(64),"64 characters maximum"),
i => s".d[$i] "),
checkWith[Foo,Bar](_.e, validateBar).withErrorPrefix(".e")
)
).withErrorPrefixComputed(prefix)
```
```scala
validateFoo(Foo("X678",Some(2),true,Seq("abc"),Bar(500,Some(Seq(8)))))
// res23: Result = Right(value = ())
validateFoo(Foo("X67",Some(-1),true,Seq("abc",""),Bar(500,Some(Seq(7)))))
// res24: Result = Left(
// value = And(
// errors = List(
// Single(message = "[Foo].a must follow pattern [A-Z]\\d{3,5}"),
// Or(
// errors = List(
// Single(message = "[Foo].bmust be even integer"),
// Single(message = "[Foo].bmust be positive integer")
// )
// ),
// Single(message = "[Foo].d[1] must be non-empty string")
// )
// )
// )
validateFoo(Foo("X678",Some(2),false,Seq("abc"),Bar(99,None)))
// res25: Result = Left(
// value = Single(message = "[Foo].e[Bar]Expected Some sequence but got None")
// )
validateFoo(Foo("X",Some(3),false,Seq("abc",""),Bar(-1,Some(Seq(7,8,9)))))
// res26: Result = Left(
// value = And(
// errors = List(
// Single(message = "[Foo].a must follow pattern [A-Z]\\d{3,5}"),
// Single(message = "[Foo].e[Bar].f must be in range 0..100 inclusive"),
// Single(message = "[Foo].e[Bar].h[0] must be even integer"),
// Single(message = "[Foo].e[Bar].h[2] must be even integer")
// )
// )
// )
```Tag validator with prefix:
```scala
evenOrPositive.apply(-1).errorsSummaryOption
// res27: Option[String] = Some(
// value = "must be even integer or must be positive integer"
// )
("prefix: " @: evenOrPositive).apply(-1).errorsSummaryOption
// res28: Option[String] = Some(
// value = "prefix: must be even integer or prefix: must be positive integer"
// )
evenOrPositive.withErrorPrefix("foo_").apply(-1).errorsSummaryOption
// res29: Option[String] = Some(
// value = "foo_must be even integer or foo_must be positive integer"
// )
evenOrPositive.withErrorPrefixComputed(i => s"($i) ").apply(-1).errorsSummaryOption
// res30: Option[String] = Some(
// value = "(-1) must be even integer or (-1) must be positive integer"
// )
```Debug validator:
```scala
// debug input and output
validateFoo.debug.apply(Foo("X678",Some(2),true,Seq("abc"),Bar(500,Some(Seq(8)))))
// Foo(X678,Some(2),true,List(abc),Bar(500,Some(List(8)))) => Valid
// res31: Result = Right(value = ())
// debug only output
validateFoo.apply(Foo("X678",Some(2),true,Seq("abc"),Bar(500,Some(Seq(8))))).debug
// Valid
// res32: Result = Right(value = ())
```Development
---Compile
sbt compile
Compile for all Scala versions
sbt +compile
Test
sbt +test
sbt rootJVM/test
sbt rootJS/test
sbt rootNative/testTest with all Scala versions
sbt +test
sbt +rootJVM/testGenerate README and docs
sbt docs/mdoc
Apply scalafixes
sbt rootJMV/scalafixAll