Skip to content

Commit

Permalink
Allow server decorators to postprocess ValidationException not atta…
Browse files Browse the repository at this point in the history
…ched error messages (smithy-lang#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.
  • Loading branch information
david-perez committed Feb 15, 2023
1 parent d7f8130 commit 1c1a3ef
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 7 deletions.
Expand Up @@ -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),
)) {
Expand All @@ -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)
}
}

Expand Down
Expand Up @@ -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

Expand Down
Expand Up @@ -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
Expand All @@ -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
}

/**
Expand All @@ -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
Expand All @@ -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(
Expand Down
@@ -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<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."
}
}

0 comments on commit 1c1a3ef

Please sign in to comment.