Skip to content

Commit bcc335f

Browse files
jacob-vincent-minkjacob-mink-1996
authored andcommitted
iterating on the appropriate types to return, creating test cases for stream and resposne enums
Keep going, there are tests failing Remove the "single item" legacy support and wrap it in the multipart content umbrella Functional testing (with a test that can be uncommented to actually test building code if cargo is available) Fixing the box/pin for the stream to behave well in real code Cleanup unused extensions Clean up the operations Ensure the stream info propagates to other parts of the generator Cleanup tests
1 parent 6b1b5cc commit bcc335f

File tree

9 files changed

+546
-128
lines changed

9 files changed

+546
-128
lines changed

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

Lines changed: 89 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ public class RustAxumServerCodegen extends AbstractRustCodegen implements Codege
8282
private static final String textXmlMimeType = "text/xml";
8383
private static final String formUrlEncodedMimeType = "application/x-www-form-urlencoded";
8484
private static final String jsonMimeType = "application/json";
85+
private static final String eventStreamMimeType = "text/event-stream";
86+
8587
// RFC 7386 support
8688
private static final String mergePatchJsonMimeType = "application/merge-patch+json";
8789
// RFC 7807 Support
@@ -451,12 +453,17 @@ private boolean isMimetypeUnknown(String mimetype) {
451453
return "*/*".equals(mimetype);
452454
}
453455

456+
private boolean isMimetypeEventStream(String mimetype) {
457+
return mimetype.toLowerCase(Locale.ROOT).startsWith(eventStreamMimeType);
458+
}
459+
454460
boolean isMimetypePlain(String mimetype) {
455461
return !(isMimetypeUnknown(mimetype) ||
456462
isMimetypeJson(mimetype) ||
457463
isMimetypeWwwFormUrlEncoded(mimetype) ||
458464
isMimetypeMultipartFormData(mimetype) ||
459-
isMimetypeMultipartRelated(mimetype));
465+
isMimetypeMultipartRelated(mimetype) ||
466+
isMimetypeEventStream(mimetype));
460467
}
461468

462469
@Override
@@ -497,18 +504,10 @@ public CodegenOperation fromOperation(String path, String httpMethod, Operation
497504
// simply lists all the types, and then we add the correct imports to
498505
// the generated library.
499506
Set<String> producesInfo = getProducesInfo(openAPI, operation);
500-
boolean producesPlainText = false;
501-
boolean producesFormUrlEncoded = false;
502507
if (producesInfo != null && !producesInfo.isEmpty()) {
503508
List<Map<String, String>> produces = new ArrayList<>(producesInfo.size());
504509

505510
for (String mimeType : producesInfo) {
506-
if (isMimetypeWwwFormUrlEncoded(mimeType)) {
507-
producesFormUrlEncoded = true;
508-
} else if (isMimetypePlain(mimeType)) {
509-
producesPlainText = true;
510-
}
511-
512511
Map<String, String> mediaType = new HashMap<>();
513512
mediaType.put("mediaType", mimeType);
514513

@@ -532,10 +531,9 @@ public CodegenOperation fromOperation(String path, String httpMethod, Operation
532531
original = ModelUtils.getReferencedApiResponse(openAPI, original);
533532

534533
// Create a unique responseID for this response, if one is not already specified with the "x-response-id" extension
534+
// The x-response-id may have an appended suffix when multiple content types are present.
535535
if (!rsp.vendorExtensions.containsKey("x-response-id")) {
536536
String[] words = rsp.message.split("[^A-Za-z ]");
537-
538-
// build responseId from both status code and description
539537
String responseId = "Status" + rsp.code + (
540538
((words.length != 0) && (!words[0].trim().isEmpty())) ?
541539
"_" + camelize(words[0].replace(" ", "_")) : ""
@@ -544,77 +542,89 @@ public CodegenOperation fromOperation(String path, String httpMethod, Operation
544542
}
545543

546544
if (rsp.dataType != null) {
547-
// Get the mimetype which is produced by this response. Note
548-
// that although in general responses produces a set of
549-
// different mimetypes currently we only support 1 per
550-
// response.
551-
String firstProduces = null;
545+
List<String> producesTypes = new ArrayList<>();
552546

553547
if (original.getContent() != null) {
554-
firstProduces = original.getContent().keySet().stream().findFirst().orElse(null);
548+
producesTypes.addAll(original.getContent().keySet());
555549
}
556550

557-
// The output mime type. This allows us to do sensible fallback
558-
// to JSON rather than using only the default operation
559-
// mimetype.
560-
String outputMime;
551+
List<Map<String, Object>> responseContentTypes = new ArrayList<>();
561552

562-
if (firstProduces == null) {
563-
if (producesFormUrlEncoded) {
564-
outputMime = formUrlEncodedMimeType;
565-
} else if (producesPlainText) {
566-
if (bytesType.equals(rsp.dataType)) {
567-
outputMime = octetMimeType;
568-
} else {
569-
outputMime = plainTextMimeType;
570-
}
571-
} else {
572-
outputMime = jsonMimeType;
573-
}
574-
} else {
575-
if (isMimetypeWwwFormUrlEncoded(firstProduces)) {
576-
producesFormUrlEncoded = true;
577-
producesPlainText = false;
578-
} else if (isMimetypePlain(firstProduces)) {
579-
producesFormUrlEncoded = false;
580-
producesPlainText = true;
581-
} else {
582-
producesFormUrlEncoded = false;
583-
producesPlainText = false;
584-
}
553+
for (String contentType : producesTypes) {
554+
Map<String, Object> contentTypeInfo = new HashMap<>();
555+
contentTypeInfo.put("mediaType", contentType);
585556

586-
outputMime = firstProduces;
557+
String outputMime = contentType;
587558

588559
// As we don't support XML, fallback to plain text
589560
if (isMimetypeXml(outputMime)) {
590561
outputMime = plainTextMimeType;
591562
}
592-
}
593563

594-
rsp.vendorExtensions.put("x-mime-type", outputMime);
595-
596-
if (producesFormUrlEncoded) {
597-
rsp.vendorExtensions.put("x-produces-form-urlencoded", true);
598-
} else if (producesPlainText) {
599-
// Plain text means that there is not structured data in
600-
// this response. So it'll either be a UTF-8 encoded string
601-
// 'plainText' or some generic 'bytes'.
602-
//
603-
// Note that we don't yet distinguish between string/binary
604-
// and string/bytes - that is we don't auto-detect whether
605-
// base64 encoding should be done. They both look like
606-
// 'producesBytes'.
607-
if (bytesType.equals(rsp.dataType)) {
608-
rsp.vendorExtensions.put("x-produces-bytes", true);
564+
contentTypeInfo.put("x-output-mime-type", outputMime);
565+
566+
// Special handling for json, form, and event stream types
567+
if (isMimetypeJson(contentType)) {
568+
contentTypeInfo.put("x-content-suffix", "Json");
569+
contentTypeInfo.put("x-serializer-json", true);
570+
} else if (isMimetypeWwwFormUrlEncoded(contentType)) {
571+
contentTypeInfo.put("x-content-suffix", "FormUrlEncoded");
572+
contentTypeInfo.put("x-serializer-form", true);
573+
} else if (isMimetypeEventStream(contentType)) {
574+
contentTypeInfo.put("x-content-suffix", "EventStream");
575+
contentTypeInfo.put("x-serializer-event-stream", true);
609576
} else {
610-
rsp.vendorExtensions.put("x-produces-plain-text", true);
577+
// Everything else is plain-text
578+
contentTypeInfo.put("x-content-suffix", "PlainText");
579+
if (bytesType.equals(rsp.dataType)) {
580+
contentTypeInfo.put("x-serializer-bytes", true);
581+
} else {
582+
contentTypeInfo.put("x-serializer-plain", true);
583+
}
611584
}
612-
} else {
613-
rsp.vendorExtensions.put("x-produces-json", true);
614-
if (isObjectType(rsp.dataType)) {
615-
rsp.dataType = objectType;
585+
586+
// Group together the x-response-id and x-content-suffix created above in order to produce
587+
// an enum variant name like StatusXXX_CamelizedDescription_Suffix
588+
if (rsp.vendorExtensions.containsKey("x-response-id") && contentTypeInfo.containsKey("x-content-suffix")) {
589+
String baseId = (String) rsp.vendorExtensions.get("x-response-id");
590+
String suffix = (String) contentTypeInfo.get("x-content-suffix");
591+
contentTypeInfo.put("x-variant-name", baseId + "_" + suffix);
592+
}
593+
594+
if (rsp.dataType != null || isMimetypeEventStream(contentType)) {
595+
String bodyType;
596+
if (contentTypeInfo.get("x-output-mime-type").equals(jsonMimeType)) {
597+
bodyType = rsp.dataType;
598+
} else if (contentTypeInfo.get("x-output-mime-type").equals(formUrlEncodedMimeType)) {
599+
bodyType = stringType;
600+
} else if (contentTypeInfo.get("x-output-mime-type").equals(plainTextMimeType)) {
601+
bodyType = bytesType.equals(rsp.dataType) ? bytesType : stringType;
602+
} else if (contentTypeInfo.get("x-output-mime-type").equals(eventStreamMimeType)) {
603+
Schema<?> ctSchema = Optional.ofNullable(original.getContent())
604+
.map(c -> c.get(contentType))
605+
.map(io.swagger.v3.oas.models.media.MediaType::getSchema)
606+
.orElse(null);
607+
if (ctSchema != null) {
608+
String resolvedType = getTypeDeclaration(ctSchema);
609+
bodyType = "std::pin::Pin<Box<dyn futures::Stream<Item = Result<" + resolvedType + ", Box<dyn std::error::Error + Send + Sync + 'static>>> + Send + 'static>>";
610+
} else {
611+
// Fall back on string streaming
612+
bodyType = "std::pin::Pin<Box<dyn futures::Stream<Item = Result<" + stringType + ", Box<dyn std::error::Error + Send + Sync + 'static>>> + Send + 'static>>";
613+
}
614+
615+
// Inform downstream logic that there is a stream enum variant - this will result in a custom debug implementation
616+
// for the enum along with stream handling in the server operation.
617+
rsp.vendorExtensions.put("x-has-event-stream-content", true);
618+
} else {
619+
bodyType = stringType;
620+
}
621+
contentTypeInfo.put("x-body-type", bodyType);
616622
}
623+
624+
responseContentTypes.add(contentTypeInfo);
617625
}
626+
627+
rsp.vendorExtensions.put("x-response-content-types", responseContentTypes);
618628
}
619629

620630
for (CodegenProperty header : rsp.headers) {
@@ -919,6 +929,19 @@ private boolean postProcessOperationWithModels(final CodegenOperation op) {
919929
}
920930
}
921931

932+
boolean hasEventStreamContent = false;
933+
if (op.responses != null) {
934+
for (CodegenResponse response : op.responses) {
935+
if (Boolean.TRUE.equals(response.vendorExtensions.get("x-has-event-stream-content"))) {
936+
hasEventStreamContent = true;
937+
break;
938+
}
939+
}
940+
}
941+
if (hasEventStreamContent) {
942+
op.vendorExtensions.put("x-has-event-stream-content", true);
943+
}
944+
922945
return hasAuthMethod;
923946
}
924947

modules/openapi-generator/src/main/resources/rust-axum/Cargo.mustache

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ frunk-enum-core = { version = "0.3", optional = true }
5151
frunk-enum-derive = { version = "0.3", optional = true }
5252
frunk_core = { version = "0.4", optional = true }
5353
frunk_derives = { version = "0.4", optional = true }
54+
futures = "0.3.31"
5455
http = "1"
5556
lazy_static = "1"
5657
regex = "1"

modules/openapi-generator/src/main/resources/rust-axum/response.mustache

Lines changed: 33 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,46 @@
1-
#[derive(Debug, PartialEq, Serialize, Deserialize)]
1+
{{#vendorExtensions}}{{#x-has-event-stream-content}}
2+
// Manual Debug implementation needed due to Stream not implementing Debug
3+
impl std::fmt::Debug for {{{operationId}}}Response {
4+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5+
match self {
6+
{{#responses}}
7+
{{#vendorExtensions}}
8+
{{#x-response-content-types}}
9+
{{{operationId}}}Response::{{{x-variant-name}}}{{^dataType}}{{#hasHeaders}} { .. }{{/hasHeaders}}{{/dataType}}{{#dataType}}{{^headers}}(..){{/headers}}{{#headers}} { body: _, .. }{{/headers}}{{/dataType}} => write!(f, "{{{x-variant-name}}}{{^dataType}}{{#hasHeaders}} {{ .. }} {{/hasHeaders}}{{/dataType}}{{#dataType}}{{^headers}}(..){{/headers}}{{#headers}} {{ body: _, .. }} {{/headers}}{{/dataType}}"),
10+
{{^-last}}
11+
{{/-last}}
12+
{{/x-response-content-types}}
13+
{{/vendorExtensions}}
14+
{{/responses}}
15+
}
16+
}
17+
}
18+
{{/x-has-event-stream-content}}{{/vendorExtensions}}
19+
{{#vendorExtensions}}{{^x-has-event-stream-content}}
20+
#[derive(Debug)]
21+
{{/x-has-event-stream-content}}{{/vendorExtensions}}
222
#[must_use]
323
#[allow(clippy::large_enum_variant)]
424
pub enum {{{operationId}}}Response {
525
{{#responses}}
26+
{{#vendorExtensions}}
27+
{{#x-response-content-types}}
628
{{#message}}
7-
/// {{{.}}}{{/message}}
8-
{{#vendorExtensions}}
9-
{{{x-response-id}}}
10-
{{/vendorExtensions}}
29+
/// {{{.}}} ({{{mediaType}}})
30+
{{/message}}
31+
{{{x-variant-name}}}
1132
{{^dataType}}
1233
{{#hasHeaders}}
1334
{
1435
{{/hasHeaders}}
1536
{{/dataType}}
1637
{{#dataType}}
1738
{{^hasHeaders}}
18-
{{#vendorExtensions}}
19-
{{#x-produces-plain-text}}
20-
(String)
21-
{{/x-produces-plain-text}}
22-
{{#x-produces-bytes}}
23-
(ByteArray)
24-
{{/x-produces-bytes}}
25-
{{^x-produces-plain-text}}
26-
{{^x-produces-bytes}}
27-
({{{dataType}}})
28-
{{/x-produces-bytes}}
29-
{{/x-produces-plain-text}}
30-
{{/vendorExtensions}}
39+
({{{x-body-type}}})
3140
{{/hasHeaders}}
3241
{{#hasHeaders}}
3342
{
34-
{{#vendorExtensions}}
35-
{{#x-produces-plain-text}}
36-
body: String,
37-
{{/x-produces-plain-text}}
38-
{{#x-produces-bytes}}
39-
body: ByteArray,
40-
{{/x-produces-bytes}}
41-
{{^x-produces-plain-text}}
42-
{{^x-produces-bytes}}
43-
body: {{{dataType}}},
44-
{{/x-produces-bytes}}
45-
{{/x-produces-plain-text}}
46-
{{/vendorExtensions}}
43+
body: {{{x-body-type}}},
4744
{{/hasHeaders}}
4845
{{/dataType}}
4946
{{#headers}}
@@ -62,6 +59,11 @@ pub enum {{{operationId}}}Response {
6259
}
6360
{{/-last}}
6461
{{/headers}}
62+
{{^-last}}
63+
,
64+
{{/-last}}
65+
{{/x-response-content-types}}
66+
{{/vendorExtensions}}
6567
{{^-last}}
6668
,
6769
{{/-last}}

0 commit comments

Comments
 (0)