Skip to content

Commit

Permalink
feat: @maturity annotation for endpoints (#17701)
Browse files Browse the repository at this point in the history
* feat: @maturity annotation for endpoints

* chore: names and meta annotations
  • Loading branch information
jbee authored Jun 20, 2024
1 parent c076476 commit e0924c5
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 12 deletions.
108 changes: 108 additions & 0 deletions dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/Maturity.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
* Copyright (c) 2004-2024, University of Oslo
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* Neither the name of the HISP project nor the names of its contributors may
* be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.hisp.dhis.common;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Can be used to mark endpoint methods with their maturity level. If types are annotated all
* endpoints of the type inherit the level unless their method has an overriding annotation.
*
* <p>Can also be used to mark endpoint parameters with their maturity level. In parameter objects
* the corresponding fields are annotated.
*
* @author Jan Bernitt
*/
@Target({
ElementType.METHOD,
ElementType.TYPE,
ElementType.PARAMETER,
ElementType.FIELD,
ElementType.ANNOTATION_TYPE
})
@Retention(RetentionPolicy.RUNTIME)
public @interface Maturity {

enum Classification {
/**
* The API is stable and rarely subject to change. Change management occurs. Once an API is
* declared stable it does not change to another classification.
*/
STABLE,
/**
* The API is not stable yet and often subject to change. No change management occurs. Usually
* it takes APIs 1-2 releases before they transition to stable.
*/
BETA,
/**
* The API is an experiment or subject to change due to factors outside the API implementation
* itself. It is subject to change or removal without any change management. An alpha API might
* transition to beta and stable at some point but might also never be suited to do so.
*/
ALPHA
}

Classification value();

/**
* The API is an experiment or subject to change due to factors outside the API implementation
* itself. It is subject to change or removal without any change management. An alpha API might
* transition to beta and stable at some point but might also never be suited to do so.
*/
@Target({ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Maturity(Classification.ALPHA)
@interface Alpha {

/**
* @return An explanation as to why the annotated element was classified as alpha
*/
String reason() default "";
}

/**
* The API is not stable yet and often subject to change. No change management occurs. Usually it
* takes APIs 1-2 releases before they transition to stable.
*/
@Target({ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Maturity(Classification.BETA)
@interface Beta {}

/**
* The API is stable and rarely subject to change. Change management occurs. Once an API is
* declared stable it does not change to another classification.
*/
@Target({ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Maturity(Classification.STABLE)
@interface Stable {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
import lombok.Value;
import org.hisp.dhis.common.EmbeddedObject;
import org.hisp.dhis.common.IdentifiableObject;
import org.hisp.dhis.common.Maturity;
import org.hisp.dhis.common.OpenApi;
import org.hisp.dhis.period.Period;
import org.springframework.http.HttpStatus;
Expand Down Expand Up @@ -216,6 +217,7 @@ public static class Endpoint {
OpenApi.Document.Group group;
Maybe<String> description = new Maybe<>();
Boolean deprecated;
@CheckForNull Maturity.Classification maturity;

@EqualsAndHashCode.Include Set<RequestMethod> methods = EnumSet.noneOf(RequestMethod.class);

Expand Down Expand Up @@ -273,7 +275,8 @@ public enum In {
boolean required;
Schema type;

Boolean deprecated;
@CheckForNull Boolean deprecated;
@CheckForNull Maturity.Classification maturity;

/**
* The default value in its string form.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,11 @@

import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.AnnotatedType;
import java.lang.reflect.Array;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.ParameterizedType;
Expand All @@ -58,6 +60,7 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
Expand All @@ -68,6 +71,7 @@
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.hisp.dhis.common.IdentifiableObject;
import org.hisp.dhis.common.Maturity;
import org.hisp.dhis.common.OpenApi;
import org.hisp.dhis.common.OpenApi.Document.Group;
import org.hisp.dhis.common.UID;
Expand Down Expand Up @@ -242,15 +246,14 @@ private static Api.Endpoint extractEndpoint(Api.Controller controller, EndpointM
consumes.add(MediaType.APPLICATION_JSON);
}

Boolean deprecated =
ConsistentAnnotatedElement.of(source).isAnnotationPresent(Deprecated.class)
? Boolean.TRUE
: null;
Boolean deprecated = source.isAnnotationPresent(Deprecated.class) ? Boolean.TRUE : null;

Group group = getEndpointGroup(source);

Maturity.Classification maturity = getMaturity(source);

Api.Endpoint endpoint =
new Api.Endpoint(controller, source, entityType, name, group, deprecated);
new Api.Endpoint(controller, source, entityType, name, group, deprecated, maturity);

endpoint.getDescription().setIfAbsent(extractDescription(source));

Expand All @@ -268,6 +271,19 @@ private static Api.Endpoint extractEndpoint(Api.Controller controller, EndpointM
return endpoint;
}

@CheckForNull
private static Maturity.Classification getMaturity(AnnotatedElement source) {
if (source.isAnnotationPresent(Maturity.class))
return source.getAnnotation(Maturity.class).value();
Optional<Annotation> meta =
Stream.of(source.getAnnotations())
.filter(a -> a.annotationType().isAnnotationPresent(Maturity.class))
.findFirst();
if (meta.isPresent()) return meta.get().annotationType().getAnnotation(Maturity.class).value();
if (source instanceof Member m) return getMaturity(m.getDeclaringClass());
return null;
}

private static Group getEndpointGroup(Method source) {
Group group = Group.DEFAULT;
if (source.getDeclaringClass().isAnnotationPresent(OpenApi.Document.class))
Expand Down Expand Up @@ -473,8 +489,10 @@ private static void extractParameters(Api.Endpoint endpoint, Set<MediaType> cons
private static Api.Parameter newGenericParameter(
Parameter source, String key, ParameterDetails details, Api.Schema type) {
boolean deprecated = source.isAnnotationPresent(Deprecated.class);
Maturity.Classification maturity = getMaturity(source);
Api.Parameter parameter =
new Api.Parameter(source, key, details.in(), details.required(), type, deprecated);
new Api.Parameter(
source, key, details.in(), details.required(), type, deprecated, maturity);
parameter.getDefaultValue().setValue(details.defaultValue());
parameter.getDescription().setIfAbsent(extractDescription(source, source.getType()));
return parameter;
Expand All @@ -485,8 +503,9 @@ private static Api.Parameter newPathParameter(
Api.Endpoint endpoint, Parameter source, String name, ParameterDetails details) {
Api.Schema type = extractInputSchema(endpoint, source.getParameterizedType());
boolean deprecated = source.isAnnotationPresent(Deprecated.class);
Maturity.Classification maturity = getMaturity(source);
Api.Parameter res =
new Api.Parameter(source, name, In.PATH, details.required(), type, deprecated);
new Api.Parameter(source, name, In.PATH, details.required(), type, deprecated, maturity);
res.getDescription().setIfAbsent(extractDescription(source, source.getType()));
return res;
}
Expand All @@ -496,8 +515,9 @@ private static Api.Parameter newQueryParameter(
Api.Endpoint endpoint, Parameter source, String name, ParameterDetails details) {
Api.Schema type = extractInputSchema(endpoint, source.getParameterizedType());
boolean deprecated = source.isAnnotationPresent(Deprecated.class);
Maturity.Classification maturity = getMaturity(source);
Api.Parameter res =
new Api.Parameter(source, name, In.QUERY, details.required(), type, deprecated);
new Api.Parameter(source, name, In.QUERY, details.required(), type, deprecated, maturity);
res.getDefaultValue().setValue(details.defaultValue());
res.getDescription().setIfAbsent(extractDescription(source, source.getType()));
return res;
Expand All @@ -521,7 +541,8 @@ private static void extractParam(
}
boolean deprecated = param.deprecated();
Api.Parameter parameter =
new Api.Parameter(endpoint.getSource(), name, In.QUERY, required, wrapped, deprecated);
new Api.Parameter(
endpoint.getSource(), name, In.QUERY, required, wrapped, deprecated, null);
endpoint.getParameters().put(name, parameter);
}

Expand Down Expand Up @@ -574,8 +595,10 @@ type instanceof Class && isGeneratorType((Class<?>) type) && annotated != null
? extractGeneratorSchema(endpoint, type, annotated.value())
: extractInputSchema(endpoint, getSubstitutedType(endpoint, property, source));
boolean deprecated = source.isAnnotationPresent(Deprecated.class);
Maturity.Classification maturity = getMaturity(source);
Api.Parameter param =
new Api.Parameter(source, property.getName(), In.QUERY, false, schema, deprecated);
new Api.Parameter(
source, property.getName(), In.QUERY, false, schema, deprecated, maturity);
Object defaultValue = property.getDefaultValue();
if (defaultValue != null) param.getDefaultValue().setValue(defaultValue.toString());
param
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -644,7 +644,8 @@ private static Api.Endpoint mergeEndpoints(Api.Endpoint a, Api.Endpoint b, Reque
primary.getEntityType(),
primary.getName() + "+" + secondary.getName(),
primary.getGroup(),
primary.getDeprecated());
primary.getDeprecated(),
primary.getMaturity());
merged
.getDescription()
.setValue(primary.getDescription().orElse(secondary.getDescription().getValue()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
import org.hisp.dhis.common.BaseIdentifiableObject;
import org.hisp.dhis.common.CodeGenerator;
import org.hisp.dhis.common.IdentifiableObject;
import org.hisp.dhis.common.Maturity;
import org.hisp.dhis.webapi.openapi.Api.Schema.Direction;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;
Expand Down Expand Up @@ -228,6 +229,7 @@ private void generatePathMethod(RequestMethod method, Api.Endpoint endpoint) {
method.name().toLowerCase(),
() -> {
addTrueMember("deprecated", endpoint.getDeprecated());
addStringMember("x-maturity", getMaturityTag(endpoint.getMaturity()));
addStringMultilineMember("description", endpoint.getDescription().orElse(NO_DESCRIPTION));
addStringMember("operationId", getUniqueOperationId(endpoint));
addInlineArrayMember("tags", List.copyOf(tags));
Expand Down Expand Up @@ -271,12 +273,17 @@ private void generateParameter(String name, Api.Parameter parameter) {
"description", parameter.getDescription().orElse(NO_DESCRIPTION));
addTrueMember("required", parameter.isRequired());
addTrueMember("deprecated", parameter.getDeprecated());
addStringMember("x-maturity", getMaturityTag(parameter.getMaturity()));
String defaultValue = parameter.getDefaultValue().orElse(null);
addObjectMember(
"schema", () -> generateSchemaOrRef(parameter.getType(), IN, defaultValue));
});
}

private static String getMaturityTag(Maturity.Classification maturity) {
return maturity == null ? null : maturity.name().toLowerCase();
}

private void generateRequestBody(Api.RequestBody requestBody) {
addStringMultilineMember("description", requestBody.getDescription().orElse(NO_DESCRIPTION));
addTrueMember("required", requestBody.isRequired());
Expand Down

0 comments on commit e0924c5

Please sign in to comment.