-
Notifications
You must be signed in to change notification settings - Fork 98
Dynamic validation #1991
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: series/0.19
Are you sure you want to change the base?
Dynamic validation #1991
Changes from all commits
f3fefe1
dd0c5d1
7b4d055
afa2f7a
356174d
93b9371
3439438
5ae2ab4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we need to limit ourselves to JVM here, this could work on all platforms. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,145 @@ | ||
| /* | ||
| * Copyright 2021-2026 Disney Streaming | ||
| * | ||
| * Licensed under the Tomorrow Open Source Technology License, Version 1.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * https://disneystreaming.github.io/TOST-1.0.txt | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| package smithy4s.dynamic | ||
|
|
||
| import smithy4s.Document | ||
| import smithy4s.RefinementProvider | ||
| import smithy4s.ShapeId | ||
| import smithy4s.schema.EnumValue | ||
| import smithy4s.schema.Primitive._ | ||
| import smithy4s.schema.Schema | ||
| import smithy4s.schema.Schema._ | ||
| import smithy4s.~> | ||
|
|
||
| /** | ||
| * Dynamically-loaded schemas only carry constraint traits (`@length`, `@range`, | ||
| * `@pattern` etc) as hints: by default, `DynamicModelCompiler` only attaches them via | ||
| * `addHints`, unlike smithy4s codegen, which reifies such traits into `RefinementSchema` | ||
| * wrappers that get enforced upon decoding. | ||
| * | ||
| * This object provides a transformation for dynamically-loaded schemas, so that validation | ||
| * hints are reintroduced at a `Schema` level. | ||
| */ | ||
| private[dynamic] object DynamicSchemaValidation { | ||
|
|
||
| def reifyConstraints(index: DynamicSchemaIndex): DynamicSchemaIndex = | ||
| new DynamicSchemaIndex { | ||
| private val schemaMap: Map[ShapeId, Schema[_]] = | ||
| index.allSchemas.map(s => s.shapeId -> reifySchema(s)).toMap | ||
| def allServices: Iterable[DynamicSchemaIndex.ServiceWrapper] = | ||
| index.allServices | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
To be safe, we could convert those services to builders and map each endpoint's input/output/errors, reifying the constraints right there. I think a simpler alternative to this would be embedding the reification process directly in the DynamicCompiler rather than in a "middleware" (here) |
||
| def allSchemas: Iterable[Schema[_]] = | ||
| schemaMap.values | ||
| def getSchema(shapeId: ShapeId): Option[Schema[_]] = | ||
| schemaMap.get(shapeId) | ||
| def metadata: Map[String, Document] = index.metadata | ||
| } | ||
|
|
||
| private def reifySchema[A](schema: Schema[A]): Schema[A] = | ||
| schema.transformTransitivelyK(ReifyConstraints) | ||
|
|
||
| private object ReifyConstraints extends (Schema ~> Schema) { | ||
|
|
||
| private implicit class SchemaOps[A](schema: Schema[A]) { | ||
| def reifyHint[B](rp: RefinementProvider[B, A, ?]): Schema[A] = | ||
| schema.hints | ||
| .get(rp.tag) | ||
| .fold(schema)(schema.validated(_)(RefinementProvider.void(rp))) | ||
| } | ||
|
|
||
| private def collection[C[_], B]( | ||
| schema: Schema.CollectionSchema[C, B] | ||
| ): Schema[C[B]] = | ||
| schema.reifyHint( | ||
| RefinementProvider.lengthConstraint[C[B]](schema.tag.iterator(_).size) | ||
| ) | ||
|
|
||
| private def enumSchema[B]( | ||
| schema: Schema.EnumerationSchema[B] | ||
| ): Schema[B] = { | ||
| val byValue: Map[B, EnumValue[B]] = | ||
| schema.values.map(v => v.value -> v).toMap | ||
| schema | ||
| .reifyHint( | ||
| RefinementProvider.lengthConstraint[B](byValue(_).stringValue.length) | ||
| ) | ||
| .reifyHint( | ||
| RefinementProvider.rangeConstraint[B, Int](byValue(_).intValue) | ||
| ) | ||
| .reifyHint( | ||
| RefinementProvider.patternConstraint[B](byValue(_).stringValue) | ||
| ) | ||
| } | ||
|
|
||
| def apply[A](schema: Schema[A]): Schema[A] = | ||
| schema match { | ||
| case t @ PrimitiveSchema(_, _, tag) => | ||
| tag match { | ||
| case PString => | ||
| t.reifyHint(RefinementProvider.stringLengthConstraint) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. also: |
||
| .reifyHint(RefinementProvider.stringPatternConstraints) | ||
| .reifyHint(RefinementProvider.idRefRefinement) | ||
| case PByte => | ||
| schema.reifyHint(RefinementProvider.numericRangeConstraints[Byte]) | ||
| case PShort => | ||
| schema.reifyHint( | ||
| RefinementProvider.numericRangeConstraints[Short] | ||
| ) | ||
| case PInt => | ||
| schema.reifyHint(RefinementProvider.numericRangeConstraints[Int]) | ||
| case PLong => | ||
| schema.reifyHint(RefinementProvider.numericRangeConstraints[Long]) | ||
| case PFloat => | ||
| schema.reifyHint( | ||
| RefinementProvider.numericRangeConstraints[Float] | ||
| ) | ||
| case PDouble => | ||
| schema.reifyHint( | ||
| RefinementProvider.numericRangeConstraints[Double] | ||
| ) | ||
| case PBigInt => | ||
| schema.reifyHint( | ||
| RefinementProvider.numericRangeConstraints[BigInt] | ||
| ) | ||
| case PBigDecimal => | ||
| schema.reifyHint( | ||
| RefinementProvider.numericRangeConstraints[BigDecimal] | ||
| ) | ||
| case PBlob => | ||
| schema.reifyHint(RefinementProvider.blobLengthConstraint) | ||
| case PTimestamp | PDocument | PBoolean | PUUID | PLocalDate | | ||
| PLocalTime | PDuration | POffsetDateTime => | ||
| schema | ||
| } | ||
| case e: EnumerationSchema[a] => | ||
| enumSchema(e).asInstanceOf[Schema[A]] | ||
| case c @ CollectionSchema(_, _, _, _) => collection(c) | ||
| case m: MapSchema[c, k, v] => | ||
| m.reifyHint( | ||
| RefinementProvider.lengthConstraint[c[k, v]](m.tag.iterator(_).size) | ||
| ) | ||
| // explicitly handling each remaining case, in order to get a "missing match" warning if the schema model changes | ||
| case b: BijectionSchema[_, _] => b | ||
| case r: RefinementSchema[_, _] => r | ||
| case s: StructSchema[_] => s | ||
| case l: LazySchema[_] => l | ||
| case u: UnionSchema[_] => u | ||
| case n: OptionSchema[_, _] => n | ||
| } | ||
| } | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,123 @@ | ||
| /* | ||
| * Copyright 2021-2026 Disney Streaming | ||
| * | ||
| * Licensed under the Tomorrow Open Source Technology License, Version 1.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * https://disneystreaming.github.io/TOST-1.0.txt | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| package smithy4s.dynamic.internals | ||
|
|
||
| import smithy4s.RefinementProvider | ||
| import smithy4s.schema.Primitive._ | ||
| import smithy4s.schema.Schema | ||
| import smithy4s.schema.Schema._ | ||
| import smithy4s.~> | ||
|
|
||
| /** | ||
| * Dynamically-loaded schemas only carry constraint traits (`@length`, `@range`, | ||
| * `@pattern` etc) as hints: `DynamicModelCompiler` only attaches them via `addHints`/ | ||
| * `addMemberHints`, unlike smithy4s codegen, which reifies such traits into | ||
| * `RefinementSchema` wrappers that get enforced upon decoding. | ||
| * | ||
| * This is a single-node (non-recursive) transform: it only inspects the hints already | ||
| * present on the schema it's given. `DynamicModelCompiler` applies it locally, at each | ||
| * point a schema's hints have just finished being merged (a shape's own top-level hints, | ||
| * or a member/field's merged hints) and are about to be handed to a consumer — composition | ||
| * across nesting levels then falls out for free, since a nested schema is already reified | ||
| * by the time its parent embeds it. | ||
| */ | ||
| private[dynamic] object ConstraintReification extends (Schema ~> Schema) { | ||
|
|
||
| private implicit class SchemaOps[A](schema: Schema[A]) { | ||
| def reifyHint[B](rp: RefinementProvider[B, A, ?]): Schema[A] = | ||
| schema.hints | ||
| .get(rp.tag) | ||
| .fold(schema)(schema.validated(_)(RefinementProvider.void(rp))) | ||
| } | ||
|
|
||
| private def collection[C[_], B]( | ||
| schema: Schema.CollectionSchema[C, B] | ||
| ): Schema[C[B]] = | ||
| schema.reifyHint( | ||
| RefinementProvider.lengthConstraint[C[B]](schema.tag.iterator(_).size) | ||
| ) | ||
|
|
||
| def apply[A](schema: Schema[A]): Schema[A] = | ||
| schema match { | ||
| case t @ PrimitiveSchema(_, _, tag) => | ||
| tag match { | ||
| case PString => | ||
| t.reifyHint(RefinementProvider.stringLengthConstraint) | ||
| .reifyHint(RefinementProvider.stringPatternConstraints) | ||
| .reifyHint(RefinementProvider.idRefRefinement) | ||
| case PByte => | ||
| schema.reifyHint(RefinementProvider.numericRangeConstraints[Byte]) | ||
| case PShort => | ||
| schema.reifyHint(RefinementProvider.numericRangeConstraints[Short]) | ||
| case PInt => | ||
| schema.reifyHint(RefinementProvider.numericRangeConstraints[Int]) | ||
| case PLong => | ||
| schema.reifyHint(RefinementProvider.numericRangeConstraints[Long]) | ||
| case PFloat => | ||
| schema.reifyHint(RefinementProvider.numericRangeConstraints[Float]) | ||
| case PDouble => | ||
| schema.reifyHint( | ||
| RefinementProvider.numericRangeConstraints[Double] | ||
| ) | ||
| case PBigInt => | ||
| schema.reifyHint( | ||
| RefinementProvider.numericRangeConstraints[BigInt] | ||
| ) | ||
| case PBigDecimal => | ||
| schema.reifyHint( | ||
| RefinementProvider.numericRangeConstraints[BigDecimal] | ||
| ) | ||
| case PBlob => | ||
| schema.reifyHint(RefinementProvider.blobLengthConstraint) | ||
| case PTimestamp | PDocument | PBoolean | PUUID | PLocalDate | | ||
| PLocalTime | PDuration | POffsetDateTime => | ||
| schema | ||
| } | ||
| case e: EnumerationSchema[_] => | ||
| val byValue = e.values.map(v => v.value -> v).toMap | ||
| schema | ||
| .reifyHint( | ||
| RefinementProvider.lengthConstraint[A]( | ||
| byValue(_).stringValue.length | ||
| ) | ||
| ) | ||
| .reifyHint( | ||
| RefinementProvider.rangeConstraint[A, Int](byValue(_).intValue) | ||
| ) | ||
| .reifyHint( | ||
| RefinementProvider.patternConstraint[A](byValue(_).stringValue) | ||
| ) | ||
| case c @ CollectionSchema(_, _, _, _) => collection(c) | ||
| case m: MapSchema[c, k, v] => | ||
| m.reifyHint( | ||
| RefinementProvider.lengthConstraint[c[k, v]](m.tag.iterator(_).size) | ||
| ) | ||
| // We need to unpack the underlying schema and apply all the hints turning them into refinements. | ||
| // This is because, for example in the container context we might have member level hints. | ||
| // For example, if we have a list that targets constrained type and adds another constraint on member level | ||
| // if we just retrieved a target member it would miss the member level constraint needed for the container case. | ||
| case r: RefinementSchema[_, _] => | ||
| // Only used in dynamic context, A is fixed to DynData = Any | ||
| apply(r.underlying).asInstanceOf[Schema[A]] | ||
| case b: BijectionSchema[_, _] => b | ||
| case s: StructSchema[_] => s | ||
| case l: LazySchema[_] => l | ||
| case u: UnionSchema[_] => u | ||
| case n: OptionSchema[_, _] => n | ||
| } | ||
|
|
||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We also have a platform-agnostic variant that works on our own model of
Model- let's addperformValidationto it too