From 1c1a3ef447ad0d3a9a13ef78ccf45384eb997ff4 Mon Sep 17 00:00:00 2001 From: david-perez Date: Wed, 15 Feb 2023 16:09:42 +0100 Subject: [PATCH] Allow server decorators to postprocess `ValidationException` not attached error messages (#2338) Should they want to, a server decorator can now postprocess the error message that arises when a constrained operation does not have the `ValidationException` shape attached to its errors. This commit adds a test to ensure that when such a decorator is registered, the `ValidationResult` can indeed be altered, but no such decorator is added to the `rust-server-codegen` plugin. --- .../server/smithy/ServerCodegenVisitor.kt | 12 +-- .../smithy/ValidateUnsupportedConstraints.kt | 3 +- .../customize/ServerCodegenDecorator.kt | 17 ++++- ...ionNotAttachedErrorMessageDecoratorTest.kt | 73 +++++++++++++++++++ 4 files changed, 98 insertions(+), 7 deletions(-) create mode 100644 codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customizations/PostprocessValidationExceptionNotAttachedErrorMessageDecoratorTest.kt diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerCodegenVisitor.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerCodegenVisitor.kt index faaf25514b..7243b1fa6c 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerCodegenVisitor.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerCodegenVisitor.kt @@ -200,10 +200,12 @@ open class ServerCodegenVisitor( val validationExceptionShapeId = validationExceptionConversionGenerator.shapeId for (validationResult in listOf( - validateOperationsWithConstrainedInputHaveValidationExceptionAttached( - model, - service, - validationExceptionShapeId, + codegenDecorator.postprocessValidationExceptionNotAttachedErrorMessage( + validateOperationsWithConstrainedInputHaveValidationExceptionAttached( + model, + service, + validationExceptionShapeId, + ), ), validateUnsupportedConstraints(model, service, codegenContext.settings.codegenConfig), )) { @@ -212,7 +214,7 @@ open class ServerCodegenVisitor( logger.log(logMessage.level, logMessage.message) } if (validationResult.shouldAbort) { - throw CodegenException("Unsupported constraints feature used; see error messages above for resolution") + throw CodegenException("Unsupported constraints feature used; see error messages above for resolution", validationResult) } } diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ValidateUnsupportedConstraints.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ValidateUnsupportedConstraints.kt index fd3f259bc7..08c7e487b5 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ValidateUnsupportedConstraints.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ValidateUnsupportedConstraints.kt @@ -130,7 +130,8 @@ private data class UnsupportedUniqueItemsTraitOnShape(val shape: Shape, val uniq UnsupportedConstraintMessageKind() data class LogMessage(val level: Level, val message: String) -data class ValidationResult(val shouldAbort: Boolean, val messages: List) +data class ValidationResult(val shouldAbort: Boolean, val messages: List) : + Throwable(message = messages.joinToString("\n") { it.message }) private val unsupportedConstraintsOnMemberShapes = allConstraintTraits - RequiredTrait::class.java diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customize/ServerCodegenDecorator.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customize/ServerCodegenDecorator.kt index 156c00421e..8e771cb122 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customize/ServerCodegenDecorator.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customize/ServerCodegenDecorator.kt @@ -11,6 +11,7 @@ import software.amazon.smithy.rust.codegen.core.smithy.customize.CombinedCoreCod import software.amazon.smithy.rust.codegen.core.smithy.customize.CoreCodegenDecorator import software.amazon.smithy.rust.codegen.core.smithy.protocols.ProtocolMap import software.amazon.smithy.rust.codegen.server.smithy.ServerCodegenContext +import software.amazon.smithy.rust.codegen.server.smithy.ValidationResult import software.amazon.smithy.rust.codegen.server.smithy.generators.ValidationExceptionConversionGenerator import software.amazon.smithy.rust.codegen.server.smithy.generators.protocol.ServerProtocolGenerator import java.util.logging.Logger @@ -23,6 +24,12 @@ typealias ServerProtocolMap = ProtocolMap { fun protocols(serviceId: ShapeId, currentProtocols: ServerProtocolMap): ServerProtocolMap = currentProtocols fun validationExceptionConversion(codegenContext: ServerCodegenContext): ValidationExceptionConversionGenerator? = null + + /** + * Injection point to allow a decorator to postprocess the error message that arises when an operation is + * constrained but the `ValidationException` shape is not attached to the operation's errors. + */ + fun postprocessValidationExceptionNotAttachedErrorMessage(validationResult: ValidationResult) = validationResult } /** @@ -33,6 +40,9 @@ interface ServerCodegenDecorator : CoreCodegenDecorator { class CombinedServerCodegenDecorator(private val decorators: List) : CombinedCoreCodegenDecorator(decorators), ServerCodegenDecorator { + + private val orderedDecorators = decorators.sortedBy { it.order } + override val name: String get() = "CombinedServerCodegenDecorator" override val order: Byte @@ -46,7 +56,12 @@ class CombinedServerCodegenDecorator(private val decorators: List + decorator.postprocessValidationExceptionNotAttachedErrorMessage(accumulated) + } companion object { fun fromClasspath( diff --git a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customizations/PostprocessValidationExceptionNotAttachedErrorMessageDecoratorTest.kt b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customizations/PostprocessValidationExceptionNotAttachedErrorMessageDecoratorTest.kt new file mode 100644 index 0000000000..10b146805e --- /dev/null +++ b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customizations/PostprocessValidationExceptionNotAttachedErrorMessageDecoratorTest.kt @@ -0,0 +1,73 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.rust.codegen.server.smithy.customizations + +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldContain +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import software.amazon.smithy.codegen.core.CodegenException +import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel +import software.amazon.smithy.rust.codegen.server.smithy.LogMessage +import software.amazon.smithy.rust.codegen.server.smithy.ValidationResult +import software.amazon.smithy.rust.codegen.server.smithy.customize.ServerCodegenDecorator +import software.amazon.smithy.rust.codegen.server.smithy.testutil.serverIntegrationTest + +internal class PostprocessValidationExceptionNotAttachedErrorMessageDecoratorTest { + @Test + fun `validation exception not attached error message is postprocessed if decorator is registered`() { + val model = + """ + namespace test + use aws.protocols#restJson1 + + @restJson1 + service TestService { + operations: ["ConstrainedOperation"], + } + + operation ConstrainedOperation { + input: ConstrainedOperationInput + } + + structure ConstrainedOperationInput { + @required + requiredString: String + } + """.asSmithyModel() + + val validationExceptionNotAttachedErrorMessageDummyPostprocessorDecorator = object : ServerCodegenDecorator { + override val name: String + get() = "ValidationExceptionNotAttachedErrorMessageDummyPostprocessorDecorator" + override val order: Byte + get() = 69 + + override fun postprocessValidationExceptionNotAttachedErrorMessage(validationResult: ValidationResult): ValidationResult { + check(validationResult.messages.size == 1) + + val level = validationResult.messages.first().level + val message = + """ +${validationResult.messages.first().message} + +There are three things all wise men fear: the sea in storm, a night with no moon, and the anger of a gentle man. + """ + + return validationResult.copy(messages = listOf(LogMessage(level, message))) + } + } + + val exception = assertThrows { + serverIntegrationTest( + model, + additionalDecorators = listOf(validationExceptionNotAttachedErrorMessageDummyPostprocessorDecorator), + ) + } + val exceptionCause = (exception.cause!! as ValidationResult) + exceptionCause.messages.size shouldBe 1 + exceptionCause.messages.first().message shouldContain "There are three things all wise men fear: the sea in storm, a night with no moon, and the anger of a gentle man." + } +}