Backfill message & parse without case sensitivity (#565)

* Backfill message & parse without case sensitivity

* Allow empty errors for XML protocols
This commit is contained in:
Russell Cohen 2021-06-30 14:59:01 -07:00 committed by GitHub
parent 59f9c42ba6
commit c10899da08
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 130 additions and 24 deletions

View File

@ -30,21 +30,34 @@ val CodegenTests = listOf(
CodegenTest("aws.protocoltests.json10#JsonRpc10", "json_rpc10"),
CodegenTest("aws.protocoltests.json#JsonProtocol", "json_rpc11"),
CodegenTest("aws.protocoltests.restjson#RestJson", "rest_json"),
CodegenTest("aws.protocoltests.restjson#RestJsonExtras", "rest_json_extas"),
CodegenTest("aws.protocoltests.restxml#RestXml", "rest_xml"),
CodegenTest("aws.protocoltests.query#AwsQuery", "aws_query"),
CodegenTest("aws.protocoltests.ec2#AwsEc2", "ec2_query"),
CodegenTest("aws.protocoltests.restjson#RestJsonExtras", "rest_json_extras"),
CodegenTest(
"aws.protocoltests.restxml#RestXml", "rest_xml",
extraConfig = """, "codegen": { "addMessageToErrors": false } """
),
CodegenTest(
"aws.protocoltests.query#AwsQuery", "aws_query",
extraConfig = """, "codegen": { "addMessageToErrors": false } """
),
CodegenTest(
"aws.protocoltests.ec2#AwsEc2", "ec2_query",
extraConfig = """, "codegen": { "addMessageToErrors": false } """
),
CodegenTest(
"aws.protocoltests.restxml.xmlns#RestXmlWithNamespace",
"rest_xml_namespace"
"rest_xml_namespace",
extraConfig = """, "codegen": { "addMessageToErrors": false } """
),
CodegenTest(
"aws.protocoltests.restxml#RestXmlExtras",
"rest_xml_extras"
"rest_xml_extras",
extraConfig = """, "codegen": { "addMessageToErrors": false } """
),
CodegenTest(
"aws.protocoltests.restxmlunwrapped#RestXmlExtrasUnwrappedErrors",
"rest_xml_extras_unwrapped"
"rest_xml_extras_unwrapped",
extraConfig = """, "codegen": { "addMessageToErrors": false } """
),
CodegenTest(
"crate#Config",

View File

@ -62,6 +62,7 @@ service RestJsonExtras {
PrimitiveIntOp,
EscapedStringValues,
NullInNonSparse,
CaseInsensitiveErrorOperation
]
}
@ -268,3 +269,23 @@ structure NullInNonSparseOutput {
operation NullInNonSparse {
output: NullInNonSparseOutput,
}
@http(uri: "/error-sensitive", method: "POST")
operation CaseInsensitiveErrorOperation {
errors: [CaseInsensitiveError]
}
@httpResponseTests([
{
id: "Upper case error modeled lower case",
protocol: "aws.protocols#restJson1",
code: 500,
body: "{\"Message\": \"hello\"}",
headers: { "X-Amzn-Errortype": "CaseInsensitiveError" },
params: { message: "hello" }
}
])
@error("server")
structure CaseInsensitiveError {
message: String
}

View File

@ -27,6 +27,7 @@ import software.amazon.smithy.rust.codegen.smithy.generators.UnionGenerator
import software.amazon.smithy.rust.codegen.smithy.generators.implBlock
import software.amazon.smithy.rust.codegen.smithy.protocols.ProtocolLoader
import software.amazon.smithy.rust.codegen.smithy.traits.SyntheticInputTrait
import software.amazon.smithy.rust.codegen.smithy.transformers.AddErrorMessage
import software.amazon.smithy.rust.codegen.smithy.transformers.RecursiveShapeBoxer
import software.amazon.smithy.rust.codegen.util.CommandFailed
import software.amazon.smithy.rust.codegen.util.getTrait
@ -74,7 +75,9 @@ class CodegenVisitor(context: PluginContext, private val codegenDecorator: RustC
httpGenerator = protocolGenerator.buildProtocolGenerator(protocolConfig)
}
private fun baselineTransform(model: Model) = model.let(RecursiveShapeBoxer::transform)
private fun baselineTransform(model: Model) =
model.let(RecursiveShapeBoxer::transform)
.letIf(settings.codegenConfig.addMessageToErrors, AddErrorMessage::transform)
fun execute() {
logger.info("generating Rust client...")

View File

@ -28,14 +28,31 @@ private const val RUNTIME_CONFIG = "runtimeConfig"
private const val CODEGEN_SETTINGS = "codegen"
private const val LICENSE = "license"
data class CodegenConfig(val renameExceptions: Boolean = true, val includeFluentClient: Boolean = true, val formatTimeoutSeconds: Int = 20) {
/**
* Configuration of codegen settings
*
* [renameExceptions]: Rename `Exception` to `Error` in the generated SDK
* [includeFluentClient]: Generate a `client` module in the generated SDK (currently the AWS SDK sets this to false
* and generates its own client)
*
* [addMessageToErrors]: Adds a `message` field automatically to all error shapes
* [formatTimeoutSeconds]: Timeout for running cargo fmt at the end of code generation
*/
data class CodegenConfig(
val renameExceptions: Boolean = true,
val includeFluentClient: Boolean = true,
val addMessageToErrors: Boolean = true,
val formatTimeoutSeconds: Int = 20
) {
companion object {
fun fromNode(node: Optional<ObjectNode>): CodegenConfig {
return if (node.isPresent) {
CodegenConfig(
node.get().getBooleanMemberOrDefault("renameErrors", true),
node.get().getBooleanMemberOrDefault("includeFluentClient", true),
node.get().getNumberMemberOrDefault("formatTimeoutSeconds", 20).toInt()
node.get().getBooleanMemberOrDefault("addMessageToErrors", true),
node.get().getNumberMemberOrDefault("formatTimeoutSeconds", 20).toInt(),
)
} else {
CodegenConfig()

View File

@ -426,6 +426,8 @@ class HttpProtocolTestGenerator(
private val AwsJson11 = "aws.protocoltests.json#JsonProtocol"
private val RestJson = "aws.protocoltests.restjson#RestJson"
private val RestXml = "aws.protocoltests.restxml#RestXml"
private val AwsQuery = "aws.protocoltests.query#AwsQuery"
private val Ec2Query = "aws.protocoltests.ec2#AwsEc2"
private val ExpectFail = setOf<FailingTest>()
private val RunOnly: Set<String>? = null

View File

@ -20,6 +20,7 @@ import software.amazon.smithy.rust.codegen.smithy.RuntimeType.Companion.StdError
import software.amazon.smithy.rust.codegen.smithy.RuntimeType.Companion.stdfmt
import software.amazon.smithy.rust.codegen.smithy.RustSymbolProvider
import software.amazon.smithy.rust.codegen.smithy.letIf
import software.amazon.smithy.rust.codegen.smithy.transformers.errorMessageMember
import software.amazon.smithy.rust.codegen.util.dq
import software.amazon.smithy.rust.codegen.util.getTrait
@ -71,9 +72,8 @@ class ErrorGenerator(
private fun renderError() {
val symbol = symbolProvider.toSymbol(shape)
val messageShape =
shape.getMember("message").or { shape.getMember("Message") }.or { shape.getMember("errorMessage") }
val message = messageShape.map { "self.${symbolProvider.toMemberName(it)}.as_deref()" }.orElse("None")
val messageShape = shape.errorMessageMember()
val message = messageShape?.let { "self.${symbolProvider.toMemberName(it)}.as_deref()" } ?: "None"
val errorKindT = RuntimeType.errorKind(symbolProvider.config().runtimeConfig)
writer.rustBlock("impl ${symbol.name}") {
val retryKindWriteable = shape.modeledRetryKind(error)?.writable(symbolProvider.config().runtimeConfig)
@ -97,7 +97,7 @@ class ErrorGenerator(
"$symbolName [${shape.id.name}]"
}
write("write!(f, ${errorDesc.dq()})?;")
messageShape.map {
messageShape?.let {
ifSet(it, symbolProvider.toSymbol(it), "&self.message") { field ->
write("""write!(f, ": {}", $field)?;""")
}

View File

@ -19,7 +19,9 @@ import software.amazon.smithy.model.traits.TimestampFormatTrait
import software.amazon.smithy.rust.codegen.rustlang.Attribute
import software.amazon.smithy.rust.codegen.rustlang.RustWriter
import software.amazon.smithy.rust.codegen.rustlang.Writable
import software.amazon.smithy.rust.codegen.rustlang.assignment
import software.amazon.smithy.rust.codegen.rustlang.rust
import software.amazon.smithy.rust.codegen.rustlang.rustBlock
import software.amazon.smithy.rust.codegen.rustlang.rustBlockTemplate
import software.amazon.smithy.rust.codegen.rustlang.rustTemplate
import software.amazon.smithy.rust.codegen.rustlang.withBlock
@ -37,6 +39,7 @@ import software.amazon.smithy.rust.codegen.smithy.generators.setterName
import software.amazon.smithy.rust.codegen.smithy.isOptional
import software.amazon.smithy.rust.codegen.smithy.protocols.parse.StructuredDataParserGenerator
import software.amazon.smithy.rust.codegen.smithy.protocols.serialize.StructuredDataSerializerGenerator
import software.amazon.smithy.rust.codegen.smithy.transformers.errorMessageMember
import software.amazon.smithy.rust.codegen.util.dq
import software.amazon.smithy.rust.codegen.util.expectMember
import software.amazon.smithy.rust.codegen.util.hasStreamingMember
@ -288,7 +291,10 @@ class HttpBoundProtocolGenerator(
let error_code = match generic.code() {
Some(code) => code,
None => return Err(#{error_symbol}::unhandled(generic))
};""",
};
let _error_message = generic.message().map(|msg|msg.to_owned());
""",
"error_symbol" to errorSymbol,
)
withBlock("Err(match error_code {", "})") {
@ -296,16 +302,33 @@ class HttpBoundProtocolGenerator(
val errorShape = model.expectShape(error, StructureShape::class.java)
val variantName = symbolProvider.toSymbol(model.expectShape(error)).name
withBlock(
"${httpBindingResolver.errorCode(errorShape).dq()} => #1T { meta: generic, kind: #1TKind::$variantName({",
"${
httpBindingResolver.errorCode(errorShape).dq()
} => #1T { meta: generic, kind: #1TKind::$variantName({",
"})},",
errorSymbol
) {
renderShapeParser(
operationShape,
errorShape,
httpBindingResolver.errorResponseBindings(errorShape),
errorSymbol
)
Attribute.AllowUnusedMut.render(this)
assignment("mut tmp") {
rustBlock("") {
renderShapeParser(
operationShape,
errorShape,
httpBindingResolver.errorResponseBindings(errorShape),
errorSymbol
)
}
}
if (errorShape.errorMessageMember() != null) {
rust(
"""
if (&tmp.message).is_none() {
tmp.message = _error_message;
}
"""
)
}
rust("tmp")
}
}
rust("_ => #T::generic(generic)", errorSymbol)

View File

@ -214,6 +214,7 @@ class XmlBindingTraitParserGenerator(
xmlError
) {
val members = errorShape.errorXmlMembers()
rust("if inp.is_empty() { return Ok(builder) }")
if (members.isNotEmpty()) {
rustTemplate(
"""
@ -226,8 +227,6 @@ class XmlBindingTraitParserGenerator(
"xml_errors" to xmlErrors
)
parseStructureInner(members, builder = "builder", Ctx(tag = "error_decoder", accum = null))
} else {
rust("let _ = inp;")
}
rust("Ok(builder)")
}

View File

@ -0,0 +1,28 @@
package software.amazon.smithy.rust.codegen.smithy.transformers
import software.amazon.smithy.model.Model
import software.amazon.smithy.model.shapes.MemberShape
import software.amazon.smithy.model.shapes.ShapeId
import software.amazon.smithy.model.shapes.StructureShape
import software.amazon.smithy.model.traits.ErrorTrait
import software.amazon.smithy.model.transform.ModelTransformer
import software.amazon.smithy.rust.codegen.util.hasTrait
import software.amazon.smithy.rust.codegen.util.orNull
import java.util.logging.Logger
fun StructureShape.errorMessageMember(): MemberShape? = this.getMember("message").or { this.getMember("Message") }.orNull()
object AddErrorMessage {
private val logger = Logger.getLogger("AddErrorMessage")
fun transform(model: Model): Model {
return ModelTransformer.create().mapShapes(model) { shape ->
val addMessageField = shape.hasTrait<ErrorTrait>() && shape is StructureShape && shape.errorMessageMember() == null
if (addMessageField && shape is StructureShape) {
logger.info("Adding message field to ${shape.id}")
shape.toBuilder().addMember("message", ShapeId.from("smithy.api#String")).build()
} else {
shape
}
}
}
}