Skip to content

Commit

Permalink
Add StringFlexibleDecodable for decoding headers values (#430)
Browse files Browse the repository at this point in the history
* Add StringFlexibleDecodable ...

... for decoding headers values added to dictionaries before decoding with the DictionaryDecoder

* swift format
  • Loading branch information
adam-fowler authored Apr 1, 2021
1 parent 95209ce commit ce219fe
Show file tree
Hide file tree
Showing 3 changed files with 89 additions and 25 deletions.
66 changes: 49 additions & 17 deletions Sources/SotoCore/Encoder/DictionaryDecoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import struct Foundation.URL

import class Foundation.NSNull
import class Foundation.NSNumber
import class Foundation.NumberFormatter

//===----------------------------------------------------------------------===//
// Dictionary Decoders
Expand Down Expand Up @@ -1031,21 +1032,30 @@ extension __DictionaryDecoder: SingleValueDecodingContainer {
// MARK: - Concrete Value Representations

extension __DictionaryDecoder {
@inline(__always) func unboxAsNumber(_ value: Any) -> NSNumber? {
if let number = value as? NSNumber {
return number
}
return (value as? StringFlexibleDecodable)?.asNumber()
}

/// Returns the given value unboxed from a container.
fileprivate func unbox(_ value: Any, as type: Bool.Type) throws -> Bool? {
guard !(value is NSNull) else { return nil }

if let bool = value as? Bool {
return bool
}

if let bool = (value as? StringFlexibleDecodable)?.asBool() {
return bool
}
throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value)
}

fileprivate func unbox(_ value: Any, as type: Int.Type) throws -> Int? {
guard !(value is NSNull) else { return nil }

guard let number = value as? NSNumber else {
guard let number = unboxAsNumber(value) else {
throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value)
}

Expand All @@ -1060,7 +1070,7 @@ extension __DictionaryDecoder {
fileprivate func unbox(_ value: Any, as type: Int8.Type) throws -> Int8? {
guard !(value is NSNull) else { return nil }

guard let number = value as? NSNumber else {
guard let number = unboxAsNumber(value) else {
throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value)
}

Expand All @@ -1075,7 +1085,7 @@ extension __DictionaryDecoder {
fileprivate func unbox(_ value: Any, as type: Int16.Type) throws -> Int16? {
guard !(value is NSNull) else { return nil }

guard let number = value as? NSNumber else {
guard let number = unboxAsNumber(value) else {
throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value)
}

Expand All @@ -1090,7 +1100,7 @@ extension __DictionaryDecoder {
fileprivate func unbox(_ value: Any, as type: Int32.Type) throws -> Int32? {
guard !(value is NSNull) else { return nil }

guard let number = value as? NSNumber else {
guard let number = unboxAsNumber(value) else {
throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value)
}

Expand All @@ -1105,7 +1115,7 @@ extension __DictionaryDecoder {
fileprivate func unbox(_ value: Any, as type: Int64.Type) throws -> Int64? {
guard !(value is NSNull) else { return nil }

guard let number = value as? NSNumber else {
guard let number = unboxAsNumber(value) else {
throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value)
}

Expand All @@ -1120,7 +1130,7 @@ extension __DictionaryDecoder {
fileprivate func unbox(_ value: Any, as type: UInt.Type) throws -> UInt? {
guard !(value is NSNull) else { return nil }

guard let number = value as? NSNumber else {
guard let number = unboxAsNumber(value) else {
throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value)
}

Expand All @@ -1135,7 +1145,7 @@ extension __DictionaryDecoder {
fileprivate func unbox(_ value: Any, as type: UInt8.Type) throws -> UInt8? {
guard !(value is NSNull) else { return nil }

guard let number = value as? NSNumber else {
guard let number = unboxAsNumber(value) else {
throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value)
}

Expand All @@ -1150,7 +1160,7 @@ extension __DictionaryDecoder {
fileprivate func unbox(_ value: Any, as type: UInt16.Type) throws -> UInt16? {
guard !(value is NSNull) else { return nil }

guard let number = value as? NSNumber else {
guard let number = unboxAsNumber(value) else {
throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value)
}

Expand All @@ -1165,7 +1175,7 @@ extension __DictionaryDecoder {
fileprivate func unbox(_ value: Any, as type: UInt32.Type) throws -> UInt32? {
guard !(value is NSNull) else { return nil }

guard let number = value as? NSNumber else {
guard let number = unboxAsNumber(value) else {
throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value)
}

Expand All @@ -1180,7 +1190,7 @@ extension __DictionaryDecoder {
fileprivate func unbox(_ value: Any, as type: UInt64.Type) throws -> UInt64? {
guard !(value is NSNull) else { return nil }

guard let number = value as? NSNumber else {
guard let number = unboxAsNumber(value) else {
throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value)
}

Expand All @@ -1195,7 +1205,7 @@ extension __DictionaryDecoder {
fileprivate func unbox(_ value: Any, as type: Float.Type) throws -> Float? {
guard !(value is NSNull) else { return nil }

if let number = value as? NSNumber {
if let number = unboxAsNumber(value) {
// We are willing to return a Float by losing precision:
// * If the original value was integral,
// * and the integral value was > Float.greatestFiniteMagnitude, we will fail
Expand Down Expand Up @@ -1242,7 +1252,7 @@ extension __DictionaryDecoder {
fileprivate func unbox(_ value: Any, as type: Double.Type) throws -> Double? {
guard !(value is NSNull) else { return nil }

if let number = value as? NSNumber {
if let number = unboxAsNumber(value) {
// We are always willing to return the number as a Double:
// * If the original value was integral, it is guaranteed to fit in a Double; we are willing to lose precision past 2^53 if you encoded a UInt64 but requested a Double
// * If it was a Float or Double, you will get back the precise value
Expand Down Expand Up @@ -1278,11 +1288,13 @@ extension __DictionaryDecoder {
fileprivate func unbox(_ value: Any, as type: String.Type) throws -> String? {
guard !(value is NSNull) else { return nil }

guard let string = value as? String else {
throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value)
if let string = value as? String {
return string
}

return string
if let string = (value as? StringFlexibleDecodable)?.asString() {
return string
}
throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value)
}

fileprivate func unbox(_ value: Any, as type: Date.Type) throws -> Date? {
Expand Down Expand Up @@ -1450,3 +1462,23 @@ internal extension DecodingError {
}
}
}

internal struct StringFlexibleDecodable {
let value: String

init(_ string: String) {
self.value = string
}

func asBool() -> Bool? {
return Bool(value)
}

func asString() -> String {
return value
}

func asNumber() -> NSNumber? {
return NumberFormatter().number(from: value)
}
}
8 changes: 1 addition & 7 deletions Sources/SotoCore/Message/AWSResponse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -169,13 +169,7 @@ public struct AWSResponse {
outputDict[headerParams[index].key] = value
continue
}
if let number = Double(stringValue) {
outputDict[headerParams[index].key] = number.truncatingRemainder(dividingBy: 1) == 0 ? Int(number) : number
} else if let boolean = Bool(stringValue) {
outputDict[headerParams[index].key] = boolean
} else {
outputDict[headerParams[index].key] = stringValue
}
outputDict[headerParams[index].key] = StringFlexibleDecodable(stringValue)
}
}
// add status code to output dictionary
Expand Down
40 changes: 39 additions & 1 deletion Tests/SotoCoreTests/AWSResponseTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,44 @@ class AWSResponseTests: XCTestCase {
XCTAssertEqual(jsonResult?.h, "test-header")
}

func testHeaderResponseTypeDecoding() {
struct Output: AWSDecodableShape {
static let _encoding = [
AWSMemberEncoding(label: "string", location: .header(locationName: "string")),
AWSMemberEncoding(label: "string2", location: .header(locationName: "string2")),
AWSMemberEncoding(label: "double", location: .header(locationName: "double")),
AWSMemberEncoding(label: "integer", location: .header(locationName: "integer")),
AWSMemberEncoding(label: "bool", location: .header(locationName: "bool")),
]
let string: String
let string2: String
let double: Double
let integer: Int
let bool: Bool
}
let response = AWSHTTPResponseImpl(
status: .ok,
headers: [
"string": "test-header",
"string2": "23",
"double": "3.14",
"integer": "901",
"bool": "false",
]
)

// JSON
var awsJSONResponse: AWSResponse?
var jsonResult: Output?
XCTAssertNoThrow(awsJSONResponse = try AWSResponse(from: response, serviceProtocol: .restjson, raw: false))
XCTAssertNoThrow(jsonResult = try awsJSONResponse?.generateOutputShape(operation: "Test"))
XCTAssertEqual(jsonResult?.string, "test-header")
XCTAssertEqual(jsonResult?.string2, "23")
XCTAssertEqual(jsonResult?.double, 3.14)
XCTAssertEqual(jsonResult?.integer, 901)
XCTAssertEqual(jsonResult?.bool, false)
}

func testStatusCodeResponseDecoding() {
struct Output: AWSDecodableShape {
static let _encoding = [AWSMemberEncoding(label: "status", location: .statusCode)]
Expand Down Expand Up @@ -370,7 +408,7 @@ class AWSResponseTests: XCTestCase {
let headers: HTTPHeaders
let body: ByteBuffer?

init(status: HTTPResponseStatus, headers: HTTPHeaders, body: ByteBuffer?) {
init(status: HTTPResponseStatus, headers: HTTPHeaders, body: ByteBuffer? = nil) {
self.status = status
self.headers = headers
self.body = body
Expand Down

0 comments on commit ce219fe

Please sign in to comment.