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

Foreign JSON members roundtrip #669

Merged
merged 11 commits into from
May 6, 2022
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
* Fixed a crash that occurred when `RouteOptions.roadClassesToAvoid` or `RouteOptions.roadClassesToAllow` properties contained multiple road classes.
* `RoadClasses.tunnel` and `RoadClasses.restricted` are no longer supported in `RouteOptions.roadClassesToAvoid` or `RouteOptions.roadClassesToAllow` properties
* Added `DirectionsOptions(url:)`, `RouteOptions(url:)` and extended existing `DirectionsOptions(waypoints:profileIdentifier:queryItems:)`, `RouteOptions(waypoints:profileIdentifier:queryItems:)`, `MatchOptions(waypoints:profileIdentifier:queryItems:)` and related convenience init methods for deserializing corresponding options object using appropriate request URL or it's query items. ([#655](https://github.com/mapbox/mapbox-directions-swift/pull/655))
* `RouteResponse`, `RouteRefreshResponse`, `MatchResponse` and all underlying types now correctly handle unrecognized (foreign) JSON values and preserve them on coding/decoding using `ForeignMemberContainer` and `ForeignMemberClassContainer` implementations. ([#669](https://github.com/mapbox/mapbox-directions-swift/pull/669))
* Fixed `Waypoint.snappedDistance` value was missing after decoding a `RouteResponse`. ([#669](https://github.com/mapbox/mapbox-directions-swift/pull/669))
1ec5 marked this conversation as resolved.
Show resolved Hide resolved
* Added `Incident` properties: `countryCode`, `countryCodeAlpha3`, `roadIsClosed`, `longDescription`, `numberOfBlockedLanes`, `congestionLevel`, `affectedRoadNames`. ([#672](https://github.com/mapbox/mapbox-directions-swift/pull/672))
* Added `departAt` and `arriveBy` properties to `RouteOptions` to allow configuring Directions routes calculation. ([#673](https://github.com/mapbox/mapbox-directions-swift/pull/673))
* Removed url request's `.json` suffix for Directions and Isochrones to follow V5 scheme. ([#678](https://github.com/mapbox/mapbox-directions-swift/pull/678))
Expand Down
38 changes: 38 additions & 0 deletions MapboxDirections.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

8 changes: 7 additions & 1 deletion Sources/MapboxDirections/AdministrativeRegion.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Foundation
import Turf

/**
`AdministrativeRegion` describes corresponding object on the route.
Expand All @@ -7,7 +8,8 @@ import Foundation

- seealso: `Intersection.regionCode`, `RouteStep.regionCode(atStepIndex:, intersectionIndex:)`
*/
public struct AdministrativeRegion: Codable, Equatable {
public struct AdministrativeRegion: Codable, Equatable, ForeignMemberContainer {
public var foreignMembers: JSONObject = [:]

private enum CodingKeys: String, CodingKey {
case countryCodeAlpha3 = "iso_3166_1_alpha3"
Expand All @@ -29,12 +31,16 @@ public struct AdministrativeRegion: Codable, Equatable {

countryCode = try container.decode(String.self, forKey: .countryCode)
countryCodeAlpha3 = try container.decodeIfPresent(String.self, forKey: .countryCodeAlpha3)

try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder)
}

public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)

try container.encode(countryCode, forKey: .countryCode)
try container.encodeIfPresent(countryCodeAlpha3, forKey: .countryCodeAlpha3)

try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder)
}
}
10 changes: 8 additions & 2 deletions Sources/MapboxDirections/DirectionsResult.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import Turf

You do not create instances of this class directly. Instead, you receive `Route` or `Match` objects when you request directions using the `Directions.calculate(_:completionHandler:)` or `Directions.calculateRoutes(matching:completionHandler:)` method.
*/
open class DirectionsResult: Codable {
private enum CodingKeys: String, CodingKey {
open class DirectionsResult: Codable, ForeignMemberContainerClass {
public var foreignMembers: JSONObject = [:]

private enum CodingKeys: String, CodingKey, CaseIterable {
case shape = "geometry"
case legs
case distance
Expand Down Expand Up @@ -64,6 +66,8 @@ open class DirectionsResult: Codable {
}

responseContainsSpeechLocale = container.contains(.speechLocale)

try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder)
}


Expand All @@ -83,6 +87,8 @@ open class DirectionsResult: Codable {
if responseContainsSpeechLocale {
try container.encode(speechLocale?.identifier, forKey: .speechLocale)
}

try encodeForeignMembers(to: encoder)
}

// MARK: Getting the Shape of the Route
Expand Down
99 changes: 99 additions & 0 deletions Sources/MapboxDirections/Extensions/ForeignMemberContainer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import Foundation
import Turf

/**
A coding key as an extensible enumeration.
*/
struct AnyCodingKey: CodingKey {
var stringValue: String
var intValue: Int?

init?(stringValue: String) {
self.stringValue = stringValue
self.intValue = nil
}

init?(intValue: Int) {
self.stringValue = String(intValue)
self.intValue = intValue
}
}

extension ForeignMemberContainer {
/**
Decodes any foreign members using the given decoder.
*/
mutating func decodeForeignMembers<WellKnownCodingKeys>(notKeyedBy _: WellKnownCodingKeys.Type, with decoder: Decoder) throws where WellKnownCodingKeys: CodingKey {
1ec5 marked this conversation as resolved.
Show resolved Hide resolved
let foreignMemberContainer = try decoder.container(keyedBy: AnyCodingKey.self)
for key in foreignMemberContainer.allKeys {
if WellKnownCodingKeys(stringValue: key.stringValue) == nil {
foreignMembers[key.stringValue] = try foreignMemberContainer.decode(JSONValue?.self, forKey: key)
}
}
}

/**
Encodes any foreign members using the given encoder.
*/
func encodeForeignMembers<WellKnownCodingKeys>(notKeyedBy _: WellKnownCodingKeys.Type, to encoder: Encoder) throws where WellKnownCodingKeys: CodingKey {
var foreignMemberContainer = encoder.container(keyedBy: AnyCodingKey.self)
for (key, value) in foreignMembers {
if let key = AnyCodingKey(stringValue: key),
WellKnownCodingKeys(stringValue: key.stringValue) == nil {
try foreignMemberContainer.encode(value, forKey: key)
}
}
}
}

/**
A GeoJSON *class* that can contain [foreign members](https://datatracker.ietf.org/doc/html/rfc7946#section-6.1) in arbitrary keys.

When subclassing `ForeignMemberContainerClass` type, you should call `decodeForeignMembers(notKeyedBy:with:)` during your `Decodable.init(from:)` initializer if your subclass has added any new properties.
*/
public protocol ForeignMemberContainerClass: AnyObject {
var foreignMembers: JSONObject { get set }

/**
Decodes any foreign members using the given decoder.

- parameter codingKeys: `CodingKeys` type which describes all properties declared in current subclass.
- parameter decoder: `Decoder` instance, which perfroms the decoding process.
*/
func decodeForeignMembers<WellKnownCodingKeys>(notKeyedBy codingKeys: WellKnownCodingKeys.Type, with decoder: Decoder) throws where WellKnownCodingKeys: CodingKey & CaseIterable

/**
Encodes any foreign members using the given encoder.

This method should be called in your `Encodable.encode(to:)` implementation only in the **base class**. Otherwise it will not encode `foreignMembers` or way overwrite it.

- parameter encoder: `Encoder` instance, performing the encoding process.
*/
func encodeForeignMembers(to encoder: Encoder) throws
}

extension ForeignMemberContainerClass {

public func decodeForeignMembers<WellKnownCodingKeys>(notKeyedBy _: WellKnownCodingKeys.Type, with decoder: Decoder) throws where WellKnownCodingKeys: CodingKey & CaseIterable {
if foreignMembers.isEmpty {
let foreignMemberContainer = try decoder.container(keyedBy: AnyCodingKey.self)
for key in foreignMemberContainer.allKeys {
if WellKnownCodingKeys(stringValue: key.stringValue) == nil {
foreignMembers[key.stringValue] = try foreignMemberContainer.decode(JSONValue?.self, forKey: key)
}
}
}
WellKnownCodingKeys.allCases.forEach {
foreignMembers.removeValue(forKey: $0.stringValue)
}
}

public func encodeForeignMembers(to encoder: Encoder) throws {
var foreignMemberContainer = encoder.container(keyedBy: AnyCodingKey.self)
for (key, value) in foreignMembers {
if let key = AnyCodingKey(stringValue: key) {
try foreignMemberContainer.encode(value, forKey: key)
}
}
}
}
38 changes: 34 additions & 4 deletions Sources/MapboxDirections/Incident.swift
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import Foundation
import Turf

/**
`Incident` describes any corresponding event, used for annotating the route.
*/
public struct Incident: Codable, Equatable {
public struct Incident: Codable, Equatable, ForeignMemberContainer {
public var foreignMembers: JSONObject = [:]
public var congestionForeignMembers: JSONObject = [:]

private enum CodingKeys: String, CodingKey {
case identifier = "id"
Expand Down Expand Up @@ -72,7 +75,9 @@ public struct Incident: Codable, Equatable {
case low
}

private struct CongestionContainer: Codable {
private struct CongestionContainer: Codable, ForeignMemberContainer {
var foreignMembers: JSONObject = [:]

// `Directions` define this as service value to indicate "no congestion calculated"
// see: https://docs.mapbox.com/api/navigation/directions/#incident-object
private static let CongestionUnavailableKey = 101
Expand All @@ -85,6 +90,24 @@ public struct Incident: Codable, Equatable {
var clampedValue: Int? {
value == Self.CongestionUnavailableKey ? nil : value
}

init(value: Int) {
self.value = value
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
value = try container.decode(Int.self, forKey: .value)

try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder)
}

func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(value, forKey: .value)

try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder)
}
}

/// Incident identifier
Expand Down Expand Up @@ -226,8 +249,11 @@ public struct Incident: Codable, Equatable {
roadIsClosed = try container.decodeIfPresent(Bool.self, forKey: .roadIsClosed)
longDescription = try container.decodeIfPresent(String.self, forKey: .longDescription)
numberOfBlockedLanes = try container.decodeIfPresent(Int.self, forKey: .numberOfBlockedLanes)
congestionLevel = try container.decodeIfPresent(CongestionContainer.self, forKey: .congestionLevel)?.clampedValue
let congestionContainer = try container.decodeIfPresent(CongestionContainer.self, forKey: .congestionLevel)
congestionLevel = congestionContainer?.clampedValue
congestionForeignMembers = congestionContainer?.foreignMembers ?? [:]
affectedRoadNames = try container.decodeIfPresent([String].self, forKey: .affectedRoadNames)
try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder)
}

public func encode(to encoder: Encoder) throws {
Expand All @@ -253,8 +279,12 @@ public struct Incident: Codable, Equatable {
try container.encodeIfPresent(longDescription, forKey: .longDescription)
try container.encodeIfPresent(numberOfBlockedLanes, forKey: .numberOfBlockedLanes)
if let congestionLevel = congestionLevel {
try container.encode(CongestionContainer(value: congestionLevel), forKey: .congestionLevel)
var congestionContainer = CongestionContainer(value: congestionLevel)
congestionContainer.foreignMembers = congestionForeignMembers
try container.encode(congestionContainer, forKey: .congestionLevel)
}
try container.encodeIfPresent(affectedRoadNames, forKey: .affectedRoadNames)

try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder)
}
}
25 changes: 23 additions & 2 deletions Sources/MapboxDirections/Intersection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import Turf
/**
A single cross street along a step.
*/
public struct Intersection {
public struct Intersection: ForeignMemberContainer {
public var foreignMembers: JSONObject = [:]
public var lanesForeignMembers: [JSONObject] = []
1ec5 marked this conversation as resolved.
Show resolved Hide resolved

// MARK: Creating an Intersection

public init(location: LocationCoordinate2D,
Expand Down Expand Up @@ -187,7 +190,9 @@ extension Intersection: Codable {
}

/// Used to code `Intersection.outletMapboxStreetsRoadClass`
private struct MapboxStreetClassCodable: Codable {
private struct MapboxStreetClassCodable: Codable, ForeignMemberContainer {
var foreignMembers: JSONObject = [:]

private enum CodingKeys: String, CodingKey {
case streetClass = "class"
}
Expand All @@ -207,6 +212,14 @@ extension Intersection: Codable {
streetClass = nil
}

try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder)
}

func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encodeIfPresent(streetClass, forKey: .streetClass)

try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder)
}
}

Expand Down Expand Up @@ -272,6 +285,9 @@ extension Intersection: Codable {
validLanes[i].indications.descriptions.contains(usableLaneIndication.rawValue) {
lanes?[i].validIndication = usableLaneIndication
}
if usableApproachLanes.count == lanesForeignMembers.count {
lanes?[i].foreignMembers = lanesForeignMembers[i]
}
}

for j in preferredApproachLanes {
Expand Down Expand Up @@ -311,6 +327,8 @@ extension Intersection: Codable {
if let geoIndex = geometryIndex {
try container.encode(geoIndex, forKey: .geometryIndex)
}

try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder)
}

public init(from decoder: Decoder) throws {
Expand All @@ -319,6 +337,7 @@ extension Intersection: Codable {
headings = try container.decode([LocationDirection].self, forKey: .headings)

if let lanes = try container.decodeIfPresent([Lane].self, forKey: .lanes) {
lanesForeignMembers = lanes.map(\.foreignMembers)
approachLanes = lanes.map { $0.indications }
usableApproachLanes = lanes.indices { $0.isValid }
preferredApproachLanes = lanes.indices { ($0.isActive ?? false) }
Expand Down Expand Up @@ -352,6 +371,8 @@ extension Intersection: Codable {
isUrban = try container.decodeIfPresent(Bool.self, forKey: .isUrban)

restStop = try container.decodeIfPresent(RestStop.self, forKey: .restStop)

try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder)
}
}

Expand Down
9 changes: 8 additions & 1 deletion Sources/MapboxDirections/Lane.swift
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import Foundation
import Turf

/**
A lane on the road approaching an intersection.
*/
struct Lane: Equatable {
struct Lane: Equatable, ForeignMemberContainer {
var foreignMembers: JSONObject = [:]

/**
The lane indications specifying the maneuvers that may be executed from the lane.
*/
Expand Down Expand Up @@ -47,6 +50,8 @@ extension Lane: Codable {
try container.encode(isValid, forKey: .valid)
try container.encodeIfPresent(isActive, forKey: .active)
try container.encodeIfPresent(validIndication, forKey: .preferred)

try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder)
}

init(from decoder: Decoder) throws {
Expand All @@ -55,5 +60,7 @@ extension Lane: Codable {
isValid = try container.decode(Bool.self, forKey: .valid)
isActive = try container.decodeIfPresent(Bool.self, forKey: .active)
validIndication = try container.decodeIfPresent(ManeuverDirection.self, forKey: .preferred)

try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder)
}
}
15 changes: 14 additions & 1 deletion Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
import Turf

public struct MapMatchingResponse {
public struct MapMatchingResponse: ForeignMemberContainer {
public var foreignMembers: JSONObject = [:]

public let httpResponse: HTTPURLResponse?

public var matches : [Match]?
Expand Down Expand Up @@ -53,5 +56,15 @@ extension MapMatchingResponse: Codable {

tracepoints = try container.decodeIfPresent([Tracepoint?].self, forKey: .tracepoints)
matches = try container.decodeIfPresent([Match].self, forKey: .matches)

try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder)
}

public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encodeIfPresent(matches, forKey: .matches)
try container.encodeIfPresent(tracepoints, forKey: .tracepoints)

try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder)
}
}
Loading