mirror of https://github.com/halo-dev/halo
Add JSON schema generator and validator (#2093)
Only validate the JSON schema when saving Extension into database.pull/2102/head
parent
264f9e39cb
commit
5f9daf4735
|
@ -44,6 +44,8 @@ bootJar {
|
|||
ext['h2.version'] = '2.1.210'
|
||||
ext {
|
||||
commonsLang3 = "3.12.0"
|
||||
jsonschemaGenerator = "4.24.3"
|
||||
jsonschemaValidator = "1.0.69"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
@ -60,6 +62,9 @@ dependencies {
|
|||
implementation "org.flywaydb:flyway-core"
|
||||
implementation "org.flywaydb:flyway-mysql"
|
||||
|
||||
implementation "com.github.victools:jsonschema-generator:$jsonschemaGenerator"
|
||||
implementation "com.github.victools:jsonschema-module-swagger-2:$jsonschemaGenerator"
|
||||
implementation "com.networknt:json-schema-validator:$jsonschemaValidator"
|
||||
implementation "org.apache.commons:commons-lang3:$commonsLang3"
|
||||
|
||||
compileOnly 'org.projectlombok:lombok'
|
||||
|
|
|
@ -2,9 +2,15 @@ package run.halo.app.extension;
|
|||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
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.stereotype.Component;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import run.halo.app.extension.exception.ExtensionConvertException;
|
||||
import run.halo.app.extension.exception.SchemaViolationException;
|
||||
import run.halo.app.extension.store.ExtensionStore;
|
||||
|
||||
/**
|
||||
|
@ -15,10 +21,14 @@ import run.halo.app.extension.store.ExtensionStore;
|
|||
@Component
|
||||
public class JSONExtensionConverter implements ExtensionConverter {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(getClass());
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
private final JsonSchemaFactory jsonSchemaFactory;
|
||||
|
||||
public JSONExtensionConverter(ObjectMapper objectMapper) {
|
||||
this.objectMapper = objectMapper;
|
||||
jsonSchemaFactory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -26,9 +36,26 @@ public class JSONExtensionConverter implements ExtensionConverter {
|
|||
var scheme = Schemes.INSTANCE.get(extension.getClass());
|
||||
var storeName = ExtensionUtil.buildStoreName(scheme, extension.metadata().getName());
|
||||
try {
|
||||
// TODO Validate the extension in ExtensionClient
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("JSON schema({}): {}", scheme.type(),
|
||||
scheme.jsonSchema().toPrettyString());
|
||||
}
|
||||
|
||||
var validator = jsonSchemaFactory.getSchema(scheme.jsonSchema());
|
||||
var extensionNode = objectMapper.valueToTree(extension);
|
||||
var errors = validator.validate(extensionNode);
|
||||
if (!CollectionUtils.isEmpty(errors)) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
// only print the errors when debug mode is enabled
|
||||
logger.error("Failed to validate Extension {}, and errors are: {}",
|
||||
extension.getClass(), errors);
|
||||
}
|
||||
throw new SchemaViolationException(
|
||||
"Failed to validate Extension " + extension.getClass(), errors);
|
||||
}
|
||||
|
||||
// keep converting
|
||||
var data = objectMapper.writeValueAsBytes(extension);
|
||||
var data = objectMapper.writeValueAsBytes(extensionNode);
|
||||
var version = extension.metadata().getVersion();
|
||||
return new ExtensionStore(storeName, data, version);
|
||||
} catch (JsonProcessingException e) {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package run.halo.app.extension;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
import lombok.Data;
|
||||
|
@ -15,31 +16,37 @@ public class Metadata {
|
|||
/**
|
||||
* Metadata name. The name is unique globally.
|
||||
*/
|
||||
@Schema(required = true)
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* Labels are like key-value format.
|
||||
*/
|
||||
@Schema(nullable = true)
|
||||
private Map<String, String> labels;
|
||||
|
||||
/**
|
||||
* Annotations are like key-value format.
|
||||
*/
|
||||
@Schema(nullable = true)
|
||||
private Map<String, String> annotations;
|
||||
|
||||
/**
|
||||
* Current version of the Extension. It will be bumped up every update.
|
||||
*/
|
||||
@Schema(nullable = true)
|
||||
private Long version;
|
||||
|
||||
/**
|
||||
* Creation timestamp of the Extension.
|
||||
*/
|
||||
@Schema(nullable = true)
|
||||
private Instant creationTimestamp;
|
||||
|
||||
/**
|
||||
* Deletion timestamp of the Extension.
|
||||
*/
|
||||
@Schema(nullable = true)
|
||||
private Instant deletionTimestamp;
|
||||
|
||||
}
|
||||
|
|
|
@ -23,9 +23,7 @@ public record Scheme(Class<? extends Extension> type,
|
|||
Assert.notNull(groupVersionKind, "GroupVersionKind of Extension must not be null");
|
||||
Assert.hasText(plural, "Plural name of Extension must not be blank");
|
||||
Assert.hasText(singular, "Singular name of Extension must not be blank");
|
||||
|
||||
//TODO Validate the json schema when we plan to integrate Extension validation.
|
||||
// Assert.notNull(jsonSchema, "Json Schema must not be null");
|
||||
Assert.notNull(jsonSchema, "Json Schema must not be null");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
package run.halo.app.extension;
|
||||
|
||||
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;
|
||||
|
@ -69,9 +74,17 @@ public enum Schemes {
|
|||
type.getName()));
|
||||
}
|
||||
|
||||
// TODO Generate the JSON schema here
|
||||
// generate JSON schema
|
||||
var module = new Swagger2Module();
|
||||
var config =
|
||||
new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2019_09, OptionPreset.PLAIN_JSON)
|
||||
.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(), null);
|
||||
gvk.plural(), gvk.singular(), jsonSchema);
|
||||
|
||||
register(scheme);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
package run.halo.app.extension.exception;
|
||||
|
||||
import com.networknt.schema.ValidationMessage;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* This exception is thrown when Schema is violation.
|
||||
*
|
||||
* @author johnniang
|
||||
*/
|
||||
public class SchemaViolationException extends ExtensionException {
|
||||
|
||||
/**
|
||||
* Validation errors.
|
||||
*/
|
||||
private final Set<ValidationMessage> errors;
|
||||
|
||||
public SchemaViolationException(Set<ValidationMessage> errors) {
|
||||
this.errors = errors;
|
||||
}
|
||||
|
||||
public SchemaViolationException(String message, Set<ValidationMessage> errors) {
|
||||
super(message);
|
||||
this.errors = errors;
|
||||
}
|
||||
|
||||
public SchemaViolationException(String message, Throwable cause,
|
||||
Set<ValidationMessage> errors) {
|
||||
super(message, cause);
|
||||
this.errors = errors;
|
||||
}
|
||||
|
||||
public SchemaViolationException(Throwable cause, Set<ValidationMessage> errors) {
|
||||
super(cause);
|
||||
this.errors = errors;
|
||||
}
|
||||
|
||||
public SchemaViolationException(String message, Throwable cause, boolean enableSuppression,
|
||||
boolean writableStackTrace, Set<ValidationMessage> errors) {
|
||||
super(message, cause, enableSuppression, writableStackTrace);
|
||||
this.errors = errors;
|
||||
}
|
||||
|
||||
public Set<ValidationMessage> getErrors() {
|
||||
return errors;
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ package run.halo.app.extension;
|
|||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
|
@ -17,12 +18,12 @@ class ExtensionUtilTest {
|
|||
new GroupVersionKind("fake.halo.run", "v1alpha1", "Fake"),
|
||||
"fakes",
|
||||
"fake",
|
||||
null);
|
||||
new ObjectNode(null));
|
||||
grouplessScheme = new Scheme(FakeExtension.class,
|
||||
new GroupVersionKind("", "v1alpha1", "Fake"),
|
||||
"fakes",
|
||||
"fake",
|
||||
null);
|
||||
new ObjectNode(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -12,6 +12,7 @@ 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)
|
||||
|
@ -57,7 +58,7 @@ class JSONExtensionConverterTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowExceptionWhenDataIsInvalid() {
|
||||
void shouldThrowConvertExceptionWhenDataIsInvalid() {
|
||||
var store = new ExtensionStore();
|
||||
store.setName("/registry/fake.halo.run/fakes/fake");
|
||||
store.setVersion(20L);
|
||||
|
@ -67,6 +68,18 @@ class JSONExtensionConverterTest {
|
|||
() -> converter.convertFrom(FakeExtension.class, store));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowSchemaViolationExceptionWhenNameNotSet() {
|
||||
var fake = new FakeExtension();
|
||||
Metadata metadata = new Metadata();
|
||||
fake.setMetadata(metadata);
|
||||
fake.setApiVersion("fake.halo.run/v1alpha1");
|
||||
fake.setKind("Fake");
|
||||
var error = assertThrows(SchemaViolationException.class, () -> converter.convertTo(fake));
|
||||
assertEquals(1, error.getErrors().size());
|
||||
assertEquals("$.metadata.name: null found, string expected",
|
||||
error.getErrors().iterator().next().getMessage());
|
||||
}
|
||||
|
||||
FakeExtension createFakeExtension(String name, Long version) {
|
||||
var fake = new FakeExtension();
|
||||
|
|
|
@ -23,9 +23,10 @@ class SchemeTest {
|
|||
assertThrows(IllegalArgumentException.class,
|
||||
() -> new Scheme(FakeExtension.class, new GroupVersionKind("", "v1alpha1", "Fake"),
|
||||
"fakes", "", null));
|
||||
|
||||
new Scheme(FakeExtension.class, new GroupVersionKind("", "v1alpha1", "Fake"), "fakes",
|
||||
"fake", null);
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
new Scheme(FakeExtension.class, new GroupVersionKind("", "v1alpha1", "Fake"), "fakes",
|
||||
"fake", null);
|
||||
});
|
||||
new Scheme(FakeExtension.class, new GroupVersionKind("", "v1alpha1", "Fake"), "fakes",
|
||||
"fake", new ObjectNode(null));
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue