Skip to content

Commit

Permalink
0.4.7 (#33)
Browse files Browse the repository at this point in the history
* 0.4.7

- Added `ZValidationModule`
- Extracted some code to separate files
- Added `Rule.flatten`, `Rule.mapK`
- Improve ZIOInterop

* Refactoring
  • Loading branch information
0lejk4 authored Jul 7, 2022
1 parent d509c4e commit 40e1102
Show file tree
Hide file tree
Showing 35 changed files with 511 additions and 358 deletions.
17 changes: 8 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ Fields is a Scala validation library that you should use because it is:
# Teaser

```scala
implicit def policy(implicit tokenService: TokenService, userService: UserService): Policy[RegisterRequest] = Policy
// Here we pass validation dependencies using implicits, but you could be doing this the way you prefer
def policy(implicit tokenService: TokenService, userService: UserService): Policy[RegisterRequest] = Policy
.builder[RegisterRequest]
.subRule(_.age)(_ >= 18, _ <= 110)
.subRule(_.email)(_.map(_.value).matchesRegex(EmailRegex))
Expand All @@ -30,18 +31,16 @@ implicit def policy(implicit tokenService: TokenService, userService: UserServic
.subRule(_.token)(_.ensureF(tokenService.validateToken, _.failMessage("invalid-token")))
.build

def validateUsername(username: Field[String])(implicit userService: UserService): Rule[zio.Task, Accumulate, ValidationError] =
// Extract complex validations to reusable methods.
def validateUsername(username: Field[String])(implicit userService: UserService): MRule =
username.minSize(1) &&
username.maxSize(10) &&
Rule {
userService.findByUsername(usernameF.value).flatMap { (user: Option[User]) =>
user match {
case Some(_) => usernameF.failMessage("username-already-exists").effect
case None => Rule.valid.effect
}
MRule.flatten {
userService.findByUsername(username.value).map {
case Some(_) => username.failMessage("username-already-exists")
case None => MRule.valid
}
}

```

## Quicklinks
Expand Down
11 changes: 6 additions & 5 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,14 @@ lazy val scalaSettings = Seq(
case _ => Seq("-Xsource:3", "-P:kind-projector:underscore-placeholders")
}
},
)

lazy val commonSettings = Seq(
libraryDependencies += "org.scalameta" %% "munit" % V.MUnit % Test,
libraryDependencies ++= (
if (scalaVersion.value == V.Scala3) List()
else List(compilerPlugin("org.typelevel" % "kind-projector" % "0.13.2" cross CrossVersion.full))
),
)

lazy val commonSettings = Seq(
libraryDependencies += "org.scalameta" %% "munit" % V.MUnit % Test
) ++ scalaSettings

lazy val `fields-core` =
Expand Down Expand Up @@ -98,7 +98,8 @@ lazy val `fields-zio` =
lazy val examples =
(project in file("examples"))
.settings(
scalaVersion := editorScala,
scalaSettings,
crossScalaVersions := Nil,
publishArtifact := false,
libraryDependencies += "org.typelevel" %% "cats-effect" % "2.5.5",
)
Expand Down
2 changes: 1 addition & 1 deletion docs/validation-syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ mapF.anyValue(_ > 4)

```scala mdoc:reset:width=100
import jap.fields._
import jap.fields.ZioInterop._
import jap.fields.ZIOInterop._
import jap.fields.fail._
import jap.fields.error._
import zio._
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ case class ERR(code: Int)

object Validation {
import jap.fields.fail._
import jap.fields.typeclass._
import jap.fields.CatsInterop._
trait ErrFailWithInstance {
implicit object FailWithErr extends FailWith[ERR, Nothing] {
Expand Down
9 changes: 4 additions & 5 deletions examples/src/main/scala/examples/GithubExample.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,11 @@ package github
import jap.fields._

import zio._
import zio.console._

object Validation {
import jap.fields.error._
import jap.fields.fail._
import jap.fields.ZioInterop._
import jap.fields.ZIOInterop._
object all extends AccumulateVM[Task, ValidationError] with CanFailWithValidationError
}
import Validation.all.*
Expand All @@ -49,7 +48,7 @@ object AddStarCmd {
.build

/** Extracted for convenience */
def checkNotStarredRule(userF: Field[String])(project: Project) =
def checkNotStarredRule(userF: Field[String])(project: Project): MRule =
userF.ensure(
!project.stargazers.contains(_),
_.failMessage("user-already-starred-project"),
Expand All @@ -68,10 +67,10 @@ object AddStarCmd {
val projectDontExist = MRule.pure(V.traverse(organizationF, projectF)(_.failMessage("project-does-not-exist")))

/** We use Rule.apply cause we sure this is lazy */
val projectRule = Rule {
val projectRule = Rule.flatten {
api
.findProject(organizationF.value, projectF.value)
.flatMap(_.fold(projectDontExist)(checkNotStarredRule(userF)).effect)
.map(_.fold(projectDontExist)(checkNotStarredRule(userF)))
}

cmdF.validate && // Call validate to use base validations
Expand Down
25 changes: 15 additions & 10 deletions examples/src/main/scala/examples/I18NExample.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,15 @@ package jap.fields
package examples
package i18n

import cats.conversions.variance
import jap.fields.FieldPathConversions.*
import jap.fields.ZioInterop.*
import jap.fields.ZIOInterop.*
import jap.fields.*
import jap.fields.error.*
import jap.fields.fail.*
import jap.fields.typeclass.*
import zio.*
import zio.console.*

import java.time.*
import java.util.UUID

/** Simple ADT that we will interpret to construct localised message */
sealed trait TranslatedMessage
Expand Down Expand Up @@ -89,8 +86,8 @@ case class Post(
created: LocalDateTime,
modified: LocalDateTime,
)
object Post:
given Policy[Post] =
object Post {
implicit val policy: Policy[Post] =
Policy
.builder[Post]
.subRule(_.id)(
Expand All @@ -101,25 +98,31 @@ object Post:
.subRule(_.description)(_.some(_.all(_.minSize(5), _.maxSize(10))))
.subRule(_.created, _.modified)(_ <= _)
.build
}

case class Blog(posts: List[Post], authorId: Long)
object Blog:
given Policy[Blog] =
object Blog {
implicit val policy: Policy[Blog] =
Policy
.builder[Blog]
.subRule(_.authorId)(_ > 0L)
.subRule(_.posts)(_.each(_.validate))
.build
}

enum Locale { case EN, UA }
sealed trait Locale
object Locale {
case object EN extends Locale
case object UA extends Locale
}

final case class I18N(locales: Map[Locale, Map[String, String]]) {
def apply(key: String)(locale: Locale): String = locales(locale)(key)

def translateAll(locale: Locale)(errors: List[TranslatedError]): Task[List[ValidationError.Message]] =
Task.collectAll(errors.map(translate(locale)))

def translate(locale: Locale)(error: TranslatedError): Task[ValidationError.Message] =
def translate(locale: Locale)(error: TranslatedError): Task[ValidationError.Message] = {
def translateMessage(msg: TranslatedMessage): String =
msg match {
case TranslatedMessage.Key(key) => apply(key)(locale)
Expand All @@ -134,6 +137,8 @@ final case class I18N(locales: Map[Locale, Map[String, String]]) {
message = translateMessage(error.message),
)
)
}

}

object I18NExample extends zio.App {
Expand Down
66 changes: 46 additions & 20 deletions examples/src/main/scala/examples/RegisterExample.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,18 @@ package jap.fields
package examples
package register

import cats._
import jap.fields._
import zio._

import scala.concurrent.Await
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
import scala.concurrent.duration._

object Validation {
import jap.fields.typeclass.Effect.future._
import jap.fields.fail._
import jap.fields.error._
// Try changing Module delclaration to see how easy it is to swap Error type
// object all extends AccumulateVM[Future, ValidationError] with CanFailWithValidationError
object all extends AccumulateVM[Future, FieldError[String]] with CanFailWithFieldStringValidationMessage
object all extends AccumulateVM[Future, FieldError[String]] with CanFailWithValidationMessageFieldString
// object all extends AccumulateVM[Future, ValidationMessage] with CanFailWithValidationMessage
}
import Validation.all._
Expand All @@ -56,42 +52,72 @@ case class RegisterRequest(
)

object RegisterRequest {
implicit val policy: Policy[RegisterRequest] = Policy

def validateUsername(username: Field[Username]): MRule = {
val usernameS = username.map(_.value)
usernameS.minSize(1) && usernameS.maxSize(10)
}

implicit val basicPolicy: Policy[RegisterRequest] = Policy
.builder[RegisterRequest]
.fieldRule(_.sub(_.username).map(_.value))(
_.minSize(1),
_.maxSize(10),
)
.subRule(_.age)(_ >= 18, _ <= 110)
.subRule(_.email)(_.map(_.value).matchesRegex(Email.EmailRegex))
.subRule(_.password)(_.nonEmpty, _.minSize(4), _.maxSize(100))
.subRule(_.password, _.passwordRepeat)(_ equalTo _)
// Commented code is the same rule rewritten in 4 different ways
// .fieldRule(_.sub(_.username).map(_.value))(_.minSize(1), _.maxSize(10))
// .subRule(_.username)(_.map(_.value).all(_.minSize(1), _.maxSize(10)))
// .subRule(_.username)(validateUsername)
.rule { request =>
val username = request.sub(_.username).map(_.value)
username.minSize(1) && username.maxSize(10)
}
.build

def policy(userService: UserService): Policy[RegisterRequest] =
/** You could extract some complex validation logic to methods, they could even be Tagless Final if you wish to reuse
* it with different Effect, Validated, Error types
*/
def validateNoActiveUser(username: Field[Username])(implicit userService: UserService) =
Rule.flatten {
userService.getUser(username.value).map {
case Some(user) if user.active => Rule.pure(username.failMessage("Already used by active user"))
case _ => Rule.valid
}
}

def asyncPolicy(implicit userService: UserService): Policy[RegisterRequest] =
Policy
.builder[RegisterRequest]
// Include Sync validation
.rule(RegisterRequest.policy.validate)
// Async validations
.subRule(_.username)(_.ensureF(userService.usernameIsAvailable, _.failMessage("Username is not available")))
// Include Basic validation
.rule(RegisterRequest.basicPolicy.validate)
// Below will work to but keep in mind not to fall for cyclic implicit resolution
// .rule(_.validate)
// Our Async validations
.subRule(_.username)(
_.ensureF(userService.usernameIsAvailable, _.failMessage("Username is not available")),
validateNoActiveUser,
)
.subRule(_.email)(_.ensureF(userService.emailIsAvailable, _.failMessage("Email is not available")))
.build
}

trait UserService {
def emailIsAvailable(email: Email): Future[Boolean]
def usernameIsAvailable(username: Username): Future[Boolean]
def getUser(username: Username): Future[Option[User]]
}

case class User(usermame: Username, active: Boolean)

object RegisterExample {
showBuildInfo()
val userService: UserService = new UserService {
def emailIsAvailable(email: Email) = Future(false)
def usernameIsAvailable(username: Username) = Future(false)
implicit val userService: UserService = new UserService {
def emailIsAvailable(email: Email) = Future(false)
def usernameIsAvailable(username: Username) = Future(false)
def getUser(username: Username): Future[Option[User]] = Future(Some(User(username, true)))
}

implicit val policy: Policy[RegisterRequest] = RegisterRequest.policy(userService)
implicit val policy: Policy[RegisterRequest] = RegisterRequest.asyncPolicy

final def main(args: Array[String]): Unit = {

Expand All @@ -104,6 +130,6 @@ object RegisterExample {
age = 2,
)

awaitFuture(showErrors("ERRORS")(Field.from(request).validate))
awaitReady(showErrors("ERRORS")(Field.from(request).validate))
}
}
34 changes: 17 additions & 17 deletions examples/src/main/scala/examples/ZioEnvExample.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,7 @@ package jap.fields
package examples
package zioenv

import jap.fields.ZioInterop._
import jap.fields._
import jap.fields.data.Accumulate
import jap.fields.error.ValidationError
import jap.fields.fail.CanFailWithValidationError
import jap.fields.syntax._
import jap.fields.typeclass.Effect
import zio._

trait HeartbeatApi {
Expand All @@ -40,30 +34,36 @@ case class HealthReport(
bloodPressure: Int,
)

object Validation extends all with ZioSyntaxAll with CanFailWithValidationError {
type ZRule[R] = Rule[RIO[R, *], Accumulate, ValidationError]
type ZPolicy[R, P] = ValidationPolicy[P, RIO[R, *], Accumulate, ValidationError]
object ZPolicy {
def builder[R, P]: ValidationPolicyBuilder[P, RIO[R, *], Accumulate, ValidationError] = ValidationPolicy.builder
}
/** In this example we focus on using ZIO environment for validation, so we define custom syntax module where Rule and
* Policy can specify R environment
*/
object Validation {
import jap.fields.data.Accumulate
import jap.fields.error.ValidationError
import jap.fields.fail.CanFailWithValidationError
import jap.fields.ZIOInterop._
object all extends ZValidationModule[Accumulate, ValidationError] with CanFailWithValidationError
}
import Validation._
import Validation.all._

object HealthReport {
def validateHeartbeat(heartbeat: Field[Int]): ZRule[Has[HeartbeatApi]] =

/** Notice in env we say only what is needed for this validation */
def validateHeartbeat(heartbeat: Field[Int]): RIORule[Has[HeartbeatApi]] =
heartbeat.ensureF(
h => ZIO.serviceWith[HeartbeatApi](_.isHeartbeatOk(h)),
_.failMessage("Check your heartbeat please"),
)

def validateBloodPressure(bloodPressure: Field[Int]): ZRule[Has[BloodPressureApi]] =
/** Notice in env we say only what is needed for this validation */
def validateBloodPressure(bloodPressure: Field[Int]): RIORule[Has[BloodPressureApi]] =
bloodPressure.ensureF(
h => ZIO.serviceWith[BloodPressureApi](_.isBloodPressureOk(h)),
_.failMessage("Check your bloodpressure please"),
)

implicit val policy: ZPolicy[Has[BloodPressureApi] with Has[HeartbeatApi], HealthReport] =
ZPolicy
implicit val policy: RIOPolicy[Has[BloodPressureApi] with Has[HeartbeatApi], HealthReport] =
RIOPolicy
.builder[Has[BloodPressureApi] with Has[HeartbeatApi], HealthReport]
.subRule(_.heartbeat)(validateHeartbeat)
.subRule(_.bloodPressure)(validateBloodPressure)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ package jap.fields
package examples
package medium

import jap.fields.ZioInterop._
import jap.fields.ZIOInterop._
import jap.fields._
import jap.fields.fail._
import zio.Task
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ package jap.fields
package examples
package medium

import jap.fields.ZioInterop._
import jap.fields.ZIOInterop._
import jap.fields._
import jap.fields.data.Accumulate
import jap.fields.error.ValidationError
Expand Down
Loading

0 comments on commit 40e1102

Please sign in to comment.