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