diff --git a/native/kotlin/api/kotlin/src/main/kotlin/rs/wordpress/api/kotlin/WpRequestExecutor.kt b/native/kotlin/api/kotlin/src/main/kotlin/rs/wordpress/api/kotlin/WpRequestExecutor.kt index 385b71078..160ba93c8 100644 --- a/native/kotlin/api/kotlin/src/main/kotlin/rs/wordpress/api/kotlin/WpRequestExecutor.kt +++ b/native/kotlin/api/kotlin/src/main/kotlin/rs/wordpress/api/kotlin/WpRequestExecutor.kt @@ -16,9 +16,11 @@ internal class WpRequestExecutor(private val dispatcher: CoroutineDispatcher = D override suspend fun execute(request: WpNetworkRequest): WpNetworkResponse = withContext(dispatcher) { - val requestBuilder = Request.Builder().url(request.url) - request.headerMap.forEach { (key, value) -> - requestBuilder.header(key, value) + val requestBuilder = Request.Builder().url(request.url()) + request.headerMap().toMap().forEach { (key, values) -> + values.forEach { value -> + requestBuilder.addHeader(key, value) + } } client.newCall(requestBuilder.build()).execute().use { response -> diff --git a/native/kotlin/example/composeApp/src/commonMain/kotlin/rs/wordpress/example/shared/di/AppModule.kt b/native/kotlin/example/composeApp/src/commonMain/kotlin/rs/wordpress/example/shared/di/AppModule.kt index 486a74c6d..02070d73b 100644 --- a/native/kotlin/example/composeApp/src/commonMain/kotlin/rs/wordpress/example/shared/di/AppModule.kt +++ b/native/kotlin/example/composeApp/src/commonMain/kotlin/rs/wordpress/example/shared/di/AppModule.kt @@ -16,7 +16,7 @@ val authModule = module { // Until this works with the included test credentials, you can grab it from the // `test_credentials` file `make test-server` will generate in the root of the repo // It's the 3rd line in that file - localTestSitePassword = "l4dvgr1UjuCKXiqkbdbXn1b5" + localTestSitePassword = "s3N7vlbdrFPDDI3MbyFUvS3P" ) } } diff --git a/native/swift/Sources/wordpress-api/WordPressAPI.swift b/native/swift/Sources/wordpress-api/WordPressAPI.swift index 317d79e73..c271bab69 100644 --- a/native/swift/Sources/wordpress-api/WordPressAPI.swift +++ b/native/swift/Sources/wordpress-api/WordPressAPI.swift @@ -92,25 +92,33 @@ public struct WordPressAPI { } } +public extension WpNetworkHeaderMap { + func toFlatMap() -> [String: String] { + self.toMap().mapValues { $0.joined(separator: ",") } + } +} + public extension WpNetworkRequest { func asURLRequest() -> URLRequest { - let url = URL(string: self.url)! + let url = URL(string: self.url())! var request = URLRequest(url: url) - request.httpMethod = self.method.rawValue - request.allHTTPHeaderFields = self.headerMap - request.httpBody = self.body + request.httpMethod = self.method().rawValue + request.allHTTPHeaderFields = self.headerMap().toFlatMap() + request.httpBody = self.body()?.contents() return request } #if DEBUG func debugPrint() { - print("\(method.rawValue) \(url)") - for (name, value) in headerMap { + print("\(method().rawValue) \(self.url())") + for (name, value) in self.headerMap().toMap() { print("\(name): \(value)") } + print("") - if let body, let text = String(data: body, encoding: .utf8) { - print(text) + + if let bodyString = self.bodyAsString() { + print(bodyString) } } #endif @@ -183,12 +191,6 @@ extension RequestMethod { } } -extension WpNetworkRequest { - init(method: RequestMethod, url: URL, headerMap: [String: String]) { - self.init(method: method, url: url.absoluteString, headerMap: headerMap, body: nil) - } -} - extension WpRestApiUrl { func asUrl() -> URL { guard let url = URL(string: stringValue) else { diff --git a/native/swift/Tests/wordpress-api/Support/HTTPStubs.swift b/native/swift/Tests/wordpress-api/Support/HTTPStubs.swift index 8c912af28..5892ca5c8 100644 --- a/native/swift/Tests/wordpress-api/Support/HTTPStubs.swift +++ b/native/swift/Tests/wordpress-api/Support/HTTPStubs.swift @@ -35,7 +35,7 @@ class HTTPStubs: SafeRequestExecutor { func stub(path: String, with response: WpNetworkResponse) { stubs.append(( - condition: { URL(string: $0.url)?.path == path }, + condition: { URL(string: $0.url())?.path == path }, response: response )) } diff --git a/native/swift/Tests/wordpress-api/WordPressAPITests.swift b/native/swift/Tests/wordpress-api/WordPressAPITests.swift index d74ca9488..c786f9d54 100644 --- a/native/swift/Tests/wordpress-api/WordPressAPITests.swift +++ b/native/swift/Tests/wordpress-api/WordPressAPITests.swift @@ -69,7 +69,9 @@ final class WordPressAPITests: XCTestCase { } catch let error as URLError { XCTAssertEqual(error.code, .timedOut) } catch { + #if canImport(WordPressAPIInternal) XCTAssertTrue(error is WordPressAPIInternal.WpApiError) + #endif } } #endif diff --git a/wp_api/src/login/login_client.rs b/wp_api/src/login/login_client.rs index 96c1153f1..3d3aadf80 100644 --- a/wp_api/src/login/login_client.rs +++ b/wp_api/src/login/login_client.rs @@ -1,9 +1,10 @@ -use std::collections::HashMap; use std::str; use std::sync::Arc; use crate::request::endpoint::WpEndpointUrl; -use crate::request::{RequestExecutor, RequestMethod, WpNetworkRequest, WpNetworkResponse}; +use crate::request::{ + RequestExecutor, RequestMethod, WpNetworkHeaderMap, WpNetworkRequest, WpNetworkResponse, +}; use super::url_discovery::{ self, FetchApiDetailsError, FetchApiRootUrlError, ParsedUrl, StateInitial, @@ -129,11 +130,11 @@ impl WpLoginClient { let api_root_request = WpNetworkRequest { method: RequestMethod::HEAD, url: WpEndpointUrl(parsed_site_url.url()), - header_map: HashMap::new(), + header_map: WpNetworkHeaderMap::default().into(), body: None, }; self.request_executor - .execute(api_root_request) + .execute(api_root_request.into()) .await .map_err(FetchApiRootUrlError::from) } @@ -143,12 +144,15 @@ impl WpLoginClient { api_root_url: &ParsedUrl, ) -> Result { self.request_executor - .execute(WpNetworkRequest { - method: RequestMethod::GET, - url: WpEndpointUrl(api_root_url.url()), - header_map: HashMap::new(), - body: None, - }) + .execute( + WpNetworkRequest { + method: RequestMethod::GET, + url: WpEndpointUrl(api_root_url.url()), + header_map: WpNetworkHeaderMap::default().into(), + body: None, + } + .into(), + ) .await .map_err(FetchApiDetailsError::from) } diff --git a/wp_api/src/request.rs b/wp_api/src/request.rs index fba2b4c41..de7348fc7 100644 --- a/wp_api/src/request.rs +++ b/wp_api/src/request.rs @@ -28,7 +28,7 @@ impl InnerRequestBuilder { WpNetworkRequest { method: RequestMethod::GET, url: url.into(), - header_map: self.header_map(), + header_map: self.header_map().into(), body: None, } } @@ -40,8 +40,10 @@ impl InnerRequestBuilder { WpNetworkRequest { method: RequestMethod::POST, url: url.into(), - header_map: self.header_map_for_post_request(), - body: serde_json::to_vec(json_body).ok(), + header_map: self.header_map_for_post_request().into(), + body: serde_json::to_vec(json_body) + .ok() + .map(|b| Arc::new(WpNetworkRequestBody::new(b))), } } @@ -49,31 +51,33 @@ impl InnerRequestBuilder { WpNetworkRequest { method: RequestMethod::DELETE, url: url.into(), - header_map: self.header_map(), + header_map: self.header_map().into(), body: None, } } - fn header_map(&self) -> HashMap { - let mut header_map = HashMap::new(); + fn header_map(&self) -> WpNetworkHeaderMap { + let mut header_map = HeaderMap::new(); header_map.insert( - http::header::ACCEPT.to_string(), - CONTENT_TYPE_JSON.to_string(), + http::header::ACCEPT, + HeaderValue::from_static(CONTENT_TYPE_JSON), ); match self.authentication { - WpAuthentication::None => None, + WpAuthentication::None => (), WpAuthentication::AuthorizationHeader { ref token } => { - header_map.insert("Authorization".to_string(), format!("Basic {}", token)) + let hv = HeaderValue::from_str(&format!("Basic {}", token)); + let hv = hv.expect("It shouldn't be possible to build WpAuthentication::AuthorizationHeader with an invalid token"); + header_map.insert(http::header::AUTHORIZATION, hv); } }; - header_map + header_map.into() } - fn header_map_for_post_request(&self) -> HashMap { + fn header_map_for_post_request(&self) -> WpNetworkHeaderMap { let mut header_map = self.header_map(); - header_map.insert( - http::header::CONTENT_TYPE.to_string(), - CONTENT_TYPE_JSON.to_string(), + header_map.inner.insert( + http::header::CONTENT_TYPE, + HeaderValue::from_static(CONTENT_TYPE_JSON), ); header_map } @@ -84,27 +88,57 @@ impl InnerRequestBuilder { pub trait RequestExecutor: Send + Sync + Debug { async fn execute( &self, - request: WpNetworkRequest, + request: Arc, ) -> Result; } +#[derive(uniffi::Object)] +pub struct WpNetworkRequestBody { + inner: Vec, +} + +impl WpNetworkRequestBody { + fn new(body: Vec) -> Self { + Self { inner: body } + } +} + +#[uniffi::export] +impl WpNetworkRequestBody { + pub fn contents(&self) -> Vec { + self.inner.clone() + } +} + // Has custom `Debug` trait implementation -#[derive(uniffi::Record)] +#[derive(uniffi::Object)] pub struct WpNetworkRequest { - pub method: RequestMethod, - pub url: WpEndpointUrl, - // TODO: We probably want to implement a specific type for these headers instead of using a - // regular HashMap. - // - // It could be something similar to `reqwest`'s [`header`](https://docs.rs/reqwest/latest/reqwest/header/index.html) - // module. - pub header_map: HashMap, - pub body: Option>, + pub(crate) method: RequestMethod, + pub(crate) url: WpEndpointUrl, + pub(crate) header_map: Arc, + pub(crate) body: Option>, } +#[uniffi::export] impl WpNetworkRequest { + pub fn method(&self) -> RequestMethod { + self.method.clone() + } + + pub fn url(&self) -> WpEndpointUrl { + self.url.clone() + } + + pub fn header_map(&self) -> Arc { + self.header_map.clone() + } + + pub fn body(&self) -> Option> { + self.body.clone() + } + pub fn body_as_string(&self) -> Option { - self.body.as_ref().map(|b| body_as_string(b)) + self.body.as_ref().map(|b| body_as_string(&b.inner)) } } @@ -137,7 +171,7 @@ pub struct WpNetworkResponse { pub header_map: Arc, } -#[derive(Debug, uniffi::Object)] +#[derive(Debug, Default, Clone, uniffi::Object)] pub struct WpNetworkHeaderMap { inner: HeaderMap, } @@ -177,6 +211,10 @@ impl WpNetworkHeaderMap { }) .collect() } + + pub fn as_header_map(&self) -> HeaderMap { + self.inner.clone() + } } #[uniffi::export] @@ -206,6 +244,18 @@ impl WpNetworkHeaderMap { .collect::>()?; Ok(Self { inner }) } + + fn to_map(&self) -> HashMap> { + let mut header_hashmap = HashMap::new(); + self.inner.iter().for_each(|(k, v)| { + let v = String::from_utf8_lossy(v.as_bytes()).into_owned(); + header_hashmap + .entry(k.as_str().to_owned()) + .or_insert_with(Vec::new) + .push(v) + }); + header_hashmap + } } impl From for WpNetworkHeaderMap { diff --git a/wp_api/src/request/endpoint.rs b/wp_api/src/request/endpoint.rs index 0cac04dc9..b7b5c8584 100644 --- a/wp_api/src/request/endpoint.rs +++ b/wp_api/src/request/endpoint.rs @@ -9,7 +9,7 @@ pub(crate) mod users_endpoint; const WP_JSON_PATH_SEGMENTS: [&str; 3] = ["wp-json", "wp", "v2"]; uniffi::custom_newtype!(WpEndpointUrl, String); -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct WpEndpointUrl(pub String); impl From for WpEndpointUrl { diff --git a/wp_api/tests/integration_test_common.rs b/wp_api/tests/integration_test_common.rs index 029842638..94236b281 100644 --- a/wp_api/tests/integration_test_common.rs +++ b/wp_api/tests/integration_test_common.rs @@ -1,6 +1,5 @@ use async_trait::async_trait; use futures::Future; -use http::HeaderMap; use std::{process::Command, sync::Arc}; use wp_api::{ request::{ @@ -160,16 +159,17 @@ impl Default for AsyncWpNetworking { impl AsyncWpNetworking { pub async fn async_request( &self, - wp_request: WpNetworkRequest, + wp_request: Arc, ) -> Result { - let request_headers: HeaderMap = (&wp_request.header_map).try_into().unwrap(); - let mut request = self .client - .request(Self::request_method(wp_request.method), wp_request.url.0) - .headers(request_headers); - if let Some(body) = wp_request.body { - request = request.body(body); + .request( + Self::request_method(wp_request.method()), + wp_request.url().0.as_str(), + ) + .headers(wp_request.header_map().as_header_map()); + if let Some(body) = wp_request.body() { + request = request.body(body.contents()); } let mut response = request.send().await?; @@ -195,7 +195,7 @@ impl AsyncWpNetworking { impl RequestExecutor for AsyncWpNetworking { async fn execute( &self, - request: WpNetworkRequest, + request: Arc, ) -> Result { self.async_request(request).await.map_err(|err| { RequestExecutionError::RequestExecutionFailed { diff --git a/wp_api/tests/test_manual_request_builder_immut.rs b/wp_api/tests/test_manual_request_builder_immut.rs index 8867ab94c..4bbe97b52 100644 --- a/wp_api/tests/test_manual_request_builder_immut.rs +++ b/wp_api/tests/test_manual_request_builder_immut.rs @@ -31,7 +31,7 @@ async fn list_users_with_edit_context(#[case] params: UserListParams) { WpApiRequestBuilder::new(TEST_CREDENTIALS_SITE_URL.to_string(), authentication) .expect("Site url is generated by our tooling"); let wp_request = request_builder.users().list_with_edit_context(¶ms); - let response = async_wp_networking.async_request(wp_request).await; + let response = async_wp_networking.async_request(wp_request.into()).await; let result = response.unwrap().parse::>(); assert!(result.is_ok(), "Response was: '{:?}'", result); } diff --git a/wp_derive_request_builder/src/generate.rs b/wp_derive_request_builder/src/generate.rs index 3f21e96a2..5988e17c1 100644 --- a/wp_derive_request_builder/src/generate.rs +++ b/wp_derive_request_builder/src/generate.rs @@ -64,7 +64,7 @@ fn generate_async_request_executor(config: &Config, parsed_enum: &ParsedEnum) -> quote! { pub async #fn_signature -> Result<#output_type, #static_wp_api_error_type> { #request_from_request_builder - self.request_executor.execute(request).await?.parse() + self.request_executor.execute(std::sync::Arc::new(request)).await?.parse() } } })