Skip to content

Commit dda6a28

Browse files
authored
Merge branch 'main' into feat/375-openapi-3.1-support
2 parents 4cf6aa8 + 0f15f00 commit dda6a28

8 files changed

Lines changed: 264 additions & 0 deletions

File tree

multiapi-engine/src/main/java/com/sngular/api/generator/plugin/openapi/OpenApiGenerator.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ private void processPackage(final String apiPackage) {
9898
private void processFile(final SpecFile specFile) {
9999

100100
final JsonNode openAPI = OpenApiUtil.getPojoFromSpecFile(baseDir, specFile);
101+
OpenApiUtil.solvePathRefs(openAPI, baseDir.resolve(specFile.getFilePath()).getParent().toUri());
101102
final String clientPackage = specFile.getClientPackage();
102103

103104
if (specFile.isCallMode()) {

multiapi-engine/src/main/java/com/sngular/api/generator/plugin/openapi/utils/OpenApiUtil.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
package com.sngular.api.generator.plugin.openapi.utils;
88

9+
import java.net.URI;
910
import java.nio.file.Path;
1011
import java.util.ArrayList;
1112
import java.util.HashMap;
@@ -91,6 +92,33 @@ public static JsonNode getPojoFromSpecFile(final Path baseDir, final SpecFile sp
9192
return SchemaUtil.getPojoFromRef(baseDir.toUri(), specFile.getFilePath());
9293
}
9394

95+
/**
96+
* Dereferences Path Item Objects that are declared as a {@code $ref} to another file (modular
97+
* contracts). These references are otherwise never resolved, so the affected paths silently
98+
* disappear from generation. The referenced Path Item is resolved and set in place, so every
99+
* downstream consumer (API grouping, path mapping and model extraction) sees the real operations.
100+
*
101+
* @param openApi the parsed root contract; its {@code paths} node is mutated in place.
102+
* @param rootFilePath base URI used to resolve relative external references.
103+
*/
104+
public static void solvePathRefs(final JsonNode openApi, final URI rootFilePath) {
105+
final JsonNode pathsNode = openApi.get(PATHS);
106+
if (pathsNode instanceof ObjectNode) {
107+
final ObjectNode paths = (ObjectNode) pathsNode;
108+
final Map<String, JsonNode> resolvedItems = new HashMap<>();
109+
paths.fields().forEachRemaining(pathItem -> {
110+
final JsonNode pathValue = pathItem.getValue();
111+
if (ApiTool.hasRef(pathValue)) {
112+
final JsonNode resolved = SchemaUtil.solveRef(ApiTool.getRefValue(pathValue), new HashMap<>(), rootFilePath);
113+
if (Objects.nonNull(resolved)) {
114+
resolvedItems.put(pathItem.getKey(), resolved);
115+
}
116+
}
117+
});
118+
resolvedItems.forEach(paths::set);
119+
}
120+
}
121+
94122
public static Map<String, JsonNode> processPaths(final JsonNode openApi, final Map<String, JsonNode> schemaMap, SpecFile specFile) {
95123
final var basicJsonNodeMap = new HashMap<>(schemaMap);
96124

multiapi-engine/src/test/java/com/sngular/api/generator/plugin/openapi/OpenApiGeneratorFixtures.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,13 @@ public final class OpenApiGeneratorFixtures {
152152
.apiPackage("com.sngular.multifileplugin.openapi31types")
153153
.modelPackage("com.sngular.multifileplugin.openapi31types.model")
154154
.clientPackage("com.sngular.multifileplugin.openapi31types.client")
155+
.modelNameSuffix("DTO").build());
156+
157+
static final List<SpecFile> TEST_EXTERNAL_PATH_ITEM_REF_GENERATION = List
158+
.of(SpecFile.builder().filePath("openapigenerator/testExternalPathItemRefsGeneration/api-test.yml")
159+
.apiPackage("com.sngular.multifileplugin.externalpathitemref")
160+
.modelPackage("com.sngular.multifileplugin.externalpathitemref.model")
161+
.clientPackage("com.sngular.multifileplugin.externalpathitemref.client")
155162
.modelNameSuffix("DTO").build());
156163

157164
static final List<SpecFile> TEST_ANY_OF_IN_RESPONSE = List
@@ -784,6 +791,24 @@ static Function<Path, Boolean> validateOpenApi31Types() {
784791

785792
final List<String> expectedTestApiModelFiles = List.of(ASSETS_PATH + "ProfileDTO.java");
786793

794+
return path -> commonTest(path, expectedTestApiFile, expectedTestApiModelFiles, DEFAULT_TARGET_API,
795+
DEFAULT_MODEL_API, Collections.emptyList(), null);
796+
}
797+
798+
static Function<Path, Boolean> validateExternalPathItemRefGeneration() {
799+
800+
final String DEFAULT_TARGET_API = "generated/com/sngular/multifileplugin/externalpathitemref";
801+
802+
final String DEFAULT_MODEL_API = "generated/com/sngular/multifileplugin/externalpathitemref/model";
803+
804+
final String COMMON_PATH = "openapigenerator/testExternalPathItemRefsGeneration/";
805+
806+
final String ASSETS_PATH = COMMON_PATH + "assets/";
807+
808+
final List<String> expectedTestApiFile = List.of(ASSETS_PATH + "DashboardApi.java");
809+
810+
final List<String> expectedTestApiModelFiles = List.of(ASSETS_PATH + "DashboardDTO.java");
811+
787812
return path -> commonTest(path, expectedTestApiFile, expectedTestApiModelFiles, DEFAULT_TARGET_API,
788813
DEFAULT_MODEL_API, Collections.emptyList(), null);
789814
}

multiapi-engine/src/test/java/com/sngular/api/generator/plugin/openapi/OpenApiGeneratorTest.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ static Stream<Arguments> fileSpecToProcess() {
8585
OpenApiGeneratorFixtures.validateExternalRefGeneration()),
8686
Arguments.of("testOpenApi31Types", OpenApiGeneratorFixtures.TEST_OPEN_API_31_TYPES,
8787
OpenApiGeneratorFixtures.validateOpenApi31Types()),
88+
Arguments.of("testExternalPathItemRefsGeneration", OpenApiGeneratorFixtures.TEST_EXTERNAL_PATH_ITEM_REF_GENERATION,
89+
OpenApiGeneratorFixtures.validateExternalPathItemRefGeneration()),
8890
Arguments.of("testAnyOfInResponse", OpenApiGeneratorFixtures.TEST_ANY_OF_IN_RESPONSE,
8991
OpenApiGeneratorFixtures.validateAnyOfInResponse()),
9092
Arguments.of("testOneOfInResponse", OpenApiGeneratorFixtures.TEST_ONE_OF_IN_RESPONSE,
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
# Reproduction for: external Path Item $ref produces 0 files silently (issue #372).
3+
# The `paths` entries are $ref'd out to a separate file (modular contract), exactly
4+
# like a Redocly/Swagger split contract. openapi 3.1.0 to match the real-world case,
5+
# but no 3.1-only schema constructs are used so the ONLY blocker under test is the
6+
# unresolved external Path Item $ref. Schemas are referenced with the already-supported
7+
# internal component ref style (#/components/schemas/...).
8+
openapi: "3.1.0"
9+
info:
10+
version: 1.0.0
11+
title: Dashboard API (modular contract)
12+
license:
13+
name: MIT
14+
servers:
15+
- url: http://localhost:8080/v1
16+
tags:
17+
- name: dashboard
18+
paths:
19+
/dashboard:
20+
$ref: "./paths/dashboard.yml"
21+
components:
22+
schemas:
23+
Dashboard:
24+
type: object
25+
properties:
26+
id:
27+
type: string
28+
title:
29+
type: string
30+
widgetCount:
31+
type: integer
32+
format: int32
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.sngular.multifileplugin.externalpathitemref;
2+
3+
import java.util.Optional;
4+
import java.util.List;
5+
import java.util.Map;
6+
import javax.validation.Valid;
7+
8+
import io.swagger.v3.oas.annotations.Operation;
9+
import io.swagger.v3.oas.annotations.Parameter;
10+
import io.swagger.v3.oas.annotations.media.Content;
11+
import io.swagger.v3.oas.annotations.media.Schema;
12+
import io.swagger.v3.oas.annotations.responses.ApiResponse;
13+
import org.springframework.http.MediaType;
14+
import org.springframework.http.HttpStatus;
15+
import org.springframework.http.ResponseEntity;
16+
import org.springframework.web.bind.annotation.*;
17+
import org.springframework.web.context.request.NativeWebRequest;
18+
19+
import com.sngular.multifileplugin.externalpathitemref.model.DashboardDTO;
20+
21+
public interface DashboardApi {
22+
23+
/**
24+
* GET /dashboard: Get the dashboard
25+
* @return The requested dashboard; (status code 200)
26+
*/
27+
28+
@Operation(
29+
operationId = "getDashboard",
30+
summary = "Get the dashboard",
31+
tags = {"dashboard"},
32+
responses = {
33+
@ApiResponse(responseCode = "200", description = "The requested dashboard", content = @Content(mediaType = "application/json", schema = @Schema(implementation = DashboardDTO.class)))
34+
}
35+
)
36+
@RequestMapping(
37+
method = RequestMethod.GET,
38+
value = "/dashboard",
39+
produces = {"application/json"}
40+
)
41+
42+
default ResponseEntity<DashboardDTO> getDashboard() {
43+
return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
44+
}
45+
46+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package com.sngular.multifileplugin.externalpathitemref.model;
2+
3+
import java.util.Objects;
4+
5+
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
6+
import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
7+
import com.fasterxml.jackson.annotation.JsonProperty;
8+
import io.swagger.v3.oas.annotations.media.Schema;
9+
10+
@JsonDeserialize(builder = DashboardDTO.DashboardDTOBuilder.class)
11+
public class DashboardDTO {
12+
13+
@JsonProperty(value ="id")
14+
private String id;
15+
@JsonProperty(value ="title")
16+
private String title;
17+
@JsonProperty(value ="widgetCount")
18+
private Integer widgetCount;
19+
20+
private DashboardDTO(DashboardDTOBuilder builder) {
21+
this.id = builder.id;
22+
this.title = builder.title;
23+
this.widgetCount = builder.widgetCount;
24+
25+
}
26+
27+
public static DashboardDTO.DashboardDTOBuilder builder() {
28+
return new DashboardDTO.DashboardDTOBuilder();
29+
}
30+
31+
@JsonPOJOBuilder(buildMethodName = "build", withPrefix = "")
32+
public static class DashboardDTOBuilder {
33+
34+
private String id;
35+
private String title;
36+
private Integer widgetCount;
37+
38+
public DashboardDTO.DashboardDTOBuilder id(String id) {
39+
this.id = id;
40+
return this;
41+
}
42+
43+
public DashboardDTO.DashboardDTOBuilder title(String title) {
44+
this.title = title;
45+
return this;
46+
}
47+
48+
public DashboardDTO.DashboardDTOBuilder widgetCount(Integer widgetCount) {
49+
this.widgetCount = widgetCount;
50+
return this;
51+
}
52+
53+
public DashboardDTO build() {
54+
DashboardDTO dashboardDTO = new DashboardDTO(this);
55+
return dashboardDTO;
56+
}
57+
}
58+
59+
@Schema(name = "id", required = false)
60+
public String getId() {
61+
return id;
62+
}
63+
public void setId(String id) {
64+
this.id = id;
65+
}
66+
67+
@Schema(name = "title", required = false)
68+
public String getTitle() {
69+
return title;
70+
}
71+
public void setTitle(String title) {
72+
this.title = title;
73+
}
74+
75+
@Schema(name = "widgetCount", required = false)
76+
public Integer getWidgetCount() {
77+
return widgetCount;
78+
}
79+
public void setWidgetCount(Integer widgetCount) {
80+
this.widgetCount = widgetCount;
81+
}
82+
83+
@Override
84+
public boolean equals(Object o) {
85+
if (this == o) {
86+
return true;
87+
}
88+
if (o == null || getClass() != o.getClass()) {
89+
return false;
90+
}
91+
DashboardDTO dashboardDTO = (DashboardDTO) o;
92+
return Objects.equals(this.id, dashboardDTO.id) && Objects.equals(this.title, dashboardDTO.title) && Objects.equals(this.widgetCount, dashboardDTO.widgetCount);
93+
}
94+
95+
@Override
96+
public int hashCode() {
97+
return Objects.hash(id, title, widgetCount);
98+
}
99+
100+
@Override
101+
public String toString() {
102+
StringBuilder sb = new StringBuilder();
103+
sb.append("DashboardDTO{");
104+
sb.append(" id:").append(id).append(",");
105+
sb.append(" title:").append(title).append(",");
106+
sb.append(" widgetCount:").append(widgetCount);
107+
sb.append("}");
108+
return sb.toString();
109+
}
110+
111+
112+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
# A standalone Path Item Object, referenced from the root contract via
3+
# `paths: /dashboard: { $ref: './paths/dashboard.yml' }`.
4+
# Note: the only top-level keys here are HTTP verbs (`get`) - there is NO
5+
# `$ref` verb, which is why `OpenApiUtil.mapApiGroups` filtering for REST
6+
# verbs finds nothing when it sees the *reference* node in the root file.
7+
get:
8+
summary: Get the dashboard
9+
operationId: getDashboard
10+
tags:
11+
- dashboard
12+
responses:
13+
'200':
14+
description: The requested dashboard
15+
content:
16+
application/json:
17+
schema:
18+
$ref: "#/components/schemas/Dashboard"

0 commit comments

Comments
 (0)