diff --git a/CHANGELOG.md b/CHANGELOG.md index 16528cabb..4c8307959 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)) +* Types that correspond to objects in the Mapbox Directions API response, such as `RouteResponse`, `RouteRefreshResponse`, `MatchResponse`, and `RouteStep`, now conform to the `ForeignMemberContainer` and `ForeignMemberClassContainer` protocols. Types that conform to these protocols can persist unrecognized properties in the response, such as properties that are in beta, even after coding and decoding. You can access these properties using the `ForeignMemberContainer.foreignMembers` and `ForeignMemberClassContainer.foreignMembers` properties. ([#669](https://github.com/mapbox/mapbox-directions-swift/pull/669)) +* Fixed an issue where decoding a `RouteResponse` incorrectly set the `Waypoint.snappedDistance` property to `nil`. ([#669](https://github.com/mapbox/mapbox-directions-swift/pull/669)) * 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)) diff --git a/MapboxDirections.xcodeproj/project.pbxproj b/MapboxDirections.xcodeproj/project.pbxproj index d21a01029..109e4cbd7 100644 --- a/MapboxDirections.xcodeproj/project.pbxproj +++ b/MapboxDirections.xcodeproj/project.pbxproj @@ -11,6 +11,14 @@ 2B01E4BA2746ABBD0002A5F7 /* RouteResponseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A3B4C9A24EB55F60085DA64 /* RouteResponseTests.swift */; }; 2B39DD40270F034700ED68E4 /* CodingOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B39DD3F270F034700ED68E4 /* CodingOperation.swift */; }; 2B4383022549C22700A3E38B /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B4383002549C22700A3E38B /* main.swift */; }; + 2B28E22327EDB2AA0029E4C1 /* ForeignMemberContainerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B28E22227EDB2A90029E4C1 /* ForeignMemberContainerTests.swift */; }; + 2B28E22427EDB2AA0029E4C1 /* ForeignMemberContainerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B28E22227EDB2A90029E4C1 /* ForeignMemberContainerTests.swift */; }; + 2B28E22527EDB2AA0029E4C1 /* ForeignMemberContainerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B28E22227EDB2A90029E4C1 /* ForeignMemberContainerTests.swift */; }; + 2B39DD40270F034700ED68E4 /* CodingOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B39DD3F270F034700ED68E4 /* CodingOperation.swift */; }; + 2B4383022549C22700A3E38B /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B4383002549C22700A3E38B /* main.swift */; }; + 2B46DB0C27EDF8580068C893 /* RouteRefreshResponseWithForeignMembers.json in Resources */ = {isa = PBXBuildFile; fileRef = 2B46DB0B27EDF8580068C893 /* RouteRefreshResponseWithForeignMembers.json */; }; + 2B46DB0D27EDF8580068C893 /* RouteRefreshResponseWithForeignMembers.json in Resources */ = {isa = PBXBuildFile; fileRef = 2B46DB0B27EDF8580068C893 /* RouteRefreshResponseWithForeignMembers.json */; }; + 2B46DB0E27EDF8580068C893 /* RouteRefreshResponseWithForeignMembers.json in Resources */ = {isa = PBXBuildFile; fileRef = 2B46DB0B27EDF8580068C893 /* RouteRefreshResponseWithForeignMembers.json */; }; 2B5407ED2451B17E006C820B /* RouteRefreshResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B5407EC2451B17E006C820B /* RouteRefreshResponse.swift */; }; 2B5407EE2451B17E006C820B /* RouteRefreshResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B5407EC2451B17E006C820B /* RouteRefreshResponse.swift */; }; 2B5407EF2451B17E006C820B /* RouteRefreshResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B5407EC2451B17E006C820B /* RouteRefreshResponse.swift */; }; @@ -42,6 +50,9 @@ 2B5F0E01273BEB3600CC2C1A /* RoadClassExclusionViolation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B5F0DFF273BEB3600CC2C1A /* RoadClassExclusionViolation.swift */; }; 2B5F0E02273BEB3600CC2C1A /* RoadClassExclusionViolation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B5F0DFF273BEB3600CC2C1A /* RoadClassExclusionViolation.swift */; }; 2B5F0E03273BEB3600CC2C1A /* RoadClassExclusionViolation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B5F0DFF273BEB3600CC2C1A /* RoadClassExclusionViolation.swift */; }; + 2B67F68227EDC2EF007BD6D2 /* RouteResponseWithForeignMembers.json in Resources */ = {isa = PBXBuildFile; fileRef = 2B67F68127EDC2EF007BD6D2 /* RouteResponseWithForeignMembers.json */; }; + 2B67F68327EDC2EF007BD6D2 /* RouteResponseWithForeignMembers.json in Resources */ = {isa = PBXBuildFile; fileRef = 2B67F68127EDC2EF007BD6D2 /* RouteResponseWithForeignMembers.json */; }; + 2B67F68427EDC2EF007BD6D2 /* RouteResponseWithForeignMembers.json in Resources */ = {isa = PBXBuildFile; fileRef = 2B67F68127EDC2EF007BD6D2 /* RouteResponseWithForeignMembers.json */; }; 2B9F3881272AE23A001DBA12 /* ProfileIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B9F387C272AE23A001DBA12 /* ProfileIdentifier.swift */; }; 2B9F3882272AE23A001DBA12 /* ProfileIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B9F387C272AE23A001DBA12 /* ProfileIdentifier.swift */; }; 2B9F3883272AE23A001DBA12 /* ProfileIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B9F387C272AE23A001DBA12 /* ProfileIdentifier.swift */; }; @@ -76,6 +87,10 @@ 2BBBD08E257FA1CD004EB3D6 /* BlockedLanes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BBBD08C257FA1CD004EB3D6 /* BlockedLanes.swift */; }; 2BBBD08F257FA1CD004EB3D6 /* BlockedLanes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BBBD08C257FA1CD004EB3D6 /* BlockedLanes.swift */; }; 2BBBD090257FA1CD004EB3D6 /* BlockedLanes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BBBD08C257FA1CD004EB3D6 /* BlockedLanes.swift */; }; + 2BEA240427CFAD2000EE05D9 /* ForeignMemberContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BEA240327CFAD2000EE05D9 /* ForeignMemberContainer.swift */; }; + 2BEA240527CFAD2000EE05D9 /* ForeignMemberContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BEA240327CFAD2000EE05D9 /* ForeignMemberContainer.swift */; }; + 2BEA240627CFAD2000EE05D9 /* ForeignMemberContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BEA240327CFAD2000EE05D9 /* ForeignMemberContainer.swift */; }; + 2BEA240727CFAD2000EE05D9 /* ForeignMemberContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BEA240327CFAD2000EE05D9 /* ForeignMemberContainer.swift */; }; 2BF398C527620CD7000C9A72 /* RouteRefreshSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BF398C427620CD7000C9A72 /* RouteRefreshSource.swift */; }; 2BF398C627620CD7000C9A72 /* RouteRefreshSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BF398C427620CD7000C9A72 /* RouteRefreshSource.swift */; }; 2BF398C727620CD7000C9A72 /* RouteRefreshSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BF398C427620CD7000C9A72 /* RouteRefreshSource.swift */; }; @@ -478,6 +493,10 @@ /* Begin PBXFileReference section */ 2B39DD3F270F034700ED68E4 /* CodingOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CodingOperation.swift; path = Sources/MapboxDirectionsCLI/CodingOperation.swift; sourceTree = ""; }; 2B4383002549C22700A3E38B /* main.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = main.swift; path = Sources/MapboxDirectionsCLI/main.swift; sourceTree = ""; }; + 2B28E22227EDB2A90029E4C1 /* ForeignMemberContainerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForeignMemberContainerTests.swift; sourceTree = ""; }; + 2B39DD3F270F034700ED68E4 /* CodingOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CodingOperation.swift; path = Sources/MapboxDirectionsCLI/CodingOperation.swift; sourceTree = ""; }; + 2B4383002549C22700A3E38B /* main.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = main.swift; path = Sources/MapboxDirectionsCLI/main.swift; sourceTree = ""; }; + 2B46DB0B27EDF8580068C893 /* RouteRefreshResponseWithForeignMembers.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = RouteRefreshResponseWithForeignMembers.json; sourceTree = ""; }; 2B5407EC2451B17E006C820B /* RouteRefreshResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteRefreshResponse.swift; sourceTree = ""; }; 2B5407F12452FA8C006C820B /* RefreshedRoute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshedRoute.swift; sourceTree = ""; }; 2B5407F6245302AB006C820B /* RouteLegAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteLegAttributes.swift; sourceTree = ""; }; @@ -487,6 +506,7 @@ 2B540808245B23BE006C820B /* incorrectRouteRefreshResponse.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = incorrectRouteRefreshResponse.json; sourceTree = ""; }; 2B5F0DFB273ACE3B00CC2C1A /* tollAndFerryDirectionsRoute.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = tollAndFerryDirectionsRoute.json; sourceTree = ""; }; 2B5F0DFF273BEB3600CC2C1A /* RoadClassExclusionViolation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoadClassExclusionViolation.swift; sourceTree = ""; }; + 2B67F68127EDC2EF007BD6D2 /* RouteResponseWithForeignMembers.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = RouteResponseWithForeignMembers.json; sourceTree = ""; }; 2B9F387C272AE23A001DBA12 /* ProfileIdentifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProfileIdentifier.swift; sourceTree = ""; }; 2B9F387D272AE23A001DBA12 /* IsochroneOptions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IsochroneOptions.swift; sourceTree = ""; }; 2B9F387E272AE23A001DBA12 /* IsochroneError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IsochroneError.swift; sourceTree = ""; }; @@ -497,6 +517,7 @@ 2BA98970253F007600B643F6 /* mapbox-directions-swift */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = "mapbox-directions-swift"; sourceTree = BUILT_PRODUCTS_DIR; }; 2BBBD05D257E61ED004EB3D6 /* MapboxStreetsRoadClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapboxStreetsRoadClass.swift; sourceTree = ""; }; 2BBBD08C257FA1CD004EB3D6 /* BlockedLanes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedLanes.swift; sourceTree = ""; }; + 2BEA240327CFAD2000EE05D9 /* ForeignMemberContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForeignMemberContainer.swift; sourceTree = ""; }; 2BF398C427620CD7000C9A72 /* RouteRefreshSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteRefreshSource.swift; sourceTree = ""; }; 2E44711627C4C80B0041CB84 /* SilentWaypoint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SilentWaypoint.swift; sourceTree = ""; }; 3556CE9922649CF2009397B5 /* MapboxDirectionsTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "MapboxDirectionsTests-Bridging-Header.h"; path = "../objc/MapboxDirectionsTests-Bridging-Header.h"; sourceTree = ""; }; @@ -732,6 +753,8 @@ 8D381B5F1FD9F592008D5A58 /* Responses */ = { isa = PBXGroup; children = ( + 2B67F68127EDC2EF007BD6D2 /* RouteResponseWithForeignMembers.json */, + 2B46DB0B27EDF8580068C893 /* RouteRefreshResponseWithForeignMembers.json */, 8D381B601FD9F5B1008D5A58 /* noDestinationName.json */, 8D381B621FDB01D1008D5A58 /* apiDestinationName.json */, ); @@ -745,6 +768,7 @@ 43208BA62343F7C300D8BD89 /* Codable.swift */, 43208BA82343F7E900D8BD89 /* CoreLocation.swift */, 8D434678219E1167008B7BF3 /* Double.swift */, + 2BEA240327CFAD2000EE05D9 /* ForeignMemberContainer.swift */, 43208BAA2343F81900D8BD89 /* GeoJSON.swift */, 35DBF00E217E17A30009D2AE /* HTTPURLResponse.swift */, DAE7EA93230B5FD10003B211 /* Measurement.swift */, @@ -864,6 +888,7 @@ DAD06E34239F0B19001A917D /* DirectionsErrorTests.swift */, DA1A110A1D01045E009F82FA /* DirectionsTests.swift */, DA6C9DB11CAECA0E00094FBC /* Fixture.swift */, + 2B28E22227EDB2A90029E4C1 /* ForeignMemberContainerTests.swift */, DAABF7912395AE9800CEEB61 /* GeoJSONTests.swift */, DA6C9D9A1CAE442B00094FBC /* Info.plist */, DAE33A1A1F215DF600C06039 /* IntersectionTests.swift */, @@ -1266,6 +1291,7 @@ 2BA2E747257A667500D7AFC6 /* incidents.json in Resources */, DA1A10CF1D00F975009F82FA /* v5_driving_dc_polyline.json in Resources */, 2B54080A245B23BE006C820B /* incorrectRouteRefreshResponse.json in Resources */, + 2B46DB0D27EDF8580068C893 /* RouteRefreshResponseWithForeignMembers.json in Resources */, C5C0D63520586419003A3B1D /* null-tracepoint.json in Resources */, AEAB390E20D7F508008F4E54 /* subLaneInstructions.json in Resources */, 2B5F0DFD273ACE3B00CC2C1A /* tollAndFerryDirectionsRoute.json in Resources */, @@ -1277,6 +1303,7 @@ 35DBF01A217F38A30009D2AE /* versions.json in Resources */, 35D92FF1218203AB000C78CB /* 2018-10-16-Liechtenstein.tar in Resources */, AEAB391220D9469A008F4E54 /* subVisualInstructions.json in Resources */, + 2B67F68327EDC2EF007BD6D2 /* RouteResponseWithForeignMembers.json in Resources */, 2B540806245B09E1006C820B /* routeRefreshRoute.json in Resources */, DACCFCAA2225359600110FC9 /* v5_driving_oldenburg_polyline.json in Resources */, C5DAACB5201AA9A7001F9261 /* match.json in Resources */, @@ -1299,6 +1326,7 @@ 2BA2E748257A667500D7AFC6 /* incidents.json in Resources */, DA1A10F31D010251009F82FA /* v5_driving_dc_polyline.json in Resources */, 2B54080B245B23BE006C820B /* incorrectRouteRefreshResponse.json in Resources */, + 2B46DB0E27EDF8580068C893 /* RouteRefreshResponseWithForeignMembers.json in Resources */, C5C0D6362058641B003A3B1D /* null-tracepoint.json in Resources */, AEAB390F20D7F50A008F4E54 /* subLaneInstructions.json in Resources */, 2B5F0DFE273ACE3B00CC2C1A /* tollAndFerryDirectionsRoute.json in Resources */, @@ -1310,6 +1338,7 @@ 35DBF01B217F38A30009D2AE /* versions.json in Resources */, 35D92FF2218203AB000C78CB /* 2018-10-16-Liechtenstein.tar in Resources */, AEAB391320D9469A008F4E54 /* subVisualInstructions.json in Resources */, + 2B67F68427EDC2EF007BD6D2 /* RouteResponseWithForeignMembers.json in Resources */, 2B540807245B09E1006C820B /* routeRefreshRoute.json in Resources */, DACCFCAB2225359600110FC9 /* v5_driving_oldenburg_polyline.json in Resources */, C5DAACB6201AA9A7001F9261 /* match.json in Resources */, @@ -1339,6 +1368,7 @@ 2BA2E746257A667500D7AFC6 /* incidents.json in Resources */, DAC05F1C1CFC1E5300FA0071 /* v5_driving_dc_polyline.json in Resources */, 2B540809245B23BE006C820B /* incorrectRouteRefreshResponse.json in Resources */, + 2B46DB0C27EDF8580068C893 /* RouteRefreshResponseWithForeignMembers.json in Resources */, C5C0D6342058523E003A3B1D /* null-tracepoint.json in Resources */, AEAB390D20D7F4F4008F4E54 /* subLaneInstructions.json in Resources */, 2B5F0DFC273ACE3B00CC2C1A /* tollAndFerryDirectionsRoute.json in Resources */, @@ -1350,6 +1380,7 @@ 35D92FF0218203AB000C78CB /* 2018-10-16-Liechtenstein.tar in Resources */, AEAB391120D9469A008F4E54 /* subVisualInstructions.json in Resources */, DACCFCA92225359600110FC9 /* v5_driving_oldenburg_polyline.json in Resources */, + 2B67F68227EDC2EF007BD6D2 /* RouteResponseWithForeignMembers.json in Resources */, 2B540805245B09E1006C820B /* routeRefreshRoute.json in Resources */, C5DAACB4201AA9A7001F9261 /* match.json in Resources */, C5A3D3981E8188FE00D494A0 /* annotation.json in Resources */, @@ -1437,6 +1468,7 @@ C5990B4D2045E74800D7DFD4 /* DirectionsOptions.swift in Sources */, DAA76D691DD127CB0015EC78 /* LaneIndication.swift in Sources */, 2B9F388E272AE23A001DBA12 /* Credentials.swift in Sources */, + 2BEA240527CFAD2000EE05D9 /* ForeignMemberContainer.swift in Sources */, 43F89F942350F952007B591E /* MapMatchingResponse.swift in Sources */, DA1A10CB1D00F969009F82FA /* RouteStep.swift in Sources */, C57D55031DB566A700B94B74 /* Intersection.swift in Sources */, @@ -1485,6 +1517,7 @@ DA1A10CE1D00F972009F82FA /* Fixture.swift in Sources */, DAE2DF6923AECB120065057A /* QuickLookTests.swift in Sources */, DA1A110C1D01045E009F82FA /* DirectionsTests.swift in Sources */, + 2B28E22427EDB2AA0029E4C1 /* ForeignMemberContainerTests.swift in Sources */, C5D1D7F31F6AFBD600A1C4F1 /* VisualInstructionTests.swift in Sources */, DA4F84EE21C08BFB008A0434 /* WaypointTests.swift in Sources */, DAABF78F2395ABA900CEEB61 /* SpokenInstructionTests.swift in Sources */, @@ -1532,6 +1565,7 @@ C5990B4E2045E74900D7DFD4 /* DirectionsOptions.swift in Sources */, DAA76D6A1DD127CB0015EC78 /* LaneIndication.swift in Sources */, 2B9F388F272AE23A001DBA12 /* Credentials.swift in Sources */, + 2BEA240627CFAD2000EE05D9 /* ForeignMemberContainer.swift in Sources */, 43F89F952350F952007B591E /* MapMatchingResponse.swift in Sources */, DA1A10F11D010247009F82FA /* RouteStep.swift in Sources */, C57D55041DB566A800B94B74 /* Intersection.swift in Sources */, @@ -1580,6 +1614,7 @@ DA1A10F51D010251009F82FA /* Fixture.swift in Sources */, DAE2DF6A23AECB120065057A /* QuickLookTests.swift in Sources */, DA1A110D1D01045E009F82FA /* DirectionsTests.swift in Sources */, + 2B28E22527EDB2AA0029E4C1 /* ForeignMemberContainerTests.swift in Sources */, C5D1D7F41F6AFBD600A1C4F1 /* VisualInstructionTests.swift in Sources */, DA4F84EF21C08BFB008A0434 /* WaypointTests.swift in Sources */, DAABF7902395ABA900CEEB61 /* SpokenInstructionTests.swift in Sources */, @@ -1627,6 +1662,7 @@ C5990B4F2045E74A00D7DFD4 /* DirectionsOptions.swift in Sources */, DAA76D6B1DD127CB0015EC78 /* LaneIndication.swift in Sources */, 2B9F3890272AE23A001DBA12 /* Credentials.swift in Sources */, + 2BEA240727CFAD2000EE05D9 /* ForeignMemberContainer.swift in Sources */, 43F89F962350F952007B591E /* MapMatchingResponse.swift in Sources */, DA1A11081D0103A3009F82FA /* RouteStep.swift in Sources */, C57D55051DB566A900B94B74 /* Intersection.swift in Sources */, @@ -1688,6 +1724,7 @@ C59094C1203DE6BC00EB2417 /* DirectionsResult.swift in Sources */, DAC05F1A1CFC077C00FA0071 /* RouteLeg.swift in Sources */, 2B9F388D272AE23A001DBA12 /* Credentials.swift in Sources */, + 2BEA240427CFAD2000EE05D9 /* ForeignMemberContainer.swift in Sources */, 43F89F932350F952007B591E /* MapMatchingResponse.swift in Sources */, C5434B8A200693D00069E887 /* Tracepoint.swift in Sources */, DA6C9DA61CAE462800094FBC /* Directions.swift in Sources */, @@ -1736,6 +1773,7 @@ DA6C9DB21CAECA0E00094FBC /* Fixture.swift in Sources */, DAE2DF6823AECB120065057A /* QuickLookTests.swift in Sources */, DA1A110B1D01045E009F82FA /* DirectionsTests.swift in Sources */, + 2B28E22327EDB2AA0029E4C1 /* ForeignMemberContainerTests.swift in Sources */, C52CE3931F6AF6E70069963D /* VisualInstructionTests.swift in Sources */, DA4F84ED21C08BFB008A0434 /* WaypointTests.swift in Sources */, DAABF78E2395ABA900CEEB61 /* SpokenInstructionTests.swift in Sources */, diff --git a/Sources/MapboxDirections/AdministrativeRegion.swift b/Sources/MapboxDirections/AdministrativeRegion.swift index 330e312ad..cedd00ba3 100644 --- a/Sources/MapboxDirections/AdministrativeRegion.swift +++ b/Sources/MapboxDirections/AdministrativeRegion.swift @@ -1,4 +1,5 @@ import Foundation +import Turf /** `AdministrativeRegion` describes corresponding object on the route. @@ -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" @@ -29,6 +31,8 @@ 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 { @@ -36,5 +40,7 @@ public struct AdministrativeRegion: Codable, Equatable { try container.encode(countryCode, forKey: .countryCode) try container.encodeIfPresent(countryCodeAlpha3, forKey: .countryCodeAlpha3) + + try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) } } diff --git a/Sources/MapboxDirections/DirectionsResult.swift b/Sources/MapboxDirections/DirectionsResult.swift index ec276e056..91a52c0d9 100644 --- a/Sources/MapboxDirections/DirectionsResult.swift +++ b/Sources/MapboxDirections/DirectionsResult.swift @@ -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 @@ -64,6 +66,8 @@ open class DirectionsResult: Codable { } responseContainsSpeechLocale = container.contains(.speechLocale) + + try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) } @@ -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 diff --git a/Sources/MapboxDirections/Extensions/ForeignMemberContainer.swift b/Sources/MapboxDirections/Extensions/ForeignMemberContainer.swift new file mode 100644 index 000000000..39bff7601 --- /dev/null +++ b/Sources/MapboxDirections/Extensions/ForeignMemberContainer.swift @@ -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(notKeyedBy _: WellKnownCodingKeys.Type, with decoder: Decoder) throws where WellKnownCodingKeys: CodingKey { + 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(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(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(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) + } + } + } +} diff --git a/Sources/MapboxDirections/Incident.swift b/Sources/MapboxDirections/Incident.swift index 5bd48becf..97122b332 100644 --- a/Sources/MapboxDirections/Incident.swift +++ b/Sources/MapboxDirections/Incident.swift @@ -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" @@ -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 @@ -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 @@ -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 { @@ -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) } } diff --git a/Sources/MapboxDirections/Intersection.swift b/Sources/MapboxDirections/Intersection.swift index 371a4af94..2a41da3dc 100644 --- a/Sources/MapboxDirections/Intersection.swift +++ b/Sources/MapboxDirections/Intersection.swift @@ -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] = [] + // MARK: Creating an Intersection public init(location: LocationCoordinate2D, @@ -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" } @@ -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) } } @@ -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 { @@ -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 { @@ -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) } @@ -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) } } diff --git a/Sources/MapboxDirections/Lane.swift b/Sources/MapboxDirections/Lane.swift index 8549a45cb..54315e358 100644 --- a/Sources/MapboxDirections/Lane.swift +++ b/Sources/MapboxDirections/Lane.swift @@ -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. */ @@ -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 { @@ -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) } } diff --git a/Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift b/Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift index 84e6b3d9e..271b95228 100644 --- a/Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift +++ b/Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift @@ -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]? @@ -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) } } diff --git a/Sources/MapboxDirections/MapMatching/Match.swift b/Sources/MapboxDirections/MapMatching/Match.swift index 6ca383bbe..b3ded83d6 100644 --- a/Sources/MapboxDirections/MapMatching/Match.swift +++ b/Sources/MapboxDirections/MapMatching/Match.swift @@ -43,7 +43,7 @@ public enum Weight: Equatable { Typically, you do not create instances of this class directly. Instead, you receive match objects when you pass a `MatchOptions` object into the `Directions.calculate(_:completionHandler:)` method. */ open class Match: DirectionsResult { - private enum CodingKeys: String, CodingKey { + private enum CodingKeys: String, CodingKey, CaseIterable { case confidence case weight case weightName = "weight_name" @@ -82,6 +82,7 @@ open class Match: DirectionsResult { weight = Weight(value: weightValue, metric: weightMetric) try super.init(from: decoder) + try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) } public override func encode(to encoder: Encoder) throws { diff --git a/Sources/MapboxDirections/MapMatching/Tracepoint.swift b/Sources/MapboxDirections/MapMatching/Tracepoint.swift index 8d1b4f29f..0a6d01d53 100644 --- a/Sources/MapboxDirections/MapMatching/Tracepoint.swift +++ b/Sources/MapboxDirections/MapMatching/Tracepoint.swift @@ -10,7 +10,7 @@ public class Tracepoint: Waypoint { */ public let countOfAlternatives: Int - private enum CodingKeys: String, CodingKey { + private enum CodingKeys: String, CodingKey, CaseIterable { case countOfAlternatives = "alternatives_count" } @@ -23,6 +23,7 @@ public class Tracepoint: Waypoint { let container = try decoder.container(keyedBy: CodingKeys.self) countOfAlternatives = try container.decode(Int.self, forKey: .countOfAlternatives) try super.init(from: decoder) + try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) } public override func encode(to encoder: Encoder) throws { diff --git a/Sources/MapboxDirections/RefreshedRoute.swift b/Sources/MapboxDirections/RefreshedRoute.swift index c2415fed0..7e4067bea 100644 --- a/Sources/MapboxDirections/RefreshedRoute.swift +++ b/Sources/MapboxDirections/RefreshedRoute.swift @@ -1,9 +1,12 @@ import Foundation +import Turf /** A skeletal route containing only the information about the route that has been refreshed. */ -public struct RefreshedRoute { +public struct RefreshedRoute: ForeignMemberContainer { + public var foreignMembers: JSONObject = [:] + /** The legs along the route, starting at the first refreshed leg index. */ @@ -18,18 +21,24 @@ extension RefreshedRoute: Codable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) legs = try container.decode([RefreshedRouteLeg].self, forKey: .legs) + + try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(legs, forKey: .legs) + + try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) } } /** A skeletal route leg containing only the information about the route leg that has been refreshed. */ -public struct RefreshedRouteLeg { +public struct RefreshedRouteLeg: ForeignMemberContainer { + public var foreignMembers: JSONObject = [:] + public var attributes: RouteLeg.Attributes } @@ -41,10 +50,14 @@ extension RefreshedRouteLeg: Codable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) attributes = try container.decode(RouteLeg.Attributes.self, forKey: .attributes) + + try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(attributes, forKey: .attributes) + + try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) } } diff --git a/Sources/MapboxDirections/RestStop.swift b/Sources/MapboxDirections/RestStop.swift index 8d2b18cda..89f0927f3 100644 --- a/Sources/MapboxDirections/RestStop.swift +++ b/Sources/MapboxDirections/RestStop.swift @@ -1,9 +1,12 @@ import Foundation +import Turf /** A [rest stop](https://wiki.openstreetmap.org/wiki/Tag:highway%3Drest_area) along the route. */ -public struct RestStop: Codable, Equatable { +public struct RestStop: Codable, Equatable, ForeignMemberContainer { + public var foreignMembers: JSONObject = [:] + /// A kind of rest stop. public enum StopType: String, Codable { /// A primitive rest stop that provides parking but no additional services. @@ -45,4 +48,24 @@ public struct RestStop: Codable, Equatable { self.type = type self.name = name } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + type = try container.decode(StopType.self, forKey: .type) + name = try container.decodeIfPresent(String.self, forKey: .name) + + try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(type, forKey: .type) + try container.encodeIfPresent(name, forKey: .name) + + try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) + } + + public static func == (lhs: Self, rhs: Self) -> Bool { + return lhs.type == rhs.type && lhs.name == rhs.name + } } diff --git a/Sources/MapboxDirections/RouteLeg.swift b/Sources/MapboxDirections/RouteLeg.swift index 6ac03b18b..dbd8a3297 100644 --- a/Sources/MapboxDirections/RouteLeg.swift +++ b/Sources/MapboxDirections/RouteLeg.swift @@ -7,8 +7,11 @@ import Turf You do not create instances of this class directly. Instead, you receive route leg objects as part of route objects when you request directions using the `Directions.calculate(_:completionHandler:)` method. */ -open class RouteLeg: Codable { - public enum CodingKeys: String, CodingKey { +open class RouteLeg: Codable, ForeignMemberContainerClass { + public var foreignMembers: JSONObject = [:] + public var attributesForeignMembers: JSONObject = [:] + + public enum CodingKeys: String, CodingKey, CaseIterable { case source case destination case steps @@ -81,6 +84,7 @@ open class RouteLeg: Codable { if let attributes = try container.decodeIfPresent(Attributes.self, forKey: .annotation) { self.attributes = attributes + self.attributesForeignMembers = attributes.foreignMembers } if let incidents = try container.decodeIfPresent([Incident].self, forKey: .incidents) { @@ -90,6 +94,8 @@ open class RouteLeg: Codable { if let viaWaypoints = try container.decodeIfPresent([SilentWaypoint].self, forKey: .viaWaypoints) { self.viaWaypoints = viaWaypoints } + + try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) } public func encode(to encoder: Encoder) throws { @@ -103,8 +109,9 @@ open class RouteLeg: Codable { try container.encodeIfPresent(typicalTravelTime, forKey: .typicalTravelTime) try container.encode(profileIdentifier, forKey: .profileIdentifier) - let attributes = self.attributes + var attributes = self.attributes if !attributes.isEmpty { + attributes.foreignMembers = self.attributesForeignMembers try container.encode(attributes, forKey: .annotation) } @@ -119,6 +126,8 @@ open class RouteLeg: Codable { if let viaWaypoints = viaWaypoints { try container.encode(viaWaypoints, forKey: .viaWaypoints) } + + try encodeForeignMembers(to: encoder) } // MARK: Getting the Endpoints of the Leg diff --git a/Sources/MapboxDirections/RouteLegAttributes.swift b/Sources/MapboxDirections/RouteLegAttributes.swift index b61cbd51a..4d032c7da 100644 --- a/Sources/MapboxDirections/RouteLegAttributes.swift +++ b/Sources/MapboxDirections/RouteLegAttributes.swift @@ -5,7 +5,9 @@ extension RouteLeg { /** A collection of per-segment attributes along a route leg. */ - public struct Attributes: Equatable { + public struct Attributes: Equatable, ForeignMemberContainer { + public var foreignMembers: JSONObject = [:] + /** An array containing the distance (measured in meters) between each coordinate in the route leg geometry. @@ -90,6 +92,8 @@ extension RouteLeg.Attributes: Codable { } else { segmentMaximumSpeedLimits = nil } + + try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) } public func encode(to encoder: Encoder) throws { @@ -104,6 +108,8 @@ extension RouteLeg.Attributes: Codable { if let speedLimitDescriptors = segmentMaximumSpeedLimits?.map({ SpeedLimitDescriptor(speed: $0) }) { try container.encode(speedLimitDescriptors, forKey: .segmentMaximumSpeedLimits) } + + try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) } /** diff --git a/Sources/MapboxDirections/RouteRefreshResponse.swift b/Sources/MapboxDirections/RouteRefreshResponse.swift index 60baa16ca..822311852 100644 --- a/Sources/MapboxDirections/RouteRefreshResponse.swift +++ b/Sources/MapboxDirections/RouteRefreshResponse.swift @@ -2,11 +2,14 @@ import Foundation #if canImport(FoundationNetworking) import FoundationNetworking #endif +import Turf /** A Directions Refresh API response. */ -public struct RouteRefreshResponse { +public struct RouteRefreshResponse: ForeignMemberContainer { + public var foreignMembers: JSONObject = [:] + /** The raw HTTP response from the Directions Refresh API. */ @@ -83,6 +86,8 @@ extension RouteRefreshResponse: Codable { } else { throw DirectionsCodingError.missingOptions } + + try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) } public func encode(to encoder: Encoder) throws { @@ -90,6 +95,8 @@ extension RouteRefreshResponse: Codable { try container.encodeIfPresent(identifier, forKey: .identifier) try container.encode(route, forKey: .route) + + try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) } } diff --git a/Sources/MapboxDirections/RouteResponse.swift b/Sources/MapboxDirections/RouteResponse.swift index 89f964b6f..ffaf59921 100644 --- a/Sources/MapboxDirections/RouteResponse.swift +++ b/Sources/MapboxDirections/RouteResponse.swift @@ -1,4 +1,5 @@ import Foundation +import Turf #if canImport(FoundationNetworking) import FoundationNetworking #endif @@ -8,7 +9,9 @@ public enum ResponseOptions { case match(MatchOptions) } -public struct RouteResponse { +public struct RouteResponse: ForeignMemberContainer { + public var foreignMembers: JSONObject = [:] + public let httpResponse: HTTPURLResponse? public let identifier: String? @@ -43,7 +46,6 @@ public struct RouteResponse { extension RouteResponse: Codable { enum CodingKeys: String, CodingKey { - case code case message case error case identifier = "uuid" @@ -128,13 +130,15 @@ extension RouteResponse: Codable { let waypoint = Waypoint(coordinate: decodedWaypoint.coordinate, coordinateAccuracy: waypointInOptions.coordinateAccuracy, name: waypointInOptions.name?.nonEmptyString ?? decodedWaypoint.name) - + waypoint.snappedDistance = decodedWaypoint.snappedDistance waypoint.targetCoordinate = waypointInOptions.targetCoordinate waypoint.heading = waypointInOptions.heading waypoint.headingAccuracy = waypointInOptions.headingAccuracy waypoint.separatesLegs = waypointInOptions.separatesLegs waypoint.allowsArrivingOnOppositeSide = waypointInOptions.allowsArrivingOnOppositeSide + waypoint.foreignMembers = decodedWaypoint.foreignMembers + return waypoint } waypoints?.first?.separatesLegs = true @@ -157,6 +161,8 @@ extension RouteResponse: Codable { } updateRoadClassExclusionViolations() + + try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) } public func encode(to encoder: Encoder) throws { @@ -164,6 +170,8 @@ extension RouteResponse: Codable { try container.encodeIfPresent(identifier, forKey: .identifier) try container.encodeIfPresent(routes, forKey: .routes) try container.encodeIfPresent(waypoints, forKey: .waypoints) + + try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) } } diff --git a/Sources/MapboxDirections/RouteStep.swift b/Sources/MapboxDirections/RouteStep.swift index 82e1e0636..fce50b070 100644 --- a/Sources/MapboxDirections/RouteStep.swift +++ b/Sources/MapboxDirections/RouteStep.swift @@ -320,7 +320,7 @@ struct Road { init(name: String, ref: String?, exits: String?, destination: String?, rotaryName: String?) { if !name.isEmpty, let ref = ref { - // Mapbox Directions API v5 encodes the ref separately from the name but redundantly includes the ref in the name for backwards compatibility. Remove the ref from the name. + // Directions API v5 profiles powered by Valhalla no longer include the ref in the name. However, the `mapbox/cycling` profile, which is powered by OSRM, still includes the ref. let parenthetical = "(\(ref))" if name == ref { self.names = nil @@ -348,7 +348,7 @@ struct Road { } extension Road: Codable { - private enum CodingKeys: String, CodingKey { + enum CodingKeys: String, CodingKey, CaseIterable { case name case ref case exits @@ -377,7 +377,7 @@ extension Road: Codable { } try container.encodeIfPresent(name, forKey: .name) } else { - try container.encodeIfPresent(ref, forKey: .name) + try container.encode(ref ?? "", forKey: .name) } if var destinations = destinations?.tagValues(joinedBy: ",") { @@ -398,8 +398,11 @@ extension Road: Codable { You do not create instances of this class directly. Instead, you receive route step objects as part of route objects when you request directions using the `Directions.calculate(_:completionHandler:)` method, setting the `includesSteps` option to `true` in the `RouteOptions` object that you pass into that method. */ -open class RouteStep: Codable { - private enum CodingKeys: String, CodingKey { +open class RouteStep: Codable, ForeignMemberContainerClass { + public var foreignMembers: JSONObject = [:] + public var maneuverForeignMembers: JSONObject = [:] + + private enum CodingKeys: String, CodingKey, CaseIterable { case shape = "geometry" case distance case drivingSide = "driving_side" @@ -417,14 +420,77 @@ open class RouteStep: Codable { case transportType = "mode" } - private enum ManeuverCodingKeys: String, CodingKey { - case instruction - case location - case type - case exitIndex = "exit" - case direction = "modifier" - case initialHeading = "bearing_before" - case finalHeading = "bearing_after" + private struct Maneuver: Codable, ForeignMemberContainer { + var foreignMembers: JSONObject = [:] + + private enum CodingKeys: String, CodingKey { + case instruction + case location + case type + case exitIndex = "exit" + case direction = "modifier" + case initialHeading = "bearing_before" + case finalHeading = "bearing_after" + } + + let instructions: String + let maneuverType: ManeuverType + let maneuverDirection: ManeuverDirection? + let maneuverLocation: Turf.LocationCoordinate2D + let initialHeading: Turf.LocationDirection? + let finalHeading: Turf.LocationDirection? + let exitIndex: Int? + + init(instructions: String, + maneuverType: ManeuverType, + maneuverDirection: ManeuverDirection?, + maneuverLocation: Turf.LocationCoordinate2D, + initialHeading: Turf.LocationDirection?, + finalHeading: Turf.LocationDirection?, + exitIndex: Int?) { + self.instructions = instructions + self.maneuverType = maneuverType + self.maneuverLocation = maneuverLocation + self.maneuverDirection = maneuverDirection + self.initialHeading = initialHeading + self.finalHeading = finalHeading + self.exitIndex = exitIndex + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + maneuverLocation = try container.decode(LocationCoordinate2DCodable.self, forKey: .location).decodedCoordinates + maneuverType = (try? container.decode(ManeuverType.self, forKey: .type)) ?? .default + maneuverDirection = try container.decodeIfPresent(ManeuverDirection.self, forKey: .direction) + exitIndex = try container.decodeIfPresent(Int.self, forKey: .exitIndex) + + initialHeading = try container.decodeIfPresent(Turf.LocationDirection.self, forKey: .initialHeading) + finalHeading = try container.decodeIfPresent(Turf.LocationDirection.self, forKey: .finalHeading) + + if let instruction = try? container.decode(String.self, forKey: .instruction) { + instructions = instruction + } else { + instructions = "\(maneuverType) \(maneuverDirection?.rawValue ?? "")" + } + + try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(instructions, forKey: .instruction) + try container.encode(maneuverType, forKey: .type) + try container.encodeIfPresent(exitIndex, forKey: .exitIndex) + + try container.encodeIfPresent(maneuverDirection, forKey: .direction) + try container.encode(LocationCoordinate2DCodable(maneuverLocation), forKey: .location) + try container.encodeIfPresent(initialHeading, forKey: .initialHeading) + try container.encodeIfPresent(finalHeading, forKey: .finalHeading) + + try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) + } } // MARK: Creating a Step @@ -526,21 +592,23 @@ open class RouteStep: Codable { try container.encode(polyLineString, forKey: .shape) } - var maneuver = container.nestedContainer(keyedBy: ManeuverCodingKeys.self, forKey: .maneuver) - try maneuver.encode(instructions, forKey: .instruction) - try maneuver.encode(maneuverType, forKey: .type) - try maneuver.encodeIfPresent(exitIndex, forKey: .exitIndex) - - try maneuver.encodeIfPresent(maneuverDirection, forKey: .direction) - try maneuver.encode(LocationCoordinate2DCodable(maneuverLocation), forKey: .location) - try maneuver.encodeIfPresent(initialHeading, forKey: .initialHeading) - try maneuver.encodeIfPresent(finalHeading, forKey: .finalHeading) + var maneuver = Maneuver(instructions: instructions, + maneuverType: maneuverType, + maneuverDirection: maneuverDirection, + maneuverLocation: maneuverLocation, + initialHeading: initialHeading, + finalHeading: finalHeading, + exitIndex: exitIndex) + maneuver.foreignMembers = maneuverForeignMembers + try container.encode(maneuver, forKey: .maneuver) try container.encodeIfPresent(speedLimitSignStandard, forKey: .speedLimitSignStandard) if let speedLimitUnit = speedLimitUnit, let unit = SpeedLimitDescriptor.UnitDescriptor(unit: speedLimitUnit) { try container.encode(unit, forKey: .speedLimitUnit) } + + try encodeForeignMembers(to: encoder) } static func decode(from decoder: Decoder, administrativeRegions: [AdministrativeRegion]) throws -> [RouteStep] { @@ -559,20 +627,42 @@ open class RouteStep: Codable { /// Used to Decode `Intersection.admin_index` private struct AdministrativeAreaIndex: Codable { + private enum CodingKeys: String, CodingKey { case administrativeRegionIndex = "admin_index" } var administrativeRegionIndex: Int? + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + administrativeRegionIndex = try container.decodeIfPresent(Int.self, forKey: .administrativeRegionIndex) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encodeIfPresent(administrativeRegionIndex, forKey: .administrativeRegionIndex) + } } /// Used to Decode `Intersection.geometry_index` private struct IntersectionShapeIndex: Codable { + private enum CodingKeys: String, CodingKey { case geometryIndex = "geometry_index" } let geometryIndex: Int? + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + geometryIndex = try container.decodeIfPresent(Int.self, forKey: .geometryIndex) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encodeIfPresent(geometryIndex, forKey: .geometryIndex) + } } @@ -582,15 +672,17 @@ open class RouteStep: Codable { init(from decoder: Decoder, administrativeRegions: [AdministrativeRegion]?) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - let maneuver = try container.nestedContainer(keyedBy: ManeuverCodingKeys.self, forKey: .maneuver) - maneuverLocation = try maneuver.decode(LocationCoordinate2DCodable.self, forKey: .location).decodedCoordinates - maneuverType = (try? maneuver.decode(ManeuverType.self, forKey: .type)) ?? .default - maneuverDirection = try maneuver.decodeIfPresent(ManeuverDirection.self, forKey: .direction) - exitIndex = try maneuver.decodeIfPresent(Int.self, forKey: .exitIndex) - - initialHeading = try maneuver.decodeIfPresent(Turf.LocationDirection.self, forKey: .initialHeading) - finalHeading = try maneuver.decodeIfPresent(Turf.LocationDirection.self, forKey: .finalHeading) + let maneuver = try container.decode(Maneuver.self, forKey: .maneuver) + + maneuverLocation = maneuver.maneuverLocation + maneuverType = maneuver.maneuverType + maneuverDirection = maneuver.maneuverDirection + exitIndex = maneuver.exitIndex + initialHeading = maneuver.initialHeading + finalHeading = maneuver.finalHeading + instructions = maneuver.instructions + maneuverForeignMembers = maneuver.foreignMembers if let polyLineString = try container.decodeIfPresent(PolyLineString.self, forKey: .shape) { shape = try LineString(polyLineString: polyLineString) @@ -598,11 +690,6 @@ open class RouteStep: Codable { shape = nil } - if let instruction = try? maneuver.decode(String.self, forKey: .instruction) { - instructions = instruction - } else { - instructions = "\(maneuverType) \(maneuverDirection?.rawValue ?? "")" - } drivingSide = try container.decode(DrivingSide.self, forKey: .drivingSide) instructionsSpokenAlongStep = try container.decodeIfPresent([SpokenInstruction].self, forKey: .instructionsSpokenAlongStep) @@ -663,6 +750,9 @@ open class RouteStep: Codable { exitNames = nil phoneticExitNames = nil } + + try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) + try decodeForeignMembers(notKeyedBy: Road.CodingKeys.self, with: decoder) } // MARK: Getting the Shape of the Step diff --git a/Sources/MapboxDirections/SilentWaypoint.swift b/Sources/MapboxDirections/SilentWaypoint.swift index 1c1afdfcb..240192b42 100644 --- a/Sources/MapboxDirections/SilentWaypoint.swift +++ b/Sources/MapboxDirections/SilentWaypoint.swift @@ -1,11 +1,14 @@ import Foundation +import Turf /** Represents a silent waypoint along the `RouteLeg`. See `RouteLeg.viaWaypoints` for more details. */ -public struct SilentWaypoint: Codable, Equatable { +public struct SilentWaypoint: Codable, Equatable, ForeignMemberContainer { + public var foreignMembers: JSONObject = [:] + public enum CodingKeys: String, CodingKey { case waypointIndex = "waypoint_index" case distanceFromStart = "distance_from_start" @@ -32,5 +35,16 @@ public struct SilentWaypoint: Codable, Equatable { waypointIndex = try container.decode(Int.self, forKey: .waypointIndex) distanceFromStart = try container.decode(Double.self, forKey: .distanceFromStart) shapeCoordinateIndex = try container.decode(Int.self, forKey: .shapeCoordinateIndex) + + try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(waypointIndex, forKey: .waypointIndex) + try container.encode(distanceFromStart, forKey: .distanceFromStart) + try container.encode(shapeCoordinateIndex, forKey: .shapeCoordinateIndex) + + try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) } } diff --git a/Sources/MapboxDirections/SpokenInstruction.swift b/Sources/MapboxDirections/SpokenInstruction.swift index c886a7e31..100b0b2d9 100644 --- a/Sources/MapboxDirections/SpokenInstruction.swift +++ b/Sources/MapboxDirections/SpokenInstruction.swift @@ -8,8 +8,10 @@ import Turf The `distanceAlongStep` property is measured from the beginning of the step associated with this object. By contrast, the `text` and `ssmlText` properties refer to the details in the following step. It is also possible for the instruction to refer to two following steps simultaneously when needed for safe navigation. */ -open class SpokenInstruction: Codable { - private enum CodingKeys: String, CodingKey { +open class SpokenInstruction: Codable, ForeignMemberContainerClass { + public var foreignMembers: JSONObject = [:] + + private enum CodingKeys: String, CodingKey, CaseIterable { case distanceAlongStep = "distanceAlongGeometry" case text = "announcement" case ssmlText = "ssmlAnnouncement" @@ -30,6 +32,24 @@ open class SpokenInstruction: Codable { self.ssmlText = ssmlText } + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + distanceAlongStep = try container.decode(LocationDistance.self, forKey: .distanceAlongStep) + text = try container.decode(String.self, forKey: .text) + ssmlText = try container.decode(String.self, forKey: .ssmlText) + + try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) + } + + open func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(distanceAlongStep, forKey: .distanceAlongStep) + try container.encode(text, forKey: .text) + try container.encode(ssmlText, forKey: .ssmlText) + + try encodeForeignMembers(to: encoder) + } + // MARK: Timing When to Say the Instruction /** diff --git a/Sources/MapboxDirections/TollCollection.swift b/Sources/MapboxDirections/TollCollection.swift index 9872bf279..a06433470 100644 --- a/Sources/MapboxDirections/TollCollection.swift +++ b/Sources/MapboxDirections/TollCollection.swift @@ -1,9 +1,11 @@ import Foundation +import Turf /** `TollCollection` describes corresponding object on the route. */ -public struct TollCollection: Codable, Equatable { +public struct TollCollection: Codable, Equatable, ForeignMemberContainer { + public var foreignMembers: JSONObject = [:] public enum CollectionType: String, Codable { case booth = "toll_booth" @@ -22,4 +24,22 @@ public struct TollCollection: Codable, Equatable { public init(type: CollectionType) { self.type = type } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + type = try container.decode(CollectionType.self, forKey: .type) + + try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(type, forKey: .type) + + try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) + } + + public static func == (lhs: Self, rhs: Self) -> Bool { + return lhs.type == rhs.type + } } diff --git a/Sources/MapboxDirections/VisualInstruction.swift b/Sources/MapboxDirections/VisualInstruction.swift index fe8ca0080..de236b3df 100644 --- a/Sources/MapboxDirections/VisualInstruction.swift +++ b/Sources/MapboxDirections/VisualInstruction.swift @@ -4,10 +4,12 @@ import Turf /** The contents of a banner that should be displayed as added visual guidance for a route. The banner instructions are children of the steps during which they should be displayed, but they refer to the maneuver in the following step. */ -open class VisualInstruction: Codable { +open class VisualInstruction: Codable, ForeignMemberContainerClass { + public var foreignMembers: JSONObject = [:] + // MARK: Creating a Visual Instruction - private enum CodingKeys: String, CodingKey { + private enum CodingKeys: String, CodingKey, CaseIterable { case text case maneuverType = "type" case maneuverDirection = "modifier" @@ -33,6 +35,8 @@ open class VisualInstruction: Codable { try container.encodeIfPresent(maneuverDirection, forKey: .maneuverDirection) try container.encode(components, forKey: .components) try container.encodeIfPresent(finalHeading, forKey: .finalHeading) + + try encodeForeignMembers(to: encoder) } public required init(from decoder: Decoder) throws { @@ -42,6 +46,8 @@ open class VisualInstruction: Codable { maneuverDirection = try container.decodeIfPresent(ManeuverDirection.self, forKey: .maneuverDirection) components = try container.decode([Component].self, forKey: .components) finalHeading = try container.decodeIfPresent(LocationDegrees.self, forKey: .finalHeading) + + try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) } // MARK: Displaying the Instruction Text diff --git a/Sources/MapboxDirections/VisualInstructionBanner.swift b/Sources/MapboxDirections/VisualInstructionBanner.swift index 9d2b7c4ed..cd78a6e05 100644 --- a/Sources/MapboxDirections/VisualInstructionBanner.swift +++ b/Sources/MapboxDirections/VisualInstructionBanner.swift @@ -8,8 +8,10 @@ internal extension CodingUserInfoKey { /** A visual instruction banner contains all the information necessary for creating a visual cue about a given `RouteStep`. */ -open class VisualInstructionBanner: Codable { - private enum CodingKeys: String, CodingKey { +open class VisualInstructionBanner: Codable, ForeignMemberContainerClass { + public var foreignMembers: JSONObject = [:] + + private enum CodingKeys: String, CodingKey, CaseIterable { case distanceAlongStep = "distanceAlongGeometry" case primaryInstruction = "primary" case secondaryInstruction = "secondary" @@ -40,6 +42,8 @@ open class VisualInstructionBanner: Codable { try container.encodeIfPresent(tertiaryInstruction, forKey: .tertiaryInstruction) try container.encodeIfPresent(quaternaryInstruction, forKey: .quaternaryInstruction) try container.encode(drivingSide, forKey: .drivingSide) + + try encodeForeignMembers(to: encoder) } required public init(from decoder: Decoder) throws { @@ -54,6 +58,8 @@ open class VisualInstructionBanner: Codable { } else { drivingSide = .default } + + try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) } // MARK: Timing When to Display the Banner diff --git a/Sources/MapboxDirections/Waypoint.swift b/Sources/MapboxDirections/Waypoint.swift index e12ef0d7b..5df5ed8cd 100644 --- a/Sources/MapboxDirections/Waypoint.swift +++ b/Sources/MapboxDirections/Waypoint.swift @@ -6,8 +6,10 @@ import Turf /** A `Waypoint` object indicates a location along a route. It may be the route’s origin or destination, or it may be another location that the route visits. A waypoint object indicates the location’s geographic location along with other optional information, such as a name or the user’s direction approaching the waypoint. You create a `RouteOptions` object using waypoint objects and also receive waypoint objects in the completion handler of the `Directions.calculate(_:completionHandler:)` method. */ -public class Waypoint: Codable { - private enum CodingKeys: String, CodingKey { +public class Waypoint: Codable, ForeignMemberContainerClass { + public var foreignMembers: JSONObject = [:] + + private enum CodingKeys: String, CodingKey, CaseIterable { case coordinate = "location" case coordinateAccuracy case targetCoordinate @@ -50,21 +52,25 @@ public class Waypoint: Codable { } snappedDistance = try container.decodeIfPresent(LocationDistance.self, forKey: .snappedDistance) + + try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(LocationCoordinate2DCodable(coordinate), forKey: .coordinate) - try container.encode(coordinateAccuracy, forKey: .coordinateAccuracy) + try container.encodeIfPresent(coordinateAccuracy, forKey: .coordinateAccuracy) let targetCoordinateCodable = targetCoordinate != nil ? LocationCoordinate2DCodable(targetCoordinate!) : nil - try container.encode(targetCoordinateCodable, forKey: .targetCoordinate) + try container.encodeIfPresent(targetCoordinateCodable, forKey: .targetCoordinate) try container.encodeIfPresent(heading, forKey: .heading) try container.encodeIfPresent(headingAccuracy, forKey: .headingAccuracy) try container.encodeIfPresent(separatesLegs, forKey: .separatesLegs) try container.encodeIfPresent(allowsArrivingOnOppositeSide, forKey: .allowsArrivingOnOppositeSide) try container.encodeIfPresent(name, forKey: .name) try container.encodeIfPresent(snappedDistance, forKey: .snappedDistance) + + try encodeForeignMembers(to: encoder) } /** diff --git a/Tests/MapboxDirectionsTests/Fixtures/Responses/RouteRefreshResponseWithForeignMembers.json b/Tests/MapboxDirectionsTests/Fixtures/Responses/RouteRefreshResponseWithForeignMembers.json new file mode 100644 index 000000000..cb1780d5c --- /dev/null +++ b/Tests/MapboxDirectionsTests/Fixtures/Responses/RouteRefreshResponseWithForeignMembers.json @@ -0,0 +1,252 @@ +{ + "code": "Ok", + "foreignRouteRefreshMember": "value", + "route": { + "legs": [ + { + "foreignLegMember": { + "foreignNestedMember": true + }, + "annotation": { + "foreignAnnotationMember": [0, 1], + "maxspeed": [ + { + "speed": 90, + "unit": "km/h" + }, + { + "speed": 90, + "unit": "km/h" + }, + { + "speed": 90, + "unit": "km/h" + }, + { + "speed": 90, + "unit": "km/h" + }, + { + "speed": 90, + "unit": "km/h" + }, + { + "speed": 90, + "unit": "km/h" + }, + { + "speed": 90, + "unit": "km/h" + }, + { + "speed": 90, + "unit": "km/h" + }, + { + "speed": 90, + "unit": "km/h" + }, + { + "speed": 90, + "unit": "km/h" + }, + { + "speed": 90, + "unit": "km/h" + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "speed": 90, + "unit": "km/h" + }, + { + "speed": 90, + "unit": "km/h" + }, + { + "speed": 90, + "unit": "km/h" + }, + { + "speed": 90, + "unit": "km/h" + }, + { + "speed": 90, + "unit": "km/h" + }, + { + "speed": 90, + "unit": "km/h" + }, + { + "speed": 90, + "unit": "km/h" + }, + { + "speed": 90, + "unit": "km/h" + }, + { + "speed": 90, + "unit": "km/h" + }, + { + "unknown": true + } + ], + "congestion": [ + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown" + ], + "duration": [ + 3.641, + 5.035, + 1.324, + 3.207, + 1.137, + 2.27, + 4.306, + 4.33, + 0.331, + 5.211, + 5.172, + 12.242, + 2.538, + 6.457, + 42.889, + 23.634, + 6.343, + 1.127, + 2.068, + 3.473, + 8.123, + 3.215, + 3.644, + 1.123, + 1.423, + 4.156, + 8.165, + 4.668, + 9.853, + 4.768, + 4.527, + 1.818, + 0.32, + 4.185, + 4.163, + 2.194, + 1.099, + 3.1, + 1.324, + 5.035, + 4.077, + 0.905 + ] + } + } + ] + } +} diff --git a/Tests/MapboxDirectionsTests/Fixtures/Responses/RouteResponseWithForeignMembers.json b/Tests/MapboxDirectionsTests/Fixtures/Responses/RouteResponseWithForeignMembers.json new file mode 100644 index 000000000..b7d21be5f --- /dev/null +++ b/Tests/MapboxDirectionsTests/Fixtures/Responses/RouteResponseWithForeignMembers.json @@ -0,0 +1,341 @@ +{ + "routes": [ + { + "weight_typical": 9.797, + "duration_typical": 3.2, + "weight_name": "auto", + "weight": 9.797, + "duration": 3.2, + "distance": 14.7, + "legs": [ + { + "via_waypoints": [ + { + "waypoint_index": 1, + "distance_from_start": 610.733, + "geometry_index": 21, + "foreignSilentWaypointMember": -1 + } + ], + "annotation": { + "distance": [ + 0 + ], + "duration": [ + 0 + ], + "speed": [ + 0 + ], + "congestion": [ + "severe" + ], + "congestion_numeric": [ + 100 + ], + "foreignAttributesMember": 0 + }, + "incidents": [ + { + "id": "12727074056824787215", + "type": "miscellaneous", + "description": "Desc.", + "long_description": "Long Desc", + "creation_time": "2020-11-04T09:51:00Z", + "start_time": "2020-11-04T07:07:50Z", + "end_time": "2020-11-04T14:00:00Z", + "impact": "minor", + "alertc_codes": [ + 1 + ], + "lanes_blocked": [ + "RIGHT", + "SIDE" + ], + "geometry_index_start": 353, + "geometry_index_end": 367, + "foreignIncidentMember": 381 + } + ], + "foreignLegMember1": 1, + "foreignLegMember2": 2, + "admins": [ + { + "iso_3166_1_alpha3": "BLR", + "iso_3166_1": "BY", + "foreignAdminMember": "value" + } + ], + "weight_typical": 9.797, + "duration_typical": 3.2, + "weight": 9.797, + "duration": 3.2, + "steps": [ + { + "foreignStepMember": true, + "voiceInstructions": [ + { + "ssmlAnnouncement": "Drive north. Then Turn right.", + "announcement": "Drive north. Then Turn right.", + "distanceAlongGeometry": 7.569, + "foreignVoiceInstructionsMember": [ + "array1", + "array2" + ] + } + ], + "intersections": [ + { + "foreignIntersectionsMember": 123, + "bearings": [ + 23 + ], + "entry": [ + true + ], + "mapbox_streets_v8": { + "class": "primary" + }, + "is_urban": false, + "admin_index": 0, + "out": 0, + "geometry_index": 0, + "location": [ + 27.258011, + 53.987196 + ], + "toll_collection": { + "type": "toll_booth", + "foreignTollMember": false + }, + "lanes": [ + { + "valid": true, + "active": false, + "valid_indication": "straight", + "indications": [ + "straight" + ], + "foreignLaneMember": 100 + }, + { + "valid": true, + "active": true, + "valid_indication": "straight", + "indications": [ + "right", + "straight" + ] + } + ], + "rest_stop": { + "type": "service_area", + "foreignRestStopMember": "test" + } + } + ], + "maneuver": { + "type": "depart", + "instruction": "Drive north on Р65/Заславль — Дзержинск — Озеро/Заслаўе — Дзяржынск — Возера.", + "bearing_after": 23, + "bearing_before": 0, + "location": [ + 27.258011, + 53.987196 + ] + }, + "name": "Заславль — Дзержинск — Озеро; Заслаўе — Дзяржынск — Возера (Р65)", + "weight_typical": 0.454, + "duration_typical": 0.5, + "duration": 0.5, + "distance": 7.6, + "driving_side": "right", + "weight": 0.454, + "mode": "driving", + "ref": "Р65", + "geometry": { + "coordinates": [ + [ + 27.258011, + 53.987196 + ], + [ + 27.258057, + 53.987258 + ] + ], + "type": "LineString" + } + }, + { + "voiceInstructions": [ + { + "ssmlAnnouncement": "You have arrived at your destination.", + "announcement": "You have arrived at your destination.", + "distanceAlongGeometry": 7.152 + } + ], + "intersections": [ + { + "bearings": [ + 30, + 108, + 203 + ], + "entry": [ + true, + true, + false + ], + "in": 2, + "turn_weight": 8, + "turn_duration": 1.636, + "mapbox_streets_v8": { + "class": "street" + }, + "is_urban": false, + "admin_index": 0, + "out": 1, + "geometry_index": 1, + "location": [ + 27.258057, + 53.987258 + ] + } + ], + "maneuver": { + "type": "turn", + "instruction": "Turn right.", + "modifier": "right", + "bearing_after": 108, + "bearing_before": 23, + "location": [ + 27.258057, + 53.987258 + ] + }, + "name": "", + "weight_typical": 9.343, + "duration_typical": 2.7, + "duration": 2.7, + "distance": 7.1, + "driving_side": "right", + "weight": 9.343, + "mode": "driving", + "geometry": { + "coordinates": [ + [ + 27.258057, + 53.987258 + ], + [ + 27.258161, + 53.987238 + ] + ], + "type": "LineString" + } + }, + { + "voiceInstructions": [], + "intersections": [ + { + "bearings": [ + 288 + ], + "entry": [ + true + ], + "in": 0, + "admin_index": 0, + "geometry_index": 2, + "location": [ + 27.258161, + 53.987238 + ] + } + ], + "maneuver": { + "type": "arrive", + "instruction": "You have arrived at your destination.", + "bearing_after": 0, + "bearing_before": 108, + "location": [ + 27.258161, + 53.987238 + ] + }, + "name": "", + "weight_typical": 0, + "duration_typical": 0, + "duration": 0, + "distance": 0, + "driving_side": "right", + "weight": 0, + "mode": "driving", + "geometry": { + "coordinates": [ + [ + 27.258161, + 53.987238 + ], + [ + 27.258161, + 53.987238 + ] + ], + "type": "LineString" + } + } + ], + "distance": 14.7, + "summary": "Р65" + } + ], + "geometry": { + "coordinates": [ + [ + 27.258011, + 53.987196 + ], + [ + 27.258057, + 53.987258 + ], + [ + 27.258161, + 53.987238 + ] + ], + "type": "LineString" + }, + "voiceLocale": "en-US", + "foreignRouteMember": 12.34 + } + ], + "waypoints": [ + { + "distance": 0.485, + "name": "Р65", + "location": [ + 27.258011, + 53.987196 + ], + "foreignWaypointMember": { + "nestedForeignWaypointMember": 500 + } + }, + { + "distance": 0.329, + "name": "Destination", + "location": [ + 27.258161, + 53.987238 + ], + "foreignWaypointMember": "test" + } + ], + "code": "Ok", + "uuid": "SRu9rntV0omDbRaFpz4IK7kUefE9u_pNoO7QBAUGTKl3w7QulUvPmw==", + "foreignRouteResponseMember": "theValue" +} diff --git a/Tests/MapboxDirectionsTests/ForeignMemberContainerTests.swift b/Tests/MapboxDirectionsTests/ForeignMemberContainerTests.swift new file mode 100644 index 000000000..0e0ab28e2 --- /dev/null +++ b/Tests/MapboxDirectionsTests/ForeignMemberContainerTests.swift @@ -0,0 +1,102 @@ +import XCTest +import Foundation +import Turf +@testable import MapboxDirections + +class ForeignMemberContainerTests: XCTestCase { + + func testRouteRefreshForeignMembersCoding() { + guard let fixtureURL = Bundle.module.url(forResource: "RouteRefreshResponseWithForeignMembers", + withExtension:"json") else { + XCTFail() + return + } + guard let fixtureData = try? Data(contentsOf: fixtureURL, options:.mappedIfSafe) else { + XCTFail() + return + } + + var fixtureJSON: [String: Any?]? + XCTAssertNoThrow(fixtureJSON = try JSONSerialization.jsonObject(with: fixtureData, options: []) as? [String: Any?]) + + let decoder = JSONDecoder() + decoder.userInfo[.credentials] = BogusCredentials + decoder.userInfo[.responseIdentifier] = "bogusId" + decoder.userInfo[.routeIndex] = 0 + decoder.userInfo[.startLegIndex] = 0 + var response: RouteRefreshResponse? + XCTAssertNoThrow(response = try decoder.decode(RouteRefreshResponse.self, from: fixtureData)) + + let encoder = JSONEncoder() + var encodedResponse: Data? + var encodedRouteRefreshJSON: [String: Any?]? + + XCTAssertNoThrow(encodedResponse = try encoder.encode(response)) + XCTAssertNoThrow(encodedRouteRefreshJSON = try JSONSerialization.jsonObject(with: encodedResponse!, options: []) as? [String: Any?]) + XCTAssertNotNil(encodedRouteRefreshJSON) + + // Remove default keys not found in the original API response. + encodedRouteRefreshJSON?.removeValue(forKey: "uuid") + + XCTAssertTrue(JSONSerialization.objectsAreEqual(fixtureJSON, encodedRouteRefreshJSON, approximate: true)) + } + + func testRouteResponseForeignMembersCoding() { + guard let fixtureURL = Bundle.module.url(forResource: "RouteResponseWithForeignMembers", + withExtension:"json") else { + XCTFail() + return + } + guard let fixtureData = try? Data(contentsOf: fixtureURL, options:.mappedIfSafe) else { + XCTFail() + return + } + + var fixtureJSON: [String: Any?]? + XCTAssertNoThrow(fixtureJSON = try JSONSerialization.jsonObject(with: fixtureData, options: []) as? [String: Any?]) + + let options = RouteOptions(coordinates: [.init(latitude: 0, + longitude: 0), + .init(latitude: 1, + longitude: 1)]) + options.shapeFormat = .geoJSON + let decoder = JSONDecoder() + decoder.userInfo[.options] = options + decoder.userInfo[.credentials] = BogusCredentials + var response: RouteResponse? + XCTAssertNoThrow(response = try decoder.decode(RouteResponse.self, from: fixtureData)) + + let encoder = JSONEncoder() + encoder.userInfo[.options] = options + encoder.userInfo[.credentials] = BogusCredentials + + var encodedResponse: Data? + var encodedRouteResponseJSON: [String: Any?]? + + XCTAssertNoThrow(encodedResponse = try encoder.encode(response)) + XCTAssertNoThrow(encodedRouteResponseJSON = try JSONSerialization.jsonObject(with: encodedResponse!, options: []) as? [String: Any?]) + XCTAssertNotNil(encodedRouteResponseJSON) + + // Remove default keys not found in the original API response. + if var encodedRoutesJSON = encodedRouteResponseJSON?["routes"] as? [[String: Any?]] { + if var encodedLegJSON = encodedRoutesJSON[0]["legs"] as? [[String: Any?]] { + encodedLegJSON[0].removeValue(forKey: "source") + encodedLegJSON[0].removeValue(forKey: "destination") + encodedLegJSON[0].removeValue(forKey: "profileIdentifier") + + encodedRoutesJSON[0]["legs"] = encodedLegJSON + encodedRouteResponseJSON?["routes"] = encodedRoutesJSON + } + } + if var encodedWaypointsJSON = encodedRouteResponseJSON?["waypoints"] as? [[String: Any?]] { + encodedWaypointsJSON[0].removeValue(forKey: "separatesLegs") + encodedWaypointsJSON[0].removeValue(forKey: "allowsArrivingOnOppositeSide") + encodedWaypointsJSON[1].removeValue(forKey: "separatesLegs") + encodedWaypointsJSON[1].removeValue(forKey: "allowsArrivingOnOppositeSide") + + encodedRouteResponseJSON?["waypoints"] = encodedWaypointsJSON + } + + XCTAssertTrue(JSONSerialization.objectsAreEqual(fixtureJSON, encodedRouteResponseJSON, approximate: true)) + } +} diff --git a/Tests/MapboxDirectionsTests/RouteStepTests.swift b/Tests/MapboxDirectionsTests/RouteStepTests.swift index 710b3324f..b91d62fc9 100644 --- a/Tests/MapboxDirectionsTests/RouteStepTests.swift +++ b/Tests/MapboxDirectionsTests/RouteStepTests.swift @@ -114,7 +114,7 @@ class RouteStepTests: XCTestCase { "weight": 2.5, "duration": 2.5, "duration_typical": 2.369, - "name": "Grove Shafter Freeway (CA 24)", + "name": "Grove Shafter Freeway", "pronunciation": "ˈaɪˌfoʊ̯n ˈtɛn", "distance": 24.5, ] as [String: Any?] @@ -225,12 +225,8 @@ class RouteStepTests: XCTestCase { var encodedStepJSON: Any? XCTAssertNoThrow(encodedStepJSON = try JSONSerialization.jsonObject(with: encodedStepData, options: [])) XCTAssertNotNil(encodedStepJSON) - - // https://github.com/mapbox/mapbox-directions-swift/issues/125 - var referenceStepJSON = stepJSON - referenceStepJSON.removeValue(forKey: "weight") - XCTAssert(JSONSerialization.objectsAreEqual(referenceStepJSON, encodedStepJSON, approximate: true)) + XCTAssert(JSONSerialization.objectsAreEqual(stepJSON, encodedStepJSON, approximate: true)) } } @@ -257,12 +253,8 @@ class RouteStepTests: XCTestCase { var encodedStepJSON: Any? XCTAssertNoThrow(encodedStepJSON = try JSONSerialization.jsonObject(with: encodedStepData, options: [])) XCTAssertNotNil(encodedStepJSON) - - // https://github.com/mapbox/mapbox-directions-swift/issues/125 - var referenceStepJSON = stepJSON - referenceStepJSON.removeValue(forKey: "weight") - XCTAssert(JSONSerialization.objectsAreEqual(referenceStepJSON, encodedStepJSON, approximate: true)) + XCTAssert(JSONSerialization.objectsAreEqual(stepJSON, encodedStepJSON, approximate: true)) } } } diff --git a/Tests/MapboxDirectionsTests/RouteTests.swift b/Tests/MapboxDirectionsTests/RouteTests.swift index f83a02e65..e40c165a3 100644 --- a/Tests/MapboxDirectionsTests/RouteTests.swift +++ b/Tests/MapboxDirectionsTests/RouteTests.swift @@ -10,7 +10,7 @@ class RouteTests: XCTestCase { "legs": [ [ "summary": "West 6th Avenue Freeway, South University Boulevard", - "weight": 1346.3, + "weight": 2346.3, "duration": 1083.4, "duration_typical": 1483.262, "steps": [], @@ -65,16 +65,8 @@ class RouteTests: XCTestCase { encodedLegJSON[0].removeValue(forKey: "profileIdentifier") encodedRouteJSON?["legs"] = encodedLegJSON } - - // https://github.com/mapbox/mapbox-directions-swift/issues/125 - var referenceRouteJSON = routeJSON - referenceRouteJSON.removeValue(forKey: "weight") - referenceRouteJSON.removeValue(forKey: "weight_name") - var referenceLegJSON = referenceRouteJSON["legs"] as! [[String: Any?]] - referenceLegJSON[0].removeValue(forKey: "weight") - referenceRouteJSON["legs"] = referenceLegJSON - XCTAssert(JSONSerialization.objectsAreEqual(referenceRouteJSON, encodedRouteJSON, approximate: true)) + XCTAssert(JSONSerialization.objectsAreEqual(routeJSON, encodedRouteJSON, approximate: true)) } } }