Data type support for Expression / MultiExpression (#190)

pull/199/head
Brian Sperlongano 2022-04-28 07:08:00 -04:00 zatwierdzone przez GitHub
rodzic 59a48e1620
commit ffb157414e
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
9 zmienionych plików z 281 dodań i 87 usunięć

Wyświetl plik

@ -431,7 +431,7 @@ public class Generate {
/** Imposm3 "mapping" to filter OSM elements that should appear in this "table". */
public static final Expression MAPPING = %s;
""".formatted(
mappingExpression
mappingExpression.generateJavaCode()
);
String tableName = "osm_" + key;
String className = lowerUnderscoreToUpperCamel(tableName);
@ -652,7 +652,7 @@ public class Generate {
/** Returns java code that will recreate an {@link MultiExpression} identical to {@code mapping}. */
private static String generateJavaCode(MultiExpression<String> mapping) {
return "MultiExpression.of(List.of(" + mapping.expressions().stream()
.map(s -> "MultiExpression.entry(%s, %s)".formatted(Format.quote(s.result()), s.expression()))
.map(s -> "MultiExpression.entry(%s, %s)".formatted(Format.quote(s.result()), s.expression().generateJavaCode()))
.collect(joining(", ")) + "))";
}

Wyświetl plik

@ -5,6 +5,7 @@ import static com.onthegomap.planetiler.TestUtils.newLineString;
import static com.onthegomap.planetiler.TestUtils.newPoint;
import static com.onthegomap.planetiler.TestUtils.rectangle;
import static com.onthegomap.planetiler.basemap.BasemapProfile.OSM_SOURCE;
import static com.onthegomap.planetiler.basemap.util.Utils.coalesce;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
@ -23,6 +24,7 @@ import com.onthegomap.planetiler.stats.Stats;
import com.onthegomap.planetiler.util.Translations;
import com.onthegomap.planetiler.util.Wikidata;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -41,10 +43,15 @@ public abstract class AbstractLayerTest {
final FeatureCollector.Factory featureCollectorFactory = new FeatureCollector.Factory(params, stats);
static void assertFeatures(int zoom, List<Map<String, Object>> expected, Iterable<FeatureCollector.Feature> actual) {
List<FeatureCollector.Feature> actualList = StreamSupport.stream(actual.spliterator(), false).toList();
assertEquals(expected.size(), actualList.size(), () -> "size: " + actualList);
for (int i = 0; i < expected.size(); i++) {
assertSubmap(expected.get(i), TestUtils.toMap(actualList.get(i), zoom));
// ensure both are sorted by layer
var expectedList =
expected.stream().sorted(Comparator.comparing(d -> coalesce(d.get("_layer"), "").toString())).toList();
var actualList = StreamSupport.stream(actual.spliterator(), false)
.sorted(Comparator.comparing(FeatureCollector.Feature::getLayer))
.toList();
assertEquals(expectedList.size(), actualList.size(), () -> "size: " + actualList);
for (int i = 0; i < expectedList.size(); i++) {
assertSubmap(expectedList.get(i), TestUtils.toMap(actualList.get(i), zoom));
}
}

Wyświetl plik

@ -164,7 +164,7 @@ class WaterTest extends AbstractLayerTest {
}
@Test
void testRiverk() {
void testRiver() {
assertFeatures(11, List.of(Map.of(
"class", "river",
"_layer", "water",

Wyświetl plik

@ -22,7 +22,7 @@ import org.locationtech.jts.geom.Geometry;
*/
public class BasemapMapping {
public static void main(String[] args) throws Exception {
public static void main(String[] args) {
var profile = new BasemapProfile(Translations.nullProvider(List.of()), PlanetilerConfig.defaults(),
Stats.inMemory());
var random = new Random(0);

Wyświetl plik

@ -1,12 +1,14 @@
package com.onthegomap.planetiler.expression;
import com.onthegomap.planetiler.reader.SourceFeature;
import com.onthegomap.planetiler.reader.WithTags;
import com.onthegomap.planetiler.util.Format;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@ -17,7 +19,7 @@ import java.util.stream.Stream;
* <p>
* Calling {@code toString()} on any expression will generate code that can be used to recreate an identical copy of the
* original expression, assuming that the generated code includes:
*
*
* <pre>
* {@code
* import static com.onthegomap.planetiler.expression.Expression.*;
@ -32,27 +34,9 @@ public interface Expression {
String RELATION_MEMBER_TYPE = "relation_member";
Set<String> supportedTypes = Set.of(LINESTRING_TYPE, POINT_TYPE, POLYGON_TYPE, RELATION_MEMBER_TYPE);
Expression TRUE = new Expression() {
@Override
public String toString() {
return "TRUE";
}
@Override
public boolean evaluate(SourceFeature input, List<String> matchKeys) {
return true;
}
};
Expression FALSE = new Expression() {
public String toString() {
return "FALSE";
}
@Override
public boolean evaluate(SourceFeature input, List<String> matchKeys) {
return false;
}
};
Expression TRUE = new Constant(true, "TRUE");
Expression FALSE = new Constant(false, "FALSE");
BiFunction<WithTags, String, Object> GET_TAG = WithTags::getTag;
static And and(Expression... children) {
return and(List.of(children));
@ -79,7 +63,7 @@ public interface Expression {
* <p>
* {@code values} can contain exact matches, "%text%" to match any value containing "text", or "" to match any value.
*/
static MatchAny matchAny(String field, String... values) {
static MatchAny matchAny(String field, Object... values) {
return matchAny(field, List.of(values));
}
@ -88,8 +72,29 @@ public interface Expression {
* <p>
* {@code values} can contain exact matches, "%text%" to match any value containing "text", or "" to match any value.
*/
static MatchAny matchAny(String field, List<String> values) {
return new MatchAny(field, values);
static MatchAny matchAny(String field, List<?> values) {
return new MatchAny(field, GET_TAG, values);
}
/**
* Returns an expression that evaluates to true if the value for {@code field} tag is any of {@code values}, when
* considering the tag as a specified data type and then converted to a string.
* <p>
* {@code values} can contain exact matches, "%text%" to match any value containing "text", or "" to match any value.
*/
static MatchAny matchAnyTyped(String field, BiFunction<WithTags, String, Object> typeGetter, Object... values) {
return matchAnyTyped(field, typeGetter, List.of(values));
}
/**
* Returns an expression that evaluates to true if the value for {@code field} tag is any of {@code values}, when
* considering the tag as a specified data type and then converted to a string.
* <p>
* {@code values} can contain exact matches, "%text%" to match any value containing "text", or "" to match any value.
*/
static MatchAny matchAnyTyped(String field, BiFunction<WithTags, String, Object> typeGetter,
List<?> values) {
return new MatchAny(field, typeGetter, values);
}
/** Returns an expression that evaluates to true if the element has any value for tag {@code field}. */
@ -115,8 +120,8 @@ public interface Expression {
return new MatchType(type);
}
private static String listToString(List<?> items) {
return items.stream().map(Object::toString).collect(Collectors.joining(", "));
private static String generateJavaCodeList(List<Expression> items) {
return items.stream().map(Expression::generateJavaCode).collect(Collectors.joining(", "));
}
private static Expression simplify(Expression initial) {
@ -232,17 +237,33 @@ public interface Expression {
* @param matchKeys list that this method call will add any key to that was responsible for triggering the match
* @return true if this expression matches the input element
*/
boolean evaluate(SourceFeature input, List<String> matchKeys);
boolean evaluate(WithTags input, List<String> matchKeys);
/** Returns Java code that can be used to reconstruct this expression. */
String generateJavaCode();
/** A constant boolean value. */
record Constant(boolean value, @Override String generateJavaCode) implements Expression {
@Override
public String toString() {
return generateJavaCode;
}
@Override
public boolean evaluate(WithTags input, List<String> matchKeys) {
return value;
}
}
record And(List<Expression> children) implements Expression {
@Override
public String toString() {
return "and(" + listToString(children) + ")";
public String generateJavaCode() {
return "and(" + generateJavaCodeList(children) + ")";
}
@Override
public boolean evaluate(SourceFeature input, List<String> matchKeys) {
public boolean evaluate(WithTags input, List<String> matchKeys) {
for (Expression child : children) {
if (!child.evaluate(input, matchKeys)) {
matchKeys.clear();
@ -256,12 +277,12 @@ public interface Expression {
record Or(List<Expression> children) implements Expression {
@Override
public String toString() {
return "or(" + listToString(children) + ")";
public String generateJavaCode() {
return "or(" + generateJavaCodeList(children) + ")";
}
@Override
public boolean evaluate(SourceFeature input, List<String> matchKeys) {
public boolean evaluate(WithTags input, List<String> matchKeys) {
int size = children.size();
// Optimization: this method consumes the most time when matching against input elements, and
// iterating through this list by index is slightly faster than an enhanced for loop
@ -297,12 +318,12 @@ public interface Expression {
record Not(Expression child) implements Expression {
@Override
public String toString() {
return "not(" + child + ")";
public String generateJavaCode() {
return "not(" + child.generateJavaCode() + ")";
}
@Override
public boolean evaluate(SourceFeature input, List<String> matchKeys) {
public boolean evaluate(WithTags input, List<String> matchKeys) {
return !child.evaluate(input, new ArrayList<>());
}
}
@ -317,28 +338,30 @@ public interface Expression {
* @param matchWhenMissing if {@code values} contained ""
*/
record MatchAny(
String field, List<String> values, Set<String> exactMatches, List<String> wildcards, boolean matchWhenMissing
String field, List<?> values, Set<String> exactMatches, List<String> wildcards, boolean matchWhenMissing,
BiFunction<WithTags, String, Object> valueGetter
) implements Expression {
private static final Pattern containsPattern = Pattern.compile("^%(.*)%$");
MatchAny(String field, List<String> values) {
MatchAny(String field, BiFunction<WithTags, String, Object> valueGetter, List<?> values) {
this(field, values,
values.stream().filter(v -> !v.contains("%")).collect(Collectors.toSet()),
values.stream().filter(v -> v.contains("%")).map(val -> {
values.stream().map(Object::toString).filter(v -> !v.contains("%")).collect(Collectors.toSet()),
values.stream().map(Object::toString).filter(v -> v.contains("%")).map(val -> {
var matcher = containsPattern.matcher(val);
if (!matcher.matches()) {
throw new IllegalArgumentException("wildcards must start/end with %: " + val);
}
return matcher.group(1);
}).toList(),
values.contains("")
values.contains(""),
valueGetter
);
}
@Override
public boolean evaluate(SourceFeature input, List<String> matchKeys) {
Object value = input.getTag(field);
public boolean evaluate(WithTags input, List<String> matchKeys) {
Object value = valueGetter.apply(input, field);
if (value == null) {
return matchWhenMissing;
} else {
@ -358,9 +381,23 @@ public interface Expression {
}
@Override
public String toString() {
return "matchAny(" + Format.quote(field) + ", " + values.stream().map(Format::quote)
.collect(Collectors.joining(", ")) + ")";
public String generateJavaCode() {
// java code generation only needed for the simple cases used by openmaptiles schema generation
List<String> valueStrings = new ArrayList<>();
if (GET_TAG != valueGetter) {
throw new UnsupportedOperationException("Code generation only supported for default getTag");
}
for (var value : values) {
if (value instanceof String string) {
valueStrings.add(Format.quote(string));
} else {
throw new UnsupportedOperationException("Code generation only supported for string values, found: " +
value.getClass().getCanonicalName() + " " + value);
}
}
return "matchAny(" + Format.quote(field) + ", " + String.join(", ", valueStrings) + ")";
}
}
@ -368,12 +405,12 @@ public interface Expression {
record MatchField(String field) implements Expression {
@Override
public String toString() {
public String generateJavaCode() {
return "matchField(" + Format.quote(field) + ")";
}
@Override
public boolean evaluate(SourceFeature input, List<String> matchKeys) {
public boolean evaluate(WithTags input, List<String> matchKeys) {
if (input.hasTag(field)) {
matchKeys.add(field);
return true;
@ -388,19 +425,23 @@ public interface Expression {
record MatchType(String type) implements Expression {
@Override
public String toString() {
public String generateJavaCode() {
return "matchType(" + Format.quote(type) + ")";
}
@Override
public boolean evaluate(SourceFeature input, List<String> matchKeys) {
return switch (type) {
case LINESTRING_TYPE -> input.canBeLine();
case POLYGON_TYPE -> input.canBePolygon();
case POINT_TYPE -> input.isPoint();
case RELATION_MEMBER_TYPE -> input.hasRelationInfo();
default -> false;
};
public boolean evaluate(WithTags input, List<String> matchKeys) {
if (input instanceof SourceFeature sourceFeature) {
return switch (type) {
case LINESTRING_TYPE -> sourceFeature.canBeLine();
case POLYGON_TYPE -> sourceFeature.canBePolygon();
case POINT_TYPE -> sourceFeature.isPoint();
case RELATION_MEMBER_TYPE -> sourceFeature.hasRelationInfo();
default -> false;
};
} else {
return false;
}
}
}
}

Wyświetl plik

@ -8,15 +8,17 @@ import static com.onthegomap.planetiler.geo.GeoUtils.EMPTY_GEOMETRY;
import com.onthegomap.planetiler.reader.SimpleFeature;
import com.onthegomap.planetiler.reader.SourceFeature;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
/**
* A list of {@link Expression Expressions} to evaluate on input elements.
@ -31,6 +33,8 @@ import java.util.function.Predicate;
*/
public record MultiExpression<T> (List<Entry<T>> expressions) {
private static final Comparator<WithId> BY_ID = Comparator.comparingInt(WithId::id);
public static <T> MultiExpression<T> of(List<Entry<T>> expressions) {
return new MultiExpression<>(expressions);
}
@ -51,7 +55,7 @@ public record MultiExpression<T> (List<Entry<T>> expressions) {
visited[expressionValue.id] = true;
List<String> matchKeys = new ArrayList<>();
if (expressionValue.expression().evaluate(input, matchKeys)) {
result.add(new Match<>(expressionValue.result, matchKeys));
result.add(new Match<>(expressionValue.result, matchKeys, expressionValue.id));
}
}
}
@ -79,9 +83,9 @@ public record MultiExpression<T> (List<Entry<T>> expressions) {
*/
private static void getRelevantMissingKeys(Expression exp, Consumer<String> acceptKey) {
if (exp instanceof Expression.And and) {
and.children().forEach(child -> getRelevantKeys(child, acceptKey));
and.children().forEach(child -> getRelevantMissingKeys(child, acceptKey));
} else if (exp instanceof Expression.Or or) {
or.children().forEach(child -> getRelevantKeys(child, acceptKey));
or.children().forEach(child -> getRelevantMissingKeys(child, acceptKey));
} else if (exp instanceof Expression.Not) {
// ignore anything that's purely used as a filter
} else if (exp instanceof Expression.MatchAny any && any.matchWhenMissing()) {
@ -100,7 +104,7 @@ public record MultiExpression<T> (List<Entry<T>> expressions) {
}
/** Returns a copy of this multi-expression that replaces every expression using {@code mapper}. */
public MultiExpression<T> map(Function<Expression, Expression> mapper) {
public MultiExpression<T> map(UnaryOperator<Expression> mapper) {
return new MultiExpression<>(
expressions.stream()
.map(entry -> entry(entry.result, mapper.apply(entry.expression).simplify()))
@ -159,7 +163,7 @@ public record MultiExpression<T> (List<Entry<T>> expressions) {
/** Returns all data values associated with expressions that match an input element. */
default List<T> getMatches(SourceFeature input) {
List<Match<T>> matches = getMatchesWithTriggers(input);
return matches.stream().map(d -> d.match).toList();
return matches.stream().sorted(BY_ID).map(d -> d.match).toList();
}
/**
@ -189,6 +193,10 @@ public record MultiExpression<T> (List<Entry<T>> expressions) {
}
}
private interface WithId {
int id();
}
private static class EmptyIndex<T> implements Index<T> {
@Override
@ -214,26 +222,37 @@ public record MultiExpression<T> (List<Entry<T>> expressions) {
private final List<Map.Entry<String, List<EntryWithId<T>>>> keyToExpressionsList;
// expressions that should match when certain tags are *not* present on an input element
private final List<Map.Entry<String, List<EntryWithId<T>>>> missingKeyToExpressionList;
// expressions that match a constant true input element
private final List<EntryWithId<T>> constantTrueExpressionList;
private KeyIndex(MultiExpression<T> expressions) {
AtomicInteger ids = new AtomicInteger();
int id = 1;
// build the indexes
Map<String, Set<EntryWithId<T>>> keyToExpressions = new HashMap<>();
Map<String, Set<EntryWithId<T>>> missingKeyToExpressions = new HashMap<>();
List<EntryWithId<T>> constants = new ArrayList<>();
for (var entry : expressions.expressions) {
Expression expression = entry.expression;
EntryWithId<T> expressionValue = new EntryWithId<>(entry.result, expression, ids.incrementAndGet());
EntryWithId<T> expressionValue = new EntryWithId<>(entry.result, expression, id++);
getRelevantKeys(expression,
key -> keyToExpressions.computeIfAbsent(key, k -> new HashSet<>()).add(expressionValue));
getRelevantMissingKeys(expression,
key -> missingKeyToExpressions.computeIfAbsent(key, k -> new HashSet<>()).add(expressionValue));
if (expression.equals(TRUE)) {
constants.add(expressionValue);
}
}
keyToExpressionsMap = new HashMap<>();
keyToExpressions.forEach((key, value) -> keyToExpressionsMap.put(key, value.stream().toList()));
keyToExpressionsList = keyToExpressionsMap.entrySet().stream().toList();
// create immutable copies for fast iteration at matching time
constantTrueExpressionList = List.copyOf(constants);
keyToExpressionsMap = keyToExpressions.entrySet().stream().collect(Collectors.toUnmodifiableMap(
Map.Entry::getKey,
entry -> entry.getValue().stream().toList()
));
keyToExpressionsList = List.copyOf(keyToExpressionsMap.entrySet());
missingKeyToExpressionList = missingKeyToExpressions.entrySet().stream()
.map(entry -> Map.entry(entry.getKey(), entry.getValue().stream().toList())).toList();
numExpressions = ids.incrementAndGet();
numExpressions = id;
}
/** Lookup matches in this index for expressions that match a certain type. */
@ -241,6 +260,9 @@ public record MultiExpression<T> (List<Entry<T>> expressions) {
public List<Match<T>> getMatchesWithTriggers(SourceFeature input) {
List<Match<T>> result = new ArrayList<>();
boolean[] visited = new boolean[numExpressions];
for (var entry : constantTrueExpressionList) {
result.add(new Match<>(entry.result, List.of(), entry.id));
}
for (var entry : missingKeyToExpressionList) {
if (!input.hasTag(entry.getKey())) {
visitExpressions(input, result, visited, entry.getValue());
@ -308,7 +330,7 @@ public record MultiExpression<T> (List<Entry<T>> expressions) {
}
/** An expression/value pair with unique ID to store whether we evaluated it yet. */
private record EntryWithId<T> (T result, Expression expression, int id) {}
private record EntryWithId<T> (T result, Expression expression, @Override int id) implements WithId {}
/**
* An {@code expression} to evaluate on input elements and {@code result} value to return when the element matches.
@ -316,5 +338,5 @@ public record MultiExpression<T> (List<Entry<T>> expressions) {
public record Entry<T> (T result, Expression expression) {}
/** The result when an expression matches, along with the input element tag {@code keys} that triggered the match. */
public record Match<T> (T match, List<String> keys) {}
public record Match<T> (T match, List<String> keys, @Override int id) implements WithId {}
}

Wyświetl plik

@ -149,11 +149,11 @@ public class Format {
}
/** Returns Java code that can re-create {@code string}: {@code null} if null, or {@code "contents"} if not empty. */
public static String quote(String string) {
public static String quote(Object string) {
if (string == null) {
return "null";
}
return '"' + StringEscapeUtils.escapeJava(string) + '"';
return '"' + StringEscapeUtils.escapeJava(string.toString()) + '"';
}
/** Returns an openstreetmap.org map link for a lat/lon */

Wyświetl plik

@ -3,8 +3,10 @@ package com.onthegomap.planetiler.expression;
import static com.onthegomap.planetiler.expression.Expression.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import com.onthegomap.planetiler.reader.WithTags;
import java.util.Set;
import org.junit.jupiter.api.Test;
@ -101,4 +103,13 @@ class ExpressionTest {
assertFalse(matchCD.contains(e -> e.equals(matchAB)));
assertFalse(or(not(matchCD)).contains(e -> e.equals(matchAB)));
}
@Test
void testStringifyExpression() {
//Ensure Expression.toString() returns valid Java code
assertEquals("matchAny(\"key\", \"true\")", matchAny("key", "true").generateJavaCode());
assertEquals("matchAny(\"key\", \"foo\")", matchAny("key", "foo").generateJavaCode());
var expression = matchAnyTyped("key", WithTags::getDirection, 1);
assertThrows(UnsupportedOperationException.class, expression::generateJavaCode);
}
}

Wyświetl plik

@ -6,10 +6,12 @@ import static com.onthegomap.planetiler.TestUtils.rectangle;
import static com.onthegomap.planetiler.expression.Expression.*;
import static com.onthegomap.planetiler.expression.MultiExpression.entry;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import com.onthegomap.planetiler.reader.SimpleFeature;
import com.onthegomap.planetiler.reader.SourceFeature;
import com.onthegomap.planetiler.reader.WithTags;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
@ -47,6 +49,62 @@ class MultiExpressionTest {
assertSameElements(List.of(), index.getMatches(featureWithTags()));
}
@Test
void testSingleElementBooleanTrue() {
var index = MultiExpression.of(List.of(
entry("a", matchAnyTyped("key", WithTags::getBoolean, true))
)).index();
assertSameElements(List.of("a"), index.getMatches(featureWithTags("key", "true")));
assertSameElements(List.of("a"), index.getMatches(featureWithTags("key", "yes")));
assertSameElements(List.of("a"), index.getMatches(featureWithTags("key", "1")));
assertSameElements(List.of("a"), index.getMatches(featureWithTags("key", "true", "otherkey", "othervalue")));
assertSameElements(List.of(), index.getMatches(featureWithTags("key2", "true", "key3", "value")));
assertSameElements(List.of(), index.getMatches(featureWithTags("key2", "value")));
assertSameElements(List.of(), index.getMatches(featureWithTags("key", "false")));
assertSameElements(List.of(), index.getMatches(featureWithTags()));
}
@Test
void testSingleElementBooleanFalse() {
var index = MultiExpression.of(List.of(
entry("a", matchAnyTyped("key", WithTags::getBoolean, false))
)).index();
assertSameElements(List.of("a"), index.getMatches(featureWithTags("key", "false")));
assertSameElements(List.of("a"), index.getMatches(featureWithTags("key", "no")));
assertSameElements(List.of("a"), index.getMatches(featureWithTags("key", "0")));
assertSameElements(List.of("a"), index.getMatches(featureWithTags("key", "false", "otherkey", "othervalue")));
assertSameElements(List.of(), index.getMatches(featureWithTags("key2", "false", "key3", "value")));
assertSameElements(List.of(), index.getMatches(featureWithTags("key2", "value")));
assertSameElements(List.of(), index.getMatches(featureWithTags("key", "true")));
assertSameElements(List.of(), index.getMatches(featureWithTags()));
}
@Test
void testSingleElementLong() {
var index = MultiExpression.of(List.of(
entry("a", matchAnyTyped("key", WithTags::getLong, 42))
)).index();
assertSameElements(List.of("a"), index.getMatches(featureWithTags("key", "42")));
assertSameElements(List.of("a"), index.getMatches(featureWithTags("key", "42", "otherkey", "othervalue")));
assertSameElements(List.of(), index.getMatches(featureWithTags("key2", "42", "key3", "value")));
assertSameElements(List.of(), index.getMatches(featureWithTags("key2", "value")));
assertSameElements(List.of(), index.getMatches(featureWithTags("key", "99")));
assertSameElements(List.of(), index.getMatches(featureWithTags()));
}
@Test
void testSingleElementDirection() {
var index = MultiExpression.of(List.of(
entry("a", matchAnyTyped("key", WithTags::getDirection, 1))
)).index();
assertSameElements(List.of("a"), index.getMatches(featureWithTags("key", "yes")));
assertSameElements(List.of("a"), index.getMatches(featureWithTags("key", "1", "otherkey", "othervalue")));
assertSameElements(List.of(), index.getMatches(featureWithTags("key2", "1", "key3", "value")));
assertSameElements(List.of(), index.getMatches(featureWithTags("key2", "value")));
assertSameElements(List.of(), index.getMatches(featureWithTags("key", "99")));
assertSameElements(List.of(), index.getMatches(featureWithTags()));
}
@Test
void testBlankStringTreatedAsNotMatch() {
var index = MultiExpression.of(List.of(
@ -75,6 +133,44 @@ class MultiExpressionTest {
assertSameElements(List.of(), index.getMatches(featureWithTags()));
}
@Test
void testStaticBooleanMatch() {
var index = MultiExpression.of(List.of(entry("t", TRUE))).index();
assertTrue(index.matches(featureWithTags("key", "value")));
index = MultiExpression.of(List.of(entry("f", FALSE))).index();
assertFalse(index.matches(featureWithTags("key", "value")));
index = MultiExpression.of(List.of(
entry("a", matchField("key")),
entry("t", TRUE),
entry("f", FALSE)
)).index();
assertSameElements(List.of("a", "t"), index.getMatches(featureWithTags("key", "value")));
index = MultiExpression.of(List.of(
entry("a", matchField("key")),
entry("t1", TRUE),
entry("t2", TRUE),
entry("t3", TRUE),
entry("f1", FALSE),
entry("f2", FALSE),
entry("f3", FALSE)
)).index();
assertSameElements(List.of("a", "t1", "t2", "t3"), index.getMatches(featureWithTags("key", "value")));
index = MultiExpression.of(List.of(
entry("t3", TRUE),
entry("t2", TRUE),
entry("t1", TRUE),
entry("a", matchField("key"))
)).index();
assertSameElements(List.of("t3", "t2", "t1", "a"), index.getMatches(featureWithTags("key", "value")));
}
@Test
void testWildcard() {
var index = MultiExpression.of(List.of(
@ -91,6 +187,23 @@ class MultiExpressionTest {
assertSameElements(List.of(), index.getMatches(featureWithTags()));
}
@Test
void testMultipleMatches() {
var feature = featureWithTags("a", "b", "c", "d");
var index = MultiExpression.of(List.of(
entry("a", matchAny("a", "b")),
entry("b", matchAny("c", "d"))
)).index();
var index2 = MultiExpression.of(List.of(
entry("b", matchAny("c", "d")),
entry("a", matchAny("a", "b"))
)).index();
assertSameElements(List.of("a", "b"), index.getMatches(feature));
assertSameElements(List.of("b", "a"), index2.getMatches(feature));
assertEquals("a", index.getOrElse(feature, "miss"));
assertEquals("b", index2.getOrElse(feature, "miss"));
}
@Test
void testMultipleWildcardsMixedWithExacts() {
var index = MultiExpression.of(List.of(
@ -191,15 +304,15 @@ class MultiExpressionTest {
))
)).index();
assertSameElements(List.of(new MultiExpression.Match<>(
"a", List.of("key1")
"a", List.of("key1"), 1
)), index.getMatchesWithTriggers(featureWithTags("key1", "val1")));
assertSameElements(List.of(new MultiExpression.Match<>(
"a", List.of("key2")
"a", List.of("key2"), 1
), new MultiExpression.Match<>(
"b", List.of("key2")
"b", List.of("key2"), 2
)), index.getMatchesWithTriggers(featureWithTags("key2", "val2")));
assertSameElements(List.of(new MultiExpression.Match<>(
"b", List.of("key3")
"b", List.of("key3"), 2
)), index.getMatchesWithTriggers(featureWithTags("key3", "val3")));
}
@ -218,7 +331,7 @@ class MultiExpressionTest {
))
)).index();
assertSameElements(List.of(new MultiExpression.Match<>(
"a", List.of("key1", "key3")
"a", List.of("key1", "key3"), 1
)), index.getMatchesWithTriggers(featureWithTags("key1", "val1", "key3", "val3")));
}