Fix Page response model inconsistent in swagger ui (#1277)

pull/1279/head
John Niang 2021-02-19 21:35:37 +08:00 committed by GitHub
parent cdc15942cf
commit 49a461f245
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 172 additions and 62 deletions

View File

@ -5,6 +5,8 @@ import static run.halo.app.model.support.HaloConst.ADMIN_TOKEN_QUERY_NAME;
import static run.halo.app.model.support.HaloConst.API_ACCESS_KEY_HEADER_NAME;
import static run.halo.app.model.support.HaloConst.API_ACCESS_KEY_QUERY_NAME;
import static run.halo.app.model.support.HaloConst.HALO_VERSION;
import static run.halo.app.utils.SwaggerUtils.customMixin;
import static run.halo.app.utils.SwaggerUtils.propertyBuilder;
import static springfox.documentation.schema.AlternateTypeRules.newRule;
import com.fasterxml.classmate.TypeResolver;
@ -19,30 +21,27 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.lang.NonNull;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.util.PathMatcher;
import run.halo.app.config.properties.HaloProperties;
import run.halo.app.model.entity.User;
import run.halo.app.security.support.UserDetail;
import springfox.documentation.builders.AlternateTypeBuilder;
import springfox.documentation.builders.AlternateTypePropertyBuilder;
import run.halo.app.utils.SwaggerUtils;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.builders.ResponseMessageBuilder;
import springfox.documentation.schema.AlternateTypeRule;
import springfox.documentation.schema.AlternateTypeRuleConvention;
import springfox.documentation.schema.WildcardType;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.ApiKey;
import springfox.documentation.service.AuthorizationScope;
import springfox.documentation.service.Contact;
import springfox.documentation.service.ResponseMessage;
import springfox.documentation.service.SecurityReference;
import springfox.documentation.service.SecurityScheme;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.contexts.SecurityContext;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger.web.DocExpansion;
@ -69,14 +68,6 @@ public class SwaggerConfiguration {
private final HaloProperties haloProperties;
private final List<ResponseMessage> globalResponses = Arrays.asList(
new ResponseMessageBuilder().code(200).message("Success").build(),
new ResponseMessageBuilder().code(400).message("Bad request").build(),
new ResponseMessageBuilder().code(401).message("Unauthorized").build(),
new ResponseMessageBuilder().code(403).message("Forbidden").build(),
new ResponseMessageBuilder().code(404).message("Not found").build(),
new ResponseMessageBuilder().code(500).message("Internal server error").build());
public SwaggerConfiguration(HaloProperties haloProperties) {
this.haloProperties = haloProperties;
}
@ -143,18 +134,13 @@ public class SwaggerConfiguration {
Assert.hasText(basePackage, "Base package must not be blank");
Assert.hasText(antPattern, "Ant pattern must not be blank");
return new Docket(DocumentationType.SWAGGER_2)
return SwaggerUtils.defaultDocket()
.groupName(groupName)
.select()
.apis(RequestHandlerSelectors.basePackage(basePackage))
.paths(PathSelectors.ant(antPattern))
.build()
.apiInfo(apiInfo())
.useDefaultResponseMessages(false)
.globalResponseMessage(RequestMethod.GET, globalResponses)
.globalResponseMessage(RequestMethod.POST, globalResponses)
.globalResponseMessage(RequestMethod.DELETE, globalResponses)
.globalResponseMessage(RequestMethod.PUT, globalResponses)
.directModelSubstitute(Temporal.class, String.class);
}
@ -166,10 +152,14 @@ public class SwaggerConfiguration {
}
private List<SecurityContext> adminSecurityContext() {
final PathMatcher pathMatcher = new AntPathMatcher();
return Collections.singletonList(
SecurityContext.builder()
.securityReferences(defaultAuth())
.forPaths(PathSelectors.regex("/api/admin/.*"))
.operationSelector(operationContext -> {
var requestMappingPattern = operationContext.requestMappingPattern();
return pathMatcher.match("/api/admin/**/*", requestMappingPattern);
})
.build()
);
}
@ -182,10 +172,14 @@ public class SwaggerConfiguration {
}
private List<SecurityContext> contentSecurityContext() {
final PathMatcher pathMatcher = new AntPathMatcher();
return Collections.singletonList(
SecurityContext.builder()
.securityReferences(contentApiAuth())
.forPaths(PathSelectors.regex("/api/content/.*"))
.operationSelector(operationContext -> {
var requestMappingPattern = operationContext.requestMappingPattern();
return pathMatcher.match("/api/content/**/*", requestMappingPattern);
})
.build()
);
}
@ -228,56 +222,55 @@ public class SwaggerConfiguration {
@Override
public List<AlternateTypeRule> rules() {
return Arrays.asList(
newRule(User.class, emptyMixin(User.class)),
newRule(UserDetail.class, emptyMixin(UserDetail.class)),
newRule(resolver.resolve(Page.class, WildcardType.class),
resolver.resolve(CustomizedPage.class, WildcardType.class)),
newRule(resolver.resolve(Pageable.class), resolver.resolve(pageableMixin())),
newRule(resolver.resolve(Sort.class), resolver.resolve(sortMixin())));
}
};
}
/**
* For controller parameter(like eg: HttpServletRequest, ModelView ...).
*
* @param clazz controller parameter class type must not be null
* @return empty type
*/
private Type emptyMixin(Class<?> clazz) {
Assert.notNull(clazz, "class type must not be null");
return new AlternateTypeBuilder()
.fullyQualifiedClassName(String
.format("%s.generated.%s", clazz.getPackage().getName(), clazz.getSimpleName()))
.withProperties(Collections.emptyList())
.build();
}
private Type sortMixin() {
return new AlternateTypeBuilder()
.fullyQualifiedClassName(String
.format("%s.generated.%s", Sort.class.getPackage().getName(),
Sort.class.getSimpleName()))
.withProperties(Collections.singletonList(property(String[].class, "sort")))
.build();
return customMixin(Sort.class,
Collections.singletonList(propertyBuilder(String[].class, "sort")));
}
private Type pageableMixin() {
return new AlternateTypeBuilder()
.fullyQualifiedClassName(String
.format("%s.generated.%s", Pageable.class.getPackage().getName(),
Pageable.class.getSimpleName()))
.withProperties(Arrays
.asList(property(Integer.class, "page"), property(Integer.class, "size"),
property(String[].class, "sort")))
.build();
return customMixin(Pageable.class, Arrays.asList(
propertyBuilder(Integer.class, "page"),
propertyBuilder(Integer.class, "size"),
propertyBuilder(String[].class, "sort")
));
}
private AlternateTypePropertyBuilder property(Class<?> type, String name) {
return new AlternateTypePropertyBuilder()
.withName(name)
.withType(type)
.withCanRead(true)
.withCanWrite(true);
/**
* Alternative page type.
*
* @param <T> content type
* @author johnniang
*/
interface CustomizedPage<T> {
List<T> getContent();
int getPage();
int getPages();
long getTotal();
int getRpp();
boolean getHasNext();
boolean getHasPrevious();
boolean getIsFirst();
boolean getIsEmpty();
boolean getHasContent();
}
}

View File

@ -0,0 +1,117 @@
package run.halo.app.utils;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
import org.springframework.http.HttpMethod;
import org.springframework.util.Assert;
import run.halo.app.model.entity.User;
import run.halo.app.security.authentication.Authentication;
import run.halo.app.security.support.UserDetail;
import springfox.documentation.builders.AlternateTypeBuilder;
import springfox.documentation.builders.AlternateTypePropertyBuilder;
import springfox.documentation.builders.ResponseBuilder;
import springfox.documentation.service.Response;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
/**
* Swagger utils.
*
* @author johnniang
*/
public final class SwaggerUtils {
private SwaggerUtils() {
}
public static Type customMixin(Class<?> clazz,
List<Consumer<AlternateTypePropertyBuilder>> properties) {
Assert.notNull(clazz, "Class must not be null");
final var typeBuilder = new AlternateTypeBuilder()
.fullyQualifiedClassName(
String.format("%s.generated.%s", clazz.getPackage().getName(),
clazz.getSimpleName()));
properties.forEach(typeBuilder::property);
return typeBuilder.build();
}
public static Type emptyMixin(Class<?> clazz) {
return customMixin(clazz, Collections.emptyList());
}
public static Consumer<AlternateTypePropertyBuilder> propertyBuilder(Class<?> clazz,
String name) {
return propertyBuilder -> propertyBuilder.type(clazz)
.name(name)
.canRead(true)
.canWrite(true);
}
public static final List<Response> GLOBAL_RESPONSES = Arrays.asList(
new ResponseBuilder().code("200").description("The request has succeeded.").isDefault(true)
.build(),
new ResponseBuilder().code("201").description(
"The request has succeeded and a new resource has been created as a result.").build(),
new ResponseBuilder().code("204").description(
"There is no content to send for this request, but the headers may be useful.").build(),
new ResponseBuilder().code("400")
.description("The server could not understand the request due to invalid syntax.")
.build(),
new ResponseBuilder().code("401").description("Although the HTTP standard specifies "
+ "\"unauthorized\", semantically this response means \"unauthenticated\"").build(),
new ResponseBuilder().code("403")
.description("The client does not have access rights to the content.").build(),
new ResponseBuilder().code("404")
.description("The server can not find the requested resource.").build(),
new ResponseBuilder().code("405").description(
"The request method is known by the server but has been disabled and cannot be used. ")
.build(),
new ResponseBuilder().code("500")
.description("The server has encountered a situation it doesn't know how to handle.")
.build(),
new ResponseBuilder().code("501")
.description("The request method is not supported by the server and cannot be handled.")
.build(),
new ResponseBuilder().code("503")
.description("The server is not ready to handle the request.").build());
public static Docket defaultDocket() {
return new Docket(DocumentationType.OAS_30)
.forCodeGeneration(true)
.ignoredParameterTypes(initIgnore())
.useDefaultResponseMessages(false)
.globalResponses(HttpMethod.GET, GLOBAL_RESPONSES)
.globalResponses(HttpMethod.POST, GLOBAL_RESPONSES)
.globalResponses(HttpMethod.DELETE, GLOBAL_RESPONSES)
.globalResponses(HttpMethod.PATCH, GLOBAL_RESPONSES)
.globalResponses(HttpMethod.PUT, GLOBAL_RESPONSES);
}
public static Optional<Class<?>> classFor(String className) {
try {
return Optional
.of(Class.forName(className, false, SwaggerUtils.class.getClassLoader()));
} catch (ClassNotFoundException e) {
return Optional.empty();
}
}
private static Class<?>[] initIgnore() {
final Set<Class<?>> ignoredClasses = new HashSet<>();
ignoredClasses.add(User.class);
ignoredClasses.add(UserDetail.class);
ignoredClasses.add(Authentication.class);
classFor(User.class.getName()).ifPresent(ignoredClasses::add);
return ignoredClasses.toArray(Class[]::new);
}
}