Skip to content

Commit

Permalink
Route refreshing TTL (#827)
Browse files Browse the repository at this point in the history
* vk/NAVIOS-1099: added route response refresh TTL field and a DirectionsError to signal refreshing has expired; Unit tests added; accepted breaking changes
  • Loading branch information
Udumft committed Jun 13, 2024
1 parent c277fff commit 9a23bdc
Show file tree
Hide file tree
Showing 7 changed files with 81 additions and 8 deletions.
6 changes: 5 additions & 1 deletion Sources/MapboxDirections/Directions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -454,7 +454,11 @@ open class Directions: NSObject {
}

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 apiError = DirectionsError(code: disposition.code,
message: disposition.message,
response: response,
underlyingError: possibleError,
refreshTTL: disposition.refreshTTL)
DispatchQueue.main.async {
completionHandler(self.credentials, .failure(apiError))
}
Expand Down
20 changes: 18 additions & 2 deletions Sources/MapboxDirections/DirectionsError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ import FoundationNetworking
*/
public enum DirectionsError: LocalizedError {

public init(code: String?, message: String?, response: URLResponse?, underlyingError error: Error?) {
public init(code: String?, message: String?, response: URLResponse?, underlyingError error: Error?, refreshTTL: Int? = nil) {
guard refreshTTL == nil || refreshTTL != 0 else {
self = .refreshExpired
return
}
if let response = response as? HTTPURLResponse {
switch (response.statusCode, code ?? "") {
case (200, "NoRoute"):
Expand Down Expand Up @@ -105,6 +109,13 @@ public enum DirectionsError: LocalizedError {
*/
case rateLimited(rateLimitInterval: TimeInterval?, rateLimit: UInt?, resetTime: Date?)

/**
The current route may no longer be refreshed.
Route's refresh TTL has expired and it is not possible to refresh it anymore. Try requesting a new one instead.
*/
case refreshExpired

/**
Unknown error case. Look at associated values for more details.
*/
Expand Down Expand Up @@ -146,6 +157,8 @@ public enum DirectionsError: LocalizedError {
#endif
let formattedCount = NumberFormatter.localizedString(from: NSNumber(value: limit), number: .decimal)
return "More than \(formattedCount) requests have been made with this access token within a period of \(formattedInterval)."
case .refreshExpired:
return "Current routes can no longer be refreshed due to their TTL expiration."
case let .unknown(_, underlying: error, _, message):
return message
?? (error as NSError?)?.userInfo[NSLocalizedFailureReasonErrorKey] as? String
Expand Down Expand Up @@ -175,6 +188,8 @@ public enum DirectionsError: LocalizedError {
}
let formattedDate: String = DateFormatter.localizedString(from: rolloverTime, dateStyle: .long, timeStyle: .long)
return "Wait until \(formattedDate) before retrying."
case .refreshExpired:
return "Try making a new route request."
case let .unknown(_, underlying: error, _, _):
return (error as NSError?)?.userInfo[NSLocalizedRecoverySuggestionErrorKey] as? String
}
Expand All @@ -190,7 +205,8 @@ extension DirectionsError: Equatable {
(.tooManyCoordinates, .tooManyCoordinates),
(.unableToLocate, .unableToLocate),
(.profileNotFound, .profileNotFound),
(.requestTooLarge, .requestTooLarge):
(.requestTooLarge, .requestTooLarge),
(.refreshExpired, .refreshExpired):
return true
case let (.network(lhsError), .network(rhsError)):
return lhsError == rhsError
Expand Down
5 changes: 3 additions & 2 deletions Sources/MapboxDirections/ResponseDisposition.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ struct ResponseDisposition: Decodable {
var code: String?
var message: String?
var error: String?
var refreshTTL: Int?

private enum CodingKeys: CodingKey {
case code, message, error
private enum CodingKeys: String, CodingKey {
case code, message, error, refreshTTL = "refresh_ttl"
}
}
23 changes: 22 additions & 1 deletion Sources/MapboxDirections/RouteResponse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,22 @@ public struct RouteResponse: ForeignMemberContainer {
*/
public var created: Date = Date()

/*
A time period during which the routes from this ``RouteResponse`` are eligable for refreshing.
`nil` value indicates that route refreshing is not available for related routes.
*/
public let refreshTTL: TimeInterval?

/*
A deadline after which the routes from this ``RouteResponse`` are eligable for refreshing.
`nil` value indicates that route refreshing is not available for related routes.
*/
public var refreshInvalidationDate: Date? {
refreshTTL.map { created.addingTimeInterval($0) }
}

/**
Managed array of `RoadClasses` restrictions specified to `RouteOptions.roadClassesToAvoid` which were violated during route calculation.
Expand All @@ -78,15 +94,17 @@ extension RouteResponse: Codable {
case identifier = "uuid"
case routes
case waypoints
case refreshTTL = "refresh_ttl"
}

public init(httpResponse: HTTPURLResponse?, identifier: String? = nil, routes: [Route]? = nil, waypoints: [Waypoint]? = nil, options: ResponseOptions, credentials: Credentials) {
public init(httpResponse: HTTPURLResponse?, identifier: String? = nil, routes: [Route]? = nil, waypoints: [Waypoint]? = nil, options: ResponseOptions, credentials: Credentials, refreshTTL: TimeInterval? = nil) {
self.httpResponse = httpResponse
self.identifier = identifier
self.options = options
self.routes = routes
self.waypoints = waypoints
self.credentials = credentials
self.refreshTTL = refreshTTL

updateRoadClassExclusionViolations()
}
Expand Down Expand Up @@ -187,6 +205,8 @@ extension RouteResponse: Codable {
routes = nil
}

self.refreshTTL = try container.decodeIfPresent(TimeInterval.self, forKey: .refreshTTL)

updateRoadClassExclusionViolations()

try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder)
Expand All @@ -197,6 +217,7 @@ extension RouteResponse: Codable {
try container.encodeIfPresent(identifier, forKey: .identifier)
try container.encodeIfPresent(routes, forKey: .routes)
try container.encodeIfPresent(waypoints, forKey: .waypoints)
try container.encodeIfPresent(refreshTTL, forKey: .refreshTTL)

try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder)
}
Expand Down
13 changes: 13 additions & 0 deletions Tests/MapboxDirectionsTests/DirectionsErrorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class DirectionsErrorTests: XCTestCase {
XCTAssertEqual(DirectionsError.invalidInput(message: nil).failureReason, nil)
XCTAssertEqual(DirectionsError.invalidInput(message: "").failureReason, "")
XCTAssertNotNil(DirectionsError.rateLimited(rateLimitInterval: nil, rateLimit: nil, resetTime: nil).failureReason)
XCTAssertNotNil(DirectionsError.refreshExpired.failureReason)
XCTAssertNotNil(DirectionsError.unknown(response: nil, underlying: nil, code: nil, message: nil).failureReason)
}

Expand All @@ -33,6 +34,7 @@ class DirectionsErrorTests: XCTestCase {
XCTAssertNil(DirectionsError.invalidInput(message: nil).recoverySuggestion)
XCTAssertNil(DirectionsError.rateLimited(rateLimitInterval: nil, rateLimit: nil, resetTime: nil).recoverySuggestion)
XCTAssertNotNil(DirectionsError.rateLimited(rateLimitInterval: nil, rateLimit: nil, resetTime: .distantFuture).recoverySuggestion)
XCTAssertNotNil(DirectionsError.refreshExpired.recoverySuggestion)
XCTAssertNil(DirectionsError.unknown(response: nil, underlying: nil, code: nil, message: nil).recoverySuggestion)

let underlyingError = NSError(domain: "com.example", code: 02134, userInfo: [NSLocalizedRecoverySuggestionErrorKey: "Try harder"])
Expand Down Expand Up @@ -63,6 +65,7 @@ class DirectionsErrorTests: XCTestCase {
XCTAssertNotEqual(DirectionsError.rateLimited(rateLimitInterval: nil, rateLimit: nil, resetTime: nil),
.rateLimited(rateLimitInterval: nil, rateLimit: nil, resetTime: .distantPast))

XCTAssertEqual(DirectionsError.refreshExpired, .refreshExpired)
enum BogusError: Error {
case bug
}
Expand All @@ -85,6 +88,16 @@ class DirectionsErrorTests: XCTestCase {
XCTAssertNotEqual(DirectionsError.noData, .requestTooLarge)
XCTAssertNotEqual(DirectionsError.noData, .invalidInput(message: nil))
XCTAssertNotEqual(DirectionsError.noData, .rateLimited(rateLimitInterval: nil, rateLimit: nil, resetTime: nil))
XCTAssertNotEqual(DirectionsError.noData, .refreshExpired)
XCTAssertNotEqual(DirectionsError.noData, .unknown(response: nil, underlying: nil, code: nil, message: ""))
}

func testRefreshExpiredError() {
XCTAssertNotEqual(DirectionsError(code: nil, message: nil, response: nil, underlyingError: nil, refreshTTL: 5),
.refreshExpired)
XCTAssertNotEqual(DirectionsError(code: nil, message: nil, response: nil, underlyingError: nil, refreshTTL: nil),
.refreshExpired)
XCTAssertEqual(DirectionsError(code: nil, message: nil, response: nil, underlyingError: nil, refreshTTL: 0),
.refreshExpired)
}
}
18 changes: 17 additions & 1 deletion Tests/MapboxDirectionsTests/RouteResponseTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ class RouteResponseTests: XCTestCase {
let routeResponse = RouteResponse(httpResponse: nil,
waypoints: waypoints,
options: responseOptions,
credentials: BogusCredentials)
credentials: BogusCredentials,
refreshTTL: 12.34)

do {
let encodedRouteResponse = try JSONEncoder().encode(routeResponse)
Expand Down Expand Up @@ -58,6 +59,7 @@ class RouteResponseTests: XCTestCase {
XCTAssertEqual(originWaypoint.separatesLegs, false, "originWaypoint should have separatesLegs set to false.")
XCTAssertEqual(decodedSeparatesLegs, true, "First and last decoded waypoints should have separatesLegs value set to true.")
XCTAssertEqual(originWaypoint.allowsArrivingOnOppositeSide, decodedAllowsArrivingOnOppositeSide, "Original and decoded allowsArrivingOnOppositeSides should be equal.")
XCTAssertEqual(routeResponse.refreshTTL, decodedRouteResponse.refreshTTL, "Original and decoded refreshTTL should be equal.")
} catch {
XCTFail("Failed with error: \(error)")
}
Expand Down Expand Up @@ -96,4 +98,18 @@ class RouteResponseTests: XCTestCase {
XCTAssertEqual(unwrappedResponse.exclusionViolations(routeIndex: 0, legIndex: 0, stepIndex: 9, intersectionIndex: 0).first!.roadClasses, .ferry)
XCTAssertEqual(unwrappedResponse.exclusionViolations(routeIndex: 0, legIndex: 0, stepIndex: 24, intersectionIndex: 7).first!.roadClasses, .toll)
}

func testRefreshTTL() {
let options = RouteOptions(coordinates: [])
let refreshTTL: TimeInterval = 60
let response = RouteResponse(httpResponse: nil,
options: .route(options),
credentials: BogusCredentials,
refreshTTL: refreshTTL)

XCTAssertNotNil(response.refreshTTL)
XCTAssertNotNil(response.refreshInvalidationDate)

XCTAssertEqual(response.refreshInvalidationDate, response.created.addingTimeInterval(refreshTTL))
}
}
4 changes: 3 additions & 1 deletion swift-package-baseline/breakage-allowlist-path.txt
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@

API breakage: enumelement DirectionsError.refreshExpired has been added as a new enum case
API breakage: constructor DirectionsError.init(code:message:response:underlyingError:) has been removed
API breakage: constructor RouteResponse.init(httpResponse:identifier:routes:waypoints:options:credentials:) has been removed

0 comments on commit 9a23bdc

Please sign in to comment.