Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .codacy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
# Codacy configuration.
#
# exclude_paths accepts glob patterns relative to the repository root and removes
# the matched files from static analysis. We exclude sources that only produce
# false positives for this project's conventions:
#
# * Gradle task glue classes: thin DefaultTask subclasses. Codacy flags them for
# "missing Javadoc", but this project does not use Javadoc, so the warning is
# noise rather than a real defect.
# * Generated test fixtures under src/test/resources: these are the generator's
# expected-output files (not hand-written source) and trip rules such as
# line-length that do not apply to generated code.
exclude_paths:
- 'scs-multiapi-gradle-plugin/src/main/groovy/com/sngular/api/generator/plugin/*Task.groovy'
- '**/src/test/resources/**'
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,6 @@ pom.xml.bak
*.bak

scs-multiapi-maven-plugin/dependency-reduced-pom.xml

# Internal analysis documents (not part of the plugin distribution)
SCS-MULTIAPI-6.3.3-CODEGEN-BLOCKER.md
2 changes: 1 addition & 1 deletion multiapi-engine/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

<groupId>com.sngular</groupId>
<artifactId>multiapi-engine</artifactId>
<version>6.4.0</version>
<version>6.4.1</version>
<packaging>jar</packaging>

<properties>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ private static List<SchemaFieldObject> processObjectProperty(
final SchemaFieldObject field;
if (ApiTool.hasRef(fieldBody)) {
final var typeName = MapperUtil.getRefSchemaName(fieldBody, fieldName);
final var refSchema = totalSchemas.get(MapperUtil.getRefSchemaKey(fieldBody));
var refSchema = totalSchemas.get(MapperUtil.getRefSchemaKey(fieldBody));
if (!antiLoopList.contains(typeName) && Objects.nonNull(refSchema) && ApiTool.hasType(refSchema)
&& ApiTool.hasItems(refSchema) || ApiTool.getRefValue(fieldBody).contains(fieldName)) {
if (antiLoopList.contains(typeName) && ApiTool.getRefValue(fieldBody).contains(fieldName)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;

import com.fasterxml.jackson.databind.JsonNode;
Expand All @@ -34,10 +36,16 @@ public static JsonNode solveRef(final String refValue, final Map<String, JsonNod
} else {
final var refValueArr = refValue.split("#");
final var filePath = refValueArr[0];
final URI actualFileBase = resolveActualBaseUri(rootFilePath, filePath);
solvedRef = getPojoFromRef(rootFilePath, filePath);
if (ApiTool.hasComponents(solvedRef)) {
schemaMap.putAll(ApiTool.getComponentSchemas(solvedRef));
solvedRef = solvedRef.findValue(MapperUtil.getKey(refValueArr[1]));
if (refValueArr.length > 1) {
solvedRef = solvedRef.findValue(MapperUtil.getKey(refValueArr[1]));
}
}
if (Objects.nonNull(solvedRef) && Objects.nonNull(actualFileBase)) {
resolveNestedFileRefs(solvedRef, actualFileBase);
}
}
} else {
Expand All @@ -46,6 +54,71 @@ public static JsonNode solveRef(final String refValue, final Map<String, JsonNod
return solvedRef;
}

/**
* Computes a clean schema-map key from a file-based $ref value.
* E.g. "./ServiceType.yml" -> "SCHEMAS/SERVICE_TYPE"
*/
static String computeFileSchemaKey(final String refValue) {
try {
final String[] parts = refValue.split("/");
final String lastRaw = parts[parts.length - 1];
String lastName = lastRaw;
if (lastName.contains(".")) {
lastName = lastName.substring(0, lastName.indexOf('.'));
}
String category = parts.length >= 2 ? parts[parts.length - 2] : "schemas";
if (".".equals(category) || "..".equals(category)) {
category = "schemas";
}
return StringUtils.upperCase(category + "/" + StringCaseUtils.titleToSnakeCase(lastName));
} catch (final Exception e) {
return null;
}
}

private static URI resolveActualBaseUri(final URI rootFilePath, final String filePath) {
try {
final String cleaned = cleanUpPath(filePath).replace('\\', '/');
final Path rootPath = Paths.get(rootFilePath);
if (Files.exists(rootPath) && Files.isDirectory(rootPath)) {
return rootPath.resolve(cleaned).normalize().getParent().toUri();
}
final Path parent = rootPath.getParent();
if (Objects.nonNull(parent)) {
return parent.resolve(cleaned).normalize().getParent().toUri();
}
return null;
} catch (final Exception e) {
return null;
}
}

private static void resolveNestedFileRefs(final JsonNode node, final URI baseUri) {
if (Objects.isNull(node) || !node.isObject()) {
return;
}
final Iterator<Entry<String, JsonNode>> fields = node.fields();
while (fields.hasNext()) {
final Entry<String, JsonNode> field = fields.next();
if (field.getValue().isObject() && field.getValue().has("$ref")) {
final String refVal = field.getValue().get("$ref").textValue();
if (StringUtils.isNotEmpty(refVal) && !refVal.startsWith("#") && !refVal.startsWith("http")) {
try {
final URI nestedBase = resolveActualBaseUri(baseUri, refVal);
final JsonNode resolved = getPojoFromRef(baseUri, refVal);
if (Objects.nonNull(resolved)) {
field.setValue(resolved);
resolveNestedFileRefs(resolved, nestedBase);
}
} catch (final Exception ignored) {
}
}
} else {
resolveNestedFileRefs(field.getValue(), baseUri);
}
}
}

public static JsonNode getPojoFromRef(final URI rootFilePath, final String refPath) {
final JsonNode schemaFile;
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

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

import java.net.URI;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
Expand All @@ -15,6 +16,7 @@
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.function.BiConsumer;

import com.fasterxml.jackson.databind.JsonNode;
Expand Down Expand Up @@ -223,11 +225,15 @@ private static List<RequestObject> mapRequestObject(
"InlineObject" + operationIdWithCap, globalObject, baseDir))
.build());
} else {
final var requestBodyNode = globalObject.getRequestBodyNode(MapperUtil.getRefSchemaKey(requestBody)).orElseThrow();
final Optional<JsonNode> requestBodyNode = globalObject.getRequestBodyNode(MapperUtil.getRefSchemaKey(requestBody));
if (requestBodyNode.isEmpty()) {
return requestObjects;
}
final JsonNode actualRequestBody = requestBodyNode.get();
requestObjects.add(RequestObject.builder()
.required(ApiTool.hasNode(requestBody, REQUIRED))
.isFormData(ApiTool.getNode(requestBodyNode, CONTENT).has("multipart/form-data"))
.contentObjects(mapContentObject(specFile, ApiTool.getNode(requestBodyNode, CONTENT),
.isFormData(ApiTool.getNode(actualRequestBody, CONTENT).has("multipart/form-data"))
.contentObjects(mapContentObject(specFile, ApiTool.getNode(actualRequestBody, CONTENT),
operationIdWithCap, globalObject, baseDir))
.build());
}
Expand All @@ -242,8 +248,11 @@ private static List<ParameterObject> mapParameterObjects(
if (Objects.nonNull(parameters) && !parameters.isEmpty()) {
for (final JsonNode parameter : parameters) {
if (ApiTool.hasRef(parameter)) {
final JsonNode refParameter = globalObject.getParameterNode(MapperUtil.getRefSchemaKey(parameter)).orElseThrow();
parameterObjects.add(buildParameterObject(specFile, globalObject, refParameter, baseDir));
final Optional<JsonNode> optRefParameter = globalObject.getParameterNode(MapperUtil.getRefSchemaKey(parameter));
if (optRefParameter.isEmpty()) {
continue;
}
parameterObjects.add(buildParameterObject(specFile, globalObject, optRefParameter.get(), baseDir));
} else if (ApiTool.hasNode(parameter, CONTENT)) {
parameterObjects.addAll(buildParameterContent(contentClassName, parameter, specFile, globalObject, baseDir));
} else {
Expand Down Expand Up @@ -344,7 +353,21 @@ private static void buildResponse(
final JsonNode response) {
var realResponse = response;
if (ApiTool.hasRef(response)) {
realResponse = globalObject.getResponseNode(MapperUtil.getRefSchemaKey(response)).orElseThrow();
final String refValue = ApiTool.getRefValue(response);
if (refValue.startsWith("#")) {
final Optional<JsonNode> resolvedResponse = globalObject.getResponseNode(MapperUtil.getRefSchemaKey(response));
if (resolvedResponse.isEmpty()) {
return;
}
realResponse = resolvedResponse.get();
} else {
try {
final URI baseUri = baseDir.resolve(specFile.getFilePath()).getParent().toUri();
realResponse = SchemaUtil.getPojoFromRef(baseUri, refValue);
} catch (final Exception e) {
return;
}
}
}
final String operationIdWithCap = operationId.substring(0, 1).toUpperCase() + operationId.substring(1);
final var content = ApiTool.getNode(realResponse, CONTENT);
Expand Down Expand Up @@ -427,6 +450,9 @@ private static JsonNode getRefSchema(JsonNode schema, SpecFile specFile, GlobalO
if (refValue.contains("schemas")) {
refSchema = SchemaUtil.solveRef(refValue, globalObject.getSchemaMap(),
baseDir.resolve(specFile.getFilePath()).getParent().toUri());
if (Objects.nonNull(refSchema) && !refValue.contains("#")) {
globalObject.getSchemaMap().put(inlinePojoName, refSchema);
}
} else if (refValue.contains("requestBodies")) {
refSchema = SchemaUtil.solveRef(refValue, globalObject.getRequestBodyMap(),
baseDir.resolve(specFile.getFilePath()).getParent().toUri());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,20 @@ public final class OpenApiGeneratorFixtures {
.clientPackage("com.sngular.multifileplugin.externalpathitemref.client")
.modelNameSuffix("DTO").build());

static final List<SpecFile> TEST_NESTED_EXTERNAL_REFS = List
.of(SpecFile.builder().filePath("openapigenerator/testNestedExternalRefs/api-test.yml")
.apiPackage("com.sngular.multifileplugin.testnestedexternalref")
.modelPackage("com.sngular.multifileplugin.testnestedexternalref.model")
.modelNameSuffix("DTO")
.useLombokModelAnnotation(true).build());

static final List<SpecFile> TEST_NO_CONTENT_RESPONSES = List
.of(SpecFile.builder().filePath("openapigenerator/testNoContentResponses/api-test.yml")
.apiPackage("com.sngular.multifileplugin.testnocontentresponses")
.modelPackage("com.sngular.multifileplugin.testnocontentresponses.model")
.modelNameSuffix("DTO")
.useLombokModelAnnotation(true).build());

static final List<SpecFile> TEST_ANY_OF_IN_RESPONSE = List
.of(SpecFile.builder().filePath("openapigenerator/testAnyOfInResponse/api-test.yml")
.apiPackage("com.sngular.multifileplugin.testanyofinresponse")
Expand Down Expand Up @@ -1370,6 +1384,37 @@ static Function<Path, Boolean> validateApiWithNoComponents() {

}

static Function<Path, Boolean> validateNestedExternalRefs() {
final String DEFAULT_TARGET_API = "generated/com/sngular/multifileplugin/testnestedexternalref";

final String DEFAULT_MODEL_API = "generated/com/sngular/multifileplugin/testnestedexternalref/model";

final String COMMON_PATH = "openapigenerator/testNestedExternalRefs/";

final List<String> expectedTestApiFiles = List.of(COMMON_PATH + "assets/ServicesApi.java");

final List<String> expectedTestApiModelFiles = List
.of(COMMON_PATH + "assets/InlineResponse200ListServicesDTO.java", COMMON_PATH + "assets/Service_typeDTO.java");

return path -> commonTest(path, expectedTestApiFiles, expectedTestApiModelFiles, DEFAULT_TARGET_API,
DEFAULT_MODEL_API, Collections.emptyList(), null);
}

static Function<Path, Boolean> validateNoContentResponses() {
final String DEFAULT_TARGET_API = "generated/com/sngular/multifileplugin/testnocontentresponses";

final String DEFAULT_MODEL_API = "generated/com/sngular/multifileplugin/testnocontentresponses/model";

final String COMMON_PATH = "openapigenerator/testNoContentResponses/";

final List<String> expectedTestApiFiles = List.of(COMMON_PATH + "assets/ItemsApi.java");

final List<String> expectedTestApiModelFiles = List.of(COMMON_PATH + "assets/ItemDTO.java");

return path -> commonTest(path, expectedTestApiFiles, expectedTestApiModelFiles, DEFAULT_TARGET_API,
DEFAULT_MODEL_API, Collections.emptyList(), null);
}

private static Boolean commonTest(final Path resultPath, final List<String> expectedFile,
final List<String> expectedModelFiles, final String targetApi, final String targetModel,
final List<String> expectedExceptionFiles, final String targetException) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ static Stream<Arguments> fileSpecToProcess() {
OpenApiGeneratorFixtures.validateWebhookPathCollision()),
Arguments.of("testExternalPathItemRefsGeneration", OpenApiGeneratorFixtures.TEST_EXTERNAL_PATH_ITEM_REF_GENERATION,
OpenApiGeneratorFixtures.validateExternalPathItemRefGeneration()),
Arguments.of("testNestedExternalRefs", OpenApiGeneratorFixtures.TEST_NESTED_EXTERNAL_REFS,
OpenApiGeneratorFixtures.validateNestedExternalRefs()),
Arguments.of("testNoContentResponses", OpenApiGeneratorFixtures.TEST_NO_CONTENT_RESPONSES,
OpenApiGeneratorFixtures.validateNoContentResponses()),
Arguments.of("testAnyOfInResponse", OpenApiGeneratorFixtures.TEST_ANY_OF_IN_RESPONSE,
OpenApiGeneratorFixtures.validateAnyOfInResponse()),
Arguments.of("testOneOfInResponse", OpenApiGeneratorFixtures.TEST_ONE_OF_IN_RESPONSE,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
openapi: "3.0.0"
info:
version: 1.0.0
title: Nested External Ref Test
license:
name: MIT
servers:
- url: http://localhost:8080/v1
paths:
/services:
get:
summary: List all services
operationId: listServices
tags:
- services
responses:
'200':
description: A list of services
content:
application/json:
schema:
$ref: "schemas/Service.yml"
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.sngular.multifileplugin.testnestedexternalref.model;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Builder;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;

@Value
public class InlineResponse200ListServicesDTO {

@JsonProperty(value ="service_id")
private String service_id;

@JsonProperty(value ="service_type")
private Service_typeDTO service_type;


@Builder
@Jacksonized
private InlineResponse200ListServicesDTO(String service_id, Service_typeDTO service_type) {
this.service_id = service_id;
this.service_type = service_type;

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.sngular.multifileplugin.testnestedexternalref.model;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Builder;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;

@Value
public class Service_typeDTO {

@JsonProperty(value ="type_code")
private Integer type_code;

@JsonProperty(value ="type_name")
private String type_name;


@Builder
@Jacksonized
private Service_typeDTO(Integer type_code, String type_name) {
this.type_code = type_code;
this.type_name = type_name;

}

}
Loading
Loading