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 "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
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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.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
 | 
			
		||||
| 
						 | 
				
			
			@ -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();
 | 
			
		||||
            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();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
| 
						 | 
				
			
			@ -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);
 | 
			
		||||
| 
						 | 
				
			
			@ -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 {
 | 
			
		||||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -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 {
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -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