Skip to content

Commit c5e733d

Browse files
committed
Throw error when decoding without options
When decoding an object from JSON without specifying a RouteOptions or MatchOptions object in the user info dictionary, throw an error instead of force-unwrapping an optional and crashing. Ensure that waypoints in legs in routes/matches have the same names and leg-separation qualities as in the overall response and options object.
1 parent 81504bb commit c5e733d

File tree

10 files changed

+129
-47
lines changed

10 files changed

+129
-47
lines changed

Sources/MapboxDirections/DirectionsError.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import Foundation
22

3+
/**
4+
An error that occurs when calculating directions.
5+
*/
36
public enum DirectionsError: LocalizedError {
47
/**
58
The server returned an empty response.
@@ -169,3 +172,13 @@ extension DirectionsError: Equatable {
169172
}
170173
}
171174
}
175+
176+
/**
177+
An error that occurs when encoding or decoding a type defined by the MapboxDirections framework.
178+
*/
179+
public enum DirectionsCodingError: Error {
180+
/**
181+
Decoding this type requires the `Decoder.userInfo` dictionary to contain the `CodingUserInfoKey.options` key.
182+
*/
183+
case missingOptions
184+
}

Sources/MapboxDirections/DirectionsOptions.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ public enum RouteShapeFormat: String, Codable {
3232
This format is an order of magnitude more precise than `polyline`.
3333
*/
3434
case polyline6
35+
36+
static let `default` = RouteShapeFormat.polyline
3537
}
3638

3739
/**

Sources/MapboxDirections/DirectionsResult.swift

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ open class DirectionsResult: Codable {
2929
distance = try container.decode(CLLocationDistance.self, forKey: .distance)
3030
expectedTravelTime = try container.decode(TimeInterval.self, forKey: .expectedTravelTime)
3131

32-
_directionsOptions = decoder.userInfo[.options] as! DirectionsOptions
32+
guard let directionsOptions = decoder.userInfo[.options] as? DirectionsOptions else {
33+
throw DirectionsCodingError.missingOptions
34+
}
35+
_directionsOptions = directionsOptions
3336

3437
if let polyLineString = try container.decodeIfPresent(PolyLineString.self, forKey: .shape) {
3538
shape = try LineString(polyLineString: polyLineString)
@@ -43,8 +46,7 @@ open class DirectionsResult: Codable {
4346
let legInfo = zip(zip(waypoints.prefix(upTo: waypoints.endIndex - 1), waypoints.suffix(from: 1)), legs)
4447

4548
for (endpoints, leg) in legInfo {
46-
leg.source = endpoints.0
47-
leg.destination = endpoints.1
49+
(leg.source, leg.destination) = endpoints
4850
}
4951

5052
accessToken = try container.decodeIfPresent(String.self, forKey: .accessToken)
@@ -53,8 +55,8 @@ open class DirectionsResult: Codable {
5355

5456
do {
5557
speechLocale = try container.decodeIfPresent(Locale.self, forKey: .speechLocale)
56-
} catch let DecodingError.typeMismatch(mismatchedType, context){
57-
guard mismatchedType == Dictionary<String, Any>.self else {
58+
} catch let DecodingError.typeMismatch(mismatchedType, context) {
59+
guard mismatchedType == [String: Any].self else {
5860
throw DecodingError.typeMismatch(mismatchedType, context)
5961
}
6062
let identifier = try container.decode(String.self, forKey: .speechLocale)
@@ -99,6 +101,19 @@ open class DirectionsResult: Codable {
99101
*/
100102
public let legs: [RouteLeg]
101103

104+
public var legSeparators: [Waypoint?] {
105+
get {
106+
return legs.isEmpty ? [] : ([legs[0].source] + legs.map { $0.destination })
107+
}
108+
set {
109+
let endpointsByLeg = zip(newValue, newValue.suffix(from: 1))
110+
for (leg, (source, destination)) in zip(legs, endpointsByLeg) {
111+
leg.source = source
112+
leg.destination = destination
113+
}
114+
}
115+
}
116+
102117
// MARK: Getting Statistics About the Route
103118

104119
/**

Sources/MapboxDirections/Extensions/Codable.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ extension PolyLineString: Codable {
2222
init(from decoder: Decoder) throws {
2323
let container = try decoder.singleValueContainer()
2424
let options = decoder.userInfo[.options] as? DirectionsOptions
25-
switch options?.shapeFormat ?? .polyline {
25+
switch options?.shapeFormat ?? .default {
2626
case .geoJSON:
2727
self = .lineString(try container.decode(LineString.self))
2828
case .polyline, .polyline6:

Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,35 @@ class MapMatchingResponse: Decodable {
1212
}
1313

1414
public required init(from decoder: Decoder) throws {
15-
let options = decoder.userInfo[.options] as? MatchOptions
1615
let container = try decoder.container(keyedBy: CodingKeys.self)
1716
code = try container.decode(String.self, forKey: .code)
18-
let waypoints = try container.decode([Waypoint].self, forKey: .tracepoints)
17+
routes = try container.decodeIfPresent([Route].self, forKey: .matches)
1918

20-
if let optionsPoints = options?.waypoints {
21-
let updatedPoints = zip(waypoints, optionsPoints).map { (arg) -> Waypoint in
22-
let (local, api) = arg
23-
24-
return Waypoint(coordinate: api.coordinate, coordinateAccuracy: local.coordinateAccuracy, name: local.name ?? api.name)
19+
// Decode waypoints from the response and update their names according to the waypoints from DirectionsOptions.waypoints.
20+
let decodedWaypoints = try container.decode([Waypoint].self, forKey: .tracepoints)
21+
if let options = decoder.userInfo[.options] as? DirectionsOptions {
22+
// The response lists the same number of tracepoints as the waypoints in the request, whether or not a given waypoint is leg-separating.
23+
waypoints = zip(decodedWaypoints, options.waypoints).map { (pair) -> Waypoint in
24+
let (decodedWaypoint, waypointInOptions) = pair
25+
let waypoint = Waypoint(coordinate: decodedWaypoint.coordinate, coordinateAccuracy: waypointInOptions.coordinateAccuracy, name: waypointInOptions.name?.nonEmptyString ?? decodedWaypoint.name)
26+
waypoint.separatesLegs = waypointInOptions.separatesLegs
27+
return waypoint
2528
}
26-
self.waypoints = updatedPoints
29+
waypoints.first?.separatesLegs = true
30+
waypoints.last?.separatesLegs = true
2731
} else {
28-
self.waypoints = waypoints
32+
waypoints = decodedWaypoints
2933
}
3034

31-
routes = try container.decodeIfPresent([Route].self, forKey: .matches)
35+
if let routes = try container.decodeIfPresent([Route].self, forKey: .matches) {
36+
// Postprocess each route.
37+
for route in routes {
38+
// Imbue each route’s legs with the leg-separating waypoints refined above.
39+
route.legSeparators = waypoints.filter { $0.separatesLegs }
40+
}
41+
self.routes = routes
42+
} else {
43+
routes = nil
44+
}
3245
}
3346
}

Sources/MapboxDirections/MapMatching/Match.swift

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,23 @@ open class Match: DirectionsResult {
1616
case tracepoints
1717
case matchOptions
1818
}
19+
20+
/**
21+
Creates a match from a decoder.
22+
23+
- precondition: If the decoder is decoding JSON data from an API response, the `Decoder.userInfo` dictionary must contain a `MatchOptions` object in the `CodingUserInfoKey.options` key. If it does not, a `DirectionsCodingError.missingOptions` error is thrown.
24+
- parameter decoder: The decoder of JSON-formatted API response data or a previously encoded `Match` object.
25+
*/
1926
public required init(from decoder: Decoder) throws {
2027
let container = try decoder.container(keyedBy: CodingKeys.self)
2128
confidence = try container.decode(Float.self, forKey: .confidence)
2229
tracepoints = try container.decodeIfPresent([Tracepoint?].self, forKey: .tracepoints) ?? []
23-
matchOptions = try container.decodeIfPresent(MatchOptions.self, forKey: .matchOptions) ?? decoder.userInfo[.options] as! MatchOptions
30+
if let matchOptions = try container.decodeIfPresent(MatchOptions.self, forKey: .matchOptions)
31+
?? decoder.userInfo[.options] as? MatchOptions {
32+
self.matchOptions = matchOptions
33+
} else {
34+
throw DirectionsCodingError.missingOptions
35+
}
2436
try super.init(from: decoder)
2537
}
2638

Sources/MapboxDirections/Route.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,19 @@ open class Route: DirectionsResult {
88
case routeOptions
99
}
1010

11+
/**
12+
Creates a route from a decoder.
13+
14+
- 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.
15+
- parameter decoder: The decoder of JSON-formatted API response data or a previously encoded `Route` object.
16+
*/
1117
public required init(from decoder: Decoder) throws {
1218
if let matchOptions = decoder.userInfo[.options] as? MatchOptions {
1319
routeOptions = RouteOptions(matchOptions: matchOptions)
20+
} else if let routeOptions = decoder.userInfo[.options] as? RouteOptions {
21+
self.routeOptions = routeOptions
1422
} else {
15-
routeOptions = decoder.userInfo[.options] as! RouteOptions
23+
throw DirectionsCodingError.missingOptions
1624
}
1725

1826
try super.init(from: decoder)

Sources/MapboxDirections/RouteLeg.swift

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,28 @@ open class RouteLeg: Codable {
2828

2929
// MARK: Creating a Leg
3030

31+
/**
32+
Creates a route leg from a decoder.
33+
34+
- 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.
35+
- parameter decoder: The decoder of JSON-formatted API response data or a previously encoded `RouteLeg` object.
36+
*/
3137
public required init(from decoder: Decoder) throws {
3238
let container = try decoder.container(keyedBy: CodingKeys.self)
33-
let options = decoder.userInfo[.options] as? DirectionsOptions
34-
3539
source = try container.decodeIfPresent(Waypoint.self, forKey: .source)
3640
destination = try container.decodeIfPresent(Waypoint.self, forKey: .destination)
3741
steps = try container.decode([RouteStep].self, forKey: .steps)
3842
name = try container.decode(String.self, forKey: .name)
3943
distance = try container.decode(CLLocationDistance.self, forKey: .distance)
4044
expectedTravelTime = try container.decode(TimeInterval.self, forKey: .expectedTravelTime)
41-
profileIdentifier = try container.decodeIfPresent(DirectionsProfileIdentifier.self, forKey: .profileIdentifier) ?? options!.profileIdentifier
45+
46+
if let profileIdentifier = try container.decodeIfPresent(DirectionsProfileIdentifier.self, forKey: .profileIdentifier) {
47+
self.profileIdentifier = profileIdentifier
48+
} else if let options = decoder.userInfo[.options] as? DirectionsOptions {
49+
profileIdentifier = options.profileIdentifier
50+
} else {
51+
throw DirectionsCodingError.missingOptions
52+
}
4253

4354
let annotation = try? container.nestedContainer(keyedBy: AnnotationCodingKeys.self, forKey: .annotation)
4455
segmentDistances = try annotation?.decodeIfPresent([CLLocationDistance].self, forKey: .segmentDistances)
@@ -70,13 +81,17 @@ open class RouteLeg: Codable {
7081
The starting point of the route leg.
7182

7283
Unless this is the first leg of the route, the source of this leg is the same as the destination of the previous leg.
84+
85+
This property is set to `nil` if the leg was decoded from a JSON RouteLeg object.
7386
*/
7487
public var source: Waypoint?
7588

7689
/**
7790
The endpoint of the route leg.
7891

7992
Unless this is the last leg of the route, the destination of this leg is the same as the source of the next leg.
93+
94+
This property is set to `nil` if the leg was decoded from a JSON RouteLeg object.
8095
*/
8196
public var destination: Waypoint?
8297

Sources/MapboxDirections/RouteResponse.swift

Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -32,35 +32,38 @@ extension RouteResponse: Codable {
3232
let container = try decoder.container(keyedBy: CodingKeys.self)
3333

3434
self.code = try container.decodeIfPresent(String.self, forKey: .code)
35-
3635
self.message = try container.decodeIfPresent(String.self, forKey: .message)
37-
3836
self.error = try container.decodeIfPresent(String.self, forKey: .error)
37+
self.uuid = try container.decodeIfPresent(String.self, forKey: .uuid)
3938

40-
let uuid = try container.decodeIfPresent(String.self, forKey: .uuid)
41-
self.uuid = uuid
42-
43-
let waypoints = try container.decodeIfPresent([Waypoint].self, forKey: .waypoints)
44-
self.waypoints = waypoints
39+
// Decode waypoints from the response and update their names according to the waypoints from DirectionsOptions.waypoints.
40+
let decodedWaypoints = try container.decodeIfPresent([Waypoint].self, forKey: .waypoints)
41+
if let decodedWaypoints = decodedWaypoints, let options = decoder.userInfo[.options] as? DirectionsOptions {
42+
// The response lists the same number of tracepoints as the waypoints in the request, whether or not a given waypoint is leg-separating.
43+
waypoints = zip(decodedWaypoints, options.waypoints).map { (pair) -> Waypoint in
44+
let (decodedWaypoint, waypointInOptions) = pair
45+
let waypoint = Waypoint(coordinate: decodedWaypoint.coordinate, coordinateAccuracy: waypointInOptions.coordinateAccuracy, name: waypointInOptions.name?.nonEmptyString ?? decodedWaypoint.name)
46+
waypoint.separatesLegs = waypointInOptions.separatesLegs
47+
return waypoint
48+
}
49+
waypoints?.first?.separatesLegs = true
50+
waypoints?.last?.separatesLegs = true
51+
} else {
52+
waypoints = decodedWaypoints
53+
}
4554

46-
let rawRoutes = try container.decodeIfPresent([Route].self, forKey: .routes)
47-
var routesWithDestinations: [Route]? = rawRoutes
48-
if let destinations = waypoints?.dropFirst() {
49-
routesWithDestinations = rawRoutes?.map({ (route) -> Route in
50-
for (leg, destination) in zip(route.legs, destinations) {
51-
if leg.destination?.name?.nonEmptyString == nil {
52-
leg.destination = destination
53-
}
55+
if let routes = try container.decodeIfPresent([Route].self, forKey: .routes) {
56+
// Postprocess each route.
57+
for route in routes {
58+
route.routeIdentifier = uuid
59+
// Imbue each route’s legs with the waypoints refined above.
60+
if let waypoints = waypoints {
61+
route.legSeparators = waypoints.filter { $0.separatesLegs }
5462
}
55-
return route
56-
})
63+
}
64+
self.routes = routes
65+
} else {
66+
routes = nil
5767
}
58-
59-
let routesWithIdentifiers = routesWithDestinations?.map({ (route) -> Route in
60-
route.routeIdentifier = uuid
61-
return route
62-
})
63-
64-
self.routes = routesWithIdentifiers
6568
}
6669
}

Sources/MapboxDirections/RouteStep.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -430,8 +430,9 @@ open class RouteStep: Codable {
430430

431431
try container.encodeIfPresent(intersections, forKey: .intersections)
432432
try container.encode(drivingSide, forKey: .drivingSide)
433-
if let shape = shape, let options = encoder.userInfo[.options] as? DirectionsOptions {
434-
let shapeFormat = options.shapeFormat
433+
if let shape = shape {
434+
let options = encoder.userInfo[.options] as? DirectionsOptions
435+
let shapeFormat = options?.shapeFormat ?? .default
435436
let polyLineString = PolyLineString(lineString: shape, shapeFormat: shapeFormat)
436437
try container.encode(polyLineString, forKey: .shape)
437438
}

0 commit comments

Comments
 (0)