From fce2e94ba5c2caa6a7b0488f8ad28f2c8c938a9b Mon Sep 17 00:00:00 2001 From: Nikolaj Schumacher Date: Thu, 26 Oct 2023 22:22:25 +0200 Subject: [PATCH 1/3] Add missing completion handler calls for specific error cases --- Sources/DBConnect/ICEDataController.swift | 44 ++++--------------- Sources/SNCFConnect/TGVDataController.swift | 48 ++++----------------- Sources/TrainConnect/TrainDataCommon.swift | 17 ++++++++ 3 files changed, 33 insertions(+), 76 deletions(-) create mode 100644 Sources/TrainConnect/TrainDataCommon.swift diff --git a/Sources/DBConnect/ICEDataController.swift b/Sources/DBConnect/ICEDataController.swift index c9fb94d..6d825fd 100644 --- a/Sources/DBConnect/ICEDataController.swift +++ b/Sources/DBConnect/ICEDataController.swift @@ -39,28 +39,16 @@ public final class ICEDataController: NSObject, TrainDataController { case .success(let response): do { let response = try response.filterSuccessfulStatusCodes() + if response.data.isEmpty { + // iceportal.de outside WiFi returns 200 with an empty body. + completionHandler(nil, TrainConnectionError.notConnected) + } let decoder = JSONDecoder() decoder.dateDecodingStrategy = .formatted(DateFormatter.yyyyMMdd) let trip = try decoder.decode(TripResponse.self, from: response.data) completionHandler(trip, nil) - } catch DecodingError.dataCorrupted(let context) { - if response.data.count == 0 { - // iceportal.de outside WiFi returns 200 with an empty body. - completionHandler(nil, TrainConnectionError.notConnected) - break - } - print(context) - } catch DecodingError.keyNotFound(let key, let context) { - print("Key '\(key)' not found:", context.debugDescription) - print("codingPath:", context.codingPath) - } catch DecodingError.valueNotFound(let value, let context) { - print("Value '\(value)' not found:", context.debugDescription) - print("codingPath:", context.codingPath) - } catch DecodingError.typeMismatch(let type, let context) { - print("Type '\(type)' mismatch:", context.debugDescription) - print("codingPath:", context.codingPath) - } catch { - print(error.localizedDescription) + } catch let error { + logDecodingError(error: error) completionHandler(nil, error) } break @@ -89,24 +77,8 @@ public final class ICEDataController: NSObject, TrainDataController { let decoder = JSONDecoder() let status = try decoder.decode(Status.self, from: response.data) completionHandler(status, nil) - } catch DecodingError.dataCorrupted(let context) { - if response.data.count == 0 { - // iceportal.de outside WiFi returns 200 with an empty body. - completionHandler(nil, TrainConnectionError.notConnected) - break - } - print(context) - } catch DecodingError.keyNotFound(let key, let context) { - print("Key '\(key)' not found:", context.debugDescription) - print("codingPath:", context.codingPath) - } catch DecodingError.valueNotFound(let value, let context) { - print("Value '\(value)' not found:", context.debugDescription) - print("codingPath:", context.codingPath) - } catch DecodingError.typeMismatch(let type, let context) { - print("Type '\(type)' mismatch:", context.debugDescription) - print("codingPath:", context.codingPath) - } catch { - print(error.localizedDescription) + } catch let error { + logDecodingError(error: error) completionHandler(nil, error) } break diff --git a/Sources/SNCFConnect/TGVDataController.swift b/Sources/SNCFConnect/TGVDataController.swift index 3723fc8..319da74 100644 --- a/Sources/SNCFConnect/TGVDataController.swift +++ b/Sources/SNCFConnect/TGVDataController.swift @@ -55,19 +55,8 @@ public class TGVDataController: NSObject, TrainDataController { decoder.dateDecodingStrategy = .formatted(DateFormatter.tgvFormatter) let trip = try decoder.decode(DetailsResponse.self, from: response.data) completionHandler(trip, nil) - } catch DecodingError.dataCorrupted(let context) { - print(context) - } catch DecodingError.keyNotFound(let key, let context) { - print("Key '\(key)' not found:", context.debugDescription) - print("codingPath:", context.codingPath) - } catch DecodingError.valueNotFound(let value, let context) { - print("Value '\(value)' not found:", context.debugDescription) - print("codingPath:", context.codingPath) - } catch DecodingError.typeMismatch(let type, let context) { - print("Type '\(type)' mismatch:", context.debugDescription) - print("codingPath:", context.codingPath) - } catch { - print(error.localizedDescription) + } catch let error { + logDecodingError(error: error) completionHandler(nil, error) } break @@ -79,6 +68,8 @@ public class TGVDataController: NSObject, TrainDataController { } } + + public func loadTrainStatus(demoMode: Bool = false, completionHandler: @escaping (TrainStatus?, Error?) -> ()) { self.loadGPS(demoMode: demoMode) { gps, error in if let gps = gps { @@ -111,19 +102,8 @@ public class TGVDataController: NSObject, TrainDataController { let decoder = JSONDecoder() let status = try decoder.decode(GPSResponse.self, from: response.data) completionHandler(status, nil) - } catch DecodingError.dataCorrupted(let context) { - print(context) - } catch DecodingError.keyNotFound(let key, let context) { - print("Key '\(key)' not found:", context.debugDescription) - print("codingPath:", context.codingPath) - } catch DecodingError.valueNotFound(let value, let context) { - print("Value '\(value)' not found:", context.debugDescription) - print("codingPath:", context.codingPath) - } catch DecodingError.typeMismatch(let type, let context) { - print("Type '\(type)' mismatch:", context.debugDescription) - print("codingPath:", context.codingPath) - } catch { - print(error.localizedDescription) + } catch let error { + logDecodingError(error: error) completionHandler(nil, error) } break @@ -145,19 +125,8 @@ public class TGVDataController: NSObject, TrainDataController { let decoder = JSONDecoder() let status = try decoder.decode(StatisticsResponse.self, from: response.data) completionHandler(status, nil) - } catch DecodingError.dataCorrupted(let context) { - print(context) - } catch DecodingError.keyNotFound(let key, let context) { - print("Key '\(key)' not found:", context.debugDescription) - print("codingPath:", context.codingPath) - } catch DecodingError.valueNotFound(let value, let context) { - print("Value '\(value)' not found:", context.debugDescription) - print("codingPath:", context.codingPath) - } catch DecodingError.typeMismatch(let type, let context) { - print("Type '\(type)' mismatch:", context.debugDescription) - print("codingPath:", context.codingPath) - } catch { - print(error.localizedDescription) + } catch let error { + logDecodingError(error: error) completionHandler(nil, error) } break @@ -184,4 +153,3 @@ public class TGVDataController: NSObject, TrainDataController { // return stop // } //} - diff --git a/Sources/TrainConnect/TrainDataCommon.swift b/Sources/TrainConnect/TrainDataCommon.swift new file mode 100644 index 0000000..34d035c --- /dev/null +++ b/Sources/TrainConnect/TrainDataCommon.swift @@ -0,0 +1,17 @@ +public func logDecodingError(error: Error) { + switch error { + case DecodingError.dataCorrupted(let context): + print(context) + case DecodingError.keyNotFound(let key, let context): + print("Key '\(key)' not found:", context.debugDescription) + print("codingPath:", context.codingPath) + case DecodingError.valueNotFound(let value, let context): + print("Value '\(value)' not found:", context.debugDescription) + print("codingPath:", context.codingPath) + case DecodingError.typeMismatch(let type, let context): + print("Type '\(type)' mismatch:", context.debugDescription) + print("codingPath:", context.codingPath) + default: + print(error.localizedDescription) + } +} From a28c5a3fb49d99944622825735d04a5bd9f785d2 Mon Sep 17 00:00:00 2001 From: Nikolaj Schumacher Date: Fri, 27 Oct 2023 00:06:57 +0200 Subject: [PATCH 2/3] Deduplicate code --- Sources/DBConnect/ICEDataController.swift | 48 ++----------- Sources/SNCFConnect/TGVDataController.swift | 69 ++----------------- Sources/TrainConnect/TrainDataCommon.swift | 36 +++++++++- .../TrainConnect/TrainDataController.swift | 31 ++++----- 4 files changed, 57 insertions(+), 127 deletions(-) diff --git a/Sources/DBConnect/ICEDataController.swift b/Sources/DBConnect/ICEDataController.swift index 6d825fd..b77352e 100644 --- a/Sources/DBConnect/ICEDataController.swift +++ b/Sources/DBConnect/ICEDataController.swift @@ -33,31 +33,10 @@ public final class ICEDataController: NSObject, TrainDataController { } public func loadTripData(demoMode: Bool = false, completionHandler: @escaping (TripResponse?, Error?) -> ()){ + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .formatted(DateFormatter.yyyyMMdd) let provider = getProvider(demoMode: demoMode) - provider.request(.trip) { result in - switch result { - case .success(let response): - do { - let response = try response.filterSuccessfulStatusCodes() - if response.data.isEmpty { - // iceportal.de outside WiFi returns 200 with an empty body. - completionHandler(nil, TrainConnectionError.notConnected) - } - let decoder = JSONDecoder() - decoder.dateDecodingStrategy = .formatted(DateFormatter.yyyyMMdd) - let trip = try decoder.decode(TripResponse.self, from: response.data) - completionHandler(trip, nil) - } catch let error { - logDecodingError(error: error) - completionHandler(nil, error) - } - break - case .failure(let error): - print(error.localizedDescription) - completionHandler(nil, error) - break - } - } + provider.loadJson(decoder: decoder, target: .trip, completionHandler: completionHandler) } @@ -68,26 +47,7 @@ public final class ICEDataController: NSObject, TrainDataController { } public func loadStatus(demoMode: Bool = false, completionHandler: @escaping (Status?, Error?) -> ()) { - let provider = getProvider(demoMode: demoMode) - provider.request(.status) { result in - switch result { - case .success(let response): - do { - let response = try response.filterSuccessfulStatusCodes() - let decoder = JSONDecoder() - let status = try decoder.decode(Status.self, from: response.data) - completionHandler(status, nil) - } catch let error { - logDecodingError(error: error) - completionHandler(nil, error) - } - break - case .failure(let error): - print(error.localizedDescription) - completionHandler(nil, error) - break - } - } + getProvider(demoMode: demoMode).loadJson(target: .status, completionHandler: completionHandler) } } diff --git a/Sources/SNCFConnect/TGVDataController.swift b/Sources/SNCFConnect/TGVDataController.swift index 319da74..58f6ac2 100644 --- a/Sources/SNCFConnect/TGVDataController.swift +++ b/Sources/SNCFConnect/TGVDataController.swift @@ -43,33 +43,12 @@ public class TGVDataController: NSObject, TrainDataController { }) } private func loadDetails(demoMode: Bool, completionHandler: @escaping (DetailsResponse?, Error?) -> ()){ - + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .formatted(DateFormatter.tgvFormatter) let provider = getProvider(demoMode: demoMode) - provider.request(.details) { result in - switch result { - case .success(let response): - do { - let response = try response.filterSuccessfulStatusCodes() - let decoder = JSONDecoder() - print(DateFormatter.tgvFormatter.string(from: .init())) - decoder.dateDecodingStrategy = .formatted(DateFormatter.tgvFormatter) - let trip = try decoder.decode(DetailsResponse.self, from: response.data) - completionHandler(trip, nil) - } catch let error { - logDecodingError(error: error) - completionHandler(nil, error) - } - break - case .failure(let error): - print(error.localizedDescription) - completionHandler(nil, error) - break - } - } + provider.loadJson(decoder: decoder, target: .details, completionHandler: completionHandler) } - - public func loadTrainStatus(demoMode: Bool = false, completionHandler: @escaping (TrainStatus?, Error?) -> ()) { self.loadGPS(demoMode: demoMode) { gps, error in if let gps = gps { @@ -93,49 +72,11 @@ public class TGVDataController: NSObject, TrainDataController { } private func loadGPS(demoMode: Bool = false, completionHandler: @escaping (GPSResponse?, Error?) -> ()) { - let provider = getProvider(demoMode: demoMode) - provider.request(.gps) { result in - switch result { - case .success(let response): - do { - let response = try response.filterSuccessfulStatusCodes() - let decoder = JSONDecoder() - let status = try decoder.decode(GPSResponse.self, from: response.data) - completionHandler(status, nil) - } catch let error { - logDecodingError(error: error) - completionHandler(nil, error) - } - break - case .failure(let error): - print(error.localizedDescription) - completionHandler(nil, error) - break - } - } + getProvider(demoMode: demoMode).loadJson(target: .gps, completionHandler: completionHandler) } private func loadStatistics(demoMode: Bool = false, completionHandler: @escaping (StatisticsResponse?, Error?) -> ()) { - let provider = getProvider(demoMode: demoMode) - provider.request(.statistics) { result in - switch result { - case .success(let response): - do { - let response = try response.filterSuccessfulStatusCodes() - let decoder = JSONDecoder() - let status = try decoder.decode(StatisticsResponse.self, from: response.data) - completionHandler(status, nil) - } catch let error { - logDecodingError(error: error) - completionHandler(nil, error) - } - break - case .failure(let error): - print(error.localizedDescription) - completionHandler(nil, error) - break - } - } + getProvider(demoMode: demoMode).loadJson(target: .statistics, completionHandler: completionHandler) } } diff --git a/Sources/TrainConnect/TrainDataCommon.swift b/Sources/TrainConnect/TrainDataCommon.swift index 34d035c..d5dcb29 100644 --- a/Sources/TrainConnect/TrainDataCommon.swift +++ b/Sources/TrainConnect/TrainDataCommon.swift @@ -1,4 +1,38 @@ -public func logDecodingError(error: Error) { +import Foundation +import Moya + +public extension MoyaProvider { + func loadJson(decoder: JSONDecoder = .init(), + target: Target, + completionHandler: @escaping (D?, Error?) -> ()) { + request(target) { result in + switch result { + case .success(let response): + do { + let response = try response.filterSuccessfulStatusCodes() + if response.data.isEmpty { + // iceportal.de outside WiFi returns 200 with an empty body. + completionHandler(nil, TrainConnectionError.notConnected) + } + else { + let result = try decoder.decode(D.self, from: response.data) + completionHandler(result, nil) + } + } catch let error { + logDecodingError(error: error) + completionHandler(nil, error) + } + break + case .failure(let error): + print(error.localizedDescription) + completionHandler(nil, error) + break + } + } + } +} + +private func logDecodingError(error: Error) { switch error { case DecodingError.dataCorrupted(let context): print(context) diff --git a/Sources/TrainConnect/TrainDataController.swift b/Sources/TrainConnect/TrainDataController.swift index db47e82..75d44b6 100644 --- a/Sources/TrainConnect/TrainDataController.swift +++ b/Sources/TrainConnect/TrainDataController.swift @@ -22,36 +22,31 @@ public class CombinedDataController: TrainDataController { } public func loadTrip(demoMode: Bool, completionHandler: @escaping (TrainTrip?, Error?) -> ()) { - var completed: Bool = false - var failed: Int = 0 - for controller in controllers { - controller.loadTrip(demoMode: demoMode) { - if let error = $1 { - failed += 1 - if failed >= self.controllers.count { - completionHandler(nil, error) - } - } else if !completed, let trip = $0 { - completed = true - completionHandler(trip, nil) - } - } + delegate(completionHandler: completionHandler) { + $0.loadTrip(demoMode: demoMode, completionHandler: $1) } } public func loadTrainStatus(demoMode: Bool, completionHandler: @escaping (TrainStatus?, Error?) -> ()) { + delegate(completionHandler: completionHandler) { + $0.loadTrainStatus(demoMode: demoMode, completionHandler: $1) + } + } + + private func delegate(completionHandler: @escaping (D?, Error?) -> (), + action: (TrainDataController, @escaping (D?, Error?) -> ()) -> ()) { var completed: Bool = false var failed: Int = 0 for controller in controllers { - controller.loadTrainStatus(demoMode: demoMode) { - if let error = $1 { + action(controller) { result, error in + if let error = error { failed += 1 if failed >= self.controllers.count { completionHandler(nil, error) } - } else if !completed, let status = $0 { + } else if !completed, let result = result { completed = true - completionHandler(status, nil) + completionHandler(result, nil) } } } From b759dd48e36d015023a125ab2a1f8d929b719711 Mon Sep 17 00:00:00 2001 From: Nikolaj Schumacher Date: Fri, 27 Oct 2023 00:38:36 +0200 Subject: [PATCH 3/3] Dramatically shorten timeout Unlike the ICE portal, the SNCF portal just times out outside the TGV. That means no error handlers were called until 1 minute has passed. --- Sources/DBConnect/ICEDataController.swift | 3 ++- Sources/SNCFConnect/TGVDataController.swift | 3 ++- Sources/TrainConnect/TrainDataCommon.swift | 9 +++++++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/Sources/DBConnect/ICEDataController.swift b/Sources/DBConnect/ICEDataController.swift index b77352e..43f5fbe 100644 --- a/Sources/DBConnect/ICEDataController.swift +++ b/Sources/DBConnect/ICEDataController.swift @@ -22,7 +22,8 @@ public final class ICEDataController: NSObject, TrainDataController { if demoMode { return MoyaProvider(stubClosure: MoyaProvider.immediatelyStub) } else { - return MoyaProvider(stubClosure: MoyaProvider.neverStub) + return MoyaProvider(stubClosure: MoyaProvider.neverStub, + session: alamofireSessionWithFasterTimeout) } } diff --git a/Sources/SNCFConnect/TGVDataController.swift b/Sources/SNCFConnect/TGVDataController.swift index 58f6ac2..fa6aab9 100644 --- a/Sources/SNCFConnect/TGVDataController.swift +++ b/Sources/SNCFConnect/TGVDataController.swift @@ -33,7 +33,8 @@ public class TGVDataController: NSObject, TrainDataController { if demoMode { return MoyaProvider(stubClosure: MoyaProvider.immediatelyStub) } else { - return MoyaProvider(stubClosure: MoyaProvider.neverStub) + return MoyaProvider(stubClosure: MoyaProvider.neverStub, + session: alamofireSessionWithFasterTimeout) } } diff --git a/Sources/TrainConnect/TrainDataCommon.swift b/Sources/TrainConnect/TrainDataCommon.swift index d5dcb29..b79ae0c 100644 --- a/Sources/TrainConnect/TrainDataCommon.swift +++ b/Sources/TrainConnect/TrainDataCommon.swift @@ -1,5 +1,6 @@ import Foundation import Moya +import Alamofire public extension MoyaProvider { func loadJson(decoder: JSONDecoder = .init(), @@ -49,3 +50,11 @@ private func logDecodingError(error: Error) { print(error.localizedDescription) } } + +public let alamofireSessionWithFasterTimeout: Alamofire.Session = { + let configuration = URLSessionConfiguration.default + // Use a very short timeout so we don't wait a minute before showing the "not connected" UI + configuration.timeoutIntervalForRequest = 2 + configuration.timeoutIntervalForResource = 2 + return Alamofire.Session(configuration: configuration) +}()