From 07f9903a062e611705f720e439d814ae9d695bcd Mon Sep 17 00:00:00 2001 From: kciesielski Date: Wed, 12 Jun 2024 15:39:29 +0200 Subject: [PATCH 1/2] Treat fields with NonEmpty or MinSize(>0) as required --- .../codec/refined/TapirCodecRefined.scala | 29 +++++++++++++++++++ .../refined/TapirCodecRefinedTestScala3.scala | 4 +-- .../codec/refined/TapirCodecRefinedTest.scala | 13 +++++++++ 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/integrations/refined/src/main/scala/sttp/tapir/codec/refined/TapirCodecRefined.scala b/integrations/refined/src/main/scala/sttp/tapir/codec/refined/TapirCodecRefined.scala index 0637a0277e..59667da10d 100644 --- a/integrations/refined/src/main/scala/sttp/tapir/codec/refined/TapirCodecRefined.scala +++ b/integrations/refined/src/main/scala/sttp/tapir/codec/refined/TapirCodecRefined.scala @@ -8,6 +8,7 @@ import eu.timepit.refined.numeric.{Greater, GreaterEqual, Less, LessEqual} import eu.timepit.refined.refineV import eu.timepit.refined.string.{MatchesRegex, Uuid} import sttp.tapir._ +import sttp.tapir.internal._ import scala.reflect.ClassTag @@ -19,6 +20,17 @@ trait TapirCodecRefined extends LowPriorityValidatorForPredicate { ): Schema[V Refined P] = vSchema.validate(refinedValidatorTranslation.validator).map[V Refined P](v => refineV[P](v).toOption)(_.value) + implicit def refinedTapirSchemaIterable[X, C[X] <: Iterable[X], P](implicit + vSchema: Schema[C[X]], + refinedValidator: Validate[C[X], P], + refinedValidatorTranslation: ValidatorForPredicate[C[X], P] + ): Schema[C[X] Refined P] = { + vSchema + .validate(refinedValidatorTranslation.validator) + .map[C[X] Refined P](v => refineV[P](v).toOption)(_.value) + .copy(isOptional = if (refinedValidatorTranslation.containsMinSizePositive) false else vSchema.isOptional) + } + implicit def codecForRefined[R, V, P, CF <: CodecFormat](implicit tm: Codec[R, V, CF], refinedValidator: Validate[V, P], @@ -62,6 +74,19 @@ trait TapirCodecRefined extends LowPriorityValidatorForPredicate { ): PrimitiveValidatorForPredicate[T, MinSize[NM]] = ValidatorForPredicate.fromPrimitiveValidator(Validator.minLength[T](ws.snd)) + implicit def validatorForNonEmptyOnIterable[X, T[X] <: Iterable[X]]: PrimitiveValidatorForPredicate[T[X], NonEmpty] = + ValidatorForPredicate.fromPrimitiveValidator(Validator.minSize[X, T](1)) + + implicit def validatorForMinSizeOnIterable[X, T[X] <: Iterable[X], NM](implicit + ws: WitnessAs[NM, Int] + ): PrimitiveValidatorForPredicate[T[X], MinSize[NM]] = + ValidatorForPredicate.fromPrimitiveValidator(Validator.minSize[X, T](ws.snd)) + + implicit def validatorForMaxSizeOnIterable[X, T[X] <: Iterable[X], NM](implicit + ws: WitnessAs[NM, Int] + ): PrimitiveValidatorForPredicate[T[X], MaxSize[NM]] = + ValidatorForPredicate.fromPrimitiveValidator(Validator.maxSize[X, T](ws.snd)) + implicit def validatorForLess[N: Numeric, NM](implicit ws: WitnessAs[NM, N]): PrimitiveValidatorForPredicate[N, Less[NM]] = ValidatorForPredicate.fromPrimitiveValidator(Validator.max(ws.snd, exclusive = true)) @@ -133,6 +158,10 @@ trait TapirCodecRefined extends LowPriorityValidatorForPredicate { trait ValidatorForPredicate[V, P] { def validator: Validator[V] + lazy val containsMinSizePositive: Boolean = validator.asPrimitiveValidators.exists { + case Validator.MinSize(a) => a > 0 + case _ => false + } def validationErrors(value: V, refinedErrorMessage: String): List[ValidationError[_]] } diff --git a/integrations/refined/src/test/scala-3/sttp/tapir/codec/refined/TapirCodecRefinedTestScala3.scala b/integrations/refined/src/test/scala-3/sttp/tapir/codec/refined/TapirCodecRefinedTestScala3.scala index 4c678673dc..6a259271f4 100644 --- a/integrations/refined/src/test/scala-3/sttp/tapir/codec/refined/TapirCodecRefinedTestScala3.scala +++ b/integrations/refined/src/test/scala-3/sttp/tapir/codec/refined/TapirCodecRefinedTestScala3.scala @@ -3,8 +3,8 @@ package sttp.tapir.codec.refined import eu.timepit.refined.api.Refined import eu.timepit.refined.boolean.Or import eu.timepit.refined.collection.{MaxSize, MinSize, NonEmpty} -import eu.timepit.refined.numeric.{Greater, GreaterEqual, Interval, Less, LessEqual, Negative, NonNegative, NonPositive, Positive} -import eu.timepit.refined.string.{IPv4, MatchesRegex} +import eu.timepit.refined.numeric.{Greater, GreaterEqual, Interval, Less, LessEqual} +import eu.timepit.refined.string.MatchesRegex import eu.timepit.refined.types.string.NonEmptyString import eu.timepit.refined.refineV import org.scalatest.flatspec.AnyFlatSpec diff --git a/integrations/refined/src/test/scala/sttp/tapir/codec/refined/TapirCodecRefinedTest.scala b/integrations/refined/src/test/scala/sttp/tapir/codec/refined/TapirCodecRefinedTest.scala index e310ccc88b..560a91418f 100644 --- a/integrations/refined/src/test/scala/sttp/tapir/codec/refined/TapirCodecRefinedTest.scala +++ b/integrations/refined/src/test/scala/sttp/tapir/codec/refined/TapirCodecRefinedTest.scala @@ -1,6 +1,8 @@ package sttp.tapir.codec.refined import eu.timepit.refined.api.Refined +import eu.timepit.refined.boolean._ +import eu.timepit.refined.collection._ import eu.timepit.refined.numeric.{Negative, NonNegative, NonPositive, Positive} import eu.timepit.refined.string.IPv4 import eu.timepit.refined.types.string.NonEmptyString @@ -59,6 +61,17 @@ class TapirCodecRefinedTest extends AnyFlatSpec with Matchers with TapirCodecRef implicitly[Schema[LimitedInt]].validator should matchPattern { case Validator.Mapped(Validator.Max(0, true), _) => } } + "Generated schema for NonEmpty and MinSize" should "not be optional" in { + assert(implicitly[Schema[List[Int]]].isOptional) + assert(!implicitly[Schema[List[Int] Refined NonEmpty]].isOptional) + assert(!implicitly[Schema[Set[Int] Refined NonEmpty]].isOptional) + assert(!implicitly[Schema[List[Int] Refined MinSize[3]]].isOptional) + assert(!implicitly[Schema[List[Int] Refined (MinSize[3] And MaxSize[6])]].isOptional) + assert(implicitly[Schema[List[Int] Refined MinSize[0]]].isOptional) + assert(implicitly[Schema[List[Int] Refined MaxSize[5]]].isOptional) + assert(implicitly[Schema[Option[List[Int] Refined NonEmpty]]].isOptional) + } + "Using refined" should "compile when using tapir endpoints" in { // this used to cause a: // [error] java.lang.StackOverflowError From fbd8e4290cd6674e9d137bf7a61c73f59bd563f5 Mon Sep 17 00:00:00 2001 From: kciesielski Date: Wed, 12 Jun 2024 16:01:38 +0200 Subject: [PATCH 2/2] Separate tests for Scala 2 and 3 --- .../codec/refined/TapirCodecRefinedTestScala2.scala | 13 ++++++++++++- .../codec/refined/TapirCodecRefinedTestScala3.scala | 13 ++++++++++++- .../tapir/codec/refined/TapirCodecRefinedTest.scala | 13 ------------- 3 files changed, 24 insertions(+), 15 deletions(-) diff --git a/integrations/refined/src/test/scala-2/sttp/tapir/codec/refined/TapirCodecRefinedTestScala2.scala b/integrations/refined/src/test/scala-2/sttp/tapir/codec/refined/TapirCodecRefinedTestScala2.scala index 714873be80..df71e29a6c 100644 --- a/integrations/refined/src/test/scala-2/sttp/tapir/codec/refined/TapirCodecRefinedTestScala2.scala +++ b/integrations/refined/src/test/scala-2/sttp/tapir/codec/refined/TapirCodecRefinedTestScala2.scala @@ -1,7 +1,7 @@ package sttp.tapir.codec.refined import eu.timepit.refined.api.Refined -import eu.timepit.refined.boolean.Or +import eu.timepit.refined.boolean._ import eu.timepit.refined.collection.{MaxSize, MinSize, NonEmpty} import eu.timepit.refined.numeric.{Greater, GreaterEqual, Interval, Less, LessEqual} import eu.timepit.refined.string.MatchesRegex @@ -159,6 +159,17 @@ class TapirCodecRefinedTestScala2 extends AnyFlatSpec with Matchers with TapirCo } } + "Generated schema for NonEmpty and MinSize" should "not be optional" in { + assert(implicitly[Schema[List[Int]]].isOptional) + assert(!implicitly[Schema[List[Int] Refined NonEmpty]].isOptional) + assert(!implicitly[Schema[Set[Int] Refined NonEmpty]].isOptional) + assert(!implicitly[Schema[List[Int] Refined MinSize[W.`3`.T]]].isOptional) + assert(!implicitly[Schema[List[Int] Refined (MinSize[W.`3`.T] And MaxSize[W.`6`.T])]].isOptional) + assert(implicitly[Schema[List[Int] Refined MinSize[W.`0`.T]]].isOptional) + assert(implicitly[Schema[List[Int] Refined MaxSize[W.`5`.T]]].isOptional) + assert(implicitly[Schema[Option[List[Int] Refined NonEmpty]]].isOptional) + } + "TapirCodecRefined" should "compile using implicit schema for refined types" in { import io.circe.refined._ import sttp.tapir diff --git a/integrations/refined/src/test/scala-3/sttp/tapir/codec/refined/TapirCodecRefinedTestScala3.scala b/integrations/refined/src/test/scala-3/sttp/tapir/codec/refined/TapirCodecRefinedTestScala3.scala index 6a259271f4..575606315c 100644 --- a/integrations/refined/src/test/scala-3/sttp/tapir/codec/refined/TapirCodecRefinedTestScala3.scala +++ b/integrations/refined/src/test/scala-3/sttp/tapir/codec/refined/TapirCodecRefinedTestScala3.scala @@ -1,7 +1,7 @@ package sttp.tapir.codec.refined import eu.timepit.refined.api.Refined -import eu.timepit.refined.boolean.Or +import eu.timepit.refined.boolean.* import eu.timepit.refined.collection.{MaxSize, MinSize, NonEmpty} import eu.timepit.refined.numeric.{Greater, GreaterEqual, Interval, Less, LessEqual} import eu.timepit.refined.string.MatchesRegex @@ -161,6 +161,17 @@ class TapirCodecRefinedTestScala3 extends AnyFlatSpec with Matchers with TapirCo case Validator.Mapped(Validator.Any(List(Validator.Min(3, true), Validator.Max(-3, true))), _) => } } + + "Generated schema for NonEmpty and MinSize" should "not be optional" in { + assert(implicitly[Schema[List[Int]]].isOptional) + assert(!implicitly[Schema[List[Int] Refined NonEmpty]].isOptional) + assert(!implicitly[Schema[Set[Int] Refined NonEmpty]].isOptional) + assert(!implicitly[Schema[List[Int] Refined MinSize[3]]].isOptional) + assert(!implicitly[Schema[List[Int] Refined (MinSize[3] And MaxSize[6])]].isOptional) + assert(implicitly[Schema[List[Int] Refined MinSize[0]]].isOptional) + assert(implicitly[Schema[List[Int] Refined MaxSize[5]]].isOptional) + assert(implicitly[Schema[Option[List[Int] Refined NonEmpty]]].isOptional) + } "TapirCodecRefined" should "compile using implicit schema for refined types" in { import io.circe.refined._ diff --git a/integrations/refined/src/test/scala/sttp/tapir/codec/refined/TapirCodecRefinedTest.scala b/integrations/refined/src/test/scala/sttp/tapir/codec/refined/TapirCodecRefinedTest.scala index 560a91418f..e310ccc88b 100644 --- a/integrations/refined/src/test/scala/sttp/tapir/codec/refined/TapirCodecRefinedTest.scala +++ b/integrations/refined/src/test/scala/sttp/tapir/codec/refined/TapirCodecRefinedTest.scala @@ -1,8 +1,6 @@ package sttp.tapir.codec.refined import eu.timepit.refined.api.Refined -import eu.timepit.refined.boolean._ -import eu.timepit.refined.collection._ import eu.timepit.refined.numeric.{Negative, NonNegative, NonPositive, Positive} import eu.timepit.refined.string.IPv4 import eu.timepit.refined.types.string.NonEmptyString @@ -61,17 +59,6 @@ class TapirCodecRefinedTest extends AnyFlatSpec with Matchers with TapirCodecRef implicitly[Schema[LimitedInt]].validator should matchPattern { case Validator.Mapped(Validator.Max(0, true), _) => } } - "Generated schema for NonEmpty and MinSize" should "not be optional" in { - assert(implicitly[Schema[List[Int]]].isOptional) - assert(!implicitly[Schema[List[Int] Refined NonEmpty]].isOptional) - assert(!implicitly[Schema[Set[Int] Refined NonEmpty]].isOptional) - assert(!implicitly[Schema[List[Int] Refined MinSize[3]]].isOptional) - assert(!implicitly[Schema[List[Int] Refined (MinSize[3] And MaxSize[6])]].isOptional) - assert(implicitly[Schema[List[Int] Refined MinSize[0]]].isOptional) - assert(implicitly[Schema[List[Int] Refined MaxSize[5]]].isOptional) - assert(implicitly[Schema[Option[List[Int] Refined NonEmpty]]].isOptional) - } - "Using refined" should "compile when using tapir endpoints" in { // this used to cause a: // [error] java.lang.StackOverflowError