From 00cbc53f67bcd8c3efb7263570d5df9397a534f3 Mon Sep 17 00:00:00 2001 From: udumft Date: Mon, 28 Mar 2022 12:19:04 +0300 Subject: [PATCH 01/10] vk-637-json-roundtrip: renamed ForeignMemberClassContainer -> ForeignMemberContainerClass --- CHANGELOG.md | 1 + MapboxDirections.xcodeproj/project.pbxproj | 38 ++ .../AdministrativeRegion.swift | 8 +- .../MapboxDirections/DirectionsResult.swift | 10 +- .../Extensions/ForeignMemberContainer.swift | 99 +++++ Sources/MapboxDirections/Incident.swift | 7 +- Sources/MapboxDirections/Intersection.swift | 25 +- Sources/MapboxDirections/Lane.swift | 9 +- .../MapMatching/MapMatchingResponse.swift | 15 +- .../MapboxDirections/MapMatching/Match.swift | 3 +- .../MapMatching/Tracepoint.swift | 3 +- Sources/MapboxDirections/RefreshedRoute.swift | 17 +- Sources/MapboxDirections/RestStop.swift | 22 +- Sources/MapboxDirections/RouteLeg.swift | 15 +- .../MapboxDirections/RouteLegAttributes.swift | 8 +- .../RouteRefreshResponse.swift | 9 +- Sources/MapboxDirections/RouteResponse.swift | 14 +- Sources/MapboxDirections/RouteStep.swift | 171 +++++++-- Sources/MapboxDirections/SilentWaypoint.swift | 16 +- .../MapboxDirections/SpokenInstruction.swift | 24 +- Sources/MapboxDirections/TollCollection.swift | 22 +- .../MapboxDirections/VisualInstruction.swift | 10 +- .../VisualInstructionBanner.swift | 10 +- Sources/MapboxDirections/Waypoint.swift | 14 +- ...outeRefreshResponseWithForeignMembers.json | 252 +++++++++++++ .../RouteResponseWithForeignMembers.json | 341 ++++++++++++++++++ .../ForeignMemberContainerTests.swift | 102 ++++++ .../RouteStepTests.swift | 12 +- Tests/MapboxDirectionsTests/RouteTests.swift | 12 +- 29 files changed, 1202 insertions(+), 87 deletions(-) create mode 100644 Sources/MapboxDirections/Extensions/ForeignMemberContainer.swift create mode 100644 Tests/MapboxDirectionsTests/Fixtures/Responses/RouteRefreshResponseWithForeignMembers.json create mode 100644 Tests/MapboxDirectionsTests/Fixtures/Responses/RouteResponseWithForeignMembers.json create mode 100644 Tests/MapboxDirectionsTests/ForeignMemberContainerTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index cf3ddcd69..fc14fc11c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * 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 unrecogized (foreign) JSON values and preserve them on coding/decoding using `ForeignMemberContainer` and `ForeignMemberClassContainer` implementations. ([#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)) diff --git a/MapboxDirections.xcodeproj/project.pbxproj b/MapboxDirections.xcodeproj/project.pbxproj index 55add5e3c..de188209c 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..bb50a517b 100644 --- a/Sources/MapboxDirections/Incident.swift +++ b/Sources/MapboxDirections/Incident.swift @@ -1,9 +1,11 @@ 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 = [:] private enum CodingKeys: String, CodingKey { case identifier = "id" @@ -228,6 +230,7 @@ public struct Incident: Codable, Equatable { numberOfBlockedLanes = try container.decodeIfPresent(Int.self, forKey: .numberOfBlockedLanes) congestionLevel = try container.decodeIfPresent(CongestionContainer.self, forKey: .congestionLevel)?.clampedValue affectedRoadNames = try container.decodeIfPresent([String].self, forKey: .affectedRoadNames) + try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) } public func encode(to encoder: Encoder) throws { @@ -256,5 +259,7 @@ public struct Incident: Codable, Equatable { try container.encode(CongestionContainer(value: congestionLevel), 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 492e39928..dad837781 100644 --- a/Sources/MapboxDirections/RestStop.swift +++ b/Sources/MapboxDirections/RestStop.swift @@ -1,9 +1,11 @@ import Foundation +import Turf /** `RestStop` describes corresponding object on the route. */ -public struct RestStop: Codable, Equatable { +public struct RestStop: Codable, Equatable, ForeignMemberContainer { + public var foreignMembers: JSONObject = [:] public enum StopType: String, Codable { case serviceArea = "service_area" @@ -22,4 +24,22 @@ public struct RestStop: Codable, Equatable { public init(type: StopType) { self.type = type } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + type = try container.decode(StopType.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/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..bf76723c0 100644 --- a/Sources/MapboxDirections/RouteStep.swift +++ b/Sources/MapboxDirections/RouteStep.swift @@ -301,7 +301,9 @@ extension Array where Element == String { /** Encapsulates all the information about a road. */ -struct Road { +struct Road: ForeignMemberContainer { + var foreignMembers: JSONObject = [:] + let names: [String]? let codes: [String]? let exitCodes: [String]? @@ -365,6 +367,8 @@ extension Road: Codable { let destinations = try container.decodeIfPresent(String.self, forKey: .destinations) let rotaryName = try container.decodeIfPresent(String.self, forKey: .rotaryName) self.init(name: name, ref: ref, exits: exits, destination: destinations, rotaryName: rotaryName) + + try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) } func encode(to encoder: Encoder) throws { @@ -390,6 +394,8 @@ extension Road: Codable { try container.encodeIfPresent(exitCodes?.tagValues(joinedBy: ";"), forKey: .exits) try container.encodeIfPresent(ref, forKey: .ref) try container.encodeIfPresent(rotaryNames?.tagValues(joinedBy: ";"), forKey: .rotaryName) + + try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) } } @@ -398,8 +404,10 @@ 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 = [:] + + private enum CodingKeys: String, CodingKey, CaseIterable { case shape = "geometry" case distance case drivingSide = "driving_side" @@ -417,14 +425,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 +597,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) + let maneuver = Maneuver(instructions: instructions, + maneuverType: maneuverType, + maneuverDirection: maneuverDirection, + maneuverLocation: maneuverLocation, + initialHeading: initialHeading, + finalHeading: finalHeading, + exitIndex: exitIndex) + 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] { @@ -558,21 +631,53 @@ open class RouteStep: Codable { /// Used to Decode `Intersection.admin_index` - private struct AdministrativeAreaIndex: Codable { + private struct AdministrativeAreaIndex: Codable, ForeignMemberContainer { + var foreignMembers: JSONObject = [:] + 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) + + try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encodeIfPresent(administrativeRegionIndex, forKey: .administrativeRegionIndex) + + try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) + } } /// Used to Decode `Intersection.geometry_index` - private struct IntersectionShapeIndex: Codable { + private struct IntersectionShapeIndex: Codable, ForeignMemberContainer { + var foreignMembers: JSONObject = [:] + 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) + + try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encodeIfPresent(geometryIndex, forKey: .geometryIndex) + + try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) + } } @@ -582,15 +687,16 @@ 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 if let polyLineString = try container.decodeIfPresent(PolyLineString.self, forKey: .shape) { shape = try LineString(polyLineString: polyLineString) @@ -598,11 +704,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 +764,8 @@ open class RouteStep: Codable { exitNames = nil phoneticExitNames = nil } + + try decodeForeignMembers(notKeyedBy: 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..77be1d823 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) + } + + public 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..cd5eab91e --- /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": "Заславль — Дзержинск — Озеро; Заслаўе — Дзяржынск — Возера", + "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 911bea22d..865ab649d 100644 --- a/Tests/MapboxDirectionsTests/RouteStepTests.swift +++ b/Tests/MapboxDirectionsTests/RouteStepTests.swift @@ -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 12d54754d..e1235e89d 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)) } } } From 6153317a9a1bff940ef53ff442b7529c5aca11d3 Mon Sep 17 00:00:00 2001 From: udumft Date: Tue, 5 Apr 2022 11:17:48 +0300 Subject: [PATCH 02/10] vk-637-json-roundtrip: removed RouteStep names coding workaround which parsed ref and name; Unit tests updated --- Sources/MapboxDirections/RouteStep.swift | 26 ++++------- .../RouteStepTests.swift | 17 +++++--- Tests/MapboxDirectionsTests/V5Tests.swift | 43 +++++++++++++++++-- 3 files changed, 57 insertions(+), 29 deletions(-) diff --git a/Sources/MapboxDirections/RouteStep.swift b/Sources/MapboxDirections/RouteStep.swift index bf76723c0..b44503d07 100644 --- a/Sources/MapboxDirections/RouteStep.swift +++ b/Sources/MapboxDirections/RouteStep.swift @@ -321,17 +321,7 @@ struct Road: ForeignMemberContainer { } 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. - let parenthetical = "(\(ref))" - if name == ref { - self.names = nil - } else { - self.names = name.replacingOccurrences(of: parenthetical, with: "").tagValues(separatedBy: ";") - } - } else { - self.names = name.isEmpty ? nil : name.tagValues(separatedBy: ";") - } + self.names = name.tagValues(separatedBy: ";") // Mapbox Directions API v5 combines the destination’s ref and name. if let destination = destination, destination.contains(": ") { @@ -350,7 +340,7 @@ struct Road: ForeignMemberContainer { } extension Road: Codable { - private enum CodingKeys: String, CodingKey { + enum CodingKeys: String, CodingKey, CaseIterable { case name case ref case exits @@ -375,10 +365,7 @@ extension Road: Codable { var container = encoder.container(keyedBy: CodingKeys.self) let ref = codes?.tagValues(joinedBy: ";") - if var name = names?.tagValues(joinedBy: ";") { - if let ref = ref { - name = "\(name) (\(ref))" - } + if let name = names?.tagValues(joinedBy: ";") { try container.encodeIfPresent(name, forKey: .name) } else { try container.encodeIfPresent(ref, forKey: .name) @@ -406,6 +393,7 @@ extension Road: Codable { */ open class RouteStep: Codable, ForeignMemberContainerClass { public var foreignMembers: JSONObject = [:] + public var maneuverForeignMembers: JSONObject = [:] private enum CodingKeys: String, CodingKey, CaseIterable { case shape = "geometry" @@ -597,14 +585,14 @@ open class RouteStep: Codable, ForeignMemberContainerClass { try container.encode(polyLineString, forKey: .shape) } - - let maneuver = Maneuver(instructions: instructions, + 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) @@ -697,6 +685,7 @@ open class RouteStep: Codable, ForeignMemberContainerClass { 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) @@ -766,6 +755,7 @@ open class RouteStep: Codable, ForeignMemberContainerClass { } 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/Tests/MapboxDirectionsTests/RouteStepTests.swift b/Tests/MapboxDirectionsTests/RouteStepTests.swift index 865ab649d..b4ebb6462 100644 --- a/Tests/MapboxDirectionsTests/RouteStepTests.swift +++ b/Tests/MapboxDirectionsTests/RouteStepTests.swift @@ -5,7 +5,8 @@ import Turf class RoadTests: XCTestCase { func testEmpty() { let r = Road(name: "", ref: nil, exits: nil, destination: nil, rotaryName: nil) - XCTAssertNil(r.names) + XCTAssertNotNil(r.names) + XCTAssertTrue(r.names?.isEmpty ?? false) XCTAssertNil(r.codes) XCTAssertNil(r.exitCodes) XCTAssertNil(r.destinations) @@ -26,10 +27,12 @@ class RoadTests: XCTestCase { // Ref only r = Road(name: "", ref: "Ref 1", exits: nil, destination: nil, rotaryName: nil) - XCTAssertNil(r.names) + XCTAssertNotNil(r.names) + XCTAssertTrue(r.names?.isEmpty ?? false) XCTAssertEqual(r.codes ?? [], [ "Ref 1" ]) r = Road(name: "", ref: "Ref 1; Ref 2", exits: nil, destination: nil, rotaryName: nil) - XCTAssertNil(r.names) + XCTAssertNotNil(r.names) + XCTAssertTrue(r.names!.isEmpty) XCTAssertEqual(r.codes ?? [], [ "Ref 1", "Ref 2" ]) // Separate Name and Ref @@ -44,14 +47,14 @@ class RoadTests: XCTestCase { XCTAssertEqual(r.codes ?? [], [ "Ref 1", "Ref 2" ]) // Ref duplicated in Name (Mapbox Directions API v5) - r = Road(name: "Way Name (Ref)", ref: "Ref", exits: nil, destination: nil, rotaryName: nil) + r = Road(name: "Way Name", ref: "Ref", exits: nil, destination: nil, rotaryName: nil) XCTAssertEqual(r.names ?? [], [ "Way Name" ]) XCTAssertEqual(r.codes ?? [], [ "Ref" ]) - r = Road(name: "Way Name 1; Way Name 2 (Ref 1; Ref 2)", ref: "Ref 1; Ref 2", exits: nil, destination: nil, rotaryName: nil) + r = Road(name: "Way Name 1; Way Name 2", ref: "Ref 1; Ref 2", exits: nil, destination: nil, rotaryName: nil) XCTAssertEqual(r.names ?? [], [ "Way Name 1", "Way Name 2"]) XCTAssertEqual(r.codes ?? [], [ "Ref 1", "Ref 2" ]) r = Road(name: "Ref 1; Ref 2", ref: "Ref 1; Ref 2", exits: nil, destination: nil, rotaryName: nil) - XCTAssertNil(r.names) + XCTAssertEqual(r.names ?? [], [ "Ref 1", "Ref 2" ]) XCTAssertEqual(r.codes ?? [], [ "Ref 1", "Ref 2" ]) } @@ -114,7 +117,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?] diff --git a/Tests/MapboxDirectionsTests/V5Tests.swift b/Tests/MapboxDirectionsTests/V5Tests.swift index 9e6405ae6..fd5bdfa88 100644 --- a/Tests/MapboxDirectionsTests/V5Tests.swift +++ b/Tests/MapboxDirectionsTests/V5Tests.swift @@ -111,7 +111,8 @@ class V5Tests: XCTestCase { XCTAssertEqual(step?.expectedTravelTime ?? 0, 31, accuracy: 1) XCTAssertEqual(step?.instructions, "Take exit 43-44 towards VA 193: George Washington Memorial Parkway") - XCTAssertNil(step?.names) + XCTAssertNotNil(step?.names) + XCTAssertTrue(step?.names?.isEmpty ?? false) XCTAssertEqual(step?.destinationCodes, ["VA 193"]) XCTAssertEqual(step?.destinations, ["George Washington Memorial Parkway", "Washington", "Georgetown Pike"]) XCTAssertEqual(step?.maneuverType, .takeOffRamp) @@ -124,12 +125,12 @@ class V5Tests: XCTestCase { XCTAssertEqual(step?.shape?.coordinates.first?.latitude ?? 0, 38.9667, accuracy: 1e-4) XCTAssertEqual(step?.shape?.coordinates.first?.longitude ?? 0, -77.1802, accuracy: 1e-4) - XCTAssertEqual(leg?.steps[32].names, nil) + XCTAssertEqual(leg?.steps[32].names, ["I-80"]) XCTAssertEqual(leg?.steps[32].codes, ["I-80"]) XCTAssertEqual(leg?.steps[32].destinationCodes, ["I-80 East", "I-90"]) XCTAssertEqual(leg?.steps[32].destinations, ["Toll Road"]) - XCTAssertEqual(leg?.steps[35].names, ["Ohio Turnpike"]) + XCTAssertEqual(leg?.steps[35].names, ["Ohio Turnpike (I-80 East)"]) XCTAssertEqual(leg?.steps[35].codes, ["I-80 East"]) XCTAssertNil(leg?.steps[35].destinationCodes) XCTAssertNil(leg?.steps[35].destinations) @@ -158,7 +159,41 @@ class V5Tests: XCTestCase { func testGeoJSON() { XCTAssertEqual(RouteShapeFormat.geoJSON.rawValue, "geojson") - test(shapeFormat: .geoJSON) + + // Removes excessive foreignMembers + let transformer: JSONTransformer = { json in + var transformed = json + let routes = (transformed["routes"] as! [JSONDictionary]) + var newRoutes = [JSONDictionary]() + + for var route in routes { + let legs = route["legs"] as! [JSONDictionary] + var newLegs = [JSONDictionary]() + + for var leg in legs { + let steps = leg["steps"] as! [JSONDictionary] + var newSteps = [JSONDictionary]() + + for var step in steps { + step.removeValue(forKey: "weight") + + newSteps.append(step) + } + + leg["steps"] = newSteps + newLegs.append(leg) + } + + route["legs"] = newLegs + newRoutes.append(route) + } + + + transformed["routes"] = newRoutes + return transformed + } + + test(shapeFormat: .geoJSON, transformer: transformer) } func testPolyline() { From f4d02dbc06eb68a020cdca6da2a4a7866d16122c Mon Sep 17 00:00:00 2001 From: udumft Date: Tue, 5 Apr 2022 17:20:28 +0300 Subject: [PATCH 03/10] vk-637-json-roundtrip: added Incident.CongstionContainer ForeignMemberContainer conformance; extended V5Tests timeout --- Sources/MapboxDirections/Incident.swift | 31 ++++++++++++++++-- Tests/MapboxDirectionsTests/V5Tests.swift | 38 ++--------------------- 2 files changed, 30 insertions(+), 39 deletions(-) diff --git a/Sources/MapboxDirections/Incident.swift b/Sources/MapboxDirections/Incident.swift index bb50a517b..97122b332 100644 --- a/Sources/MapboxDirections/Incident.swift +++ b/Sources/MapboxDirections/Incident.swift @@ -6,6 +6,7 @@ import Turf */ public struct Incident: Codable, Equatable, ForeignMemberContainer { public var foreignMembers: JSONObject = [:] + public var congestionForeignMembers: JSONObject = [:] private enum CodingKeys: String, CodingKey { case identifier = "id" @@ -74,7 +75,9 @@ public struct Incident: Codable, Equatable, ForeignMemberContainer { 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 @@ -87,6 +90,24 @@ public struct Incident: Codable, Equatable, ForeignMemberContainer { 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 @@ -228,7 +249,9 @@ public struct Incident: Codable, Equatable, ForeignMemberContainer { 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) } @@ -256,7 +279,9 @@ public struct Incident: Codable, Equatable, ForeignMemberContainer { 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) diff --git a/Tests/MapboxDirectionsTests/V5Tests.swift b/Tests/MapboxDirectionsTests/V5Tests.swift index fd5bdfa88..d470288c1 100644 --- a/Tests/MapboxDirectionsTests/V5Tests.swift +++ b/Tests/MapboxDirectionsTests/V5Tests.swift @@ -63,7 +63,7 @@ class V5Tests: XCTestCase { } XCTAssertNotNil(task) - waitForExpectations(timeout: 10) { (error) in + waitForExpectations(timeout: 15) { (error) in XCTAssertNil(error, "Error: \(error!)") XCTAssertEqual(task.state, .completed) } @@ -159,41 +159,7 @@ class V5Tests: XCTestCase { func testGeoJSON() { XCTAssertEqual(RouteShapeFormat.geoJSON.rawValue, "geojson") - - // Removes excessive foreignMembers - let transformer: JSONTransformer = { json in - var transformed = json - let routes = (transformed["routes"] as! [JSONDictionary]) - var newRoutes = [JSONDictionary]() - - for var route in routes { - let legs = route["legs"] as! [JSONDictionary] - var newLegs = [JSONDictionary]() - - for var leg in legs { - let steps = leg["steps"] as! [JSONDictionary] - var newSteps = [JSONDictionary]() - - for var step in steps { - step.removeValue(forKey: "weight") - - newSteps.append(step) - } - - leg["steps"] = newSteps - newLegs.append(leg) - } - - route["legs"] = newLegs - newRoutes.append(route) - } - - - transformed["routes"] = newRoutes - return transformed - } - - test(shapeFormat: .geoJSON, transformer: transformer) + test(shapeFormat: .geoJSON) } func testPolyline() { From 8dcb969d0712f48a0b0913e489b25a6285cc39ab Mon Sep 17 00:00:00 2001 From: udumft Date: Mon, 11 Apr 2022 14:57:29 +0300 Subject: [PATCH 04/10] vk-637-json-roundtrip: restored RouteStep naming and ref coding logic; CHANGELOG appended; tests fixed. --- CHANGELOG.md | 3 ++- Sources/MapboxDirections/RouteStep.swift | 18 +++++++++++++++--- .../RouteResponseWithForeignMembers.json | 2 +- .../MapboxDirectionsTests/RouteStepTests.swift | 15 ++++++--------- Tests/MapboxDirectionsTests/V5Tests.swift | 7 +++---- 5 files changed, 27 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc14fc11c..61f1eb7ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,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 unrecogized (foreign) JSON values and preserve them on coding/decoding using `ForeignMemberContainer` and `ForeignMemberClassContainer` implementations. ([#669](https://github.com/mapbox/mapbox-directions-swift/pull/669)) +* `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)) * 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)) diff --git a/Sources/MapboxDirections/RouteStep.swift b/Sources/MapboxDirections/RouteStep.swift index b44503d07..83af60272 100644 --- a/Sources/MapboxDirections/RouteStep.swift +++ b/Sources/MapboxDirections/RouteStep.swift @@ -321,7 +321,16 @@ struct Road: ForeignMemberContainer { } init(name: String, ref: String?, exits: String?, destination: String?, rotaryName: String?) { - self.names = name.tagValues(separatedBy: ";") + if !name.isEmpty, let ref = ref { + let parenthetical = "(\(ref))" + if name == ref { + self.names = nil + } else { + self.names = name.replacingOccurrences(of: parenthetical, with: "").tagValues(separatedBy: ";") + } + } else { + self.names = name.isEmpty ? nil : name.tagValues(separatedBy: ";") + } // Mapbox Directions API v5 combines the destination’s ref and name. if let destination = destination, destination.contains(": ") { @@ -365,10 +374,13 @@ extension Road: Codable { var container = encoder.container(keyedBy: CodingKeys.self) let ref = codes?.tagValues(joinedBy: ";") - if let name = names?.tagValues(joinedBy: ";") { + if var name = names?.tagValues(joinedBy: ";") { + if let ref = ref { + name = "\(name) (\(ref))" + } 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: ",") { diff --git a/Tests/MapboxDirectionsTests/Fixtures/Responses/RouteResponseWithForeignMembers.json b/Tests/MapboxDirectionsTests/Fixtures/Responses/RouteResponseWithForeignMembers.json index cd5eab91e..b7d21be5f 100644 --- a/Tests/MapboxDirectionsTests/Fixtures/Responses/RouteResponseWithForeignMembers.json +++ b/Tests/MapboxDirectionsTests/Fixtures/Responses/RouteResponseWithForeignMembers.json @@ -144,7 +144,7 @@ 53.987196 ] }, - "name": "Заславль — Дзержинск — Озеро; Заслаўе — Дзяржынск — Возера", + "name": "Заславль — Дзержинск — Озеро; Заслаўе — Дзяржынск — Возера (Р65)", "weight_typical": 0.454, "duration_typical": 0.5, "duration": 0.5, diff --git a/Tests/MapboxDirectionsTests/RouteStepTests.swift b/Tests/MapboxDirectionsTests/RouteStepTests.swift index b4ebb6462..4a6367948 100644 --- a/Tests/MapboxDirectionsTests/RouteStepTests.swift +++ b/Tests/MapboxDirectionsTests/RouteStepTests.swift @@ -5,8 +5,7 @@ import Turf class RoadTests: XCTestCase { func testEmpty() { let r = Road(name: "", ref: nil, exits: nil, destination: nil, rotaryName: nil) - XCTAssertNotNil(r.names) - XCTAssertTrue(r.names?.isEmpty ?? false) + XCTAssertNil(r.names) XCTAssertNil(r.codes) XCTAssertNil(r.exitCodes) XCTAssertNil(r.destinations) @@ -27,12 +26,10 @@ class RoadTests: XCTestCase { // Ref only r = Road(name: "", ref: "Ref 1", exits: nil, destination: nil, rotaryName: nil) - XCTAssertNotNil(r.names) - XCTAssertTrue(r.names?.isEmpty ?? false) + XCTAssertNil(r.names) XCTAssertEqual(r.codes ?? [], [ "Ref 1" ]) r = Road(name: "", ref: "Ref 1; Ref 2", exits: nil, destination: nil, rotaryName: nil) - XCTAssertNotNil(r.names) - XCTAssertTrue(r.names!.isEmpty) + XCTAssertNil(r.names) XCTAssertEqual(r.codes ?? [], [ "Ref 1", "Ref 2" ]) // Separate Name and Ref @@ -47,14 +44,14 @@ class RoadTests: XCTestCase { XCTAssertEqual(r.codes ?? [], [ "Ref 1", "Ref 2" ]) // Ref duplicated in Name (Mapbox Directions API v5) - r = Road(name: "Way Name", ref: "Ref", exits: nil, destination: nil, rotaryName: nil) + r = Road(name: "Way Name (Ref)", ref: "Ref", exits: nil, destination: nil, rotaryName: nil) XCTAssertEqual(r.names ?? [], [ "Way Name" ]) XCTAssertEqual(r.codes ?? [], [ "Ref" ]) - r = Road(name: "Way Name 1; Way Name 2", ref: "Ref 1; Ref 2", exits: nil, destination: nil, rotaryName: nil) + r = Road(name: "Way Name 1; Way Name 2 (Ref 1; Ref 2)", ref: "Ref 1; Ref 2", exits: nil, destination: nil, rotaryName: nil) XCTAssertEqual(r.names ?? [], [ "Way Name 1", "Way Name 2"]) XCTAssertEqual(r.codes ?? [], [ "Ref 1", "Ref 2" ]) r = Road(name: "Ref 1; Ref 2", ref: "Ref 1; Ref 2", exits: nil, destination: nil, rotaryName: nil) - XCTAssertEqual(r.names ?? [], [ "Ref 1", "Ref 2" ]) + XCTAssertNil(r.names) XCTAssertEqual(r.codes ?? [], [ "Ref 1", "Ref 2" ]) } diff --git a/Tests/MapboxDirectionsTests/V5Tests.swift b/Tests/MapboxDirectionsTests/V5Tests.swift index d470288c1..595b314d8 100644 --- a/Tests/MapboxDirectionsTests/V5Tests.swift +++ b/Tests/MapboxDirectionsTests/V5Tests.swift @@ -111,8 +111,7 @@ class V5Tests: XCTestCase { XCTAssertEqual(step?.expectedTravelTime ?? 0, 31, accuracy: 1) XCTAssertEqual(step?.instructions, "Take exit 43-44 towards VA 193: George Washington Memorial Parkway") - XCTAssertNotNil(step?.names) - XCTAssertTrue(step?.names?.isEmpty ?? false) + XCTAssertNil(step?.names) XCTAssertEqual(step?.destinationCodes, ["VA 193"]) XCTAssertEqual(step?.destinations, ["George Washington Memorial Parkway", "Washington", "Georgetown Pike"]) XCTAssertEqual(step?.maneuverType, .takeOffRamp) @@ -125,12 +124,12 @@ class V5Tests: XCTestCase { XCTAssertEqual(step?.shape?.coordinates.first?.latitude ?? 0, 38.9667, accuracy: 1e-4) XCTAssertEqual(step?.shape?.coordinates.first?.longitude ?? 0, -77.1802, accuracy: 1e-4) - XCTAssertEqual(leg?.steps[32].names, ["I-80"]) + XCTAssertEqual(leg?.steps[32].names, nil) XCTAssertEqual(leg?.steps[32].codes, ["I-80"]) XCTAssertEqual(leg?.steps[32].destinationCodes, ["I-80 East", "I-90"]) XCTAssertEqual(leg?.steps[32].destinations, ["Toll Road"]) - XCTAssertEqual(leg?.steps[35].names, ["Ohio Turnpike (I-80 East)"]) + XCTAssertEqual(leg?.steps[35].names, ["Ohio Turnpike"]) XCTAssertEqual(leg?.steps[35].codes, ["I-80 East"]) XCTAssertNil(leg?.steps[35].destinationCodes) XCTAssertNil(leg?.steps[35].destinations) From 90da3e3907826d1a66d17447eb37687498e902c6 Mon Sep 17 00:00:00 2001 From: udumft Date: Mon, 11 Apr 2022 15:05:07 +0300 Subject: [PATCH 05/10] vk-637-json-roundtrip: commented name and ref coding --- Sources/MapboxDirections/RouteStep.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/MapboxDirections/RouteStep.swift b/Sources/MapboxDirections/RouteStep.swift index 83af60272..d9de47f5d 100644 --- a/Sources/MapboxDirections/RouteStep.swift +++ b/Sources/MapboxDirections/RouteStep.swift @@ -322,6 +322,7 @@ struct Road: ForeignMemberContainer { init(name: String, ref: String?, exits: String?, destination: String?, rotaryName: String?) { if !name.isEmpty, let ref = ref { + // 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 From 965422486143f1922d91c36148fe1e89705fb20e Mon Sep 17 00:00:00 2001 From: udumft Date: Thu, 5 May 2022 15:03:17 +0300 Subject: [PATCH 06/10] vk-637-json-roundtrip: removed internal struct Road ForeginMemberContainer conformance to avoid duplicate members coding. Reverted unit test timeout value. --- Sources/MapboxDirections/RouteStep.swift | 8 +------- Tests/MapboxDirectionsTests/V5Tests.swift | 2 +- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/Sources/MapboxDirections/RouteStep.swift b/Sources/MapboxDirections/RouteStep.swift index d9de47f5d..56a79b0cb 100644 --- a/Sources/MapboxDirections/RouteStep.swift +++ b/Sources/MapboxDirections/RouteStep.swift @@ -301,9 +301,7 @@ extension Array where Element == String { /** Encapsulates all the information about a road. */ -struct Road: ForeignMemberContainer { - var foreignMembers: JSONObject = [:] - +struct Road { let names: [String]? let codes: [String]? let exitCodes: [String]? @@ -367,8 +365,6 @@ extension Road: Codable { let destinations = try container.decodeIfPresent(String.self, forKey: .destinations) let rotaryName = try container.decodeIfPresent(String.self, forKey: .rotaryName) self.init(name: name, ref: ref, exits: exits, destination: destinations, rotaryName: rotaryName) - - try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) } func encode(to encoder: Encoder) throws { @@ -394,8 +390,6 @@ extension Road: Codable { try container.encodeIfPresent(exitCodes?.tagValues(joinedBy: ";"), forKey: .exits) try container.encodeIfPresent(ref, forKey: .ref) try container.encodeIfPresent(rotaryNames?.tagValues(joinedBy: ";"), forKey: .rotaryName) - - try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) } } diff --git a/Tests/MapboxDirectionsTests/V5Tests.swift b/Tests/MapboxDirectionsTests/V5Tests.swift index 595b314d8..9e6405ae6 100644 --- a/Tests/MapboxDirectionsTests/V5Tests.swift +++ b/Tests/MapboxDirectionsTests/V5Tests.swift @@ -63,7 +63,7 @@ class V5Tests: XCTestCase { } XCTAssertNotNil(task) - waitForExpectations(timeout: 15) { (error) in + waitForExpectations(timeout: 10) { (error) in XCTAssertNil(error, "Error: \(error!)") XCTAssertEqual(task.state, .completed) } From 67c2344ac2d551122d3b68f107f66a5535c0c3cd Mon Sep 17 00:00:00 2001 From: udumft Date: Thu, 5 May 2022 16:21:37 +0300 Subject: [PATCH 07/10] vk-637-json-roundtrip: added fixed RestStop coding --- Sources/MapboxDirections/RestStop.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/MapboxDirections/RestStop.swift b/Sources/MapboxDirections/RestStop.swift index e3ffb80ce..89f0927f3 100644 --- a/Sources/MapboxDirections/RestStop.swift +++ b/Sources/MapboxDirections/RestStop.swift @@ -52,6 +52,7 @@ public struct RestStop: Codable, Equatable, ForeignMemberContainer { 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) } @@ -59,11 +60,12 @@ public struct RestStop: Codable, Equatable, ForeignMemberContainer { 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 + return lhs.type == rhs.type && lhs.name == rhs.name } } From 78dd46e4d93b31412c33b3aefeed3bbfafa0f859 Mon Sep 17 00:00:00 2001 From: udumft Date: Thu, 5 May 2022 16:40:42 +0300 Subject: [PATCH 08/10] vk-6370json-roundtrip: opened SpolokenInstruction.encode for subclassing --- Sources/MapboxDirections/SpokenInstruction.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/MapboxDirections/SpokenInstruction.swift b/Sources/MapboxDirections/SpokenInstruction.swift index 77be1d823..100b0b2d9 100644 --- a/Sources/MapboxDirections/SpokenInstruction.swift +++ b/Sources/MapboxDirections/SpokenInstruction.swift @@ -41,7 +41,7 @@ open class SpokenInstruction: Codable, ForeignMemberContainerClass { try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) } - public func encode(to encoder: Encoder) throws { + 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) From 09e6b507518bc0a359ecd0404ecf4293b90f91a8 Mon Sep 17 00:00:00 2001 From: udumft Date: Fri, 6 May 2022 10:23:32 +0300 Subject: [PATCH 09/10] vk-637-json-roundtrip: removed excessive ForeignMemberContainer conformance from IntersectionShapeIndex and AdministrativeAreaIndex --- Sources/MapboxDirections/RouteStep.swift | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/Sources/MapboxDirections/RouteStep.swift b/Sources/MapboxDirections/RouteStep.swift index 56a79b0cb..fce50b070 100644 --- a/Sources/MapboxDirections/RouteStep.swift +++ b/Sources/MapboxDirections/RouteStep.swift @@ -626,8 +626,7 @@ open class RouteStep: Codable, ForeignMemberContainerClass { /// Used to Decode `Intersection.admin_index` - private struct AdministrativeAreaIndex: Codable, ForeignMemberContainer { - var foreignMembers: JSONObject = [:] + private struct AdministrativeAreaIndex: Codable { private enum CodingKeys: String, CodingKey { case administrativeRegionIndex = "admin_index" @@ -638,21 +637,16 @@ open class RouteStep: Codable, ForeignMemberContainerClass { init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) administrativeRegionIndex = try container.decodeIfPresent(Int.self, forKey: .administrativeRegionIndex) - - try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encodeIfPresent(administrativeRegionIndex, forKey: .administrativeRegionIndex) - - try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) } } /// Used to Decode `Intersection.geometry_index` - private struct IntersectionShapeIndex: Codable, ForeignMemberContainer { - var foreignMembers: JSONObject = [:] + private struct IntersectionShapeIndex: Codable { private enum CodingKeys: String, CodingKey { case geometryIndex = "geometry_index" @@ -663,15 +657,11 @@ open class RouteStep: Codable, ForeignMemberContainerClass { init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) geometryIndex = try container.decodeIfPresent(Int.self, forKey: .geometryIndex) - - try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encodeIfPresent(geometryIndex, forKey: .geometryIndex) - - try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) } } From bb506b6343d11e1c12233c9e49bfa742df3d2d09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguy=E1=BB=85n?= Date: Fri, 6 May 2022 13:18:41 -0700 Subject: [PATCH 10/10] Copyedited changelog --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e65f9622..4c8307959 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +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)) +* 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))