improve type-safe intention mapping

This commit is contained in:
Jonathan Shook 2022-02-15 21:23:51 -06:00
parent 35871718d5
commit a1834ca5ef
5 changed files with 267 additions and 79 deletions

View File

@ -15,6 +15,7 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.*;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.LongFunction;
@ -97,8 +98,8 @@ public class ParsedOp implements LongFunction<Map<String, ?>>, StaticFieldReader
}
@Override
public boolean isDefinedDynamic(String field) {
return tmap.isDefinedDynamic(field);
public boolean isDynamic(String field) {
return tmap.isDynamic(field);
}
@ -261,15 +262,18 @@ public class ParsedOp implements LongFunction<Map<String, ?>>, StaticFieldReader
* @param type The value type which the field must be assignable to
* @return A function which can provide a value for the given name and type
*/
public <V> Optional<LongFunction<V>> getAsOptionalFunction(String name, Class<? extends V> type) {
public <V> Optional<LongFunction<V>> getAsOptionalFunction(String name, Class<V> type) {
return tmap.getAsOptionalFunction(name, type);
}
public <V extends Enum<V>> Optional<LongFunction<V>> getAsOptionalEnumFunction(String name, Class<V> type) {
return tmap.getAsOptionalEnumFunction(name, type);
}
public <V> Optional<LongFunction<String>> getAsOptionalFunction(String name) {
return this.getAsOptionalFunction(name, String.class);
}
public <V> LongFunction<? extends V> getAsRequiredFunction(String name, Class<? extends V> type) {
public <V> LongFunction<V> getAsRequiredFunction(String name, Class<? extends V> type) {
return tmap.getAsRequiredFunction(name, type);
}
@ -289,8 +293,8 @@ public class ParsedOp implements LongFunction<Map<String, ?>>, StaticFieldReader
}
/**
* Get a LongFunction that first creates a LongFunction of String as in {@link #getAsFunction(String, Class)}, but then
* applies the result and cached it for subsequent access. This relies on {@link ObjectCache} internally.
* Get a LongFunction that first creates a LongFunction of String as in {@link #getAsRequiredFunction(String, Class)}, but then
* applies the result and caches it for subsequent access. This relies on {@link ObjectCache} internally.
*
* @param fieldname The name of the field which could contain a static or dynamic value
* @param defaultValue The default value to use in the init function if the fieldname is not defined as static nor dynamic
@ -420,12 +424,32 @@ public class ParsedOp implements LongFunction<Map<String, ?>>, StaticFieldReader
* @return Optionally, an enum value which matches, or {@link Optional#empty()}
* @throws OpConfigError if more than one field matches
*/
public <E extends Enum<E>> Optional<NamedTarget<E>> getTypeFromEnum(Class<E> enumclass) {
return tmap.getOptionalTypeFromEnum(enumclass);
public <E extends Enum<E>,V> Optional<TypeAndTarget<E,V>> getTypeFromEnum(Class<E> enumclass, Class<V> valueClass) {
return tmap.getOptionalTargetEnum(enumclass,valueClass);
}
public <E extends Enum<E>> NamedTarget<E> getRequiredTypeFromEnum(Class<E> enumclass) {
return tmap.getRequiredTypeFromEnum(enumclass);
public <E extends Enum<E>,V> Optional<TypeAndTarget<E,V>> getOptionalTargetEnum(
Class<E> enumclass,
Class<V> valueClass
){
return tmap.getOptionalTargetEnum(enumclass,valueClass);
}
public <E extends Enum<E>,V> Optional<TypeAndTarget<E,V>> getOptionalTargetEnum(
Class<E> enumclass,
Class<V> valueClass,
String alternateTypeField,
String alternateValueField
) {
return tmap.getOptionalTargetEnum(enumclass, valueClass, alternateTypeField, alternateValueField);
}
public <E extends Enum<E>,V> TypeAndTarget<E,V> getTargetEnum(Class<E> enumclass, Class<V> valueClass) {
return tmap.getTargetEnum(enumclass, valueClass);
}
public <E extends Enum<E>,V> TypeAndTarget<E,V> getTargetEnum(Class<E> enumclass, Class<V> valueclass, String tname, String vname) {
return tmap.getTargetEnum(enumclass, valueclass,tname,vname);
}
public <E extends Enum<E>> Optional<E> getOptionalEnumFromField(Class<E> enumclass, String fieldName) {
@ -435,4 +459,67 @@ public class ParsedOp implements LongFunction<Map<String, ?>>, StaticFieldReader
public <E extends Enum<E>> E getEnumFromFieldOr(Class<E> enumClass, E defaultEnum, String fieldName) {
return getOptionalEnumFromField(enumClass,fieldName).orElse(defaultEnum);
}
public <FA,FE> Optional<LongFunction<FA>> enhance(
Optional<LongFunction<FA>> func,
String field,
Class<FE> type,
FE defaultFe,
BiFunction<FA,FE,FA> combiner
) {
if (func.isEmpty()) {
return func;
}
LongFunction<FE> fieldEnhancerFunc = getAsFunctionOr(field, defaultFe);
LongFunction<FA> faLongFunction = func.get();
LongFunction<FA> lfa = l -> combiner.apply(faLongFunction.apply(l),fieldEnhancerFunc.apply(l));
return Optional.of(lfa);
}
public <FA,FE> Optional<LongFunction<FA>> enhance(
Optional<LongFunction<FA>> func,
String field,
Class<FE> type,
BiFunction<FA,FE,FA> combiner
) {
Optional<LongFunction<FE>> fieldEnhancerFunc = getAsOptionalFunction(field, type);
if (func.isEmpty()||fieldEnhancerFunc.isEmpty()) {
return func;
}
LongFunction<FA> faLongFunction = func.get();
LongFunction<FE> feLongFunction = fieldEnhancerFunc.get();
LongFunction<FA> lfa = l -> combiner.apply(faLongFunction.apply(l),feLongFunction.apply(l));
return Optional.of(lfa);
}
public <FA,FE> LongFunction<FA> enhance(
LongFunction<FA> func,
String field,
Class<FE> type,
BiFunction<FA,FE,FA> combiner
) {
Optional<LongFunction<FE>> fieldEnhancerFunc = getAsOptionalFunction(field, type);
if (fieldEnhancerFunc.isEmpty()) {
return func;
}
LongFunction<FE> feLongFunction = fieldEnhancerFunc.get();
LongFunction<FA> lfa = l -> combiner.apply(func.apply(l),feLongFunction.apply(l));
return lfa;
}
public <FA,FE extends Enum<FE>> LongFunction<FA> enhanceEnum(
LongFunction<FA> func,
String field,
Class<FE> type,
BiFunction<FA,FE,FA> combiner
) {
Optional<LongFunction<FE>> fieldEnhancerFunc = getAsOptionalEnumFunction(field, type);
if (fieldEnhancerFunc.isEmpty()) {
return func;
}
LongFunction<FE> feLongFunction = fieldEnhancerFunc.get();
LongFunction<FA> lfa = l -> combiner.apply(func.apply(l),feLongFunction.apply(l));
return lfa;
}
}

View File

@ -107,6 +107,9 @@ public class NBTypeSafeConversions implements NBTypeConverters {
return Float.parseFloat(in);
}
public static boolean to_boolean(String in) {
return Boolean.parseBoolean(in);
}
}

View File

@ -1,49 +0,0 @@
package io.nosqlbench.engine.api.templating;
import java.util.function.LongFunction;
/**
* <p>The result type from calling {@link ParsedOp#getRequiredTypeFromEnum(Class)}, which
* captures the matching enum type as well as the field name and a value function.</p>
*
* <p>The <em>enumId</em> is type-safe enum value from the provided enum to the above method.
* The <em>field</em> is the field name which was passed. The <em>targetFunction</em> is
* a {@link LongFunction} of Object which can be called to return an associated target value.</p>
*
* For example, with an emum like <pre>{@code
* public enum LandMovers {
* BullDozer,
* DumpTruck
* }
* }</pre>
*
* and a parsed op like <pre>{@code
* (json)
* {
* "op": {
* "bulldozer": "{dozerid}"
* }
* }
*
* (yaml)
* op:
* bulldozer: "{dozerid}
* }</pre>
* the result will be returned with the following: <pre>{@code
* enumId: BullDozer
* field: bulldozer
* targetFunction: (long l) -> ...
* }</pre>
* @param <E>
*/
public class NamedTarget<E extends Enum<E>> {
public final E enumId;
public final String field;
public final LongFunction<?> targetFunction;
public NamedTarget(E enumId, String matchingOpFieldName, LongFunction<?> value) {
this.enumId = enumId;
this.field = matchingOpFieldName;
this.targetFunction = value;
}
}

View File

@ -367,6 +367,11 @@ public class ParsedTemplateMap implements LongFunction<Map<String, ?>>, StaticFi
return getAsRequiredFunction(name, String.class);
}
public <V extends Enum<V>> Optional<LongFunction<V>> getAsOptionalEnumFunction(String name, Class<V> type) {
Optional<LongFunction<String>> nameFunc = this.getAsOptionalFunction(name, String.class);
return nameFunc.map((f) -> (l) -> Enum.valueOf(type, f.apply(l)));
}
/**
* Get the op field as a {@link LongFunction}
*
@ -378,11 +383,23 @@ public class ParsedTemplateMap implements LongFunction<Map<String, ?>>, StaticFi
public <V> Optional<LongFunction<V>> getAsOptionalFunction(String name, Class<? extends V> type) {
if (isStatic(name)) {
V value = getStaticValue(name);
return Optional.of((cycle) -> value);
} else if (isDefinedDynamic(name)) {
if (type.isAssignableFrom(value.getClass())) {
return Optional.of((cycle) -> value);
} else if (NBTypeConverter.canConvert(value, type)) {
V converted = NBTypeConverter.convert(value, type);
return Optional.of((cycle) -> converted);
} else {
throw new OpConfigError(
"function for '" + name + "' yielded a " + value.getClass().getCanonicalName()
+ " type, which is not assignable to a " + type.getCanonicalName() + "'"
);
}
} else if (isDynamic(name)) {
Object testValue = dynamics.get(name).apply(0L);
if (type.isAssignableFrom(testValue.getClass())) {
return Optional.of((LongFunction<V>) dynamics.get(name));
} else if (NBTypeConverter.canConvert(testValue, type)) {
return Optional.of(l -> NBTypeConverter.convert(dynamics.get(name).apply(l), type));
} else {
throw new OpConfigError(
"function for '" + name + "' yielded a " + testValue.getClass().getCanonicalName()
@ -393,8 +410,8 @@ public class ParsedTemplateMap implements LongFunction<Map<String, ?>>, StaticFi
}
}
public <V> LongFunction<? extends V> getAsRequiredFunction(String name, Class<? extends V> type) {
Optional<? extends LongFunction<? extends V>> sf = getAsOptionalFunction(name, type);
public <V> LongFunction<V> getAsRequiredFunction(String name, Class<? extends V> type) {
Optional<? extends LongFunction<V>> sf = getAsOptionalFunction(name, type);
return sf.orElseThrow(() -> new OpConfigError("The op field '" + name + "' is required, but it wasn't found in the op template."));
}
@ -439,7 +456,7 @@ public class ParsedTemplateMap implements LongFunction<Map<String, ?>>, StaticFi
} else {
throw new OpConfigError("Unable to compose string to object cache with non-String value of type " + defaultValue.getClass().getCanonicalName());
}
} else if (isDefinedDynamic(fieldname)) {
} else if (isDynamic(fieldname)) {
LongFunction<V> f = l -> get(fieldname, l);
V testValue = f.apply(0);
if (testValue instanceof String) {
@ -608,6 +625,55 @@ public class ParsedTemplateMap implements LongFunction<Map<String, ?>>, StaticFi
}
public <E extends Enum<E>, V> Optional<TypeAndTarget<E, V>> getOptionalTargetEnum(
Class<E> enumclass,
String typeFieldName,
String valueFieldName,
Class<V> valueClass
) {
if (isStatic(typeFieldName)) {
String enumValue = statics.get(typeFieldName).toString();
E verifiedEnumValue;
try {
verifiedEnumValue = Enum.valueOf(enumclass, enumValue);
} catch (IllegalArgumentException iae) {
throw new OpConfigError("type designator field '" + typeFieldName + "' had value of '" + enumValue + ", but this failed to match " +
"any of known types in " + EnumSet.allOf(enumclass));
}
if (isDefined(valueFieldName)) {
if (isStatic(typeFieldName)) {
return Optional.of(
new TypeAndTarget<E, V>(verifiedEnumValue, typeFieldName, l -> NBTypeConverter.convert(statics.get(valueFieldName), valueClass))
);
} else if (isDynamic(valueFieldName)) {
return Optional.of(
new TypeAndTarget<E, V>(verifiedEnumValue, typeFieldName, getAsRequiredFunction(valueFieldName, valueClass))
);
}
}
} else if (isDynamic(typeFieldName)) {
throw new OpConfigError("The op template field '" + typeFieldName + "' must be a static value. You can not vary it by cycle.");
}
return Optional.empty();
}
public <E extends Enum<E>, V> Optional<TypeAndTarget<E, V>> getOptionalTargetEnum(
Class<E> enumclass,
Class<V> valueClass,
String alternateTypeField,
String alternateValueField
) {
Optional<TypeAndTarget<E, V>> result = getOptionalTargetEnum(enumclass, valueClass);
if (result.isPresent()) {
return result;
}
return getOptionalTargetEnum(enumclass,alternateTypeField,alternateValueField,valueClass);
}
/**
* Given an enum of any type, return the enum value which is found in any of the field names of the op template,
* ignoring case and any non-word characters. This is useful for matching op templates to op types where the presence
@ -619,37 +685,58 @@ public class ParsedTemplateMap implements LongFunction<Map<String, ?>>, StaticFi
* @return Optionally, an enum value which matches, or {@link Optional#empty()}
* @throws OpConfigError if more than one field matches
*/
public <E extends Enum<E>> Optional<NamedTarget<E>> getOptionalTypeFromEnum(Class<E> enumclass) {
List<NamedTarget<E>> matched = new ArrayList<>();
public <E extends Enum<E>, V> Optional<TypeAndTarget<E, V>> getOptionalTargetEnum(
Class<E> enumclass,
Class<? extends V> valueClass
) {
List<TypeAndTarget<E, V>> matched = new ArrayList<>();
for (E e : EnumSet.allOf(enumclass)) {
String lowerenum = e.name().toLowerCase(Locale.ROOT).replaceAll("[^\\w]", "");
for (String s : statics.keySet()) {
String lowerkey = s.toLowerCase(Locale.ROOT).replaceAll("[^\\w]", "");
if (lowerkey.equals(lowerenum)) {
matched.add(new NamedTarget<>(e, s, null));
matched.add(new TypeAndTarget<>(e, s, null));
}
}
for (String s : dynamics.keySet()) {
String lowerkey = s.toLowerCase(Locale.ROOT).replaceAll("[^\\w]", "");
if (lowerkey.equals(lowerenum)) {
matched.add(new NamedTarget<>(e, s, null));
matched.add(new TypeAndTarget<>(e, s, null));
}
}
}
if (matched.size() == 1) {
NamedTarget<E> prototype = matched.get(0);
LongFunction<? extends String> asFunction = getAsRequiredFunction(prototype.field);
return Optional.of(new NamedTarget<>(prototype.enumId, prototype.field, asFunction));
}
if (matched.size() > 1) {
TypeAndTarget<E, V> prototype = matched.get(0);
LongFunction<V> asFunction = getAsRequiredFunction(prototype.field, valueClass);
return Optional.of(new TypeAndTarget<E, V>(prototype.enumId, prototype.field, asFunction));
} else if (matched.size() > 1) {
throw new OpConfigError("Multiple matches were found from op template fieldnames ["
+ getDefinedNames() + "] to possible enums: [" + EnumSet.allOf(enumclass) + "]");
}
return Optional.empty();
}
public <E extends Enum<E>> NamedTarget<E> getRequiredTypeFromEnum(Class<E> enumclass) {
Optional<NamedTarget<E>> typeFromEnum = getOptionalTypeFromEnum(enumclass);
public <E extends Enum<E>,V> TypeAndTarget<E,V> getTargetEnum(
Class<E> enumclass,
Class<V> valueClass,
String tname,
String vname
) {
Optional<TypeAndTarget<E, V>> optionalMappedEnum = getOptionalTargetEnum(enumclass, valueClass);
if (optionalMappedEnum.isPresent()) {
return optionalMappedEnum.get();
}
Optional<TypeAndTarget<E, V>> optionalSpecifiedEnum = getOptionalTargetEnum(enumclass, tname, vname, valueClass);
if (optionalSpecifiedEnum.isPresent()) {
return optionalSpecifiedEnum.get();
}
throw new OpConfigError("Unable to map the type and target for possible values " + EnumSet.allOf(enumclass) +" either by key or by fields " + tname + " and " + vname + ". " +
"Fields considered: static:" + statics.keySet() + " dynamic:" + dynamics.keySet());
}
public <E extends Enum<E>, V> TypeAndTarget<E, V> getTargetEnum(Class<E> enumclass, Class<V> valueClass) {
Optional<TypeAndTarget<E, V>> typeFromEnum = getOptionalTargetEnum(enumclass, valueClass);
return typeFromEnum.orElseThrow(
() -> {
@ -666,12 +753,12 @@ public class ParsedTemplateMap implements LongFunction<Map<String, ?>>, StaticFi
/**
* Map a named op field to an enum
*
* @param enumclass The type of enum to look within
* @param fieldname The field name to look for
* @param <E> The generic type of the enum
* @param enumclass The type of enum to look within
* @param fieldname The field name to look for
* @param <E> The generic type of the enum
* @return An optional enum value
*/
public <E extends Enum<E>> Optional<E> getOptionalEnumFromField(Class<E> enumclass,String fieldname) {
public <E extends Enum<E>> Optional<E> getOptionalEnumFromField(Class<E> enumclass, String fieldname) {
Optional<String> enumField = getOptionalStaticConfig(fieldname, String.class);
if (enumField.isEmpty()) {
@ -696,4 +783,5 @@ public class ParsedTemplateMap implements LongFunction<Map<String, ?>>, StaticFi
return Optional.empty();
}
}

View File

@ -0,0 +1,59 @@
package io.nosqlbench.engine.api.templating;
import java.util.function.LongFunction;
/**
* <p>A convenient pattern for users to specify a command is that of <em>type and target</em>. This
* emphasizes that users often need to specify what kind of action to take, and what subject to
* take the action on. This concept pervades programming, exmplified by a simple <pre>{@code function(object) } call</pre>
* </p>
*
* <p>
* To facilitate this pattern in op templates with the help of type safety, this helper type allows
* for the scanning of a map for a matching enum field. If any map key matches one of the possible
* enum values, case-insensitively, and with only a single match, then the matching enum value is
* taken as a specific type of action, and the matching value in the map is taken as the intended target.
*</p>
*
* <p>Further, the target may be indirect, such as a binding, rather than a specific literal or structured
* value. In such cases, only a lambda-style function may be available. The provided <em>targetFunction</em> is
* a {@link LongFunction} of Object which can be called to return an associated target value once the
* cycle value is known.</p>
*
* For example, with an enum like <pre>{@code
* public enum LandMovers {
* BullDozer,
* DumpTruck
* }
* }</pre>
*
* and a parsed op like <pre>{@code
* (json)
* {
* "op": {
* "bulldozer": "{dozerid}"
* }
* }
*
* (yaml)
* op:
* bulldozer: "{dozerid}
* }</pre>
* the result will be returned with the following: <pre>{@code
* enumId: (Enum field) BullDozer
* field: (String) bulldozer
* targetFunction: (long l) -> template function for {dozerid}
* }</pre>
* @param <E>
*/
public class TypeAndTarget<E extends Enum<E>,T> {
public final E enumId;
public final String field;
public final LongFunction<T> targetFunction;
public TypeAndTarget(E enumId, String matchingOpFieldName, LongFunction<T> value) {
this.enumId = enumId;
this.field = matchingOpFieldName;
this.targetFunction = value;
}
}