Skip to content

Commit 6bc8525

Browse files
author
Luc DUZAN
committed
Add validator to codec generated from refined type
1 parent 4986fc4 commit 6bc8525

File tree

2 files changed

+65
-8
lines changed

2 files changed

+65
-8
lines changed

refined/src/main/scala/sttp/tapir/codec/refined/TapirCodecRefined.scala

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,52 @@ package sttp.tapir.codec.refined
22

33
import sttp.tapir._
44
import eu.timepit.refined.api.{Refined, Validate}
5+
import eu.timepit.refined.collection.NonEmpty
56
import eu.timepit.refined.refineV
7+
import eu.timepit.refined.string.MatchesRegex
8+
import shapeless.Witness
69

7-
trait TapirCodecRefined {
8-
implicit def codecForRefined[V, P, CF <: CodecFormat, R](implicit tm: Codec[V, CF, R], validator: Validate[V, P]): Codec[V Refined P, CF, R] =
10+
import scala.reflect.ClassTag
11+
12+
trait RefinedValidatorTranslation[V, P] {
13+
def tapirValidator: Validator[V]
14+
def listError(value: V, refinedErrorMessage: String): List[ValidationError[_]]
15+
}
16+
17+
object RefinedValidatorTranslation {
18+
def fromPrimitiveValidator[V, P](validator: Validator.Primitive[V]) = new RefinedValidatorTranslation[V, P] {
19+
override def tapirValidator: Validator[V] = validator
20+
override def listError(value: V, refinedErrorMessage: String): List[ValidationError[_]] = List(ValidationError[V](validator, value))
21+
}
22+
}
23+
24+
trait TapirCodecRefined extends ImplicitGenericRefinedValidator {
25+
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] = {
926
implicitly[Codec[V, CF, R]]
27+
.validate(refinedValidatorTranslation.tapirValidator) // in reality if this validator has to fail, it will fail before in mapDecode while trying to construct refined type
1028
.mapDecode { v: V =>
1129
refineV[P](v) match {
1230
case Right(refined) => DecodeResult.Value(refined)
13-
//TODO: exploit error
14-
case Left(_) => DecodeResult.InvalidValue(List())
31+
case Left(errorMessage) => {
32+
DecodeResult.InvalidValue(refinedValidatorTranslation.listError(v, errorMessage))
33+
}
1534
}
1635
}(_.value)
36+
}
37+
38+
implicit val nonEmptyStringRefinedTranslator: RefinedValidatorTranslation[String, NonEmpty] =
39+
RefinedValidatorTranslation.fromPrimitiveValidator[String, NonEmpty](Validator.minLength(1))
40+
41+
implicit def matchesRegexRefinedTranslator[S <: String](implicit ws: Witness.Aux[S]): RefinedValidatorTranslation[String, MatchesRegex[S]] =
42+
RefinedValidatorTranslation.fromPrimitiveValidator(Validator.pattern(ws.value))
43+
}
44+
45+
trait ImplicitGenericRefinedValidator {
46+
implicit def genericRefinedValidatorTranslation[V, P: ClassTag](implicit refinedValidator: Validate[V, P]): RefinedValidatorTranslation[V, P] = new RefinedValidatorTranslation[V, P] {
47+
override val tapirValidator: Validator.Custom[V] = Validator.Custom(
48+
refinedValidator.isValid(_),
49+
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
50+
51+
override def listError(value: V, refinedErrorMessage: String): List[ValidationError[_]] = List(ValidationError[V](tapirValidator.copy(message = refinedErrorMessage), value))
52+
}
1753
}
Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,42 @@
11
package sttp.tapir.codec.refined
22

3+
import eu.timepit.refined.api.Refined
34
import eu.timepit.refined.collection.NonEmpty
4-
import eu.timepit.refined.refineMV
5+
import eu.timepit.refined.string.{IPv4, MatchesRegex}
6+
import eu.timepit.refined.{W, refineMV, refineV}
57
import eu.timepit.refined.types.string.NonEmptyString
68
import org.scalatest.{FlatSpec, Matchers}
79
import sttp.tapir.Codec.PlainCodec
8-
import sttp.tapir.DecodeResult
10+
import sttp.tapir.{DecodeResult, ValidationError, Validator}
911

1012
class TapirCodecRefinedTest extends FlatSpec with Matchers with TapirCodecRefined {
1113

1214
val nonEmptyStringCodec = implicitly[PlainCodec[NonEmptyString]]
1315

14-
it should "return DecodResult.Invalid if subtype can't be refined" in {
15-
nonEmptyStringCodec.decode("") should matchPattern{case DecodeResult.InvalidValue(_) =>}
16+
17+
"Generated codec" should "return DecodResult.Invalid if subtype can't be refined with correct tapir validator if available" in {
18+
val expectedValidator: Validator[String] = Validator.minLength(1)
19+
nonEmptyStringCodec.decode("") should matchPattern{case DecodeResult.InvalidValue(List(ValidationError(validator, "", _))) if validator == expectedValidator=>}
1620
}
1721

1822
it should "correctly delegate to raw parser and refine it" in {
1923
nonEmptyStringCodec.decode("vive le fromage") shouldBe DecodeResult.Value(refineMV[NonEmpty]("vive le fromage"))
2024
}
25+
26+
it should "return DecodResult.Invalid if subtype can't be refined with derived tapir validator if non tapir validator available" in {
27+
type IPString = String Refined IPv4
28+
val IPStringCodec = implicitly[PlainCodec[IPString]]
29+
30+
val expectedMsg = refineV[IPv4]("192.168.0.1000").left.get
31+
IPStringCodec.decode("192.168.0.1000") should matchPattern{case DecodeResult.InvalidValue(List(ValidationError(Validator.Custom(_, `expectedMsg`), "192.168.0.1000", _)))=>}
32+
}
33+
34+
"Generated codec for MatchesRegex" should "use tapir Validator.Pattern" in {
35+
type VariableConstraint = MatchesRegex[W.`"[a-zA-Z][-a-zA-Z0-9_]*"`.T]
36+
type VariableString = String Refined VariableConstraint
37+
val identifierCodec = implicitly[PlainCodec[VariableString]]
38+
39+
val expectedValidator: Validator[String] = Validator.pattern("[a-zA-Z][-a-zA-Z0-9_]*")
40+
identifierCodec.decode("-bad") should matchPattern{case DecodeResult.InvalidValue(List(ValidationError(validator, "-bad", _))) if validator == expectedValidator=>}
41+
}
2142
}

0 commit comments

Comments
 (0)