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
Original file line number Diff line number Diff line change
Expand Up @@ -1137,7 +1137,8 @@ public CodegenOperation fromOperation(String path, String httpMethod, Operation
// codegenOperation.vendorExtensions from the extension we just set on 'operation'.
// Only for libraries that support Pageable; respect manual x-spring-paginated: false override.
if (isPageableSupported()) {
SpringPageableScanUtils.applyAutoXSpringPaginatedIfNeeded(operation, autoXSpringPaginated);
SpringPageableScanUtils.applyAutoXSpringPaginatedIfNeeded(
openAPI, operation, autoXSpringPaginated);
}

CodegenOperation codegenOperation = super.fromOperation(path, httpMethod, operation, servers);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1444,7 +1444,8 @@ public CodegenOperation fromOperation(String path, String httpMethod, Operation
// Only for libraries that support Pageable (spring-boot, spring-cloud);
// respect manual x-spring-paginated: false override.
if (isPageableSupported()) {
SpringPageableScanUtils.applyAutoXSpringPaginatedIfNeeded(operation, autoXSpringPaginated);
SpringPageableScanUtils.applyAutoXSpringPaginatedIfNeeded(
openAPI, operation, autoXSpringPaginated);
}

// add Pageable import only if x-spring-paginated explicitly used AND it's a pageable-supporting library.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,15 @@ public void applyPageableAnnotations(
* pagination query parameters (page, size, sort).
*/
public static boolean willBePageable(Operation operation, boolean autoXSpringPaginated) {
return willBePageable(null, operation, autoXSpringPaginated);
}

/**
* Returns {@code true} if the given operation will have a Pageable parameter injected,
* resolving component parameter references against the supplied OpenAPI document.
*/
public static boolean willBePageable(
OpenAPI openAPI, Operation operation, boolean autoXSpringPaginated) {
Boolean xSpringPaginated = getXSpringPaginated(operation);
if (xSpringPaginated != null) {
return xSpringPaginated;
Expand All @@ -188,7 +197,7 @@ public static boolean willBePageable(Operation operation, boolean autoXSpringPag
return false;
}
Set<String> paramNames = operation.getParameters().stream()
.map(Parameter::getName)
.map(parameter -> resolveParameter(openAPI, parameter).getName())
.collect(Collectors.toSet());
return paramNames.containsAll(DEFAULT_PAGEABLE_QUERY_PARAMS);
}
Expand Down Expand Up @@ -216,7 +225,7 @@ public static Boolean getXSpringPaginated(Operation operation) {
* Auto-detects Pageable pagination query parameters and, when detected, mutates the
* operation by setting {@code x-spring-paginated: true} on its vendor extensions.
*
* <p>Detection is delegated to {@link #willBePageable(Operation, boolean)}. If the
* <p>Detection is delegated to {@link #willBePageable(OpenAPI, Operation, boolean)}. If the
* operation is already explicitly flagged ({@code x-spring-paginated: true/false})
* this method is a read-only pass-through — it returns the explicit value without
* mutating extensions. Only auto-detected operations (those whose flag was
Expand All @@ -234,7 +243,21 @@ public static Boolean getXSpringPaginated(Operation operation) {
*/
public static boolean applyAutoXSpringPaginatedIfNeeded(
Operation operation, boolean autoXSpringPaginated) {
if (!willBePageable(operation, autoXSpringPaginated)) {
return applyAutoXSpringPaginatedIfNeeded(null, operation, autoXSpringPaginated);
}

/**
* Auto-detects pagination parameters after resolving component parameter references against
* the supplied OpenAPI document.
*
* @param openAPI the OpenAPI document used to resolve parameter references
* @param operation the raw OpenAPI {@link Operation} to inspect (and possibly mutate)
* @param autoXSpringPaginated whether auto-detection is enabled for this generator
* @return {@code true} if the operation is (or was just marked as) paginated
*/
public static boolean applyAutoXSpringPaginatedIfNeeded(
OpenAPI openAPI, Operation operation, boolean autoXSpringPaginated) {
if (!willBePageable(openAPI, operation, autoXSpringPaginated)) {
return false;
}
if (getXSpringPaginated(operation) == null) {
Expand Down Expand Up @@ -467,10 +490,11 @@ public static Map<String, List<String>> scanSortValidationEnums(
String operationId = operation.getOperationId();
if (operationId == null
|| operation.getParameters() == null
|| !willBePageable(operation, autoXSpringPaginated)) {
|| !willBePageable(openAPI, operation, autoXSpringPaginated)) {
continue;
}
for (Parameter param : operation.getParameters()) {
for (Parameter unresolvedParam : operation.getParameters()) {
Parameter param = resolveParameter(openAPI, unresolvedParam);
if (!SORT.equals(param.getName())) {
continue;
}
Expand Down Expand Up @@ -522,15 +546,16 @@ public static Map<String, PageableDefaultsData> scanPageableDefaults(
for (Operation operation : pathEntry.getValue().readOperations()) {
String operationId = operation.getOperationId();
if (operationId == null
|| !willBePageable(operation, autoXSpringPaginated)
|| !willBePageable(openAPI, operation, autoXSpringPaginated)
|| operation.getParameters() == null) {
continue;
}
Integer pageDefault = null;
Integer sizeDefault = null;
List<SortFieldDefault> sortDefaults = new ArrayList<>();

for (Parameter param : operation.getParameters()) {
for (Parameter unresolvedParam : operation.getParameters()) {
Parameter param = resolveParameter(openAPI, unresolvedParam);
Schema<?> schema = param.getSchema();
if (schema == null) {
continue;
Expand Down Expand Up @@ -603,15 +628,16 @@ public static Map<String, PageableConstraintsData> scanPageableConstraints(
for (Operation operation : pathEntry.getValue().readOperations()) {
String operationId = operation.getOperationId();
if (operationId == null
|| !willBePageable(operation, autoXSpringPaginated)
|| !willBePageable(openAPI, operation, autoXSpringPaginated)
|| operation.getParameters() == null) {
continue;
}
int maxPage = -1;
int maxSize = -1;
int minPage = -1;
int minSize = -1;
for (Parameter param : operation.getParameters()) {
for (Parameter unresolvedParam : operation.getParameters()) {
Parameter param = resolveParameter(openAPI, unresolvedParam);
Schema<?> schema = param.getSchema();
if (schema == null) {
continue;
Expand Down Expand Up @@ -648,6 +674,10 @@ public static Map<String, PageableConstraintsData> scanPageableConstraints(
return result;
}

private static Parameter resolveParameter(OpenAPI openAPI, Parameter parameter) {
return ModelUtils.getReferencedParameter(openAPI, parameter);
}

private static Integer toIntInclusiveMax(ModelUtils.ResolvedMaxBound maxBound) {
if (maxBound == null) {
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7462,6 +7462,38 @@ public void autoXSpringPaginatedDetectsAllThreeParams() throws IOException {
.assertParameter("pageable").hasType("Pageable");
}

@Test
public void autoXSpringPaginatedResolvesOas31ReferencedParameters() throws IOException {
Map<String, Object> props = new HashMap<>();
props.put(INTERFACE_ONLY, "true");
props.put(SpringCodegen.SKIP_DEFAULT_INTERFACE, "true");
props.put(SpringCodegen.USE_TAGS, "true");
props.put(SpringCodegen.USE_SPRING_BOOT3, "true");
props.put(SpringCodegen.AUTO_X_SPRING_PAGINATED, "true");
props.put(SpringCodegen.GENERATE_PAGEABLE_CONSTRAINT_VALIDATION, "true");
props.put(SpringCodegen.GENERATE_SORT_VALIDATION, "true");

Map<String, File> files = generateFromContract(
"src/test/resources/3_1/spring/issue_24719.yaml", SPRING_BOOT, props);

assertThat(files).containsKeys("ValidPageable.java", "ValidSort.java");
JavaFileAssert.assertThat(files.get("ItemsApi.java"))
.assertMethod("listItems")
.doesNotHaveParameter("page")
.doesNotHaveParameter("size")
.doesNotHaveParameter("sort")
.assertParameter("pageable")
.hasType("Pageable");
JavaFileAssert.assertThat(files.get("ItemsApi.java"))
.fileContains("@PageableDefault(page = 0, size = 20)")
.fileContains("@SortDefault.SortDefaults({@SortDefault(sort = {\"name\"}, direction = Sort.Direction.DESC)})")
.fileContains("@ValidPageable(")
.fileContains("maxSize = 100")
.fileContains("minSize = 1")
.fileContains("minPage = 0")
.fileContains("@ValidSort(allowedValues = {\"name,asc\", \"name,desc\"})");
}

@Test
public void autoXSpringPaginatedManualFalseTakesPrecedence() throws IOException {
Map<String, Object> props = new HashMap<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4635,6 +4635,38 @@ public void generatePageableConstraintValidationDoesNotGenerateFileWhenBeanValid

// ========== AUTO X-SPRING-PAGINATED TESTS ==========

@Test
public void autoXSpringPaginatedResolvesOas31ReferencedParameters() throws Exception {
Map<String, Object> additionalProperties = new HashMap<>();
additionalProperties.put(USE_TAGS, "true");
additionalProperties.put(DOCUMENTATION_PROVIDER, "springdoc");
additionalProperties.put(INTERFACE_ONLY, "true");
additionalProperties.put(SKIP_DEFAULT_INTERFACE, "true");
additionalProperties.put(AUTO_X_SPRING_PAGINATED, "true");
additionalProperties.put(GENERATE_PAGEABLE_CONSTRAINT_VALIDATION, "true");
additionalProperties.put(GENERATE_SORT_VALIDATION, "true");

Map<String, File> files = generateFromContract(
"src/test/resources/3_1/spring/issue_24719.yaml", additionalProperties);

Assert.assertNotNull(files.get("ValidPageable.kt"));
Assert.assertNotNull(files.get("ValidSort.kt"));
File itemsApi = files.get("ItemsApi.kt");
assertFileContains(itemsApi.toPath(),
"pageable: Pageable",
"@PageableDefault(page = 0, size = 20)",
"@SortDefault.SortDefaults(SortDefault(sort = [\"name\"], direction = Sort.Direction.DESC))",
"@ValidPageable(",
"maxSize = 100",
"minSize = 1",
"minPage = 0",
"@ValidSort(allowedValues = [\"name,asc\", \"name,desc\"])");
assertFileNotContains(itemsApi.toPath(),
"@RequestParam(value = \"page\"",
"@RequestParam(value = \"size\"",
"@RequestParam(value = \"sort\"");
}

@Test
public void generatePageableConstraintValidationResolvesMaximumFromAllOfRef() throws Exception {
Map<String, Object> additionalProperties = new HashMap<>();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.openapitools.codegen.languages;

import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.PathItem;
Expand Down Expand Up @@ -362,6 +363,70 @@ public void applyAutoXSpringPaginatedIfNeeded_noParams_doesNotSetExtension() {
assertThat(op.getExtensions()).isNull();
}

@Test
public void referencedParameters_areResolvedForDetectionAndScans() {
Schema<?> pageSchema = new IntegerSchema()
.minimum(BigDecimal.ZERO)
._default(0);
Schema<?> sizeSchema = new IntegerSchema()
.minimum(BigDecimal.ONE)
.maximum(BigDecimal.valueOf(100))
._default(20);
Schema<?> sortSchema = new ArraySchema()
.items(new StringSchema()._enum(List.of("name,asc", "name,desc")))
._default(List.of("name,desc"));

Components components = new Components()
.addParameters("Page", new Parameter().name("page").in("query").schema(pageSchema))
.addParameters("Size", new Parameter().name("size").in("query").schema(sizeSchema))
.addParameters("Sort", new Parameter().name("sort").in("query").schema(sortSchema));

Operation operation = new Operation()
.operationId("listItems")
.addParametersItem(new Parameter().$ref("#/components/parameters/Page"))
.addParametersItem(new Parameter().$ref("#/components/parameters/Size"))
.addParametersItem(new Parameter().$ref("#/components/parameters/Sort"));
OpenAPI openAPI = new OpenAPI()
.components(components)
.paths(new Paths().addPathItem("/items", new PathItem().get(operation)));

assertThat(SpringPageableScanUtils.applyAutoXSpringPaginatedIfNeeded(openAPI, operation, true))
.isTrue();
assertThat(operation.getExtensions()).containsEntry("x-spring-paginated", Boolean.TRUE);

Map<String, SpringPageableScanUtils.PageableDefaultsData> defaults =
SpringPageableScanUtils.scanPageableDefaults(openAPI, true);
assertThat(defaults).containsKey("listItems");
assertThat(defaults.get("listItems").page).isEqualTo(0);
assertThat(defaults.get("listItems").size).isEqualTo(20);
assertThat(defaults.get("listItems").sortDefaults)
.extracting(defaultValue -> defaultValue.field, defaultValue -> defaultValue.direction)
.containsExactly(org.assertj.core.groups.Tuple.tuple("name", "DESC"));

Map<String, SpringPageableScanUtils.PageableConstraintsData> constraints =
SpringPageableScanUtils.scanPageableConstraints(openAPI, true);
assertThat(constraints).containsKey("listItems");
assertThat(constraints.get("listItems").minPage).isEqualTo(0);
assertThat(constraints.get("listItems").minSize).isEqualTo(1);
assertThat(constraints.get("listItems").maxSize).isEqualTo(100);

assertThat(SpringPageableScanUtils.scanSortValidationEnums(openAPI, true))
.containsEntry("listItems", List.of("name,asc", "name,desc"));
}

@Test
public void unresolvedParameterReferences_doNotTriggerAutoDetection() {
Operation operation = new Operation()
.addParametersItem(new Parameter().$ref("#/components/parameters/Page"))
.addParametersItem(new Parameter().$ref("#/components/parameters/Size"))
.addParametersItem(new Parameter().$ref("#/components/parameters/Sort"));
OpenAPI openAPI = new OpenAPI().components(new Components());

assertThat(SpringPageableScanUtils.applyAutoXSpringPaginatedIfNeeded(openAPI, operation, true))
.isFalse();
assertThat(operation.getExtensions()).isNull();
}

// -------------------------------------------------------------------------
// applyPageableAnnotations
// -------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
openapi: 3.1.0
info:
title: Referenced pageable parameters
version: 1.0.0
paths:
/items:
get:
tags:
- items
operationId: listItems
parameters:
- $ref: '#/components/parameters/Page'
- $ref: '#/components/parameters/Size'
- $ref: '#/components/parameters/Sort'
responses:
'200':
description: OK
content:
application/json:
schema:
type: array
items:
type: string
components:
parameters:
Page:
name: page
in: query
required: false
schema:
type: integer
format: int32
minimum: 0
default: 0
Size:
name: size
in: query
required: false
schema:
type: integer
format: int32
minimum: 1
maximum: 100
default: 20
Sort:
name: sort
in: query
required: false
schema:
type: array
default:
- name,desc
items:
type: string
enum:
- name,asc
- name,desc
Loading