Skip to content

Commit fb107b4

Browse files
Fix multipart vec types (#995)
* Build support for modelling vec types * Adopt vec types in Rust * Adopt Vec types in Swift * Adopt Vec types in Kotlin * Fix Rust tests * Put multipart form data into `Vec<WpMultipartFormField>` (#997) --------- Co-authored-by: Tony Li <[email protected]>
1 parent d1f81fe commit fb107b4

File tree

5 files changed

+215
-82
lines changed

5 files changed

+215
-82
lines changed

native/kotlin/api/kotlin/src/main/kotlin/rs/wordpress/api/kotlin/WpRequestExecutor.kt

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import uniffi.wp_api.RequestExecutionErrorReason
1818
import uniffi.wp_api.RequestExecutionException
1919
import uniffi.wp_api.RequestExecutor
2020
import uniffi.wp_api.RequestMethod
21+
import uniffi.wp_api.WpMultipartFormField
2122
import uniffi.wp_api.WpMultipartFormRequest
2223
import uniffi.wp_api.WpNetworkHeaderMap
2324
import uniffi.wp_api.WpNetworkRequest
@@ -73,23 +74,27 @@ class WpRequestExecutor(
7374
private fun buildMultipartBody(request: WpMultipartFormRequest): MultipartBody {
7475
val multipartBodyBuilder = MultipartBody.Builder().setType(MultipartBody.FORM)
7576

76-
request.fields().forEach { (k, v) ->
77-
multipartBodyBuilder.addFormDataPart(k, v)
78-
}
79-
80-
request.files().forEach { (name, fileInfo) ->
81-
val file = fileResolver.getFile(fileInfo.filePath)
82-
if (file == null || !file.canBeUploaded()) {
83-
throw RequestExecutionException.MediaFileNotFound(filePath = fileInfo.filePath)
77+
request.form().forEach { field ->
78+
when (field) {
79+
is WpMultipartFormField.Text -> {
80+
multipartBodyBuilder.addFormDataPart(field.name, value = field.value)
81+
}
82+
is WpMultipartFormField.File -> {
83+
val fileInfo = field.file
84+
val file = fileResolver.getFile(fileInfo.filePath)
85+
if (file == null || !file.canBeUploaded()) {
86+
throw RequestExecutionException.MediaFileNotFound(filePath = fileInfo.filePath)
87+
}
88+
val mimeType = fileInfo.mimeType ?: "application/octet-stream"
89+
val filename = fileInfo.fileName ?: file.name
90+
val requestBody = file.asRequestBody(mimeType.toMediaType())
91+
multipartBodyBuilder.addFormDataPart(
92+
name = field.name,
93+
filename = filename,
94+
body = requestBody
95+
)
96+
}
8497
}
85-
val mimeType = fileInfo.mimeType ?: "application/octet-stream"
86-
val filename = fileInfo.fileName ?: file.name
87-
val requestBody = file.asRequestBody(mimeType.toMediaType())
88-
multipartBodyBuilder.addFormDataPart(
89-
name = name,
90-
filename = filename,
91-
body = requestBody
92-
)
9398
}
9499

95100
return multipartBodyBuilder.build()

native/swift/Sources/wordpress-api/SafeRequestExecutor.swift

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -388,31 +388,33 @@ extension WpMultipartFormRequest: NetworkRequestContent {
388388
var request = try buildURLRequest(additionalHeaders: headers)
389389

390390
var form = [MultipartFormField]()
391-
for (name, value) in fields() {
392-
form.append(.init(text: value, name: name))
393-
}
394-
for (name, file) in files() {
395-
var mimeType = file.mimeType
396-
397-
#if canImport(UniformTypeIdentifiers)
398-
if mimeType == nil {
399-
mimeType = UTType(
400-
filenameExtension: URL(fileURLWithPath: file.filePath).pathExtension
401-
)?.preferredMIMEType
402-
}
403-
#endif
404-
405-
do {
406-
try form.append(
407-
.init(
408-
fileAtPath: file.filePath,
409-
name: name,
410-
filename: file.fileName,
411-
mimeType: mimeType
391+
for field in self.form() {
392+
switch field {
393+
case .text(let name, let value):
394+
form.append(MultipartFormField(text: value, name: name))
395+
case .file(let name, let file):
396+
var mimeType = file.mimeType
397+
398+
#if canImport(UniformTypeIdentifiers)
399+
if mimeType == nil {
400+
mimeType = UTType(
401+
filenameExtension: URL(fileURLWithPath: file.filePath).pathExtension
402+
)?.preferredMIMEType
403+
}
404+
#endif
405+
406+
do {
407+
try form.append(
408+
.init(
409+
fileAtPath: file.filePath,
410+
name: name,
411+
filename: file.fileName,
412+
mimeType: mimeType
413+
)
412414
)
413-
)
414-
} catch {
415-
throw RequestExecutionError.MediaFileNotFound(filePath: file.filePath)
415+
} catch {
416+
throw RequestExecutionError.MediaFileNotFound(filePath: file.filePath)
417+
}
416418
}
417419
}
418420

wp_api/src/request.rs

Lines changed: 137 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -118,17 +118,18 @@ impl InnerRequestBuilder {
118118
where
119119
T: ?Sized + Serialize + RequiresMultipartForm,
120120
{
121-
let mut fields = HashMap::new();
121+
let mut form = Vec::new();
122+
122123
if let Ok(serde_json::Value::Object(object)) = serde_json::to_value(params) {
123124
for (key, value) in object {
124-
if let serde_json::Value::String(s) = value {
125-
fields.insert(key, s);
126-
} else {
127-
fields.insert(key, value.to_string());
128-
}
125+
form.extend(WpMultipartFormField::from_json(key, value));
129126
}
130127
}
131128

129+
for (name, file) in params.multipart_form_files() {
130+
form.push(WpMultipartFormField::File { name, file });
131+
}
132+
132133
let mut header_map = self.header_map_for_post_request();
133134
header_map.inner.insert(
134135
http::header::CONTENT_TYPE,
@@ -140,8 +141,7 @@ impl InnerRequestBuilder {
140141
method: RequestMethod::POST,
141142
url: url.into(),
142143
header_map: header_map.into(),
143-
fields,
144-
files: params.multipart_form_files(),
144+
form,
145145
}
146146
}
147147

@@ -333,6 +333,50 @@ impl NetworkRequestAccessor for WpNetworkRequest {
333333
}
334334
}
335335

336+
#[derive(Debug, Clone, PartialEq, Eq, uniffi::Enum)]
337+
pub enum WpMultipartFormField {
338+
Text {
339+
name: String,
340+
value: String,
341+
},
342+
File {
343+
name: String,
344+
file: MultipartFormFile,
345+
},
346+
}
347+
348+
impl WpMultipartFormField {
349+
pub fn from_json(key: String, value: serde_json::Value) -> Vec<Self> {
350+
match value {
351+
serde_json::Value::String(s) => vec![Self::Text {
352+
name: key,
353+
value: s,
354+
}],
355+
serde_json::Value::Array(arr) => arr
356+
.into_iter()
357+
.enumerate()
358+
.flat_map(|(idx, v)| Self::from_json(format!("{key}[{idx}]"), v))
359+
.collect(),
360+
serde_json::Value::Object(obj) => obj
361+
.into_iter()
362+
.flat_map(|(nested_key, nested_value)| {
363+
Self::from_json(format!("{key}[{nested_key}]"), nested_value)
364+
})
365+
.collect(),
366+
_ => vec![Self::Text {
367+
name: key,
368+
value: value.to_string(),
369+
}],
370+
}
371+
}
372+
}
373+
374+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, uniffi::Enum)]
375+
pub enum WpMultipartFormFieldValue {
376+
String(String),
377+
Array(Vec<String>),
378+
}
379+
336380
#[derive(Debug, Clone, PartialEq, Eq, uniffi::Record)]
337381
pub struct MultipartFormFile {
338382
pub file_path: String,
@@ -350,18 +394,13 @@ pub struct WpMultipartFormRequest {
350394
pub(crate) method: RequestMethod,
351395
pub(crate) url: WpEndpointUrl,
352396
pub(crate) header_map: Arc<WpNetworkHeaderMap>,
353-
pub(crate) fields: HashMap<String, String>,
354-
pub(crate) files: HashMap<String, MultipartFormFile>,
397+
pub(crate) form: Vec<WpMultipartFormField>,
355398
}
356399

357400
#[uniffi::export]
358401
impl WpMultipartFormRequest {
359-
pub fn fields(&self) -> HashMap<String, String> {
360-
self.fields.clone()
361-
}
362-
363-
pub fn files(&self) -> HashMap<String, MultipartFormFile> {
364-
self.files.clone()
402+
pub fn form(&self) -> Vec<WpMultipartFormField> {
403+
self.form.clone()
365404
}
366405
}
367406

@@ -373,11 +412,10 @@ impl std::fmt::Debug for WpMultipartFormRequest {
373412
method: '{:?}',
374413
url: '{:?}',
375414
header_map: '{:?}',
376-
fields: '{:?}',
377-
files: '{:?}'
415+
form: '{:?}'
378416
}}
379417
"},
380-
self.method, self.url, self.header_map, self.fields, self.files
418+
self.method, self.url, self.header_map, self.form
381419
);
382420
s.pop(); // Remove the new line at the end
383421
write!(f, "{s}")
@@ -1034,6 +1072,86 @@ mod tests {
10341072
}
10351073
}
10361074

1075+
#[rstest]
1076+
#[case(serde_json::Value::String("test".to_string()), "test")]
1077+
#[case(serde_json::json!(42), "42")]
1078+
#[case(serde_json::json!(true), "true")]
1079+
#[case(serde_json::Value::Null, "null")]
1080+
fn test_multipart_form_field_from_json_primitives(
1081+
#[case] value: serde_json::Value,
1082+
#[case] expected: &str,
1083+
) {
1084+
let result = WpMultipartFormField::from_json("key".to_string(), value);
1085+
assert_eq!(result.len(), 1);
1086+
assert_eq!(
1087+
result[0],
1088+
WpMultipartFormField::Text {
1089+
name: "key".to_string(),
1090+
value: expected.to_string()
1091+
}
1092+
);
1093+
}
1094+
1095+
#[rstest]
1096+
#[case(
1097+
r#"{"key": ["a", "b", "c"]}"#,
1098+
vec![
1099+
("key[0]", "a"),
1100+
("key[1]", "b"),
1101+
("key[2]", "c"),
1102+
]
1103+
)]
1104+
#[case(
1105+
r#"{"key": [["a", "b"], ["c", "d"]]}"#,
1106+
vec![
1107+
("key[0][0]", "a"),
1108+
("key[0][1]", "b"),
1109+
("key[1][0]", "c"),
1110+
("key[1][1]", "d"),
1111+
]
1112+
)]
1113+
#[case(
1114+
r#"{"key": {"foo": "bar", "baz": "qux"}}"#,
1115+
vec![
1116+
("key[baz]", "qux"),
1117+
("key[foo]", "bar"),
1118+
]
1119+
)]
1120+
#[case(
1121+
r#"{"key": {"outer": {"inner": "value"}}}"#,
1122+
vec![("key[outer][inner]", "value")]
1123+
)]
1124+
#[case(
1125+
r#"{"key": {"tags": ["tag1", "tag2"], "metadata": {"author": "john"}}}"#,
1126+
vec![
1127+
("key[metadata][author]", "john"),
1128+
("key[tags][0]", "tag1"),
1129+
("key[tags][1]", "tag2"),
1130+
]
1131+
)]
1132+
fn test_multipart_form_field_from_json_complex(
1133+
#[case] json: serde_json::Value,
1134+
#[case] expected: Vec<(&str, &str)>,
1135+
) {
1136+
let serde_json::Value::Object(obj) = json else {
1137+
panic!("Expected JSON object");
1138+
};
1139+
assert_eq!(obj.len(), 1, "Expected exactly one key-value pair");
1140+
let (key, value) = obj.into_iter().next().unwrap();
1141+
let result = WpMultipartFormField::from_json(key, value);
1142+
assert_eq!(result.len(), expected.len());
1143+
1144+
for (i, (expected_name, expected_value)) in expected.iter().enumerate() {
1145+
assert_eq!(
1146+
result[i],
1147+
WpMultipartFormField::Text {
1148+
name: expected_name.to_string(),
1149+
value: expected_value.to_string()
1150+
}
1151+
);
1152+
}
1153+
}
1154+
10371155
#[rstest]
10381156
#[case(r#"<foo>"#, ResponseBodyType::MaybeHtml)]
10391157
#[case(r#"</foo>"#, ResponseBodyType::MaybeHtml)]

wp_api/src/reqwest_request_executor.rs

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
use crate::{
22
api_error::{InvalidSslErrorReason, RequestExecutionError, RequestExecutionErrorReason},
3-
request::RequestContext,
43
request::{
5-
NetworkRequestAccessor, RequestExecutor, RequestMethod, WpMultipartFormRequest,
6-
WpNetworkHeaderMap, WpNetworkRequest, WpNetworkResponse, user_agent,
4+
NetworkRequestAccessor, RequestContext, RequestExecutor, RequestMethod,
5+
WpMultipartFormField, WpMultipartFormRequest, WpNetworkHeaderMap, WpNetworkRequest,
6+
WpNetworkResponse, user_agent,
77
},
88
};
99
use async_trait::async_trait;
@@ -116,24 +116,27 @@ impl RequestExecutor for ReqwestRequestExecutor {
116116
.headers(upload_request.header_map().to_header_map());
117117
let mut form = reqwest::multipart::Form::new();
118118

119-
for (name, file) in upload_request.files() {
120-
let file_path = file.file_path;
121-
let mut file_header_map = HeaderMap::new();
122-
if let Some(mime_type) = &file.mime_type {
123-
file_header_map.insert(
124-
http::header::CONTENT_TYPE,
125-
HeaderValue::from_str(mime_type).unwrap(),
126-
);
119+
for field in upload_request.form() {
120+
match field {
121+
WpMultipartFormField::Text { name, value } => {
122+
form = form.text(name, value);
123+
}
124+
WpMultipartFormField::File { name, file } => {
125+
let file_path = file.file_path;
126+
let mut file_header_map = HeaderMap::new();
127+
if let Some(mime_type) = &file.mime_type {
128+
file_header_map.insert(
129+
http::header::CONTENT_TYPE,
130+
HeaderValue::from_str(mime_type).unwrap(),
131+
);
132+
}
133+
let part = Part::file(file_path)
134+
.await
135+
.unwrap()
136+
.headers(file_header_map);
137+
form = form.part(name, part);
138+
}
127139
}
128-
let part = Part::file(file_path)
129-
.await
130-
.unwrap()
131-
.headers(file_header_map);
132-
form = form.part(name, part);
133-
}
134-
135-
for (k, v) in upload_request.fields() {
136-
form = form.text(k, v)
137140
}
138141

139142
let request = request.multipart(form);

0 commit comments

Comments
 (0)