Skip to content

Commit

Permalink
Support range/length refinement providers for enums (#1283)
Browse files Browse the repository at this point in the history
Co-authored-by: Jakub Kozłowski <[email protected]>
  • Loading branch information
Baccata and kubukoz authored Nov 2, 2023
1 parent f973517 commit 45970f5
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 58 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# 0.18.3

* Support constraint traits on members targeting enums

Although it's weird to allow it, it is actually supported in Smithy.

* Tweak operation schema `*Input` and `*Output` functions

Some schema visitor will adjust their behaviour if a shape is the input or the output of an operation. For this reason we have a `InputOutput` class with a `Input` and `Output` hint that you can add to schemas to adjust the behaviour. `OperationSchema` has functions to work on input schemas and output schemas of an operation. This change makes these functions automatically add the relevant hint.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package smithy4s.example

import smithy4s.Hints
import smithy4s.Schema
import smithy4s.ShapeId
import smithy4s.ShapeTag
import smithy4s.schema.Schema.struct

/** @param card
* FaceCard types
*/
final case class StructureConstrainingEnum(letter: Option[Letters] = None, card: Option[FaceCard] = None)

object StructureConstrainingEnum extends ShapeTag.Companion[StructureConstrainingEnum] {
val id: ShapeId = ShapeId("smithy4s.example", "StructureConstrainingEnum")

val hints: Hints = Hints.empty

implicit val schema: Schema[StructureConstrainingEnum] = struct(
Letters.schema.validated(smithy.api.Length(min = Some(2L), max = None)).validated(smithy.api.Pattern(s"$$aaa$$")).optional[StructureConstrainingEnum]("letter", _.letter),
FaceCard.schema.validated(smithy.api.Range(min = None, max = Some(scala.math.BigDecimal(1.0)))).optional[StructureConstrainingEnum]("card", _.card),
){
StructureConstrainingEnum.apply
}.withId(id).addHints(hints)
}
138 changes: 80 additions & 58 deletions modules/core/src/smithy4s/RefinementProvider.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package smithy4s

import smithy.api.Length
import smithy.api.Pattern
import smithy.api.Range

/**
* Given a constraint of type C, an RefinementProvider can produce a Refinement that
Expand All @@ -39,9 +40,46 @@ trait RefinementProvider[C, A, B] { self =>
}
}

object RefinementProvider {
object RefinementProvider extends LowPriorityImplicits {

private abstract class SimpleImpl[C, A](implicit _tag: ShapeTag[C])
type Simple[C, A] = RefinementProvider[C, A, A]

implicit val stringLengthConstraint: Simple[Length, String] =
new LengthConstraint[String](_.length)

implicit val blobLengthConstraint: Simple[Length, Blob] =
new LengthConstraint[Blob](_.size)

implicit def iterableLengthConstraint[C[_], A](implicit
ev: C[A] <:< Iterable[A]
): Simple[Length, C[A]] =
new LengthConstraint[C[A]](ca => ev(ca).size)

implicit def mapLengthConstraint[K, V]: Simple[Length, Map[K, V]] =
new LengthConstraint[Map[K, V]](_.size)

implicit val stringPatternConstraints: Simple[Pattern, String] =
new PatternConstraint[String](identity)

implicit def numericRangeConstraints[N: Numeric]
: Simple[smithy.api.Range, N] = new RangeConstraint[N, N](identity[N])

// Lazy to avoid some pernicious recursive initialisation issue between
// the ShapeId static object and the generated code that makes use of it,
// as the `IdRef` type is referenced here.
//
// The problem only occurs in JS/Native.
lazy implicit val idRefRefinement
: RefinementProvider[smithy.api.IdRef, String, ShapeId] =
Refinement.drivenBy[smithy.api.IdRef](
ShapeId.parse(_: String) match {
case None => Left("Invalid ShapeId")
case Some(value) => Right(value)
},
(_: ShapeId).show
)

private[smithy4s] abstract class SimpleImpl[C, A](implicit _tag: ShapeTag[C])
extends RefinementProvider[C, A, A] {

val tag: ShapeTag[C] = _tag
Expand All @@ -61,28 +99,7 @@ object RefinementProvider {

}

type Simple[C, A] = RefinementProvider[C, A, A]

implicit def isomorphismConstraint[C, A, A0](implicit
constraintOnA: Simple[C, A],
iso: Bijection[A, A0]
): Simple[C, A0] = constraintOnA.imapFull[A0, A0](iso, iso)

implicit val stringLengthConstraint: Simple[Length, String] =
new LengthConstraint[String](_.length)

implicit val blobLengthConstraint: Simple[Length, Blob] =
new LengthConstraint[Blob](_.size)

implicit def iterableLengthConstraint[C[_], A](implicit
ev: C[A] <:< Iterable[A]
): Simple[Length, C[A]] =
new LengthConstraint[C[A]](ca => ev(ca).size)

implicit def mapLengthConstraint[K, V]: Simple[Length, Map[K, V]] =
new LengthConstraint[Map[K, V]](_.size)

private class LengthConstraint[A](getLength: A => Int)
private[smithy4s] class LengthConstraint[A](getLength: A => Int)
extends SimpleImpl[Length, A] {

def get(lengthHint: Length): A => Either[String, Unit] = { (a: A) =>
Expand Down Expand Up @@ -111,33 +128,31 @@ object RefinementProvider {
}
}

implicit val stringPatternConstraints: Simple[Pattern, String] =
new SimpleImpl[Pattern, String] {

def get(
pattern: Pattern
): String => Either[String, Unit] = {
val regex = pattern.value.r
(input: String) =>
if (regex.findFirstIn(input).isDefined) Right(())
else
Left(
s"String '$input' does not match pattern '${pattern.value}'"
)
private[smithy4s] class PatternConstraint[E](getValue: E => String)
extends SimpleImpl[Pattern, E] {

def get(pattern: Pattern): E => Either[String, Unit] = {
val regex = pattern.value.r
(input: E) => {
val value = getValue(input)
if (regex.findFirstIn(getValue(input)).isDefined) Right(())
else
Left(
s"String '$value' does not match pattern '${pattern.value}'"
)
}

}
}

implicit def numericRangeConstraints[N: Numeric]
: Simple[smithy.api.Range, N] = new SimpleImpl[smithy.api.Range, N] {

private[smithy4s] class RangeConstraint[A, N: Numeric](getValue: A => N)
extends SimpleImpl[Range, A] {
def get(
range: smithy.api.Range
): N => Either[String, Unit] = {
): A => Either[String, Unit] = {
val N = implicitly[Numeric[N]]

(n: N) =>
val value = BigDecimal(N.toDouble(n))
(a: A) =>
val value = BigDecimal(N.toDouble(getValue(a)))
(range.min, range.max) match {
case (Some(min), Some(max)) =>
if (value >= min && value <= max) Right(())
Expand All @@ -162,18 +177,25 @@ object RefinementProvider {
}
}

// Lazy to avoid some pernicious recursive initialisation issue between
// the ShapeId static object and the generated code that makes use of it,
// as the `IdRef` type is referenced here.
//
// The problem only occurs in JS/Native.
lazy implicit val idRefRefinement
: RefinementProvider[smithy.api.IdRef, String, ShapeId] =
Refinement.drivenBy[smithy.api.IdRef](
ShapeId.parse(_: String) match {
case None => Left("Invalid ShapeId")
case Some(value) => Right(value)
},
(_: ShapeId).show
)
}

private[smithy4s] trait LowPriorityImplicits {

implicit def enumLengthConstraint[E <: Enumeration.Value]
: RefinementProvider[Length, E, E] =
new RefinementProvider.LengthConstraint[E](e => e.value.size)

implicit def enumRangeConstraint[E <: Enumeration.Value]
: RefinementProvider[Range, E, E] =
new RefinementProvider.RangeConstraint[E, Int](e => e.intValue)

implicit def enumPatternConstraint[E <: Enumeration.Value]
: RefinementProvider[Pattern, E, E] =
new RefinementProvider.PatternConstraint[E](e => e.value)

implicit def isomorphismConstraint[C, A, A0](implicit
constraintOnA: RefinementProvider.Simple[C, A],
iso: Bijection[A, A0]
): RefinementProvider[C, A0, A0] = constraintOnA.imapFull[A0, A0](iso, iso)

}
15 changes: 15 additions & 0 deletions sampleSpecs/constrainedEnum.smithy
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
$version: "2"

namespace smithy4s.example

// see https://github.com/disneystreaming/smithy4s/issues/1282
// We're testing that the render code compiles correctly, which
// depends on the presence of a RefinementProvider between Range
// and an enumeration value.
structure StructureConstrainingEnum {
@length(min: 2)
@pattern("$aaa$")
letter: Letters
@range(max: 1)
card: FaceCard
}

0 comments on commit 45970f5

Please sign in to comment.