Config API updates

This commit is contained in:
Jonathan Shook 2021-07-22 17:40:12 -05:00
parent 03d903f130
commit b8a3c98aca
5 changed files with 196 additions and 146 deletions

View File

@ -2,21 +2,21 @@ package io.nosqlbench.nb.api.config.standard;
import io.nosqlbench.nb.api.errors.BasicError;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.math.BigDecimal;
import java.util.*;
import java.util.regex.Pattern;
public class ConfigModel implements NBConfigModel {
private final LinkedHashMap<String, Param<?>> elements = new LinkedHashMap<>();
private final Map<String, Param<?>> paramsByName = new LinkedHashMap<>();
private final List<Param<?>> params = new ArrayList<>();
private Param<?> lastAdded = null;
private final Class<?> ofType;
private ConfigModel(Class<?> ofType, Param<?>... params) {
this.ofType = ofType;
for (Param<?> param : params) {
this.elements.put(param.getName(), param);
add(param);
}
}
@ -24,55 +24,27 @@ public class ConfigModel implements NBConfigModel {
return new ConfigModel(ofType, params);
}
public ConfigModel optional(String name, Class<?> clazz) {
add(new Param<>(name, clazz, "", false, null));
return this;
}
public ConfigModel optional(String name, Class<?> clazz, String description) {
add(new Param<>(name, clazz, description, false, null));
return this;
}
public ConfigModel required(String name, Class<?> clazz, String description) {
add(new Param<>(name, clazz, description, true, null));
return this;
}
public <T> ConfigModel add(Param<T> param) {
this.elements.put(param.name, param);
this.params.add(param);
for (String name : param.names) {
paramsByName.put(name, param);
}
lastAdded = null;
return this;
}
public ConfigModel required(String name, Class<?> clazz) {
add(new Param<>(name, clazz, "", true, null));
return this;
}
public ConfigModel defaults(String name, Object defaultValue) {
add(new Param<>(name, defaultValue.getClass(), "", true, defaultValue));
return this;
}
public ConfigModel defaults(String name, Object defaultValue, String description) {
add(new Param<>(name, defaultValue.getClass(), description, true, defaultValue));
return this;
}
public ConfigModel describedAs(String descriptionOfLastElement) {
lastAdded.setDescription(descriptionOfLastElement);
return this;
}
public NBConfigModel asReadOnly() {
return this;
}
@Override
public Map<String, Param<?>> getElements() {
return Collections.unmodifiableMap(elements);
public Map<String, Param<?>> getNamedParams() {
return Collections.unmodifiableMap(paramsByName);
}
@Override
public List<Param<?>> getParams() {
return new ArrayList<>((this.params));
}
@Override
@ -80,59 +52,51 @@ public class ConfigModel implements NBConfigModel {
return ofType;
}
@Override
public void assertValidConfig(Map<String, ?> config) {
for (String configkey : config.keySet()) {
Param<?> element = this.elements.get(configkey);
if (element == null) {
StringBuilder paramhelp = new StringBuilder(
"Unknown config parameter in config model '" + configkey + "' " +
"while configuring " + getOf().getSimpleName()
+ ", possible parameter names are " + this.elements.keySet() + "."
);
ConfigSuggestions.getForParam(this, configkey)
.ifPresent(suggestion -> paramhelp.append(" ").append(suggestion));
throw new BasicError(paramhelp.toString());
}
Object value = config.get(configkey);
Object testValue = convertValueTo(ofType.getSimpleName(), configkey, value, element.getType());
}
for (Param<?> element : elements.values()) {
if (element.isRequired() && element.getDefaultValue() == null) {
if (!config.containsKey(element.getName())) {
throw new RuntimeException("A required config element named '" + element.getName() +
"' and type '" + element.getType().getSimpleName() + "' was not found\n" +
"for configuring a " + getOf().getSimpleName());
}
}
}
}
private Object convertValueTo(String configName, String paramName, Object value, Class<?> type) {
public static <T> T convertValueTo(String configName, String paramName, Object value, Class<T> type) {
try {
if (type.isAssignableFrom(value.getClass())) {
return type.cast(value);
} else if (Number.class.isAssignableFrom(type)
&& Number.class.isAssignableFrom(value.getClass())) {
} else if (Number.class.isAssignableFrom(value.getClass())) { // A numeric value, and do we have a compatible target type?
Number number = (Number) value;
if (type.equals(Float.class)) {
return number.floatValue();
} else if (type.equals(Integer.class)) {
return number.intValue();
} else if (type.equals(Double.class)) {
return number.doubleValue();
} else if (type.equals(Long.class)) {
return number.longValue();
} else if (type.equals(Byte.class)) {
return number.byteValue();
} else if (type.equals(Short.class)) {
return number.shortValue();
// This series of double fake-outs is heinous, but it works to get around design
// holes in Java generics while preserving some type inference for the caller.
// If you are reading this code and you can find a better way, please change it!
if (type.equals(Float.class) || type == float.class) {
return (T) (Float) number.floatValue();
} else if (type.equals(Integer.class) || type == int.class) {
return (T) (Integer) number.intValue();
} else if (type.equals(Double.class) || type == double.class) {
return (T) (Double) number.doubleValue();
} else if (type.equals(Long.class) || type == long.class) {
return (T) (Long) number.longValue();
} else if (type.equals(Byte.class) || type == byte.class) {
return (T) (Byte) number.byteValue();
} else if (type.equals(Short.class) || type == short.class) {
return (T) (Short) number.shortValue();
} else {
throw new RuntimeException("Number type " + type.getSimpleName() + " could " +
" not be converted from " + value.getClass().getSimpleName());
}
} else if (value instanceof CharSequence) { // A stringy type, and do we have a compatible target type?
String string = ((CharSequence) value).toString();
if (type == int.class || type == Integer.class) {
return (T) Integer.valueOf(string);
} else if (type == char.class || type == Character.class && string.length() == 1) {
return (T) (Character) string.charAt(0);
} else if (type == long.class || type == Long.class) {
return (T) Long.valueOf(string);
} else if (type == float.class || type == Float.class) {
return (T) Float.valueOf(string);
} else if (type == double.class || type == Double.class) {
return (T) Double.valueOf(string);
} else if (type == BigDecimal.class) {
return (T) BigDecimal.valueOf(Double.parseDouble(string));
} else {
throw new RuntimeException("CharSequence type " + type.getSimpleName() + " could " +
" not be converted from " + value.getClass().getSimpleName());
}
}
} catch (Exception e) {
@ -146,30 +110,129 @@ public class ConfigModel implements NBConfigModel {
);
}
@Override
public NBConfiguration extract(Map<String, ?> sharedConfig) {
LinkedHashMap<String, Object> extracted = new LinkedHashMap<>();
for (String providedCfgField : sharedConfig.keySet()) {
if (getNamedParams().containsKey(providedCfgField)) {
extracted.put(providedCfgField, sharedConfig.remove(providedCfgField));
}
}
return new NBConfiguration(this, extracted);
}
private void assertDistinctSynonyms(Map<String, ?> config) {
List<String> names = new ArrayList<>();
for (Param<?> param : getParams()) {
names.clear();
for (String s : param.getNames()) {
if (config.containsKey(s)) {
names.add(s);
}
}
if (names.size() > 1) {
throw new NBConfigError("Multiple names for the same parameter were provided: " + names);
}
}
}
@Override
public NBConfiguration apply(Map<String, ?> config) {
assertValidConfig(config);
LinkedHashMap<String, Object> validConfig = new LinkedHashMap<>();
elements.forEach((k, v) -> {
String name = v.getName();
Class<?> type = v.getType();
Object cval = config.get(name);
if (cval == null && v.isRequired()) {
cval = v.getDefaultValue();
for (Param<?> param : params) {
Class<?> type = param.getType();
List<String> found = new ArrayList<>();
String activename = null;
Object cval = null;
for (String name : param.names) {
if (config.containsKey(name)) {
cval = config.get(name);
activename = name;
break;
}
}
if (cval == null && param.isRequired()) {
cval = param.getDefaultValue(); // We know this will be valid. It was validated, correct?
}
if (cval != null) {
cval = convertValueTo(ofType.getSimpleName(), k, cval, type);
validConfig.put(name, cval);
cval = convertValueTo(ofType.getSimpleName(), activename, cval, type);
validConfig.put(activename, cval);
}
});
}
return new NBConfiguration(this.asReadOnly(), validConfig);
}
@Override
public void assertValidConfig(Map<String, ?> config) {
assertRequiredFields(config);
assertNoExtraneousFields(config);
assertDistinctSynonyms(config);
}
@Override
public Param<?> getParam(String... names) {
for (String name : names) {
if (this.getNamedParams().containsKey(name)) {
return this.getNamedParams().get(name);
}
}
return null;
}
public ConfigModel validIfRegex(String s) {
Pattern regex = Pattern.compile(s);
lastAdded.setRegex(regex);
return this;
}
private void assertRequiredFields(Map<String, ?> config) {
// For each known configuration model ...
for (Param<?> param : params) {
if (param.isRequired() && param.getDefaultValue() == null) {
boolean provided = false;
for (String name : param.names) {
if (config.containsKey(name)) {
provided = true;
break;
}
}
if (!provided) {
throw new RuntimeException("A required config element named '" + param.names +
"' and type '" + param.getType().getSimpleName() + "' was not found\n" +
"for configuring a " + getOf().getSimpleName());
}
}
}
}
private void assertNoExtraneousFields(Map<String, ?> config) {
// For each provided configuration element ...
for (String configkey : config.keySet()) {
Param<?> element = this.paramsByName.get(configkey);
if (element == null) {
StringBuilder paramhelp = new StringBuilder(
"Unknown config parameter '" + configkey + "' in config model while configuring " + getOf().getSimpleName()
+ ", possible parameter names are " + this.paramsByName.keySet() + "."
);
ConfigSuggestions.getForParam(this, configkey)
.ifPresent(suggestion -> paramhelp.append(" ").append(suggestion));
throw new BasicError(paramhelp.toString());
}
Object value = config.get(configkey);
Object testValue = convertValueTo(ofType.getSimpleName(), configkey, value, element.getType());
}
}
@Override
public NBConfigModel add(NBConfigModel otherModel) {
for (Param<?> param : otherModel.getParams()) {
add(param);
}
return this;
}
}

View File

@ -8,8 +8,22 @@ import java.util.stream.Collectors;
public class ConfigSuggestions {
public static Optional<String> getForParam(ConfigModel model, String param) {
return suggestAlternateCase(model,param)
.or(() -> suggestAlternates(model,param));
}
private static Optional<String> suggestAlternateCase(ConfigModel model, String param) {
for (String cname : model.getNamedParams().keySet()) {
if (cname.equalsIgnoreCase(param)) {
return Optional.of("Did you mean '" + cname + "'?");
}
}
return Optional.empty();
}
private static Optional<String> suggestAlternates(ConfigModel model, String param) {
Map<Integer, Set<String>> suggestions = new HashMap<>();
for (String candidate : model.getElements().keySet()) {
for (String candidate : model.getNamedParams().keySet()) {
try {
Integer distance = LevenshteinDistance.getDefaultInstance().apply(param, candidate);
Set<String> strings = suggestions.computeIfAbsent(distance, d -> new HashSet<>());

View File

@ -1,50 +1,25 @@
package io.nosqlbench.nb.api.config;
import io.nosqlbench.nb.api.config.standard.ConfigModel;
import io.nosqlbench.nb.api.config.standard.NBConfiguration;
import io.nosqlbench.nb.api.config.standard.Param;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
public class ConfigModelTest {
@Test
void optional() {
}
public void testMultipleParams() {
ConfigModel cm = ConfigModel.of(ConfigModelTest.class,
Param.defaultTo(List.of("a","b"),"value").setRequired(false),
Param.required("c",int.class));
NBConfiguration cfg = cm.apply(Map.of("c", 232));
assertThat(cfg.getOptional("a")).isEmpty();
assertThat(cfg.get("c",int.class)).isEqualTo(232);
@Test
void testOptional() {
}
@Test
void required() {
}
@Test
void testRequired() {
}
@Test
void defaultto() {
}
@Test
void testDefaultto() {
}
@Test
void asReadOnly() {
}
@Test
void getElements() {
}
@Test
void getOf() {
}
@Test
void assertValidConfig() {
}
@Test
void apply() {
}
}

View File

@ -2,16 +2,13 @@ package io.nosqlbench.nb.api.config.standard;
import org.junit.jupiter.api.Test;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
public class ConfigElementTest {
@Test
public void testRegex() {
Param<String> cfgmodel =
new Param<>("testvar",String.class,"testing a var",false,null).setRegex("WOO");
Param<String> cfgmodel = Param.defaultTo("testvar", "default").setRegex("WOO");
assertThat(cfgmodel.validate("WOO").isValid()).isTrue();
}
}

View File

@ -2,9 +2,10 @@ package io.nosqlbench.virtdata.library.basics.shared.stateful;
import io.nosqlbench.nb.api.config.standard.ConfigModel;
import io.nosqlbench.nb.api.config.standard.NBConfigModel;
import io.nosqlbench.nb.api.config.standard.NBMapConfigurable;
import io.nosqlbench.nb.api.config.standard.Param;
import io.nosqlbench.virtdata.api.annotations.Example;
import io.nosqlbench.virtdata.api.annotations.ThreadSafeMapper;
import io.nosqlbench.nb.api.config.standard.NBMapConfigurable;
import java.util.Map;
import java.util.function.Function;
@ -51,6 +52,6 @@ public class LoadElement implements Function<Object,Object>, NBMapConfigurable {
@Override
public NBConfigModel getConfigModel() {
return ConfigModel.of(this.getClass()).optional("<mapname>", Map.class);
return ConfigModel.of(this.getClass(), Param.optional("<mapname>", Map.class));
}
}