From af18cb8ffef63494b3523598083b798bf334ff57 Mon Sep 17 00:00:00 2001 From: msosnicki Date: Thu, 29 Jan 2026 15:34:18 +0100 Subject: [PATCH 1/9] Validators for collections --- .../smithy4s/example/AccountId.scala | 18 + .../ConstrainedListConstrainedMember.scala | 16 + .../generated/smithy4s/example/DeviceId.scala | 18 + .../example/ValidatedConstrainedList.scala | 19 + ...atedConstrainedListConstrainedMember.scala | 19 + ...strainedListRefinedConstrainedMember.scala | 20 + ...alidatedConstrainedListRefinedMember.scala | 18 + .../example/ValidatedConstrainedMap.scala | 20 + .../ValidatedListConstrainedMember.scala | 19 + .../example/ValidatedMapConstrainedKey.scala | 20 + .../ValidatedMapConstrainedValue.scala | 20 + .../example/ValidatedRefinedList.scala | 22 + ...alidatedRefinedListConstrainedMember.scala | 22 + .../smithy4s/example/ValidatedString.scala | 2 +- .../generated/smithy4s/example/package.scala | 13 + .../src/main/smithy4s/example/instances.scala | 13 + .../src/smithy4s/ValidatedNewtypesSpec.scala | 134 ++++-- .../smithy4s/codegen/internals/Renderer.scala | 69 ++-- .../codegen/internals/SmithyToIR.scala | 31 +- modules/core/src/smithy4s/Validator.scala | 381 ++++++++++++++++-- .../META-INF/smithy/smithy4s.meta.smithy | 7 +- sampleSpecs/validated-newtype.smithy | 89 ++++ 22 files changed, 886 insertions(+), 104 deletions(-) create mode 100644 modules/bootstrapped/src/generated/smithy4s/example/AccountId.scala create mode 100644 modules/bootstrapped/src/generated/smithy4s/example/ConstrainedListConstrainedMember.scala create mode 100644 modules/bootstrapped/src/generated/smithy4s/example/DeviceId.scala create mode 100644 modules/bootstrapped/src/generated/smithy4s/example/ValidatedConstrainedList.scala create mode 100644 modules/bootstrapped/src/generated/smithy4s/example/ValidatedConstrainedListConstrainedMember.scala create mode 100644 modules/bootstrapped/src/generated/smithy4s/example/ValidatedConstrainedListRefinedConstrainedMember.scala create mode 100644 modules/bootstrapped/src/generated/smithy4s/example/ValidatedConstrainedListRefinedMember.scala create mode 100644 modules/bootstrapped/src/generated/smithy4s/example/ValidatedConstrainedMap.scala create mode 100644 modules/bootstrapped/src/generated/smithy4s/example/ValidatedListConstrainedMember.scala create mode 100644 modules/bootstrapped/src/generated/smithy4s/example/ValidatedMapConstrainedKey.scala create mode 100644 modules/bootstrapped/src/generated/smithy4s/example/ValidatedMapConstrainedValue.scala create mode 100644 modules/bootstrapped/src/generated/smithy4s/example/ValidatedRefinedList.scala create mode 100644 modules/bootstrapped/src/generated/smithy4s/example/ValidatedRefinedListConstrainedMember.scala create mode 100644 modules/bootstrapped/src/main/smithy4s/example/instances.scala diff --git a/modules/bootstrapped/src/generated/smithy4s/example/AccountId.scala b/modules/bootstrapped/src/generated/smithy4s/example/AccountId.scala new file mode 100644 index 0000000000..8bb0916e90 --- /dev/null +++ b/modules/bootstrapped/src/generated/smithy4s/example/AccountId.scala @@ -0,0 +1,18 @@ +package smithy4s.example + +import smithy4s.Bijection +import smithy4s.Hints +import smithy4s.Schema +import smithy4s.ShapeId +import smithy4s.ValidatedNewtype +import smithy4s.Validator +import smithy4s.schema.Schema.string + +object AccountId extends ValidatedNewtype[String] { + val id: ShapeId = ShapeId("smithy4s.example", "AccountId") + val hints: Hints = Hints.empty + val underlyingSchema: Schema[String] = string.withId(id).addHints(hints).validated(smithy.api.Pattern("[a-zA-Z0-9]+")) + val validator: Validator[String, AccountId] = Validator.Builder.simple[String].validating(smithy.api.Pattern("[a-zA-Z0-9]+")).biject(Bijection[String, AccountId](_.asInstanceOf[AccountId], value(_))).build() + implicit val schema: Schema[AccountId] = validator.toSchema(underlyingSchema) + @inline def apply(a: String): Either[String, AccountId] = validator.validate(a) +} diff --git a/modules/bootstrapped/src/generated/smithy4s/example/ConstrainedListConstrainedMember.scala b/modules/bootstrapped/src/generated/smithy4s/example/ConstrainedListConstrainedMember.scala new file mode 100644 index 0000000000..e3b58774f5 --- /dev/null +++ b/modules/bootstrapped/src/generated/smithy4s/example/ConstrainedListConstrainedMember.scala @@ -0,0 +1,16 @@ +package smithy4s.example + +import smithy4s.Hints +import smithy4s.Newtype +import smithy4s.Schema +import smithy4s.ShapeId +import smithy4s.schema.Schema.bijection +import smithy4s.schema.Schema.list +import smithy4s.schema.Schema.string + +object ConstrainedListConstrainedMember extends Newtype[List[String]] { + val id: ShapeId = ShapeId("smithy4s.example", "ConstrainedListConstrainedMember") + val hints: Hints = Hints.empty + val underlyingSchema: Schema[List[String]] = list(string.addMemberHints().validated(smithy.api.Length(min = None, max = Some(2L)))).withId(id).addHints(hints).validated(smithy.api.Length(min = None, max = Some(1L))) + implicit val schema: Schema[ConstrainedListConstrainedMember] = bijection(underlyingSchema, asBijection) +} diff --git a/modules/bootstrapped/src/generated/smithy4s/example/DeviceId.scala b/modules/bootstrapped/src/generated/smithy4s/example/DeviceId.scala new file mode 100644 index 0000000000..6dd8496c39 --- /dev/null +++ b/modules/bootstrapped/src/generated/smithy4s/example/DeviceId.scala @@ -0,0 +1,18 @@ +package smithy4s.example + +import smithy4s.Bijection +import smithy4s.Hints +import smithy4s.Schema +import smithy4s.ShapeId +import smithy4s.ValidatedNewtype +import smithy4s.Validator +import smithy4s.schema.Schema.string + +object DeviceId extends ValidatedNewtype[String] { + val id: ShapeId = ShapeId("smithy4s.example", "DeviceId") + val hints: Hints = Hints.empty + val underlyingSchema: Schema[String] = string.withId(id).addHints(hints).validated(smithy.api.Length(min = Some(1L), max = None)) + val validator: Validator[String, DeviceId] = Validator.Builder.simple[String].validating(smithy.api.Length(min = Some(1L), max = None)).biject(Bijection[String, DeviceId](_.asInstanceOf[DeviceId], value(_))).build() + implicit val schema: Schema[DeviceId] = validator.toSchema(underlyingSchema) + @inline def apply(a: String): Either[String, DeviceId] = validator.validate(a) +} diff --git a/modules/bootstrapped/src/generated/smithy4s/example/ValidatedConstrainedList.scala b/modules/bootstrapped/src/generated/smithy4s/example/ValidatedConstrainedList.scala new file mode 100644 index 0000000000..a3bb4533da --- /dev/null +++ b/modules/bootstrapped/src/generated/smithy4s/example/ValidatedConstrainedList.scala @@ -0,0 +1,19 @@ +package smithy4s.example + +import smithy4s.Bijection +import smithy4s.Hints +import smithy4s.Schema +import smithy4s.ShapeId +import smithy4s.ValidatedNewtype +import smithy4s.Validator +import smithy4s.schema.Schema.list +import smithy4s.schema.Schema.string + +object ValidatedConstrainedList extends ValidatedNewtype[List[String]] { + val id: ShapeId = ShapeId("smithy4s.example", "ValidatedConstrainedList") + val hints: Hints = Hints.empty + val underlyingSchema: Schema[List[String]] = list(string).withId(id).addHints(hints).validated(smithy.api.Length(min = None, max = Some(1L))) + val validator: Validator[List[String], ValidatedConstrainedList] = Validator.Builder.list[String].validating(smithy.api.Length(min = None, max = Some(1L))).biject(Bijection[List[String], ValidatedConstrainedList](_.asInstanceOf[ValidatedConstrainedList], value(_))).build() + implicit val schema: Schema[ValidatedConstrainedList] = validator.toSchema(underlyingSchema) + @inline def apply(a: List[String]): Either[String, ValidatedConstrainedList] = validator.validate(a) +} diff --git a/modules/bootstrapped/src/generated/smithy4s/example/ValidatedConstrainedListConstrainedMember.scala b/modules/bootstrapped/src/generated/smithy4s/example/ValidatedConstrainedListConstrainedMember.scala new file mode 100644 index 0000000000..978bfefe90 --- /dev/null +++ b/modules/bootstrapped/src/generated/smithy4s/example/ValidatedConstrainedListConstrainedMember.scala @@ -0,0 +1,19 @@ +package smithy4s.example + +import smithy4s.Bijection +import smithy4s.Hints +import smithy4s.Schema +import smithy4s.ShapeId +import smithy4s.ValidatedNewtype +import smithy4s.Validator +import smithy4s.schema.Schema.list +import smithy4s.schema.Schema.string + +object ValidatedConstrainedListConstrainedMember extends ValidatedNewtype[List[String]] { + val id: ShapeId = ShapeId("smithy4s.example", "ValidatedConstrainedListConstrainedMember") + val hints: Hints = Hints.empty + val underlyingSchema: Schema[List[String]] = list(string.addMemberHints().validated(smithy.api.Length(min = None, max = Some(2L)))).withId(id).addHints(hints).validated(smithy.api.Length(min = None, max = Some(1L))) + val validator: Validator[List[String], ValidatedConstrainedListConstrainedMember] = Validator.Builder.list[String].validating(smithy.api.Length(min = None, max = Some(1L))).validatingMember(smithy.api.Length(min = None, max = Some(2L))).biject(Bijection[List[String], ValidatedConstrainedListConstrainedMember](_.asInstanceOf[ValidatedConstrainedListConstrainedMember], value(_))).build() + implicit val schema: Schema[ValidatedConstrainedListConstrainedMember] = validator.toSchema(underlyingSchema) + @inline def apply(a: List[String]): Either[String, ValidatedConstrainedListConstrainedMember] = validator.validate(a) +} diff --git a/modules/bootstrapped/src/generated/smithy4s/example/ValidatedConstrainedListRefinedConstrainedMember.scala b/modules/bootstrapped/src/generated/smithy4s/example/ValidatedConstrainedListRefinedConstrainedMember.scala new file mode 100644 index 0000000000..30e1ce9841 --- /dev/null +++ b/modules/bootstrapped/src/generated/smithy4s/example/ValidatedConstrainedListRefinedConstrainedMember.scala @@ -0,0 +1,20 @@ +package smithy4s.example + +import smithy4s.Bijection +import smithy4s.Hints +import smithy4s.Schema +import smithy4s.ShapeId +import smithy4s.ValidatedNewtype +import smithy4s.Validator +import smithy4s.example.instances._ +import smithy4s.schema.Schema.list + + +object ValidatedConstrainedListRefinedConstrainedMember extends ValidatedNewtype[List[Name]] { + val id: ShapeId = ShapeId("smithy4s.example", "ValidatedConstrainedListRefinedConstrainedMember") + val hints: Hints = Hints.empty + val underlyingSchema: Schema[List[Name]] = list(Name.schema.addMemberHints().validated(smithy.api.Length(min = None, max = Some(2L)))).withId(id).addHints(hints).validated(smithy.api.Length(min = None, max = Some(1L))) + val validator: Validator[List[Name], ValidatedConstrainedListRefinedConstrainedMember] = Validator.Builder.list[Name].validating(smithy.api.Length(min = None, max = Some(1L))).validatingMember(smithy.api.Length(min = None, max = Some(2L))).biject(Bijection[List[Name], ValidatedConstrainedListRefinedConstrainedMember](_.asInstanceOf[ValidatedConstrainedListRefinedConstrainedMember], value(_))).build() + implicit val schema: Schema[ValidatedConstrainedListRefinedConstrainedMember] = validator.toSchema(underlyingSchema) + @inline def apply(a: List[Name]): Either[String, ValidatedConstrainedListRefinedConstrainedMember] = validator.validate(a) +} diff --git a/modules/bootstrapped/src/generated/smithy4s/example/ValidatedConstrainedListRefinedMember.scala b/modules/bootstrapped/src/generated/smithy4s/example/ValidatedConstrainedListRefinedMember.scala new file mode 100644 index 0000000000..a503b8ab1c --- /dev/null +++ b/modules/bootstrapped/src/generated/smithy4s/example/ValidatedConstrainedListRefinedMember.scala @@ -0,0 +1,18 @@ +package smithy4s.example + +import smithy4s.Bijection +import smithy4s.Hints +import smithy4s.Schema +import smithy4s.ShapeId +import smithy4s.ValidatedNewtype +import smithy4s.Validator +import smithy4s.schema.Schema.list + +object ValidatedConstrainedListRefinedMember extends ValidatedNewtype[List[Name]] { + val id: ShapeId = ShapeId("smithy4s.example", "ValidatedConstrainedListRefinedMember") + val hints: Hints = Hints.empty + val underlyingSchema: Schema[List[Name]] = list(Name.schema).withId(id).addHints(hints).validated(smithy.api.Length(min = None, max = Some(1L))) + val validator: Validator[List[Name], ValidatedConstrainedListRefinedMember] = Validator.Builder.list[Name].validating(smithy.api.Length(min = None, max = Some(1L))).biject(Bijection[List[Name], ValidatedConstrainedListRefinedMember](_.asInstanceOf[ValidatedConstrainedListRefinedMember], value(_))).build() + implicit val schema: Schema[ValidatedConstrainedListRefinedMember] = validator.toSchema(underlyingSchema) + @inline def apply(a: List[Name]): Either[String, ValidatedConstrainedListRefinedMember] = validator.validate(a) +} diff --git a/modules/bootstrapped/src/generated/smithy4s/example/ValidatedConstrainedMap.scala b/modules/bootstrapped/src/generated/smithy4s/example/ValidatedConstrainedMap.scala new file mode 100644 index 0000000000..ef430de8b2 --- /dev/null +++ b/modules/bootstrapped/src/generated/smithy4s/example/ValidatedConstrainedMap.scala @@ -0,0 +1,20 @@ +package smithy4s.example + +import smithy4s.Bijection +import smithy4s.Hints +import smithy4s.Schema +import smithy4s.ShapeId +import smithy4s.ValidatedNewtype +import smithy4s.Validator +import smithy4s.schema.Schema.int +import smithy4s.schema.Schema.map +import smithy4s.schema.Schema.string + +object ValidatedConstrainedMap extends ValidatedNewtype[Map[String, Int]] { + val id: ShapeId = ShapeId("smithy4s.example", "ValidatedConstrainedMap") + val hints: Hints = Hints.empty + val underlyingSchema: Schema[Map[String, Int]] = map(string, int).withId(id).addHints(hints).validated(smithy.api.Length(min = None, max = Some(1L))) + val validator: Validator[Map[String, Int], ValidatedConstrainedMap] = Validator.Builder.map[String, Int].validating(smithy.api.Length(min = None, max = Some(1L))).biject(Bijection[Map[String, Int], ValidatedConstrainedMap](_.asInstanceOf[ValidatedConstrainedMap], value(_))).build() + implicit val schema: Schema[ValidatedConstrainedMap] = validator.toSchema(underlyingSchema) + @inline def apply(a: Map[String, Int]): Either[String, ValidatedConstrainedMap] = validator.validate(a) +} diff --git a/modules/bootstrapped/src/generated/smithy4s/example/ValidatedListConstrainedMember.scala b/modules/bootstrapped/src/generated/smithy4s/example/ValidatedListConstrainedMember.scala new file mode 100644 index 0000000000..aabd95684a --- /dev/null +++ b/modules/bootstrapped/src/generated/smithy4s/example/ValidatedListConstrainedMember.scala @@ -0,0 +1,19 @@ +package smithy4s.example + +import smithy4s.Bijection +import smithy4s.Hints +import smithy4s.Schema +import smithy4s.ShapeId +import smithy4s.ValidatedNewtype +import smithy4s.Validator +import smithy4s.schema.Schema.list +import smithy4s.schema.Schema.string + +object ValidatedListConstrainedMember extends ValidatedNewtype[List[String]] { + val id: ShapeId = ShapeId("smithy4s.example", "ValidatedListConstrainedMember") + val hints: Hints = Hints.empty + val underlyingSchema: Schema[List[String]] = list(string.addMemberHints().validated(smithy.api.Length(min = None, max = Some(2L)))).withId(id).addHints(hints) + val validator: Validator[List[String], ValidatedListConstrainedMember] = Validator.Builder.list[String].validatingMember(smithy.api.Length(min = None, max = Some(2L))).biject(Bijection[List[String], ValidatedListConstrainedMember](_.asInstanceOf[ValidatedListConstrainedMember], value(_))).build() + implicit val schema: Schema[ValidatedListConstrainedMember] = validator.toSchema(underlyingSchema) + @inline def apply(a: List[String]): Either[String, ValidatedListConstrainedMember] = validator.validate(a) +} diff --git a/modules/bootstrapped/src/generated/smithy4s/example/ValidatedMapConstrainedKey.scala b/modules/bootstrapped/src/generated/smithy4s/example/ValidatedMapConstrainedKey.scala new file mode 100644 index 0000000000..01861c95f6 --- /dev/null +++ b/modules/bootstrapped/src/generated/smithy4s/example/ValidatedMapConstrainedKey.scala @@ -0,0 +1,20 @@ +package smithy4s.example + +import smithy4s.Bijection +import smithy4s.Hints +import smithy4s.Schema +import smithy4s.ShapeId +import smithy4s.ValidatedNewtype +import smithy4s.Validator +import smithy4s.schema.Schema.int +import smithy4s.schema.Schema.map +import smithy4s.schema.Schema.string + +object ValidatedMapConstrainedKey extends ValidatedNewtype[Map[String, Int]] { + val id: ShapeId = ShapeId("smithy4s.example", "ValidatedMapConstrainedKey") + val hints: Hints = Hints.empty + val underlyingSchema: Schema[Map[String, Int]] = map(string.addMemberHints().validated(smithy.api.Length(min = None, max = Some(2L))), int).withId(id).addHints(hints) + val validator: Validator[Map[String, Int], ValidatedMapConstrainedKey] = Validator.Builder.map[String, Int].validatingKey(smithy.api.Length(min = None, max = Some(2L))).biject(Bijection[Map[String, Int], ValidatedMapConstrainedKey](_.asInstanceOf[ValidatedMapConstrainedKey], value(_))).build() + implicit val schema: Schema[ValidatedMapConstrainedKey] = validator.toSchema(underlyingSchema) + @inline def apply(a: Map[String, Int]): Either[String, ValidatedMapConstrainedKey] = validator.validate(a) +} diff --git a/modules/bootstrapped/src/generated/smithy4s/example/ValidatedMapConstrainedValue.scala b/modules/bootstrapped/src/generated/smithy4s/example/ValidatedMapConstrainedValue.scala new file mode 100644 index 0000000000..2769c0dcc6 --- /dev/null +++ b/modules/bootstrapped/src/generated/smithy4s/example/ValidatedMapConstrainedValue.scala @@ -0,0 +1,20 @@ +package smithy4s.example + +import smithy4s.Bijection +import smithy4s.Hints +import smithy4s.Schema +import smithy4s.ShapeId +import smithy4s.ValidatedNewtype +import smithy4s.Validator +import smithy4s.schema.Schema.int +import smithy4s.schema.Schema.map +import smithy4s.schema.Schema.string + +object ValidatedMapConstrainedValue extends ValidatedNewtype[Map[String, Int]] { + val id: ShapeId = ShapeId("smithy4s.example", "ValidatedMapConstrainedValue") + val hints: Hints = Hints.empty + val underlyingSchema: Schema[Map[String, Int]] = map(string, int.addMemberHints().validated(smithy.api.Range(min = None, max = Some(scala.math.BigDecimal(2.0))))).withId(id).addHints(hints) + val validator: Validator[Map[String, Int], ValidatedMapConstrainedValue] = Validator.Builder.map[String, Int].validatingValue(smithy.api.Range(min = None, max = Some(scala.math.BigDecimal(2.0)))).biject(Bijection[Map[String, Int], ValidatedMapConstrainedValue](_.asInstanceOf[ValidatedMapConstrainedValue], value(_))).build() + implicit val schema: Schema[ValidatedMapConstrainedValue] = validator.toSchema(underlyingSchema) + @inline def apply(a: Map[String, Int]): Either[String, ValidatedMapConstrainedValue] = validator.validate(a) +} diff --git a/modules/bootstrapped/src/generated/smithy4s/example/ValidatedRefinedList.scala b/modules/bootstrapped/src/generated/smithy4s/example/ValidatedRefinedList.scala new file mode 100644 index 0000000000..d637a34828 --- /dev/null +++ b/modules/bootstrapped/src/generated/smithy4s/example/ValidatedRefinedList.scala @@ -0,0 +1,22 @@ +package smithy4s.example + +import smithy4s.Bijection +import smithy4s.Hints +import smithy4s.Schema +import smithy4s.ShapeId +import smithy4s.ValidatedNewtype +import smithy4s.Validator +import smithy4s.refined.NonEmptyList +import smithy4s.schema.Schema.list +import smithy4s.schema.Schema.string + +object ValidatedRefinedList extends ValidatedNewtype[NonEmptyList[String]] { + val id: ShapeId = ShapeId("smithy4s.example", "ValidatedRefinedList") + val hints: Hints = Hints( + smithy4s.example.NonEmptyListFormat(), + ).lazily + val underlyingSchema: Schema[NonEmptyList[String]] = list(string).refined[NonEmptyList[String]](smithy4s.example.NonEmptyListFormat()).withId(id).addHints(hints) + val validator: Validator[NonEmptyList[String], ValidatedRefinedList] = Validator.Builder.list[String].refined[NonEmptyList[String]](smithy4s.example.NonEmptyListFormat()).biject(Bijection[NonEmptyList[String], ValidatedRefinedList](_.asInstanceOf[ValidatedRefinedList], value(_))).build() + implicit val schema: Schema[ValidatedRefinedList] = validator.toSchema(underlyingSchema) + @inline def apply(a: NonEmptyList[String]): Either[String, ValidatedRefinedList] = validator.validate(a) +} diff --git a/modules/bootstrapped/src/generated/smithy4s/example/ValidatedRefinedListConstrainedMember.scala b/modules/bootstrapped/src/generated/smithy4s/example/ValidatedRefinedListConstrainedMember.scala new file mode 100644 index 0000000000..8048a9deb8 --- /dev/null +++ b/modules/bootstrapped/src/generated/smithy4s/example/ValidatedRefinedListConstrainedMember.scala @@ -0,0 +1,22 @@ +package smithy4s.example + +import smithy4s.Bijection +import smithy4s.Hints +import smithy4s.Schema +import smithy4s.ShapeId +import smithy4s.ValidatedNewtype +import smithy4s.Validator +import smithy4s.refined.NonEmptyList +import smithy4s.schema.Schema.list +import smithy4s.schema.Schema.string + +object ValidatedRefinedListConstrainedMember extends ValidatedNewtype[NonEmptyList[String]] { + val id: ShapeId = ShapeId("smithy4s.example", "ValidatedRefinedListConstrainedMember") + val hints: Hints = Hints( + smithy4s.example.NonEmptyListFormat(), + ).lazily + val underlyingSchema: Schema[NonEmptyList[String]] = list(string.addMemberHints().validated(smithy.api.Length(min = None, max = Some(2L)))).refined[NonEmptyList[String]](smithy4s.example.NonEmptyListFormat()).withId(id).addHints(hints) + val validator: Validator[NonEmptyList[String], ValidatedRefinedListConstrainedMember] = Validator.Builder.list[String].validatingMember(smithy.api.Length(min = None, max = Some(2L))).refined[NonEmptyList[String]](smithy4s.example.NonEmptyListFormat()).biject(Bijection[NonEmptyList[String], ValidatedRefinedListConstrainedMember](_.asInstanceOf[ValidatedRefinedListConstrainedMember], value(_))).build() + implicit val schema: Schema[ValidatedRefinedListConstrainedMember] = validator.toSchema(underlyingSchema) + @inline def apply(a: NonEmptyList[String]): Either[String, ValidatedRefinedListConstrainedMember] = validator.validate(a) +} diff --git a/modules/bootstrapped/src/generated/smithy4s/example/ValidatedString.scala b/modules/bootstrapped/src/generated/smithy4s/example/ValidatedString.scala index 1e5f93e27d..95a7cce08b 100644 --- a/modules/bootstrapped/src/generated/smithy4s/example/ValidatedString.scala +++ b/modules/bootstrapped/src/generated/smithy4s/example/ValidatedString.scala @@ -12,7 +12,7 @@ object ValidatedString extends ValidatedNewtype[String] { val id: ShapeId = ShapeId("smithy4s.example", "ValidatedString") val hints: Hints = Hints.empty val underlyingSchema: Schema[String] = string.withId(id).addHints(hints).validated(smithy.api.Length(min = Some(1L), max = None)).validated(smithy.api.Pattern("[a-zA-Z0-9]+")) - val validator: Validator[String, ValidatedString] = Validator.of[String, ValidatedString](Bijection[String, ValidatedString](_.asInstanceOf[ValidatedString], value(_))).validating(smithy.api.Length(min = Some(1L), max = None)).alsoValidating(smithy.api.Pattern("[a-zA-Z0-9]+")) + val validator: Validator[String, ValidatedString] = Validator.Builder.simple[String].validating(smithy.api.Length(min = Some(1L), max = None)).validating(smithy.api.Pattern("[a-zA-Z0-9]+")).biject(Bijection[String, ValidatedString](_.asInstanceOf[ValidatedString], value(_))).build() implicit val schema: Schema[ValidatedString] = validator.toSchema(underlyingSchema) @inline def apply(a: String): Either[String, ValidatedString] = validator.validate(a) } diff --git a/modules/bootstrapped/src/generated/smithy4s/example/package.scala b/modules/bootstrapped/src/generated/smithy4s/example/package.scala index 536149eca0..380168bce5 100644 --- a/modules/bootstrapped/src/generated/smithy4s/example/package.scala +++ b/modules/bootstrapped/src/generated/smithy4s/example/package.scala @@ -43,6 +43,7 @@ package object example { /** This is a simple example of a "quoted string" */ type AString = smithy4s.example.AString.Type + type AccountId = smithy4s.example.AccountId.Type type AdditionalProperties = smithy4s.example.AdditionalProperties.Type type Age = smithy4s.example.Age.Type /** Multiple line doc comment for another string @@ -57,11 +58,13 @@ package object example { type CityId = smithy4s.example.CityId.Type type CitySummaries = smithy4s.example.CitySummaries.Type type ConstrainedList = smithy4s.example.ConstrainedList.Type + type ConstrainedListConstrainedMember = smithy4s.example.ConstrainedListConstrainedMember.Type type ConstrainedMap = smithy4s.example.ConstrainedMap.Type type CustomErrorMessageType = smithy4s.example.CustomErrorMessageType.Type type DefaultStringMap = smithy4s.example.DefaultStringMap.Type @deprecated(message = "N/A", since = "N/A") type DeprecatedString = smithy4s.example.DeprecatedString.Type + type DeviceId = smithy4s.example.DeviceId.Type type DogName = smithy4s.example.DogName.Type type ExtraData = smithy4s.example.ExtraData.Type type FancyList = smithy4s.example.FancyList.Type @@ -127,6 +130,16 @@ package object example { type UnwrappedFancyList = smithy4s.example.UnwrappedFancyList.Type type Username = smithy4s.example.Username.Type type UuidTrait = smithy4s.example.UuidTrait.Type + type ValidatedConstrainedList = smithy4s.example.ValidatedConstrainedList.Type + type ValidatedConstrainedListConstrainedMember = smithy4s.example.ValidatedConstrainedListConstrainedMember.Type + type ValidatedConstrainedListRefinedConstrainedMember = smithy4s.example.ValidatedConstrainedListRefinedConstrainedMember.Type + type ValidatedConstrainedListRefinedMember = smithy4s.example.ValidatedConstrainedListRefinedMember.Type + type ValidatedConstrainedMap = smithy4s.example.ValidatedConstrainedMap.Type + type ValidatedListConstrainedMember = smithy4s.example.ValidatedListConstrainedMember.Type + type ValidatedMapConstrainedKey = smithy4s.example.ValidatedMapConstrainedKey.Type + type ValidatedMapConstrainedValue = smithy4s.example.ValidatedMapConstrainedValue.Type + type ValidatedRefinedList = smithy4s.example.ValidatedRefinedList.Type + type ValidatedRefinedListConstrainedMember = smithy4s.example.ValidatedRefinedListConstrainedMember.Type type ValidatedString = smithy4s.example.ValidatedString.Type } \ No newline at end of file diff --git a/modules/bootstrapped/src/main/smithy4s/example/instances.scala b/modules/bootstrapped/src/main/smithy4s/example/instances.scala new file mode 100644 index 0000000000..2cc92d409e --- /dev/null +++ b/modules/bootstrapped/src/main/smithy4s/example/instances.scala @@ -0,0 +1,13 @@ +package smithy4s.example + +import smithy4s.RefinementProvider + +object instances { + + implicit val lengthProviderValidatedRefinedPrimitive + : RefinementProvider.Simple[ + smithy.api.Length, + Name + ] = + RefinementProvider.lengthConstraint(_.value.value.length) +} diff --git a/modules/bootstrapped/test/src/smithy4s/ValidatedNewtypesSpec.scala b/modules/bootstrapped/test/src/smithy4s/ValidatedNewtypesSpec.scala index 60ab601204..dbe78ec238 100644 --- a/modules/bootstrapped/test/src/smithy4s/ValidatedNewtypesSpec.scala +++ b/modules/bootstrapped/test/src/smithy4s/ValidatedNewtypesSpec.scala @@ -17,7 +17,25 @@ package smithy4s import smithy4s.schema.Schema.string +import smithy4s.schema.Schema.list +import smithy4s.refined.NonEmptyList +import smithy4s.example.Name +import smithy4s.example.ValidatedConstrainedList +import smithy4s.example.ValidatedListConstrainedMember +import smithy4s.example.ValidatedConstrainedListConstrainedMember +import smithy4s.example.ValidatedConstrainedListRefinedMember +import smithy4s.example.ValidatedConstrainedListRefinedConstrainedMember +import smithy4s.example.ValidatedMapConstrainedKey +import smithy4s.example.ValidatedConstrainedMap +import smithy4s.example.ValidatedMapConstrainedValue +import smithy4s.example.ValidatedRefinedListConstrainedMember +import smithy4s.example.AccountId +import smithy4s.example.DeviceId + import munit.Assertions +import cats.data.Validated.Valid +import smithy.api.Length +import smithy4s.example.NonEmptyListFormat class ValidatedNewtypesSpec() extends munit.FunSuite { val id1 = "id1" @@ -54,56 +72,96 @@ class ValidatedNewtypesSpec() extends munit.FunSuite { ) } - type DeviceId = DeviceId.Type - object DeviceId extends ValidatedNewtype[String] { - - val id: ShapeId = ShapeId("foo", "DeviceId") - val hints: Hints = Hints.empty + test("Validated constrained list") { + expect(ValidatedConstrainedList(List("foo")).isRight) + expect(ValidatedConstrainedList(List("foo", "bar")).isLeft) + } - val underlyingSchema: Schema[String] = string - .withId(id) - .addHints(hints) - .validated(smithy.api.Length(min = Some(1L), max = None)) + test("Validated list constrained member") { + expect(ValidatedListConstrainedMember(List("f")).isRight) + expect(ValidatedListConstrainedMember(List("foo")).isLeft) + } - val validator: Validator[String, DeviceId] = Validator - .of[String, DeviceId]( - Bijection[String, DeviceId](_.asInstanceOf[DeviceId], value(_)) - ) - .validating(smithy.api.Length(min = Some(1L), max = None)) + test("Validated constrained list constrained member") { + expect(ValidatedConstrainedListConstrainedMember(List("f")).isRight) + expect(ValidatedConstrainedListConstrainedMember(List("foo")).isLeft) + expect(ValidatedConstrainedListConstrainedMember(List("f", "g")).isLeft) + expect(ValidatedConstrainedListConstrainedMember(List("foo", "h")).isLeft) + } - implicit val schema: Schema[DeviceId] = - validator.toSchema(underlyingSchema) + test("Validated constrained list refined member") { + def mkName(str: String) = smithy4s.refined.Name(str) match { + case Left(msg) => fail(msg) + case Right(v) => v + } + expect( + ValidatedConstrainedListRefinedMember( + List(Name(mkName("foo"))) + ).isRight + ) + expect( + ValidatedConstrainedListRefinedMember( + List( + Name(mkName("foo")), + Name(mkName("bar")) + ) + ).isLeft + ) + } - @inline def apply(a: String): Either[String, DeviceId] = - validator.validate(a) + test("Validated constrained list refined & constrained member") { + expect( + ValidatedConstrainedListRefinedConstrainedMember( + List(Name(mkName("fo"))) + ).isRight + ) + expect( + ValidatedConstrainedListRefinedConstrainedMember( + List( + Name(mkName("fo")), + Name(mkName("ba")) + ) + ).isLeft + ) + expect( + ValidatedConstrainedListRefinedConstrainedMember( + List( + Name(mkName("foo")) + ) + ).isLeft + ) } - type AccountId = AccountId.Type - - object AccountId extends ValidatedNewtype[String] { - def id: smithy4s.ShapeId = ShapeId("foo", "AccountId") - val hints: Hints = Hints.empty + test("Validated refined list constrainer member") { + expect(ValidatedRefinedListConstrainedMember(mkNel("fo")).isRight) + expect(ValidatedRefinedListConstrainedMember(mkNel("fo", "foo")).isLeft) + } - val underlyingSchema: Schema[String] = string - .withId(id) - .addHints(hints) - .validated(smithy.api.Length(min = Some(1L), max = None)) - .validated(smithy.api.Pattern("[a-zA-Z0-9]+")) + test("Validated constrained map") { + expect(ValidatedConstrainedMap(Map("foo" -> 1)).isRight) + expect(ValidatedConstrainedMap(Map("foo" -> 1, "bar" -> 2)).isLeft) + } - val validator: Validator[String, AccountId] = Validator - .of[String, AccountId]( - Bijection[String, AccountId](_.asInstanceOf[AccountId], value(_)) - ) - .validating(smithy.api.Length(min = Some(1L), max = None)) - .alsoValidating(smithy.api.Pattern("[a-zA-Z0-9]+")) + test("Validated map constrained key") { + expect(ValidatedMapConstrainedKey(Map("a" -> 1, "b" -> 2)).isRight) + expect(ValidatedMapConstrainedKey(Map("a" -> 1, "bar" -> 2)).isLeft) + } - implicit val schema: Schema[AccountId] = - validator.toSchema(underlyingSchema) + test("Validated map constrained value") { + expect(ValidatedMapConstrainedValue(Map("a" -> 1, "b" -> 2)).isRight) + expect(ValidatedMapConstrainedValue(Map("a" -> 1, "b" -> 3)).isLeft) + } - @inline def apply(a: String): Either[String, AccountId] = - validator.validate(a) + private def mkNel[A](elems: A*) = + smithy4s.refined.NonEmptyList(elems.toList) match { + case Left(msg) => fail(msg) + case Right(v) => v + } + private def mkName(str: String) = smithy4s.refined.Name(str) match { + case Left(msg) => fail(msg) + case Right(v) => v } } diff --git a/modules/codegen/src/smithy4s/codegen/internals/Renderer.scala b/modules/codegen/src/smithy4s/codegen/internals/Renderer.scala index 5bf4960603..947de50ba9 100644 --- a/modules/codegen/src/smithy4s/codegen/internals/Renderer.scala +++ b/modules/codegen/src/smithy4s/codegen/internals/Renderer.scala @@ -1548,15 +1548,34 @@ private[internals] class Renderer(compilationUnit: CompilationUnit) { self => hints: List[Hint] ): Lines = { val validator = { - val tags = hints.collect { case t: Hint.Constraint => t } - tags match { - case h :: tail => - ( - line".validating(${renderHint(h.native)})" +: - tail.map { tag => line".alsoValidating(${renderHint(tag.native)})" } - ).intercalate(Line.empty) - case _ => Line.empty + + def validateConstraints(h: List[Hint], validateMethod: Line) = h + .collect { case t: Hint.Constraint => t } + .foldMap(constraint => line".$validateMethod(${renderHint(constraint.native)})") + def validateType(t: Type): (Line, Line) = t match { + case Type.Collection(collectionTpe, memberTpe, memberHints) => + val builderMethod = collectionTpe match { + case CollectionType.List => line"list" + case CollectionType.Vector => line"vector" + case CollectionType.IndexedSeq => line"indexedSeq" + case CollectionType.Set => line"set" + } + line"$builderMethod[$memberTpe]" -> validateConstraints(memberHints, line"validatingMember") + case Type.Map(keyTpe, keyHints, valueTpe, valueHints) => + val validatingKey = validateConstraints(keyHints, line"validatingKey") + val validatingValue = validateConstraints(valueHints, line"validatingValue") + line"map[$keyTpe, $valueTpe]" -> (validatingKey + validatingValue) + case external: Type.ExternalType => + validateType(external.underlyingTpe) + case _ => line"simple[$tpe]" -> Line.empty } + val (builderType, validatingMembers) = validateType(tpe) + val validatingDirect = validateConstraints(hints, line"validating") + lines( + line"val validator: $Validator_[$tpe, $name] = $Validator_.Builder.$builderType$validatingDirect$validatingMembers${renderRefined( + tpe + )}.biject($Bijection_[$tpe, $name](_.asInstanceOf[$name], value(_))).build()" + ) } val definition = @@ -1568,13 +1587,12 @@ private[internals] class Renderer(compilationUnit: CompilationUnit) { self => lines( documentationAnnotation(hints), deprecationAnnotation(hints), + renderScalaImports(hints), obj(name, line"$ValidatedNewtype_[$tpe]")( renderId(shapeId), renderHintsVal(hints), line"val underlyingSchema: $Schema_[$tpe] = ${tpe.schemaRef}$trailingCalls", - lines( - line"val validator: $Validator_[$tpe, $name] = $Validator_.of[$tpe, $name]($Bijection_[$tpe, $name](_.asInstanceOf[$name], value(_)))$validator" - ), + validator, lines( line"implicit val schema: $Schema_[$name] = ${definition}validator.toSchema(underlyingSchema)$closing" ), @@ -1645,18 +1663,8 @@ private[internals] class Renderer(compilationUnit: CompilationUnit) { self => NameRef(ns, s"$name.schema").toLine case Type.Alias(ns, name, _, _) => NameRef(ns, s"$name.underlyingSchema").toLine - case Type.Ref(ns, name) => NameRef(ns, s"$name.schema").toLine - case e @ Type.ExternalType( - _, - _, - _, - maybeProviderImport, - underlyingTpe, - hint - ) => - line"${underlyingTpe.schemaRef}.refined[${e: Type}](${renderHint(hint)})${maybeProviderImport - .map { providerImport => Import(providerImport).toLine } - .getOrElse(Line.empty)}" + case Type.Ref(ns, name) => NameRef(ns, s"$name.schema").toLine + case e: Type.ExternalType => line"${e.underlyingTpe.schemaRef}${renderRefined(e)}" case Nullable(underlying) => line"${underlying.schemaRef}.option" } @@ -1706,6 +1714,21 @@ private[internals] class Renderer(compilationUnit: CompilationUnit) { self => .run(true) ._2 + private def renderRefined(tpe: Type): Line = tpe match { + case e @ Type.ExternalType( + _, + _, + _, + maybeProviderImport, + underlyingTpe, + hint + ) => + line".refined[${e: Type}](${renderHint(hint)})${maybeProviderImport + .map { providerImport => Import(providerImport).toLine } + .getOrElse(Line.empty)}" + case _ => Line.empty + } + def renderId(shapeId: ShapeId): Line = { val ns = shapeId.getNamespace() val name = shapeId.getName() diff --git a/modules/codegen/src/smithy4s/codegen/internals/SmithyToIR.scala b/modules/codegen/src/smithy4s/codegen/internals/SmithyToIR.scala index 38ae23bc7d..d3d0142f42 100644 --- a/modules/codegen/src/smithy4s/codegen/internals/SmithyToIR.scala +++ b/modules/codegen/src/smithy4s/codegen/internals/SmithyToIR.scala @@ -646,7 +646,7 @@ private[codegen] class SmithyToIR( shape.hasTrait(classOf[smithy4s.meta.UnwrapTrait]) } - def primitive( + private def primitive( shape: Shape, primitiveId: String, primitive: Primitive @@ -706,7 +706,7 @@ private[codegen] class SmithyToIR( } } - def listShape(x: ListShape): Option[Type] = { + def listShape(x: ListShape): Option[Type] = x.getMember() .accept(this) .map { tpe => @@ -728,12 +728,16 @@ private[codegen] class SmithyToIR( } } .map { tpe => - val externalOrBase = - getExternalOrBase(x, tpe) - val isUnwrapped = !isExternal(externalOrBase) || isUnwrappedShape(x) - Type.Alias(x.namespace, x.name, externalOrBase, isUnwrapped) + val externalOrBase = getExternalOrBase(x, tpe) + val shouldValidate = x.hasTrait(classOf[ValidateNewtypeTrait]) + if (shouldValidate) + Type.ValidatedAlias(x.namespace, x.name, externalOrBase) + else { + val isUnwrapped = + !isExternal(externalOrBase) || isUnwrappedShape(x) + Type.Alias(x.namespace, x.name, externalOrBase, isUnwrapped) + } } - } @nowarn("msg=class SetShape in package shapes is deprecated") override def setShape(x: SetShape): Option[Type] = { @@ -769,8 +773,13 @@ private[codegen] class SmithyToIR( )).map { tpe => val externalOrBase = getExternalOrBase(x, tpe) - val isUnwrapped = !isExternal(externalOrBase) || isUnwrappedShape(x) - Type.Alias(x.namespace, x.name, externalOrBase, isUnwrapped) + val shouldValidate = x.hasTrait(classOf[ValidateNewtypeTrait]) + if (shouldValidate) + Type.ValidatedAlias(x.namespace, x.name, externalOrBase) + else { + val isUnwrapped = !isExternal(externalOrBase) || isUnwrappedShape(x) + Type.Alias(x.namespace, x.name, externalOrBase, isUnwrapped) + } } def byteShape(x: ByteShape): Option[Type] = @@ -846,9 +855,7 @@ private[codegen] class SmithyToIR( builder .addTraits(x.getAllTraits().asScala.map(_._2).asJavaCollection) - builder - .build() - .accept(this) + builder.build().accept(this) } def timestampShape(x: TimestampShape): Option[Type] = diff --git a/modules/core/src/smithy4s/Validator.scala b/modules/core/src/smithy4s/Validator.scala index 822e60dc0c..2e12613657 100644 --- a/modules/core/src/smithy4s/Validator.scala +++ b/modules/core/src/smithy4s/Validator.scala @@ -16,55 +16,378 @@ package smithy4s -sealed trait Validator[A, B] { +sealed trait Validator[A, B] { self => def validate(value: A): Either[String, B] + // todo: deprecate def toSchema(a: Schema[A]): Schema[B] def alsoValidating[C](constraint: C)(implicit ev: RefinementProvider.Simple[C, A] - ): Validator[A, B] + ): Validator[A, B] = toBuilder.validating(constraint).build() + + def toBuilder: Validator.Builder[A, B] + } object Validator { - def of[A, B](bijection: Bijection[A, B]): ValidatorBuilder[A, B] = - new ValidatorBuilder[A, B](bijection) - - final class ValidatorBuilder[A, B] private[smithy4s] ( - bijection: Bijection[A, B] - ) { + trait Builder[A, B] { self => def validating[C](constraint: C)(implicit ev: RefinementProvider.Simple[C, A] - ): Validator[A, B] = - new ValidatorImpl[A, B](List(ev.make(constraint)), bijection) + ): Builder[A, B] + + def biject[B0](implicit bijection: Bijection[B, B0]): Builder[A, B0] = + new Builder.Bijected(self, bijection) + + def build(): Validator[A, B] } - private class ValidatorImpl[A, B]( - refinements: List[Refinement.Aux[_, A, A]], - bijection: Bijection[A, B] - ) extends Validator[A, B] { + private[smithy4s] final class RefinedSyntax[A](builder: Builder[A, A]) { + def refined[C]: Builder.PartiallyAppliedRefinedBuilder[A, C] = + new Builder.PartiallyAppliedRefinedBuilder[A, C](builder) + } + + object Builder { + + implicit def toRefinedSyntax[A, B](builder: Builder[A, B])(implicit + ev: A =:= B + ): RefinedSyntax[A] = + new RefinedSyntax(builder.asInstanceOf[Builder[A, A]]) + + private[smithy4s] class PartiallyAppliedRefinedBuilder[A, B]( + base: Builder[A, A] + ) { + def apply[C](c: C)(implicit + ev: RefinementProvider[C, A, B] + ): Builder[B, B] = new Builder.Refined(base, ev.make(c), Vector.empty) + } + + trait Collection[Col[_], E] extends Builder[Col[E], Col[E]] { + override def validating[C](constraint: C)(implicit + ev: RefinementProvider.Simple[C, Col[E]] + ): Builder.Collection[Col, E] + + def validatingMember[C](constraint: C)(implicit + ev: RefinementProvider.Simple[C, E] + ): Collection[Col, E] + + } + + trait KeyValue[K, V] extends Builder[Map[K, V], Map[K, V]] { + override def validating[C](constraint: C)(implicit + ev: RefinementProvider.Simple[C, Map[K, V]] + ): KeyValue[K, V] + + def validatingKey[C](constraint: C)(implicit + ev: RefinementProvider.Simple[C, K] + ): KeyValue[K, V] + + def validatingValue[C](constraint: C)(implicit + ev: RefinementProvider.Simple[C, V] + ): KeyValue[K, V] + + } + + def simple[A]: Builder[A, A] = new Simple(Vector.empty) + + def list[E]: Builder.Collection[List, E] = + collection(schema.CollectionTag.ListTag) + + def set[E]: Builder.Collection[Set, E] = + collection(schema.CollectionTag.SetTag) + + def vector[E]: Builder.Collection[Vector, E] = + collection(schema.CollectionTag.VectorTag) + + def indexedSeq[E]: Builder.Collection[IndexedSeq, E] = + collection(schema.CollectionTag.IndexedSeqTag) + + def map[K, V]: Builder.KeyValue[K, V] = + new KeyValueImpl[K, V](None, Vector.empty, Vector.empty) + + private def collection[C[_], E]( + tag: schema.CollectionTag[C] + ): Builder.Collection[C, E] = collection(tag.iterator(_)) + + private def collection[Col[_], E]( + getIterator: Col[E] => Iterator[E] + ): Builder.Collection[Col, E] = + new CollectionImpl(getIterator, None, Vector.empty) + + private final class Simple[A]( + refinements: Vector[Refinement[A, A]] + ) extends Builder[A, A] { self => + + override def validating[C](constraint: C)(implicit + ev: RefinementProvider.Simple[C, A] + ): Builder[A, A] = + new Simple[A](refinements :+ ev.make(constraint)) + + override def build(): Validator[A, A] = new Validator[A, A] { + override def validate(value: A): Either[String, A] = + refinements + .foldLeft(Right(value): Either[String, A]) { + case (valueOrError, refinement) => + valueOrError.flatMap(refinement.apply) + } + + override def toSchema(a: Schema[A]): Schema[A] = + refinements + .foldLeft(a) { (schema, refinement) => + schema.refined[A](refinement) + } + + override def toBuilder: Builder[A, A] = self + + } + + } + + private final class Bijected[A, B, B0]( + source: Builder[A, B], + bijectTarget: Bijection[B, B0] + ) extends Builder[A, B0] { self => + + override def biject[B1](implicit + bijection: Bijection[B0, B1] + ): Builder[A, B1] = + new Bijected( + source, + bijectTarget.imapTarget(bijection) + ) + + override def validating[C](constraint: C)(implicit + ev: RefinementProvider.Simple[C, A] + ): Builder[A, B0] = + new Bijected( + source.validating(constraint), + bijectTarget + ) + + override def build(): Validator[A, B0] = new Validator[A, B0] { + val sourceValidator = source.build() + + override def validate(value: A): Either[String, B0] = + sourceValidator.validate(value).map(bijectTarget.to) + + override def toSchema(a: Schema[A]): Schema[B0] = + sourceValidator.toSchema(a).biject(bijectTarget) + + override def toBuilder: Builder[A, B0] = self + } + } + + private final class Refined[A, B]( + mainBuilder: Builder[A, A], + refinement: Refinement[A, B], + additionalRefinements: Vector[Refinement[B, B]] + ) extends Builder[B, B] { self => + + private val mainValidator = mainBuilder.build() + override def validating[C]( + c: C + )(implicit ev: RefinementProvider.Simple[C, B]): Builder[B, B] = + new Refined( + mainBuilder, + refinement, + additionalRefinements = additionalRefinements :+ ev.make(c) + ) + + override def build(): Validator[B, B] = new Validator[B, B] { + override def validate(value: B): Either[String, B] = + mainValidator + .validate(refinement.from(value)) + .flatMap(_ => + additionalRefinements.foldLeft(right(value)) { case (acc, ref) => + acc.flatMap(_ => ref.apply(value)) + } + ) + override def toSchema(a: Schema[B]): Schema[B] = a + + override def toBuilder: Builder[B, B] = self + + } + + } + + private final class KeyValueImpl[K, V]( + mainBuilder: Option[Builder[Map[K, V], Map[K, V]]], + keyRefinements: Vector[Refinement[K, K]], + valueRefinements: Vector[Refinement[V, V]] + ) extends Builder.KeyValue[K, V] { self => + + override def validating[C](constraint: C)(implicit + ev: RefinementProvider.Simple[C, Map[K, V]] + ): KeyValue[K, V] = + new KeyValueImpl( + mainBuilder = mainBuilder + .map(_.validating(constraint)) + .orElse(Some(simple.validating(constraint))), + keyRefinements = keyRefinements, + valueRefinements = valueRefinements + ) + + override def validatingKey[C](constraint: C)(implicit + ev: RefinementProvider.Simple[C, K] + ): KeyValue[K, V] = new KeyValueImpl( + mainBuilder = mainBuilder, + keyRefinements = keyRefinements :+ ev.make(constraint), + valueRefinements = valueRefinements + ) + + override def validatingValue[C](constraint: C)(implicit + ev: RefinementProvider.Simple[C, V] + ): KeyValue[K, V] = new KeyValueImpl( + mainBuilder = mainBuilder, + keyRefinements = keyRefinements, + valueRefinements = valueRefinements :+ ev.make(constraint) + ) + + override def build(): Validator[Map[K, V], Map[K, V]] = + new Validator[Map[K, V], Map[K, V]] { + val mainValidator = mainBuilder.map(_.build()) + override def validate( + value: Map[K, V] + ): Either[String, Map[K, V]] = { + val mainValidated = + mainValidator.map(_.validate(value)).getOrElse(right(())) + def validateKeySet = validateCollection( + value.iterator + ) { case (k, v) => + validateRefinements(keyRefinements)(k).flatMap(_ => + validateRefinements(valueRefinements)(v) + ) + } + mainValidated.flatMap(_ => validateKeySet).map(_ => value) + } + + override def toSchema(a: Schema[Map[K, V]]): Schema[Map[K, V]] = { + val main = mainValidator.map(_.toSchema(a)).getOrElse(a) + main match { + case mapSchema @ Schema.MapSchema(_, _, key, value) => + val newKey = keyRefinements.foldLeft(key) { + case (acc, refinement) => acc.refined(refinement) + } + val newValue = valueRefinements.foldLeft(value) { + case (acc, refinement) => acc.refined(refinement) + } + mapSchema.copy(key = newKey, value = newValue) + case _ => main + } + } + + override def toBuilder: Builder[Map[K, V], Map[K, V]] = self - override def validate(value: A): Either[String, B] = { - refinements - .foldLeft(Right(value): Either[String, A]) { - case (valueOrError, refinement) => - valueOrError.flatMap(refinement.apply) } - .map(bijection.apply) + } - override def alsoValidating[C](constraint: C)(implicit - ev: RefinementProvider.Simple[C, A] - ): Validator[A, B] = - new ValidatorImpl[A, B](refinements :+ ev.make(constraint), bijection) + private final class CollectionImpl[Col[_], Elem]( + getIterator: Col[Elem] => Iterator[Elem], + mainBuilder: Option[Builder[Col[Elem], Col[Elem]]], + memberRefinements: Vector[Refinement[Elem, Elem]] + ) extends Builder.Collection[Col, Elem] { self => + + override def validating[C](constraint: C)(implicit + ev: RefinementProvider.Simple[C, Col[Elem]] + ): Collection[Col, Elem] = + new CollectionImpl( + getIterator, + mainBuilder = mainBuilder + .map(_.validating(constraint)) + .orElse(Some(simple.validating(constraint))), + memberRefinements = memberRefinements + ) - override def toSchema(a: Schema[A]): Schema[B] = { - refinements - .foldLeft(a) { (schema, refinement) => - schema.refined[A](refinement) + override def validatingMember[C]( + constraint: C + )(implicit + ev: RefinementProvider.Simple[C, Elem] + ): Collection[Col, Elem] = + new CollectionImpl( + getIterator, + mainBuilder, + memberRefinements :+ ev.make(constraint) + ) + + override def build(): Validator[Col[Elem], Col[Elem]] = + new Validator[Col[Elem], Col[Elem]] { + val mainValidator = mainBuilder.map(_.build()) + override def validate( + value: Col[Elem] + ): Either[String, Col[Elem]] = { + val elementsValidated = validateCollection(getIterator(value)) { + validateRefinements(memberRefinements) + } + val mainValidated = + mainValidator + .map(_.validate(value).map(_ => ())) + .getOrElse( + right(()) + ) + mainValidated.flatMap(_ => elementsValidated.map(_ => value)) + } + + override def toSchema(a: Schema[Col[Elem]]): Schema[Col[Elem]] = { + val main = mainValidator.map(_.toSchema(a)).getOrElse(a) + main match { + case collectionSchema @ Schema.CollectionSchema( + shapeId, + hints, + tag, + member + ) => + val newMember = memberRefinements.foldLeft(member) { + case (acc, refinement) => acc.refined(refinement) + } + collectionSchema.copy(member = newMember) + case _ => main + } + } + + override def toBuilder: Builder[Col[Elem], Col[Elem]] = self } - .biject(bijection) } + + private def right[A](value: A): Either[String, A] = Right(value) + + private def validateRefinements[A]( + refinements: Vector[Refinement[A, A]] + )(elem: A): Either[String, Unit] = refinements + .foldLeft(right(elem)) { case (result, constraint) => + result.flatMap(_ => constraint(elem)) + } + .map(_ => ()) + + private def validateCollection[A]( + it: Iterator[A] + )(validate: A => Either[String, Unit]): Either[String, Unit] = { + var acc: Either[String, Unit] = Right(()) + while (it.hasNext && acc.isRight) { + acc = validate(it.next()) + } + acc + } + + } + + @deprecated + def of[A, B](bijection: Bijection[A, B]): ValidatorBuilder[A, B] = + new ValidatorBuilder[A, B](bijection) + + @deprecated + final class ValidatorBuilder[A, B] private[smithy4s] ( + bijection: Bijection[A, B] + ) { + def validating[C](constraint: C)(implicit + ev: RefinementProvider.Simple[C, A] + ): Validator[A, B] = + Validator.Builder + .simple[A] + .validating(constraint) + .biject(bijection) + .build() } + } diff --git a/modules/protocol/resources/META-INF/smithy/smithy4s.meta.smithy b/modules/protocol/resources/META-INF/smithy/smithy4s.meta.smithy index 7d02d8987b..de86e48ab2 100644 --- a/modules/protocol/resources/META-INF/smithy/smithy4s.meta.smithy +++ b/modules/protocol/resources/META-INF/smithy/smithy4s.meta.smithy @@ -196,7 +196,12 @@ list scalaImports { :is( number[trait|range], string[trait|pattern], - string[trait|length] + string[trait|length], + :test(-[trait]-> [trait|smithy4s.meta#refinement]), + list[trait|length], + map[trait|length], + list:test(> [trait|length], > [trait|range], > [trait|pattern]), + map:test(> [trait|length], > [trait|range], > [trait|pattern]) )""" conflicts: [unwrap] ) diff --git a/sampleSpecs/validated-newtype.smithy b/sampleSpecs/validated-newtype.smithy index 8819b906f2..3e2a73e9a1 100644 --- a/sampleSpecs/validated-newtype.smithy +++ b/sampleSpecs/validated-newtype.smithy @@ -3,12 +3,21 @@ $version: "2" namespace smithy4s.example use smithy4s.meta#validateNewtype +use smithy4s.meta#scalaImports @length(min: 1) @pattern("[a-zA-Z0-9]+") @validateNewtype string ValidatedString +@pattern("[a-zA-Z0-9]+") +@validateNewtype +string AccountId + +@validateNewtype +@length(min: 1) +string DeviceId + @length(min: 1) @pattern("[a-zA-Z0-9]+") string NonValidatedString @@ -16,3 +25,83 @@ string NonValidatedString structure ValidatedFoo { name: ValidatedString = "abc" } + +@length(max: 1) +@validateNewtype +list ValidatedConstrainedList { + member: String +} + +@validateNewtype +list ValidatedListConstrainedMember { + @length(max: 2) + member: String +} + +@validateNewtype +@length(max: 1) +list ValidatedConstrainedListConstrainedMember { + @length(max: 2) + member: String +} + +@length(max: 1) +list ConstrainedListConstrainedMember { + @length(max: 2) + member: String +} + +@validateNewtype +@length(max: 1) +list ValidatedConstrainedListRefinedMember { + member: Name +} + +@validateNewtype +@length(max: 1) +@scalaImports(["smithy4s.example.instances._"]) +list ValidatedConstrainedListRefinedConstrainedMember { + @length(max:2) + member: Name +} + +@nonEmptyListFormat +@validateNewtype +list ValidatedRefinedList { + member: String +} + +@nonEmptyListFormat +@validateNewtype +list ValidatedRefinedListConstrainedMember { + @length(max: 2) + member: String +} + +@length(max: 1) +@validateNewtype +map ValidatedConstrainedMap { + key: String + value: Integer +} + +@validateNewtype +map ValidatedMapConstrainedKey { + @length(max: 2) + key: String + value: Integer +} + +@validateNewtype +map ValidatedMapConstrainedValue { + key: String + @range(max: 2) + value: Integer +} + +// @nonEmptyMapFormat +// @validateNewtype +// map ValidatedRefinedMap { +// key: String +// value: Integer +// } \ No newline at end of file From 0cabe459125c5e33e05e8792fce020b73303f24a Mon Sep 17 00:00:00 2001 From: msosnicki Date: Thu, 29 Jan 2026 16:00:45 +0100 Subject: [PATCH 2/9] Fix warnings --- .../test/src/smithy4s/ValidatedNewtypesSpec.scala | 6 ------ modules/core/src/smithy4s/Validator.scala | 6 +++--- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/modules/bootstrapped/test/src/smithy4s/ValidatedNewtypesSpec.scala b/modules/bootstrapped/test/src/smithy4s/ValidatedNewtypesSpec.scala index a793b4acaa..7a256d3ee3 100644 --- a/modules/bootstrapped/test/src/smithy4s/ValidatedNewtypesSpec.scala +++ b/modules/bootstrapped/test/src/smithy4s/ValidatedNewtypesSpec.scala @@ -16,8 +16,6 @@ package smithy4s -import smithy4s.schema.Schema.string -import smithy4s.schema.Schema.list import smithy4s.refined.NonEmptyList import smithy4s.example.Name import smithy4s.example.ValidatedConstrainedList @@ -116,10 +114,6 @@ class ValidatedNewtypesSpec() extends munit.FunSuite { } test("Validated constrained list refined member") { - def mkName(str: String) = smithy4s.refined.Name(str) match { - case Left(msg) => fail(msg) - case Right(v) => v - } expect( ValidatedConstrainedListRefinedMember( List(Name(mkName("foo"))) diff --git a/modules/core/src/smithy4s/Validator.scala b/modules/core/src/smithy4s/Validator.scala index 2e12613657..09e1b9760f 100644 --- a/modules/core/src/smithy4s/Validator.scala +++ b/modules/core/src/smithy4s/Validator.scala @@ -333,9 +333,9 @@ object Validator { val main = mainValidator.map(_.toSchema(a)).getOrElse(a) main match { case collectionSchema @ Schema.CollectionSchema( - shapeId, - hints, - tag, + _, + _, + _, member ) => val newMember = memberRefinements.foldLeft(member) { From ec9d71849ce6baa24e469a8d47f1975065c0dd9b Mon Sep 17 00:00:00 2001 From: msosnicki Date: Thu, 29 Jan 2026 16:20:03 +0100 Subject: [PATCH 3/9] Fix unused warning --- modules/core/src/smithy4s/Validator.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/core/src/smithy4s/Validator.scala b/modules/core/src/smithy4s/Validator.scala index 09e1b9760f..e0d7e7fd45 100644 --- a/modules/core/src/smithy4s/Validator.scala +++ b/modules/core/src/smithy4s/Validator.scala @@ -52,8 +52,10 @@ object Validator { implicit def toRefinedSyntax[A, B](builder: Builder[A, B])(implicit ev: A =:= B - ): RefinedSyntax[A] = + ): RefinedSyntax[A] = { + val _ = ev new RefinedSyntax(builder.asInstanceOf[Builder[A, A]]) + } private[smithy4s] class PartiallyAppliedRefinedBuilder[A, B]( base: Builder[A, A] From 49ed87f5b2b9c78d1b0aa180f87413d76f5656f7 Mon Sep 17 00:00:00 2001 From: msosnicki Date: Thu, 29 Jan 2026 16:25:38 +0100 Subject: [PATCH 4/9] More warnings --- .../bootstrapped/src/main/smithy4s/example/instances.scala | 7 ++----- .../test/src/smithy4s/ValidatedNewtypesSpec.scala | 4 ---- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/modules/bootstrapped/src/main/smithy4s/example/instances.scala b/modules/bootstrapped/src/main/smithy4s/example/instances.scala index 2cc92d409e..a410205480 100644 --- a/modules/bootstrapped/src/main/smithy4s/example/instances.scala +++ b/modules/bootstrapped/src/main/smithy4s/example/instances.scala @@ -4,10 +4,7 @@ import smithy4s.RefinementProvider object instances { - implicit val lengthProviderValidatedRefinedPrimitive - : RefinementProvider.Simple[ - smithy.api.Length, - Name - ] = + implicit val lengthProviderName + : RefinementProvider.Simple[smithy.api.Length, Name] = RefinementProvider.lengthConstraint(_.value.value.length) } diff --git a/modules/bootstrapped/test/src/smithy4s/ValidatedNewtypesSpec.scala b/modules/bootstrapped/test/src/smithy4s/ValidatedNewtypesSpec.scala index 7a256d3ee3..dcec9b47b1 100644 --- a/modules/bootstrapped/test/src/smithy4s/ValidatedNewtypesSpec.scala +++ b/modules/bootstrapped/test/src/smithy4s/ValidatedNewtypesSpec.scala @@ -16,7 +16,6 @@ package smithy4s -import smithy4s.refined.NonEmptyList import smithy4s.example.Name import smithy4s.example.ValidatedConstrainedList import smithy4s.example.ValidatedListConstrainedMember @@ -31,9 +30,6 @@ import smithy4s.example.AccountId import smithy4s.example.DeviceId import munit.Assertions -import cats.data.Validated.Valid -import smithy.api.Length -import smithy4s.example.NonEmptyListFormat class ValidatedNewtypesSpec() extends munit.FunSuite { val id1 = "id1" From ae68e19cd594f6f8df5322e1659d63101805fd4c Mon Sep 17 00:00:00 2001 From: msosnicki Date: Thu, 29 Jan 2026 16:31:31 +0100 Subject: [PATCH 5/9] Fix renderer --- modules/codegen/src/smithy4s/codegen/internals/Renderer.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/codegen/src/smithy4s/codegen/internals/Renderer.scala b/modules/codegen/src/smithy4s/codegen/internals/Renderer.scala index 947de50ba9..49038ff013 100644 --- a/modules/codegen/src/smithy4s/codegen/internals/Renderer.scala +++ b/modules/codegen/src/smithy4s/codegen/internals/Renderer.scala @@ -1720,7 +1720,7 @@ private[internals] class Renderer(compilationUnit: CompilationUnit) { self => _, _, maybeProviderImport, - underlyingTpe, + _, hint ) => line".refined[${e: Type}](${renderHint(hint)})${maybeProviderImport From 9afb9f6eea69943fa4a0661a1e7fdf7875cf8596 Mon Sep 17 00:00:00 2001 From: msosnicki Date: Fri, 30 Jan 2026 07:46:30 +0100 Subject: [PATCH 6/9] Fix mima + test messages --- build.sbt | 2 + .../src/smithy4s/ValidatedNewtypesSpec.scala | 60 ++++++++++++++----- 2 files changed, 47 insertions(+), 15 deletions(-) diff --git a/build.sbt b/build.sbt index 5e058b0613..6bbdb57f1f 100644 --- a/build.sbt +++ b/build.sbt @@ -303,6 +303,8 @@ lazy val core = projectMatrix ProblemFilters.exclude[MissingClassProblem]("alloy.proto.ProtoCompactOffsetDateTime"), // originating in an Alloy update that removed ProtoCompactOffsetDateTime ProblemFilters.exclude[MissingClassProblem]("alloy.proto.ProtoCompactOffsetDateTime$"), + // private class that got removed + ProblemFilters.exclude[MissingClassProblem]("smithy4s.Validator$ValidatorImpl") ) ) .jvmPlatform(allJvmScalaVersions, jvmDimSettings) diff --git a/modules/bootstrapped/test/src/smithy4s/ValidatedNewtypesSpec.scala b/modules/bootstrapped/test/src/smithy4s/ValidatedNewtypesSpec.scala index dcec9b47b1..603a9186f4 100644 --- a/modules/bootstrapped/test/src/smithy4s/ValidatedNewtypesSpec.scala +++ b/modules/bootstrapped/test/src/smithy4s/ValidatedNewtypesSpec.scala @@ -94,19 +94,34 @@ class ValidatedNewtypesSpec() extends munit.FunSuite { test("Validated constrained list") { expect(ValidatedConstrainedList(List("foo")).isRight) - expect(ValidatedConstrainedList(List("foo", "bar")).isLeft) + expect.same( + ValidatedConstrainedList(List("foo", "bar")), + Left("length required to be <= 1, but was 2") + ) } test("Validated list constrained member") { expect(ValidatedListConstrainedMember(List("f")).isRight) - expect(ValidatedListConstrainedMember(List("foo")).isLeft) + expect.same( + ValidatedListConstrainedMember(List("foo")), + Left("length required to be <= 2, but was 3") + ) } test("Validated constrained list constrained member") { expect(ValidatedConstrainedListConstrainedMember(List("f")).isRight) - expect(ValidatedConstrainedListConstrainedMember(List("foo")).isLeft) - expect(ValidatedConstrainedListConstrainedMember(List("f", "g")).isLeft) - expect(ValidatedConstrainedListConstrainedMember(List("foo", "h")).isLeft) + expect.same( + ValidatedConstrainedListConstrainedMember(List("foo")), + Left("length required to be <= 2, but was 3") + ) + expect.same( + ValidatedConstrainedListConstrainedMember(List("f", "g")), + Left("length required to be <= 1, but was 2") + ) + expect.same( + ValidatedConstrainedListConstrainedMember(List("foo", "h")), + Left("length required to be <= 1, but was 2") + ) } test("Validated constrained list refined member") { @@ -115,13 +130,14 @@ class ValidatedNewtypesSpec() extends munit.FunSuite { List(Name(mkName("foo"))) ).isRight ) - expect( + expect.same( ValidatedConstrainedListRefinedMember( List( Name(mkName("foo")), Name(mkName("bar")) ) - ).isLeft + ), + Left("length required to be <= 1, but was 2") ) } @@ -132,41 +148,55 @@ class ValidatedNewtypesSpec() extends munit.FunSuite { List(Name(mkName("fo"))) ).isRight ) - expect( + expect.same( ValidatedConstrainedListRefinedConstrainedMember( List( Name(mkName("fo")), Name(mkName("ba")) ) - ).isLeft + ), + Left("length required to be <= 1, but was 2") ) - expect( + expect.same( ValidatedConstrainedListRefinedConstrainedMember( List( Name(mkName("foo")) ) - ).isLeft + ), + Left("length required to be <= 2, but was 3") ) } test("Validated refined list constrainer member") { expect(ValidatedRefinedListConstrainedMember(mkNel("fo")).isRight) - expect(ValidatedRefinedListConstrainedMember(mkNel("fo", "foo")).isLeft) + expect.same( + ValidatedRefinedListConstrainedMember(mkNel("fo", "foo")), + Left("length required to be <= 2, but was 3") + ) } test("Validated constrained map") { expect(ValidatedConstrainedMap(Map("foo" -> 1)).isRight) - expect(ValidatedConstrainedMap(Map("foo" -> 1, "bar" -> 2)).isLeft) + expect.same( + ValidatedConstrainedMap(Map("foo" -> 1, "bar" -> 2)), + Left("length required to be <= 1, but was 2") + ) } test("Validated map constrained key") { expect(ValidatedMapConstrainedKey(Map("a" -> 1, "b" -> 2)).isRight) - expect(ValidatedMapConstrainedKey(Map("a" -> 1, "bar" -> 2)).isLeft) + expect.same( + ValidatedMapConstrainedKey(Map("a" -> 1, "bar" -> 2)), + Left("length required to be <= 2, but was 3") + ) } test("Validated map constrained value") { expect(ValidatedMapConstrainedValue(Map("a" -> 1, "b" -> 2)).isRight) - expect(ValidatedMapConstrainedValue(Map("a" -> 1, "b" -> 3)).isLeft) + expect.same( + ValidatedMapConstrainedValue(Map("a" -> 1, "b" -> 3)), + Left("Input must be <= 2.0, but was 3.0") + ) } private def mkNel[A](elems: A*) = From bcb32951d90fe38b0e20f8ca13373bea235defec Mon Sep 17 00:00:00 2001 From: msosnicki Date: Fri, 30 Jan 2026 08:59:51 +0100 Subject: [PATCH 7/9] Add more collection variations into tests --- .../ConstrainedListConstrainedMember.scala | 16 ------- ...nstrainedIndexedSeqConstrainedMember.scala | 19 ++++++++ ...atedConstrainedListConstrainedMember.scala | 19 -------- ...strainedListRefinedConstrainedMember.scala | 20 -------- ...rainedVectorRefinedConstrainedMember.scala | 20 ++++++++ .../ValidatedListConstrainedMember.scala | 19 -------- .../ValidatedMapConstrainedValue.scala | 9 ++-- .../ValidatedSetConstrainedMember.scala | 21 +++++++++ .../generated/smithy4s/example/package.scala | 7 ++- .../src/smithy4s/ValidatedNewtypesSpec.scala | 46 ++++++++++--------- sampleSpecs/validated-newtype.smithy | 19 ++++---- 11 files changed, 100 insertions(+), 115 deletions(-) delete mode 100644 modules/bootstrapped/src/generated/smithy4s/example/ConstrainedListConstrainedMember.scala create mode 100644 modules/bootstrapped/src/generated/smithy4s/example/ValidatedConstrainedIndexedSeqConstrainedMember.scala delete mode 100644 modules/bootstrapped/src/generated/smithy4s/example/ValidatedConstrainedListConstrainedMember.scala delete mode 100644 modules/bootstrapped/src/generated/smithy4s/example/ValidatedConstrainedListRefinedConstrainedMember.scala create mode 100644 modules/bootstrapped/src/generated/smithy4s/example/ValidatedConstrainedVectorRefinedConstrainedMember.scala delete mode 100644 modules/bootstrapped/src/generated/smithy4s/example/ValidatedListConstrainedMember.scala create mode 100644 modules/bootstrapped/src/generated/smithy4s/example/ValidatedSetConstrainedMember.scala diff --git a/modules/bootstrapped/src/generated/smithy4s/example/ConstrainedListConstrainedMember.scala b/modules/bootstrapped/src/generated/smithy4s/example/ConstrainedListConstrainedMember.scala deleted file mode 100644 index e3b58774f5..0000000000 --- a/modules/bootstrapped/src/generated/smithy4s/example/ConstrainedListConstrainedMember.scala +++ /dev/null @@ -1,16 +0,0 @@ -package smithy4s.example - -import smithy4s.Hints -import smithy4s.Newtype -import smithy4s.Schema -import smithy4s.ShapeId -import smithy4s.schema.Schema.bijection -import smithy4s.schema.Schema.list -import smithy4s.schema.Schema.string - -object ConstrainedListConstrainedMember extends Newtype[List[String]] { - val id: ShapeId = ShapeId("smithy4s.example", "ConstrainedListConstrainedMember") - val hints: Hints = Hints.empty - val underlyingSchema: Schema[List[String]] = list(string.addMemberHints().validated(smithy.api.Length(min = None, max = Some(2L)))).withId(id).addHints(hints).validated(smithy.api.Length(min = None, max = Some(1L))) - implicit val schema: Schema[ConstrainedListConstrainedMember] = bijection(underlyingSchema, asBijection) -} diff --git a/modules/bootstrapped/src/generated/smithy4s/example/ValidatedConstrainedIndexedSeqConstrainedMember.scala b/modules/bootstrapped/src/generated/smithy4s/example/ValidatedConstrainedIndexedSeqConstrainedMember.scala new file mode 100644 index 0000000000..c260f52a6e --- /dev/null +++ b/modules/bootstrapped/src/generated/smithy4s/example/ValidatedConstrainedIndexedSeqConstrainedMember.scala @@ -0,0 +1,19 @@ +package smithy4s.example + +import smithy4s.Bijection +import smithy4s.Hints +import smithy4s.Schema +import smithy4s.ShapeId +import smithy4s.ValidatedNewtype +import smithy4s.Validator +import smithy4s.schema.Schema.indexedSeq +import smithy4s.schema.Schema.string + +object ValidatedConstrainedIndexedSeqConstrainedMember extends ValidatedNewtype[IndexedSeq[String]] { + val id: ShapeId = ShapeId("smithy4s.example", "ValidatedConstrainedIndexedSeqConstrainedMember") + val hints: Hints = Hints.empty + val underlyingSchema: Schema[IndexedSeq[String]] = indexedSeq(string.addMemberHints().validated(smithy.api.Length(min = None, max = Some(2L)))).withId(id).addHints(hints).validated(smithy.api.Length(min = None, max = Some(1L))) + val validator: Validator[IndexedSeq[String], ValidatedConstrainedIndexedSeqConstrainedMember] = Validator.Builder.indexedSeq[String].validating(smithy.api.Length(min = None, max = Some(1L))).validatingMember(smithy.api.Length(min = None, max = Some(2L))).biject(Bijection[IndexedSeq[String], ValidatedConstrainedIndexedSeqConstrainedMember](_.asInstanceOf[ValidatedConstrainedIndexedSeqConstrainedMember], value(_))).build() + implicit val schema: Schema[ValidatedConstrainedIndexedSeqConstrainedMember] = validator.toSchema(underlyingSchema) + @inline def apply(a: IndexedSeq[String]): Either[String, ValidatedConstrainedIndexedSeqConstrainedMember] = validator.validate(a) +} diff --git a/modules/bootstrapped/src/generated/smithy4s/example/ValidatedConstrainedListConstrainedMember.scala b/modules/bootstrapped/src/generated/smithy4s/example/ValidatedConstrainedListConstrainedMember.scala deleted file mode 100644 index 978bfefe90..0000000000 --- a/modules/bootstrapped/src/generated/smithy4s/example/ValidatedConstrainedListConstrainedMember.scala +++ /dev/null @@ -1,19 +0,0 @@ -package smithy4s.example - -import smithy4s.Bijection -import smithy4s.Hints -import smithy4s.Schema -import smithy4s.ShapeId -import smithy4s.ValidatedNewtype -import smithy4s.Validator -import smithy4s.schema.Schema.list -import smithy4s.schema.Schema.string - -object ValidatedConstrainedListConstrainedMember extends ValidatedNewtype[List[String]] { - val id: ShapeId = ShapeId("smithy4s.example", "ValidatedConstrainedListConstrainedMember") - val hints: Hints = Hints.empty - val underlyingSchema: Schema[List[String]] = list(string.addMemberHints().validated(smithy.api.Length(min = None, max = Some(2L)))).withId(id).addHints(hints).validated(smithy.api.Length(min = None, max = Some(1L))) - val validator: Validator[List[String], ValidatedConstrainedListConstrainedMember] = Validator.Builder.list[String].validating(smithy.api.Length(min = None, max = Some(1L))).validatingMember(smithy.api.Length(min = None, max = Some(2L))).biject(Bijection[List[String], ValidatedConstrainedListConstrainedMember](_.asInstanceOf[ValidatedConstrainedListConstrainedMember], value(_))).build() - implicit val schema: Schema[ValidatedConstrainedListConstrainedMember] = validator.toSchema(underlyingSchema) - @inline def apply(a: List[String]): Either[String, ValidatedConstrainedListConstrainedMember] = validator.validate(a) -} diff --git a/modules/bootstrapped/src/generated/smithy4s/example/ValidatedConstrainedListRefinedConstrainedMember.scala b/modules/bootstrapped/src/generated/smithy4s/example/ValidatedConstrainedListRefinedConstrainedMember.scala deleted file mode 100644 index 30e1ce9841..0000000000 --- a/modules/bootstrapped/src/generated/smithy4s/example/ValidatedConstrainedListRefinedConstrainedMember.scala +++ /dev/null @@ -1,20 +0,0 @@ -package smithy4s.example - -import smithy4s.Bijection -import smithy4s.Hints -import smithy4s.Schema -import smithy4s.ShapeId -import smithy4s.ValidatedNewtype -import smithy4s.Validator -import smithy4s.example.instances._ -import smithy4s.schema.Schema.list - - -object ValidatedConstrainedListRefinedConstrainedMember extends ValidatedNewtype[List[Name]] { - val id: ShapeId = ShapeId("smithy4s.example", "ValidatedConstrainedListRefinedConstrainedMember") - val hints: Hints = Hints.empty - val underlyingSchema: Schema[List[Name]] = list(Name.schema.addMemberHints().validated(smithy.api.Length(min = None, max = Some(2L)))).withId(id).addHints(hints).validated(smithy.api.Length(min = None, max = Some(1L))) - val validator: Validator[List[Name], ValidatedConstrainedListRefinedConstrainedMember] = Validator.Builder.list[Name].validating(smithy.api.Length(min = None, max = Some(1L))).validatingMember(smithy.api.Length(min = None, max = Some(2L))).biject(Bijection[List[Name], ValidatedConstrainedListRefinedConstrainedMember](_.asInstanceOf[ValidatedConstrainedListRefinedConstrainedMember], value(_))).build() - implicit val schema: Schema[ValidatedConstrainedListRefinedConstrainedMember] = validator.toSchema(underlyingSchema) - @inline def apply(a: List[Name]): Either[String, ValidatedConstrainedListRefinedConstrainedMember] = validator.validate(a) -} diff --git a/modules/bootstrapped/src/generated/smithy4s/example/ValidatedConstrainedVectorRefinedConstrainedMember.scala b/modules/bootstrapped/src/generated/smithy4s/example/ValidatedConstrainedVectorRefinedConstrainedMember.scala new file mode 100644 index 0000000000..e6f738fa27 --- /dev/null +++ b/modules/bootstrapped/src/generated/smithy4s/example/ValidatedConstrainedVectorRefinedConstrainedMember.scala @@ -0,0 +1,20 @@ +package smithy4s.example + +import smithy4s.Bijection +import smithy4s.Hints +import smithy4s.Schema +import smithy4s.ShapeId +import smithy4s.ValidatedNewtype +import smithy4s.Validator +import smithy4s.example.instances._ +import smithy4s.schema.Schema.vector + + +object ValidatedConstrainedVectorRefinedConstrainedMember extends ValidatedNewtype[Vector[Name]] { + val id: ShapeId = ShapeId("smithy4s.example", "ValidatedConstrainedVectorRefinedConstrainedMember") + val hints: Hints = Hints.empty + val underlyingSchema: Schema[Vector[Name]] = vector(Name.schema.addMemberHints().validated(smithy.api.Length(min = None, max = Some(2L)))).withId(id).addHints(hints).validated(smithy.api.Length(min = None, max = Some(1L))) + val validator: Validator[Vector[Name], ValidatedConstrainedVectorRefinedConstrainedMember] = Validator.Builder.vector[Name].validating(smithy.api.Length(min = None, max = Some(1L))).validatingMember(smithy.api.Length(min = None, max = Some(2L))).biject(Bijection[Vector[Name], ValidatedConstrainedVectorRefinedConstrainedMember](_.asInstanceOf[ValidatedConstrainedVectorRefinedConstrainedMember], value(_))).build() + implicit val schema: Schema[ValidatedConstrainedVectorRefinedConstrainedMember] = validator.toSchema(underlyingSchema) + @inline def apply(a: Vector[Name]): Either[String, ValidatedConstrainedVectorRefinedConstrainedMember] = validator.validate(a) +} diff --git a/modules/bootstrapped/src/generated/smithy4s/example/ValidatedListConstrainedMember.scala b/modules/bootstrapped/src/generated/smithy4s/example/ValidatedListConstrainedMember.scala deleted file mode 100644 index aabd95684a..0000000000 --- a/modules/bootstrapped/src/generated/smithy4s/example/ValidatedListConstrainedMember.scala +++ /dev/null @@ -1,19 +0,0 @@ -package smithy4s.example - -import smithy4s.Bijection -import smithy4s.Hints -import smithy4s.Schema -import smithy4s.ShapeId -import smithy4s.ValidatedNewtype -import smithy4s.Validator -import smithy4s.schema.Schema.list -import smithy4s.schema.Schema.string - -object ValidatedListConstrainedMember extends ValidatedNewtype[List[String]] { - val id: ShapeId = ShapeId("smithy4s.example", "ValidatedListConstrainedMember") - val hints: Hints = Hints.empty - val underlyingSchema: Schema[List[String]] = list(string.addMemberHints().validated(smithy.api.Length(min = None, max = Some(2L)))).withId(id).addHints(hints) - val validator: Validator[List[String], ValidatedListConstrainedMember] = Validator.Builder.list[String].validatingMember(smithy.api.Length(min = None, max = Some(2L))).biject(Bijection[List[String], ValidatedListConstrainedMember](_.asInstanceOf[ValidatedListConstrainedMember], value(_))).build() - implicit val schema: Schema[ValidatedListConstrainedMember] = validator.toSchema(underlyingSchema) - @inline def apply(a: List[String]): Either[String, ValidatedListConstrainedMember] = validator.validate(a) -} diff --git a/modules/bootstrapped/src/generated/smithy4s/example/ValidatedMapConstrainedValue.scala b/modules/bootstrapped/src/generated/smithy4s/example/ValidatedMapConstrainedValue.scala index 2769c0dcc6..d180c539e1 100644 --- a/modules/bootstrapped/src/generated/smithy4s/example/ValidatedMapConstrainedValue.scala +++ b/modules/bootstrapped/src/generated/smithy4s/example/ValidatedMapConstrainedValue.scala @@ -6,15 +6,14 @@ import smithy4s.Schema import smithy4s.ShapeId import smithy4s.ValidatedNewtype import smithy4s.Validator -import smithy4s.schema.Schema.int import smithy4s.schema.Schema.map import smithy4s.schema.Schema.string -object ValidatedMapConstrainedValue extends ValidatedNewtype[Map[String, Int]] { +object ValidatedMapConstrainedValue extends ValidatedNewtype[Map[String, String]] { val id: ShapeId = ShapeId("smithy4s.example", "ValidatedMapConstrainedValue") val hints: Hints = Hints.empty - val underlyingSchema: Schema[Map[String, Int]] = map(string, int.addMemberHints().validated(smithy.api.Range(min = None, max = Some(scala.math.BigDecimal(2.0))))).withId(id).addHints(hints) - val validator: Validator[Map[String, Int], ValidatedMapConstrainedValue] = Validator.Builder.map[String, Int].validatingValue(smithy.api.Range(min = None, max = Some(scala.math.BigDecimal(2.0)))).biject(Bijection[Map[String, Int], ValidatedMapConstrainedValue](_.asInstanceOf[ValidatedMapConstrainedValue], value(_))).build() + val underlyingSchema: Schema[Map[String, String]] = map(string, string.addMemberHints().validated(smithy.api.Pattern(s"^[a-zA-Z0-9]+$$"))).withId(id).addHints(hints) + val validator: Validator[Map[String, String], ValidatedMapConstrainedValue] = Validator.Builder.map[String, String].validatingValue(smithy.api.Pattern(s"^[a-zA-Z0-9]+$$")).biject(Bijection[Map[String, String], ValidatedMapConstrainedValue](_.asInstanceOf[ValidatedMapConstrainedValue], value(_))).build() implicit val schema: Schema[ValidatedMapConstrainedValue] = validator.toSchema(underlyingSchema) - @inline def apply(a: Map[String, Int]): Either[String, ValidatedMapConstrainedValue] = validator.validate(a) + @inline def apply(a: Map[String, String]): Either[String, ValidatedMapConstrainedValue] = validator.validate(a) } diff --git a/modules/bootstrapped/src/generated/smithy4s/example/ValidatedSetConstrainedMember.scala b/modules/bootstrapped/src/generated/smithy4s/example/ValidatedSetConstrainedMember.scala new file mode 100644 index 0000000000..3bdc2492f2 --- /dev/null +++ b/modules/bootstrapped/src/generated/smithy4s/example/ValidatedSetConstrainedMember.scala @@ -0,0 +1,21 @@ +package smithy4s.example + +import smithy4s.Bijection +import smithy4s.Hints +import smithy4s.Schema +import smithy4s.ShapeId +import smithy4s.ValidatedNewtype +import smithy4s.Validator +import smithy4s.schema.Schema.set +import smithy4s.schema.Schema.string + +object ValidatedSetConstrainedMember extends ValidatedNewtype[Set[String]] { + val id: ShapeId = ShapeId("smithy4s.example", "ValidatedSetConstrainedMember") + val hints: Hints = Hints( + smithy.api.UniqueItems(), + ).lazily + val underlyingSchema: Schema[Set[String]] = set(string.addMemberHints().validated(smithy.api.Length(min = None, max = Some(2L)))).withId(id).addHints(hints) + val validator: Validator[Set[String], ValidatedSetConstrainedMember] = Validator.Builder.set[String].validatingMember(smithy.api.Length(min = None, max = Some(2L))).biject(Bijection[Set[String], ValidatedSetConstrainedMember](_.asInstanceOf[ValidatedSetConstrainedMember], value(_))).build() + implicit val schema: Schema[ValidatedSetConstrainedMember] = validator.toSchema(underlyingSchema) + @inline def apply(a: Set[String]): Either[String, ValidatedSetConstrainedMember] = validator.validate(a) +} diff --git a/modules/bootstrapped/src/generated/smithy4s/example/package.scala b/modules/bootstrapped/src/generated/smithy4s/example/package.scala index 380168bce5..eb5e356a84 100644 --- a/modules/bootstrapped/src/generated/smithy4s/example/package.scala +++ b/modules/bootstrapped/src/generated/smithy4s/example/package.scala @@ -58,7 +58,6 @@ package object example { type CityId = smithy4s.example.CityId.Type type CitySummaries = smithy4s.example.CitySummaries.Type type ConstrainedList = smithy4s.example.ConstrainedList.Type - type ConstrainedListConstrainedMember = smithy4s.example.ConstrainedListConstrainedMember.Type type ConstrainedMap = smithy4s.example.ConstrainedMap.Type type CustomErrorMessageType = smithy4s.example.CustomErrorMessageType.Type type DefaultStringMap = smithy4s.example.DefaultStringMap.Type @@ -130,16 +129,16 @@ package object example { type UnwrappedFancyList = smithy4s.example.UnwrappedFancyList.Type type Username = smithy4s.example.Username.Type type UuidTrait = smithy4s.example.UuidTrait.Type + type ValidatedConstrainedIndexedSeqConstrainedMember = smithy4s.example.ValidatedConstrainedIndexedSeqConstrainedMember.Type type ValidatedConstrainedList = smithy4s.example.ValidatedConstrainedList.Type - type ValidatedConstrainedListConstrainedMember = smithy4s.example.ValidatedConstrainedListConstrainedMember.Type - type ValidatedConstrainedListRefinedConstrainedMember = smithy4s.example.ValidatedConstrainedListRefinedConstrainedMember.Type type ValidatedConstrainedListRefinedMember = smithy4s.example.ValidatedConstrainedListRefinedMember.Type type ValidatedConstrainedMap = smithy4s.example.ValidatedConstrainedMap.Type - type ValidatedListConstrainedMember = smithy4s.example.ValidatedListConstrainedMember.Type + type ValidatedConstrainedVectorRefinedConstrainedMember = smithy4s.example.ValidatedConstrainedVectorRefinedConstrainedMember.Type type ValidatedMapConstrainedKey = smithy4s.example.ValidatedMapConstrainedKey.Type type ValidatedMapConstrainedValue = smithy4s.example.ValidatedMapConstrainedValue.Type type ValidatedRefinedList = smithy4s.example.ValidatedRefinedList.Type type ValidatedRefinedListConstrainedMember = smithy4s.example.ValidatedRefinedListConstrainedMember.Type + type ValidatedSetConstrainedMember = smithy4s.example.ValidatedSetConstrainedMember.Type type ValidatedString = smithy4s.example.ValidatedString.Type } \ No newline at end of file diff --git a/modules/bootstrapped/test/src/smithy4s/ValidatedNewtypesSpec.scala b/modules/bootstrapped/test/src/smithy4s/ValidatedNewtypesSpec.scala index 603a9186f4..0af32286a7 100644 --- a/modules/bootstrapped/test/src/smithy4s/ValidatedNewtypesSpec.scala +++ b/modules/bootstrapped/test/src/smithy4s/ValidatedNewtypesSpec.scala @@ -18,10 +18,10 @@ package smithy4s import smithy4s.example.Name import smithy4s.example.ValidatedConstrainedList -import smithy4s.example.ValidatedListConstrainedMember -import smithy4s.example.ValidatedConstrainedListConstrainedMember +import smithy4s.example.ValidatedSetConstrainedMember +import smithy4s.example.ValidatedConstrainedIndexedSeqConstrainedMember import smithy4s.example.ValidatedConstrainedListRefinedMember -import smithy4s.example.ValidatedConstrainedListRefinedConstrainedMember +import smithy4s.example.ValidatedConstrainedVectorRefinedConstrainedMember import smithy4s.example.ValidatedMapConstrainedKey import smithy4s.example.ValidatedConstrainedMap import smithy4s.example.ValidatedMapConstrainedValue @@ -100,26 +100,28 @@ class ValidatedNewtypesSpec() extends munit.FunSuite { ) } - test("Validated list constrained member") { - expect(ValidatedListConstrainedMember(List("f")).isRight) + test("Validated set constrained member") { + expect(ValidatedSetConstrainedMember(Set("f")).isRight) expect.same( - ValidatedListConstrainedMember(List("foo")), + ValidatedSetConstrainedMember(Set("foo")), Left("length required to be <= 2, but was 3") ) } - test("Validated constrained list constrained member") { - expect(ValidatedConstrainedListConstrainedMember(List("f")).isRight) + test("Validated constrained indexed seq constrained member") { + expect( + ValidatedConstrainedIndexedSeqConstrainedMember(IndexedSeq("f")).isRight + ) expect.same( - ValidatedConstrainedListConstrainedMember(List("foo")), + ValidatedConstrainedIndexedSeqConstrainedMember(IndexedSeq("foo")), Left("length required to be <= 2, but was 3") ) expect.same( - ValidatedConstrainedListConstrainedMember(List("f", "g")), + ValidatedConstrainedIndexedSeqConstrainedMember(IndexedSeq("f", "g")), Left("length required to be <= 1, but was 2") ) expect.same( - ValidatedConstrainedListConstrainedMember(List("foo", "h")), + ValidatedConstrainedIndexedSeqConstrainedMember(IndexedSeq("foo", "h")), Left("length required to be <= 1, but was 2") ) } @@ -141,16 +143,16 @@ class ValidatedNewtypesSpec() extends munit.FunSuite { ) } - test("Validated constrained list refined & constrained member") { + test("Validated constrained vector refined & constrained member") { expect( - ValidatedConstrainedListRefinedConstrainedMember( - List(Name(mkName("fo"))) + ValidatedConstrainedVectorRefinedConstrainedMember( + Vector(Name(mkName("fo"))) ).isRight ) expect.same( - ValidatedConstrainedListRefinedConstrainedMember( - List( + ValidatedConstrainedVectorRefinedConstrainedMember( + Vector( Name(mkName("fo")), Name(mkName("ba")) ) @@ -158,8 +160,8 @@ class ValidatedNewtypesSpec() extends munit.FunSuite { Left("length required to be <= 1, but was 2") ) expect.same( - ValidatedConstrainedListRefinedConstrainedMember( - List( + ValidatedConstrainedVectorRefinedConstrainedMember( + Vector( Name(mkName("foo")) ) ), @@ -192,10 +194,12 @@ class ValidatedNewtypesSpec() extends munit.FunSuite { } test("Validated map constrained value") { - expect(ValidatedMapConstrainedValue(Map("a" -> 1, "b" -> 2)).isRight) + expect( + ValidatedMapConstrainedValue(Map("a" -> "123", "b" -> "456")).isRight + ) expect.same( - ValidatedMapConstrainedValue(Map("a" -> 1, "b" -> 3)), - Left("Input must be <= 2.0, but was 3.0") + ValidatedMapConstrainedValue(Map("a" -> "123", "b" -> "4-5-6")), + Left("String '4-5-6' does not match pattern '^[a-zA-Z0-9]+$'") ) } diff --git a/sampleSpecs/validated-newtype.smithy b/sampleSpecs/validated-newtype.smithy index 3e2a73e9a1..03ab57a0aa 100644 --- a/sampleSpecs/validated-newtype.smithy +++ b/sampleSpecs/validated-newtype.smithy @@ -33,20 +33,16 @@ list ValidatedConstrainedList { } @validateNewtype -list ValidatedListConstrainedMember { +@uniqueItems +list ValidatedSetConstrainedMember { @length(max: 2) member: String } @validateNewtype @length(max: 1) -list ValidatedConstrainedListConstrainedMember { - @length(max: 2) - member: String -} - -@length(max: 1) -list ConstrainedListConstrainedMember { +@smithy4s.meta#indexedSeq +list ValidatedConstrainedIndexedSeqConstrainedMember { @length(max: 2) member: String } @@ -60,7 +56,8 @@ list ValidatedConstrainedListRefinedMember { @validateNewtype @length(max: 1) @scalaImports(["smithy4s.example.instances._"]) -list ValidatedConstrainedListRefinedConstrainedMember { +@smithy4s.meta#vector +list ValidatedConstrainedVectorRefinedConstrainedMember { @length(max:2) member: Name } @@ -95,8 +92,8 @@ map ValidatedMapConstrainedKey { @validateNewtype map ValidatedMapConstrainedValue { key: String - @range(max: 2) - value: Integer + @pattern("^[a-zA-Z0-9]+$") + value: String } // @nonEmptyMapFormat From 98dd7a068111ed48c2da52d773bf2bd7f9b1db10 Mon Sep 17 00:00:00 2001 From: msosnicki Date: Fri, 30 Jan 2026 12:20:36 +0100 Subject: [PATCH 8/9] Fix docs --- .../04-codegen/01-customisation/15-validated-newtypes.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/modules/docs/resources/markdown/04-codegen/01-customisation/15-validated-newtypes.md b/modules/docs/resources/markdown/04-codegen/01-customisation/15-validated-newtypes.md index c049eee12a..8013764cc3 100644 --- a/modules/docs/resources/markdown/04-codegen/01-customisation/15-validated-newtypes.md +++ b/modules/docs/resources/markdown/04-codegen/01-customisation/15-validated-newtypes.md @@ -37,6 +37,7 @@ import smithy4s.schema.Schema.string type Name = Name.Type object Name extends ValidatedNewtype[String] { + val id: ShapeId = ShapeId("smithy4s.example", "Name") val hints: Hints = Hints.empty @@ -46,13 +47,15 @@ object Name extends ValidatedNewtype[String] { .withId(id) .addHints(hints) .validated(smithy.api.Length(min = Some(5L), max = None)) - + val validator: Validator[String, Name] = - Validator.of[String, Name](Bijection[String, Name](_.asInstanceOf[Name], value(_))) + Validator.Builder.simple[String] .validating(smithy.api.Length(min = Some(5L), max = None)) + .biject(Bijection[String, Name](_.asInstanceOf[Name], value(_))).build() implicit val schema: Schema[Name] = validator.toSchema(underlyingSchema) @inline def apply(a: String): Either[String, Name] = validator.validate(a) } + ``` From 824b92ed01797806fdb92a6a07954f3828e54017 Mon Sep 17 00:00:00 2001 From: msosnicki Date: Fri, 20 Feb 2026 16:52:47 +0100 Subject: [PATCH 9/9] Make builders smithy4s private --- modules/core/src/smithy4s/Validator.scala | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/modules/core/src/smithy4s/Validator.scala b/modules/core/src/smithy4s/Validator.scala index e0d7e7fd45..f93ae42154 100644 --- a/modules/core/src/smithy4s/Validator.scala +++ b/modules/core/src/smithy4s/Validator.scala @@ -32,7 +32,7 @@ sealed trait Validator[A, B] { self => object Validator { - trait Builder[A, B] { self => + private[smithy4s] trait Builder[A, B] { self => def validating[C](constraint: C)(implicit ev: RefinementProvider.Simple[C, A] ): Builder[A, B] @@ -65,7 +65,8 @@ object Validator { ): Builder[B, B] = new Builder.Refined(base, ev.make(c), Vector.empty) } - trait Collection[Col[_], E] extends Builder[Col[E], Col[E]] { + private[smithy4s] trait Collection[Col[_], E] + extends Builder[Col[E], Col[E]] { override def validating[C](constraint: C)(implicit ev: RefinementProvider.Simple[C, Col[E]] ): Builder.Collection[Col, E] @@ -76,7 +77,8 @@ object Validator { } - trait KeyValue[K, V] extends Builder[Map[K, V], Map[K, V]] { + private[smithy4s] trait KeyValue[K, V] + extends Builder[Map[K, V], Map[K, V]] { override def validating[C](constraint: C)(implicit ev: RefinementProvider.Simple[C, Map[K, V]] ): KeyValue[K, V]