From 6f299a21a951c7da8c132c89afea427802cd96f0 Mon Sep 17 00:00:00 2001 From: Oguz Kocer Date: Mon, 18 Nov 2024 19:42:03 -0500 Subject: [PATCH 1/8] Create `/media` endpoint --- Cargo.lock | 16 +++ native/kotlin/api/kotlin/build.gradle.kts | 2 +- .../kotlin/MediaEndpointTest.kt | 16 +++ .../wordpress/api/kotlin/WpRequestExecutor.kt | 41 ++++++ native/kotlin/build.gradle.kts | 11 ++ test_media.jpg | Bin 0 -> 780 bytes wp_api/src/media.rs | 61 +++++++++ wp_api/src/request.rs | 8 +- wp_api/src/request/endpoint/media_endpoint.rs | 119 +++++++++++++++++- wp_api_integration_tests/Cargo.toml | 2 +- wp_api_integration_tests/src/lib.rs | 56 ++++++++- .../tests/test_media_mut.rs | 29 +++-- 12 files changed, 348 insertions(+), 13 deletions(-) create mode 100644 test_media.jpg diff --git a/Cargo.lock b/Cargo.lock index c6967ddaf..dd782d657 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1697,6 +1697,7 @@ dependencies = [ "js-sys", "log", "mime", + "mime_guess", "native-tls", "once_cell", "percent-encoding", @@ -1709,10 +1710,12 @@ dependencies = [ "system-configuration", "tokio", "tokio-native-tls", + "tokio-util", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", "windows-registry", ] @@ -2939,6 +2942,19 @@ version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +[[package]] +name = "wasm-streams" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "web-sys" version = "0.3.69" diff --git a/native/kotlin/api/kotlin/build.gradle.kts b/native/kotlin/api/kotlin/build.gradle.kts index 67db7699f..d3d2cf6c3 100644 --- a/native/kotlin/api/kotlin/build.gradle.kts +++ b/native/kotlin/api/kotlin/build.gradle.kts @@ -115,13 +115,13 @@ val generateUniFFIBindingsTask = tasks.register("generateUniFFIBindings") inputs.dir("$cargoProjectRoot/$rustModuleName/") } - tasks.named("compileKotlin").configure { dependsOn(generateUniFFIBindingsTask) } tasks.named("processIntegrationTestResources").configure { dependsOn(rootProject.tasks.named("copyDesktopJniLibs")) dependsOn(rootProject.tasks.named("copyTestCredentials")) + dependsOn(rootProject.tasks.named("copyTestMedia")) } project.afterEvaluate { diff --git a/native/kotlin/api/kotlin/src/integrationTest/kotlin/MediaEndpointTest.kt b/native/kotlin/api/kotlin/src/integrationTest/kotlin/MediaEndpointTest.kt index 00409dfc5..7d914a97a 100644 --- a/native/kotlin/api/kotlin/src/integrationTest/kotlin/MediaEndpointTest.kt +++ b/native/kotlin/api/kotlin/src/integrationTest/kotlin/MediaEndpointTest.kt @@ -3,9 +3,11 @@ package rs.wordpress.api.kotlin import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Test +import uniffi.wp_api.MediaCreateParams import uniffi.wp_api.MediaListParams import uniffi.wp_api.SparseMediaFieldWithEditContext import uniffi.wp_api.wpAuthenticationFromUsernameAndPassword +import kotlin.test.assertEquals import kotlin.test.assertNotNull private const val MEDIA_ID_611: Long = 611 @@ -63,4 +65,18 @@ class MediaEndpointTest { assertNotNull(sparseMedia) assertNull(sparseMedia.slug) } + + @Test + fun testUploadMediaRequest() = runTest { + val title = "Testing media upload from Kotlin" + val response = client.request { requestBuilder -> + requestBuilder.media().upload( + params = MediaCreateParams(title = title), + "test_media.jpg", + "image/jpg" + ) + }.assertSuccessAndRetrieveData().data + assertEquals(title, response.title.rendered) + // TODO: Restore server + } } 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 6086b0743..ff2c9c6c0 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 @@ -3,13 +3,19 @@ package rs.wordpress.api.kotlin import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import okhttp3.MediaType +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.MultipartBody import okhttp3.OkHttpClient import okhttp3.Request +import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.toRequestBody +import uniffi.wp_api.MediaUploadRequest import uniffi.wp_api.RequestExecutor import uniffi.wp_api.WpNetworkHeaderMap import uniffi.wp_api.WpNetworkRequest import uniffi.wp_api.WpNetworkResponse +import java.io.File class WpRequestExecutor( private val okHttpClient: OkHttpClient = OkHttpClient(), @@ -29,6 +35,41 @@ class WpRequestExecutor( } } + okHttpClient.newCall(requestBuilder.build()).execute().use { response -> + return@withContext WpNetworkResponse( + body = response.body?.bytes() ?: ByteArray(0), + statusCode = response.code.toUShort(), + headerMap = WpNetworkHeaderMap.fromMultiMap(response.headers.toMultimap()) + ) + } + } + + override suspend fun uploadMedia(mediaUploadRequest: MediaUploadRequest): WpNetworkResponse = + withContext(dispatcher) { + val requestBuilder = Request.Builder().url(mediaUploadRequest.url()) + val multipartBodyBuilder = MultipartBody.Builder() + .setType(MultipartBody.FORM) + mediaUploadRequest.mediaParams().forEach { (k, v) -> + multipartBodyBuilder.addFormDataPart(k, v) + } + // TODO: This probably doesn't work with Android - and it looks wrong + val file = + File(WpRequestExecutor::class.java.classLoader!!.getResource(mediaUploadRequest.filePath())!!.file) + multipartBodyBuilder.addFormDataPart( + name = "file", + filename = file.name, + body = file.asRequestBody(mediaUploadRequest.fileContentType().toMediaType()) + ) + requestBuilder.method( + method = mediaUploadRequest.method().toString(), + body = multipartBodyBuilder.build() + ) + mediaUploadRequest.headerMap().toMap().forEach { (key, values) -> + values.forEach { value -> + requestBuilder.addHeader(key, value) + } + } + okHttpClient.newCall(requestBuilder.build()).execute().use { response -> return@withContext WpNetworkResponse( body = response.body?.bytes() ?: ByteArray(0), diff --git a/native/kotlin/build.gradle.kts b/native/kotlin/build.gradle.kts index ea12a77fd..5657723eb 100644 --- a/native/kotlin/build.gradle.kts +++ b/native/kotlin/build.gradle.kts @@ -77,10 +77,21 @@ fun setupJniAndBindings() { into(jniLibsPath) } + tasks.register("deleteTestResources") { + delete = setOf(generatedTestResourcesPath) + } + tasks.register("copyTestCredentials") { + dependsOn(tasks.named("deleteTestResources")) from("$cargoProjectRoot/test_credentials.json") into(generatedTestResourcesPath) } + + tasks.register("copyTestMedia") { + dependsOn(tasks.named("deleteTestResources")) + from("$cargoProjectRoot/test_media.jpg") + into(generatedTestResourcesPath) + } } fun getNativeLibraryExtension(): String { diff --git a/test_media.jpg b/test_media.jpg new file mode 100644 index 0000000000000000000000000000000000000000..85b2794c8dfdcc1bcd506998b80b85d239b4ccfc GIT binary patch literal 780 zcmb7>&r2Io5XWcUOZG>y!M8E~ifgm01*3@WuEAER1ZxZKK~U5ldl3pbD2RyOytP*q z??$A(c<8ZLEj{)Rur0Rq(0`z}o?hp@#2N!VIPi9c;WOX)zPIv9{)F$(_iC>Il4KMt z05Bkr9FQt?ikQ~<1=an{`X$xnCc~)iWVd|nO-l15sAGY?O6Mk<00$<^9&d0zR^$XFOvDwWdk^z7sK z$@pX?Rw8KIwv%;wbGhC)@Phc>AwPm^K?X7$2Z6cBU6h}o3s5~zOQa0d#`ZSj@+&wT zfe9|?O;6b6B!N8wsCOrrD+skn0Mo2i{zFu^3;Yg40(lK_#3~-|Yo>iC?a~ZW%+wEJ z3ZiWn_{~*Dm6Zr63SjgvTLax7T0XpSR1MbNFKjr!u72-ROW#LZ6Iq(>a@%i>pa+#p y@$^3?BaJANV)KLcKq@qGYXV&ewxYRz9>n$O+vUF5==|*6;_B(d=l, + /// The date the post was published, as GMT. + #[uniffi(default = None)] + #[serde(skip_serializing_if = "Option::is_none")] + pub date_gmt: Option, + /// An alphanumeric identifier for the post unique to its type. + #[uniffi(default = None)] + #[serde(skip_serializing_if = "Option::is_none")] + pub slug: Option, + /// A named status for the post. + /// One of: publish, future, draft, pending, private + #[uniffi(default = None)] + #[serde(skip_serializing_if = "Option::is_none")] + pub status: Option, + /// The title for the post. + #[uniffi(default = None)] + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option, + /// The ID for the author of the post. + #[uniffi(default = None)] + #[serde(skip_serializing_if = "Option::is_none")] + pub author: Option, + /// Whether or not comments are open on the post. + /// One of: open, closed + #[uniffi(default = None)] + #[serde(skip_serializing_if = "Option::is_none")] + pub comment_status: Option, + /// Whether or not the post can be pinged. + /// One of: open, closed + #[uniffi(default = None)] + #[serde(skip_serializing_if = "Option::is_none")] + pub ping_status: Option, + /// The theme file to use to display the post. + #[uniffi(default = None)] + #[serde(skip_serializing_if = "Option::is_none")] + pub template: Option, + /// Alternative text to display when attachment is not displayed. + #[uniffi(default = None)] + #[serde(skip_serializing_if = "Option::is_none")] + pub alt_text: Option, + /// The attachment caption. + #[uniffi(default = None)] + #[serde(skip_serializing_if = "Option::is_none")] + pub caption: Option, + /// The attachment description. + #[uniffi(default = None)] + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + /// The ID for the associated post of the attachment. + #[serde(rename = "post")] + #[uniffi(default = None)] + #[serde(skip_serializing_if = "Option::is_none")] + pub post_id: Option, + // meta field is omitted for now: https://github.com/Automattic/wordpress-rs/issues/381 +} + #[derive(Debug, Serialize, Deserialize, uniffi::Record, WpContextual)] pub struct SparseMedia { #[WpContext(edit, embed, view)] diff --git a/wp_api/src/request.rs b/wp_api/src/request.rs index 7eee4bf42..94c33ecdd 100644 --- a/wp_api/src/request.rs +++ b/wp_api/src/request.rs @@ -1,6 +1,6 @@ use std::{collections::HashMap, fmt::Debug, sync::Arc}; -use endpoint::ApiEndpointUrl; +use endpoint::{media_endpoint::MediaUploadRequest, ApiEndpointUrl}; use http::{HeaderMap, HeaderName, HeaderValue}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use url::Url; @@ -16,6 +16,7 @@ use self::endpoint::WpEndpointUrl; pub mod endpoint; const CONTENT_TYPE_JSON: &str = "application/json"; +const CONTENT_TYPE_MULTIPART: &str = "multipart/form-data"; const LINK_HEADER_KEY: &str = "Link"; const HEADER_KEY_WP_TOTAL: &str = "X-WP-Total"; const HEADER_KEY_WP_TOTAL_PAGES: &str = "X-WP-TotalPages"; @@ -123,6 +124,11 @@ pub trait RequestExecutor: Send + Sync + Debug { &self, request: Arc, ) -> Result; + + async fn upload_media( + &self, + media_upload_request: Arc, + ) -> Result; } #[derive(uniffi::Object)] diff --git a/wp_api/src/request/endpoint/media_endpoint.rs b/wp_api/src/request/endpoint/media_endpoint.rs index 12ad01335..a15db64cc 100644 --- a/wp_api/src/request/endpoint/media_endpoint.rs +++ b/wp_api/src/request/endpoint/media_endpoint.rs @@ -1,12 +1,16 @@ -use super::{AsNamespace, DerivedRequest, WpNamespace}; +use std::{collections::HashMap, sync::Arc}; + +use super::{AsNamespace, DerivedRequest, WpEndpointUrl, WpNamespace}; use crate::{ media::{ MediaId, MediaListParams, MediaUpdateParams, MediaWithEditContext, SparseMediaFieldWithEditContext, SparseMediaFieldWithEmbedContext, SparseMediaFieldWithViewContext, }, + request::{RequestMethod, WpNetworkHeaderMap, CONTENT_TYPE_MULTIPART}, SparseField, }; +use http::HeaderValue; use wp_derive_request_builder::WpDerivedRequest; #[derive(WpDerivedRequest)] @@ -15,6 +19,8 @@ enum MediaRequest { List, #[contextual_get(url = "/media/", output = crate::media::SparseMedia, filter_by = crate::media::SparseMediaField)] Retrieve, + #[post(url = "/media", params = &crate::media::MediaCreateParams, output = crate::media::MediaWithEditContext)] + Create, #[delete(url = "/media/", output = crate::media::MediaDeleteResponse)] Delete, #[post(url = "/media/", params = &MediaUpdateParams, output = MediaWithEditContext)] @@ -65,6 +71,112 @@ impl SparseField for SparseMediaFieldWithViewContext { } } +#[derive(uniffi::Object)] +pub struct MediaUploadRequest { + pub(crate) method: RequestMethod, + pub(crate) url: WpEndpointUrl, + pub(crate) header_map: Arc, + pub(crate) file_path: String, + pub(crate) file_content_type: String, + pub(crate) media_params: HashMap, +} + +#[uniffi::export] +impl MediaUploadRequest { + 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 file_path(&self) -> String { + self.file_path.clone() + } + + pub fn file_content_type(&self) -> String { + self.file_content_type.clone() + } + + pub fn media_params(&self) -> HashMap { + self.media_params.clone() + } +} + +impl std::fmt::Debug for MediaUploadRequest { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut s = format!( + indoc::indoc! {" + MediaUploadRequest {{ + method: '{:?}', + url: '{:?}', + header_map: '{:?}', + file_path: '{:?}' + file_content_type: '{:?}' + media_params: '{:?}' + }} + "}, + self.method, + self.url, + self.header_map, + self.file_path, + self.file_content_type, + self.media_params + ); + s.pop(); // Remove the new line at the end + write!(f, "{}", s) + } +} + +#[uniffi::export] +impl MediaRequestBuilder { + pub fn upload( + &self, + params: &crate::media::MediaCreateParams, + file_path: String, + file_content_type: String, + ) -> MediaUploadRequest { + let url = self.endpoint.create(); + let mut header_map = self.inner.header_map(); + header_map.inner.insert( + http::header::CONTENT_TYPE, + HeaderValue::from_static(CONTENT_TYPE_MULTIPART), + ); + MediaUploadRequest { + method: RequestMethod::POST, + url: self.endpoint.create().into(), + header_map: header_map.into(), + file_path, + file_content_type, + // TODO: There should be a better way to do this, but it should be enough for testing + media_params: serde_json::from_str(&serde_json::to_string(¶ms).unwrap()).unwrap(), + } + } +} + +#[uniffi::export] +impl MediaRequestExecutor { + pub async fn upload( + &self, + params: &crate::media::MediaCreateParams, + file_path: String, + file_content_type: String, + ) -> Result { + let request = self + .request_builder + .upload(params, file_path, file_content_type); + self.request_executor + .upload_media(Arc::new(request)) + .await? + .parse() + } +} + #[cfg(test)] mod tests { use super::*; @@ -81,6 +193,11 @@ mod tests { use rstest::*; use std::sync::Arc; + #[rstest] + fn create_media(endpoint: MediaRequestEndpoint) { + validate_wp_v2_endpoint(endpoint.create(), "/media"); + } + #[rstest] fn delete_media(endpoint: MediaRequestEndpoint) { validate_wp_v2_endpoint(endpoint.delete(&MediaId(54)), "/media/54?force=true"); diff --git a/wp_api_integration_tests/Cargo.toml b/wp_api_integration_tests/Cargo.toml index 5b5fc0aa7..d5b02c599 100644 --- a/wp_api_integration_tests/Cargo.toml +++ b/wp_api_integration_tests/Cargo.toml @@ -13,7 +13,7 @@ async-trait = { workspace = true } clap = { workspace = true, features = ["derive"] } futures = { workspace = true } http = { workspace = true } -reqwest = { workspace = true, features = [ "json" ] } +reqwest = { workspace = true, features = [ "multipart", "json", "stream" ] } serde = { workspace = true, features = [ "derive" ] } serde_json = { workspace = true } tokio = { workspace = true, features = [ "full" ] } diff --git a/wp_api_integration_tests/src/lib.rs b/wp_api_integration_tests/src/lib.rs index 6d272d201..32e6945d8 100644 --- a/wp_api_integration_tests/src/lib.rs +++ b/wp_api_integration_tests/src/lib.rs @@ -1,10 +1,13 @@ use async_trait::async_trait; +use http::{HeaderMap, HeaderValue}; +use reqwest::multipart::Part; use std::sync::Arc; use wp_api::{ media::MediaId, posts::{CategoryId, PostId, TagId}, request::{ - RequestExecutor, RequestMethod, WpNetworkHeaderMap, WpNetworkRequest, WpNetworkResponse, + endpoint::media_endpoint::MediaUploadRequest, RequestExecutor, RequestMethod, + WpNetworkHeaderMap, WpNetworkRequest, WpNetworkResponse, }, users::UserId, ParsedUrl, RequestExecutionError, WpApiClient, WpApiError, WpAuthentication, WpErrorCode, @@ -159,6 +162,45 @@ impl AsyncWpNetworking { }) } + pub async fn upload_media_request( + &self, + media_upload_request: Arc, + ) -> Result { + let request = self + .client + .request( + Self::request_method(media_upload_request.method()), + media_upload_request.url().0.as_str(), + ) + .headers(media_upload_request.header_map().as_header_map()); + let file_path = media_upload_request.file_path(); + let mut file_header_map = HeaderMap::new(); + file_header_map.insert( + http::header::CONTENT_TYPE, + HeaderValue::from_str(&media_upload_request.file_content_type()).unwrap(), + ); + let mut form = reqwest::multipart::Form::new().part( + "file", + Part::file(file_path) + .await + .unwrap() + .headers(file_header_map), + ); + for (k, v) in media_upload_request.media_params() { + form = form.text(k, v) + } + + let request = request.multipart(form); + let mut response = request.send().await?; + + let header_map = std::mem::take(response.headers_mut()); + Ok(WpNetworkResponse { + status_code: response.status().as_u16(), + body: response.bytes().await.unwrap().to_vec(), + header_map: Arc::new(WpNetworkHeaderMap::new(header_map)), + }) + } + fn request_method(method: RequestMethod) -> http::Method { match method { RequestMethod::GET => reqwest::Method::GET, @@ -182,6 +224,18 @@ impl RequestExecutor for AsyncWpNetworking { } }) } + + async fn upload_media( + &self, + media_upload_request: Arc, + ) -> Result { + self.upload_media_request(media_upload_request) + .await + .map_err(|err| RequestExecutionError::RequestExecutionFailed { + status_code: err.status().map(|s| s.as_u16()), + reason: err.to_string(), + }) + } } pub trait AssertResponse { diff --git a/wp_api_integration_tests/tests/test_media_mut.rs b/wp_api_integration_tests/tests/test_media_mut.rs index 99b2df80b..db289dbc5 100644 --- a/wp_api_integration_tests/tests/test_media_mut.rs +++ b/wp_api_integration_tests/tests/test_media_mut.rs @@ -1,13 +1,33 @@ use macro_helper::generate_update_test; use serial_test::serial; use wp_api::{ - media::MediaUpdateParams, + media::{MediaCreateParams, MediaUpdateParams}, posts::{PostCommentStatus, PostPingStatus, PostStatus}, }; use wp_api_integration_tests::{ api_client, backend::RestoreServer, AssertResponse, FIRST_POST_ID, MEDIA_ID_611, }; +#[tokio::test] +#[serial] +async fn upload_media() { + let title = "Foo media"; + let created_media = api_client() + .media() + .upload( + &MediaCreateParams { + title: Some(title.to_string()), + ..Default::default() + }, + "../test_media.jpg".to_string(), + "image/jpeg".to_string(), + ) + .await + .assert_response(); + assert_eq!(created_media.data.title.rendered.as_str(), title); + RestoreServer::db().await; +} + #[tokio::test] #[serial] async fn delete_media() { @@ -61,13 +81,6 @@ generate_update_test!( PostPingStatus::Closed ); -// TODO: `POST_TEMPLATE_SINGLE_WITH_SIDEBAR` doesn't work for `/media`. -//generate_update_test!( -// update_template, -// template, -// POST_TEMPLATE_SINGLE_WITH_SIDEBAR.to_string() -//); - generate_update_test!(update_alt_text, alt_text, "new_alt_text".to_string()); generate_update_test!(update_caption, caption, "new_caption".to_string()); From d4cf233411bf95892fcc8c2e295e1ed0ca3070d9 Mon Sep 17 00:00:00 2001 From: Oguz Kocer Date: Wed, 27 Nov 2024 04:34:49 -0500 Subject: [PATCH 2/8] Manually implement create media response type and endpoint --- .../kotlin/MediaEndpointTest.kt | 4 +- wp_api/src/request/endpoint/media_endpoint.rs | 58 +++++++++++++++++-- .../tests/test_media_mut.rs | 2 +- 3 files changed, 55 insertions(+), 9 deletions(-) diff --git a/native/kotlin/api/kotlin/src/integrationTest/kotlin/MediaEndpointTest.kt b/native/kotlin/api/kotlin/src/integrationTest/kotlin/MediaEndpointTest.kt index 7d914a97a..6a6711e44 100644 --- a/native/kotlin/api/kotlin/src/integrationTest/kotlin/MediaEndpointTest.kt +++ b/native/kotlin/api/kotlin/src/integrationTest/kotlin/MediaEndpointTest.kt @@ -67,10 +67,10 @@ class MediaEndpointTest { } @Test - fun testUploadMediaRequest() = runTest { + fun testCreateMediaRequest() = runTest { val title = "Testing media upload from Kotlin" val response = client.request { requestBuilder -> - requestBuilder.media().upload( + requestBuilder.media().create( params = MediaCreateParams(title = title), "test_media.jpg", "image/jpg" diff --git a/wp_api/src/request/endpoint/media_endpoint.rs b/wp_api/src/request/endpoint/media_endpoint.rs index a15db64cc..4804e1fc4 100644 --- a/wp_api/src/request/endpoint/media_endpoint.rs +++ b/wp_api/src/request/endpoint/media_endpoint.rs @@ -7,7 +7,10 @@ use crate::{ SparseMediaFieldWithEditContext, SparseMediaFieldWithEmbedContext, SparseMediaFieldWithViewContext, }, - request::{RequestMethod, WpNetworkHeaderMap, CONTENT_TYPE_MULTIPART}, + request::{ + ParsedResponse, RequestMethod, WpNetworkHeaderMap, WpNetworkResponse, + CONTENT_TYPE_MULTIPART, + }, SparseField, }; use http::HeaderValue; @@ -19,8 +22,6 @@ enum MediaRequest { List, #[contextual_get(url = "/media/", output = crate::media::SparseMedia, filter_by = crate::media::SparseMediaField)] Retrieve, - #[post(url = "/media", params = &crate::media::MediaCreateParams, output = crate::media::MediaWithEditContext)] - Create, #[delete(url = "/media/", output = crate::media::MediaDeleteResponse)] Delete, #[post(url = "/media/", params = &MediaUpdateParams, output = MediaWithEditContext)] @@ -71,6 +72,51 @@ impl SparseField for SparseMediaFieldWithViewContext { } } +impl MediaRequestEndpoint { + pub fn create(&self) -> crate::request::endpoint::ApiEndpointUrl { + self.api_base_url + .by_extending_and_splitting_by_forward_slash([ + MediaRequest::namespace().as_str(), + "media", + ]) + .into() + } +} + +#[derive(Debug, serde::Serialize, serde::Deserialize, uniffi::Record)] +#[serde(transparent)] +pub struct MediaRequestCreateResponse { + pub data: crate::media::MediaWithEditContext, + #[serde(skip)] + pub header_map: std::sync::Arc, +} + +impl From for ParsedResponse { + fn from(value: MediaRequestCreateResponse) -> Self { + Self { + data: value.data, + header_map: value.header_map, + next_page_params: None, + prev_page_params: None, + } + } +} +impl From> for MediaRequestCreateResponse { + fn from(value: ParsedResponse) -> Self { + Self { + data: value.data, + header_map: value.header_map, + } + } +} + +#[uniffi::export] +fn parse_as_media_request_create_response( + response: WpNetworkResponse, +) -> Result { + response.parse() +} + #[derive(uniffi::Object)] pub struct MediaUploadRequest { pub(crate) method: RequestMethod, @@ -135,7 +181,7 @@ impl std::fmt::Debug for MediaUploadRequest { #[uniffi::export] impl MediaRequestBuilder { - pub fn upload( + pub fn create( &self, params: &crate::media::MediaCreateParams, file_path: String, @@ -161,7 +207,7 @@ impl MediaRequestBuilder { #[uniffi::export] impl MediaRequestExecutor { - pub async fn upload( + pub async fn create( &self, params: &crate::media::MediaCreateParams, file_path: String, @@ -169,7 +215,7 @@ impl MediaRequestExecutor { ) -> Result { let request = self .request_builder - .upload(params, file_path, file_content_type); + .create(params, file_path, file_content_type); self.request_executor .upload_media(Arc::new(request)) .await? diff --git a/wp_api_integration_tests/tests/test_media_mut.rs b/wp_api_integration_tests/tests/test_media_mut.rs index db289dbc5..81e1637ec 100644 --- a/wp_api_integration_tests/tests/test_media_mut.rs +++ b/wp_api_integration_tests/tests/test_media_mut.rs @@ -14,7 +14,7 @@ async fn upload_media() { let title = "Foo media"; let created_media = api_client() .media() - .upload( + .create( &MediaCreateParams { title: Some(title.to_string()), ..Default::default() From 438761285be910c521232a22c4a02cf7da5187ad Mon Sep 17 00:00:00 2001 From: Oguz Kocer Date: Wed, 27 Nov 2024 04:45:22 -0500 Subject: [PATCH 3/8] Restore test server from Kotlin --- .../src/integrationTest/kotlin/IntegrationTestHelpers.kt | 8 ++++++++ .../src/integrationTest/kotlin/MediaEndpointTest.kt | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/native/kotlin/api/kotlin/src/integrationTest/kotlin/IntegrationTestHelpers.kt b/native/kotlin/api/kotlin/src/integrationTest/kotlin/IntegrationTestHelpers.kt index 66463a71d..b0c875a50 100644 --- a/native/kotlin/api/kotlin/src/integrationTest/kotlin/IntegrationTestHelpers.kt +++ b/native/kotlin/api/kotlin/src/integrationTest/kotlin/IntegrationTestHelpers.kt @@ -1,5 +1,7 @@ package rs.wordpress.api.kotlin +import okhttp3.OkHttpClient +import okhttp3.Request import uniffi.wp_api.UserId import uniffi.wp_api.WpErrorCode @@ -26,3 +28,9 @@ fun WpRequestResult.wpErrorCode(): WpErrorCode { assert(this is WpRequestResult.WpError) return (this as WpRequestResult.WpError).errorCode } + +fun restoreTestServer() { + OkHttpClient().newCall( + Request.Builder().url("http://localhost:4000/restore?db=true&plugins=true").build() + ).execute() +} \ No newline at end of file diff --git a/native/kotlin/api/kotlin/src/integrationTest/kotlin/MediaEndpointTest.kt b/native/kotlin/api/kotlin/src/integrationTest/kotlin/MediaEndpointTest.kt index 6a6711e44..de343cf91 100644 --- a/native/kotlin/api/kotlin/src/integrationTest/kotlin/MediaEndpointTest.kt +++ b/native/kotlin/api/kotlin/src/integrationTest/kotlin/MediaEndpointTest.kt @@ -77,6 +77,6 @@ class MediaEndpointTest { ) }.assertSuccessAndRetrieveData().data assertEquals(title, response.title.rendered) - // TODO: Restore server + restoreTestServer() } } From ec27338b17930f8f2418d987d21d47e075644b5d Mon Sep 17 00:00:00 2001 From: Oguz Kocer Date: Wed, 27 Nov 2024 04:57:58 -0500 Subject: [PATCH 4/8] impl From for HashMap --- wp_api/src/media.rs | 33 ++++++++++++++++++- wp_api/src/request/endpoint/media_endpoint.rs | 9 +++-- .../tests/test_media_mut.rs | 2 +- 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/wp_api/src/media.rs b/wp_api/src/media.rs index 24863465d..964acfa49 100644 --- a/wp_api/src/media.rs +++ b/wp_api/src/media.rs @@ -11,7 +11,7 @@ use crate::{ EnumFromStrParsingError, JsonValue, UserId, WpApiParamOrder, }; use serde::{Deserialize, Serialize}; -use std::{num::ParseIntError, str::FromStr}; +use std::{collections::HashMap, num::ParseIntError, str::FromStr}; use strum_macros::IntoStaticStr; use wp_contextual::WpContextual; @@ -488,6 +488,37 @@ pub struct MediaCreateParams { // meta field is omitted for now: https://github.com/Automattic/wordpress-rs/issues/381 } +impl From for HashMap { + fn from(params: MediaCreateParams) -> Self { + let mut map = HashMap::new(); + let mut add = |k: &str, v: Option| { + if let Some(v) = v { + map.insert(k.to_string(), v); + } + }; + add("date", params.date); + add("date_gmt", params.date_gmt); + add("slug", params.slug); + add("status", params.status.map(|x| x.as_str().to_string())); + add("title", params.title); + add("author", params.author.map(|x| x.to_string())); + add( + "comment_status", + params.comment_status.map(|x| x.as_str().to_string()), + ); + add( + "ping_status", + params.ping_status.map(|x| x.as_str().to_string()), + ); + add("template", params.template); + add("alt_text", params.alt_text); + add("caption", params.caption); + add("description", params.description); + add("post", params.post_id.map(|x| x.to_string())); + map + } +} + #[derive(Debug, Serialize, Deserialize, uniffi::Record, WpContextual)] pub struct SparseMedia { #[WpContext(edit, embed, view)] diff --git a/wp_api/src/request/endpoint/media_endpoint.rs b/wp_api/src/request/endpoint/media_endpoint.rs index 4804e1fc4..c61fdb194 100644 --- a/wp_api/src/request/endpoint/media_endpoint.rs +++ b/wp_api/src/request/endpoint/media_endpoint.rs @@ -3,7 +3,7 @@ use std::{collections::HashMap, sync::Arc}; use super::{AsNamespace, DerivedRequest, WpEndpointUrl, WpNamespace}; use crate::{ media::{ - MediaId, MediaListParams, MediaUpdateParams, MediaWithEditContext, + MediaCreateParams, MediaId, MediaListParams, MediaUpdateParams, MediaWithEditContext, SparseMediaFieldWithEditContext, SparseMediaFieldWithEmbedContext, SparseMediaFieldWithViewContext, }, @@ -183,7 +183,7 @@ impl std::fmt::Debug for MediaUploadRequest { impl MediaRequestBuilder { pub fn create( &self, - params: &crate::media::MediaCreateParams, + params: MediaCreateParams, file_path: String, file_content_type: String, ) -> MediaUploadRequest { @@ -199,8 +199,7 @@ impl MediaRequestBuilder { header_map: header_map.into(), file_path, file_content_type, - // TODO: There should be a better way to do this, but it should be enough for testing - media_params: serde_json::from_str(&serde_json::to_string(¶ms).unwrap()).unwrap(), + media_params: params.into(), } } } @@ -209,7 +208,7 @@ impl MediaRequestBuilder { impl MediaRequestExecutor { pub async fn create( &self, - params: &crate::media::MediaCreateParams, + params: MediaCreateParams, file_path: String, file_content_type: String, ) -> Result { diff --git a/wp_api_integration_tests/tests/test_media_mut.rs b/wp_api_integration_tests/tests/test_media_mut.rs index 81e1637ec..48e24ab24 100644 --- a/wp_api_integration_tests/tests/test_media_mut.rs +++ b/wp_api_integration_tests/tests/test_media_mut.rs @@ -15,7 +15,7 @@ async fn upload_media() { let created_media = api_client() .media() .create( - &MediaCreateParams { + MediaCreateParams { title: Some(title.to_string()), ..Default::default() }, From e775ba9fa588cc962188017c4fde0acf6c6e1f08 Mon Sep 17 00:00:00 2001 From: Oguz Kocer Date: Wed, 27 Nov 2024 05:15:59 -0500 Subject: [PATCH 5/8] Implement MediaUploadRequestExecutionError --- .../rs/wordpress/api/kotlin/WpApiClient.kt | 1 + .../wordpress/api/kotlin/WpRequestExecutor.kt | 7 +++- .../wordpress/api/kotlin/WpRequestResult.kt | 2 ++ wp_api/src/api_error.rs | 32 +++++++++++++++++++ wp_api/src/lib.rs | 5 ++- wp_api/src/request.rs | 6 ++-- wp_api_integration_tests/src/lib.rs | 15 +++++---- 7 files changed, 58 insertions(+), 10 deletions(-) diff --git a/native/kotlin/api/kotlin/src/main/kotlin/rs/wordpress/api/kotlin/WpApiClient.kt b/native/kotlin/api/kotlin/src/main/kotlin/rs/wordpress/api/kotlin/WpApiClient.kt index 9987d2c43..13fb970f9 100644 --- a/native/kotlin/api/kotlin/src/main/kotlin/rs/wordpress/api/kotlin/WpApiClient.kt +++ b/native/kotlin/api/kotlin/src/main/kotlin/rs/wordpress/api/kotlin/WpApiClient.kt @@ -42,6 +42,7 @@ constructor( statusCode = exception.statusCode, reason = exception.reason ) + is WpApiException.MediaFileNotFound -> WpRequestResult.MediaFileNotFound() is WpApiException.ResponseParsingException -> WpRequestResult.ResponseParsingError( reason = exception.reason, response = exception.response, 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 ff2c9c6c0..b1d43f09c 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 @@ -11,6 +11,7 @@ import okhttp3.Request import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.toRequestBody import uniffi.wp_api.MediaUploadRequest +import uniffi.wp_api.MediaUploadRequestExecutionException import uniffi.wp_api.RequestExecutor import uniffi.wp_api.WpNetworkHeaderMap import uniffi.wp_api.WpNetworkRequest @@ -54,7 +55,11 @@ class WpRequestExecutor( } // TODO: This probably doesn't work with Android - and it looks wrong val file = - File(WpRequestExecutor::class.java.classLoader!!.getResource(mediaUploadRequest.filePath())!!.file) + WpRequestExecutor::class.java.classLoader?.getResource(mediaUploadRequest.filePath())?.file?.let { + File( + it + ) + } ?: throw MediaUploadRequestExecutionException.MediaFileNotFound() multipartBodyBuilder.addFormDataPart( name = "file", filename = file.name, diff --git a/native/kotlin/api/kotlin/src/main/kotlin/rs/wordpress/api/kotlin/WpRequestResult.kt b/native/kotlin/api/kotlin/src/main/kotlin/rs/wordpress/api/kotlin/WpRequestResult.kt index f487294f7..083f64947 100644 --- a/native/kotlin/api/kotlin/src/main/kotlin/rs/wordpress/api/kotlin/WpRequestResult.kt +++ b/native/kotlin/api/kotlin/src/main/kotlin/rs/wordpress/api/kotlin/WpRequestResult.kt @@ -20,6 +20,8 @@ sealed class WpRequestResult { val reason: String, ) : WpRequestResult() + class MediaFileNotFound : WpRequestResult() + class SiteUrlParsingError( val reason: String, ) : WpRequestResult() diff --git a/wp_api/src/api_error.rs b/wp_api/src/api_error.rs index fc3443501..9ec233ef3 100644 --- a/wp_api/src/api_error.rs +++ b/wp_api/src/api_error.rs @@ -23,6 +23,8 @@ pub enum WpApiError { status_code: Option, reason: String, }, + #[error("Media file not found")] + MediaFileNotFound, #[error("Error while parsing. \nReason: {}\nResponse: {}", reason, response)] ResponseParsingError { reason: String, response: String }, #[error("Error while parsing site url: {}", reason)] @@ -320,6 +322,21 @@ pub enum RequestExecutionError { }, } +#[derive(Debug, PartialEq, Eq, thiserror::Error, uniffi::Error)] +pub enum MediaUploadRequestExecutionError { + #[error( + "Request execution failed!\nStatus Code: '{:?}'.\nResponse: '{}'", + status_code, + reason + )] + RequestExecutionFailed { + status_code: Option, + reason: String, + }, + #[error("Media file not found")] + MediaFileNotFound, +} + impl From for WpApiError { fn from(value: RequestExecutionError) -> Self { match value { @@ -333,3 +350,18 @@ impl From for WpApiError { } } } + +impl From for WpApiError { + fn from(value: MediaUploadRequestExecutionError) -> Self { + match value { + MediaUploadRequestExecutionError::RequestExecutionFailed { + status_code, + reason, + } => Self::RequestExecutionFailed { + status_code, + reason, + }, + MediaUploadRequestExecutionError::MediaFileNotFound => Self::MediaFileNotFound, + } + } +} diff --git a/wp_api/src/lib.rs b/wp_api/src/lib.rs index 0cfe1f974..c9ade3de7 100644 --- a/wp_api/src/lib.rs +++ b/wp_api/src/lib.rs @@ -1,7 +1,10 @@ #![allow(dead_code, unused_variables)] pub use api_client::{WpApiClient, WpApiRequestBuilder}; -pub use api_error::{ParsedRequestError, RequestExecutionError, WpApiError, WpError, WpErrorCode}; +pub use api_error::{ + MediaUploadRequestExecutionError, ParsedRequestError, RequestExecutionError, WpApiError, + WpError, WpErrorCode, +}; pub use parsed_url::{ParseUrlError, ParsedUrl}; use plugins::*; use serde::{Deserialize, Serialize}; diff --git a/wp_api/src/request.rs b/wp_api/src/request.rs index 94c33ecdd..49e696673 100644 --- a/wp_api/src/request.rs +++ b/wp_api/src/request.rs @@ -6,7 +6,9 @@ use serde::{de::DeserializeOwned, Deserialize, Serialize}; use url::Url; use crate::{ - api_error::{ParsedRequestError, RequestExecutionError, WpError}, + api_error::{ + MediaUploadRequestExecutionError, ParsedRequestError, RequestExecutionError, WpError, + }, url_query::{FromUrlQueryPairs, UrlQueryPairsMap}, WpApiError, WpAuthentication, }; @@ -128,7 +130,7 @@ pub trait RequestExecutor: Send + Sync + Debug { async fn upload_media( &self, media_upload_request: Arc, - ) -> Result; + ) -> Result; } #[derive(uniffi::Object)] diff --git a/wp_api_integration_tests/src/lib.rs b/wp_api_integration_tests/src/lib.rs index 32e6945d8..bd8e456fb 100644 --- a/wp_api_integration_tests/src/lib.rs +++ b/wp_api_integration_tests/src/lib.rs @@ -10,7 +10,8 @@ use wp_api::{ WpNetworkHeaderMap, WpNetworkRequest, WpNetworkResponse, }, users::UserId, - ParsedUrl, RequestExecutionError, WpApiClient, WpApiError, WpAuthentication, WpErrorCode, + MediaUploadRequestExecutionError, ParsedUrl, RequestExecutionError, WpApiClient, WpApiError, + WpAuthentication, WpErrorCode, }; // A `TestCredentials::instance()` function will be generated by this @@ -228,13 +229,15 @@ impl RequestExecutor for AsyncWpNetworking { async fn upload_media( &self, media_upload_request: Arc, - ) -> Result { + ) -> Result { self.upload_media_request(media_upload_request) .await - .map_err(|err| RequestExecutionError::RequestExecutionFailed { - status_code: err.status().map(|s| s.as_u16()), - reason: err.to_string(), - }) + .map_err( + |err| MediaUploadRequestExecutionError::RequestExecutionFailed { + status_code: err.status().map(|s| s.as_u16()), + reason: err.to_string(), + }, + ) } } From 93fa48fc5c7be61bc66503f433f34158f58752e0 Mon Sep 17 00:00:00 2001 From: Oguz Kocer Date: Mon, 2 Dec 2024 07:05:48 -0500 Subject: [PATCH 6/8] Removed addressed TODO from Kotlin WpRequestExecutor --- .../main/kotlin/rs/wordpress/api/kotlin/WpRequestExecutor.kt | 2 -- 1 file changed, 2 deletions(-) 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 b1d43f09c..58984689a 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 @@ -3,7 +3,6 @@ package rs.wordpress.api.kotlin import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import okhttp3.MediaType import okhttp3.MediaType.Companion.toMediaType import okhttp3.MultipartBody import okhttp3.OkHttpClient @@ -53,7 +52,6 @@ class WpRequestExecutor( mediaUploadRequest.mediaParams().forEach { (k, v) -> multipartBodyBuilder.addFormDataPart(k, v) } - // TODO: This probably doesn't work with Android - and it looks wrong val file = WpRequestExecutor::class.java.classLoader?.getResource(mediaUploadRequest.filePath())?.file?.let { File( From 593db7e549a2d207d91b3d2b8d9ed96a4e20fec8 Mon Sep 17 00:00:00 2001 From: Oguz Kocer Date: Mon, 2 Dec 2024 07:31:49 -0500 Subject: [PATCH 7/8] Add file_path to MediaFileNotFound error --- .../kotlin/rs/wordpress/api/kotlin/WpApiClient.kt | 4 +++- .../rs/wordpress/api/kotlin/WpRequestExecutor.kt | 5 +++-- .../rs/wordpress/api/kotlin/WpRequestResult.kt | 4 +++- wp_api/src/api_error.rs | 12 +++++++----- 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/native/kotlin/api/kotlin/src/main/kotlin/rs/wordpress/api/kotlin/WpApiClient.kt b/native/kotlin/api/kotlin/src/main/kotlin/rs/wordpress/api/kotlin/WpApiClient.kt index 13fb970f9..c9c9fb6d6 100644 --- a/native/kotlin/api/kotlin/src/main/kotlin/rs/wordpress/api/kotlin/WpApiClient.kt +++ b/native/kotlin/api/kotlin/src/main/kotlin/rs/wordpress/api/kotlin/WpApiClient.kt @@ -42,7 +42,9 @@ constructor( statusCode = exception.statusCode, reason = exception.reason ) - is WpApiException.MediaFileNotFound -> WpRequestResult.MediaFileNotFound() + is WpApiException.MediaFileNotFound -> WpRequestResult.MediaFileNotFound( + filePath = exception.filePath + ) is WpApiException.ResponseParsingException -> WpRequestResult.ResponseParsingError( reason = exception.reason, response = exception.response, 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 58984689a..b6905717f 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 @@ -52,12 +52,13 @@ class WpRequestExecutor( mediaUploadRequest.mediaParams().forEach { (k, v) -> multipartBodyBuilder.addFormDataPart(k, v) } + val filePath = mediaUploadRequest.filePath() val file = - WpRequestExecutor::class.java.classLoader?.getResource(mediaUploadRequest.filePath())?.file?.let { + WpRequestExecutor::class.java.classLoader?.getResource(filePath)?.file?.let { File( it ) - } ?: throw MediaUploadRequestExecutionException.MediaFileNotFound() + } ?: throw MediaUploadRequestExecutionException.MediaFileNotFound(filePath) multipartBodyBuilder.addFormDataPart( name = "file", filename = file.name, diff --git a/native/kotlin/api/kotlin/src/main/kotlin/rs/wordpress/api/kotlin/WpRequestResult.kt b/native/kotlin/api/kotlin/src/main/kotlin/rs/wordpress/api/kotlin/WpRequestResult.kt index 083f64947..6c1962436 100644 --- a/native/kotlin/api/kotlin/src/main/kotlin/rs/wordpress/api/kotlin/WpRequestResult.kt +++ b/native/kotlin/api/kotlin/src/main/kotlin/rs/wordpress/api/kotlin/WpRequestResult.kt @@ -20,7 +20,9 @@ sealed class WpRequestResult { val reason: String, ) : WpRequestResult() - class MediaFileNotFound : WpRequestResult() + class MediaFileNotFound( + val filePath: String + ) : WpRequestResult() class SiteUrlParsingError( val reason: String, diff --git a/wp_api/src/api_error.rs b/wp_api/src/api_error.rs index 9ec233ef3..16d552e2b 100644 --- a/wp_api/src/api_error.rs +++ b/wp_api/src/api_error.rs @@ -23,8 +23,8 @@ pub enum WpApiError { status_code: Option, reason: String, }, - #[error("Media file not found")] - MediaFileNotFound, + #[error("Media file not found at file path: {}", file_path)] + MediaFileNotFound { file_path: String }, #[error("Error while parsing. \nReason: {}\nResponse: {}", reason, response)] ResponseParsingError { reason: String, response: String }, #[error("Error while parsing site url: {}", reason)] @@ -333,8 +333,8 @@ pub enum MediaUploadRequestExecutionError { status_code: Option, reason: String, }, - #[error("Media file not found")] - MediaFileNotFound, + #[error("Media file not found at file path: {}", file_path)] + MediaFileNotFound { file_path: String }, } impl From for WpApiError { @@ -361,7 +361,9 @@ impl From for WpApiError { status_code, reason, }, - MediaUploadRequestExecutionError::MediaFileNotFound => Self::MediaFileNotFound, + MediaUploadRequestExecutionError::MediaFileNotFound { file_path } => { + Self::MediaFileNotFound { file_path } + } } } } From 1b45f6af1f4af781df4ec2ee2b2ce3d4d52b0388 Mon Sep 17 00:00:00 2001 From: Jeremy Massel <1123407+jkmassel@users.noreply.github.com> Date: Mon, 2 Dec 2024 15:17:13 -0700 Subject: [PATCH 8/8] Patch up failing Swift stuff --- native/swift/Sources/wordpress-api/Exports.swift | 1 + .../swift/Sources/wordpress-api/SafeRequestExecutor.swift | 6 +++++- native/swift/Tests/wordpress-api/Support/HTTPStubs.swift | 4 ++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/native/swift/Sources/wordpress-api/Exports.swift b/native/swift/Sources/wordpress-api/Exports.swift index ef2ede586..39806042b 100644 --- a/native/swift/Sources/wordpress-api/Exports.swift +++ b/native/swift/Sources/wordpress-api/Exports.swift @@ -90,6 +90,7 @@ public typealias PostsRequestListWithEmbedContextResponse = WordPressAPIInternal // MARK: - Media public typealias SparseMedia = WordPressAPIInternal.SparseMedia +public typealias MediaUploadRequest = WordPressAPIInternal.MediaUploadRequest public typealias MediaWithEditContext = WordPressAPIInternal.MediaWithEditContext public typealias MediaWithViewContext = WordPressAPIInternal.MediaWithViewContext public typealias MediaWithEmbedContext = WordPressAPIInternal.MediaWithEmbedContext diff --git a/native/swift/Sources/wordpress-api/SafeRequestExecutor.swift b/native/swift/Sources/wordpress-api/SafeRequestExecutor.swift index ab0e38895..1b4011017 100644 --- a/native/swift/Sources/wordpress-api/SafeRequestExecutor.swift +++ b/native/swift/Sources/wordpress-api/SafeRequestExecutor.swift @@ -20,7 +20,11 @@ extension SafeRequestExecutor { } -extension URLSession: RequestExecutor {} +extension URLSession: RequestExecutor { + public func uploadMedia(mediaUploadRequest: MediaUploadRequest) async throws -> WpNetworkResponse { + try WpNetworkResponse(body: Data(), statusCode: 500, headerMap: .fromMap(hashMap: [:])) + } +} extension URLSession: SafeRequestExecutor { diff --git a/native/swift/Tests/wordpress-api/Support/HTTPStubs.swift b/native/swift/Tests/wordpress-api/Support/HTTPStubs.swift index eaa10fab7..b84d2bbba 100644 --- a/native/swift/Tests/wordpress-api/Support/HTTPStubs.swift +++ b/native/swift/Tests/wordpress-api/Support/HTTPStubs.swift @@ -30,6 +30,10 @@ final class HTTPStubs: SafeRequestExecutor { } } + func uploadMedia(mediaUploadRequest: MediaUploadRequest) async throws -> WpNetworkResponse { + try WpNetworkResponse(body: Data(), statusCode: 500, headerMap: .fromMap(hashMap: [:])) + } + private func stub(for request: WpNetworkRequest) -> WpNetworkResponse? { stubs.first { stub in stub.condition(request) }? .response