1717package org .openapitools .codegen .languages ;
1818
1919import com .google .common .collect .ImmutableMap ;
20- import com .fasterxml .jackson .databind .node .ArrayNode ;
2120import com .samskivert .mustache .Mustache ;
2221import com .samskivert .mustache .Mustache .Lambda ;
2322import com .samskivert .mustache .Template ;
@@ -103,6 +102,7 @@ public class KotlinSpringServerCodegen extends AbstractKotlinCodegen
103102 public static final String USE_REQUEST_MAPPING_ON_INTERFACE = "useRequestMappingOnInterface" ;
104103 public static final String AUTO_X_SPRING_PAGINATED = "autoXSpringPaginated" ;
105104 public static final String GENERATE_SORT_VALIDATION = "generateSortValidation" ;
105+ public static final String GENERATE_PAGEABLE_CONSTRAINT_VALIDATION = "generatePageableConstraintValidation" ;
106106 public static final String USE_SEALED_RESPONSE_INTERFACES = "useSealedResponseInterfaces" ;
107107 public static final String COMPANION_OBJECT = "companionObject" ;
108108
@@ -170,6 +170,7 @@ public String getDescription() {
170170 @ Setter private boolean useResponseEntity = true ;
171171 @ Setter private boolean autoXSpringPaginated = false ;
172172 @ Setter private boolean generateSortValidation = false ;
173+ @ Setter private boolean generatePageableConstraintValidation = false ;
173174 @ Setter private boolean useSealedResponseInterfaces = false ;
174175 @ Setter private boolean companionObject = false ;
175176
@@ -190,7 +191,10 @@ public String getDescription() {
190191 private Map <String , List <String >> sortValidationEnums = new HashMap <>();
191192
192193 // Map from operationId to pageable defaults for @PageableDefault/@SortDefault annotation generation
193- private Map <String , PageableDefaultsData > pageableDefaultsRegistry = new HashMap <>();
194+ private Map <String , SpringPageableScanUtils .PageableDefaultsData > pageableDefaultsRegistry = new HashMap <>();
195+
196+ // Map from operationId to pageable constraints for @ValidPageable annotation generation
197+ private Map <String , SpringPageableScanUtils .PageableConstraintsData > pageableConstraintsRegistry = new HashMap <>();
194198
195199 public KotlinSpringServerCodegen () {
196200 super ();
@@ -285,6 +289,7 @@ public KotlinSpringServerCodegen() {
285289 addOption (SCHEMA_IMPLEMENTS_FIELDS , "A map of single field or a list of fields per schema name that should be prepended with `override` (serves similar purpose as `x-kotlin-implements-fields`, but is fully decoupled from the api spec). Example: yaml `schemaImplementsFields: {Pet: id, Category: [name, id], Dog: [bark, breed]}` marks fields to be prepended with `override` in schemas `Pet` (field `id`), `Category` (fields `name`, `id`) and `Dog` (fields `bark`, `breed`)" , "empty map" );
286290 addSwitch (AUTO_X_SPRING_PAGINATED , "Automatically add x-spring-paginated to operations that have 'page', 'size', and 'sort' query parameters. When enabled, operations with all three parameters will have Pageable support automatically applied. Operations with x-spring-paginated explicitly set to false will not be auto-detected." , autoXSpringPaginated );
287291 addSwitch (GENERATE_SORT_VALIDATION , "Generate a @ValidSort annotation and SortValidator class, and apply @ValidSort to paginated operations whose 'sort' parameter has enum values. Requires useBeanValidation=true and library=spring-boot." , generateSortValidation );
292+ addSwitch (GENERATE_PAGEABLE_CONSTRAINT_VALIDATION , "Generate a @ValidPageable annotation and PageableConstraintValidator class, and apply @ValidPageable to paginated operations whose 'page' or 'size' parameter has a maximum constraint. Requires useBeanValidation=true and library=spring-boot." , generatePageableConstraintValidation );
288293 addSwitch (COMPANION_OBJECT , "Whether to generate companion objects in data classes, enabling companion extensions." , companionObject );
289294 supportedLibraries .put (SPRING_BOOT , "Spring-boot Server application." );
290295 supportedLibraries .put (SPRING_CLOUD_LIBRARY ,
@@ -721,6 +726,10 @@ public void processOpts() {
721726 this .setGenerateSortValidation (convertPropertyToBoolean (GENERATE_SORT_VALIDATION ));
722727 }
723728 writePropertyBack (GENERATE_SORT_VALIDATION , generateSortValidation );
729+ if (additionalProperties .containsKey (GENERATE_PAGEABLE_CONSTRAINT_VALIDATION ) && library .equals (SPRING_BOOT )) {
730+ this .setGeneratePageableConstraintValidation (convertPropertyToBoolean (GENERATE_PAGEABLE_CONSTRAINT_VALIDATION ));
731+ }
732+ writePropertyBack (GENERATE_PAGEABLE_CONSTRAINT_VALIDATION , generatePageableConstraintValidation );
724733 if (isUseSpringBoot3 () && isUseSpringBoot4 ()) {
725734 throw new IllegalArgumentException ("Choose between Spring Boot 3 and Spring Boot 4" );
726735 }
@@ -1059,9 +1068,18 @@ public CodegenOperation fromOperation(String path, String httpMethod, Operation
10591068 }
10601069
10611070 // #8315 Remove matching Spring Data Web default query params if 'x-spring-paginated' with Pageable is used
1062- // Build pageable parameter annotations (@ValidSort, @PageableDefault, @SortDefault.SortDefaults)
1071+ // Build pageable parameter annotations (@ValidPageable, @ ValidSort, @PageableDefault, @SortDefault.SortDefaults)
10631072 List <String > pageableAnnotations = new ArrayList <>();
10641073
1074+ if (generatePageableConstraintValidation && useBeanValidation && pageableConstraintsRegistry .containsKey (codegenOperation .operationId )) {
1075+ SpringPageableScanUtils .PageableConstraintsData constraints = pageableConstraintsRegistry .get (codegenOperation .operationId );
1076+ List <String > attrs = new ArrayList <>();
1077+ if (constraints .maxSize >= 0 ) attrs .add ("maxSize = " + constraints .maxSize );
1078+ if (constraints .maxPage >= 0 ) attrs .add ("maxPage = " + constraints .maxPage );
1079+ pageableAnnotations .add ("@ValidPageable(" + String .join (", " , attrs ) + ")" );
1080+ codegenOperation .imports .add ("ValidPageable" );
1081+ }
1082+
10651083 if (generateSortValidation && useBeanValidation && sortValidationEnums .containsKey (codegenOperation .operationId )) {
10661084 List <String > allowedSortValues = sortValidationEnums .get (codegenOperation .operationId );
10671085 String allowedValuesStr = allowedSortValues .stream ()
@@ -1073,7 +1091,7 @@ public CodegenOperation fromOperation(String path, String httpMethod, Operation
10731091
10741092 // Generate @PageableDefault / @SortDefault.SortDefaults annotations if defaults are present
10751093 if (pageableDefaultsRegistry .containsKey (codegenOperation .operationId )) {
1076- PageableDefaultsData defaults = pageableDefaultsRegistry .get (codegenOperation .operationId );
1094+ SpringPageableScanUtils . PageableDefaultsData defaults = pageableDefaultsRegistry .get (codegenOperation .operationId );
10771095
10781096 if (defaults .page != null || defaults .size != null ) {
10791097 List <String > attrs = new ArrayList <>();
@@ -1113,11 +1131,30 @@ public void preprocessOpenAPI(OpenAPI openAPI) {
11131131 }
11141132
11151133 if (SPRING_BOOT .equals (library ) && generateSortValidation && useBeanValidation ) {
1116- scanSortValidationEnums (openAPI );
1134+ sortValidationEnums = SpringPageableScanUtils .scanSortValidationEnums (openAPI , autoXSpringPaginated );
1135+ if (!sortValidationEnums .isEmpty ()) {
1136+ importMapping .putIfAbsent ("ValidSort" , configPackage + ".ValidSort" );
1137+ supportingFiles .add (new SupportingFile ("validSort.mustache" ,
1138+ (sourceFolder + File .separator + configPackage ).replace ("." , File .separator ), "ValidSort.kt" ));
1139+ }
11171140 }
11181141
11191142 if (SPRING_BOOT .equals (library )) {
1120- scanPageableDefaults (openAPI );
1143+ pageableDefaultsRegistry = SpringPageableScanUtils .scanPageableDefaults (openAPI , autoXSpringPaginated );
1144+ if (!pageableDefaultsRegistry .isEmpty ()) {
1145+ importMapping .putIfAbsent ("PageableDefault" , "org.springframework.data.web.PageableDefault" );
1146+ importMapping .putIfAbsent ("SortDefault" , "org.springframework.data.web.SortDefault" );
1147+ importMapping .putIfAbsent ("Sort" , "org.springframework.data.domain.Sort" );
1148+ }
1149+ }
1150+
1151+ if (SPRING_BOOT .equals (library ) && generatePageableConstraintValidation && useBeanValidation ) {
1152+ pageableConstraintsRegistry = SpringPageableScanUtils .scanPageableConstraints (openAPI , autoXSpringPaginated );
1153+ if (!pageableConstraintsRegistry .isEmpty ()) {
1154+ importMapping .putIfAbsent ("ValidPageable" , configPackage + ".ValidPageable" );
1155+ supportingFiles .add (new SupportingFile ("validPageable.mustache" ,
1156+ (sourceFolder + File .separator + configPackage ).replace ("." , File .separator ), "ValidPageable.kt" ));
1157+ }
11211158 }
11221159
11231160 if (!additionalProperties .containsKey (TITLE )) {
@@ -1186,184 +1223,11 @@ public void preprocessOpenAPI(OpenAPI openAPI) {
11861223 }
11871224
11881225 /**
1189- * Scans the OpenAPI spec for pageable operations whose page/size/sort parameters have default values,
1190- * builds the {@link #pageableDefaultsRegistry}, and registers required import mappings.
1191- * Called from {@link #preprocessOpenAPI} for all spring-boot generations.
1192- */
1193- private void scanPageableDefaults (OpenAPI openAPI ) {
1194- if (openAPI .getPaths () == null ) {
1195- return ;
1196- }
1197- for (Map .Entry <String , PathItem > pathEntry : openAPI .getPaths ().entrySet ()) {
1198- for (Operation operation : pathEntry .getValue ().readOperations ()) {
1199- String operationId = operation .getOperationId ();
1200- if (operationId == null || !willBePageable (operation )) {
1201- continue ;
1202- }
1203- if (operation .getParameters () == null ) {
1204- continue ;
1205- }
1206- Integer pageDefault = null ;
1207- Integer sizeDefault = null ;
1208- List <SortFieldDefault > sortDefaults = new ArrayList <>();
1209-
1210- for (Parameter param : operation .getParameters ()) {
1211- Schema <?> schema = param .getSchema ();
1212- if (schema == null ) {
1213- continue ;
1214- }
1215- if (schema .get$ref () != null ) {
1216- schema = ModelUtils .getReferencedSchema (openAPI , schema );
1217- }
1218- if (schema == null || schema .getDefault () == null ) {
1219- continue ;
1220- }
1221- Object defaultValue = schema .getDefault ();
1222- switch (param .getName ()) {
1223- case "page" :
1224- if (defaultValue instanceof Number ) {
1225- pageDefault = ((Number ) defaultValue ).intValue ();
1226- }
1227- break ;
1228- case "size" :
1229- if (defaultValue instanceof Number ) {
1230- sizeDefault = ((Number ) defaultValue ).intValue ();
1231- }
1232- break ;
1233- case "sort" :
1234- List <String > sortValues = new ArrayList <>();
1235- if (defaultValue instanceof String ) {
1236- sortValues .add ((String ) defaultValue );
1237- } else if (defaultValue instanceof ArrayNode ) {
1238- ((ArrayNode ) defaultValue ).forEach (node -> sortValues .add (node .asText ()));
1239- } else if (defaultValue instanceof List ) {
1240- for (Object item : (List <?>) defaultValue ) {
1241- sortValues .add (item .toString ());
1242- }
1243- }
1244- for (String sortStr : sortValues ) {
1245- String [] parts = sortStr .split ("," , 2 );
1246- String field = parts [0 ].trim ();
1247- String direction = parts .length > 1 ? parts [1 ].trim ().toUpperCase (Locale .ROOT ) : "ASC" ;
1248- sortDefaults .add (new SortFieldDefault (field , direction ));
1249- }
1250- break ;
1251- default :
1252- break ;
1253- }
1254- }
1255-
1256- PageableDefaultsData data = new PageableDefaultsData (pageDefault , sizeDefault , sortDefaults );
1257- if (data .hasAny ()) {
1258- pageableDefaultsRegistry .put (operationId , data );
1259- }
1260- }
1261- }
1262- if (!pageableDefaultsRegistry .isEmpty ()) {
1263- importMapping .putIfAbsent ("PageableDefault" , "org.springframework.data.web.PageableDefault" );
1264- importMapping .putIfAbsent ("SortDefault" , "org.springframework.data.web.SortDefault" );
1265- importMapping .putIfAbsent ("Sort" , "org.springframework.data.domain.Sort" );
1266- }
1267- }
1268-
1269- /** Carries a parsed sort field and its direction (always "ASC" or "DESC") from the spec default. */
1270- private static final class SortFieldDefault {
1271- final String field ;
1272- final String direction ;
1273-
1274- SortFieldDefault (String field , String direction ) {
1275- this .field = field ;
1276- this .direction = direction ;
1277- }
1278- }
1279-
1280- /** Carries parsed default values for page, size, and sort fields from a pageable operation. */
1281- private static final class PageableDefaultsData {
1282- final Integer page ;
1283- final Integer size ;
1284- final List <SortFieldDefault > sortDefaults ;
1285-
1286- PageableDefaultsData (Integer page , Integer size , List <SortFieldDefault > sortDefaults ) {
1287- this .page = page ;
1288- this .size = size ;
1289- this .sortDefaults = sortDefaults ;
1290- }
1291-
1292- boolean hasAny () {
1293- return page != null || size != null || !sortDefaults .isEmpty ();
1294- }
1295- }
1296-
1297- /**
1298- * Scans the OpenAPI spec for paginated operations whose 'sort' parameter has enum values,
1299- * builds the {@link #sortValidationEnums} registry, and registers the ValidSort.kt supporting file.
1300- * Called from {@link #preprocessOpenAPI} when {@code generateSortValidation} is enabled.
1301- */
1302- private void scanSortValidationEnums (OpenAPI openAPI ) {
1303- if (openAPI .getPaths () == null ) {
1304- return ;
1305- }
1306- boolean foundAny = false ;
1307- for (Map .Entry <String , PathItem > pathEntry : openAPI .getPaths ().entrySet ()) {
1308- for (Operation operation : pathEntry .getValue ().readOperations ()) {
1309- String operationId = operation .getOperationId ();
1310- if (operationId == null || !willBePageable (operation )) {
1311- continue ;
1312- }
1313- if (operation .getParameters () == null ) {
1314- continue ;
1315- }
1316- for (Parameter param : operation .getParameters ()) {
1317- if (!"sort" .equals (param .getName ())) {
1318- continue ;
1319- }
1320- Schema <?> schema = param .getSchema ();
1321- if (schema == null ) {
1322- continue ;
1323- }
1324- if (schema .get$ref () != null ) {
1325- schema = ModelUtils .getReferencedSchema (openAPI , schema );
1326- }
1327- if (schema == null || schema .getEnum () == null || schema .getEnum ().isEmpty ()) {
1328- continue ;
1329- }
1330- List <String > enumValues = schema .getEnum ().stream ()
1331- .map (Object ::toString )
1332- .collect (Collectors .toList ());
1333- sortValidationEnums .put (operationId , enumValues );
1334- foundAny = true ;
1335- }
1336- }
1337- }
1338- if (foundAny ) {
1339- importMapping .putIfAbsent ("ValidSort" , configPackage + ".ValidSort" );
1340- supportingFiles .add (new SupportingFile ("validSort.mustache" ,
1341- (sourceFolder + File .separator + configPackage ).replace ("." , File .separator ), "ValidSort.kt" ));
1342- }
1343- }
1344-
1345- /**
1346- * Returns true if the given operation will have a Pageable parameter injected — either because
1347- * it has {@code x-spring-paginated: true} explicitly, or because {@link #autoXSpringPaginated}
1348- * is enabled and the operation has all three default pagination query parameters (page, size, sort).
1226+ * Returns true if the given operation will have a Pageable parameter injected.
1227+ * Delegates to {@link SpringPageableScanUtils#willBePageable}.
13491228 */
13501229 private boolean willBePageable (Operation operation ) {
1351- if (operation .getExtensions () != null ) {
1352- Object paginated = operation .getExtensions ().get ("x-spring-paginated" );
1353- if (Boolean .FALSE .equals (paginated )) {
1354- return false ;
1355- }
1356- if (Boolean .TRUE .equals (paginated )) {
1357- return true ;
1358- }
1359- }
1360- if (autoXSpringPaginated && operation .getParameters () != null ) {
1361- Set <String > paramNames = operation .getParameters ().stream ()
1362- .map (Parameter ::getName )
1363- .collect (Collectors .toSet ());
1364- return paramNames .containsAll (Arrays .asList ("page" , "size" , "sort" ));
1365- }
1366- return false ;
1230+ return SpringPageableScanUtils .willBePageable (operation , autoXSpringPaginated );
13671231 }
13681232
13691233 @ Override
0 commit comments