diff --git a/Package.swift b/Package.swift index 3c6c9ffa..1177ccda 100644 --- a/Package.swift +++ b/Package.swift @@ -16,7 +16,7 @@ if useLocalFramework { path: "./common/target/ios/libferrostar-rs.xcframework" ) } else { - let releaseTag = "0.0.31" + let releaseTag = "0.0.32" let releaseChecksum = "a21146211e43922850a287b7cb713e3515d9b4f3863c2a1066a3f4d8af1d302c" binaryTarget = .binaryTarget( name: "FerrostarCoreRS", diff --git a/android/build.gradle b/android/build.gradle index 5c899d84..7345f8af 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -23,5 +23,5 @@ publishing { allprojects { group = "com.stadiamaps.ferrostar" - version = "0.0.31" + version = "0.0.32" } diff --git a/android/core/build.gradle b/android/core/build.gradle index 53a5d9e2..3e1e564a 100644 --- a/android/core/build.gradle +++ b/android/core/build.gradle @@ -45,6 +45,7 @@ dependencies { implementation 'androidx.core:core-ktx:1.12.0' implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'com.squareup.okhttp3:okhttp' + implementation 'com.squareup.moshi:moshi:1.15.1' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0' diff --git a/android/core/src/androidTest/java/com/stadiamaps/ferrostar/core/ValhallaCoreTest.kt b/android/core/src/androidTest/java/com/stadiamaps/ferrostar/core/ValhallaCoreTest.kt index c093c314..ff7f7b51 100644 --- a/android/core/src/androidTest/java/com/stadiamaps/ferrostar/core/ValhallaCoreTest.kt +++ b/android/core/src/androidTest/java/com/stadiamaps/ferrostar/core/ValhallaCoreTest.kt @@ -239,7 +239,7 @@ class ValhallaCoreTest { MockInterceptor().apply { rule(post, url eq valhallaEndpointUrl) { respond(simpleRoute, MEDIATYPE_JSON) } - rule(get) { respond { throw IllegalStateException("an IO error") } } + rule(get) { respond { throw IllegalStateException("Expected only one request") } } } val core = FerrostarCore( @@ -276,4 +276,35 @@ class ValhallaCoreTest { routes.first().geometry) } } + + @Test + fun valhallaRequestWithCostingOptions(): TestResult { + val interceptor = + MockInterceptor().apply { + rule(post, url eq valhallaEndpointUrl) { respond(simpleRoute, MEDIATYPE_JSON) } + + rule(get) { respond { throw IllegalStateException("Expected only one request") } } + } + val core = + FerrostarCore( + valhallaEndpointURL = URL(valhallaEndpointUrl), + profile = "auto", + httpClient = OkHttpClient.Builder().addInterceptor(interceptor).build(), + locationProvider = SimulatedLocationProvider(), + costingOptions = mapOf("auto" to mapOf("useTolls" to 0))) + + return runTest { + val routes = + core.getRoutes( + UserLocation( + GeographicCoordinate(60.5347155, -149.543469), 12.0, null, Instant.now(), null), + waypoints = + listOf( + Waypoint( + coordinate = GeographicCoordinate(60.5349908, -149.5485806), + kind = WaypointKind.BREAK))) + + assertEquals(routes.count(), 1) + } + } } diff --git a/android/core/src/main/java/com/stadiamaps/ferrostar/core/FerrostarCore.kt b/android/core/src/main/java/com/stadiamaps/ferrostar/core/FerrostarCore.kt index a0f587c2..cc44d14d 100644 --- a/android/core/src/main/java/com/stadiamaps/ferrostar/core/FerrostarCore.kt +++ b/android/core/src/main/java/com/stadiamaps/ferrostar/core/FerrostarCore.kt @@ -1,5 +1,8 @@ package com.stadiamaps.ferrostar.core +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.Moshi +import com.squareup.moshi.adapter import java.net.URL import java.util.concurrent.Executors import kotlinx.coroutines.CoroutineScope @@ -28,6 +31,10 @@ data class FerrostarCoreState( val isCalculatingNewRoute: Boolean ) +private val moshi: Moshi = Moshi.Builder().build() +@OptIn(ExperimentalStdlibApi::class) +private val jsonAdapter: JsonAdapter> = moshi.adapter>() + /** * This is the entrypoint for end users of Ferrostar on Android, and is responsible for "driving" * the navigation with location updates and other events. @@ -107,9 +114,11 @@ class FerrostarCore( profile: String, httpClient: OkHttpClient, locationProvider: LocationProvider, + costingOptions: Map = emptyMap(), ) : this( RouteProvider.RouteAdapter( - RouteAdapter.newValhallaHttp(valhallaEndpointURL.toString(), profile)), + RouteAdapter.newValhallaHttp( + valhallaEndpointURL.toString(), profile, jsonAdapter.toJson(costingOptions))), httpClient, locationProvider, ) diff --git a/android/demo-app/src/main/java/com/stadiamaps/ferrostar/MainActivity.kt b/android/demo-app/src/main/java/com/stadiamaps/ferrostar/MainActivity.kt index 2da278f6..3122003a 100644 --- a/android/demo-app/src/main/java/com/stadiamaps/ferrostar/MainActivity.kt +++ b/android/demo-app/src/main/java/com/stadiamaps/ferrostar/MainActivity.kt @@ -76,7 +76,7 @@ class MainActivity : ComponentActivity(), AndroidTtsStatusListener { profile = "bicycle", httpClient = httpClient, locationProvider = locationProvider, - ) + costingOptions = mapOf("bicycle" to mapOf("use_roads" to 0.2))) private lateinit var ttsObserver: AndroidTtsObserver diff --git a/apple/DemoApp/Demo/DemoNavigationView.swift b/apple/DemoApp/Demo/DemoNavigationView.swift index 6b112c72..3fd89aba 100644 --- a/apple/DemoApp/Demo/DemoNavigationView.swift +++ b/apple/DemoApp/Demo/DemoNavigationView.swift @@ -37,12 +37,13 @@ struct DemoNavigationView: View { let simulated = SimulatedLocationProvider(location: initialLocation) simulated.warpFactor = 2 locationProvider = simulated - ferrostarCore = FerrostarCore( + ferrostarCore = try! FerrostarCore( valhallaEndpointUrl: URL( string: "https://api.stadiamaps.com/route/v1?api_key=\(APIKeys.shared.stadiaMapsAPIKey)" )!, - profile: "pedestrian", - locationProvider: locationProvider + profile: "bicycle", + locationProvider: locationProvider, + costingOptions: ["bicycle": ["use_roads": 0.2]] ) // NOTE: Not all applications will need a delegate. Read the NavigationDelegate documentation for details. ferrostarCore.delegate = navigationDelegate diff --git a/apple/Sources/FerrostarCore/FerrostarCore.swift b/apple/Sources/FerrostarCore/FerrostarCore.swift index 1278a885..6abe6bb8 100644 --- a/apple/Sources/FerrostarCore/FerrostarCore.swift +++ b/apple/Sources/FerrostarCore/FerrostarCore.swift @@ -113,11 +113,20 @@ public protocol FerrostarCoreDelegate: AnyObject { valhallaEndpointUrl: URL, profile: String, locationProvider: LocationProviding, + costingOptions: [String: Any] = [:], networkSession: URLRequestLoading = URLSession.shared - ) { - let adapter = RouteAdapter.newValhallaHttp( + ) throws { + guard let jsonCostingOptions = try String( + data: JSONSerialization.data(withJSONObject: costingOptions), + encoding: .utf8 + ) else { + throw InstantiationError.JsonError + } + + let adapter = try RouteAdapter.newValhallaHttp( endpointUrl: valhallaEndpointUrl.absoluteString, - profile: profile + profile: profile, + costingOptionsJson: jsonCostingOptions ) self.init( routeProvider: .routeAdapter(adapter), diff --git a/apple/Sources/UniFFI/ferrostar.swift b/apple/Sources/UniFFI/ferrostar.swift index f7625da1..5a36e839 100644 --- a/apple/Sources/UniFFI/ferrostar.swift +++ b/apple/Sources/UniFFI/ferrostar.swift @@ -661,11 +661,14 @@ public class RouteAdapter: try! rustCall { uniffi_ferrostar_fn_free_routeadapter(pointer, $0) } } - public static func newValhallaHttp(endpointUrl: String, profile: String) -> RouteAdapter { - RouteAdapter(unsafeFromRawPointer: try! rustCall { + public static func newValhallaHttp(endpointUrl: String, profile: String, + costingOptionsJson: String?) throws -> RouteAdapter + { + try RouteAdapter(unsafeFromRawPointer: rustCallWithError(FfiConverterTypeInstantiationError.lift) { uniffi_ferrostar_fn_constructor_routeadapter_new_valhalla_http( FfiConverterString.lower(endpointUrl), - FfiConverterString.lower(profile), $0 + FfiConverterString.lower(profile), + FfiConverterOptionString.lower(costingOptionsJson), $0 ) }) } @@ -2470,6 +2473,38 @@ public func FfiConverterTypeWaypoint_lower(_ value: Waypoint) -> RustBuffer { FfiConverterTypeWaypoint.lower(value) } +public enum InstantiationError { + case JsonError + + fileprivate static func uniffiErrorHandler(_ error: RustBuffer) throws -> Error { + try FfiConverterTypeInstantiationError.lift(error) + } +} + +public struct FfiConverterTypeInstantiationError: FfiConverterRustBuffer { + typealias SwiftType = InstantiationError + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> InstantiationError { + let variant: Int32 = try readInt(&buf) + switch variant { + case 1: return .JsonError + + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + public static func write(_ value: InstantiationError, into buf: inout [UInt8]) { + switch value { + case .JsonError: + writeInt(&buf, Int32(1)) + } + } +} + +extension InstantiationError: Equatable, Hashable {} + +extension InstantiationError: Error {} + // Note that we don't yet support `indirect` for enums. // See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. /** @@ -3726,12 +3761,15 @@ public func createOsrmResponseParser(polylinePrecision: UInt32) -> RouteResponse * * This is provided as a convenience for use from foreign code when creating your own [`routing_adapters::RouteAdapter`]. */ -public func createValhallaRequestGenerator(endpointUrl: String, profile: String) -> RouteRequestGenerator { - try! FfiConverterTypeRouteRequestGenerator.lift( - try! rustCall { +public func createValhallaRequestGenerator(endpointUrl: String, profile: String, + costingOptionsJson: String?) throws -> RouteRequestGenerator +{ + try FfiConverterTypeRouteRequestGenerator.lift( + rustCallWithError(FfiConverterTypeInstantiationError.lift) { uniffi_ferrostar_fn_func_create_valhalla_request_generator( FfiConverterString.lower(endpointUrl), - FfiConverterString.lower(profile), $0 + FfiConverterString.lower(profile), + FfiConverterOptionString.lower(costingOptionsJson), $0 ) } ) @@ -3828,7 +3866,7 @@ private var initializationResult: InitializationResult { if uniffi_ferrostar_checksum_func_create_osrm_response_parser() != 46856 { return InitializationResult.apiChecksumMismatch } - if uniffi_ferrostar_checksum_func_create_valhalla_request_generator() != 27528 { + if uniffi_ferrostar_checksum_func_create_valhalla_request_generator() != 24001 { return InitializationResult.apiChecksumMismatch } if uniffi_ferrostar_checksum_func_get_route_polyline() != 53320 { @@ -3873,7 +3911,7 @@ private var initializationResult: InitializationResult { if uniffi_ferrostar_checksum_constructor_routeadapter_new() != 15081 { return InitializationResult.apiChecksumMismatch } - if uniffi_ferrostar_checksum_constructor_routeadapter_new_valhalla_http() != 24553 { + if uniffi_ferrostar_checksum_constructor_routeadapter_new_valhalla_http() != 22565 { return InitializationResult.apiChecksumMismatch } diff --git a/apple/Tests/FerrostarCoreTests/FerrostarCoreTests.swift b/apple/Tests/FerrostarCoreTests/FerrostarCoreTests.swift index b1a361e8..54d9437f 100644 --- a/apple/Tests/FerrostarCoreTests/FerrostarCoreTests.swift +++ b/apple/Tests/FerrostarCoreTests/FerrostarCoreTests.swift @@ -150,6 +150,39 @@ final class FerrostarCoreTests: XCTestCase { assertSnapshot(of: routes, as: .dump) } + @MainActor + func testValhalalCostingOptionsJSON() async throws { + let mockSession = MockURLSession() + mockSession.registerMock( + forURL: valhallaEndpointUrl, + withData: sampleRouteData, + andResponse: successfulJSONResponse + ) + + // The main feature of this test is that it uses this constructor, + // which can throw, and similarly getRoutes may not always work with invalid input + let core = try FerrostarCore( + valhallaEndpointUrl: valhallaEndpointUrl, + profile: "low_speed_vehicle", + locationProvider: SimulatedLocationProvider(), + costingOptions: ["low_speed_vehicle": ["vehicle_type": "golf_cart"]], + networkSession: mockSession + ) + + // Tests that the core generates a request and then the mocked parser returns the expected routes + let routes = try await core.getRoutes( + initialLocation: UserLocation( + coordinates: GeographicCoordinate(lat: 60.5347155, lng: -149.543469), + horizontalAccuracy: 0, + courseOverGround: nil, + timestamp: Date(), + speed: nil + ), + waypoints: [Waypoint(coordinate: GeographicCoordinate(lat: 60.5349908, lng: -149.5485806), kind: .break)] + ) + assertSnapshot(of: routes, as: .dump) + } + @MainActor func testCustomRouteProvider() async throws { let expectation = expectation(description: "The custom route provider should be called once") diff --git a/apple/Tests/FerrostarCoreTests/ValhallaCoreTests.swift b/apple/Tests/FerrostarCoreTests/ValhallaCoreTests.swift index 5f7da358..29011779 100644 --- a/apple/Tests/FerrostarCoreTests/ValhallaCoreTests.swift +++ b/apple/Tests/FerrostarCoreTests/ValhallaCoreTests.swift @@ -14,7 +14,7 @@ final class ValhallaCoreTests: XCTestCase { andResponse: successfulJSONResponse ) - let core = FerrostarCore( + let core = try FerrostarCore( valhallaEndpointUrl: valhallaEndpointUrl, profile: "auto", locationProvider: SimulatedLocationProvider(), diff --git a/apple/Tests/FerrostarCoreTests/__Snapshots__/FerrostarCoreTests/testValhalalCostingOptionsJSON.1.txt b/apple/Tests/FerrostarCoreTests/__Snapshots__/FerrostarCoreTests/testValhalalCostingOptionsJSON.1.txt new file mode 100644 index 00000000..ce00d071 --- /dev/null +++ b/apple/Tests/FerrostarCoreTests/__Snapshots__/FerrostarCoreTests/testValhalalCostingOptionsJSON.1.txt @@ -0,0 +1,107 @@ +▿ 1 element + ▿ Route + ▿ bbox: BoundingBox + ▿ ne: GeographicCoordinate + - lat: 60.535008 + - lng: -149.543469 + ▿ sw: GeographicCoordinate + - lat: 60.534716 + - lng: -149.548581 + - distance: 284.0 + ▿ geometry: 10 elements + ▿ GeographicCoordinate + - lat: 60.534716 + - lng: -149.543469 + ▿ GeographicCoordinate + - lat: 60.534782 + - lng: -149.543879 + ▿ GeographicCoordinate + - lat: 60.534829 + - lng: -149.544134 + ▿ GeographicCoordinate + - lat: 60.534856 + - lng: -149.5443 + ▿ GeographicCoordinate + - lat: 60.534887 + - lng: -149.544533 + ▿ GeographicCoordinate + - lat: 60.534941 + - lng: -149.544976 + ▿ GeographicCoordinate + - lat: 60.534971 + - lng: -149.545485 + ▿ GeographicCoordinate + - lat: 60.535003 + - lng: -149.546177 + ▿ GeographicCoordinate + - lat: 60.535008 + - lng: -149.546937 + ▿ GeographicCoordinate + - lat: 60.534991 + - lng: -149.548581 + ▿ steps: 2 elements + ▿ RouteStep + - distance: 284.0 + - duration: 11.488 + ▿ geometry: 10 elements + ▿ GeographicCoordinate + - lat: 60.534716 + - lng: -149.543469 + ▿ GeographicCoordinate + - lat: 60.534782 + - lng: -149.543879 + ▿ GeographicCoordinate + - lat: 60.534829 + - lng: -149.544134 + ▿ GeographicCoordinate + - lat: 60.534856 + - lng: -149.5443 + ▿ GeographicCoordinate + - lat: 60.534887 + - lng: -149.544533 + ▿ GeographicCoordinate + - lat: 60.534941 + - lng: -149.544976 + ▿ GeographicCoordinate + - lat: 60.534971 + - lng: -149.545485 + ▿ GeographicCoordinate + - lat: 60.535003 + - lng: -149.546177 + ▿ GeographicCoordinate + - lat: 60.535008 + - lng: -149.546937 + ▿ GeographicCoordinate + - lat: 60.534991 + - lng: -149.548581 + - instruction: "Drive west on AK 1/Seward Highway." + ▿ roadName: Optional + - some: "Seward Highway" + - spokenInstructions: 0 elements + - visualInstructions: 0 elements + ▿ RouteStep + - distance: 0.0 + - duration: 0.0 + ▿ geometry: 2 elements + ▿ GeographicCoordinate + - lat: 60.534991 + - lng: -149.548581 + ▿ GeographicCoordinate + - lat: 60.534991 + - lng: -149.548581 + - instruction: "You have arrived at your destination." + ▿ roadName: Optional + - some: "Seward Highway" + - spokenInstructions: 0 elements + - visualInstructions: 0 elements + ▿ waypoints: 2 elements + ▿ Waypoint + ▿ coordinate: GeographicCoordinate + - lat: 60.534715 + - lng: -149.543469 + - kind: WaypointKind.break + ▿ Waypoint + ▿ coordinate: GeographicCoordinate + - lat: 60.534991 + - lng: -149.548581 + - kind: WaypointKind.break diff --git a/common/Cargo.lock b/common/Cargo.lock index 09399534..8c8c72c6 100644 --- a/common/Cargo.lock +++ b/common/Cargo.lock @@ -336,7 +336,7 @@ checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" [[package]] name = "ferrostar" -version = "0.0.31" +version = "0.0.32" dependencies = [ "assert-json-diff", "geo", diff --git a/common/ferrostar/Cargo.toml b/common/ferrostar/Cargo.toml index 29ae0f41..68d32820 100644 --- a/common/ferrostar/Cargo.toml +++ b/common/ferrostar/Cargo.toml @@ -2,7 +2,7 @@ lints.workspace = true [package] name = "ferrostar" -version = "0.0.31" +version = "0.0.32" readme = "README.md" description = "The core of modern turn-by-turn navigation." keywords = ["navigation", "routing", "valhalla", "osrm"] diff --git a/common/ferrostar/src/lib.rs b/common/ferrostar/src/lib.rs index 6bb50d17..4bc47d7a 100644 --- a/common/ferrostar/src/lib.rs +++ b/common/ferrostar/src/lib.rs @@ -21,6 +21,7 @@ use std::str::FromStr; use std::sync::Arc; use uuid::Uuid; +use crate::routing_adapters::error::InstantiationError; use routing_adapters::{RouteRequestGenerator, RouteResponseParser}; uniffi::setup_scaffolding!(); @@ -55,8 +56,15 @@ impl UniffiCustomTypeConverter for Uuid { fn create_valhalla_request_generator( endpoint_url: String, profile: String, -) -> Arc { - Arc::new(ValhallaHttpRequestGenerator::new(endpoint_url, profile)) + costing_options_json: Option, +) -> Result, InstantiationError> { + Ok(Arc::new( + ValhallaHttpRequestGenerator::with_costing_options_json( + endpoint_url, + profile, + costing_options_json, + )?, + )) } /// Creates a [`RouteResponseParser`] capable of parsing OSRM responses. diff --git a/common/ferrostar/src/routing_adapters/error.rs b/common/ferrostar/src/routing_adapters/error.rs index c7020830..5c42208b 100644 --- a/common/ferrostar/src/routing_adapters/error.rs +++ b/common/ferrostar/src/routing_adapters/error.rs @@ -4,6 +4,13 @@ use uniffi::UnexpectedUniFFICallbackError; // The trouble appears to be with generating "flat" enum bindings that are used with callback // interfaces when the underlying actually has fields. #[derive(Debug, thiserror::Error, uniffi::Error)] +pub enum InstantiationError { + #[error("Error generating JSON for the request.")] + JsonError, +} + +// TODO: See comment above +#[derive(Debug, thiserror::Error, uniffi::Error)] pub enum RoutingRequestGenerationError { #[error("Too few waypoints were provided to compute a route.")] NotEnoughWaypoints, @@ -19,6 +26,12 @@ impl From for RoutingRequestGenerationError { } } +impl From for InstantiationError { + fn from(_: serde_json::Error) -> Self { + InstantiationError::JsonError + } +} + impl From for RoutingRequestGenerationError { fn from(_: serde_json::Error) -> Self { RoutingRequestGenerationError::JsonError diff --git a/common/ferrostar/src/routing_adapters/mod.rs b/common/ferrostar/src/routing_adapters/mod.rs index 52ae0985..c5181f4a 100644 --- a/common/ferrostar/src/routing_adapters/mod.rs +++ b/common/ferrostar/src/routing_adapters/mod.rs @@ -1,4 +1,5 @@ use crate::models::Waypoint; +use crate::routing_adapters::error::InstantiationError; use crate::{ create_osrm_response_parser, create_valhalla_request_generator, models::{Route, UserLocation}, @@ -99,10 +100,15 @@ impl RouteAdapter { } #[uniffi::constructor] - pub fn new_valhalla_http(endpoint_url: String, profile: String) -> Self { - let request_generator = create_valhalla_request_generator(endpoint_url, profile); + pub fn new_valhalla_http( + endpoint_url: String, + profile: String, + costing_options_json: Option, + ) -> Result { + let request_generator = + create_valhalla_request_generator(endpoint_url, profile, costing_options_json)?; let response_parser = create_osrm_response_parser(6); - Self::new(request_generator, response_parser) + Ok(Self::new(request_generator, response_parser)) } // diff --git a/common/ferrostar/src/routing_adapters/valhalla.rs b/common/ferrostar/src/routing_adapters/valhalla.rs index 35a043ef..a7aa7ab1 100644 --- a/common/ferrostar/src/routing_adapters/valhalla.rs +++ b/common/ferrostar/src/routing_adapters/valhalla.rs @@ -16,17 +16,37 @@ pub struct ValhallaHttpRequestGenerator { /// /// Users *may* include a query string with an API key. endpoint_url: String, + /// The Valhalla costing model to use. profile: String, - // TODO: more tunable parameters; a dict that gets inserted seems like a bare minimum; we can also allow higher level ones + // TODO: Language, units, and other top-level parameters + /// JSON costing options to pass through. + costing_options: JsonValue, } impl ValhallaHttpRequestGenerator { - pub fn new(endpoint_url: String, profile: String) -> Self { + pub fn new(endpoint_url: String, profile: String, costing_options: Option) -> Self { Self { endpoint_url, profile, + costing_options: costing_options.unwrap_or(json!({})), } } + + pub fn with_costing_options_json( + endpoint_url: String, + profile: String, + costing_options_json: Option, + ) -> Result { + let parsed_costing_options: JsonValue = match costing_options_json.as_deref() { + Some(options) => serde_json::from_str(options)?, + None => json!({}), + }; + Ok(Self { + endpoint_url, + profile, + costing_options: parsed_costing_options, + }) + } } impl RouteRequestGenerator for ValhallaHttpRequestGenerator { @@ -63,6 +83,7 @@ impl RouteRequestGenerator for ValhallaHttpRequestGenerator { }) })) .collect(); + // NOTE: We currently use the OSRM format, as it is the richest one. // Though it would be nice to use PBF if we can get the required data. // However, certain info (like banners) are only available in the OSRM format. @@ -82,6 +103,7 @@ impl RouteRequestGenerator for ValhallaHttpRequestGenerator { "voice_instructions": true, "costing": &self.profile, "locations": locations, + "costing_options": &self.costing_options, }); let body = serde_json::to_vec(&args)?; Ok(RouteRequest::HttpPost { @@ -134,7 +156,7 @@ mod tests { #[test] fn not_enough_locations() { let generator = - ValhallaHttpRequestGenerator::new(ENDPOINT_URL.to_string(), COSTING.to_string()); + ValhallaHttpRequestGenerator::new(ENDPOINT_URL.to_string(), COSTING.to_string(), None); // At least two locations are required assert!(matches!( @@ -143,27 +165,38 @@ mod tests { )); } - fn generate_body(user_location: UserLocation, waypoints: Vec) -> JsonValue { - let generator = - ValhallaHttpRequestGenerator::new(ENDPOINT_URL.to_string(), COSTING.to_string()); - - let RouteRequest::HttpPost { - url: request_url, - headers, - body, - } = generator - .generate_request(user_location, waypoints) - .unwrap(); - - assert_eq!(ENDPOINT_URL, request_url); - assert_eq!(headers["Content-Type"], "application/json".to_string()); + fn generate_body( + user_location: UserLocation, + waypoints: Vec, + costing_options_json: Option, + ) -> JsonValue { + let generator = ValhallaHttpRequestGenerator::with_costing_options_json( + ENDPOINT_URL.to_string(), + COSTING.to_string(), + costing_options_json, + ) + .expect("Unable to create request generator"); - from_slice(&body).expect("Failed to parse request body as JSON") + match generator.generate_request(user_location, waypoints) { + Ok(RouteRequest::HttpPost { + url: request_url, + headers, + body, + }) => { + assert_eq!(ENDPOINT_URL, request_url); + assert_eq!(headers["Content-Type"], "application/json".to_string()); + from_slice(&body).expect("Failed to parse request body as JSON") + } + Err(e) => { + println!("Failed to generate request: {:?}", e); + json!(null) + } + } } #[test] fn request_body_without_course() { - let body_json = generate_body(USER_LOCATION, WAYPOINTS.to_vec()); + let body_json = generate_body(USER_LOCATION, WAYPOINTS.to_vec(), None); assert_json_include!( actual: body_json, @@ -183,14 +216,14 @@ mod tests { "lat": 2.0, "lon": 3.0, } - ] + ], }) ); } #[test] fn request_body_with_course() { - let body_json = generate_body(USER_LOCATION_WITH_COURSE, WAYPOINTS.to_vec()); + let body_json = generate_body(USER_LOCATION_WITH_COURSE, WAYPOINTS.to_vec(), None); assert_json_include!( actual: body_json, @@ -211,7 +244,39 @@ mod tests { "lat": 2.0, "lon": 3.0, } - ] + ], + }) + ); + } + + #[test] + fn request_body_without_costing_options() { + let body_json = generate_body(USER_LOCATION, WAYPOINTS.to_vec(), None); + + assert_json_include!( + actual: body_json, + expected: json!({ + "costing_options": {}, + }) + ); + } + + #[test] + fn request_body_with_costing_options() { + let body_json = generate_body( + USER_LOCATION, + WAYPOINTS.to_vec(), + Some(r#"{"bicycle": {"bicycle_type": "Road"}}"#.to_string()), + ); + + assert_json_include!( + actual: body_json, + expected: json!({ + "costing_options": { + "bicycle": { + "bicycle_type": "Road", + }, + }, }) ); } @@ -219,7 +284,7 @@ mod tests { #[test] fn request_body_with_invalid_horizontal_accuracy() { let generator = - ValhallaHttpRequestGenerator::new(ENDPOINT_URL.to_string(), COSTING.to_string()); + ValhallaHttpRequestGenerator::new(ENDPOINT_URL.to_string(), COSTING.to_string(), None); let location = UserLocation { coordinates: GeographicCoordinate { lat: 0.0, lng: 0.0 }, horizontal_accuracy: -6.0, @@ -259,7 +324,7 @@ mod tests { "lat": 2.0, "lon": 3.0, } - ] + ], }) ); }