diff --git a/README.md b/README.md index 6837463e..10d94f05 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Maven and Gradle - [Usage](#usage) - [Calling an API from your service (callMode)](#calling-an-api-from-your-service-callmode) - [Object-typed query parameters and multipart bodies](#object-typed-query-parameters-and-multipart-bodies) - - [Streaming endpoints (application/x-ndjson)](#streaming-endpoints-applicationx-ndjson) + - [Streaming endpoints (NDJSON and JSON Lines)](#streaming-endpoints-ndjson-and-json-lines) - [Camel case Java names (useCamelCaseNames)](#camel-case-java-names-usecamelcasenames) - [Unknown enum values (useUnknownEnumValue)](#unknown-enum-values-useunknownenumvalue) - [Property Validation](#property-validation) @@ -1093,12 +1093,14 @@ own: every generated API takes one argument per part (`MultipartFile file, String comment`). A multipart body that references a component schema keeps taking that model. -### Streaming endpoints (application/x-ndjson) +### Streaming endpoints (NDJSON and JSON Lines) An operation whose successful response, or whose request body, is -`application/x-ndjson` (or the older `application/stream+json`) streams one JSON +`application/x-ndjson` or `application/jsonl` (the same format) streams one JSON document per line. Its schema is the type of each item, or an array of it, and -an OpenAPI 3.2 `itemSchema` is read the same way: +an OpenAPI 3.2 `itemSchema` is read the same way. The deprecated +`application/stream+json`, which Spring Framework 7 no longer streams, fails the +generation: ```yaml responses: @@ -1148,6 +1150,34 @@ for it. or a `Mono>>` from `...WithHttpInfo`, and send a streamed body from a `Flux`. **Reactive `@HttpExchange` interfaces** return a `Mono>>`. +- Spring's codecs stream `application/x-ndjson` only. The generated + `ApiWebClient` registers `application/jsonl` too, but a WebFlux server or a + reactive `@HttpExchange` interface uses the application's codecs, which need + it registered for JSON Lines: + + ```java + @Bean + CodecCustomizer jsonLinesCodecCustomizer(final JsonMapper mapper) { + final MediaType jsonLines = new MediaType("application", "jsonl"); + return configurer -> { + final JacksonJsonEncoder encoder = new JacksonJsonEncoder(mapper, + MediaType.APPLICATION_JSON, MediaType.APPLICATION_NDJSON, jsonLines); + encoder.setStreamingMediaTypes( + List.of(MediaType.APPLICATION_NDJSON, jsonLines)); + configurer.defaultCodecs().jacksonJsonEncoder(encoder); + configurer.defaultCodecs().jacksonJsonDecoder(new JacksonJsonDecoder( + mapper, MediaType.APPLICATION_JSON, MediaType.APPLICATION_NDJSON, + jsonLines)); + }; + } + ``` + + This is Spring Boot 4 (`org.springframework.boot.http.codec.CodecCustomizer`). + On Spring Boot 2 and 3 it is + `org.springframework.boot.web.codec.CodecCustomizer`, with an `ObjectMapper` + and `Jackson2JsonEncoder`/`Jackson2JsonDecoder` set through + `jackson2JsonEncoder`/`jackson2JsonDecoder`. The customizer applies to the + WebFlux server and to the WebClients built from Boot's `WebClient.Builder`. - The **blocking clients** (RestClient, and `@HttpExchange` without `reactive`) read a response whole, so they cannot hand the items over as they arrive, and the generation fails for a streaming operation: generate that client with diff --git a/multiapi-engine/pom.xml b/multiapi-engine/pom.xml index 66440ce9..560692d4 100644 --- a/multiapi-engine/pom.xml +++ b/multiapi-engine/pom.xml @@ -4,7 +4,7 @@ com.sngular multiapi-engine - 8.3.0 + 8.3.1 jar diff --git a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/openapi/utils/MapperPathUtil.java b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/openapi/utils/MapperPathUtil.java index 3a56b311..b87aa4a4 100644 --- a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/openapi/utils/MapperPathUtil.java +++ b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/openapi/utils/MapperPathUtil.java @@ -61,10 +61,13 @@ public class MapperPathUtil { public static final String SCHEMA = "schema"; /** - * The media types that stream a sequence of JSON documents, one per line, which Spring reads and writes item by item. Their - * schema is the type of each item, or an array of it. + * The media types that stream a sequence of JSON documents, one per line: NDJSON and JSON Lines, which are the same format. + * Their schema is the type of each item, or an array of it. */ - private static final Set STREAMING_MEDIA_TYPES = Set.of("application/x-ndjson", "application/stream+json"); + private static final Set STREAMING_MEDIA_TYPES = Set.of("application/x-ndjson", "application/jsonl"); + + /** Deprecated for application/x-ndjson, and no longer streamed by Spring Framework 7. */ + private static final String STREAM_JSON_MEDIA_TYPE = "application/stream+json"; private static final String JSON_MEDIA_TYPE = "application/json"; @@ -190,6 +193,10 @@ public static boolean isStreamingMediaType(final String mediaType) { * content. */ private static void applyStreaming(final OperationObject operation, final SpecFile specFile) { + if (operation.getProduces().contains(STREAM_JSON_MEDIA_TYPE) || operation.getConsumes().contains(STREAM_JSON_MEDIA_TYPE)) { + throw new CodeGenerationException("Operation '" + operation.getOperationId() + "' declares " + STREAM_JSON_MEDIA_TYPE + ", which is deprecated and " + + "which Spring Framework 7 no longer streams: declare the stream as application/x-ndjson or application/jsonl"); + } final Optional streamedResponse = operation.getResponseObjects().stream() .filter(response -> StringUtils.startsWith(response.getResponseName(), "2")) .filter(response -> response.getContentObjects().stream().anyMatch(ContentObject::isStreaming)) @@ -209,7 +216,7 @@ private static void applyStreaming(final OperationObject operation, final SpecFi } final boolean streams = operation.isStreamingResponse() || operation.getRequestObjects().stream().anyMatch(RequestObject::isStreaming); if (streams && specFile.isCallMode() && !specFile.isReactive()) { - throw new CodeGenerationException("Operation '" + operation.getOperationId() + "' streams application/x-ndjson, which the blocking clients cannot " + throw new CodeGenerationException("Operation '" + operation.getOperationId() + "' streams its items, which the blocking clients cannot " + "consume: generate its client with reactive=true (a WebClient client or a reactive @HttpExchange interface)"); } } diff --git a/multiapi-engine/src/main/resources/templates/openapi/template.ftlh b/multiapi-engine/src/main/resources/templates/openapi/template.ftlh index 8c45b92e..dc8bef57 100644 --- a/multiapi-engine/src/main/resources/templates/openapi/template.ftlh +++ b/multiapi-engine/src/main/resources/templates/openapi/template.ftlh @@ -74,7 +74,7 @@ produces = {"${operation.streamingMediaType}"}${consumesStream} ) default ResponseEntity ${operation.operationId}Ndjson(${streamingEndpointArgs(operation, path, true, true)})${throwsIo} { - return NdjsonSupport.stream(${streamingMethodCall(operation, path)}, servletRequest, servletResponse); + return NdjsonSupport.stream(${streamingMethodCall(operation, path)}, "${operation.streamingMediaType}", servletRequest, servletResponse); } <#if operation.streamingAlsoJson> diff --git a/multiapi-engine/src/main/resources/templates/openapi/templateNdjsonSupport.ftlh b/multiapi-engine/src/main/resources/templates/openapi/templateNdjsonSupport.ftlh index 7c7a6419..1098f27a 100644 --- a/multiapi-engine/src/main/resources/templates/openapi/templateNdjsonSupport.ftlh +++ b/multiapi-engine/src/main/resources/templates/openapi/templateNdjsonSupport.ftlh @@ -25,31 +25,29 @@ import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBo import org.springframework.web.servlet.support.RequestContextUtils; /** - * Sends and reads application/x-ndjson, one JSON document per line, for the API interfaces of this package. Streamed + * Sends and reads application/x-ndjson and application/jsonl, one JSON document per line, for the API interfaces of this package. Streamed * responses are written by Spring MVC on its asynchronous executor, each item sent to the client as soon as it is produced, * and the stream the operation returns is closed once it is sent, also when sending fails. Items are serialized with the * application's ObjectMapper. */ public final class NdjsonSupport { - private static final MediaType APPLICATION_NDJSON = MediaType.parseMediaType("application/x-ndjson"); - private NdjsonSupport() { } /** - * The response streaming the items of the given one, with its status and headers. A response without items, such as the - * NOT_IMPLEMENTED of an operation that is not implemented, is sent without a body. + * The response streaming the items of the given one as the given media type, with its status and headers. A response + * without items, such as the NOT_IMPLEMENTED of an operation that is not implemented, is sent without a body. */ - public static ResponseEntity stream(final ResponseEntity> response, final HttpServletRequest servletRequest, - final HttpServletResponse servletResponse) { + public static ResponseEntity stream(final ResponseEntity> response, final String mediaType, + final HttpServletRequest servletRequest, final HttpServletResponse servletResponse) { final ResponseEntity.BodyBuilder builder = ResponseEntity.status(response.getStatusCode()).headers(response.getHeaders()); final Stream items = response.getBody(); if (items == null) { return builder.build(); } final ObjectMapper mapper = objectMapper(servletRequest); - return builder.contentType(APPLICATION_NDJSON).body(output -> write(items, mapper, output, servletResponse)); + return builder.contentType(MediaType.parseMediaType(mediaType)).body(output -> write(items, mapper, output, servletResponse)); } /** The response sending the items of the given one as a JSON array, with its status and headers. */ diff --git a/multiapi-engine/src/main/resources/templates/openapi/templateWebClient.ftlh b/multiapi-engine/src/main/resources/templates/openapi/templateWebClient.ftlh index 99ca6f53..1459f98e 100644 --- a/multiapi-engine/src/main/resources/templates/openapi/templateWebClient.ftlh +++ b/multiapi-engine/src/main/resources/templates/openapi/templateWebClient.ftlh @@ -6,6 +6,7 @@ import java.text.ParsePosition; import java.time.OffsetDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Date; @@ -158,17 +159,22 @@ public class ApiWebClient { } private static WebClient buildWebClient(final ${mapperType} mapper) { - // application/x-ndjson is read and written with the same mapper, one item per line. + // application/x-ndjson and application/jsonl are read and written with the same mapper, one item per line. final MediaType ndjson = new MediaType("application", "x-ndjson"); + final MediaType jsonLines = new MediaType("application", "jsonl"); ExchangeStrategies strategies = ExchangeStrategies .builder() .codecs(clientDefaultCodecsConfigurer -> { <#if isJackson3> - clientDefaultCodecsConfigurer.defaultCodecs().jacksonJsonEncoder(new JacksonJsonEncoder(mapper, MediaType.APPLICATION_JSON, ndjson)); - clientDefaultCodecsConfigurer.defaultCodecs().jacksonJsonDecoder(new JacksonJsonDecoder(mapper, MediaType.APPLICATION_JSON, ndjson)); + final JacksonJsonEncoder encoder = new JacksonJsonEncoder(mapper, MediaType.APPLICATION_JSON, ndjson, jsonLines); + encoder.setStreamingMediaTypes(Arrays.asList(ndjson, jsonLines)); + clientDefaultCodecsConfigurer.defaultCodecs().jacksonJsonEncoder(encoder); + clientDefaultCodecsConfigurer.defaultCodecs().jacksonJsonDecoder(new JacksonJsonDecoder(mapper, MediaType.APPLICATION_JSON, ndjson, jsonLines)); <#else> - clientDefaultCodecsConfigurer.defaultCodecs().jackson2JsonEncoder(new Jackson2JsonEncoder(mapper, MediaType.APPLICATION_JSON, ndjson)); - clientDefaultCodecsConfigurer.defaultCodecs().jackson2JsonDecoder(new Jackson2JsonDecoder(mapper, MediaType.APPLICATION_JSON, ndjson)); + final Jackson2JsonEncoder encoder = new Jackson2JsonEncoder(mapper, MediaType.APPLICATION_JSON, ndjson, jsonLines); + encoder.setStreamingMediaTypes(Arrays.asList(ndjson, jsonLines)); + clientDefaultCodecsConfigurer.defaultCodecs().jackson2JsonEncoder(encoder); + clientDefaultCodecsConfigurer.defaultCodecs().jackson2JsonDecoder(new Jackson2JsonDecoder(mapper, MediaType.APPLICATION_JSON, ndjson, jsonLines)); }).build(); WebClient.Builder webClientBuilder = WebClient.builder().exchangeStrategies(strategies); diff --git a/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/openapi/OpenApiGeneratorFixtures.java b/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/openapi/OpenApiGeneratorFixtures.java index 9e78675a..bb737ab8 100644 --- a/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/openapi/OpenApiGeneratorFixtures.java +++ b/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/openapi/OpenApiGeneratorFixtures.java @@ -367,6 +367,12 @@ public final class OpenApiGeneratorFixtures { .modelPackage("com.sngular.multifileplugin.ndjsonmixedrequest.model") .modelNameSuffix("DTO").useTagsGroup(true).build()); + static final List TEST_NDJSON_STREAMING_STREAM_JSON = List + .of(SpecFile.builder().filePath("openapigenerator/testNdjsonStreaming/api-stream-json.yml") + .apiPackage("com.sngular.multifileplugin.ndjsonstreamjson") + .modelPackage("com.sngular.multifileplugin.ndjsonstreamjson.model") + .modelNameSuffix("DTO").useTagsGroup(true).build()); + static final List TEST_HTTP_EXCHANGE_CLIENT = List .of(SpecFile.builder().filePath("openapigenerator/testHttpExchangeClient/api-test.yml") .apiPackage("com.sngular.multifileplugin.httpexchange") diff --git a/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/openapi/OpenApiGeneratorTest.java b/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/openapi/OpenApiGeneratorTest.java index 1c973178..51a6f114 100644 --- a/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/openapi/OpenApiGeneratorTest.java +++ b/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/openapi/OpenApiGeneratorTest.java @@ -275,6 +275,14 @@ void testNdjsonStreamingJsonAlternativeMustListTheItems() { .hasMessageContaining("must be the array of them"); } + @Test + void testDeprecatedStreamJsonIsRefused() { + Assertions.assertThatThrownBy(() -> openApiGenerator.processFileSpec(OpenApiGeneratorFixtures.TEST_NDJSON_STREAMING_STREAM_JSON)) + .isInstanceOf(CodeGenerationException.class) + .hasMessageContaining("listItems") + .hasMessageContaining("application/stream+json"); + } + @Test void testNdjsonStreamedBodyMustBeItsOnlyContent() { Assertions.assertThatThrownBy(() -> openApiGenerator.processFileSpec(OpenApiGeneratorFixtures.TEST_NDJSON_STREAMING_MIXED_REQUEST)) diff --git a/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/api-stream-json.yml b/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/api-stream-json.yml new file mode 100644 index 00000000..12f970cf --- /dev/null +++ b/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/api-stream-json.yml @@ -0,0 +1,23 @@ +openapi: "3.0.3" +info: + title: A stream declared as the deprecated application/stream+json + version: 1.0.0 +paths: + /items: + get: + tags: [items] + operationId: listItems + responses: + '200': + description: One item per line + content: + application/stream+json: + schema: + $ref: "#/components/schemas/Item" +components: + schemas: + Item: + type: object + properties: + id: + type: string diff --git a/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/api-test.yml b/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/api-test.yml index 2d2f365a..00bdbe73 100644 --- a/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/api-test.yml +++ b/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/api-test.yml @@ -26,6 +26,33 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + /items/lines: + get: + tags: [items] + operationId: listItemLines + responses: + '200': + description: One item per line, as JSON Lines + content: + application/jsonl: + schema: + $ref: "#/components/schemas/Item" + post: + tags: [items] + operationId: importItemLines + requestBody: + required: true + content: + application/jsonl: + schema: + $ref: "#/components/schemas/Item" + responses: + '200': + description: Import summary + content: + application/json: + schema: + $ref: "#/components/schemas/ImportResult" /items/export: get: tags: [items] diff --git a/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/httpexchange/ItemsApi.java b/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/httpexchange/ItemsApi.java index d2917d5e..28089a3c 100644 --- a/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/httpexchange/ItemsApi.java +++ b/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/httpexchange/ItemsApi.java @@ -42,6 +42,21 @@ public interface ItemsApi { @GetExchange(url = "/items", accept = {"application/x-ndjson"}) Mono>> listItems(@RequestParam(name = "filter", required = false) String filter); + /** + * GET /items/lines + * @return One item per line, as JSON Lines (status code 200); + */ + @GetExchange(url = "/items/lines", accept = {"application/jsonl"}) + Mono>> listItemLines(); + + /** + * POST /items/lines + * @param itemDTO (required) + * @return Import summary (status code 200); + */ + @PostExchange(url = "/items/lines", accept = {"application/json"}, contentType = "application/jsonl") + Mono> importItemLines(@RequestBody(required = true) Flux itemDTO); + /** * GET /items/export * @return The items, as a JSON array or one per line (status code 200); diff --git a/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative-boot3/EventsApi.java b/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative-boot3/EventsApi.java index 34e21974..b6f2f947 100644 --- a/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative-boot3/EventsApi.java +++ b/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative-boot3/EventsApi.java @@ -40,7 +40,7 @@ public interface EventsApi { produces = {"application/x-ndjson"} ) default ResponseEntity streamEventsNdjson(@Parameter(hidden = true) final HttpServletRequest servletRequest, @Parameter(hidden = true) final HttpServletResponse servletResponse) { - return NdjsonSupport.stream(streamEvents(), servletRequest, servletResponse); + return NdjsonSupport.stream(streamEvents(), "application/x-ndjson", servletRequest, servletResponse); } /** diff --git a/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative-boot3/ItemsApi.java b/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative-boot3/ItemsApi.java index eeba0b35..62a144d2 100644 --- a/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative-boot3/ItemsApi.java +++ b/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative-boot3/ItemsApi.java @@ -45,7 +45,7 @@ public interface ItemsApi { produces = {"application/x-ndjson"} ) default ResponseEntity listItemsNdjson(@Parameter(name = "filter", required = false, schema = @Schema(description = "")) @RequestParam(name = "filter", required = false) String filter, @Parameter(hidden = true) final HttpServletRequest servletRequest, @Parameter(hidden = true) final HttpServletResponse servletResponse) { - return NdjsonSupport.stream(listItems(filter), servletRequest, servletResponse); + return NdjsonSupport.stream(listItems(filter), "application/x-ndjson", servletRequest, servletResponse); } /** @@ -55,6 +55,59 @@ default ResponseEntity> listItems(final String filter) { return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); } + /** + * GET /items/lines, sending the items {@link #listItemLines} returns as application/jsonl, one JSON document per line. + */ + @Operation( + operationId = "listItemLines", + tags = {"items"}, + responses = { + @ApiResponse(responseCode = "200", description = "One item per line, as JSON Lines", content = @Content(mediaType = "application/jsonl", schema = @Schema(implementation = ItemDTO.class))) + } + ) + @RequestMapping( + method = RequestMethod.GET, + value = "/items/lines", + produces = {"application/jsonl"} + ) + default ResponseEntity listItemLinesNdjson(@Parameter(hidden = true) final HttpServletRequest servletRequest, @Parameter(hidden = true) final HttpServletResponse servletResponse) { + return NdjsonSupport.stream(listItemLines(), "application/jsonl", servletRequest, servletResponse); + } + + /** + * GET /items/lines. Implement this method rather than the endpoint above: the items are sent as they are produced, and the stream is closed once they are sent. + */ + default ResponseEntity> listItemLines() { + return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + } + + /** + * POST /items/lines, reading the application/jsonl body, one JSON document per line, as the items {@link #importItemLines} receives. + */ + @Operation( + operationId = "importItemLines", + tags = {"items"}, + responses = { + @ApiResponse(responseCode = "200", description = "Import summary", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ImportResultDTO.class))) + } + ) + @RequestMapping( + method = RequestMethod.POST, + value = "/items/lines", + produces = {"application/json"}, + consumes = {"application/jsonl"} + ) + default ResponseEntity importItemLinesNdjson(@Parameter(hidden = true) final HttpServletRequest servletRequest) throws IOException { + return importItemLines(NdjsonSupport.read(servletRequest, new TypeReference() {})); + } + + /** + * POST /items/lines. Implement this method rather than the endpoint above: the body items are read as the stream is consumed. + */ + default ResponseEntity importItemLines(final Stream itemDTO) { + return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + } + /** * GET /items/export, sending the items {@link #exportItems} returns as application/x-ndjson, one JSON document per line. */ @@ -71,7 +124,7 @@ default ResponseEntity> listItems(final String filter) { produces = {"application/x-ndjson"} ) default ResponseEntity exportItemsNdjson(@Parameter(hidden = true) final HttpServletRequest servletRequest, @Parameter(hidden = true) final HttpServletResponse servletResponse) { - return NdjsonSupport.stream(exportItems(), servletRequest, servletResponse); + return NdjsonSupport.stream(exportItems(), "application/x-ndjson", servletRequest, servletResponse); } /** diff --git a/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative-boot3/NdjsonSupport.java b/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative-boot3/NdjsonSupport.java index 09676305..e734b19e 100644 --- a/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative-boot3/NdjsonSupport.java +++ b/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative-boot3/NdjsonSupport.java @@ -21,31 +21,29 @@ import org.springframework.web.servlet.support.RequestContextUtils; /** - * Sends and reads application/x-ndjson, one JSON document per line, for the API interfaces of this package. Streamed + * Sends and reads application/x-ndjson and application/jsonl, one JSON document per line, for the API interfaces of this package. Streamed * responses are written by Spring MVC on its asynchronous executor, each item sent to the client as soon as it is produced, * and the stream the operation returns is closed once it is sent, also when sending fails. Items are serialized with the * application's ObjectMapper. */ public final class NdjsonSupport { - private static final MediaType APPLICATION_NDJSON = MediaType.parseMediaType("application/x-ndjson"); - private NdjsonSupport() { } /** - * The response streaming the items of the given one, with its status and headers. A response without items, such as the - * NOT_IMPLEMENTED of an operation that is not implemented, is sent without a body. + * The response streaming the items of the given one as the given media type, with its status and headers. A response + * without items, such as the NOT_IMPLEMENTED of an operation that is not implemented, is sent without a body. */ - public static ResponseEntity stream(final ResponseEntity> response, final HttpServletRequest servletRequest, - final HttpServletResponse servletResponse) { + public static ResponseEntity stream(final ResponseEntity> response, final String mediaType, + final HttpServletRequest servletRequest, final HttpServletResponse servletResponse) { final ResponseEntity.BodyBuilder builder = ResponseEntity.status(response.getStatusCode()).headers(response.getHeaders()); final Stream items = response.getBody(); if (items == null) { return builder.build(); } final ObjectMapper mapper = objectMapper(servletRequest); - return builder.contentType(APPLICATION_NDJSON).body(output -> write(items, mapper, output, servletResponse)); + return builder.contentType(MediaType.parseMediaType(mediaType)).body(output -> write(items, mapper, output, servletResponse)); } /** The response sending the items of the given one as a JSON array, with its status and headers. */ diff --git a/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative-boot4/EventsApi.java b/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative-boot4/EventsApi.java index 34e21974..b6f2f947 100644 --- a/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative-boot4/EventsApi.java +++ b/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative-boot4/EventsApi.java @@ -40,7 +40,7 @@ public interface EventsApi { produces = {"application/x-ndjson"} ) default ResponseEntity streamEventsNdjson(@Parameter(hidden = true) final HttpServletRequest servletRequest, @Parameter(hidden = true) final HttpServletResponse servletResponse) { - return NdjsonSupport.stream(streamEvents(), servletRequest, servletResponse); + return NdjsonSupport.stream(streamEvents(), "application/x-ndjson", servletRequest, servletResponse); } /** diff --git a/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative-boot4/ItemsApi.java b/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative-boot4/ItemsApi.java index 535bed4b..123979cc 100644 --- a/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative-boot4/ItemsApi.java +++ b/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative-boot4/ItemsApi.java @@ -45,7 +45,7 @@ public interface ItemsApi { produces = {"application/x-ndjson"} ) default ResponseEntity listItemsNdjson(@Parameter(name = "filter", required = false, schema = @Schema(description = "")) @RequestParam(name = "filter", required = false) String filter, @Parameter(hidden = true) final HttpServletRequest servletRequest, @Parameter(hidden = true) final HttpServletResponse servletResponse) { - return NdjsonSupport.stream(listItems(filter), servletRequest, servletResponse); + return NdjsonSupport.stream(listItems(filter), "application/x-ndjson", servletRequest, servletResponse); } /** @@ -55,6 +55,59 @@ default ResponseEntity> listItems(final String filter) { return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); } + /** + * GET /items/lines, sending the items {@link #listItemLines} returns as application/jsonl, one JSON document per line. + */ + @Operation( + operationId = "listItemLines", + tags = {"items"}, + responses = { + @ApiResponse(responseCode = "200", description = "One item per line, as JSON Lines", content = @Content(mediaType = "application/jsonl", schema = @Schema(implementation = ItemDTO.class))) + } + ) + @RequestMapping( + method = RequestMethod.GET, + value = "/items/lines", + produces = {"application/jsonl"} + ) + default ResponseEntity listItemLinesNdjson(@Parameter(hidden = true) final HttpServletRequest servletRequest, @Parameter(hidden = true) final HttpServletResponse servletResponse) { + return NdjsonSupport.stream(listItemLines(), "application/jsonl", servletRequest, servletResponse); + } + + /** + * GET /items/lines. Implement this method rather than the endpoint above: the items are sent as they are produced, and the stream is closed once they are sent. + */ + default ResponseEntity> listItemLines() { + return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + } + + /** + * POST /items/lines, reading the application/jsonl body, one JSON document per line, as the items {@link #importItemLines} receives. + */ + @Operation( + operationId = "importItemLines", + tags = {"items"}, + responses = { + @ApiResponse(responseCode = "200", description = "Import summary", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ImportResultDTO.class))) + } + ) + @RequestMapping( + method = RequestMethod.POST, + value = "/items/lines", + produces = {"application/json"}, + consumes = {"application/jsonl"} + ) + default ResponseEntity importItemLinesNdjson(@Parameter(hidden = true) final HttpServletRequest servletRequest) throws IOException { + return importItemLines(NdjsonSupport.read(servletRequest, new TypeReference() {})); + } + + /** + * POST /items/lines. Implement this method rather than the endpoint above: the body items are read as the stream is consumed. + */ + default ResponseEntity importItemLines(final Stream itemDTO) { + return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + } + /** * GET /items/export, sending the items {@link #exportItems} returns as application/x-ndjson, one JSON document per line. */ @@ -71,7 +124,7 @@ default ResponseEntity> listItems(final String filter) { produces = {"application/x-ndjson"} ) default ResponseEntity exportItemsNdjson(@Parameter(hidden = true) final HttpServletRequest servletRequest, @Parameter(hidden = true) final HttpServletResponse servletResponse) { - return NdjsonSupport.stream(exportItems(), servletRequest, servletResponse); + return NdjsonSupport.stream(exportItems(), "application/x-ndjson", servletRequest, servletResponse); } /** diff --git a/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative-boot4/NdjsonSupport.java b/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative-boot4/NdjsonSupport.java index 0ae8f5d3..5e1cd828 100644 --- a/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative-boot4/NdjsonSupport.java +++ b/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative-boot4/NdjsonSupport.java @@ -22,31 +22,29 @@ import org.springframework.web.servlet.support.RequestContextUtils; /** - * Sends and reads application/x-ndjson, one JSON document per line, for the API interfaces of this package. Streamed + * Sends and reads application/x-ndjson and application/jsonl, one JSON document per line, for the API interfaces of this package. Streamed * responses are written by Spring MVC on its asynchronous executor, each item sent to the client as soon as it is produced, * and the stream the operation returns is closed once it is sent, also when sending fails. Items are serialized with the * application's ObjectMapper. */ public final class NdjsonSupport { - private static final MediaType APPLICATION_NDJSON = MediaType.parseMediaType("application/x-ndjson"); - private NdjsonSupport() { } /** - * The response streaming the items of the given one, with its status and headers. A response without items, such as the - * NOT_IMPLEMENTED of an operation that is not implemented, is sent without a body. + * The response streaming the items of the given one as the given media type, with its status and headers. A response + * without items, such as the NOT_IMPLEMENTED of an operation that is not implemented, is sent without a body. */ - public static ResponseEntity stream(final ResponseEntity> response, final HttpServletRequest servletRequest, - final HttpServletResponse servletResponse) { + public static ResponseEntity stream(final ResponseEntity> response, final String mediaType, + final HttpServletRequest servletRequest, final HttpServletResponse servletResponse) { final ResponseEntity.BodyBuilder builder = ResponseEntity.status(response.getStatusCode()).headers(response.getHeaders()); final Stream items = response.getBody(); if (items == null) { return builder.build(); } final ObjectMapper mapper = objectMapper(servletRequest); - return builder.contentType(APPLICATION_NDJSON).body(output -> write(items, mapper, output, servletResponse)); + return builder.contentType(MediaType.parseMediaType(mediaType)).body(output -> write(items, mapper, output, servletResponse)); } /** The response sending the items of the given one as a JSON array, with its status and headers. */ diff --git a/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative/EventsApi.java b/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative/EventsApi.java index afb483d7..1aa2b340 100644 --- a/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative/EventsApi.java +++ b/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative/EventsApi.java @@ -40,7 +40,7 @@ public interface EventsApi { produces = {"application/x-ndjson"} ) default ResponseEntity streamEventsNdjson(@Parameter(hidden = true) final HttpServletRequest servletRequest, @Parameter(hidden = true) final HttpServletResponse servletResponse) { - return NdjsonSupport.stream(streamEvents(), servletRequest, servletResponse); + return NdjsonSupport.stream(streamEvents(), "application/x-ndjson", servletRequest, servletResponse); } /** diff --git a/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative/ItemsApi.java b/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative/ItemsApi.java index 9b9402b4..614352fe 100644 --- a/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative/ItemsApi.java +++ b/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative/ItemsApi.java @@ -45,7 +45,7 @@ public interface ItemsApi { produces = {"application/x-ndjson"} ) default ResponseEntity listItemsNdjson(@Parameter(name = "filter", required = false, schema = @Schema(description = "")) @RequestParam(name = "filter", required = false) String filter, @Parameter(hidden = true) final HttpServletRequest servletRequest, @Parameter(hidden = true) final HttpServletResponse servletResponse) { - return NdjsonSupport.stream(listItems(filter), servletRequest, servletResponse); + return NdjsonSupport.stream(listItems(filter), "application/x-ndjson", servletRequest, servletResponse); } /** @@ -55,6 +55,59 @@ default ResponseEntity> listItems(final String filter) { return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); } + /** + * GET /items/lines, sending the items {@link #listItemLines} returns as application/jsonl, one JSON document per line. + */ + @Operation( + operationId = "listItemLines", + tags = {"items"}, + responses = { + @ApiResponse(responseCode = "200", description = "One item per line, as JSON Lines", content = @Content(mediaType = "application/jsonl", schema = @Schema(implementation = ItemDTO.class))) + } + ) + @RequestMapping( + method = RequestMethod.GET, + value = "/items/lines", + produces = {"application/jsonl"} + ) + default ResponseEntity listItemLinesNdjson(@Parameter(hidden = true) final HttpServletRequest servletRequest, @Parameter(hidden = true) final HttpServletResponse servletResponse) { + return NdjsonSupport.stream(listItemLines(), "application/jsonl", servletRequest, servletResponse); + } + + /** + * GET /items/lines. Implement this method rather than the endpoint above: the items are sent as they are produced, and the stream is closed once they are sent. + */ + default ResponseEntity> listItemLines() { + return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + } + + /** + * POST /items/lines, reading the application/jsonl body, one JSON document per line, as the items {@link #importItemLines} receives. + */ + @Operation( + operationId = "importItemLines", + tags = {"items"}, + responses = { + @ApiResponse(responseCode = "200", description = "Import summary", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ImportResultDTO.class))) + } + ) + @RequestMapping( + method = RequestMethod.POST, + value = "/items/lines", + produces = {"application/json"}, + consumes = {"application/jsonl"} + ) + default ResponseEntity importItemLinesNdjson(@Parameter(hidden = true) final HttpServletRequest servletRequest) throws IOException { + return importItemLines(NdjsonSupport.read(servletRequest, new TypeReference() {})); + } + + /** + * POST /items/lines. Implement this method rather than the endpoint above: the body items are read as the stream is consumed. + */ + default ResponseEntity importItemLines(final Stream itemDTO) { + return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + } + /** * GET /items/export, sending the items {@link #exportItems} returns as application/x-ndjson, one JSON document per line. */ @@ -71,7 +124,7 @@ default ResponseEntity> listItems(final String filter) { produces = {"application/x-ndjson"} ) default ResponseEntity exportItemsNdjson(@Parameter(hidden = true) final HttpServletRequest servletRequest, @Parameter(hidden = true) final HttpServletResponse servletResponse) { - return NdjsonSupport.stream(exportItems(), servletRequest, servletResponse); + return NdjsonSupport.stream(exportItems(), "application/x-ndjson", servletRequest, servletResponse); } /** diff --git a/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative/NdjsonSupport.java b/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative/NdjsonSupport.java index aa8abfe7..1c018a09 100644 --- a/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative/NdjsonSupport.java +++ b/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative/NdjsonSupport.java @@ -21,31 +21,29 @@ import org.springframework.web.servlet.support.RequestContextUtils; /** - * Sends and reads application/x-ndjson, one JSON document per line, for the API interfaces of this package. Streamed + * Sends and reads application/x-ndjson and application/jsonl, one JSON document per line, for the API interfaces of this package. Streamed * responses are written by Spring MVC on its asynchronous executor, each item sent to the client as soon as it is produced, * and the stream the operation returns is closed once it is sent, also when sending fails. Items are serialized with the * application's ObjectMapper. */ public final class NdjsonSupport { - private static final MediaType APPLICATION_NDJSON = MediaType.parseMediaType("application/x-ndjson"); - private NdjsonSupport() { } /** - * The response streaming the items of the given one, with its status and headers. A response without items, such as the - * NOT_IMPLEMENTED of an operation that is not implemented, is sent without a body. + * The response streaming the items of the given one as the given media type, with its status and headers. A response + * without items, such as the NOT_IMPLEMENTED of an operation that is not implemented, is sent without a body. */ - public static ResponseEntity stream(final ResponseEntity> response, final HttpServletRequest servletRequest, - final HttpServletResponse servletResponse) { + public static ResponseEntity stream(final ResponseEntity> response, final String mediaType, + final HttpServletRequest servletRequest, final HttpServletResponse servletResponse) { final ResponseEntity.BodyBuilder builder = ResponseEntity.status(response.getStatusCode()).headers(response.getHeaders()); final Stream items = response.getBody(); if (items == null) { return builder.build(); } final ObjectMapper mapper = objectMapper(servletRequest); - return builder.contentType(APPLICATION_NDJSON).body(output -> write(items, mapper, output, servletResponse)); + return builder.contentType(MediaType.parseMediaType(mediaType)).body(output -> write(items, mapper, output, servletResponse)); } /** The response sending the items of the given one as a JSON array, with its status and headers. */ diff --git a/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/reactive/ItemsApi.java b/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/reactive/ItemsApi.java index ca926590..040a8d5b 100644 --- a/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/reactive/ItemsApi.java +++ b/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/reactive/ItemsApi.java @@ -50,6 +50,50 @@ default ResponseEntity> listItems(@Parameter(name = "filter", requ return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); } + /** + * GET /items/lines + * @return One item per line, as JSON Lines; (status code 200) + * @throws WebClientResponseException if an error occurs while attempting to invoke the API + */ + @Operation( + operationId = "listItemLines", + tags = {"items"}, + responses = { + @ApiResponse(responseCode = "200", description = "One item per line, as JSON Lines", content = @Content(mediaType = "application/jsonl", schema = @Schema(implementation = ItemDTO.class))) + } + ) + @RequestMapping( + method = RequestMethod.GET, + value = "/items/lines", + produces = {"application/jsonl"} + ) + default ResponseEntity> listItemLines(@ApiIgnore final ServerWebExchange exchange) { + return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + } + + /** + * POST /items/lines + * @param itemDTO (required) + * @return Import summary; (status code 200) + * @throws WebClientResponseException if an error occurs while attempting to invoke the API + */ + @Operation( + operationId = "importItemLines", + tags = {"items"}, + responses = { + @ApiResponse(responseCode = "200", description = "Import summary", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ImportResultDTO.class))) + } + ) + @RequestMapping( + method = RequestMethod.POST, + value = "/items/lines", + produces = {"application/json"}, + consumes = {"application/jsonl"} + ) + default ResponseEntity> importItemLines(@Parameter(name = "itemDTO", description = "", required = true, schema = @Schema(description = "")) @Valid @RequestBody Flux itemDTO, @ApiIgnore final ServerWebExchange exchange) { + return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + } + /** * GET /items/export * @return The items, as a JSON array or one per line; (status code 200) diff --git a/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/webclient/ItemsApi.java b/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/webclient/ItemsApi.java index 227a3810..a315de93 100644 --- a/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/webclient/ItemsApi.java +++ b/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/webclient/ItemsApi.java @@ -114,6 +114,91 @@ public Mono>> listItemsWithHttpInfo(String filter) return listItemsRequestCreation(filter).toEntityFlux(localVarReturnType); } + /** + * GET /items/lines: "" + * @return One item per line, as JSON Lines; (status code 200) + * @throws WebClientResponseException if an error occurs while attempting to invoke the API + */ + private ResponseSpec listItemLinesRequestCreation() throws WebClientResponseException { + Object postBody = null; + final Map pathParams = new HashMap(); + + final MultiValueMap queryParams = new LinkedMultiValueMap(); + final HttpHeaders headerParams = new HttpHeaders(); + final MultiValueMap cookieParams = new LinkedMultiValueMap(); + final MultiValueMap formParams = new LinkedMultiValueMap(); + final String[] localVarAccepts = {"application/jsonl"}; + final List localVarAccept = apiWebClient.selectHeaderAccept(localVarAccepts); + final String[] localVarContentTypes = {}; + final MediaType localVarContentType = apiWebClient.selectHeaderContentType(localVarContentTypes); + + String[] localVarAuthNames = new String[] {}; + + ParameterizedTypeReference localVarReturnType = new ParameterizedTypeReference() {}; + return apiWebClient.invokeAPI(basePath,"/items/lines", HttpMethod.GET, pathParams, queryParams, postBody, headerParams, cookieParams, formParams, localVarAccept, localVarContentType, localVarAuthNames, localVarReturnType); + + } + + /** + * GET /items/lines + * @return One item per line, as JSON Lines; (status code 200) + * @throws WebClientResponseException if an error occurs while attempting to invoke the API + */ + public Flux listItemLines() throws WebClientResponseException { + ParameterizedTypeReference localVarReturnType = new ParameterizedTypeReference() {}; + return listItemLinesRequestCreation().bodyToFlux(localVarReturnType); + } + + public Mono>> listItemLinesWithHttpInfo() throws WebClientResponseException { + ParameterizedTypeReference localVarReturnType = new ParameterizedTypeReference() {}; + return listItemLinesRequestCreation().toEntityFlux(localVarReturnType); + } + + /** + * POST /items/lines: "" + * @param itemDTO (required) + * @return Import summary; (status code 200) + * @throws WebClientResponseException if an error occurs while attempting to invoke the API + */ + private ResponseSpec importItemLinesRequestCreation(Flux itemDTO) throws WebClientResponseException { + Object postBody = itemDTO; + if (itemDTO == null) { + throw new WebClientResponseException("Missing the required parameter ''itemDTO'' when calling importItemLines", HttpStatus.BAD_REQUEST.value(), HttpStatus.BAD_REQUEST.getReasonPhrase(), null, null, null); + } + final Map pathParams = new HashMap(); + + final MultiValueMap queryParams = new LinkedMultiValueMap(); + final HttpHeaders headerParams = new HttpHeaders(); + final MultiValueMap cookieParams = new LinkedMultiValueMap(); + final MultiValueMap formParams = new LinkedMultiValueMap(); + final String[] localVarAccepts = {"application/json"}; + final List localVarAccept = apiWebClient.selectHeaderAccept(localVarAccepts); + final String[] localVarContentTypes = {"application/jsonl"}; + final MediaType localVarContentType = apiWebClient.selectHeaderContentType(localVarContentTypes); + + String[] localVarAuthNames = new String[] {}; + + ParameterizedTypeReference localVarReturnType = new ParameterizedTypeReference() {}; + return apiWebClient.invokeAPI(basePath,"/items/lines", HttpMethod.POST, pathParams, queryParams, postBody, headerParams, cookieParams, formParams, localVarAccept, localVarContentType, localVarAuthNames, localVarReturnType); + + } + + /** + * POST /items/lines + * @param itemDTO (required) + * @return Import summary; (status code 200) + * @throws WebClientResponseException if an error occurs while attempting to invoke the API + */ + public Mono importItemLines(Flux itemDTO) throws WebClientResponseException { + ParameterizedTypeReference localVarReturnType = new ParameterizedTypeReference() {}; + return importItemLinesRequestCreation(itemDTO).bodyToMono(localVarReturnType); + } + + public Mono> importItemLinesWithHttpInfo(Flux itemDTO) throws WebClientResponseException { + ParameterizedTypeReference localVarReturnType = new ParameterizedTypeReference() {}; + return importItemLinesRequestCreation(itemDTO).toEntity(localVarReturnType); + } + /** * GET /items/export: "" * @return The items, as a JSON array or one per line; (status code 200) diff --git a/multiapi-engine/src/test/resources/openapigenerator/testWebClientApiGeneration/assets/springboot4/client/ApiWebClient.java b/multiapi-engine/src/test/resources/openapigenerator/testWebClientApiGeneration/assets/springboot4/client/ApiWebClient.java index 69a1b0e8..8f877b07 100644 --- a/multiapi-engine/src/test/resources/openapigenerator/testWebClientApiGeneration/assets/springboot4/client/ApiWebClient.java +++ b/multiapi-engine/src/test/resources/openapigenerator/testWebClientApiGeneration/assets/springboot4/client/ApiWebClient.java @@ -6,6 +6,7 @@ import java.time.OffsetDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Date; @@ -137,13 +138,16 @@ private static JsonMapper createDefaultObjectMapper(final DateFormat defaultDate } private static WebClient buildWebClient(final JsonMapper mapper) { - // application/x-ndjson is read and written with the same mapper, one item per line. + // application/x-ndjson and application/jsonl are read and written with the same mapper, one item per line. final MediaType ndjson = new MediaType("application", "x-ndjson"); + final MediaType jsonLines = new MediaType("application", "jsonl"); ExchangeStrategies strategies = ExchangeStrategies .builder() .codecs(clientDefaultCodecsConfigurer -> { - clientDefaultCodecsConfigurer.defaultCodecs().jacksonJsonEncoder(new JacksonJsonEncoder(mapper, MediaType.APPLICATION_JSON, ndjson)); - clientDefaultCodecsConfigurer.defaultCodecs().jacksonJsonDecoder(new JacksonJsonDecoder(mapper, MediaType.APPLICATION_JSON, ndjson)); + final JacksonJsonEncoder encoder = new JacksonJsonEncoder(mapper, MediaType.APPLICATION_JSON, ndjson, jsonLines); + encoder.setStreamingMediaTypes(Arrays.asList(ndjson, jsonLines)); + clientDefaultCodecsConfigurer.defaultCodecs().jacksonJsonEncoder(encoder); + clientDefaultCodecsConfigurer.defaultCodecs().jacksonJsonDecoder(new JacksonJsonDecoder(mapper, MediaType.APPLICATION_JSON, ndjson, jsonLines)); }).build(); WebClient.Builder webClientBuilder = WebClient.builder().exchangeStrategies(strategies); return webClientBuilder.build(); diff --git a/scs-multiapi-gradle-plugin/build.gradle b/scs-multiapi-gradle-plugin/build.gradle index 6ee459d5..d2d73075 100644 --- a/scs-multiapi-gradle-plugin/build.gradle +++ b/scs-multiapi-gradle-plugin/build.gradle @@ -21,7 +21,7 @@ repositories { } group = 'com.sngular' -version = '8.3.0' +version = '8.3.1' def SCSMultiApiPluginGroupId = group def SCSMultiApiPluginVersion = version @@ -31,7 +31,7 @@ dependencies { shadow localGroovy() shadow gradleApi() - implementation 'com.sngular:multiapi-engine:8.3.0' + implementation 'com.sngular:multiapi-engine:8.3.1' testImplementation 'org.assertj:assertj-core:3.24.2' testImplementation 'com.puppycrawl.tools:checkstyle:10.12.3' testImplementation 'org.junit.platform:junit-platform-launcher:1.9.2' diff --git a/scs-multiapi-maven-plugin/pom.xml b/scs-multiapi-maven-plugin/pom.xml index f97b5455..8cb25044 100644 --- a/scs-multiapi-maven-plugin/pom.xml +++ b/scs-multiapi-maven-plugin/pom.xml @@ -4,7 +4,7 @@ com.sngular scs-multiapi-maven-plugin - 8.3.0 + 8.3.1 maven-plugin AsyncApi - OpenApi Code Generator Maven Plugin @@ -272,7 +272,7 @@ com.sngular multiapi-engine - 8.3.0 + 8.3.1 org.apache.maven