From 3d6cc2fc9e34d186cb48fdc5100f6505e300152f Mon Sep 17 00:00:00 2001 From: Jeroen Bakker Date: Fri, 15 Dec 2017 14:59:13 +0100 Subject: [PATCH 1/9] Support for Decodable failsOnEmptyData --- Sources/Moya/Response.swift | 21 ++++++-- .../SignalProducer+Response.swift | 4 +- Sources/RxMoya/Observable+Response.swift | 4 +- Sources/RxMoya/Single+Response.swift | 4 +- Tests/Observable+MoyaSpec.swift | 50 +++++++++++++++++++ Tests/SignalProducer+MoyaSpec.swift | 50 +++++++++++++++++++ Tests/Single+MoyaSpec.swift | 50 +++++++++++++++++++ Tests/TestHelpers.swift | 6 +++ 8 files changed, 179 insertions(+), 10 deletions(-) diff --git a/Sources/Moya/Response.swift b/Sources/Moya/Response.swift index e2d62bdb9..5b464d249 100644 --- a/Sources/Moya/Response.swift +++ b/Sources/Moya/Response.swift @@ -121,7 +121,7 @@ public extension Response { /// /// - parameter atKeyPath: Optional key path at which to parse object. /// - parameter using: A `JSONDecoder` instance which is used to decode data to an object. - func map(_ type: D.Type, atKeyPath keyPath: String? = nil, using decoder: JSONDecoder = JSONDecoder()) throws -> D { + func map(_ type: D.Type, atKeyPath keyPath: String? = nil, using decoder: JSONDecoder = JSONDecoder(), failsOnEmptyData: Bool = true) throws -> D { let serializeToData: (Any) throws -> Data? = { (jsonObject) in guard JSONSerialization.isValidJSONObject(jsonObject) else { return nil @@ -133,9 +133,14 @@ public extension Response { } } let jsonData: Data - if let keyPath = keyPath { - guard let jsonObject = (try mapJSON() as? NSDictionary)?.value(forKeyPath: keyPath) else { - throw MoyaError.jsonMapping(self) + keyPathCheck: if let keyPath = keyPath { + guard let jsonObject = (try mapJSON(failsOnEmptyData: failsOnEmptyData) as? NSDictionary)?.value(forKeyPath: keyPath) else { + if failsOnEmptyData { + throw MoyaError.jsonMapping(self) + } else { + jsonData = data + break keyPathCheck + } } if let data = try serializeToData(jsonObject) { @@ -157,7 +162,15 @@ public extension Response { } else { jsonData = data } + do { + if jsonData.count < 1 && !failsOnEmptyData { + if let emptyJSONObjectData = "{}".data(using: .utf8), let emptyDecodableValue = try? decoder.decode(D.self, from: emptyJSONObjectData) { + return emptyDecodableValue + } else if let emptyJSONArrayData = "[{}]".data(using: .utf8), let emptyDecodableValue = try? decoder.decode(D.self, from: emptyJSONArrayData) { + return emptyDecodableValue + } + } return try decoder.decode(D.self, from: jsonData) } catch let error { throw MoyaError.objectMapping(error, self) diff --git a/Sources/ReactiveMoya/SignalProducer+Response.swift b/Sources/ReactiveMoya/SignalProducer+Response.swift index 6fe036d4e..2fbc7633d 100644 --- a/Sources/ReactiveMoya/SignalProducer+Response.swift +++ b/Sources/ReactiveMoya/SignalProducer+Response.swift @@ -54,9 +54,9 @@ extension SignalProducerProtocol where Value == Response, Error == MoyaError { } /// Maps received data at key path into a Decodable object. If the conversion fails, the signal errors. - public func map(_ type: D.Type, atKeyPath keyPath: String? = nil, using decoder: JSONDecoder = JSONDecoder()) -> SignalProducer { + public func map(_ type: D.Type, atKeyPath keyPath: String? = nil, using decoder: JSONDecoder = JSONDecoder(), failsOnEmptyData: Bool = true) -> SignalProducer { return producer.flatMap(.latest) { response -> SignalProducer in - return unwrapThrowable { try response.map(type, atKeyPath: keyPath, using: decoder) } + return unwrapThrowable { try response.map(type, atKeyPath: keyPath, using: decoder, failsOnEmptyData: failsOnEmptyData) } } } } diff --git a/Sources/RxMoya/Observable+Response.swift b/Sources/RxMoya/Observable+Response.swift index 5138451e4..b8608147e 100644 --- a/Sources/RxMoya/Observable+Response.swift +++ b/Sources/RxMoya/Observable+Response.swift @@ -54,9 +54,9 @@ extension ObservableType where E == Response { } /// Maps received data at key path into a Decodable object. If the conversion fails, the signal errors. - public func map(_ type: D.Type, atKeyPath keyPath: String? = nil, using decoder: JSONDecoder = JSONDecoder()) -> Observable { + public func map(_ type: D.Type, atKeyPath keyPath: String? = nil, using decoder: JSONDecoder = JSONDecoder(), failsOnEmptyData: Bool = true) -> Observable { return flatMap { response -> Observable in - return Observable.just(try response.map(type, atKeyPath: keyPath, using: decoder)) + return Observable.just(try response.map(type, atKeyPath: keyPath, using: decoder, failsOnEmptyData: failsOnEmptyData)) } } } diff --git a/Sources/RxMoya/Single+Response.swift b/Sources/RxMoya/Single+Response.swift index 06642587e..d3c732517 100644 --- a/Sources/RxMoya/Single+Response.swift +++ b/Sources/RxMoya/Single+Response.swift @@ -54,9 +54,9 @@ extension PrimitiveSequence where TraitType == SingleTrait, ElementType == Respo } /// Maps received data at key path into a Decodable object. If the conversion fails, the signal errors. - public func map(_ type: D.Type, atKeyPath keyPath: String? = nil, using decoder: JSONDecoder = JSONDecoder()) -> Single { + public func map(_ type: D.Type, atKeyPath keyPath: String? = nil, using decoder: JSONDecoder = JSONDecoder(), failsOnEmptyData: Bool = true) -> Single { return flatMap { response -> Single in - return Single.just(try response.map(type, atKeyPath: keyPath, using: decoder)) + return Single.just(try response.map(type, atKeyPath: keyPath, using: decoder, failsOnEmptyData: failsOnEmptyData)) } } } diff --git a/Tests/Observable+MoyaSpec.swift b/Tests/Observable+MoyaSpec.swift index 1e6eb894d..933366981 100644 --- a/Tests/Observable+MoyaSpec.swift +++ b/Tests/Observable+MoyaSpec.swift @@ -309,6 +309,31 @@ class ObservableMoyaSpec: QuickSpec { expect(receivedObjects?.count) == 3 expect(receivedObjects?.map { $0.title }) == ["Hello, Moya!", "Hello, Moya!", "Hello, Moya!"] } + + it("maps empty data to a decodable object with optional properties") { + let observable = Response(statusCode: 200, data: Data()).asObservable() + + var receivedObjects: OptionalIssue? + _ = observable.map(OptionalIssue.self, using: decoder, failsOnEmptyData: false).subscribe(onNext: { object in + receivedObjects = object + }) + expect(receivedObjects).notTo(beNil()) + expect(receivedObjects?.title).to(beNil()) + expect(receivedObjects?.createdAt).to(beNil()) + } + + it("maps empty data to a decodable array with optional properties") { + let observable = Response(statusCode: 200, data: Data()).asObservable() + + var receivedObjects: [OptionalIssue]? + _ = observable.map([OptionalIssue].self, using: decoder, failsOnEmptyData: false).subscribe(onNext: { object in + receivedObjects = object + }) + expect(receivedObjects).notTo(beNil()) + expect(receivedObjects?.count) == 1 + expect(receivedObjects?.first?.title).to(beNil()) + expect(receivedObjects?.first?.createdAt).to(beNil()) + } context("when using key path mapping") { it("maps data representing a json to a decodable object") { @@ -343,6 +368,31 @@ class ObservableMoyaSpec: QuickSpec { expect(receivedObjects?.first?.title) == "Hello, Moya!" expect(receivedObjects?.first?.createdAt) == formatter.date(from: "1995-01-14T12:34:56")! } + + it("maps empty data to a decodable object with optional properties") { + let observable = Response(statusCode: 200, data: Data()).asObservable() + + var receivedObjects: OptionalIssue? + _ = observable.map(OptionalIssue.self, atKeyPath: "issue", using: decoder, failsOnEmptyData: false).subscribe(onNext: { object in + receivedObjects = object + }) + expect(receivedObjects).notTo(beNil()) + expect(receivedObjects?.title).to(beNil()) + expect(receivedObjects?.createdAt).to(beNil()) + } + + it("maps empty data to a decodable array with optional properties") { + let observable = Response(statusCode: 200, data: Data()).asObservable() + + var receivedObjects: [OptionalIssue]? + _ = observable.map([OptionalIssue].self, atKeyPath: "issue", using: decoder, failsOnEmptyData: false).subscribe(onNext: { object in + receivedObjects = object + }) + expect(receivedObjects).notTo(beNil()) + expect(receivedObjects?.count) == 1 + expect(receivedObjects?.first?.title).to(beNil()) + expect(receivedObjects?.first?.createdAt).to(beNil()) + } it("map Int data to an Int value") { let json: [String: Any] = ["count": 1] diff --git a/Tests/SignalProducer+MoyaSpec.swift b/Tests/SignalProducer+MoyaSpec.swift index f13669217..e66f1e651 100644 --- a/Tests/SignalProducer+MoyaSpec.swift +++ b/Tests/SignalProducer+MoyaSpec.swift @@ -294,6 +294,31 @@ class SignalProducerMoyaSpec: QuickSpec { expect(receivedObjects?.count) == 3 expect(receivedObjects?.map { $0.title }) == ["Hello, Moya!", "Hello, Moya!", "Hello, Moya!"] } + + it("maps empty data to a decodable object with optional properties") { + let signal = signalSendingData(Data()) + + var receivedObjects: OptionalIssue? + _ = signal.map(OptionalIssue.self, using: decoder, failsOnEmptyData: false).startWithResult { result in + receivedObjects = result.value + } + expect(receivedObjects).notTo(beNil()) + expect(receivedObjects?.title).to(beNil()) + expect(receivedObjects?.createdAt).to(beNil()) + } + + it("maps empty data to a decodable array with optional properties") { + let signal = signalSendingData(Data()) + + var receivedObjects: [OptionalIssue]? + _ = signal.map([OptionalIssue].self, using: decoder, failsOnEmptyData: false).startWithResult { result in + receivedObjects = result.value + } + expect(receivedObjects).notTo(beNil()) + expect(receivedObjects?.count) == 1 + expect(receivedObjects?.first?.title).to(beNil()) + expect(receivedObjects?.first?.createdAt).to(beNil()) + } context("when using key path mapping") { it("maps data representing a json to a decodable object") { @@ -328,6 +353,31 @@ class SignalProducerMoyaSpec: QuickSpec { expect(receivedObjects?.first?.title) == "Hello, Moya!" expect(receivedObjects?.first?.createdAt) == formatter.date(from: "1995-01-14T12:34:56")! } + + it("maps empty data to a decodable object with optional properties") { + let signal = signalSendingData(Data()) + + var receivedObjects: OptionalIssue? + _ = signal.map(OptionalIssue.self, atKeyPath: "issue", using: decoder, failsOnEmptyData: false).startWithResult { result in + receivedObjects = result.value + } + expect(receivedObjects).notTo(beNil()) + expect(receivedObjects?.title).to(beNil()) + expect(receivedObjects?.createdAt).to(beNil()) + } + + it("maps empty data to a decodable array with optional properties") { + let signal = signalSendingData(Data()) + + var receivedObjects: [OptionalIssue]? + _ = signal.map([OptionalIssue].self, atKeyPath: "issue", using: decoder, failsOnEmptyData: false).startWithResult { result in + receivedObjects = result.value + } + expect(receivedObjects).notTo(beNil()) + expect(receivedObjects?.count) == 1 + expect(receivedObjects?.first?.title).to(beNil()) + expect(receivedObjects?.first?.createdAt).to(beNil()) + } it("map Int data to an Int value") { let json: [String: Any] = ["count": 1] diff --git a/Tests/Single+MoyaSpec.swift b/Tests/Single+MoyaSpec.swift index 50ae5edf8..644a227fb 100644 --- a/Tests/Single+MoyaSpec.swift +++ b/Tests/Single+MoyaSpec.swift @@ -304,6 +304,31 @@ class SingleMoyaSpec: QuickSpec { expect(receivedObjects?.count) == 3 expect(receivedObjects?.map { $0.title }) == ["Hello, Moya!", "Hello, Moya!", "Hello, Moya!"] } + + it("maps empty data to a decodable object with optional properties") { + let single = Response(statusCode: 200, data: Data()).asSingle() + + var receivedObjects: OptionalIssue? + _ = single.map(OptionalIssue.self, using: decoder, failsOnEmptyData: false).subscribe(onSuccess: { object in + receivedObjects = object + }) + expect(receivedObjects).notTo(beNil()) + expect(receivedObjects?.title).to(beNil()) + expect(receivedObjects?.createdAt).to(beNil()) + } + + it("maps empty data to a decodable array with optional properties") { + let single = Response(statusCode: 200, data: Data()).asSingle() + + var receivedObjects: [OptionalIssue]? + _ = single.map([OptionalIssue].self, using: decoder, failsOnEmptyData: false).subscribe(onSuccess: { object in + receivedObjects = object + }) + expect(receivedObjects).notTo(beNil()) + expect(receivedObjects?.count) == 1 + expect(receivedObjects?.first?.title).to(beNil()) + expect(receivedObjects?.first?.createdAt).to(beNil()) + } context("when using key path mapping") { it("maps data representing a json to a decodable object") { @@ -338,6 +363,31 @@ class SingleMoyaSpec: QuickSpec { expect(receivedObjects?.first?.title) == "Hello, Moya!" expect(receivedObjects?.first?.createdAt) == formatter.date(from: "1995-01-14T12:34:56")! } + + it("maps empty data to a decodable object with optional properties") { + let single = Response(statusCode: 200, data: Data()).asSingle() + + var receivedObjects: OptionalIssue? + _ = single.map(OptionalIssue.self, atKeyPath: "issue", using: decoder, failsOnEmptyData: false).subscribe(onSuccess: { object in + receivedObjects = object + }) + expect(receivedObjects).notTo(beNil()) + expect(receivedObjects?.title).to(beNil()) + expect(receivedObjects?.createdAt).to(beNil()) + } + + it("maps empty data to a decodable array with optional properties") { + let single = Response(statusCode: 200, data: Data()).asSingle() + + var receivedObjects: [OptionalIssue]? + _ = single.map([OptionalIssue].self, atKeyPath: "issue", using: decoder, failsOnEmptyData: false).subscribe(onSuccess: { object in + receivedObjects = object + }) + expect(receivedObjects).notTo(beNil()) + expect(receivedObjects?.count) == 1 + expect(receivedObjects?.first?.title).to(beNil()) + expect(receivedObjects?.first?.createdAt).to(beNil()) + } it("map Int data to an Int value") { let json: [String: Any] = ["count": 1] diff --git a/Tests/TestHelpers.swift b/Tests/TestHelpers.swift index 434aaf1ab..0fb45559e 100644 --- a/Tests/TestHelpers.swift +++ b/Tests/TestHelpers.swift @@ -233,3 +233,9 @@ struct Issue: Codable { case rating } } + +// A fixture for testing optional Decodable mapping +struct OptionalIssue: Codable { + let title: String? + let createdAt: Date? +} From 48794d5d01df4a4be1740f46ef282184df7fac1d Mon Sep 17 00:00:00 2001 From: Jeroen Bakker Date: Fri, 15 Dec 2017 15:16:51 +0100 Subject: [PATCH 2/9] Disabled type body length from SwiftLint - Unit tests are all too long --- .swiftlint.yml | 1 + Tests/.swiftlint.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.swiftlint.yml b/.swiftlint.yml index fe285fe2f..c8fa5c8f2 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -1,6 +1,7 @@ disabled_rules: - line_length - type_name + - type_body_length opt_in_rules: - empty_count diff --git a/Tests/.swiftlint.yml b/Tests/.swiftlint.yml index 4cd6ae70e..bd95fec33 100644 --- a/Tests/.swiftlint.yml +++ b/Tests/.swiftlint.yml @@ -6,6 +6,7 @@ disabled_rules: - force_try - function_body_length - cyclomatic_complexity + - type_body_length opt_in_rules: - empty_count From e2125363a895dc873a81d7cd7abe868ceafca85f Mon Sep 17 00:00:00 2001 From: Jeroen Bakker Date: Fri, 15 Dec 2017 15:17:38 +0100 Subject: [PATCH 3/9] Disable SwiftLint type body check for Test target --- .swiftlint.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.swiftlint.yml b/.swiftlint.yml index c8fa5c8f2..fe285fe2f 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -1,7 +1,6 @@ disabled_rules: - line_length - type_name - - type_body_length opt_in_rules: - empty_count From 666e99e11f94b6b1ce800c68ba8891cec058a7ba Mon Sep 17 00:00:00 2001 From: Jeroen Bakker Date: Fri, 15 Dec 2017 15:23:24 +0100 Subject: [PATCH 4/9] Resolved Trailing Whitespaces warnings --- Sources/Moya/Response.swift | 1 - Tests/Observable+MoyaSpec.swift | 15 +++++++-------- Tests/SignalProducer+MoyaSpec.swift | 16 ++++++++-------- Tests/Single+MoyaSpec.swift | 16 ++++++++-------- 4 files changed, 23 insertions(+), 25 deletions(-) diff --git a/Sources/Moya/Response.swift b/Sources/Moya/Response.swift index 5b464d249..01920b83c 100644 --- a/Sources/Moya/Response.swift +++ b/Sources/Moya/Response.swift @@ -162,7 +162,6 @@ public extension Response { } else { jsonData = data } - do { if jsonData.count < 1 && !failsOnEmptyData { if let emptyJSONObjectData = "{}".data(using: .utf8), let emptyDecodableValue = try? decoder.decode(D.self, from: emptyJSONObjectData) { diff --git a/Tests/Observable+MoyaSpec.swift b/Tests/Observable+MoyaSpec.swift index 933366981..8ac3e2ad4 100644 --- a/Tests/Observable+MoyaSpec.swift +++ b/Tests/Observable+MoyaSpec.swift @@ -309,10 +309,9 @@ class ObservableMoyaSpec: QuickSpec { expect(receivedObjects?.count) == 3 expect(receivedObjects?.map { $0.title }) == ["Hello, Moya!", "Hello, Moya!", "Hello, Moya!"] } - it("maps empty data to a decodable object with optional properties") { let observable = Response(statusCode: 200, data: Data()).asObservable() - + var receivedObjects: OptionalIssue? _ = observable.map(OptionalIssue.self, using: decoder, failsOnEmptyData: false).subscribe(onNext: { object in receivedObjects = object @@ -321,10 +320,10 @@ class ObservableMoyaSpec: QuickSpec { expect(receivedObjects?.title).to(beNil()) expect(receivedObjects?.createdAt).to(beNil()) } - + it("maps empty data to a decodable array with optional properties") { let observable = Response(statusCode: 200, data: Data()).asObservable() - + var receivedObjects: [OptionalIssue]? _ = observable.map([OptionalIssue].self, using: decoder, failsOnEmptyData: false).subscribe(onNext: { object in receivedObjects = object @@ -368,10 +367,10 @@ class ObservableMoyaSpec: QuickSpec { expect(receivedObjects?.first?.title) == "Hello, Moya!" expect(receivedObjects?.first?.createdAt) == formatter.date(from: "1995-01-14T12:34:56")! } - + it("maps empty data to a decodable object with optional properties") { let observable = Response(statusCode: 200, data: Data()).asObservable() - + var receivedObjects: OptionalIssue? _ = observable.map(OptionalIssue.self, atKeyPath: "issue", using: decoder, failsOnEmptyData: false).subscribe(onNext: { object in receivedObjects = object @@ -380,10 +379,10 @@ class ObservableMoyaSpec: QuickSpec { expect(receivedObjects?.title).to(beNil()) expect(receivedObjects?.createdAt).to(beNil()) } - + it("maps empty data to a decodable array with optional properties") { let observable = Response(statusCode: 200, data: Data()).asObservable() - + var receivedObjects: [OptionalIssue]? _ = observable.map([OptionalIssue].self, atKeyPath: "issue", using: decoder, failsOnEmptyData: false).subscribe(onNext: { object in receivedObjects = object diff --git a/Tests/SignalProducer+MoyaSpec.swift b/Tests/SignalProducer+MoyaSpec.swift index e66f1e651..55ebfe12c 100644 --- a/Tests/SignalProducer+MoyaSpec.swift +++ b/Tests/SignalProducer+MoyaSpec.swift @@ -294,10 +294,10 @@ class SignalProducerMoyaSpec: QuickSpec { expect(receivedObjects?.count) == 3 expect(receivedObjects?.map { $0.title }) == ["Hello, Moya!", "Hello, Moya!", "Hello, Moya!"] } - + it("maps empty data to a decodable object with optional properties") { let signal = signalSendingData(Data()) - + var receivedObjects: OptionalIssue? _ = signal.map(OptionalIssue.self, using: decoder, failsOnEmptyData: false).startWithResult { result in receivedObjects = result.value @@ -306,10 +306,10 @@ class SignalProducerMoyaSpec: QuickSpec { expect(receivedObjects?.title).to(beNil()) expect(receivedObjects?.createdAt).to(beNil()) } - + it("maps empty data to a decodable array with optional properties") { let signal = signalSendingData(Data()) - + var receivedObjects: [OptionalIssue]? _ = signal.map([OptionalIssue].self, using: decoder, failsOnEmptyData: false).startWithResult { result in receivedObjects = result.value @@ -353,10 +353,10 @@ class SignalProducerMoyaSpec: QuickSpec { expect(receivedObjects?.first?.title) == "Hello, Moya!" expect(receivedObjects?.first?.createdAt) == formatter.date(from: "1995-01-14T12:34:56")! } - + it("maps empty data to a decodable object with optional properties") { let signal = signalSendingData(Data()) - + var receivedObjects: OptionalIssue? _ = signal.map(OptionalIssue.self, atKeyPath: "issue", using: decoder, failsOnEmptyData: false).startWithResult { result in receivedObjects = result.value @@ -365,10 +365,10 @@ class SignalProducerMoyaSpec: QuickSpec { expect(receivedObjects?.title).to(beNil()) expect(receivedObjects?.createdAt).to(beNil()) } - + it("maps empty data to a decodable array with optional properties") { let signal = signalSendingData(Data()) - + var receivedObjects: [OptionalIssue]? _ = signal.map([OptionalIssue].self, atKeyPath: "issue", using: decoder, failsOnEmptyData: false).startWithResult { result in receivedObjects = result.value diff --git a/Tests/Single+MoyaSpec.swift b/Tests/Single+MoyaSpec.swift index 644a227fb..1bcf60e08 100644 --- a/Tests/Single+MoyaSpec.swift +++ b/Tests/Single+MoyaSpec.swift @@ -304,10 +304,10 @@ class SingleMoyaSpec: QuickSpec { expect(receivedObjects?.count) == 3 expect(receivedObjects?.map { $0.title }) == ["Hello, Moya!", "Hello, Moya!", "Hello, Moya!"] } - + it("maps empty data to a decodable object with optional properties") { let single = Response(statusCode: 200, data: Data()).asSingle() - + var receivedObjects: OptionalIssue? _ = single.map(OptionalIssue.self, using: decoder, failsOnEmptyData: false).subscribe(onSuccess: { object in receivedObjects = object @@ -316,10 +316,10 @@ class SingleMoyaSpec: QuickSpec { expect(receivedObjects?.title).to(beNil()) expect(receivedObjects?.createdAt).to(beNil()) } - + it("maps empty data to a decodable array with optional properties") { let single = Response(statusCode: 200, data: Data()).asSingle() - + var receivedObjects: [OptionalIssue]? _ = single.map([OptionalIssue].self, using: decoder, failsOnEmptyData: false).subscribe(onSuccess: { object in receivedObjects = object @@ -363,10 +363,10 @@ class SingleMoyaSpec: QuickSpec { expect(receivedObjects?.first?.title) == "Hello, Moya!" expect(receivedObjects?.first?.createdAt) == formatter.date(from: "1995-01-14T12:34:56")! } - + it("maps empty data to a decodable object with optional properties") { let single = Response(statusCode: 200, data: Data()).asSingle() - + var receivedObjects: OptionalIssue? _ = single.map(OptionalIssue.self, atKeyPath: "issue", using: decoder, failsOnEmptyData: false).subscribe(onSuccess: { object in receivedObjects = object @@ -375,10 +375,10 @@ class SingleMoyaSpec: QuickSpec { expect(receivedObjects?.title).to(beNil()) expect(receivedObjects?.createdAt).to(beNil()) } - + it("maps empty data to a decodable array with optional properties") { let single = Response(statusCode: 200, data: Data()).asSingle() - + var receivedObjects: [OptionalIssue]? _ = single.map([OptionalIssue].self, atKeyPath: "issue", using: decoder, failsOnEmptyData: false).subscribe(onSuccess: { object in receivedObjects = object From 7a5c924a0d3367053483e892d9ebf8f51c951ffd Mon Sep 17 00:00:00 2001 From: Jeroen Bakker Date: Mon, 18 Dec 2017 18:22:08 +0100 Subject: [PATCH 5/9] Resolved Swiftlint warnings - Allow the file length to be 1000 before giving a warning for tests script only --- Changelog.md | 1 + Tests/.swiftlint.yml | 3 +++ Tests/TestHelpers.swift | 3 +-- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Changelog.md b/Changelog.md index 19b9dd887..580bffc59 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,6 +1,7 @@ # Next ### Added - **Breaking Change** Added a `.requestCustomJSONEncodable` case to `Task`. [#1443](https://github.com/Moya/Moya/pull/1443) by [@evgeny-sureev](https://github.com/evgeny-sureev). +- Added `failsOnEmptyData` boolean support for the `Decodable` map functions. [#1508](https://github.com/Moya/Moya/pull/1508) by [@jeroenbb94](https://github.com/Jeroenbb94). ### Changed - **Breaking Change** Updated minimum version of `ReactiveSwift` to 3.0. diff --git a/Tests/.swiftlint.yml b/Tests/.swiftlint.yml index bd95fec33..928e50fb6 100644 --- a/Tests/.swiftlint.yml +++ b/Tests/.swiftlint.yml @@ -10,3 +10,6 @@ disabled_rules: opt_in_rules: - empty_count + +file_length: + warning: 1000 \ No newline at end of file diff --git a/Tests/TestHelpers.swift b/Tests/TestHelpers.swift index 0fb45559e..6535dd281 100644 --- a/Tests/TestHelpers.swift +++ b/Tests/TestHelpers.swift @@ -51,8 +51,7 @@ extension GitHub: TargetType { } extension GitHub: Equatable { - - static func ==(lhs: GitHub, rhs: GitHub) -> Bool { + static func == (lhs: GitHub, rhs: GitHub) -> Bool { switch (lhs, rhs) { case (.zen, .zen): return true case let (.userProfile(username1), .userProfile(username2)): return username1 == username2 From 4808f3e21f353cbf2c06b1375390cb14066b9aad Mon Sep 17 00:00:00 2001 From: Jeroen Bakker Date: Mon, 18 Dec 2017 18:25:29 +0100 Subject: [PATCH 6/9] Resolved sunshinejr commits in the PR --- Contributing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Contributing.md b/Contributing.md index 87a6de563..92d59a989 100644 --- a/Contributing.md +++ b/Contributing.md @@ -2,7 +2,7 @@ As the creators, and maintainers of this project, we want to ensure that the project lives and continues to grow. Not blocked by any singular person's computer time. One of the simplest ways of doing this is by encouraging a larger set of shallow contributors. Through this, we hope to mitigate the problems of a project that needs updates but there's no-one who has the power to do so. -#### Development Process +#### Develop Process We maintain two permanent, protected branches: `master` and `develop`. From 326f9d137b977ec44e5b14316bf29dccbca61521 Mon Sep 17 00:00:00 2001 From: Jeroen Bakker Date: Mon, 18 Dec 2017 18:27:08 +0100 Subject: [PATCH 7/9] Typo fix --- Contributing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Contributing.md b/Contributing.md index 92d59a989..87a6de563 100644 --- a/Contributing.md +++ b/Contributing.md @@ -2,7 +2,7 @@ As the creators, and maintainers of this project, we want to ensure that the project lives and continues to grow. Not blocked by any singular person's computer time. One of the simplest ways of doing this is by encouraging a larger set of shallow contributors. Through this, we hope to mitigate the problems of a project that needs updates but there's no-one who has the power to do so. -#### Develop Process +#### Development Process We maintain two permanent, protected branches: `master` and `develop`. From 4619147a09ff1e8fa12e085b84ff63b29ea2cb24 Mon Sep 17 00:00:00 2001 From: Jeroen Bakker Date: Wed, 20 Dec 2017 09:19:28 +0100 Subject: [PATCH 8/9] Added docs for the new property failsOnEmptyData on map --- docs/Examples/Response.md | 70 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/docs/Examples/Response.md b/docs/Examples/Response.md index 21afc9027..843180650 100644 --- a/docs/Examples/Response.md +++ b/docs/Examples/Response.md @@ -214,8 +214,9 @@ struct User: Decodable { } ``` -Moya allows us to easily get our `User` from the response with the `map(_: D.Type, atKeyPath: String?, using: JSONDecoder)` extension. +Moya allows us to easily get our `User` from the response with the `map(_: D.Type, atKeyPath: String?, using: JSONDecoder, failsOnEmptyData: Bool)` extension. Both `atKeyPath` and `using` are optional, meaning in most cases you'll use `map(_:)`. +The `failsOnEmptyData` property (default: true), describes whether it should throw an error when data is empty or simply return `Decodable` initialized with nil (note: your object must allow optionals or you'll still get thrown an error). A basic example would be: ```swift @@ -337,3 +338,70 @@ provider.rx.request(.users) } } ``` + +The above assumes your backend always returns data and if it doesn't, throwns an error. +But if you don't want to receive an error, we can set `failsOnEmptyData` to false. + +The data returned looks like this: + +```json +{ + "users": [] +} +``` + +Our updated `User` type looks like this: + +```swift +struct User: Decodable { + let id: String? + let firstName: String? + let lastName: String? + let updated: Date? +} +``` + +Our handling of the result now has to do slightly more: + +```swift +provider.request(.user) { result in + switch result { + case let .success(moyaResponse): + do { + let filteredResponse = try moyaResponse.filterSuccessfulStatusCodes() + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .secondsSince1970 + let users = try filteredResponse.map([User].self, atKeyPath: "users", using: decoder, failsOnEmptyData: false) // user is of type [User] + // Because the failsOnEmptyData is false and our user object allows optional, our array got initialized with an empty User object + // Do something with your users. + } + catch let error { + // Here we get either statusCode error or objectMapping error. + // TODO: handle the error == best. comment. ever. + } + case let .failure(error): + // TODO: handle the error == best. comment. ever. + } +} +``` + +In `RxSwift` this could look something like: + +```swift +let decoder = JSONDecoder() +decoder.dateDecodingStrategy = .secondsSince1970 +provider.rx.request(.users) + .filterSuccessfulStatusCodes() + .map([User].self, atKeyPath: "users", using: decoder, failsOnEmptyData: false) + .subscribe { event in + switch event { + case .success(let users): + // Notice that now we do not get a Response object anymore but rather an array of User objects + // Because the failsOnEmptyData is false and our user object allows optional, our array got initialized with an empty User object + // do something with the user + case .error(let error): + // handle the error, which can be an underlying error, a status code error, or an object mapping error + } + } +} +``` \ No newline at end of file From 9d514ab122cc2f0044b97ae7c66bc79d8a30b191 Mon Sep 17 00:00:00 2001 From: Jeroen Bakker Date: Wed, 20 Dec 2017 09:30:05 +0100 Subject: [PATCH 9/9] Added Breaking Change to the changeling Thx to SD10 --- Changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Changelog.md b/Changelog.md index 580bffc59..ffca59114 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,7 +1,7 @@ # Next ### Added - **Breaking Change** Added a `.requestCustomJSONEncodable` case to `Task`. [#1443](https://github.com/Moya/Moya/pull/1443) by [@evgeny-sureev](https://github.com/evgeny-sureev). -- Added `failsOnEmptyData` boolean support for the `Decodable` map functions. [#1508](https://github.com/Moya/Moya/pull/1508) by [@jeroenbb94](https://github.com/Jeroenbb94). +- **Breaking Change** Added `failsOnEmptyData` boolean support for the `Decodable` map functions. [#1508](https://github.com/Moya/Moya/pull/1508) by [@jeroenbb94](https://github.com/Jeroenbb94). ### Changed - **Breaking Change** Updated minimum version of `ReactiveSwift` to 3.0.