From 7db34bc273acad7fb2cf6b5f457f8047cb49250f Mon Sep 17 00:00:00 2001 From: Jerrad Thramer Date: Fri, 17 Jan 2020 12:58:56 -0700 Subject: [PATCH 01/24] WIP: Surfacing `MapMatchingResponse` and `RouteResponse` as public types. Still having issues with Codable. --- MapboxDirections.xcodeproj/project.pbxproj | 30 ++--- Sources/MapboxDirections/Directions.swift | 120 +++++++++++------- .../MapboxDirections/DirectionsError.swift | 4 +- .../MapMatching/MapMatchingResponse.swift | 53 ++++---- .../MapboxDirections/MapMatching/Match.swift | 3 - .../MapMatching/MatchResponse.swift | 31 ----- Sources/MapboxDirections/RouteResponse.swift | 44 ++++++- 7 files changed, 149 insertions(+), 136 deletions(-) delete mode 100644 Sources/MapboxDirections/MapMatching/MatchResponse.swift diff --git a/MapboxDirections.xcodeproj/project.pbxproj b/MapboxDirections.xcodeproj/project.pbxproj index f3f907de6..38724b164 100644 --- a/MapboxDirections.xcodeproj/project.pbxproj +++ b/MapboxDirections.xcodeproj/project.pbxproj @@ -63,14 +63,10 @@ 439255792344113D006EEE88 /* DirectionsError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4392557523440EC2006EEE88 /* DirectionsError.swift */; }; 4392557A2344113E006EEE88 /* DirectionsError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4392557523440EC2006EEE88 /* DirectionsError.swift */; }; 4392557B2344113F006EEE88 /* DirectionsError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4392557523440EC2006EEE88 /* DirectionsError.swift */; }; - 43F89F932350F952007B591E /* MatchResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43F89F922350F952007B591E /* MatchResponse.swift */; }; - 43F89F942350F952007B591E /* MatchResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43F89F922350F952007B591E /* MatchResponse.swift */; }; - 43F89F952350F952007B591E /* MatchResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43F89F922350F952007B591E /* MatchResponse.swift */; }; - 43F89F962350F952007B591E /* MatchResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43F89F922350F952007B591E /* MatchResponse.swift */; }; - 43F89F98235778DE007B591E /* MapMatchingResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43F89F97235778DE007B591E /* MapMatchingResponse.swift */; }; - 43F89F99235778DE007B591E /* MapMatchingResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43F89F97235778DE007B591E /* MapMatchingResponse.swift */; }; - 43F89F9A235778DE007B591E /* MapMatchingResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43F89F97235778DE007B591E /* MapMatchingResponse.swift */; }; - 43F89F9B235778DE007B591E /* MapMatchingResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43F89F97235778DE007B591E /* MapMatchingResponse.swift */; }; + 43F89F932350F952007B591E /* MapMatchingResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43F89F922350F952007B591E /* MapMatchingResponse.swift */; }; + 43F89F942350F952007B591E /* MapMatchingResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43F89F922350F952007B591E /* MapMatchingResponse.swift */; }; + 43F89F952350F952007B591E /* MapMatchingResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43F89F922350F952007B591E /* MapMatchingResponse.swift */; }; + 43F89F962350F952007B591E /* MapMatchingResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43F89F922350F952007B591E /* MapMatchingResponse.swift */; }; 8D381B611FD9F5B1008D5A58 /* noDestinationName.json in Resources */ = {isa = PBXBuildFile; fileRef = 8D381B601FD9F5B1008D5A58 /* noDestinationName.json */; }; 8D381B631FDB01D1008D5A58 /* apiDestinationName.json in Resources */ = {isa = PBXBuildFile; fileRef = 8D381B621FDB01D1008D5A58 /* apiDestinationName.json */; }; 8D381B641FDB0898008D5A58 /* noDestinationName.json in Resources */ = {isa = PBXBuildFile; fileRef = 8D381B601FD9F5B1008D5A58 /* noDestinationName.json */; }; @@ -348,8 +344,7 @@ 438BFEC0233D805500457294 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 438BFEC1233D854D00457294 /* DirectionsProfileIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectionsProfileIdentifier.swift; sourceTree = ""; }; 4392557523440EC2006EEE88 /* DirectionsError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectionsError.swift; sourceTree = ""; }; - 43F89F922350F952007B591E /* MatchResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchResponse.swift; sourceTree = ""; }; - 43F89F97235778DE007B591E /* MapMatchingResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapMatchingResponse.swift; sourceTree = ""; }; + 43F89F922350F952007B591E /* MapMatchingResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapMatchingResponse.swift; sourceTree = ""; }; 8D381B601FD9F5B1008D5A58 /* noDestinationName.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = noDestinationName.json; sourceTree = ""; }; 8D381B621FDB01D1008D5A58 /* apiDestinationName.json */ = {isa = PBXFileReference; explicitFileType = text.json; path = apiDestinationName.json; sourceTree = ""; }; 8D381B691FDB101F008D5A58 /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = ""; }; @@ -585,10 +580,9 @@ C5DAAC9E20191683001F9261 /* MapMatching */ = { isa = PBXGroup; children = ( - 43F89F97235778DE007B591E /* MapMatchingResponse.swift */, C5DAAC9920191675001F9261 /* Match.swift */, C5434B8B200695A50069E887 /* MatchOptions.swift */, - 43F89F922350F952007B591E /* MatchResponse.swift */, + 43F89F922350F952007B591E /* MapMatchingResponse.swift */, C5434B89200693D00069E887 /* Tracepoint.swift */, ); path = MapMatching; @@ -1179,7 +1173,6 @@ 35DBF010217E17A30009D2AE /* HTTPURLResponse.swift in Sources */, 8D43467A219E15C3008B7BF3 /* Double.swift in Sources */, C53A02261E92C26E009837BD /* AttributeOptions.swift in Sources */, - 43F89F99235778DE007B591E /* MapMatchingResponse.swift in Sources */, DA1A10C91D00F969009F82FA /* RouteLeg.swift in Sources */, C52552BA1FA15D5E00B1545C /* VisualInstructionBanner.swift in Sources */, 35828C9F217A003F00ED546E /* OfflineDirections.swift in Sources */, @@ -1199,7 +1192,7 @@ 431E93D023466D7500A71B44 /* Codable.swift in Sources */, C5990B4D2045E74800D7DFD4 /* DirectionsOptions.swift in Sources */, DAA76D691DD127CB0015EC78 /* LaneIndication.swift in Sources */, - 43F89F942350F952007B591E /* MatchResponse.swift in Sources */, + 43F89F942350F952007B591E /* MapMatchingResponse.swift in Sources */, DA1A10CB1D00F969009F82FA /* RouteStep.swift in Sources */, C57D55031DB566A700B94B74 /* Intersection.swift in Sources */, C52CE38F1F6AF63C0069963D /* SpokenInstruction.swift in Sources */, @@ -1252,7 +1245,6 @@ 35DBF011217E17A30009D2AE /* HTTPURLResponse.swift in Sources */, 8D43467B219E15C4008B7BF3 /* Double.swift in Sources */, C53A02271E92C26F009837BD /* AttributeOptions.swift in Sources */, - 43F89F9A235778DE007B591E /* MapMatchingResponse.swift in Sources */, DA1A10EF1D010247009F82FA /* RouteLeg.swift in Sources */, C52552BB1FA15D5F00B1545C /* VisualInstructionBanner.swift in Sources */, 35828CA0217A003F00ED546E /* OfflineDirections.swift in Sources */, @@ -1272,7 +1264,7 @@ 431E93D123466D7600A71B44 /* Codable.swift in Sources */, C5990B4E2045E74900D7DFD4 /* DirectionsOptions.swift in Sources */, DAA76D6A1DD127CB0015EC78 /* LaneIndication.swift in Sources */, - 43F89F952350F952007B591E /* MatchResponse.swift in Sources */, + 43F89F952350F952007B591E /* MapMatchingResponse.swift in Sources */, DA1A10F11D010247009F82FA /* RouteStep.swift in Sources */, C57D55041DB566A800B94B74 /* Intersection.swift in Sources */, C52CE3901F6AF63D0069963D /* SpokenInstruction.swift in Sources */, @@ -1325,7 +1317,6 @@ 35DBF012217E17A30009D2AE /* HTTPURLResponse.swift in Sources */, 8D43467C219E15C6008B7BF3 /* Double.swift in Sources */, C53A02281E92C271009837BD /* AttributeOptions.swift in Sources */, - 43F89F9B235778DE007B591E /* MapMatchingResponse.swift in Sources */, DA1A11061D0103A3009F82FA /* RouteLeg.swift in Sources */, C52552BC1FA15D6000B1545C /* VisualInstructionBanner.swift in Sources */, 35828CA1217A003F00ED546E /* OfflineDirections.swift in Sources */, @@ -1345,7 +1336,7 @@ 431E93D223466D7700A71B44 /* Codable.swift in Sources */, C5990B4F2045E74A00D7DFD4 /* DirectionsOptions.swift in Sources */, DAA76D6B1DD127CB0015EC78 /* LaneIndication.swift in Sources */, - 43F89F962350F952007B591E /* MatchResponse.swift in Sources */, + 43F89F962350F952007B591E /* MapMatchingResponse.swift in Sources */, DA1A11081D0103A3009F82FA /* RouteStep.swift in Sources */, C57D55051DB566A900B94B74 /* Intersection.swift in Sources */, C52CE3911F6AF63E0069963D /* SpokenInstruction.swift in Sources */, @@ -1371,7 +1362,6 @@ 8D434679219E1167008B7BF3 /* Double.swift in Sources */, DA2E03EB1CB0E13D00D1269A /* RouteOptions.swift in Sources */, C582BA2E2073ED6300647DAA /* Array.swift in Sources */, - 43F89F98235778DE007B591E /* MapMatchingResponse.swift in Sources */, C52552B91FA15D5900B1545C /* VisualInstructionBanner.swift in Sources */, 35828C9E217A003F00ED546E /* OfflineDirections.swift in Sources */, DAE7EA94230B5FD10003B211 /* Measurement.swift in Sources */, @@ -1390,7 +1380,7 @@ 431E93CF23466D7400A71B44 /* Codable.swift in Sources */, C59094C1203DE6BC00EB2417 /* DirectionsResult.swift in Sources */, DAC05F1A1CFC077C00FA0071 /* RouteLeg.swift in Sources */, - 43F89F932350F952007B591E /* MatchResponse.swift in Sources */, + 43F89F932350F952007B591E /* MapMatchingResponse.swift in Sources */, C5434B8A200693D00069E887 /* Tracepoint.swift in Sources */, DA6C9DA61CAE462800094FBC /* Directions.swift in Sources */, C55FB44B1F6AEBF6006BD1E9 /* SpokenInstruction.swift in Sources */, diff --git a/Sources/MapboxDirections/Directions.swift b/Sources/MapboxDirections/Directions.swift index 6f110796a..3238a49b9 100644 --- a/Sources/MapboxDirections/Directions.swift +++ b/Sources/MapboxDirections/Directions.swift @@ -77,7 +77,7 @@ open class Directions: NSObject { If the request was canceled or there was an error obtaining the routes, this argument is `nil`. This is not to be confused with the situation in which no results were found, in which case the array is present but empty. - parameter error: The error that occurred, or `nil` if the placemarks were obtained successfully. */ - public typealias RouteCompletionHandler = (_ waypoints: [Waypoint]?, _ routes: [Route]?, _ error: DirectionsError?) -> Void + public typealias RouteCompletionHandler = (_ response: RouteResponse) -> Void /** A closure (block) to be called when a map matching request is complete. @@ -85,7 +85,7 @@ open class Directions: NSObject { If the request was canceled or there was an error obtaining the matches, this argument is `nil`. This is not to be confused with the situation in which no matches were found, in which case the array is present but empty. - parameter error: The error that occurred, or `nil` if the placemarks were obtained successfully. */ - public typealias MatchCompletionHandler = (_ matches: [Match]?, _ error: DirectionsError?) -> Void + public typealias MatchCompletionHandler = (_ response: MapMatchingResponse) -> Void // MARK: Creating a Directions Object @@ -158,48 +158,55 @@ open class Directions: NSObject { let requestTask = URLSession.shared.dataTask(with: request) { (possibleData, possibleResponse, possibleError) in let responseEndDate = Date() guard let response = possibleResponse, ["application/json", "text/html"].contains(response.mimeType) else { - completionHandler(nil, nil, .invalidResponse) + let response = RouteResponse(code: nil, message: nil, error: .invalidResponse(possibleResponse), uuid: nil, routes: nil, waypoints: nil) + completionHandler(response) return } guard let data = possibleData else { - completionHandler(nil, nil, .noData) + let response = RouteResponse(code: nil, message: nil, error: .noData, uuid: nil, routes: nil, waypoints: nil) + completionHandler(response) return } if let error = possibleError { - completionHandler(nil, nil, .unknown(response: possibleResponse, underlying: error, code: nil, message: nil)) + let unknownError = DirectionsError.unknown(response: possibleResponse, underlying: error, code: nil, message: nil) + let response = RouteResponse(code: nil, message: nil, error: unknownError , uuid: nil, routes: nil, waypoints: nil) + completionHandler(response) return } DispatchQueue.global(qos: .userInitiated).async { do { let decoder = DirectionsDecoder(options: options) - let result = try decoder.decode(RouteResponse.self, from: data) + var result = try decoder.decode(RouteResponse.self, from: data) guard (result.code == nil && result.message == nil) || result.code == "Ok" else { let apiError = Directions.informativeError(code: result.code, message: result.message, response: response, underlyingError: possibleError) + result.error = apiError DispatchQueue.main.async { - completionHandler(nil, nil, apiError) + completionHandler(result) } return } - guard let routes = result.routes else { + guard result.routes != nil else { + result.error = .unableToRoute DispatchQueue.main.async { - completionHandler(result.waypoints, nil, .unableToRoute) + completionHandler(result) } return } - self.postprocess(routes, fetchStartDate: fetchStart, responseEndDate: responseEndDate, uuid: result.uuid) + result.postprocess(accessToken: self.accessToken, apiEndpoint: self.apiEndpoint, fetchStartDate: fetchStart, responseEndDate: responseEndDate) DispatchQueue.main.async { - completionHandler(result.waypoints, routes, nil) + completionHandler(result) } } catch { DispatchQueue.main.async { let bailError = Directions.informativeError(code: nil, message: nil, response: response, underlyingError: error) - completionHandler(nil, nil, bailError) + let response = RouteResponse(code: nil, message: nil, error: bailError, uuid: nil, routes: nil, waypoints: nil) + completionHandler(response) } } } @@ -227,47 +234,54 @@ open class Directions: NSObject { let requestTask = URLSession.shared.dataTask(with: request) { (possibleData, possibleResponse, possibleError) in let responseEndDate = Date() guard let response = possibleResponse, response.mimeType == "application/json" else { - completionHandler(nil, .invalidResponse) + let result = MapMatchingResponse(code: nil, message: nil, error: .invalidResponse(possibleResponse), matches: nil, tracepoints: nil) + completionHandler(result) return } guard let data = possibleData else { - completionHandler(nil, .noData) - return + let result = MapMatchingResponse(code: nil, message: nil, error: .noData, matches: nil, tracepoints: nil) + return completionHandler(result) } if let error = possibleError { - completionHandler(nil, .unknown(response: possibleResponse, underlying: error, code: nil, message: nil)) + let unknownError = DirectionsError.unknown(response: possibleResponse, underlying: error, code: nil, message: nil) + let response = MapMatchingResponse(code: nil, message: nil, error: unknownError, matches: nil, tracepoints: nil) + completionHandler(response) return } DispatchQueue.global(qos: .userInitiated).async { do { let decoder = DirectionsDecoder(options: options) - let result = try decoder.decode(MatchResponse.self, from: data) + var result = try decoder.decode(MapMatchingResponse.self, from: data) guard result.code == "Ok" else { let apiError = Directions.informativeError(code: result.code, message: result.message, response: response, underlyingError: possibleError) + result.error = apiError DispatchQueue.main.async { - completionHandler(nil, apiError) + completionHandler(result) } return } - guard let matches = result.matches else { + guard result.matches != nil else { + result.error = .unableToRoute DispatchQueue.main.async { - completionHandler(nil, .unableToRoute) + completionHandler(result) } return } - self.postprocess(matches, fetchStartDate: fetchStart, responseEndDate: responseEndDate, uuid: nil) + result.postprocess(accessToken: self.accessToken, apiEndpoint: self.apiEndpoint, fetchStartDate: fetchStart, responseEndDate: responseEndDate) DispatchQueue.main.async { - completionHandler(matches, nil) + completionHandler(result) } } catch { DispatchQueue.main.async { - completionHandler(nil, .unknown(response: response, underlying: error, code: nil, message: nil)) + let caughtError = DirectionsError.unknown(response: response, underlying: error, code: nil, message: nil) + let result = MapMatchingResponse(code: nil, message: nil, error: caughtError, matches: nil, tracepoints: nil) + completionHandler(result) } } } @@ -295,47 +309,57 @@ open class Directions: NSObject { let requestTask = URLSession.shared.dataTask(with: request) { (possibleData, possibleResponse, possibleError) in let responseEndDate = Date() guard let response = possibleResponse, response.mimeType == "application/json" else { - completionHandler(nil, nil, .invalidResponse) + let error = DirectionsError.invalidResponse(possibleResponse) + let result = RouteResponse(code: nil, message: nil, error: error, uuid: nil, routes: nil, waypoints: nil) + completionHandler(result) return } guard let data = possibleData else { - completionHandler(nil, nil, .noData) + let result = RouteResponse(code: nil, message: nil, error: .noData, uuid: nil, routes: nil, waypoints: nil) + completionHandler(result) return } if let error = possibleError { - completionHandler(nil, nil, .unknown(response: possibleResponse, underlying: error, code: nil, message: nil)) + let unknownError = DirectionsError.unknown(response: possibleResponse, underlying: error, code: nil, message: nil) + let result = RouteResponse(code: nil, message: nil, error: unknownError, uuid: nil, routes: nil, waypoints: nil) + completionHandler(result) return } DispatchQueue.global(qos: .userInitiated).async { do { - let decoder = DirectionsDecoder(options: options) - let result = try decoder.decode(MapMatchingResponse.self, from: data) + let decoder = DirectionsDecoder(options: options, decodingRouteResponseFromMatchService: true) + var result = try decoder.decode(RouteResponse.self, from: data) guard result.code == "Ok" else { let apiError = Directions.informativeError(code: result.code, message:nil, response: response, underlyingError: possibleError) + result.error = apiError DispatchQueue.main.async { - completionHandler(nil, nil, apiError) + completionHandler(result) } return } - guard let routes = result.routes else { + guard result.routes != nil else { + result.error = .unableToRoute DispatchQueue.main.async { - completionHandler(result.waypoints, nil, .unableToRoute) + completionHandler(result) } return } - self.postprocess(routes, fetchStartDate: fetchStart, responseEndDate: responseEndDate, uuid: nil) + result.postprocess(accessToken: self.accessToken, apiEndpoint: self.apiEndpoint, fetchStartDate: fetchStart, responseEndDate: responseEndDate) + DispatchQueue.main.async { - completionHandler(result.waypoints, routes, nil) + completionHandler(result) } } catch { DispatchQueue.main.async { - completionHandler(nil, nil, .unknown(response: response, underlying: error, code: nil, message: nil)) + let caughtError = DirectionsError.unknown(response: response, underlying: error, code: nil, message: nil) + let result = RouteResponse(code: nil, message: nil, error: caughtError, uuid: nil, routes: nil, waypoints: nil) + completionHandler(result) } } } @@ -441,29 +465,21 @@ open class Directions: NSObject { return .unknown(response: response, underlying: error, code: code, message: message) } - /** - Adds request- or response-specific information to each result in a response. - */ - func postprocess(_ results: [DirectionsResult], fetchStartDate: Date, responseEndDate: Date, uuid: String?) { - for result in results { - result.accessToken = self.accessToken - result.apiEndpoint = self.apiEndpoint - result.routeIdentifier = uuid - result.fetchStartDate = fetchStartDate - result.responseEndDate = responseEndDate - } - } } public extension CodingUserInfoKey { static let options = CodingUserInfoKey(rawValue: "com.mapbox.directions.coding.routeOptions")! + static let routesFromMatch = CodingUserInfoKey(rawValue: "com.mapbox.directions.coding.routesFromMatch")! + static let tracepoints = CodingUserInfoKey(rawValue: "com.mapbox.directions.coding.tracepoints")! + + } public class DirectionsDecoder: JSONDecoder { /** Initializes a `DirectionsDecoder` with a given `RouteOption`. */ - public init(options: DirectionsOptions) { + public init(options: DirectionsOptions, decodingRouteResponseFromMatchService: Bool = false) { super.init() routeOptions = options } @@ -476,9 +492,17 @@ public class DirectionsDecoder: JSONDecoder { } } + var decodingRouteResponseFromMatchService: Bool { + get { + return userInfo[.routesFromMatch] as? Bool ?? false + } set { + userInfo[.routesFromMatch] = newValue + } + } + var tracepoints: [Tracepoint?]? { get { - return userInfo[.tracepoints] as? [Tracepoint?] + return userInfo[.routesFromMatch] as? [Tracepoint?] } set { userInfo[.tracepoints] = newValue } diff --git a/Sources/MapboxDirections/DirectionsError.swift b/Sources/MapboxDirections/DirectionsError.swift index 7d084beeb..3f82357cb 100644 --- a/Sources/MapboxDirections/DirectionsError.swift +++ b/Sources/MapboxDirections/DirectionsError.swift @@ -3,7 +3,7 @@ import Foundation /** An error that occurs when calculating directions. */ -public enum DirectionsError: LocalizedError { +public enum DirectionsError: LocalizedError, Codable { /** The server returned an empty response. */ @@ -14,7 +14,7 @@ public enum DirectionsError: LocalizedError { /** The server returned a response that isn’t correctly formatted. */ - case invalidResponse + case invalidResponse(_: URLResponse?) /** No route could be found between the specified locations. diff --git a/Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift b/Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift index 6bb2c6c8b..d6d1fa2d0 100644 --- a/Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift +++ b/Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift @@ -1,14 +1,18 @@ import Foundation public struct MapMatchingResponse { - public var code: String - public var routes : [Route]? - public var waypoints: [Waypoint] + public var code: String? + public var message: String? + public var error: DirectionsError? + public var matches : [Match]? + public var tracepoints: [Tracepoint?]? } -extension MapMatchingResponse: Decodable { +extension MapMatchingResponse: Codable { private enum CodingKeys: String, CodingKey { case code + case message + case error case matches = "matchings" case tracepoints } @@ -16,34 +20,27 @@ extension MapMatchingResponse: Decodable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) code = try container.decode(String.self, forKey: .code) - routes = try container.decodeIfPresent([Route].self, forKey: .matches) + message = try container.decodeIfPresent(String.self, forKey: .message) + tracepoints = try container.decodeIfPresent([Tracepoint?].self, forKey: .tracepoints) + matches = try container.decodeIfPresent([Match].self, forKey: .matches) - // Decode waypoints from the response and update their names according to the waypoints from DirectionsOptions.waypoints. - // Map Matching API responses can contain null tracepoints. Null tracepoints can’t correspond to waypoints, so they’re irrelevant to the decoded structure. - let decodedWaypoints = try container.decode([Waypoint?].self, forKey: .tracepoints).compactMap { $0 } - if let options = decoder.userInfo[.options] as? DirectionsOptions { - // The response lists the same number of tracepoints as the waypoints in the request, whether or not a given waypoint is leg-separating. - waypoints = zip(decodedWaypoints, options.waypoints).map { (pair) -> Waypoint in - let (decodedWaypoint, waypointInOptions) = pair - let waypoint = Waypoint(coordinate: decodedWaypoint.coordinate, coordinateAccuracy: waypointInOptions.coordinateAccuracy, name: waypointInOptions.name?.nonEmptyString ?? decodedWaypoint.name) - waypoint.separatesLegs = waypointInOptions.separatesLegs - return waypoint + if let points = self.tracepoints { + matches?.forEach { + $0.tracepoints = points } - waypoints.first?.separatesLegs = true - waypoints.last?.separatesLegs = true - } else { - waypoints = decodedWaypoints + } + } + + func postprocess(accessToken: String, apiEndpoint: URL, fetchStartDate: Date, responseEndDate: Date) { + guard let matches = self.matches else { + return } - if let routes = try container.decodeIfPresent([Route].self, forKey: .matches) { - // Postprocess each route. - for route in routes { - // Imbue each route’s legs with the leg-separating waypoints refined above. - route.legSeparators = waypoints.filter { $0.separatesLegs } - } - self.routes = routes - } else { - routes = nil + for result in matches { + result.accessToken = accessToken + result.apiEndpoint = apiEndpoint + result.fetchStartDate = fetchStartDate + result.responseEndDate = responseEndDate } } } diff --git a/Sources/MapboxDirections/MapMatching/Match.swift b/Sources/MapboxDirections/MapMatching/Match.swift index b2052555a..2b12fac48 100644 --- a/Sources/MapboxDirections/MapMatching/Match.swift +++ b/Sources/MapboxDirections/MapMatching/Match.swift @@ -3,9 +3,6 @@ import CoreLocation import Polyline import struct Turf.LineString -extension CodingUserInfoKey { - static let tracepoints = CodingUserInfoKey(rawValue: "com.mapbox.directions.coding.tracepoints")! -} /** A `Match` object defines a single route that was created from a series of points that were matched against a road network. diff --git a/Sources/MapboxDirections/MapMatching/MatchResponse.swift b/Sources/MapboxDirections/MapMatching/MatchResponse.swift deleted file mode 100644 index a665a6cbd..000000000 --- a/Sources/MapboxDirections/MapMatching/MatchResponse.swift +++ /dev/null @@ -1,31 +0,0 @@ -import Foundation - -public struct MatchResponse { - public var code: String - public var message: String? - public var matches : [Match]? - public var tracepoints: [Tracepoint?]? -} - -extension MatchResponse: Codable { - private enum CodingKeys: String, CodingKey { - case code - case message - case matches = "matchings" - case tracepoints - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - code = try container.decode(String.self, forKey: .code) - message = try container.decodeIfPresent(String.self, forKey: .message) - tracepoints = try container.decodeIfPresent([Tracepoint?].self, forKey: .tracepoints) - matches = try container.decodeIfPresent([Match].self, forKey: .matches) - - if let points = self.tracepoints { - matches?.forEach { - $0.tracepoints = points - } - } - } -} diff --git a/Sources/MapboxDirections/RouteResponse.swift b/Sources/MapboxDirections/RouteResponse.swift index b5ee41f7f..cab3bf0a7 100644 --- a/Sources/MapboxDirections/RouteResponse.swift +++ b/Sources/MapboxDirections/RouteResponse.swift @@ -3,7 +3,7 @@ import Foundation public struct RouteResponse { public var code: String? public var message: String? - public var error: String? + public var error: DirectionsError? public let uuid: String? public let routes: [Route]? public let waypoints: [Waypoint]? @@ -17,18 +17,26 @@ extension RouteResponse: Codable { case uuid case routes case waypoints + + // Matching-service specific keys. Used in decode only. + case matches = "matchings" + case tracepoints } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) + // Is this an edge-case, where we are creating a route response from the map matching service? + let fromMatchingService = (decoder as? DirectionsDecoder)?.decodingRouteResponseFromMatchService ?? false self.code = try container.decodeIfPresent(String.self, forKey: .code) self.message = try container.decodeIfPresent(String.self, forKey: .message) - self.error = try container.decodeIfPresent(String.self, forKey: .error) + if let apiError = try container.decodeIfPresent(String.self, forKey: .error) { + error = .unknown(response: nil, underlying: nil, code: self.code, message: apiError) + } self.uuid = try container.decodeIfPresent(String.self, forKey: .uuid) // Decode waypoints from the response and update their names according to the waypoints from DirectionsOptions.waypoints. - let decodedWaypoints = try container.decodeIfPresent([Waypoint].self, forKey: .waypoints) + let decodedWaypoints = try container.decodeIfPresent([Waypoint?].self, forKey: fromMatchingService ? .tracepoints : .waypoints)?.compactMap { $0 } if let decodedWaypoints = decodedWaypoints, let options = decoder.userInfo[.options] as? DirectionsOptions { // The response lists the same number of tracepoints as the waypoints in the request, whether or not a given waypoint is leg-separating. waypoints = zip(decodedWaypoints, options.waypoints).map { (pair) -> Waypoint in @@ -43,7 +51,7 @@ extension RouteResponse: Codable { waypoints = decodedWaypoints } - if let routes = try container.decodeIfPresent([Route].self, forKey: .routes) { + if let routes = try container.decodeIfPresent([Route].self, forKey: fromMatchingService ? .matches : .routes) { // Postprocess each route. for route in routes { route.routeIdentifier = uuid @@ -57,4 +65,32 @@ extension RouteResponse: Codable { routes = nil } } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encodeIfPresent(code, forKey: .code) + try container.encodeIfPresent(message, forKey: .message) + try container.encodeIfPresent(error, forKey: .error) + try container.encodeIfPresent(uuid, forKey: .uuid) + try container.encodeIfPresent(routes, forKey: .routes) + try container.encodeIfPresent(waypoints, forKey: .waypoints) + } + + /** + Adds request- or response-specific information to each result in a response. + */ + func postprocess(accessToken: String, apiEndpoint: URL, fetchStartDate: Date, responseEndDate: Date) { + guard let routes = self.routes else { + return + } + + for result in routes { + result.accessToken = accessToken + result.apiEndpoint = apiEndpoint + result.routeIdentifier = uuid + result.fetchStartDate = fetchStartDate + result.responseEndDate = responseEndDate + } + } + } From 6edcb84677e0b6e2a33a313db8ef72efd6ea9fde Mon Sep 17 00:00:00 2001 From: Jerrad Thramer Date: Fri, 17 Jan 2020 14:24:14 -0700 Subject: [PATCH 02/24] Adding error-case initializer for RouteResponse/MapMatchingResponse --- Sources/MapboxDirections/Directions.swift | 22 +++++++++---------- .../MapMatching/MapMatchingResponse.swift | 4 ++++ Sources/MapboxDirections/RouteResponse.swift | 4 ++++ 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/Sources/MapboxDirections/Directions.swift b/Sources/MapboxDirections/Directions.swift index 3238a49b9..e5da56fb8 100644 --- a/Sources/MapboxDirections/Directions.swift +++ b/Sources/MapboxDirections/Directions.swift @@ -158,20 +158,20 @@ open class Directions: NSObject { let requestTask = URLSession.shared.dataTask(with: request) { (possibleData, possibleResponse, possibleError) in let responseEndDate = Date() guard let response = possibleResponse, ["application/json", "text/html"].contains(response.mimeType) else { - let response = RouteResponse(code: nil, message: nil, error: .invalidResponse(possibleResponse), uuid: nil, routes: nil, waypoints: nil) + let response = RouteResponse(error: .invalidResponse(possibleResponse)) completionHandler(response) return } guard let data = possibleData else { - let response = RouteResponse(code: nil, message: nil, error: .noData, uuid: nil, routes: nil, waypoints: nil) + let response = RouteResponse(error: .noData) completionHandler(response) return } if let error = possibleError { let unknownError = DirectionsError.unknown(response: possibleResponse, underlying: error, code: nil, message: nil) - let response = RouteResponse(code: nil, message: nil, error: unknownError , uuid: nil, routes: nil, waypoints: nil) + let response = RouteResponse(error: unknownError) completionHandler(response) return } @@ -205,7 +205,7 @@ open class Directions: NSObject { } catch { DispatchQueue.main.async { let bailError = Directions.informativeError(code: nil, message: nil, response: response, underlyingError: error) - let response = RouteResponse(code: nil, message: nil, error: bailError, uuid: nil, routes: nil, waypoints: nil) + let response = RouteResponse(error: bailError) completionHandler(response) } } @@ -240,13 +240,13 @@ open class Directions: NSObject { } guard let data = possibleData else { - let result = MapMatchingResponse(code: nil, message: nil, error: .noData, matches: nil, tracepoints: nil) + let result = MapMatchingResponse(error: .noData) return completionHandler(result) } if let error = possibleError { let unknownError = DirectionsError.unknown(response: possibleResponse, underlying: error, code: nil, message: nil) - let response = MapMatchingResponse(code: nil, message: nil, error: unknownError, matches: nil, tracepoints: nil) + let response = MapMatchingResponse(error: unknownError) completionHandler(response) return } @@ -280,7 +280,7 @@ open class Directions: NSObject { } catch { DispatchQueue.main.async { let caughtError = DirectionsError.unknown(response: response, underlying: error, code: nil, message: nil) - let result = MapMatchingResponse(code: nil, message: nil, error: caughtError, matches: nil, tracepoints: nil) + let result = MapMatchingResponse( error: caughtError) completionHandler(result) } } @@ -310,20 +310,20 @@ open class Directions: NSObject { let responseEndDate = Date() guard let response = possibleResponse, response.mimeType == "application/json" else { let error = DirectionsError.invalidResponse(possibleResponse) - let result = RouteResponse(code: nil, message: nil, error: error, uuid: nil, routes: nil, waypoints: nil) + let result = RouteResponse(error: error) completionHandler(result) return } guard let data = possibleData else { - let result = RouteResponse(code: nil, message: nil, error: .noData, uuid: nil, routes: nil, waypoints: nil) + let result = RouteResponse(error: .noData) completionHandler(result) return } if let error = possibleError { let unknownError = DirectionsError.unknown(response: possibleResponse, underlying: error, code: nil, message: nil) - let result = RouteResponse(code: nil, message: nil, error: unknownError, uuid: nil, routes: nil, waypoints: nil) + let result = RouteResponse(error: unknownError) completionHandler(result) return } @@ -358,7 +358,7 @@ open class Directions: NSObject { } catch { DispatchQueue.main.async { let caughtError = DirectionsError.unknown(response: response, underlying: error, code: nil, message: nil) - let result = RouteResponse(code: nil, message: nil, error: caughtError, uuid: nil, routes: nil, waypoints: nil) + let result = RouteResponse(error: caughtError) completionHandler(result) } } diff --git a/Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift b/Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift index d6d1fa2d0..0de49c1e6 100644 --- a/Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift +++ b/Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift @@ -17,6 +17,10 @@ extension MapMatchingResponse: Codable { case tracepoints } + public init(error: DirectionsError) { + self.init(code: nil, message: nil, error: error, matches: nil, tracepoints: nil) + } + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) code = try container.decode(String.self, forKey: .code) diff --git a/Sources/MapboxDirections/RouteResponse.swift b/Sources/MapboxDirections/RouteResponse.swift index cab3bf0a7..5cb2285ed 100644 --- a/Sources/MapboxDirections/RouteResponse.swift +++ b/Sources/MapboxDirections/RouteResponse.swift @@ -22,6 +22,10 @@ extension RouteResponse: Codable { case matches = "matchings" case tracepoints } + public init(error: DirectionsError) { + self.init(code: nil, message: nil, error: error, uuid: nil, routes: nil, waypoints: nil) + } + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) From 77f075b42fc5f160ea6b68a25b728720e647ed8d Mon Sep 17 00:00:00 2001 From: Jerrad Thramer Date: Fri, 24 Jan 2020 17:29:36 -0700 Subject: [PATCH 03/24] WIP: Removing options object from models, encapsulating authentication tokens into new `DirectionsCredentials` object, Reversing hacky match-to-route mechanism --- MapboxDirections.xcodeproj/project.pbxproj | 18 +++++ Sources/MapboxDirections/Directions.swift | 79 +++---------------- .../DirectionsCredentials.swift | 30 +++++++ .../MapboxDirections/DirectionsOptions.swift | 11 +++ .../MapboxDirections/DirectionsResult.swift | 32 ++------ .../MapboxDirections/MapMatching/Match.swift | 21 +---- Sources/MapboxDirections/Route.swift | 42 ---------- Sources/MapboxDirections/RouteResponse.swift | 32 ++++++-- 8 files changed, 103 insertions(+), 162 deletions(-) create mode 100644 Sources/MapboxDirections/DirectionsCredentials.swift diff --git a/MapboxDirections.xcodeproj/project.pbxproj b/MapboxDirections.xcodeproj/project.pbxproj index 38724b164..ef4fcc14c 100644 --- a/MapboxDirections.xcodeproj/project.pbxproj +++ b/MapboxDirections.xcodeproj/project.pbxproj @@ -63,6 +63,14 @@ 439255792344113D006EEE88 /* DirectionsError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4392557523440EC2006EEE88 /* DirectionsError.swift */; }; 4392557A2344113E006EEE88 /* DirectionsError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4392557523440EC2006EEE88 /* DirectionsError.swift */; }; 4392557B2344113F006EEE88 /* DirectionsError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4392557523440EC2006EEE88 /* DirectionsError.swift */; }; + 43D6617023DBA58E0062BFFE /* (null) in Sources */ = {isa = PBXBuildFile; }; + 43D6617123DBA58E0062BFFE /* (null) in Sources */ = {isa = PBXBuildFile; }; + 43D6617223DBA58E0062BFFE /* (null) in Sources */ = {isa = PBXBuildFile; }; + 43D6617323DBA58E0062BFFE /* (null) in Sources */ = {isa = PBXBuildFile; }; + 43EBD3AD23DBC06800B09D05 /* DirectionsCredentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43EBD3AC23DBC06800B09D05 /* DirectionsCredentials.swift */; }; + 43EBD3AE23DBC06800B09D05 /* DirectionsCredentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43EBD3AC23DBC06800B09D05 /* DirectionsCredentials.swift */; }; + 43EBD3AF23DBC06800B09D05 /* DirectionsCredentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43EBD3AC23DBC06800B09D05 /* DirectionsCredentials.swift */; }; + 43EBD3B023DBC06800B09D05 /* DirectionsCredentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43EBD3AC23DBC06800B09D05 /* DirectionsCredentials.swift */; }; 43F89F932350F952007B591E /* MapMatchingResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43F89F922350F952007B591E /* MapMatchingResponse.swift */; }; 43F89F942350F952007B591E /* MapMatchingResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43F89F922350F952007B591E /* MapMatchingResponse.swift */; }; 43F89F952350F952007B591E /* MapMatchingResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43F89F922350F952007B591E /* MapMatchingResponse.swift */; }; @@ -344,6 +352,7 @@ 438BFEC0233D805500457294 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 438BFEC1233D854D00457294 /* DirectionsProfileIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectionsProfileIdentifier.swift; sourceTree = ""; }; 4392557523440EC2006EEE88 /* DirectionsError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectionsError.swift; sourceTree = ""; }; + 43EBD3AC23DBC06800B09D05 /* DirectionsCredentials.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DirectionsCredentials.swift; sourceTree = ""; }; 43F89F922350F952007B591E /* MapMatchingResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapMatchingResponse.swift; sourceTree = ""; }; 8D381B601FD9F5B1008D5A58 /* noDestinationName.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = noDestinationName.json; sourceTree = ""; }; 8D381B621FDB01D1008D5A58 /* apiDestinationName.json */ = {isa = PBXFileReference; explicitFileType = text.json; path = apiDestinationName.json; sourceTree = ""; }; @@ -606,6 +615,7 @@ C58EA7A91E9D7EAD008F98CE /* Congestion.swift */, 35DBF004217DF0D90009D2AE /* CoordinateBounds.swift */, DD6254731AE70CB700017857 /* Directions.swift */, + 43EBD3AC23DBC06800B09D05 /* DirectionsCredentials.swift */, 4392557523440EC2006EEE88 /* DirectionsError.swift */, C59094BE203B800300EB2417 /* DirectionsOptions.swift */, 438BFEC1233D854D00457294 /* DirectionsProfileIdentifier.swift */, @@ -1184,6 +1194,7 @@ 35EFD00C207DFACA00BF3873 /* VisualInstruction.swift in Sources */, C584E3F71F201C7B00BBC9BB /* RoadClasses.swift in Sources */, C5DAACA220195AB2001F9261 /* MatchOptions.swift in Sources */, + 43D6617123DBA58E0062BFFE /* (null) in Sources */, DA1A10CC1D00F969009F82FA /* Waypoint.swift in Sources */, 35DBF006217DF0D90009D2AE /* CoordinateBounds.swift in Sources */, DAD06E3D23A00A01001A917D /* QuickLook.swift in Sources */, @@ -1191,6 +1202,7 @@ DA1A10C81D00F969009F82FA /* Route.swift in Sources */, 431E93D023466D7500A71B44 /* Codable.swift in Sources */, C5990B4D2045E74800D7DFD4 /* DirectionsOptions.swift in Sources */, + 43EBD3AE23DBC06800B09D05 /* DirectionsCredentials.swift in Sources */, DAA76D691DD127CB0015EC78 /* LaneIndication.swift in Sources */, 43F89F942350F952007B591E /* MapMatchingResponse.swift in Sources */, DA1A10CB1D00F969009F82FA /* RouteStep.swift in Sources */, @@ -1256,6 +1268,7 @@ 35EFD00D207DFACA00BF3873 /* VisualInstruction.swift in Sources */, C584E3F81F201C7C00BBC9BB /* RoadClasses.swift in Sources */, C5DAACA320195AB2001F9261 /* MatchOptions.swift in Sources */, + 43D6617223DBA58E0062BFFE /* (null) in Sources */, DA1A10F21D010247009F82FA /* Waypoint.swift in Sources */, 35DBF007217DF0D90009D2AE /* CoordinateBounds.swift in Sources */, DAD06E3E23A00A02001A917D /* QuickLook.swift in Sources */, @@ -1263,6 +1276,7 @@ DA1A10EE1D010247009F82FA /* Route.swift in Sources */, 431E93D123466D7600A71B44 /* Codable.swift in Sources */, C5990B4E2045E74900D7DFD4 /* DirectionsOptions.swift in Sources */, + 43EBD3AF23DBC06800B09D05 /* DirectionsCredentials.swift in Sources */, DAA76D6A1DD127CB0015EC78 /* LaneIndication.swift in Sources */, 43F89F952350F952007B591E /* MapMatchingResponse.swift in Sources */, DA1A10F11D010247009F82FA /* RouteStep.swift in Sources */, @@ -1328,6 +1342,7 @@ 35EFD00E207DFACA00BF3873 /* VisualInstruction.swift in Sources */, C584E3F91F201C7D00BBC9BB /* RoadClasses.swift in Sources */, C5DAACA420195AB3001F9261 /* MatchOptions.swift in Sources */, + 43D6617323DBA58E0062BFFE /* (null) in Sources */, DA1A11091D0103A3009F82FA /* Waypoint.swift in Sources */, 35DBF008217DF0D90009D2AE /* CoordinateBounds.swift in Sources */, DAD06E3F23A00A02001A917D /* QuickLook.swift in Sources */, @@ -1335,6 +1350,7 @@ DA1A11051D0103A3009F82FA /* Route.swift in Sources */, 431E93D223466D7700A71B44 /* Codable.swift in Sources */, C5990B4F2045E74A00D7DFD4 /* DirectionsOptions.swift in Sources */, + 43EBD3B023DBC06800B09D05 /* DirectionsCredentials.swift in Sources */, DAA76D6B1DD127CB0015EC78 /* LaneIndication.swift in Sources */, 43F89F962350F952007B591E /* MapMatchingResponse.swift in Sources */, DA1A11081D0103A3009F82FA /* RouteStep.swift in Sources */, @@ -1372,6 +1388,7 @@ DAC05F161CFBFAC400FA0071 /* Waypoint.swift in Sources */, C57D55081DB58C0200B94B74 /* Lane.swift in Sources */, DAC05F181CFC075300FA0071 /* Route.swift in Sources */, + 43D6617023DBA58E0062BFFE /* (null) in Sources */, C59094BF203B800300EB2417 /* DirectionsOptions.swift in Sources */, 35DBF005217DF0D90009D2AE /* CoordinateBounds.swift in Sources */, DAD06E3C23A00A01001A917D /* QuickLook.swift in Sources */, @@ -1379,6 +1396,7 @@ DAA76D681DD127CB0015EC78 /* LaneIndication.swift in Sources */, 431E93CF23466D7400A71B44 /* Codable.swift in Sources */, C59094C1203DE6BC00EB2417 /* DirectionsResult.swift in Sources */, + 43EBD3AD23DBC06800B09D05 /* DirectionsCredentials.swift in Sources */, DAC05F1A1CFC077C00FA0071 /* RouteLeg.swift in Sources */, 43F89F932350F952007B591E /* MapMatchingResponse.swift in Sources */, C5434B8A200693D00069E887 /* Tracepoint.swift in Sources */, diff --git a/Sources/MapboxDirections/Directions.swift b/Sources/MapboxDirections/Directions.swift index e5da56fb8..a362438be 100644 --- a/Sources/MapboxDirections/Directions.swift +++ b/Sources/MapboxDirections/Directions.swift @@ -5,15 +5,6 @@ typealias JSONDictionary = [String: Any] /// Indicates that an error occurred in MapboxDirections. public let MBDirectionsErrorDomain = "com.mapbox.directions.ErrorDomain" -/// The Mapbox access token specified in the main application bundle’s Info.plist. -let defaultAccessToken = Bundle.main.object(forInfoDictionaryKey: "MGLMapboxAccessToken") as? String -let defaultApiEndPointURLString = Bundle.main.object(forInfoDictionaryKey: "MGLMapboxAPIBaseURL") as? String - -var skuToken: String? { - guard let mbx: AnyClass = NSClassFromString("MBXAccounts") else { return nil } - guard mbx.responds(to: Selector(("serviceSkuToken"))) else { return nil } - return mbx.value(forKeyPath: "serviceSkuToken") as? String -} /// The user agent string for any HTTP requests performed directly within this library. let userAgent: String = { @@ -96,15 +87,7 @@ open class Directions: NSObject { */ public static let shared = Directions(accessToken: nil) - /** - The API endpoint to use when requesting directions. - */ - public private(set) var apiEndpoint: URL - - /** - The Mapbox access token to associate with the request. - */ - public let accessToken: String + public let credentials: DirectionsCredentials /** Initializes a newly created directions object with an optional access token and host. @@ -112,20 +95,8 @@ open class Directions: NSObject { - parameter accessToken: A Mapbox [access token](https://docs.mapbox.com/help/glossary/access-token/). If an access token is not specified when initializing the directions object, it should be specified in the `MGLMapboxAccessToken` key in the main application bundle’s Info.plist. - parameter host: An optional hostname to the server API. The [Mapbox Directions API](https://docs.mapbox.com/api/navigation/#directions) endpoint is used by default. */ - public init(accessToken: String?, host: String?) { - let accessToken = accessToken ?? defaultAccessToken - precondition(accessToken != nil && !accessToken!.isEmpty, "A Mapbox access token is required. Go to . In Info.plist, set the MGLMapboxAccessToken key to your access token, or use the Directions(accessToken:host:) initializer.") - - self.accessToken = accessToken! - - if let host = host, !host.isEmpty { - var baseURLComponents = URLComponents() - baseURLComponents.scheme = "https" - baseURLComponents.host = host - apiEndpoint = baseURLComponents.url! - } else { - apiEndpoint = URL(string:(defaultApiEndPointURLString ?? "https://api.mapbox.com"))! - } + public init(credentials: DirectionsCredentials) { + self.credentials = credentials } /** @@ -153,10 +124,9 @@ open class Directions: NSObject { - returns: The data task used to perform the HTTP request. If, while waiting for the completion handler to execute, you no longer want the resulting routes, cancel this task. */ @discardableResult open func calculate(_ options: RouteOptions, completionHandler: @escaping RouteCompletionHandler) -> URLSessionDataTask { - let fetchStart = Date() + options.fetchStartDate = Date() let request = urlRequest(forCalculating: options) let requestTask = URLSession.shared.dataTask(with: request) { (possibleData, possibleResponse, possibleError) in - let responseEndDate = Date() guard let response = possibleResponse, ["application/json", "text/html"].contains(response.mimeType) else { let response = RouteResponse(error: .invalidResponse(possibleResponse)) completionHandler(response) @@ -280,7 +250,7 @@ open class Directions: NSObject { } catch { DispatchQueue.main.async { let caughtError = DirectionsError.unknown(response: response, underlying: error, code: nil, message: nil) - let result = MapMatchingResponse( error: caughtError) + let result = MapMatchingResponse(error: caughtError) completionHandler(result) } } @@ -330,7 +300,10 @@ open class Directions: NSObject { DispatchQueue.global(qos: .userInitiated).async { do { - let decoder = DirectionsDecoder(options: options, decodingRouteResponseFromMatchService: true) + + //FIXME: FIX TO USE MATCHING RESPONSE -> ROUTE RESPONSE + let decoder = JSONDecoder() + decoder.userInfo[.options] = options var result = try decoder.decode(RouteResponse.self, from: data) guard result.code == "Ok" else { let apiError = Directions.informativeError(code: result.code, message:nil, response: response, underlyingError: possibleError) @@ -474,37 +447,3 @@ public extension CodingUserInfoKey { } - -public class DirectionsDecoder: JSONDecoder { - /** - Initializes a `DirectionsDecoder` with a given `RouteOption`. - */ - public init(options: DirectionsOptions, decodingRouteResponseFromMatchService: Bool = false) { - super.init() - routeOptions = options - } - - var routeOptions: DirectionsOptions? { - get { - return userInfo[.options] as? DirectionsOptions - } set { - userInfo[.options] = newValue - } - } - - var decodingRouteResponseFromMatchService: Bool { - get { - return userInfo[.routesFromMatch] as? Bool ?? false - } set { - userInfo[.routesFromMatch] = newValue - } - } - - var tracepoints: [Tracepoint?]? { - get { - return userInfo[.routesFromMatch] as? [Tracepoint?] - } set { - userInfo[.tracepoints] = newValue - } - } -} diff --git a/Sources/MapboxDirections/DirectionsCredentials.swift b/Sources/MapboxDirections/DirectionsCredentials.swift new file mode 100644 index 000000000..9740997c3 --- /dev/null +++ b/Sources/MapboxDirections/DirectionsCredentials.swift @@ -0,0 +1,30 @@ +import Foundation + +/// The Mapbox access token specified in the main application bundle’s Info.plist. +let defaultAccessToken = Bundle.main.object(forInfoDictionaryKey: "MGLMapboxAccessToken") as? String +let defaultApiEndPointURLString = Bundle.main.object(forInfoDictionaryKey: "MGLMapboxAPIBaseURL") as? String + +public struct DirectionsCredentials { + public let accessToken: String? + public let host: URL? + public var skuToken: String? { + guard let mbx: AnyClass = NSClassFromString("MBXAccounts") else { return nil } + guard mbx.responds(to: Selector(("serviceSkuToken"))) else { return nil } + return mbx.value(forKeyPath: "serviceSkuToken") as? String + } + + init(accessToken: String? = nil, host: URL? = nil) throws { + self.accessToken = accessToken ?? defaultAccessToken + + precondition(accessToken != nil && !accessToken!.isEmpty, "A Mapbox access token is required. Go to . In Info.plist, set the MGLMapboxAccessToken key to your access token, or use the Directions(accessToken:host:) initializer.") + + if let host = host { + self.host = host + } else if let defaultHostString = defaultApiEndPointURLString { + self.host = URL(string: defaultHostString) + } else { + self.host = nil + } + } +} + diff --git a/Sources/MapboxDirections/DirectionsOptions.swift b/Sources/MapboxDirections/DirectionsOptions.swift index a3de84a85..8d08739c7 100644 --- a/Sources/MapboxDirections/DirectionsOptions.swift +++ b/Sources/MapboxDirections/DirectionsOptions.swift @@ -281,6 +281,17 @@ open class DirectionsOptions: Codable { */ open var includesVisualInstructions = false + /** + The time immediately before a `Directions` object fetched this result. + + If you manually start fetching a task returned by `Directions.url(forCalculating:)`, this property is set to `nil`; use the `URLSessionTaskTransactionMetrics.fetchStartDate` property instead. This property may also be set to `nil` if you create this result from a JSON object or encoded object. + + This property does not persist after encoding and decoding. + */ + open var fetchStartDate: Date? + + + // MARK: Getting the Request URL /** diff --git a/Sources/MapboxDirections/DirectionsResult.swift b/Sources/MapboxDirections/DirectionsResult.swift index dc614272c..3fc9a0b58 100644 --- a/Sources/MapboxDirections/DirectionsResult.swift +++ b/Sources/MapboxDirections/DirectionsResult.swift @@ -23,12 +23,11 @@ open class DirectionsResult: Codable { // MARK: Creating a Directions Result - init(legs: [RouteLeg], shape: LineString?, distance: CLLocationDistance, expectedTravelTime: TimeInterval, options: DirectionsOptions) { + init(legs: [RouteLeg], shape: LineString?, distance: CLLocationDistance, expectedTravelTime: TimeInterval) { self.legs = legs self.shape = shape self.distance = distance self.expectedTravelTime = expectedTravelTime - _directionsOptions = options } public required init(from decoder: Decoder) throws { @@ -36,30 +35,14 @@ open class DirectionsResult: Codable { legs = try container.decode([RouteLeg].self, forKey: .legs) distance = try container.decode(CLLocationDistance.self, forKey: .distance) expectedTravelTime = try container.decode(TimeInterval.self, forKey: .expectedTravelTime) - - guard let directionsOptions = decoder.userInfo[.options] as? DirectionsOptions else { - throw DirectionsCodingError.missingOptions - } - _directionsOptions = directionsOptions if let polyLineString = try container.decodeIfPresent(PolyLineString.self, forKey: .shape) { shape = try LineString(polyLineString: polyLineString) + } else { shape = nil } - - // Associate each leg JSON with a source and destination. The sequence of destinations is offset by one from the sequence of sources. - - let waypoints = directionsOptions.legSeparators //we don't want to name via points - let legInfo = zip(zip(waypoints.prefix(upTo: waypoints.endIndex - 1), waypoints.suffix(from: 1)), legs) - - for (endpoints, leg) in legInfo { - leg.source = endpoints.0 - leg.destination = endpoints.1 - } - - accessToken = try container.decodeIfPresent(String.self, forKey: .accessToken) - apiEndpoint = try container.decodeIfPresent(URL.self, forKey: .apiEndpoint) + routeIdentifier = try container.decodeIfPresent(String.self, forKey: .routeIdentifier) if let identifier = try container.decodeIfPresent(String.self, forKey: .speechLocale) { @@ -69,17 +52,18 @@ open class DirectionsResult: Codable { } } + public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(legs, forKey: .legs) if let shape = shape { - let polyLineString = PolyLineString(lineString: shape, shapeFormat: directionsOptions.shapeFormat) + let options = encoder.userInfo[.options] as? DirectionsOptions + let shapeFormat = options?.shapeFormat ?? .default + let polyLineString = PolyLineString(lineString: shape, shapeFormat: shapeFormat) try container.encode(polyLineString, forKey: .shape) } try container.encode(distance, forKey: .distance) try container.encode(expectedTravelTime, forKey: .expectedTravelTime) - try container.encodeIfPresent(accessToken, forKey: .accessToken) - try container.encodeIfPresent(apiEndpoint, forKey: .apiEndpoint) try container.encodeIfPresent(routeIdentifier, forKey: .routeIdentifier) try container.encodeIfPresent(speechLocale?.identifier, forKey: .speechLocale) } @@ -94,7 +78,7 @@ open class DirectionsResult: Codable { Using the [Mapbox Maps SDK for iOS](https://docs.mapbox.com/ios/maps/) or [Mapbox Maps SDK for macOS](https://mapbox.github.io/mapbox-gl-native/macos/), you can create an `MGLPolyline` object using these coordinates to display an overview of the route on an `MGLMapView`. */ public let shape: LineString? - + // MARK: Getting the Legs Along the Route /** diff --git a/Sources/MapboxDirections/MapMatching/Match.swift b/Sources/MapboxDirections/MapMatching/Match.swift index 2b12fac48..a05fe0966 100644 --- a/Sources/MapboxDirections/MapMatching/Match.swift +++ b/Sources/MapboxDirections/MapMatching/Match.swift @@ -12,7 +12,6 @@ open class Match: DirectionsResult { private enum CodingKeys: String, CodingKey { case confidence case tracepoints - case matchOptions } /** @@ -28,11 +27,10 @@ open class Match: DirectionsResult { - parameter tracepoints: Tracepoints on the road network that match the tracepoints in `options`. - parameter options: The criteria to match. */ - public init(legs: [RouteLeg], shape: LineString?, distance: CLLocationDistance, expectedTravelTime: TimeInterval, confidence: Float, tracepoints: [Tracepoint?], options: MatchOptions) { - matchOptions = options + public init(legs: [RouteLeg], shape: LineString?, distance: CLLocationDistance, expectedTravelTime: TimeInterval, confidence: Float, tracepoints: [Tracepoint?]) { self.confidence = confidence self.tracepoints = tracepoints - super.init(legs: legs, shape: shape, distance: distance, expectedTravelTime: expectedTravelTime, options: options) + super.init(legs: legs, shape: shape, distance: distance, expectedTravelTime: expectedTravelTime) } /** @@ -45,12 +43,6 @@ open class Match: DirectionsResult { let container = try decoder.container(keyedBy: CodingKeys.self) confidence = try container.decode(Float.self, forKey: .confidence) tracepoints = try container.decodeIfPresent([Tracepoint?].self, forKey: .tracepoints) ?? [] - if let matchOptions = try container.decodeIfPresent(MatchOptions.self, forKey: .matchOptions) - ?? decoder.userInfo[.options] as? MatchOptions { - self.matchOptions = matchOptions - } else { - throw DirectionsCodingError.missingOptions - } try super.init(from: decoder) } @@ -58,7 +50,6 @@ open class Match: DirectionsResult { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(confidence, forKey: .confidence) try container.encode(tracepoints, forKey: .tracepoints) - try container.encode(matchOptions, forKey: .matchOptions) try super.encode(to: encoder) } @@ -74,14 +65,6 @@ open class Match: DirectionsResult { */ open var tracepoints: [Tracepoint?] - public override var directionsOptions: DirectionsOptions { - return matchOptions - } - - /** - `MatchOptions` used to create the match request. - */ - public let matchOptions: MatchOptions } extension Match: Equatable { diff --git a/Sources/MapboxDirections/Route.swift b/Sources/MapboxDirections/Route.swift index 44bfd02d6..63c00edc8 100644 --- a/Sources/MapboxDirections/Route.swift +++ b/Sources/MapboxDirections/Route.swift @@ -7,48 +7,6 @@ import Turf Typically, you do not create instances of this class directly. Instead, you receive route objects when you request directions using the `Directions.calculate(_:completionHandler:)` or `Directions.calculateRoutes(matching:completionHandler:)` method. However, if you use the `Directions.url(forCalculating:)` method instead, you can use `JSONDecoder` to convert the HTTP response into a `RouteResponse` or `MapMatchingResponse` object and access the `RouteResponse.routes` or `MapMatchingResponse.routes` property. */ open class Route: DirectionsResult { - private enum CodingKeys: String, CodingKey { - case routeOptions - } - - /** - Initializes a route. - - Typically, you do not create instances of this class directly. Instead, you receive route objects when you request directions using the `Directions.calculate(_:completionHandler:)` method. - - - parameter legs: The legs that are traversed in order. - - parameter shape: The roads or paths taken as a contiguous polyline. - - parameter distance: The route’s distance, measured in meters. - - parameter expectedTravelTime: The route’s expected travel time, measured in seconds. - - parameter options: The criteria for producing a route with these parameters. - */ - public init(legs: [RouteLeg], shape: LineString?, distance: CLLocationDistance, expectedTravelTime: TimeInterval, options: RouteOptions) { - routeOptions = options - super.init(legs: legs, shape: shape, distance: distance, expectedTravelTime: expectedTravelTime, options: options) - } - - /** - Creates a route from a decoder. - - - precondition: If the decoder is decoding JSON data from an API response, the `Decoder.userInfo` dictionary must contain a `RouteOptions` or `MatchOptions` object in the `CodingUserInfoKey.options` key. If it does not, a `DirectionsCodingError.missingOptions` error is thrown. - - parameter decoder: The decoder of JSON-formatted API response data or a previously encoded `Route` object. - */ - public required init(from decoder: Decoder) throws { - if let matchOptions = decoder.userInfo[.options] as? MatchOptions { - routeOptions = RouteOptions(matchOptions: matchOptions) - } else if let routeOptions = decoder.userInfo[.options] as? RouteOptions { - self.routeOptions = routeOptions - } else { - throw DirectionsCodingError.missingOptions - } - - try super.init(from: decoder) - } - - public override var directionsOptions: DirectionsOptions { - return routeOptions as DirectionsOptions - } - public var routeOptions: RouteOptions } extension Route: Equatable { diff --git a/Sources/MapboxDirections/RouteResponse.swift b/Sources/MapboxDirections/RouteResponse.swift index 5cb2285ed..17330670c 100644 --- a/Sources/MapboxDirections/RouteResponse.swift +++ b/Sources/MapboxDirections/RouteResponse.swift @@ -7,6 +7,20 @@ public struct RouteResponse { public let uuid: String? public let routes: [Route]? public let waypoints: [Waypoint]? + + public let request: RouteOptions + public let credentials: DirectionsCredentials +// public let accessToken: String +// public let endpoint: URL? + + /** + The time when this `RouteResponse` object was created, which is immediately upon recieving the raw URL response. + + If you manually start fetching a task returned by `Directions.url(forCalculating:)`, this property is set to `nil`; use the `URLSessionTaskTransactionMetrics.responseEndDate` property instead. This property may also be set to `nil` if you create this result from a JSON object or encoded object. + + This property does not persist after encoding and decoding. + */ + public var created: Date = Date() } extension RouteResponse: Codable { @@ -17,20 +31,24 @@ extension RouteResponse: Codable { case uuid case routes case waypoints - - // Matching-service specific keys. Used in decode only. - case matches = "matchings" - case tracepoints } public init(error: DirectionsError) { self.init(code: nil, message: nil, error: error, uuid: nil, routes: nil, waypoints: nil) } +// public init(matchResponse match: MapMatchingResponse) { +// self.code = match.code +// self.message = match.message +// self.error = match.error +// self.routes = match.matches +// self.waypoints = match.tracepoints +// +// } + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) // Is this an edge-case, where we are creating a route response from the map matching service? - let fromMatchingService = (decoder as? DirectionsDecoder)?.decodingRouteResponseFromMatchService ?? false self.code = try container.decodeIfPresent(String.self, forKey: .code) self.message = try container.decodeIfPresent(String.self, forKey: .message) @@ -40,7 +58,7 @@ extension RouteResponse: Codable { self.uuid = try container.decodeIfPresent(String.self, forKey: .uuid) // Decode waypoints from the response and update their names according to the waypoints from DirectionsOptions.waypoints. - let decodedWaypoints = try container.decodeIfPresent([Waypoint?].self, forKey: fromMatchingService ? .tracepoints : .waypoints)?.compactMap { $0 } + let decodedWaypoints = try container.decodeIfPresent([Waypoint?].self, forKey: .waypoints) if let decodedWaypoints = decodedWaypoints, let options = decoder.userInfo[.options] as? DirectionsOptions { // The response lists the same number of tracepoints as the waypoints in the request, whether or not a given waypoint is leg-separating. waypoints = zip(decodedWaypoints, options.waypoints).map { (pair) -> Waypoint in @@ -55,7 +73,7 @@ extension RouteResponse: Codable { waypoints = decodedWaypoints } - if let routes = try container.decodeIfPresent([Route].self, forKey: fromMatchingService ? .matches : .routes) { + if let routes = try container.decodeIfPresent([Route].self, forKey: .routes) { // Postprocess each route. for route in routes { route.routeIdentifier = uuid From ee31e7f8b04a6174362646214e26fd0ed666cdd1 Mon Sep 17 00:00:00 2001 From: Jerrad Thramer Date: Mon, 27 Jan 2020 16:36:17 -0700 Subject: [PATCH 04/24] WIP: Integrating credentials. --- Sources/MapboxDirections/Directions.swift | 63 ++++++++----------- .../DirectionsCredentials.swift | 10 +-- .../MapMatching/MapMatchingResponse.swift | 16 ++++- .../MapboxDirections/OfflineDirections.swift | 4 +- Sources/MapboxDirections/RouteResponse.swift | 8 +-- 5 files changed, 49 insertions(+), 52 deletions(-) diff --git a/Sources/MapboxDirections/Directions.swift b/Sources/MapboxDirections/Directions.swift index a362438be..d92bc966b 100644 --- a/Sources/MapboxDirections/Directions.swift +++ b/Sources/MapboxDirections/Directions.swift @@ -85,7 +85,7 @@ open class Directions: NSObject { To use this object, a Mapbox [access token](https://docs.mapbox.com/help/glossary/access-token/) should be specified in the `MGLMapboxAccessToken` key in the main application bundle’s Info.plist. */ - public static let shared = Directions(accessToken: nil) + public static let shared = Directions() public let credentials: DirectionsCredentials @@ -95,20 +95,10 @@ open class Directions: NSObject { - parameter accessToken: A Mapbox [access token](https://docs.mapbox.com/help/glossary/access-token/). If an access token is not specified when initializing the directions object, it should be specified in the `MGLMapboxAccessToken` key in the main application bundle’s Info.plist. - parameter host: An optional hostname to the server API. The [Mapbox Directions API](https://docs.mapbox.com/api/navigation/#directions) endpoint is used by default. */ - public init(credentials: DirectionsCredentials) { + public init(credentials: DirectionsCredentials = .init()) { self.credentials = credentials } - /** - Initializes a newly created directions object with an optional access token. - - The directions object sends requests to the [Mapbox Directions API](https://docs.mapbox.com/api/navigation/#directions) endpoint. - - - parameter accessToken: A Mapbox [access token](https://docs.mapbox.com/help/glossary/access-token/). If an access token is not specified when initializing the directions object, it should be specified in the `MGLMapboxAccessToken` key in the main application bundle’s Info.plist. - */ - public convenience init(accessToken: String?) { - self.init(accessToken: accessToken, host: nil) - } // MARK: Getting Directions @@ -128,27 +118,29 @@ open class Directions: NSObject { let request = urlRequest(forCalculating: options) let requestTask = URLSession.shared.dataTask(with: request) { (possibleData, possibleResponse, possibleError) in guard let response = possibleResponse, ["application/json", "text/html"].contains(response.mimeType) else { - let response = RouteResponse(error: .invalidResponse(possibleResponse)) + let response = RouteResponse(credentials: self.credentials, options: options, error: .invalidResponse(possibleResponse)) completionHandler(response) return } guard let data = possibleData else { - let response = RouteResponse(error: .noData) + let response = RouteResponse(credentials: self.credentials, options: options, error: .noData) completionHandler(response) return } if let error = possibleError { let unknownError = DirectionsError.unknown(response: possibleResponse, underlying: error, code: nil, message: nil) - let response = RouteResponse(error: unknownError) + let response = RouteResponse(credentials: self.credentials, options: options, error: unknownError) completionHandler(response) return } DispatchQueue.global(qos: .userInitiated).async { do { - let decoder = DirectionsDecoder(options: options) + let decoder = JSONDecoder() + decoder.userInfo = [.options: options] + var result = try decoder.decode(RouteResponse.self, from: data) guard (result.code == nil && result.message == nil) || result.code == "Ok" else { let apiError = Directions.informativeError(code: result.code, message: result.message, response: response, underlyingError: possibleError) @@ -167,15 +159,13 @@ open class Directions: NSObject { return } - result.postprocess(accessToken: self.accessToken, apiEndpoint: self.apiEndpoint, fetchStartDate: fetchStart, responseEndDate: responseEndDate) - DispatchQueue.main.async { completionHandler(result) } } catch { DispatchQueue.main.async { let bailError = Directions.informativeError(code: nil, message: nil, response: response, underlyingError: error) - let response = RouteResponse(error: bailError) + let response = RouteResponse(credentials: self.credentials, options: options, error: bailError) completionHandler(response) } } @@ -199,7 +189,7 @@ open class Directions: NSObject { - returns: The data task used to perform the HTTP request. If, while waiting for the completion handler to execute, you no longer want the resulting matches, cancel this task. */ @discardableResult open func calculate(_ options: MatchOptions, completionHandler: @escaping MatchCompletionHandler) -> URLSessionDataTask { - let fetchStart = Date() + options.fetchStartDate = Date() let request = urlRequest(forCalculating: options) let requestTask = URLSession.shared.dataTask(with: request) { (possibleData, possibleResponse, possibleError) in let responseEndDate = Date() @@ -210,20 +200,21 @@ open class Directions: NSObject { } guard let data = possibleData else { - let result = MapMatchingResponse(error: .noData) + let result = MapMatchingResponse(credentials: self.credentials, options: options, error: .noData) return completionHandler(result) } if let error = possibleError { let unknownError = DirectionsError.unknown(response: possibleResponse, underlying: error, code: nil, message: nil) - let response = MapMatchingResponse(error: unknownError) + let response = MapMatchingResponse(credentials: self.credentials, options: options, error: unknownError) completionHandler(response) return } DispatchQueue.global(qos: .userInitiated).async { do { - let decoder = DirectionsDecoder(options: options) + let decoder = JSONDecoder() + decoder.userInfo = [.options: options] var result = try decoder.decode(MapMatchingResponse.self, from: data) guard result.code == "Ok" else { let apiError = Directions.informativeError(code: result.code, message: result.message, response: response, underlyingError: possibleError) @@ -241,16 +232,14 @@ open class Directions: NSObject { } return } - - result.postprocess(accessToken: self.accessToken, apiEndpoint: self.apiEndpoint, fetchStartDate: fetchStart, responseEndDate: responseEndDate) - + DispatchQueue.main.async { completionHandler(result) } } catch { DispatchQueue.main.async { let caughtError = DirectionsError.unknown(response: response, underlying: error, code: nil, message: nil) - let result = MapMatchingResponse(error: caughtError) + let result = MapMatchingResponse(credentials: self.credentials, options: options, error: caughtError) completionHandler(result) } } @@ -274,26 +263,26 @@ open class Directions: NSObject { - returns: The data task used to perform the HTTP request. If, while waiting for the completion handler to execute, you no longer want the resulting routes, cancel this task. */ @discardableResult open func calculateRoutes(matching options: MatchOptions, completionHandler: @escaping RouteCompletionHandler) -> URLSessionDataTask { - let fetchStart = Date() + options.fetchStartDate = Date() let request = urlRequest(forCalculating: options) let requestTask = URLSession.shared.dataTask(with: request) { (possibleData, possibleResponse, possibleError) in let responseEndDate = Date() guard let response = possibleResponse, response.mimeType == "application/json" else { let error = DirectionsError.invalidResponse(possibleResponse) - let result = RouteResponse(error: error) + let result = RouteResponse(credentials: self.credentials, options: options, error: error) completionHandler(result) return } guard let data = possibleData else { - let result = RouteResponse(error: .noData) + let result = RouteResponse(credentials: self.credentials, options: options, error: .noData) completionHandler(result) return } if let error = possibleError { let unknownError = DirectionsError.unknown(response: possibleResponse, underlying: error, code: nil, message: nil) - let result = RouteResponse(error: unknownError) + let result = RouteResponse(credentials: self.credentials, options: options, error: unknownError) completionHandler(result) return } @@ -321,9 +310,7 @@ open class Directions: NSObject { } return } - - result.postprocess(accessToken: self.accessToken, apiEndpoint: self.apiEndpoint, fetchStartDate: fetchStart, responseEndDate: responseEndDate) - + DispatchQueue.main.async { completionHandler(result) @@ -331,7 +318,7 @@ open class Directions: NSObject { } catch { DispatchQueue.main.async { let caughtError = DirectionsError.unknown(response: response, underlying: error, code: nil, message: nil) - let result = RouteResponse(error: caughtError) + let result = RouteResponse(credentials: self.credentials, options: options, error: caughtError) completionHandler(result) } } @@ -369,13 +356,13 @@ open class Directions: NSObject { open func url(forCalculating options: DirectionsOptions, httpMethod: String) -> URL { let includesQuery = httpMethod != "POST" var params = (includesQuery ? options.urlQueryItems : []) - params += [URLQueryItem(name: "access_token", value: accessToken)] + params += [URLQueryItem(name: "access_token", value: credentials.accessToken)] - if let skuToken = skuToken { + if let skuToken = credentials.skuToken { params += [URLQueryItem(name: "sku", value: skuToken)] } - let unparameterizedURL = URL(string: includesQuery ? options.path : options.abridgedPath, relativeTo: apiEndpoint)! + let unparameterizedURL = URL(string: includesQuery ? options.path : options.abridgedPath, relativeTo: credentials.host)! var components = URLComponents(url: unparameterizedURL, resolvingAgainstBaseURL: true)! components.queryItems = params return components.url! diff --git a/Sources/MapboxDirections/DirectionsCredentials.swift b/Sources/MapboxDirections/DirectionsCredentials.swift index 9740997c3..f7959c223 100644 --- a/Sources/MapboxDirections/DirectionsCredentials.swift +++ b/Sources/MapboxDirections/DirectionsCredentials.swift @@ -6,24 +6,24 @@ let defaultApiEndPointURLString = Bundle.main.object(forInfoDictionaryKey: "MGLM public struct DirectionsCredentials { public let accessToken: String? - public let host: URL? + public let host: URL public var skuToken: String? { guard let mbx: AnyClass = NSClassFromString("MBXAccounts") else { return nil } guard mbx.responds(to: Selector(("serviceSkuToken"))) else { return nil } return mbx.value(forKeyPath: "serviceSkuToken") as? String } - init(accessToken: String? = nil, host: URL? = nil) throws { + public init(accessToken: String? = nil, host: URL? = nil) { self.accessToken = accessToken ?? defaultAccessToken precondition(accessToken != nil && !accessToken!.isEmpty, "A Mapbox access token is required. Go to . In Info.plist, set the MGLMapboxAccessToken key to your access token, or use the Directions(accessToken:host:) initializer.") if let host = host { self.host = host - } else if let defaultHostString = defaultApiEndPointURLString { - self.host = URL(string: defaultHostString) + } else if let defaultHostString = defaultApiEndPointURLString, let defaultHost = URL(string: defaultHostString) { + self.host = defaultHost } else { - self.host = nil + self.host = URL(string: "https://api.mapbox.com")! } } } diff --git a/Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift b/Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift index 0de49c1e6..75cd9fe6e 100644 --- a/Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift +++ b/Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift @@ -6,6 +6,18 @@ public struct MapMatchingResponse { public var error: DirectionsError? public var matches : [Match]? public var tracepoints: [Tracepoint?]? + + public let options: MatchOptions + public let credentials: DirectionsCredentials + + /** + The time when this `MapMatchingResponse` object was created, which is immediately upon recieving the raw URL response. + + If you manually start fetching a task returned by `Directions.url(forCalculating:)`, this property is set to `nil`; use the `URLSessionTaskTransactionMetrics.responseEndDate` property instead. This property may also be set to `nil` if you create this result from a JSON object or encoded object. + + This property does not persist after encoding and decoding. + */ + public var created: Date = Date() } extension MapMatchingResponse: Codable { @@ -17,8 +29,8 @@ extension MapMatchingResponse: Codable { case tracepoints } - public init(error: DirectionsError) { - self.init(code: nil, message: nil, error: error, matches: nil, tracepoints: nil) + public init(credentials: DirectionsCredentials, options: MatchOptions, error: DirectionsError) { + self.init(code: nil, message: nil, error: error, matches: nil, tracepoints: nil, options: options, credentials: credentials) } public init(from decoder: Decoder) throws { diff --git a/Sources/MapboxDirections/OfflineDirections.swift b/Sources/MapboxDirections/OfflineDirections.swift index 19c012ec2..f1fcab174 100644 --- a/Sources/MapboxDirections/OfflineDirections.swift +++ b/Sources/MapboxDirections/OfflineDirections.swift @@ -32,9 +32,9 @@ extension Directions: OfflineDirectionsProtocol { The URL to a list of available versions. */ public var availableVersionsURL: URL { - let url = apiEndpoint.appendingPathComponent("route-tiles/v1").appendingPathComponent("versions") + let url = credentials.host.appendingPathComponent("route-tiles/v1").appendingPathComponent("versions") var components = URLComponents(url: url, resolvingAgainstBaseURL: true) - components?.queryItems = [URLQueryItem(name: "access_token", value: accessToken)] + components?.queryItems = [URLQueryItem(name: "access_token", value: credentials.accessToken)] return components!.url! } diff --git a/Sources/MapboxDirections/RouteResponse.swift b/Sources/MapboxDirections/RouteResponse.swift index 17330670c..40ee5d371 100644 --- a/Sources/MapboxDirections/RouteResponse.swift +++ b/Sources/MapboxDirections/RouteResponse.swift @@ -8,10 +8,8 @@ public struct RouteResponse { public let routes: [Route]? public let waypoints: [Waypoint]? - public let request: RouteOptions + public let options: RouteOptions public let credentials: DirectionsCredentials -// public let accessToken: String -// public let endpoint: URL? /** The time when this `RouteResponse` object was created, which is immediately upon recieving the raw URL response. @@ -32,8 +30,8 @@ extension RouteResponse: Codable { case routes case waypoints } - public init(error: DirectionsError) { - self.init(code: nil, message: nil, error: error, uuid: nil, routes: nil, waypoints: nil) + public init(credentials: DirectionsCredentials, options: RouteOptions, error: DirectionsError) { + self.init(code: nil, message: nil, error: error, uuid: nil, routes: nil, waypoints: nil, options: options, credentials: credentials) } // public init(matchResponse match: MapMatchingResponse) { From 30b19dc1a27d961fa646e8dbe6742373db96aa75 Mon Sep 17 00:00:00 2001 From: Jerrad Thramer Date: Thu, 30 Jan 2020 14:09:32 -0700 Subject: [PATCH 05/24] WIP: cutting down on errors --- Sources/MapboxDirections/Directions.swift | 63 ++++---------- .../MapboxDirections/DirectionsError.swift | 66 ++++++++++++++- .../MapboxDirections/DirectionsOptions.swift | 1 + .../MapboxDirections/DirectionsResult.swift | 45 ++-------- .../MapMatching/MapMatchingResponse.swift | 35 +++++--- .../MapboxDirections/OfflineDirections.swift | 4 +- Sources/MapboxDirections/RouteResponse.swift | 83 +++++++++++++------ 7 files changed, 171 insertions(+), 126 deletions(-) diff --git a/Sources/MapboxDirections/Directions.swift b/Sources/MapboxDirections/Directions.swift index d92bc966b..3af82699b 100644 --- a/Sources/MapboxDirections/Directions.swift +++ b/Sources/MapboxDirections/Directions.swift @@ -118,20 +118,20 @@ open class Directions: NSObject { let request = urlRequest(forCalculating: options) let requestTask = URLSession.shared.dataTask(with: request) { (possibleData, possibleResponse, possibleError) in guard let response = possibleResponse, ["application/json", "text/html"].contains(response.mimeType) else { - let response = RouteResponse(credentials: self.credentials, options: options, error: .invalidResponse(possibleResponse)) + let response = RouteResponse(credentials: self.credentials, options: .route(options), error: .invalidResponse(possibleResponse)) completionHandler(response) return } guard let data = possibleData else { - let response = RouteResponse(credentials: self.credentials, options: options, error: .noData) + let response = RouteResponse(credentials: self.credentials, options: .route(options), error: .noData) completionHandler(response) return } if let error = possibleError { let unknownError = DirectionsError.unknown(response: possibleResponse, underlying: error, code: nil, message: nil) - let response = RouteResponse(credentials: self.credentials, options: options, error: unknownError) + let response = RouteResponse(credentials: self.credentials, options: .route(options), error: unknownError) completionHandler(response) return } @@ -143,7 +143,7 @@ open class Directions: NSObject { var result = try decoder.decode(RouteResponse.self, from: data) guard (result.code == nil && result.message == nil) || result.code == "Ok" else { - let apiError = Directions.informativeError(code: result.code, message: result.message, response: response, underlyingError: possibleError) + let apiError = DirectionsError(code: result.code, message: result.message, response: response, underlyingError: possibleError) result.error = apiError DispatchQueue.main.async { completionHandler(result) @@ -164,8 +164,8 @@ open class Directions: NSObject { } } catch { DispatchQueue.main.async { - let bailError = Directions.informativeError(code: nil, message: nil, response: response, underlyingError: error) - let response = RouteResponse(credentials: self.credentials, options: options, error: bailError) + let bailError = DirectionsError(code: nil, message: nil, response: response, underlyingError: error) + let response = RouteResponse(credentials: self.credentials, options: .route(options), error: bailError) completionHandler(response) } } @@ -192,9 +192,8 @@ open class Directions: NSObject { options.fetchStartDate = Date() let request = urlRequest(forCalculating: options) let requestTask = URLSession.shared.dataTask(with: request) { (possibleData, possibleResponse, possibleError) in - let responseEndDate = Date() guard let response = possibleResponse, response.mimeType == "application/json" else { - let result = MapMatchingResponse(code: nil, message: nil, error: .invalidResponse(possibleResponse), matches: nil, tracepoints: nil) + let result = MapMatchingResponse(credentials: self.credentials, options: options, error: .invalidResponse(possibleResponse)) completionHandler(result) return } @@ -217,7 +216,7 @@ open class Directions: NSObject { decoder.userInfo = [.options: options] var result = try decoder.decode(MapMatchingResponse.self, from: data) guard result.code == "Ok" else { - let apiError = Directions.informativeError(code: result.code, message: result.message, response: response, underlyingError: possibleError) + let apiError = DirectionsError(code: result.code, message: result.message, response: response, underlyingError: possibleError) result.error = apiError DispatchQueue.main.async { completionHandler(result) @@ -266,23 +265,22 @@ open class Directions: NSObject { options.fetchStartDate = Date() let request = urlRequest(forCalculating: options) let requestTask = URLSession.shared.dataTask(with: request) { (possibleData, possibleResponse, possibleError) in - let responseEndDate = Date() guard let response = possibleResponse, response.mimeType == "application/json" else { let error = DirectionsError.invalidResponse(possibleResponse) - let result = RouteResponse(credentials: self.credentials, options: options, error: error) + let result = RouteResponse(credentials: self.credentials, options: .match(options), error: error) completionHandler(result) return } guard let data = possibleData else { - let result = RouteResponse(credentials: self.credentials, options: options, error: .noData) + let result = RouteResponse(credentials: self.credentials, options: .match(options), error: .noData) completionHandler(result) return } if let error = possibleError { let unknownError = DirectionsError.unknown(response: possibleResponse, underlying: error, code: nil, message: nil) - let result = RouteResponse(credentials: self.credentials, options: options, error: unknownError) + let result = RouteResponse(credentials: self.credentials, options: .match(options), error: unknownError) completionHandler(result) return } @@ -295,7 +293,7 @@ open class Directions: NSObject { decoder.userInfo[.options] = options var result = try decoder.decode(RouteResponse.self, from: data) guard result.code == "Ok" else { - let apiError = Directions.informativeError(code: result.code, message:nil, response: response, underlyingError: possibleError) + let apiError = DirectionsError(code: result.code, message:nil, response: response, underlyingError: possibleError) result.error = apiError DispatchQueue.main.async { completionHandler(result) @@ -318,7 +316,7 @@ open class Directions: NSObject { } catch { DispatchQueue.main.async { let caughtError = DirectionsError.unknown(response: response, underlying: error, code: nil, message: nil) - let result = RouteResponse(credentials: self.credentials, options: options, error: caughtError) + let result = RouteResponse(credentials: self.credentials, options: .match(options), error: caughtError) completionHandler(result) } } @@ -392,44 +390,11 @@ open class Directions: NSObject { request.setValue(userAgent, forHTTPHeaderField: "User-Agent") return request } - - // MARK: Postprocessing Responses - - /** - Returns an error that supplements the given underlying error with additional information from the an HTTP response’s body or headers. - */ - static func informativeError(code: String?, message: String?, response: URLResponse?, underlyingError error: Error?) -> DirectionsError { - if let response = response as? HTTPURLResponse { - switch (response.statusCode, code ?? "") { - case (200, "NoRoute"): - return .unableToRoute - case (200, "NoSegment"): - return .unableToLocate - case (200, "NoMatch"): - return .noMatches - case (422, "TooManyCoordinates"): - return .tooManyCoordinates - case (404, "ProfileNotFound"): - return .profileNotFound - - case (413, _): - return .requestTooLarge - case (422, "InvalidInput"): - return .invalidInput(message: message) - case (429, _): - return .rateLimited(rateLimitInterval: response.rateLimitInterval, rateLimit: response.rateLimit, resetTime: response.rateLimitResetTime) - default: - return .unknown(response: response, underlying: error, code: code, message: message) - } - } - return .unknown(response: response, underlying: error, code: code, message: message) - } - } public extension CodingUserInfoKey { static let options = CodingUserInfoKey(rawValue: "com.mapbox.directions.coding.routeOptions")! - static let routesFromMatch = CodingUserInfoKey(rawValue: "com.mapbox.directions.coding.routesFromMatch")! + static let credentials = CodingUserInfoKey(rawValue: "com.mapbox.directions.coding.credentials")! static let tracepoints = CodingUserInfoKey(rawValue: "com.mapbox.directions.coding.tracepoints")! diff --git a/Sources/MapboxDirections/DirectionsError.swift b/Sources/MapboxDirections/DirectionsError.swift index 3f82357cb..4b922dfc7 100644 --- a/Sources/MapboxDirections/DirectionsError.swift +++ b/Sources/MapboxDirections/DirectionsError.swift @@ -3,7 +3,65 @@ import Foundation /** An error that occurs when calculating directions. */ -public enum DirectionsError: LocalizedError, Codable { +public enum DirectionsError: LocalizedError { + + var code: String? { + switch self { + case .unableToRoute : + return "NoRoute" + case .unableToLocate: + return "NoSegment" + case .noMatches: + return "NoMatch" + case .tooManyCoordinates: + return "TooManyCoordinates" + case .profileNotFound: + return "ProfileNotFound" + case .invalidInput(_): + return "InvalidInput" + default: + return nil + } + } + + var message: String? { + switch self { + case let .invalidInput(message: message): + return message + case let .unkonw + default: + <#code#> + } + } + + + public init(code: String?, message: String?, response: URLResponse?, underlyingError error: Error?) { + if let response = response as? HTTPURLResponse { + switch (response.statusCode, code ?? "") { + case (200, "NoRoute"): + self = .unableToRoute + case (200, "NoSegment"): + self = .unableToLocate + case (200, "NoMatch"): + self = .noMatches + case (422, "TooManyCoordinates"): + self = .tooManyCoordinates + case (404, "ProfileNotFound"): + self = .profileNotFound + + case (413, _): + self = .requestTooLarge + case (422, "InvalidInput"): + self = .invalidInput(message: message) + case (429, _): + self = .rateLimited(rateLimitInterval: response.rateLimitInterval, rateLimit: response.rateLimit, resetTime: response.rateLimitResetTime) + default: + self = .unknown(response: response, underlying: error, code: code, message: message) + } + } + self = .unknown(response: response, underlying: error, code: code, message: message) + } + /** The server returned an empty response. */ @@ -181,4 +239,10 @@ public enum DirectionsCodingError: Error { Decoding this type requires the `Decoder.userInfo` dictionary to contain the `CodingUserInfoKey.options` key. */ case missingOptions + + + /** + Decoding this type requires the `Decoder.userInfo` dictionary to contain the `CodingUserInfoKey.credentials` key. + */ + case missingCredentials } diff --git a/Sources/MapboxDirections/DirectionsOptions.swift b/Sources/MapboxDirections/DirectionsOptions.swift index 8d08739c7..85b58a384 100644 --- a/Sources/MapboxDirections/DirectionsOptions.swift +++ b/Sources/MapboxDirections/DirectionsOptions.swift @@ -429,6 +429,7 @@ open class DirectionsOptions: Codable { ] return components.percentEncodedQuery ?? "" } + } extension DirectionsOptions: Equatable { diff --git a/Sources/MapboxDirections/DirectionsResult.swift b/Sources/MapboxDirections/DirectionsResult.swift index 3fc9a0b58..dd4315ce9 100644 --- a/Sources/MapboxDirections/DirectionsResult.swift +++ b/Sources/MapboxDirections/DirectionsResult.swift @@ -15,8 +15,6 @@ open class DirectionsResult: Codable { case distance case expectedTravelTime = "duration" case directionsOptions - case accessToken - case apiEndpoint case routeIdentifier case speechLocale = "voiceLocale" } @@ -130,33 +128,6 @@ open class DirectionsResult: Codable { */ open var speechLocale: Locale? - // MARK: Reproducing the Route - - /** - Criteria for reproducing this route. - - The route options object’s profileIdentifier property reflects the primary mode of transportation used for the route. Individual steps along the route might use different modes of transportation as necessary. - */ - public var directionsOptions: DirectionsOptions { - return _directionsOptions - } - - private let _directionsOptions: DirectionsOptions - - /** - The [access token](https://docs.mapbox.com/help/glossary/access-token/) used to make the directions request. - - This property is set automatically if a request is made via `Directions.calculate(_:completionHandler:)`. - */ - open var accessToken: String? - - /** - The endpoint used to make the directions request. - - This property is set automatically if a request is made via `Directions.calculate(_:completionHandler:)`. - */ - open var apiEndpoint: URL? - // MARK: Auditing the Server Response /** @@ -191,11 +162,11 @@ extension DirectionsResult: CustomStringConvertible { } } -extension DirectionsResult: CustomQuickLookConvertible { - func debugQuickLookObject() -> Any? { - guard let shape = shape else { - return nil - } - return debugQuickLookURL(illustrating: shape, profileIdentifier: directionsOptions.profileIdentifier) - } -} +//extension DirectionsResult: CustomQuickLookConvertible { +// func debugQuickLookObject() -> Any? { +// guard let shape = shape else { +// return nil +// } +// return debugQuickLookURL(illustrating: shape, profileIdentifier: directionsOptions.profileIdentifier) +// } +//} diff --git a/Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift b/Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift index 75cd9fe6e..ea2d01422 100644 --- a/Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift +++ b/Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift @@ -35,6 +35,17 @@ extension MapMatchingResponse: Codable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) + + guard let options = decoder.userInfo[.options] as? MatchOptions else { + throw DirectionsCodingError.missingOptions + } + self.options = options + + guard let credentials = decoder.userInfo[.credentials] as? DirectionsCredentials else { + throw DirectionsCodingError.missingCredentials + } + self.credentials = credentials + code = try container.decode(String.self, forKey: .code) message = try container.decodeIfPresent(String.self, forKey: .message) tracepoints = try container.decodeIfPresent([Tracepoint?].self, forKey: .tracepoints) @@ -47,16 +58,16 @@ extension MapMatchingResponse: Codable { } } - func postprocess(accessToken: String, apiEndpoint: URL, fetchStartDate: Date, responseEndDate: Date) { - guard let matches = self.matches else { - return - } - - for result in matches { - result.accessToken = accessToken - result.apiEndpoint = apiEndpoint - result.fetchStartDate = fetchStartDate - result.responseEndDate = responseEndDate - } - } +// func postprocess(accessToken: String, apiEndpoint: URL, fetchStartDate: Date, responseEndDate: Date) { +// guard let matches = self.matches else { +// return +// } +// +// for result in matches { +// result.accessToken = accessToken +// result.apiEndpoint = apiEndpoint +// result.fetchStartDate = fetchStartDate +// result.responseEndDate = responseEndDate +// } +// } } diff --git a/Sources/MapboxDirections/OfflineDirections.swift b/Sources/MapboxDirections/OfflineDirections.swift index f1fcab174..3879c46be 100644 --- a/Sources/MapboxDirections/OfflineDirections.swift +++ b/Sources/MapboxDirections/OfflineDirections.swift @@ -46,10 +46,10 @@ extension Directions: OfflineDirectionsProtocol { - returns: The URL to generate and download the tile pack that covers the coordinate bounds. */ public func tilesURL(for coordinateBounds: CoordinateBounds, version: OfflineVersion) -> URL { - let url = apiEndpoint.appendingPathComponent("route-tiles/v1").appendingPathComponent(coordinateBounds.description) + let url = credentials.host.appendingPathComponent("route-tiles/v1").appendingPathComponent(coordinateBounds.description) var components = URLComponents(url: url, resolvingAgainstBaseURL: true) components?.queryItems = [URLQueryItem(name: "version", value: version), - URLQueryItem(name: "access_token", value: accessToken)] + URLQueryItem(name: "access_token", value: credentials.accessToken)] return components!.url! } diff --git a/Sources/MapboxDirections/RouteResponse.swift b/Sources/MapboxDirections/RouteResponse.swift index 40ee5d371..e9ce397cf 100644 --- a/Sources/MapboxDirections/RouteResponse.swift +++ b/Sources/MapboxDirections/RouteResponse.swift @@ -1,14 +1,19 @@ import Foundation +public enum RouteResponseOptions { + case route(RouteOptions) + case match(MatchOptions) +} + public struct RouteResponse { - public var code: String? - public var message: String? +// public var code: String? +// public var message: String? public var error: DirectionsError? public let uuid: String? public let routes: [Route]? public let waypoints: [Waypoint]? - public let options: RouteOptions + public let options: RouteResponseOptions public let credentials: DirectionsCredentials /** @@ -30,7 +35,7 @@ extension RouteResponse: Codable { case routes case waypoints } - public init(credentials: DirectionsCredentials, options: RouteOptions, error: DirectionsError) { + public init(credentials: DirectionsCredentials, options: RouteResponseOptions, error: DirectionsError) { self.init(code: nil, message: nil, error: error, uuid: nil, routes: nil, waypoints: nil, options: options, credentials: credentials) } @@ -46,7 +51,22 @@ extension RouteResponse: Codable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - // Is this an edge-case, where we are creating a route response from the map matching service? + + guard let credentials = decoder.userInfo[.credentials] as? DirectionsCredentials else { + throw DirectionsCodingError.missingOptions + } + + self.credentials = credentials + + if let routeOptions = decoder.userInfo[.options] as? RouteOptions { + self.options = .route(routeOptions) + } else if let matchOptions = decoder.userInfo[.options] as? MatchOptions { + self.options = .match(matchOptions) + } else { + throw DirectionsCodingError.missingOptions + } + + self.code = try container.decodeIfPresent(String.self, forKey: .code) self.message = try container.decodeIfPresent(String.self, forKey: .message) @@ -56,10 +76,19 @@ extension RouteResponse: Codable { self.uuid = try container.decodeIfPresent(String.self, forKey: .uuid) // Decode waypoints from the response and update their names according to the waypoints from DirectionsOptions.waypoints. - let decodedWaypoints = try container.decodeIfPresent([Waypoint?].self, forKey: .waypoints) - if let decodedWaypoints = decodedWaypoints, let options = decoder.userInfo[.options] as? DirectionsOptions { + let decodedWaypoints = try container.decodeIfPresent([Waypoint?].self, forKey: .waypoints)?.compactMap{ $0 } + var optionsWaypoints: [Waypoint] = [] + switch self.options { + case let .route(options): + optionsWaypoints = options.waypoints + case let .match(options): + optionsWaypoints = options.waypoints + } + + + if let decodedWaypoints = decodedWaypoints { // The response lists the same number of tracepoints as the waypoints in the request, whether or not a given waypoint is leg-separating. - waypoints = zip(decodedWaypoints, options.waypoints).map { (pair) -> Waypoint in + waypoints = zip(decodedWaypoints, optionsWaypoints).map { (pair) -> Waypoint in let (decodedWaypoint, waypointInOptions) = pair let waypoint = Waypoint(coordinate: decodedWaypoint.coordinate, coordinateAccuracy: waypointInOptions.coordinateAccuracy, name: waypointInOptions.name?.nonEmptyString ?? decodedWaypoint.name) waypoint.separatesLegs = waypointInOptions.separatesLegs @@ -88,29 +117,33 @@ extension RouteResponse: Codable { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) + + + try container.encodeIfPresent(code, forKey: .code) try container.encodeIfPresent(message, forKey: .message) - try container.encodeIfPresent(error, forKey: .error) + //FIXME: Encode Error? +// try container.encodeIfPresent(error, forKey: .error) try container.encodeIfPresent(uuid, forKey: .uuid) try container.encodeIfPresent(routes, forKey: .routes) try container.encodeIfPresent(waypoints, forKey: .waypoints) } - /** - Adds request- or response-specific information to each result in a response. - */ - func postprocess(accessToken: String, apiEndpoint: URL, fetchStartDate: Date, responseEndDate: Date) { - guard let routes = self.routes else { - return - } - - for result in routes { - result.accessToken = accessToken - result.apiEndpoint = apiEndpoint - result.routeIdentifier = uuid - result.fetchStartDate = fetchStartDate - result.responseEndDate = responseEndDate - } - } +// /** +// Adds request- or response-specific information to each result in a response. +// */ +// func postprocess(accessToken: String, apiEndpoint: URL, fetchStartDate: Date, responseEndDate: Date) { +// guard let routes = self.routes else { +// return +// } +// +// for result in routes { +// result.accessToken = accessToken +// result.apiEndpoint = apiEndpoint +// result.routeIdentifier = uuid +// result.fetchStartDate = fetchStartDate +// result.responseEndDate = responseEndDate +// } +// } } From 2566411aea49186ceb36cceb08e04f8914c729bd Mon Sep 17 00:00:00 2001 From: Jerrad Thramer Date: Fri, 7 Feb 2020 02:17:32 -0700 Subject: [PATCH 06/24] WIP: Fixed all compiler errors, Response model is now coherent, need to fix tests --- Directions Example/ViewController.swift | 11 +- MapboxDirections.xcodeproj/project.pbxproj | 10 ++ Sources/MapboxDirections/Directions.swift | 154 +++++++++--------- .../MapboxDirections/DirectionsError.swift | 30 ---- .../MapboxDirections/DirectionsOptions.swift | 1 + .../MapMatching/MapMatchingResponse.swift | 31 +--- .../ResponseDisposition.swift | 12 ++ Sources/MapboxDirections/RouteResponse.swift | 91 +++-------- 8 files changed, 137 insertions(+), 203 deletions(-) create mode 100644 Sources/MapboxDirections/ResponseDisposition.swift diff --git a/Directions Example/ViewController.swift b/Directions Example/ViewController.swift index 15ac92a7a..43d891a23 100644 --- a/Directions Example/ViewController.swift +++ b/Directions Example/ViewController.swift @@ -70,13 +70,13 @@ class ViewController: UIViewController, MBDrawingViewDelegate { options.routeShapeResolution = .full options.attributeOptions = [.congestionLevel, .maximumSpeedLimit] - Directions.shared.calculate(options) { (waypoints, routes, error) in + Directions.shared.calculate(options) { (response, error) in if let error = error { print("Error calculating directions: \(error)") return } - if let route = routes?.first, let leg = route.legs.first { + if let route = response.routes?.first, let leg = route.legs.first { print("Route via \(leg):") let distanceFormatter = LengthFormatter() @@ -89,7 +89,8 @@ class ViewController: UIViewController, MBDrawingViewDelegate { print("Distance: \(formattedDistance); ETA: \(formattedTravelTime!)") for step in leg.steps { - print("\(step.instructions) [\(step.maneuverType) \(step.maneuverDirection)]") + let direction = step.maneuverDirection?.rawValue ?? "none" + print("\(step.instructions) [\(step.maneuverType) \(direction)]") if step.distance > 0 { let formattedDistance = distanceFormatter.string(fromMeters: step.distance) print("— \(step.transportType) for \(formattedDistance) —") @@ -143,7 +144,7 @@ class ViewController: UIViewController, MBDrawingViewDelegate { func makeMatchRequest(locations: [CLLocationCoordinate2D]) { let matchOptions = MatchOptions(coordinates: locations) - Directions.shared.calculate(matchOptions) { (matches, error) in + Directions.shared.calculate(matchOptions) { (response, error) in if let error = error { let errorString = """ ⚠️ Error Enountered. ⚠️ @@ -156,7 +157,7 @@ class ViewController: UIViewController, MBDrawingViewDelegate { return } - guard let matches = matches, let match = matches.first else { return } + guard let matches = response.matches, let match = matches.first else { return } if let annotations = self.mapView.annotations { self.mapView.removeAnnotations(annotations) diff --git a/MapboxDirections.xcodeproj/project.pbxproj b/MapboxDirections.xcodeproj/project.pbxproj index ef4fcc14c..39ea03710 100644 --- a/MapboxDirections.xcodeproj/project.pbxproj +++ b/MapboxDirections.xcodeproj/project.pbxproj @@ -55,6 +55,10 @@ 431E93D023466D7500A71B44 /* Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43208BA62343F7C300D8BD89 /* Codable.swift */; }; 431E93D123466D7600A71B44 /* Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43208BA62343F7C300D8BD89 /* Codable.swift */; }; 431E93D223466D7700A71B44 /* Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43208BA62343F7C300D8BD89 /* Codable.swift */; }; + 43538E3823ED463100E010D4 /* ResponseDisposition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43538E3623ED3B1600E010D4 /* ResponseDisposition.swift */; }; + 43538E3923ED463100E010D4 /* ResponseDisposition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43538E3623ED3B1600E010D4 /* ResponseDisposition.swift */; }; + 43538E3A23ED463200E010D4 /* ResponseDisposition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43538E3623ED3B1600E010D4 /* ResponseDisposition.swift */; }; + 43538E3B23ED463400E010D4 /* ResponseDisposition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43538E3623ED3B1600E010D4 /* ResponseDisposition.swift */; }; 438BFEC2233D854D00457294 /* DirectionsProfileIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 438BFEC1233D854D00457294 /* DirectionsProfileIdentifier.swift */; }; 438BFEC3233D854D00457294 /* DirectionsProfileIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 438BFEC1233D854D00457294 /* DirectionsProfileIdentifier.swift */; }; 438BFEC4233D854D00457294 /* DirectionsProfileIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 438BFEC1233D854D00457294 /* DirectionsProfileIdentifier.swift */; }; @@ -348,6 +352,7 @@ 43208BA82343F7E900D8BD89 /* CoreLocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreLocation.swift; sourceTree = ""; }; 43208BAA2343F81900D8BD89 /* GeoJSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeoJSON.swift; sourceTree = ""; }; 43208BAC2343FF5500D8BD89 /* RouteResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteResponse.swift; sourceTree = ""; }; + 43538E3623ED3B1600E010D4 /* ResponseDisposition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseDisposition.swift; sourceTree = ""; }; 438BFEBC233D7FA900457294 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; 438BFEC0233D805500457294 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 438BFEC1233D854D00457294 /* DirectionsProfileIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectionsProfileIdentifier.swift; sourceTree = ""; }; @@ -633,6 +638,7 @@ DAC05F191CFC077C00FA0071 /* RouteLeg.swift */, DA2E03EA1CB0E13D00D1269A /* RouteOptions.swift */, 43208BAC2343FF5500D8BD89 /* RouteResponse.swift */, + 43538E3623ED3B1600E010D4 /* ResponseDisposition.swift */, DA2E03E81CB0E0B000D1269A /* RouteStep.swift */, C55FB44A1F6AEBF6006BD1E9 /* SpokenInstruction.swift */, 35EFD00A207DFACA00BF3873 /* VisualInstruction.swift */, @@ -1178,6 +1184,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 43538E3923ED463100E010D4 /* ResponseDisposition.swift in Sources */, 431E93C0234664A200A71B44 /* DrivingSide.swift in Sources */, C5DAAC9B2019167C001F9261 /* Match.swift in Sources */, 35DBF010217E17A30009D2AE /* HTTPURLResponse.swift in Sources */, @@ -1252,6 +1259,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 43538E3A23ED463200E010D4 /* ResponseDisposition.swift in Sources */, 431E93C1234664A200A71B44 /* DrivingSide.swift in Sources */, C5DAAC9C2019167D001F9261 /* Match.swift in Sources */, 35DBF011217E17A30009D2AE /* HTTPURLResponse.swift in Sources */, @@ -1326,6 +1334,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 43538E3B23ED463400E010D4 /* ResponseDisposition.swift in Sources */, 431E93C2234664A200A71B44 /* DrivingSide.swift in Sources */, C5DAAC9D2019167E001F9261 /* Match.swift in Sources */, 35DBF012217E17A30009D2AE /* HTTPURLResponse.swift in Sources */, @@ -1372,6 +1381,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 43538E3823ED463100E010D4 /* ResponseDisposition.swift in Sources */, 431E93BF234664A200A71B44 /* DrivingSide.swift in Sources */, C51538CC1E807FF00093FF3E /* AttributeOptions.swift in Sources */, 35DBF00F217E17A30009D2AE /* HTTPURLResponse.swift in Sources */, diff --git a/Sources/MapboxDirections/Directions.swift b/Sources/MapboxDirections/Directions.swift index 3af82699b..f51ca64d6 100644 --- a/Sources/MapboxDirections/Directions.swift +++ b/Sources/MapboxDirections/Directions.swift @@ -68,7 +68,7 @@ open class Directions: NSObject { If the request was canceled or there was an error obtaining the routes, this argument is `nil`. This is not to be confused with the situation in which no results were found, in which case the array is present but empty. - parameter error: The error that occurred, or `nil` if the placemarks were obtained successfully. */ - public typealias RouteCompletionHandler = (_ response: RouteResponse) -> Void + public typealias RouteCompletionHandler = (_ response: RouteResponse, _ error: DirectionsError?) -> Void /** A closure (block) to be called when a map matching request is complete. @@ -76,7 +76,7 @@ open class Directions: NSObject { If the request was canceled or there was an error obtaining the matches, this argument is `nil`. This is not to be confused with the situation in which no matches were found, in which case the array is present but empty. - parameter error: The error that occurred, or `nil` if the placemarks were obtained successfully. */ - public typealias MatchCompletionHandler = (_ response: MapMatchingResponse) -> Void + public typealias MatchCompletionHandler = (_ response: MapMatchingResponse, _ error: DirectionsError?) -> Void // MARK: Creating a Directions Object @@ -113,26 +113,26 @@ open class Directions: NSObject { - parameter completionHandler: The closure (block) to call with the resulting routes. This closure is executed on the application’s main thread. - returns: The data task used to perform the HTTP request. If, while waiting for the completion handler to execute, you no longer want the resulting routes, cancel this task. */ - @discardableResult open func calculate(_ options: RouteOptions, completionHandler: @escaping RouteCompletionHandler) -> URLSessionDataTask { + @discardableResult open func calculate(_ options: RouteOptions, completionHandler: @escaping RouteCompletionHandler /* FIXME: VARIENT TYPE */) -> URLSessionDataTask { options.fetchStartDate = Date() let request = urlRequest(forCalculating: options) let requestTask = URLSession.shared.dataTask(with: request) { (possibleData, possibleResponse, possibleError) in - guard let response = possibleResponse, ["application/json", "text/html"].contains(response.mimeType) else { - let response = RouteResponse(credentials: self.credentials, options: .route(options), error: .invalidResponse(possibleResponse)) - completionHandler(response) + guard let response = possibleResponse, ["application/json", "text/html"].contains(response.mimeType), let httpResponse = response as? HTTPURLResponse else { + let response = RouteResponse(httpResponse: possibleResponse as? HTTPURLResponse, options: .route(options), credentials: self.credentials) + completionHandler(response, .invalidResponse(possibleResponse)) return } guard let data = possibleData else { - let response = RouteResponse(credentials: self.credentials, options: .route(options), error: .noData) - completionHandler(response) + let response = RouteResponse(httpResponse: httpResponse, options: .route(options), credentials: self.credentials) + completionHandler(response, .noData) return } if let error = possibleError { + let response = RouteResponse(httpResponse: httpResponse, options: .route(options), credentials: self.credentials) let unknownError = DirectionsError.unknown(response: possibleResponse, underlying: error, code: nil, message: nil) - let response = RouteResponse(credentials: self.credentials, options: .route(options), error: unknownError) - completionHandler(response) + completionHandler(response, unknownError) return } @@ -141,32 +141,31 @@ open class Directions: NSObject { let decoder = JSONDecoder() decoder.userInfo = [.options: options] - var result = try decoder.decode(RouteResponse.self, from: data) - guard (result.code == nil && result.message == nil) || result.code == "Ok" else { - let apiError = DirectionsError(code: result.code, message: result.message, response: response, underlyingError: possibleError) - result.error = apiError + let disposition = try decoder.decode(ResponseDisposition.self, from: data) + let result = try decoder.decode(RouteResponse.self, from: data) + guard (disposition.code == nil && disposition.message == nil) || disposition.code == "Ok" else { + let apiError = DirectionsError(code: disposition.code, message: disposition.message, response: response, underlyingError: possibleError) DispatchQueue.main.async { - completionHandler(result) + completionHandler(result, apiError) } return } guard result.routes != nil else { - result.error = .unableToRoute DispatchQueue.main.async { - completionHandler(result) + completionHandler(result, .unableToRoute) } return } DispatchQueue.main.async { - completionHandler(result) + completionHandler(result, nil) } } catch { DispatchQueue.main.async { let bailError = DirectionsError(code: nil, message: nil, response: response, underlyingError: error) - let response = RouteResponse(credentials: self.credentials, options: .route(options), error: bailError) - completionHandler(response) + let response = RouteResponse(httpResponse: httpResponse, options: .route(options), credentials: self.credentials) + completionHandler(response, bailError) } } } @@ -192,21 +191,21 @@ open class Directions: NSObject { options.fetchStartDate = Date() let request = urlRequest(forCalculating: options) let requestTask = URLSession.shared.dataTask(with: request) { (possibleData, possibleResponse, possibleError) in - guard let response = possibleResponse, response.mimeType == "application/json" else { - let result = MapMatchingResponse(credentials: self.credentials, options: options, error: .invalidResponse(possibleResponse)) - completionHandler(result) + guard let response = possibleResponse, response.mimeType == "application/json", let httpResponse = response as? HTTPURLResponse else { + let result = MapMatchingResponse(httpResponse: possibleResponse as? HTTPURLResponse, options: options, credentials: self.credentials) + completionHandler(result, .invalidResponse(possibleResponse)) return } guard let data = possibleData else { - let result = MapMatchingResponse(credentials: self.credentials, options: options, error: .noData) - return completionHandler(result) + let result = MapMatchingResponse(httpResponse: httpResponse, options: options, credentials: self.credentials) + return completionHandler(result, .noData) } if let error = possibleError { let unknownError = DirectionsError.unknown(response: possibleResponse, underlying: error, code: nil, message: nil) - let response = MapMatchingResponse(credentials: self.credentials, options: options, error: unknownError) - completionHandler(response) + let response = MapMatchingResponse(httpResponse: httpResponse, options: options, credentials: self.credentials) + completionHandler(response, unknownError) return } @@ -214,32 +213,32 @@ open class Directions: NSObject { do { let decoder = JSONDecoder() decoder.userInfo = [.options: options] - var result = try decoder.decode(MapMatchingResponse.self, from: data) - guard result.code == "Ok" else { - let apiError = DirectionsError(code: result.code, message: result.message, response: response, underlyingError: possibleError) - result.error = apiError + + let disposition = try decoder.decode(ResponseDisposition.self, from: data) + let result = try decoder.decode(MapMatchingResponse.self, from: data) + guard disposition.code == "Ok" else { + let apiError = DirectionsError(code: disposition.code, message: disposition.message, response: response, underlyingError: possibleError) DispatchQueue.main.async { - completionHandler(result) + completionHandler(result, apiError) } return } guard result.matches != nil else { - result.error = .unableToRoute DispatchQueue.main.async { - completionHandler(result) + completionHandler(result, .unableToRoute) } return } DispatchQueue.main.async { - completionHandler(result) + completionHandler(result, nil) } } catch { DispatchQueue.main.async { let caughtError = DirectionsError.unknown(response: response, underlying: error, code: nil, message: nil) - let result = MapMatchingResponse(credentials: self.credentials, options: options, error: caughtError) - completionHandler(result) + let result = MapMatchingResponse(httpResponse: httpResponse, options: options, credentials: self.credentials) + completionHandler(result, caughtError) } } } @@ -265,63 +264,59 @@ open class Directions: NSObject { options.fetchStartDate = Date() let request = urlRequest(forCalculating: options) let requestTask = URLSession.shared.dataTask(with: request) { (possibleData, possibleResponse, possibleError) in - guard let response = possibleResponse, response.mimeType == "application/json" else { - let error = DirectionsError.invalidResponse(possibleResponse) - let result = RouteResponse(credentials: self.credentials, options: .match(options), error: error) - completionHandler(result) + guard let response = possibleResponse, ["application/json", "text/html"].contains(response.mimeType), let httpResponse = response as? HTTPURLResponse else { + let response = RouteResponse(httpResponse: possibleResponse as? HTTPURLResponse, options: .match(options), credentials: self.credentials) + completionHandler(response, .invalidResponse(possibleResponse)) return } - guard let data = possibleData else { - let result = RouteResponse(credentials: self.credentials, options: .match(options), error: .noData) - completionHandler(result) - return - } + guard let data = possibleData else { + let response = RouteResponse(httpResponse: httpResponse, options: .match(options), credentials: self.credentials) + completionHandler(response, .noData) + return + } - if let error = possibleError { + if let error = possibleError { + let response = RouteResponse(httpResponse: httpResponse, options: .match(options), credentials: self.credentials) let unknownError = DirectionsError.unknown(response: possibleResponse, underlying: error, code: nil, message: nil) - let result = RouteResponse(credentials: self.credentials, options: .match(options), error: unknownError) - completionHandler(result) + completionHandler(response, unknownError) return } - DispatchQueue.global(qos: .userInitiated).async { - do { - - //FIXME: FIX TO USE MATCHING RESPONSE -> ROUTE RESPONSE - let decoder = JSONDecoder() - decoder.userInfo[.options] = options - var result = try decoder.decode(RouteResponse.self, from: data) - guard result.code == "Ok" else { - let apiError = DirectionsError(code: result.code, message:nil, response: response, underlyingError: possibleError) - result.error = apiError + DispatchQueue.global(qos: .userInitiated).async { + do { + let decoder = JSONDecoder() + decoder.userInfo = [.options: options] + + let disposition = try decoder.decode(ResponseDisposition.self, from: data) + let result = try decoder.decode(RouteResponse.self, from: data) + guard disposition.code == "Ok" else { + let apiError = DirectionsError(code: disposition.code, message: disposition.message, response: response, underlyingError: possibleError) + DispatchQueue.main.async { + completionHandler(result, apiError) + } + return + } + + guard result.routes != nil else { + DispatchQueue.main.async { + completionHandler(result, .unableToRoute) + } + return + } + DispatchQueue.main.async { - completionHandler(result) + completionHandler(result, nil) } - return - } - - guard result.routes != nil else { - result.error = .unableToRoute + } catch { DispatchQueue.main.async { - completionHandler(result) + let bailError = DirectionsError(code: nil, message: nil, response: response, underlyingError: error) + let response = RouteResponse(httpResponse: httpResponse, options: .match(options), credentials: self.credentials) + completionHandler(response, bailError) } - return - } - - - DispatchQueue.main.async { - completionHandler(result) - } - } catch { - DispatchQueue.main.async { - let caughtError = DirectionsError.unknown(response: response, underlying: error, code: nil, message: nil) - let result = RouteResponse(credentials: self.credentials, options: .match(options), error: caughtError) - completionHandler(result) } } } - } requestTask.priority = 1 requestTask.resume() @@ -394,6 +389,7 @@ open class Directions: NSObject { public extension CodingUserInfoKey { static let options = CodingUserInfoKey(rawValue: "com.mapbox.directions.coding.routeOptions")! + static let httpResponse = CodingUserInfoKey(rawValue: "com.mapbox.directions.coding.httpResponse")! static let credentials = CodingUserInfoKey(rawValue: "com.mapbox.directions.coding.credentials")! static let tracepoints = CodingUserInfoKey(rawValue: "com.mapbox.directions.coding.tracepoints")! diff --git a/Sources/MapboxDirections/DirectionsError.swift b/Sources/MapboxDirections/DirectionsError.swift index 4b922dfc7..5b73d6b4c 100644 --- a/Sources/MapboxDirections/DirectionsError.swift +++ b/Sources/MapboxDirections/DirectionsError.swift @@ -5,36 +5,6 @@ import Foundation */ public enum DirectionsError: LocalizedError { - var code: String? { - switch self { - case .unableToRoute : - return "NoRoute" - case .unableToLocate: - return "NoSegment" - case .noMatches: - return "NoMatch" - case .tooManyCoordinates: - return "TooManyCoordinates" - case .profileNotFound: - return "ProfileNotFound" - case .invalidInput(_): - return "InvalidInput" - default: - return nil - } - } - - var message: String? { - switch self { - case let .invalidInput(message: message): - return message - case let .unkonw - default: - <#code#> - } - } - - public init(code: String?, message: String?, response: URLResponse?, underlyingError error: Error?) { if let response = response as? HTTPURLResponse { switch (response.statusCode, code ?? "") { diff --git a/Sources/MapboxDirections/DirectionsOptions.swift b/Sources/MapboxDirections/DirectionsOptions.swift index 85b58a384..fb63c0db4 100644 --- a/Sources/MapboxDirections/DirectionsOptions.swift +++ b/Sources/MapboxDirections/DirectionsOptions.swift @@ -129,6 +129,7 @@ open class DirectionsOptions: Codable { self.profileIdentifier = profileIdentifier ?? .automobile } + private enum CodingKeys: String, CodingKey { case waypoints case profileIdentifier diff --git a/Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift b/Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift index ea2d01422..a53cd7dc4 100644 --- a/Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift +++ b/Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift @@ -1,9 +1,8 @@ import Foundation public struct MapMatchingResponse { - public var code: String? - public var message: String? - public var error: DirectionsError? + public let httpResponse: HTTPURLResponse? + public var matches : [Match]? public var tracepoints: [Tracepoint?]? @@ -22,20 +21,19 @@ public struct MapMatchingResponse { extension MapMatchingResponse: Codable { private enum CodingKeys: String, CodingKey { - case code - case message - case error case matches = "matchings" case tracepoints } - - public init(credentials: DirectionsCredentials, options: MatchOptions, error: DirectionsError) { - self.init(code: nil, message: nil, error: error, matches: nil, tracepoints: nil, options: options, credentials: credentials) + + public init(httpResponse: HTTPURLResponse, options: MatchOptions, credentials: DirectionsCredentials) { + self.init(httpResponse: httpResponse, matches: nil, tracepoints: nil, options: options, credentials: credentials) } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) + self.httpResponse = decoder.userInfo[.httpResponse] as? HTTPURLResponse + guard let options = decoder.userInfo[.options] as? MatchOptions else { throw DirectionsCodingError.missingOptions } @@ -46,8 +44,6 @@ extension MapMatchingResponse: Codable { } self.credentials = credentials - code = try container.decode(String.self, forKey: .code) - message = try container.decodeIfPresent(String.self, forKey: .message) tracepoints = try container.decodeIfPresent([Tracepoint?].self, forKey: .tracepoints) matches = try container.decodeIfPresent([Match].self, forKey: .matches) @@ -57,17 +53,4 @@ extension MapMatchingResponse: Codable { } } } - -// func postprocess(accessToken: String, apiEndpoint: URL, fetchStartDate: Date, responseEndDate: Date) { -// guard let matches = self.matches else { -// return -// } -// -// for result in matches { -// result.accessToken = accessToken -// result.apiEndpoint = apiEndpoint -// result.fetchStartDate = fetchStartDate -// result.responseEndDate = responseEndDate -// } -// } } diff --git a/Sources/MapboxDirections/ResponseDisposition.swift b/Sources/MapboxDirections/ResponseDisposition.swift new file mode 100644 index 000000000..409b3c802 --- /dev/null +++ b/Sources/MapboxDirections/ResponseDisposition.swift @@ -0,0 +1,12 @@ +import Foundation + + +struct ResponseDisposition: Decodable { + var code: String? + var message: String? + var error: String? + + private enum CodingKeys: CodingKey { + case code, message, error + } +} diff --git a/Sources/MapboxDirections/RouteResponse.swift b/Sources/MapboxDirections/RouteResponse.swift index e9ce397cf..3e314315d 100644 --- a/Sources/MapboxDirections/RouteResponse.swift +++ b/Sources/MapboxDirections/RouteResponse.swift @@ -1,19 +1,18 @@ import Foundation -public enum RouteResponseOptions { +public enum ResponseOptions { case route(RouteOptions) case match(MatchOptions) } public struct RouteResponse { -// public var code: String? -// public var message: String? - public var error: DirectionsError? - public let uuid: String? + public let httpResponse: HTTPURLResponse? + + public let identifier: String? public let routes: [Route]? public let waypoints: [Waypoint]? - public let options: RouteResponseOptions + public let options: ResponseOptions public let credentials: DirectionsCredentials /** @@ -31,61 +30,47 @@ extension RouteResponse: Codable { case code case message case error - case uuid + case identifier case routes case waypoints } - public init(credentials: DirectionsCredentials, options: RouteResponseOptions, error: DirectionsError) { - self.init(code: nil, message: nil, error: error, uuid: nil, routes: nil, waypoints: nil, options: options, credentials: credentials) - } - -// public init(matchResponse match: MapMatchingResponse) { -// self.code = match.code -// self.message = match.message -// self.error = match.error -// self.routes = match.matches -// self.waypoints = match.tracepoints -// -// } + public init(httpResponse: HTTPURLResponse?, options: ResponseOptions, credentials: DirectionsCredentials) { + self.init(httpResponse: httpResponse, identifier: nil, routes: nil, waypoints: nil, options: options, credentials: credentials) + } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) + self.httpResponse = decoder.userInfo[.httpResponse] as? HTTPURLResponse + guard let credentials = decoder.userInfo[.credentials] as? DirectionsCredentials else { - throw DirectionsCodingError.missingOptions + throw DirectionsCodingError.missingCredentials } self.credentials = credentials - if let routeOptions = decoder.userInfo[.options] as? RouteOptions { - self.options = .route(routeOptions) - } else if let matchOptions = decoder.userInfo[.options] as? MatchOptions { - self.options = .match(matchOptions) + if let options = decoder.userInfo[.options] as? RouteOptions { + self.options = .route(options) + } else if let options = decoder.userInfo[.options] as? MatchOptions { + self.options = .match(options) } else { throw DirectionsCodingError.missingOptions } - - - self.code = try container.decodeIfPresent(String.self, forKey: .code) - self.message = try container.decodeIfPresent(String.self, forKey: .message) - if let apiError = try container.decodeIfPresent(String.self, forKey: .error) { - error = .unknown(response: nil, underlying: nil, code: self.code, message: apiError) - } - self.uuid = try container.decodeIfPresent(String.self, forKey: .uuid) + self.identifier = try container.decodeIfPresent(String.self, forKey: .identifier) // Decode waypoints from the response and update their names according to the waypoints from DirectionsOptions.waypoints. let decodedWaypoints = try container.decodeIfPresent([Waypoint?].self, forKey: .waypoints)?.compactMap{ $0 } var optionsWaypoints: [Waypoint] = [] - switch self.options { - case let .route(options): - optionsWaypoints = options.waypoints - case let .match(options): - optionsWaypoints = options.waypoints - } - + switch options { + case let .match(options: matchOpts): + optionsWaypoints = matchOpts.waypoints + case let .route(options: routeOpts): + optionsWaypoints = routeOpts.waypoints + } + if let decodedWaypoints = decodedWaypoints { // The response lists the same number of tracepoints as the waypoints in the request, whether or not a given waypoint is leg-separating. waypoints = zip(decodedWaypoints, optionsWaypoints).map { (pair) -> Waypoint in @@ -103,7 +88,7 @@ extension RouteResponse: Codable { if let routes = try container.decodeIfPresent([Route].self, forKey: .routes) { // Postprocess each route. for route in routes { - route.routeIdentifier = uuid + route.routeIdentifier = identifier // Imbue each route’s legs with the waypoints refined above. if let waypoints = waypoints { route.legSeparators = waypoints.filter { $0.separatesLegs } @@ -117,33 +102,9 @@ extension RouteResponse: Codable { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) - - - - try container.encodeIfPresent(code, forKey: .code) - try container.encodeIfPresent(message, forKey: .message) - //FIXME: Encode Error? -// try container.encodeIfPresent(error, forKey: .error) - try container.encodeIfPresent(uuid, forKey: .uuid) + try container.encodeIfPresent(identifier, forKey: .identifier) try container.encodeIfPresent(routes, forKey: .routes) try container.encodeIfPresent(waypoints, forKey: .waypoints) } - -// /** -// Adds request- or response-specific information to each result in a response. -// */ -// func postprocess(accessToken: String, apiEndpoint: URL, fetchStartDate: Date, responseEndDate: Date) { -// guard let routes = self.routes else { -// return -// } -// -// for result in routes { -// result.accessToken = accessToken -// result.apiEndpoint = apiEndpoint -// result.routeIdentifier = uuid -// result.fetchStartDate = fetchStartDate -// result.responseEndDate = responseEndDate -// } -// } } From 48abd96069cbe131f076040b0e1bb3c04ef48bf8 Mon Sep 17 00:00:00 2001 From: Jerrad Thramer Date: Mon, 10 Feb 2020 16:13:42 -0700 Subject: [PATCH 07/24] Fixing more tests, three to go. --- MapboxDirections.xcodeproj/project.pbxproj | 8 +++ Sources/MapboxDirections/Directions.swift | 21 ++++--- .../DirectionsCredentials.swift | 2 +- .../MapboxDirections/DirectionsError.swift | 5 +- .../MapMatching/MapMatchingResponse.swift | 7 +-- .../MapboxDirections/MapMatching/Match.swift | 60 +++++++++++++++---- Sources/MapboxDirections/RouteResponse.swift | 23 ++++++- .../AnnotationTests.swift | 10 ++-- .../DirectionsCredentialsTests.swift | 22 +++++++ .../DirectionsErrorTests.swift | 10 ++-- .../DirectionsTests.swift | 24 ++++---- Tests/MapboxDirectionsTests/MatchTests.swift | 46 +++++++------- .../OfflineDirectionsTests.swift | 8 ++- .../RoutableMatchTests.swift | 25 ++++---- .../RouteOptionsTests.swift | 1 + Tests/MapboxDirectionsTests/RouteTests.swift | 2 +- Tests/MapboxDirectionsTests/V5Tests.swift | 41 ++++++------- .../VisualInstructionTests.swift | 51 +++++++++------- 18 files changed, 231 insertions(+), 135 deletions(-) create mode 100644 Tests/MapboxDirectionsTests/DirectionsCredentialsTests.swift diff --git a/MapboxDirections.xcodeproj/project.pbxproj b/MapboxDirections.xcodeproj/project.pbxproj index 39ea03710..796360039 100644 --- a/MapboxDirections.xcodeproj/project.pbxproj +++ b/MapboxDirections.xcodeproj/project.pbxproj @@ -59,6 +59,9 @@ 43538E3923ED463100E010D4 /* ResponseDisposition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43538E3623ED3B1600E010D4 /* ResponseDisposition.swift */; }; 43538E3A23ED463200E010D4 /* ResponseDisposition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43538E3623ED3B1600E010D4 /* ResponseDisposition.swift */; }; 43538E3B23ED463400E010D4 /* ResponseDisposition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43538E3623ED3B1600E010D4 /* ResponseDisposition.swift */; }; + 43538E3D23ED6A2000E010D4 /* DirectionsCredentialsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43538E3C23ED6A2000E010D4 /* DirectionsCredentialsTests.swift */; }; + 43538E3E23ED6A2000E010D4 /* DirectionsCredentialsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43538E3C23ED6A2000E010D4 /* DirectionsCredentialsTests.swift */; }; + 43538E3F23ED6A2000E010D4 /* DirectionsCredentialsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43538E3C23ED6A2000E010D4 /* DirectionsCredentialsTests.swift */; }; 438BFEC2233D854D00457294 /* DirectionsProfileIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 438BFEC1233D854D00457294 /* DirectionsProfileIdentifier.swift */; }; 438BFEC3233D854D00457294 /* DirectionsProfileIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 438BFEC1233D854D00457294 /* DirectionsProfileIdentifier.swift */; }; 438BFEC4233D854D00457294 /* DirectionsProfileIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 438BFEC1233D854D00457294 /* DirectionsProfileIdentifier.swift */; }; @@ -353,6 +356,7 @@ 43208BAA2343F81900D8BD89 /* GeoJSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeoJSON.swift; sourceTree = ""; }; 43208BAC2343FF5500D8BD89 /* RouteResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteResponse.swift; sourceTree = ""; }; 43538E3623ED3B1600E010D4 /* ResponseDisposition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseDisposition.swift; sourceTree = ""; }; + 43538E3C23ED6A2000E010D4 /* DirectionsCredentialsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectionsCredentialsTests.swift; sourceTree = ""; }; 438BFEBC233D7FA900457294 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; 438BFEC0233D805500457294 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 438BFEC1233D854D00457294 /* DirectionsProfileIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectionsProfileIdentifier.swift; sourceTree = ""; }; @@ -657,6 +661,7 @@ C5247D701E818A24004B6154 /* AnnotationTests.swift */, DAD06E34239F0B19001A917D /* DirectionsErrorTests.swift */, DA1A110A1D01045E009F82FA /* DirectionsTests.swift */, + 43538E3C23ED6A2000E010D4 /* DirectionsCredentialsTests.swift */, DA6C9DB11CAECA0E00094FBC /* Fixture.swift */, DAABF7912395AE9800CEEB61 /* GeoJSONTests.swift */, DA6C9D9A1CAE442B00094FBC /* Info.plist */, @@ -1251,6 +1256,7 @@ 35CC310C2285739700EA1966 /* WalkingOptionsTests.swift in Sources */, DABE6C7F236A37E200D370F4 /* JSONSerialization.swift in Sources */, DAABF7932395AE9800CEEB61 /* GeoJSONTests.swift in Sources */, + 43538E3E23ED6A2000E010D4 /* DirectionsCredentialsTests.swift in Sources */, F4D785F01DDD82C100FF4665 /* RouteStepTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1326,6 +1332,7 @@ 35CC310D2285739700EA1966 /* WalkingOptionsTests.swift in Sources */, DABE6C80236A37E200D370F4 /* JSONSerialization.swift in Sources */, DAABF7942395AE9800CEEB61 /* GeoJSONTests.swift in Sources */, + 43538E3F23ED6A2000E010D4 /* DirectionsCredentialsTests.swift in Sources */, F4D785F11DDD82C100FF4665 /* RouteStepTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1448,6 +1455,7 @@ 35CC310B2285739700EA1966 /* WalkingOptionsTests.swift in Sources */, DABE6C7E236A37E200D370F4 /* JSONSerialization.swift in Sources */, DAABF7922395AE9800CEEB61 /* GeoJSONTests.swift in Sources */, + 43538E3D23ED6A2000E010D4 /* DirectionsCredentialsTests.swift in Sources */, F4D785EF1DDD82C100FF4665 /* RouteStepTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Sources/MapboxDirections/Directions.swift b/Sources/MapboxDirections/Directions.swift index f51ca64d6..928b2d583 100644 --- a/Sources/MapboxDirections/Directions.swift +++ b/Sources/MapboxDirections/Directions.swift @@ -139,7 +139,8 @@ open class Directions: NSObject { DispatchQueue.global(qos: .userInitiated).async { do { let decoder = JSONDecoder() - decoder.userInfo = [.options: options] + decoder.userInfo = [.options: options, + .credentials: self.credentials] let disposition = try decoder.decode(ResponseDisposition.self, from: data) let result = try decoder.decode(RouteResponse.self, from: data) @@ -212,7 +213,8 @@ open class Directions: NSObject { DispatchQueue.global(qos: .userInitiated).async { do { let decoder = JSONDecoder() - decoder.userInfo = [.options: options] + decoder.userInfo = [.options: options, + .credentials: self.credentials] let disposition = try decoder.decode(ResponseDisposition.self, from: data) let result = try decoder.decode(MapMatchingResponse.self, from: data) @@ -286,27 +288,30 @@ open class Directions: NSObject { DispatchQueue.global(qos: .userInitiated).async { do { let decoder = JSONDecoder() - decoder.userInfo = [.options: options] + decoder.userInfo = [.options: options, + .credentials: self.credentials] let disposition = try decoder.decode(ResponseDisposition.self, from: data) - let result = try decoder.decode(RouteResponse.self, from: data) + let result = try decoder.decode(MapMatchingResponse.self, from: data) + + let routeResponse = RouteResponse(matching: result, options: options, credentials: self.credentials) guard disposition.code == "Ok" else { let apiError = DirectionsError(code: disposition.code, message: disposition.message, response: response, underlyingError: possibleError) DispatchQueue.main.async { - completionHandler(result, apiError) + completionHandler(routeResponse, apiError) } return } - guard result.routes != nil else { + guard routeResponse.routes != nil else { DispatchQueue.main.async { - completionHandler(result, .unableToRoute) + completionHandler(routeResponse, .unableToRoute) } return } DispatchQueue.main.async { - completionHandler(result, nil) + completionHandler(routeResponse, nil) } } catch { DispatchQueue.main.async { diff --git a/Sources/MapboxDirections/DirectionsCredentials.swift b/Sources/MapboxDirections/DirectionsCredentials.swift index f7959c223..651e3d4ba 100644 --- a/Sources/MapboxDirections/DirectionsCredentials.swift +++ b/Sources/MapboxDirections/DirectionsCredentials.swift @@ -4,7 +4,7 @@ import Foundation let defaultAccessToken = Bundle.main.object(forInfoDictionaryKey: "MGLMapboxAccessToken") as? String let defaultApiEndPointURLString = Bundle.main.object(forInfoDictionaryKey: "MGLMapboxAPIBaseURL") as? String -public struct DirectionsCredentials { +public struct DirectionsCredentials: Equatable { public let accessToken: String? public let host: URL public var skuToken: String? { diff --git a/Sources/MapboxDirections/DirectionsError.swift b/Sources/MapboxDirections/DirectionsError.swift index 5b73d6b4c..84be3c7ca 100644 --- a/Sources/MapboxDirections/DirectionsError.swift +++ b/Sources/MapboxDirections/DirectionsError.swift @@ -101,7 +101,7 @@ public enum DirectionsError: LocalizedError { return "The server returned an empty response." case let .invalidInput(message): return message - case .invalidResponse: + case .invalidResponse(_): return "The server returned a response that isn’t correctly formatted." case .unableToRoute: return "No route could be found between the specified locations." @@ -163,7 +163,6 @@ extension DirectionsError: Equatable { public static func == (lhs: DirectionsError, rhs: DirectionsError) -> Bool { switch (lhs, rhs) { case (.noData, .noData), - (.invalidResponse, .invalidResponse), (.unableToRoute, .unableToRoute), (.noMatches, .noMatches), (.tooManyCoordinates, .tooManyCoordinates), @@ -171,6 +170,8 @@ extension DirectionsError: Equatable { (.profileNotFound, .profileNotFound), (.requestTooLarge, .requestTooLarge): return true + case let (.invalidResponse(lhsResponse), .invalidResponse(rhsResponse)): + return lhsResponse == rhsResponse case let (.invalidInput(lhsMessage), .invalidInput(rhsMessage)): return lhsMessage == rhsMessage case (.rateLimited(let lhsRateLimitInterval, let lhsRateLimit, let lhsResetTime), diff --git a/Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift b/Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift index a53cd7dc4..3131bbc36 100644 --- a/Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift +++ b/Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift @@ -46,11 +46,6 @@ extension MapMatchingResponse: Codable { tracepoints = try container.decodeIfPresent([Tracepoint?].self, forKey: .tracepoints) matches = try container.decodeIfPresent([Match].self, forKey: .matches) - - if let points = self.tracepoints { - matches?.forEach { - $0.tracepoints = points - } - } + } } diff --git a/Sources/MapboxDirections/MapMatching/Match.swift b/Sources/MapboxDirections/MapMatching/Match.swift index a05fe0966..c32073526 100644 --- a/Sources/MapboxDirections/MapMatching/Match.swift +++ b/Sources/MapboxDirections/MapMatching/Match.swift @@ -3,6 +3,38 @@ import CoreLocation import Polyline import struct Turf.LineString +public enum Weight: Equatable { + case routability(value: Float) + case other(value: Float, metric: String) + + public init(value: Float, metric: String) { + switch metric { + case "routability": + self = .routability(value: value) + default: + self = .other(value: value, metric: metric) + } + } + + var metric: String { + switch self { + case .routability(value: _): + return "routability" + case let .other(value: _, metric: value): + return value + } + } + + var value: Float { + switch self { + case let .routability(value: weight): + return weight + case let .other(value: weight, metric: _): + return weight + } + } +} + /** A `Match` object defines a single route that was created from a series of points that were matched against a road network. @@ -11,7 +43,8 @@ import struct Turf.LineString open class Match: DirectionsResult { private enum CodingKeys: String, CodingKey { case confidence - case tracepoints + case weight + case weightName = "weight_name" } /** @@ -27,9 +60,9 @@ open class Match: DirectionsResult { - parameter tracepoints: Tracepoints on the road network that match the tracepoints in `options`. - parameter options: The criteria to match. */ - public init(legs: [RouteLeg], shape: LineString?, distance: CLLocationDistance, expectedTravelTime: TimeInterval, confidence: Float, tracepoints: [Tracepoint?]) { + public init(legs: [RouteLeg], shape: LineString?, distance: CLLocationDistance, expectedTravelTime: TimeInterval, confidence: Float, weight: Weight) { self.confidence = confidence - self.tracepoints = tracepoints + self.weight = weight super.init(legs: legs, shape: shape, distance: distance, expectedTravelTime: expectedTravelTime) } @@ -42,29 +75,30 @@ open class Match: DirectionsResult { public required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) confidence = try container.decode(Float.self, forKey: .confidence) - tracepoints = try container.decodeIfPresent([Tracepoint?].self, forKey: .tracepoints) ?? [] + let weightValue = try container.decode(Float.self, forKey: .weight) + let weightMetric = try container.decode(String.self, forKey: .weightName) + + weight = Weight(value: weightValue, metric: weightMetric) + try super.init(from: decoder) } public override func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(confidence, forKey: .confidence) - try container.encode(tracepoints, forKey: .tracepoints) + try container.encode(weight.value, forKey: .weight) + try container.encode(weight.metric, forKey: .weightName) + try super.encode(to: encoder) } + open var weight: Weight + /** A number between 0 and 1 that indicates the Map Matching API’s confidence that the match is accurate. A higher confidence means the match is more likely to be accurate. */ open var confidence: Float - /** - Tracepoints on the road network that match the tracepoints in the match options. - - Any outlier tracepoint is omitted from the match. This array represents an outlier tracepoint if the element is `nil`. - */ - open var tracepoints: [Tracepoint?] - } extension Match: Equatable { @@ -74,7 +108,7 @@ extension Match: Equatable { lhs.expectedTravelTime == rhs.expectedTravelTime && lhs.speechLocale == rhs.speechLocale && lhs.confidence == rhs.confidence && - lhs.tracepoints == rhs.tracepoints && + lhs.weight == rhs.weight && lhs.legs == rhs.legs && lhs.shape == rhs.shape } diff --git a/Sources/MapboxDirections/RouteResponse.swift b/Sources/MapboxDirections/RouteResponse.swift index 3e314315d..2f3f67721 100644 --- a/Sources/MapboxDirections/RouteResponse.swift +++ b/Sources/MapboxDirections/RouteResponse.swift @@ -30,7 +30,7 @@ extension RouteResponse: Codable { case code case message case error - case identifier + case identifier = "uuid" case routes case waypoints } @@ -39,6 +39,27 @@ extension RouteResponse: Codable { self.init(httpResponse: httpResponse, identifier: nil, routes: nil, waypoints: nil, options: options, credentials: credentials) } + public init(matching response: MapMatchingResponse, options: MatchOptions, credentials: DirectionsCredentials) { + let decoder = JSONDecoder() + let encoder = JSONEncoder() + + let routes: [Route]? = response.matches?.compactMap({ (match) -> Route? in + guard let json = try? encoder.encode(match) else { return nil } + guard let route = try? decoder.decode(Route.self, from: json) else { return nil } + return route + }) + + // CONVERT WAYPOINTS + + let waypoints: [Waypoint]? = response.tracepoints?.compactMap({ (trace) -> Waypoint? in + guard let json = try? encoder.encode(trace) else { return nil } + guard let waypoint = try? decoder.decode(Waypoint.self, from: json) else { return nil } + return waypoint + }) + + self.init(httpResponse: response.httpResponse, identifier: nil, routes: routes, waypoints: waypoints, options: .match(options), credentials: credentials) + } + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) diff --git a/Tests/MapboxDirectionsTests/AnnotationTests.swift b/Tests/MapboxDirectionsTests/AnnotationTests.swift index 5135078c8..b00193ab0 100644 --- a/Tests/MapboxDirectionsTests/AnnotationTests.swift +++ b/Tests/MapboxDirectionsTests/AnnotationTests.swift @@ -23,7 +23,7 @@ class AnnotationTests: XCTestCase { ] stub(condition: isHost("api.mapbox.com") - && containsQueryParams(queryParams)) { _ in + && containsQueryParams(queryParams)) { _ in let path = Bundle(for: type(of: self)).path(forResource: "annotation", ofType: "json") return OHHTTPStubsResponse(fileAtPath: path!, statusCode: 200, headers: ["Content-Type": "application/json"]) } @@ -38,12 +38,12 @@ class AnnotationTests: XCTestCase { options.routeShapeResolution = .full options.attributeOptions = [.distance, .expectedTravelTime, .speed, .congestionLevel, .maximumSpeedLimit] var route: Route? - let task = Directions(accessToken: BogusToken).calculate(options) { (waypoints, routes, error) in + let task = Directions(credentials: BogusCredentials).calculate(options) { (response, error) in XCTAssertNil(error, "Error: \(error!.localizedDescription)") - XCTAssertNotNil(routes) - XCTAssertEqual(routes!.count, 1) - route = routes!.first! + XCTAssertNotNil(response.routes) + XCTAssertEqual(response.routes!.count, 1) + route = response.routes!.first! expectation.fulfill() } diff --git a/Tests/MapboxDirectionsTests/DirectionsCredentialsTests.swift b/Tests/MapboxDirectionsTests/DirectionsCredentialsTests.swift new file mode 100644 index 000000000..f69e97346 --- /dev/null +++ b/Tests/MapboxDirectionsTests/DirectionsCredentialsTests.swift @@ -0,0 +1,22 @@ +import XCTest +#if !SWIFT_PACKAGE +import OHHTTPStubs +import CoreLocation +@testable import MapboxDirections + +class DirectionsCredentialsTests: XCTestCase { + func testDefaultConfiguration() { + let credentials = DirectionsCredentials(accessToken: BogusToken) + XCTAssertEqual(credentials.accessToken, BogusToken) + XCTAssertEqual(credentials.host.absoluteString, "https://api.mapbox.com") + } + + func testCustomConfiguration() { + let token = "deadbeefcafebebe" + let host = URL(string: "https://hello.world")! + let credentials = DirectionsCredentials(accessToken: token, host: host) + XCTAssertEqual(credentials.accessToken, token) + XCTAssertEqual(credentials.host, host) + } +} +#endif diff --git a/Tests/MapboxDirectionsTests/DirectionsErrorTests.swift b/Tests/MapboxDirectionsTests/DirectionsErrorTests.swift index 7938483aa..4ba57eb49 100644 --- a/Tests/MapboxDirectionsTests/DirectionsErrorTests.swift +++ b/Tests/MapboxDirectionsTests/DirectionsErrorTests.swift @@ -4,7 +4,7 @@ import XCTest class DirectionsErrorTests: XCTestCase { func testFailureReasons() { XCTAssertNotNil(DirectionsError.noData.failureReason) - XCTAssertNotNil(DirectionsError.invalidResponse.failureReason) + XCTAssertNotNil(DirectionsError.invalidResponse(nil).failureReason) XCTAssertNotNil(DirectionsError.unableToRoute.failureReason) XCTAssertNotNil(DirectionsError.noMatches.failureReason) XCTAssertNotNil(DirectionsError.tooManyCoordinates.failureReason) @@ -19,7 +19,7 @@ class DirectionsErrorTests: XCTestCase { func testRecoverySuggestions() { XCTAssertNil(DirectionsError.noData.recoverySuggestion) - XCTAssertNil(DirectionsError.invalidResponse.recoverySuggestion) + XCTAssertNil(DirectionsError.invalidResponse(nil).recoverySuggestion) XCTAssertNotNil(DirectionsError.unableToRoute.recoverySuggestion) XCTAssertNotNil(DirectionsError.noMatches.recoverySuggestion) XCTAssertNotNil(DirectionsError.tooManyCoordinates.recoverySuggestion) @@ -41,7 +41,9 @@ class DirectionsErrorTests: XCTestCase { XCTAssertEqual(DirectionsError.invalidInput(message: nil), .invalidInput(message: nil)) XCTAssertNotEqual(DirectionsError.invalidInput(message: nil), .invalidInput(message: "")) - XCTAssertEqual(DirectionsError.invalidResponse, .invalidResponse) + XCTAssertEqual(DirectionsError.invalidResponse(nil), .invalidResponse(nil)) + XCTAssertNotEqual(DirectionsError.invalidResponse(nil), .invalidResponse(HTTPURLResponse())) + XCTAssertEqual(DirectionsError.unableToRoute, .unableToRoute) XCTAssertEqual(DirectionsError.noMatches, .noMatches) XCTAssertEqual(DirectionsError.tooManyCoordinates, .tooManyCoordinates) @@ -73,7 +75,7 @@ class DirectionsErrorTests: XCTestCase { XCTAssertNotEqual(DirectionsError.unknown(response: nil, underlying: nil, code: nil, message: nil), .unknown(response: nil, underlying: nil, code: nil, message: "")) - XCTAssertNotEqual(DirectionsError.noData, .invalidResponse) + XCTAssertNotEqual(DirectionsError.noData, .invalidResponse(nil)) XCTAssertNotEqual(DirectionsError.noData, .unableToRoute) XCTAssertNotEqual(DirectionsError.noData, .noMatches) XCTAssertNotEqual(DirectionsError.noData, .tooManyCoordinates) diff --git a/Tests/MapboxDirectionsTests/DirectionsTests.swift b/Tests/MapboxDirectionsTests/DirectionsTests.swift index ad05b655d..e1e67ce98 100644 --- a/Tests/MapboxDirectionsTests/DirectionsTests.swift +++ b/Tests/MapboxDirectionsTests/DirectionsTests.swift @@ -5,6 +5,7 @@ import CoreLocation @testable import MapboxDirections let BogusToken = "pk.feedCafeDadeDeadBeef-BadeBede.FadeCafeDadeDeed-BadeBede" +let BogusCredentials = DirectionsCredentials(accessToken: BogusToken) let BadResponse = """ @@ -37,9 +38,8 @@ class DirectionsTests: XCTestCase { } func testConfiguration() { - let directions = Directions(accessToken: BogusToken) - XCTAssertEqual(directions.accessToken, BogusToken) - XCTAssertEqual(directions.apiEndpoint.absoluteString, "https://api.mapbox.com") + let directions = Directions(credentials: BogusCredentials) + XCTAssertEqual(directions.credentials, BogusCredentials) } let maximumCoordinateCount = 795 @@ -49,7 +49,7 @@ class DirectionsTests: XCTestCase { let coordinates = Array(repeating: CLLocationCoordinate2D(latitude: 0, longitude: 0), count: maximumCoordinateCount) let options = RouteOptions(coordinates: coordinates) - let directions = Directions(accessToken: BogusToken) + let directions = Directions(credentials: BogusCredentials) let url = directions.url(forCalculating: options, httpMethod: "GET") XCTAssertLessThanOrEqual(url.absoluteString.count, MaximumURLLength, "maximumCoordinateCount is too high") @@ -66,7 +66,7 @@ class DirectionsTests: XCTestCase { let coordinates = Array(repeating: CLLocationCoordinate2D(latitude: 0, longitude: 0), count: maximumCoordinateCount + 1) let options = RouteOptions(coordinates: coordinates) - let directions = Directions(accessToken: BogusToken) + let directions = Directions(credentials: BogusCredentials) let request = directions.urlRequest(forCalculating: options) XCTAssertEqual(request.httpMethod, "POST") @@ -89,11 +89,11 @@ class DirectionsTests: XCTestCase { let one = CLLocation(coordinate: CLLocationCoordinate2D(latitude: 0.0, longitude: 0.0)) let two = CLLocation(coordinate: CLLocationCoordinate2D(latitude: 2.0, longitude: 2.0)) - let directions = Directions(accessToken: BogusToken) + let directions = Directions(credentials: BogusCredentials) let opts = RouteOptions(locations: [one, two]) - directions.calculate(opts, completionHandler: { (waypoints, routes, error) in + directions.calculate(opts, completionHandler: { (response, error) in expectation.fulfill() - XCTAssertNil(routes, "Unexpected route response") + XCTAssertNil(response.routes, "Unexpected route response") XCTAssertNotNil(error, "No error returned") XCTAssertEqual(error, .requestTooLarge) }) @@ -111,11 +111,11 @@ class DirectionsTests: XCTestCase { let one = CLLocation(coordinate: CLLocationCoordinate2D(latitude: 0.0, longitude: 0.0)) let two = CLLocation(coordinate: CLLocationCoordinate2D(latitude: 2.0, longitude: 2.0)) - let directions = Directions(accessToken: BogusToken) + let directions = Directions(credentials: BogusCredentials) let opts = RouteOptions(locations: [one, two]) - directions.calculate(opts, completionHandler: { (waypoints, routes, error) in + directions.calculate(opts, completionHandler: { (response, error) in expectation.fulfill() - XCTAssertNil(routes, "Unexpected route response") + XCTAssertNil(response.routes, "Unexpected route response") XCTAssertNotNil(error, "No error returned") switch error { case .invalidResponse?: @@ -132,7 +132,7 @@ class DirectionsTests: XCTestCase { let headerFields = ["X-Rate-Limit-Interval" : "60", "X-Rate-Limit-Limit" : "600", "X-Rate-Limit-Reset" : "1479460584"] let response = HTTPURLResponse(url: url, statusCode: 429, httpVersion: nil, headerFields: headerFields) - let resultError = Directions.informativeError(code: "429", message: "Hit rate limit", response: response, underlyingError: nil) + let resultError = DirectionsError(code: "429", message: "Hit rate limit", response: response, underlyingError: nil) if case let .rateLimited(rateLimitInterval, rateLimit, resetTime) = resultError { XCTAssertEqual(rateLimitInterval, 60.0) XCTAssertEqual(rateLimit, 600) diff --git a/Tests/MapboxDirectionsTests/MatchTests.swift b/Tests/MapboxDirectionsTests/MatchTests.swift index 2e56a586f..da6bc6f30 100644 --- a/Tests/MapboxDirectionsTests/MatchTests.swift +++ b/Tests/MapboxDirectionsTests/MatchTests.swift @@ -30,15 +30,15 @@ class MatchTests: XCTestCase { return OHHTTPStubsResponse(fileAtPath: path!, statusCode: 200, headers: ["Content-Type": "application/json"]) } - var match: Match! + var response: MapMatchingResponse! let matchOptions = MatchOptions(coordinates: locations) matchOptions.includesSteps = true matchOptions.routeShapeResolution = .full - let task = Directions(accessToken: BogusToken).calculate(matchOptions) { (matches, error) in + let task = Directions(credentials: BogusCredentials).calculate(matchOptions) { (resp, error) in XCTAssertNil(error, "Error: \(error!)") - match = matches!.first! + response = resp expectation.fulfill() } @@ -49,29 +49,29 @@ class MatchTests: XCTestCase { XCTAssertEqual(task.state, .completed) } - let opts = match.matchOptions + #warning("need tests for route response and match response") + let match = response.matches!.first! + let opts = response.options XCTAssert(matchOptions == opts) XCTAssertNotNil(match) XCTAssertNotNil(match.shape) XCTAssertEqual(match.shape!.coordinates.count, 18) - XCTAssertEqual(match.accessToken, BogusToken) - XCTAssertEqual(match.apiEndpoint, URL(string: "https://api.mapbox.com")) XCTAssertEqual(match.routeIdentifier, nil) - let tracePoints = match.tracepoints - XCTAssertNotNil(tracePoints) - XCTAssertEqual(tracePoints.first!!.countOfAlternatives, 0) - XCTAssertEqual(tracePoints.last!!.name, "West G Street") +// let tracePoints = match.tracepoints +// XCTAssertNotNil(tracePoints) +// XCTAssertEqual(tracePoints.first!!.countOfAlternatives, 0) +// XCTAssertEqual(tracePoints.last!!.name, "West G Street") // confirming actual decoded values is important because the Directions API // uses an atypical precision level for polyline encoding - XCTAssertEqual(round(match!.shape!.coordinates.first!.latitude), 33) - XCTAssertEqual(round(match!.shape!.coordinates.first!.longitude), -117) - XCTAssertEqual(match!.legs.count, 6) - XCTAssertEqual(match!.confidence, 0.95, accuracy: 1e-2) + XCTAssertEqual(round(match.shape!.coordinates.first!.latitude), 33) + XCTAssertEqual(round(match.shape!.coordinates.first!.longitude), -117) + XCTAssertEqual(match.legs.count, 6) + XCTAssertEqual(match.confidence, 0.95, accuracy: 1e-2) - let leg = match!.legs.first! + let leg = match.legs.first! XCTAssertEqual(leg.name, "North Harbor Drive") XCTAssertEqual(leg.steps.count, 2) @@ -116,14 +116,14 @@ class MatchTests: XCTestCase { return OHHTTPStubsResponse(fileAtPath: path!, statusCode: 200, headers: ["Content-Type": "application/json"]) } - var match: Match! + var response: MapMatchingResponse! let matchOptions = MatchOptions(coordinates: locations) matchOptions.includesSteps = true matchOptions.routeShapeResolution = .full - let task = Directions(accessToken: BogusToken).calculate(matchOptions) { (matches, error) in + let task = Directions(credentials: BogusCredentials).calculate(matchOptions) { (resp, error) in XCTAssertNil(error, "Error: \(error!)") - match = matches!.first! + response = resp expectation.fulfill() } XCTAssertNotNil(task) @@ -133,10 +133,11 @@ class MatchTests: XCTestCase { XCTAssertEqual(task.state, .completed) } + let match = response.matches!.first! XCTAssertNotNil(match) - let tracepoints = match.tracepoints - XCTAssertEqual(tracepoints.count, 7) - XCTAssertEqual(tracepoints.first!, nil) +// let tracepoints = match.tracepoints +// XCTAssertEqual(tracepoints.count, 7) +// XCTAssertEqual(tracepoints.first!, nil) // Encode and decode the match securely. // This may raise an Objective-C exception if an error is encountered which will fail the tests. @@ -149,8 +150,7 @@ class MatchTests: XCTestCase { let unarchivedMatch = try! decoder.decode(Match.self, from: encodedString.data(using: .utf8)!) XCTAssertEqual(match.confidence, unarchivedMatch.confidence) - XCTAssertEqual(match.matchOptions, unarchivedMatch.matchOptions) - XCTAssertEqual(match.tracepoints, unarchivedMatch.tracepoints) +// XCTAssertEqual(match.tracepoints, unarchivedMatch.tracepoints) } #endif diff --git a/Tests/MapboxDirectionsTests/OfflineDirectionsTests.swift b/Tests/MapboxDirectionsTests/OfflineDirectionsTests.swift index 115b6bb38..fad7c6e64 100644 --- a/Tests/MapboxDirectionsTests/OfflineDirectionsTests.swift +++ b/Tests/MapboxDirectionsTests/OfflineDirectionsTests.swift @@ -6,11 +6,13 @@ import OHHTTPStubs class OfflineDirectionsTests: XCTestCase { let token = "foo" let host = "api.mapbox.com" + let hostURL = URL(string: "https://api.mapbox.com")! func testAvailableVersions() { - let directions = Directions(accessToken: token, host: host) + let credentials = DirectionsCredentials(accessToken: token, host: hostURL) + let directions = Directions(credentials: credentials) - XCTAssertEqual(directions.accessToken, token) +// XCTAssertEqual(directions.accessToken, token) let versionsExpectation = expectation(description: "Fetching available versions should return results") @@ -45,7 +47,7 @@ class OfflineDirectionsTests: XCTestCase { } func testDownloadTiles() { - let directions = Directions(accessToken: token, host: host) + let directions = Directions(credentials: BogusCredentials) let bounds = CoordinateBounds(coordinates: [CLLocationCoordinate2D(latitude: 37.7890, longitude: -122.4337), CLLocationCoordinate2D(latitude: 37.7881, longitude: -122.4318)]) diff --git a/Tests/MapboxDirectionsTests/RoutableMatchTests.swift b/Tests/MapboxDirectionsTests/RoutableMatchTests.swift index 358cf39c8..1bd4b2c40 100644 --- a/Tests/MapboxDirectionsTests/RoutableMatchTests.swift +++ b/Tests/MapboxDirectionsTests/RoutableMatchTests.swift @@ -26,8 +26,7 @@ class RoutableMatchTest: XCTestCase { return OHHTTPStubsResponse(fileAtPath: path!, statusCode: 200, headers: ["Content-Type": "application/json"]) } - var route: Route! - var waypoints: [Waypoint]! + var routeResponse: RouteResponse! let matchOptions = MatchOptions(coordinates: locations) matchOptions.includesSteps = true @@ -36,28 +35,30 @@ class RoutableMatchTest: XCTestCase { waypoint.separatesLegs = false } - let task = Directions(accessToken: BogusToken).calculateRoutes(matching: matchOptions) { (wpoints, routes, error) in + let task = Directions(credentials: BogusCredentials).calculateRoutes(matching: matchOptions) { (response, error) in XCTAssertNil(error, "Error: \(error!)") - route = routes!.first! - waypoints = wpoints + routeResponse = response expectation.fulfill() } XCTAssertNotNil(task) - waitForExpectations(timeout: 2) { (error) in + waitForExpectations(timeout: 200000) { (error) in XCTAssertNil(error, "Error: \(error!)") XCTAssertEqual(task.state, .completed) } + let route = routeResponse.routes!.first! XCTAssertNotNil(route) XCTAssertNotNil(route.shape) XCTAssertEqual(route.shape!.coordinates.count, 18) - XCTAssertEqual(route.accessToken, BogusToken) - XCTAssertEqual(route.apiEndpoint, URL(string: "https://api.mapbox.com")) + #warning("Add a test for DirectionsCredentials") +// XCTAssertEqual(route.accessToken, BogusToken) +// XCTAssertEqual(route.apiEndpoint, URL(string: "https://api.mapbox.com")) XCTAssertEqual(route.routeIdentifier, nil) + let waypoints = routeResponse.waypoints! XCTAssertNotNil(waypoints) XCTAssertEqual(waypoints.first!.name, "North Harbor Drive") XCTAssertEqual(waypoints.last!.name, "West G Street") @@ -65,11 +66,11 @@ class RoutableMatchTest: XCTestCase { // confirming actual decoded values is important because the Directions API // uses an atypical precision level for polyline encoding - XCTAssertEqual(round(route!.shape!.coordinates.first!.latitude), 33) - XCTAssertEqual(round(route!.shape!.coordinates.first!.longitude), -117) - XCTAssertEqual(route!.legs.count, 6) + XCTAssertEqual(round(route.shape!.coordinates.first!.latitude), 33) + XCTAssertEqual(round(route.shape!.coordinates.first!.longitude), -117) + XCTAssertEqual(route.legs.count, 6) - let leg = route!.legs.first! + let leg = route.legs.first! XCTAssertEqual(leg.name, "North Harbor Drive") XCTAssertEqual(leg.steps.count, 2) diff --git a/Tests/MapboxDirectionsTests/RouteOptionsTests.swift b/Tests/MapboxDirectionsTests/RouteOptionsTests.swift index 2c1616c35..149a5fcb6 100644 --- a/Tests/MapboxDirectionsTests/RouteOptionsTests.swift +++ b/Tests/MapboxDirectionsTests/RouteOptionsTests.swift @@ -54,6 +54,7 @@ class RouteOptionsTests: XCTestCase { let subject = RouteOptions(waypoints: waypoints) let decoder = JSONDecoder() decoder.userInfo[.options] = subject + decoder.userInfo[.credentials] = DirectionsCredentials(accessToken: "foo", host: URL(string: "https://test.website")!) var response: RouteResponse? XCTAssertNoThrow(response = try decoder.decode(RouteResponse.self, from: fixtureData)) XCTAssertNotNil(response) diff --git a/Tests/MapboxDirectionsTests/RouteTests.swift b/Tests/MapboxDirectionsTests/RouteTests.swift index 2a031bf84..f32ce0b81 100644 --- a/Tests/MapboxDirectionsTests/RouteTests.swift +++ b/Tests/MapboxDirectionsTests/RouteTests.swift @@ -36,7 +36,7 @@ class RouteTests: XCTestCase { let expectedLeg = RouteLeg(steps: [], name: "West 6th Avenue Freeway, South University Boulevard", distance: 17036.8, expectedTravelTime: 1083.4, profileIdentifier: .automobileAvoidingTraffic) expectedLeg.source = options.waypoints[0] expectedLeg.destination = options.waypoints[1] - let expectedRoute = Route(legs: [expectedLeg], shape: nil, distance: 17036.8, expectedTravelTime: 1083.4, options: options) + let expectedRoute = Route(legs: [expectedLeg], shape: nil, distance: 17036.8, expectedTravelTime: 1083.4) XCTAssertEqual(route, expectedRoute) if let route = route { diff --git a/Tests/MapboxDirectionsTests/V5Tests.swift b/Tests/MapboxDirectionsTests/V5Tests.swift index f449ade8d..0f9fb5d77 100644 --- a/Tests/MapboxDirectionsTests/V5Tests.swift +++ b/Tests/MapboxDirectionsTests/V5Tests.swift @@ -47,15 +47,11 @@ class V5Tests: XCTestCase { options.includesSpokenInstructions = true options.locale = Locale(identifier: "en_US") options.includesExitRoundaboutManeuver = true - var route: Route? - let task = Directions(accessToken: BogusToken).calculate(options) { (waypoints, routes, error) in + var response: RouteResponse! + let task = Directions(credentials: BogusCredentials).calculate(options) { (resp, error) in XCTAssertNil(error, "Error: \(error!)") - XCTAssertEqual(waypoints?.count, 2) - - XCTAssertNotNil(routes) - XCTAssertEqual(routes!.count, 2) - route = routes!.first! + response = resp expectation.fulfill() } @@ -66,10 +62,15 @@ class V5Tests: XCTestCase { XCTAssertEqual(task.state, .completed) } - test(route, options: options) + XCTAssertEqual(response.waypoints?.count, 2) + XCTAssertEqual(response.routes?.count, 2) + + + + test(response.routes!.first!) } - func test(_ route: Route?, options: RouteOptions) { + func test(_ route: Route?) { XCTAssertNotNil(route) guard let route = route else { return @@ -77,8 +78,6 @@ class V5Tests: XCTestCase { XCTAssertNotNil(route.shape) XCTAssertEqual(route.shape!.coordinates.count, 30_097) - XCTAssertEqual(route.accessToken, BogusToken) - XCTAssertEqual(route.apiEndpoint, URL(string: "https://api.mapbox.com")) XCTAssertEqual(route.routeIdentifier?.count, 25) XCTAssertTrue(route.routeIdentifier?.starts(with: "cjsb5x") ?? false) XCTAssertEqual(route.speechLocale?.identifier, "en-US") @@ -89,9 +88,6 @@ class V5Tests: XCTestCase { XCTAssertEqual(route.shape?.coordinates.first?.longitude ?? 0, -122, accuracy: 1) XCTAssertEqual(route.legs.count, 1) - let opts = route.routeOptions - XCTAssertEqual(opts, options) - XCTAssertEqual(route.legs.count, 1) let leg = route.legs.first XCTAssertEqual(leg?.name, "Dwight D. Eisenhower Highway, I-80") @@ -250,14 +246,14 @@ class V5Tests: XCTestCase { options.includesExitRoundaboutManeuver = true var route: Route? - let task = Directions(accessToken: BogusToken).calculate(options) { (waypoints, routes, error) in + let task = Directions(credentials: BogusCredentials).calculate(options) { (response, error) in XCTAssertNil(error, "Error: \(error!)") - XCTAssertEqual(waypoints?.count, 3) + XCTAssertEqual(response.waypoints?.count, 3) - XCTAssertNotNil(routes) - XCTAssertEqual(routes!.count, 1) - route = routes!.first! + XCTAssertNotNil(response.routes) + XCTAssertEqual(response.routes!.count, 1) + route = response.routes!.first! expectation.fulfill() } @@ -290,13 +286,12 @@ class V5Tests: XCTestCase { let decoder = JSONDecoder() decoder.userInfo[.options] = options + decoder.userInfo[.credentials] = DirectionsCredentials(accessToken: "foo", host: URL(string: "http://sample.website")) let result = try! decoder.decode(RouteResponse.self, from: data) let routes = result.routes let route = routes!.first! - route.accessToken = BogusToken - route.apiEndpoint = URL(string: "https://api.mapbox.com") - route.routeIdentifier = result.uuid + // Encode and decode the route securely. @@ -312,7 +307,7 @@ class V5Tests: XCTestCase { var newRoute: Route? XCTAssertNoThrow(newRoute = try decoder.decode(Route.self, from: jsonData)) XCTAssertNotNil(newRoute) - test(newRoute, options: options) + test(newRoute) } } } diff --git a/Tests/MapboxDirectionsTests/VisualInstructionTests.swift b/Tests/MapboxDirectionsTests/VisualInstructionTests.swift index 7b8fed06c..8a4dde9a6 100644 --- a/Tests/MapboxDirectionsTests/VisualInstructionTests.swift +++ b/Tests/MapboxDirectionsTests/VisualInstructionTests.swift @@ -95,13 +95,11 @@ class VisualInstructionsTests: XCTestCase { options.includesSpokenInstructions = true options.distanceMeasurementSystem = .imperial options.includesVisualInstructions = true - var route: Route? - let task = Directions(accessToken: BogusToken).calculate(options) { (waypoints, routes, error) in + var response: RouteResponse! + let task = Directions(credentials: BogusCredentials).calculate(options) { (resp, error) in XCTAssertNil(error, "Error: \(error!.localizedDescription)") - XCTAssertNotNil(routes) - XCTAssertEqual(routes!.count, 1) - route = routes!.first! + response = resp expectation.fulfill() } @@ -112,10 +110,15 @@ class VisualInstructionsTests: XCTestCase { XCTAssertEqual(task.state, .completed) } + guard let route = response.routes?.first else { + XCTFail("No routes in response") + return + } + XCTAssertNotNil(route) - XCTAssertEqual(route!.routeIdentifier, "cjgy4xps418g17mo7l2pdm734") + XCTAssertEqual(route.routeIdentifier, "cjgy4xps418g17mo7l2pdm734") - let leg = route!.legs.first! + let leg = route.legs.first! let step = leg.steps[1] XCTAssertEqual(step.instructionsSpokenAlongStep!.count, 3) @@ -185,13 +188,11 @@ class VisualInstructionsTests: XCTestCase { options.includesAlternativeRoutes = false options.includesVisualInstructions = true - var route: Route? - let task = Directions(accessToken: BogusToken).calculate(options) { (waypoints, routes, error) in + var response: RouteResponse! + let task = Directions(credentials: BogusCredentials).calculate(options) { (resp, error) in XCTAssertNil(error, "Error: \(error!.localizedDescription)") - XCTAssertNotNil(routes) - XCTAssertEqual(routes!.count, 1) - route = routes!.first! + response = resp expectation.fulfill() } @@ -202,10 +203,15 @@ class VisualInstructionsTests: XCTestCase { XCTAssertEqual(task.state, .completed) } + guard let route = response.routes?.first else { + XCTFail("No routes in response") + return + } + XCTAssertNotNil(route) - XCTAssertEqual(route!.routeIdentifier, "cjikck25m00v279ms1knttdgc") + XCTAssertEqual(route.routeIdentifier, "cjikck25m00v279ms1knttdgc") - let step = route!.legs.first!.steps.first! + let step = route.legs.first!.steps.first! let visualInstructions = step.instructionsDisplayedAlongStep let tertiaryInstruction = visualInstructions?.first?.tertiaryInstruction @@ -255,13 +261,11 @@ class VisualInstructionsTests: XCTestCase { options.includesAlternativeRoutes = false options.includesVisualInstructions = true - var route: Route? - let task = Directions(accessToken: BogusToken).calculate(options) { (waypoints, routes, error) in + var response: RouteResponse! + let task = Directions(credentials: BogusCredentials).calculate(options) { (resp, error) in XCTAssertNil(error, "Error: \(error!.localizedDescription)") - XCTAssertNotNil(routes) - XCTAssertEqual(routes!.count, 1) - route = routes!.first! + response = resp expectation.fulfill() } @@ -272,10 +276,15 @@ class VisualInstructionsTests: XCTestCase { XCTAssertEqual(task.state, .completed) } + guard let route = response.routes?.first else { + XCTFail("No routes in response") + return + } + XCTAssertNotNil(route) - XCTAssertEqual(route!.routeIdentifier, "cjilrvx2200447omltwdayvm4") + XCTAssertEqual(route.routeIdentifier, "cjilrvx2200447omltwdayvm4") - let step = route!.legs.first!.steps.first! + let step = route.legs.first!.steps.first! let visualInstructions = step.instructionsDisplayedAlongStep let tertiaryInstruction = visualInstructions?.first?.tertiaryInstruction From d98e073a3dbaf4e3c7bf829449847dcf5533a711 Mon Sep 17 00:00:00 2001 From: Jerrad Thramer Date: Wed, 12 Feb 2020 17:01:32 -0700 Subject: [PATCH 08/24] Fixing remaining tests. --- Sources/MapboxDirections/Directions.swift | 128 ++++++++++++------ .../MapboxDirections/DirectionsError.swift | 3 +- .../MapboxDirections/DirectionsResult.swift | 14 ++ Sources/MapboxDirections/RouteLeg.swift | 15 ++ Sources/MapboxDirections/RouteResponse.swift | 2 + .../DirectionsTests.swift | 2 +- 6 files changed, 119 insertions(+), 45 deletions(-) diff --git a/Sources/MapboxDirections/Directions.swift b/Sources/MapboxDirections/Directions.swift index 928b2d583..d0d773020 100644 --- a/Sources/MapboxDirections/Directions.swift +++ b/Sources/MapboxDirections/Directions.swift @@ -142,16 +142,27 @@ open class Directions: NSObject { decoder.userInfo = [.options: options, .credentials: self.credentials] - let disposition = try decoder.decode(ResponseDisposition.self, from: data) - let result = try decoder.decode(RouteResponse.self, from: data) + guard let disposition = try? decoder.decode(ResponseDisposition.self, from: data) else { + let apiError = DirectionsError(code: nil, message: nil, response: possibleResponse, underlyingError: possibleError) + let response = RouteResponse(httpResponse: httpResponse, options: .route(options), credentials: self.credentials) + + DispatchQueue.main.async { + completionHandler(response, apiError) + } + return + } + guard (disposition.code == nil && disposition.message == nil) || disposition.code == "Ok" else { let apiError = DirectionsError(code: disposition.code, message: disposition.message, response: response, underlyingError: possibleError) + let response = RouteResponse(httpResponse: httpResponse, options: .route(options), credentials: self.credentials) + DispatchQueue.main.async { - completionHandler(result, apiError) + completionHandler(response, apiError) } return } + let result = try decoder.decode(RouteResponse.self, from: data) guard result.routes != nil else { DispatchQueue.main.async { completionHandler(result, .unableToRoute) @@ -215,16 +226,27 @@ open class Directions: NSObject { let decoder = JSONDecoder() decoder.userInfo = [.options: options, .credentials: self.credentials] + guard let disposition = try? decoder.decode(ResponseDisposition.self, from: data) else { + let apiError = DirectionsError(code: nil, message: nil, response: possibleResponse, underlyingError: possibleError) + let response = MapMatchingResponse(httpResponse: httpResponse, options: options, credentials: self.credentials) + + DispatchQueue.main.async { + completionHandler(response, apiError) + } + return + } + + guard disposition.code == "Ok" else { + let apiError = DirectionsError(code: disposition.code, message: disposition.message, response: response, underlyingError: possibleError) + let response = MapMatchingResponse(httpResponse: httpResponse, options: options, credentials: self.credentials) + + DispatchQueue.main.async { + completionHandler(response, apiError) + } + return + } - let disposition = try decoder.decode(ResponseDisposition.self, from: data) let result = try decoder.decode(MapMatchingResponse.self, from: data) - guard disposition.code == "Ok" else { - let apiError = DirectionsError(code: disposition.code, message: disposition.message, response: response, underlyingError: possibleError) - DispatchQueue.main.async { - completionHandler(result, apiError) - } - return - } guard result.matches != nil else { DispatchQueue.main.async { @@ -272,56 +294,76 @@ open class Directions: NSObject { return } - guard let data = possibleData else { - let response = RouteResponse(httpResponse: httpResponse, options: .match(options), credentials: self.credentials) - completionHandler(response, .noData) - return - } + guard let data = possibleData else { + let response = RouteResponse(httpResponse: httpResponse, options: .match(options), credentials: self.credentials) + completionHandler(response, .noData) + return + } - if let error = possibleError { + if let error = possibleError { let response = RouteResponse(httpResponse: httpResponse, options: .match(options), credentials: self.credentials) let unknownError = DirectionsError.unknown(response: possibleResponse, underlying: error, code: nil, message: nil) completionHandler(response, unknownError) return } - DispatchQueue.global(qos: .userInitiated).async { - do { - let decoder = JSONDecoder() - decoder.userInfo = [.options: options, - .credentials: self.credentials] - - let disposition = try decoder.decode(ResponseDisposition.self, from: data) - let result = try decoder.decode(MapMatchingResponse.self, from: data) + DispatchQueue.global(qos: .userInitiated).async { + do { + let decoder = JSONDecoder() + decoder.userInfo = [.options: options, + .credentials: self.credentials] + + + guard let disposition = try? decoder.decode(ResponseDisposition.self, from: data) else { + let apiError = DirectionsError(code: nil, message: nil, response: possibleResponse, underlyingError: possibleError) + let response = RouteResponse(httpResponse: httpResponse, options: .match(options), credentials: self.credentials) - let routeResponse = RouteResponse(matching: result, options: options, credentials: self.credentials) - guard disposition.code == "Ok" else { - let apiError = DirectionsError(code: disposition.code, message: disposition.message, response: response, underlyingError: possibleError) - DispatchQueue.main.async { - completionHandler(routeResponse, apiError) - } - return + DispatchQueue.main.async { + completionHandler(response, apiError) } + return + } + + guard disposition.code == "Ok" else { + let apiError = DirectionsError(code: disposition.code, message: disposition.message, response: response, underlyingError: possibleError) + let response = RouteResponse(httpResponse: httpResponse, options: .match(options), credentials: self.credentials) - guard routeResponse.routes != nil else { - DispatchQueue.main.async { - completionHandler(routeResponse, .unableToRoute) - } - return + DispatchQueue.main.async { + completionHandler(response, apiError) } - + return + } + + let result = try decoder.decode(MapMatchingResponse.self, from: data) + + let routeResponse = RouteResponse(matching: result, options: options, credentials: self.credentials) + guard disposition.code == "Ok" else { + let apiError = DirectionsError(code: disposition.code, message: disposition.message, response: response, underlyingError: possibleError) DispatchQueue.main.async { - completionHandler(routeResponse, nil) + completionHandler(routeResponse, apiError) } - } catch { + return + } + + guard routeResponse.routes != nil else { DispatchQueue.main.async { - let bailError = DirectionsError(code: nil, message: nil, response: response, underlyingError: error) - let response = RouteResponse(httpResponse: httpResponse, options: .match(options), credentials: self.credentials) - completionHandler(response, bailError) + completionHandler(routeResponse, .unableToRoute) } + return + } + + DispatchQueue.main.async { + completionHandler(routeResponse, nil) + } + } catch { + DispatchQueue.main.async { + let bailError = DirectionsError(code: nil, message: nil, response: response, underlyingError: error) + let response = RouteResponse(httpResponse: httpResponse, options: .match(options), credentials: self.credentials) + completionHandler(response, bailError) } } } + } requestTask.priority = 1 requestTask.resume() diff --git a/Sources/MapboxDirections/DirectionsError.swift b/Sources/MapboxDirections/DirectionsError.swift index 84be3c7ca..0d5da16f1 100644 --- a/Sources/MapboxDirections/DirectionsError.swift +++ b/Sources/MapboxDirections/DirectionsError.swift @@ -28,8 +28,9 @@ public enum DirectionsError: LocalizedError { default: self = .unknown(response: response, underlying: error, code: code, message: message) } + } else { + self = .unknown(response: response, underlying: error, code: code, message: message) } - self = .unknown(response: response, underlying: error, code: code, message: message) } /** diff --git a/Sources/MapboxDirections/DirectionsResult.swift b/Sources/MapboxDirections/DirectionsResult.swift index dd4315ce9..9950c2634 100644 --- a/Sources/MapboxDirections/DirectionsResult.swift +++ b/Sources/MapboxDirections/DirectionsResult.swift @@ -30,7 +30,21 @@ open class DirectionsResult: Codable { public required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) + + guard let options = decoder.userInfo[.options] else { + throw DirectionsCodingError.missingOptions + } + legs = try container.decode([RouteLeg].self, forKey: .legs) + + //populate legs with origin and destination + if let options = options as? DirectionsOptions { + let waypoints = options.waypoints + legs.populate(waypoints: waypoints) + } else { + throw DirectionsCodingError.missingOptions + } + distance = try container.decode(CLLocationDistance.self, forKey: .distance) expectedTravelTime = try container.decode(TimeInterval.self, forKey: .expectedTravelTime) diff --git a/Sources/MapboxDirections/RouteLeg.swift b/Sources/MapboxDirections/RouteLeg.swift index d86e18010..bacfaf2ab 100644 --- a/Sources/MapboxDirections/RouteLeg.swift +++ b/Sources/MapboxDirections/RouteLeg.swift @@ -282,3 +282,18 @@ extension RouteLeg: CustomQuickLookConvertible { return debugQuickLookURL(illustrating: LineString(coordinates)) } } + + +public extension Array where Element == RouteLeg { + /** + Populates source and destination information for each leg with waypoint information, typically gathered from DirectionsOptions. + */ + func populate(waypoints: [Waypoint]) { + let legInfo = zip(zip(waypoints.prefix(upTo: waypoints.endIndex - 1), waypoints.suffix(from: 1)), self) + + for (endpoints, leg) in legInfo { + leg.source = endpoints.0 + leg.destination = endpoints.1 + } + } +} diff --git a/Sources/MapboxDirections/RouteResponse.swift b/Sources/MapboxDirections/RouteResponse.swift index 2f3f67721..61ba434f2 100644 --- a/Sources/MapboxDirections/RouteResponse.swift +++ b/Sources/MapboxDirections/RouteResponse.swift @@ -43,6 +43,8 @@ extension RouteResponse: Codable { let decoder = JSONDecoder() let encoder = JSONEncoder() + decoder.userInfo[.options] = options + let routes: [Route]? = response.matches?.compactMap({ (match) -> Route? in guard let json = try? encoder.encode(match) else { return nil } guard let route = try? decoder.decode(Route.self, from: json) else { return nil } diff --git a/Tests/MapboxDirectionsTests/DirectionsTests.swift b/Tests/MapboxDirectionsTests/DirectionsTests.swift index e1e67ce98..2d23b6c33 100644 --- a/Tests/MapboxDirectionsTests/DirectionsTests.swift +++ b/Tests/MapboxDirectionsTests/DirectionsTests.swift @@ -83,7 +83,7 @@ class DirectionsTests: XCTestCase { OHHTTPStubs.stubRequests(passingTest: { (request) -> Bool in return request.url!.absoluteString.contains("https://api.mapbox.com/directions") }) { (_) -> OHHTTPStubsResponse in - return OHHTTPStubsResponse(data: BadResponse.data(using: .utf8)!, statusCode: 413, headers: ["Content-Type" : "application/json"]) + return OHHTTPStubsResponse(data: BadResponse.data(using: .utf8)!, statusCode: 413, headers: ["Content-Type" : "text/html"]) } let expectation = self.expectation(description: "Async callback") let one = CLLocation(coordinate: CLLocationCoordinate2D(latitude: 0.0, longitude: 0.0)) From efb5f2eb35d498d8f767117418c32154215830bb Mon Sep 17 00:00:00 2001 From: Jerrad Thramer Date: Wed, 12 Feb 2020 17:11:36 -0700 Subject: [PATCH 09/24] Fixing issue where SPM would fail compilation. --- Sources/MapboxDirections/RouteResponse.swift | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Sources/MapboxDirections/RouteResponse.swift b/Sources/MapboxDirections/RouteResponse.swift index 61ba434f2..53fa07b70 100644 --- a/Sources/MapboxDirections/RouteResponse.swift +++ b/Sources/MapboxDirections/RouteResponse.swift @@ -35,8 +35,13 @@ extension RouteResponse: Codable { case waypoints } - public init(httpResponse: HTTPURLResponse?, options: ResponseOptions, credentials: DirectionsCredentials) { - self.init(httpResponse: httpResponse, identifier: nil, routes: nil, waypoints: nil, options: options, credentials: credentials) + public init(httpResponse: HTTPURLResponse?, identifier: String? = nil, routes: [Route]? = nil, waypoints: [Waypoint]? = nil, options: ResponseOptions, credentials: DirectionsCredentials) { + self.httpResponse = httpResponse + self.identifier = identifier + self.routes = routes + self.waypoints = waypoints + self.options = options + self.credentials = credentials } public init(matching response: MapMatchingResponse, options: MatchOptions, credentials: DirectionsCredentials) { From 6448a27c7266a020906dc65730089ef164378451 Mon Sep 17 00:00:00 2001 From: Jerrad Thramer Date: Wed, 12 Feb 2020 17:15:44 -0700 Subject: [PATCH 10/24] Applying same SPM fix to MapMatchingResponse --- .../MapMatching/MapMatchingResponse.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift b/Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift index 3131bbc36..6ff18ffff 100644 --- a/Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift +++ b/Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift @@ -25,8 +25,12 @@ extension MapMatchingResponse: Codable { case tracepoints } - public init(httpResponse: HTTPURLResponse, options: MatchOptions, credentials: DirectionsCredentials) { - self.init(httpResponse: httpResponse, matches: nil, tracepoints: nil, options: options, credentials: credentials) + public init(httpResponse: HTTPURLResponse?, matches: [Match]? = nil, tracepoints: [Tracepoint]? = nil, options: MatchOptions, credentials: DirectionsCredentials) { + self.httpResponse = httpResponse + self.matches = matches + self.tracepoints = tracepoints + self.options = options + self.credentials = credentials } public init(from decoder: Decoder) throws { From 288d8af884484516c2bcc538e9db0d3c9db8f664 Mon Sep 17 00:00:00 2001 From: Jerrad Thramer Date: Wed, 12 Feb 2020 17:22:38 -0700 Subject: [PATCH 11/24] removing redundant logic --- Sources/MapboxDirections/Directions.swift | 8 -------- 1 file changed, 8 deletions(-) diff --git a/Sources/MapboxDirections/Directions.swift b/Sources/MapboxDirections/Directions.swift index d0d773020..75a7a79dd 100644 --- a/Sources/MapboxDirections/Directions.swift +++ b/Sources/MapboxDirections/Directions.swift @@ -337,14 +337,6 @@ open class Directions: NSObject { let result = try decoder.decode(MapMatchingResponse.self, from: data) let routeResponse = RouteResponse(matching: result, options: options, credentials: self.credentials) - guard disposition.code == "Ok" else { - let apiError = DirectionsError(code: disposition.code, message: disposition.message, response: response, underlyingError: possibleError) - DispatchQueue.main.async { - completionHandler(routeResponse, apiError) - } - return - } - guard routeResponse.routes != nil else { DispatchQueue.main.async { completionHandler(routeResponse, .unableToRoute) From 4e59896ea93fb0f21f68039b918a392095bee9aa Mon Sep 17 00:00:00 2001 From: Jerrad Thramer Date: Wed, 12 Feb 2020 17:24:21 -0700 Subject: [PATCH 12/24] Fixing issue where model mismatch results in a silent fail by propagating failure via throwable initalizer --- Sources/MapboxDirections/Directions.swift | 2 +- Sources/MapboxDirections/RouteResponse.swift | 18 ++++++++---------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/Sources/MapboxDirections/Directions.swift b/Sources/MapboxDirections/Directions.swift index 75a7a79dd..b54674f58 100644 --- a/Sources/MapboxDirections/Directions.swift +++ b/Sources/MapboxDirections/Directions.swift @@ -336,7 +336,7 @@ open class Directions: NSObject { let result = try decoder.decode(MapMatchingResponse.self, from: data) - let routeResponse = RouteResponse(matching: result, options: options, credentials: self.credentials) + let routeResponse = try RouteResponse(matching: result, options: options, credentials: self.credentials) guard routeResponse.routes != nil else { DispatchQueue.main.async { completionHandler(routeResponse, .unableToRoute) diff --git a/Sources/MapboxDirections/RouteResponse.swift b/Sources/MapboxDirections/RouteResponse.swift index 53fa07b70..2a5155f22 100644 --- a/Sources/MapboxDirections/RouteResponse.swift +++ b/Sources/MapboxDirections/RouteResponse.swift @@ -44,23 +44,21 @@ extension RouteResponse: Codable { self.credentials = credentials } - public init(matching response: MapMatchingResponse, options: MatchOptions, credentials: DirectionsCredentials) { + public init(matching response: MapMatchingResponse, options: MatchOptions, credentials: DirectionsCredentials) throws { let decoder = JSONDecoder() let encoder = JSONEncoder() decoder.userInfo[.options] = options - let routes: [Route]? = response.matches?.compactMap({ (match) -> Route? in - guard let json = try? encoder.encode(match) else { return nil } - guard let route = try? decoder.decode(Route.self, from: json) else { return nil } + let routes: [Route]? = try response.matches?.compactMap({ (match) -> Route? in + let json = try encoder.encode(match) + let route = try decoder.decode(Route.self, from: json) return route }) - - // CONVERT WAYPOINTS - - let waypoints: [Waypoint]? = response.tracepoints?.compactMap({ (trace) -> Waypoint? in - guard let json = try? encoder.encode(trace) else { return nil } - guard let waypoint = try? decoder.decode(Waypoint.self, from: json) else { return nil } + + let waypoints: [Waypoint]? = try response.tracepoints?.compactMap({ (trace) -> Waypoint? in + let json = try encoder.encode(trace) + let waypoint = try decoder.decode(Waypoint.self, from: json) return waypoint }) From 8c7b29df34c19f496099154f1162080699a42ef0 Mon Sep 17 00:00:00 2001 From: Jerrad Thramer Date: Mon, 17 Feb 2020 11:14:10 -0700 Subject: [PATCH 13/24] Adding documentation, and test change. --- Sources/MapboxDirections/Directions.swift | 22 +++++++++---------- .../DirectionsCredentialsTests.swift | 2 +- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/Sources/MapboxDirections/Directions.swift b/Sources/MapboxDirections/Directions.swift index b54674f58..912fa5c5a 100644 --- a/Sources/MapboxDirections/Directions.swift +++ b/Sources/MapboxDirections/Directions.swift @@ -60,21 +60,18 @@ open class Directions: NSObject { /** A closure (block) to be called when a directions request is complete. - - parameter waypoints: An array of `Waypoint` objects. Each waypoint object corresponds to a `Waypoint` object in the original `RouteOptions` object. The locations and names of these waypoints are the result of conflating the original waypoints to known roads. The waypoints may include additional information that was not specified in the original waypoints. + - parameter response: A `RouteResponse` object that contains the entire payload of the Directions API solution. See `RouteResponse.swift` for more information. - If the request was canceled or there was an error obtaining the routes, this argument may be `nil`. - - parameter routes: An array of `Route` objects. The preferred route is first; any alternative routes come next if the `RouteOptions` object’s `includesAlternativeRoutes` property was set to `true`. The preferred route depends on the route options object’s `profileIdentifier` property. - - If the request was canceled or there was an error obtaining the routes, this argument is `nil`. This is not to be confused with the situation in which no results were found, in which case the array is present but empty. - - parameter error: The error that occurred, or `nil` if the placemarks were obtained successfully. + - parameter error: The error that occurred, or `nil` if the solution was obtained successfully. */ public typealias RouteCompletionHandler = (_ response: RouteResponse, _ error: DirectionsError?) -> Void /** A closure (block) to be called when a map matching request is complete. - If the request was canceled or there was an error obtaining the matches, this argument is `nil`. This is not to be confused with the situation in which no matches were found, in which case the array is present but empty. - - parameter error: The error that occurred, or `nil` if the placemarks were obtained successfully. + - parameter response: A `MapMatching` object that contains the entire payload of the Directions Map Matching API solution. See `MapMatchingResponse.swift` for more information. + + - parameter error: The error that occurred, or `nil` if the solution was obtained successfully. */ public typealias MatchCompletionHandler = (_ response: MapMatchingResponse, _ error: DirectionsError?) -> Void @@ -92,9 +89,9 @@ open class Directions: NSObject { /** Initializes a newly created directions object with an optional access token and host. - - parameter accessToken: A Mapbox [access token](https://docs.mapbox.com/help/glossary/access-token/). If an access token is not specified when initializing the directions object, it should be specified in the `MGLMapboxAccessToken` key in the main application bundle’s Info.plist. - - parameter host: An optional hostname to the server API. The [Mapbox Directions API](https://docs.mapbox.com/api/navigation/#directions) endpoint is used by default. + - parameter credentials: A `DirectionsCredentials` object that, optionally, contains customized Token and Endpoint information. If no credentials object is supplied, then defaults are used. */ + public init(credentials: DirectionsCredentials = .init()) { self.credentials = credentials } @@ -426,11 +423,12 @@ open class Directions: NSObject { } } +/** + Keys to pass to populate a `userInfo` dictionary, which is passed to the `JSONDecoder` upon trying to decode a `RouteResponse` or `MapMatchingResponse`. + */ public extension CodingUserInfoKey { static let options = CodingUserInfoKey(rawValue: "com.mapbox.directions.coding.routeOptions")! static let httpResponse = CodingUserInfoKey(rawValue: "com.mapbox.directions.coding.httpResponse")! static let credentials = CodingUserInfoKey(rawValue: "com.mapbox.directions.coding.credentials")! static let tracepoints = CodingUserInfoKey(rawValue: "com.mapbox.directions.coding.tracepoints")! - - } diff --git a/Tests/MapboxDirectionsTests/DirectionsCredentialsTests.swift b/Tests/MapboxDirectionsTests/DirectionsCredentialsTests.swift index f69e97346..ca7557582 100644 --- a/Tests/MapboxDirectionsTests/DirectionsCredentialsTests.swift +++ b/Tests/MapboxDirectionsTests/DirectionsCredentialsTests.swift @@ -13,7 +13,7 @@ class DirectionsCredentialsTests: XCTestCase { func testCustomConfiguration() { let token = "deadbeefcafebebe" - let host = URL(string: "https://hello.world")! + let host = URL(string: "https://example.com")! let credentials = DirectionsCredentials(accessToken: token, host: host) XCTAssertEqual(credentials.accessToken, token) XCTAssertEqual(credentials.host, host) From 49ad7d9954e3393e2bbf8dfb7b0eec079cc204e6 Mon Sep 17 00:00:00 2001 From: Jerrad Thramer Date: Mon, 17 Feb 2020 11:54:38 -0700 Subject: [PATCH 14/24] Adding test coverage --- MapboxDirections.xcodeproj/project.pbxproj | 8 +++ .../MatchOptionsTests.swift | 71 +++++++++++++++++++ .../RouteOptionsTests.swift | 4 +- .../WalkingOptionsTests.swift | 2 +- 4 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 Tests/MapboxDirectionsTests/MatchOptionsTests.swift diff --git a/MapboxDirections.xcodeproj/project.pbxproj b/MapboxDirections.xcodeproj/project.pbxproj index 796360039..fef223b3d 100644 --- a/MapboxDirections.xcodeproj/project.pbxproj +++ b/MapboxDirections.xcodeproj/project.pbxproj @@ -62,6 +62,9 @@ 43538E3D23ED6A2000E010D4 /* DirectionsCredentialsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43538E3C23ED6A2000E010D4 /* DirectionsCredentialsTests.swift */; }; 43538E3E23ED6A2000E010D4 /* DirectionsCredentialsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43538E3C23ED6A2000E010D4 /* DirectionsCredentialsTests.swift */; }; 43538E3F23ED6A2000E010D4 /* DirectionsCredentialsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43538E3C23ED6A2000E010D4 /* DirectionsCredentialsTests.swift */; }; + 4376A52723FB13D400C6038D /* MatchOptionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4376A52623FB13D400C6038D /* MatchOptionsTests.swift */; }; + 4376A52823FB13D400C6038D /* MatchOptionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4376A52623FB13D400C6038D /* MatchOptionsTests.swift */; }; + 4376A52923FB13D400C6038D /* MatchOptionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4376A52623FB13D400C6038D /* MatchOptionsTests.swift */; }; 438BFEC2233D854D00457294 /* DirectionsProfileIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 438BFEC1233D854D00457294 /* DirectionsProfileIdentifier.swift */; }; 438BFEC3233D854D00457294 /* DirectionsProfileIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 438BFEC1233D854D00457294 /* DirectionsProfileIdentifier.swift */; }; 438BFEC4233D854D00457294 /* DirectionsProfileIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 438BFEC1233D854D00457294 /* DirectionsProfileIdentifier.swift */; }; @@ -357,6 +360,7 @@ 43208BAC2343FF5500D8BD89 /* RouteResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteResponse.swift; sourceTree = ""; }; 43538E3623ED3B1600E010D4 /* ResponseDisposition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseDisposition.swift; sourceTree = ""; }; 43538E3C23ED6A2000E010D4 /* DirectionsCredentialsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectionsCredentialsTests.swift; sourceTree = ""; }; + 4376A52623FB13D400C6038D /* MatchOptionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchOptionsTests.swift; sourceTree = ""; }; 438BFEBC233D7FA900457294 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; 438BFEC0233D805500457294 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 438BFEC1233D854D00457294 /* DirectionsProfileIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectionsProfileIdentifier.swift; sourceTree = ""; }; @@ -675,6 +679,7 @@ DAE2DF6B23AED2280065057A /* RouteTests.swift */, DA8F3A7123B56D3B00B56786 /* RouteLegTests.swift */, DAE9E0F31EB7DE2E001E8E8B /* RouteOptionsTests.swift */, + 4376A52623FB13D400C6038D /* MatchOptionsTests.swift */, F4D785EE1DDD82C100FF4665 /* RouteStepTests.swift */, DAABF78D2395ABA900CEEB61 /* SpokenInstructionTests.swift */, DA6C9DAB1CAEC72800094FBC /* V5Tests.swift */, @@ -1240,6 +1245,7 @@ DAE2DF6D23AED2280065057A /* RouteTests.swift in Sources */, 35DBF015217E199E0009D2AE /* OfflineDirectionsTests.swift in Sources */, C53A02291E92C27A009837BD /* AnnotationTests.swift in Sources */, + 4376A52823FB13D400C6038D /* MatchOptionsTests.swift in Sources */, DA688B3F21B89ECD00C9BB25 /* VisualInstructionComponentTests.swift in Sources */, DAD06E36239F0B19001A917D /* DirectionsErrorTests.swift in Sources */, C596663A2048AECD00C45CE5 /* RoutableMatchTests.swift in Sources */, @@ -1316,6 +1322,7 @@ DAE2DF6E23AED2280065057A /* RouteTests.swift in Sources */, 35DBF016217E199E0009D2AE /* OfflineDirectionsTests.swift in Sources */, C53A022A1E92C27B009837BD /* AnnotationTests.swift in Sources */, + 4376A52923FB13D400C6038D /* MatchOptionsTests.swift in Sources */, DA688B4021B89ECD00C9BB25 /* VisualInstructionComponentTests.swift in Sources */, DAD06E37239F0B19001A917D /* DirectionsErrorTests.swift in Sources */, C596663B2048AECE00C45CE5 /* RoutableMatchTests.swift in Sources */, @@ -1439,6 +1446,7 @@ DAE2DF6C23AED2280065057A /* RouteTests.swift in Sources */, 35DBF014217E199E0009D2AE /* OfflineDirectionsTests.swift in Sources */, C5247D711E818A24004B6154 /* AnnotationTests.swift in Sources */, + 4376A52723FB13D400C6038D /* MatchOptionsTests.swift in Sources */, DA688B3E21B89ECD00C9BB25 /* VisualInstructionComponentTests.swift in Sources */, DAD06E35239F0B19001A917D /* DirectionsErrorTests.swift in Sources */, C59666392048A20E00C45CE5 /* RoutableMatchTests.swift in Sources */, diff --git a/Tests/MapboxDirectionsTests/MatchOptionsTests.swift b/Tests/MapboxDirectionsTests/MatchOptionsTests.swift new file mode 100644 index 000000000..d69901f11 --- /dev/null +++ b/Tests/MapboxDirectionsTests/MatchOptionsTests.swift @@ -0,0 +1,71 @@ +import XCTest +import CoreLocation +@testable import MapboxDirections + +class MatchOptionsTests: XCTestCase { + func testCoding() { + let options = testMatchOptions + + let encoded: Data = try! JSONEncoder().encode(options) + let optionsString: String = String(data: encoded, encoding: .utf8)! + + let unarchivedOptions: MatchOptions = try! JSONDecoder().decode(MatchOptions.self, from: optionsString.data(using: .utf8)!) + + XCTAssertNotNil(unarchivedOptions) + + let coordinates = testCoordinates + let unarchivedWaypoints = unarchivedOptions.waypoints + XCTAssertEqual(unarchivedWaypoints.count, coordinates.count) + XCTAssertEqual(unarchivedWaypoints[0].coordinate.latitude, coordinates[0].latitude) + XCTAssertEqual(unarchivedWaypoints[0].coordinate.longitude, coordinates[0].longitude) + XCTAssertEqual(unarchivedWaypoints[1].coordinate.latitude, coordinates[1].latitude) + XCTAssertEqual(unarchivedWaypoints[1].coordinate.longitude, coordinates[1].longitude) + XCTAssertEqual(unarchivedWaypoints[2].coordinate.latitude, coordinates[2].latitude) + XCTAssertEqual(unarchivedWaypoints[2].coordinate.longitude, coordinates[2].longitude) + + XCTAssertEqual(unarchivedOptions.resamplesTraces, options.resamplesTraces) + } + + // MARK: API name-handling tests + + private static var testTracepoints: [Tracepoint] { + let one = CLLocationCoordinate2D(latitude: 39.27664, longitude:-84.41139) + let two = CLLocationCoordinate2D(latitude: 39.27277, longitude:-84.41226) + return [one, two].map { Tracepoint(coordinate: $0, countOfAlternatives: 0, name: nil) } + } + + + func testWaypointSerialization() { + let origin = Waypoint(coordinate: CLLocationCoordinate2D(latitude: 39.15031, longitude: -84.47182), name: "XU") + let destination = Waypoint(coordinate: CLLocationCoordinate2D(latitude: 39.12971, longitude: -84.51638), name: "UC") + let options = MatchOptions(waypoints: [origin, destination]) + XCTAssertEqual(options.coordinates, "-84.47182,39.15031;-84.51638,39.12971") + XCTAssertTrue(options.urlQueryItems.contains(URLQueryItem(name: "waypoint_names", value: "XU;UC"))) + } + + func testRouteOptionsConvertedFromMatchOptions() { + let matchOpts = testMatchOptions + let subject = RouteOptions(matchOptions: matchOpts) + + XCTAssertEqual(subject.includesSteps, matchOpts.includesSteps) + XCTAssertEqual(subject.shapeFormat, matchOpts.shapeFormat) + XCTAssertEqual(subject.attributeOptions, matchOpts.attributeOptions) + XCTAssertEqual(subject.routeShapeResolution, matchOpts.routeShapeResolution) + XCTAssertEqual(subject.locale, matchOpts.locale) + XCTAssertEqual(subject.includesSpokenInstructions, matchOpts.includesSpokenInstructions) + XCTAssertEqual(subject.includesVisualInstructions, matchOpts.includesVisualInstructions) + } +} + +fileprivate let testCoordinates = [ + CLLocationCoordinate2D(latitude: 52.5109, longitude: 13.4301), + CLLocationCoordinate2D(latitude: 52.5080, longitude: 13.4265), + CLLocationCoordinate2D(latitude: 52.5021, longitude: 13.4316), +] + + +var testMatchOptions: MatchOptions { + let opts = MatchOptions(coordinates: testCoordinates, profileIdentifier: .automobileAvoidingTraffic) + opts.resamplesTraces = true + return opts +} diff --git a/Tests/MapboxDirectionsTests/RouteOptionsTests.swift b/Tests/MapboxDirectionsTests/RouteOptionsTests.swift index 149a5fcb6..aa5eacc43 100644 --- a/Tests/MapboxDirectionsTests/RouteOptionsTests.swift +++ b/Tests/MapboxDirectionsTests/RouteOptionsTests.swift @@ -141,7 +141,7 @@ class RouteOptionsTests: XCTestCase { } } -let testCoordinates = [ +fileprivate let testCoordinates = [ CLLocationCoordinate2D(latitude: 52.5109, longitude: 13.4301), CLLocationCoordinate2D(latitude: 52.5080, longitude: 13.4265), CLLocationCoordinate2D(latitude: 52.5021, longitude: 13.4316), @@ -159,6 +159,6 @@ var testRouteOptions: RouteOptions { opts.distanceMeasurementSystem = .metric opts.includesVisualInstructions = true opts.roadClassesToAvoid = .toll - + return opts } diff --git a/Tests/MapboxDirectionsTests/WalkingOptionsTests.swift b/Tests/MapboxDirectionsTests/WalkingOptionsTests.swift index 0217c4a5b..d481c8dfc 100644 --- a/Tests/MapboxDirectionsTests/WalkingOptionsTests.swift +++ b/Tests/MapboxDirectionsTests/WalkingOptionsTests.swift @@ -13,7 +13,7 @@ class WalkingOptionsTests: XCTestCase { let options = RouteOptions(waypoints: waypoints, profileIdentifier: DirectionsProfileIdentifier.walking) var queryItems = options.urlQueryItems - XCTAssertEqual(queryItems.first { $0.name == "alley_bias" }?.value, "0.0") + XCTAssertEqual(queryItems.first { $0.name == "alley_bias" }?.value, "0.0") XCTAssertEqual(queryItems.first { $0.name == "walkway_bias" }?.value, "0.0") XCTAssertEqual(queryItems.first { $0.name == "walking_speed" }?.value, "1.42") From a024d57762bb07672685d62f6cce6c0b1e979733 Mon Sep 17 00:00:00 2001 From: Jerrad Thramer Date: Mon, 17 Feb 2020 12:34:49 -0700 Subject: [PATCH 15/24] Adding offline error case, polish to remaining tests and adding a bit of last-minute documentation --- Sources/MapboxDirections/Directions.swift | 52 +++++++++++++++---- .../MapboxDirections/DirectionsError.swift | 31 ++++++----- .../DirectionsTests.swift | 29 ++++++++++- Tests/MapboxDirectionsTests/MatchTests.swift | 9 ++-- 4 files changed, 92 insertions(+), 29 deletions(-) diff --git a/Sources/MapboxDirections/Directions.swift b/Sources/MapboxDirections/Directions.swift index 2b31b6687..515838e9e 100644 --- a/Sources/MapboxDirections/Directions.swift +++ b/Sources/MapboxDirections/Directions.swift @@ -110,10 +110,20 @@ open class Directions: NSObject { - parameter completionHandler: The closure (block) to call with the resulting routes. This closure is executed on the application’s main thread. - returns: The data task used to perform the HTTP request. If, while waiting for the completion handler to execute, you no longer want the resulting routes, cancel this task. */ - @discardableResult open func calculate(_ options: RouteOptions, completionHandler: @escaping RouteCompletionHandler /* FIXME: VARIENT TYPE */) -> URLSessionDataTask { + @discardableResult open func calculate(_ options: RouteOptions, completionHandler: @escaping RouteCompletionHandler) -> URLSessionDataTask { options.fetchStartDate = Date() let request = urlRequest(forCalculating: options) let requestTask = URLSession.shared.dataTask(with: request) { (possibleData, possibleResponse, possibleError) in + + let offlineErrors: [URLError.Code] = [.cannotConnectToHost, .dataNotAllowed, .notConnectedToInternet] + if let urlError = possibleError as? URLError, offlineErrors.contains(urlError.code) { + let response = RouteResponse(httpResponse: possibleResponse as? HTTPURLResponse, options: .route(options), credentials: self.credentials) + DispatchQueue.main.async { + completionHandler(response, .noConnection(underlying: urlError)) + } + return + } + guard let response = possibleResponse, ["application/json", "text/html"].contains(response.mimeType), let httpResponse = response as? HTTPURLResponse else { let response = RouteResponse(httpResponse: possibleResponse as? HTTPURLResponse, options: .route(options), credentials: self.credentials) DispatchQueue.main.async { @@ -206,19 +216,29 @@ open class Directions: NSObject { options.fetchStartDate = Date() let request = urlRequest(forCalculating: options) let requestTask = URLSession.shared.dataTask(with: request) { (possibleData, possibleResponse, possibleError) in + + let offlineErrors: [URLError.Code] = [.cannotConnectToHost, .dataNotAllowed, .notConnectedToInternet] + if let urlError = possibleError as? URLError, offlineErrors.contains(urlError.code) { + let response = MapMatchingResponse(httpResponse: possibleResponse as? HTTPURLResponse, options: options, credentials: self.credentials) + DispatchQueue.main.async { + completionHandler(response, .noConnection(underlying: urlError)) + } + return + } + guard let response = possibleResponse, response.mimeType == "application/json", let httpResponse = response as? HTTPURLResponse else { - let result = MapMatchingResponse(httpResponse: possibleResponse as? HTTPURLResponse, options: options, credentials: self.credentials) + let response = MapMatchingResponse(httpResponse: possibleResponse as? HTTPURLResponse, options: options, credentials: self.credentials) DispatchQueue.main.async { - completionHandler(result, .invalidResponse(possibleResponse)) + completionHandler(response, .invalidResponse(possibleResponse)) } return } guard let data = possibleData else { - let result = MapMatchingResponse(httpResponse: httpResponse, options: options, credentials: self.credentials) + let response = MapMatchingResponse(httpResponse: httpResponse, options: options, credentials: self.credentials) DispatchQueue.main.async { - completionHandler(result, .noData) + completionHandler(response, .noData) } return } @@ -257,23 +277,23 @@ open class Directions: NSObject { return } - let result = try decoder.decode(MapMatchingResponse.self, from: data) + let response = try decoder.decode(MapMatchingResponse.self, from: data) - guard result.matches != nil else { + guard response.matches != nil else { DispatchQueue.main.async { - completionHandler(result, .unableToRoute) + completionHandler(response, .unableToRoute) } return } DispatchQueue.main.async { - completionHandler(result, nil) + completionHandler(response, nil) } } catch { DispatchQueue.main.async { let caughtError = DirectionsError.unknown(response: response, underlying: error, code: nil, message: nil) - let result = MapMatchingResponse(httpResponse: httpResponse, options: options, credentials: self.credentials) - completionHandler(result, caughtError) + let response = MapMatchingResponse(httpResponse: httpResponse, options: options, credentials: self.credentials) + completionHandler(response, caughtError) } } } @@ -299,6 +319,16 @@ open class Directions: NSObject { options.fetchStartDate = Date() let request = urlRequest(forCalculating: options) let requestTask = URLSession.shared.dataTask(with: request) { (possibleData, possibleResponse, possibleError) in + + let offlineErrors: [URLError.Code] = [.cannotConnectToHost, .dataNotAllowed, .notConnectedToInternet] + if let urlError = possibleError as? URLError, offlineErrors.contains(urlError.code) { + let response = RouteResponse(httpResponse: possibleResponse as? HTTPURLResponse, options: .match(options), credentials: self.credentials) + DispatchQueue.main.async { + completionHandler(response, .noConnection(underlying: urlError)) + } + return + } + guard let response = possibleResponse, ["application/json", "text/html"].contains(response.mimeType), let httpResponse = response as? HTTPURLResponse else { let response = RouteResponse(httpResponse: possibleResponse as? HTTPURLResponse, options: .match(options), credentials: self.credentials) DispatchQueue.main.async { diff --git a/Sources/MapboxDirections/DirectionsError.swift b/Sources/MapboxDirections/DirectionsError.swift index 0d5da16f1..0f619fb3c 100644 --- a/Sources/MapboxDirections/DirectionsError.swift +++ b/Sources/MapboxDirections/DirectionsError.swift @@ -33,11 +33,19 @@ public enum DirectionsError: LocalizedError { } } + /** + There is no network connection available to perform the network request. + */ + case noConnection(underlying: URLError?) + /** The server returned an empty response. */ case noData + /** + The API recieved input that it didn't understand. + */ case invalidInput(message: String?) /** @@ -94,10 +102,17 @@ public enum DirectionsError: LocalizedError { */ case rateLimited(rateLimitInterval: TimeInterval?, rateLimit: UInt?, resetTime: Date?) + + /** + Unknown error case. Look at associated values for more details. + */ + case unknown(response: URLResponse?, underlying: Error?, code: String?, message: String?) public var failureReason: String? { switch self { + case .noConnection(_): + return "The client does not have a network connection to the server." case .noData: return "The server returned an empty response." case let .invalidInput(message): @@ -134,7 +149,7 @@ public enum DirectionsError: LocalizedError { public var recoverySuggestion: String? { switch self { - case .noData, .invalidInput, .invalidResponse: + case .noConnection(underlying: _), .noData, .invalidInput, .invalidResponse: return nil case .unableToRoute: return "Make sure it is possible to travel between the locations with the mode of transportation implied by the profileIdentifier option. For example, it is impossible to travel by car from one continent to another without either a land bridge or a ferry connection." @@ -171,6 +186,8 @@ extension DirectionsError: Equatable { (.profileNotFound, .profileNotFound), (.requestTooLarge, .requestTooLarge): return true + case let (.noConnection(underlying: lhsError), .noConnection(underlying: rhsError)): + return lhsError == rhsError case let (.invalidResponse(lhsResponse), .invalidResponse(rhsResponse)): return lhsResponse == rhsResponse case let (.invalidInput(lhsMessage), .invalidInput(rhsMessage)): @@ -187,17 +204,7 @@ extension DirectionsError: Equatable { && lhsUnderlying?.localizedDescription == rhsUnderlying?.localizedDescription && lhsCode == rhsCode && lhsMessage == rhsMessage - case (.noData, _), - (.invalidResponse, _), - (.unableToRoute, _), - (.noMatches, _), - (.tooManyCoordinates, _), - (.unableToLocate, _), - (.profileNotFound, _), - (.requestTooLarge, _), - (.invalidInput, _), - (.rateLimited, _), - (.unknown, _): + default: return false } } diff --git a/Tests/MapboxDirectionsTests/DirectionsTests.swift b/Tests/MapboxDirectionsTests/DirectionsTests.swift index 2d23b6c33..b75bbdd0b 100644 --- a/Tests/MapboxDirectionsTests/DirectionsTests.swift +++ b/Tests/MapboxDirectionsTests/DirectionsTests.swift @@ -119,7 +119,7 @@ class DirectionsTests: XCTestCase { XCTAssertNotNil(error, "No error returned") switch error { case .invalidResponse?: - break // pass + break // pass default: XCTFail("Wrong type of error.") } @@ -141,5 +141,32 @@ class DirectionsTests: XCTestCase { XCTFail("Code 429 should be interpreted as a rate limiting error.") } } + + func testDownNetwork() { + let notConnected = NSError(domain: NSURLErrorDomain, code: URLError.notConnectedToInternet.rawValue) as! URLError + + OHHTTPStubs.stubRequests(passingTest: { (request) -> Bool in + return request.url!.absoluteString.contains("https://api.mapbox.com/directions") + }) { (_) -> OHHTTPStubsResponse in + return OHHTTPStubsResponse(error: notConnected) + } + + let expectation = self.expectation(description: "Async callback") + let one = CLLocation(coordinate: CLLocationCoordinate2D(latitude: 0.0, longitude: 0.0)) + let two = CLLocation(coordinate: CLLocationCoordinate2D(latitude: 2.0, longitude: 2.0)) + + let directions = Directions(credentials: BogusCredentials) + let opts = RouteOptions(locations: [one, two]) + directions.calculate(opts, completionHandler: { (response, error) in + expectation.fulfill() + XCTAssertNil(response.routes, "Unexpected route response") + if case let .noConnection(urlError) = error { + XCTAssertEqual(urlError, notConnected) + } else { + XCTFail("correct error not found") + } + }) + wait(for: [expectation], timeout: 2.0) + } } #endif diff --git a/Tests/MapboxDirectionsTests/MatchTests.swift b/Tests/MapboxDirectionsTests/MatchTests.swift index da6bc6f30..c04f8e439 100644 --- a/Tests/MapboxDirectionsTests/MatchTests.swift +++ b/Tests/MapboxDirectionsTests/MatchTests.swift @@ -49,7 +49,6 @@ class MatchTests: XCTestCase { XCTAssertEqual(task.state, .completed) } - #warning("need tests for route response and match response") let match = response.matches!.first! let opts = response.options XCTAssert(matchOptions == opts) @@ -59,10 +58,10 @@ class MatchTests: XCTestCase { XCTAssertEqual(match.shape!.coordinates.count, 18) XCTAssertEqual(match.routeIdentifier, nil) -// let tracePoints = match.tracepoints -// XCTAssertNotNil(tracePoints) -// XCTAssertEqual(tracePoints.first!!.countOfAlternatives, 0) -// XCTAssertEqual(tracePoints.last!!.name, "West G Street") + let tracePoints = response.tracepoints + XCTAssertNotNil(tracePoints) + XCTAssertEqual(tracePoints!.first!!.countOfAlternatives, 0) + XCTAssertEqual(tracePoints!.last!!.name, "West G Street") // confirming actual decoded values is important because the Directions API // uses an atypical precision level for polyline encoding From 1d2337198e37f8ce5e3bfabc006653eac14d2e89 Mon Sep 17 00:00:00 2001 From: Jerrad Thramer Date: Mon, 17 Feb 2020 12:50:58 -0700 Subject: [PATCH 16/24] Fixing final remaining issues --- Sources/MapboxDirections/Directions.swift | 7 ++++++- .../DirectionsCredentials.swift | 18 ++++++++++++++++++ .../MapboxDirections/DirectionsResult.swift | 17 ++++++++--------- .../RoutableMatchTests.swift | 3 --- 4 files changed, 32 insertions(+), 13 deletions(-) diff --git a/Sources/MapboxDirections/Directions.swift b/Sources/MapboxDirections/Directions.swift index 515838e9e..528a9556c 100644 --- a/Sources/MapboxDirections/Directions.swift +++ b/Sources/MapboxDirections/Directions.swift @@ -83,7 +83,12 @@ open class Directions: NSObject { To use this object, a Mapbox [access token](https://docs.mapbox.com/help/glossary/access-token/) should be specified in the `MGLMapboxAccessToken` key in the main application bundle’s Info.plist. */ public static let shared = Directions() - + + /** + The Authorization & Authentication credentials that are used for this service. + + If nothing is provided, the default behavior is to read credential values from the developer's Info.plist. + */ public let credentials: DirectionsCredentials /** diff --git a/Sources/MapboxDirections/DirectionsCredentials.swift b/Sources/MapboxDirections/DirectionsCredentials.swift index 651e3d4ba..ccdba001d 100644 --- a/Sources/MapboxDirections/DirectionsCredentials.swift +++ b/Sources/MapboxDirections/DirectionsCredentials.swift @@ -5,14 +5,32 @@ let defaultAccessToken = Bundle.main.object(forInfoDictionaryKey: "MGLMapboxAcce let defaultApiEndPointURLString = Bundle.main.object(forInfoDictionaryKey: "MGLMapboxAPIBaseURL") as? String public struct DirectionsCredentials: Equatable { + + /** + The mapbox access token. You can find this in your Mapbox account dashboard. + */ public let accessToken: String? + + /** + The host to reach. defaults to `api.mapbox.com`. + */ public let host: URL + + /** + The SKU Token associated with the request. Used for billing. + */ public var skuToken: String? { guard let mbx: AnyClass = NSClassFromString("MBXAccounts") else { return nil } guard mbx.responds(to: Selector(("serviceSkuToken"))) else { return nil } return mbx.value(forKeyPath: "serviceSkuToken") as? String } + /** + Intialize a new credential. + + - parameter accessToken: Optional. An access token to provide. If this value is nil, the SDK will attempt to find a token from your app's `info.plist`. + - parameter host: Optional. A parameter to pass a custom host. If `nil` is provided, the SDK will attempt to find a host from your app's `info.plist`, and barring that will default to `https://api.mapbox.com`. + */ public init(accessToken: String? = nil, host: URL? = nil) { self.accessToken = accessToken ?? defaultAccessToken diff --git a/Sources/MapboxDirections/DirectionsResult.swift b/Sources/MapboxDirections/DirectionsResult.swift index 9950c2634..db79aedff 100644 --- a/Sources/MapboxDirections/DirectionsResult.swift +++ b/Sources/MapboxDirections/DirectionsResult.swift @@ -175,12 +175,11 @@ extension DirectionsResult: CustomStringConvertible { return legs.map { $0.name }.joined(separator: " – ") } } - -//extension DirectionsResult: CustomQuickLookConvertible { -// func debugQuickLookObject() -> Any? { -// guard let shape = shape else { -// return nil -// } -// return debugQuickLookURL(illustrating: shape, profileIdentifier: directionsOptions.profileIdentifier) -// } -//} +extension DirectionsResult: CustomQuickLookConvertible { + func debugQuickLookObject() -> Any? { + guard let shape = shape else { + return nil + } + return debugQuickLookURL(illustrating: shape, profileIdentifier: .automobile) + } +} diff --git a/Tests/MapboxDirectionsTests/RoutableMatchTests.swift b/Tests/MapboxDirectionsTests/RoutableMatchTests.swift index 1bd4b2c40..a9f61eb47 100644 --- a/Tests/MapboxDirectionsTests/RoutableMatchTests.swift +++ b/Tests/MapboxDirectionsTests/RoutableMatchTests.swift @@ -53,9 +53,6 @@ class RoutableMatchTest: XCTestCase { XCTAssertNotNil(route) XCTAssertNotNil(route.shape) XCTAssertEqual(route.shape!.coordinates.count, 18) - #warning("Add a test for DirectionsCredentials") -// XCTAssertEqual(route.accessToken, BogusToken) -// XCTAssertEqual(route.apiEndpoint, URL(string: "https://api.mapbox.com")) XCTAssertEqual(route.routeIdentifier, nil) let waypoints = routeResponse.waypoints! From 63b194193125ae9d75e648d46ee4fee2f28f65fe Mon Sep 17 00:00:00 2001 From: Jerrad Thramer Date: Mon, 17 Feb 2020 12:54:34 -0700 Subject: [PATCH 17/24] Just kidding, found some more. I think we're now ready for review. --- Tests/MapboxDirectionsTests/MatchTests.swift | 7 +++---- Tests/MapboxDirectionsTests/OfflineDirectionsTests.swift | 1 - 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/Tests/MapboxDirectionsTests/MatchTests.swift b/Tests/MapboxDirectionsTests/MatchTests.swift index c04f8e439..c97fdd04e 100644 --- a/Tests/MapboxDirectionsTests/MatchTests.swift +++ b/Tests/MapboxDirectionsTests/MatchTests.swift @@ -134,9 +134,9 @@ class MatchTests: XCTestCase { let match = response.matches!.first! XCTAssertNotNil(match) -// let tracepoints = match.tracepoints -// XCTAssertEqual(tracepoints.count, 7) -// XCTAssertEqual(tracepoints.first!, nil) + let tracepoints = response.tracepoints! + XCTAssertEqual(tracepoints.count, 7) + XCTAssertEqual(tracepoints.first!, nil) // Encode and decode the match securely. // This may raise an Objective-C exception if an error is encountered which will fail the tests. @@ -149,7 +149,6 @@ class MatchTests: XCTestCase { let unarchivedMatch = try! decoder.decode(Match.self, from: encodedString.data(using: .utf8)!) XCTAssertEqual(match.confidence, unarchivedMatch.confidence) -// XCTAssertEqual(match.tracepoints, unarchivedMatch.tracepoints) } #endif diff --git a/Tests/MapboxDirectionsTests/OfflineDirectionsTests.swift b/Tests/MapboxDirectionsTests/OfflineDirectionsTests.swift index fad7c6e64..3fd6403ad 100644 --- a/Tests/MapboxDirectionsTests/OfflineDirectionsTests.swift +++ b/Tests/MapboxDirectionsTests/OfflineDirectionsTests.swift @@ -12,7 +12,6 @@ class OfflineDirectionsTests: XCTestCase { let credentials = DirectionsCredentials(accessToken: token, host: hostURL) let directions = Directions(credentials: credentials) -// XCTAssertEqual(directions.accessToken, token) let versionsExpectation = expectation(description: "Fetching available versions should return results") From 65e5e22d1e486462647b11991fa75db8aaae6475 Mon Sep 17 00:00:00 2001 From: Jerrad Thramer Date: Mon, 17 Feb 2020 12:56:34 -0700 Subject: [PATCH 18/24] typo --- Sources/MapboxDirections/Directions.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/MapboxDirections/Directions.swift b/Sources/MapboxDirections/Directions.swift index 528a9556c..d1b745141 100644 --- a/Sources/MapboxDirections/Directions.swift +++ b/Sources/MapboxDirections/Directions.swift @@ -69,7 +69,7 @@ open class Directions: NSObject { /** A closure (block) to be called when a map matching request is complete. - - parameter response: A `MapMatching` object that contains the entire payload of the Directions Map Matching API solution. See `MapMatchingResponse.swift` for more information. + - parameter response: A `MapMatchingResponse` object that contains the entire payload of the Directions Map Matching API solution. See `MapMatchingResponse.swift` for more information. - parameter error: The error that occurred, or `nil` if the solution was obtained successfully. */ From 750be2442b11c4b61b69b0788f4cd4f8b54f35da Mon Sep 17 00:00:00 2001 From: Jerrad Thramer Date: Mon, 17 Feb 2020 14:06:07 -0700 Subject: [PATCH 19/24] Implementing error case that wraps any kind of URLError-based network error. --- Sources/MapboxDirections/Directions.swift | 15 ++++++--------- Sources/MapboxDirections/DirectionsError.swift | 8 ++++---- Tests/MapboxDirectionsTests/DirectionsTests.swift | 2 +- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/Sources/MapboxDirections/Directions.swift b/Sources/MapboxDirections/Directions.swift index d1b745141..506bff28a 100644 --- a/Sources/MapboxDirections/Directions.swift +++ b/Sources/MapboxDirections/Directions.swift @@ -120,11 +120,10 @@ open class Directions: NSObject { let request = urlRequest(forCalculating: options) let requestTask = URLSession.shared.dataTask(with: request) { (possibleData, possibleResponse, possibleError) in - let offlineErrors: [URLError.Code] = [.cannotConnectToHost, .dataNotAllowed, .notConnectedToInternet] - if let urlError = possibleError as? URLError, offlineErrors.contains(urlError.code) { + if let urlError = possibleError as? URLError { let response = RouteResponse(httpResponse: possibleResponse as? HTTPURLResponse, options: .route(options), credentials: self.credentials) DispatchQueue.main.async { - completionHandler(response, .noConnection(underlying: urlError)) + completionHandler(response, .network(urlError)) } return } @@ -222,11 +221,10 @@ open class Directions: NSObject { let request = urlRequest(forCalculating: options) let requestTask = URLSession.shared.dataTask(with: request) { (possibleData, possibleResponse, possibleError) in - let offlineErrors: [URLError.Code] = [.cannotConnectToHost, .dataNotAllowed, .notConnectedToInternet] - if let urlError = possibleError as? URLError, offlineErrors.contains(urlError.code) { + if let urlError = possibleError as? URLError { let response = MapMatchingResponse(httpResponse: possibleResponse as? HTTPURLResponse, options: options, credentials: self.credentials) DispatchQueue.main.async { - completionHandler(response, .noConnection(underlying: urlError)) + completionHandler(response, .network(urlError)) } return } @@ -325,11 +323,10 @@ open class Directions: NSObject { let request = urlRequest(forCalculating: options) let requestTask = URLSession.shared.dataTask(with: request) { (possibleData, possibleResponse, possibleError) in - let offlineErrors: [URLError.Code] = [.cannotConnectToHost, .dataNotAllowed, .notConnectedToInternet] - if let urlError = possibleError as? URLError, offlineErrors.contains(urlError.code) { + if let urlError = possibleError as? URLError { let response = RouteResponse(httpResponse: possibleResponse as? HTTPURLResponse, options: .match(options), credentials: self.credentials) DispatchQueue.main.async { - completionHandler(response, .noConnection(underlying: urlError)) + completionHandler(response, .network(urlError)) } return } diff --git a/Sources/MapboxDirections/DirectionsError.swift b/Sources/MapboxDirections/DirectionsError.swift index 0f619fb3c..05fadd1ed 100644 --- a/Sources/MapboxDirections/DirectionsError.swift +++ b/Sources/MapboxDirections/DirectionsError.swift @@ -36,7 +36,7 @@ public enum DirectionsError: LocalizedError { /** There is no network connection available to perform the network request. */ - case noConnection(underlying: URLError?) + case network(_: URLError?) /** The server returned an empty response. @@ -111,7 +111,7 @@ public enum DirectionsError: LocalizedError { public var failureReason: String? { switch self { - case .noConnection(_): + case .network(_): return "The client does not have a network connection to the server." case .noData: return "The server returned an empty response." @@ -149,7 +149,7 @@ public enum DirectionsError: LocalizedError { public var recoverySuggestion: String? { switch self { - case .noConnection(underlying: _), .noData, .invalidInput, .invalidResponse: + case .network(underlying: _), .noData, .invalidInput, .invalidResponse: return nil case .unableToRoute: return "Make sure it is possible to travel between the locations with the mode of transportation implied by the profileIdentifier option. For example, it is impossible to travel by car from one continent to another without either a land bridge or a ferry connection." @@ -186,7 +186,7 @@ extension DirectionsError: Equatable { (.profileNotFound, .profileNotFound), (.requestTooLarge, .requestTooLarge): return true - case let (.noConnection(underlying: lhsError), .noConnection(underlying: rhsError)): + case let (.network(underlying: lhsError), .network(underlying: rhsError)): return lhsError == rhsError case let (.invalidResponse(lhsResponse), .invalidResponse(rhsResponse)): return lhsResponse == rhsResponse diff --git a/Tests/MapboxDirectionsTests/DirectionsTests.swift b/Tests/MapboxDirectionsTests/DirectionsTests.swift index b75bbdd0b..8cc4d3d9f 100644 --- a/Tests/MapboxDirectionsTests/DirectionsTests.swift +++ b/Tests/MapboxDirectionsTests/DirectionsTests.swift @@ -160,7 +160,7 @@ class DirectionsTests: XCTestCase { directions.calculate(opts, completionHandler: { (response, error) in expectation.fulfill() XCTAssertNil(response.routes, "Unexpected route response") - if case let .noConnection(urlError) = error { + if case let .network(urlError) = error { XCTAssertEqual(urlError, notConnected) } else { XCTFail("correct error not found") From 72a85fc9491b6b6bd94f3ff88b5d165fbbec27eb Mon Sep 17 00:00:00 2001 From: Jerrad Thramer Date: Mon, 17 Feb 2020 14:49:10 -0700 Subject: [PATCH 20/24] Fixing broken test case --- Sources/MapboxDirections/DirectionsError.swift | 4 ++-- Tests/MapboxDirectionsTests/DirectionsTests.swift | 8 +++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Sources/MapboxDirections/DirectionsError.swift b/Sources/MapboxDirections/DirectionsError.swift index 05fadd1ed..47a340c56 100644 --- a/Sources/MapboxDirections/DirectionsError.swift +++ b/Sources/MapboxDirections/DirectionsError.swift @@ -149,7 +149,7 @@ public enum DirectionsError: LocalizedError { public var recoverySuggestion: String? { switch self { - case .network(underlying: _), .noData, .invalidInput, .invalidResponse: + case .network(_), .noData, .invalidInput, .invalidResponse: return nil case .unableToRoute: return "Make sure it is possible to travel between the locations with the mode of transportation implied by the profileIdentifier option. For example, it is impossible to travel by car from one continent to another without either a land bridge or a ferry connection." @@ -186,7 +186,7 @@ extension DirectionsError: Equatable { (.profileNotFound, .profileNotFound), (.requestTooLarge, .requestTooLarge): return true - case let (.network(underlying: lhsError), .network(underlying: rhsError)): + case let (.network(lhsError), .network(rhsError)): return lhsError == rhsError case let (.invalidResponse(lhsResponse), .invalidResponse(rhsResponse)): return lhsResponse == rhsResponse diff --git a/Tests/MapboxDirectionsTests/DirectionsTests.swift b/Tests/MapboxDirectionsTests/DirectionsTests.swift index 8cc4d3d9f..2239facfb 100644 --- a/Tests/MapboxDirectionsTests/DirectionsTests.swift +++ b/Tests/MapboxDirectionsTests/DirectionsTests.swift @@ -160,7 +160,13 @@ class DirectionsTests: XCTestCase { directions.calculate(opts, completionHandler: { (response, error) in expectation.fulfill() XCTAssertNil(response.routes, "Unexpected route response") - if case let .network(urlError) = error { + + guard let error = error else { + XCTFail("Missing 'no connection' error.") + return + } + + if case DirectionsError.network(let urlError) = error { XCTAssertEqual(urlError, notConnected) } else { XCTFail("correct error not found") From 2243678017f205ade93299e07d1eaab9e79946dd Mon Sep 17 00:00:00 2001 From: Jerrad Thramer Date: Mon, 17 Feb 2020 18:40:11 -0700 Subject: [PATCH 21/24] Implementing suggestion from @SebastianOsinski to remove dead code paths. Thanks Sebastian! --- Sources/MapboxDirections/Directions.swift | 26 ----------------------- 1 file changed, 26 deletions(-) diff --git a/Sources/MapboxDirections/Directions.swift b/Sources/MapboxDirections/Directions.swift index 506bff28a..5f6decba1 100644 --- a/Sources/MapboxDirections/Directions.swift +++ b/Sources/MapboxDirections/Directions.swift @@ -144,15 +144,6 @@ open class Directions: NSObject { return } - if let error = possibleError { - let response = RouteResponse(httpResponse: httpResponse, options: .route(options), credentials: self.credentials) - let unknownError = DirectionsError.unknown(response: possibleResponse, underlying: error, code: nil, message: nil) - DispatchQueue.main.async { - completionHandler(response, unknownError) - } - return - } - DispatchQueue.global(qos: .userInitiated).async { do { let decoder = JSONDecoder() @@ -246,14 +237,6 @@ open class Directions: NSObject { return } - if let error = possibleError { - let unknownError = DirectionsError.unknown(response: possibleResponse, underlying: error, code: nil, message: nil) - let response = MapMatchingResponse(httpResponse: httpResponse, options: options, credentials: self.credentials) - DispatchQueue.main.async { - completionHandler(response, unknownError) - } - return - } DispatchQueue.global(qos: .userInitiated).async { do { @@ -347,15 +330,6 @@ open class Directions: NSObject { return } - if let error = possibleError { - let response = RouteResponse(httpResponse: httpResponse, options: .match(options), credentials: self.credentials) - let unknownError = DirectionsError.unknown(response: possibleResponse, underlying: error, code: nil, message: nil) - DispatchQueue.main.async { - completionHandler(response, unknownError) - } - return - } - DispatchQueue.global(qos: .userInitiated).async { do { let decoder = JSONDecoder() From 7c3644f0454d5fca79d026c87d22afdde39c6033 Mon Sep 17 00:00:00 2001 From: Jerrad Thramer Date: Tue, 18 Feb 2020 14:14:04 -0700 Subject: [PATCH 22/24] Removing extraneous optional. --- Sources/MapboxDirections/DirectionsError.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/MapboxDirections/DirectionsError.swift b/Sources/MapboxDirections/DirectionsError.swift index 47a340c56..8c8147e67 100644 --- a/Sources/MapboxDirections/DirectionsError.swift +++ b/Sources/MapboxDirections/DirectionsError.swift @@ -36,7 +36,7 @@ public enum DirectionsError: LocalizedError { /** There is no network connection available to perform the network request. */ - case network(_: URLError?) + case network(_: URLError) /** The server returned an empty response. From 5065b92d62cc2d00f9f50e3e6082310fae85fc69 Mon Sep 17 00:00:00 2001 From: Jerrad Thramer Date: Fri, 3 Apr 2020 12:09:34 -0600 Subject: [PATCH 23/24] Adding documentation for `Weight` --- Sources/MapboxDirections/MapMatching/Match.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/MapboxDirections/MapMatching/Match.swift b/Sources/MapboxDirections/MapMatching/Match.swift index c32073526..668cee287 100644 --- a/Sources/MapboxDirections/MapMatching/Match.swift +++ b/Sources/MapboxDirections/MapMatching/Match.swift @@ -3,7 +3,11 @@ import CoreLocation import Polyline import struct Turf.LineString +/** + A `Weight` enum represents the weight given to a specific match by the Directions API. The default metric is a compound index called "routability", which is duration-based with additional penalties for less desirable maneuvers. + */ public enum Weight: Equatable { + case routability(value: Float) case other(value: Float, metric: String) From a6607fbcd788a0575b752d3a0e9bee74e9d8ddcd Mon Sep 17 00:00:00 2001 From: Jerrad Thramer Date: Fri, 3 Apr 2020 12:44:07 -0600 Subject: [PATCH 24/24] Adding simple test for DirectionsCredentials --- MapboxDirections.xcodeproj/project.pbxproj | 8 ++++++++ Tests/MapboxDirectionsTests/CredentialsTests.swift | 13 +++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 Tests/MapboxDirectionsTests/CredentialsTests.swift diff --git a/MapboxDirections.xcodeproj/project.pbxproj b/MapboxDirections.xcodeproj/project.pbxproj index fef223b3d..462852948 100644 --- a/MapboxDirections.xcodeproj/project.pbxproj +++ b/MapboxDirections.xcodeproj/project.pbxproj @@ -77,6 +77,9 @@ 43D6617123DBA58E0062BFFE /* (null) in Sources */ = {isa = PBXBuildFile; }; 43D6617223DBA58E0062BFFE /* (null) in Sources */ = {isa = PBXBuildFile; }; 43D6617323DBA58E0062BFFE /* (null) in Sources */ = {isa = PBXBuildFile; }; + 43D992FD2437B93E008A2D74 /* CredentialsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43D992FB2437B8D2008A2D74 /* CredentialsTests.swift */; }; + 43D992FE2437B93F008A2D74 /* CredentialsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43D992FB2437B8D2008A2D74 /* CredentialsTests.swift */; }; + 43D992FF2437B940008A2D74 /* CredentialsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43D992FB2437B8D2008A2D74 /* CredentialsTests.swift */; }; 43EBD3AD23DBC06800B09D05 /* DirectionsCredentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43EBD3AC23DBC06800B09D05 /* DirectionsCredentials.swift */; }; 43EBD3AE23DBC06800B09D05 /* DirectionsCredentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43EBD3AC23DBC06800B09D05 /* DirectionsCredentials.swift */; }; 43EBD3AF23DBC06800B09D05 /* DirectionsCredentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43EBD3AC23DBC06800B09D05 /* DirectionsCredentials.swift */; }; @@ -365,6 +368,7 @@ 438BFEC0233D805500457294 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 438BFEC1233D854D00457294 /* DirectionsProfileIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectionsProfileIdentifier.swift; sourceTree = ""; }; 4392557523440EC2006EEE88 /* DirectionsError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectionsError.swift; sourceTree = ""; }; + 43D992FB2437B8D2008A2D74 /* CredentialsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CredentialsTests.swift; sourceTree = ""; }; 43EBD3AC23DBC06800B09D05 /* DirectionsCredentials.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DirectionsCredentials.swift; sourceTree = ""; }; 43F89F922350F952007B591E /* MapMatchingResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapMatchingResponse.swift; sourceTree = ""; }; 8D381B601FD9F5B1008D5A58 /* noDestinationName.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = noDestinationName.json; sourceTree = ""; }; @@ -663,6 +667,7 @@ children = ( DA6C9DAD1CAEC93800094FBC /* Fixtures */, C5247D701E818A24004B6154 /* AnnotationTests.swift */, + 43D992FB2437B8D2008A2D74 /* CredentialsTests.swift */, DAD06E34239F0B19001A917D /* DirectionsErrorTests.swift */, DA1A110A1D01045E009F82FA /* DirectionsTests.swift */, 43538E3C23ED6A2000E010D4 /* DirectionsCredentialsTests.swift */, @@ -1243,6 +1248,7 @@ files = ( DAE9E0F51EB7DE2E001E8E8B /* RouteOptionsTests.swift in Sources */, DAE2DF6D23AED2280065057A /* RouteTests.swift in Sources */, + 43D992FE2437B93F008A2D74 /* CredentialsTests.swift in Sources */, 35DBF015217E199E0009D2AE /* OfflineDirectionsTests.swift in Sources */, C53A02291E92C27A009837BD /* AnnotationTests.swift in Sources */, 4376A52823FB13D400C6038D /* MatchOptionsTests.swift in Sources */, @@ -1320,6 +1326,7 @@ files = ( DAE9E0F61EB7DE2E001E8E8B /* RouteOptionsTests.swift in Sources */, DAE2DF6E23AED2280065057A /* RouteTests.swift in Sources */, + 43D992FF2437B940008A2D74 /* CredentialsTests.swift in Sources */, 35DBF016217E199E0009D2AE /* OfflineDirectionsTests.swift in Sources */, C53A022A1E92C27B009837BD /* AnnotationTests.swift in Sources */, 4376A52923FB13D400C6038D /* MatchOptionsTests.swift in Sources */, @@ -1444,6 +1451,7 @@ files = ( DAE9E0F41EB7DE2E001E8E8B /* RouteOptionsTests.swift in Sources */, DAE2DF6C23AED2280065057A /* RouteTests.swift in Sources */, + 43D992FD2437B93E008A2D74 /* CredentialsTests.swift in Sources */, 35DBF014217E199E0009D2AE /* OfflineDirectionsTests.swift in Sources */, C5247D711E818A24004B6154 /* AnnotationTests.swift in Sources */, 4376A52723FB13D400C6038D /* MatchOptionsTests.swift in Sources */, diff --git a/Tests/MapboxDirectionsTests/CredentialsTests.swift b/Tests/MapboxDirectionsTests/CredentialsTests.swift new file mode 100644 index 000000000..d38ffd5e3 --- /dev/null +++ b/Tests/MapboxDirectionsTests/CredentialsTests.swift @@ -0,0 +1,13 @@ +import XCTest +@testable import MapboxDirections + +class CredentialsTests: XCTestCase { + + func testCredentialsCreation() { + let testURL = URL(string: "https://example.com")! + let subject = DirectionsCredentials(accessToken: "test", host: testURL) + + XCTAssertEqual(subject.accessToken, "test") + XCTAssertEqual(subject.host, testURL) + } +}