diff --git a/nb-api/src/main/java/io/nosqlbench/nb/api/config/standard/ConfigModel.java b/nb-api/src/main/java/io/nosqlbench/nb/api/config/standard/ConfigModel.java index 2852bb1bc..44e881027 100644 --- a/nb-api/src/main/java/io/nosqlbench/nb/api/config/standard/ConfigModel.java +++ b/nb-api/src/main/java/io/nosqlbench/nb/api/config/standard/ConfigModel.java @@ -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> elements = new LinkedHashMap<>(); + private final Map> paramsByName = new LinkedHashMap<>(); + private final List> 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 ConfigModel add(Param 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> getElements() { - return Collections.unmodifiableMap(elements); + public Map> getNamedParams() { + return Collections.unmodifiableMap(paramsByName); + } + + @Override + public List> getParams() { + return new ArrayList<>((this.params)); } @Override @@ -80,59 +52,51 @@ public class ConfigModel implements NBConfigModel { return ofType; } - @Override - public void assertValidConfig(Map 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 convertValueTo(String configName, String paramName, Object value, Class 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 sharedConfig) { + LinkedHashMap 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 config) { + List 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 config) { assertValidConfig(config); LinkedHashMap 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 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 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 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 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; + } } diff --git a/nb-api/src/main/java/io/nosqlbench/nb/api/config/standard/ConfigSuggestions.java b/nb-api/src/main/java/io/nosqlbench/nb/api/config/standard/ConfigSuggestions.java index 5f1eb9eb2..59b16d420 100644 --- a/nb-api/src/main/java/io/nosqlbench/nb/api/config/standard/ConfigSuggestions.java +++ b/nb-api/src/main/java/io/nosqlbench/nb/api/config/standard/ConfigSuggestions.java @@ -8,8 +8,22 @@ import java.util.stream.Collectors; public class ConfigSuggestions { public static Optional getForParam(ConfigModel model, String param) { + return suggestAlternateCase(model,param) + .or(() -> suggestAlternates(model,param)); + } + + private static Optional 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 suggestAlternates(ConfigModel model, String param) { Map> suggestions = new HashMap<>(); - for (String candidate : model.getElements().keySet()) { + for (String candidate : model.getNamedParams().keySet()) { try { Integer distance = LevenshteinDistance.getDefaultInstance().apply(param, candidate); Set strings = suggestions.computeIfAbsent(distance, d -> new HashSet<>()); diff --git a/nb-api/src/test/java/io/nosqlbench/nb/api/config/ConfigModelTest.java b/nb-api/src/test/java/io/nosqlbench/nb/api/config/ConfigModelTest.java index 8fda35e2a..1504aa43e 100644 --- a/nb-api/src/test/java/io/nosqlbench/nb/api/config/ConfigModelTest.java +++ b/nb-api/src/test/java/io/nosqlbench/nb/api/config/ConfigModelTest.java @@ -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() { } } diff --git a/nb-api/src/test/java/io/nosqlbench/nb/api/config/standard/ConfigElementTest.java b/nb-api/src/test/java/io/nosqlbench/nb/api/config/standard/ConfigElementTest.java index 58500d72a..4f3671c6c 100644 --- a/nb-api/src/test/java/io/nosqlbench/nb/api/config/standard/ConfigElementTest.java +++ b/nb-api/src/test/java/io/nosqlbench/nb/api/config/standard/ConfigElementTest.java @@ -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 cfgmodel = - new Param<>("testvar",String.class,"testing a var",false,null).setRegex("WOO"); + Param cfgmodel = Param.defaultTo("testvar", "default").setRegex("WOO"); assertThat(cfgmodel.validate("WOO").isValid()).isTrue(); } } diff --git a/virtdata-lib-basics/src/main/java/io/nosqlbench/virtdata/library/basics/shared/stateful/LoadElement.java b/virtdata-lib-basics/src/main/java/io/nosqlbench/virtdata/library/basics/shared/stateful/LoadElement.java index a4ad47177..2764d0142 100644 --- a/virtdata-lib-basics/src/main/java/io/nosqlbench/virtdata/library/basics/shared/stateful/LoadElement.java +++ b/virtdata-lib-basics/src/main/java/io/nosqlbench/virtdata/library/basics/shared/stateful/LoadElement.java @@ -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, NBMapConfigurable { @Override public NBConfigModel getConfigModel() { - return ConfigModel.of(this.getClass()).optional("", Map.class); + return ConfigModel.of(this.getClass(), Param.optional("", Map.class)); } }