Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Public Response Types #406

Merged
merged 25 commits into from
Apr 4, 2020
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
7db34bc
WIP: Surfacing `MapMatchingResponse` and `RouteResponse` as public ty…
Jan 17, 2020
6edcb84
Adding error-case initializer for RouteResponse/MapMatchingResponse
Jan 17, 2020
77f075b
WIP: Removing options object from models, encapsulating authenticatio…
Jan 25, 2020
ee31e7f
WIP: Integrating credentials.
Jan 27, 2020
30b19dc
WIP: cutting down on errors
Jan 30, 2020
2566411
WIP: Fixed all compiler errors, Response model is now coherent, need …
Feb 7, 2020
48abd96
Fixing more tests, three to go.
Feb 10, 2020
d98e073
Fixing remaining tests.
Feb 13, 2020
efb5f2e
Fixing issue where SPM would fail compilation.
Feb 13, 2020
6448a27
Applying same SPM fix to MapMatchingResponse
Feb 13, 2020
288d8af
removing redundant logic
Feb 13, 2020
4e59896
Fixing issue where model mismatch results in a silent fail by propaga…
Feb 13, 2020
8c7b29d
Adding documentation, and test change.
Feb 17, 2020
9a5b4ad
Merge branch 'master' into jerrad/directions-calculate-interface
Feb 17, 2020
49ad7d9
Adding test coverage
Feb 17, 2020
a024d57
Adding offline error case, polish to remaining tests and adding a bit…
Feb 17, 2020
1d23371
Fixing final remaining issues
Feb 17, 2020
63b1941
Just kidding, found some more. I think we're now ready for review.
Feb 17, 2020
65e5e22
typo
Feb 17, 2020
750be24
Implementing error case that wraps any kind of URLError-based network…
Feb 17, 2020
72a85fc
Fixing broken test case
Feb 17, 2020
2243678
Implementing suggestion from @SebastianOsinski to remove dead code pa…
Feb 18, 2020
7c3644f
Removing extraneous optional.
Feb 18, 2020
5065b92
Adding documentation for `Weight`
Apr 3, 2020
a6607fb
Adding simple test for DirectionsCredentials
Apr 3, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions Directions Example/ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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) —")
Expand Down Expand Up @@ -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. ⚠️
Expand All @@ -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)
Expand Down
74 changes: 54 additions & 20 deletions MapboxDirections.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

342 changes: 149 additions & 193 deletions Sources/MapboxDirections/Directions.swift

Large diffs are not rendered by default.

48 changes: 48 additions & 0 deletions Sources/MapboxDirections/DirectionsCredentials.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
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: 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

precondition(accessToken != nil && !accessToken!.isEmpty, "A Mapbox access token is required. Go to <https://account.mapbox.com/access-tokens/>. 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, let defaultHost = URL(string: defaultHostString) {
self.host = defaultHost
} else {
self.host = URL(string: "https://api.mapbox.com")!
}
}
}

73 changes: 58 additions & 15 deletions Sources/MapboxDirections/DirectionsError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,54 @@ import Foundation
An error that occurs when calculating directions.
*/
public enum DirectionsError: LocalizedError {

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)
}
} else {
self = .unknown(response: response, underlying: error, code: code, message: message)
}
}

/**
There is no network connection available to perform the network request.
*/
case network(_: URLError)

/**
The server returned an empty response.
*/
case noData

/**
The API recieved input that it didn't understand.
*/
case invalidInput(message: String?)

/**
The server returned a response that isn’t correctly formatted.
*/
case invalidResponse
case invalidResponse(_: URLResponse?)

/**
No route could be found between the specified locations.
Expand Down Expand Up @@ -65,15 +102,22 @@ 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 .network(_):
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):
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."
Expand Down Expand Up @@ -105,7 +149,7 @@ public enum DirectionsError: LocalizedError {

public var recoverySuggestion: String? {
switch self {
case .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."
Expand Down Expand Up @@ -135,14 +179,17 @@ 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),
(.unableToLocate, .unableToLocate),
(.profileNotFound, .profileNotFound),
(.requestTooLarge, .requestTooLarge):
return true
case let (.network(lhsError), .network(rhsError)):
return lhsError == rhsError
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),
Expand All @@ -157,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
}
}
Expand All @@ -181,4 +218,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
}
13 changes: 13 additions & 0 deletions Sources/MapboxDirections/DirectionsOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ open class DirectionsOptions: Codable {
self.profileIdentifier = profileIdentifier ?? .automobile
}


private enum CodingKeys: String, CodingKey {
case waypoints
case profileIdentifier
Expand Down Expand Up @@ -281,6 +282,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

/**
Expand Down Expand Up @@ -418,6 +430,7 @@ open class DirectionsOptions: Codable {
]
return components.percentEncodedQuery ?? ""
}

}

extension DirectionsOptions: Equatable {
Expand Down
Loading