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;
|
||||
|
||||
@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.PageRequest;
|
||||
import org.springframework.data.support.PageableExecutionUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.Assert;
|
||||
import run.halo.app.extension.store.ExtensionStore;
|
||||
import run.halo.app.extension.store.ExtensionStoreClient;
|
||||
|
@ -18,21 +17,25 @@ import run.halo.app.extension.store.ExtensionStoreClient;
|
|||
*
|
||||
* @author johnniang
|
||||
*/
|
||||
@Service
|
||||
public class DefaultExtensionClient implements ExtensionClient {
|
||||
|
||||
private final ExtensionStoreClient storeClient;
|
||||
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.converter = converter;
|
||||
this.schemeManager = schemeManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <E extends Extension> List<E> list(Class<E> type, Predicate<E> predicate,
|
||||
Comparator<E> comparator) {
|
||||
var scheme = Schemes.INSTANCE.get(type);
|
||||
var scheme = schemeManager.get(type);
|
||||
var storeNamePrefix = ExtensionUtil.buildStoreNamePrefix(scheme);
|
||||
|
||||
var storesStream = storeClient.listByNamePrefix(storeNamePrefix).stream()
|
||||
|
@ -59,7 +62,7 @@ public class DefaultExtensionClient implements ExtensionClient {
|
|||
|
||||
@Override
|
||||
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);
|
||||
return storeClient.fetchByName(storeName)
|
||||
|
@ -68,7 +71,9 @@ public class DefaultExtensionClient implements ExtensionClient {
|
|||
|
||||
@Override
|
||||
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);
|
||||
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.JsonProperty;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* ExtensionOperator contains some getters and setters for required fields of Extension.
|
||||
|
@ -13,11 +14,28 @@ public interface ExtensionOperator {
|
|||
|
||||
@Schema(required = true)
|
||||
@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)
|
||||
@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)
|
||||
@JsonProperty("metadata")
|
||||
|
@ -46,7 +64,7 @@ public interface ExtensionOperator {
|
|||
* {@link #setMetadata(MetadataOperator)}.
|
||||
*
|
||||
* @param metadata is Extension metadata.
|
||||
* @see #setMetadata(MetadataOperator)
|
||||
* @see #setMetadata(MetadataOperator)
|
||||
*/
|
||||
@Deprecated(forRemoval = true)
|
||||
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;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
public boolean hasGroup() {
|
||||
return StringUtils.hasText(group);
|
||||
}
|
||||
|
||||
/**
|
||||
* Composes GroupVersionKind from API version and kind name.
|
||||
*
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
package run.halo.app.extension;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import com.networknt.schema.JsonSchemaFactory;
|
||||
import com.networknt.schema.SpecVersion;
|
||||
import java.io.IOException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
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 ObjectMapper objectMapper;
|
||||
public static final ObjectMapper OBJECT_MAPPER;
|
||||
private final JsonSchemaFactory jsonSchemaFactory;
|
||||
|
||||
public JSONExtensionConverter(ObjectMapper objectMapper) {
|
||||
this.objectMapper = objectMapper;
|
||||
private final SchemeManager schemeManager;
|
||||
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <E extends Extension> ExtensionStore convertTo(E extension) {
|
||||
var gvk = extension.groupVersionKind();
|
||||
var scheme = Schemes.INSTANCE.get(gvk);
|
||||
var scheme = schemeManager.get(gvk);
|
||||
var storeName = ExtensionUtil.buildStoreName(scheme, extension.getMetadata().getName());
|
||||
try {
|
||||
if (logger.isDebugEnabled()) {
|
||||
|
@ -42,9 +53,10 @@ public class JSONExtensionConverter implements ExtensionConverter {
|
|||
scheme.jsonSchema().toPrettyString());
|
||||
}
|
||||
|
||||
var data = OBJECT_MAPPER.writeValueAsBytes(extension);
|
||||
|
||||
var validator = jsonSchemaFactory.getSchema(scheme.jsonSchema());
|
||||
var extensionNode = objectMapper.valueToTree(extension);
|
||||
var errors = validator.validate(extensionNode);
|
||||
var errors = validator.validate(OBJECT_MAPPER.readTree(data));
|
||||
if (!CollectionUtils.isEmpty(errors)) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
// 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);
|
||||
}
|
||||
|
||||
// keep converting
|
||||
var data = objectMapper.writeValueAsBytes(extensionNode);
|
||||
var version = extension.getMetadata().getVersion();
|
||||
return new ExtensionStore(storeName, data, version);
|
||||
} catch (JsonProcessingException e) {
|
||||
} catch (IOException e) {
|
||||
throw new ExtensionConvertException("Failed write Extension as bytes", e);
|
||||
}
|
||||
}
|
||||
|
@ -67,7 +77,7 @@ public class JSONExtensionConverter implements ExtensionConverter {
|
|||
@Override
|
||||
public <E extends Extension> E convertFrom(Class<E> type, ExtensionStore extensionStore) {
|
||||
try {
|
||||
var extension = objectMapper.readValue(extensionStore.getData(), type);
|
||||
var extension = OBJECT_MAPPER.readValue(extensionStore.getData(), type);
|
||||
extension.getMetadata().setVersion(extensionStore.getVersion());
|
||||
return extension;
|
||||
} catch (IOException e) {
|
||||
|
|
|
@ -3,6 +3,7 @@ package run.halo.app.extension;
|
|||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* Metadata of Extension.
|
||||
|
@ -10,6 +11,7 @@ import lombok.Data;
|
|||
* @author johnniang
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode
|
||||
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 java.time.Instant;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* MetadataOperator contains some getters and setters for required fields of metadata.
|
||||
|
@ -51,4 +52,31 @@ public interface MetadataOperator {
|
|||
|
||||
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;
|
||||
|
||||
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 run.halo.app.extension.exception.ExtensionException;
|
||||
|
||||
/**
|
||||
* 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");
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
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.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
|
||||
|
@ -22,68 +24,187 @@ import java.io.IOException;
|
|||
*/
|
||||
@JsonSerialize(using = Unstructured.UnstructuredSerializer.class)
|
||||
@JsonDeserialize(using = Unstructured.UnstructuredDeserializer.class)
|
||||
@SuppressWarnings("rawtypes")
|
||||
public class Unstructured implements Extension {
|
||||
|
||||
public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||
public static final ObjectMapper OBJECT_MAPPER = JSONExtensionConverter.OBJECT_MAPPER;
|
||||
|
||||
static {
|
||||
OBJECT_MAPPER.registerModule(new JavaTimeModule());
|
||||
}
|
||||
|
||||
private final ObjectNode extension;
|
||||
private final Map data;
|
||||
|
||||
public Unstructured() {
|
||||
this(OBJECT_MAPPER.createObjectNode());
|
||||
this(new HashMap());
|
||||
}
|
||||
|
||||
public Unstructured(ObjectNode extension) {
|
||||
this.extension = extension;
|
||||
public Unstructured(Map data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getApiVersion() {
|
||||
return extension.get("apiVersion").asText();
|
||||
return (String) data.get("apiVersion");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKind() {
|
||||
return extension.get("kind").asText();
|
||||
return (String) data.get("kind");
|
||||
}
|
||||
|
||||
@Override
|
||||
public MetadataOperator getMetadata() {
|
||||
var metaMap = extension.get("metadata");
|
||||
return OBJECT_MAPPER.convertValue(metaMap, Metadata.class);
|
||||
return new UnstructuredMetadata();
|
||||
}
|
||||
|
||||
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
|
||||
public void setApiVersion(String apiVersion) {
|
||||
extension.put("apiVersion", apiVersion);
|
||||
setNestedValue(data, apiVersion, "apiVersion");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setKind(String kind) {
|
||||
extension.put("kind", kind);
|
||||
setNestedValue(data, kind, "kind");
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public void setMetadata(MetadataOperator metadata) {
|
||||
JsonNode metaNode = OBJECT_MAPPER.valueToTree(metadata);
|
||||
extension.set("metadata", metaNode);
|
||||
Map metadataMap = OBJECT_MAPPER.convertValue(metadata, Map.class);
|
||||
data.put("metadata", metadataMap);
|
||||
}
|
||||
|
||||
ObjectNode getExtension() {
|
||||
return extension;
|
||||
static Optional<Object> getNestedValue(Map map, String... fields) {
|
||||
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> {
|
||||
|
||||
@Override
|
||||
public void serialize(Unstructured value, JsonGenerator gen, SerializerProvider serializers)
|
||||
throws IOException {
|
||||
gen.writeTree(value.extension);
|
||||
gen.writeObject(value.data);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -93,7 +214,8 @@ public class Unstructured implements Extension {
|
|||
@Override
|
||||
public Unstructured deserialize(JsonParser p, DeserializationContext ctxt)
|
||||
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.context.ApplicationListener;
|
||||
import org.springframework.lang.NonNull;
|
||||
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.security.authentication.pat.PersonalAccessToken;
|
||||
import run.halo.app.security.authorization.Role;
|
||||
import run.halo.app.security.authorization.RoleBinding;
|
||||
|
||||
@Component
|
||||
public class SchemeInitializer implements ApplicationListener<ApplicationStartedEvent> {
|
||||
|
||||
private final SchemeManager schemeManager;
|
||||
|
||||
public SchemeInitializer(SchemeManager schemeManager) {
|
||||
this.schemeManager = schemeManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(ApplicationStartedEvent event) {
|
||||
Schemes.INSTANCE.register(Role.class);
|
||||
Schemes.INSTANCE.register(PersonalAccessToken.class);
|
||||
Schemes.INSTANCE.register(Plugin.class);
|
||||
public void onApplicationEvent(@NonNull ApplicationStartedEvent event) {
|
||||
schemeManager.register(Role.class);
|
||||
schemeManager.register(RoleBinding.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.stereotype.Component;
|
||||
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.plugin.event.HaloPluginLoadedEvent;
|
||||
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 final ExtensionClient extensionClient;
|
||||
|
||||
public PluginLoadedListener(ExtensionClient extensionClient) {
|
||||
public PluginLoadedListener(ExtensionClient extensionClient, SchemeManager schemeManager) {
|
||||
this.extensionClient = extensionClient;
|
||||
|
||||
// TODO Optimize schemes register
|
||||
Schemes.INSTANCE.register(Plugin.class);
|
||||
Schemes.INSTANCE.register(ReverseProxy.class);
|
||||
schemeManager.register(Plugin.class);
|
||||
schemeManager.register(ReverseProxy.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -49,6 +49,10 @@ public class PolicyRule {
|
|||
*/
|
||||
String[] verbs;
|
||||
|
||||
public PolicyRule() {
|
||||
this(null, null, null, null, null);
|
||||
}
|
||||
|
||||
public PolicyRule(String[] apiGroups, String[] resources, String[] resourceNames,
|
||||
String[] nonResourceURLs, String[] verbs) {
|
||||
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.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
@ -16,7 +17,7 @@ import static org.mockito.Mockito.when;
|
|||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import java.util.List;
|
||||
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.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
|
@ -37,12 +38,16 @@ class DefaultExtensionClientTest {
|
|||
@Mock
|
||||
ExtensionConverter converter;
|
||||
|
||||
@Mock
|
||||
SchemeManager schemeManager;
|
||||
|
||||
@InjectMocks
|
||||
DefaultExtensionClient client;
|
||||
|
||||
@BeforeAll
|
||||
static void before() {
|
||||
Schemes.INSTANCE.register(FakeExtension.class);
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
lenient().when(schemeManager.get(eq(FakeExtension.class)))
|
||||
.thenReturn(Scheme.buildFromType(FakeExtension.class));
|
||||
}
|
||||
|
||||
FakeExtension createFakeExtension(String name, Long version) {
|
||||
|
@ -93,6 +98,9 @@ class DefaultExtensionClientTest {
|
|||
class UnRegisteredExtension extends AbstractExtension {
|
||||
}
|
||||
|
||||
when(schemeManager.get(eq(UnRegisteredExtension.class)))
|
||||
.thenThrow(SchemeNotFoundException.class);
|
||||
|
||||
assertThrows(SchemeNotFoundException.class,
|
||||
() -> client.list(UnRegisteredExtension.class, null, null));
|
||||
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",
|
||||
plural = "fakes",
|
||||
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.databind.ObjectMapper;
|
||||
import java.io.IOException;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
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.SchemaViolationException;
|
||||
import run.halo.app.extension.store.ExtensionStore;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class JSONExtensionConverterTest {
|
||||
|
||||
JSONExtensionConverter converter;
|
||||
|
||||
ObjectMapper objectMapper;
|
||||
|
||||
@BeforeAll
|
||||
static void beforeAll() {
|
||||
Schemes.INSTANCE.register(FakeExtension.class);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
objectMapper = new ObjectMapper();
|
||||
converter = new JSONExtensionConverter(objectMapper);
|
||||
DefaultSchemeManager schemeManager = new DefaultSchemeManager(null);
|
||||
converter = new JSONExtensionConverter(schemeManager);
|
||||
objectMapper = JSONExtensionConverter.OBJECT_MAPPER;
|
||||
|
||||
schemeManager.register(FakeExtension.class);
|
||||
}
|
||||
|
||||
@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;
|
||||
|
||||
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 com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import run.halo.app.extension.exception.ExtensionException;
|
||||
|
||||
class SchemeTest {
|
||||
|
||||
|
@ -31,4 +34,37 @@ class SchemeTest {
|
|||
"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;
|
||||
|
||||
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.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.json.JSONException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.skyscreamer.jsonassert.JSONAssert;
|
||||
|
||||
class UnstructuredTest {
|
||||
|
||||
|
@ -30,25 +32,36 @@ class UnstructuredTest {
|
|||
}
|
||||
""";
|
||||
|
||||
@BeforeAll
|
||||
static void setUpGlobally() {
|
||||
Schemes.INSTANCE.register(FakeExtension.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSerializeCorrectly() throws JsonProcessingException {
|
||||
var extensionNode = (ObjectNode) objectMapper.readTree(extensionJson);
|
||||
var extension = new Unstructured(extensionNode);
|
||||
Map extensionMap = objectMapper.readValue(extensionJson, Map.class);
|
||||
var extension = new Unstructured(extensionMap);
|
||||
|
||||
var gotNode = objectMapper.valueToTree(extension);
|
||||
assertEquals(extensionNode, gotNode);
|
||||
assertEquals(objectMapper.readTree(extensionJson), gotNode);
|
||||
}
|
||||
|
||||
@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 wantJsonNode = objectMapper.readTree(extensionJson);
|
||||
assertEquals(wantJsonNode, extension.getExtension());
|
||||
var gotJson = objectMapper.writeValueAsString(extension);
|
||||
JSONAssert.assertEquals(extensionJson, gotJson, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -57,7 +70,7 @@ class UnstructuredTest {
|
|||
|
||||
assertEquals("fake.halo.run/v1alpha1", extension.getApiVersion());
|
||||
assertEquals("Fake", extension.getKind());
|
||||
assertEquals(createMetadata(), extension.getMetadata());
|
||||
metadataDeepEquals(createMetadata(), extension.getMetadata());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -69,7 +82,7 @@ class UnstructuredTest {
|
|||
|
||||
assertEquals("fake.halo.run/v1alpha1", extension.getApiVersion());
|
||||
assertEquals("Fake", extension.getKind());
|
||||
assertEquals(createMetadata(), extension.getMetadata());
|
||||
assertTrue(metadataDeepEquals(createMetadata(), extension.getMetadata()));
|
||||
}
|
||||
|
||||
private Metadata createMetadata() {
|
||||
|
@ -81,4 +94,4 @@ class UnstructuredTest {
|
|||
return metadata;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -59,7 +59,7 @@ class YamlPluginFinderTest {
|
|||
"requires": ">=2.0.0",
|
||||
"pluginClass": "run.halo.app.plugin.BasePlugin"
|
||||
},
|
||||
"apiVersion": "v1",
|
||||
"apiVersion": "plugin.halo.run/v1alpha1",
|
||||
"kind": "Plugin",
|
||||
"metadata": {
|
||||
"name": "plugin-1",
|
||||
|
@ -72,7 +72,7 @@ class YamlPluginFinderTest {
|
|||
}
|
||||
""",
|
||||
JsonUtils.objectToJson(plugin),
|
||||
false);
|
||||
true);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -154,7 +154,7 @@ class YamlPluginFinderTest {
|
|||
void deserializeLicense() throws JSONException, JsonProcessingException {
|
||||
String pluginJson = """
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
"apiVersion": "plugin.halo.run/v1alpha1",
|
||||
"kind": "Plugin",
|
||||
"metadata": {
|
||||
"name": "plugin-1"
|
||||
|
|
Loading…
Reference in New Issue