Skip to content

Commit

Permalink
add swagger tools
Browse files Browse the repository at this point in the history
  • Loading branch information
SirCotare committed Sep 27, 2024
1 parent c5dd449 commit af7de53
Show file tree
Hide file tree
Showing 11 changed files with 248 additions and 7 deletions.
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,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

0 comments on commit af7de53

Please sign in to comment.