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

@@ -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;
}
}