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,10 +531,9 @@ 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(" ", "_")) : ""
Expand All @@ -544,77 +542,89 @@ public CodegenOperation fromOperation(String path, String httpMethod, Operation
}

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);
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;
List<Map<String, Object>> responseContentTypes = new ArrayList<>();

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;
}
for (String contentType : producesTypes) {
Map<String, Object> contentTypeInfo = new HashMap<>();
contentTypeInfo.put("mediaType", contentType);

outputMime = firstProduces;
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");
if (bytesType.equals(rsp.dataType)) {
contentTypeInfo.put("x-serializer-bytes", true);
} else {
contentTypeInfo.put("x-serializer-plain", true);
}
}
} 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(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);
}

responseContentTypes.add(contentTypeInfo);
}

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

for (CodegenProperty header : rsp.headers) {
Expand Down Expand Up @@ -919,6 +929,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 @@
#[derive(Debug, PartialEq, Serialize, Deserialize)]
{{#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)]
{{/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