Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes in iron: union, intersection, Not[Empty] and other cases #3858

Merged
merged 3 commits into from
Jun 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,12 @@ object IntersectionTypeMirror {
def rec(tpe: TypeRepr): TypeRepr = {
tpe.dealias match
case AndType(left, right) => concatTypes(rec(left), rec(right))
case t => prependTypes(t, TypeRepr.of[EmptyTuple])
case t =>
// Intentionally using `tpe` instead of `t`. Dealiased representation `t` "loses" information
// about the original type from the intersection. For example, an Iron predicate `MinLength[N]`
// would be dealiased to `DescribedAs[Length[GreaterEqual[N]], _]`.
// Then, a given `ValidatorForPredicate[T, MinLength[N]]` would not be used in implicit resolution.
prependTypes(tpe, TypeRepr.of[EmptyTuple])
}
val tupled =
TypeRepr.of[A].dealias match {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,12 @@ trait TapirCodecIron extends DescriptionWitness with LowPriorityValidatorForPred
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
inline given validatorForMaxLengthOnString[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
inline given validatorForMinLengthOnString[T <: String, NM <: Int](using
witness: ValueOf[NM]
): PrimitiveValidatorForPredicate[T, MinLength[NM]] =
ValidatorForPredicate.fromPrimitiveValidator(Validator.minLength[T](witness.value))
Expand Down Expand Up @@ -133,8 +133,8 @@ trait TapirCodecIron extends DescriptionWitness with LowPriorityValidatorForPred
case _: EmptyTuple => Nil
case _: (head *: tail) =>
val headValidator: ValidatorForPredicate[N, ?] = summonFrom {
case pv: PrimitiveValidatorForPredicate[N, head] => pv
case _ => summonInline[ValidatorForPredicate[N, head]]
case pv: PrimitiveValidatorForPredicate[N, `head`] => pv
case _ => summonInline[ValidatorForPredicate[N, head]]
}
headValidator.asInstanceOf[ValidatorForPredicate[N, Any]] :: summonValidators[N, tail]
}
Expand Down Expand Up @@ -234,14 +234,14 @@ trait TapirCodecIron extends DescriptionWitness with LowPriorityValidatorForPred
singleton: ValueOf[Num]
): ValidatorForPredicate[N, P] =
validatorForLessEqual[N, Num].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]]

inline validator: PrimitiveValidatorForPredicate[N, id.Predicate]
): PrimitiveValidatorForPredicate[N, P] =
validator.asInstanceOf[PrimitiveValidatorForPredicate[N, P]]
}

private[iron] trait ValidatorForPredicate[Value, Predicate] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,12 @@ object UnionTypeMirror {
val (c1, rec1) = rec(left)
val (c2, rec2) = rec(right)
(c1 + c2, concatTypes(rec1, rec2))
case t => (1, prependTypes(t, TypeRepr.of[EmptyTuple]))
case t =>
// Intentionally using `tpe` instead of `t`. Dealiased representation `t` "loses" information
// about the original type from the union. For example, an Iron predicate `MinLength[N]`
// would be dealiased to `DescribedAs[Length[GreaterEqual[N]], _]`.
// Then, a given `ValidatorForPredicate[T, MinLength[N]]` would not be used in implicit resolution.
(1, prependTypes(tpe, TypeRepr.of[EmptyTuple]))
}
val (size, tupled) =
TypeRepr.of[A].dealias match {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,46 @@ class TapirCodecIronTestScala3 extends AnyFlatSpec with Matchers {
}
}

"Generated schema for described Int with extra constrains" should "apply given constrains" in {
val schema = implicitly[Schema[Int :| (Positive DescribedAs "Age should be positive")]]

schema.validator should matchPattern { case Validator.Mapped(Validator.Min(0, true), _) =>
}
}

"Generated schema for described String with extra constrains" should "apply given constrains" in {
type Constraint = (Not[Empty] & Alphanumeric) DescribedAs "name should not be empty and only made of alphanumeric characters"
type VariableString = String :| Constraint
val schema = implicitly[Schema[VariableString]]

schema.validator should matchPattern {
case Validator.Mapped(Validator.All(List(Validator.MinLength(1, false), Validator.Custom(_, _))), _) =>
}
val codec = implicitly[PlainCodec[VariableString]]
codec.decode("alpha1") shouldBe a[DecodeResult.Value[_]]
codec.decode("bad!") shouldBe a[DecodeResult.InvalidValue]
codec.decode("") shouldBe a[DecodeResult.InvalidValue]
codec.decode("954") shouldBe a[DecodeResult.Value[_]]
codec.decode("spaces not allowed") shouldBe a[DecodeResult.InvalidValue]
}

"Generated schema for non empty string" should "use a MinLength validator" in {
type VariableString = String :| Not[Empty]
val schema = implicitly[Schema[VariableString]]

schema.validator should matchPattern { case Validator.Mapped(Validator.MinLength(1, false), _) =>
}
}

"Generated schema for union and intersection on string" should "use a list of tapir validations" in {
type VariableString = String :| (MinLength[33] & MaxLength[83])
val schema = implicitly[Schema[VariableString]]

schema.validator should matchPattern {
case Validator.Mapped(Validator.All(List(Validator.MinLength(33, false), Validator.MaxLength(83, false))), _) =>
}
}

"Generated codec for Less" should "use tapir Validator.max" in {
type IntConstraint = Less[3]
type LimitedInt = Int :| IntConstraint
Expand Down
Loading