Fixes and tests for decoding binary items and empty lists in DynamoDBEvent.Decoder (#103)

Cleaned up version of
[#61](https://github.com/awslabs/swift-aws-lambda-events/pull/61) that
also has tests.

This PR makes it so that in lists, the decoder will not try to access
the 0th index of an empty array.

This PR will also make it so that `AttributeValue`s of type `.binary`
will properly decode to base64 Strings when String is put as the type in
the the `Decodable`. To avoid including `Data` in the code, I included
code from the original source of the base 64 decoding methods
(https://github.com/fabianfett/swift-base64-kit) to add in the
corresponding encoding methods. However, I saw lots of Foundation is
imported throughout so I reverted it. What are your thoughts on this
matter?

I also added tests that aim to make these failure points more resilient
in the future.
This commit is contained in:
Ben Rosen
2025-10-22 14:29:40 -05:00
committed by GitHub
parent f6fc221450
commit a85c7a7846
2 changed files with 70 additions and 6 deletions
+15 -6
View File
@@ -462,11 +462,14 @@ extension DynamoDBEvent {
func decode(_ type: String.Type, forKey key: K) throws -> String {
let value = try getValue(forKey: key)
guard case .string(let string) = value else {
switch value {
case .string(let string):
return string
case .binary(let binary):
return Data(binary).base64EncodedString()
default:
throw self.createTypeMismatchError(type: type, forKey: key, value: value)
}
return string
}
func decode(_ type: Double.Type, forKey key: K) throws -> Double {
@@ -651,11 +654,14 @@ extension DynamoDBEvent {
}
func decode(_: String.Type) throws -> String {
guard case .string(let string) = self.value else {
switch self.value {
case .string(let string):
return string
case .binary(let binary):
return Data(binary).base64EncodedString()
default:
throw self.createTypeMismatchError(type: String.self, value: self.value)
}
return string
}
func decode(_: Double.Type) throws -> Double {
@@ -769,6 +775,9 @@ extension DynamoDBEvent {
self.codingPath = codingPath
self.array = array
self.count = array.count
// No need to decode if array is empty
self.isAtEnd = array.isEmpty
}
mutating func decodeNil() throws -> Bool {
@@ -251,4 +251,59 @@ struct DynamoDBTests {
#expect(test?.foo == "bar")
#expect(test?.xyz == 123)
}
@Test func decoderEmptyList() {
let value: [String: DynamoDBEvent.AttributeValue] = [
"fooList": .list([])
]
struct Test: Codable {
let fooList: [Int]
}
let test = try? DynamoDBEvent.Decoder().decode(Test.self, from: value)
#expect(test?.fooList == [])
}
@Test func decoderNonEmptyList() {
let value: [String: DynamoDBEvent.AttributeValue] = [
"fooList": .list([.string("test")])
]
struct Test: Codable {
let fooList: [String]
}
let test = try? DynamoDBEvent.Decoder().decode(Test.self, from: value)
#expect(test?.fooList == ["test"])
}
@Test func decoderBinaryToBase64KeyedDecodingContainer() {
let value: [String: DynamoDBEvent.AttributeValue] = [
"xyz": .binary([0x74, 0x65, 0x73, 0x74]) // UTF8 for "test"
]
struct Test: Codable {
let xyz: String
}
let test = try? DynamoDBEvent.Decoder().decode(Test.self, from: value)
#expect(test?.xyz == "dGVzdA==") // base64 for "test"
}
@Test func decoderBinaryToBase64SingleValueDecodingContainer() {
let value: DynamoDBEvent.AttributeValue = .binary([0x74, 0x65, 0x73, 0x74]) // UTF8 for "test"
let test = try? DynamoDBEvent.Decoder().decode(String.self, from: value)
#expect(test == "dGVzdA==") // base64 for "test"
}
@Test func decoderBinaryToBase64UnkeyedDecodingContainer() {
let value: DynamoDBEvent.AttributeValue = .list([
.binary([0x74, 0x65, 0x73, 0x74]) // UTF8 for "test"
])
let test = try? DynamoDBEvent.Decoder().decode([String].self, from: value)
#expect(test == ["dGVzdA=="]) // base64 for "test"
}
}