Skip to content

Commit

Permalink
Throw error when decoding without options
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
1ec5 committed Nov 23, 2019
1 parent 81504bb commit c5e733d
Show file tree
Hide file tree
Showing 10 changed files with 129 additions and 47 deletions.
13 changes: 13 additions & 0 deletions Sources/MapboxDirections/DirectionsError.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import Foundation

/**
An error that occurs when calculating directions.
*/
public enum DirectionsError: LocalizedError {
/**
The server returned an empty response.
Expand Down Expand Up @@ -169,3 +172,13 @@ extension DirectionsError: Equatable {
}
}
}

/**
An error that occurs when encoding or decoding a type defined by the MapboxDirections framework.
*/
public enum DirectionsCodingError: Error {
/**
Decoding this type requires the `Decoder.userInfo` dictionary to contain the `CodingUserInfoKey.options` key.
*/
case missingOptions
}
2 changes: 2 additions & 0 deletions Sources/MapboxDirections/DirectionsOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ public enum RouteShapeFormat: String, Codable {
This format is an order of magnitude more precise than `polyline`.
*/
case polyline6

static let `default` = RouteShapeFormat.polyline
}

/**
Expand Down
25 changes: 20 additions & 5 deletions Sources/MapboxDirections/DirectionsResult.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ open class DirectionsResult: Codable {
distance = try container.decode(CLLocationDistance.self, forKey: .distance)
expectedTravelTime = try container.decode(TimeInterval.self, forKey: .expectedTravelTime)

_directionsOptions = decoder.userInfo[.options] as! DirectionsOptions
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)
Expand All @@ -43,8 +46,7 @@ open class DirectionsResult: Codable {
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
(leg.source, leg.destination) = endpoints
}

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

do {
speechLocale = try container.decodeIfPresent(Locale.self, forKey: .speechLocale)
} catch let DecodingError.typeMismatch(mismatchedType, context){
guard mismatchedType == Dictionary<String, Any>.self else {
} catch let DecodingError.typeMismatch(mismatchedType, context) {
guard mismatchedType == [String: Any].self else {
throw DecodingError.typeMismatch(mismatchedType, context)
}
let identifier = try container.decode(String.self, forKey: .speechLocale)
Expand Down Expand Up @@ -99,6 +101,19 @@ open class DirectionsResult: Codable {
*/
public let legs: [RouteLeg]

public var legSeparators: [Waypoint?] {
get {
return legs.isEmpty ? [] : ([legs[0].source] + legs.map { $0.destination })
}
set {
let endpointsByLeg = zip(newValue, newValue.suffix(from: 1))
for (leg, (source, destination)) in zip(legs, endpointsByLeg) {
leg.source = source
leg.destination = destination
}
}
}

// MARK: Getting Statistics About the Route

/**
Expand Down
2 changes: 1 addition & 1 deletion Sources/MapboxDirections/Extensions/Codable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ extension PolyLineString: Codable {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let options = decoder.userInfo[.options] as? DirectionsOptions
switch options?.shapeFormat ?? .polyline {
switch options?.shapeFormat ?? .default {
case .geoJSON:
self = .lineString(try container.decode(LineString.self))
case .polyline, .polyline6:
Expand Down
33 changes: 23 additions & 10 deletions Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,35 @@ class MapMatchingResponse: Decodable {
}

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

if let optionsPoints = options?.waypoints {
let updatedPoints = zip(waypoints, optionsPoints).map { (arg) -> Waypoint in
let (local, api) = arg

return Waypoint(coordinate: api.coordinate, coordinateAccuracy: local.coordinateAccuracy, name: local.name ?? api.name)
// Decode waypoints from the response and update their names according to the waypoints from DirectionsOptions.waypoints.
let decodedWaypoints = try container.decode([Waypoint].self, forKey: .tracepoints)
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
}
self.waypoints = updatedPoints
waypoints.first?.separatesLegs = true
waypoints.last?.separatesLegs = true
} else {
self.waypoints = waypoints
waypoints = decodedWaypoints
}

routes = try container.decodeIfPresent([Route].self, forKey: .matches)
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
}
}
}
14 changes: 13 additions & 1 deletion Sources/MapboxDirections/MapMatching/Match.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,23 @@ open class Match: DirectionsResult {
case tracepoints
case matchOptions
}

/**
Creates a match from a decoder.

- 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.
- parameter decoder: The decoder of JSON-formatted API response data or a previously encoded `Match` object.
*/
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) ?? []
matchOptions = try container.decodeIfPresent(MatchOptions.self, forKey: .matchOptions) ?? decoder.userInfo[.options] as! MatchOptions
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)
}

Expand Down
10 changes: 9 additions & 1 deletion Sources/MapboxDirections/Route.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,19 @@ open class Route: DirectionsResult {
case routeOptions
}

/**
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 {
routeOptions = decoder.userInfo[.options] as! RouteOptions
throw DirectionsCodingError.missingOptions
}

try super.init(from: decoder)
Expand Down
21 changes: 18 additions & 3 deletions Sources/MapboxDirections/RouteLeg.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,28 @@ open class RouteLeg: Codable {

// MARK: Creating a Leg

/**
Creates a route leg 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 `RouteLeg` object.
*/
public required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let options = decoder.userInfo[.options] as? DirectionsOptions

source = try container.decodeIfPresent(Waypoint.self, forKey: .source)
destination = try container.decodeIfPresent(Waypoint.self, forKey: .destination)
steps = try container.decode([RouteStep].self, forKey: .steps)
name = try container.decode(String.self, forKey: .name)
distance = try container.decode(CLLocationDistance.self, forKey: .distance)
expectedTravelTime = try container.decode(TimeInterval.self, forKey: .expectedTravelTime)
profileIdentifier = try container.decodeIfPresent(DirectionsProfileIdentifier.self, forKey: .profileIdentifier) ?? options!.profileIdentifier

if let profileIdentifier = try container.decodeIfPresent(DirectionsProfileIdentifier.self, forKey: .profileIdentifier) {
self.profileIdentifier = profileIdentifier
} else if let options = decoder.userInfo[.options] as? DirectionsOptions {
profileIdentifier = options.profileIdentifier
} else {
throw DirectionsCodingError.missingOptions
}

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

Unless this is the first leg of the route, the source of this leg is the same as the destination of the previous leg.

This property is set to `nil` if the leg was decoded from a JSON RouteLeg object.
*/
public var source: Waypoint?

/**
The endpoint of the route leg.

Unless this is the last leg of the route, the destination of this leg is the same as the source of the next leg.

This property is set to `nil` if the leg was decoded from a JSON RouteLeg object.
*/
public var destination: Waypoint?

Expand Down
51 changes: 27 additions & 24 deletions Sources/MapboxDirections/RouteResponse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,35 +32,38 @@ extension RouteResponse: Codable {
let container = try decoder.container(keyedBy: CodingKeys.self)

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)
self.uuid = try container.decodeIfPresent(String.self, forKey: .uuid)

let uuid = try container.decodeIfPresent(String.self, forKey: .uuid)
self.uuid = uuid

let waypoints = try container.decodeIfPresent([Waypoint].self, forKey: .waypoints)
self.waypoints = waypoints
// 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 {
// 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
}
waypoints?.first?.separatesLegs = true
waypoints?.last?.separatesLegs = true
} else {
waypoints = decodedWaypoints
}

let rawRoutes = try container.decodeIfPresent([Route].self, forKey: .routes)
var routesWithDestinations: [Route]? = rawRoutes
if let destinations = waypoints?.dropFirst() {
routesWithDestinations = rawRoutes?.map({ (route) -> Route in
for (leg, destination) in zip(route.legs, destinations) {
if leg.destination?.name?.nonEmptyString == nil {
leg.destination = destination
}
if let routes = try container.decodeIfPresent([Route].self, forKey: .routes) {
// Postprocess each route.
for route in routes {
route.routeIdentifier = uuid
// Imbue each route’s legs with the waypoints refined above.
if let waypoints = waypoints {
route.legSeparators = waypoints.filter { $0.separatesLegs }
}
return route
})
}
self.routes = routes
} else {
routes = nil
}

let routesWithIdentifiers = routesWithDestinations?.map({ (route) -> Route in
route.routeIdentifier = uuid
return route
})

self.routes = routesWithIdentifiers
}
}
5 changes: 3 additions & 2 deletions Sources/MapboxDirections/RouteStep.swift
Original file line number Diff line number Diff line change
Expand Up @@ -430,8 +430,9 @@ open class RouteStep: Codable {

try container.encodeIfPresent(intersections, forKey: .intersections)
try container.encode(drivingSide, forKey: .drivingSide)
if let shape = shape, let options = encoder.userInfo[.options] as? DirectionsOptions {
let shapeFormat = options.shapeFormat
if let shape = shape {
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)
}
Expand Down

0 comments on commit c5e733d

Please sign in to comment.