mirror of https://github.com/smithy-lang/smithy-rs
Impl std::error::Error for a shape's ConstraintViolation struct (#3431)
## Motivation and Context Each constrained shape has an associated `ConstraintViolation` structure, which at the moment does not implement `std::error::Error`. ## Description All of the `ConstraintViolation` structures now implements `std::fmt::Display` and the `std::error::Error` marker trait. There is a difference between the message used for `pub(crate) fn as_validation_exception_field` and for `std::fmt::Display` as the latter does not know of the particular field which is being set in the structure. Hence, it uses the shape ID instead. For example, for a constrained `int`, the following is the message used in `Display`: ``` write!(f, "Value for `com.aws.example#MyNumberSensitive`failed to satisfy constraint: Member must be greater than or equal to 100") ``` and the following is used for `as_validation_exception_field`: ``` format!("Value at '{}' failed to satisfy constraint: Member must be greater than or equal to 100", &path), ``` ## Testing Tests have been added to ensure: 1. `std::fmt::Display` has all of the variants for the enum that represents the associated `Error` type in the `ConstraintViolation` struct. 2. `ConstraintViolation` implements `std::error::Error`. --------- Co-authored-by: Fahad Zubair <fahadzub@amazon.com>
This commit is contained in:
parent
1f5cb697d1
commit
42701d5b22
|
@ -41,3 +41,9 @@ message = "Fix an S3 crate's dependency on `ahash` so the crate can be compiled
|
|||
references = ["smithy-rs#3590", "aws-sdk-rust#1131"]
|
||||
meta = { "breaking" = false, "tada" = false, "bug" = true }
|
||||
author = "ysaito1001"
|
||||
|
||||
[[smithy-rs]]
|
||||
message = "Implement `std::error::Error` for `ConstraintViolation`"
|
||||
references = ["smithy-rs#3430"]
|
||||
meta = { "breaking" = false, "tada" = true, "bug" = false, "target" = "server" }
|
||||
author = "drganjoo"
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package software.amazon.smithy.rust.codegen.server.smithy
|
||||
|
||||
import software.amazon.smithy.model.shapes.Shape
|
||||
import software.amazon.smithy.model.traits.EnumTrait
|
||||
|
||||
fun EnumTrait.validationErrorMessage() =
|
||||
"Value at '{}' failed to satisfy constraint: Member must satisfy enum value set: [${enumValueSet()}]"
|
||||
|
||||
fun EnumTrait.shapeConstraintViolationDisplayMessage(shape: Shape) =
|
||||
"Value provided for '${shape.id}' failed to satisfy constraint: Member must satisfy enum value set: [${enumValueSet()}]"
|
||||
|
||||
fun EnumTrait.enumValueSet() = this.enumDefinitionValues.joinToString(", ")
|
|
@ -5,20 +5,21 @@
|
|||
|
||||
package software.amazon.smithy.rust.codegen.server.smithy
|
||||
|
||||
import software.amazon.smithy.model.shapes.Shape
|
||||
import software.amazon.smithy.model.traits.LengthTrait
|
||||
|
||||
fun LengthTrait.validationErrorMessage(): String {
|
||||
val beginning = "Value with length {} at '{}' failed to satisfy constraint: Member must have length "
|
||||
val ending =
|
||||
if (this.min.isPresent && this.max.isPresent) {
|
||||
"between ${this.min.get()} and ${this.max.get()}, inclusive"
|
||||
} else if (this.min.isPresent) {
|
||||
(
|
||||
"greater than or equal to ${this.min.get()}"
|
||||
)
|
||||
} else {
|
||||
check(this.max.isPresent)
|
||||
"less than or equal to ${this.max.get()}"
|
||||
}
|
||||
return "$beginning$ending"
|
||||
}
|
||||
fun LengthTrait.validationErrorMessage() =
|
||||
"Value with length {} at '{}' failed to satisfy constraint: Member must have length ${this.lengthDescription()}"
|
||||
|
||||
fun LengthTrait.shapeConstraintViolationDisplayMessage(shape: Shape) =
|
||||
"Value with length {} provided for '${shape.id}' failed to satisfy constraint: Member must have length ${this.lengthDescription()}"
|
||||
|
||||
fun LengthTrait.lengthDescription() =
|
||||
if (this.min.isPresent && this.max.isPresent) {
|
||||
"between ${this.min.get()} and ${this.max.get()}, inclusive"
|
||||
} else if (this.min.isPresent) {
|
||||
"greater than or equal to ${this.min.get()}"
|
||||
} else {
|
||||
check(this.max.isPresent)
|
||||
"less than or equal to ${this.max.get()}"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package software.amazon.smithy.rust.codegen.server.smithy
|
||||
|
||||
import software.amazon.smithy.model.shapes.Shape
|
||||
import software.amazon.smithy.model.traits.PatternTrait
|
||||
|
||||
fun PatternTrait.validationErrorMessage() =
|
||||
"Value at '{}' failed to satisfy constraint: Member must satisfy regular expression pattern: {}"
|
||||
|
||||
fun PatternTrait.shapeConstraintViolationDisplayMessage(shape: Shape) =
|
||||
"Value provided for `${shape.id}` failed to satisfy the constraint: Member must match the regular expression pattern: {}"
|
|
@ -5,20 +5,21 @@
|
|||
|
||||
package software.amazon.smithy.rust.codegen.server.smithy
|
||||
|
||||
import software.amazon.smithy.model.shapes.Shape
|
||||
import software.amazon.smithy.model.traits.RangeTrait
|
||||
|
||||
fun RangeTrait.validationErrorMessage(): String {
|
||||
val beginning = "Value at '{}' failed to satisfy constraint: Member must be "
|
||||
val ending =
|
||||
if (this.min.isPresent && this.max.isPresent) {
|
||||
"between ${this.min.get()} and ${this.max.get()}, inclusive"
|
||||
} else if (this.min.isPresent) {
|
||||
(
|
||||
"greater than or equal to ${this.min.get()}"
|
||||
)
|
||||
} else {
|
||||
check(this.max.isPresent)
|
||||
"less than or equal to ${this.max.get()}"
|
||||
}
|
||||
return "$beginning$ending"
|
||||
}
|
||||
fun RangeTrait.validationErrorMessage() =
|
||||
"Value at '{}' failed to satisfy constraint: Member must be ${this.rangeDescription()}"
|
||||
|
||||
fun RangeTrait.shapeConstraintViolationDisplayMessage(shape: Shape) =
|
||||
"Value for `${shape.id}`failed to satisfy constraint: Member must be ${this.rangeDescription()}"
|
||||
|
||||
fun RangeTrait.rangeDescription() =
|
||||
if (this.min.isPresent && this.max.isPresent) {
|
||||
"between ${this.min.get()} and ${this.max.get()}, inclusive"
|
||||
} else if (this.min.isPresent) {
|
||||
"greater than or equal to ${this.min.get()}"
|
||||
} else {
|
||||
check(this.max.isPresent)
|
||||
"less than or equal to ${this.max.get()}"
|
||||
}
|
||||
|
|
|
@ -5,9 +5,16 @@
|
|||
|
||||
package software.amazon.smithy.rust.codegen.server.smithy
|
||||
|
||||
import software.amazon.smithy.model.shapes.Shape
|
||||
import software.amazon.smithy.model.traits.UniqueItemsTrait
|
||||
|
||||
fun UniqueItemsTrait.validationErrorMessage() =
|
||||
// We're using the `Debug` representation of `Vec<usize>` here e.g. `[0, 2, 3]`, which is the exact format we need
|
||||
// to match the expected format of the error message in the protocol tests.
|
||||
"Value with repeated values at indices {:?} at '{}' failed to satisfy constraint: Member must have unique values"
|
||||
|
||||
fun UniqueItemsTrait.shapeConstraintViolationDisplayMessage(shape: Shape) =
|
||||
"""
|
||||
Value with repeated values at indices {:?} provided for '${shape.id}'
|
||||
failed to satisfy constraint: Member must have unique values
|
||||
""".trimIndent().replace("\n", "")
|
||||
|
|
|
@ -161,8 +161,7 @@ class SmithyValidationExceptionConversionGenerator(private val codegenContext: S
|
|||
|
||||
override fun enumShapeConstraintViolationImplBlock(enumTrait: EnumTrait) =
|
||||
writable {
|
||||
val enumValueSet = enumTrait.enumDefinitionValues.joinToString(", ")
|
||||
val message = "Value at '{}' failed to satisfy constraint: Member must satisfy enum value set: [$enumValueSet]"
|
||||
val message = enumTrait.validationErrorMessage()
|
||||
rustTemplate(
|
||||
"""
|
||||
pub(crate) fn as_validation_exception_field(self, path: #{String}) -> crate::model::ValidationExceptionField {
|
||||
|
|
|
@ -9,6 +9,8 @@ import software.amazon.smithy.model.shapes.CollectionShape
|
|||
import software.amazon.smithy.rust.codegen.core.rustlang.Visibility
|
||||
import software.amazon.smithy.rust.codegen.core.rustlang.join
|
||||
import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate
|
||||
import software.amazon.smithy.rust.codegen.core.rustlang.writable
|
||||
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType
|
||||
import software.amazon.smithy.rust.codegen.core.smithy.makeRustBoxed
|
||||
import software.amazon.smithy.rust.codegen.core.util.hasTrait
|
||||
import software.amazon.smithy.rust.codegen.core.util.letIf
|
||||
|
@ -80,8 +82,22 @@ class CollectionConstraintViolationGenerator(
|
|||
${constraintViolationVisibility.toRustQualifier()} enum $constraintViolationName {
|
||||
#{ConstraintViolationVariants:W}
|
||||
}
|
||||
|
||||
impl #{Display} for $constraintViolationName {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let message = match self {
|
||||
#{VariantDisplayMessages:W}
|
||||
};
|
||||
write!(f, "{message}")
|
||||
}
|
||||
}
|
||||
|
||||
impl #{Error} for $constraintViolationName {}
|
||||
""",
|
||||
"ConstraintViolationVariants" to constraintViolationVariants.join(",\n"),
|
||||
"Error" to RuntimeType.StdError,
|
||||
"Display" to RuntimeType.Display,
|
||||
"VariantDisplayMessages" to generateDisplayMessageForEachVariant(shape.isReachableFromOperationInput() && isMemberConstrained),
|
||||
)
|
||||
|
||||
if (shape.isReachableFromOperationInput()) {
|
||||
|
@ -96,4 +112,20 @@ class CollectionConstraintViolationGenerator(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateDisplayMessageForEachVariant(memberConstraintVariantPresent: Boolean) =
|
||||
writable {
|
||||
for (constraintsInfo in collectionConstraintsInfo) {
|
||||
constraintsInfo.shapeConstraintViolationDisplayMessage(shape).invoke(this)
|
||||
}
|
||||
|
||||
if (memberConstraintVariantPresent) {
|
||||
rustTemplate(
|
||||
"""
|
||||
Self::Member(index, failing_member) => format!("Value at index {index} failed to satisfy constraint. {}",
|
||||
failing_member)
|
||||
""",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ package software.amazon.smithy.rust.codegen.server.smithy.generators
|
|||
|
||||
import software.amazon.smithy.codegen.core.Symbol
|
||||
import software.amazon.smithy.model.shapes.BlobShape
|
||||
import software.amazon.smithy.model.shapes.Shape
|
||||
import software.amazon.smithy.model.traits.LengthTrait
|
||||
import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter
|
||||
import software.amazon.smithy.rust.codegen.core.rustlang.Visibility
|
||||
|
@ -18,6 +19,7 @@ import software.amazon.smithy.rust.codegen.core.rustlang.render
|
|||
import software.amazon.smithy.rust.codegen.core.rustlang.rust
|
||||
import software.amazon.smithy.rust.codegen.core.rustlang.rustBlock
|
||||
import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate
|
||||
import software.amazon.smithy.rust.codegen.core.rustlang.writable
|
||||
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType
|
||||
import software.amazon.smithy.rust.codegen.core.smithy.expectRustMetadata
|
||||
import software.amazon.smithy.rust.codegen.core.smithy.makeMaybeConstrained
|
||||
|
@ -26,6 +28,7 @@ import software.amazon.smithy.rust.codegen.core.util.orNull
|
|||
import software.amazon.smithy.rust.codegen.server.smithy.InlineModuleCreator
|
||||
import software.amazon.smithy.rust.codegen.server.smithy.PubCrateConstraintViolationSymbolProvider
|
||||
import software.amazon.smithy.rust.codegen.server.smithy.ServerCodegenContext
|
||||
import software.amazon.smithy.rust.codegen.server.smithy.shapeConstraintViolationDisplayMessage
|
||||
import software.amazon.smithy.rust.codegen.server.smithy.traits.isReachableFromOperationInput
|
||||
import software.amazon.smithy.rust.codegen.server.smithy.validationErrorMessage
|
||||
|
||||
|
@ -128,8 +131,22 @@ class ConstrainedBlobGenerator(
|
|||
pub enum ${constraintViolation.name} {
|
||||
#{Variants:W}
|
||||
}
|
||||
|
||||
impl #{Display} for ${constraintViolation.name} {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let message = match self {
|
||||
#{VariantDisplayMessages:W}
|
||||
};
|
||||
write!(f, "{message}")
|
||||
}
|
||||
}
|
||||
|
||||
impl #{Error} for ${constraintViolation.name} {}
|
||||
""",
|
||||
"Variants" to constraintsInfo.map { it.constraintViolationVariant }.join(",\n"),
|
||||
"Error" to RuntimeType.StdError,
|
||||
"Display" to RuntimeType.Display,
|
||||
"VariantDisplayMessages" to generateDisplayMessageForEachVariant(),
|
||||
)
|
||||
|
||||
if (shape.isReachableFromOperationInput()) {
|
||||
|
@ -143,9 +160,22 @@ class ConstrainedBlobGenerator(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateDisplayMessageForEachVariant() =
|
||||
writable {
|
||||
blobConstraintsInfo.forEach {
|
||||
it.shapeConstraintViolationDisplayMessage(shape).invoke(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class BlobLength(val lengthTrait: LengthTrait) {
|
||||
// Each type of constraint that can be put on a Blob must implement the BlobConstraintGenerator
|
||||
// interface. This allows the
|
||||
interface BlobConstraintGenerator {
|
||||
fun shapeConstraintViolationDisplayMessage(shape: Shape): Writable
|
||||
}
|
||||
|
||||
data class BlobLength(val lengthTrait: LengthTrait) : BlobConstraintGenerator {
|
||||
fun toTraitInfo(): TraitInfo =
|
||||
TraitInfo(
|
||||
{ rust("Self::check_length(&value)?;") },
|
||||
|
@ -159,7 +189,8 @@ data class BlobLength(val lengthTrait: LengthTrait) {
|
|||
Self::Length(length) => crate::model::ValidationExceptionField {
|
||||
message: format!("${lengthTrait.validationErrorMessage()}", length, &path),
|
||||
path,
|
||||
},""",
|
||||
},
|
||||
""",
|
||||
)
|
||||
},
|
||||
this::renderValidationFunction,
|
||||
|
@ -188,4 +219,15 @@ data class BlobLength(val lengthTrait: LengthTrait) {
|
|||
""",
|
||||
)
|
||||
}
|
||||
|
||||
override fun shapeConstraintViolationDisplayMessage(shape: Shape) =
|
||||
writable {
|
||||
rustTemplate(
|
||||
"""
|
||||
Self::Length(length) => {
|
||||
format!("${lengthTrait.shapeConstraintViolationDisplayMessage(shape).replace("#", "##")}", length)
|
||||
},
|
||||
""",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import software.amazon.smithy.codegen.core.Symbol
|
|||
import software.amazon.smithy.codegen.core.SymbolProvider
|
||||
import software.amazon.smithy.model.shapes.CollectionShape
|
||||
import software.amazon.smithy.model.shapes.EnumShape
|
||||
import software.amazon.smithy.model.shapes.Shape
|
||||
import software.amazon.smithy.model.shapes.StructureShape
|
||||
import software.amazon.smithy.model.shapes.UnionShape
|
||||
import software.amazon.smithy.model.traits.LengthTrait
|
||||
|
@ -16,19 +17,23 @@ import software.amazon.smithy.model.traits.Trait
|
|||
import software.amazon.smithy.model.traits.UniqueItemsTrait
|
||||
import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter
|
||||
import software.amazon.smithy.rust.codegen.core.rustlang.Visibility
|
||||
import software.amazon.smithy.rust.codegen.core.rustlang.Writable
|
||||
import software.amazon.smithy.rust.codegen.core.rustlang.docs
|
||||
import software.amazon.smithy.rust.codegen.core.rustlang.documentShape
|
||||
import software.amazon.smithy.rust.codegen.core.rustlang.join
|
||||
import software.amazon.smithy.rust.codegen.core.rustlang.rust
|
||||
import software.amazon.smithy.rust.codegen.core.rustlang.rustBlock
|
||||
import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate
|
||||
import software.amazon.smithy.rust.codegen.core.rustlang.writable
|
||||
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType
|
||||
import software.amazon.smithy.rust.codegen.core.smithy.expectRustMetadata
|
||||
import software.amazon.smithy.rust.codegen.core.util.PANIC
|
||||
import software.amazon.smithy.rust.codegen.core.util.hasTrait
|
||||
import software.amazon.smithy.rust.codegen.core.util.orNull
|
||||
import software.amazon.smithy.rust.codegen.server.smithy.PubCrateConstraintViolationSymbolProvider
|
||||
import software.amazon.smithy.rust.codegen.server.smithy.ServerCodegenContext
|
||||
import software.amazon.smithy.rust.codegen.server.smithy.canReachConstrainedShape
|
||||
import software.amazon.smithy.rust.codegen.server.smithy.shapeConstraintViolationDisplayMessage
|
||||
import software.amazon.smithy.rust.codegen.server.smithy.supportedCollectionConstraintTraits
|
||||
import software.amazon.smithy.rust.codegen.server.smithy.validationErrorMessage
|
||||
|
||||
|
@ -315,6 +320,16 @@ sealed class CollectionTraitInfo {
|
|||
}
|
||||
},
|
||||
)
|
||||
|
||||
override fun shapeConstraintViolationDisplayMessage(shape: Shape) =
|
||||
writable {
|
||||
rustTemplate(
|
||||
"""
|
||||
Self::UniqueItems { duplicate_indices, .. } =>
|
||||
format!("${uniqueItemsTrait.shapeConstraintViolationDisplayMessage(shape).replace("#", "##")}", &duplicate_indices),
|
||||
""",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class Length(val lengthTrait: LengthTrait) : CollectionTraitInfo() {
|
||||
|
@ -354,6 +369,17 @@ sealed class CollectionTraitInfo {
|
|||
}
|
||||
},
|
||||
)
|
||||
|
||||
override fun shapeConstraintViolationDisplayMessage(shape: Shape) =
|
||||
writable {
|
||||
rustTemplate(
|
||||
"""
|
||||
Self::Length(length) => {
|
||||
format!("${lengthTrait.shapeConstraintViolationDisplayMessage(shape).replace("#", "##")}", length)
|
||||
},
|
||||
""",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -386,4 +412,6 @@ sealed class CollectionTraitInfo {
|
|||
}
|
||||
|
||||
abstract fun toTraitInfo(): TraitInfo
|
||||
|
||||
abstract fun shapeConstraintViolationDisplayMessage(shape: Shape): Writable
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ import software.amazon.smithy.rust.codegen.core.util.redactIfNecessary
|
|||
import software.amazon.smithy.rust.codegen.server.smithy.InlineModuleCreator
|
||||
import software.amazon.smithy.rust.codegen.server.smithy.PubCrateConstraintViolationSymbolProvider
|
||||
import software.amazon.smithy.rust.codegen.server.smithy.ServerCodegenContext
|
||||
import software.amazon.smithy.rust.codegen.server.smithy.shapeConstraintViolationDisplayMessage
|
||||
import software.amazon.smithy.rust.codegen.server.smithy.traits.isReachableFromOperationInput
|
||||
import software.amazon.smithy.rust.codegen.server.smithy.validationErrorMessage
|
||||
|
||||
|
@ -135,13 +136,23 @@ class ConstrainedNumberGenerator(
|
|||
writer.renderTryFrom(unconstrainedTypeName, name, constraintViolation, constraintsInfo)
|
||||
|
||||
inlineModuleCreator(constraintViolation) {
|
||||
rust(
|
||||
rustTemplate(
|
||||
"""
|
||||
##[derive(Debug, PartialEq)]
|
||||
pub enum ${constraintViolation.name} {
|
||||
Range($unconstrainedTypeName),
|
||||
}
|
||||
|
||||
impl #{Display} for ${constraintViolation.name} {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "${rangeInfo.rangeTrait.shapeConstraintViolationDisplayMessage(shape).replace("#", "##")}")
|
||||
}
|
||||
}
|
||||
|
||||
impl #{Error} for ${constraintViolation.name} {}
|
||||
""",
|
||||
"Error" to RuntimeType.StdError,
|
||||
"Display" to RuntimeType.Display,
|
||||
)
|
||||
|
||||
if (shape.isReachableFromOperationInput()) {
|
||||
|
|
|
@ -37,6 +37,7 @@ import software.amazon.smithy.rust.codegen.server.smithy.InlineModuleCreator
|
|||
import software.amazon.smithy.rust.codegen.server.smithy.PubCrateConstraintViolationSymbolProvider
|
||||
import software.amazon.smithy.rust.codegen.server.smithy.ServerCargoDependency
|
||||
import software.amazon.smithy.rust.codegen.server.smithy.ServerCodegenContext
|
||||
import software.amazon.smithy.rust.codegen.server.smithy.shapeConstraintViolationDisplayMessage
|
||||
import software.amazon.smithy.rust.codegen.server.smithy.supportedStringConstraintTraits
|
||||
import software.amazon.smithy.rust.codegen.server.smithy.traits.isReachableFromOperationInput
|
||||
import software.amazon.smithy.rust.codegen.server.smithy.validationErrorMessage
|
||||
|
@ -126,7 +127,6 @@ class ConstrainedStringGenerator(
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
impl #{From}<$name> for $inner {
|
||||
fn from(value: $name) -> Self {
|
||||
value.into_inner()
|
||||
|
@ -158,8 +158,22 @@ class ConstrainedStringGenerator(
|
|||
pub enum ${constraintViolation.name} {
|
||||
#{Variants:W}
|
||||
}
|
||||
|
||||
impl #{Display} for ${constraintViolation.name} {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let message = match self {
|
||||
#{VariantDisplayMessages:W}
|
||||
};
|
||||
write!(f, "{message}")
|
||||
}
|
||||
}
|
||||
|
||||
impl #{Error} for ${constraintViolation.name} {}
|
||||
""",
|
||||
"Variants" to constraintsInfo.map { it.constraintViolationVariant }.join(",\n"),
|
||||
"Error" to RuntimeType.StdError,
|
||||
"Display" to RuntimeType.Display,
|
||||
"VariantDisplayMessages" to generateDisplayMessageForEachVariant(),
|
||||
)
|
||||
|
||||
if (shape.isReachableFromOperationInput()) {
|
||||
|
@ -174,6 +188,13 @@ class ConstrainedStringGenerator(
|
|||
}
|
||||
}
|
||||
|
||||
private fun generateDisplayMessageForEachVariant() =
|
||||
writable {
|
||||
stringConstraintsInfo.forEach {
|
||||
it.shapeConstraintViolationDisplayMessage(shape).invoke(this)
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderTests(shape: Shape) {
|
||||
val testCases = TraitInfo.testCases(constraintsInfo)
|
||||
|
||||
|
@ -239,6 +260,17 @@ data class Length(val lengthTrait: LengthTrait) : StringTraitInfo() {
|
|||
""",
|
||||
)
|
||||
}
|
||||
|
||||
override fun shapeConstraintViolationDisplayMessage(shape: Shape) =
|
||||
writable {
|
||||
rustTemplate(
|
||||
"""
|
||||
Self::Length(length) => {
|
||||
format!("${lengthTrait.shapeConstraintViolationDisplayMessage(shape).replace("#", "##")}", length)
|
||||
},
|
||||
""",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class Pattern(val symbol: Symbol, val patternTrait: PatternTrait, val isSensitive: Boolean) : StringTraitInfo() {
|
||||
|
@ -278,12 +310,11 @@ data class Pattern(val symbol: Symbol, val patternTrait: PatternTrait, val isSen
|
|||
}
|
||||
|
||||
fun errorMessage(): Writable {
|
||||
val pattern = patternTrait.pattern
|
||||
|
||||
return writable {
|
||||
val pattern = patternTrait.pattern.toString().replace("#", "##")
|
||||
rust(
|
||||
"""
|
||||
format!("Value at '{}' failed to satisfy constraint: Member must satisfy regular expression pattern: {}", &path, r##"$pattern"##)
|
||||
format!("${patternTrait.validationErrorMessage()}", &path, r##"$pattern"##)
|
||||
""",
|
||||
)
|
||||
}
|
||||
|
@ -297,7 +328,7 @@ data class Pattern(val symbol: Symbol, val patternTrait: PatternTrait, val isSen
|
|||
constraintViolation: Symbol,
|
||||
unconstrainedTypeName: String,
|
||||
): Writable {
|
||||
val pattern = patternTrait.pattern
|
||||
val pattern = patternTrait.pattern.toString().replace("#", "##")
|
||||
val errorMessageForUnsupportedRegex =
|
||||
"""The regular expression $pattern is not supported by the `regex` crate; feel free to file an issue under https://github.com/smithy-lang/smithy-rs/issues for support"""
|
||||
|
||||
|
@ -327,6 +358,19 @@ data class Pattern(val symbol: Symbol, val patternTrait: PatternTrait, val isSen
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun shapeConstraintViolationDisplayMessage(shape: Shape) =
|
||||
writable {
|
||||
val errorMessage = patternTrait.shapeConstraintViolationDisplayMessage(shape).replace("#", "##")
|
||||
val pattern = patternTrait.pattern.toString().replace("#", "##")
|
||||
rustTemplate(
|
||||
"""
|
||||
Self::Pattern(_) => {
|
||||
format!(r##"$errorMessage"##, r##"$pattern"##)
|
||||
},
|
||||
""",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
sealed class StringTraitInfo {
|
||||
|
@ -349,4 +393,6 @@ sealed class StringTraitInfo {
|
|||
}
|
||||
|
||||
abstract fun toTraitInfo(): TraitInfo
|
||||
|
||||
abstract fun shapeConstraintViolationDisplayMessage(shape: Shape): Writable
|
||||
}
|
||||
|
|
|
@ -10,12 +10,16 @@ import software.amazon.smithy.model.shapes.StringShape
|
|||
import software.amazon.smithy.model.traits.LengthTrait
|
||||
import software.amazon.smithy.rust.codegen.core.rustlang.Visibility
|
||||
import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate
|
||||
import software.amazon.smithy.rust.codegen.core.rustlang.writable
|
||||
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType
|
||||
import software.amazon.smithy.rust.codegen.core.smithy.makeRustBoxed
|
||||
import software.amazon.smithy.rust.codegen.core.util.getTrait
|
||||
import software.amazon.smithy.rust.codegen.core.util.hasTrait
|
||||
import software.amazon.smithy.rust.codegen.core.util.letIf
|
||||
import software.amazon.smithy.rust.codegen.server.smithy.InlineModuleCreator
|
||||
import software.amazon.smithy.rust.codegen.server.smithy.PubCrateConstraintViolationSymbolProvider
|
||||
import software.amazon.smithy.rust.codegen.server.smithy.ServerCodegenContext
|
||||
import software.amazon.smithy.rust.codegen.server.smithy.shapeConstraintViolationDisplayMessage
|
||||
import software.amazon.smithy.rust.codegen.server.smithy.traits.ConstraintViolationRustBoxTrait
|
||||
import software.amazon.smithy.rust.codegen.server.smithy.traits.isReachableFromOperationInput
|
||||
|
||||
|
@ -84,8 +88,23 @@ class MapConstraintViolationGenerator(
|
|||
${if (keyConstraintViolationExists) "##[doc(hidden)] Key(#{KeyConstraintViolationSymbol})," else ""}
|
||||
${if (valueConstraintViolationExists) "##[doc(hidden)] Value(#{KeySymbol}, #{ValueConstraintViolationSymbol})," else ""}
|
||||
}
|
||||
|
||||
impl #{Display} for $constraintViolationName {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
${if (shape.hasTrait<LengthTrait>()) "#{LengthMatchingArm}" else ""}
|
||||
${if (keyConstraintViolationExists) """Self::Key(key_constraint_violation) => write!(f, "{}", key_constraint_violation),""" else ""}
|
||||
${if (valueConstraintViolationExists) """Self::Value(_, value_constraint_violation) => write!(f, "{}", value_constraint_violation),""" else ""}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl #{Error} for $constraintViolationName {}
|
||||
""",
|
||||
*constraintViolationCodegenScope,
|
||||
"LengthMatchingArm" to lengthMatchingArm(),
|
||||
"Error" to RuntimeType.StdError,
|
||||
"Display" to RuntimeType.Display,
|
||||
)
|
||||
|
||||
if (shape.isReachableFromOperationInput()) {
|
||||
|
@ -107,4 +126,17 @@ class MapConstraintViolationGenerator(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun lengthMatchingArm() =
|
||||
writable {
|
||||
shape.getTrait<LengthTrait>()?.let {
|
||||
rustTemplate(
|
||||
"""
|
||||
Self::Length(length) => {
|
||||
write!(f, "${it.shapeConstraintViolationDisplayMessage(shape).replace("#", "##")}", length)
|
||||
},
|
||||
""",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ import software.amazon.smithy.rust.codegen.core.smithy.module
|
|||
import software.amazon.smithy.rust.codegen.core.util.dq
|
||||
import software.amazon.smithy.rust.codegen.server.smithy.PubCrateConstraintViolationSymbolProvider
|
||||
import software.amazon.smithy.rust.codegen.server.smithy.ServerCodegenContext
|
||||
import software.amazon.smithy.rust.codegen.server.smithy.shapeConstraintViolationDisplayMessage
|
||||
import software.amazon.smithy.rust.codegen.server.smithy.traits.isReachableFromOperationInput
|
||||
|
||||
open class ConstrainedEnum(
|
||||
|
@ -50,8 +51,18 @@ open class ConstrainedEnum(
|
|||
"""
|
||||
##[derive(Debug, PartialEq)]
|
||||
pub struct $constraintViolationName(pub(crate) #{String});
|
||||
|
||||
impl #{Display} for $constraintViolationName {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, r##"${context.enumTrait.shapeConstraintViolationDisplayMessage(shape).replace("#", "##")}"##)
|
||||
}
|
||||
}
|
||||
|
||||
impl #{Error} for $constraintViolationName {}
|
||||
""",
|
||||
*codegenScope,
|
||||
"Error" to RuntimeType.StdError,
|
||||
"Display" to RuntimeType.Display,
|
||||
)
|
||||
|
||||
if (shape.isReachableFromOperationInput()) {
|
||||
|
|
|
@ -154,6 +154,23 @@ class UnconstrainedUnionGenerator(
|
|||
constraintViolations().forEach { renderConstraintViolation(this, it) }
|
||||
}
|
||||
|
||||
rustTemplate(
|
||||
"""
|
||||
impl #{Display} for $constraintViolationName {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
#{ConstraintVariants:W}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl #{Error} for $constraintViolationName {}
|
||||
""",
|
||||
"Error" to RuntimeType.StdError,
|
||||
"Display" to RuntimeType.Display,
|
||||
"ConstraintVariants" to generateDisplayMessageForEachVariant(),
|
||||
)
|
||||
|
||||
if (shape.isReachableFromOperationInput()) {
|
||||
rustBlock("impl $constraintViolationName") {
|
||||
rustBlockTemplate(
|
||||
|
@ -171,6 +188,17 @@ class UnconstrainedUnionGenerator(
|
|||
}
|
||||
}
|
||||
|
||||
private fun generateDisplayMessageForEachVariant() =
|
||||
writable {
|
||||
constraintViolations().forEach {
|
||||
rustTemplate(
|
||||
"""
|
||||
Self::${it.name()}(inner) => write!(f, "{inner}"),
|
||||
""",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class ConstraintViolation(val forMember: MemberShape) {
|
||||
fun name() = forMember.memberName.toPascalCase()
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ import software.amazon.smithy.model.node.ArrayNode
|
|||
import software.amazon.smithy.model.shapes.CollectionShape
|
||||
import software.amazon.smithy.model.shapes.ListShape
|
||||
import software.amazon.smithy.model.shapes.SetShape
|
||||
import software.amazon.smithy.model.shapes.StringShape
|
||||
import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter
|
||||
import software.amazon.smithy.rust.codegen.core.rustlang.Writable
|
||||
import software.amazon.smithy.rust.codegen.core.rustlang.rust
|
||||
|
@ -25,6 +26,7 @@ import software.amazon.smithy.rust.codegen.core.rustlang.rustBlock
|
|||
import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate
|
||||
import software.amazon.smithy.rust.codegen.core.rustlang.withBlock
|
||||
import software.amazon.smithy.rust.codegen.core.rustlang.writable
|
||||
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType
|
||||
import software.amazon.smithy.rust.codegen.core.testutil.TestWorkspace
|
||||
import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel
|
||||
import software.amazon.smithy.rust.codegen.core.testutil.compileAndTest
|
||||
|
@ -177,6 +179,10 @@ class ConstrainedCollectionGeneratorTest {
|
|||
val codegenContext = serverTestCodegenContext(testCase.model)
|
||||
|
||||
val project = TestWorkspace.testProject(codegenContext.symbolProvider)
|
||||
project.withModule(ServerRustModule.Model) {
|
||||
TestUtility.generateIsDisplay().invoke(this)
|
||||
TestUtility.generateIsError().invoke(this)
|
||||
}
|
||||
|
||||
for (shape in listOf(constrainedListShape, constrainedSetShape)) {
|
||||
val shapeName =
|
||||
|
@ -256,7 +262,15 @@ class ConstrainedCollectionGeneratorTest {
|
|||
withBlock("let expected_err = ", ";") {
|
||||
rustTemplate("#{ExpectedError:W}", "ExpectedError" to expectedErrorWritable)
|
||||
}
|
||||
rust("assert_eq!(err, expected_err);")
|
||||
rust(
|
||||
"""
|
||||
assert_eq!(err, expected_err);
|
||||
is_error(&err);
|
||||
is_display(&err);
|
||||
// Ensure that the `std::fmt::Display` implementation for `ConstraintViolation` error works.
|
||||
assert_eq!(err.to_string(), expected_err.to_string());
|
||||
""".trimMargin(),
|
||||
)
|
||||
} ?: run {
|
||||
rust("constrained_res.unwrap_err();")
|
||||
}
|
||||
|
@ -291,6 +305,110 @@ class ConstrainedCollectionGeneratorTest {
|
|||
writer.toString() shouldContain "pub struct ConstrainedList(pub(crate) ::std::vec::Vec<::std::string::String>);"
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `error trait implemented for ConstraintViolation should work for constrained member`() {
|
||||
val model =
|
||||
"""
|
||||
${'$'}version: "1.0"
|
||||
|
||||
namespace test
|
||||
|
||||
use aws.protocols#restJson1
|
||||
use smithy.framework#ValidationException
|
||||
|
||||
// The `ConstraintViolation` code generated for a constrained map that is not reachable from an
|
||||
// operation does not have the `Key`, or `Value` variants. Hence, we need to define a service
|
||||
// and an operation that uses the constrained map.
|
||||
@restJson1
|
||||
service MyService {
|
||||
version: "2023-04-01",
|
||||
operations: [
|
||||
MyOperation,
|
||||
]
|
||||
}
|
||||
|
||||
@http(method: "POST", uri: "/echo")
|
||||
operation MyOperation {
|
||||
input: MyOperationInput
|
||||
errors : [ValidationException]
|
||||
}
|
||||
|
||||
@input
|
||||
structure MyOperationInput {
|
||||
member1: ConstrainedList,
|
||||
member2: ConstrainedSet,
|
||||
}
|
||||
|
||||
@length(min: 2, max: 69)
|
||||
list ConstrainedList {
|
||||
member: ConstrainedString
|
||||
}
|
||||
|
||||
@length(min: 2, max: 69)
|
||||
set ConstrainedSet {
|
||||
member: ConstrainedString
|
||||
}
|
||||
|
||||
@pattern("#\\d+")
|
||||
string ConstrainedString
|
||||
""".asSmithyModel().let(ShapesReachableFromOperationInputTagger::transform)
|
||||
|
||||
val codegenContext = serverTestCodegenContext(model)
|
||||
val symbolProvider = codegenContext.symbolProvider
|
||||
val project = TestWorkspace.testProject(symbolProvider)
|
||||
|
||||
project.withModule(ServerRustModule.Model) {
|
||||
TestUtility.generateIsDisplay().invoke(this)
|
||||
TestUtility.generateIsError().invoke(this)
|
||||
TestUtility.renderConstrainedString(
|
||||
codegenContext, this,
|
||||
model.lookup<StringShape>("test#ConstrainedString"),
|
||||
)
|
||||
|
||||
rustTemplate(
|
||||
"""
|
||||
// Define `ValidationExceptionField` since it is required by the `ConstraintViolation` code for constrained maps,
|
||||
// and the complete SDK generation process, which would generate it, is not invoked as part of the test.
|
||||
pub struct ValidationExceptionField {
|
||||
pub message: String,
|
||||
pub path: String
|
||||
}
|
||||
""",
|
||||
"Result" to RuntimeType.std.resolve("Result"),
|
||||
)
|
||||
|
||||
val constrainedListShape = model.lookup<ListShape>("test#ConstrainedList")
|
||||
val constrainedSetShape = model.lookup<ListShape>("test#ConstrainedSet")
|
||||
render(codegenContext, this, constrainedListShape)
|
||||
render(codegenContext, this, constrainedSetShape)
|
||||
|
||||
unitTest(
|
||||
name = "try_from_fail_invalid_constrained_list",
|
||||
test = """
|
||||
let constrained_error = ConstrainedString::try_from("one".to_string()).unwrap_err();
|
||||
let error = crate::model::constrained_list::ConstraintViolation::Member(0, constrained_error);
|
||||
is_error(&error);
|
||||
is_display(&error);
|
||||
assert_eq!("Value at index 0 failed to satisfy constraint. Value provided for `test#ConstrainedString` failed to satisfy the constraint: Member must match the regular expression pattern: #\\d+",
|
||||
error.to_string());
|
||||
""",
|
||||
)
|
||||
unitTest(
|
||||
name = "try_from_fail_invalid_constrained_set",
|
||||
test = """
|
||||
let constrained_error = ConstrainedString::try_from("one".to_string()).unwrap_err();
|
||||
let error = crate::model::constrained_set::ConstraintViolation::Member(0, constrained_error);
|
||||
is_error(&error);
|
||||
is_display(&error);
|
||||
assert_eq!("Value at index 0 failed to satisfy constraint. Value provided for `test#ConstrainedString` failed to satisfy the constraint: Member must match the regular expression pattern: #\\d+",
|
||||
error.to_string());
|
||||
""",
|
||||
)
|
||||
}
|
||||
|
||||
project.compileAndTest()
|
||||
}
|
||||
|
||||
private fun render(
|
||||
codegenContext: ServerCodegenContext,
|
||||
writer: RustWriter,
|
||||
|
|
|
@ -15,13 +15,17 @@ import org.junit.jupiter.params.provider.ArgumentsSource
|
|||
import software.amazon.smithy.model.Model
|
||||
import software.amazon.smithy.model.node.ObjectNode
|
||||
import software.amazon.smithy.model.shapes.MapShape
|
||||
import software.amazon.smithy.model.shapes.StringShape
|
||||
import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter
|
||||
import software.amazon.smithy.rust.codegen.core.rustlang.rustBlock
|
||||
import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate
|
||||
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType
|
||||
import software.amazon.smithy.rust.codegen.core.testutil.TestWorkspace
|
||||
import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel
|
||||
import software.amazon.smithy.rust.codegen.core.testutil.compileAndTest
|
||||
import software.amazon.smithy.rust.codegen.core.testutil.unitTest
|
||||
import software.amazon.smithy.rust.codegen.core.util.lookup
|
||||
import software.amazon.smithy.rust.codegen.core.util.toSnakeCase
|
||||
import software.amazon.smithy.rust.codegen.server.smithy.ServerCodegenContext
|
||||
import software.amazon.smithy.rust.codegen.server.smithy.ServerRustModule
|
||||
import software.amazon.smithy.rust.codegen.server.smithy.createTestInlineModuleCreator
|
||||
|
@ -79,6 +83,9 @@ class ConstrainedMapGeneratorTest {
|
|||
val project = TestWorkspace.testProject(symbolProvider)
|
||||
|
||||
project.withModule(ServerRustModule.Model) {
|
||||
TestUtility.generateIsDisplay().invoke(this)
|
||||
TestUtility.generateIsError().invoke(this)
|
||||
|
||||
render(codegenContext, this, constrainedMapShape)
|
||||
|
||||
val instantiator = ServerInstantiator(codegenContext)
|
||||
|
@ -101,7 +108,10 @@ class ConstrainedMapGeneratorTest {
|
|||
test = """
|
||||
let map = build_invalid_map();
|
||||
let constrained_res: Result<ConstrainedMap, _> = map.try_into();
|
||||
constrained_res.unwrap_err();
|
||||
let error = constrained_res.unwrap_err();
|
||||
|
||||
is_display(&error);
|
||||
is_error(&error);
|
||||
""",
|
||||
)
|
||||
unitTest(
|
||||
|
@ -150,6 +160,161 @@ class ConstrainedMapGeneratorTest {
|
|||
writer.toString() shouldContain "pub struct ConstrainedMap(pub(crate) ::std::collections::HashMap<::std::string::String, ::std::string::String>);"
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `error trait implemented for ConstraintViolation should work for constrained key and value`() {
|
||||
val model =
|
||||
"""
|
||||
${'$'}version: "2"
|
||||
|
||||
namespace test
|
||||
|
||||
use aws.protocols#restJson1
|
||||
use smithy.framework#ValidationException
|
||||
|
||||
// The `ConstraintViolation` code generated for a constrained map that is not reachable from an
|
||||
// operation does not have the `Key`, or `Value` variants. Hence, we need to define a service
|
||||
// and an operation that uses the constrained map.
|
||||
@restJson1
|
||||
service MyService {
|
||||
version: "2023-04-01",
|
||||
operations: [
|
||||
MyOperation,
|
||||
]
|
||||
}
|
||||
|
||||
@http(method: "POST", uri: "/echo")
|
||||
operation MyOperation {
|
||||
input:= {
|
||||
member1: ConstrainedMapWithConstrainedKey,
|
||||
member2: ConstrainedMapWithConstrainedKeyAndValue,
|
||||
member3: ConstrainedMapWithConstrainedValue,
|
||||
},
|
||||
output:= {},
|
||||
errors : [ValidationException]
|
||||
}
|
||||
|
||||
@length(min: 2, max: 69)
|
||||
map ConstrainedMapWithConstrainedKey {
|
||||
key: ConstrainedKey,
|
||||
value: String
|
||||
}
|
||||
|
||||
@length(min: 2, max: 69)
|
||||
map ConstrainedMapWithConstrainedValue {
|
||||
key: String,
|
||||
value: ConstrainedValue
|
||||
}
|
||||
|
||||
@length(min: 2, max: 69)
|
||||
map ConstrainedMapWithConstrainedKeyAndValue {
|
||||
key: ConstrainedKey,
|
||||
value: ConstrainedValue,
|
||||
}
|
||||
|
||||
@pattern("#\\d+")
|
||||
string ConstrainedKey
|
||||
|
||||
@pattern("A-Z")
|
||||
string ConstrainedValue
|
||||
""".asSmithyModel().let(ShapesReachableFromOperationInputTagger::transform)
|
||||
val constrainedKeyShape = model.lookup<StringShape>("test#ConstrainedKey")
|
||||
val constrainedValueShape = model.lookup<StringShape>("test#ConstrainedValue")
|
||||
|
||||
val codegenContext = serverTestCodegenContext(model)
|
||||
val symbolProvider = codegenContext.symbolProvider
|
||||
val project = TestWorkspace.testProject(symbolProvider)
|
||||
|
||||
project.withModule(ServerRustModule.Model) {
|
||||
TestUtility.generateIsDisplay().invoke(this)
|
||||
TestUtility.generateIsError().invoke(this)
|
||||
TestUtility.renderConstrainedString(codegenContext, this, constrainedKeyShape)
|
||||
TestUtility.renderConstrainedString(codegenContext, this, constrainedValueShape)
|
||||
|
||||
val mapsToVerify =
|
||||
listOf(
|
||||
model.lookup<MapShape>("test#ConstrainedMapWithConstrainedKey"),
|
||||
model.lookup<MapShape>("test#ConstrainedMapWithConstrainedKeyAndValue"),
|
||||
model.lookup<MapShape>("test#ConstrainedMapWithConstrainedValue"),
|
||||
)
|
||||
|
||||
rustTemplate(
|
||||
"""
|
||||
fn build_invalid_constrained_map_with_constrained_key() -> #{HashMap}<ConstrainedKey, String> {
|
||||
let mut m = ::std::collections::HashMap::new();
|
||||
m.insert(ConstrainedKey("1".to_string()), "Y".to_string());
|
||||
m
|
||||
}
|
||||
fn build_invalid_constrained_map_with_constrained_key_and_value() -> std::collections::HashMap<ConstrainedKey, ConstrainedValue> {
|
||||
let mut m = ::std::collections::HashMap::new();
|
||||
m.insert(ConstrainedKey("1".to_string()), ConstrainedValue("Y".to_string()));
|
||||
m
|
||||
}
|
||||
fn build_invalid_constrained_map_with_constrained_value() -> std::collections::HashMap<String, ConstrainedValue> {
|
||||
let mut m = ::std::collections::HashMap::new();
|
||||
m.insert("1".to_string(), ConstrainedValue("Y".to_string()));
|
||||
m
|
||||
}
|
||||
|
||||
// Define `ValidationExceptionField` since it is required by the `ConstraintViolation` code for constrained maps,
|
||||
// and the complete SDK generation process, which would generate it, is not invoked as part of the test.
|
||||
pub struct ValidationExceptionField {
|
||||
pub message: String,
|
||||
pub path: String
|
||||
}
|
||||
""",
|
||||
"HashMap" to RuntimeType.HashMap,
|
||||
)
|
||||
|
||||
for (mapToVerify in mapsToVerify) {
|
||||
val rustShapeName = mapToVerify.toShapeId().name
|
||||
val rustShapeSnakeCaseName = rustShapeName.toSnakeCase()
|
||||
|
||||
render(codegenContext, this, mapToVerify)
|
||||
|
||||
unitTest(
|
||||
name = "try_from_fail_$rustShapeSnakeCaseName",
|
||||
test = """
|
||||
let map = build_invalid_$rustShapeSnakeCaseName();
|
||||
let constrained_res: Result<$rustShapeName, $rustShapeSnakeCaseName::ConstraintViolation> = map.try_into();
|
||||
let error = constrained_res.unwrap_err();
|
||||
is_error(&error);
|
||||
is_display(&error);
|
||||
assert_eq!("Value with length 1 provided for 'test#$rustShapeName' failed to satisfy constraint: Member must have length between 2 and 69, inclusive", error.to_string());
|
||||
""",
|
||||
)
|
||||
}
|
||||
|
||||
unitTest(
|
||||
name = "try_constrained_key",
|
||||
test =
|
||||
"""
|
||||
let error = constrained_map_with_constrained_key::ConstraintViolation::Key(constrained_key::ConstraintViolation::Pattern("some error".to_string()));
|
||||
assert_eq!(error.to_string(), "Value provided for `test#ConstrainedKey` failed to satisfy the constraint: Member must match the regular expression pattern: #\\d+");
|
||||
""",
|
||||
)
|
||||
unitTest(
|
||||
name = "try_constrained_value",
|
||||
test =
|
||||
"""
|
||||
let error = constrained_map_with_constrained_value::ConstraintViolation::Value("some_key".to_string(), constrained_value::ConstraintViolation::Pattern("some error".to_string()));
|
||||
assert_eq!(error.to_string(), "Value provided for `test#ConstrainedValue` failed to satisfy the constraint: Member must match the regular expression pattern: A-Z");
|
||||
""",
|
||||
)
|
||||
unitTest(
|
||||
name = "try_constrained_key_and_value",
|
||||
test =
|
||||
"""
|
||||
let error = constrained_map_with_constrained_key_and_value::ConstraintViolation::Key(constrained_key::ConstraintViolation::Pattern("some error".to_string()));
|
||||
assert_eq!(error.to_string(), "Value provided for `test#ConstrainedKey` failed to satisfy the constraint: Member must match the regular expression pattern: #\\d+");
|
||||
let error = constrained_map_with_constrained_key_and_value::ConstraintViolation::Value(ConstrainedKey("1".to_string()), constrained_value::ConstraintViolation::Pattern("some error".to_string()));
|
||||
assert_eq!(error.to_string(), "Value provided for `test#ConstrainedValue` failed to satisfy the constraint: Member must match the regular expression pattern: A-Z");
|
||||
""",
|
||||
)
|
||||
}
|
||||
|
||||
project.compileAndTest()
|
||||
}
|
||||
|
||||
private fun render(
|
||||
codegenContext: ServerCodegenContext,
|
||||
writer: RustWriter,
|
||||
|
|
|
@ -73,6 +73,9 @@ class ConstrainedNumberGeneratorTest {
|
|||
val project = TestWorkspace.testProject(symbolProvider)
|
||||
|
||||
project.withModule(ServerRustModule.Model) {
|
||||
TestUtility.generateIsDisplay().invoke(this)
|
||||
TestUtility.generateIsError().invoke(this)
|
||||
|
||||
ConstrainedNumberGenerator(
|
||||
codegenContext,
|
||||
this.createTestInlineModuleCreator(),
|
||||
|
@ -91,7 +94,9 @@ class ConstrainedNumberGeneratorTest {
|
|||
name = "try_from_fail",
|
||||
test = """
|
||||
let constrained_res: Result<${testCase.shapeName}, _> = ${testCase.invalidValue}.try_into();
|
||||
constrained_res.unwrap_err();
|
||||
let error = constrained_res.unwrap_err();
|
||||
is_error(&error);
|
||||
is_display(&error);
|
||||
""",
|
||||
)
|
||||
unitTest(
|
||||
|
|
|
@ -53,9 +53,9 @@ class ConstrainedStringGeneratorTest {
|
|||
Triple(
|
||||
"""
|
||||
@length(min: 3, max: 10)
|
||||
@pattern("^a string$")
|
||||
@pattern("^a # string$")
|
||||
""",
|
||||
"a string", "an invalid string",
|
||||
"a # string", "an invalid string",
|
||||
),
|
||||
Triple("@pattern(\"123\")", "some pattern 123 in the middle", "no pattern at all"),
|
||||
).map {
|
||||
|
@ -86,6 +86,9 @@ class ConstrainedStringGeneratorTest {
|
|||
val project = TestWorkspace.testProject(symbolProvider)
|
||||
|
||||
project.withModule(ServerRustModule.Model) {
|
||||
TestUtility.generateIsDisplay().invoke(this)
|
||||
TestUtility.generateIsError().invoke(this)
|
||||
|
||||
ConstrainedStringGenerator(
|
||||
codegenContext,
|
||||
this.createTestInlineModuleCreator(),
|
||||
|
@ -106,7 +109,9 @@ class ConstrainedStringGeneratorTest {
|
|||
test = """
|
||||
let string = "${testCase.invalidString}".to_owned();
|
||||
let constrained_res: Result<ConstrainedString, _> = string.try_into();
|
||||
constrained_res.unwrap_err();
|
||||
let error = constrained_res.unwrap_err();
|
||||
is_error(&error);
|
||||
is_display(&error);
|
||||
""",
|
||||
)
|
||||
unitTest(
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package software.amazon.smithy.rust.codegen.server.smithy.generators
|
||||
|
||||
import software.amazon.smithy.model.shapes.StringShape
|
||||
import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter
|
||||
import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate
|
||||
import software.amazon.smithy.rust.codegen.core.rustlang.writable
|
||||
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType
|
||||
import software.amazon.smithy.rust.codegen.server.smithy.ServerCodegenContext
|
||||
import software.amazon.smithy.rust.codegen.server.smithy.createTestInlineModuleCreator
|
||||
import software.amazon.smithy.rust.codegen.server.smithy.customizations.SmithyValidationExceptionConversionGenerator
|
||||
|
||||
object TestUtility {
|
||||
fun generateIsDisplay() =
|
||||
writable {
|
||||
rustTemplate(
|
||||
"""
|
||||
pub fn is_display<T : #{Display}>(_t: &T) { }
|
||||
""",
|
||||
"Display" to RuntimeType.Display,
|
||||
)
|
||||
}
|
||||
|
||||
fun generateIsError() =
|
||||
writable {
|
||||
rustTemplate(
|
||||
"""
|
||||
pub fn is_error<T : #{Error}>(_t: &T) { }
|
||||
""",
|
||||
"Error" to RuntimeType.StdError,
|
||||
)
|
||||
}
|
||||
|
||||
fun renderConstrainedString(
|
||||
codegenContext: ServerCodegenContext,
|
||||
writer: RustWriter,
|
||||
constrainedStringShape: StringShape,
|
||||
) {
|
||||
val validationExceptionConversionGenerator = SmithyValidationExceptionConversionGenerator(codegenContext)
|
||||
ConstrainedStringGenerator(
|
||||
codegenContext,
|
||||
writer.createTestInlineModuleCreator(),
|
||||
writer,
|
||||
constrainedStringShape,
|
||||
validationExceptionConversionGenerator,
|
||||
).render()
|
||||
}
|
||||
}
|
|
@ -11,6 +11,7 @@ import software.amazon.smithy.rust.codegen.core.rustlang.rust
|
|||
import software.amazon.smithy.rust.codegen.core.smithy.CoreCodegenConfig
|
||||
import software.amazon.smithy.rust.codegen.core.testutil.TestWorkspace
|
||||
import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel
|
||||
import software.amazon.smithy.rust.codegen.core.testutil.compileAndTest
|
||||
import software.amazon.smithy.rust.codegen.core.testutil.testModule
|
||||
import software.amazon.smithy.rust.codegen.core.testutil.unitTest
|
||||
import software.amazon.smithy.rust.codegen.core.util.lookup
|
||||
|
@ -71,6 +72,9 @@ class UnconstrainedMapGeneratorTest {
|
|||
|
||||
serverIntegrationTest(model) { _, rustCrate ->
|
||||
rustCrate.testModule {
|
||||
TestUtility.generateIsDisplay().invoke(this)
|
||||
TestUtility.generateIsError().invoke(this)
|
||||
|
||||
unitTest("map_a_unconstrained_fail_to_constrain_with_some_error") {
|
||||
rust(
|
||||
"""
|
||||
|
@ -105,8 +109,17 @@ class UnconstrainedMapGeneratorTest {
|
|||
);
|
||||
|
||||
let actual_err = crate::constrained::map_a_constrained::MapAConstrained::try_from(map_a_unconstrained).unwrap_err();
|
||||
|
||||
assert!(actual_err == missing_string_expected_err || actual_err == missing_int_expected_err);
|
||||
|
||||
is_display(&actual_err);
|
||||
is_error(&actual_err);
|
||||
|
||||
let error_str = actual_err.to_string();
|
||||
assert!(
|
||||
error_str == "`string` was not provided but it is required when building `StructureC`"
|
||||
|| error_str
|
||||
== "`int` was not provided but it is required when building `StructureC`"
|
||||
);
|
||||
""",
|
||||
)
|
||||
}
|
||||
|
@ -162,5 +175,7 @@ class UnconstrainedMapGeneratorTest {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
project.compileAndTest()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -59,6 +59,9 @@ class UnconstrainedUnionGeneratorTest {
|
|||
}
|
||||
|
||||
project.withModule(ServerRustModule.UnconstrainedModule) unconstrainedModuleWriter@{
|
||||
TestUtility.generateIsDisplay().invoke(this)
|
||||
TestUtility.generateIsError().invoke(this)
|
||||
|
||||
project.withModule(ServerRustModule.Model) modelsModuleWriter@{
|
||||
UnconstrainedUnionGenerator(codegenContext, project.createInlineModuleCreator(), this@modelsModuleWriter, unionShape).render()
|
||||
|
||||
|
@ -71,11 +74,13 @@ class UnconstrainedUnionGeneratorTest {
|
|||
let expected_err = crate::model::union::ConstraintViolation::Structure(
|
||||
crate::model::structure::ConstraintViolation::MissingRequiredMember,
|
||||
);
|
||||
|
||||
let err = crate::model::Union::try_from(union_unconstrained).unwrap_err();
|
||||
assert_eq!(
|
||||
expected_err,
|
||||
crate::model::Union::try_from(union_unconstrained).unwrap_err()
|
||||
expected_err, err
|
||||
);
|
||||
is_display(&err);
|
||||
is_error(&err);
|
||||
assert_eq!(err.to_string(), "`required_member` was not provided but it is required when building `Structure`");
|
||||
""",
|
||||
)
|
||||
|
||||
|
|
Loading…
Reference in New Issue