mirror of https://github.com/halo-dev/halo
Add feature to generate APIs for schemes automatically (#2158)
* Add ExtensionEndpointInstaller * Refactor Schemes with SchemeManager * Add some unit tests 1. Add ExtensionCompositeRouterFunctionTest 2. Add ExtensionConfigurationTest 3. Refactor Unstructured 4. Fix bad ObjectMapper in Json converter. * Fix bad scheme registrationpull/2161/head
parent
fcbf0031a4
commit
e52db6859f
|
|
@ -1,4 +0,0 @@
|
||||||
@startuml
|
|
||||||
ExceptionHandlingWebHandler -> FilteringWebHandler
|
|
||||||
FilteringWebHandler contains filters and DispatcherHandler
|
|
||||||
@enduml
|
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
package run.halo.app.config;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||||
|
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
|
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;
|
||||||
|
import run.halo.app.extension.SchemeWatcherManager.SchemeWatcher;
|
||||||
|
import run.halo.app.extension.store.ExtensionStoreClient;
|
||||||
|
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
public class ExtensionConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
RouterFunction<ServerResponse> extensionsRouterFunction(ExtensionClient client,
|
||||||
|
SchemeWatcherManager watcherManager) {
|
||||||
|
return new ExtensionCompositeRouterFunction(client, watcherManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
ExtensionClient extensionClient(ExtensionStoreClient storeClient, SchemeManager schemeManager) {
|
||||||
|
var converter = new JSONExtensionConverter(schemeManager);
|
||||||
|
return new DefaultExtensionClient(storeClient, converter, schemeManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
SchemeManager schemeManager(SchemeWatcherManager watcherManager, List<SchemeWatcher> watchers) {
|
||||||
|
return new DefaultSchemeManager(watcherManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
SchemeWatcherManager schemeWatcherManager() {
|
||||||
|
return new DefaultSchemeWatcherManager();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -16,4 +16,15 @@ public abstract class AbstractExtension implements Extension {
|
||||||
|
|
||||||
private MetadataOperator metadata;
|
private MetadataOperator metadata;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getApiVersion() {
|
||||||
|
var apiVersionFromGvk = Extension.super.getApiVersion();
|
||||||
|
return apiVersionFromGvk != null ? apiVersionFromGvk : this.apiVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getKind() {
|
||||||
|
var kindFromGvk = Extension.super.getKind();
|
||||||
|
return kindFromGvk != null ? kindFromGvk : this.kind;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import java.util.function.Predicate;
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.data.domain.PageRequest;
|
import org.springframework.data.domain.PageRequest;
|
||||||
import org.springframework.data.support.PageableExecutionUtils;
|
import org.springframework.data.support.PageableExecutionUtils;
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
import run.halo.app.extension.store.ExtensionStore;
|
import run.halo.app.extension.store.ExtensionStore;
|
||||||
import run.halo.app.extension.store.ExtensionStoreClient;
|
import run.halo.app.extension.store.ExtensionStoreClient;
|
||||||
|
|
@ -18,21 +17,25 @@ import run.halo.app.extension.store.ExtensionStoreClient;
|
||||||
*
|
*
|
||||||
* @author johnniang
|
* @author johnniang
|
||||||
*/
|
*/
|
||||||
@Service
|
|
||||||
public class DefaultExtensionClient implements ExtensionClient {
|
public class DefaultExtensionClient implements ExtensionClient {
|
||||||
|
|
||||||
private final ExtensionStoreClient storeClient;
|
private final ExtensionStoreClient storeClient;
|
||||||
private final ExtensionConverter converter;
|
private final ExtensionConverter converter;
|
||||||
|
|
||||||
public DefaultExtensionClient(ExtensionStoreClient storeClient, ExtensionConverter converter) {
|
private final SchemeManager schemeManager;
|
||||||
|
|
||||||
|
public DefaultExtensionClient(ExtensionStoreClient storeClient,
|
||||||
|
ExtensionConverter converter,
|
||||||
|
SchemeManager schemeManager) {
|
||||||
this.storeClient = storeClient;
|
this.storeClient = storeClient;
|
||||||
this.converter = converter;
|
this.converter = converter;
|
||||||
|
this.schemeManager = schemeManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public <E extends Extension> List<E> list(Class<E> type, Predicate<E> predicate,
|
public <E extends Extension> List<E> list(Class<E> type, Predicate<E> predicate,
|
||||||
Comparator<E> comparator) {
|
Comparator<E> comparator) {
|
||||||
var scheme = Schemes.INSTANCE.get(type);
|
var scheme = schemeManager.get(type);
|
||||||
var storeNamePrefix = ExtensionUtil.buildStoreNamePrefix(scheme);
|
var storeNamePrefix = ExtensionUtil.buildStoreNamePrefix(scheme);
|
||||||
|
|
||||||
var storesStream = storeClient.listByNamePrefix(storeNamePrefix).stream()
|
var storesStream = storeClient.listByNamePrefix(storeNamePrefix).stream()
|
||||||
|
|
@ -59,7 +62,7 @@ public class DefaultExtensionClient implements ExtensionClient {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public <E extends Extension> Optional<E> fetch(Class<E> type, String name) {
|
public <E extends Extension> Optional<E> fetch(Class<E> type, String name) {
|
||||||
var scheme = Schemes.INSTANCE.get(type);
|
var scheme = schemeManager.get(type);
|
||||||
|
|
||||||
var storeName = ExtensionUtil.buildStoreName(scheme, name);
|
var storeName = ExtensionUtil.buildStoreName(scheme, name);
|
||||||
return storeClient.fetchByName(storeName)
|
return storeClient.fetchByName(storeName)
|
||||||
|
|
@ -68,7 +71,9 @@ public class DefaultExtensionClient implements ExtensionClient {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public <E extends Extension> void create(E extension) {
|
public <E extends Extension> void create(E extension) {
|
||||||
extension.getMetadata().setCreationTimestamp(Instant.now());
|
var metadata = extension.getMetadata();
|
||||||
|
metadata.setCreationTimestamp(Instant.now());
|
||||||
|
// extension.setMetadata(metadata);
|
||||||
var extensionStore = converter.convertTo(extension);
|
var extensionStore = converter.convertTo(extension);
|
||||||
storeClient.create(extensionStore.getName(), extensionStore.getData());
|
storeClient.create(extensionStore.getName(), extensionStore.getData());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
package run.halo.app.extension;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import org.springframework.lang.NonNull;
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
|
import run.halo.app.extension.SchemeWatcherManager.SchemeRegistered;
|
||||||
|
import run.halo.app.extension.SchemeWatcherManager.SchemeUnregistered;
|
||||||
|
|
||||||
|
public class DefaultSchemeManager implements SchemeManager {
|
||||||
|
|
||||||
|
private final List<Scheme> schemes;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private final SchemeWatcherManager watcherManager;
|
||||||
|
|
||||||
|
public DefaultSchemeManager(@Nullable SchemeWatcherManager watcherManager) {
|
||||||
|
this.watcherManager = watcherManager;
|
||||||
|
schemes = new LinkedList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void register(@NonNull Scheme scheme) {
|
||||||
|
if (!schemes.contains(scheme)) {
|
||||||
|
schemes.add(scheme);
|
||||||
|
getWatchers().forEach(watcher -> watcher.onChange(new SchemeRegistered(scheme)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void unregister(@NonNull Scheme scheme) {
|
||||||
|
if (schemes.contains(scheme)) {
|
||||||
|
schemes.remove(scheme);
|
||||||
|
getWatchers().forEach(watcher -> watcher.onChange(new SchemeUnregistered(scheme)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@NonNull
|
||||||
|
public List<Scheme> schemes() {
|
||||||
|
return Collections.unmodifiableList(schemes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private List<SchemeWatcherManager.SchemeWatcher> getWatchers() {
|
||||||
|
if (this.watcherManager == null) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
return Optional.ofNullable(this.watcherManager.watchers()).orElse(Collections.emptyList());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
package run.halo.app.extension;
|
||||||
|
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.lang.NonNull;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
public class DefaultSchemeWatcherManager implements SchemeWatcherManager {
|
||||||
|
|
||||||
|
private final List<SchemeWatcher> watchers;
|
||||||
|
|
||||||
|
public DefaultSchemeWatcherManager() {
|
||||||
|
watchers = new LinkedList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void register(@NonNull SchemeWatcher watcher) {
|
||||||
|
Assert.notNull(watcher, "Scheme watcher must not be null");
|
||||||
|
watchers.add(watcher);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void unregister(@NonNull SchemeWatcher watcher) {
|
||||||
|
Assert.notNull(watcher, "Scheme watcher must not be null");
|
||||||
|
watchers.remove(watcher);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<SchemeWatcher> watchers() {
|
||||||
|
// we have to copy the watchers entirely to prevent concurrent modification.
|
||||||
|
return List.copyOf(watchers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
package run.halo.app.extension;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
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.RouterFunctions;
|
||||||
|
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.SchemeWatcherManager.SchemeWatcher;
|
||||||
|
|
||||||
|
public class ExtensionCompositeRouterFunction implements
|
||||||
|
RouterFunction<ServerResponse>, SchemeWatcher {
|
||||||
|
|
||||||
|
private final Map<Scheme, RouterFunction<ServerResponse>> schemeRouterFuncMapper;
|
||||||
|
|
||||||
|
private final ExtensionClient client;
|
||||||
|
|
||||||
|
public ExtensionCompositeRouterFunction(ExtensionClient client,
|
||||||
|
SchemeWatcherManager watcherManager) {
|
||||||
|
this.client = client;
|
||||||
|
schemeRouterFuncMapper = new ConcurrentHashMap<>();
|
||||||
|
if (watcherManager != null) {
|
||||||
|
watcherManager.register(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@NonNull
|
||||||
|
public Mono<HandlerFunction<ServerResponse>> route(@NonNull ServerRequest request) {
|
||||||
|
return Flux.fromIterable(getRouterFunctions())
|
||||||
|
.concatMap(routerFunction -> routerFunction.route(request))
|
||||||
|
.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void accept(@NonNull RouterFunctions.Visitor visitor) {
|
||||||
|
getRouterFunctions().forEach(routerFunction -> routerFunction.accept(visitor));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Iterable<RouterFunction<ServerResponse>> getRouterFunctions() {
|
||||||
|
// TODO Copy router functions here
|
||||||
|
return Collections.unmodifiableCollection(schemeRouterFuncMapper.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onChange(SchemeWatcherManager.ChangeEvent event) {
|
||||||
|
if (event instanceof SchemeWatcherManager.SchemeRegistered registeredEvent) {
|
||||||
|
var scheme = registeredEvent.getNewScheme();
|
||||||
|
var factory = new ExtensionRouterFunctionFactory(scheme, client);
|
||||||
|
this.schemeRouterFuncMapper.put(scheme, factory.create());
|
||||||
|
} else if (event instanceof SchemeWatcherManager.SchemeUnregistered unregisteredEvent) {
|
||||||
|
this.schemeRouterFuncMapper.remove(unregisteredEvent.getDeletedScheme());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ package run.halo.app.extension;
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ExtensionOperator contains some getters and setters for required fields of Extension.
|
* ExtensionOperator contains some getters and setters for required fields of Extension.
|
||||||
|
|
@ -13,11 +14,28 @@ public interface ExtensionOperator {
|
||||||
|
|
||||||
@Schema(required = true)
|
@Schema(required = true)
|
||||||
@JsonProperty("apiVersion")
|
@JsonProperty("apiVersion")
|
||||||
String getApiVersion();
|
default String getApiVersion() {
|
||||||
|
final var gvk = getClass().getAnnotation(GVK.class);
|
||||||
|
if (gvk == null) {
|
||||||
|
// return null if having no GVK annotation
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (StringUtils.hasText(gvk.group())) {
|
||||||
|
return gvk.group() + "/" + gvk.version();
|
||||||
|
}
|
||||||
|
return gvk.version();
|
||||||
|
}
|
||||||
|
|
||||||
@Schema(required = true)
|
@Schema(required = true)
|
||||||
@JsonProperty("kind")
|
@JsonProperty("kind")
|
||||||
String getKind();
|
default String getKind() {
|
||||||
|
final var gvk = getClass().getAnnotation(GVK.class);
|
||||||
|
if (gvk == null) {
|
||||||
|
// return null if having no GVK annotation
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return gvk.kind();
|
||||||
|
}
|
||||||
|
|
||||||
@Schema(required = true, implementation = Metadata.class)
|
@Schema(required = true, implementation = Metadata.class)
|
||||||
@JsonProperty("metadata")
|
@JsonProperty("metadata")
|
||||||
|
|
@ -46,7 +64,7 @@ public interface ExtensionOperator {
|
||||||
* {@link #setMetadata(MetadataOperator)}.
|
* {@link #setMetadata(MetadataOperator)}.
|
||||||
*
|
*
|
||||||
* @param metadata is Extension metadata.
|
* @param metadata is Extension metadata.
|
||||||
* @see #setMetadata(MetadataOperator)
|
* @see #setMetadata(MetadataOperator)
|
||||||
*/
|
*/
|
||||||
@Deprecated(forRemoval = true)
|
@Deprecated(forRemoval = true)
|
||||||
default void metadata(MetadataOperator metadata) {
|
default void metadata(MetadataOperator metadata) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,162 @@
|
||||||
|
package run.halo.app.extension;
|
||||||
|
|
||||||
|
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
|
||||||
|
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
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.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);
|
||||||
|
// TODO More handlers here
|
||||||
|
return route()
|
||||||
|
.GET(getHandler.pathPattern(), getHandler)
|
||||||
|
.GET(listHandler.pathPattern(), listHandler)
|
||||||
|
.POST(createHandler.pathPattern(), createHandler)
|
||||||
|
.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 {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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())))
|
||||||
|
.doOnSuccess(client::create)
|
||||||
|
.map(unstructured ->
|
||||||
|
client.fetch(scheme.type(), unstructured.getMetadata().getName())
|
||||||
|
.orElseThrow(() -> new ExtensionNotFoundException(
|
||||||
|
scheme.groupVersionKind() + " " + unstructured.getMetadata().getName()
|
||||||
|
+ "was not found")))
|
||||||
|
.flatMap(extension -> ServerResponse
|
||||||
|
.ok()
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.bodyValue(extension))
|
||||||
|
.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) {
|
||||||
|
// TODO Resolve predicate and comparator from request
|
||||||
|
var extensions = client.list(scheme.type(), null, null);
|
||||||
|
return ServerResponse
|
||||||
|
.ok()
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.bodyValue(extensions);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package run.halo.app.extension;
|
package run.halo.app.extension;
|
||||||
|
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GroupVersionKind contains group, version and kind name of an Extension.
|
* GroupVersionKind contains group, version and kind name of an Extension.
|
||||||
|
|
@ -26,6 +27,10 @@ public record GroupVersionKind(String group, String version, String kind) {
|
||||||
return new GroupVersion(group, version);
|
return new GroupVersion(group, version);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean hasGroup() {
|
||||||
|
return StringUtils.hasText(group);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Composes GroupVersionKind from API version and kind name.
|
* Composes GroupVersionKind from API version and kind name.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
package run.halo.app.extension;
|
package run.halo.app.extension;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||||
import com.networknt.schema.JsonSchemaFactory;
|
import com.networknt.schema.JsonSchemaFactory;
|
||||||
import com.networknt.schema.SpecVersion;
|
import com.networknt.schema.SpecVersion;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.util.CollectionUtils;
|
import org.springframework.util.CollectionUtils;
|
||||||
import run.halo.app.extension.exception.ExtensionConvertException;
|
import run.halo.app.extension.exception.ExtensionConvertException;
|
||||||
|
|
@ -23,18 +24,28 @@ public class JSONExtensionConverter implements ExtensionConverter {
|
||||||
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(getClass());
|
private final Logger logger = LoggerFactory.getLogger(getClass());
|
||||||
|
|
||||||
private final ObjectMapper objectMapper;
|
public static final ObjectMapper OBJECT_MAPPER;
|
||||||
private final JsonSchemaFactory jsonSchemaFactory;
|
private final JsonSchemaFactory jsonSchemaFactory;
|
||||||
|
|
||||||
public JSONExtensionConverter(ObjectMapper objectMapper) {
|
private final SchemeManager schemeManager;
|
||||||
this.objectMapper = objectMapper;
|
|
||||||
|
|
||||||
|
static {
|
||||||
|
OBJECT_MAPPER = Jackson2ObjectMapperBuilder.json()
|
||||||
|
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
|
||||||
|
.featuresToDisable(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public JSONExtensionConverter(SchemeManager schemeManager) {
|
||||||
|
this.schemeManager = schemeManager;
|
||||||
jsonSchemaFactory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909);
|
jsonSchemaFactory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public <E extends Extension> ExtensionStore convertTo(E extension) {
|
public <E extends Extension> ExtensionStore convertTo(E extension) {
|
||||||
var gvk = extension.groupVersionKind();
|
var gvk = extension.groupVersionKind();
|
||||||
var scheme = Schemes.INSTANCE.get(gvk);
|
var scheme = schemeManager.get(gvk);
|
||||||
var storeName = ExtensionUtil.buildStoreName(scheme, extension.getMetadata().getName());
|
var storeName = ExtensionUtil.buildStoreName(scheme, extension.getMetadata().getName());
|
||||||
try {
|
try {
|
||||||
if (logger.isDebugEnabled()) {
|
if (logger.isDebugEnabled()) {
|
||||||
|
|
@ -42,9 +53,10 @@ public class JSONExtensionConverter implements ExtensionConverter {
|
||||||
scheme.jsonSchema().toPrettyString());
|
scheme.jsonSchema().toPrettyString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var data = OBJECT_MAPPER.writeValueAsBytes(extension);
|
||||||
|
|
||||||
var validator = jsonSchemaFactory.getSchema(scheme.jsonSchema());
|
var validator = jsonSchemaFactory.getSchema(scheme.jsonSchema());
|
||||||
var extensionNode = objectMapper.valueToTree(extension);
|
var errors = validator.validate(OBJECT_MAPPER.readTree(data));
|
||||||
var errors = validator.validate(extensionNode);
|
|
||||||
if (!CollectionUtils.isEmpty(errors)) {
|
if (!CollectionUtils.isEmpty(errors)) {
|
||||||
if (logger.isDebugEnabled()) {
|
if (logger.isDebugEnabled()) {
|
||||||
// only print the errors when debug mode is enabled
|
// only print the errors when debug mode is enabled
|
||||||
|
|
@ -55,11 +67,9 @@ public class JSONExtensionConverter implements ExtensionConverter {
|
||||||
"Failed to validate Extension " + extension.getClass(), errors);
|
"Failed to validate Extension " + extension.getClass(), errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
// keep converting
|
|
||||||
var data = objectMapper.writeValueAsBytes(extensionNode);
|
|
||||||
var version = extension.getMetadata().getVersion();
|
var version = extension.getMetadata().getVersion();
|
||||||
return new ExtensionStore(storeName, data, version);
|
return new ExtensionStore(storeName, data, version);
|
||||||
} catch (JsonProcessingException e) {
|
} catch (IOException e) {
|
||||||
throw new ExtensionConvertException("Failed write Extension as bytes", e);
|
throw new ExtensionConvertException("Failed write Extension as bytes", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -67,7 +77,7 @@ public class JSONExtensionConverter implements ExtensionConverter {
|
||||||
@Override
|
@Override
|
||||||
public <E extends Extension> E convertFrom(Class<E> type, ExtensionStore extensionStore) {
|
public <E extends Extension> E convertFrom(Class<E> type, ExtensionStore extensionStore) {
|
||||||
try {
|
try {
|
||||||
var extension = objectMapper.readValue(extensionStore.getData(), type);
|
var extension = OBJECT_MAPPER.readValue(extensionStore.getData(), type);
|
||||||
extension.getMetadata().setVersion(extensionStore.getVersion());
|
extension.getMetadata().setVersion(extensionStore.getVersion());
|
||||||
return extension;
|
return extension;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package run.halo.app.extension;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Metadata of Extension.
|
* Metadata of Extension.
|
||||||
|
|
@ -10,6 +11,7 @@ import lombok.Data;
|
||||||
* @author johnniang
|
* @author johnniang
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
|
@EqualsAndHashCode
|
||||||
public class Metadata implements MetadataOperator {
|
public class Metadata implements MetadataOperator {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MetadataOperator contains some getters and setters for required fields of metadata.
|
* MetadataOperator contains some getters and setters for required fields of metadata.
|
||||||
|
|
@ -51,4 +52,31 @@ public interface MetadataOperator {
|
||||||
|
|
||||||
void setDeletionTimestamp(Instant deletionTimestamp);
|
void setDeletionTimestamp(Instant deletionTimestamp);
|
||||||
|
|
||||||
|
static boolean metadataDeepEquals(MetadataOperator left, MetadataOperator right) {
|
||||||
|
if (left == null && right == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (left == null || right == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!Objects.equals(left.getName(), right.getName())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!Objects.equals(left.getLabels(), right.getLabels())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!Objects.equals(left.getAnnotations(), right.getAnnotations())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!Objects.equals(left.getCreationTimestamp(), right.getCreationTimestamp())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!Objects.equals(left.getDeletionTimestamp(), right.getDeletionTimestamp())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!Objects.equals(left.getVersion(), right.getVersion())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,15 @@
|
||||||
package run.halo.app.extension;
|
package run.halo.app.extension;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
import com.github.victools.jsonschema.generator.Option;
|
||||||
|
import com.github.victools.jsonschema.generator.OptionPreset;
|
||||||
|
import com.github.victools.jsonschema.generator.SchemaGenerator;
|
||||||
|
import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder;
|
||||||
|
import com.github.victools.jsonschema.generator.SchemaVersion;
|
||||||
|
import com.github.victools.jsonschema.module.swagger2.Swagger2Module;
|
||||||
|
import org.springframework.lang.NonNull;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
|
import run.halo.app.extension.exception.ExtensionException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class represents scheme of an Extension.
|
* This class represents scheme of an Extension.
|
||||||
|
|
@ -26,4 +34,55 @@ public record Scheme(Class<? extends Extension> type,
|
||||||
Assert.notNull(jsonSchema, "Json Schema must not be null");
|
Assert.notNull(jsonSchema, "Json Schema must not be null");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds Scheme from type with @GVK annotation.
|
||||||
|
*
|
||||||
|
* @param type is Extension type with GVK annotation.
|
||||||
|
* @return Scheme definition.
|
||||||
|
* @throws ExtensionException when the type has not annotated @GVK.
|
||||||
|
*/
|
||||||
|
public static Scheme buildFromType(Class<? extends Extension> type) {
|
||||||
|
// concrete scheme from annotation
|
||||||
|
var gvk = getGvkFromType(type);
|
||||||
|
|
||||||
|
// TODO Move the generation logic outside.
|
||||||
|
// generate JSON schema
|
||||||
|
var module = new Swagger2Module();
|
||||||
|
var config =
|
||||||
|
new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2019_09, OptionPreset.PLAIN_JSON)
|
||||||
|
.with(
|
||||||
|
// See https://victools.github.io/jsonschema-generator/#generator-options
|
||||||
|
// fore more.
|
||||||
|
Option.INLINE_ALL_SCHEMAS,
|
||||||
|
Option.MAP_VALUES_AS_ADDITIONAL_PROPERTIES
|
||||||
|
)
|
||||||
|
.with(module)
|
||||||
|
.build();
|
||||||
|
var generator = new SchemaGenerator(config);
|
||||||
|
var jsonSchema = generator.generateSchema(type);
|
||||||
|
|
||||||
|
return new Scheme(type,
|
||||||
|
new GroupVersionKind(gvk.group(), gvk.version(), gvk.kind()),
|
||||||
|
gvk.plural(),
|
||||||
|
gvk.singular(),
|
||||||
|
jsonSchema);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets GVK annotation from Extension type.
|
||||||
|
*
|
||||||
|
* @param type is Extension type with GVK annotation.
|
||||||
|
* @return GVK annotation.
|
||||||
|
* @throws ExtensionException when the type has not annotated @GVK.
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public static GVK getGvkFromType(@NonNull Class<? extends Extension> type) {
|
||||||
|
var gvk = type.getAnnotation(GVK.class);
|
||||||
|
if (gvk == null) {
|
||||||
|
throw new ExtensionException(
|
||||||
|
String.format("Annotation %s needs to be on Extension %s", GVK.class.getName(),
|
||||||
|
type.getName()));
|
||||||
|
}
|
||||||
|
return gvk;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
package run.halo.app.extension;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import org.springframework.lang.NonNull;
|
||||||
|
import run.halo.app.extension.exception.SchemeNotFoundException;
|
||||||
|
|
||||||
|
public interface SchemeManager {
|
||||||
|
|
||||||
|
void register(@NonNull Scheme scheme);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers an Extension using its type.
|
||||||
|
*
|
||||||
|
* @param type is Extension type.
|
||||||
|
* @param <T> Extension class.
|
||||||
|
*/
|
||||||
|
default <T extends Extension> void register(Class<T> type) {
|
||||||
|
register(Scheme.buildFromType(type));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void unregister(@NonNull Scheme scheme);
|
||||||
|
|
||||||
|
default int size() {
|
||||||
|
return schemes().size();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
List<Scheme> schemes();
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
default Optional<Scheme> fetch(@NonNull GroupVersionKind gvk) {
|
||||||
|
return schemes().stream()
|
||||||
|
.filter(scheme -> Objects.equals(scheme.groupVersionKind(), gvk))
|
||||||
|
.findFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
default Scheme get(@NonNull GroupVersionKind gvk) {
|
||||||
|
return fetch(gvk).orElseThrow(
|
||||||
|
() -> new SchemeNotFoundException("Scheme was not found for " + gvk));
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
default Scheme get(Class<? extends Extension> type) {
|
||||||
|
var gvk = Scheme.getGvkFromType(type);
|
||||||
|
return get(new GroupVersionKind(gvk.group(), gvk.version(), gvk.kind()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
default Scheme get(Extension ext) {
|
||||||
|
var gvk = ext.groupVersionKind();
|
||||||
|
return get(gvk);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
package run.halo.app.extension;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.lang.NonNull;
|
||||||
|
|
||||||
|
public interface SchemeWatcherManager {
|
||||||
|
|
||||||
|
void register(@NonNull SchemeWatcher watcher);
|
||||||
|
|
||||||
|
void unregister(@NonNull SchemeWatcher watcher);
|
||||||
|
|
||||||
|
List<SchemeWatcher> watchers();
|
||||||
|
|
||||||
|
interface SchemeWatcher {
|
||||||
|
|
||||||
|
void onChange(ChangeEvent event);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChangeEvent {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class SchemeRegistered implements ChangeEvent {
|
||||||
|
private final Scheme newScheme;
|
||||||
|
|
||||||
|
public SchemeRegistered(Scheme newScheme) {
|
||||||
|
this.newScheme = newScheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Scheme getNewScheme() {
|
||||||
|
return newScheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SchemeUnregistered implements ChangeEvent {
|
||||||
|
|
||||||
|
private final Scheme deletedScheme;
|
||||||
|
|
||||||
|
public SchemeUnregistered(Scheme deletedScheme) {
|
||||||
|
this.deletedScheme = deletedScheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Scheme getDeletedScheme() {
|
||||||
|
return deletedScheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,147 +0,0 @@
|
||||||
package run.halo.app.extension;
|
|
||||||
|
|
||||||
import com.github.victools.jsonschema.generator.Option;
|
|
||||||
import com.github.victools.jsonschema.generator.OptionPreset;
|
|
||||||
import com.github.victools.jsonschema.generator.SchemaGenerator;
|
|
||||||
import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder;
|
|
||||||
import com.github.victools.jsonschema.generator.SchemaVersion;
|
|
||||||
import com.github.victools.jsonschema.module.swagger2.Swagger2Module;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.Set;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import run.halo.app.extension.exception.ExtensionException;
|
|
||||||
import run.halo.app.extension.exception.SchemeNotFoundException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Schemes is aggregation of schemes and responsible for managing and organizing schemes.
|
|
||||||
*
|
|
||||||
* @author johnniang
|
|
||||||
*/
|
|
||||||
public enum Schemes {
|
|
||||||
|
|
||||||
INSTANCE;
|
|
||||||
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(getClass());
|
|
||||||
|
|
||||||
/**
|
|
||||||
* All registered schemes.
|
|
||||||
*/
|
|
||||||
private final Set<Scheme> schemes;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The map mapping type and scheme of Extension.
|
|
||||||
*/
|
|
||||||
private final Map<Class<? extends Extension>, Scheme> typeToScheme;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The map mapping GroupVersionKind and type of Extension.
|
|
||||||
*/
|
|
||||||
private final Map<GroupVersionKind, Scheme> gvkToScheme;
|
|
||||||
|
|
||||||
Schemes() {
|
|
||||||
schemes = new HashSet<>();
|
|
||||||
typeToScheme = new HashMap<>();
|
|
||||||
gvkToScheme = new HashMap<>();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear registered schemes.
|
|
||||||
* This method is only for test.
|
|
||||||
*/
|
|
||||||
void clear() {
|
|
||||||
schemes.clear();
|
|
||||||
typeToScheme.clear();
|
|
||||||
gvkToScheme.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers an Extension using its type.
|
|
||||||
*
|
|
||||||
* @param type is Extension type.
|
|
||||||
* @param <T> Extension class.
|
|
||||||
*/
|
|
||||||
public <T extends Extension> void register(Class<T> type) {
|
|
||||||
// concrete scheme from annotation
|
|
||||||
var gvk = type.getAnnotation(GVK.class);
|
|
||||||
if (gvk == null) {
|
|
||||||
// should never happen
|
|
||||||
throw new ExtensionException(
|
|
||||||
String.format("Annotation %s needs to be on Extension %s", GVK.class.getName(),
|
|
||||||
type.getName()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO Move the generation logic outside.
|
|
||||||
// generate JSON schema
|
|
||||||
var module = new Swagger2Module();
|
|
||||||
var config =
|
|
||||||
new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2019_09, OptionPreset.PLAIN_JSON)
|
|
||||||
.with(
|
|
||||||
// See https://victools.github.io/jsonschema-generator/#generator-options
|
|
||||||
// fore more.
|
|
||||||
Option.INLINE_ALL_SCHEMAS,
|
|
||||||
Option.MAP_VALUES_AS_ADDITIONAL_PROPERTIES
|
|
||||||
)
|
|
||||||
.with(module)
|
|
||||||
.build();
|
|
||||||
var generator = new SchemaGenerator(config);
|
|
||||||
var jsonSchema = generator.generateSchema(type);
|
|
||||||
|
|
||||||
var scheme = new Scheme(type, new GroupVersionKind(gvk.group(), gvk.version(), gvk.kind()),
|
|
||||||
gvk.plural(), gvk.singular(), jsonSchema);
|
|
||||||
|
|
||||||
register(scheme);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers a Scheme of Extension.
|
|
||||||
*
|
|
||||||
* @param scheme is fresh scheme of Extension.
|
|
||||||
*/
|
|
||||||
public void register(Scheme scheme) {
|
|
||||||
boolean added = schemes.add(scheme);
|
|
||||||
if (!added) {
|
|
||||||
logger.warn("Scheme " + scheme
|
|
||||||
+ " has been registered before, please check the repeat register.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
typeToScheme.put(scheme.type(), scheme);
|
|
||||||
gvkToScheme.put(scheme.groupVersionKind(), scheme);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches a Scheme using Extension type.
|
|
||||||
*
|
|
||||||
* @param type is Extension type.
|
|
||||||
* @return an optional Scheme.
|
|
||||||
*/
|
|
||||||
public Optional<Scheme> fetch(Class<? extends Extension> type) {
|
|
||||||
return Optional.ofNullable(typeToScheme.get(type));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public Optional<Scheme> fetch(GroupVersionKind gvk) {
|
|
||||||
return Optional.ofNullable(gvkToScheme.get(gvk));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets a scheme using Extension type.
|
|
||||||
*
|
|
||||||
* @param type is Extension type.
|
|
||||||
* @return non-null Extension scheme.
|
|
||||||
* @throws SchemeNotFoundException when the Extension is not found.
|
|
||||||
*/
|
|
||||||
public Scheme get(Class<? extends Extension> type) {
|
|
||||||
return fetch(type).orElseThrow(() -> new SchemeNotFoundException(
|
|
||||||
"Scheme was not found for Extension " + type.getSimpleName()));
|
|
||||||
}
|
|
||||||
|
|
||||||
public Scheme get(GroupVersionKind gvk) {
|
|
||||||
return fetch(gvk).orElseThrow(() -> new SchemeNotFoundException(
|
|
||||||
"Scheme was not found for GVK " + gvk));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -4,15 +4,17 @@ import com.fasterxml.jackson.core.JsonGenerator;
|
||||||
import com.fasterxml.jackson.core.JsonParser;
|
import com.fasterxml.jackson.core.JsonParser;
|
||||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||||
import com.fasterxml.jackson.databind.JsonDeserializer;
|
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
|
||||||
import com.fasterxml.jackson.databind.JsonSerializer;
|
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
|
||||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unstructured is a generic Extension, which wraps ObjectNode to maintain the Extension data, like
|
* Unstructured is a generic Extension, which wraps ObjectNode to maintain the Extension data, like
|
||||||
|
|
@ -22,68 +24,187 @@ import java.io.IOException;
|
||||||
*/
|
*/
|
||||||
@JsonSerialize(using = Unstructured.UnstructuredSerializer.class)
|
@JsonSerialize(using = Unstructured.UnstructuredSerializer.class)
|
||||||
@JsonDeserialize(using = Unstructured.UnstructuredDeserializer.class)
|
@JsonDeserialize(using = Unstructured.UnstructuredDeserializer.class)
|
||||||
|
@SuppressWarnings("rawtypes")
|
||||||
public class Unstructured implements Extension {
|
public class Unstructured implements Extension {
|
||||||
|
|
||||||
public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
public static final ObjectMapper OBJECT_MAPPER = JSONExtensionConverter.OBJECT_MAPPER;
|
||||||
|
|
||||||
static {
|
private final Map data;
|
||||||
OBJECT_MAPPER.registerModule(new JavaTimeModule());
|
|
||||||
}
|
|
||||||
|
|
||||||
private final ObjectNode extension;
|
|
||||||
|
|
||||||
public Unstructured() {
|
public Unstructured() {
|
||||||
this(OBJECT_MAPPER.createObjectNode());
|
this(new HashMap());
|
||||||
}
|
}
|
||||||
|
|
||||||
public Unstructured(ObjectNode extension) {
|
public Unstructured(Map data) {
|
||||||
this.extension = extension;
|
this.data = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getApiVersion() {
|
public String getApiVersion() {
|
||||||
return extension.get("apiVersion").asText();
|
return (String) data.get("apiVersion");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getKind() {
|
public String getKind() {
|
||||||
return extension.get("kind").asText();
|
return (String) data.get("kind");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MetadataOperator getMetadata() {
|
public MetadataOperator getMetadata() {
|
||||||
var metaMap = extension.get("metadata");
|
return new UnstructuredMetadata();
|
||||||
return OBJECT_MAPPER.convertValue(metaMap, Metadata.class);
|
}
|
||||||
|
|
||||||
|
class UnstructuredMetadata implements MetadataOperator {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return (String) getNestedValue(data, "metadata", "name").orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, String> getLabels() {
|
||||||
|
return getNestedStringStringMap(data, "metadata", "labels").orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, String> getAnnotations() {
|
||||||
|
return getNestedStringStringMap(data, "metadata", "annotations").orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Long getVersion() {
|
||||||
|
return getNestedLong(data, "metadata", "version").orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Instant getCreationTimestamp() {
|
||||||
|
return getNestedInstant(data, "metadata", "creationTimestamp").orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Instant getDeletionTimestamp() {
|
||||||
|
return getNestedInstant(data, "metadata", "deletionTimestamp").orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setName(String name) {
|
||||||
|
setNestedValue(data, name, "metadata", "name");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setLabels(Map<String, String> labels) {
|
||||||
|
setNestedValue(data, labels, "metadata", "labels");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setAnnotations(Map<String, String> annotations) {
|
||||||
|
setNestedValue(data, annotations, "metadata", "annotations");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setVersion(Long version) {
|
||||||
|
setNestedValue(data, version, "metadata", "version");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setCreationTimestamp(Instant creationTimestamp) {
|
||||||
|
setNestedValue(data, creationTimestamp, "metadata", "creationTimestamp");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setDeletionTimestamp(Instant deletionTimestamp) {
|
||||||
|
setNestedValue(data, deletionTimestamp, "metadata", "deletionTimestamp");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setApiVersion(String apiVersion) {
|
public void setApiVersion(String apiVersion) {
|
||||||
extension.put("apiVersion", apiVersion);
|
setNestedValue(data, apiVersion, "apiVersion");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setKind(String kind) {
|
public void setKind(String kind) {
|
||||||
extension.put("kind", kind);
|
setNestedValue(data, kind, "kind");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
public void setMetadata(MetadataOperator metadata) {
|
public void setMetadata(MetadataOperator metadata) {
|
||||||
JsonNode metaNode = OBJECT_MAPPER.valueToTree(metadata);
|
Map metadataMap = OBJECT_MAPPER.convertValue(metadata, Map.class);
|
||||||
extension.set("metadata", metaNode);
|
data.put("metadata", metadataMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
ObjectNode getExtension() {
|
static Optional<Object> getNestedValue(Map map, String... fields) {
|
||||||
return extension;
|
if (fields == null || fields.length == 0) {
|
||||||
|
return Optional.of(map);
|
||||||
|
}
|
||||||
|
Map tempMap = map;
|
||||||
|
for (int i = 0; i < fields.length - 1; i++) {
|
||||||
|
Object value = tempMap.get(fields[i]);
|
||||||
|
if (!(value instanceof Map)) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
tempMap = (Map<?, ?>) value;
|
||||||
|
}
|
||||||
|
return Optional.ofNullable(tempMap.get(fields[fields.length - 1]));
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Add other convenient methods here to set and get nested fields in the future.
|
@SuppressWarnings("unchecked")
|
||||||
|
static void setNestedValue(Map map, Object value, String... fields) {
|
||||||
|
if (fields == null || fields.length == 0) {
|
||||||
|
// do nothing when no fields provided
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var prevFields = Arrays.stream(fields, 0, fields.length - 1)
|
||||||
|
.toArray(String[]::new);
|
||||||
|
getNestedMap(map, prevFields).ifPresent(m -> {
|
||||||
|
var lastField = fields[fields.length - 1];
|
||||||
|
m.put(lastField, value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static Optional<Map> getNestedMap(Map map, String... fields) {
|
||||||
|
return getNestedValue(map, fields).map(value -> (Map) value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
static Optional<Map<String, String>> getNestedStringStringMap(Map map, String... fields) {
|
||||||
|
return getNestedValue(map, fields)
|
||||||
|
.map(labelsObj -> {
|
||||||
|
var labels = (Map) labelsObj;
|
||||||
|
var result = new HashMap<String, String>();
|
||||||
|
labels.forEach((key, value) -> result.put((String) key, (String) value));
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static Optional<Instant> getNestedInstant(Map map, String... fields) {
|
||||||
|
return getNestedValue(map, fields)
|
||||||
|
.map(instantValue -> {
|
||||||
|
if (instantValue instanceof Instant instant) {
|
||||||
|
return instant;
|
||||||
|
}
|
||||||
|
return Instant.parse(instantValue.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static Optional<Long> getNestedLong(Map map, String... fields) {
|
||||||
|
return getNestedValue(map, fields)
|
||||||
|
.map(longObj -> {
|
||||||
|
if (longObj instanceof Long l) {
|
||||||
|
return l;
|
||||||
|
}
|
||||||
|
return Long.valueOf(longObj.toString());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public static class UnstructuredSerializer extends JsonSerializer<Unstructured> {
|
public static class UnstructuredSerializer extends JsonSerializer<Unstructured> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void serialize(Unstructured value, JsonGenerator gen, SerializerProvider serializers)
|
public void serialize(Unstructured value, JsonGenerator gen, SerializerProvider serializers)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
gen.writeTree(value.extension);
|
gen.writeObject(value.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -93,7 +214,8 @@ public class Unstructured implements Extension {
|
||||||
@Override
|
@Override
|
||||||
public Unstructured deserialize(JsonParser p, DeserializationContext ctxt)
|
public Unstructured deserialize(JsonParser p, DeserializationContext ctxt)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
return new Unstructured(p.getCodec().readTree(p));
|
Map data = p.getCodec().readValue(p, Map.class);
|
||||||
|
return new Unstructured(data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
package run.halo.app.extension.exception;
|
||||||
|
|
||||||
|
public class ExtensionNotFoundException extends ExtensionException {
|
||||||
|
|
||||||
|
public ExtensionNotFoundException() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExtensionNotFoundException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExtensionNotFoundException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExtensionNotFoundException(Throwable cause) {
|
||||||
|
super(cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExtensionNotFoundException(String message, Throwable cause, boolean enableSuppression,
|
||||||
|
boolean writableStackTrace) {
|
||||||
|
super(message, cause, enableSuppression, writableStackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,19 +2,28 @@ package run.halo.app.infra;
|
||||||
|
|
||||||
import org.springframework.boot.context.event.ApplicationStartedEvent;
|
import org.springframework.boot.context.event.ApplicationStartedEvent;
|
||||||
import org.springframework.context.ApplicationListener;
|
import org.springframework.context.ApplicationListener;
|
||||||
|
import org.springframework.lang.NonNull;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import run.halo.app.extension.Schemes;
|
import run.halo.app.extension.SchemeManager;
|
||||||
import run.halo.app.plugin.Plugin;
|
import run.halo.app.plugin.Plugin;
|
||||||
import run.halo.app.security.authentication.pat.PersonalAccessToken;
|
import run.halo.app.security.authentication.pat.PersonalAccessToken;
|
||||||
import run.halo.app.security.authorization.Role;
|
import run.halo.app.security.authorization.Role;
|
||||||
|
import run.halo.app.security.authorization.RoleBinding;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class SchemeInitializer implements ApplicationListener<ApplicationStartedEvent> {
|
public class SchemeInitializer implements ApplicationListener<ApplicationStartedEvent> {
|
||||||
|
|
||||||
|
private final SchemeManager schemeManager;
|
||||||
|
|
||||||
|
public SchemeInitializer(SchemeManager schemeManager) {
|
||||||
|
this.schemeManager = schemeManager;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onApplicationEvent(ApplicationStartedEvent event) {
|
public void onApplicationEvent(@NonNull ApplicationStartedEvent event) {
|
||||||
Schemes.INSTANCE.register(Role.class);
|
schemeManager.register(Role.class);
|
||||||
Schemes.INSTANCE.register(PersonalAccessToken.class);
|
schemeManager.register(RoleBinding.class);
|
||||||
Schemes.INSTANCE.register(Plugin.class);
|
schemeManager.register(PersonalAccessToken.class);
|
||||||
|
schemeManager.register(Plugin.class);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import org.springframework.core.io.DefaultResourceLoader;
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import run.halo.app.extension.ExtensionClient;
|
import run.halo.app.extension.ExtensionClient;
|
||||||
import run.halo.app.extension.Schemes;
|
import run.halo.app.extension.SchemeManager;
|
||||||
import run.halo.app.infra.utils.YamlUnstructuredLoader;
|
import run.halo.app.infra.utils.YamlUnstructuredLoader;
|
||||||
import run.halo.app.plugin.event.HaloPluginLoadedEvent;
|
import run.halo.app.plugin.event.HaloPluginLoadedEvent;
|
||||||
import run.halo.app.plugin.resources.ReverseProxy;
|
import run.halo.app.plugin.resources.ReverseProxy;
|
||||||
|
|
@ -20,12 +20,12 @@ public class PluginLoadedListener implements ApplicationListener<HaloPluginLoade
|
||||||
private static final String REVERSE_PROXY_NAME = "extensions/reverseProxy.yaml";
|
private static final String REVERSE_PROXY_NAME = "extensions/reverseProxy.yaml";
|
||||||
private final ExtensionClient extensionClient;
|
private final ExtensionClient extensionClient;
|
||||||
|
|
||||||
public PluginLoadedListener(ExtensionClient extensionClient) {
|
public PluginLoadedListener(ExtensionClient extensionClient, SchemeManager schemeManager) {
|
||||||
this.extensionClient = extensionClient;
|
this.extensionClient = extensionClient;
|
||||||
|
|
||||||
// TODO Optimize schemes register
|
// TODO Optimize schemes register
|
||||||
Schemes.INSTANCE.register(Plugin.class);
|
schemeManager.register(Plugin.class);
|
||||||
Schemes.INSTANCE.register(ReverseProxy.class);
|
schemeManager.register(ReverseProxy.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,10 @@ public class PolicyRule {
|
||||||
*/
|
*/
|
||||||
String[] verbs;
|
String[] verbs;
|
||||||
|
|
||||||
|
public PolicyRule() {
|
||||||
|
this(null, null, null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
public PolicyRule(String[] apiGroups, String[] resources, String[] resourceNames,
|
public PolicyRule(String[] apiGroups, String[] resources, String[] resourceNames,
|
||||||
String[] nonResourceURLs, String[] verbs) {
|
String[] nonResourceURLs, String[] verbs) {
|
||||||
this.apiGroups = nullElseEmpty(apiGroups);
|
this.apiGroups = nullElseEmpty(apiGroups);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,146 @@
|
||||||
|
package run.halo.app.config;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
|
||||||
|
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.security.test.context.support.WithMockUser;
|
||||||
|
import org.springframework.test.annotation.DirtiesContext;
|
||||||
|
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||||
|
import run.halo.app.extension.FakeExtension;
|
||||||
|
import run.halo.app.extension.Metadata;
|
||||||
|
import run.halo.app.extension.Scheme;
|
||||||
|
import run.halo.app.extension.SchemeManager;
|
||||||
|
import run.halo.app.security.authorization.PolicyRule;
|
||||||
|
import run.halo.app.security.authorization.Role;
|
||||||
|
import run.halo.app.security.authorization.RoleGetter;
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
@AutoConfigureWebTestClient
|
||||||
|
@AutoConfigureTestDatabase
|
||||||
|
class ExtensionConfigurationTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
WebTestClient webClient;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
SchemeManager schemeManager;
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
RoleGetter roleGetter;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
// disable authorization
|
||||||
|
var rule = new PolicyRule();
|
||||||
|
rule.setApiGroups(new String[] {"*"});
|
||||||
|
rule.setResources(new String[] {"*"});
|
||||||
|
rule.setVerbs(new String[] {"*"});
|
||||||
|
var role = new Role();
|
||||||
|
role.setRules(List.of(rule));
|
||||||
|
when(roleGetter.getRole(anyString())).thenReturn(role);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void cleanUp() {
|
||||||
|
schemeManager.fetch(Scheme.buildFromType(FakeExtension.class).groupVersionKind())
|
||||||
|
.ifPresent(schemeManager::unregister);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@WithMockUser
|
||||||
|
void shouldReturnNotFoundWhenSchemeNotRegistered() {
|
||||||
|
webClient.get()
|
||||||
|
.uri("/apis/fake.halo.run/v1alpha1/fakes")
|
||||||
|
.exchange()
|
||||||
|
.expectStatus().isNotFound();
|
||||||
|
|
||||||
|
webClient.get()
|
||||||
|
.uri("/apis/fake.halo.run/v1alpha1/fakes/my-fake")
|
||||||
|
.exchange()
|
||||||
|
.expectStatus().isNotFound();
|
||||||
|
|
||||||
|
webClient.post()
|
||||||
|
.uri("/apis/fake.halo.run/v1alpha1/fakes")
|
||||||
|
.bodyValue(new FakeExtension())
|
||||||
|
.exchange()
|
||||||
|
.expectStatus().isNotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@WithMockUser
|
||||||
|
void shouldListExtensionsWhenSchemeRegistered() {
|
||||||
|
schemeManager.register(FakeExtension.class);
|
||||||
|
|
||||||
|
webClient.get()
|
||||||
|
.uri("/apis/fake.halo.run/v1alpha1/fakes")
|
||||||
|
.exchange()
|
||||||
|
.expectStatus().isOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@WithMockUser
|
||||||
|
@DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD)
|
||||||
|
void shouldCreateExtensionWhenSchemeRegistered() {
|
||||||
|
schemeManager.register(FakeExtension.class);
|
||||||
|
|
||||||
|
getCreateExtensionResponse()
|
||||||
|
.expectStatus().isOk()
|
||||||
|
.expectBody(FakeExtension.class)
|
||||||
|
.consumeWith(result -> {
|
||||||
|
var gotFake = result.getResponseBody();
|
||||||
|
assertNotNull(gotFake);
|
||||||
|
assertEquals("my-fake", gotFake.getMetadata().getName());
|
||||||
|
assertNotNull(gotFake.getMetadata().getVersion());
|
||||||
|
assertNotNull(gotFake.getMetadata().getCreationTimestamp());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@WithMockUser
|
||||||
|
@DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD)
|
||||||
|
void shouldGetExtensionWhenSchemeRegistered() {
|
||||||
|
schemeManager.register(FakeExtension.class);
|
||||||
|
|
||||||
|
// create the Extension
|
||||||
|
getCreateExtensionResponse().expectStatus().isOk();
|
||||||
|
|
||||||
|
webClient.get()
|
||||||
|
.uri("/apis/fake.halo.run/v1alpha1/fakes/my-fake")
|
||||||
|
.exchange()
|
||||||
|
.expectStatus().isOk()
|
||||||
|
.expectBody(FakeExtension.class)
|
||||||
|
.consumeWith(result -> {
|
||||||
|
var gotFake = result.getResponseBody();
|
||||||
|
assertNotNull(gotFake);
|
||||||
|
assertEquals("my-fake", gotFake.getMetadata().getName());
|
||||||
|
assertNotNull(gotFake.getMetadata().getVersion());
|
||||||
|
assertNotNull(gotFake.getMetadata().getCreationTimestamp());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -9,6 +9,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.anyString;
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.lenient;
|
||||||
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.times;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
@ -16,7 +17,7 @@ import static org.mockito.Mockito.when;
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import org.junit.jupiter.api.BeforeAll;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.mockito.InjectMocks;
|
import org.mockito.InjectMocks;
|
||||||
|
|
@ -37,12 +38,16 @@ class DefaultExtensionClientTest {
|
||||||
@Mock
|
@Mock
|
||||||
ExtensionConverter converter;
|
ExtensionConverter converter;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
SchemeManager schemeManager;
|
||||||
|
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
DefaultExtensionClient client;
|
DefaultExtensionClient client;
|
||||||
|
|
||||||
@BeforeAll
|
@BeforeEach
|
||||||
static void before() {
|
void setUp() {
|
||||||
Schemes.INSTANCE.register(FakeExtension.class);
|
lenient().when(schemeManager.get(eq(FakeExtension.class)))
|
||||||
|
.thenReturn(Scheme.buildFromType(FakeExtension.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
FakeExtension createFakeExtension(String name, Long version) {
|
FakeExtension createFakeExtension(String name, Long version) {
|
||||||
|
|
@ -93,6 +98,9 @@ class DefaultExtensionClientTest {
|
||||||
class UnRegisteredExtension extends AbstractExtension {
|
class UnRegisteredExtension extends AbstractExtension {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
when(schemeManager.get(eq(UnRegisteredExtension.class)))
|
||||||
|
.thenThrow(SchemeNotFoundException.class);
|
||||||
|
|
||||||
assertThrows(SchemeNotFoundException.class,
|
assertThrows(SchemeNotFoundException.class,
|
||||||
() -> client.list(UnRegisteredExtension.class, null, null));
|
() -> client.list(UnRegisteredExtension.class, null, null));
|
||||||
assertThrows(SchemeNotFoundException.class,
|
assertThrows(SchemeNotFoundException.class,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,120 @@
|
||||||
|
package run.halo.app.extension;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.isA;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import run.halo.app.extension.SchemeWatcherManager.SchemeRegistered;
|
||||||
|
import run.halo.app.extension.SchemeWatcherManager.SchemeUnregistered;
|
||||||
|
import run.halo.app.extension.SchemeWatcherManager.SchemeWatcher;
|
||||||
|
import run.halo.app.extension.exception.ExtensionException;
|
||||||
|
import run.halo.app.extension.exception.SchemeNotFoundException;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class DefaultSchemeManagerTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
SchemeWatcherManager watcherManager;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
DefaultSchemeManager schemeManager;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldThrowExceptionWhenNoGvkAnnotation() {
|
||||||
|
class WithoutGvkExtension extends AbstractExtension {
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThrows(ExtensionException.class,
|
||||||
|
() -> schemeManager.register(WithoutGvkExtension.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldGetNothingWhenUnregistered() {
|
||||||
|
final var gvk = new GroupVersionKind("fake.halo.run", "v1alpha1", "Fake");
|
||||||
|
var scheme = schemeManager.fetch(gvk);
|
||||||
|
assertFalse(scheme.isPresent());
|
||||||
|
|
||||||
|
assertThrows(SchemeNotFoundException.class, () -> schemeManager.get(gvk));
|
||||||
|
assertThrows(SchemeNotFoundException.class, () -> schemeManager.get(FakeExtension.class));
|
||||||
|
assertThrows(SchemeNotFoundException.class, () -> schemeManager.get(new FakeExtension()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldGetSchemeWhenRegistered() {
|
||||||
|
schemeManager.register(FakeExtension.class);
|
||||||
|
final var gvk = new GroupVersionKind("fake.halo.run", "v1alpha1", "Fake");
|
||||||
|
var scheme = schemeManager.fetch(gvk);
|
||||||
|
assertTrue(scheme.isPresent());
|
||||||
|
|
||||||
|
assertEquals(gvk, schemeManager.get(gvk).groupVersionKind());
|
||||||
|
assertEquals(gvk, schemeManager.get(FakeExtension.class).groupVersionKind());
|
||||||
|
assertEquals(gvk, schemeManager.get(new FakeExtension()).groupVersionKind());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldUnregisterSuccessfully() {
|
||||||
|
schemeManager.register(FakeExtension.class);
|
||||||
|
Scheme scheme = schemeManager.get(FakeExtension.class);
|
||||||
|
assertNotNull(scheme);
|
||||||
|
|
||||||
|
schemeManager.unregister(scheme);
|
||||||
|
assertThrows(SchemeNotFoundException.class, () -> schemeManager.get(FakeExtension.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldTriggerOnChangeOnlyOnceWhenRegisterTwice() {
|
||||||
|
final var watcher = mock(SchemeWatcher.class);
|
||||||
|
when(watcherManager.watchers()).thenReturn(List.of(watcher));
|
||||||
|
|
||||||
|
schemeManager.register(FakeExtension.class);
|
||||||
|
verify(watcherManager, times(1)).watchers();
|
||||||
|
verify(watcher, times(1)).onChange(isA(SchemeRegistered.class));
|
||||||
|
|
||||||
|
schemeManager.register(FakeExtension.class);
|
||||||
|
verify(watcherManager, times(1)).watchers();
|
||||||
|
verify(watcher, times(1)).onChange(isA(SchemeRegistered.class));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldTriggerOnChangeOnlyOnceWhenUnregisterTwice() {
|
||||||
|
|
||||||
|
final var watcher = mock(SchemeWatcher.class);
|
||||||
|
when(watcherManager.watchers()).thenReturn(List.of(watcher));
|
||||||
|
|
||||||
|
schemeManager.register(FakeExtension.class);
|
||||||
|
|
||||||
|
var scheme = schemeManager.get(FakeExtension.class);
|
||||||
|
|
||||||
|
schemeManager.unregister(scheme);
|
||||||
|
verify(watcherManager, times(2)).watchers();
|
||||||
|
verify(watcher, times(1)).onChange(isA(SchemeUnregistered.class));
|
||||||
|
|
||||||
|
schemeManager.unregister(scheme);
|
||||||
|
verify(watcherManager, times(2)).watchers();
|
||||||
|
verify(watcher, times(1)).onChange(isA(SchemeUnregistered.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getSizeOfSchemes() {
|
||||||
|
assertEquals(0, schemeManager.size());
|
||||||
|
schemeManager.register(FakeExtension.class);
|
||||||
|
assertEquals(1, schemeManager.size());
|
||||||
|
schemeManager.unregister(schemeManager.get(FakeExtension.class));
|
||||||
|
assertEquals(0, schemeManager.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
package run.halo.app.extension;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import run.halo.app.extension.SchemeWatcherManager.SchemeWatcher;
|
||||||
|
|
||||||
|
class DefaultSchemeWatcherManagerTest {
|
||||||
|
|
||||||
|
DefaultSchemeWatcherManager watcherManager;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
watcherManager = new DefaultSchemeWatcherManager();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldThrowExceptionWhenRegisterNullWatcher() {
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> watcherManager.register(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldThrowExceptionWhenUnregisterNullWatcher() {
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> watcherManager.unregister(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRegisterSuccessfully() {
|
||||||
|
var watcher = mock(SchemeWatcher.class);
|
||||||
|
watcherManager.register(watcher);
|
||||||
|
|
||||||
|
assertEquals(watcherManager.watchers(), List.of(watcher));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldUnregisterSuccessfully() {
|
||||||
|
var watcher = mock(SchemeWatcher.class);
|
||||||
|
watcherManager.register(watcher);
|
||||||
|
assertEquals(List.of(watcher), watcherManager.watchers());
|
||||||
|
|
||||||
|
watcherManager.unregister(watcher);
|
||||||
|
assertEquals(Collections.emptyList(), watcherManager.watchers());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnCopyOfWatchers() {
|
||||||
|
var watcher = mock(SchemeWatcher.class);
|
||||||
|
watcherManager.register(watcher);
|
||||||
|
assertEquals(List.of(watcher), watcherManager.watchers());
|
||||||
|
|
||||||
|
var watchersBeforeRegister = watcherManager.watchers();
|
||||||
|
watcherManager.unregister(watcher);
|
||||||
|
|
||||||
|
// watchers are not changed even if unregistered
|
||||||
|
assertEquals(List.of(watcher), watchersBeforeRegister);
|
||||||
|
assertEquals(Collections.emptyList(), watcherManager.watchers());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
package run.halo.app.extension;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
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.SchemeWatcherManager.SchemeRegistered;
|
||||||
|
import run.halo.app.extension.SchemeWatcherManager.SchemeUnregistered;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class ExtensionCompositeRouterFunctionTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
ExtensionClient client;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRouteWhenSchemeRegistered() {
|
||||||
|
var extensionRouterFunc = new ExtensionCompositeRouterFunction(client, null);
|
||||||
|
|
||||||
|
var exchange = MockServerWebExchange.from(
|
||||||
|
MockServerHttpRequest.get("/apis/fake.halo.run/v1alpha1/fakes").build());
|
||||||
|
|
||||||
|
var messageReaders = HandlerStrategies.withDefaults().messageReaders();
|
||||||
|
ServerRequest request = ServerRequest.create(exchange, messageReaders);
|
||||||
|
|
||||||
|
var handlerFunc = extensionRouterFunc.route(request).block();
|
||||||
|
assertNull(handlerFunc);
|
||||||
|
|
||||||
|
// trigger registering scheme
|
||||||
|
extensionRouterFunc.onChange(
|
||||||
|
new SchemeRegistered(Scheme.buildFromType(FakeExtension.class)));
|
||||||
|
|
||||||
|
handlerFunc = extensionRouterFunc.route(request).block();
|
||||||
|
assertNotNull(handlerFunc);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotRouteWhenSchemeUnregistered() {
|
||||||
|
var extensionRouterFunc = new ExtensionCompositeRouterFunction(client, null);
|
||||||
|
|
||||||
|
var exchange = MockServerWebExchange.from(
|
||||||
|
MockServerHttpRequest.get("/apis/fake.halo.run/v1alpha1/fakes").build());
|
||||||
|
|
||||||
|
var messageReaders = HandlerStrategies.withDefaults().messageReaders();
|
||||||
|
|
||||||
|
// trigger registering scheme
|
||||||
|
extensionRouterFunc.onChange(
|
||||||
|
new SchemeRegistered(Scheme.buildFromType(FakeExtension.class)));
|
||||||
|
|
||||||
|
ServerRequest request = ServerRequest.create(exchange, messageReaders);
|
||||||
|
var handlerFunc = extensionRouterFunc.route(request).block();
|
||||||
|
assertNotNull(handlerFunc);
|
||||||
|
|
||||||
|
// trigger registering scheme
|
||||||
|
extensionRouterFunc.onChange(
|
||||||
|
new SchemeUnregistered(Scheme.buildFromType(FakeExtension.class)));
|
||||||
|
handlerFunc = extensionRouterFunc.route(request).block();
|
||||||
|
assertNull(handlerFunc);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRegisterWatcherIfWatcherManagerIsNotNull() {
|
||||||
|
var watcherManager = mock(SchemeWatcherManager.class);
|
||||||
|
var routerFunction = new ExtensionCompositeRouterFunction(client, watcherManager);
|
||||||
|
verify(watcherManager, times(1)).register(eq(routerFunction));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
package run.halo.app.extension;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
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.exception.ExtensionConvertException;
|
||||||
|
import run.halo.app.extension.exception.ExtensionNotFoundException;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class ExtensionCreateHandlerTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
ExtensionClient client;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldBuildPathPatternCorrectly() {
|
||||||
|
var scheme = Scheme.buildFromType(FakeExtension.class);
|
||||||
|
var getHandler = new ExtensionCreateHandler(scheme, client);
|
||||||
|
var pathPattern = getHandler.pathPattern();
|
||||||
|
assertEquals("/apis/fake.halo.run/v1alpha1/fakes", pathPattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldHandleCorrectly() {
|
||||||
|
final var fake = new FakeExtension();
|
||||||
|
var metadata = new Metadata();
|
||||||
|
metadata.setName("my-fake");
|
||||||
|
fake.setMetadata(metadata);
|
||||||
|
|
||||||
|
var unstructured = new Unstructured();
|
||||||
|
unstructured.setMetadata(metadata);
|
||||||
|
unstructured.setApiVersion("fake.halo.run/v1alpha1");
|
||||||
|
unstructured.setKind("Fake");
|
||||||
|
|
||||||
|
var serverRequest = MockServerRequest.builder()
|
||||||
|
.body(Mono.just(unstructured));
|
||||||
|
when(client.fetch(eq(FakeExtension.class), eq("my-fake"))).thenReturn(Optional.of(fake));
|
||||||
|
|
||||||
|
var scheme = Scheme.buildFromType(FakeExtension.class);
|
||||||
|
var getHandler = new ExtensionCreateHandler(scheme, client);
|
||||||
|
var responseMono = getHandler.handle(serverRequest);
|
||||||
|
|
||||||
|
StepVerifier.create(responseMono)
|
||||||
|
.consumeNextWith(response -> {
|
||||||
|
assertEquals(HttpStatus.OK, response.statusCode());
|
||||||
|
assertEquals(MediaType.APPLICATION_JSON, response.headers().getContentType());
|
||||||
|
assertTrue(response instanceof EntityResponse<?>);
|
||||||
|
assertEquals(fake, ((EntityResponse<?>) response).entity());
|
||||||
|
})
|
||||||
|
.verifyComplete();
|
||||||
|
verify(client, times(1)).fetch(eq(FakeExtension.class), eq("my-fake"));
|
||||||
|
verify(client, times(1)).create(eq(unstructured));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnErrorWhenNoBodyProvided() {
|
||||||
|
var serverRequest = MockServerRequest.builder()
|
||||||
|
.body(Mono.empty());
|
||||||
|
var scheme = Scheme.buildFromType(FakeExtension.class);
|
||||||
|
var getHandler = new ExtensionCreateHandler(scheme, client);
|
||||||
|
var responseMono = getHandler.handle(serverRequest);
|
||||||
|
StepVerifier.create(responseMono)
|
||||||
|
.verifyError(ExtensionConvertException.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnErrorWhenExtensionNotFound() {
|
||||||
|
final var unstructured = new Unstructured();
|
||||||
|
var metadata = new Metadata();
|
||||||
|
metadata.setName("my-fake");
|
||||||
|
unstructured.setMetadata(metadata);
|
||||||
|
unstructured.setApiVersion("fake.halo.run/v1alpha1");
|
||||||
|
unstructured.setKind("Fake");
|
||||||
|
|
||||||
|
var serverRequest = MockServerRequest.builder()
|
||||||
|
.body(Mono.just(unstructured));
|
||||||
|
when(client.fetch(eq(FakeExtension.class), eq("my-fake"))).thenReturn(Optional.empty());
|
||||||
|
|
||||||
|
var scheme = Scheme.buildFromType(FakeExtension.class);
|
||||||
|
var getHandler = new ExtensionCreateHandler(scheme, client);
|
||||||
|
var responseMono = getHandler.handle(serverRequest);
|
||||||
|
|
||||||
|
StepVerifier.create(responseMono)
|
||||||
|
.verifyError(ExtensionNotFoundException.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
package run.halo.app.extension;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
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.exception.ExtensionNotFoundException;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class ExtensionGetHandlerTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
ExtensionClient client;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldBuildPathPatternCorrectly() {
|
||||||
|
var scheme = Scheme.buildFromType(FakeExtension.class);
|
||||||
|
var getHandler = new ExtensionGetHandler(scheme, client);
|
||||||
|
var pathPattern = getHandler.pathPattern();
|
||||||
|
assertEquals("/apis/fake.halo.run/v1alpha1/fakes/{name}", pathPattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldHandleCorrectly() {
|
||||||
|
var scheme = Scheme.buildFromType(FakeExtension.class);
|
||||||
|
var getHandler = new ExtensionGetHandler(scheme, client);
|
||||||
|
var serverRequest = MockServerRequest.builder()
|
||||||
|
.pathVariable("name", "my-fake")
|
||||||
|
.build();
|
||||||
|
final var fake = new FakeExtension();
|
||||||
|
when(client.fetch(eq(FakeExtension.class), eq("my-fake"))).thenReturn(Optional.of(fake));
|
||||||
|
|
||||||
|
var responseMono = getHandler.handle(serverRequest);
|
||||||
|
|
||||||
|
StepVerifier.create(responseMono)
|
||||||
|
.consumeNextWith(response -> {
|
||||||
|
assertEquals(HttpStatus.OK, response.statusCode());
|
||||||
|
assertEquals(MediaType.APPLICATION_JSON, response.headers().getContentType());
|
||||||
|
assertTrue(response instanceof EntityResponse<?>);
|
||||||
|
assertEquals(fake, ((EntityResponse<?>) response).entity());
|
||||||
|
})
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldThrowExceptionWhenExtensionNotFound() {
|
||||||
|
var scheme = Scheme.buildFromType(FakeExtension.class);
|
||||||
|
var getHandler = new ExtensionGetHandler(scheme, client);
|
||||||
|
var serverRequest = MockServerRequest.builder()
|
||||||
|
.pathVariable("name", "my-fake")
|
||||||
|
.build();
|
||||||
|
when(client.fetch(eq(FakeExtension.class), eq("my-fake"))).thenReturn(Optional.empty());
|
||||||
|
|
||||||
|
assertThrows(ExtensionNotFoundException.class, () -> getHandler.handle(serverRequest));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
package run.halo.app.extension;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
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;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class ExtensionListHandlerTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
ExtensionClient client;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldBuildPathPatternCorrectly() {
|
||||||
|
var scheme = Scheme.buildFromType(FakeExtension.class);
|
||||||
|
var getHandler = new ExtensionListHandler(scheme, client);
|
||||||
|
var pathPattern = getHandler.pathPattern();
|
||||||
|
assertEquals("/apis/fake.halo.run/v1alpha1/fakes", pathPattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldHandleCorrectly() {
|
||||||
|
var scheme = Scheme.buildFromType(FakeExtension.class);
|
||||||
|
var getHandler = new ExtensionListHandler(scheme, client);
|
||||||
|
var serverRequest = MockServerRequest.builder().build();
|
||||||
|
final var fake = new FakeExtension();
|
||||||
|
when(client.list(eq(FakeExtension.class), any(), any())).thenReturn(List.of(fake));
|
||||||
|
|
||||||
|
var responseMono = getHandler.handle(serverRequest);
|
||||||
|
|
||||||
|
StepVerifier.create(responseMono)
|
||||||
|
.consumeNextWith(response -> {
|
||||||
|
assertEquals(HttpStatus.OK, response.statusCode());
|
||||||
|
assertEquals(MediaType.APPLICATION_JSON, response.headers().getContentType());
|
||||||
|
assertTrue(response instanceof EntityResponse<?>);
|
||||||
|
assertEquals(List.of(fake), ((EntityResponse<?>) response).entity());
|
||||||
|
})
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
package run.halo.app.extension;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.http.codec.HttpMessageReader;
|
||||||
|
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||||
|
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||||
|
import org.springframework.web.reactive.function.server.HandlerFunction;
|
||||||
|
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;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class ExtensionRouterFunctionFactoryTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
ExtensionClient client;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCreateSuccessfully() {
|
||||||
|
var scheme = Scheme.buildFromType(FakeExtension.class);
|
||||||
|
var factory = new ExtensionRouterFunctionFactory(scheme, client);
|
||||||
|
|
||||||
|
var routerFunction = factory.create();
|
||||||
|
|
||||||
|
testCases().forEach(testCase -> {
|
||||||
|
List<HttpMessageReader<?>> messageReaders =
|
||||||
|
HandlerStrategies.withDefaults().messageReaders();
|
||||||
|
var request = ServerRequest.create(testCase.webExchange, messageReaders);
|
||||||
|
var handlerFunc = routerFunction.route(request).block();
|
||||||
|
assertInstanceOf(testCase.expectHandlerType, handlerFunc);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
List<TestCase> testCases() {
|
||||||
|
var listWebExchange = MockServerWebExchange.from(
|
||||||
|
MockServerHttpRequest.get("/apis/fake.halo.run/v1alpha1/fakes").build());
|
||||||
|
|
||||||
|
var getWebExchange = MockServerWebExchange.from(
|
||||||
|
MockServerHttpRequest.get("/apis/fake.halo.run/v1alpha1/fakes/my-fake").build()
|
||||||
|
);
|
||||||
|
|
||||||
|
var createWebExchange = MockServerWebExchange.from(
|
||||||
|
MockServerHttpRequest.post("/apis/fake.halo.run/v1alpha1/fakes").body("{}")
|
||||||
|
);
|
||||||
|
|
||||||
|
return List.of(
|
||||||
|
new TestCase(listWebExchange, ListHandler.class),
|
||||||
|
new TestCase(getWebExchange, GetHandler.class),
|
||||||
|
new TestCase(createWebExchange, CreateHandler.class)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
record TestCase(ServerWebExchange webExchange,
|
||||||
|
Class<? extends HandlerFunction<ServerResponse>> expectHandlerType) {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -5,5 +5,5 @@ package run.halo.app.extension;
|
||||||
kind = "Fake",
|
kind = "Fake",
|
||||||
plural = "fakes",
|
plural = "fakes",
|
||||||
singular = "fake")
|
singular = "fake")
|
||||||
class FakeExtension extends AbstractExtension {
|
public class FakeExtension extends AbstractExtension {
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,31 +6,25 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import org.junit.jupiter.api.BeforeAll;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
|
||||||
import run.halo.app.extension.exception.ExtensionConvertException;
|
import run.halo.app.extension.exception.ExtensionConvertException;
|
||||||
import run.halo.app.extension.exception.SchemaViolationException;
|
import run.halo.app.extension.exception.SchemaViolationException;
|
||||||
import run.halo.app.extension.store.ExtensionStore;
|
import run.halo.app.extension.store.ExtensionStore;
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
|
||||||
class JSONExtensionConverterTest {
|
class JSONExtensionConverterTest {
|
||||||
|
|
||||||
JSONExtensionConverter converter;
|
JSONExtensionConverter converter;
|
||||||
|
|
||||||
ObjectMapper objectMapper;
|
ObjectMapper objectMapper;
|
||||||
|
|
||||||
@BeforeAll
|
|
||||||
static void beforeAll() {
|
|
||||||
Schemes.INSTANCE.register(FakeExtension.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
objectMapper = new ObjectMapper();
|
DefaultSchemeManager schemeManager = new DefaultSchemeManager(null);
|
||||||
converter = new JSONExtensionConverter(objectMapper);
|
converter = new JSONExtensionConverter(schemeManager);
|
||||||
|
objectMapper = JSONExtensionConverter.OBJECT_MAPPER;
|
||||||
|
|
||||||
|
schemeManager.register(FakeExtension.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
package run.halo.app.extension;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
import static run.halo.app.extension.MetadataOperator.metadataDeepEquals;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Map;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class MetadataOperatorTest {
|
||||||
|
|
||||||
|
Instant now = Instant.now();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testMetadataDeepEqualsWithSameType() {
|
||||||
|
assertTrue(metadataDeepEquals(null, null));
|
||||||
|
|
||||||
|
var left = createFullMetadata();
|
||||||
|
var right = createFullMetadata();
|
||||||
|
assertFalse(metadataDeepEquals(left, null));
|
||||||
|
assertFalse(metadataDeepEquals(null, right));
|
||||||
|
assertTrue(metadataDeepEquals(left, right));
|
||||||
|
|
||||||
|
left.setDeletionTimestamp(null);
|
||||||
|
assertFalse(metadataDeepEquals(left, right));
|
||||||
|
right.setDeletionTimestamp(null);
|
||||||
|
assertTrue(metadataDeepEquals(left, right));
|
||||||
|
|
||||||
|
left.setCreationTimestamp(null);
|
||||||
|
assertFalse(metadataDeepEquals(left, right));
|
||||||
|
right.setCreationTimestamp(null);
|
||||||
|
assertTrue(metadataDeepEquals(left, right));
|
||||||
|
|
||||||
|
left.setVersion(null);
|
||||||
|
assertFalse(metadataDeepEquals(left, right));
|
||||||
|
right.setVersion(null);
|
||||||
|
assertTrue(metadataDeepEquals(left, right));
|
||||||
|
|
||||||
|
left.setAnnotations(null);
|
||||||
|
assertFalse(metadataDeepEquals(left, right));
|
||||||
|
right.setAnnotations(null);
|
||||||
|
assertTrue(metadataDeepEquals(left, right));
|
||||||
|
|
||||||
|
left.setLabels(null);
|
||||||
|
assertFalse(metadataDeepEquals(left, right));
|
||||||
|
right.setLabels(null);
|
||||||
|
assertTrue(metadataDeepEquals(left, right));
|
||||||
|
|
||||||
|
left.setName(null);
|
||||||
|
assertFalse(metadataDeepEquals(left, right));
|
||||||
|
right.setName(null);
|
||||||
|
assertTrue(metadataDeepEquals(left, right));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testMetadataDeepEqualsWithDifferentType() {
|
||||||
|
var mockMetadata = mock(MetadataOperator.class);
|
||||||
|
when(mockMetadata.getName()).thenReturn("fake-name");
|
||||||
|
when(mockMetadata.getLabels()).thenReturn(Map.of("fake-label-key", "fake-label-value"));
|
||||||
|
when(mockMetadata.getAnnotations()).thenReturn(Map.of("fake-anno-key", "fake-anno-value"));
|
||||||
|
when(mockMetadata.getVersion()).thenReturn(123L);
|
||||||
|
when(mockMetadata.getCreationTimestamp()).thenReturn(now);
|
||||||
|
when(mockMetadata.getDeletionTimestamp()).thenReturn(now);
|
||||||
|
|
||||||
|
var metadata = createFullMetadata();
|
||||||
|
assertTrue(metadataDeepEquals(metadata, mockMetadata));
|
||||||
|
}
|
||||||
|
|
||||||
|
Metadata createFullMetadata() {
|
||||||
|
var metadata = new Metadata();
|
||||||
|
metadata.setName("fake-name");
|
||||||
|
metadata.setLabels(Map.of("fake-label-key", "fake-label-value"));
|
||||||
|
metadata.setAnnotations(Map.of("fake-anno-key", "fake-anno-value"));
|
||||||
|
metadata.setVersion(123L);
|
||||||
|
metadata.setCreationTimestamp(now);
|
||||||
|
metadata.setDeletionTimestamp(now);
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
package run.halo.app.extension;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import run.halo.app.extension.ExtensionRouterFunctionFactory.PathPatternGenerator;
|
||||||
|
|
||||||
|
class PathPatternGeneratorTest {
|
||||||
|
|
||||||
|
@GVK(group = "fake.halo.run", version = "v1alpha1", kind = "Fake",
|
||||||
|
singular = "fake", plural = "fakes")
|
||||||
|
private static class GroupExtension extends AbstractExtension {
|
||||||
|
}
|
||||||
|
|
||||||
|
@GVK(group = "", version = "v1alpha1", kind = "Fake",
|
||||||
|
singular = "fake", plural = "fakes")
|
||||||
|
private static class GrouplessExtension extends AbstractExtension {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void buildGroupedExtensionPathPattern() {
|
||||||
|
var scheme = Scheme.buildFromType(GroupExtension.class);
|
||||||
|
var pathPattern = PathPatternGenerator.buildExtensionPathPattern(scheme);
|
||||||
|
assertEquals("/apis/fake.halo.run/v1alpha1/fakes", pathPattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void buildGrouplessExtensionPathPattern() {
|
||||||
|
var scheme = Scheme.buildFromType(GrouplessExtension.class);
|
||||||
|
var pathPattern = PathPatternGenerator.buildExtensionPathPattern(scheme);
|
||||||
|
assertEquals("/api/v1alpha1/fakes", pathPattern);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
package run.halo.app.extension;
|
package run.halo.app.extension;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import run.halo.app.extension.exception.ExtensionException;
|
||||||
|
|
||||||
class SchemeTest {
|
class SchemeTest {
|
||||||
|
|
||||||
|
|
@ -31,4 +34,37 @@ class SchemeTest {
|
||||||
"fake", new ObjectNode(null));
|
"fake", new ObjectNode(null));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldThrowExceptionWhenTypeHasNoGvkAnno() {
|
||||||
|
class NoGvkExtension extends AbstractExtension {
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThrows(ExtensionException.class,
|
||||||
|
() -> Scheme.getGvkFromType(NoGvkExtension.class));
|
||||||
|
assertThrows(ExtensionException.class,
|
||||||
|
() -> Scheme.buildFromType(NoGvkExtension.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldGetGvkFromTypeWithGvkAnno() {
|
||||||
|
var gvk = Scheme.getGvkFromType(FakeExtension.class);
|
||||||
|
assertEquals("fake.halo.run", gvk.group());
|
||||||
|
assertEquals("v1alpha1", gvk.version());
|
||||||
|
assertEquals("Fake", gvk.kind());
|
||||||
|
assertEquals("fake", gvk.singular());
|
||||||
|
assertEquals("fakes", gvk.plural());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCreateSchemeSuccessfully() {
|
||||||
|
var scheme = Scheme.buildFromType(FakeExtension.class);
|
||||||
|
assertEquals(new GroupVersionKind("fake.halo.run", "v1alpha1", "Fake"),
|
||||||
|
scheme.groupVersionKind());
|
||||||
|
assertEquals("fake", scheme.singular());
|
||||||
|
assertEquals("fakes", scheme.plural());
|
||||||
|
assertNotNull(scheme.jsonSchema());
|
||||||
|
assertEquals(FakeExtension.class, scheme.type());
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
package run.halo.app.extension;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
||||||
import static run.halo.app.extension.GroupVersionKind.fromAPIVersionAndKind;
|
|
||||||
|
|
||||||
import java.util.Optional;
|
|
||||||
import org.junit.jupiter.api.AfterEach;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import run.halo.app.extension.exception.ExtensionException;
|
|
||||||
import run.halo.app.extension.exception.SchemeNotFoundException;
|
|
||||||
|
|
||||||
class SchemesTest {
|
|
||||||
|
|
||||||
|
|
||||||
@AfterEach
|
|
||||||
void cleanUp() {
|
|
||||||
Schemes.INSTANCE.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testRegister() {
|
|
||||||
Schemes.INSTANCE.register(FakeExtension.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldThrowExceptionWithoutGVKAnnotation() {
|
|
||||||
class WithoutGVKExtension extends AbstractExtension {
|
|
||||||
}
|
|
||||||
|
|
||||||
assertThrows(ExtensionException.class,
|
|
||||||
() -> Schemes.INSTANCE.register(WithoutGVKExtension.class));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldFetchNothingWhenUnregistered() {
|
|
||||||
var scheme = Schemes.INSTANCE.fetch(FakeExtension.class);
|
|
||||||
assertEquals(Optional.empty(), scheme);
|
|
||||||
assertThrows(SchemeNotFoundException.class,
|
|
||||||
() -> Schemes.INSTANCE.get(FakeExtension.class));
|
|
||||||
|
|
||||||
var gvk = fromAPIVersionAndKind("fake.halo.run/v1alpha1", "Fake");
|
|
||||||
scheme = Schemes.INSTANCE.fetch(gvk);
|
|
||||||
assertEquals(Optional.empty(), scheme);
|
|
||||||
assertThrows(SchemeNotFoundException.class, () -> Schemes.INSTANCE.get(gvk));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldFetchFakeWhenRegistered() {
|
|
||||||
Schemes.INSTANCE.register(FakeExtension.class);
|
|
||||||
|
|
||||||
var scheme = Schemes.INSTANCE.fetch(FakeExtension.class);
|
|
||||||
assertTrue(scheme.isPresent());
|
|
||||||
|
|
||||||
scheme = Schemes.INSTANCE.fetch(fromAPIVersionAndKind("fake.halo.run/v1alpha1", "Fake"));
|
|
||||||
assertTrue(scheme.isPresent());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,14 +1,16 @@
|
||||||
package run.halo.app.extension;
|
package run.halo.app.extension;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import static run.halo.app.extension.MetadataOperator.metadataDeepEquals;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import org.junit.jupiter.api.BeforeAll;
|
import org.json.JSONException;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.skyscreamer.jsonassert.JSONAssert;
|
||||||
|
|
||||||
class UnstructuredTest {
|
class UnstructuredTest {
|
||||||
|
|
||||||
|
|
@ -30,25 +32,36 @@ class UnstructuredTest {
|
||||||
}
|
}
|
||||||
""";
|
""";
|
||||||
|
|
||||||
@BeforeAll
|
|
||||||
static void setUpGlobally() {
|
|
||||||
Schemes.INSTANCE.register(FakeExtension.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldSerializeCorrectly() throws JsonProcessingException {
|
void shouldSerializeCorrectly() throws JsonProcessingException {
|
||||||
var extensionNode = (ObjectNode) objectMapper.readTree(extensionJson);
|
Map extensionMap = objectMapper.readValue(extensionJson, Map.class);
|
||||||
var extension = new Unstructured(extensionNode);
|
var extension = new Unstructured(extensionMap);
|
||||||
|
|
||||||
var gotNode = objectMapper.valueToTree(extension);
|
var gotNode = objectMapper.valueToTree(extension);
|
||||||
assertEquals(extensionNode, gotNode);
|
assertEquals(objectMapper.readTree(extensionJson), gotNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldDeserializeCorrectly() throws JsonProcessingException {
|
void shouldSetCreationTimestamp() throws JsonProcessingException, JSONException {
|
||||||
|
Map extensionMap = objectMapper.readValue(extensionJson, Map.class);
|
||||||
|
var extension = new Unstructured(extensionMap);
|
||||||
|
|
||||||
|
System.out.println(objectMapper.writeValueAsString(extension));
|
||||||
|
var beforeChange = objectMapper.writeValueAsString(extension);
|
||||||
|
|
||||||
|
var metadata = extension.getMetadata();
|
||||||
|
metadata.setCreationTimestamp(metadata.getCreationTimestamp());
|
||||||
|
|
||||||
|
var afterChange = objectMapper.writeValueAsString(extension);
|
||||||
|
|
||||||
|
JSONAssert.assertEquals(beforeChange, afterChange, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldDeserializeCorrectly() throws JsonProcessingException, JSONException {
|
||||||
var extension = objectMapper.readValue(extensionJson, Unstructured.class);
|
var extension = objectMapper.readValue(extensionJson, Unstructured.class);
|
||||||
var wantJsonNode = objectMapper.readTree(extensionJson);
|
var gotJson = objectMapper.writeValueAsString(extension);
|
||||||
assertEquals(wantJsonNode, extension.getExtension());
|
JSONAssert.assertEquals(extensionJson, gotJson, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -57,7 +70,7 @@ class UnstructuredTest {
|
||||||
|
|
||||||
assertEquals("fake.halo.run/v1alpha1", extension.getApiVersion());
|
assertEquals("fake.halo.run/v1alpha1", extension.getApiVersion());
|
||||||
assertEquals("Fake", extension.getKind());
|
assertEquals("Fake", extension.getKind());
|
||||||
assertEquals(createMetadata(), extension.getMetadata());
|
metadataDeepEquals(createMetadata(), extension.getMetadata());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -69,7 +82,7 @@ class UnstructuredTest {
|
||||||
|
|
||||||
assertEquals("fake.halo.run/v1alpha1", extension.getApiVersion());
|
assertEquals("fake.halo.run/v1alpha1", extension.getApiVersion());
|
||||||
assertEquals("Fake", extension.getKind());
|
assertEquals("Fake", extension.getKind());
|
||||||
assertEquals(createMetadata(), extension.getMetadata());
|
assertTrue(metadataDeepEquals(createMetadata(), extension.getMetadata()));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Metadata createMetadata() {
|
private Metadata createMetadata() {
|
||||||
|
|
@ -81,4 +94,4 @@ class UnstructuredTest {
|
||||||
return metadata;
|
return metadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ class YamlPluginFinderTest {
|
||||||
"requires": ">=2.0.0",
|
"requires": ">=2.0.0",
|
||||||
"pluginClass": "run.halo.app.plugin.BasePlugin"
|
"pluginClass": "run.halo.app.plugin.BasePlugin"
|
||||||
},
|
},
|
||||||
"apiVersion": "v1",
|
"apiVersion": "plugin.halo.run/v1alpha1",
|
||||||
"kind": "Plugin",
|
"kind": "Plugin",
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"name": "plugin-1",
|
"name": "plugin-1",
|
||||||
|
|
@ -72,7 +72,7 @@ class YamlPluginFinderTest {
|
||||||
}
|
}
|
||||||
""",
|
""",
|
||||||
JsonUtils.objectToJson(plugin),
|
JsonUtils.objectToJson(plugin),
|
||||||
false);
|
true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -154,7 +154,7 @@ class YamlPluginFinderTest {
|
||||||
void deserializeLicense() throws JSONException, JsonProcessingException {
|
void deserializeLicense() throws JSONException, JsonProcessingException {
|
||||||
String pluginJson = """
|
String pluginJson = """
|
||||||
{
|
{
|
||||||
"apiVersion": "v1",
|
"apiVersion": "plugin.halo.run/v1alpha1",
|
||||||
"kind": "Plugin",
|
"kind": "Plugin",
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"name": "plugin-1"
|
"name": "plugin-1"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue