-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Generalize strategy to define Fields from a set of Types
- Loading branch information
Showing
7 changed files
with
347 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
9 changes: 9 additions & 0 deletions
9
...ssive-annotations/src/main/java/io/github/axonivy/json/schema/annotations/MultiTypes.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package io.github.axonivy.json.schema.annotations; | ||
|
||
import java.util.Set; | ||
|
||
public interface MultiTypes { | ||
|
||
Set<Class<?>> types(); | ||
|
||
} |
35 changes: 35 additions & 0 deletions
35
...ve-annotations/src/main/java/io/github/axonivy/json/schema/annotations/TypesAsFields.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
package io.github.axonivy.json.schema.annotations; | ||
|
||
import static java.lang.annotation.ElementType.FIELD; | ||
import static java.lang.annotation.ElementType.METHOD; | ||
import static java.lang.annotation.ElementType.TYPE; | ||
|
||
import java.lang.annotation.Retention; | ||
import java.lang.annotation.RetentionPolicy; | ||
import java.lang.annotation.Target; | ||
|
||
|
||
@Target({FIELD, METHOD, TYPE}) | ||
@Retention(RetentionPolicy.RUNTIME) | ||
public @interface TypesAsFields { | ||
|
||
/** the provider of at least all valid-subtypes */ | ||
public Class<? extends FieldRegistry> value(); | ||
|
||
public static interface FieldRegistry extends MultiTypes { | ||
|
||
default String fieldName(Class<?> type) { | ||
return type.getSimpleName(); | ||
} | ||
|
||
default String fieldDescription(@SuppressWarnings("unused") Class<?> type) { | ||
return null; | ||
} | ||
|
||
default Class<?> valueType(Class<?> type) { | ||
return type; | ||
} | ||
|
||
} | ||
|
||
} |
73 changes: 73 additions & 0 deletions
73
...e-annotations/src/main/java/io/github/axonivy/json/schema/impl/TypesAsFieldsProvider.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
package io.github.axonivy.json.schema.impl; | ||
|
||
import java.lang.reflect.Constructor; | ||
import java.util.Optional; | ||
|
||
import com.fasterxml.classmate.ResolvedType; | ||
import com.fasterxml.jackson.databind.node.ObjectNode; | ||
import com.github.victools.jsonschema.generator.CustomDefinition; | ||
import com.github.victools.jsonschema.generator.CustomDefinitionProviderV2; | ||
import com.github.victools.jsonschema.generator.SchemaGenerationContext; | ||
import com.github.victools.jsonschema.generator.SchemaKeyword; | ||
|
||
import io.github.axonivy.json.schema.annotations.TypesAsFields; | ||
import io.github.axonivy.json.schema.annotations.TypesAsFields.FieldRegistry; | ||
|
||
|
||
public class TypesAsFieldsProvider implements CustomDefinitionProviderV2 { | ||
|
||
@Override | ||
public CustomDefinition provideCustomSchemaDefinition(ResolvedType javaType, SchemaGenerationContext context) { | ||
TypesAsFields typesAsFields = javaType.getErasedType().getAnnotation(TypesAsFields.class); | ||
if (typesAsFields == null) { | ||
return null; | ||
} | ||
ObjectNode std = context.createStandardDefinition(javaType, this); | ||
String properties = context.getKeyword(SchemaKeyword.TAG_PROPERTIES); | ||
var props = Optional.ofNullable((ObjectNode)std.get(properties)) | ||
.orElseGet(() -> std.putObject(properties)); | ||
FieldRegistry registry = create(typesAsFields.value()); | ||
new FieldCreator(registry, context).typesAsFields(props); | ||
return new CustomDefinition(std); | ||
} | ||
|
||
private static class FieldCreator { | ||
|
||
private final FieldRegistry registry; | ||
private final SchemaGenerationContext context; | ||
|
||
private FieldCreator(FieldRegistry registry, SchemaGenerationContext context) { | ||
this.registry = registry; | ||
this.context = context; | ||
} | ||
|
||
private void typesAsFields(ObjectNode props) { | ||
registry.types().stream() | ||
.sorted((c1,c2) -> String.CASE_INSENSITIVE_ORDER.compare(c1.getSimpleName(), c2.getSimpleName())) | ||
.forEachOrdered(type -> { | ||
toProperty(props, type); | ||
}); | ||
} | ||
|
||
private void toProperty(ObjectNode props, Class<?> type) { | ||
String name = registry.fieldName(type); | ||
var refType = registry.valueType(type); | ||
var inner = context.getTypeContext().resolve(refType); | ||
var def = context.createDefinitionReference(inner); | ||
Optional.ofNullable(registry.fieldDescription(type)).ifPresent(desc -> { | ||
def.put(context.getKeyword(SchemaKeyword.TAG_DESCRIPTION), desc); | ||
}); | ||
props.set(name, def); | ||
} | ||
|
||
} | ||
|
||
private static FieldRegistry create(Class<? extends FieldRegistry> registryType) { | ||
try { | ||
Constructor<?> constructor = registryType.getConstructors()[0]; | ||
return (FieldRegistry) constructor.newInstance(); | ||
} catch (Exception ex) { | ||
throw new RuntimeException("Failed to create type registry: "+registryType+". Does it have a public zero-arg constructor?", ex); | ||
} | ||
} | ||
} |
188 changes: 188 additions & 0 deletions
188
...sive-annotations/src/test/java/io/github/axonivy/json/schema/tests/TestTypesAsFields.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,188 @@ | ||
package io.github.axonivy.json.schema.tests; | ||
|
||
import static org.assertj.core.api.Assertions.assertThat; | ||
|
||
import java.util.ArrayList; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.Set; | ||
|
||
import org.junit.jupiter.api.Test; | ||
|
||
import com.fasterxml.jackson.databind.JsonNode; | ||
import com.fasterxml.jackson.databind.node.ObjectNode; | ||
|
||
import io.github.axonivy.json.schema.annotations.TypesAsFields; | ||
import io.github.axonivy.json.schema.annotations.TypesAsFields.FieldRegistry; | ||
|
||
|
||
class TestTypesAsFields { | ||
|
||
@Test | ||
void fields() { | ||
ObjectNode schema = new ExpressiveSchemaGenerator().generateSchema(MyRootType.class); | ||
JsonNode defs = schema.get("$defs"); | ||
|
||
JsonNode props = defs.get(Collection.class.getSimpleName()).get("properties"); | ||
assertThat(namesOf(props)) | ||
.containsExactly("Another", "Container", "Specific"); | ||
|
||
assertThat(props.get("Another").get("$ref").asText()) | ||
.isEqualTo("#/$defs/Another"); | ||
|
||
JsonNode another = defs.get("Another").get("properties"); | ||
assertThat(namesOf(another)) | ||
.contains("common", "version"); | ||
} | ||
|
||
static class MyRootType { | ||
public Collection provider; | ||
} | ||
|
||
@TypesAsFields(LocalFactory.class) | ||
public static interface Collection extends Map<String, Generic> { } | ||
|
||
public static class LocalFactory implements FieldRegistry { | ||
@Override | ||
public Set<Class<?>> types() { | ||
return Set.of(Specific.class, Another.class, Container.class); | ||
} | ||
} | ||
|
||
@Test | ||
void customFieldName() { | ||
ObjectNode schema = new ExpressiveSchemaGenerator().generateSchema(MyRootType2.class); | ||
JsonNode defs = schema.get("$defs"); | ||
|
||
JsonNode props = defs.get(CollectionCustomFieldNames.class.getSimpleName()).get("properties"); | ||
assertThat(namesOf(props)) | ||
.containsExactly("Another", "Container", "specialName"); | ||
} | ||
|
||
static class MyRootType2 { | ||
public CollectionCustomFieldNames provider; | ||
} | ||
|
||
@TypesAsFields(LocalFactory2.class) | ||
public static interface CollectionCustomFieldNames extends Map<String, Generic> { } | ||
|
||
public static class LocalFactory2 implements FieldRegistry { | ||
@Override | ||
public Set<Class<?>> types() { | ||
return Set.of(Specific.class, Another.class, Container.class); | ||
} | ||
|
||
@Override | ||
public String fieldName(Class<?> type) { | ||
if (Specific.class == type) { | ||
return "specialName"; | ||
} | ||
return type.getSimpleName(); | ||
} | ||
} | ||
|
||
@Test | ||
void customBaseType() { | ||
ObjectNode schema = new ExpressiveSchemaGenerator().generateSchema(MyRootType3.class); | ||
JsonNode defs = schema.get("$defs"); | ||
|
||
JsonNode props = defs.get(CollectionOnBase.class.getSimpleName()).get("properties"); | ||
assertThat(namesOf(props)) | ||
.containsExactly("Another", "Container", "Specific"); | ||
|
||
assertThat(props.get("Another").get("$ref").asText()) | ||
.as("ref to common base") | ||
.isEqualTo("#/$defs/Base"); | ||
} | ||
|
||
static class MyRootType3 { | ||
public CollectionOnBase provider; | ||
} | ||
|
||
@TypesAsFields(LocalFactory3.class) | ||
public static interface CollectionOnBase extends Map<String, Generic> { } | ||
|
||
public static class LocalFactory3 implements FieldRegistry { | ||
@Override | ||
public Set<Class<?>> types() { | ||
return Set.of(Specific.class, Another.class, Container.class); | ||
} | ||
|
||
@Override | ||
public Class<?> valueType(Class<?> type) { | ||
return Base.class; // resolve all fields to the same type | ||
} | ||
} | ||
|
||
@Test | ||
void customDescription() { | ||
ObjectNode schema = new ExpressiveSchemaGenerator().generateSchema(MyRootType4.class); | ||
JsonNode defs = schema.get("$defs"); | ||
|
||
JsonNode props = defs.get(DescribedCollection.class.getSimpleName()).get("properties"); | ||
assertThat(namesOf(props)) | ||
.containsExactly("Another", "Container", "Specific"); | ||
|
||
JsonNode another = props.get("Another"); | ||
assertThat(namesOf(another)) | ||
.containsOnly("$ref", "description"); | ||
assertThat(another.get("description").asText()) | ||
.isEqualTo("Lorem ipsum"); | ||
} | ||
|
||
static class MyRootType4 { | ||
public DescribedCollection provider; | ||
} | ||
|
||
@TypesAsFields(LocalFactory4.class) | ||
public static interface DescribedCollection extends Map<String, Generic> { } | ||
|
||
public static class LocalFactory4 implements FieldRegistry { | ||
@Override | ||
public Set<Class<?>> types() { | ||
return Set.of(Specific.class, Another.class, Container.class); | ||
} | ||
|
||
@Override | ||
public String fieldDescription(Class<?> type) { | ||
if (type == Another.class) { | ||
return "Lorem ipsum"; | ||
} | ||
return null; | ||
} | ||
} | ||
|
||
|
||
static List<String> namesOf(JsonNode defs) { | ||
var names = new ArrayList<String>(); | ||
defs.fieldNames().forEachRemaining(names::add); | ||
return names; | ||
} | ||
|
||
public static interface Generic { | ||
String id(); | ||
} | ||
|
||
public static class Base implements Generic { | ||
@Override | ||
public String id() { | ||
return null; | ||
} | ||
|
||
public String common; | ||
} | ||
|
||
public static class Specific extends Base implements Generic { | ||
public String customName; | ||
} | ||
|
||
public static class Another extends Base implements Generic { | ||
public int version; | ||
} | ||
|
||
public static class Container extends Base implements Generic { | ||
public Generic child; | ||
} | ||
|
||
|
||
} |