Skip to content

Commit

Permalink
Config metadata: Support for polymorphic types #114
Browse files Browse the repository at this point in the history
* unit tests
* tracking subclass label
  • Loading branch information
andrus committed Jan 4, 2017
1 parent e21a1a3 commit c135422
Show file tree
Hide file tree
Showing 6 changed files with 190 additions and 50 deletions.
1 change: 1 addition & 0 deletions RELEASE-NOTES.md
Expand Up @@ -6,6 +6,7 @@
* #105 Default command name generation should convert camel-case to dashes
* #112 Explicit short option names
* #113 Move io.bootique.application to io.bootique.meta.application
* #114 Config metadata: Support for polymorphic types

## 0.20

Expand Down
Expand Up @@ -34,25 +34,26 @@ public ConfigSectionGenerator(ConsoleAppender out) {
public Object visitObjectMetadata(ConfigObjectMetadata metadata) {
printNode(metadata, false);

List<ConfigMetadataNode> sortedChildren = metadata.getProperties()
.stream()
.sorted(Comparator.comparing(MetadataNode::getName))
List<ConfigObjectMetadata> subconfigs = metadata.getAllSubConfigs()
.map(md -> md.accept(new ConfigMetadataVisitor<ConfigObjectMetadata>() {
@Override
public ConfigObjectMetadata visitObjectMetadata(ConfigObjectMetadata metadata) {
return metadata.isAbstractType() || metadata.getProperties().isEmpty() ? null : metadata;
}
}))
.filter(md -> md != null)
.collect(Collectors.toList());

if (sortedChildren.isEmpty()) {
return null;
}

ConfigMetadataNode last = sortedChildren.get(sortedChildren.size() - 1);

ConfigSectionGenerator childGenerator = new ConfigSectionGenerator(out.withOffset(DEFAULT_OFFSET));
sortedChildren.forEach(p -> {
p.accept(childGenerator);
if (!subconfigs.isEmpty()) {
ConfigObjectMetadata last = subconfigs.get(subconfigs.size() - 1);
subconfigs.forEach(md -> {
printObjectNoSubclasses(md);

if (p != last) {
out.println();
}
});
if (md != last) {
out.println();
}
});
}

return null;
}
Expand All @@ -67,7 +68,6 @@ public Object visitValueMetadata(ConfigValueMetadata metadata) {
public Object visitListMetadata(ConfigListMetadata metadata) {
printNode(metadata, false);

// TODO: should support multiple element types (from META-INF/services/PolymorphicConfiguration)
metadata.getElementType().accept(new ConfigSectionListGenerator(out.withOffset(DEFAULT_OFFSET)));

return null;
Expand All @@ -77,13 +77,39 @@ public Object visitListMetadata(ConfigListMetadata metadata) {
public Object visitMapMetadata(ConfigMapMetadata metadata) {
printNode(metadata, false);

// TODO: should support multiple element types (from META-INF/services/PolymorphicConfiguration)
metadata.getValuesType().accept(
new ConfigSectionMapGenerator(metadata.getKeysType(), out.withOffset(DEFAULT_OFFSET)));

return null;
}

protected void printObjectNoSubclasses(ConfigObjectMetadata metadata) {

ConsoleAppender shifted = out.withOffset(DEFAULT_OFFSET);

if (metadata.getTypeLabel() != null) {
shifted.println("# Designator of subtype: ", typeLabel(metadata.getType()));
shifted.println("type: ", metadata.getTypeLabel());
shifted.println();
}

List<ConfigMetadataNode> sortedChildren = metadata.getProperties()
.stream()
.sorted(Comparator.comparing(MetadataNode::getName))
.collect(Collectors.toList());

ConfigMetadataNode last = sortedChildren.get(sortedChildren.size() - 1);
ConfigSectionGenerator childGenerator = new ConfigSectionGenerator(shifted);
sortedChildren.forEach(p -> {
p.accept(childGenerator);

if (p != last) {
out.println();
}
});

}

protected void printNode(ConfigValueMetadata metadata, boolean asValue) {
Type valueType = metadata.getType();

Expand Down
@@ -1,9 +1,11 @@
package io.bootique.meta.config;

import com.fasterxml.jackson.annotation.JsonTypeName;
import io.bootique.annotation.BQConfig;
import io.bootique.annotation.BQConfigProperty;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Collection;
Expand Down Expand Up @@ -100,7 +102,9 @@ protected ConfigMetadataNode compileObjectMetadata(Descriptor descriptor) {
ConfigObjectMetadata.Builder builder = ConfigObjectMetadata
.builder(baseObject)
.name(descriptor.getName())
.type(descriptor.getType());
.type(descriptor.getType())
.abstractType(isAbstract(descriptor.getTypeClass()))
.typeLabel(extractTypeLabel(descriptor.getTypeClass()));

// note that root config object known to Bootique doesn't require BQConfig annotation (though it would help in
// determining description). Objects nested within the root config do. Otherwise they will be treated as
Expand All @@ -127,6 +131,19 @@ protected ConfigMetadataNode compileObjectMetadata(Descriptor descriptor) {
return builder.build();
}

protected String extractTypeLabel(Class<?> type) {
// TODO: get rid of Jackson annotations dependency .. devise our own that reflect Bootique style of config factory
// subclassing...

JsonTypeName typeName = type.getAnnotation(JsonTypeName.class);
return typeName != null ? typeName.value() : null;
}

protected boolean isAbstract(Class<?> type) {
int modifiers = type.getModifiers();
return Modifier.isAbstract(modifiers) || Modifier.isInterface(modifiers);
}

protected ConfigMetadataNode compileValueMetadata(Descriptor descriptor) {
return ConfigValueMetadata
.builder(descriptor.getName())
Expand Down
Expand Up @@ -33,6 +33,8 @@ public Stream<ConfigMetadataNode> visitMapMetadata(ConfigMapMetadata metadata) {
}
};

private boolean abstractType;
private String typeLabel;
private Collection<ConfigMetadataNode> subConfigs;
private Collection<ConfigMetadataNode> properties;

Expand Down Expand Up @@ -65,6 +67,19 @@ public <T> T accept(ConfigMetadataVisitor<T> visitor) {
return visitor.visitObjectMetadata(this);
}

/**
* Returns an optional label that is used as a type designator for polymorphic config objects.
*
* @return an optional label that is used as a type designator for polymorphic config objects.
*/
public String getTypeLabel() {
return typeLabel;
}

public boolean isAbstractType() {
return abstractType;
}

public Collection<ConfigMetadataNode> getProperties() {
return properties;
}
Expand Down Expand Up @@ -113,5 +128,15 @@ public Builder addSubConfig(ConfigMetadataNode subConfig) {
toBuild.subConfigs.add(subConfig);
return this;
}

public Builder typeLabel(String label) {
toBuild.typeLabel = label;
return this;
}

public Builder abstractType(boolean isAbstract) {
toBuild.abstractType = isAbstract;
return this;
}
}
}
Expand Up @@ -254,11 +254,71 @@ public void testVisitMapOfListsOfObjects() {
);
}

@Test
public void testVisitObjectConfig_Inheritance() {

ConfigObjectMetadata sub1 = ConfigObjectMetadata.builder()
.type(Config3.class)
.typeLabel("c3")
.addProperty(ConfigValueMetadata.builder("p0").type(Boolean.class).build())
.addProperty(ConfigValueMetadata.builder("p1").type(String.class).build())
.build();

ConfigObjectMetadata sub2 = ConfigObjectMetadata.builder()
.type(Config4.class)
.typeLabel("c4")
.addProperty(ConfigValueMetadata.builder("p2").type(Integer.TYPE).description("Designates an integer value").build())
.addProperty(ConfigValueMetadata.builder("p3").type(Bootique.class).build())
.build();

ConfigObjectMetadata m1Config = ConfigObjectMetadata
.builder("m1root")
.description("Root config of M1")
.type(ConfigRoot1.class)
.abstractType(true)
.addProperty(ConfigValueMetadata.builder("pa1").type(Integer.TYPE).build())
.addSubConfig(sub1)
.addSubConfig(sub2)
.build();

assertLines(m1Config,
"# Type: io.bootique.help.config.ConfigSectionGeneratorTest$ConfigRoot1",
"# Root config of M1",
"m1root:",
" # Designator of subtype: io.bootique.help.config.ConfigSectionGeneratorTest$Config3",
" type: c3",
"",
" # Type: boolean",
" p0: <true|false>",
"",
" # Type: String",
" p1: <string>",
"",
" # Designator of subtype: io.bootique.help.config.ConfigSectionGeneratorTest$Config4",
" type: c4",
"",
" # Type: int",
" # Designates an integer value",
" p2: <int>",
"",
" # Type: io.bootique.Bootique",
" p3: <value>"
);
}

public static class ConfigRoot1 {

}

public static class ConfigRoot2 {

}

public static class Config3 {

}

public static class Config4 {

}
}

0 comments on commit c135422

Please sign in to comment.