Skip to content

Commit 4ba87ae

Browse files
authored
[rust] Added support for text/plain to reqwest clients (#20643)
* [rust] Added support for text/plain to reqwest-trait client * Updated samples * [rust] Added support for text/plain to reqwest client * Updated samples * cleanup * reduced compiler warnings * fixed text/plain content with charset * Only deserialize text/plain if the API produces it
1 parent 515c882 commit 4ba87ae

File tree

83 files changed

+1187
-174
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

83 files changed

+1187
-174
lines changed

modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenOperation.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,24 @@ public boolean isRestful() {
315315
return isRestfulIndex() || isRestfulShow() || isRestfulCreate() || isRestfulUpdate() || isRestfulDestroy();
316316
}
317317

318+
/**
319+
* Check if operation produces text/plain responses.
320+
* NOTE: This does not mean it _only_ produces text/plain, just that it is one of the produces types.
321+
*
322+
* @return true if at least one produces is text/plain
323+
*/
324+
public boolean producesTextPlain() {
325+
if (produces != null) {
326+
for (Map<String, String> produce : produces) {
327+
if ("text/plain".equalsIgnoreCase(produce.get("mediaType").split(";")[0].trim())
328+
&& "String".equals(returnType)) {
329+
return true;
330+
}
331+
}
332+
}
333+
return false;
334+
}
335+
318336
/**
319337
* Get the substring except baseName from path
320338
*

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -841,17 +841,10 @@ public int compare(CodegenParameter one, CodegenParameter another) {
841841
if (NATIVE.equals(getLibrary()) || APACHE.equals(getLibrary())) {
842842
OperationMap operations = objs.getOperations();
843843
List<CodegenOperation> operationList = operations.getOperation();
844-
Pattern methodPattern = Pattern.compile("^(.*):([^:]*)$");
845844
for (CodegenOperation op : operationList) {
846845
// add extension to indicate content type is `text/plain` and the response type is `String`
847-
if (op.produces != null) {
848-
for (Map<String, String> produce : op.produces) {
849-
if ("text/plain".equalsIgnoreCase(produce.get("mediaType").split(";")[0].trim())
850-
&& "String".equals(op.returnType)) {
851-
op.vendorExtensions.put("x-java-text-plain-string", true);
852-
continue;
853-
}
854-
}
846+
if ("String".equals(op.returnType) && op.producesTextPlain()) {
847+
op.vendorExtensions.put("x-java-text-plain-string", true);
855848
}
856849
}
857850
}

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustClientCodegen.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -677,6 +677,10 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<Mo
677677
operation.vendorExtensions.put("x-group-parameters", Boolean.TRUE);
678678
}
679679

680+
if (operation.producesTextPlain() && "String".equals(operation.returnType)) {
681+
operation.vendorExtensions.put("x-supports-plain-text", Boolean.TRUE);
682+
}
683+
680684
// update return type to conform to rust standard
681685
/*
682686
if (operation.returnType != null) {

modules/openapi-generator/src/main/resources/rust/reqwest-trait/api.mustache

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ use mockall::automock;
77
{{/mockall}}
88
use reqwest;
99
use std::sync::Arc;
10-
use serde::{Deserialize, Serialize};
10+
use serde::{Deserialize, Serialize, de::Error as _};
1111
use crate::{apis::ResponseContent, models};
1212
use super::{Error, configuration};
13+
use crate::apis::ContentType;
1314

1415
{{#mockall}}
1516
#[cfg_attr(feature = "mockall", automock)]
@@ -336,6 +337,16 @@ impl {{classname}} for {{classname}}Client {
336337
let local_var_resp = local_var_client.execute(local_var_req).await?;
337338

338339
let local_var_status = local_var_resp.status();
340+
{{^supportMultipleResponses}}
341+
{{#returnType}}
342+
let local_var_content_type = local_var_resp
343+
.headers()
344+
.get("content-type")
345+
.and_then(|v| v.to_str().ok())
346+
.unwrap_or("application/octet-stream");
347+
let local_var_content_type = super::ContentType::from(local_var_content_type);
348+
{{/returnType}}
349+
{{/supportMultipleResponses}}
339350
let local_var_content = local_var_resp.text().await?;
340351

341352
if !local_var_status.is_client_error() && !local_var_status.is_server_error() {
@@ -344,7 +355,16 @@ impl {{classname}} for {{classname}}Client {
344355
Ok(())
345356
{{/returnType}}
346357
{{#returnType}}
347-
serde_json::from_str(&local_var_content).map_err(Error::from)
358+
match local_var_content_type {
359+
ContentType::Json => serde_json::from_str(&local_var_content).map_err(Error::from),
360+
{{#vendorExtensions.x-supports-plain-text}}
361+
ContentType::Text => return Ok(local_var_content),
362+
{{/vendorExtensions.x-supports-plain-text}}
363+
{{^vendorExtensions.x-supports-plain-text}}
364+
ContentType::Text => return Err(Error::from(serde_json::Error::custom("Received `text/plain` content type response that cannot be converted to `{{returnType}}`"))),
365+
{{/vendorExtensions.x-supports-plain-text}}
366+
ContentType::Unsupported(local_var_unknown_type) => return Err(Error::from(serde_json::Error::custom(format!("Received `{local_var_unknown_type}` content type response that cannot be converted to `{{returnType}}`")))),
367+
}
348368
{{/returnType}}
349369
{{/supportMultipleResponses}}
350370
{{#supportMultipleResponses}}

modules/openapi-generator/src/main/resources/rust/reqwest-trait/api_mod.mustache

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,27 @@ pub fn parse_deep_object(prefix: &str, value: &serde_json::Value) -> Vec<(String
128128
unimplemented!("Only objects are supported with style=deepObject")
129129
}
130130

131+
/// Internal use only
132+
/// A content type supported by this client.
133+
#[allow(dead_code)]
134+
enum ContentType {
135+
Json,
136+
Text,
137+
Unsupported(String)
138+
}
139+
140+
impl From<&str> for ContentType {
141+
fn from(content_type: &str) -> Self {
142+
if content_type.starts_with("application") && content_type.contains("json") {
143+
return Self::Json;
144+
} else if content_type.starts_with("text/plain") {
145+
return Self::Text;
146+
} else {
147+
return Self::Unsupported(content_type.to_string());
148+
}
149+
}
150+
}
151+
131152
{{#apiInfo}}
132153
{{#apis}}
133154
pub mod {{{classFilename}}};

modules/openapi-generator/src/main/resources/rust/reqwest/api.mustache

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
{{>partial_header}}
22

33
use reqwest;
4-
use serde::{Deserialize, Serialize};
4+
use serde::{Deserialize, Serialize, de::Error as _};
55
use crate::{apis::ResponseContent, models};
6-
use super::{Error, configuration};
6+
use super::{Error, configuration, ContentType};
77

88
{{#operations}}
99
{{#operation}}
@@ -355,6 +355,18 @@ pub {{#supportAsync}}async {{/supportAsync}}fn {{{operationId}}}(configuration:
355355
let resp = configuration.client.execute(req){{#supportAsync}}.await{{/supportAsync}}?;
356356

357357
let status = resp.status();
358+
{{^supportMultipleResponses}}
359+
{{^isResponseFile}}
360+
{{#returnType}}
361+
let content_type = resp
362+
.headers()
363+
.get("content-type")
364+
.and_then(|v| v.to_str().ok())
365+
.unwrap_or("application/octet-stream");
366+
let content_type = super::ContentType::from(content_type);
367+
{{/returnType}}
368+
{{/isResponseFile}}
369+
{{/supportMultipleResponses}}
358370

359371
if !status.is_client_error() && !status.is_server_error() {
360372
{{^supportMultipleResponses}}
@@ -367,7 +379,16 @@ pub {{#supportAsync}}async {{/supportAsync}}fn {{{operationId}}}(configuration:
367379
{{/returnType}}
368380
{{#returnType}}
369381
let content = resp.text(){{#supportAsync}}.await{{/supportAsync}}?;
370-
serde_json::from_str(&content).map_err(Error::from)
382+
match content_type {
383+
ContentType::Json => serde_json::from_str(&content).map_err(Error::from),
384+
{{#vendorExtensions.x-supports-plain-text}}
385+
ContentType::Text => return Ok(content),
386+
{{/vendorExtensions.x-supports-plain-text}}
387+
{{^vendorExtensions.x-supports-plain-text}}
388+
ContentType::Text => return Err(Error::from(serde_json::Error::custom("Received `text/plain` content type response that cannot be converted to `{{returnType}}`"))),
389+
{{/vendorExtensions.x-supports-plain-text}}
390+
ContentType::Unsupported(unknown_type) => return Err(Error::from(serde_json::Error::custom(format!("Received `{unknown_type}` content type response that cannot be converted to `{{returnType}}`")))),
391+
}
371392
{{/returnType}}
372393
{{/isResponseFile}}
373394
{{/supportMultipleResponses}}

modules/openapi-generator/src/main/resources/rust/reqwest/api_mod.mustache

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,27 @@ pub fn parse_deep_object(prefix: &str, value: &serde_json::Value) -> Vec<(String
134134
unimplemented!("Only objects are supported with style=deepObject")
135135
}
136136

137+
/// Internal use only
138+
/// A content type supported by this client.
139+
#[allow(dead_code)]
140+
enum ContentType {
141+
Json,
142+
Text,
143+
Unsupported(String)
144+
}
145+
146+
impl From<&str> for ContentType {
147+
fn from(content_type: &str) -> Self {
148+
if content_type.starts_with("application") && content_type.contains("json") {
149+
return Self::Json;
150+
} else if content_type.starts_with("text/plain") {
151+
return Self::Text;
152+
} else {
153+
return Self::Unsupported(content_type.to_string());
154+
}
155+
}
156+
}
157+
137158
{{#apiInfo}}
138159
{{#apis}}
139160
pub mod {{{classFilename}}};

modules/openapi-generator/src/test/resources/3_0/rust/petstore.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,10 @@ paths:
495495
application/json:
496496
schema:
497497
type: string
498+
text/plain:
499+
schema:
500+
type: string
501+
498502
'400':
499503
description: Invalid username/password supplied
500504
/user/logout:

samples/client/others/rust/reqwest-regression-16119/src/apis/default_api.rs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@
1010

1111

1212
use reqwest;
13-
use serde::{Deserialize, Serialize};
13+
use serde::{Deserialize, Serialize, de::Error as _};
1414
use crate::{apis::ResponseContent, models};
15-
use super::{Error, configuration};
15+
use super::{Error, configuration, ContentType};
1616

1717

1818
/// struct for typed errors of method [`repro`]
@@ -36,10 +36,20 @@ pub fn repro(configuration: &configuration::Configuration, ) -> Result<models::P
3636
let resp = configuration.client.execute(req)?;
3737

3838
let status = resp.status();
39+
let content_type = resp
40+
.headers()
41+
.get("content-type")
42+
.and_then(|v| v.to_str().ok())
43+
.unwrap_or("application/octet-stream");
44+
let content_type = super::ContentType::from(content_type);
3945

4046
if !status.is_client_error() && !status.is_server_error() {
4147
let content = resp.text()?;
42-
serde_json::from_str(&content).map_err(Error::from)
48+
match content_type {
49+
ContentType::Json => serde_json::from_str(&content).map_err(Error::from),
50+
ContentType::Text => return Err(Error::from(serde_json::Error::custom("Received `text/plain` content type response that cannot be converted to `models::Parent`"))),
51+
ContentType::Unsupported(unknown_type) => return Err(Error::from(serde_json::Error::custom(format!("Received `{unknown_type}` content type response that cannot be converted to `models::Parent`")))),
52+
}
4353
} else {
4454
let content = resp.text()?;
4555
let entity: Option<ReproError> = serde_json::from_str(&content).ok();

samples/client/others/rust/reqwest-regression-16119/src/apis/mod.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,27 @@ pub fn parse_deep_object(prefix: &str, value: &serde_json::Value) -> Vec<(String
9090
unimplemented!("Only objects are supported with style=deepObject")
9191
}
9292

93+
/// Internal use only
94+
/// A content type supported by this client.
95+
#[allow(dead_code)]
96+
enum ContentType {
97+
Json,
98+
Text,
99+
Unsupported(String)
100+
}
101+
102+
impl From<&str> for ContentType {
103+
fn from(content_type: &str) -> Self {
104+
if content_type.starts_with("application") && content_type.contains("json") {
105+
return Self::Json;
106+
} else if content_type.starts_with("text/plain") {
107+
return Self::Text;
108+
} else {
109+
return Self::Unsupported(content_type.to_string());
110+
}
111+
}
112+
}
113+
93114
pub mod default_api;
94115

95116
pub mod configuration;

0 commit comments

Comments
 (0)