Unverified Commit 1c1a3ef4 authored by david-perez's avatar david-perez Committed by GitHub
Browse files

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.
parent d7f81308
Loading
Loading
Loading
Loading
+7 −5
Original line number Diff line number Diff line
@@ -200,11 +200,13 @@ open class ServerCodegenVisitor(

        val validationExceptionShapeId = validationExceptionConversionGenerator.shapeId
        for (validationResult in listOf(
            codegenDecorator.postprocessValidationExceptionNotAttachedErrorMessage(
                validateOperationsWithConstrainedInputHaveValidationExceptionAttached(
                    model,
                    service,
                    validationExceptionShapeId,
                ),
            ),
            validateUnsupportedConstraints(model, service, codegenContext.settings.codegenConfig),
        )) {
            for (logMessage in validationResult.messages) {
@@ -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)
            }
        }

+2 −1
Original line number Diff line number Diff line
@@ -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<LogMessage>)
data class ValidationResult(val shouldAbort: Boolean, val messages: List<LogMessage>) :
    Throwable(message = messages.joinToString("\n") { it.message })

private val unsupportedConstraintsOnMemberShapes = allConstraintTraits - RequiredTrait::class.java

+16 −1
Original line number Diff line number Diff line
@@ -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<ServerProtocolGenerator, ServerCodegen
interface ServerCodegenDecorator : CoreCodegenDecorator<ServerCodegenContext> {
    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<ServerCodegenContext> {
class CombinedServerCodegenDecorator(private val decorators: List<ServerCodegenDecorator>) :
    CombinedCoreCodegenDecorator<ServerCodegenContext, ServerCodegenDecorator>(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<ServerCodegenD
    override fun validationExceptionConversion(codegenContext: ServerCodegenContext): ValidationExceptionConversionGenerator =
        // We use `firstNotNullOf` instead of `firstNotNullOfOrNull` because the [SmithyValidationExceptionDecorator]
        // is registered.
        decorators.sortedBy { it.order }.firstNotNullOf { it.validationExceptionConversion(codegenContext) }
        orderedDecorators.firstNotNullOf { it.validationExceptionConversion(codegenContext) }

    override fun postprocessValidationExceptionNotAttachedErrorMessage(validationResult: ValidationResult): ValidationResult =
        orderedDecorators.foldRight(validationResult) { decorator, accumulated ->
            decorator.postprocessValidationExceptionNotAttachedErrorMessage(accumulated)
        }

    companion object {
        fun fromClasspath(
+73 −0
Original line number Diff line number Diff line
/*
 * 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<CodegenException> {
            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."
    }
}