From 4986fc47528aa8139abb319ee06c8ba358bfb542 Mon Sep 17 00:00:00 2001 From: Luc DUZAN Date: Thu, 30 Jan 2020 23:06:59 +0100 Subject: [PATCH 1/5] Add support for refined --- build.sbt | 11 ++++++++++ project/Versions.scala | 1 + .../codec/refined/TapirCodecRefined.scala | 17 +++++++++++++++ .../sttp/tapir/codec/refined/package.scala | 3 +++ .../codec/refined/TapirCodecRefinedTest.scala | 21 +++++++++++++++++++ 5 files changed, 53 insertions(+) create mode 100644 refined/src/main/scala/sttp/tapir/codec/refined/TapirCodecRefined.scala create mode 100644 refined/src/main/scala/sttp/tapir/codec/refined/package.scala create mode 100644 refined/src/test/scala/sttp/tapir/codec/refined/TapirCodecRefinedTest.scala diff --git a/build.sbt b/build.sbt index e973cf8bdf..34e46a8f8e 100644 --- a/build.sbt +++ b/build.sbt @@ -112,6 +112,17 @@ lazy val tapirCats: Project = (project in file("cats")) ) .dependsOn(core) +lazy val tapirRefined: Project = (project in file("refined")) + .settings(commonSettings) + .settings( + name := "tapir-refined", + libraryDependencies ++= Seq( + "eu.timepit" %% "refined" % Versions.refined, + scalaTest % "test" + ) + ) + .dependsOn(core) + // json lazy val circeJson: Project = (project in file("json/circe")) diff --git a/project/Versions.scala b/project/Versions.scala index ef310aa1f2..ee35534516 100644 --- a/project/Versions.scala +++ b/project/Versions.scala @@ -15,4 +15,5 @@ object Versions { val sprayJson = "1.3.5" val scalaCheck = "1.14.1" val scalaTest = "3.0.8" + val refined = "0.9.12" } diff --git a/refined/src/main/scala/sttp/tapir/codec/refined/TapirCodecRefined.scala b/refined/src/main/scala/sttp/tapir/codec/refined/TapirCodecRefined.scala new file mode 100644 index 0000000000..e2d2c06736 --- /dev/null +++ b/refined/src/main/scala/sttp/tapir/codec/refined/TapirCodecRefined.scala @@ -0,0 +1,17 @@ +package sttp.tapir.codec.refined + +import sttp.tapir._ +import eu.timepit.refined.api.{Refined, Validate} +import eu.timepit.refined.refineV + +trait TapirCodecRefined { + implicit def codecForRefined[V, P, CF <: CodecFormat, R](implicit tm: Codec[V, CF, R], validator: Validate[V, P]): Codec[V Refined P, CF, R] = + implicitly[Codec[V, CF, R]] + .mapDecode { v: V => + refineV[P](v) match { + case Right(refined) => DecodeResult.Value(refined) + //TODO: exploit error + case Left(_) => DecodeResult.InvalidValue(List()) + } + }(_.value) +} diff --git a/refined/src/main/scala/sttp/tapir/codec/refined/package.scala b/refined/src/main/scala/sttp/tapir/codec/refined/package.scala new file mode 100644 index 0000000000..c41822793a --- /dev/null +++ b/refined/src/main/scala/sttp/tapir/codec/refined/package.scala @@ -0,0 +1,3 @@ +package sttp.tapir.codec + +package object refined extends TapirCodecRefined diff --git a/refined/src/test/scala/sttp/tapir/codec/refined/TapirCodecRefinedTest.scala b/refined/src/test/scala/sttp/tapir/codec/refined/TapirCodecRefinedTest.scala new file mode 100644 index 0000000000..7de267364c --- /dev/null +++ b/refined/src/test/scala/sttp/tapir/codec/refined/TapirCodecRefinedTest.scala @@ -0,0 +1,21 @@ +package sttp.tapir.codec.refined + +import eu.timepit.refined.collection.NonEmpty +import eu.timepit.refined.refineMV +import eu.timepit.refined.types.string.NonEmptyString +import org.scalatest.{FlatSpec, Matchers} +import sttp.tapir.Codec.PlainCodec +import sttp.tapir.DecodeResult + +class TapirCodecRefinedTest extends FlatSpec with Matchers with TapirCodecRefined { + + val nonEmptyStringCodec = implicitly[PlainCodec[NonEmptyString]] + + it should "return DecodResult.Invalid if subtype can't be refined" in { + nonEmptyStringCodec.decode("") should matchPattern{case DecodeResult.InvalidValue(_) =>} + } + + it should "correctly delegate to raw parser and refine it" in { + nonEmptyStringCodec.decode("vive le fromage") shouldBe DecodeResult.Value(refineMV[NonEmpty]("vive le fromage")) + } +} From 6bc8525b485bf2689df9652d81d23940c38e7f92 Mon Sep 17 00:00:00 2001 From: Luc DUZAN Date: Fri, 31 Jan 2020 15:15:24 +0100 Subject: [PATCH 2/5] Add validator to codec generated from refined type --- .../codec/refined/TapirCodecRefined.scala | 44 +++++++++++++++++-- .../codec/refined/TapirCodecRefinedTest.scala | 29 ++++++++++-- 2 files changed, 65 insertions(+), 8 deletions(-) diff --git a/refined/src/main/scala/sttp/tapir/codec/refined/TapirCodecRefined.scala b/refined/src/main/scala/sttp/tapir/codec/refined/TapirCodecRefined.scala index e2d2c06736..5380abba60 100644 --- a/refined/src/main/scala/sttp/tapir/codec/refined/TapirCodecRefined.scala +++ b/refined/src/main/scala/sttp/tapir/codec/refined/TapirCodecRefined.scala @@ -2,16 +2,52 @@ package sttp.tapir.codec.refined import sttp.tapir._ import eu.timepit.refined.api.{Refined, Validate} +import eu.timepit.refined.collection.NonEmpty import eu.timepit.refined.refineV +import eu.timepit.refined.string.MatchesRegex +import shapeless.Witness -trait TapirCodecRefined { - implicit def codecForRefined[V, P, CF <: CodecFormat, R](implicit tm: Codec[V, CF, R], validator: Validate[V, P]): Codec[V Refined P, CF, R] = +import scala.reflect.ClassTag + +trait RefinedValidatorTranslation[V, P] { + def tapirValidator: Validator[V] + def listError(value: V, refinedErrorMessage: String): List[ValidationError[_]] +} + +object RefinedValidatorTranslation { + def fromPrimitiveValidator[V, P](validator: Validator.Primitive[V]) = new RefinedValidatorTranslation[V, P] { + override def tapirValidator: Validator[V] = validator + override def listError(value: V, refinedErrorMessage: String): List[ValidationError[_]] = List(ValidationError[V](validator, value)) + } +} + +trait TapirCodecRefined extends ImplicitGenericRefinedValidator { + implicit def codecForRefined[V, P, CF <: CodecFormat, R](implicit tm: Codec[V, CF, R], refinedValidator: Validate[V, P], refinedValidatorTranslation: RefinedValidatorTranslation[V, P]): Codec[V Refined P, CF, R] = { implicitly[Codec[V, CF, R]] + .validate(refinedValidatorTranslation.tapirValidator) // in reality if this validator has to fail, it will fail before in mapDecode while trying to construct refined type .mapDecode { v: V => refineV[P](v) match { case Right(refined) => DecodeResult.Value(refined) - //TODO: exploit error - case Left(_) => DecodeResult.InvalidValue(List()) + case Left(errorMessage) => { + DecodeResult.InvalidValue(refinedValidatorTranslation.listError(v, errorMessage)) + } } }(_.value) + } + + implicit val nonEmptyStringRefinedTranslator: RefinedValidatorTranslation[String, NonEmpty] = + RefinedValidatorTranslation.fromPrimitiveValidator[String, NonEmpty](Validator.minLength(1)) + + implicit def matchesRegexRefinedTranslator[S <: String](implicit ws: Witness.Aux[S]): RefinedValidatorTranslation[String, MatchesRegex[S]] = + RefinedValidatorTranslation.fromPrimitiveValidator(Validator.pattern(ws.value)) +} + +trait ImplicitGenericRefinedValidator { + implicit def genericRefinedValidatorTranslation[V, P: ClassTag](implicit refinedValidator: Validate[V, P]): RefinedValidatorTranslation[V, P] = new RefinedValidatorTranslation[V, P] { + override val tapirValidator: Validator.Custom[V] = Validator.Custom( + refinedValidator.isValid(_), + implicitly[ClassTag[P]].runtimeClass.toString) //for the moment there is no way to get a human description of a predicate/validator without having a concrete value to run it + + override def listError(value: V, refinedErrorMessage: String): List[ValidationError[_]] = List(ValidationError[V](tapirValidator.copy(message = refinedErrorMessage), value)) + } } diff --git a/refined/src/test/scala/sttp/tapir/codec/refined/TapirCodecRefinedTest.scala b/refined/src/test/scala/sttp/tapir/codec/refined/TapirCodecRefinedTest.scala index 7de267364c..37788629e4 100644 --- a/refined/src/test/scala/sttp/tapir/codec/refined/TapirCodecRefinedTest.scala +++ b/refined/src/test/scala/sttp/tapir/codec/refined/TapirCodecRefinedTest.scala @@ -1,21 +1,42 @@ package sttp.tapir.codec.refined +import eu.timepit.refined.api.Refined import eu.timepit.refined.collection.NonEmpty -import eu.timepit.refined.refineMV +import eu.timepit.refined.string.{IPv4, MatchesRegex} +import eu.timepit.refined.{W, refineMV, refineV} import eu.timepit.refined.types.string.NonEmptyString import org.scalatest.{FlatSpec, Matchers} import sttp.tapir.Codec.PlainCodec -import sttp.tapir.DecodeResult +import sttp.tapir.{DecodeResult, ValidationError, Validator} class TapirCodecRefinedTest extends FlatSpec with Matchers with TapirCodecRefined { val nonEmptyStringCodec = implicitly[PlainCodec[NonEmptyString]] - it should "return DecodResult.Invalid if subtype can't be refined" in { - nonEmptyStringCodec.decode("") should matchPattern{case DecodeResult.InvalidValue(_) =>} + + "Generated codec" should "return DecodResult.Invalid if subtype can't be refined with correct tapir validator if available" in { + val expectedValidator: Validator[String] = Validator.minLength(1) + nonEmptyStringCodec.decode("") should matchPattern{case DecodeResult.InvalidValue(List(ValidationError(validator, "", _))) if validator == expectedValidator=>} } it should "correctly delegate to raw parser and refine it" in { nonEmptyStringCodec.decode("vive le fromage") shouldBe DecodeResult.Value(refineMV[NonEmpty]("vive le fromage")) } + + it should "return DecodResult.Invalid if subtype can't be refined with derived tapir validator if non tapir validator available" in { + type IPString = String Refined IPv4 + val IPStringCodec = implicitly[PlainCodec[IPString]] + + val expectedMsg = refineV[IPv4]("192.168.0.1000").left.get + IPStringCodec.decode("192.168.0.1000") should matchPattern{case DecodeResult.InvalidValue(List(ValidationError(Validator.Custom(_, `expectedMsg`), "192.168.0.1000", _)))=>} + } + + "Generated codec for MatchesRegex" should "use tapir Validator.Pattern" in { + type VariableConstraint = MatchesRegex[W.`"[a-zA-Z][-a-zA-Z0-9_]*"`.T] + type VariableString = String Refined VariableConstraint + val identifierCodec = implicitly[PlainCodec[VariableString]] + + val expectedValidator: Validator[String] = Validator.pattern("[a-zA-Z][-a-zA-Z0-9_]*") + identifierCodec.decode("-bad") should matchPattern{case DecodeResult.InvalidValue(List(ValidationError(validator, "-bad", _))) if validator == expectedValidator=>} + } } From 580f9eaa6e025992831ff567fda094c99fda7969 Mon Sep 17 00:00:00 2001 From: Luc DUZAN Date: Fri, 31 Jan 2020 15:20:03 +0100 Subject: [PATCH 3/5] Move cats and refined to tapir folder --- build.sbt | 5 +++-- .../main/scala/sttp/tapir/codec/cats/TapirCodecCats.scala | 0 .../cats}/src/main/scala/sttp/tapir/codec/cats/package.scala | 0 .../scala/sttp/tapir/codec/cats/TapirCodecCatsTest.scala | 0 .../scala/sttp/tapir/codec/refined/TapirCodecRefined.scala | 0 .../src/main/scala/sttp/tapir/codec/refined/package.scala | 0 .../sttp/tapir/codec/refined/TapirCodecRefinedTest.scala | 0 7 files changed, 3 insertions(+), 2 deletions(-) rename {cats => integration/cats}/src/main/scala/sttp/tapir/codec/cats/TapirCodecCats.scala (100%) rename {cats => integration/cats}/src/main/scala/sttp/tapir/codec/cats/package.scala (100%) rename {cats => integration/cats}/src/test/scala/sttp/tapir/codec/cats/TapirCodecCatsTest.scala (100%) rename {refined => integration/refined}/src/main/scala/sttp/tapir/codec/refined/TapirCodecRefined.scala (100%) rename {refined => integration/refined}/src/main/scala/sttp/tapir/codec/refined/package.scala (100%) rename {refined => integration/refined}/src/test/scala/sttp/tapir/codec/refined/TapirCodecRefinedTest.scala (100%) diff --git a/build.sbt b/build.sbt index 34e46a8f8e..7c1af20cbd 100644 --- a/build.sbt +++ b/build.sbt @@ -41,6 +41,7 @@ lazy val rootProject = (project in file(".")) .aggregate( core, tapirCats, + tapirRefined, circeJson, playJson, sprayJson, @@ -100,7 +101,7 @@ lazy val tests: Project = (project in file("tests")) // cats -lazy val tapirCats: Project = (project in file("cats")) +lazy val tapirCats: Project = (project in file("integration/cats")) .settings(commonSettings) .settings( name := "tapir-cats", @@ -112,7 +113,7 @@ lazy val tapirCats: Project = (project in file("cats")) ) .dependsOn(core) -lazy val tapirRefined: Project = (project in file("refined")) +lazy val tapirRefined: Project = (project in file("integration/refined")) .settings(commonSettings) .settings( name := "tapir-refined", diff --git a/cats/src/main/scala/sttp/tapir/codec/cats/TapirCodecCats.scala b/integration/cats/src/main/scala/sttp/tapir/codec/cats/TapirCodecCats.scala similarity index 100% rename from cats/src/main/scala/sttp/tapir/codec/cats/TapirCodecCats.scala rename to integration/cats/src/main/scala/sttp/tapir/codec/cats/TapirCodecCats.scala diff --git a/cats/src/main/scala/sttp/tapir/codec/cats/package.scala b/integration/cats/src/main/scala/sttp/tapir/codec/cats/package.scala similarity index 100% rename from cats/src/main/scala/sttp/tapir/codec/cats/package.scala rename to integration/cats/src/main/scala/sttp/tapir/codec/cats/package.scala diff --git a/cats/src/test/scala/sttp/tapir/codec/cats/TapirCodecCatsTest.scala b/integration/cats/src/test/scala/sttp/tapir/codec/cats/TapirCodecCatsTest.scala similarity index 100% rename from cats/src/test/scala/sttp/tapir/codec/cats/TapirCodecCatsTest.scala rename to integration/cats/src/test/scala/sttp/tapir/codec/cats/TapirCodecCatsTest.scala diff --git a/refined/src/main/scala/sttp/tapir/codec/refined/TapirCodecRefined.scala b/integration/refined/src/main/scala/sttp/tapir/codec/refined/TapirCodecRefined.scala similarity index 100% rename from refined/src/main/scala/sttp/tapir/codec/refined/TapirCodecRefined.scala rename to integration/refined/src/main/scala/sttp/tapir/codec/refined/TapirCodecRefined.scala diff --git a/refined/src/main/scala/sttp/tapir/codec/refined/package.scala b/integration/refined/src/main/scala/sttp/tapir/codec/refined/package.scala similarity index 100% rename from refined/src/main/scala/sttp/tapir/codec/refined/package.scala rename to integration/refined/src/main/scala/sttp/tapir/codec/refined/package.scala diff --git a/refined/src/test/scala/sttp/tapir/codec/refined/TapirCodecRefinedTest.scala b/integration/refined/src/test/scala/sttp/tapir/codec/refined/TapirCodecRefinedTest.scala similarity index 100% rename from refined/src/test/scala/sttp/tapir/codec/refined/TapirCodecRefinedTest.scala rename to integration/refined/src/test/scala/sttp/tapir/codec/refined/TapirCodecRefinedTest.scala From 3958cb583ad4ebeb0d2e911386274fb35afd5acb Mon Sep 17 00:00:00 2001 From: Luc DUZAN Date: Mon, 3 Feb 2020 18:45:28 +0100 Subject: [PATCH 4/5] Add custom binding for refined Less and Greater --- .../codec/refined/TapirCodecRefined.scala | 15 ++++++- .../codec/refined/TapirCodecRefinedTest.scala | 39 ++++++++++++++++++- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/integration/refined/src/main/scala/sttp/tapir/codec/refined/TapirCodecRefined.scala b/integration/refined/src/main/scala/sttp/tapir/codec/refined/TapirCodecRefined.scala index 5380abba60..8f106cae2d 100644 --- a/integration/refined/src/main/scala/sttp/tapir/codec/refined/TapirCodecRefined.scala +++ b/integration/refined/src/main/scala/sttp/tapir/codec/refined/TapirCodecRefined.scala @@ -1,10 +1,11 @@ package sttp.tapir.codec.refined import sttp.tapir._ -import eu.timepit.refined.api.{Refined, Validate} +import eu.timepit.refined.api.{Max, Refined, Validate} import eu.timepit.refined.collection.NonEmpty import eu.timepit.refined.refineV import eu.timepit.refined.string.MatchesRegex +import eu.timepit.refined.numeric.{Greater, GreaterEqual, Less, LessEqual} import shapeless.Witness import scala.reflect.ClassTag @@ -40,6 +41,18 @@ trait TapirCodecRefined extends ImplicitGenericRefinedValidator { implicit def matchesRegexRefinedTranslator[S <: String](implicit ws: Witness.Aux[S]): RefinedValidatorTranslation[String, MatchesRegex[S]] = RefinedValidatorTranslation.fromPrimitiveValidator(Validator.pattern(ws.value)) + + implicit def lessRefinedTranslator[N: Numeric, NM <: N](implicit ws: Witness.Aux[NM]): RefinedValidatorTranslation[N, Less[NM]] = + RefinedValidatorTranslation.fromPrimitiveValidator(Validator.max(ws.value, exclusive = true)) + + implicit def lessEqualRefinedTranslator[N: Numeric, NM <: N](implicit ws: Witness.Aux[NM]): RefinedValidatorTranslation[N, LessEqual[NM]] = + RefinedValidatorTranslation.fromPrimitiveValidator(Validator.max(ws.value, exclusive = false)) + + implicit def maxRefinedTranslator[N: Numeric, NM <: N](implicit ws: Witness.Aux[NM]): RefinedValidatorTranslation[N, Greater[NM]] = + RefinedValidatorTranslation.fromPrimitiveValidator(Validator.min(ws.value, exclusive = true)) + + implicit def maxEqualRefinedTranslator[N: Numeric, NM <: N](implicit ws: Witness.Aux[NM]): RefinedValidatorTranslation[N, GreaterEqual[NM]] = + RefinedValidatorTranslation.fromPrimitiveValidator(Validator.min(ws.value, exclusive = false)) } trait ImplicitGenericRefinedValidator { diff --git a/integration/refined/src/test/scala/sttp/tapir/codec/refined/TapirCodecRefinedTest.scala b/integration/refined/src/test/scala/sttp/tapir/codec/refined/TapirCodecRefinedTest.scala index 37788629e4..3222175c6e 100644 --- a/integration/refined/src/test/scala/sttp/tapir/codec/refined/TapirCodecRefinedTest.scala +++ b/integration/refined/src/test/scala/sttp/tapir/codec/refined/TapirCodecRefinedTest.scala @@ -1,7 +1,8 @@ package sttp.tapir.codec.refined -import eu.timepit.refined.api.Refined +import eu.timepit.refined.api.{Max, Refined} import eu.timepit.refined.collection.NonEmpty +import eu.timepit.refined.numeric.{Greater, GreaterEqual, Less, LessEqual} import eu.timepit.refined.string.{IPv4, MatchesRegex} import eu.timepit.refined.{W, refineMV, refineV} import eu.timepit.refined.types.string.NonEmptyString @@ -39,4 +40,40 @@ class TapirCodecRefinedTest extends FlatSpec with Matchers with TapirCodecRefine val expectedValidator: Validator[String] = Validator.pattern("[a-zA-Z][-a-zA-Z0-9_]*") identifierCodec.decode("-bad") should matchPattern{case DecodeResult.InvalidValue(List(ValidationError(validator, "-bad", _))) if validator == expectedValidator=>} } + + "Generated codec for Less" should "use tapir Validator.drMax" in { + type IntConstraint = Less[W.`3`.T] + type LimitedInt = Int Refined IntConstraint + val limitedIntCodec = implicitly[PlainCodec[LimitedInt]] + + val expectedValidator: Validator[Int] = Validator.max(3, exclusive = true) + limitedIntCodec.decode("3") should matchPattern{case DecodeResult.InvalidValue(List(ValidationError(validator, 3, _))) if validator == expectedValidator=>} + } + + "Generated codec for LessEqual" should "use tapir Validator.drMax" in { + type IntConstraint = LessEqual[W.`3`.T] + type LimitedInt = Int Refined IntConstraint + val limitedIntCodec = implicitly[PlainCodec[LimitedInt]] + + val expectedValidator: Validator[Int] = Validator.max(3, exclusive = false) + limitedIntCodec.decode("4") should matchPattern{case DecodeResult.InvalidValue(List(ValidationError(validator, 4, _))) if validator == expectedValidator=>} + } + + "Generated codec for Max" should "use tapir Validator.drMax" in { + type IntConstraint = Greater[W.`3`.T] + type LimitedInt = Int Refined IntConstraint + val limitedIntCodec = implicitly[PlainCodec[LimitedInt]] + + val expectedValidator: Validator[Int] = Validator.min(3, exclusive = true) + limitedIntCodec.decode("3") should matchPattern{case DecodeResult.InvalidValue(List(ValidationError(validator, 3, _))) if validator == expectedValidator=>} + } + + "Generated codec for MaxEqual" should "use tapir Validator.drMax" in { + type IntConstraint = GreaterEqual[W.`3`.T] + type LimitedInt = Int Refined IntConstraint + val limitedIntCodec = implicitly[PlainCodec[LimitedInt]] + + val expectedValidator: Validator[Int] = Validator.min(3, exclusive = false) + limitedIntCodec.decode("2") should matchPattern{case DecodeResult.InvalidValue(List(ValidationError(validator, 2, _))) if validator == expectedValidator=>} + } } From 68519ecb2bce6c54bfc5d136e7c3c9d030f12647 Mon Sep 17 00:00:00 2001 From: Luc DUZAN Date: Wed, 5 Feb 2020 10:21:22 +0100 Subject: [PATCH 5/5] Setup doc for tapir-refined --- doc/endpoint/customtypes.md | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/doc/endpoint/customtypes.md b/doc/endpoint/customtypes.md index cd63b619af..049c81b79d 100644 --- a/doc/endpoint/customtypes.md +++ b/doc/endpoint/customtypes.md @@ -92,8 +92,7 @@ For example, given following coproduct: sealed trait Entity{ def kind: String } -case class Person(firstName:String, lastName:String) extends Entity { - def kind: String = "person" +case class Person(firstName:String, lastName:String) extends Entity { def kind: String = "person" } case class Organization(name: String) extends Entity { def kind: String = "org" @@ -139,6 +138,18 @@ Non-standard collections can be unwrapped in the modification path by providing The `tapir-cats` module contains `Schema[_]` instances for some cats datatypes. See the `tapir.codec.cats.TapirCodecCats` trait or `import sttp.tapir.codec.cats._` to bring the implicit values into scope. +### Schema for refined type + +If you use [refined](https://github.com/fthomas/refined), the `tapir-refined` module will provide an implicit codecs for +`T Refined P` as long as a codecs for `T` already exists. +It will add a validator to your already existing codecs and just wrap/unwrap the value from/to its refined equivalent. +Some predicates will bind correctly to the vanilla tapir Validator, while others will bind to a custom validator that +might not be very clear when reading the generated OpenAPI documentation. Correctly bound predicates can be found in +`integration/refined/src/main/scala/sttp/tapir/codec/refined/TapirCodecRefined.scala`. +If you are not satisfied with the validator generated by `tapir-refined`, you can provide an implicit +`RefinedValidatorTranslation[T, P]` in scope using `RefinedValidator.fromPrimitiveValidator' to build it (do not +hesitate to contribute your work). + ## Next Read on about [validation](validation.html).