From 5c7392270508b774a3be858bbc3f3f175316fe81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pawlik?= Date: Thu, 10 Aug 2023 16:30:13 +0200 Subject: [PATCH] Implement support for iron types (#3038) --- build.sbt | 18 +- .../typelevel/IntersectionTypeMirror.scala | 70 ++++++ .../tapir/typelevel/UnionTypeMirror.scala | 72 ++++++ doc/contributing.md | 2 + doc/endpoint/integrations.md | 18 ++ doc/stability.md | 1 + .../sttp/iron/codec/iron/TapirCodecIron.scala | 227 ++++++++++++++++++ .../scala/sttp/iron/codec/iron/package.scala | 3 + .../codec/iron/TapirCodecIronTestScala3.scala | 218 +++++++++++++++++ project/Versions.scala | 1 + 10 files changed, 629 insertions(+), 1 deletion(-) create mode 100644 core/src/main/scala-3/sttp/tapir/typelevel/IntersectionTypeMirror.scala create mode 100644 core/src/main/scala-3/sttp/tapir/typelevel/UnionTypeMirror.scala create mode 100644 integrations/iron/src/main/scala/sttp/iron/codec/iron/TapirCodecIron.scala create mode 100644 integrations/iron/src/main/scala/sttp/iron/codec/iron/package.scala create mode 100644 integrations/iron/src/test/scala-3/sttp/iron/codec/iron/TapirCodecIronTestScala3.scala diff --git a/build.sbt b/build.sbt index 16c6d5f433..091db72913 100644 --- a/build.sbt +++ b/build.sbt @@ -164,6 +164,7 @@ lazy val rawAllAggregates = core.projectRefs ++ catsEffect.projectRefs ++ enumeratum.projectRefs ++ refined.projectRefs ++ + iron.projectRefs ++ zio1.projectRefs ++ zio.projectRefs ++ newtype.projectRefs ++ @@ -634,7 +635,7 @@ lazy val refined: ProjectMatrix = (projectMatrix in file("integrations/refined") ) .jvmPlatform(scalaVersions = scala2And3Versions) .jsPlatform( - scalaVersions = scala2And3Versions, + scalaVersions = List(scala3), settings = commonJsSettings ++ Seq( libraryDependencies ++= Seq( "io.github.cquiroz" %%% "scala-java-time" % Versions.jsScalaJavaTime % Test @@ -643,6 +644,21 @@ lazy val refined: ProjectMatrix = (projectMatrix in file("integrations/refined") ) .dependsOn(core, circeJson % Test) +lazy val iron: ProjectMatrix = (projectMatrix in file("integrations/iron")) + .settings(commonSettings) + .settings( + name := "tapir-iron", + libraryDependencies ++= Seq( + "io.github.iltotore" %% "iron" % Versions.iron, + scalaTest.value % Test + ) + ) + .jvmPlatform(scalaVersions = List(scala3)) + .jsPlatform( + scalaVersions = List(scala3) + ) + .dependsOn(core) + lazy val zio1: ProjectMatrix = (projectMatrix in file("integrations/zio1")) .settings(commonSettings) .settings( diff --git a/core/src/main/scala-3/sttp/tapir/typelevel/IntersectionTypeMirror.scala b/core/src/main/scala-3/sttp/tapir/typelevel/IntersectionTypeMirror.scala new file mode 100644 index 0000000000..da5af7c6e2 --- /dev/null +++ b/core/src/main/scala-3/sttp/tapir/typelevel/IntersectionTypeMirror.scala @@ -0,0 +1,70 @@ +package sttp.tapir.typelevel +import scala.quoted.Quotes + +import scala.annotation.implicitNotFound +import scala.quoted.* +import scala.collection.View.Empty + +trait IntersectionTypeMirror[A] { + + type ElementTypes <: Tuple +} + +// Building a class is more convenient to instantiate using macros +class IntersectionTypeMirrorImpl[A, T <: Tuple] extends IntersectionTypeMirror[A] { + override type ElementTypes = T +} + +object IntersectionTypeMirror { + + transparent inline given derived[A]: IntersectionTypeMirror[A] = ${ derivedImpl[A] } + + private def derivedImpl[A](using Quotes, Type[A]): Expr[IntersectionTypeMirror[A]] = { + import quotes.reflect.* + + val tplPrependType = TypeRepr.of[? *: ?] + val tplConcatType = TypeRepr.of[Tuple.Concat] + + def prependTypes(head: TypeRepr, tail: TypeRepr): TypeRepr = + AppliedType(tplPrependType, List(head, tail)) + + def concatTypes(left: TypeRepr, right: TypeRepr): TypeRepr = + AppliedType(tplConcatType, List(left, right)) + + def rec(tpe: TypeRepr): TypeRepr = { + tpe.dealias match + case AndType(left, right) => concatTypes(rec(left), rec(right)) + case t => prependTypes(t, TypeRepr.of[EmptyTuple]) + } + val tupled = + TypeRepr.of[A].dealias match { + case and: AndType => rec(and).asType.asInstanceOf[Type[Elems]] + case tpe => report.errorAndAbort(s"${tpe.show} is not an intersection type") + } + type Elems + + given Type[Elems] = tupled + + Apply( // Passing the type using quotations causes the type to not be inlined + TypeApply( + Select.unique( + New( + Applied( + TypeTree.of[IntersectionTypeMirrorImpl], + List( + TypeTree.of[A], + TypeTree.of[Elems] + ) + ) + ), + "" + ), + List( + TypeTree.of[A], + TypeTree.of[Elems] + ) + ), + Nil + ).asExprOf[IntersectionTypeMirror[A]] + } +} diff --git a/core/src/main/scala-3/sttp/tapir/typelevel/UnionTypeMirror.scala b/core/src/main/scala-3/sttp/tapir/typelevel/UnionTypeMirror.scala new file mode 100644 index 0000000000..2da8d1c408 --- /dev/null +++ b/core/src/main/scala-3/sttp/tapir/typelevel/UnionTypeMirror.scala @@ -0,0 +1,72 @@ +package sttp.tapir.typelevel + +import scala.quoted.Quotes + +import scala.annotation.implicitNotFound +import scala.quoted.* +import scala.collection.View.Empty + +trait UnionTypeMirror[A] { + + type ElementTypes <: Tuple +} + +// Building a class is more convenient to instantiate using macros +class UnionTypeMirrorImpl[A, T <: Tuple] extends UnionTypeMirror[A] { + + override type ElementTypes = T +} + +object UnionTypeMirror { + transparent inline given derived[A]: UnionTypeMirror[A] = ${ derivedImpl[A] } + + private def derivedImpl[A](using Quotes, Type[A]): Expr[UnionTypeMirror[A]] = { + import quotes.reflect.* + + val tplPrependType = TypeRepr.of[? *: ?] + val tplConcatType = TypeRepr.of[Tuple.Concat] + + def prependTypes(head: TypeRepr, tail: TypeRepr): TypeRepr = + AppliedType(tplPrependType, List(head, tail)) + + def concatTypes(left: TypeRepr, right: TypeRepr): TypeRepr = + AppliedType(tplConcatType, List(left, right)) + + def rec(tpe: TypeRepr): TypeRepr = + tpe.dealias match { + case OrType(left, right) => concatTypes(rec(left), rec(right)) + case t => prependTypes(t, TypeRepr.of[EmptyTuple]) + } + val tupled = + TypeRepr.of[A].dealias match { + case or: OrType => rec(or).asType.asInstanceOf[Type[Elems]] + case tpe => report.errorAndAbort(s"${tpe.show} is not a union type") + } + + type Elems + + given Type[Elems] = tupled + + Apply( // Passing the type using quotations causes the type to not be inlined + TypeApply( + Select.unique( + New( + Applied( + TypeTree.of[UnionTypeMirrorImpl], + List( + TypeTree.of[A], + TypeTree.of[Elems] + ) + ) + ), + "" + ), + List( + TypeTree.of[A], + TypeTree.of[Elems] + ) + ), + Nil + ).asExprOf[UnionTypeMirror[A]] + } +} diff --git a/doc/contributing.md b/doc/contributing.md index e4e5421705..01909f7047 100644 --- a/doc/contributing.md +++ b/doc/contributing.md @@ -15,4 +15,6 @@ Tuple-concatenating code is copied from [akka-http](https://github.com/akka/akka Parts of generic derivation configuration is copied from [circe](https://github.com/circe/circe/blob/master/modules/generic-extras/src/main/scala/io/circe/generic/extras/Configuration.scala) +Implementation of mirror for union and intersection types are originally implemented by [Iltotore](https://github.com/Iltotore) in [this gist](https://gist.github.com/Iltotore/eece20188d383f7aee16a0b89eeb887f) + Tapir logo & stickers have been drawn by [impurepics](https://twitter.com/impurepics). \ No newline at end of file diff --git a/doc/endpoint/integrations.md b/doc/endpoint/integrations.md index e8e055c52d..32d5ee1f38 100644 --- a/doc/endpoint/integrations.md +++ b/doc/endpoint/integrations.md @@ -48,6 +48,24 @@ If you are not satisfied with the validator generated by `tapir-refined`, you ca `ValidatorForPredicate[T, P]` in scope using `ValidatorForPredicate.fromPrimitiveValidator` to build it (do not hesitate to contribute your work!). +## Iron integration + +If you use [iron](https://github.com/Iltotore/iron), the `tapir-iron` module will provide implicit codecs and +validators for `T :| P` as long as a codec for `T` already exists: + +```scala +"com.softwaremill.sttp.tapir" %% "tapir-iron" % "@VERSION@" +``` + +The module is only available for Scala 3 since iron is not designed to work with Scala 2. + +You'll need to extend the `sttp.tapir.codec.refined.TapirCodecIron` +trait or `import sttp.tapir.codec.iron._` to bring the implicit values into scope. + +The iron codecs contain a validator which apply the constraint to validated value. + +Similarly to `tapir-refined`, you can find the predicate logic in `integrations/iron/src/main/scala/sttp/iron/codec/iron/TapirCodecIron.scala` and provide your own given `ValidatorForPredicate[T, P]` in scope using `ValidatorForPredicate.fromPrimitiveValidator` + ## Enumeratum integration The `tapir-enumeratum` module provides schemas, validators and codecs for [Enumeratum](https://github.com/lloydmeta/enumeratum) diff --git a/doc/stability.md b/doc/stability.md index 28ff011420..96599e822f 100644 --- a/doc/stability.md +++ b/doc/stability.md @@ -68,6 +68,7 @@ The modules are categorised using the following levels: | zio | experimental | | zio1 | stabilising | | zio-prelude | experimental | +| iron | experimental | ## JSON modules 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 new file mode 100644 index 0000000000..032f447fcb --- /dev/null +++ b/integrations/iron/src/main/scala/sttp/iron/codec/iron/TapirCodecIron.scala @@ -0,0 +1,227 @@ +package sttp.tapir.codec.iron + +import io.github.iltotore.iron.Constraint +import io.github.iltotore.iron.:| +import io.github.iltotore.iron.refineEither +import io.github.iltotore.iron.refineOption +import io.github.iltotore.iron.constraint.any.* +import io.github.iltotore.iron.constraint.string.* +import io.github.iltotore.iron.constraint.collection.* +import io.github.iltotore.iron.constraint.numeric.* + +import sttp.tapir.Codec +import sttp.tapir.CodecFormat +import sttp.tapir.DecodeResult +import sttp.tapir.Schema +import sttp.tapir.Validator +import sttp.tapir.Validator.Primitive +import sttp.tapir.ValidationError +import sttp.tapir.ValidationResult +import sttp.tapir.typelevel.IntersectionTypeMirror +import sttp.tapir.typelevel.UnionTypeMirror + +import scala.compiletime.* +import scala.util.NotGiven + +trait TapirCodecIron extends DescriptionWitness with LowPriorityValidatorForPredicate { + + inline given ironTypeSchema[Value, Predicate](using + inline vSchema: Schema[Value], + inline constraint: Constraint[Value, Predicate], + inline validatorTranslation: ValidatorForPredicate[Value, Predicate] + ): Schema[Value :| Predicate] = + vSchema.validate(validatorTranslation.validator).map[Value :| Predicate](v => v.refineOption[Predicate])(identity) + + inline given [Representation, Value, Predicate, CF <: CodecFormat](using + inline tm: Codec[Representation, Value, CF], + inline constraint: Constraint[Value, Predicate], + inline validatorTranslation: ValidatorForPredicate[Value, Predicate] + ): Codec[Representation, Value :| Predicate, CF] = + summon[Codec[Representation, Value, CF]] + .validate(validatorTranslation.validator) + .mapDecode { (v: Value) => + v.refineEither[Predicate] match { + case Right(refined) => DecodeResult.Value[Value :| Predicate](refined) + case Left(errorMessage) => + DecodeResult.InvalidValue(validatorTranslation.makeErrors(v, constraint.message)) + } + }(identity) + + inline given (using + inline vSchema: Schema[String], + inline constraint: Constraint[String, ValidUUID], + inline validatorTranslation: ValidatorForPredicate[String, ValidUUID] + ): Schema[String :| ValidUUID] = + ironTypeSchema[String, ValidUUID].format("uuid") + + inline given PrimitiveValidatorForPredicate[String, Not[Empty]] = + ValidatorForPredicate.fromPrimitiveValidator[String, Not[Empty]](Validator.minLength(1)) + + inline given validatorForMatchesRegexpString[S <: String](using witness: ValueOf[S]): PrimitiveValidatorForPredicate[String, Match[S]] = + ValidatorForPredicate.fromPrimitiveValidator(Validator.pattern[String](witness.value)) + + inline given validatorForMaxSizeOnString[T <: String, NM <: Int](using + witness: ValueOf[NM] + ): PrimitiveValidatorForPredicate[T, MaxLength[NM]] = + ValidatorForPredicate.fromPrimitiveValidator(Validator.maxLength[T](witness.value)) + + inline given validatorForMinSizeOnString[T <: String, NM <: Int](using + witness: ValueOf[NM] + ): PrimitiveValidatorForPredicate[T, MinLength[NM]] = + ValidatorForPredicate.fromPrimitiveValidator(Validator.minLength[T](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)) + + inline given validatorForLessEqual[N: Numeric, NM <: N](using witness: ValueOf[NM]): PrimitiveValidatorForPredicate[N, LessEqual[NM]] = + ValidatorForPredicate.fromPrimitiveValidator(Validator.max(witness.value, exclusive = false)) + + inline given validatorForGreater[N: Numeric, NM <: N](using witness: ValueOf[NM]): PrimitiveValidatorForPredicate[N, Greater[NM]] = + ValidatorForPredicate.fromPrimitiveValidator(Validator.min(witness.value, exclusive = true)) + + inline given validatorForGreaterEqual[N: Numeric, NM <: N](using + witness: ValueOf[NM] + ): PrimitiveValidatorForPredicate[N, GreaterEqual[NM]] = + ValidatorForPredicate.fromPrimitiveValidator(Validator.min(witness.value, exclusive = false)) + + inline given validatorForStrictEqual[N: Numeric, NM <: N](using + witness: ValueOf[NM] + ): PrimitiveValidatorForPredicate[N, StrictEqual[NM]] = + ValidatorForPredicate.fromPrimitiveValidator( + Validator.enumeration[N](List(witness.value)) + ) + + private inline def summonValidators[N, A <: Tuple]: List[ValidatorForPredicate[N, Any]] = { + inline erasedValue[A] match + case _: EmptyTuple => Nil + case _: (head *: tail) => + summonInline[ValidatorForPredicate[N, head]] + .asInstanceOf[ValidatorForPredicate[N, Any]] :: summonValidators[N, tail] + } + + inline given validatorForAnd[N, Predicates](using mirror: IntersectionTypeMirror[Predicates]): ValidatorForPredicate[N, Predicates] = + new ValidatorForPredicate[N, Predicates] { + + val intersectionConstraint = new Constraint.IntersectionConstraint[N, Predicates] + val validatorsForPredicates: List[ValidatorForPredicate[N, Any]] = summonValidators[N, mirror.ElementTypes] + + override def validator: Validator[N] = Validator.all(validatorsForPredicates.map(_.validator): _*) + + override def makeErrors(value: N, errorMessage: String): List[ValidationError[_]] = + if (!intersectionConstraint.test(value)) + List( + ValidationError[N]( + Validator.Custom(_ => + ValidationResult.Invalid(intersectionConstraint.message) // at this point the validator is already failed anyway + ), + value + ) + ) + else Nil + + } + + inline given validatorForOr[N, Predicates](using mirror: UnionTypeMirror[Predicates]): ValidatorForPredicate[N, Predicates] = + new ValidatorForPredicate[N, Predicates] { + + val unionConstraint = new Constraint.UnionConstraint[N, Predicates] + val validatorsForPredicates: List[ValidatorForPredicate[N, Any]] = summonValidators[N, mirror.ElementTypes] + + override def validator: Validator[N] = Validator.any(validatorsForPredicates.map(_.validator): _*) + + override def makeErrors(value: N, errorMessage: String): List[ValidationError[_]] = + if (!unionConstraint.test(value)) + List( + ValidationError[N]( + Validator.Custom(_ => + ValidationResult.Invalid(unionConstraint.message) // at this point the validator is already failed anyway + ), + value + ) + ) + else Nil + + } + + inline given validatorForDescribedAnd[N, P](using + id: IsDescription[P], + mirror: IntersectionTypeMirror[id.Predicate] + ): ValidatorForPredicate[N, P] = + validatorForAnd[N, id.Predicate].asInstanceOf[ValidatorForPredicate[N, P]] + + inline given validatorForDescribedOr[N, P](using + id: IsDescription[P], + mirror: UnionTypeMirror[id.Predicate] + ): ValidatorForPredicate[N, P] = + validatorForOr[N, id.Predicate].asInstanceOf[ValidatorForPredicate[N, P]] + + inline given validatorForDescribedPrimitive[N, P](using + id: IsDescription[P], + notUnion: NotGiven[UnionTypeMirror[id.Predicate]], + notIntersection: NotGiven[IntersectionTypeMirror[id.Predicate]], + inline validator: ValidatorForPredicate[N, id.Predicate] + ): ValidatorForPredicate[N, P] = + validator.asInstanceOf[ValidatorForPredicate[N, P]] + +} + +private[iron] trait ValidatorForPredicate[Value, Predicate] { + def validator: Validator[Value] + def makeErrors(value: Value, errorMessage: String): List[ValidationError[_]] +} + +private[iron] trait PrimitiveValidatorForPredicate[Value, Predicate] extends ValidatorForPredicate[Value, Predicate] { + def validator: Validator.Primitive[Value] + def makeErrors(value: Value, errorMessage: String): List[ValidationError[_]] +} + +private[iron] object ValidatorForPredicate { + def fromPrimitiveValidator[Value, Predicate]( + primitiveValidator: Validator.Primitive[Value] + ): PrimitiveValidatorForPredicate[Value, Predicate] = + new PrimitiveValidatorForPredicate[Value, Predicate] { + override def validator: Validator.Primitive[Value] = primitiveValidator + override def makeErrors(value: Value, errorMessage: String): List[ValidationError[_]] = + List(ValidationError[Value](primitiveValidator, value)) + } +} + +private[iron] trait LowPriorityValidatorForPredicate { + + inline given [Value, Predicate](using + inline constraint: Constraint[Value, Predicate], + id: IsDescription[Predicate], + notIntersection: NotGiven[IntersectionTypeMirror[id.Predicate]], + notUnion: NotGiven[UnionTypeMirror[id.Predicate]] + ): ValidatorForPredicate[Value, Predicate] = + new ValidatorForPredicate[Value, Predicate] { + + override val validator: Validator.Custom[Value] = Validator.Custom { v => + if (constraint.test(v)) ValidationResult.Valid + else ValidationResult.Invalid(constraint.message) + } + + override def makeErrors(value: Value, errorMessage: String): List[ValidationError[_]] = + List(ValidationError[Value](validator, value, Nil, Some(errorMessage))) + } +} + +private[iron] trait DescriptionWitness { + + trait IsDescription[A] { + type Predicate + type Description + } + + class DescriptionTypeMirrorImpl[A, P, D <: String] extends IsDescription[A] { + override type Predicate = P + override type Description = D + } + object IsDescription { + transparent inline given derived[A]: IsDescription[A] = + inline erasedValue[A] match { + case _: DescribedAs[p, d] => new DescriptionTypeMirrorImpl[A, p, d] + } + } + +} diff --git a/integrations/iron/src/main/scala/sttp/iron/codec/iron/package.scala b/integrations/iron/src/main/scala/sttp/iron/codec/iron/package.scala new file mode 100644 index 0000000000..293488e261 --- /dev/null +++ b/integrations/iron/src/main/scala/sttp/iron/codec/iron/package.scala @@ -0,0 +1,3 @@ +package sttp.tapir.codec + +package object iron extends TapirCodecIron 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 new file mode 100644 index 0000000000..4b57a4faac --- /dev/null +++ b/integrations/iron/src/test/scala-3/sttp/iron/codec/iron/TapirCodecIronTestScala3.scala @@ -0,0 +1,218 @@ +package sttp.iron.codec.iron + +import io.github.iltotore.iron.* + +import sttp.tapir.Codec.PlainCodec +import sttp.tapir.CodecFormat +import sttp.tapir.CodecFormat.TextPlain +import sttp.tapir.Codec +import sttp.tapir.Schema + +import sttp.tapir.codec.iron.given +import sttp.tapir.codec.iron.* + +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers +import sttp.tapir.DecodeResult +import io.github.iltotore.iron.constraint.all.* +import sttp.tapir.Validator +import sttp.tapir.ValidationError + +class TapirCodecIronTestScala3 extends AnyFlatSpec with Matchers { + + val schema: Schema[Double :| Positive] = summon[Schema[Double :| Positive]] + + val codec: Codec[String, Double :| Positive, TextPlain] = + summon[Codec[String, Double :| Positive, TextPlain]] + + "Generated codec" should "correctly delegate to raw parser and refine it" in { + 10.refineEither[Positive] match { + case Right(nes) => codec.decode("10") shouldBe DecodeResult.Value(nes) + case Left(_) => fail() + } + } + + "Generated codec for MatchesRegex" should "use tapir Validator.Pattern" in { + type VariableConstraint = Match["[a-zA-Z][-a-zA-Z0-9_]*"] + type VariableString = String :| 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 => + } + } + + it should "decode value matching pattern" in { + type VariableConstraint = Match["[a-zA-Z][-a-zA-Z0-9_]*"] + type VariableString = String :| VariableConstraint + val identifierCodec = implicitly[PlainCodec[VariableString]] + "ok".refineEither[VariableConstraint] match { + case Right(s) => identifierCodec.decode("ok") shouldBe DecodeResult.Value(s) + case Left(_) => fail() + } + } + + "Generated codec for MaxLength on string" should "use tapir Validator.maxLength" in { + type VariableConstraint = MaxLength[2] + type VariableString = String :| VariableConstraint + val identifierCodec = implicitly[PlainCodec[VariableString]] + + val expectedValidator: Validator[String] = Validator.maxLength(2) + identifierCodec.decode("bad") should matchPattern { + case DecodeResult.InvalidValue(List(ValidationError(validator, "bad", _, _))) if validator == expectedValidator => + } + } + + "Generated codec for MinLength on string" should "use tapir Validator.minLength" in { + type VariableConstraint = MinLength[42] + type VariableString = String :| VariableConstraint + val identifierCodec = implicitly[PlainCodec[VariableString]] + + val expectedValidator: Validator[String] = Validator.minLength(42) + identifierCodec.decode("bad") should matchPattern { + case DecodeResult.InvalidValue(List(ValidationError(validator, "bad", _, _))) if validator == expectedValidator => + } + } + + "Generated codec for Less" should "use tapir Validator.max" in { + type IntConstraint = Less[3] + type LimitedInt = Int :| 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.max" in { + type IntConstraint = LessEqual[3] + type LimitedInt = Int :| IntConstraint + val limitedIntCodec = summon[PlainCodec[LimitedInt]] + + val expectedValidator: Validator[Int] = Validator.max(3) + limitedIntCodec.decode("4") should matchPattern { + case DecodeResult.InvalidValue(List(ValidationError(validator, 4, _, _))) if validator == expectedValidator => + } + } + + "Generated codec for Greater" should "use tapir Validator.min" in { + type IntConstraint = Greater[3] + type LimitedInt = Int :| IntConstraint + val limitedIntCodec = summon[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 GreaterEqual" should "use tapir Validator.min" in { + type IntConstraint = GreaterEqual[3] + type LimitedInt = Int :| IntConstraint + val limitedIntCodec = summon[PlainCodec[LimitedInt]] + + val expectedValidator: Validator[Int] = Validator.min(3) + limitedIntCodec.decode("2") should matchPattern { + case DecodeResult.InvalidValue(List(ValidationError(validator, 2, _, _))) if validator == expectedValidator => + } + } + + "Generated validator for Greater" should "use tapir Validator.min" in { + type IntConstraint = Greater[3] + type LimitedInt = Int :| IntConstraint + + summon[Schema[LimitedInt]].validator should matchPattern { case Validator.Mapped(Validator.Min(3, true), _) => + } + } + + "Generated validator for Interval.Open" should "use tapir Validator.min and Validator.max" in { + type IntConstraint = Interval.Open[1, 3] + type LimitedInt = Int :| IntConstraint + + summon[Schema[LimitedInt]].validator should matchPattern { + case Validator.Mapped(Validator.All(List(Validator.Min(1, true), Validator.Max(3, true))), _) => + } + } + + "Generated validator for Interval.Close" should "use tapir Validator.min and Validator.max" in { + type IntConstraint = Interval.Closed[1, 3] + type LimitedInt = Int :| IntConstraint + + summon[Schema[LimitedInt]].validator should matchPattern { + case Validator.Mapped( + Validator.All( + List( + Validator.Any(List(Validator.Min(1, true), Validator.Enumeration(List(1), _, _))), + Validator.Any(List(Validator.Max(3, true), Validator.Enumeration(List(3), _, _))) + ) + ), + _ + ) => + } + } + + "Generated validator for Interval.OpenClose" should "use tapir Validator.min and Validator.max" in { + type IntConstraint = Interval.OpenClosed[1, 3] + type LimitedInt = Int :| IntConstraint + + summon[Schema[LimitedInt]].validator should matchPattern { + case Validator.Mapped( + Validator.All( + List( + Validator.Min(1, true), + Validator.Any(List(Validator.Max(3, true), Validator.Enumeration(List(3), _, _))) + ) + ), + _ + ) => + } + } + + "Generated validator for Interval.ClosedOpen" should "use tapir Validator.min and Validator.max" in { + type IntConstraint = Interval.ClosedOpen[1, 3] + type LimitedInt = Int :| IntConstraint + + summon[Schema[LimitedInt]].validator should matchPattern { + case Validator.Mapped( + Validator.All( + List( + Validator.Any(List(Validator.Min(1, true), Validator.Enumeration(List(1), _, _))), + Validator.Max(3, true) + ) + ), + _ + ) => + } + } + + "Generated validator for intersection of constraints" should "use tapir Validator.min and Validator.max" in { + type IntConstraint = Greater[1] & Less[3] + type LimitedInt = Int :| IntConstraint + + summon[Schema[LimitedInt]].validator should matchPattern { + case Validator.Mapped(Validator.All(List(Validator.Min(1, true), Validator.Max(3, true))), _) => + } + } + + "Generated validator for union of constraints" should "use tapir Validator.min and Validator.max" in { + type IntConstraint = Less[1] | Greater[3] + type LimitedInt = Int :| IntConstraint + + summon[Schema[LimitedInt]].validator should matchPattern { + case Validator.Mapped(Validator.Any(List(Validator.Max(1, true), Validator.Min(3, true))), _) => + } + } + + "Generated validator for described union" should "use tapir Validator.min and Validator.max" in { + type IntConstraint = (Less[1] | Greater[3]) 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.Any(List(Validator.Max(1, true), Validator.Min(3, true))), _) => + } + } + +} diff --git a/project/Versions.scala b/project/Versions.scala index c9d5bb47ce..cb668fe29e 100644 --- a/project/Versions.scala +++ b/project/Versions.scala @@ -27,6 +27,7 @@ object Versions { val scalaTest = "3.2.16" val scalaTestPlusScalaCheck = "3.2.16.0" val refined = "0.11.0" + val iron = "2.1.0" val enumeratum = "1.7.3" val zio1 = "1.0.18" val zio1InteropCats = "13.0.0.2"