diff --git a/boat-scaffold/src/main/java/com/backbase/oss/codegen/doc/BoatDocsGenerator.java b/boat-scaffold/src/main/java/com/backbase/oss/codegen/doc/BoatDocsGenerator.java index e94a6e251..95221d656 100644 --- a/boat-scaffold/src/main/java/com/backbase/oss/codegen/doc/BoatDocsGenerator.java +++ b/boat-scaffold/src/main/java/com/backbase/oss/codegen/doc/BoatDocsGenerator.java @@ -1,9 +1,13 @@ package com.backbase.oss.codegen.doc; +import com.backbase.oss.codegen.utils.DeprecationExtensions; +import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.Operation; import io.swagger.v3.oas.models.servers.Server; +import java.time.LocalDate; import java.util.List; import java.util.Map; +import java.util.Optional; import lombok.extern.slf4j.Slf4j; import org.openapitools.codegen.CliOption; import org.openapitools.codegen.CodegenConstants; @@ -30,6 +34,16 @@ public BoatDocsGenerator() { typeAliases = new HashMap<>(); } + @Override + public void preprocessOpenAPI(OpenAPI openAPI) { + super.preprocessOpenAPI(openAPI); + boolean specDeprecated = DeprecationExtensions.isSpecDeprecated(openAPI.getInfo()); + Optional sunsetDate = DeprecationExtensions.getSunsetDate(openAPI.getInfo()); + additionalProperties.put("boatApiDeprecated", specDeprecated); + additionalProperties.put("boatApiDeprecationMessage", + DeprecationExtensions.buildDeprecationMessage(sunsetDate)); + } + @Override public CodegenOperation fromOperation(String path, String httpMethod, Operation operation, List servers) { CodegenOperation codegenOperation = super.fromOperation(path, httpMethod, operation, servers); @@ -51,9 +65,21 @@ public CodegenOperation fromOperation(String path, String httpMethod, Operation } } + applyDeprecationIfNeeded(codegenOperation); return codegenOperation; } + private void applyDeprecationIfNeeded(CodegenOperation codegenOperation) { + Boolean specDeprecated = (Boolean) additionalProperties.get("boatApiDeprecated"); + if (specDeprecated != null && specDeprecated) { + codegenOperation.isDeprecated = true; + String message = (String) additionalProperties.get("boatApiDeprecationMessage"); + if (message != null && !codegenOperation.vendorExtensions.containsKey(DeprecationExtensions.X_BOAT_DEPRECATION_MESSAGE)) { + codegenOperation.vendorExtensions.put(DeprecationExtensions.X_BOAT_DEPRECATION_MESSAGE, message); + } + } + } + @Override public String getName() { return NAME; diff --git a/boat-scaffold/src/main/java/com/backbase/oss/codegen/java/BoatCodeGenUtils.java b/boat-scaffold/src/main/java/com/backbase/oss/codegen/java/BoatCodeGenUtils.java index c9bf4892a..626f74b1b 100644 --- a/boat-scaffold/src/main/java/com/backbase/oss/codegen/java/BoatCodeGenUtils.java +++ b/boat-scaffold/src/main/java/com/backbase/oss/codegen/java/BoatCodeGenUtils.java @@ -1,5 +1,6 @@ package com.backbase.oss.codegen.java; +import com.backbase.oss.codegen.utils.DeprecationExtensions; import io.swagger.v3.oas.models.media.Schema; import java.util.Locale; import java.util.Map; @@ -8,7 +9,11 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.RequiredArgsConstructor; +import org.openapitools.codegen.CodegenModel; +import org.openapitools.codegen.CodegenOperation; import org.openapitools.codegen.CodegenProperty; +import org.openapitools.codegen.model.ModelMap; +import org.openapitools.codegen.model.ModelsMap; import org.openapitools.codegen.utils.ModelUtils; @NoArgsConstructor(access = AccessLevel.PRIVATE) @@ -36,6 +41,44 @@ private static String formatValue(CodegenProperty cp, boolean defaultToNull, Str : null; } + public static void applyDeprecationIfNeeded(CodegenOperation codegenOperation, Map additionalProperties) { + Boolean specDeprecated = (Boolean) additionalProperties.get("boatApiDeprecated"); + if (specDeprecated != null && specDeprecated) { + codegenOperation.isDeprecated = true; + String message = (String) additionalProperties.get("boatApiDeprecationMessage"); + if (message != null && !codegenOperation.vendorExtensions.containsKey(DeprecationExtensions.X_BOAT_DEPRECATION_MESSAGE)) { + codegenOperation.vendorExtensions.put(DeprecationExtensions.X_BOAT_DEPRECATION_MESSAGE, message); + } + } + } + + public static void applyDeprecationToPropertyIfNeeded(CodegenProperty property, Map additionalProperties) { + Boolean specDeprecated = (Boolean) additionalProperties.get("boatApiDeprecated"); + if (specDeprecated != null && specDeprecated) { + property.deprecated = true; + String message = (String) additionalProperties.get("boatApiDeprecationMessage"); + if (message != null && !property.vendorExtensions.containsKey(DeprecationExtensions.X_BOAT_DEPRECATION_MESSAGE)) { + property.vendorExtensions.put(DeprecationExtensions.X_BOAT_DEPRECATION_MESSAGE, message); + } + } + } + + public static void applyDeprecationToAllModelsIfNeeded(Map objs, Map additionalProperties) { + Boolean specDeprecated = (Boolean) additionalProperties.get("boatApiDeprecated"); + if (specDeprecated != null && specDeprecated) { + String message = (String) additionalProperties.get("boatApiDeprecationMessage"); + for (ModelsMap modelsMap : objs.values()) { + for (ModelMap modelMap : modelsMap.getModels()) { + CodegenModel model = modelMap.getModel(); + model.isDeprecated = true; + if (message != null && !model.vendorExtensions.containsKey(DeprecationExtensions.X_BOAT_DEPRECATION_MESSAGE)) { + model.vendorExtensions.put(DeprecationExtensions.X_BOAT_DEPRECATION_MESSAGE, message); + } + } + } + } + } + @RequiredArgsConstructor(staticName = "of") @Getter static class CodegenValueType { diff --git a/boat-scaffold/src/main/java/com/backbase/oss/codegen/java/BoatJavaCodeGen.java b/boat-scaffold/src/main/java/com/backbase/oss/codegen/java/BoatJavaCodeGen.java index 15949b96b..e2f2439be 100644 --- a/boat-scaffold/src/main/java/com/backbase/oss/codegen/java/BoatJavaCodeGen.java +++ b/boat-scaffold/src/main/java/com/backbase/oss/codegen/java/BoatJavaCodeGen.java @@ -1,16 +1,22 @@ package com.backbase.oss.codegen.java; import com.backbase.oss.codegen.java.BoatCodeGenUtils.CodegenValueType; +import com.backbase.oss.codegen.utils.DeprecationExtensions; +import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.media.Schema; import lombok.Getter; import lombok.Setter; import org.openapitools.codegen.CliOption; +import org.openapitools.codegen.CodegenModel; +import org.openapitools.codegen.CodegenOperation; import org.openapitools.codegen.CodegenProperty; import org.openapitools.codegen.SupportingFile; import org.openapitools.codegen.languages.JavaClientCodegen; +import org.openapitools.codegen.model.ModelsMap; import org.openapitools.codegen.utils.ModelUtils; import java.io.File; +import java.util.Map; import static com.backbase.oss.codegen.java.BoatCodeGenUtils.getCollectionCodegenValue; @@ -42,6 +48,33 @@ public BoatJavaCodeGen() { this.setGenerateClientAsBean(true); } + @Override + public void preprocessOpenAPI(OpenAPI openAPI) { + super.preprocessOpenAPI(openAPI); + DeprecationExtensions.populateDeprecationAdditionalProperties(openAPI, additionalProperties); + } + + @Override + public CodegenOperation fromOperation(String path, String httpMethod, io.swagger.v3.oas.models.Operation operation, + java.util.List servers) { + CodegenOperation codegenOperation = super.fromOperation(path, httpMethod, operation, servers); + BoatCodeGenUtils.applyDeprecationIfNeeded(codegenOperation, additionalProperties); + return codegenOperation; + } + + @Override + public void postProcessModelProperty(CodegenModel model, CodegenProperty property) { + super.postProcessModelProperty(model, property); + BoatCodeGenUtils.applyDeprecationToPropertyIfNeeded(property, additionalProperties); + } + + @Override + public Map postProcessAllModels(Map objs) { + Map result = super.postProcessAllModels(objs); + BoatCodeGenUtils.applyDeprecationToAllModelsIfNeeded(result, additionalProperties); + return result; + } + @Override public String getName() { return NAME; diff --git a/boat-scaffold/src/main/java/com/backbase/oss/codegen/java/BoatSpringCodeGen.java b/boat-scaffold/src/main/java/com/backbase/oss/codegen/java/BoatSpringCodeGen.java index ec2c7448f..53c0e2036 100644 --- a/boat-scaffold/src/main/java/com/backbase/oss/codegen/java/BoatSpringCodeGen.java +++ b/boat-scaffold/src/main/java/com/backbase/oss/codegen/java/BoatSpringCodeGen.java @@ -8,8 +8,10 @@ import static org.openapitools.codegen.utils.StringUtils.camelize; import com.backbase.oss.codegen.java.BoatCodeGenUtils.CodegenValueType; +import com.backbase.oss.codegen.utils.DeprecationExtensions; import com.samskivert.mustache.Mustache; import com.samskivert.mustache.Template.Fragment; +import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.Operation; import io.swagger.v3.oas.models.media.Schema; import io.swagger.v3.oas.models.parameters.Parameter; @@ -20,6 +22,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -38,6 +41,7 @@ import org.openapitools.codegen.SupportingFile; import org.openapitools.codegen.config.GlobalSettings; import org.openapitools.codegen.languages.SpringCodegen; +import org.openapitools.codegen.model.ModelsMap; import org.openapitools.codegen.templating.mustache.IndentedLambda; import org.openapitools.codegen.utils.ModelUtils; @@ -372,6 +376,12 @@ private boolean needApiUtil() { && this.apiTemplateFiles.containsKey("apiDelegate.mustache"); } + @Override + public void preprocessOpenAPI(OpenAPI openAPI) { + super.preprocessOpenAPI(openAPI); + DeprecationExtensions.populateDeprecationAdditionalProperties(openAPI, additionalProperties); + } + /** This method has been overridden in order to add a parameter to codegen operation for adding HttpServletRequest to the service interface. There is a relevant httpServletParam.mustache file. @@ -392,6 +402,7 @@ public CodegenOperation fromOperation(String path, String httpMethod, Operation if (codegenOperation.returnType != null) { codegenOperation.returnType = codegenOperation.returnType.replace("@Valid", ""); } + BoatCodeGenUtils.applyDeprecationIfNeeded(codegenOperation, additionalProperties); return codegenOperation; } @@ -413,6 +424,15 @@ public void postProcessModelProperty(CodegenModel model, CodegenProperty propert model.imports.add("BigDecimalCustomSerializer"); model.imports.add(JSON_SERIALIZE); } + + BoatCodeGenUtils.applyDeprecationToPropertyIfNeeded(property, additionalProperties); + } + + @Override + public Map postProcessAllModels(Map objs) { + Map result = super.postProcessAllModels(objs); + BoatCodeGenUtils.applyDeprecationToAllModelsIfNeeded(result, additionalProperties); + return result; } private boolean shouldSerializeBigDecimalAsString(CodegenProperty property) { @@ -428,4 +448,5 @@ private boolean isDataTypeString(CodegenProperty property) { return Stream.of(property.baseType, property.dataType, property.datatypeWithEnum) .anyMatch("string"::equalsIgnoreCase); } + } diff --git a/boat-scaffold/src/main/java/com/backbase/oss/codegen/utils/DeprecationExtensions.java b/boat-scaffold/src/main/java/com/backbase/oss/codegen/utils/DeprecationExtensions.java new file mode 100644 index 000000000..18c9b75d0 --- /dev/null +++ b/boat-scaffold/src/main/java/com/backbase/oss/codegen/utils/DeprecationExtensions.java @@ -0,0 +1,80 @@ +package com.backbase.oss.codegen.utils; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.Map; +import java.util.Optional; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; + +public final class DeprecationExtensions { + public static final String X_DEPRECATED = "x-deprecated"; + public static final String X_SUNSET_DATE = "x-sunset-date"; + public static final String X_BOAT_DEPRECATION_MESSAGE = "x-boat-deprecation-message"; + private static final String BOAT_API_DEPRECATED = "boatApiDeprecated"; + private static final String BOAT_API_DEPRECATION_MESSAGE = "boatApiDeprecationMessage"; + + private DeprecationExtensions() { + // utility class + } + + /** + * Sets the deprecation related additional properties. + * + * @param openAPI The spec. + * @param properties The additional properties to populate. + */ + public static void populateDeprecationAdditionalProperties(OpenAPI openAPI, Map properties) { + boolean specDeprecated = DeprecationExtensions.isSpecDeprecated(openAPI.getInfo()); + Optional sunsetDate = DeprecationExtensions.getSunsetDate(openAPI.getInfo()); + properties.put(BOAT_API_DEPRECATED, specDeprecated); + properties.put(BOAT_API_DEPRECATION_MESSAGE, + DeprecationExtensions.buildDeprecationMessage(sunsetDate)); + } + + public static boolean isSpecDeprecated(Info info) { + if (info == null || info.getExtensions() == null) { + return false; + } + return isTrue(info.getExtensions().get(X_DEPRECATED)); + } + + public static Optional getSunsetDate(Info info) { + if (info == null || info.getExtensions() == null) { + return Optional.empty(); + } + Object dateValue = info.getExtensions().get(X_SUNSET_DATE); + if (dateValue == null) { + return Optional.empty(); + } + String dateStr = dateValue.toString().trim(); + if (dateStr.isEmpty()) { + return Optional.empty(); + } + try { + LocalDate date = LocalDate.parse(dateStr, DateTimeFormatter.ISO_LOCAL_DATE); + return Optional.of(date); + } catch (DateTimeParseException e) { + return Optional.empty(); + } + } + + public static String buildDeprecationMessage(Optional sunsetDate) { + if (sunsetDate.isEmpty()) { + return "This API is deprecated."; + } + return "This API is deprecated and will be removed on " + sunsetDate.get() + "."; + } + + private static boolean isTrue(Object value) { + if (value instanceof Boolean) { + return (Boolean) value; + } + if (value instanceof String) { + return ((String) value).equalsIgnoreCase("true"); + } + return false; + } +} diff --git a/boat-scaffold/src/main/templates/boat-docs/index.mustache b/boat-scaffold/src/main/templates/boat-docs/index.mustache index e526cba3f..56948229d 100644 --- a/boat-scaffold/src/main/templates/boat-docs/index.mustache +++ b/boat-scaffold/src/main/templates/boat-docs/index.mustache @@ -44,7 +44,7 @@ {{#operation}}
-

{{#summary}}{{.}}{{/summary}}{{^summary}}{{nickname}}{{/summary}}{{#isDeprecated}}Deprecated {{/isDeprecated}}

+

{{#summary}}{{.}}{{/summary}}{{^summary}}{{nickname}}{{/summary}}{{#isDeprecated}}Deprecated{{#vendorExtensions.x-boat-deprecation-message}}: {{.}}{{/vendorExtensions.x-boat-deprecation-message}} {{/isDeprecated}}

{{#description}}

{{description}}

{{/description}}

{{notes}}


diff --git a/boat-scaffold/src/main/templates/boat-docs/param.mustache b/boat-scaffold/src/main/templates/boat-docs/param.mustache index e411a0ae5..972fa1b41 100644 --- a/boat-scaffold/src/main/templates/boat-docs/param.mustache +++ b/boat-scaffold/src/main/templates/boat-docs/param.mustache @@ -55,7 +55,7 @@
{{/isEnum}} - {{#deprecated}} Deprecated {{/deprecated}} + {{#deprecated}} Deprecated{{#vendorExtensions.x-boat-deprecation-message}}: {{.}}{{/vendorExtensions.x-boat-deprecation-message}} {{/deprecated}} {{#hasExamples}}
diff --git a/boat-scaffold/src/main/templates/boat-java/api.mustache b/boat-scaffold/src/main/templates/boat-java/api.mustache index 3cfd92db0..aa858b1e1 100644 --- a/boat-scaffold/src/main/templates/boat-java/api.mustache +++ b/boat-scaffold/src/main/templates/boat-java/api.mustache @@ -52,7 +52,7 @@ public class {{classname}} { {{/returnType}} * @throws ApiException if fails to make API call {{#isDeprecated}} - * @deprecated + * @deprecated{{#vendorExtensions.x-boat-deprecation-message}} {{.}}{{/vendorExtensions.x-boat-deprecation-message}} {{/isDeprecated}} {{#externalDocs}} * {{description}} diff --git a/boat-scaffold/src/main/templates/boat-java/libraries/okhttp-gson/api.mustache b/boat-scaffold/src/main/templates/boat-java/libraries/okhttp-gson/api.mustache index 9519ce13c..14b7e9330 100644 --- a/boat-scaffold/src/main/templates/boat-java/libraries/okhttp-gson/api.mustache +++ b/boat-scaffold/src/main/templates/boat-java/libraries/okhttp-gson/api.mustache @@ -107,7 +107,7 @@ public class {{classname}} { {{/responses.0}} {{#isDeprecated}} - * @deprecated + * @deprecated{{#vendorExtensions.x-boat-deprecation-message}} {{.}}{{/vendorExtensions.x-boat-deprecation-message}} {{/isDeprecated}} {{#externalDocs}} * {{description}} @@ -300,7 +300,7 @@ public class {{classname}} { {{/responses.0}} {{#isDeprecated}} - * @deprecated + * @deprecated{{#vendorExtensions.x-boat-deprecation-message}} {{.}}{{/vendorExtensions.x-boat-deprecation-message}} {{/isDeprecated}} {{#externalDocs}} * {{description}} @@ -341,7 +341,7 @@ public class {{classname}} { {{/responses.0}} {{#isDeprecated}} - * @deprecated + * @deprecated{{#vendorExtensions.x-boat-deprecation-message}} {{.}}{{/vendorExtensions.x-boat-deprecation-message}} {{/isDeprecated}} {{#externalDocs}} * {{description}} @@ -410,7 +410,7 @@ public class {{classname}} { {{/responses.0}} {{#isDeprecated}} - * @deprecated + * @deprecated{{#vendorExtensions.x-boat-deprecation-message}} {{.}}{{/vendorExtensions.x-boat-deprecation-message}} {{/isDeprecated}} {{#externalDocs}} * {{description}} @@ -473,7 +473,7 @@ public class {{classname}} { {{/responses.0}} {{#isDeprecated}} - * @deprecated + * @deprecated{{#vendorExtensions.x-boat-deprecation-message}} {{.}}{{/vendorExtensions.x-boat-deprecation-message}} {{/isDeprecated}} */ {{#isDeprecated}} @@ -498,7 +498,7 @@ public class {{classname}} { {{/responses.0}} {{#isDeprecated}} - * @deprecated + * @deprecated{{#vendorExtensions.x-boat-deprecation-message}} {{.}}{{/vendorExtensions.x-boat-deprecation-message}} {{/isDeprecated}} */ {{#isDeprecated}} @@ -531,7 +531,7 @@ public class {{classname}} { {{/responses.0}} {{#isDeprecated}} - * @deprecated + * @deprecated{{#vendorExtensions.x-boat-deprecation-message}} {{.}}{{/vendorExtensions.x-boat-deprecation-message}} {{/isDeprecated}} */ {{#isDeprecated}} @@ -564,7 +564,7 @@ public class {{classname}} { {{/responses.0}} {{#isDeprecated}} - * @deprecated + * @deprecated{{#vendorExtensions.x-boat-deprecation-message}} {{.}}{{/vendorExtensions.x-boat-deprecation-message}} {{/isDeprecated}} */ {{#isDeprecated}} @@ -591,7 +591,7 @@ public class {{classname}} { {{/responses.0}} {{#isDeprecated}} - * @deprecated + * @deprecated{{#vendorExtensions.x-boat-deprecation-message}} {{.}}{{/vendorExtensions.x-boat-deprecation-message}} {{/isDeprecated}} {{#externalDocs}} * {{description}} diff --git a/boat-scaffold/src/main/templates/boat-java/libraries/okhttp-gson/pojo.mustache b/boat-scaffold/src/main/templates/boat-java/libraries/okhttp-gson/pojo.mustache index cb19eb603..dcba959b3 100644 --- a/boat-scaffold/src/main/templates/boat-java/libraries/okhttp-gson/pojo.mustache +++ b/boat-scaffold/src/main/templates/boat-java/libraries/okhttp-gson/pojo.mustache @@ -23,17 +23,17 @@ import {{invokerPackage}}.JSON; /** * {{description}}{{^description}}{{classname}}{{/description}}{{#isDeprecated}} - * @deprecated{{/isDeprecated}} + * @deprecated{{#vendorExtensions.x-boat-deprecation-message}} {{.}}{{/vendorExtensions.x-boat-deprecation-message}}{{/isDeprecated}} */{{#isDeprecated}} @Deprecated{{/isDeprecated}} {{#swagger1AnnotationLibrary}} {{#description}} -@ApiModel(description = "{{{.}}}") +@ApiModel(description = "{{{.}}}{{#isDeprecated}}{{#vendorExtensions.x-boat-deprecation-message}} {{.}}{{/vendorExtensions.x-boat-deprecation-message}}{{/isDeprecated}}") {{/description}} {{/swagger1AnnotationLibrary}} {{#swagger2AnnotationLibrary}} {{#description}} -@Schema(description = "{{{.}}}") +@Schema(description = "{{{.}}}{{#isDeprecated}}{{#vendorExtensions.x-boat-deprecation-message}} {{.}}{{/vendorExtensions.x-boat-deprecation-message}}{{/isDeprecated}}"{{#isDeprecated}}, deprecated = true{{/isDeprecated}}) {{/description}} {{/swagger2AnnotationLibrary}} {{>additionalModelTypeAnnotations}}{{>generatedAnnotation}}{{#discriminator}}{{>typeInfoAnnotation}}{{/discriminator}}{{>xmlAnnotation}} @@ -155,7 +155,7 @@ public class {{classname}} {{#parent}}extends {{{.}}} {{/parent}}{{#vendorExtens {{/maximum}} * @return {{name}} {{#deprecated}} - * @deprecated + * @deprecated{{#vendorExtensions.x-boat-deprecation-message}} {{.}}{{/vendorExtensions.x-boat-deprecation-message}} {{/deprecated}} */ {{#deprecated}} @@ -167,10 +167,10 @@ public class {{classname}} {{#parent}}extends {{{.}}} {{/parent}}{{#vendorExtens {{/useBeanValidation}} {{#swagger1AnnotationLibrary}} - @ApiModelProperty({{#example}}example = "{{{.}}}", {{/example}}{{#required}}required = {{required}}, {{/required}}value = "{{{description}}}") + @ApiModelProperty({{#example}}example = "{{{.}}}", {{/example}}{{#required}}required = {{required}}, {{/required}}value = "{{{description}}}{{#deprecated}}{{#vendorExtensions.x-boat-deprecation-message}} {{.}}{{/vendorExtensions.x-boat-deprecation-message}}{{/deprecated}}") {{/swagger1AnnotationLibrary}} {{#swagger2AnnotationLibrary}} - @Schema({{#example}}example = "{{{.}}}", {{/example}}requiredMode = {{#required}}Schema.RequiredMode.REQUIRED{{/required}}{{^required}}Schema.RequiredMode.NOT_REQUIRED{{/required}}, description = "{{{description}}}") + @Schema({{#example}}example = "{{{.}}}", {{/example}}requiredMode = {{#required}}Schema.RequiredMode.REQUIRED{{/required}}{{^required}}Schema.RequiredMode.NOT_REQUIRED{{/required}}, description = "{{{description}}}{{#deprecated}}{{#vendorExtensions.x-boat-deprecation-message}} {{.}}{{/vendorExtensions.x-boat-deprecation-message}}{{/deprecated}}"{{#deprecated}}, deprecated = true{{/deprecated}}) {{/swagger2AnnotationLibrary}} {{#vendorExtensions.x-extra-annotation}} {{{vendorExtensions.x-extra-annotation}}} diff --git a/boat-scaffold/src/main/templates/boat-java/pojo.mustache b/boat-scaffold/src/main/templates/boat-java/pojo.mustache index 0b5479df4..fe9605598 100644 --- a/boat-scaffold/src/main/templates/boat-java/pojo.mustache +++ b/boat-scaffold/src/main/templates/boat-java/pojo.mustache @@ -1,6 +1,6 @@ /** * {{description}}{{^description}}{{classname}}{{/description}}{{#isDeprecated}} - * @deprecated{{/isDeprecated}} + * @deprecated{{#vendorExtensions.x-boat-deprecation-message}} {{.}}{{/vendorExtensions.x-boat-deprecation-message}}{{/isDeprecated}} */{{#isDeprecated}} @Deprecated{{/isDeprecated}} {{#swagger1AnnotationLibrary}} @@ -215,7 +215,7 @@ public class {{classname}} {{#parent}}extends {{{.}}} {{/parent}}{{#vendorExtens {{/maximum}} * @return {{name}} {{#deprecated}} - * @deprecated + * @deprecated{{#vendorExtensions.x-boat-deprecation-message}} {{.}}{{/vendorExtensions.x-boat-deprecation-message}} {{/deprecated}} */ {{#deprecated}} diff --git a/boat-scaffold/src/main/templates/boat-spring/api.mustache b/boat-scaffold/src/main/templates/boat-spring/api.mustache index 41500ef63..7794da425 100644 --- a/boat-scaffold/src/main/templates/boat-spring/api.mustache +++ b/boat-scaffold/src/main/templates/boat-spring/api.mustache @@ -151,7 +151,7 @@ public interface {{classname}} { * @return {{#responses}}{{message}} (status code {{code}}){{^-last}} * or {{/-last}}{{/responses}} {{#isDeprecated}} - * @deprecated + * @deprecated{{#vendorExtensions.x-boat-deprecation-message}} {{.}}{{/vendorExtensions.x-boat-deprecation-message}} {{/isDeprecated}} {{#externalDocs}} * {{description}} @@ -171,7 +171,12 @@ public interface {{classname}} { summary = "{{{.}}}", {{/summary}} {{#notes}} - description = "{{{.}}}", + description = "{{{.}}}{{#isDeprecated}}{{#vendorExtensions.x-boat-deprecation-message}} {{.}}{{/vendorExtensions.x-boat-deprecation-message}}{{/isDeprecated}}", + {{/notes}} + {{^notes}} + {{#isDeprecated}} + description = "{{#vendorExtensions.x-boat-deprecation-message}}{{.}}{{/vendorExtensions.x-boat-deprecation-message}}", + {{/isDeprecated}} {{/notes}} {{#isDeprecated}} deprecated = true, diff --git a/boat-scaffold/src/main/templates/boat-spring/apiController.mustache b/boat-scaffold/src/main/templates/boat-spring/apiController.mustache index 47e4a7b73..475b55577 100644 --- a/boat-scaffold/src/main/templates/boat-spring/apiController.mustache +++ b/boat-scaffold/src/main/templates/boat-spring/apiController.mustache @@ -109,7 +109,7 @@ public class {{classname}}Controller implements {{classname}} { * @return {{#responses}}{{message}} (status code {{code}}){{^-last}} * or {{/-last}}{{/responses}} {{#isDeprecated}} - * @deprecated + * @deprecated{{#vendorExtensions.x-boat-deprecation-message}} {{.}}{{/vendorExtensions.x-boat-deprecation-message}} {{/isDeprecated}} {{#externalDocs}} * {{description}} diff --git a/boat-scaffold/src/main/templates/boat-spring/apiDelegate.mustache b/boat-scaffold/src/main/templates/boat-spring/apiDelegate.mustache index 44d989952..685c893a0 100644 --- a/boat-scaffold/src/main/templates/boat-spring/apiDelegate.mustache +++ b/boat-scaffold/src/main/templates/boat-spring/apiDelegate.mustache @@ -59,7 +59,7 @@ public interface {{classname}}Delegate { * @return {{#responses}}{{message}} (status code {{code}}){{^-last}} * or {{/-last}}{{/responses}} {{#isDeprecated}} - * @deprecated + * @deprecated{{#vendorExtensions.x-boat-deprecation-message}} {{.}}{{/vendorExtensions.x-boat-deprecation-message}} {{/isDeprecated}} {{#externalDocs}} * {{description}} diff --git a/boat-scaffold/src/main/templates/boat-spring/pojo.mustache b/boat-scaffold/src/main/templates/boat-spring/pojo.mustache index 82a0a6eb0..b98891922 100644 --- a/boat-scaffold/src/main/templates/boat-spring/pojo.mustache +++ b/boat-scaffold/src/main/templates/boat-spring/pojo.mustache @@ -1,6 +1,6 @@ /** * {{description}}{{^description}}{{classname}}{{/description}}{{#isDeprecated}} - * @deprecated{{/isDeprecated}} + * @deprecated{{#vendorExtensions.x-boat-deprecation-message}} {{.}}{{/vendorExtensions.x-boat-deprecation-message}}{{/isDeprecated}} */ {{>additionalModelTypeAnnotations}} @@ -9,10 +9,17 @@ {{/isDeprecated}} {{#description}} {{#swagger1AnnotationLibrary}} -@ApiModel(description = "{{{description}}}") +@ApiModel(description = "{{{description}}}{{#isDeprecated}}{{#vendorExtensions.x-boat-deprecation-message}} {{.}}{{/vendorExtensions.x-boat-deprecation-message}}{{/isDeprecated}}") {{/swagger1AnnotationLibrary}} {{#swagger2AnnotationLibrary}} -@Schema({{#name}}name = "{{name}}", {{/name}}description = "{{{description}}}"{{#deprecated}}, deprecated = true{{/deprecated}}) +@Schema({{#name}}name = "{{name}}", {{/name}}description = "{{{description}}}{{#isDeprecated}}{{#vendorExtensions.x-boat-deprecation-message}} {{.}}{{/vendorExtensions.x-boat-deprecation-message}}{{/isDeprecated}}"{{#deprecated}}, deprecated = true{{/deprecated}}) +{{/swagger2AnnotationLibrary}} +{{/description}} +{{^description}} +{{#swagger2AnnotationLibrary}} +{{#isDeprecated}} +@Schema(description = "{{#vendorExtensions.x-boat-deprecation-message}}{{.}}{{/vendorExtensions.x-boat-deprecation-message}}", deprecated = true) +{{/isDeprecated}} {{/swagger2AnnotationLibrary}} {{/description}} {{#discriminator}} @@ -208,7 +215,7 @@ public {{>sealed}}class {{classname}}{{#parent}} extends {{{parent}}}{{/parent}} {{/maximum}} * @return {{name}} {{#deprecated}} - * @deprecated + * @deprecated{{#vendorExtensions.x-boat-deprecation-message}} {{.}}{{/vendorExtensions.x-boat-deprecation-message}} {{/deprecated}} */ {{#vendorExtensions.x-extra-annotation}} @@ -221,7 +228,7 @@ public {{>sealed}}class {{classname}}{{#parent}} extends {{{parent}}}{{/parent}} {{#required}}@NotNull{{/required}} {{/useBeanValidation}} {{#swagger2AnnotationLibrary}} - @Schema(name = "{{{baseName}}}"{{#isReadOnly}}, accessMode = Schema.AccessMode.READ_ONLY{{/isReadOnly}}{{#example}}, example = "{{{.}}}"{{/example}}{{#description}}, description = "{{{.}}}"{{/description}}{{#deprecated}}, deprecated = true{{/deprecated}}, requiredMode = {{#required}}Schema.RequiredMode.REQUIRED{{/required}}{{^required}}Schema.RequiredMode.NOT_REQUIRED{{/required}}) + @Schema(name = "{{{baseName}}}"{{#isReadOnly}}, accessMode = Schema.AccessMode.READ_ONLY{{/isReadOnly}}{{#example}}, example = "{{{.}}}"{{/example}}{{#description}}, description = "{{{.}}}{{#deprecated}}{{#vendorExtensions.x-boat-deprecation-message}} {{.}}{{/vendorExtensions.x-boat-deprecation-message}}{{/deprecated}}"{{/description}}{{^description}}{{#deprecated}}, description = "{{#vendorExtensions.x-boat-deprecation-message}}{{.}}{{/vendorExtensions.x-boat-deprecation-message}}"{{/deprecated}}{{/description}}{{#deprecated}}, deprecated = true{{/deprecated}}, requiredMode = {{#required}}Schema.RequiredMode.REQUIRED{{/required}}{{^required}}Schema.RequiredMode.NOT_REQUIRED{{/required}}) {{/swagger2AnnotationLibrary}} {{#swagger1AnnotationLibrary}} @ApiModelProperty({{#example}}example = "{{{.}}}", {{/example}}{{#required}}required = {{required}}, {{/required}}{{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}value = "{{{description}}}") @@ -252,7 +259,7 @@ public {{>sealed}}class {{classname}}{{#parent}} extends {{{parent}}}{{/parent}} {{^lombok.Setter}} {{#deprecated}} /** - * @deprecated + * @deprecated{{#vendorExtensions.x-boat-deprecation-message}} {{.}}{{/vendorExtensions.x-boat-deprecation-message}} */ {{/deprecated}} {{#vendorExtensions.x-setter-extra-annotation}} diff --git a/boat-scaffold/src/test/java/com/backbase/oss/codegen/java/BoatCommonJavaCodeGenTests.java b/boat-scaffold/src/test/java/com/backbase/oss/codegen/java/BoatCommonJavaCodeGenTests.java index 38873b077..066955e14 100644 --- a/boat-scaffold/src/test/java/com/backbase/oss/codegen/java/BoatCommonJavaCodeGenTests.java +++ b/boat-scaffold/src/test/java/com/backbase/oss/codegen/java/BoatCommonJavaCodeGenTests.java @@ -168,4 +168,44 @@ private void assertVariableDeclarator(CompilationUnit requestClass, String field assertFalse(listDeclarator.getInitializer().isPresent()); } } + + @Test + void shouldGenerateDeprecationAnnotationsFromSpecLevel() throws IOException { + var modelPackage = "com.backbase.model"; + var apiPackage = "com.backbase.api"; + var input = new File("src/test/resources/boat-spring/deprecated-spec.yaml"); + var output = TEST_OUTPUT + "/shouldGenerateDeprecationAnnotationsFromSpecLevel"; + + var codegen = new BoatJavaCodeGen(); + codegen.setOutputDir(output); + codegen.setInputSpec(input.getAbsolutePath()); + codegen.setModelPackage(modelPackage); + codegen.setApiPackage(apiPackage); + + var openApiInput = new OpenAPIParser() + .readLocation(input.getAbsolutePath(), null, new ParseOptions()) + .getOpenAPI(); + var clientOptInput = new ClientOptInput(); + clientOptInput.config(codegen); + clientOptInput.openAPI(openApiInput); + + List files = new DefaultGenerator().opts(clientOptInput).generate(); + + // Verify API class has @Deprecated and deprecation message + File apiFile = files.stream().filter(file -> file.getName().equals("ItemsApi.java")) + .findFirst() + .orElseThrow(); + String apiContent = Files.readString(apiFile.toPath()); + assertTrue(apiContent.contains("@Deprecated"), "API should have @Deprecated annotation"); + assertTrue(apiContent.contains("@deprecated"), "API should have @deprecated Javadoc tag"); + assertTrue(apiContent.contains("will be removed on 2026-12-31"), "API should have sunset date in message"); + + // Verify model with deprecated property + File itemFile = files.stream().filter(file -> file.getName().equals("Item.java")) + .findFirst() + .orElseThrow(); + String itemContent = Files.readString(itemFile.toPath()); + assertTrue(itemContent.contains("@Deprecated"), "Item model should have @Deprecated annotation"); + assertTrue(itemContent.contains("will be removed on 2026-12-31"), "Item model should have sunset date in message"); + } } diff --git a/boat-scaffold/src/test/java/com/backbase/oss/codegen/java/BoatSpringCodeGenTests.java b/boat-scaffold/src/test/java/com/backbase/oss/codegen/java/BoatSpringCodeGenTests.java index 1d24004c4..8e084e722 100644 --- a/boat-scaffold/src/test/java/com/backbase/oss/codegen/java/BoatSpringCodeGenTests.java +++ b/boat-scaffold/src/test/java/com/backbase/oss/codegen/java/BoatSpringCodeGenTests.java @@ -617,4 +617,62 @@ static Stream unwrapEscapedQuotesCases() { Arguments.of("prefix\\\\\"quoted\\\\\"suffix", "prefix\\\"quoted\\\"suffix"), Arguments.of("\\\"", "\\\"")); } + + @Test + void shouldGenerateDeprecationAnnotationsFromSpecLevel() throws IOException { + var modelPackage = "com.backbase.model"; + var apiPackage = "com.backbase.api"; + var input = new File("src/test/resources/boat-spring/deprecated-spec.yaml"); + var output = TEST_OUTPUT + "/shouldGenerateDeprecationAnnotationsFromSpecLevel"; + + var codegen = new BoatSpringCodeGen(); + codegen.setLibrary("spring-boot"); + codegen.setInterfaceOnly(true); + codegen.setSkipDefaultInterface(true); + codegen.setOutputDir(output); + codegen.setInputSpec(input.getAbsolutePath()); + codegen.setModelPackage(modelPackage); + codegen.setApiPackage(apiPackage); + codegen.additionalProperties().put(SpringCodegen.USE_SPRING_BOOT3, Boolean.TRUE.toString()); + + var openApiInput = new OpenAPIParser() + .readLocation(input.getAbsolutePath(), null, new ParseOptions()) + .getOpenAPI(); + var clientOptInput = new ClientOptInput(); + clientOptInput.config(codegen); + clientOptInput.openAPI(openApiInput); + + List files = new DefaultGenerator().opts(clientOptInput).generate(); + + // Verify API interface has @Deprecated and deprecation message + File apiFile = files.stream().filter(file -> file.getName().equals("ItemsApi.java")) + .findFirst() + .orElseThrow(); + String apiContent = Files.readString(apiFile.toPath()); + assertTrue(apiContent.contains("@Deprecated"), "API should have @Deprecated annotation"); + assertTrue(apiContent.contains("@deprecated"), "API should have @deprecated Javadoc tag"); + assertTrue(apiContent.contains("will be removed on 2026-12-31"), "API should have sunset date in message"); + + // Verify model with deprecated property + File itemFile = files.stream().filter(file -> file.getName().equals("Item.java")) + .findFirst() + .orElseThrow(); + String itemContent = Files.readString(itemFile.toPath()); + assertTrue(itemContent.contains("@Deprecated"), "Item model should have @Deprecated annotation"); + assertTrue(itemContent.contains("will be removed on 2026-12-31"), "Item model should have sunset date in message"); + + // Verify getDescription property is marked deprecated (it has deprecated: true in spec) + MethodDeclaration getDescriptionMethod = StaticJavaParser.parse(itemFile) + .findAll(MethodDeclaration.class) + .stream() + .filter(it -> "getDescription".equals(it.getName().toString())) + .findFirst() + .orElseThrow(); + assertTrue(getDescriptionMethod.isAnnotationPresent("Deprecated"), + "getDescription should have @Deprecated annotation"); + int getDescriptionStart = itemContent.indexOf("public String getDescription()"); + String getDescriptionBlock = itemContent.substring(itemContent.lastIndexOf("/**", getDescriptionStart), getDescriptionStart); + assertTrue(getDescriptionBlock.contains("@deprecated") && getDescriptionBlock.contains("will be removed on 2026-12-31"), + "getDescription Javadoc should contain @deprecated with sunset date"); + } } diff --git a/boat-scaffold/src/test/java/com/backbase/oss/codegen/utils/DeprecationExtensionsTests.java b/boat-scaffold/src/test/java/com/backbase/oss/codegen/utils/DeprecationExtensionsTests.java new file mode 100644 index 000000000..ed5bdc05f --- /dev/null +++ b/boat-scaffold/src/test/java/com/backbase/oss/codegen/utils/DeprecationExtensionsTests.java @@ -0,0 +1,182 @@ +package com.backbase.oss.codegen.utils; + +import io.swagger.v3.oas.models.info.Info; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class DeprecationExtensionsTests { + + @Test + void isSpecDeprecated_nullInfo() { + assertFalse(DeprecationExtensions.isSpecDeprecated(null)); + } + + @Test + void isSpecDeprecated_nullExtensions() { + Info info = new Info(); + assertFalse(DeprecationExtensions.isSpecDeprecated(info)); + } + + @Test + void isSpecDeprecated_booleanTrue() { + Info info = new Info(); + Map extensions = new HashMap<>(); + extensions.put("x-deprecated", true); + info.setExtensions(extensions); + assertTrue(DeprecationExtensions.isSpecDeprecated(info)); + } + + @Test + void isSpecDeprecated_booleanFalse() { + Info info = new Info(); + Map extensions = new HashMap<>(); + extensions.put("x-deprecated", false); + info.setExtensions(extensions); + assertFalse(DeprecationExtensions.isSpecDeprecated(info)); + } + + @Test + void isSpecDeprecated_stringTrue() { + Info info = new Info(); + Map extensions = new HashMap<>(); + extensions.put("x-deprecated", "true"); + info.setExtensions(extensions); + assertTrue(DeprecationExtensions.isSpecDeprecated(info)); + } + + @Test + void isSpecDeprecated_stringTrueUppercase() { + Info info = new Info(); + Map extensions = new HashMap<>(); + extensions.put("x-deprecated", "TRUE"); + info.setExtensions(extensions); + assertTrue(DeprecationExtensions.isSpecDeprecated(info)); + } + + @Test + void isSpecDeprecated_stringTrueMixed() { + Info info = new Info(); + Map extensions = new HashMap<>(); + extensions.put("x-deprecated", "TrUe"); + info.setExtensions(extensions); + assertTrue(DeprecationExtensions.isSpecDeprecated(info)); + } + + @Test + void isSpecDeprecated_stringFalse() { + Info info = new Info(); + Map extensions = new HashMap<>(); + extensions.put("x-deprecated", "false"); + info.setExtensions(extensions); + assertFalse(DeprecationExtensions.isSpecDeprecated(info)); + } + + @Test + void isSpecDeprecated_stringEmpty() { + Info info = new Info(); + Map extensions = new HashMap<>(); + extensions.put("x-deprecated", ""); + info.setExtensions(extensions); + assertFalse(DeprecationExtensions.isSpecDeprecated(info)); + } + + @Test + void isSpecDeprecated_numberZero() { + Info info = new Info(); + Map extensions = new HashMap<>(); + extensions.put("x-deprecated", 0); + info.setExtensions(extensions); + assertFalse(DeprecationExtensions.isSpecDeprecated(info)); + } + + @Test + void isSpecDeprecated_numberOne() { + Info info = new Info(); + Map extensions = new HashMap<>(); + extensions.put("x-deprecated", 1); + info.setExtensions(extensions); + assertFalse(DeprecationExtensions.isSpecDeprecated(info)); + } + + @Test + void isSpecDeprecated_keyMissing() { + Info info = new Info(); + Map extensions = new HashMap<>(); + info.setExtensions(extensions); + assertFalse(DeprecationExtensions.isSpecDeprecated(info)); + } + + @Test + void getSunsetDate_nullInfo() { + Optional result = DeprecationExtensions.getSunsetDate(null); + assertTrue(result.isEmpty()); + } + + @Test + void getSunsetDate_nullExtensions() { + Info info = new Info(); + Optional result = DeprecationExtensions.getSunsetDate(info); + assertTrue(result.isEmpty()); + } + + @Test + void getSunsetDate_validDate() { + Info info = new Info(); + Map extensions = new HashMap<>(); + extensions.put("x-sunset-date", "2026-12-31"); + info.setExtensions(extensions); + Optional result = DeprecationExtensions.getSunsetDate(info); + assertTrue(result.isPresent()); + assertEquals(LocalDate.of(2026, 12, 31), result.get()); + } + + @Test + void getSunsetDate_invalidDate() { + Info info = new Info(); + Map extensions = new HashMap<>(); + extensions.put("x-sunset-date", "not-a-date"); + info.setExtensions(extensions); + Optional result = DeprecationExtensions.getSunsetDate(info); + assertTrue(result.isEmpty()); + } + + @Test + void getSunsetDate_emptyString() { + Info info = new Info(); + Map extensions = new HashMap<>(); + extensions.put("x-sunset-date", ""); + info.setExtensions(extensions); + Optional result = DeprecationExtensions.getSunsetDate(info); + assertTrue(result.isEmpty()); + } + + @Test + void getSunsetDate_keyMissing() { + Info info = new Info(); + Map extensions = new HashMap<>(); + info.setExtensions(extensions); + Optional result = DeprecationExtensions.getSunsetDate(info); + assertTrue(result.isEmpty()); + } + + @Test + void buildDeprecationMessage_withoutDate() { + String message = DeprecationExtensions.buildDeprecationMessage(Optional.empty()); + assertEquals("This API is deprecated.", message); + } + + @Test + void buildDeprecationMessage_withDate() { + LocalDate date = LocalDate.of(2026, 12, 31); + String message = DeprecationExtensions.buildDeprecationMessage(Optional.of(date)); + assertEquals("This API is deprecated and will be removed on 2026-12-31.", message); + } +} diff --git a/boat-scaffold/src/test/resources/boat-spring/deprecated-spec.yaml b/boat-scaffold/src/test/resources/boat-spring/deprecated-spec.yaml new file mode 100644 index 000000000..252db922c --- /dev/null +++ b/boat-scaffold/src/test/resources/boat-spring/deprecated-spec.yaml @@ -0,0 +1,85 @@ +openapi: 3.0.3 +info: + title: Deprecated API Spec + version: 1.0.0 + description: Test spec with x-deprecated and x-sunset-date + x-deprecated: true + x-sunset-date: 2026-12-31 + +servers: + - url: https://api.example.com + +paths: + /items: + get: + operationId: getItems + summary: Get all items + tags: + - Items + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Item' + post: + operationId: createItem + summary: Create an item + tags: + - Items + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Item' + responses: + '201': + description: Created + content: + application/json: + schema: + $ref: '#/components/schemas/Item' + + /items/{id}: + get: + operationId: getItem + summary: Get item by ID + tags: + - Items + deprecated: true + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Item' + +components: + schemas: + Item: + type: object + required: + - id + - name + properties: + id: + type: string + description: Item ID + name: + type: string + description: Item name + description: + type: string + description: Item description + deprecated: true diff --git a/plan-deprecation-annotation-2.md b/plan-deprecation-annotation-2.md new file mode 100644 index 000000000..0b515f5e1 --- /dev/null +++ b/plan-deprecation-annotation-2.md @@ -0,0 +1,201 @@ +# Plan: `@Deprecated` annotation generation for deprecated API elements + +## Goal + +When generating Java code (and, where the target language supports it, other +languages), BOAT should mark generated code as deprecated based on two +independent signals in the OpenAPI spec: + +1. **Standard OAS `deprecated: true`** on an operation, parameter, schema, or + property — endpoint/element-level deprecation. +2. **Custom `x-deprecated: true` on `info`** — deprecates the *entire* spec + (every generated operation/model). May be paired with a custom + `x-sunset-date` (ISO date) on `info`, giving the expected removal date. + When present, the sunset date must be included in the deprecation + message wherever the target annotation/comment mechanism supports a + free-text message. + +## Current state (from codebase survey) + +- Standard `deprecated: true` already flows through unmodified: + openapi-generator's `DefaultCodegen` auto-populates + `CodegenOperation.isDeprecated`, `CodegenProperty.deprecated`, + `CodegenModel.isDeprecated` from the OAS `deprecated` flag, and BOAT's + existing mustache templates already emit a bare `@Deprecated` annotation + + `@deprecated` Javadoc tag for these: + - `boat-scaffold/src/main/templates/boat-java/api.mustache`, + `pojo.mustache` + - `boat-scaffold/src/main/templates/boat-spring/api.mustache`, + `apiController.mustache`, `apiDelegate.mustache`, `pojo.mustache` + - `boat-scaffold/src/main/templates/boat-swift5/api.mustache` (also shows + precedent for reading a custom vendor extension, + `x-bb-api-deprecation-description`, straight from a template) + - `boat-scaffold/src/main/templates/boat-docs/*.mustache` (badge only, no + message) + + **Gap:** no message/sunset-date text is ever attached — the tag is always + bare `@deprecated` with nothing after it. + +- `x-deprecated` / `x-sunset-date` today are read in exactly one place: + `boat-quay/boat-quay-rules/src/main/kotlin/com/backbase/oss/boat/quay/ruleset/InfoBlockSunsetDateChecker.kt` + (lint rule B015). It reads them off `context.api.info.extensions` as raw + string-literal map keys (`"x-deprecated"`, `"x-sunset-date"`) — there is + no shared constants class for these keys, and boat-quay is not on + boat-scaffold's classpath, so the codegen side needs its own copy of this + logic (same key names, same tri-state boolean parsing). + + **Gap:** nothing in boat-scaffold reads `info`-level extensions at all, and + nothing propagates a whole-spec deprecation flag down into every generated + operation/model. + +- `boat-engine`'s `Deprecator` transformer and `boat-maven-plugin`'s + `RemoveDeprecatedMojo` are unrelated — they *strip out* deprecated content + rather than annotate it. Not touched by this feature. + +## Design + +### 1. Read the info-level extensions once per spec + +Add a small shared helper (new class, since no existing constants class +covers this) in `boat-scaffold`, e.g. +`com.backbase.oss.codegen.utils.DeprecationExtensions`, exposing: + +```java +public static final String X_DEPRECATED = "x-deprecated"; +public static final String X_SUNSET_DATE = "x-sunset-date"; + +static boolean isSpecDeprecated(Info info); // mirrors B015's isTrue() tri-state parsing +static Optional getSunsetDate(Info info); // mirrors B015's ISO_LOCAL_DATE parsing +static String buildDeprecationMessage(Optional sunsetDate); // "This API is deprecated" [+ " and will be removed on {date}."] +``` + +Reusing the exact same key names and parsing rules as B015 keeps lint and +codegen consistent for anyone comparing behavior. + +### 2. Propagate whole-spec deprecation into codegen + +In each affected generator's `preprocessOpenAPI(OpenAPI openAPI)` override +(new override where one doesn't exist yet — none currently touch +`info.extensions`): + +- Compute `specDeprecated` + `deprecationMessage` once. +- Put them into `additionalProperties` (`boatApiDeprecated`, + `boatApiDeprecationMessage`) so every template can see them globally, + following the existing convention used for other spec-wide flags exposed + via `additionalProperties` (e.g. in `BoatDocsGenerator`'s constructor). + +Generators to update: +- `BoatJavaCodeGen` (`boat-scaffold/src/main/java/com/backbase/oss/codegen/java/BoatJavaCodeGen.java`) +- `BoatSpringCodeGen` (same package) — `BoatWebhooksCodeGen` inherits it for free. +- `BoatDocsGenerator` (docs, so the badge can show the message). + +### 3. Force element-level `isDeprecated`/`deprecated` when the whole spec is deprecated + +Even when an individual operation/property doesn't set `deprecated: true`, +if `boatApiDeprecated` is true it must still be rendered as deprecated. +Override, per generator: + +- `fromOperation(...)`: after calling `super.fromOperation(...)`, if + `additionalProperties.get("boatApiDeprecated") == true`, force + `op.isDeprecated = true`. Mirrors the existing pattern in + `BoatSpringCodeGen.fromOperation` (already post-processes the result of + `super.fromOperation`). +- `postProcessModelProperty(model, property)`: same idea, force + `property.deprecated = true`. Mirrors the existing + `BoatSpringCodeGen.postProcessModelProperty` pattern (already mutates + `property.vendorExtensions` after calling `super`). +- `postProcessModels(...)` / `postProcessAllModels(...)`: force + `model.isDeprecated = true` for every model when spec-deprecated. + +### 4. Carry the message alongside the flag + +Rather than only a boolean, also stash `boatApiDeprecationMessage` on the +operation/model/property itself (via `vendorExtensions`, the same mechanism +already used for `x-bb-api-deprecation-description` in the swift5 template) +so templates can render it without re-deriving it: + +- In the same `fromOperation`/`postProcessModelProperty`/model-processing + hooks above, when forcing `isDeprecated`/`deprecated` to true, also set + `vendorExtensions.put("x-boat-deprecation-message", deprecationMessage)` + if not already present (an operation-level `deprecated: true` with no + spec-level info still gets bare deprecation, no message — matches spec: + the message only exists when `x-sunset-date`/`x-deprecated` are present). + +### 5. Template changes + +Update the Javadoc `@deprecated` lines (annotation itself stays bare +`@Deprecated` — the Java `@Deprecated` annotation has no free-text message +slot pre/post Java 9; only the Javadoc tag can carry text) to render the +message when present, falling back to today's bare tag otherwise: + +- `boat-scaffold/src/main/templates/boat-java/api.mustache` +- `boat-scaffold/src/main/templates/boat-java/pojo.mustache` +- `boat-scaffold/src/main/templates/boat-spring/api.mustache` +- `boat-scaffold/src/main/templates/boat-spring/apiController.mustache` +- `boat-scaffold/src/main/templates/boat-spring/apiDelegate.mustache` +- `boat-scaffold/src/main/templates/boat-spring/pojo.mustache` (also feeds the + message into the existing `@Schema(deprecated = true, ...)`/`@Operation` + swagger annotation as a `description` addendum, since springdoc has no + message field either) + +Pattern (illustrative, exact mustache syntax to match existing style): + +```mustache +{{#isDeprecated}} + * @deprecated{{#vendorExtensions.x-boat-deprecation-message}} {{.}}{{/vendorExtensions.x-boat-deprecation-message}} +{{/isDeprecated}} +... +{{#isDeprecated}} + @Deprecated +{{/isDeprecated}} +``` + +Docs template (`boat-scaffold/src/main/templates/boat-docs/index.mustache`, +`param.mustache`) gets the message rendered next to the existing +"Deprecated" badge. + +### 6. Other languages + +Explicitly out of initial scope beyond Java/Spring, but the mechanism +generalizes cleanly since `vendorExtensions` + `additionalProperties` are +generator-agnostic: +- **Swift** already has a working precedent + (`x-bb-api-deprecation-description` → `@available(*, deprecated, message: + "...")`). Could be pointed at the same + `x-boat-deprecation-message`/`boatApiDeprecated` values in a follow-up, + since Swift's annotation *does* support a message. +- **TypeScript/Angular**: JSDoc `@deprecated` supports free text the same + way Javadoc does — same template pattern applies to + `boat-angular` templates in a follow-up. +- Not implementing these now; noting the extension point so it's not a + redesign later. + +## Testing + +- Unit tests for `DeprecationExtensions` (boolean tri-state parsing, missing + info, missing extensions, invalid date). +- Codegen tests (extend existing fixtures under + `boat-scaffold/src/test/resources` and the corresponding test classes, + e.g. `BoatCommonJavaCodeGenTests`, boat-spring codegen tests): + - Spec with `info.x-deprecated: true` + `x-sunset-date` → every generated + method/model carries `@Deprecated` and a Javadoc `@deprecated` line with + the sunset date. + - Spec with only an operation-level `deprecated: true` (no info-level + flags) → unchanged existing behavior (bare `@Deprecated`, no message) — + regression guard for the existing feature. + - Spec with `info.x-deprecated: true` but no `x-sunset-date` → message + without a date ("This API is deprecated."). + - Spec with neither → no deprecation anywhere (regression guard). +- Docs generator test: sunset-date message appears in generated HTML next + to the deprecated badge. + +## Rollout + +- No new CLI flags/config needed — behavior is driven entirely by spec + content (`deprecated`, `info.x-deprecated`, `info.x-sunset-date`), + consistent with how B015 already treats these as always-on, spec-driven + signals. +- Version bump + CHANGELOG entry per existing release conventions. +- Update BOAT docs (README/wiki or boat-docs pages describing supported + `x-*` extensions) to document `x-deprecated`/`x-sunset-date` behavior for + codegen, not just for the B015 lint rule.