Popularity
1.9
Stable
Activity
2.8
Growing
42
3
2

Programming language: Scala
License: MIT License

Dupin alternatives and similar packages

Based on the "Data Binding and Validation" category.
Alternatively, view Dupin alternatives based on common mentions on social networks and blogs.

Do you think we are missing an alternative of Dupin or a related project?

Add another 'Data Binding and Validation' Package

README

Dupin

Maven Central Sonatype Nexus (Snapshots) Build Status codecov.io License: MIT

Dupin is a minimal, idiomatic, customizable validation for Scala.

Table of contents

  1. Motivation
  2. Quick start
  3. Derivation
  4. Predefined validators
  5. Customization
    1. Message customization
    2. Kind customization
    3. Custom validating package
  6. Integration example
  7. Notes

Motivation

You may find Dupin useful if you want to...

  • return something richer than String as validation message
  • use custom data kind for validation (Future, IO, etc...)
  • reuse validation parts across whole project

Quick start

Add dupin dependency to the build file, let's assume you are using sbt:

libraryDependencies += Seq(
  "com.github.yakivy" %% "dupin-core" % "0.1.4"
)

Describe the domain:

case class Name(value: String)
case class Member(name: Name, age: Int)
case class Team(name: Name, members: Seq[Member])

Define some validators:

import dupin.all._
import cats.implicits._

implicit val nameValidator = BaseValidator[Name]
    .root(_.value.nonEmpty, _.path + " should be non empty")

implicit val memberValidator = BaseValidator[Member]
    .combineP(_.name)(nameValidator)
    .combinePR(_.age)(a => a > 18 && a < 40, _.path + " should be between 18 and 40")

implicit val teamValidator = BaseValidator[Team]
    .combinePI(_.name)
    .combineP(_.members)(element(memberValidator))
    .combineR(_.members.size <= 8, _ => "team should be fed with two pizzas!")

Validate them all:

import dupin.all._

val validTeam = Team(
    Name("Bears"),
    List(
        Member(Name("Yakiv"), 26),
        Member(Name("Myroslav"), 31),
        Member(Name("Andrii"), 25)
    )
)

val invalidTeam = Team(
    Name(""),
    Member(Name(""), 0) :: (1 to 10).map(_ => Member(Name("valid name"), 20)).toList
)

val a = validTeam.validate.either
val b = validTeam.isValid
val c = invalidTeam.validate.list

assert(a == Right(validTeam))
assert(b)
assert(c == List(
    ".name should be non empty",
    ".members.[0].name should be non empty",
    ".members.[0].age should be between 18 and 40",
    "team should be fed with two pizzas!"
))

Derivation

If you are a fan of value classes or self descriptive types, validators can be easily derived:

implicit val nameValidator = BaseValidator[Name]
    .root(_.value.nonEmpty, _.path + " should be non empty")
implicit val ageValidator = BaseValidator[Int]
    .root(a => a > 18 && a < 40, _.path + " should be between 18 and 40")

implicit val memberValidator = BaseValidator[Member].derive

Validation messages will look like:

val validMember = Member(Name("Yakiv"), 27)
val invalidMember = Member(Name(""), 0)

val validationResult = validMember.isValid
val messages = invalidMember.validate.list

assert(validationResult)
assert(messages == List(
    ".name should be non empty",
    ".age should be between 18 and 40",
))

Predefined validators

The more validators you have, the more logic can be reused without writing validators from the scratch. Let's write common validators for minimum and maximum Int value:

import dupin.all._

def min(value: Int) = BaseValidator[Int].root(_ > value, _.path + " should be greater than " + value)
def max(value: Int) = BaseValidator[Int].root(_ < value, _.path + " should be less than " + value)

And since validators can be combined, you can create validators from other validators:

import dupin.all._

implicit val memberValidator = BaseValidator[Member].path(_.age)(min(18) && max(40))

val invalidMember = Member(Name("Ada"), 0)
val messages = invalidMember.validate.list

assert(messages == List(".age should be greater than 18"))

You can find full list of validators that provided out of the box in dupin.instances.DupinInstances

Message customization

But not many real projects use strings as validation messages, for example you want to support internationalization:

case class I18nMessage(
    description: String,
    key: String,
    params: List[String]
)

As BaseValidator[R] is just a type alias for Validator[String, R, Id], you can define own validator type with builder:

import dupin.all._

type I18nValidator[R] = Validator[I18nMessage, R, cats.Id]
def I18nValidator[R] = Validator[I18nMessage, R, cats.Id]

And start creating validators with custom messages:

import dupin.all._

implicit val nameValidator = I18nValidator[Name].root(_.value.nonEmpty, c => I18nMessage(
    c.path + " should be non empty",
    "validator.name.empty",
    List(c.path.toString())
))

implicit val memberValidator = I18nValidator[Member]
    .combinePI(_.name)
    .combinePR(_.age)(a => a > 18 && a < 40, c => I18nMessage(
        c.path + " should be between 18 and 40",
        "validator.member.age",
        List(c.path.toString())
    ))

Validation messages will look like:

import dupin.all._

val invalidMember = Member(Name(""), 0)
val messages: List[I18nMessage] = invalidMember.validate.list

assert(messages == List(
    I18nMessage(
        ".name should be non empty",
        "validator.name.empty",
        List(".name")
    ),
    I18nMessage(
        ".age should be between 18 and 40",
        "validator.member.age",
        List(".age")
    )
))

Kind customization

For example you want to allow only using of limited list of names and they are stored in the database:

import scala.concurrent.Future

class NameService {
    private val allowedNames = Set("Ada")
    def contains(name: String): Future[Boolean] =
        // Emulation of DB call
        Future.successful(allowedNames(name))
}

So to be able to handle checks that returns Future[Boolean], you just need to define your own validator type with builder:

import cats.Applicative
import dupin.all._
import scala.concurrent.Future

type FutureValidator[R] = Validator[String, R, Future]
def FutureValidator[R](implicit A: Applicative[Future]) = Validator[String, R, Future]

Then you can create validators with generic dsl (don't forget to import required type classes, as minimum Functor[Future]):

import cats.implicits._
import dupin.all._
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future

val nameService = new NameService

implicit val nameValidator = FutureValidator[Name].root(
    n => nameService.contains(n.value), _.path + " should be non empty"
)

implicit val memberValidator = FutureValidator[Member]
    .combinePI(_.name)
    .combinePR(_.age)(a => Future.successful(a > 18 && a < 40), _.path + " should be between 18 and 40")

Validation result will look like:

import cats.data.NonEmptyList
import cats.implicits._
import dupin.all._
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future

val invalidMember = Member(Name(""), 0)
val messages: Future[Either[NonEmptyList[String], Member]] = invalidMember.validate.map(_.either)

assert(Await.result(messages, Duration.Inf) == Left(NonEmptyList.of(
    ".name should be non empty",
    ".age should be between 18 and 40"
)))

Custom validating package

To avoid imports boilerplate and isolating all customizations you can define your own dupin package:

package dupin

import cats.Applicative
import cats.instances.FutureInstances
import dupin.instances.DupinInstances
import dupin.syntax.DupinSyntax
import scala.concurrent.Future

package object custom
    extends DupinCoreDsl with DupinInstances with DupinSyntax
        with FutureInstances {
    type CustomValidator[R] = Validator[I18nMessage, R, Future]
    def CustomValidator[R](implicit A: Applicative[Future]) = Validator[I18nMessage, R, Future]
}

Then you can start using your own validator type with single import:

import dupin.custom._
import scala.concurrent.ExecutionContext.Implicits.global

val nameService = new NameService

implicit val nameValidator = CustomValidator[Name](
    n => nameService.contains(n.value), c => I18nMessage(
        c.path + " should be non empty",
        "validator.name.empty",
        List(c.path.toString())
    )
)

val validName = Name("Ada")

assert(Await.result(validName.isValid, Duration.Inf))

Integration example

For example you are using play framework in your project, then instead of parsing dtos directly from JsValue, you can create your own wrapper around it and inject validation logic there, like:

import cats.data.NonEmptyList
import dupin.all._
import dupin.core.Success
import play.api.libs.json.JsValue
import play.api.libs.json.Reads

case class JsonContent(jsValue: JsValue) {
    def as[A: Reads](
        implicit validator: BaseValidator[A] = BaseValidator.success
    ): Either[NonEmptyList[String], A] = {
        jsValue.validate[A].asEither match {
            case Right(value) => validator.validate(value).either
            case Left(errors) => Left(NonEmptyList(errors.toString, Nil))
        }
    }
}

= BaseValidator.success - will allow you to successfully parse dtos that don't have validator.


*Note that all licence references and agreements mentioned in the Dupin README section above are relevant to that project's source code only.