diff --git a/docs/generators/java-camel.md b/docs/generators/java-camel.md index 1bef17ecf9c2..6ddd7bb96cad 100644 --- a/docs/generators/java-camel.md +++ b/docs/generators/java-camel.md @@ -104,7 +104,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |sortParamsByRequiredFlag|Sort method arguments to place required parameters before optional parameters.| |true| |sourceFolder|source folder for generated code| |src/main/java| |springApiVersion|Value for 'version' attribute in @RequestMapping (for Spring 7 and above).| |null| -|substituteGenericPagedModel|Detect schemas that represent paginated responses (an object with a 'content' array property and a pagination-metadata property) and replace their generated references with org.springframework.data.web.PagedModel<T>. The detected page schemas and the pagination metadata schema are suppressed from code generation. Only applies when library=spring-boot.| |false| +|substituteGenericPagedModel|Detect schemas that represent paginated responses (an object with a 'content' array property and a 'page' pagination-metadata property) and replace their generated references with PagedModel<T>. By default this uses a generated type in the config package (default 'org.openapitools.configuration'), but `importMappings.PagedModel` can override it to a custom/FQCN-mapped type. The detected page schemas and the pagination metadata schema are suppressed from code generation. Only applies when library=spring-boot.| |false| |testOutput|Set output folder for models and APIs tests| |${project.build.directory}/generated-test-sources/openapi| |title|server title name or client service name| |OpenAPI Spring| |unhandledException|Declare operation methods to throw a generic exception and allow unhandled exceptions (useful for Spring `@ControllerAdvice` directives).| |false| diff --git a/docs/generators/kotlin-spring.md b/docs/generators/kotlin-spring.md index 39bcf6c6d463..9177ce41915c 100644 --- a/docs/generators/kotlin-spring.md +++ b/docs/generators/kotlin-spring.md @@ -58,7 +58,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |sortModelPropertiesByRequiredFlag|Sort model properties to place required parameters before optional parameters.| |null| |sortParamsByRequiredFlag|Sort method arguments to place required parameters before optional parameters.| |null| |sourceFolder|source folder for generated code| |src/main/kotlin| -|substituteGenericPagedModel|Detect schemas that represent paginated responses (an object with a 'content' array property and a pagination-metadata property) and replace their generated references with org.springframework.data.web.PagedModel<T>. The detected page schemas and the pagination metadata schema are suppressed from code generation. Only applies when library=spring-boot.| |false| +|substituteGenericPagedModel|Detect schemas that represent paginated responses (an object with a 'content' array property and a 'page' pagination-metadata property) and replace their generated references with PagedModel<T>. By default this uses a generated type in the config package (default 'org.openapitools.configuration'), but `importMappings.PagedModel` can override it to a custom/FQCN-mapped type. The detected page schemas and the pagination metadata schema are suppressed from code generation. Only applies when library=spring-boot.| |false| |title|server title name or client service name| |OpenAPI Kotlin Spring| |useBeanValidation|Use BeanValidation API annotations to validate data types| |true| |useFeignClientUrl|Whether to generate Feign client with url parameter.| |true| diff --git a/docs/generators/spring.md b/docs/generators/spring.md index c674a42c6099..76bd12c9f43b 100644 --- a/docs/generators/spring.md +++ b/docs/generators/spring.md @@ -97,7 +97,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |sortParamsByRequiredFlag|Sort method arguments to place required parameters before optional parameters.| |true| |sourceFolder|source folder for generated code| |src/main/java| |springApiVersion|Value for 'version' attribute in @RequestMapping (for Spring 7 and above).| |null| -|substituteGenericPagedModel|Detect schemas that represent paginated responses (an object with a 'content' array property and a pagination-metadata property) and replace their generated references with org.springframework.data.web.PagedModel<T>. The detected page schemas and the pagination metadata schema are suppressed from code generation. Only applies when library=spring-boot.| |false| +|substituteGenericPagedModel|Detect schemas that represent paginated responses (an object with a 'content' array property and a 'page' pagination-metadata property) and replace their generated references with PagedModel<T>. By default this uses a generated type in the config package (default 'org.openapitools.configuration'), but `importMappings.PagedModel` can override it to a custom/FQCN-mapped type. The detected page schemas and the pagination metadata schema are suppressed from code generation. Only applies when library=spring-boot.| |false| |testOutput|Set output folder for models and APIs tests| |${project.build.directory}/generated-test-sources/openapi| |title|server title name or client service name| |OpenAPI Spring| |unhandledException|Declare operation methods to throw a generic exception and allow unhandled exceptions (useful for Spring `@ControllerAdvice` directives).| |false| diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinSpringServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinSpringServerCodegen.java index 9c1e9478b7ec..9c5c8a798691 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinSpringServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinSpringServerCodegen.java @@ -200,6 +200,8 @@ public String getDescription() { // Map from schema name to detected paged-model info (populated when substituteGenericPagedModel=true) private Map pagedModelRegistry = new HashMap<>(); + // Simple class name of the PagedModel substitute (derived from importMapping; defaults to "PagedModel") + private String pagedModelClassName = "PagedModel"; public KotlinSpringServerCodegen() { super(); @@ -296,9 +298,9 @@ public KotlinSpringServerCodegen() { addSwitch(GENERATE_SORT_VALIDATION, "Generate a @ValidSort annotation and SortValidator class, and apply @ValidSort to the injected Pageable parameter of operations whose 'sort' parameter has enum values. The annotation validates that sort values in the Pageable object match the allowed enum values from the spec. Requires useBeanValidation=true and library=spring-boot.", generateSortValidation); addSwitch(GENERATE_PAGEABLE_CONSTRAINT_VALIDATION, "Generate a @ValidPageable annotation and PageableConstraintValidator class, and apply @ValidPageable to the injected Pageable parameter of operations whose 'page' or 'size' parameter specifies a maximum constraint. The annotation enforces those constraints on the Pageable object that replaces the individual page/size query parameters. Requires useBeanValidation=true and library=spring-boot.", generatePageableConstraintValidation); addSwitch(SUBSTITUTE_GENERIC_PAGED_MODEL, - "Detect schemas that represent paginated responses (an object with a 'content' array property and a " + "Detect schemas that represent paginated responses (an object with a 'content' array property and a 'page' " + "pagination-metadata property) and replace their generated references with " - + "org.springframework.data.web.PagedModel. The detected page schemas and the pagination metadata " + + "PagedModel. By default this uses a generated type in the config package (default 'org.openapitools.configuration'), but `importMappings.PagedModel` can override it to a custom/FQCN-mapped type. The detected page schemas and the pagination metadata " + "schema are suppressed from code generation. Only applies when library=spring-boot.", substituteGenericPagedModel); addSwitch(COMPANION_OBJECT, "Whether to generate companion objects in data classes, enabling companion extensions.", companionObject); @@ -1142,21 +1144,24 @@ public CodegenOperation fromOperation(String path, String httpMethod, Operation pagedModelRegistry.get(codegenOperation.returnBaseType); if (detected != null) { String oldType = codegenOperation.returnType; - String newBaseType = "PagedModel<" + detected.itemSchemaName + ">"; + // Run through toModelName so that schemaMappings (e.g. User → com.example.MyUser) + // are honored: the mapped name is used both in the type arg and for import resolution. + String itemType = toModelName(detected.itemSchemaName); + String newBaseType = pagedModelClassName + "<" + itemType + ">"; codegenOperation.returnType = newBaseType; - codegenOperation.returnBaseType = "PagedModel"; + codegenOperation.returnBaseType = pagedModelClassName; // Clear any container flag — PagedModel is not itself a List/array codegenOperation.returnContainer = null; - // Add item type import (needed for PagedModel in method signature) - codegenOperation.imports.add(detected.itemSchemaName); - codegenOperation.imports.add("PagedModel"); + // Add item type import (needed for PagedModel in method signature) + codegenOperation.imports.add(itemType); + codegenOperation.imports.add(pagedModelClassName); // Remove paged schema import when no annotations are generated — // the class is suppressed and not referenced anywhere if (getAnnotationLibrary() == AnnotationLibrary.NONE) { codegenOperation.imports.remove(detected.schemaName); } - LOGGER.info("substituteGenericPagedModel: operation '{}': replacing return type '{}' with PagedModel<{}>", - codegenOperation.operationId, oldType, detected.itemSchemaName); + LOGGER.info("substituteGenericPagedModel: operation '{}': replacing return type '{}' with {}<{}>", + codegenOperation.operationId, oldType, pagedModelClassName, itemType); } } @@ -1202,7 +1207,22 @@ public void preprocessOpenAPI(OpenAPI openAPI) { if (SPRING_BOOT.equals(library) && substituteGenericPagedModel) { pagedModelRegistry = PagedModelScanUtils.scanPagedModels(openAPI); if (!pagedModelRegistry.isEmpty()) { - importMapping.putIfAbsent("PagedModel", "org.springframework.data.web.PagedModel"); + boolean customMapping = importMapping.containsKey("PagedModel"); + importMapping.putIfAbsent("PagedModel", configPackage + ".PagedModel"); + if (!customMapping) { + // No custom class provided — generate the simple PagedModel into the config package. + supportingFiles.add(new SupportingFile("pagedModel.mustache", + (sourceFolder + File.separator + configPackage).replace(".", File.separator), "PagedModel.kt")); + } + // Derive the actual simple class name from the FQN in importMapping so that a + // custom mapping (e.g. "PagedModel" → "com.example.MyPagedModel") is respected. + // The simple name of the FQN becomes the token used in generated code, and is + // registered in importMapping so that template import resolution works. + String fqn = importMapping.get("PagedModel"); + pagedModelClassName = fqn.substring(fqn.lastIndexOf('.') + 1); + if (!pagedModelClassName.equals("PagedModel")) { + importMapping.putIfAbsent(pagedModelClassName, fqn); + } LOGGER.info("substituteGenericPagedModel: detected {} paged-model schema(s): {}", pagedModelRegistry.size(), pagedModelRegistry.keySet()); } diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java index 81e1bb177c01..86c81fbd4ce8 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java @@ -203,6 +203,8 @@ public enum RequestMappingMode { private Map pageableConstraintsRegistry = new HashMap<>(); // Map from schema name to detected paged-model info (populated when substituteGenericPagedModel=true) private Map pagedModelRegistry = new HashMap<>(); + // Simple class name of the PagedModel substitute (derived from importMapping; defaults to "PagedModel") + private String pagedModelClassName = "PagedModel"; public SpringCodegen() { super(); @@ -374,9 +376,9 @@ public SpringCodegen() { + "Requires useBeanValidation=true and library=spring-boot.", generatePageableConstraintValidation)); cliOptions.add(CliOption.newBoolean(SUBSTITUTE_GENERIC_PAGED_MODEL, - "Detect schemas that represent paginated responses (an object with a 'content' array property and a " + "Detect schemas that represent paginated responses (an object with a 'content' array property and a 'page' " + "pagination-metadata property) and replace their generated references with " - + "org.springframework.data.web.PagedModel. The detected page schemas and the pagination metadata " + + "PagedModel. By default this uses a generated type in the config package (default 'org.openapitools.configuration'), but `importMappings.PagedModel` can override it to a custom/FQCN-mapped type. The detected page schemas and the pagination metadata " + "schema are suppressed from code generation. Only applies when library=spring-boot.", substituteGenericPagedModel)); @@ -870,7 +872,22 @@ public void preprocessOpenAPI(OpenAPI openAPI) { if (SPRING_BOOT.equals(library) && substituteGenericPagedModel) { pagedModelRegistry = PagedModelScanUtils.scanPagedModels(openAPI); if (!pagedModelRegistry.isEmpty()) { - importMapping.putIfAbsent("PagedModel", "org.springframework.data.web.PagedModel"); + boolean customMapping = importMapping.containsKey("PagedModel"); + importMapping.putIfAbsent("PagedModel", configPackage + ".PagedModel"); + if (!customMapping) { + // No custom class provided — generate the simple PagedModel into the config package. + supportingFiles.add(new SupportingFile("pagedModel.mustache", + (sourceFolder + File.separator + configPackage).replace(".", java.io.File.separator), "PagedModel.java")); + } + // Derive the actual simple class name from the FQN in importMapping so that a + // custom mapping (e.g. "PagedModel" → "com.example.MyPagedModel") is respected. + // The simple name of the FQN becomes the token used in generated code, and is + // registered in importMapping so that template import resolution works. + String fqn = importMapping.get("PagedModel"); + pagedModelClassName = fqn.substring(fqn.lastIndexOf('.') + 1); + if (!pagedModelClassName.equals("PagedModel")) { + importMapping.put(pagedModelClassName, fqn); + } LOGGER.info("substituteGenericPagedModel: detected {} paged-model schema(s): {}", pagedModelRegistry.size(), pagedModelRegistry.keySet()); } @@ -1352,21 +1369,24 @@ public CodegenOperation fromOperation(String path, String httpMethod, Operation pagedModelRegistry.get(codegenOperation.returnBaseType); if (detected != null) { String oldType = codegenOperation.returnType; - String newBaseType = "PagedModel<" + detected.itemSchemaName + ">"; + // Run through toModelName so that schemaMappings (e.g. User → com.example.MyUser) + // are honored: the mapped name is used both in the type arg and for import resolution. + String itemType = toModelName(detected.itemSchemaName); + String newBaseType = pagedModelClassName + "<" + itemType + ">"; codegenOperation.returnType = newBaseType; - codegenOperation.returnBaseType = "PagedModel"; + codegenOperation.returnBaseType = pagedModelClassName; // Clear any container flag — PagedModel is not itself a List/array codegenOperation.returnContainer = null; - // Add item type import (needed for PagedModel in method signature) - codegenOperation.imports.add(detected.itemSchemaName); - codegenOperation.imports.add("PagedModel"); + // Add item type import (needed for PagedModel in method signature) + codegenOperation.imports.add(itemType); + codegenOperation.imports.add(pagedModelClassName); // Remove paged schema import when no annotations are generated — // the class is suppressed and not referenced anywhere if (getAnnotationLibrary() == AnnotationLibrary.NONE) { codegenOperation.imports.remove(detected.schemaName); } - LOGGER.info("substituteGenericPagedModel: operation '{}': replacing return type '{}' with PagedModel<{}>", - codegenOperation.operationId, oldType, detected.itemSchemaName); + LOGGER.info("substituteGenericPagedModel: operation '{}': replacing return type '{}' with {}<{}>", + codegenOperation.operationId, oldType, pagedModelClassName, itemType); } } diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/pagedModel.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/pagedModel.mustache new file mode 100644 index 000000000000..7bf2059aae37 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/JavaSpring/pagedModel.mustache @@ -0,0 +1,95 @@ +package {{configPackage}}; + +import java.util.List; + +/** + * Simple generic paged response wrapper generated by openapi-generator. + * + *

Holds a page of content items and pagination metadata. Jackson deserializes this from the + * standard Spring {@code Page} JSON shape: + *

+ * {
+ *   "content": [...],
+ *   "page": { "size": 20, "totalElements": 100, "totalPages": 5, "number": 0 }
+ * }
+ * 
+ * + *

To use your own class instead, set {@code importMappings.PagedModel} in the generator config. + */ +public class PagedModel { + + private List content; + private PageMetadata page; + + public PagedModel() {} + + public PagedModel(List content, PageMetadata page) { + this.content = content; + this.page = page; + } + + public List getContent() { + return content; + } + + public void setContent(List content) { + this.content = content; + } + + public PageMetadata getPage() { + return page; + } + + public void setPage(PageMetadata page) { + this.page = page; + } + + public static class PageMetadata { + + private long size; + private long totalElements; + private long totalPages; + private long number; + + public PageMetadata() {} + + public PageMetadata(long size, long totalElements, long totalPages, long number) { + this.size = size; + this.totalElements = totalElements; + this.totalPages = totalPages; + this.number = number; + } + + public long getSize() { + return size; + } + + public void setSize(long size) { + this.size = size; + } + + public long getTotalElements() { + return totalElements; + } + + public void setTotalElements(long totalElements) { + this.totalElements = totalElements; + } + + public long getTotalPages() { + return totalPages; + } + + public void setTotalPages(long totalPages) { + this.totalPages = totalPages; + } + + public long getNumber() { + return number; + } + + public void setNumber(long number) { + this.number = number; + } + } +} diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/pagedModel.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/pagedModel.mustache new file mode 100644 index 000000000000..02aee68605b0 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/pagedModel.mustache @@ -0,0 +1,27 @@ +package {{configPackage}} + +/** + * Simple generic paged response wrapper generated by openapi-generator. + * + * Holds a page of content items and pagination metadata. Jackson deserializes this from the + * standard Spring `Page` JSON shape: + * ```json + * { + * "content": [...], + * "page": { "size": 20, "totalElements": 100, "totalPages": 5, "number": 0 } + * } + * ``` + * + * To use your own class instead, set `importMappings.PagedModel` in the generator config. + */ +data class PagedModel( + val content: List, + val page: PageMetadata, +) { + data class PageMetadata( + val size: Long, + val totalElements: Long, + val totalPages: Long, + val number: Long, + ) +} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java index 573a24198dd6..2b1397ce1b15 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java @@ -7253,9 +7253,9 @@ public void substituteGenericPagedModel_importsPagedModelAndItemTypeInApiFile() Map files = generateFromContract( "src/test/resources/3_0/spring/petstore-paged-model.yaml", SPRING_BOOT, props); - // The api file must import both PagedModel and the item type + // The api file must import both the generated PagedModel and the item type JavaFileAssert.assertThat(files.get("UserApi.java")) - .fileContains("import org.springframework.data.web.PagedModel") + .fileContains("import org.openapitools.configuration.PagedModel") .fileContains("import org.openapitools.model.User"); } @@ -7313,4 +7313,75 @@ public void substituteGenericPagedModel_suppressesPageMetaWhenNoAnnotations() th // PageMeta is referenced by SearchResult (a non-paged schema) → must be kept assertThat(files).containsKey("PageMeta.java"); } + + @Test + public void substituteGenericPagedModel_respectsSchemaMappingForItemType() throws IOException { + // When the item schema (User) is mapped to an external FQN via schemaMappings, + // the PagedModel type arg must use the mapped FQN, not the raw schema name. + Map props = commonPagedModelProps(); + + Map files = generateFromContract( + "src/test/resources/3_0/spring/petstore-paged-model.yaml", SPRING_BOOT, props, + configurator -> configurator.addSchemaMapping("User", "com.example.external.ExternalUser")); + + JavaFileAssert.assertThat(files.get("UserApi.java")) + .assertMethod("listUsers") + .hasReturnType("ResponseEntity>"); + } + + @Test + public void substituteGenericPagedModel_respectsSchemaMappingWithImportMappingForItemType() throws IOException { + // When the item schema (User) is mapped to an external FQN via schemaMappings, + // the PagedModel type arg must use the mapped FQN, not the raw schema name. + Map props = commonPagedModelProps(); + + Map files = generateFromContract( + "src/test/resources/3_0/spring/petstore-paged-model.yaml", SPRING_BOOT, props, + configurator -> configurator + .addSchemaMapping("User", "ExternalUser") + .addImportMapping("ExternalUser", "com.example.external.ExternalUser")); + + JavaFileAssert.assertThat(files.get("UserApi.java")) + .hasImports("com.example.external.ExternalUser") + .assertMethod("listUsers") + .hasReturnType("ResponseEntity>"); + } + + @Test + public void substituteGenericPagedModel_generatesPagedModelSupportingFile() throws IOException { + Map files = generateFromContract( + "src/test/resources/3_0/spring/petstore-paged-model.yaml", SPRING_BOOT, commonPagedModelProps()); + + assertThat(files).containsKey("PagedModel.java"); + } + + @Test + public void substituteGenericPagedModel_doesNotGeneratePagedModelFileWhenCustomMapping() throws IOException { + Map props = commonPagedModelProps(); + + Map files = generateFromContract( + "src/test/resources/3_0/spring/petstore-paged-model.yaml", SPRING_BOOT, props, + configurator -> configurator + .addImportMapping("PagedModel", "com.example.custom.MyPagedModel")); + + assertThat(files).doesNotContainKey("PagedModel.java"); + } + + @Test + public void substituteGenericPagedModel_respectsCustomImportMappingClassName() throws IOException { + // When the user remaps "PagedModel" to a FQN with a different simple class name, + // the generated code must use that simple name (not "PagedModel") as the type token + // and emit the correct import for the custom FQN. + Map props = commonPagedModelProps(); + + Map files = generateFromContract( + "src/test/resources/3_0/spring/petstore-paged-model.yaml", SPRING_BOOT, props, + configurator -> configurator + .addImportMapping("PagedModel", "com.example.custom.MyPagedModel")); + + JavaFileAssert.assertThat(files.get("UserApi.java")) + .hasImports("com.example.custom.MyPagedModel") + .assertMethod("listUsers") + .hasReturnType("ResponseEntity>"); + } } diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java index 26d46750c015..4183e1cdd027 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java @@ -5604,7 +5604,8 @@ public void substituteGenericPagedModel_importsPagedModelAndItemTypeInApiFile() File userApi = files.get("UserApi.kt"); assertThat(userApi).isNotNull(); String content = Files.readString(userApi.toPath()); - assertThat(content).contains("import org.springframework.data.web.PagedModel"); + // The api file must import the generated PagedModel and the item type + assertThat(content).contains("import org.openapitools.configuration.PagedModel"); assertThat(content).contains("import org.openapitools.model.User"); } @@ -5662,4 +5663,62 @@ public void substituteGenericPagedModel_suppressesPageMetaWhenNoAnnotations() th // PageMeta is referenced by SearchResult (a non-paged schema) → must be kept assertThat(files).containsKey("PageMeta.kt"); } + + @Test + public void substituteGenericPagedModel_respectsSchemaMappingForItemType() throws IOException { + // When the item schema (User) is mapped to an external FQN via schemaMappings, + // the PagedModel type arg must use the mapped FQN, not the raw schema name. + Map files = generateFromContract( + "src/test/resources/3_0/spring/petstore-paged-model.yaml", + commonKotlinPagedModelProps(), + new HashMap<>(), + configurator -> configurator.addSchemaMapping("User", "com.example.external.ExternalUser")); + + File userApi = files.get("UserApi.kt"); + assertThat(userApi).isNotNull(); + String content = Files.readString(userApi.toPath()); + // Return type must use the schema-mapped FQN, not the raw schema name + assertThat(content).contains("PagedModel"); + // toModelImport of a dotted name returns the FQN as-is → correct import + assertThat(content).contains("import com.example.external.ExternalUser"); + } + + @Test + public void substituteGenericPagedModel_generatesPagedModelSupportingFile() throws IOException { + Map files = generateFromContract( + "src/test/resources/3_0/spring/petstore-paged-model.yaml", commonKotlinPagedModelProps()); + + assertThat(files).containsKey("PagedModel.kt"); + } + + @Test + public void substituteGenericPagedModel_doesNotGeneratePagedModelFileWhenCustomMapping() throws IOException { + Map files = generateFromContract( + "src/test/resources/3_0/spring/petstore-paged-model.yaml", + commonKotlinPagedModelProps(), + new HashMap<>(), + configurator -> configurator + .addImportMapping("PagedModel", "com.example.custom.MyPagedModel")); + + assertThat(files).doesNotContainKey("PagedModel.kt"); + } + + @Test + public void substituteGenericPagedModel_respectsCustomImportMappingClassName() throws IOException { + // When the user remaps "PagedModel" to a FQN with a different simple class name, + // the generated code must use that simple name (not "PagedModel") as the type token + // and emit the correct import for the custom FQN. + Map files = generateFromContract( + "src/test/resources/3_0/spring/petstore-paged-model.yaml", + commonKotlinPagedModelProps(), + new HashMap<>(), + configurator -> configurator + .addImportMapping("PagedModel", "com.example.custom.MyPagedModel")); + + File userApi = files.get("UserApi.kt"); + assertThat(userApi).isNotNull(); + String content = Files.readString(userApi.toPath()); + assertThat(content).contains("MyPagedModel"); + assertThat(content).contains("import com.example.custom.MyPagedModel"); + } } diff --git a/samples/server/petstore/kotlin-springboot-paged-model/.openapi-generator/FILES b/samples/server/petstore/kotlin-springboot-paged-model/.openapi-generator/FILES index b3da4fa290fd..cf18998de776 100644 --- a/samples/server/petstore/kotlin-springboot-paged-model/.openapi-generator/FILES +++ b/samples/server/petstore/kotlin-springboot-paged-model/.openapi-generator/FILES @@ -12,6 +12,7 @@ src/main/kotlin/org/openapitools/api/OrderApi.kt src/main/kotlin/org/openapitools/api/PetApi.kt src/main/kotlin/org/openapitools/api/UserApi.kt src/main/kotlin/org/openapitools/configuration/EnumConverterConfiguration.kt +src/main/kotlin/org/openapitools/configuration/PagedModel.kt src/main/kotlin/org/openapitools/model/Order.kt src/main/kotlin/org/openapitools/model/PageMeta.kt src/main/kotlin/org/openapitools/model/Pet.kt diff --git a/samples/server/petstore/kotlin-springboot-paged-model/src/main/kotlin/org/openapitools/api/OrderApi.kt b/samples/server/petstore/kotlin-springboot-paged-model/src/main/kotlin/org/openapitools/api/OrderApi.kt index 118371deeb4d..632eb07d8d8d 100644 --- a/samples/server/petstore/kotlin-springboot-paged-model/src/main/kotlin/org/openapitools/api/OrderApi.kt +++ b/samples/server/petstore/kotlin-springboot-paged-model/src/main/kotlin/org/openapitools/api/OrderApi.kt @@ -6,7 +6,7 @@ package org.openapitools.api import org.openapitools.model.Order -import org.springframework.data.web.PagedModel +import org.openapitools.configuration.PagedModel import org.springframework.http.HttpStatus import org.springframework.http.MediaType import org.springframework.http.ResponseEntity diff --git a/samples/server/petstore/kotlin-springboot-paged-model/src/main/kotlin/org/openapitools/api/PetApi.kt b/samples/server/petstore/kotlin-springboot-paged-model/src/main/kotlin/org/openapitools/api/PetApi.kt index 0e79a47e5f4c..ca0a0f67d9ce 100644 --- a/samples/server/petstore/kotlin-springboot-paged-model/src/main/kotlin/org/openapitools/api/PetApi.kt +++ b/samples/server/petstore/kotlin-springboot-paged-model/src/main/kotlin/org/openapitools/api/PetApi.kt @@ -5,7 +5,7 @@ */ package org.openapitools.api -import org.springframework.data.web.PagedModel +import org.openapitools.configuration.PagedModel import org.openapitools.model.Pet import org.openapitools.model.SearchResult import org.springframework.http.HttpStatus diff --git a/samples/server/petstore/kotlin-springboot-paged-model/src/main/kotlin/org/openapitools/api/UserApi.kt b/samples/server/petstore/kotlin-springboot-paged-model/src/main/kotlin/org/openapitools/api/UserApi.kt index bdcd87592940..4361b508a781 100644 --- a/samples/server/petstore/kotlin-springboot-paged-model/src/main/kotlin/org/openapitools/api/UserApi.kt +++ b/samples/server/petstore/kotlin-springboot-paged-model/src/main/kotlin/org/openapitools/api/UserApi.kt @@ -5,7 +5,7 @@ */ package org.openapitools.api -import org.springframework.data.web.PagedModel +import org.openapitools.configuration.PagedModel import org.openapitools.model.User import org.openapitools.model.UserList import org.springframework.http.HttpStatus diff --git a/samples/server/petstore/kotlin-springboot-paged-model/src/main/kotlin/org/openapitools/configuration/PagedModel.kt b/samples/server/petstore/kotlin-springboot-paged-model/src/main/kotlin/org/openapitools/configuration/PagedModel.kt new file mode 100644 index 000000000000..7a67cb221720 --- /dev/null +++ b/samples/server/petstore/kotlin-springboot-paged-model/src/main/kotlin/org/openapitools/configuration/PagedModel.kt @@ -0,0 +1,27 @@ +package org.openapitools.configuration + +/** + * Simple generic paged response wrapper generated by openapi-generator. + * + * Holds a page of content items and pagination metadata. Jackson deserializes this from the + * standard Spring `Page` JSON shape: + * ```json + * { + * "content": [...], + * "page": { "size": 20, "totalElements": 100, "totalPages": 5, "number": 0 } + * } + * ``` + * + * To use your own class instead, set `importMappings.PagedModel` in the generator config. + */ +data class PagedModel( + val content: List, + val page: PageMetadata, +) { + data class PageMetadata( + val size: Long, + val totalElements: Long, + val totalPages: Long, + val number: Long, + ) +} diff --git a/samples/server/petstore/springboot-paged-model/.openapi-generator/FILES b/samples/server/petstore/springboot-paged-model/.openapi-generator/FILES index e30215256f5e..e51fe865d3ac 100644 --- a/samples/server/petstore/springboot-paged-model/.openapi-generator/FILES +++ b/samples/server/petstore/springboot-paged-model/.openapi-generator/FILES @@ -5,6 +5,7 @@ src/main/java/org/openapitools/api/OrderApi.java src/main/java/org/openapitools/api/PetApi.java src/main/java/org/openapitools/api/UserApi.java src/main/java/org/openapitools/configuration/EnumConverterConfiguration.java +src/main/java/org/openapitools/configuration/PagedModel.java src/main/java/org/openapitools/model/Order.java src/main/java/org/openapitools/model/PageMeta.java src/main/java/org/openapitools/model/Pet.java diff --git a/samples/server/petstore/springboot-paged-model/src/main/java/org/openapitools/api/OrderApi.java b/samples/server/petstore/springboot-paged-model/src/main/java/org/openapitools/api/OrderApi.java index 9fed008db18a..ab356fab0377 100644 --- a/samples/server/petstore/springboot-paged-model/src/main/java/org/openapitools/api/OrderApi.java +++ b/samples/server/petstore/springboot-paged-model/src/main/java/org/openapitools/api/OrderApi.java @@ -7,7 +7,7 @@ import org.springframework.lang.Nullable; import org.openapitools.model.Order; -import org.springframework.data.web.PagedModel; +import org.openapitools.configuration.PagedModel; import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; diff --git a/samples/server/petstore/springboot-paged-model/src/main/java/org/openapitools/api/PetApi.java b/samples/server/petstore/springboot-paged-model/src/main/java/org/openapitools/api/PetApi.java index d60b776f61d9..38f1cb067fc8 100644 --- a/samples/server/petstore/springboot-paged-model/src/main/java/org/openapitools/api/PetApi.java +++ b/samples/server/petstore/springboot-paged-model/src/main/java/org/openapitools/api/PetApi.java @@ -6,7 +6,7 @@ package org.openapitools.api; import org.springframework.lang.Nullable; -import org.springframework.data.web.PagedModel; +import org.openapitools.configuration.PagedModel; import org.openapitools.model.Pet; import org.openapitools.model.SearchResult; import org.springframework.http.ResponseEntity; diff --git a/samples/server/petstore/springboot-paged-model/src/main/java/org/openapitools/api/UserApi.java b/samples/server/petstore/springboot-paged-model/src/main/java/org/openapitools/api/UserApi.java index 6d6429c0c32d..774dd92d1728 100644 --- a/samples/server/petstore/springboot-paged-model/src/main/java/org/openapitools/api/UserApi.java +++ b/samples/server/petstore/springboot-paged-model/src/main/java/org/openapitools/api/UserApi.java @@ -6,7 +6,7 @@ package org.openapitools.api; import org.springframework.lang.Nullable; -import org.springframework.data.web.PagedModel; +import org.openapitools.configuration.PagedModel; import org.openapitools.model.User; import org.openapitools.model.UserList; import org.springframework.http.ResponseEntity; diff --git a/samples/server/petstore/springboot-paged-model/src/main/java/org/openapitools/configuration/PagedModel.java b/samples/server/petstore/springboot-paged-model/src/main/java/org/openapitools/configuration/PagedModel.java new file mode 100644 index 000000000000..cebb0f6ce284 --- /dev/null +++ b/samples/server/petstore/springboot-paged-model/src/main/java/org/openapitools/configuration/PagedModel.java @@ -0,0 +1,95 @@ +package org.openapitools.configuration; + +import java.util.List; + +/** + * Simple generic paged response wrapper generated by openapi-generator. + * + *

Holds a page of content items and pagination metadata. Jackson deserializes this from the + * standard Spring {@code Page} JSON shape: + *

+ * {
+ *   "content": [...],
+ *   "page": { "size": 20, "totalElements": 100, "totalPages": 5, "number": 0 }
+ * }
+ * 
+ * + *

To use your own class instead, set {@code importMappings.PagedModel} in the generator config. + */ +public class PagedModel { + + private List content; + private PageMetadata page; + + public PagedModel() {} + + public PagedModel(List content, PageMetadata page) { + this.content = content; + this.page = page; + } + + public List getContent() { + return content; + } + + public void setContent(List content) { + this.content = content; + } + + public PageMetadata getPage() { + return page; + } + + public void setPage(PageMetadata page) { + this.page = page; + } + + public static class PageMetadata { + + private long size; + private long totalElements; + private long totalPages; + private long number; + + public PageMetadata() {} + + public PageMetadata(long size, long totalElements, long totalPages, long number) { + this.size = size; + this.totalElements = totalElements; + this.totalPages = totalPages; + this.number = number; + } + + public long getSize() { + return size; + } + + public void setSize(long size) { + this.size = size; + } + + public long getTotalElements() { + return totalElements; + } + + public void setTotalElements(long totalElements) { + this.totalElements = totalElements; + } + + public long getTotalPages() { + return totalPages; + } + + public void setTotalPages(long totalPages) { + this.totalPages = totalPages; + } + + public long getNumber() { + return number; + } + + public void setNumber(long number) { + this.number = number; + } + } +}