Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ public class RustAxumServerCodegen extends AbstractRustCodegen implements Codege
private static final String textXmlMimeType = "text/xml";
private static final String formUrlEncodedMimeType = "application/x-www-form-urlencoded";
private static final String jsonMimeType = "application/json";
private static final String eventStreamMimeType = "text/event-stream";

// RFC 7386 support
private static final String mergePatchJsonMimeType = "application/merge-patch+json";
// RFC 7807 Support
Expand Down Expand Up @@ -451,12 +453,17 @@ private boolean isMimetypeUnknown(String mimetype) {
return "*/*".equals(mimetype);
}

private boolean isMimetypeEventStream(String mimetype) {
return mimetype.toLowerCase(Locale.ROOT).startsWith(eventStreamMimeType);
}

boolean isMimetypePlain(String mimetype) {
return !(isMimetypeUnknown(mimetype) ||
isMimetypeJson(mimetype) ||
isMimetypeWwwFormUrlEncoded(mimetype) ||
isMimetypeMultipartFormData(mimetype) ||
isMimetypeMultipartRelated(mimetype));
isMimetypeMultipartRelated(mimetype) ||
isMimetypeEventStream(mimetype));
}

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

for (String mimeType : producesInfo) {
if (isMimetypeWwwFormUrlEncoded(mimeType)) {
producesFormUrlEncoded = true;
} else if (isMimetypePlain(mimeType)) {
producesPlainText = true;
}

Map<String, String> mediaType = new HashMap<>();
mediaType.put("mediaType", mimeType);

Expand All @@ -532,91 +531,125 @@ public CodegenOperation fromOperation(String path, String httpMethod, Operation
original = ModelUtils.getReferencedApiResponse(openAPI, original);

// Create a unique responseID for this response, if one is not already specified with the "x-response-id" extension
// The x-response-id may have an appended suffix when multiple content types are present.
if (!rsp.vendorExtensions.containsKey("x-response-id")) {
String[] words = rsp.message.split("[^A-Za-z ]");

// build responseId from both status code and description
String responseId = "Status" + rsp.code + (
((words.length != 0) && (!words[0].trim().isEmpty())) ?
"_" + camelize(words[0].replace(" ", "_")) : ""
);
rsp.vendorExtensions.put("x-response-id", responseId);
}

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

if (original.getContent() != null) {
firstProduces = original.getContent().keySet().stream().findFirst().orElse(null);
}
if (original.getContent() != null) {
producesTypes.addAll(original.getContent().keySet());
}

// The output mime type. This allows us to do sensible fallback
// to JSON rather than using only the default operation
// mimetype.
String outputMime;

if (firstProduces == null) {
if (producesFormUrlEncoded) {
outputMime = formUrlEncodedMimeType;
} else if (producesPlainText) {
if (bytesType.equals(rsp.dataType)) {
outputMime = octetMimeType;
} else {
outputMime = plainTextMimeType;
}
} else {
outputMime = jsonMimeType;
}
} else {
if (isMimetypeWwwFormUrlEncoded(firstProduces)) {
producesFormUrlEncoded = true;
producesPlainText = false;
} else if (isMimetypePlain(firstProduces)) {
producesFormUrlEncoded = false;
producesPlainText = true;
} else {
producesFormUrlEncoded = false;
producesPlainText = false;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jacob-mink-1996
I think the PR is too big to review. How about support streaming responses only? Support multiple response type in another PR?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might only be large because of the integration tests changed - lots of response enums have new names to support e.g. a 200 with two different content types.

I don’t have time at the moment to dedicate toward splitting the change, and again, am still unfamiliar with the repository. I do not know what exists/does not exist that should be done beyond slapping down some unit test yaml :)

Perhaps this PR should be used as inspiration if you would like to implement one or both of those features in isolation?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @jacob-mink-1996
I’m also hoping to find some time this week or next to work on this feature. l will be pushing commits here and we can work together.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome @linxGnu. That makes me feel a lot better. I can wait until you get your hands dirty before I try to split the features if you still want that.

Just to point it out - the reason I went down the route of coupling these features was that introducing another Boolean check for isStream in the vendor features made things unwieldy - just using the list directly with checks seemed to clean things up, and then this followed naturally.

List<Map<String, Object>> responseContentTypes = new ArrayList<>();

outputMime = firstProduces;
// If there are no content types (no body), create a single variant without content
if (producesTypes.isEmpty()) {
Map<String, Object> contentTypeInfo = new HashMap<>();

// Use the response-id directly as the variant name for responses without content
if (rsp.vendorExtensions.containsKey("x-response-id")) {
String baseId = (String) rsp.vendorExtensions.get("x-response-id");
contentTypeInfo.put("x-variant-name", baseId);
}

responseContentTypes.add(contentTypeInfo);
} else {
// Process each content type
for (String contentType : producesTypes) {
Map<String, Object> contentTypeInfo = new HashMap<>();
contentTypeInfo.put("mediaType", contentType);

String outputMime = contentType;

// As we don't support XML, fallback to plain text
if (isMimetypeXml(outputMime)) {
outputMime = plainTextMimeType;
}
}

rsp.vendorExtensions.put("x-mime-type", outputMime);

if (producesFormUrlEncoded) {
rsp.vendorExtensions.put("x-produces-form-urlencoded", true);
} else if (producesPlainText) {
// Plain text means that there is not structured data in
// this response. So it'll either be a UTF-8 encoded string
// 'plainText' or some generic 'bytes'.
//
// Note that we don't yet distinguish between string/binary
// and string/bytes - that is we don't auto-detect whether
// base64 encoding should be done. They both look like
// 'producesBytes'.
if (bytesType.equals(rsp.dataType)) {
rsp.vendorExtensions.put("x-produces-bytes", true);
contentTypeInfo.put("x-output-mime-type", outputMime);

// Special handling for json, form, and event stream types
if (isMimetypeJson(contentType)) {
contentTypeInfo.put("x-content-suffix", "Json");
contentTypeInfo.put("x-serializer-json", true);
} else if (isMimetypeWwwFormUrlEncoded(contentType)) {
contentTypeInfo.put("x-content-suffix", "FormUrlEncoded");
contentTypeInfo.put("x-serializer-form", true);
} else if (isMimetypeEventStream(contentType)) {
contentTypeInfo.put("x-content-suffix", "EventStream");
contentTypeInfo.put("x-serializer-event-stream", true);
} else {
rsp.vendorExtensions.put("x-produces-plain-text", true);
// Everything else is plain-text
contentTypeInfo.put("x-content-suffix", "PlainText");
// Note: serializer flags will be set after determining the actual body type
}
} else {
rsp.vendorExtensions.put("x-produces-json", true);
if (isObjectType(rsp.dataType)) {
rsp.dataType = objectType;

// Group together the x-response-id and x-content-suffix created above in order to produce
// an enum variant name like StatusXXX_CamelizedDescription_Suffix
if (rsp.vendorExtensions.containsKey("x-response-id") && contentTypeInfo.containsKey("x-content-suffix")) {
String baseId = (String) rsp.vendorExtensions.get("x-response-id");
String suffix = (String) contentTypeInfo.get("x-content-suffix");
contentTypeInfo.put("x-variant-name", baseId + "_" + suffix);
}

if (rsp.dataType != null || isMimetypeEventStream(contentType)) {
String bodyType;
if (contentTypeInfo.get("x-output-mime-type").equals(jsonMimeType)) {
bodyType = rsp.dataType;
} else if (contentTypeInfo.get("x-output-mime-type").equals(formUrlEncodedMimeType)) {
bodyType = stringType;
} else if (contentTypeInfo.get("x-output-mime-type").equals(plainTextMimeType)) {
bodyType = bytesType.equals(rsp.dataType) ? bytesType : stringType;
} else if (contentTypeInfo.get("x-output-mime-type").equals(octetMimeType)) {
// For octet-stream, always use ByteArray
bodyType = bytesType;
} else if (contentTypeInfo.get("x-output-mime-type").equals(eventStreamMimeType)) {
Schema<?> ctSchema = Optional.ofNullable(original.getContent())
.map(c -> c.get(contentType))
.map(io.swagger.v3.oas.models.media.MediaType::getSchema)
.orElse(null);
if (ctSchema != null) {
String resolvedType = getTypeDeclaration(ctSchema);
bodyType = "std::pin::Pin<Box<dyn futures::Stream<Item = Result<" + resolvedType + ", Box<dyn std::error::Error + Send + Sync + 'static>>> + Send + 'static>>";
} else {
// Fall back on string streaming
bodyType = "std::pin::Pin<Box<dyn futures::Stream<Item = Result<" + stringType + ", Box<dyn std::error::Error + Send + Sync + 'static>>> + Send + 'static>>";
}

// Inform downstream logic that there is a stream enum variant - this will result in a custom debug implementation
// for the enum along with stream handling in the server operation.
rsp.vendorExtensions.put("x-has-event-stream-content", true);
} else {
bodyType = stringType;
}
contentTypeInfo.put("x-body-type", bodyType);
contentTypeInfo.put("dataType", bodyType); // Also set dataType for template conditionals

// Set serializer flags based on the actual body type for plain-text/octet-stream
if (!contentTypeInfo.containsKey("x-serializer-json") &&
!contentTypeInfo.containsKey("x-serializer-form") &&
!contentTypeInfo.containsKey("x-serializer-event-stream")) {
if (bytesType.equals(bodyType)) {
contentTypeInfo.put("x-serializer-bytes", true);
} else {
contentTypeInfo.put("x-serializer-plain", true);
}
}
}

responseContentTypes.add(contentTypeInfo);
}
}

rsp.vendorExtensions.put("x-response-content-types", responseContentTypes);

for (CodegenProperty header : rsp.headers) {
if (uuidType.equals(header.dataType)) {
additionalProperties.put("apiUsesUuid", true);
Expand Down Expand Up @@ -919,6 +952,19 @@ private boolean postProcessOperationWithModels(final CodegenOperation op) {
}
}

boolean hasEventStreamContent = false;
if (op.responses != null) {
for (CodegenResponse response : op.responses) {
if (Boolean.TRUE.equals(response.vendorExtensions.get("x-has-event-stream-content"))) {
hasEventStreamContent = true;
break;
}
}
}
if (hasEventStreamContent) {
op.vendorExtensions.put("x-has-event-stream-content", true);
}

return hasAuthMethod;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ frunk-enum-core = { version = "0.3", optional = true }
frunk-enum-derive = { version = "0.3", optional = true }
frunk_core = { version = "0.4", optional = true }
frunk_derives = { version = "0.4", optional = true }
futures = "0.3.31"
http = "1"
lazy_static = "1"
regex = "1"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,49 +1,46 @@
{{#vendorExtensions}}{{#x-has-event-stream-content}}
// Manual Debug implementation needed due to Stream not implementing Debug
impl std::fmt::Debug for {{{operationId}}}Response {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
{{#responses}}
{{#vendorExtensions}}
{{#x-response-content-types}}
{{{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}}"),
{{^-last}}
{{/-last}}
{{/x-response-content-types}}
{{/vendorExtensions}}
{{/responses}}
}
}
}
{{/x-has-event-stream-content}}{{/vendorExtensions}}
{{#vendorExtensions}}{{^x-has-event-stream-content}}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
{{/x-has-event-stream-content}}{{/vendorExtensions}}
#[must_use]
#[allow(clippy::large_enum_variant)]
pub enum {{{operationId}}}Response {
{{#responses}}
{{#vendorExtensions}}
{{#x-response-content-types}}
{{#message}}
/// {{{.}}}{{/message}}
{{#vendorExtensions}}
{{{x-response-id}}}
{{/vendorExtensions}}
/// {{{.}}} ({{{mediaType}}})
{{/message}}
{{{x-variant-name}}}
{{^dataType}}
{{#hasHeaders}}
{
{{/hasHeaders}}
{{/dataType}}
{{#dataType}}
{{^hasHeaders}}
{{#vendorExtensions}}
{{#x-produces-plain-text}}
(String)
{{/x-produces-plain-text}}
{{#x-produces-bytes}}
(ByteArray)
{{/x-produces-bytes}}
{{^x-produces-plain-text}}
{{^x-produces-bytes}}
({{{dataType}}})
{{/x-produces-bytes}}
{{/x-produces-plain-text}}
{{/vendorExtensions}}
({{{x-body-type}}})
{{/hasHeaders}}
{{#hasHeaders}}
{
{{#vendorExtensions}}
{{#x-produces-plain-text}}
body: String,
{{/x-produces-plain-text}}
{{#x-produces-bytes}}
body: ByteArray,
{{/x-produces-bytes}}
{{^x-produces-plain-text}}
{{^x-produces-bytes}}
body: {{{dataType}}},
{{/x-produces-bytes}}
{{/x-produces-plain-text}}
{{/vendorExtensions}}
body: {{{x-body-type}}},
{{/hasHeaders}}
{{/dataType}}
{{#headers}}
Expand All @@ -62,6 +59,11 @@ pub enum {{{operationId}}}Response {
}
{{/-last}}
{{/headers}}
{{^-last}}
,
{{/-last}}
{{/x-response-content-types}}
{{/vendorExtensions}}
{{^-last}}
,
{{/-last}}
Expand Down
Loading