Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ab 253 extract swagger utilities to our backend lib #11

Merged
merged 2 commits into from
Sep 27, 2024
Merged
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
5 changes: 5 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@
<artifactId>spring-boot-starter-json</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
</dependency>

<!-- Utilities -->
<dependency>
<groupId>org.projectlombok</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package it.aboutbits.springboot.toolbox.autoconfiguration.swagger;

import it.aboutbits.springboot.toolbox.swagger.CustomTypeModelConverter;
import it.aboutbits.springboot.toolbox.swagger.CustomTypePropertyCustomizer;
import it.aboutbits.springboot.toolbox.swagger.type.CustomTypeModelConverter;
import it.aboutbits.springboot.toolbox.swagger.type.CustomTypePropertyCustomizer;
import org.springframework.context.annotation.Import;

import java.lang.annotation.ElementType;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package it.aboutbits.springboot.toolbox.mvc.response;

import org.springframework.lang.Nullable;

import java.util.List;
import java.util.Map;

public record ErrorResponse(
@Nullable String message,
@Nullable Map<String, List<String>> errors
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package it.aboutbits.springboot.toolbox.mvc.response;

import lombok.NonNull;

public record ItemResponse<T>(@NonNull T data) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package it.aboutbits.springboot.toolbox.mvc.response;

import lombok.NonNull;

import java.util.List;

public record ListResponse<T>(@NonNull List<T> data) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package it.aboutbits.springboot.toolbox.mvc.response;

import lombok.NonNull;
import org.springframework.data.domain.Page;

import java.util.List;

public record PagedResponse<T>(
@NonNull
List<T> data,
@NonNull
MetaWithPagination meta
) {

public record MetaWithPagination(
@NonNull
Pagination pagination
) {
public record Pagination(long page, int size, long totalElements) {
public static Pagination of(@NonNull Page<?> page) {
return new Pagination(
page.getNumber(),
page.getSize(),
page.getTotalElements()
);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package it.aboutbits.springboot.toolbox.swagger.annotations;

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

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface SwaggerScopedAuth {
String value();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package it.aboutbits.springboot.toolbox.swagger.customization.alphabetical_model_order;

import io.swagger.v3.oas.models.OpenAPI;
import org.springdoc.core.customizers.OpenApiCustomizer;

import java.util.TreeMap;

public class OrderModelsCustomizer implements OpenApiCustomizer {
@Override
public void customise(OpenAPI openApi) {
var components = openApi.getComponents();

components.schemas(new TreeMap<>(components.getSchemas()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package it.aboutbits.springboot.toolbox.swagger.customization.authorization_docs;

import io.swagger.v3.oas.models.Operation;
import it.aboutbits.springboot.toolbox.swagger.annotations.SwaggerScopedAuth;
import lombok.extern.slf4j.Slf4j;
import org.springdoc.core.customizers.OperationCustomizer;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.method.HandlerMethod;

import java.util.ArrayList;
import java.util.Optional;

@Slf4j
public class AuthorizationDescriptor implements OperationCustomizer {
@Override
public Operation customize(Operation operation, HandlerMethod handlerMethod) {
try {
var additionalDescription = new ArrayList<String>();

var maybeAnnotation = Optional.ofNullable(handlerMethod.getMethodAnnotation(PreAuthorize.class));
if (maybeAnnotation.isPresent()) {
var annotation = maybeAnnotation.get();
additionalDescription.add("<b>Authorization:</b> " + annotation.value());
}

var maybeAnnotation2 = Optional.ofNullable(handlerMethod.getMethodAnnotation(SwaggerScopedAuth.class));
if (maybeAnnotation2.isPresent()) {
var annotation = maybeAnnotation2.get();
additionalDescription.add("<b>Scoped Authorization:</b> " + annotation.value());
}

if (!additionalDescription.isEmpty()) {

var currentDescription = Optional.ofNullable(operation.getDescription());

var description = String.join("<br />", additionalDescription);
if (currentDescription.isPresent()) {
description = "<p>" + description + "</p>" + currentDescription.get();
}

operation.description(
description
);
}
} catch (Exception e) {
log.error("Error when creating swagger documentation for authorities.", e);
}
return operation;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package it.aboutbits.springboot.toolbox.swagger.customization.default_not_null;

import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.media.Schema;
import org.springdoc.core.customizers.OpenApiCustomizer;

import java.util.ArrayList;

public class NullableCustomizer implements OpenApiCustomizer {
@Override
@SuppressWarnings("unchecked")
public void customise(OpenAPI openApi) {
openApi.getComponents().getSchemas().values()
.forEach(schema -> {
var requiredProperties = new ArrayList<String>();
((Schema<?>) schema).getProperties().forEach((propertyName, property) -> {
if (property.getNullable() == null || !property.getNullable()) {
requiredProperties.add(propertyName);
}
});
schema.setRequired(requiredProperties);
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package it.aboutbits.springboot.toolbox.swagger.customization.default_not_null;

import io.swagger.v3.core.converter.AnnotatedType;
import io.swagger.v3.core.jackson.ModelResolver;
import io.swagger.v3.oas.models.media.Schema;
import org.springdoc.core.customizers.PropertyCustomizer;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.Arrays;

@Component
public class NullablePropertyCustomizer implements PropertyCustomizer {
static {
/*
We need this because the ModelResolver will only process these whitelisted annotations.
We will then be able to manipulate each property based on the set annotations.
*/

var list = new ArrayList<>(ModelResolver.NOT_NULL_ANNOTATIONS);
list.add("Nullable");

ModelResolver.NOT_NULL_ANNOTATIONS = list;
}

@Override
public Schema<?> customize(Schema property, AnnotatedType annotatedType) {
/*
Mark the nullable ones as nullable.
*/

if (annotatedType.getCtxAnnotations() != null && Arrays.stream(annotatedType.getCtxAnnotations())
.anyMatch(a -> "Nullable".equals(a.annotationType().getSimpleName()))) {
property.setNullable(true);
}

return property;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package it.aboutbits.springboot.toolbox.swagger.customization.error_response;

import io.swagger.v3.core.converter.ModelConverters;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.media.Content;
import io.swagger.v3.oas.models.media.MediaType;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.responses.ApiResponse;
import io.swagger.v3.oas.models.responses.ApiResponses;
import it.aboutbits.springboot.toolbox.mvc.response.ErrorResponse;
import org.springdoc.core.customizers.OpenApiCustomizer;

import java.util.Map;

public class ErrorCustomizer implements OpenApiCustomizer {
@Override
public void customise(OpenAPI openApi) {
openApi.getComponents()
.getSchemas()
.putAll(
ModelConverters.getInstance().read(ErrorResponse.class)
);

var errorResponseSchema = openApi.getComponents().getSchemas().get("ErrorResponse");
@SuppressWarnings("unchecked")
Map<String, Schema<?>> props = errorResponseSchema.getProperties();
for (var prop : props.values()) {
prop.nullable(true);
}

openApi.getPaths()
.values()
.forEach(
pathItem -> pathItem.readOperations()
.forEach(
operation -> {
ApiResponses apiResponses = operation.getResponses();
apiResponses.addApiResponse(
"400",
createApiResponse(
"Bad Request",
errorResponseSchema
)
);
apiResponses.addApiResponse(
"404",
createApiResponse(
"Not Found",
errorResponseSchema
)
);
}
)
);
}

private ApiResponse createApiResponse(String message, Schema<?> schema) {
var mediaType = new MediaType();
mediaType.schema(schema);
return new ApiResponse().description(message)
.content(new Content().addMediaType(
org.springframework.http.MediaType.APPLICATION_JSON_VALUE,
mediaType
));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package it.aboutbits.springboot.toolbox.swagger.customization.logout_route;

import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.responses.ApiResponses;
import lombok.RequiredArgsConstructor;
import org.springdoc.core.customizers.OpenApiCustomizer;

@RequiredArgsConstructor
public class LogoutCustomizer implements OpenApiCustomizer {
private final String logoutUrl;

@Override
public void customise(OpenAPI openApi) {
var operation = new Operation();
operation.addTagsItem("Authentication API");
operation.summary("Logout the current user");
operation.responses(new ApiResponses());

var pathItem = new PathItem();
pathItem.setPost(operation);

openApi.getPaths().addPathItem(logoutUrl, pathItem);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package it.aboutbits.springboot.toolbox.swagger;
package it.aboutbits.springboot.toolbox.swagger.type;

import io.swagger.v3.core.converter.AnnotatedType;
import io.swagger.v3.core.converter.ModelConverter;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package it.aboutbits.springboot.toolbox.swagger;
package it.aboutbits.springboot.toolbox.swagger.type;

import com.fasterxml.jackson.databind.type.SimpleType;
import io.swagger.v3.core.converter.AnnotatedType;
Expand All @@ -25,13 +25,17 @@ public Schema<?> customize(Schema property, AnnotatedType annotatedType) {

var displayName = rawClass.getSimpleName();

Class<?> wrappedType;
if (EntityId.class.isAssignableFrom(rawClass)) {
displayName = resolveEntityIdDisplayName(rawClass);
}

var constructor = RecordReflectionUtil.getCanonicalConstructor(rawClass);
var wrappedType = constructor.getParameters()[0].getType();

if (rawClass.equals(EntityId.class)) {
wrappedType = simpleType.getBindings().getBoundType(0).getRawClass();
} else {
var constructor = RecordReflectionUtil.getCanonicalConstructor(rawClass);
wrappedType = constructor.getParameters()[0].getType();
}

if (Short.class.isAssignableFrom(wrappedType)) {
property.type("integer");
Expand Down