Add label and field selector to Extension list API (#2279)

#### What type of PR is this?

/kind feature
/kind api-change
/area core
/milestone 2.0

#### What this PR does / why we need it:

Add label and field selector to Extension list API for filtering Extensions.

<img width="322" alt="image" src="https://user-images.githubusercontent.com/16865714/181462887-549162fd-5e8d-4cec-834c-24875ada4789.png">

#### Does this PR introduce a user-facing change?

```release-note
None
```
pull/2290/head
John Niang 2022-07-29 13:22:14 +08:00 committed by GitHub
parent fdbb513cb2
commit 3640dca0a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 1154 additions and 373 deletions

View File

@ -77,6 +77,7 @@ dependencies {
implementation "io.seruco.encoding:base62:$base62"
implementation "org.pf4j:pf4j:$pf4j"
compileOnly 'org.projectlombok:lombok'
testCompileOnly 'org.projectlombok:lombok'
annotationProcessor "org.springframework.boot:spring-boot-configuration-processor"
@ -87,6 +88,7 @@ dependencies {
runtimeOnly 'org.postgresql:postgresql'
annotationProcessor 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
@ -95,5 +97,4 @@ dependencies {
tasks.named('test') {
useJUnitPlatform()
testLogging.showStandardStreams = true
}

View File

@ -19,7 +19,6 @@ import run.halo.app.extension.DefaultExtensionClient;
import run.halo.app.extension.DefaultSchemeManager;
import run.halo.app.extension.DefaultSchemeWatcherManager;
import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.ExtensionCompositeRouterFunction;
import run.halo.app.extension.JSONExtensionConverter;
import run.halo.app.extension.SchemeManager;
import run.halo.app.extension.SchemeWatcherManager;
@ -27,6 +26,7 @@ import run.halo.app.extension.SchemeWatcherManager.SchemeWatcher;
import run.halo.app.extension.controller.Controller;
import run.halo.app.extension.controller.ControllerBuilder;
import run.halo.app.extension.controller.ControllerManager;
import run.halo.app.extension.router.ExtensionCompositeRouterFunction;
import run.halo.app.extension.store.ExtensionStoreClient;
import run.halo.app.plugin.HaloPluginManager;
import run.halo.app.plugin.resources.JsBundleRuleProvider;

View File

@ -1,333 +0,0 @@
package run.halo.app.extension;
import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;
import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;
import static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuilder;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import java.net.URI;
import java.util.Objects;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.description.type.TypeDescription;
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder;
import org.springframework.http.MediaType;
import org.springframework.lang.NonNull;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.function.server.HandlerFunction;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import run.halo.app.extension.exception.ExtensionConvertException;
import run.halo.app.extension.exception.ExtensionNotFoundException;
public class ExtensionRouterFunctionFactory {
private final Scheme scheme;
private final ExtensionClient client;
public ExtensionRouterFunctionFactory(Scheme scheme, ExtensionClient client) {
this.scheme = scheme;
this.client = client;
}
@NonNull
public RouterFunction<ServerResponse> create() {
var getHandler = new ExtensionGetHandler(scheme, client);
var listHandler = new ExtensionListHandler(scheme, client);
var createHandler = new ExtensionCreateHandler(scheme, client);
var updateHandler = new ExtensionUpdateHandler(scheme, client);
var deleteHandler = new ExtensionDeleteHandler(scheme, client);
// TODO More handlers here
var gvk = scheme.groupVersionKind();
var tagName = gvk.toString();
return SpringdocRouteBuilder.route()
.GET(getHandler.pathPattern(), getHandler,
builder -> builder.operationId("Get" + gvk)
.description("Get " + gvk)
.tag(tagName)
.parameter(parameterBuilder().in(ParameterIn.PATH)
.name("name")
.description("Name of " + scheme.singular()))
.response(responseBuilder().responseCode("200")
.description("Response single " + scheme.singular())
.implementation(scheme.type())))
.GET(listHandler.pathPattern(), listHandler,
builder -> builder.operationId("List" + gvk)
.description("List " + gvk)
.tag(tagName)
.parameter(parameterBuilder().in(ParameterIn.QUERY)
.name("page")
.description("Page index")
.implementation(Integer.class))
.parameter(parameterBuilder().in(ParameterIn.QUERY)
.name("size")
.description("Size of one page")
.implementation(Integer.class))
.parameter(parameterBuilder().in(ParameterIn.QUERY)
.name("sort")
.description("Sort by some fields. Like metadata.name,desc"))
.response(responseBuilder().responseCode("200")
.description("Response " + scheme.plural())
.implementation(generateListResultClass())))
.POST(createHandler.pathPattern(), createHandler,
builder -> builder.operationId("Create" + gvk)
.description("Create " + gvk)
.tag(tagName)
.requestBody(requestBodyBuilder()
.description("Fresh " + scheme.singular())
.implementation(scheme.type()))
.response(responseBuilder().responseCode("200")
.description("Response " + scheme.plural() + " created just now")
.implementation(scheme.type())))
.PUT(updateHandler.pathPattern(), updateHandler,
builder -> builder.operationId("Update" + gvk)
.description("Update " + gvk)
.tag(tagName)
.parameter(parameterBuilder().in(ParameterIn.PATH)
.name("name")
.description("Name of " + scheme.singular()))
.requestBody(requestBodyBuilder()
.description("Updated " + scheme.singular())
.implementation(scheme.type()))
.response(responseBuilder().responseCode("200")
.description("Response " + scheme.plural() + " updated just now")
.implementation(scheme.type())))
.DELETE(deleteHandler.pathPattern(), deleteHandler,
builder -> builder.operationId("Delete" + gvk)
.description("Delete " + gvk)
.tag(tagName)
.parameter(parameterBuilder().in(ParameterIn.PATH)
.name("name")
.description("Name of " + scheme.singular()))
.response(responseBuilder().responseCode("200")
.description("Response " + scheme.singular() + " deleted just now")))
.build();
}
interface PathPatternGenerator {
String pathPattern();
static String buildExtensionPathPattern(Scheme scheme) {
var gvk = scheme.groupVersionKind();
StringBuilder pattern = new StringBuilder();
if (gvk.hasGroup()) {
pattern.append("/apis/").append(gvk.group());
} else {
pattern.append("/api");
}
return pattern.append('/').append(gvk.version()).append('/').append(scheme.plural())
.toString();
}
}
interface GetHandler extends HandlerFunction<ServerResponse>, PathPatternGenerator {
}
interface ListHandler extends HandlerFunction<ServerResponse>, PathPatternGenerator {
}
interface CreateHandler extends HandlerFunction<ServerResponse>, PathPatternGenerator {
}
interface UpdateHandler extends HandlerFunction<ServerResponse>, PathPatternGenerator {
}
interface DeleteHandler extends HandlerFunction<ServerResponse>, PathPatternGenerator {
}
static class ExtensionCreateHandler implements CreateHandler {
private final Scheme scheme;
private final ExtensionClient client;
public ExtensionCreateHandler(Scheme scheme, ExtensionClient client) {
this.scheme = scheme;
this.client = client;
}
@Override
@NonNull
public Mono<ServerResponse> handle(@NonNull ServerRequest request) {
return request.bodyToMono(Unstructured.class)
.switchIfEmpty(Mono.error(() -> new ExtensionConvertException(
"Cannot read body to " + scheme.groupVersionKind())))
.flatMap(extToCreate -> Mono.fromCallable(() -> {
var name = extToCreate.getMetadata().getName();
client.create(extToCreate);
return client.fetch(scheme.type(), name)
.orElseThrow(() -> new ExtensionNotFoundException(
"Extension with name " + name + " was not found"));
}))
.flatMap(createdExt -> ServerResponse
.created(URI.create(pathPattern() + "/" + createdExt.getMetadata().getName()))
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(createdExt))
.cast(ServerResponse.class);
}
@Override
public String pathPattern() {
return PathPatternGenerator.buildExtensionPathPattern(scheme);
}
}
static class ExtensionListHandler implements ListHandler {
private final Scheme scheme;
private final ExtensionClient client;
public ExtensionListHandler(Scheme scheme, ExtensionClient client) {
this.scheme = scheme;
this.client = client;
}
@Override
@NonNull
public Mono<ServerResponse> handle(@NonNull ServerRequest request) {
int page = request.queryParam("page")
.filter(StringUtils::hasLength)
.map(Integer::parseUnsignedInt).orElse(0);
int size = request.queryParam("size")
.filter(StringUtils::hasLength)
.map(Integer::parseUnsignedInt).orElse(0);
// TODO Resolve predicate and comparator from request
var listResult = client.list(scheme.type(), null, null, page, size);
return ServerResponse
.ok()
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(listResult);
}
@Override
public String pathPattern() {
return PathPatternGenerator.buildExtensionPathPattern(scheme);
}
}
static class ExtensionGetHandler implements GetHandler {
private final Scheme scheme;
private final ExtensionClient client;
public ExtensionGetHandler(Scheme scheme, ExtensionClient client) {
this.scheme = scheme;
this.client = client;
}
@Override
public String pathPattern() {
return PathPatternGenerator.buildExtensionPathPattern(scheme) + "/{name}";
}
@Override
@NonNull
public Mono<ServerResponse> handle(@NonNull ServerRequest request) {
var extensionName = request.pathVariable("name");
var extension = client.fetch(scheme.type(), extensionName)
.orElseThrow(() -> new ExtensionNotFoundException(
scheme.groupVersionKind() + " was not found"));
return ServerResponse
.ok()
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(extension);
}
}
static class ExtensionUpdateHandler implements UpdateHandler {
private final Scheme scheme;
private final ExtensionClient client;
ExtensionUpdateHandler(Scheme scheme, ExtensionClient client) {
this.scheme = scheme;
this.client = client;
}
@Override
public Mono<ServerResponse> handle(ServerRequest request) {
String name = request.pathVariable("name");
return request.bodyToMono(Unstructured.class)
.filter(unstructured -> unstructured.getMetadata() != null
&& StringUtils.hasText(unstructured.getMetadata().getName())
&& Objects.equals(unstructured.getMetadata().getName(), name))
.switchIfEmpty(Mono.error(() -> new ExtensionConvertException(
"Cannot read body to " + scheme.groupVersionKind())))
.flatMap(extToUpdate -> Mono.fromCallable(() -> {
client.update(extToUpdate);
return client.fetch(scheme.type(), name)
.orElseThrow(() -> new ExtensionNotFoundException(
"Extension with name " + name + " was not found"));
}))
.flatMap(updated -> ServerResponse
.ok()
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(updated));
}
@Override
public String pathPattern() {
return PathPatternGenerator.buildExtensionPathPattern(scheme) + "/{name}";
}
}
static class ExtensionDeleteHandler implements DeleteHandler {
private final Scheme scheme;
private final ExtensionClient client;
ExtensionDeleteHandler(Scheme scheme, ExtensionClient client) {
this.scheme = scheme;
this.client = client;
}
@Override
public Mono<ServerResponse> handle(ServerRequest request) {
String name = request.pathVariable("name");
return getExtension(name)
.flatMap(extension ->
Mono.fromRunnable(() -> client.delete(extension)).thenReturn(extension))
.flatMap(extension -> this.getExtension(name))
.flatMap(extension -> ServerResponse
.ok()
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(extension));
}
private Mono<? extends Extension> getExtension(String name) {
return Mono.justOrEmpty(client.fetch(scheme.type(), name))
.switchIfEmpty(Mono.error(() -> new ExtensionNotFoundException(
"Extension with name " + name + " was not found")));
}
@Override
public String pathPattern() {
return PathPatternGenerator.buildExtensionPathPattern(scheme) + "/{name}";
}
}
private Class<?> generateListResultClass() {
var generic =
TypeDescription.Generic.Builder.parameterizedType(ListResult.class, scheme.type())
.build();
return new ByteBuddy()
.subclass(generic)
.name(scheme.groupVersionKind().kind() + "List")
.make()
.load(this.getClass().getClassLoader())
.getLoaded();
}
}

View File

@ -1,4 +1,4 @@
package run.halo.app.extension;
package run.halo.app.extension.router;
import java.util.Collections;
import java.util.Map;
@ -11,6 +11,9 @@ import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.Scheme;
import run.halo.app.extension.SchemeWatcherManager;
import run.halo.app.extension.SchemeWatcherManager.SchemeWatcher;
public class ExtensionCompositeRouterFunction implements

View File

@ -0,0 +1,51 @@
package run.halo.app.extension.router;
import java.net.URI;
import org.springframework.http.MediaType;
import org.springframework.lang.NonNull;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.Scheme;
import run.halo.app.extension.Unstructured;
import run.halo.app.extension.exception.ExtensionConvertException;
import run.halo.app.extension.exception.ExtensionNotFoundException;
class ExtensionCreateHandler implements ExtensionRouterFunctionFactory.CreateHandler {
private final Scheme scheme;
private final ExtensionClient client;
public ExtensionCreateHandler(Scheme scheme, ExtensionClient client) {
this.scheme = scheme;
this.client = client;
}
@Override
@NonNull
public Mono<ServerResponse> handle(@NonNull ServerRequest request) {
return request.bodyToMono(Unstructured.class)
.switchIfEmpty(Mono.error(() -> new ExtensionConvertException(
"Cannot read body to " + scheme.groupVersionKind())))
.flatMap(extToCreate -> Mono.fromCallable(() -> {
var name = extToCreate.getMetadata().getName();
client.create(extToCreate);
return client.fetch(scheme.type(), name)
.orElseThrow(() -> new ExtensionNotFoundException(
"Extension with name " + name + " was not found"));
}))
.flatMap(createdExt -> ServerResponse
.created(URI.create(pathPattern() + "/" + createdExt.getMetadata().getName()))
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(createdExt))
.cast(ServerResponse.class);
}
@Override
public String pathPattern() {
return ExtensionRouterFunctionFactory.PathPatternGenerator.buildExtensionPathPattern(
scheme);
}
}

View File

@ -0,0 +1,48 @@
package run.halo.app.extension.router;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import run.halo.app.extension.Extension;
import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.Scheme;
import run.halo.app.extension.exception.ExtensionNotFoundException;
class ExtensionDeleteHandler implements ExtensionRouterFunctionFactory.DeleteHandler {
private final Scheme scheme;
private final ExtensionClient client;
ExtensionDeleteHandler(Scheme scheme, ExtensionClient client) {
this.scheme = scheme;
this.client = client;
}
@Override
public Mono<ServerResponse> handle(ServerRequest request) {
String name = request.pathVariable("name");
return getExtension(name)
.flatMap(extension ->
Mono.fromRunnable(() -> client.delete(extension)).thenReturn(extension))
.flatMap(extension -> this.getExtension(name))
.flatMap(extension -> ServerResponse
.ok()
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(extension));
}
private Mono<? extends Extension> getExtension(String name) {
return Mono.justOrEmpty(client.fetch(scheme.type(), name))
.switchIfEmpty(Mono.error(() -> new ExtensionNotFoundException(
"Extension with name " + name + " was not found")));
}
@Override
public String pathPattern() {
return ExtensionRouterFunctionFactory.PathPatternGenerator.buildExtensionPathPattern(scheme)
+ "/{name}";
}
}

View File

@ -0,0 +1,41 @@
package run.halo.app.extension.router;
import org.springframework.http.MediaType;
import org.springframework.lang.NonNull;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.Scheme;
import run.halo.app.extension.exception.ExtensionNotFoundException;
class ExtensionGetHandler implements ExtensionRouterFunctionFactory.GetHandler {
private final Scheme scheme;
private final ExtensionClient client;
public ExtensionGetHandler(Scheme scheme, ExtensionClient client) {
this.scheme = scheme;
this.client = client;
}
@Override
public String pathPattern() {
return ExtensionRouterFunctionFactory.PathPatternGenerator.buildExtensionPathPattern(scheme)
+ "/{name}";
}
@Override
@NonNull
public Mono<ServerResponse> handle(@NonNull ServerRequest request) {
var extensionName = request.pathVariable("name");
var extension = client.fetch(scheme.type(), extensionName)
.orElseThrow(() -> new ExtensionNotFoundException(
scheme.groupVersionKind() + " was not found"));
return ServerResponse
.ok()
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(extension);
}
}

View File

@ -0,0 +1,54 @@
package run.halo.app.extension.router;
import static run.halo.app.extension.router.selector.SelectorUtil.labelAndFieldSelectorToPredicate;
import org.springframework.boot.convert.ApplicationConversionService;
import org.springframework.http.MediaType;
import org.springframework.lang.NonNull;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.Scheme;
class ExtensionListHandler implements ExtensionRouterFunctionFactory.ListHandler {
private final Scheme scheme;
private final ExtensionClient client;
public ExtensionListHandler(Scheme scheme, ExtensionClient client) {
this.scheme = scheme;
this.client = client;
}
@Override
@NonNull
public Mono<ServerResponse> handle(@NonNull ServerRequest request) {
var conversionService = ApplicationConversionService.getSharedInstance();
var page =
request.queryParam("page")
.map(pageString -> conversionService.convert(pageString, Integer.class))
.orElse(0);
var size = request.queryParam("size")
.map(sizeString -> conversionService.convert(sizeString, Integer.class))
.orElse(0);
var labelSelectors = request.queryParams().get("labelSelector");
var fieldSelectors = request.queryParams().get("fieldSelector");
// TODO Resolve comparator from request
var listResult = client.list(scheme.type(),
labelAndFieldSelectorToPredicate(labelSelectors, fieldSelectors), null, page, size);
return ServerResponse
.ok()
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(listResult);
}
@Override
public String pathPattern() {
return ExtensionRouterFunctionFactory.PathPatternGenerator.buildExtensionPathPattern(
scheme);
}
}

View File

@ -0,0 +1,144 @@
package run.halo.app.extension.router;
import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;
import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;
import static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuilder;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.description.type.TypeDescription;
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder;
import org.springframework.lang.NonNull;
import org.springframework.web.reactive.function.server.HandlerFunction;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.ListResult;
import run.halo.app.extension.Scheme;
public class ExtensionRouterFunctionFactory {
private final Scheme scheme;
private final ExtensionClient client;
public ExtensionRouterFunctionFactory(Scheme scheme, ExtensionClient client) {
this.scheme = scheme;
this.client = client;
}
@NonNull
public RouterFunction<ServerResponse> create() {
var getHandler = new ExtensionGetHandler(scheme, client);
var listHandler = new ExtensionListHandler(scheme, client);
var createHandler = new ExtensionCreateHandler(scheme, client);
var updateHandler = new ExtensionUpdateHandler(scheme, client);
var deleteHandler = new ExtensionDeleteHandler(scheme, client);
// TODO More handlers here
var gvk = scheme.groupVersionKind();
var tagName = gvk.toString();
return SpringdocRouteBuilder.route()
.GET(getHandler.pathPattern(), getHandler,
builder -> builder.operationId("Get" + gvk)
.description("Get " + gvk)
.tag(tagName)
.parameter(parameterBuilder().in(ParameterIn.PATH)
.name("name")
.description("Name of " + scheme.singular()))
.response(responseBuilder().responseCode("200")
.description("Response single " + scheme.singular())
.implementation(scheme.type())))
.GET(listHandler.pathPattern(), listHandler,
builder -> {
builder.operationId("List" + gvk)
.description("List " + gvk)
.tag(tagName)
.response(responseBuilder().responseCode("200")
.description("Response " + scheme.plural())
.implementation(generateListResultClass()));
QueryParamBuildUtil.buildParametersFromType(builder, ListRequest.class);
})
.POST(createHandler.pathPattern(), createHandler,
builder -> builder.operationId("Create" + gvk)
.description("Create " + gvk)
.tag(tagName)
.requestBody(requestBodyBuilder()
.description("Fresh " + scheme.singular())
.implementation(scheme.type()))
.response(responseBuilder().responseCode("200")
.description("Response " + scheme.plural() + " created just now")
.implementation(scheme.type())))
.PUT(updateHandler.pathPattern(), updateHandler,
builder -> builder.operationId("Update" + gvk)
.description("Update " + gvk)
.tag(tagName)
.parameter(parameterBuilder().in(ParameterIn.PATH)
.name("name")
.description("Name of " + scheme.singular()))
.requestBody(requestBodyBuilder()
.description("Updated " + scheme.singular())
.implementation(scheme.type()))
.response(responseBuilder().responseCode("200")
.description("Response " + scheme.plural() + " updated just now")
.implementation(scheme.type())))
.DELETE(deleteHandler.pathPattern(), deleteHandler,
builder -> builder.operationId("Delete" + gvk)
.description("Delete " + gvk)
.tag(tagName)
.parameter(parameterBuilder().in(ParameterIn.PATH)
.name("name")
.description("Name of " + scheme.singular()))
.response(responseBuilder().responseCode("200")
.description("Response " + scheme.singular() + " deleted just now")))
.build();
}
interface PathPatternGenerator {
String pathPattern();
static String buildExtensionPathPattern(Scheme scheme) {
var gvk = scheme.groupVersionKind();
StringBuilder pattern = new StringBuilder();
if (gvk.hasGroup()) {
pattern.append("/apis/").append(gvk.group());
} else {
pattern.append("/api");
}
return pattern.append('/').append(gvk.version()).append('/').append(scheme.plural())
.toString();
}
}
interface GetHandler extends HandlerFunction<ServerResponse>, PathPatternGenerator {
}
interface ListHandler extends HandlerFunction<ServerResponse>, PathPatternGenerator {
}
interface CreateHandler extends HandlerFunction<ServerResponse>, PathPatternGenerator {
}
interface UpdateHandler extends HandlerFunction<ServerResponse>, PathPatternGenerator {
}
interface DeleteHandler extends HandlerFunction<ServerResponse>, PathPatternGenerator {
}
private Class<?> generateListResultClass() {
var generic =
TypeDescription.Generic.Builder.parameterizedType(ListResult.class, scheme.type())
.build();
return new ByteBuddy()
.subclass(generic)
.name(scheme.groupVersionKind().kind() + "List")
.make()
.load(this.getClass().getClassLoader())
.getLoaded();
}
}

View File

@ -0,0 +1,52 @@
package run.halo.app.extension.router;
import java.util.Objects;
import org.springframework.http.MediaType;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.Scheme;
import run.halo.app.extension.Unstructured;
import run.halo.app.extension.exception.ExtensionConvertException;
import run.halo.app.extension.exception.ExtensionNotFoundException;
class ExtensionUpdateHandler implements ExtensionRouterFunctionFactory.UpdateHandler {
private final Scheme scheme;
private final ExtensionClient client;
ExtensionUpdateHandler(Scheme scheme, ExtensionClient client) {
this.scheme = scheme;
this.client = client;
}
@Override
public Mono<ServerResponse> handle(ServerRequest request) {
String name = request.pathVariable("name");
return request.bodyToMono(Unstructured.class)
.filter(unstructured -> unstructured.getMetadata() != null
&& StringUtils.hasText(unstructured.getMetadata().getName())
&& Objects.equals(unstructured.getMetadata().getName(), name))
.switchIfEmpty(Mono.error(() -> new ExtensionConvertException(
"Cannot read body to " + scheme.groupVersionKind())))
.flatMap(extToUpdate -> Mono.fromCallable(() -> {
client.update(extToUpdate);
return client.fetch(scheme.type(), name)
.orElseThrow(() -> new ExtensionNotFoundException(
"Extension with name " + name + " was not found"));
}))
.flatMap(updated -> ServerResponse
.ok()
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(updated));
}
@Override
public String pathPattern() {
return ExtensionRouterFunctionFactory.PathPatternGenerator.buildExtensionPathPattern(scheme)
+ "/{name}";
}
}

View File

@ -0,0 +1,22 @@
package run.halo.app.extension.router;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List;
import lombok.Data;
@Data
public class ListRequest {
@Schema(description = "The page number. Zero indicates no page.")
private Integer page;
@Schema(description = "Size of one page. Zero indicates no limit.")
private Integer size;
@Schema(description = "Label selector for filtering.")
private List<String> labelSelector;
@Schema(description = "Field selector for filtering.")
private List<String> fieldSelector;
}

View File

@ -0,0 +1,87 @@
package run.halo.app.extension.router;
import static org.springdoc.core.fn.builders.arrayschema.Builder.arraySchemaBuilder;
import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;
import static org.springdoc.core.fn.builders.schema.Builder.schemaBuilder;
import io.swagger.v3.core.converter.ModelConverters;
import io.swagger.v3.oas.annotations.enums.Explode;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.enums.ParameterStyle;
import io.swagger.v3.oas.models.media.ArraySchema;
import io.swagger.v3.oas.models.media.Schema;
import java.lang.reflect.Type;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.springdoc.core.fn.builders.operation.Builder;
public final class QueryParamBuildUtil {
private QueryParamBuildUtil() {
}
private static <T> T defaultIfNull(T t, T defaultValue) {
return t == null ? defaultValue : t;
}
private static <T> List<T> defaultIfNull(List<T> list, List<T> defaultValue) {
return list == null ? defaultValue : list;
}
private static String toStringOrNull(Object obj) {
return obj == null ? null : obj.toString();
}
@SuppressWarnings({"rawtypes", "unchecked"})
public static void buildParametersFromType(Builder operationBuilder, Type queryParamType) {
var resolvedSchema =
ModelConverters.getInstance().readAllAsResolvedSchema(queryParamType);
var properties = (Map<String, Schema>) resolvedSchema.schema.getProperties();
var requiredNames = defaultIfNull(resolvedSchema.schema.getRequired(),
Collections.emptyList());
properties.forEach((propName, propSchema) -> {
final var paramBuilder = parameterBuilder().in(ParameterIn.QUERY);
paramBuilder.name(propSchema.getName())
.description(propSchema.getDescription())
.style(ParameterStyle.FORM)
.explode(Explode.TRUE);
if (requiredNames.contains(propSchema.getName())) {
paramBuilder.required(true);
}
if (propSchema instanceof ArraySchema arraySchema) {
paramBuilder.array(arraySchemaBuilder()
.uniqueItems(defaultIfNull(arraySchema.getUniqueItems(), false))
.minItems(defaultIfNull(arraySchema.getMinItems(), 0))
.maxItems(defaultIfNull(arraySchema.getMaxItems(), Integer.MAX_VALUE))
.arraySchema(convertSchemaBuilder(arraySchema))
.schema(convertSchemaBuilder(arraySchema.getItems()))
);
} else {
paramBuilder.schema(convertSchemaBuilder(propSchema));
}
operationBuilder.parameter(paramBuilder);
});
}
private static org.springdoc.core.fn.builders.schema.Builder convertSchemaBuilder(
Schema<?> schema) {
var allowableValues = new String[0];
if (schema.getEnum() != null) {
allowableValues = schema.getEnum().stream()
.map(Object::toString)
.toArray(String[]::new);
}
return schemaBuilder()
.name(schema.getName())
.type(schema.getType())
.description(schema.getDescription())
.format(schema.getFormat())
.deprecated(defaultIfNull(schema.getDeprecated(), false))
.nullable(defaultIfNull(schema.getNullable(), false))
.allowableValues(allowableValues)
.defaultValue(toStringOrNull(schema.getDefault()))
.example(toStringOrNull(schema.getExample()));
}
}

View File

@ -0,0 +1,36 @@
package run.halo.app.extension.router.selector;
import java.util.function.Predicate;
import org.springframework.core.convert.converter.Converter;
import org.springframework.lang.NonNull;
import run.halo.app.extension.Extension;
public class FieldCriteriaPredicateConverter<E extends Extension>
implements Converter<SelectorCriteria, Predicate<E>> {
@Override
@NonNull
public Predicate<E> convert(SelectorCriteria criteria) {
// current we only support name field.
return ext -> {
if ("name".equals(criteria.key())) {
var name = ext.getMetadata().getName();
if (name == null) {
return false;
}
switch (criteria.operator()) {
case Equals -> {
return criteria.values().contains(name);
}
case NotEquals -> {
return !criteria.values().contains(name);
}
default -> {
return false;
}
}
}
return false;
};
}
}

View File

@ -0,0 +1,41 @@
package run.halo.app.extension.router.selector;
import java.util.function.Predicate;
import org.springframework.core.convert.converter.Converter;
import org.springframework.lang.NonNull;
import run.halo.app.extension.Extension;
public class LabelCriteriaPredicateConverter<E extends Extension>
implements Converter<SelectorCriteria, Predicate<E>> {
@Override
@NonNull
public Predicate<E> convert(SelectorCriteria criteria) {
return ext -> {
var labels = ext.getMetadata().getLabels();
switch (criteria.operator()) {
case Equals -> {
if (labels == null || !labels.containsKey(criteria.key())) {
return false;
}
return criteria.values().contains(labels.get(criteria.key()));
}
case NotEquals -> {
if (labels == null || !labels.containsKey(criteria.key())) {
return false;
}
return !criteria.values().contains(labels.get(criteria.key()));
}
case NotExist -> {
return labels == null || !labels.containsKey(criteria.key());
}
case Exist -> {
return labels != null && labels.containsKey(criteria.key());
}
default -> {
return false;
}
}
};
}
}

View File

@ -0,0 +1,85 @@
package run.halo.app.extension.router.selector;
import java.util.Set;
import org.springframework.core.convert.converter.Converter;
import org.springframework.lang.Nullable;
public enum Operator implements Converter<String, SelectorCriteria> {
Equals("=", 2) {
@Override
@Nullable
public SelectorCriteria convert(@Nullable String selector) {
if (preFlightCheck(selector, 3)) {
var i = selector.indexOf(getOperator());
if (i > 0 && (i + getOperator().length()) < selector.length() - 1) {
String key = selector.substring(0, i);
String value = selector.substring(i + getOperator().length());
return new SelectorCriteria(key, this, Set.of(value));
}
}
return null;
}
},
NotEquals("!=", 1) {
@Override
@Nullable
public SelectorCriteria convert(@Nullable String selector) {
if (preFlightCheck(selector, 4)) {
var i = selector.indexOf(getOperator());
if (i > 0 && (i + getOperator().length()) < selector.length() - 1) {
String key = selector.substring(0, i);
String value = selector.substring(i + getOperator().length());
return new SelectorCriteria(key, this, Set.of(value));
}
}
return null;
}
},
NotExist("!", 0) {
@Override
@Nullable
public SelectorCriteria convert(@Nullable String selector) {
if (preFlightCheck(selector, 2)) {
if (selector.startsWith(getOperator())) {
return new SelectorCriteria(selector.substring(1), this, Set.of());
}
}
return null;
}
},
Exist("", Integer.MAX_VALUE) {
@Override
public SelectorCriteria convert(String selector) {
if (preFlightCheck(selector, 1)) {
// TODO validate the source with regex in the future
return new SelectorCriteria(selector, this, Set.of());
}
return null;
}
};
private final String operator;
/**
* Parse order.
*/
private final int order;
Operator(String operator, int order) {
this.operator = operator;
this.order = order;
}
public String getOperator() {
return operator;
}
public int getOrder() {
return order;
}
protected boolean preFlightCheck(String selector, int minLength) {
return selector != null && selector.length() >= minLength;
}
}

View File

@ -0,0 +1,27 @@
package run.halo.app.extension.router.selector;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Objects;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.convert.converter.Converter;
import org.springframework.lang.Nullable;
@Slf4j
public class SelectorConverter implements Converter<String, SelectorCriteria> {
@Override
@Nullable
public SelectorCriteria convert(@Nullable String selector) {
return Arrays.stream(Operator.values())
.sorted(Comparator.comparing(Operator::getOrder))
.map(operator -> {
log.debug("Resolving selector: {} with operator: {}", selector, operator);
return operator.convert(selector);
})
.filter(Objects::nonNull)
.findFirst()
.orElse(null);
}
}

View File

@ -0,0 +1,7 @@
package run.halo.app.extension.router.selector;
import java.util.Set;
public record SelectorCriteria(String key, Operator operator, Set<String> values) {
}

View File

@ -0,0 +1,61 @@
package run.halo.app.extension.router.selector;
import java.util.List;
import java.util.function.Predicate;
import org.springframework.data.util.Predicates;
import org.springframework.web.server.ServerWebInputException;
import run.halo.app.extension.Extension;
public final class SelectorUtil {
private SelectorUtil() {
}
public static <E extends Extension> Predicate<E> labelSelectorsToPredicate(
List<String> labelSelectors) {
if (labelSelectors == null) {
labelSelectors = List.of();
}
final var labelPredicateConverter =
new SelectorConverter().andThen(new LabelCriteriaPredicateConverter<E>());
return labelSelectors.stream()
.map(selector -> {
var predicate = labelPredicateConverter.convert(selector);
if (predicate == null) {
throw new ServerWebInputException("Invalid label selector: " + selector);
}
return predicate;
})
.reduce(Predicate::and)
.orElse(Predicates.isTrue());
}
public static <E extends Extension> Predicate<E> fieldSelectorToPredicate(
List<String> fieldSelectors) {
if (fieldSelectors == null) {
fieldSelectors = List.of();
}
final var fieldPredicateConverter =
new SelectorConverter().andThen(new FieldCriteriaPredicateConverter<E>());
return fieldSelectors.stream()
.map(selector -> {
var predicate = fieldPredicateConverter.convert(selector);
if (predicate == null) {
throw new ServerWebInputException("Invalid field selector: " + selector);
}
return predicate;
})
.reduce(Predicate::and)
.orElse(Predicates.isTrue());
}
public static <E extends Extension> Predicate<E> labelAndFieldSelectorToPredicate(
List<String> labelSelectors, List<String> fieldSelectors) {
return SelectorUtil.<E>labelSelectorsToPredicate(labelSelectors)
.and(fieldSelectorToPredicate(fieldSelectors));
}
}

View File

@ -114,6 +114,7 @@ class ExtensionConfigurationTest {
var metadata = new Metadata();
metadata.setName("my-fake");
metadata.setLabels(Map.of("label-key", "label-value"));
var fake = new FakeExtension();
fake.setMetadata(metadata);
@ -158,14 +159,37 @@ class ExtensionConfigurationTest {
@Test
@WithMockUser
void shouldListExtensionsWhenSchemeRegistered() {
webClient.get()
.uri("/apis/fake.halo.run/v1alpha1/fakes")
webClient.get().uri("/apis/fake.halo.run/v1alpha1/fakes")
.exchange()
.expectStatus().isOk()
.expectBodyList(FakeExtension.class)
.hasSize(1);
.expectBody().jsonPath("$.items.length()").isEqualTo(1);
}
@Test
@WithMockUser
void shouldListExtensionsWithMatchedSelectors() {
webClient.get().uri(uriBuilder -> uriBuilder
.path("/apis/fake.halo.run/v1alpha1/fakes")
.queryParam("labelSelector", "label-key=label-value")
.queryParam("fieldSelector", "name=my-fake")
.build())
.exchange()
.expectStatus().isOk()
.expectBody().jsonPath("$.items.length()").isEqualTo(1);
}
@Test
@WithMockUser
void shouldListExtensionsWithMismatchedSelectors() {
webClient.get().uri(uriBuilder -> uriBuilder
.path("/apis/fake.halo.run/v1alpha1/fakes")
.queryParam("labelSelector", "label-key=invalid-label-value")
.queryParam("fieldSelector", "name=invalid-name")
.build())
.exchange()
.expectStatus().isOk()
.expectBody().jsonPath("$.items.length()").isEqualTo(0);
}
@Test
@WithMockUser
@ -219,20 +243,6 @@ class ExtensionConfigurationTest {
.getResponseBody();
}
WebTestClient.ResponseSpec getCreateExtensionResponse() {
var metadata = new Metadata();
metadata.setName("my-fake");
var fake = new FakeExtension();
fake.setMetadata(metadata);
return webClient.post()
.uri("/apis/fake.halo.run/v1alpha1/fakes")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(fake)
.exchange();
}
}
}

View File

@ -1,4 +1,4 @@
package run.halo.app.extension;
package run.halo.app.extension.router;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
@ -15,6 +15,10 @@ import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.web.reactive.function.server.HandlerStrategies;
import org.springframework.web.reactive.function.server.ServerRequest;
import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.FakeExtension;
import run.halo.app.extension.Scheme;
import run.halo.app.extension.SchemeWatcherManager;
import run.halo.app.extension.SchemeWatcherManager.SchemeRegistered;
import run.halo.app.extension.SchemeWatcherManager.SchemeUnregistered;

View File

@ -1,4 +1,4 @@
package run.halo.app.extension;
package run.halo.app.extension.router;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ -24,7 +24,11 @@ import org.springframework.mock.web.reactive.function.server.MockServerRequest;
import org.springframework.web.reactive.function.server.EntityResponse;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import run.halo.app.extension.ExtensionRouterFunctionFactory.ExtensionCreateHandler;
import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.FakeExtension;
import run.halo.app.extension.Metadata;
import run.halo.app.extension.Scheme;
import run.halo.app.extension.Unstructured;
import run.halo.app.extension.exception.ExtensionConvertException;
import run.halo.app.extension.exception.ExtensionNotFoundException;

View File

@ -1,4 +1,4 @@
package run.halo.app.extension;
package run.halo.app.extension.router;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
@ -23,7 +23,11 @@ import org.springframework.mock.web.reactive.function.server.MockServerRequest;
import org.springframework.web.reactive.function.server.EntityResponse;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import run.halo.app.extension.ExtensionRouterFunctionFactory.ExtensionDeleteHandler;
import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.FakeExtension;
import run.halo.app.extension.Metadata;
import run.halo.app.extension.Scheme;
import run.halo.app.extension.Unstructured;
import run.halo.app.extension.exception.ExtensionNotFoundException;
@ExtendWith(MockitoExtension.class)

View File

@ -1,4 +1,4 @@
package run.halo.app.extension;
package run.halo.app.extension.router;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
@ -16,7 +16,9 @@ import org.springframework.http.MediaType;
import org.springframework.mock.web.reactive.function.server.MockServerRequest;
import org.springframework.web.reactive.function.server.EntityResponse;
import reactor.test.StepVerifier;
import run.halo.app.extension.ExtensionRouterFunctionFactory.ExtensionGetHandler;
import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.FakeExtension;
import run.halo.app.extension.Scheme;
import run.halo.app.extension.exception.ExtensionNotFoundException;
@ExtendWith(MockitoExtension.class)

View File

@ -1,4 +1,4 @@
package run.halo.app.extension;
package run.halo.app.extension.router;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ -17,7 +17,10 @@ import org.springframework.http.MediaType;
import org.springframework.mock.web.reactive.function.server.MockServerRequest;
import org.springframework.web.reactive.function.server.EntityResponse;
import reactor.test.StepVerifier;
import run.halo.app.extension.ExtensionRouterFunctionFactory.ExtensionListHandler;
import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.FakeExtension;
import run.halo.app.extension.ListResult;
import run.halo.app.extension.Scheme;
@ExtendWith(MockitoExtension.class)
class ExtensionListHandlerTest {
@ -39,7 +42,6 @@ class ExtensionListHandlerTest {
var listHandler = new ExtensionListHandler(scheme, client);
var serverRequest = MockServerRequest.builder().build();
final var fake = new FakeExtension();
// when(client.list(same(FakeExtension.class), any(), any())).thenReturn(List.of(fake));
var fakeListResult = new ListResult<>(0, 0, 1, List.of(fake));
when(client.list(same(FakeExtension.class), any(), any(), anyInt(), anyInt()))
.thenReturn(fakeListResult);

View File

@ -1,4 +1,4 @@
package run.halo.app.extension;
package run.halo.app.extension.router;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
@ -15,10 +15,14 @@ import org.springframework.web.reactive.function.server.HandlerStrategies;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ServerWebExchange;
import run.halo.app.extension.ExtensionRouterFunctionFactory.CreateHandler;
import run.halo.app.extension.ExtensionRouterFunctionFactory.GetHandler;
import run.halo.app.extension.ExtensionRouterFunctionFactory.ListHandler;
import run.halo.app.extension.ExtensionRouterFunctionFactory.UpdateHandler;
import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.FakeExtension;
import run.halo.app.extension.Scheme;
import run.halo.app.extension.router.ExtensionRouterFunctionFactory;
import run.halo.app.extension.router.ExtensionRouterFunctionFactory.CreateHandler;
import run.halo.app.extension.router.ExtensionRouterFunctionFactory.GetHandler;
import run.halo.app.extension.router.ExtensionRouterFunctionFactory.ListHandler;
import run.halo.app.extension.router.ExtensionRouterFunctionFactory.UpdateHandler;
@ExtendWith(MockitoExtension.class)
class ExtensionRouterFunctionFactoryTest {

View File

@ -1,4 +1,4 @@
package run.halo.app.extension;
package run.halo.app.extension.router;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
@ -25,7 +25,11 @@ import org.springframework.mock.web.reactive.function.server.MockServerRequest;
import org.springframework.web.reactive.function.server.EntityResponse;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import run.halo.app.extension.ExtensionRouterFunctionFactory.ExtensionUpdateHandler;
import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.FakeExtension;
import run.halo.app.extension.Metadata;
import run.halo.app.extension.Scheme;
import run.halo.app.extension.Unstructured;
import run.halo.app.extension.exception.ExtensionConvertException;
import run.halo.app.extension.exception.ExtensionNotFoundException;

View File

@ -1,9 +1,12 @@
package run.halo.app.extension;
package run.halo.app.extension.router;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
import run.halo.app.extension.ExtensionRouterFunctionFactory.PathPatternGenerator;
import run.halo.app.extension.AbstractExtension;
import run.halo.app.extension.GVK;
import run.halo.app.extension.Scheme;
import run.halo.app.extension.router.ExtensionRouterFunctionFactory.PathPatternGenerator;
class PathPatternGeneratorTest {

View File

@ -0,0 +1,75 @@
package run.halo.app.extension.router.selector;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import java.util.Set;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import run.halo.app.extension.Extension;
import run.halo.app.extension.FakeExtension;
import run.halo.app.extension.Metadata;
class FieldCriteriaPredicateConverterTest {
FieldCriteriaPredicateConverter<Extension> converter;
@BeforeEach
void setUp() {
converter = new FieldCriteriaPredicateConverter<>();
}
@Test
void shouldConvertNameEqualsCorrectly() {
var criteria = new SelectorCriteria("name", Operator.Equals, Set.of("value1", "value2"));
var predicate = converter.convert(criteria);
assertNotNull(predicate);
var fakeExt = new FakeExtension();
var metadata = new Metadata();
fakeExt.setMetadata(metadata);
assertFalse(predicate.test(fakeExt));
metadata.setName("value1");
assertTrue(predicate.test(fakeExt));
metadata.setName("value2");
assertTrue(predicate.test(fakeExt));
metadata.setName("invalid-value");
assertFalse(predicate.test(fakeExt));
}
@Test
void shouldConvertNameNotEqualsCorrectly() {
var criteria = new SelectorCriteria("name", Operator.NotEquals, Set.of("value1", "value2"));
var predicate = converter.convert(criteria);
assertNotNull(predicate);
var fake = new FakeExtension();
var metadata = new Metadata();
fake.setMetadata(metadata);
assertFalse(predicate.test(fake));
metadata.setName("not-contain-value");
assertTrue(predicate.test(fake));
metadata.setName("value1");
assertFalse(predicate.test(fake));
metadata.setName("value2");
assertFalse(predicate.test(fake));
}
@Test
void shouldReturnAlwaysFalseIfCriteriaKeyNotSupported() {
var criteria =
new SelectorCriteria("unsupported-field", Operator.Equals, Set.of("value1", "value2"));
var predicate = converter.convert(criteria);
assertNotNull(predicate);
assertFalse(predicate.test(mock(Extension.class)));
}
}

View File

@ -0,0 +1,101 @@
package run.halo.app.extension.router.selector;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.Map;
import java.util.Set;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import run.halo.app.extension.Extension;
import run.halo.app.extension.FakeExtension;
import run.halo.app.extension.Metadata;
class LabelCriteriaPredicateConverterTest {
LabelCriteriaPredicateConverter<Extension> convert;
@BeforeEach
void setUp() {
convert = new LabelCriteriaPredicateConverter<>();
}
@Test
void shouldConvertEqualsCorrectly() {
var criteria = new SelectorCriteria("name", Operator.Equals, Set.of("value1", "value2"));
var predicate = convert.convert(criteria);
assertNotNull(predicate);
var fakeExt = new FakeExtension();
var metadata = new Metadata();
fakeExt.setMetadata(metadata);
assertFalse(predicate.test(fakeExt));
metadata.setLabels(Map.of("name", "value"));
assertFalse(predicate.test(fakeExt));
metadata.setLabels(Map.of("name", "value1"));
assertTrue(predicate.test(fakeExt));
metadata.setLabels(Map.of("name", "value2"));
assertTrue(predicate.test(fakeExt));
}
@Test
void shouldConvertNotEqualsCorrectly() {
var criteria = new SelectorCriteria("name", Operator.NotEquals, Set.of("value1", "value2"));
var predicate = convert.convert(criteria);
assertNotNull(predicate);
var fakeExt = new FakeExtension();
var metadata = new Metadata();
fakeExt.setMetadata(metadata);
assertFalse(predicate.test(fakeExt));
metadata.setLabels(Map.of("name", "value"));
assertTrue(predicate.test(fakeExt));
metadata.setLabels(Map.of("name", "value1"));
assertFalse(predicate.test(fakeExt));
metadata.setLabels(Map.of("name", "value2"));
assertFalse(predicate.test(fakeExt));
}
@Test
void shouldConvertNotExistCorrectly() {
var criteria = new SelectorCriteria("name", Operator.NotExist, Set.of());
var predicate = convert.convert(criteria);
assertNotNull(predicate);
var fake = new FakeExtension();
var metadata = new Metadata();
fake.setMetadata(metadata);
assertTrue(predicate.test(fake));
metadata.setLabels(Map.of("not-a-name", ""));
assertTrue(predicate.test(fake));
metadata.setLabels(Map.of("name", ""));
assertFalse(predicate.test(fake));
}
@Test
void shouldConvertExistCorrectly() {
var criteria = new SelectorCriteria("name", Operator.Exist, Set.of());
var predicate = convert.convert(criteria);
assertNotNull(predicate);
var fake = new FakeExtension();
var metadata = new Metadata();
fake.setMetadata(metadata);
assertFalse(predicate.test(fake));
metadata.setLabels(Map.of("not-a-name", ""));
assertFalse(predicate.test(fake));
metadata.setLabels(Map.of("name", ""));
assertTrue(predicate.test(fake));
}
}

View File

@ -0,0 +1,55 @@
package run.halo.app.extension.router.selector;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static run.halo.app.extension.router.selector.Operator.Equals;
import static run.halo.app.extension.router.selector.Operator.Exist;
import static run.halo.app.extension.router.selector.Operator.NotEquals;
import static run.halo.app.extension.router.selector.Operator.NotExist;
import java.util.List;
import java.util.Set;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
@Slf4j
class OperatorTest {
@Test
void shouldConvertCorrectly() {
record TestCase(String source, Operator converter, SelectorCriteria expected) {
}
List.of(
new TestCase("", Equals, null),
new TestCase("=", Equals, null),
new TestCase("=value", Equals, null),
new TestCase("name=", Equals, null),
new TestCase("name=value", Equals,
new SelectorCriteria("name", Equals, Set.of("value"))),
new TestCase("", NotEquals, null),
new TestCase("=", NotEquals, null),
new TestCase("!", NotEquals, null),
new TestCase("!=", NotEquals, null),
new TestCase("!=value", NotEquals, null),
new TestCase("name!=", NotEquals, null),
new TestCase("name!=value", NotEquals,
new SelectorCriteria("name", NotEquals, Set.of("value"))),
new TestCase("", NotExist, null),
new TestCase("!", NotExist, null),
new TestCase("!name", NotExist, new SelectorCriteria("name", NotExist, Set.of())),
new TestCase("name", NotExist, null),
new TestCase("na!me", NotExist, null),
new TestCase("name!", NotExist, null),
new TestCase("name", Exist, new SelectorCriteria("name", Exist, Set.of())),
new TestCase("", Exist, null),
new TestCase("!", Exist, new SelectorCriteria("!", Exist, Set.of())),
new TestCase("a", Exist, new SelectorCriteria("a", Exist, Set.of()))
).forEach(testCase -> {
log.debug("Testing: {}", testCase);
assertEquals(testCase.expected(), testCase.converter().convert(testCase.source()));
});
}
}

View File

@ -0,0 +1,42 @@
package run.halo.app.extension.router.selector;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static run.halo.app.extension.router.selector.Operator.Equals;
import java.util.List;
import java.util.Set;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
@Slf4j
class SelectorConverterTest {
SelectorConverter converter = new SelectorConverter();
@Test
void shouldConvertCorrectly() {
record TestCase(String selector, SelectorCriteria expected) {
}
List.of(
new TestCase("", null),
new TestCase("name=value",
new SelectorCriteria("name", Equals, Set.of("value"))),
new TestCase("name!=value",
new SelectorCriteria("name", Operator.NotEquals, Set.of("value"))),
new TestCase("name",
new SelectorCriteria("name", Operator.Exist, Set.of())),
new TestCase("!name",
new SelectorCriteria("name", Operator.NotExist, Set.of())),
new TestCase("name",
new SelectorCriteria("name", Operator.Exist, Set.of())),
new TestCase("name!=",
new SelectorCriteria("name!=", Operator.Exist, Set.of())),
new TestCase("==",
new SelectorCriteria("==", Operator.Exist, Set.of()))
).forEach(testCase -> {
log.debug("Testing: {}", testCase);
assertEquals(testCase.expected, converter.convert(testCase.selector));
});
}
}

View File

@ -0,0 +1,44 @@
package run.halo.app.extension.router.selector;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static run.halo.app.extension.router.selector.SelectorUtil.labelAndFieldSelectorToPredicate;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
import run.halo.app.extension.Extension;
import run.halo.app.extension.FakeExtension;
import run.halo.app.extension.Metadata;
class SelectorUtilTest {
@Test
void shouldConvertCorrectlyIfSelectorsAreNull() {
var predicate = labelAndFieldSelectorToPredicate(null, null);
assertTrue(predicate.test(mock(Extension.class)));
}
@Test
void shouldConvertCorrectlyIfSelectorsAreNotNull() {
var predicate = labelAndFieldSelectorToPredicate(List.of("label-name=label-value"),
List.of("name=fake-name"));
assertNotNull(predicate);
var fake = new FakeExtension();
var metadata = new Metadata();
fake.setMetadata(metadata);
assertFalse(predicate.test(fake));
metadata.setName("fake-name");
assertFalse(predicate.test(fake));
metadata.setLabels(Map.of("label-name", "label-value"));
assertTrue(predicate.test(fake));
metadata.setName("invalid-name");
assertFalse(predicate.test(fake));
}
}