@@ -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
0 commit comments