From e95ced8becbdbe27cd5cfb4919b5325767cf5fc8 Mon Sep 17 00:00:00 2001 From: Jacob Mink <3.141jvmink@gmail.com> Date: Wed, 8 Oct 2025 16:52:03 -0500 Subject: [PATCH] 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 --- .../languages/RustAxumServerCodegen.java | 155 ++++++++++-------- .../main/resources/rust-axum/Cargo.mustache | 1 + .../resources/rust-axum/response.mustache | 64 ++++---- .../rust-axum/server-operation.mustache | 106 ++++++++---- .../rust/RustAxumServerCodegenTest.java | 119 ++++++++++++++ .../rust-axum/test-anyof-allof-response.yaml | 100 +++++++++++ .../rust-axum/test-complex-event-stream.yaml | 48 ++++++ .../test-multiple-content-types.yaml | 33 ++++ .../3_1/rust-axum/test-oneof-response.yaml | 48 ++++++ 9 files changed, 546 insertions(+), 128 deletions(-) create mode 100644 modules/openapi-generator/src/test/resources/3_1/rust-axum/test-anyof-allof-response.yaml create mode 100644 modules/openapi-generator/src/test/resources/3_1/rust-axum/test-complex-event-stream.yaml create mode 100644 modules/openapi-generator/src/test/resources/3_1/rust-axum/test-multiple-content-types.yaml create mode 100644 modules/openapi-generator/src/test/resources/3_1/rust-axum/test-oneof-response.yaml diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustAxumServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustAxumServerCodegen.java index 1366797328a5..0965c870de0b 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustAxumServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustAxumServerCodegen.java @@ -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 @@ -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 @@ -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 producesInfo = getProducesInfo(openAPI, operation); - boolean producesPlainText = false; - boolean producesFormUrlEncoded = false; if (producesInfo != null && !producesInfo.isEmpty()) { List> produces = new ArrayList<>(producesInfo.size()); for (String mimeType : producesInfo) { - if (isMimetypeWwwFormUrlEncoded(mimeType)) { - producesFormUrlEncoded = true; - } else if (isMimetypePlain(mimeType)) { - producesPlainText = true; - } - Map mediaType = new HashMap<>(); mediaType.put("mediaType", mimeType); @@ -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(" ", "_")) : "" @@ -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 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> 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 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>> + Send + 'static>>"; + } else { + // Fall back on string streaming + bodyType = "std::pin::Pin>> + 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) { @@ -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; } diff --git a/modules/openapi-generator/src/main/resources/rust-axum/Cargo.mustache b/modules/openapi-generator/src/main/resources/rust-axum/Cargo.mustache index 92f89a371e4f..399236a5fcb7 100644 --- a/modules/openapi-generator/src/main/resources/rust-axum/Cargo.mustache +++ b/modules/openapi-generator/src/main/resources/rust-axum/Cargo.mustache @@ -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" diff --git a/modules/openapi-generator/src/main/resources/rust-axum/response.mustache b/modules/openapi-generator/src/main/resources/rust-axum/response.mustache index d99db9516cd3..5328e2f2c738 100644 --- a/modules/openapi-generator/src/main/resources/rust-axum/response.mustache +++ b/modules/openapi-generator/src/main/resources/rust-axum/response.mustache @@ -1,13 +1,34 @@ -#[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}} { @@ -15,35 +36,11 @@ pub enum {{{operationId}}}Response { {{/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}} @@ -62,6 +59,11 @@ pub enum {{{operationId}}}Response { } {{/-last}} {{/headers}} + {{^-last}} + , + {{/-last}} + {{/x-response-content-types}} + {{/vendorExtensions}} {{^-last}} , {{/-last}} diff --git a/modules/openapi-generator/src/main/resources/rust-axum/server-operation.mustache b/modules/openapi-generator/src/main/resources/rust-axum/server-operation.mustache index c6d463f21791..2d600d713f83 100644 --- a/modules/openapi-generator/src/main/resources/rust-axum/server-operation.mustache +++ b/modules/openapi-generator/src/main/resources/rust-axum/server-operation.mustache @@ -289,12 +289,14 @@ let result = api_impl.as_ref().{{#vendorExtensions}}{{{x-operation-id}}}{{/vendo let resp = match result { Ok(rsp) => match rsp { {{#responses}} - apis::{{classFilename}}::{{{operationId}}}Response::{{#vendorExtensions}}{{x-response-id}}{{/vendorExtensions}} +{{#vendorExtensions}} +{{#x-response-content-types}} + apis::{{classFilename}}::{{{operationId}}}Response::{{{x-variant-name}}} {{#dataType}} -{{^headers}} +{{^hasHeaders}} (body) -{{/headers}} -{{#headers}} +{{/hasHeaders}} +{{#hasHeaders}} {{#-first}} { body, @@ -303,10 +305,10 @@ let result = api_impl.as_ref().{{#vendorExtensions}}{{{x-operation-id}}}{{/vendo {{#-last}} } {{/-last}} -{{/headers}} +{{/hasHeaders}} {{/dataType}} {{^dataType}} -{{#headers}} +{{#hasHeaders}} {{#-first}} { {{/-first}} @@ -314,9 +316,27 @@ let result = api_impl.as_ref().{{#vendorExtensions}}{{{x-operation-id}}}{{/vendo {{#-last}} } {{/-last}} -{{/headers}} +{{/hasHeaders}} {{/dataType}} => { +{{#dataType}} +{{#x-serializer-event-stream}} + // Convert TryStream to SSE stream + use futures::StreamExt; + use axum::response::IntoResponse; + let sse_stream = body.map(|result| { + match result { + Ok(data) => { + // Convert data to SSE Event + Ok(axum::response::sse::Event::default().json_data(data)?) + }, + Err(e) => Err(e) + } + }); + let body_response = axum::response::Sse::new(sse_stream).into_response(); + return Ok(body_response); +{{/x-serializer-event-stream}} +{{^x-serializer-event-stream}} {{#headers}} {{^required}} if let Some({{{name}}}) = {{{name}}} { @@ -348,60 +368,84 @@ let result = api_impl.as_ref().{{#vendorExtensions}}{{{x-operation-id}}}{{/vendo {{^range}} let mut response = response.status({{{code}}}); {{/range}} -{{#produces}} -{{#-first}} -{{#dataType}} -{{#vendorExtensions}} { let mut response_headers = response.headers_mut().unwrap(); response_headers.insert( CONTENT_TYPE, - HeaderValue::from_static("{{{x-mime-type}}}")); + HeaderValue::from_static("{{{x-output-mime-type}}}")); } - -{{/vendorExtensions}} -{{/dataType}} -{{/-first}} -{{/produces}} -{{#dataType}} -{{#vendorExtensions}} -{{#x-produces-json}} +{{#x-serializer-json}} {{^allowBlockingResponseSerialize}} + let body_clone = body.clone(); let body_content = tokio::task::spawn_blocking(move || {{/allowBlockingResponseSerialize}} {{#allowBlockingResponseSerialize}} let body_content = {{/allowBlockingResponseSerialize}} - serde_json::to_vec(&body).map_err(|e| { + serde_json::to_vec(&{{^allowBlockingResponseSerialize}}body_clone{{/allowBlockingResponseSerialize}}{{#allowBlockingResponseSerialize}}body{{/allowBlockingResponseSerialize}}).map_err(|e| { error!(error = ?e); StatusCode::INTERNAL_SERVER_ERROR }){{^allowBlockingResponseSerialize}}).await.unwrap(){{/allowBlockingResponseSerialize}}?; -{{/x-produces-json}} -{{#x-produces-form-urlencoded}} +{{/x-serializer-json}} +{{#x-serializer-form}} {{^allowBlockingResponseSerialize}} + let body_clone = body.clone(); let body_content = tokio::task::spawn_blocking(move || {{/allowBlockingResponseSerialize}} {{#allowBlockingResponseSerialize}} let body_content = {{/allowBlockingResponseSerialize}} - serde_html_form::to_string(body).map_err(|e| { + serde_html_form::to_string({{^allowBlockingResponseSerialize}}body_clone{{/allowBlockingResponseSerialize}}{{#allowBlockingResponseSerialize}}body{{/allowBlockingResponseSerialize}}).map_err(|e| { error!(error = ?e); StatusCode::INTERNAL_SERVER_ERROR }){{^allowBlockingResponseSerialize}}).await.unwrap(){{/allowBlockingResponseSerialize}}?; -{{/x-produces-form-urlencoded}} -{{#x-produces-bytes}} - let body_content = body.0; -{{/x-produces-bytes}} -{{#x-produces-plain-text}} +{{/x-serializer-form}} +{{#x-serializer-plain}} let body_content = body; -{{/x-produces-plain-text}} -{{/vendorExtensions}} +{{/x-serializer-plain}} +{{#x-serializer-bytes}} + let body_content = body.0; +{{/x-serializer-bytes}} response.body(Body::from(body_content)) +{{/x-serializer-event-stream}} {{/dataType}} {{^dataType}} +{{#headers}} + {{^required}} + if let Some({{{name}}}) = {{{name}}} { + {{/required}} + let {{{name}}} = match header::IntoHeaderValue({{{name}}}).try_into() { + Ok(val) => val, + Err(e) => { + return Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(Body::from(format!("An internal server error occurred handling {{name}} header - {e}"))).map_err(|e| { error!(error = ?e); StatusCode::INTERNAL_SERVER_ERROR }); + } + }; + + + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers.insert( + HeaderName::from_static("{{{nameInLowerCase}}}"), + {{name}} + ); + } + {{^required}} + } + {{/required}} +{{/headers}} +{{#range}} + response.status::(body.code.parse().unwrap()); // {{{code}}} +{{/range}} +{{^range}} + let mut response = response.status({{{code}}}); +{{/range}} response.body(Body::empty()) {{/dataType}} }, +{{/x-response-content-types}} +{{/vendorExtensions}} {{/responses}} }, Err({{#ownedRequest}}_{{/ownedRequest}}why) => { diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/rust/RustAxumServerCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/rust/RustAxumServerCodegenTest.java index bb4150d38089..5b7a43f7d53a 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/rust/RustAxumServerCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/rust/RustAxumServerCodegenTest.java @@ -34,4 +34,123 @@ public void testPreventDuplicateOperationDeclaration() throws IOException { TestUtils.assertFileExists(outputPath); TestUtils.assertFileContains(outputPath, routerSpec); } + + @Test + public void testMultipleContentTypesPerStatusCode() throws IOException { + Path target = Files.createTempDirectory("test"); + final CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("rust-axum") + .setInputSpec("src/test/resources/3_1/rust-axum/test-multiple-content-types.yaml") + .setSkipOverwrite(false) + .setOutputDir(target.toAbsolutePath().toString().replace("\\", "/")); + + List files = new DefaultGenerator().opts(configurator.toClientOptInput()).generate(); + files.forEach(File::deleteOnExit); + + Path apiPath = Path.of(target.toString(), "/src/apis/default.rs"); + TestUtils.assertFileExists(apiPath); + + TestUtils.assertFileContains(apiPath, "pub enum TestGetResponse"); + + TestUtils.assertFileContains(apiPath, "Status200_SuccessResponseWithMultipleContentTypes_Json"); + TestUtils.assertFileContains(apiPath, "Status200_SuccessResponseWithMultipleContentTypes_EventStream"); + TestUtils.assertFileContains(apiPath, "Status200_SuccessResponseWithMultipleContentTypes_PlainText"); + + TestUtils.assertFileContains(apiPath, "(application/json)"); + TestUtils.assertFileContains(apiPath, "(text/event-stream)"); + TestUtils.assertFileContains(apiPath, "(text/plain)"); + + TestUtils.assertFileContains(apiPath, "Status200_SuccessResponseWithMultipleContentTypes_Json"); + TestUtils.assertFileContains(apiPath, "Status200_SuccessResponseWithMultipleContentTypes_EventStream"); + TestUtils.assertFileContains(apiPath, "Status200_SuccessResponseWithMultipleContentTypes_PlainText"); + + TestUtils.assertFileContains(apiPath, "(models::TestGet200Response)"); + TestUtils.assertFileContains(apiPath, "(std::pin::Pin>> + Send + 'static>>)"); + TestUtils.assertFileContains(apiPath, "(String)"); + + TestUtils.assertFileContains(apiPath, "Status400_BadRequest_Json"); + } + + @Test + public void testComplexEventStreamType() throws IOException { + Path target = Files.createTempDirectory("test"); + final CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("rust-axum") + .setInputSpec("src/test/resources/3_1/rust-axum/test-complex-event-stream.yaml") + .setSkipOverwrite(false) + .setOutputDir(target.toAbsolutePath().toString().replace("\\", "/")); + + List files = new DefaultGenerator().opts(configurator.toClientOptInput()).generate(); + files.forEach(File::deleteOnExit); + + Path apiPath = Path.of(target.toString(), "/src/apis/default.rs"); + TestUtils.assertFileExists(apiPath); + + TestUtils.assertFileContains(apiPath, "pub enum EventsGetResponse"); + TestUtils.assertFileContains(apiPath, "Status200_SuccessResponseWithComplexEventStream_EventStream"); + TestUtils.assertFileContains(apiPath, "(text/event-stream)"); + TestUtils.assertFileContains(apiPath, "(std::pin::Pin>> + Send + 'static>>)"); + TestUtils.assertFileContains(apiPath, "Status400_BadRequest_Json"); + } + + @Test + public void testOneOfResponse() throws IOException { + Path target = Files.createTempDirectory("test"); + final CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("rust-axum") + .setInputSpec("src/test/resources/3_1/rust-axum/test-oneof-response.yaml") + .setSkipOverwrite(false) + .setOutputDir(target.toAbsolutePath().toString().replace("\\", "/")); + + List files = new DefaultGenerator().opts(configurator.toClientOptInput()).generate(); + files.forEach(File::deleteOnExit); + + Path apiPath = Path.of(target.toString(), "/src/apis/default.rs"); + TestUtils.assertFileExists(apiPath); + + TestUtils.assertFileContains(apiPath, "pub enum PetsGetResponse"); + + TestUtils.assertFileContains(apiPath, "Status200_APetObject_Json"); + TestUtils.assertFileContains(apiPath, "Status200_APetObject_EventStream"); + TestUtils.assertFileContains(apiPath, "(application/json)"); + TestUtils.assertFileContains(apiPath, "(text/event-stream)"); + TestUtils.assertFileContains(apiPath, "(models::PetsGet200Response)"); + TestUtils.assertFileContains(apiPath, "(std::pin::Pin>> + Send + 'static>>)"); + TestUtils.assertFileContains(apiPath, "Status400_BadRequest_Json"); + } + + @Test + public void testAnyOfAllOfResponse() throws IOException { + Path target = Files.createTempDirectory("test"); + final CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("rust-axum") + .setInputSpec("src/test/resources/3_1/rust-axum/test-anyof-allof-response.yaml") + .setSkipOverwrite(false) + .setOutputDir(target.toAbsolutePath().toString().replace("\\", "/")); + + List files = new DefaultGenerator().opts(configurator.toClientOptInput()).generate(); + files.forEach(File::deleteOnExit); + + Path apiPath = Path.of(target.toString(), "/src/apis/default.rs"); + TestUtils.assertFileExists(apiPath); + + TestUtils.assertFileContains(apiPath, "pub enum AnimalsGetResponse"); + TestUtils.assertFileContains(apiPath, "pub enum HybridGetResponse"); + + TestUtils.assertFileContains(apiPath, "Status200_AnAnimalObject_Json"); + TestUtils.assertFileContains(apiPath, "Status200_AnAnimalObject_EventStream"); + + TestUtils.assertFileContains(apiPath, "Status200_AHybridAnimal_Json"); + TestUtils.assertFileContains(apiPath, "Status200_AHybridAnimal_EventStream"); + + TestUtils.assertFileContains(apiPath, "(application/json)"); + TestUtils.assertFileContains(apiPath, "(text/event-stream)"); + + TestUtils.assertFileContains(apiPath, "(models::AnimalsGet200Response)"); + TestUtils.assertFileContains(apiPath, "(models::HybridGet200Response)"); + TestUtils.assertFileContains(apiPath, "(std::pin::Pin>> + Send + 'static>>)"); + TestUtils.assertFileContains(apiPath, "(std::pin::Pin>> + Send + 'static>>)"); + + TestUtils.assertFileContains(apiPath, "Status400_BadRequest_Json"); + } } \ No newline at end of file diff --git a/modules/openapi-generator/src/test/resources/3_1/rust-axum/test-anyof-allof-response.yaml b/modules/openapi-generator/src/test/resources/3_1/rust-axum/test-anyof-allof-response.yaml new file mode 100644 index 000000000000..1001916e278b --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_1/rust-axum/test-anyof-allof-response.yaml @@ -0,0 +1,100 @@ +openapi: 3.0.0 +info: + title: AnyOf AllOf Response API + version: 1.0.0 +paths: + /animals: + get: + summary: Get an animal (anyOf test) + responses: + '200': + description: An animal object + content: + application/json: + schema: + anyOf: + - $ref: '#/components/schemas/Dog' + - $ref: '#/components/schemas/Cat' + - $ref: '#/components/schemas/Bird' + text/event-stream: + schema: + anyOf: + - $ref: '#/components/schemas/Dog' + - $ref: '#/components/schemas/Cat' + - $ref: '#/components/schemas/Bird' + '400': + description: Bad request + content: + application/json: + schema: + type: object + properties: + error: + type: string + /hybrid: + get: + summary: Get a hybrid animal (allOf test) + responses: + '200': + description: A hybrid animal + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/BaseAnimal' + - $ref: '#/components/schemas/FlyingAnimal' + text/event-stream: + schema: + allOf: + - $ref: '#/components/schemas/BaseAnimal' + - $ref: '#/components/schemas/FlyingAnimal' + '400': + description: Bad request + content: + application/json: + schema: + type: object + properties: + error: + type: string +components: + schemas: + BaseAnimal: + type: object + properties: + name: + type: string + age: + type: integer + required: + - name + FlyingAnimal: + type: object + properties: + canFly: + type: boolean + maxAltitude: + type: integer + required: + - canFly + Dog: + type: object + properties: + bark: + type: boolean + breed: + type: string + Cat: + type: object + properties: + hunts: + type: boolean + age: + type: integer + Bird: + type: object + properties: + canFly: + type: boolean + species: + type: string diff --git a/modules/openapi-generator/src/test/resources/3_1/rust-axum/test-complex-event-stream.yaml b/modules/openapi-generator/src/test/resources/3_1/rust-axum/test-complex-event-stream.yaml new file mode 100644 index 000000000000..66f13d477247 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_1/rust-axum/test-complex-event-stream.yaml @@ -0,0 +1,48 @@ +openapi: 3.0.0 +info: + title: Test API with Complex Event Stream + version: 1.0.0 +paths: + /events: + get: + summary: Event stream with complex data type + responses: + '200': + description: Success response with complex event stream + content: + text/event-stream: + schema: + type: object + properties: + id: + type: string + format: uuid + timestamp: + type: string + format: date-time + event_type: + type: string + enum: [user_action, system_event, error] + data: + type: object + properties: + user_id: + type: integer + action: + type: string + metadata: + type: object + additionalProperties: true + severity: + type: string + enum: [low, medium, high, critical] + required: [id, timestamp, event_type, data] + '400': + description: Bad request + content: + application/json: + schema: + type: object + properties: + error: + type: string diff --git a/modules/openapi-generator/src/test/resources/3_1/rust-axum/test-multiple-content-types.yaml b/modules/openapi-generator/src/test/resources/3_1/rust-axum/test-multiple-content-types.yaml new file mode 100644 index 000000000000..870540f70937 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_1/rust-axum/test-multiple-content-types.yaml @@ -0,0 +1,33 @@ +openapi: 3.0.0 +info: + title: Test API + version: 1.0.0 +paths: + /test: + get: + summary: Test endpoint with multiple content types + responses: + '200': + description: Success response with multiple content types + content: + application/json: + schema: + type: object + properties: + message: + type: string + text/event-stream: + schema: + type: string + text/plain: + schema: + type: string + '400': + description: Bad request + content: + application/json: + schema: + type: object + properties: + error: + type: string diff --git a/modules/openapi-generator/src/test/resources/3_1/rust-axum/test-oneof-response.yaml b/modules/openapi-generator/src/test/resources/3_1/rust-axum/test-oneof-response.yaml new file mode 100644 index 000000000000..35fe96a09fbd --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_1/rust-axum/test-oneof-response.yaml @@ -0,0 +1,48 @@ +openapi: 3.0.0 +info: + title: OneOf Response API + version: 1.0.0 +paths: + /pets: + get: + summary: Get a pet + responses: + '200': + description: A pet object + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/Dog' + - $ref: '#/components/schemas/Cat' + text/event-stream: + schema: + oneOf: + - $ref: '#/components/schemas/Dog' + - $ref: '#/components/schemas/Cat' + '400': + description: Bad request + content: + application/json: + schema: + type: object + properties: + error: + type: string +components: + schemas: + Dog: + type: object + properties: + bark: + type: boolean + breed: + type: string + enum: [Dingo, Husky, Retriever, Shepherd] + Cat: + type: object + properties: + hunts: + type: boolean + age: + type: integer