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

Iron: Support MinLength, MaxLength, Not[Empty] for Iterables #3843

Merged
merged 1 commit into from
Jun 14, 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 @@ -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
Expand All @@ -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],
Expand Down Expand Up @@ -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))

Expand All @@ -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] =
Expand Down Expand Up @@ -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] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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]]
Expand Down
Loading