Skip to content

Commit

Permalink
Support MinLength, MaxLength, Not[Empty] for Iron
Browse files Browse the repository at this point in the history
  • Loading branch information
kciesielski committed Jun 13, 2024
1 parent c5c59e1 commit 1b09646
Show file tree
Hide file tree
Showing 2 changed files with 53 additions and 11 deletions.
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,17 @@ 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] =
println("Getting schema for iterable")
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 +95,20 @@ 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]] =
println(s"Building validator from primitive validator min length ${witness.value}")
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,15 +134,19 @@ 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] =
new ValidatorForPredicate[N, Predicates] {

val intersectionConstraint = new Constraint.IntersectionConstraint[N, Predicates]
val validatorsForPredicates: List[ValidatorForPredicate[N, Any]] = summonValidators[N, mirror.ElementTypes]
println(s"Validators: ${validatorsForPredicates.map(_.validator)}")

override def validator: Validator[N] = Validator.all(validatorsForPredicates.map(_.validator): _*)

Expand Down Expand Up @@ -222,6 +250,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

0 comments on commit 1b09646

Please sign in to comment.