Skip to content
Draft
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 @@ -1100,6 +1100,8 @@ public CodegenOperation fromOperation(String path, String httpMethod, Operation
List<String> attrs = new ArrayList<>();
if (constraints.maxSize >= 0) attrs.add("maxSize = " + constraints.maxSize);
if (constraints.maxPage >= 0) attrs.add("maxPage = " + constraints.maxPage);
if (constraints.minSize >= 0) attrs.add("minSize = " + constraints.minSize);
if (constraints.minPage >= 0) attrs.add("minPage = " + constraints.minPage);
pageableAnnotations.add("@ValidPageable(" + String.join(", ", attrs) + ")");
codegenOperation.imports.add("ValidPageable");
}
Expand Down Expand Up @@ -1212,7 +1214,7 @@ public void preprocessOpenAPI(OpenAPI openAPI) {
}

if (substituteGenericPagedModel) {
pagedModelRegistry = PagedModelScanUtils.scanPagedModels(openAPI);
pagedModelRegistry = PagedModelScanUtils.scanPagedModels(openAPI, this::toModelName);
if (!pagedModelRegistry.isEmpty()) {
boolean customMapping = importMapping.containsKey("PagedModel");
importMapping.putIfAbsent("PagedModel", configPackage + ".PagedModel");
Expand Down Expand Up @@ -1383,32 +1385,35 @@ public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs)
if (getAnnotationLibrary() == AnnotationLibrary.NONE) {
// No @ApiResponse annotations are generated when annotationLibrary=none,
// so paged schemas are not referenced anywhere → safe to suppress.
Set<String> metaSchemasToCheck = new HashSet<>();
// metaSchemasToCheck maps transformed name (for imports check) → raw name (for objs.remove)
Map<String, String> metaSchemasToCheck = new LinkedHashMap<>();
for (PagedModelScanUtils.DetectedPagedModel detected : pagedModelRegistry.values()) {
if (detected.metaSchemaName != null) {
metaSchemasToCheck.add(detected.metaSchemaName);
metaSchemasToCheck.put(detected.metaSchemaName, detected.rawMetaSchemaName);
}
}
// Remove paged schemas first so reference checks below reflect the post-suppression state.
for (Map.Entry<String, PagedModelScanUtils.DetectedPagedModel> entry : pagedModelRegistry.entrySet()) {
String schemaName = entry.getKey();
PagedModelScanUtils.DetectedPagedModel detected = entry.getValue();
if (objs.remove(schemaName) != null) {
// objs is keyed by raw schema name (DefaultGenerator uses the raw OpenAPI name as key)
if (objs.remove(detected.rawSchemaName) != null) {
LOGGER.info("substituteGenericPagedModel: suppressing model '{}' — replaced by PagedModel<{}>",
schemaName, detected.itemSchemaName);
detected.rawSchemaName, detected.itemSchemaName);
}
}
// Suppress meta schemas only when no remaining (non-suppressed) schema references them.
// Example: if SearchResult has a 'page: PageMeta' property, PageMeta must be kept.
for (String metaName : metaSchemasToCheck) {
for (Map.Entry<String, String> metaEntry : metaSchemasToCheck.entrySet()) {
String metaName = metaEntry.getKey(); // transformed — matches cm.imports values
String rawMetaName = metaEntry.getValue(); // raw — matches objs key
boolean referencedElsewhere = objs.values().stream()
.flatMap(mm -> mm.getModels().stream())
.map(ModelMap::getModel)
.anyMatch(cm -> cm.imports.contains(metaName));
if (referencedElsewhere) {
LOGGER.info("substituteGenericPagedModel: keeping pagination metadata model '{}'"
+ " — referenced by a non-paged schema", metaName);
} else if (objs.remove(metaName) != null) {
} else if (objs.remove(rawMetaName) != null) {
LOGGER.info("substituteGenericPagedModel: suppressing pagination metadata model '{}'"
+ " — replaced by PagedModel.PageMetadata", metaName);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import org.openapitools.codegen.utils.ModelUtils;

import java.util.*;
import java.util.function.UnaryOperator;

/**
* Language-agnostic utility for detecting OpenAPI schemas that represent paginated responses
Expand Down Expand Up @@ -61,21 +62,56 @@ private PagedModelScanUtils() {}
/**
* Carries the result of a single detected paged-model schema.
*
* @param schemaName Name of the detected schema to suppress (e.g. {@code UserPage}).
* @param itemSchemaName Simple name of the array item type (e.g. {@code User}).
* @param metaSchemaName Name of the pagination-metadata schema to suppress
* (e.g. {@code PageMetadata}), or {@code null} if it could not
* be resolved to a named component.
* <p>Two name variants are stored for each schema:</p>
* <ul>
* <li><b>transformed</b> ({@code schemaName} / {@code metaSchemaName}) — the model name
* after the generator's {@code toModelName()} has been applied. These are the names
* that appear in codegen-operation imports and {@code CodegenModel.imports}, so they
* must be used for import removal / import-presence checks.</li>
* <li><b>raw</b> ({@code rawSchemaName} / {@code rawMetaSchemaName}) — the original
* OpenAPI component-schema name. {@code DefaultGenerator} keys {@code allProcessedModels}
* (the {@code objs} map passed to {@code postProcessAllModels}) by the <em>raw</em>
* schema name, so these values must be used for {@code objs.remove()} calls.</li>
* </ul>
*
* <p>When {@link #scanPagedModels(OpenAPI)} is used (no transform), the raw and transformed
* names are identical. When {@link #scanPagedModels(OpenAPI, UnaryOperator)} is used, they
* may differ (e.g. {@code rawSchemaName="UserPage"}, {@code schemaName="UserPageDto"}).</p>
*
* @param schemaName Transformed model name of the detected paged schema.
* @param itemSchemaName Raw item schema name (always raw; callers apply
* {@code toModelName()} at the point of use).
* @param metaSchemaName Transformed model name of the pagination-metadata schema,
* or {@code null} if unresolved.
* @param rawSchemaName Raw OpenAPI schema name of the paged schema (for {@code objs.remove}).
* @param rawMetaSchemaName Raw OpenAPI schema name of the pagination-metadata schema
* (for {@code objs.remove}), or {@code null} if unresolved.
*/
public static final class DetectedPagedModel {
/** Transformed model name — use for import removal / import-presence checks. */
public final String schemaName;
public final String itemSchemaName;
/** Transformed meta model name — use for import-presence checks. */
public final String metaSchemaName;

/** Raw OpenAPI schema name — use for {@code objs.remove()} in {@code postProcessAllModels}. */
public final String rawSchemaName;
/** Raw OpenAPI meta schema name — use for {@code objs.remove()} in {@code postProcessAllModels}. */
public final String rawMetaSchemaName;

/**
* Convenience constructor used when no name transform is active (raw == transformed).
*/
public DetectedPagedModel(String schemaName, String itemSchemaName, String metaSchemaName) {
this(schemaName, itemSchemaName, metaSchemaName, schemaName, metaSchemaName);
}

DetectedPagedModel(String schemaName, String itemSchemaName, String metaSchemaName,
String rawSchemaName, String rawMetaSchemaName) {
this.schemaName = schemaName;
this.itemSchemaName = itemSchemaName;
this.metaSchemaName = metaSchemaName;
this.rawSchemaName = rawSchemaName;
this.rawMetaSchemaName = rawMetaSchemaName;
}
}

Expand Down Expand Up @@ -112,7 +148,43 @@ public static Map<String, DetectedPagedModel> scanPagedModels(OpenAPI openAPI) {
}

/**
* Returns {@code true} if the given schema looks like a pagination-metadata schema.
* Convenience overload that scans for paged-model schemas and immediately re-keys the
* resulting map by applying {@code toModelName} to every schema name.
*
* <p>Generator classes must use this overload (passing {@code this::toModelName}) so that
* the registry keys match the model-name-processed values used at lookup time
* (e.g. {@code codegenOperation.returnBaseType}, {@code objs} keys). This ensures
* correctness when {@code modelNameSuffix}, {@code modelNamePrefix}, {@code schemaMapping},
* or {@code modelNameMapping} are active.</p>
*
* <p>{@code itemSchemaName} inside each {@link DetectedPagedModel} is intentionally left as
* the raw spec name because every call site already passes it through {@code toModelName()}
* at the point of use.</p>
*
* @param openAPI the parsed OpenAPI document
* @param toModelName name-transformation function supplied by the generator
* (typically {@code this::toModelName})
* @return map from transformed schema name to {@link DetectedPagedModel}
*/
public static Map<String, DetectedPagedModel> scanPagedModels(
OpenAPI openAPI, UnaryOperator<String> toModelName) {
Map<String, DetectedPagedModel> raw = scanPagedModels(openAPI);
if (raw.isEmpty()) {
return raw;
}
Map<String, DetectedPagedModel> result = new LinkedHashMap<>();
for (Map.Entry<String, DetectedPagedModel> entry : raw.entrySet()) {
DetectedPagedModel d = entry.getValue();
String rawKey = entry.getKey();
String newKey = toModelName.apply(rawKey);
String rawMeta = d.metaSchemaName;
String newMeta = rawMeta != null ? toModelName.apply(rawMeta) : null;
result.put(newKey, new DetectedPagedModel(newKey, d.itemSchemaName, newMeta, rawKey, rawMeta));
}
return result;
}

/**
*
* <p>The heuristic checks that at least {@value #PAGINATION_FIELD_THRESHOLD} of the
* well-known field names ({@code size}, {@code number}, {@code page},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -872,7 +872,7 @@ public void preprocessOpenAPI(OpenAPI openAPI) {
}

if (substituteGenericPagedModel) {
pagedModelRegistry = PagedModelScanUtils.scanPagedModels(openAPI);
pagedModelRegistry = PagedModelScanUtils.scanPagedModels(openAPI, this::toModelName);
if (!pagedModelRegistry.isEmpty()) {
boolean customMapping = importMapping.containsKey("PagedModel");
importMapping.putIfAbsent("PagedModel", configPackage + ".PagedModel");
Expand Down Expand Up @@ -1272,6 +1272,8 @@ public CodegenOperation fromOperation(String path, String httpMethod, Operation
List<String> attrs = new ArrayList<>();
if (constraints.maxSize >= 0) attrs.add("maxSize = " + constraints.maxSize);
if (constraints.maxPage >= 0) attrs.add("maxPage = " + constraints.maxPage);
if (constraints.minSize >= 0) attrs.add("minSize = " + constraints.minSize);
if (constraints.minPage >= 0) attrs.add("minPage = " + constraints.minPage);
pageableAnnotations.add("@ValidPageable(" + String.join(", ", attrs) + ")");
codegenOperation.imports.add("ValidPageable");
}
Expand Down Expand Up @@ -1450,32 +1452,35 @@ public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs)
if (getAnnotationLibrary() == AnnotationLibrary.NONE) {
// No @ApiResponse annotations are generated when annotationLibrary=none,
// so paged schemas are not referenced anywhere → safe to suppress.
Set<String> metaSchemasToCheck = new HashSet<>();
// metaSchemasToCheck maps transformed name (for imports check) → raw name (for objs.remove)
Map<String, String> metaSchemasToCheck = new LinkedHashMap<>();
for (PagedModelScanUtils.DetectedPagedModel detected : pagedModelRegistry.values()) {
if (detected.metaSchemaName != null) {
metaSchemasToCheck.add(detected.metaSchemaName);
metaSchemasToCheck.put(detected.metaSchemaName, detected.rawMetaSchemaName);
}
}
// Remove paged schemas first so reference checks below reflect the post-suppression state.
for (Map.Entry<String, PagedModelScanUtils.DetectedPagedModel> entry : pagedModelRegistry.entrySet()) {
String schemaName = entry.getKey();
PagedModelScanUtils.DetectedPagedModel detected = entry.getValue();
if (objs.remove(schemaName) != null) {
// objs is keyed by raw schema name (DefaultGenerator uses the raw OpenAPI name as key)
if (objs.remove(detected.rawSchemaName) != null) {
LOGGER.info("substituteGenericPagedModel: suppressing model '{}' — replaced by PagedModel<{}>",
schemaName, detected.itemSchemaName);
detected.rawSchemaName, detected.itemSchemaName);
}
}
// Suppress meta schemas only when no remaining (non-suppressed) schema references them.
// Example: if SearchResult has a 'page: PageMeta' property, PageMeta must be kept.
for (String metaName : metaSchemasToCheck) {
for (Map.Entry<String, String> metaEntry : metaSchemasToCheck.entrySet()) {
String metaName = metaEntry.getKey(); // transformed — matches cm.imports values
String rawMetaName = metaEntry.getValue(); // raw — matches objs key
boolean referencedElsewhere = objs.values().stream()
.flatMap(mm -> mm.getModels().stream())
.map(ModelMap::getModel)
.anyMatch(cm -> cm.imports.contains(metaName));
if (referencedElsewhere) {
LOGGER.info("substituteGenericPagedModel: keeping pagination metadata model '{}'"
+ " — referenced by a non-paged schema", metaName);
} else if (objs.remove(metaName) != null) {
} else if (objs.remove(rawMetaName) != null) {
LOGGER.info("substituteGenericPagedModel: suppressing pagination metadata model '{}'"
+ " — replaced by PagedModel.PageMetadata", metaName);
}
Expand Down
Loading
Loading