diff --git a/Malibu.xcodeproj/project.pbxproj b/Malibu.xcodeproj/project.pbxproj index 2d80693..2125d86 100644 --- a/Malibu.xcodeproj/project.pbxproj +++ b/Malibu.xcodeproj/project.pbxproj @@ -7,6 +7,12 @@ objects = { /* Begin PBXBuildFile section */ + 792501B421B7F9DB000FC5DB /* ResponseDecoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 792501B321B7F9DB000FC5DB /* ResponseDecoding.swift */; }; + 792501BA21B8068B000FC5DB /* ResponseDecoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 792501B321B7F9DB000FC5DB /* ResponseDecoding.swift */; }; + 792501BB21B8068C000FC5DB /* ResponseDecoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 792501B321B7F9DB000FC5DB /* ResponseDecoding.swift */; }; + 792501BC21B806A3000FC5DB /* ResponseDecodingSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 792501B521B7FB9E000FC5DB /* ResponseDecodingSpec.swift */; }; + 792501BD21B806A3000FC5DB /* ResponseDecodingSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 792501B521B7FB9E000FC5DB /* ResponseDecodingSpec.swift */; }; + 792501BE21B806A4000FC5DB /* ResponseDecodingSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 792501B521B7FB9E000FC5DB /* ResponseDecodingSpec.swift */; }; D503D38D1C8DA7730009BDAD /* Serializing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D503D38C1C8DA7730009BDAD /* Serializing.swift */; }; D503D38E1C8DA7730009BDAD /* Serializing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D503D38C1C8DA7730009BDAD /* Serializing.swift */; }; D503D3901C8DA95B0009BDAD /* JsonSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D503D38F1C8DA95B0009BDAD /* JsonSerializer.swift */; }; @@ -247,6 +253,8 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 792501B321B7F9DB000FC5DB /* ResponseDecoding.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResponseDecoding.swift; sourceTree = ""; }; + 792501B521B7FB9E000FC5DB /* ResponseDecodingSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResponseDecodingSpec.swift; sourceTree = ""; }; D500FD111C3AABED00782D78 /* Playground-iOS.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = "Playground-iOS.playground"; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; D503D38C1C8DA7730009BDAD /* Serializing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Serializing.swift; sourceTree = ""; }; D503D38F1C8DA95B0009BDAD /* JsonSerializer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JsonSerializer.swift; sourceTree = ""; }; @@ -520,6 +528,7 @@ D5B965161C922BDC0099D2F9 /* MimeType.swift */, D5B965171C922BDC0099D2F9 /* Response.swift */, D5B965181C922BDC0099D2F9 /* ResponseSerialization.swift */, + 792501B321B7F9DB000FC5DB /* ResponseDecoding.swift */, D5B965191C922BDC0099D2F9 /* ResponseValidation.swift */, D5779BC41D991C8F00123D98 /* Mock.swift */, D5779BCA1D991D4B00123D98 /* ResponseHandler.swift */, @@ -533,6 +542,7 @@ D5B965261C922C420099D2F9 /* MimeTypeSpec.swift */, D5B965281C922C420099D2F9 /* ResponseSpec.swift */, D5B965271C922C420099D2F9 /* ResponseSerializationSpec.swift */, + 792501B521B7FB9E000FC5DB /* ResponseDecodingSpec.swift */, D5B965291C922C420099D2F9 /* ResponseValidationSpec.swift */, D5779BCD1D991EBA00123D98 /* ResponseHandlerSpec.swift */, D5779BD11D99203E00123D98 /* MockSpec.swift */, @@ -1054,6 +1064,7 @@ D51FC8821F73016300D722AD /* ContentTypeValidator.swift in Sources */, D51FC85E1F73016300D722AD /* Malibu.swift in Sources */, D51FC8621F73016300D722AD /* JsonEncoder.swift in Sources */, + 792501BB21B8068C000FC5DB /* ResponseDecoding.swift in Sources */, D51FC8601F73016300D722AD /* Networking.swift in Sources */, D51FC86D1F73016300D722AD /* Method.swift in Sources */, D51FC86F1F73016300D722AD /* SessionConfiguration.swift in Sources */, @@ -1077,6 +1088,7 @@ D51FC8911F7301C300D722AD /* QueryBuilderSpec.swift in Sources */, D51FC88A1F7301C300D722AD /* NetworkErrorSpec.swift in Sources */, D51FC8981F7301C300D722AD /* RequestSpec.swift in Sources */, + 792501BE21B806A4000FC5DB /* ResponseDecodingSpec.swift in Sources */, D51FC8901F7301C300D722AD /* UtilsSpec.swift in Sources */, D51FC8931F7301C300D722AD /* AsynchronousOperationSpec.swift in Sources */, D51FC89D1F7301C300D722AD /* ResponseSpec.swift in Sources */, @@ -1120,6 +1132,7 @@ D503D38D1C8DA7730009BDAD /* Serializing.swift in Sources */, D5B964F51C92125E0099D2F9 /* Request.swift in Sources */, D5B965111C922BCF0099D2F9 /* SessionConfiguration.swift in Sources */, + 792501B421B7F9DB000FC5DB /* ResponseDecoding.swift in Sources */, D5B9650F1C922BCF0099D2F9 /* Method.swift in Sources */, D5ABDDAA1CD78BC700C78B5F /* MultipartFormEncoder.swift in Sources */, DAA50FF11C97738400D01F78 /* Header.swift in Sources */, @@ -1159,6 +1172,7 @@ D598662F1D9DA42C00D4D90F /* RequestStorageSpec.swift in Sources */, D5B9653C1C92340C0099D2F9 /* UtilsSpec.swift in Sources */, DAEE2BB71C7F8C5D0000FB12 /* JsonEncoderSpec.swift in Sources */, + 792501BC21B806A3000FC5DB /* ResponseDecodingSpec.swift in Sources */, D5779BD21D99203E00123D98 /* MockSpec.swift in Sources */, D5B9652C1C922C420099D2F9 /* ResponseSerializationSpec.swift in Sources */, D5B964EE1C916CBF0099D2F9 /* Mocks.swift in Sources */, @@ -1218,6 +1232,7 @@ D5B9651F1C922BDC0099D2F9 /* ResponseSerialization.swift in Sources */, D5EB7C611CB01E73003A3BA9 /* RequestPolicies.swift in Sources */, DA5B4CF91C8E5936004E21ED /* NetworkError.swift in Sources */, + 792501BA21B8068B000FC5DB /* ResponseDecoding.swift in Sources */, D5A3DC891CD164750084F451 /* Logging.swift in Sources */, D5445A0B1E3CE16F00445406 /* RequestConvertible.swift in Sources */, D5B9650E1C922BCF0099D2F9 /* ContentType.swift in Sources */, @@ -1241,6 +1256,7 @@ D59866301D9DA42C00D4D90F /* RequestStorageSpec.swift in Sources */, D5B9653D1C92340C0099D2F9 /* UtilsSpec.swift in Sources */, DAEE2BB81C7F8C5E0000FB12 /* JsonEncoderSpec.swift in Sources */, + 792501BD21B806A3000FC5DB /* ResponseDecodingSpec.swift in Sources */, D5779BD31D99203E00123D98 /* MockSpec.swift in Sources */, D5B9652D1C922C420099D2F9 /* ResponseSerializationSpec.swift in Sources */, D5B964EF1C916CBF0099D2F9 /* Mocks.swift in Sources */, diff --git a/MalibuTests/Specs/Response/ResponseDecodingSpec.swift b/MalibuTests/Specs/Response/ResponseDecodingSpec.swift new file mode 100644 index 0000000..872b6ec --- /dev/null +++ b/MalibuTests/Specs/Response/ResponseDecodingSpec.swift @@ -0,0 +1,80 @@ +@testable import Malibu +import When +import Quick +import Nimble + +final class ResponseDecodingSpec: QuickSpec, NetworkPromiseSpec { + var networkPromise: NetworkPromise! + var request: URLRequest! + var data: Data! + + override func spec() { + describe("ResponseDecoding") { + let url = URL(string: "http://api.loc")! + let response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: "HTTP/2.0", headerFields: nil)! + let decoder = JSONDecoder() + + // MARK: - Specs + + beforeEach { + self.networkPromise = NetworkPromise() + self.request = URLRequest(url: URL(string: "http://api.loc")!) + self.data = try! JSONSerialization.data( + withJSONObject: [["name": "Taylor"]], + options: JSONSerialization.WritingOptions() + ) + } + + describe("#decode") { + var promise: Promise<[Int]>! + + beforeEach { + promise = self.networkPromise.decode(using: [Int].self, decoder: decoder) + } + + context("when response is rejected") { + it("rejects promise with an error") { + self.testFailedResponse(promise) + } + } + + context("when response is resolved") { + context("when decoding fails") { + it("rejects promise with an error") { + self.data = "string".data(using: String.Encoding.utf32) + let response = self.makeResponse(statusCode: 200, data: self.data) + + self.testFailedPromise( + promise, + error: NetworkError.responseDecodingFailed(type: [Int].self, response: response), + response: response.httpUrlResponse) + } + } + + context("when there is no data in response") { + it("rejects promise with an error") { + self.data = Data() + + self.testFailedPromise( + promise, + error: NetworkError.noDataInResponse, + response: response + ) + } + } + + context("when decoding succeeded") { + it("resolves promise with the specified type") { + let string = "[1,2,3]" + self.data = string.data(using: String.Encoding.utf8) + + self.testSucceededPromise(promise, response: response) { result in + expect(result == [1, 2, 3]).to(beTrue()) + } + } + } + } + } + } + } +} diff --git a/README.md b/README.md index 0e6bf53..21aada0 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,8 @@ past. Enjoy the ride! * [Backfoot surfer](#backfoot-surfer) * [Response](#response) * [Serialization](#serialization) - * [Validation](#validation) + * [Validation](#validation) + * [Decoding](#decoding) * [Logging](#logging) * [Installation](#installation) * [Author](#author) @@ -547,6 +548,26 @@ networking.request(.fetchBoards).validate(statusCodes: [200]) networking.request(.fetchBoards).validate(using: CustomValidator()) ``` +### Decoding + +**Malibu** is able to convert the response body into models that conform to `Decodable`: + +```swift +// Declare your model conforming to `Decodable` protocol +struct User: Decodable { + let name: String + let dob: Date +} + +// Set up a `JSONDecoder` +let decoder = JSONDecoder() +decoder.keyDecodingStrategy = .convertFromSnakeCase +decoder.dateDecodingStrategy = .iso8601 + +// Decode your response body +networkPromise.decode(using: User.self, decoder: decoder) +``` + ## Logging If you want to see some request, response and error info in the console, you diff --git a/Sources/NetworkError.swift b/Sources/NetworkError.swift index adedae4..3c96cdd 100644 --- a/Sources/NetworkError.swift +++ b/Sources/NetworkError.swift @@ -11,6 +11,7 @@ public enum NetworkError: Error { case jsonArraySerializationFailed(response: Response) case jsonDictionarySerializationFailed(response: Response) case stringSerializationFailed(encoding: UInt, response: Response) + case responseDecodingFailed(type: Decodable.Type, response: Response) public var reason: String { var text: String @@ -36,6 +37,8 @@ public enum NetworkError: Error { text = "No JSON dictionary in response data" case .stringSerializationFailed(let encoding, _): text = "String could not be serialized with encoding: \(encoding)" + case .responseDecodingFailed(let type, _): + text = "Response could not be decoded into \(type)" } return NSLocalizedString(text, comment: "") diff --git a/Sources/Response/ResponseDecoding.swift b/Sources/Response/ResponseDecoding.swift new file mode 100644 index 0000000..7a1ac7a --- /dev/null +++ b/Sources/Response/ResponseDecoding.swift @@ -0,0 +1,18 @@ +import Foundation +import When + +// MARK: - Decoding + +public extension Promise where T: Response { + func decode(using type: U.Type, decoder: JSONDecoder) -> Promise { + return then { response -> U in + guard !response.data.isEmpty else { throw NetworkError.noDataInResponse } + + do { + return try decoder.decode(type, from: response.data) + } catch { + throw NetworkError.responseDecodingFailed(type: type, response: response) + } + } + } +}