mirror of https://github.com/halo-dev/halo
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
parent
fdbb513cb2
commit
3640dca0a1
|
|
@ -77,6 +77,7 @@ dependencies {
|
||||||
implementation "io.seruco.encoding:base62:$base62"
|
implementation "io.seruco.encoding:base62:$base62"
|
||||||
implementation "org.pf4j:pf4j:$pf4j"
|
implementation "org.pf4j:pf4j:$pf4j"
|
||||||
compileOnly 'org.projectlombok:lombok'
|
compileOnly 'org.projectlombok:lombok'
|
||||||
|
testCompileOnly 'org.projectlombok:lombok'
|
||||||
|
|
||||||
annotationProcessor "org.springframework.boot:spring-boot-configuration-processor"
|
annotationProcessor "org.springframework.boot:spring-boot-configuration-processor"
|
||||||
|
|
||||||
|
|
@ -87,6 +88,7 @@ dependencies {
|
||||||
runtimeOnly 'org.postgresql:postgresql'
|
runtimeOnly 'org.postgresql:postgresql'
|
||||||
|
|
||||||
annotationProcessor 'org.projectlombok:lombok'
|
annotationProcessor 'org.projectlombok:lombok'
|
||||||
|
testAnnotationProcessor 'org.projectlombok:lombok'
|
||||||
|
|
||||||
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||||
testImplementation 'org.springframework.security:spring-security-test'
|
testImplementation 'org.springframework.security:spring-security-test'
|
||||||
|
|
@ -95,5 +97,4 @@ dependencies {
|
||||||
|
|
||||||
tasks.named('test') {
|
tasks.named('test') {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
testLogging.showStandardStreams = true
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ import run.halo.app.extension.DefaultExtensionClient;
|
||||||
import run.halo.app.extension.DefaultSchemeManager;
|
import run.halo.app.extension.DefaultSchemeManager;
|
||||||
import run.halo.app.extension.DefaultSchemeWatcherManager;
|
import run.halo.app.extension.DefaultSchemeWatcherManager;
|
||||||
import run.halo.app.extension.ExtensionClient;
|
import run.halo.app.extension.ExtensionClient;
|
||||||
import run.halo.app.extension.ExtensionCompositeRouterFunction;
|
|
||||||
import run.halo.app.extension.JSONExtensionConverter;
|
import run.halo.app.extension.JSONExtensionConverter;
|
||||||
import run.halo.app.extension.SchemeManager;
|
import run.halo.app.extension.SchemeManager;
|
||||||
import run.halo.app.extension.SchemeWatcherManager;
|
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.Controller;
|
||||||
import run.halo.app.extension.controller.ControllerBuilder;
|
import run.halo.app.extension.controller.ControllerBuilder;
|
||||||
import run.halo.app.extension.controller.ControllerManager;
|
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.extension.store.ExtensionStoreClient;
|
||||||
import run.halo.app.plugin.HaloPluginManager;
|
import run.halo.app.plugin.HaloPluginManager;
|
||||||
import run.halo.app.plugin.resources.JsBundleRuleProvider;
|
import run.halo.app.plugin.resources.JsBundleRuleProvider;
|
||||||
|
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package run.halo.app.extension;
|
package run.halo.app.extension.router;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Map;
|
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 org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
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;
|
import run.halo.app.extension.SchemeWatcherManager.SchemeWatcher;
|
||||||
|
|
||||||
public class ExtensionCompositeRouterFunction implements
|
public class ExtensionCompositeRouterFunction implements
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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}";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -114,6 +114,7 @@ class ExtensionConfigurationTest {
|
||||||
|
|
||||||
var metadata = new Metadata();
|
var metadata = new Metadata();
|
||||||
metadata.setName("my-fake");
|
metadata.setName("my-fake");
|
||||||
|
metadata.setLabels(Map.of("label-key", "label-value"));
|
||||||
var fake = new FakeExtension();
|
var fake = new FakeExtension();
|
||||||
fake.setMetadata(metadata);
|
fake.setMetadata(metadata);
|
||||||
|
|
||||||
|
|
@ -158,14 +159,37 @@ class ExtensionConfigurationTest {
|
||||||
@Test
|
@Test
|
||||||
@WithMockUser
|
@WithMockUser
|
||||||
void shouldListExtensionsWhenSchemeRegistered() {
|
void shouldListExtensionsWhenSchemeRegistered() {
|
||||||
webClient.get()
|
webClient.get().uri("/apis/fake.halo.run/v1alpha1/fakes")
|
||||||
.uri("/apis/fake.halo.run/v1alpha1/fakes")
|
|
||||||
.exchange()
|
.exchange()
|
||||||
.expectStatus().isOk()
|
.expectStatus().isOk()
|
||||||
.expectBodyList(FakeExtension.class)
|
.expectBody().jsonPath("$.items.length()").isEqualTo(1);
|
||||||
.hasSize(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
|
@Test
|
||||||
@WithMockUser
|
@WithMockUser
|
||||||
|
|
@ -219,20 +243,6 @@ class ExtensionConfigurationTest {
|
||||||
.getResponseBody();
|
.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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -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.assertNotNull;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
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.mock.web.server.MockServerWebExchange;
|
||||||
import org.springframework.web.reactive.function.server.HandlerStrategies;
|
import org.springframework.web.reactive.function.server.HandlerStrategies;
|
||||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
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.SchemeRegistered;
|
||||||
import run.halo.app.extension.SchemeWatcherManager.SchemeUnregistered;
|
import run.halo.app.extension.SchemeWatcherManager.SchemeUnregistered;
|
||||||
|
|
||||||
|
|
@ -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.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
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 org.springframework.web.reactive.function.server.EntityResponse;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import reactor.test.StepVerifier;
|
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.ExtensionConvertException;
|
||||||
import run.halo.app.extension.exception.ExtensionNotFoundException;
|
import run.halo.app.extension.exception.ExtensionNotFoundException;
|
||||||
|
|
||||||
|
|
@ -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.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
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 org.springframework.web.reactive.function.server.EntityResponse;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import reactor.test.StepVerifier;
|
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;
|
import run.halo.app.extension.exception.ExtensionNotFoundException;
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
|
@ -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.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
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.mock.web.reactive.function.server.MockServerRequest;
|
||||||
import org.springframework.web.reactive.function.server.EntityResponse;
|
import org.springframework.web.reactive.function.server.EntityResponse;
|
||||||
import reactor.test.StepVerifier;
|
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;
|
import run.halo.app.extension.exception.ExtensionNotFoundException;
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
|
@ -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.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
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.mock.web.reactive.function.server.MockServerRequest;
|
||||||
import org.springframework.web.reactive.function.server.EntityResponse;
|
import org.springframework.web.reactive.function.server.EntityResponse;
|
||||||
import reactor.test.StepVerifier;
|
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)
|
@ExtendWith(MockitoExtension.class)
|
||||||
class ExtensionListHandlerTest {
|
class ExtensionListHandlerTest {
|
||||||
|
|
@ -39,7 +42,6 @@ class ExtensionListHandlerTest {
|
||||||
var listHandler = new ExtensionListHandler(scheme, client);
|
var listHandler = new ExtensionListHandler(scheme, client);
|
||||||
var serverRequest = MockServerRequest.builder().build();
|
var serverRequest = MockServerRequest.builder().build();
|
||||||
final var fake = new FakeExtension();
|
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));
|
var fakeListResult = new ListResult<>(0, 0, 1, List.of(fake));
|
||||||
when(client.list(same(FakeExtension.class), any(), any(), anyInt(), anyInt()))
|
when(client.list(same(FakeExtension.class), any(), any(), anyInt(), anyInt()))
|
||||||
.thenReturn(fakeListResult);
|
.thenReturn(fakeListResult);
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package run.halo.app.extension;
|
package run.halo.app.extension.router;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
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.ServerRequest;
|
||||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
import org.springframework.web.server.ServerWebExchange;
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
import run.halo.app.extension.ExtensionRouterFunctionFactory.CreateHandler;
|
import run.halo.app.extension.ExtensionClient;
|
||||||
import run.halo.app.extension.ExtensionRouterFunctionFactory.GetHandler;
|
import run.halo.app.extension.FakeExtension;
|
||||||
import run.halo.app.extension.ExtensionRouterFunctionFactory.ListHandler;
|
import run.halo.app.extension.Scheme;
|
||||||
import run.halo.app.extension.ExtensionRouterFunctionFactory.UpdateHandler;
|
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)
|
@ExtendWith(MockitoExtension.class)
|
||||||
class ExtensionRouterFunctionFactoryTest {
|
class ExtensionRouterFunctionFactoryTest {
|
||||||
|
|
@ -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.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
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 org.springframework.web.reactive.function.server.EntityResponse;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import reactor.test.StepVerifier;
|
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.ExtensionConvertException;
|
||||||
import run.halo.app.extension.exception.ExtensionNotFoundException;
|
import run.halo.app.extension.exception.ExtensionNotFoundException;
|
||||||
|
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
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.assertEquals;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
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 {
|
class PathPatternGeneratorTest {
|
||||||
|
|
||||||
|
|
@ -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)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue