Skip to content

Commit

Permalink
Merge pull request #113 from guilhermearaujo/decodable
Browse files Browse the repository at this point in the history
Add response decoding using Decodable
  • Loading branch information
vadymmarkov authored Dec 7, 2018
2 parents cdb400e + fdea449 commit dbea29c
Show file tree
Hide file tree
Showing 5 changed files with 139 additions and 1 deletion.
16 changes: 16 additions & 0 deletions Malibu.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -247,6 +253,8 @@
/* End PBXContainerItemProxy section */

/* Begin PBXFileReference section */
792501B321B7F9DB000FC5DB /* ResponseDecoding.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResponseDecoding.swift; sourceTree = "<group>"; };
792501B521B7FB9E000FC5DB /* ResponseDecodingSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResponseDecodingSpec.swift; sourceTree = "<group>"; };
D500FD111C3AABED00782D78 /* Playground-iOS.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = "Playground-iOS.playground"; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
D503D38C1C8DA7730009BDAD /* Serializing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Serializing.swift; sourceTree = "<group>"; };
D503D38F1C8DA95B0009BDAD /* JsonSerializer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JsonSerializer.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -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 */,
Expand All @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand All @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand All @@ -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 */,
Expand Down
80 changes: 80 additions & 0 deletions MalibuTests/Specs/Response/ResponseDecodingSpec.swift
Original file line number Diff line number Diff line change
@@ -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())
}
}
}
}
}
}
}
}
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions Sources/NetworkError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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: "")
Expand Down
18 changes: 18 additions & 0 deletions Sources/Response/ResponseDecoding.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import Foundation
import When

// MARK: - Decoding

public extension Promise where T: Response {
func decode<U: Decodable>(using type: U.Type, decoder: JSONDecoder) -> Promise<U> {
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)
}
}
}
}

0 comments on commit dbea29c

Please sign in to comment.