From 72c8921ac84878a457282831e2f34dc7103a87ee Mon Sep 17 00:00:00 2001 From: kciesielski Date: Thu, 13 Jun 2024 15:00:13 +0200 Subject: [PATCH] Support MinLength, MaxLength, Not[Empty] for Iron --- .../sttp/iron/codec/iron/TapirCodecIron.scala | 37 +++++++++++++++++-- .../codec/iron/TapirCodecIronTestScala3.scala | 24 ++++++++---- 2 files changed, 50 insertions(+), 11 deletions(-) diff --git a/integrations/iron/src/main/scala/sttp/iron/codec/iron/TapirCodecIron.scala b/integrations/iron/src/main/scala/sttp/iron/codec/iron/TapirCodecIron.scala index 19f00a2155..3498f0ad5c 100644 --- a/integrations/iron/src/main/scala/sttp/iron/codec/iron/TapirCodecIron.scala +++ b/integrations/iron/src/main/scala/sttp/iron/codec/iron/TapirCodecIron.scala @@ -10,11 +10,10 @@ import io.github.iltotore.iron.constraint.string.* import io.github.iltotore.iron.constraint.collection.* import io.github.iltotore.iron.constraint.numeric.* -import sttp.shared.Identity - import sttp.tapir.Codec import sttp.tapir.CodecFormat import sttp.tapir.DecodeResult +import sttp.tapir.internal.* import sttp.tapir.Schema import sttp.tapir.Validator import sttp.tapir.Validator.Primitive @@ -35,6 +34,16 @@ trait TapirCodecIron extends DescriptionWitness with LowPriorityValidatorForPred ): Schema[Value :| Predicate] = vSchema.validate(validatorTranslation.validator).map[Value :| Predicate](v => v.refineOption[Predicate])(identity) + inline given ironTypeSchemaIterable[X, C[X] <: Iterable[X], Predicate](using + inline vSchema: Schema[C[X]], + inline constraint: Constraint[C[X], Predicate], + inline validatorTranslation: ValidatorForPredicate[C[X], Predicate] + ): Schema[C[X] :| Predicate] = + vSchema + .validate(validatorTranslation.validator) + .map[C[X] :| Predicate](v => v.refineOption[Predicate])(identity) + .copy(isOptional = if (validatorTranslation.containsMinSizePositive) false else vSchema.isOptional) + inline given ironTypeCodec[Representation, Value, Predicate, CF <: CodecFormat](using inline tm: Codec[Representation, Value, CF], inline constraint: Constraint[Value, Predicate], @@ -85,6 +94,19 @@ trait TapirCodecIron extends DescriptionWitness with LowPriorityValidatorForPred ): PrimitiveValidatorForPredicate[T, MinLength[NM]] = ValidatorForPredicate.fromPrimitiveValidator(Validator.minLength[T](witness.value)) + inline given validatorForMinLengthOnIterable[X, C[X] <: Iterable[X], NM <: Int](using + witness: ValueOf[NM] + ): PrimitiveValidatorForPredicate[C[X], MinLength[NM]] = + ValidatorForPredicate.fromPrimitiveValidator(Validator.minSize[X, C](witness.value)) + + inline given validatorForNonEmptyIterable[X, C[X] <: Iterable[X], NM <: Int]: PrimitiveValidatorForPredicate[C[X], Not[Empty]] = + ValidatorForPredicate.fromPrimitiveValidator(Validator.minSize[X, C](1)) + + inline given validatorForMaxLengthOnIterable[X, C[X] <: Iterable[X], NM <: Int](using + witness: ValueOf[NM] + ): PrimitiveValidatorForPredicate[C[X], MaxLength[NM]] = + ValidatorForPredicate.fromPrimitiveValidator(Validator.maxSize[X, C](witness.value)) + inline given validatorForLess[N: Numeric, NM <: N](using witness: ValueOf[NM]): PrimitiveValidatorForPredicate[N, Less[NM]] = ValidatorForPredicate.fromPrimitiveValidator(Validator.max(witness.value, exclusive = true)) @@ -110,8 +132,11 @@ trait TapirCodecIron extends DescriptionWitness with LowPriorityValidatorForPred inline erasedValue[A] match case _: EmptyTuple => Nil case _: (head *: tail) => - summonInline[ValidatorForPredicate[N, head]] - .asInstanceOf[ValidatorForPredicate[N, Any]] :: summonValidators[N, tail] + val headValidator: ValidatorForPredicate[N, ?] = summonFrom { + case pv: PrimitiveValidatorForPredicate[N, head] => pv + case _ => summonInline[ValidatorForPredicate[N, head]] + } + headValidator.asInstanceOf[ValidatorForPredicate[N, Any]] :: summonValidators[N, tail] } inline given validatorForAnd[N, Predicates](using mirror: IntersectionTypeMirror[Predicates]): ValidatorForPredicate[N, Predicates] = @@ -222,6 +247,10 @@ trait TapirCodecIron extends DescriptionWitness with LowPriorityValidatorForPred private[iron] trait ValidatorForPredicate[Value, Predicate] { def validator: Validator[Value] def makeErrors(value: Value, errorMessage: String): List[ValidationError[_]] + lazy val containsMinSizePositive: Boolean = validator.asPrimitiveValidators.exists { + case Validator.MinSize(a) => a > 0 + case _ => false + } } private[iron] trait PrimitiveValidatorForPredicate[Value, Predicate] extends ValidatorForPredicate[Value, Predicate] { diff --git a/integrations/iron/src/test/scala-3/sttp/iron/codec/iron/TapirCodecIronTestScala3.scala b/integrations/iron/src/test/scala-3/sttp/iron/codec/iron/TapirCodecIronTestScala3.scala index 9089f9c176..2291f598ff 100644 --- a/integrations/iron/src/test/scala-3/sttp/iron/codec/iron/TapirCodecIronTestScala3.scala +++ b/integrations/iron/src/test/scala-3/sttp/iron/codec/iron/TapirCodecIronTestScala3.scala @@ -191,16 +191,16 @@ class TapirCodecIronTestScala3 extends AnyFlatSpec with Matchers { } "Generated validator for union of constraints" should "use tapir Validator.min and strict equality (enumeration)" in { - type IntConstraint = StrictEqual[3] | Greater[5] + type IntConstraint = StrictEqual[3] | Greater[5] type LimitedInt = Int :| IntConstraint summon[Schema[LimitedInt]].validator should matchPattern { case Validator.Mapped(Validator.Any(List(Validator.Enumeration(List(3), _, _), Validator.Min(5, true))), _) => } } - + "Generated validator for union of constraints" should "put muiltiple StrictEquality into a single enum and follow with the rest of constrains" in { - type IntConstraint = StrictEqual[3] | StrictEqual[4] | StrictEqual[13] | GreaterEqual[23] + type IntConstraint = StrictEqual[3] | StrictEqual[4] | StrictEqual[13] | GreaterEqual[23] type LimitedInt = Int :| IntConstraint summon[Schema[LimitedInt]].validator should matchPattern { @@ -242,7 +242,7 @@ class TapirCodecIronTestScala3 extends AnyFlatSpec with Matchers { case Validator.Mapped(Validator.Any(List(Validator.Max(1, true), Validator.Min(3, true))), _) => } } - + "Generated validator for described union" should "work with strings" in { type StrConstraint = (Match["[a-c]*"] | Match["[x-z]*"]) DescribedAs ("Some description") type LimitedStr = String :| StrConstraint @@ -252,16 +252,26 @@ class TapirCodecIronTestScala3 extends AnyFlatSpec with Matchers { identifierCodec.decode("yzx") shouldBe DecodeResult.Value("yzx") identifierCodec.decode("aax") shouldBe a[DecodeResult.InvalidValue] } - + "Generated validator for described single constraint" should "use tapir Validator.max" in { type IntConstraint = (Less[1]) DescribedAs ("Should be included in less than 1 or more than 3") type LimitedInt = Int :| IntConstraint - summon[Schema[LimitedInt]].validator should matchPattern { - case Validator.Mapped(Validator.Max(1, true), _) => + summon[Schema[LimitedInt]].validator should matchPattern { case Validator.Mapped(Validator.Max(1, true), _) => } } + "Generated schema for NonEmpty and MinSize" should "not be optional" in { + assert(implicitly[Schema[List[Int]]].isOptional) + assert(!implicitly[Schema[List[Int] :| Not[Empty]]].isOptional) + assert(!implicitly[Schema[Set[Int] :| Not[Empty]]].isOptional) + assert(!implicitly[Schema[List[Int] :| MinLength[3]]].isOptional) + assert(!implicitly[Schema[List[Int] :| (MinLength[3] & MaxLength[6])]].isOptional) + assert(implicitly[Schema[List[Int] :| MinLength[0]]].isOptional) + assert(implicitly[Schema[List[Int] :| MaxLength[5]]].isOptional) + assert(implicitly[Schema[Option[List[Int] :| Not[Empty]]]].isOptional) + } + "Instances for opaque refined type" should "be correctly derived" in: summon[Schema[RefinedInt]] summon[Codec[String, RefinedInt, TextPlain]]