Skip to content

Commit db6c9d5

Browse files
committed
fix(openapi): rename duplicate delinquency tag history operation id
1 parent 7eb7b78 commit db6c9d5

5 files changed

Lines changed: 102 additions & 7 deletions

File tree

fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalLoanActionTemplateStepDef.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public void retrieveWcLoanActionTemplate(final String templateType) {
4949
final Long loanId = getCreatedLoanId();
5050

5151
final WorkingCapitalLoanCommandTemplateData response = ok(
52-
() -> fineractClient.workingCapitalLoanTransactions().retrieveWorkingCapitalLoanTemplate1(loanId, templateType));
52+
() -> fineractClient.workingCapitalLoanTransactions().retrieveWorkingCapitalLoanActionTemplate(loanId, templateType));
5353
testContext().set(TestContextKey.WC_LOAN_ACTION_TEMPLATE_RESPONSE, response);
5454
log.info("Retrieved WC loan action template for loan ID: {} with templateType: {}", loanId, templateType);
5555
}
@@ -69,7 +69,7 @@ public void retrieveTemplateWithInvalidType(final String templateType) {
6969
final Long loanId = getCreatedLoanId();
7070

7171
final CallFailedRuntimeException exception = fail(
72-
() -> fineractClient.workingCapitalLoanTransactions().retrieveWorkingCapitalLoanTemplate1(loanId, templateType));
72+
() -> fineractClient.workingCapitalLoanTransactions().retrieveWorkingCapitalLoanActionTemplate(loanId, templateType));
7373

7474
assertThat(exception.getStatus()).as("HTTP status code").isEqualTo(400);
7575
assertThat(exception.getMessage()).as("Error message should reference the invalid command").contains(templateType);
@@ -81,7 +81,7 @@ public void retrieveTemplateWithoutType() {
8181
final Long loanId = getCreatedLoanId();
8282

8383
final CallFailedRuntimeException exception = fail(
84-
() -> fineractClient.workingCapitalLoanTransactions().retrieveWorkingCapitalLoanTemplate1(loanId, (String) null));
84+
() -> fineractClient.workingCapitalLoanTransactions().retrieveWorkingCapitalLoanActionTemplate(loanId, (String) null));
8585

8686
assertThat(exception.getStatus()).as("HTTP status code").isEqualTo(400);
8787
assertThat(exception.getMessage()).as("Error message should reference unrecognized command").contains("command");
@@ -91,7 +91,7 @@ public void retrieveTemplateWithoutType() {
9191
@Then("Retrieving WC loan action template for non-existent loan id {long} results in a 404 error")
9292
public void retrieveTemplateForNonExistentLoan(final Long loanId) {
9393
final CallFailedRuntimeException exception = fail(
94-
() -> fineractClient.workingCapitalLoanTransactions().retrieveWorkingCapitalLoanTemplate1(loanId, "approve"));
94+
() -> fineractClient.workingCapitalLoanTransactions().retrieveWorkingCapitalLoanActionTemplate(loanId, "approve"));
9595

9696
assertThat(exception.getStatus()).as("HTTP status code").isEqualTo(404);
9797
assertThat(exception.getMessage()).as("Error message should indicate loan not found").contains("does not exist");

fineract-provider/build.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,9 @@ resolve {
8888
buildClasspath = classpath
8989
outputDir = file("${buildDir}/resources/main/static")
9090
openApiFile = file("${buildDir}/tmp/swagger/fineract-input.yaml")
91+
readerClass = 'org.apache.fineract.infrastructure.openapi.FineractOperationIdReader'
9192
sortOutput = true
92-
dependsOn(prepareInputYaml)
93+
dependsOn(prepareInputYaml, classes)
9394
}
9495

9596
configurations {
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.fineract.infrastructure.openapi;
20+
21+
import io.swagger.v3.jaxrs2.Reader;
22+
import io.swagger.v3.oas.annotations.Operation;
23+
import io.swagger.v3.oas.models.OpenAPI;
24+
import jakarta.ws.rs.HttpMethod;
25+
import jakarta.ws.rs.Path;
26+
import java.lang.annotation.Annotation;
27+
import java.lang.reflect.Method;
28+
import java.util.ArrayList;
29+
import java.util.LinkedHashMap;
30+
import java.util.List;
31+
import java.util.Map;
32+
import java.util.Set;
33+
34+
public final class FineractOperationIdReader extends Reader {
35+
36+
// Check explicit @Operation ids first, then let Swagger build the spec.
37+
@Override
38+
public OpenAPI read(Set<Class<?>> classes, Map<String, Object> resources) {
39+
ExplicitOperationValidator.validate(classes);
40+
return super.read(classes, resources);
41+
}
42+
43+
static final class ExplicitOperationValidator {
44+
45+
static void validate(Set<Class<?>> classes) {
46+
Map<String, List<String>> operationIds = new LinkedHashMap<>();
47+
for (Class<?> resourceClass : classes) {
48+
// No @Path means this class is not a JAX-RS resource.
49+
if (resourceClass.getAnnotation(Path.class) == null) {
50+
continue;
51+
}
52+
53+
for (Method method : resourceClass.getMethods()) {
54+
Operation operation = method.getAnnotation(Operation.class);
55+
String operationId = trimToNull(operation == null ? null : operation.operationId());
56+
57+
// We only care about actual endpoints that set an id explicitly.
58+
if (operationId == null || !hasHttpMethod(method)) {
59+
continue;
60+
}
61+
62+
operationIds.computeIfAbsent(operationId, ignored -> new ArrayList<>())
63+
.add(resourceClass.getSimpleName() + "#" + method.getName());
64+
65+
}
66+
}
67+
68+
List<String> duplicates = operationIds.entrySet().stream().filter(e -> e.getValue().size() > 1)
69+
.map(e -> e.getKey() + " -> " + String.join(", ", e.getValue())).sorted().toList();
70+
if (!duplicates.isEmpty()) {
71+
throw new IllegalStateException(
72+
"Duplicate explicit OpenAPI operationIds detected:\n - " + String.join("\n - ", duplicates));
73+
}
74+
}
75+
}
76+
77+
// GET, POST, etc. are meta-annotated with @HttpMethod.
78+
private static boolean hasHttpMethod(Method method) {
79+
for (Annotation annotation : method.getAnnotations()) {
80+
if (annotation.annotationType().getAnnotation(HttpMethod.class) != null) {
81+
return true;
82+
}
83+
}
84+
return false;
85+
}
86+
87+
private static String trimToNull(String value) {
88+
if (value == null) {
89+
return null;
90+
}
91+
String trimmed = value.trim();
92+
return trimmed.isEmpty() ? null : trimmed;
93+
}
94+
}

fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanApiResource.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ public List<WorkingCapitalLoanDelinquencyTagHistoryData> getDelinquencyRangeSche
209209
@Path("external-id/{externalId}/delinquencyrangetags")
210210
@Consumes({ MediaType.TEXT_HTML, MediaType.APPLICATION_JSON })
211211
@Produces(MediaType.APPLICATION_JSON)
212-
@Operation(summary = "Retrieve the Loan Delinquency Tag history using the Loan Id", description = "", operationId = "getDelinquencyRangeScheduleTagHistoryById")
212+
@Operation(summary = "Retrieve the Loan Delinquency Tag history using the Loan External Id", description = "", operationId = "getDelinquencyRangeScheduleTagHistoryByExternalId")
213213
@ApiResponses({
214214
@ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = WorkingCapitalLoanApiResourceSwagger.GetWorkingCapitalLoanDelinquencyRangeScheduleTagHistoryResponse.class)))) })
215215
public List<WorkingCapitalLoanDelinquencyTagHistoryData> getDelinquencyRangeScheduleTagHistoryById(

fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanTransactionsApiResource.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ public WorkingCapitalLoanTransactionData retrieveTransactionByExternalLoanIdAndT
142142
@GET
143143
@Path("{loanId}/template")
144144
@Produces({ MediaType.APPLICATION_JSON })
145-
@Operation(operationId = "retrieveWorkingCapitalLoanTemplate", summary = "Retrieve Working Capital Loan action template", description = "Returns loan data for applying the proper loan action")
145+
@Operation(operationId = "retrieveWorkingCapitalLoanActionTemplate", summary = "Retrieve Working Capital Loan action template", description = "Returns loan data for applying the proper loan action")
146146
public WorkingCapitalLoanCommandTemplateData retrieveWorkingCapitalLoanTemplate(
147147
@PathParam("loanId") @Parameter(description = "loanId", required = true) final Long loanId,
148148
@QueryParam("templateType") @Parameter(description = "templateType") final String templateType,

0 commit comments

Comments
 (0)