Skip to content

Commit 0b13235

Browse files
david-perezHarry Barber
authored andcommitted
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.
1 parent 1eec94c commit 0b13235

File tree

4 files changed

+98
-7
lines changed

4 files changed

+98
-7
lines changed

codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerCodegenVisitor.kt

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -197,10 +197,12 @@ open class ServerCodegenVisitor(
197197

198198
val validationExceptionShapeId = validationExceptionConversionGenerator.shapeId
199199
for (validationResult in listOf(
200-
validateOperationsWithConstrainedInputHaveValidationExceptionAttached(
201-
model,
202-
service,
203-
validationExceptionShapeId,
200+
codegenDecorator.postprocessValidationExceptionNotAttachedErrorMessage(
201+
validateOperationsWithConstrainedInputHaveValidationExceptionAttached(
202+
model,
203+
service,
204+
validationExceptionShapeId,
205+
),
204206
),
205207
validateUnsupportedConstraints(model, service, codegenContext.settings.codegenConfig),
206208
)) {
@@ -209,7 +211,7 @@ open class ServerCodegenVisitor(
209211
logger.log(logMessage.level, logMessage.message)
210212
}
211213
if (validationResult.shouldAbort) {
212-
throw CodegenException("Unsupported constraints feature used; see error messages above for resolution")
214+
throw CodegenException("Unsupported constraints feature used; see error messages above for resolution", validationResult)
213215
}
214216
}
215217

codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ValidateUnsupportedConstraints.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,8 @@ private data class UnsupportedUniqueItemsTraitOnShape(val shape: Shape, val uniq
130130
UnsupportedConstraintMessageKind()
131131

132132
data class LogMessage(val level: Level, val message: String)
133-
data class ValidationResult(val shouldAbort: Boolean, val messages: List<LogMessage>)
133+
data class ValidationResult(val shouldAbort: Boolean, val messages: List<LogMessage>) :
134+
Throwable(message = messages.joinToString("\n") { it.message })
134135

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

codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customize/ServerCodegenDecorator.kt

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import software.amazon.smithy.rust.codegen.core.smithy.customize.CombinedCoreCod
1111
import software.amazon.smithy.rust.codegen.core.smithy.customize.CoreCodegenDecorator
1212
import software.amazon.smithy.rust.codegen.core.smithy.protocols.ProtocolMap
1313
import software.amazon.smithy.rust.codegen.server.smithy.ServerCodegenContext
14+
import software.amazon.smithy.rust.codegen.server.smithy.ValidationResult
1415
import software.amazon.smithy.rust.codegen.server.smithy.generators.ValidationExceptionConversionGenerator
1516
import software.amazon.smithy.rust.codegen.server.smithy.generators.protocol.ServerProtocolGenerator
1617
import java.util.logging.Logger
@@ -23,6 +24,12 @@ typealias ServerProtocolMap = ProtocolMap<ServerProtocolGenerator, ServerCodegen
2324
interface ServerCodegenDecorator : CoreCodegenDecorator<ServerCodegenContext> {
2425
fun protocols(serviceId: ShapeId, currentProtocols: ServerProtocolMap): ServerProtocolMap = currentProtocols
2526
fun validationExceptionConversion(codegenContext: ServerCodegenContext): ValidationExceptionConversionGenerator? = null
27+
28+
/**
29+
* Injection point to allow a decorator to postprocess the error message that arises when an operation is
30+
* constrained but the `ValidationException` shape is not attached to the operation's errors.
31+
*/
32+
fun postprocessValidationExceptionNotAttachedErrorMessage(validationResult: ValidationResult) = validationResult
2633
}
2734

2835
/**
@@ -33,6 +40,9 @@ interface ServerCodegenDecorator : CoreCodegenDecorator<ServerCodegenContext> {
3340
class CombinedServerCodegenDecorator(private val decorators: List<ServerCodegenDecorator>) :
3441
CombinedCoreCodegenDecorator<ServerCodegenContext, ServerCodegenDecorator>(decorators),
3542
ServerCodegenDecorator {
43+
44+
private val orderedDecorators = decorators.sortedBy { it.order }
45+
3646
override val name: String
3747
get() = "CombinedServerCodegenDecorator"
3848
override val order: Byte
@@ -46,7 +56,12 @@ class CombinedServerCodegenDecorator(private val decorators: List<ServerCodegenD
4656
override fun validationExceptionConversion(codegenContext: ServerCodegenContext): ValidationExceptionConversionGenerator =
4757
// We use `firstNotNullOf` instead of `firstNotNullOfOrNull` because the [SmithyValidationExceptionDecorator]
4858
// is registered.
49-
decorators.sortedBy { it.order }.firstNotNullOf { it.validationExceptionConversion(codegenContext) }
59+
orderedDecorators.firstNotNullOf { it.validationExceptionConversion(codegenContext) }
60+
61+
override fun postprocessValidationExceptionNotAttachedErrorMessage(validationResult: ValidationResult): ValidationResult =
62+
orderedDecorators.foldRight(validationResult) { decorator, accumulated ->
63+
decorator.postprocessValidationExceptionNotAttachedErrorMessage(accumulated)
64+
}
5065

5166
companion object {
5267
fun fromClasspath(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package software.amazon.smithy.rust.codegen.server.smithy.customizations
7+
8+
import io.kotest.matchers.shouldBe
9+
import io.kotest.matchers.string.shouldContain
10+
import org.junit.jupiter.api.Test
11+
import org.junit.jupiter.api.assertThrows
12+
import software.amazon.smithy.codegen.core.CodegenException
13+
import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel
14+
import software.amazon.smithy.rust.codegen.server.smithy.LogMessage
15+
import software.amazon.smithy.rust.codegen.server.smithy.ValidationResult
16+
import software.amazon.smithy.rust.codegen.server.smithy.customize.ServerCodegenDecorator
17+
import software.amazon.smithy.rust.codegen.server.smithy.testutil.serverIntegrationTest
18+
19+
internal class PostprocessValidationExceptionNotAttachedErrorMessageDecoratorTest {
20+
@Test
21+
fun `validation exception not attached error message is postprocessed if decorator is registered`() {
22+
val model =
23+
"""
24+
namespace test
25+
use aws.protocols#restJson1
26+
27+
@restJson1
28+
service TestService {
29+
operations: ["ConstrainedOperation"],
30+
}
31+
32+
operation ConstrainedOperation {
33+
input: ConstrainedOperationInput
34+
}
35+
36+
structure ConstrainedOperationInput {
37+
@required
38+
requiredString: String
39+
}
40+
""".asSmithyModel()
41+
42+
val validationExceptionNotAttachedErrorMessageDummyPostprocessorDecorator = object : ServerCodegenDecorator {
43+
override val name: String
44+
get() = "ValidationExceptionNotAttachedErrorMessageDummyPostprocessorDecorator"
45+
override val order: Byte
46+
get() = 69
47+
48+
override fun postprocessValidationExceptionNotAttachedErrorMessage(validationResult: ValidationResult): ValidationResult {
49+
check(validationResult.messages.size == 1)
50+
51+
val level = validationResult.messages.first().level
52+
val message =
53+
"""
54+
${validationResult.messages.first().message}
55+
56+
There are three things all wise men fear: the sea in storm, a night with no moon, and the anger of a gentle man.
57+
"""
58+
59+
return validationResult.copy(messages = listOf(LogMessage(level, message)))
60+
}
61+
}
62+
63+
val exception = assertThrows<CodegenException> {
64+
serverIntegrationTest(
65+
model,
66+
additionalDecorators = listOf(validationExceptionNotAttachedErrorMessageDummyPostprocessorDecorator),
67+
)
68+
}
69+
val exceptionCause = (exception.cause!! as ValidationResult)
70+
exceptionCause.messages.size shouldBe 1
71+
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."
72+
}
73+
}

0 commit comments

Comments
 (0)