Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
The diff you're trying to view is too large. We only load the first 3000 changed files.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ class AwsJsonProtocol(private val version: String) : ProtocolGenerator {

override fun contentType(): String = "application/x-amz-json-$version"

override fun parsesModeledErrorBodies(): Boolean = true

override fun writeSerializeRequest(writer: ZigWriter, ctx: OperationContext) {
val inputName = "${ctx.operationName}Input"
val targetPrefix = ctx.service.id.name
Expand Down Expand Up @@ -108,10 +110,19 @@ class AwsJsonProtocol(private val version: String) : ProtocolGenerator {
// Match error codes to ServiceError variants
for (info in ctx.errorInfos) {
writer.openBlock("if (std.mem.eql(u8, error_code, \"\$L\")) {", info.smithyName)
writer.write("return .{ .arena = arena, .kind = .{ .\$L = .{", info.variantName)
writer.write(" .message = owned_message,")
writer.write(" .request_id = owned_request_id,")
writer.write("} } };")
writer.openBlock(
"const parsed_error: ?errors.\$L = aws.json.parseJsonObject(errors.\$L, body, arena_alloc) catch |err| switch (err) {",
info.structName, info.structName,
)
writer.write("error.OutOfMemory => return error.OutOfMemory,")
writer.write("else => null,")
writer.closeBlock("};")
writer.openBlock("if (parsed_error) |parsed| {")
writer.write("var typed_error = parsed;")
writer.write("typed_error.message = owned_message;")
writer.write("typed_error.request_id = owned_request_id;")
writer.write("return .{ .arena = arena, .kind = .{ .\$L = typed_error } };", info.variantName)
writer.closeBlock("}")
writer.closeBlock("}")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import software.amazon.smithy.model.shapes.OperationShape
import software.amazon.smithy.model.shapes.ServiceShape
import software.amazon.smithy.model.shapes.ShapeId
import software.amazon.smithy.model.shapes.StructureShape
import software.amazon.smithy.model.traits.DocumentationTrait
import software.amazon.smithy.model.traits.ErrorTrait
import software.amazon.smithy.model.traits.RequiredTrait
import software.amazon.smithy.zig.ZigContext
Expand All @@ -29,7 +30,13 @@ class AwsJsonProtocolTest {
@TempDir
lateinit var tempDir: Path

private fun buildTestModel(): Model {
private fun buildTestModel(version: String): Model {
val protocolTrait = when (version) {
"1.0" -> "awsJson1_0"
"1.1" -> "awsJson1_1"
else -> error("Unsupported AWS JSON version: $version")
}

return Model.assembler()
.addShape(
StructureShape.builder()
Expand All @@ -38,6 +45,28 @@ class AwsJsonProtocolTest {
.addMember("message", ShapeId.from("smithy.api#String"))
.build()
)
.addShape(
StructureShape.builder()
.id("test#ConditionalCheckFailedException")
.addTrait(ErrorTrait("client"))
.addTrait(DocumentationTrait("The condition on the request was not satisfied."))
.addMember(
MemberShape.builder()
.id("test#ConditionalCheckFailedException\$Message")
.target("smithy.api#String")
.addTrait(DocumentationTrait("A normalized explanation of the failure."))
.build()
)
.addMember("requestId", ShapeId.from("smithy.api#String"))
.addMember(
MemberShape.builder()
.id("test#ConditionalCheckFailedException\$Item")
.target("test#AttributeMap")
.addTrait(DocumentationTrait("The item that caused the condition check to fail."))
.build()
)
.build()
)
.addShape(
StructureShape.builder()
.id("test#AttributeValue")
Expand Down Expand Up @@ -72,6 +101,7 @@ class AwsJsonProtocolTest {
.input(ShapeId.from("test#PutItemInput"))
.output(ShapeId.from("test#PutItemOutput"))
.addError(ShapeId.from("test#ResourceNotFoundException"))
.addError(ShapeId.from("test#ConditionalCheckFailedException"))
.build()
)
.addShape(
Expand Down Expand Up @@ -118,14 +148,33 @@ class AwsJsonProtocolTest {
.output(ShapeId.from("test#GetItemOutput"))
.build()
)
.addShape(
ServiceShape.builder()
.id("test#DynamoDB_20120810")
.version("2012-08-10")
.addOperation(ShapeId.from("test#PutItem"))
.addOperation(ShapeId.from("test#ListTables"))
.addOperation(ShapeId.from("test#GetItem"))
.build()
.addUnparsedModel(
"aws-json-traits.smithy",
"""
${'$'}version: "2"
namespace aws.protocols

@trait(selector: "service")
structure awsJson1_0 {}

@trait(selector: "service")
structure awsJson1_1 {}
""".trimIndent(),
)
.addUnparsedModel(
"service.smithy",
"""
${'$'}version: "2"
namespace test

use aws.protocols#$protocolTrait

@$protocolTrait
service DynamoDB_20120810 {
version: "2012-08-10"
operations: [PutItem, ListTables, GetItem]
}
""".trimIndent(),
)
.assemble()
.unwrap()
Expand Down Expand Up @@ -154,7 +203,7 @@ class AwsJsonProtocolTest {
}

private fun generateFiles(version: String): Map<String, String> {
val model = buildTestModel()
val model = buildTestModel(version)
val context = createContext(model)
val service = model.expectShape(
ShapeId.from("test#DynamoDB_20120810"),
Expand Down Expand Up @@ -373,6 +422,74 @@ class AwsJsonProtocolTest {
)
}

@Test
fun modeledErrorsIncludeMembersImportsDocsAndWireNames() {
for (version in listOf("1.0", "1.1")) {
val files = generateFiles(version)
val errors = files["errors.zig"]!!
val exception = errors
.substringAfter("pub const ConditionalCheckFailedException = struct {")
.substringBefore("\npub const ResourceNotFoundException")

assertTrue(errors.contains("const aws = @import(\"aws\");"), "AWS JSON $version error maps should import aws")
assertTrue(
errors.contains("const AttributeValue = @import(\"attribute_value.zig\").AttributeValue;"),
"AWS JSON $version errors should import map value types",
)
assertTrue(
errors.contains("/// The condition on the request was not satisfied."),
"AWS JSON $version errors should retain exception documentation",
)
assertTrue(
exception.contains("/// The item that caused the condition check to fail."),
"AWS JSON $version errors should retain member documentation",
)
assertTrue(
exception.contains("item: ?[]const aws.map.MapEntry(AttributeValue) = null,"),
"AWS JSON $version errors should use output-style map optionality",
)
assertTrue(exception.contains(".message = \"Message\","), "AWS JSON $version should preserve Message wire casing")
assertTrue(exception.contains(".request_id = \"requestId\","), "AWS JSON $version should map reserved request IDs")
assertTrue(exception.contains(".item = \"Item\","), "AWS JSON $version should map modeled member names")
assertTrue(
Regex("(?m)^ message: ").findAll(exception).count() == 1,
"AWS JSON $version should not duplicate normalized message",
)
assertTrue(
Regex("(?m)^ request_id: ").findAll(exception).count() == 1,
"AWS JSON $version should not duplicate normalized request_id",
)
}
}

@Test
fun modeledErrorParserDispatchesAndFallsBackWithoutSwallowingOom() {
for (version in listOf("1.0", "1.1")) {
val files = generateFiles(version)
val op = files["put_item.zig"]!!

assertTrue(op.contains("const errors = @import(\"errors.zig\");"), "AWS JSON $version should import modeled errors")
assertTrue(
op.contains("aws.json.parseJsonObject(errors.ConditionalCheckFailedException, body, arena_alloc)"),
"AWS JSON $version should deserialize recognized error bodies",
)
assertTrue(op.contains("typed_error.message = owned_message;"), "AWS JSON $version should normalize error messages")
assertTrue(op.contains("typed_error.request_id = owned_request_id;"), "AWS JSON $version should normalize request IDs")
assertTrue(op.contains("else => null,"), "AWS JSON $version malformed typed bodies should fall through")
assertTrue(op.contains(".code = owned_code,"), "AWS JSON $version fallback should preserve the error code")
assertTrue(op.contains(".message = owned_message,"), "AWS JSON $version fallback should preserve the message")
assertTrue(op.contains(".http_status = status,"), "AWS JSON $version fallback should preserve HTTP status")
assertTrue(
op.contains("error.OutOfMemory => return error.OutOfMemory,"),
"AWS JSON $version typed parsing should propagate allocation failures",
)
assertTrue(
op.contains("parseErrorResponse(client.allocator, response.body, response.status) catch return error.OutOfMemory;"),
"AWS JSON $version operations should propagate diagnostic allocation failures",
)
}
}

// ---- Helper functions tests ----

@Test
Expand Down
Loading