diff --git a/.github/workflows/gradle-pr.yml b/.github/workflows/gradle-pr.yml index 84913fa3..99e6e816 100644 --- a/.github/workflows/gradle-pr.yml +++ b/.github/workflows/gradle-pr.yml @@ -29,7 +29,7 @@ jobs: continue-on-error: true - name: Warn about version specification if: steps.plugin-version-check.outcome != 'success' - uses: thollander/actions-comment-pull-request@v1 + uses: thollander/actions-comment-pull-request@v2 with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} message: Project version has not been updated in build.gradle. Please, update your version using https://semver.org specifications diff --git a/.github/workflows/maven-pr.yml b/.github/workflows/maven-pr.yml index 8fa70ffb..ebeff5da 100644 --- a/.github/workflows/maven-pr.yml +++ b/.github/workflows/maven-pr.yml @@ -28,7 +28,7 @@ jobs: continue-on-error: true - name: Warn about version specification if: ${{ steps.engine_version_check.outcome != 'success' || steps.maven_plugin_version_check.outcome != 'success' }} - uses: thollander/actions-comment-pull-request@v1 + uses: thollander/actions-comment-pull-request@v2 with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} message: Project version has not been updated in pom.xml. Please, update your version using https://semver.org specifications diff --git a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/ApiTool.java b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/ApiTool.java index c271dc9e..f9aa1508 100644 --- a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/ApiTool.java +++ b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/ApiTool.java @@ -21,11 +21,13 @@ import com.sngular.api.generator.plugin.asyncapi.util.FactoryTypeEnum; import com.sngular.api.generator.plugin.common.files.FileLocation; import com.sngular.api.generator.plugin.common.model.TypeConstants; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.IteratorUtils; import org.apache.commons.collections4.Transformer; import org.apache.commons.lang3.StringUtils; +@Slf4j public final class ApiTool { public static final String FORMAT = "format"; @@ -224,19 +226,27 @@ public static String getType(final JsonNode schema) { return ""; } final JsonNode typeNode = getNode(schema, "type"); - if (typeNode.isArray()) { - // OpenAPI 3.1 / JSON Schema 2020-12: "type" may be an array (e.g. ["string", "null"]). - // Resolve to the first non-"null" entry; "null" only marks the type as nullable. - String resolvedType = ""; - for (final JsonNode element : typeNode) { - if (!TypeConstants.NULL.equalsIgnoreCase(element.asText())) { - resolvedType = element.asText(); - break; - } + return typeNode.isArray() ? getArrayType(typeNode) : StringUtils.defaultIfEmpty(typeNode.textValue(), ""); + } + + private static String getArrayType(final JsonNode typeNode) { + // OpenAPI 3.1 / JSON Schema 2020-12: "type" may be an array (e.g. ["string", "null"]). + // "null" only marks the type as nullable; keep the concrete (non-"null") types. + final List concreteTypes = new ArrayList<>(); + for (final JsonNode element : typeNode) { + if (!TypeConstants.NULL.equalsIgnoreCase(element.asText())) { + concreteTypes.add(element.asText()); } - return resolvedType; } - return StringUtils.defaultIfEmpty(typeNode.textValue(), ""); + if (concreteTypes.isEmpty()) { + // Only "null" (or an empty array): no concrete type to generate, fall back to object. + return TypeConstants.OBJECT; + } + if (concreteTypes.size() > 1) { + log.warn("Schema declares a multi-type union {}; only '{}' is generated, the remaining types are ignored.", + concreteTypes, concreteTypes.get(0)); + } + return concreteTypes.get(0); } public static boolean hasItems(final JsonNode schema) { diff --git a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/openapi/OpenApiGenerator.java b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/openapi/OpenApiGenerator.java index 770ad158..6add6dd0 100644 --- a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/openapi/OpenApiGenerator.java +++ b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/openapi/OpenApiGenerator.java @@ -98,6 +98,7 @@ private void processPackage(final String apiPackage) { private void processFile(final SpecFile specFile) { final JsonNode openAPI = OpenApiUtil.getPojoFromSpecFile(baseDir, specFile); + OpenApiUtil.mergeWebhooksIntoPaths(openAPI); OpenApiUtil.solvePathRefs(openAPI, baseDir.resolve(specFile.getFilePath()).getParent().toUri()); final String clientPackage = specFile.getClientPackage(); diff --git a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/openapi/utils/OpenApiUtil.java b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/openapi/utils/OpenApiUtil.java index 6c62c1d4..5dbc47fe 100644 --- a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/openapi/utils/OpenApiUtil.java +++ b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/openapi/utils/OpenApiUtil.java @@ -35,6 +35,8 @@ public class OpenApiUtil { public static final String PATHS = "paths"; + public static final String WEBHOOKS = "webhooks"; + static final Set REST_VERB_SET = Set.of("get", "post", "delete", "patch", "put"); private OpenApiUtil() { @@ -92,6 +94,52 @@ public static JsonNode getPojoFromSpecFile(final Path baseDir, final SpecFile sp return SchemaUtil.getPojoFromRef(baseDir.toUri(), specFile.getFilePath()); } + /** + * Merges the OpenAPI 3.1 top-level {@code webhooks} object into {@code paths} so the existing + * path pipeline generates a handler interface and the request/response payload models for each + * webhook. Each webhook is a Path Item Object keyed by name; it is added under a {@code "/"}- + * prefixed key (webhooks have no URL) so the by-url grouping treats the webhook name as the + * endpoint. Existing {@code paths} entries take precedence and are never overwritten. + * + * @param openApi the parsed root contract; its {@code paths} node is created/extended in place. + */ + public static void mergeWebhooksIntoPaths(final JsonNode openApi) { + final JsonNode webhooks = openApi.get(WEBHOOKS); + if (webhooks instanceof ObjectNode && openApi instanceof ObjectNode) { + final ObjectNode root = (ObjectNode) openApi; + final ObjectNode paths = root.has(PATHS) && root.get(PATHS).isObject() + ? (ObjectNode) root.get(PATHS) + : root.putObject(PATHS); + webhooks.fields().forEachRemaining(webhook -> { + final String webhookName = webhook.getKey(); + final String pathKey = webhookName.startsWith("/") ? webhookName : "/" + webhookName; + // A leading-slash-only key would break the by-url grouping (pathUrl.split("/")[1]). + if (StringUtils.isNotBlank(StringUtils.strip(webhookName, "/")) && !paths.has(pathKey)) { + defaultOperationTags(webhook.getValue(), StringUtils.strip(webhookName, "/")); + paths.set(pathKey, webhook.getValue()); + } + }); + } + } + + /** + * Ensures every operation of a webhook-derived Path Item carries a {@code tags} entry. Webhook + * operations normally omit {@code tags}, but the path pipeline requires one; a missing/empty + * {@code tags} is defaulted to the webhook name so generation works in both grouping modes. + */ + private static void defaultOperationTags(final JsonNode pathItem, final String defaultTag) { + if (pathItem instanceof ObjectNode) { + pathItem.fields().forEachRemaining(field -> { + if (REST_VERB_SET.contains(field.getKey()) && field.getValue() instanceof ObjectNode) { + final ObjectNode operation = (ObjectNode) field.getValue(); + if (!ApiTool.hasNode(operation, "tags") || !operation.get("tags").isArray() || operation.get("tags").isEmpty()) { + operation.putArray("tags").add(defaultTag); + } + } + }); + } + } + /** * Dereferences Path Item Objects that are declared as a {@code $ref} to another file (modular * contracts). These references are otherwise never resolved, so the affected paths silently 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 6299c6f9..5403e541 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 @@ -147,12 +147,33 @@ public final class OpenApiGeneratorFixtures { .clientPackage("com.sngular.multifileplugin.externalref.client").modelNamePrefix("Api") .modelNameSuffix("DTO").build()); + static final List TEST_OPEN_API_31_UNION = List + .of(SpecFile.builder().filePath("openapigenerator/testOpenApi31Union/api-test.yml") + .apiPackage("com.sngular.multifileplugin.openapi31union") + .modelPackage("com.sngular.multifileplugin.openapi31union.model") + .clientPackage("com.sngular.multifileplugin.openapi31union.client") + .modelNameSuffix("DTO").build()); + + static final List TEST_WEBHOOK_PATH_COLLISION = List + .of(SpecFile.builder().filePath("openapigenerator/testWebhookPathCollision/api-test.yml") + .apiPackage("com.sngular.multifileplugin.webhookpathcollision") + .modelPackage("com.sngular.multifileplugin.webhookpathcollision.model") + .clientPackage("com.sngular.multifileplugin.webhookpathcollision.client") + .modelNameSuffix("DTO").build()); + + static final List TEST_WEBHOOKS = List + .of(SpecFile.builder().filePath("openapigenerator/testWebhooks/api-test.yml") + .apiPackage("com.sngular.multifileplugin.webhooks") + .modelPackage("com.sngular.multifileplugin.webhooks.model") + .clientPackage("com.sngular.multifileplugin.webhooks.client") + .modelNameSuffix("DTO").build()); + static final List TEST_OPEN_API_31_TYPES = List .of(SpecFile.builder().filePath("openapigenerator/testOpenApi31Types/api-test.yml") .apiPackage("com.sngular.multifileplugin.openapi31types") .modelPackage("com.sngular.multifileplugin.openapi31types.model") .clientPackage("com.sngular.multifileplugin.openapi31types.client") - .modelNameSuffix("DTO").build()); + .modelNameSuffix("DTO").build()); static final List TEST_EXTERNAL_PATH_ITEM_REF_GENERATION = List .of(SpecFile.builder().filePath("openapigenerator/testExternalPathItemRefsGeneration/api-test.yml") @@ -777,6 +798,60 @@ static Function validateExternalRefGeneration() { DEFAULT_MODEL_API, expectedExceptionFiles, DEFAULT_EXCEPTION_API); } + static Function validateOpenApi31Union() { + + final String DEFAULT_TARGET_API = "generated/com/sngular/multifileplugin/openapi31union"; + + final String DEFAULT_MODEL_API = "generated/com/sngular/multifileplugin/openapi31union/model"; + + final String COMMON_PATH = "openapigenerator/testOpenApi31Union/"; + + final String ASSETS_PATH = COMMON_PATH + "assets/"; + + final List expectedTestApiFile = List.of(ASSETS_PATH + "ItemApi.java"); + + final List expectedTestApiModelFiles = List.of(ASSETS_PATH + "ItemDTO.java"); + + return path -> commonTest(path, expectedTestApiFile, expectedTestApiModelFiles, DEFAULT_TARGET_API, + DEFAULT_MODEL_API, Collections.emptyList(), null); + } + + static Function validateWebhookPathCollision() { + + final String DEFAULT_TARGET_API = "generated/com/sngular/multifileplugin/webhookpathcollision"; + + final String DEFAULT_MODEL_API = "generated/com/sngular/multifileplugin/webhookpathcollision/model"; + + final String COMMON_PATH = "openapigenerator/testWebhookPathCollision/"; + + final String ASSETS_PATH = COMMON_PATH + "assets/"; + + final List expectedTestApiFile = List.of(ASSETS_PATH + "NewPetApi.java"); + + final List expectedTestApiModelFiles = List.of(ASSETS_PATH + "PetDTO.java"); + + return path -> commonTest(path, expectedTestApiFile, expectedTestApiModelFiles, DEFAULT_TARGET_API, + DEFAULT_MODEL_API, Collections.emptyList(), null); + } + + static Function validateWebhooks() { + + final String DEFAULT_TARGET_API = "generated/com/sngular/multifileplugin/webhooks"; + + final String DEFAULT_MODEL_API = "generated/com/sngular/multifileplugin/webhooks/model"; + + final String COMMON_PATH = "openapigenerator/testWebhooks/"; + + final String ASSETS_PATH = COMMON_PATH + "assets/"; + + final List expectedTestApiFile = List.of(ASSETS_PATH + "NewPetApi.java"); + + final List expectedTestApiModelFiles = List.of(ASSETS_PATH + "PetDTO.java"); + + return path -> commonTest(path, expectedTestApiFile, expectedTestApiModelFiles, DEFAULT_TARGET_API, + DEFAULT_MODEL_API, Collections.emptyList(), null); + } + static Function validateOpenApi31Types() { final String DEFAULT_TARGET_API = "generated/com/sngular/multifileplugin/openapi31types"; @@ -793,25 +868,25 @@ static Function validateOpenApi31Types() { return path -> commonTest(path, expectedTestApiFile, expectedTestApiModelFiles, DEFAULT_TARGET_API, DEFAULT_MODEL_API, Collections.emptyList(), null); - } - - static Function validateExternalPathItemRefGeneration() { + } - final String DEFAULT_TARGET_API = "generated/com/sngular/multifileplugin/externalpathitemref"; + static Function validateExternalPathItemRefGeneration() { - final String DEFAULT_MODEL_API = "generated/com/sngular/multifileplugin/externalpathitemref/model"; + final String DEFAULT_TARGET_API = "generated/com/sngular/multifileplugin/externalpathitemref"; - final String COMMON_PATH = "openapigenerator/testExternalPathItemRefsGeneration/"; + final String DEFAULT_MODEL_API = "generated/com/sngular/multifileplugin/externalpathitemref/model"; - final String ASSETS_PATH = COMMON_PATH + "assets/"; + final String COMMON_PATH = "openapigenerator/testExternalPathItemRefsGeneration/"; - final List expectedTestApiFile = List.of(ASSETS_PATH + "DashboardApi.java"); + final String ASSETS_PATH = COMMON_PATH + "assets/"; - final List expectedTestApiModelFiles = List.of(ASSETS_PATH + "DashboardDTO.java"); + final List expectedTestApiFile = List.of(ASSETS_PATH + "DashboardApi.java"); - return path -> commonTest(path, expectedTestApiFile, expectedTestApiModelFiles, DEFAULT_TARGET_API, - DEFAULT_MODEL_API, Collections.emptyList(), null); - } + final List expectedTestApiModelFiles = List.of(ASSETS_PATH + "DashboardDTO.java"); + + return path -> commonTest(path, expectedTestApiFile, expectedTestApiModelFiles, DEFAULT_TARGET_API, + DEFAULT_MODEL_API, Collections.emptyList(), null); + } static Function validateAnyOfInResponse() { 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 9ff2db7c..72588900 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 @@ -85,6 +85,12 @@ static Stream fileSpecToProcess() { OpenApiGeneratorFixtures.validateExternalRefGeneration()), Arguments.of("testOpenApi31Types", OpenApiGeneratorFixtures.TEST_OPEN_API_31_TYPES, OpenApiGeneratorFixtures.validateOpenApi31Types()), + Arguments.of("testWebhooks", OpenApiGeneratorFixtures.TEST_WEBHOOKS, + OpenApiGeneratorFixtures.validateWebhooks()), + Arguments.of("testOpenApi31Union", OpenApiGeneratorFixtures.TEST_OPEN_API_31_UNION, + OpenApiGeneratorFixtures.validateOpenApi31Union()), + Arguments.of("testWebhookPathCollision", OpenApiGeneratorFixtures.TEST_WEBHOOK_PATH_COLLISION, + OpenApiGeneratorFixtures.validateWebhookPathCollision()), Arguments.of("testExternalPathItemRefsGeneration", OpenApiGeneratorFixtures.TEST_EXTERNAL_PATH_ITEM_REF_GENERATION, OpenApiGeneratorFixtures.validateExternalPathItemRefGeneration()), Arguments.of("testAnyOfInResponse", OpenApiGeneratorFixtures.TEST_ANY_OF_IN_RESPONSE, diff --git a/multiapi-engine/src/test/resources/openapigenerator/testOpenApi31Union/api-test.yml b/multiapi-engine/src/test/resources/openapigenerator/testOpenApi31Union/api-test.yml new file mode 100644 index 00000000..fac249e2 --- /dev/null +++ b/multiapi-engine/src/test/resources/openapigenerator/testOpenApi31Union/api-test.yml @@ -0,0 +1,40 @@ +--- +# Edge-case regression for OpenAPI 3.1 array-valued `type` (issue #375): +# a genuine multi-type union (no "null") and a "null"-only type. +# The generator picks the first concrete type for a union (logging a warning) and +# falls back to `object` when the array has no concrete type. +openapi: "3.1.0" +info: + version: 1.0.0 + title: Union types API (OpenAPI 3.1) + license: + name: MIT +servers: + - url: http://localhost:8080/v1 +tags: + - name: item +paths: + /item: + get: + summary: Get the item + operationId: getItem + tags: + - item + responses: + '200': + description: The requested item + content: + application/json: + schema: + $ref: "#/components/schemas/Item" +components: + schemas: + Item: + type: object + properties: + id: + type: string + code: + type: ["string", "integer"] + note: + type: ["null"] diff --git a/multiapi-engine/src/test/resources/openapigenerator/testOpenApi31Union/assets/ItemApi.java b/multiapi-engine/src/test/resources/openapigenerator/testOpenApi31Union/assets/ItemApi.java new file mode 100644 index 00000000..797d58ca --- /dev/null +++ b/multiapi-engine/src/test/resources/openapigenerator/testOpenApi31Union/assets/ItemApi.java @@ -0,0 +1,46 @@ +package com.sngular.multifileplugin.openapi31union; + +import java.util.Optional; +import java.util.List; +import java.util.Map; +import javax.validation.Valid; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import org.springframework.http.MediaType; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.context.request.NativeWebRequest; + +import com.sngular.multifileplugin.openapi31union.model.ItemDTO; + +public interface ItemApi { + + /** + * GET /item: Get the item + * @return The requested item; (status code 200) + */ + + @Operation( + operationId = "getItem", + summary = "Get the item", + tags = {"item"}, + responses = { + @ApiResponse(responseCode = "200", description = "The requested item", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ItemDTO.class))) + } + ) + @RequestMapping( + method = RequestMethod.GET, + value = "/item", + produces = {"application/json"} + ) + + default ResponseEntity getItem() { + return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + } + +} diff --git a/multiapi-engine/src/test/resources/openapigenerator/testOpenApi31Union/assets/ItemDTO.java b/multiapi-engine/src/test/resources/openapigenerator/testOpenApi31Union/assets/ItemDTO.java new file mode 100644 index 00000000..50d37fac --- /dev/null +++ b/multiapi-engine/src/test/resources/openapigenerator/testOpenApi31Union/assets/ItemDTO.java @@ -0,0 +1,112 @@ +package com.sngular.multifileplugin.openapi31union.model; + +import java.util.Objects; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; + +@JsonDeserialize(builder = ItemDTO.ItemDTOBuilder.class) +public class ItemDTO { + + @JsonProperty(value ="id") + private String id; + @JsonProperty(value ="code") + private String code; + @JsonProperty(value ="note") + private Object note; + + private ItemDTO(ItemDTOBuilder builder) { + this.id = builder.id; + this.code = builder.code; + this.note = builder.note; + + } + + public static ItemDTO.ItemDTOBuilder builder() { + return new ItemDTO.ItemDTOBuilder(); + } + + @JsonPOJOBuilder(buildMethodName = "build", withPrefix = "") + public static class ItemDTOBuilder { + + private String id; + private String code; + private Object note; + + public ItemDTO.ItemDTOBuilder id(String id) { + this.id = id; + return this; + } + + public ItemDTO.ItemDTOBuilder code(String code) { + this.code = code; + return this; + } + + public ItemDTO.ItemDTOBuilder note(Object note) { + this.note = note; + return this; + } + + public ItemDTO build() { + ItemDTO itemDTO = new ItemDTO(this); + return itemDTO; + } + } + + @Schema(name = "id", required = false) + public String getId() { + return id; + } + public void setId(String id) { + this.id = id; + } + + @Schema(name = "code", required = false) + public String getCode() { + return code; + } + public void setCode(String code) { + this.code = code; + } + + @Schema(name = "note", required = false) + public Object getNote() { + return note; + } + public void setNote(Object note) { + this.note = note; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ItemDTO itemDTO = (ItemDTO) o; + return Objects.equals(this.id, itemDTO.id) && Objects.equals(this.code, itemDTO.code) && Objects.equals(this.note, itemDTO.note); + } + + @Override + public int hashCode() { + return Objects.hash(id, code, note); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("ItemDTO{"); + sb.append(" id:").append(id).append(","); + sb.append(" code:").append(code).append(","); + sb.append(" note:").append(note); + sb.append("}"); + return sb.toString(); + } + + +} diff --git a/multiapi-engine/src/test/resources/openapigenerator/testWebhookPathCollision/api-test.yml b/multiapi-engine/src/test/resources/openapigenerator/testWebhookPathCollision/api-test.yml new file mode 100644 index 00000000..c6301065 --- /dev/null +++ b/multiapi-engine/src/test/resources/openapigenerator/testWebhookPathCollision/api-test.yml @@ -0,0 +1,51 @@ +--- +# Edge-case regression for OpenAPI 3.1 webhooks (issue #375): a webhook whose name +# collides with an existing path. Existing `paths` entries must take precedence, so +# the `/newPet` GET is generated and the colliding webhook is dropped. +openapi: "3.1.0" +info: + version: 1.0.0 + title: Webhook/path collision API (OpenAPI 3.1) + license: + name: MIT +servers: + - url: http://localhost:8080/v1 +tags: + - name: pet +paths: + /newPet: + get: + summary: Get the latest new pet + operationId: getNewPet + tags: + - pet + responses: + '200': + description: The latest new pet + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" +webhooks: + newPet: + post: + summary: New pet notification (must be ignored, collides with /newPet) + operationId: newPetWebhook + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + responses: + '200': + description: Notification acknowledged +components: + schemas: + Pet: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string diff --git a/multiapi-engine/src/test/resources/openapigenerator/testWebhookPathCollision/assets/NewPetApi.java b/multiapi-engine/src/test/resources/openapigenerator/testWebhookPathCollision/assets/NewPetApi.java new file mode 100644 index 00000000..452f37e3 --- /dev/null +++ b/multiapi-engine/src/test/resources/openapigenerator/testWebhookPathCollision/assets/NewPetApi.java @@ -0,0 +1,46 @@ +package com.sngular.multifileplugin.webhookpathcollision; + +import java.util.Optional; +import java.util.List; +import java.util.Map; +import javax.validation.Valid; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import org.springframework.http.MediaType; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.context.request.NativeWebRequest; + +import com.sngular.multifileplugin.webhookpathcollision.model.PetDTO; + +public interface NewPetApi { + + /** + * GET /newPet: Get the latest new pet + * @return The latest new pet; (status code 200) + */ + + @Operation( + operationId = "getNewPet", + summary = "Get the latest new pet", + tags = {"pet"}, + responses = { + @ApiResponse(responseCode = "200", description = "The latest new pet", content = @Content(mediaType = "application/json", schema = @Schema(implementation = PetDTO.class))) + } + ) + @RequestMapping( + method = RequestMethod.GET, + value = "/newPet", + produces = {"application/json"} + ) + + default ResponseEntity getNewPet() { + return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + } + +} diff --git a/multiapi-engine/src/test/resources/openapigenerator/testWebhookPathCollision/assets/PetDTO.java b/multiapi-engine/src/test/resources/openapigenerator/testWebhookPathCollision/assets/PetDTO.java new file mode 100644 index 00000000..2eabc332 --- /dev/null +++ b/multiapi-engine/src/test/resources/openapigenerator/testWebhookPathCollision/assets/PetDTO.java @@ -0,0 +1,94 @@ +package com.sngular.multifileplugin.webhookpathcollision.model; + +import java.util.Objects; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; + +@JsonDeserialize(builder = PetDTO.PetDTOBuilder.class) +public class PetDTO { + + @JsonProperty(value ="name") + private String name; + @JsonProperty(value ="id") + private Long id; + + private PetDTO(PetDTOBuilder builder) { + this.name = builder.name; + this.id = builder.id; + + } + + public static PetDTO.PetDTOBuilder builder() { + return new PetDTO.PetDTOBuilder(); + } + + @JsonPOJOBuilder(buildMethodName = "build", withPrefix = "") + public static class PetDTOBuilder { + + private String name; + private Long id; + + public PetDTO.PetDTOBuilder name(String name) { + this.name = name; + return this; + } + + public PetDTO.PetDTOBuilder id(Long id) { + this.id = id; + return this; + } + + public PetDTO build() { + PetDTO petDTO = new PetDTO(this); + return petDTO; + } + } + + @Schema(name = "name", required = false) + public String getName() { + return name; + } + public void setName(String name) { + this.name = name; + } + + @Schema(name = "id", required = false) + public Long getId() { + return id; + } + public void setId(Long id) { + this.id = id; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PetDTO petDTO = (PetDTO) o; + return Objects.equals(this.name, petDTO.name) && Objects.equals(this.id, petDTO.id); + } + + @Override + public int hashCode() { + return Objects.hash(name, id); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("PetDTO{"); + sb.append(" name:").append(name).append(","); + sb.append(" id:").append(id); + sb.append("}"); + return sb.toString(); + } + + +} diff --git a/multiapi-engine/src/test/resources/openapigenerator/testWebhooks/api-test.yml b/multiapi-engine/src/test/resources/openapigenerator/testWebhooks/api-test.yml new file mode 100644 index 00000000..dfdf1ea5 --- /dev/null +++ b/multiapi-engine/src/test/resources/openapigenerator/testWebhooks/api-test.yml @@ -0,0 +1,40 @@ +--- +# Regression for OpenAPI 3.1 top-level `webhooks` support (issue #375). +# A webhooks-only contract (no `paths`): each webhook is a Path Item Object keyed by +# name. The generator merges these into the path pipeline so a handler interface and the +# request payload model are generated. +openapi: "3.1.0" +info: + version: 1.0.0 + title: Webhooks API (OpenAPI 3.1) + license: + name: MIT +servers: + - url: http://localhost:8080/v1 +tags: + - name: webhook +webhooks: + newPet: + post: + summary: New pet notification + operationId: newPetWebhook + # No `tags`: webhook operations normally omit them. The merge defaults the tag + # to the webhook name so generation works in the default (by-url) grouping mode. + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + responses: + '200': + description: Notification acknowledged +components: + schemas: + Pet: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string diff --git a/multiapi-engine/src/test/resources/openapigenerator/testWebhooks/assets/NewPetApi.java b/multiapi-engine/src/test/resources/openapigenerator/testWebhooks/assets/NewPetApi.java new file mode 100644 index 00000000..99ee3f07 --- /dev/null +++ b/multiapi-engine/src/test/resources/openapigenerator/testWebhooks/assets/NewPetApi.java @@ -0,0 +1,47 @@ +package com.sngular.multifileplugin.webhooks; + +import java.util.Optional; +import java.util.List; +import java.util.Map; +import javax.validation.Valid; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import org.springframework.http.MediaType; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.context.request.NativeWebRequest; + +import com.sngular.multifileplugin.webhooks.model.PetDTO; + +public interface NewPetApi { + + /** + * POST /newPet: New pet notification + * @param petDTO + * @return Notification acknowledged; (status code 200) + */ + + @Operation( + operationId = "newPetWebhook", + summary = "New pet notification", + tags = {"newPet"}, + responses = { + @ApiResponse(responseCode = "200", description = "Notification acknowledged") + } + ) + @RequestMapping( + method = RequestMethod.POST, + value = "/newPet", + produces = {"application/json"} + ) + + default ResponseEntity newPetWebhook(@Parameter(name = "petDTO", description = "", required = false, schema = @Schema(description = "")) @Valid @RequestBody PetDTO petDTO) { + return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + } + +} diff --git a/multiapi-engine/src/test/resources/openapigenerator/testWebhooks/assets/PetDTO.java b/multiapi-engine/src/test/resources/openapigenerator/testWebhooks/assets/PetDTO.java new file mode 100644 index 00000000..39e40029 --- /dev/null +++ b/multiapi-engine/src/test/resources/openapigenerator/testWebhooks/assets/PetDTO.java @@ -0,0 +1,94 @@ +package com.sngular.multifileplugin.webhooks.model; + +import java.util.Objects; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; + +@JsonDeserialize(builder = PetDTO.PetDTOBuilder.class) +public class PetDTO { + + @JsonProperty(value ="name") + private String name; + @JsonProperty(value ="id") + private Long id; + + private PetDTO(PetDTOBuilder builder) { + this.name = builder.name; + this.id = builder.id; + + } + + public static PetDTO.PetDTOBuilder builder() { + return new PetDTO.PetDTOBuilder(); + } + + @JsonPOJOBuilder(buildMethodName = "build", withPrefix = "") + public static class PetDTOBuilder { + + private String name; + private Long id; + + public PetDTO.PetDTOBuilder name(String name) { + this.name = name; + return this; + } + + public PetDTO.PetDTOBuilder id(Long id) { + this.id = id; + return this; + } + + public PetDTO build() { + PetDTO petDTO = new PetDTO(this); + return petDTO; + } + } + + @Schema(name = "name", required = false) + public String getName() { + return name; + } + public void setName(String name) { + this.name = name; + } + + @Schema(name = "id", required = false) + public Long getId() { + return id; + } + public void setId(Long id) { + this.id = id; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PetDTO petDTO = (PetDTO) o; + return Objects.equals(this.name, petDTO.name) && Objects.equals(this.id, petDTO.id); + } + + @Override + public int hashCode() { + return Objects.hash(name, id); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("PetDTO{"); + sb.append(" name:").append(name).append(","); + sb.append(" id:").append(id); + sb.append("}"); + return sb.toString(); + } + + +}