kopia lustrzana https://github.com/onthegomap/planetiler
158 wiersze
7.0 KiB
Java
158 wiersze
7.0 KiB
Java
package com.onthegomap.planetiler.custommap;
|
|
|
|
import static com.onthegomap.planetiler.custommap.expression.ConfigExpression.*;
|
|
import static com.onthegomap.planetiler.custommap.expression.ConfigExpressionScript.unescape;
|
|
|
|
import com.onthegomap.planetiler.custommap.expression.ConfigExpression;
|
|
import com.onthegomap.planetiler.custommap.expression.ConfigExpressionScript;
|
|
import com.onthegomap.planetiler.custommap.expression.ParseException;
|
|
import com.onthegomap.planetiler.custommap.expression.ScriptContext;
|
|
import com.onthegomap.planetiler.custommap.expression.ScriptEnvironment;
|
|
import com.onthegomap.planetiler.expression.DataType;
|
|
import com.onthegomap.planetiler.expression.MultiExpression;
|
|
import com.onthegomap.planetiler.util.Memoized;
|
|
import com.onthegomap.planetiler.util.Try;
|
|
import java.util.ArrayList;
|
|
import java.util.Collection;
|
|
import java.util.HashMap;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Objects;
|
|
import java.util.Set;
|
|
import java.util.stream.Collectors;
|
|
import java.util.stream.Stream;
|
|
|
|
/**
|
|
* Parses user-defined YAML into an {@link ConfigExpressionParser expression} that can be evaluated against an input
|
|
* feature.
|
|
*
|
|
* @param <I> Input type of the expression
|
|
*/
|
|
public class ConfigExpressionParser<I extends ScriptContext> {
|
|
|
|
private static final Memoized<EvaluateInput, ?> MEMOIZED = Memoized.memoize(arg -> ConfigExpressionParser
|
|
.parse(arg.expression, TagValueProducer.EMPTY, arg.root.description(), arg.clazz).apply(arg.root));
|
|
private final TagValueProducer tagValueProducer;
|
|
private final ScriptEnvironment<I> input;
|
|
|
|
public ConfigExpressionParser(TagValueProducer tagValueProducer, ScriptEnvironment<I> input) {
|
|
this.tagValueProducer = tagValueProducer;
|
|
this.input = input;
|
|
}
|
|
|
|
/**
|
|
* Returns an expression parsed from user-defined YAML that can be evaluated against an input of type {@code <I>} and
|
|
* returns output of type {@code <O>}.
|
|
*
|
|
* @param object a map or list of tag criteria
|
|
* @param tagValueProducer a TagValueProducer
|
|
* @param <I> Input type of the expression
|
|
* @param <O> Return type of the expression
|
|
*/
|
|
public static <I extends ScriptContext, O> ConfigExpression<I, O> parse(Object object,
|
|
TagValueProducer tagValueProducer, ScriptEnvironment<I> context, Class<O> outputClass) {
|
|
return new ConfigExpressionParser<>(tagValueProducer, context).parse(object, outputClass).simplify();
|
|
}
|
|
|
|
/**
|
|
* Attempts to evaluate {@code expression} from a yaml config, using only globally-available environmental variables
|
|
* from the {@code root} context.
|
|
*/
|
|
public static <T> Try<T> tryStaticEvaluate(Contexts.Root root, Object expression, Class<T> resultType) {
|
|
if (expression == null) {
|
|
return Try.success(null);
|
|
}
|
|
return MEMOIZED.tryApply(new EvaluateInput(root, expression, resultType), resultType);
|
|
}
|
|
|
|
private <O> ConfigExpression<I, O> parse(Object object, Class<O> output) {
|
|
if (object == null) {
|
|
return ConfigExpression.constOf(null);
|
|
} else if (ConfigExpressionScript.isScript(object)) {
|
|
return ConfigExpression.script(signature(output), ConfigExpressionScript.extractScript(object));
|
|
} else if (object instanceof Collection<?> collection) {
|
|
return parseMatch(collection, true, output);
|
|
} else if (object instanceof Map<?, ?> map) {
|
|
if (map.get("type") != null) {
|
|
var map2 = new HashMap<>(map);
|
|
var type = map2.remove("type");
|
|
DataType dataType = DataType.from(Objects.toString(type));
|
|
if (!dataType.id().equals(type)) {
|
|
throw new ParseException("Unrecognized datatype '" + type + "' supported values: " +
|
|
Stream.of(DataType.values()).map(DataType::id).collect(
|
|
Collectors.joining(", ")));
|
|
}
|
|
var child = parse(map2, Object.class);
|
|
return cast(signature(output), child, dataType);
|
|
} else {
|
|
var keys = map.keySet();
|
|
if (keys.equals(Set.of("coalesce")) && map.get("coalesce")instanceof Collection<?> cases) {
|
|
return coalesce(cases.stream().map(item -> parse(item, output)).toList());
|
|
} else if (keys.equals(Set.of("match"))) {
|
|
return parseMatch(map.get("match"), true, output);
|
|
} else if (keys.equals(Set.of("default_value", "overrides"))) {
|
|
var match = parseMatch(map.get("overrides"), false, output);
|
|
var defaultValue = parse(map.get("default_value"), output);
|
|
return match.withDefaultValue(defaultValue);
|
|
} else if (keys.equals(Set.of("tag_value"))) {
|
|
var tagProducer = parse(map.get("tag_value"), String.class);
|
|
return getTag(signature(output), tagProducer);
|
|
} else if (keys.equals(Set.of("arg_value"))) {
|
|
var keyProducer = parse(map.get("arg_value"), String.class);
|
|
return getArg(signature(output), keyProducer);
|
|
} else if (keys.equals(Set.of("value"))) {
|
|
return parse(map.get("value"), output);
|
|
}
|
|
try {
|
|
return parseMatch(map, true, output);
|
|
} catch (ParseException e) {
|
|
throw new ParseException("Failed to parse: " + map);
|
|
}
|
|
}
|
|
} else {
|
|
object = unescape(object);
|
|
return constOf(TypeConversion.convert(object, output));
|
|
}
|
|
}
|
|
|
|
private <O> ConfigExpression.Match<I, O> parseMatch(Object match, boolean allowElse, Class<O> output) {
|
|
List<MultiExpression.Entry<ConfigExpression<I, O>>> conditions = new ArrayList<>();
|
|
ConfigExpression<I, O> fallback = constOf(null);
|
|
if (match instanceof Collection<?> items) {
|
|
for (var item : items) {
|
|
if (item instanceof Map<?, ?> map) {
|
|
if (map.keySet().equals(Set.of("if", "value"))) {
|
|
conditions.add(MultiExpression.entry(parse(map.get("value"), output),
|
|
BooleanExpressionParser.parse(map.get("if"), tagValueProducer, input)));
|
|
} else if (allowElse && map.keySet().equals(Set.of("else"))) {
|
|
fallback = parse(map.get("else"), output);
|
|
break;
|
|
} else {
|
|
throw new ParseException(
|
|
"Invalid match case. Expected if/then" + (allowElse ? " or else" : "") + ", got: " + match);
|
|
}
|
|
}
|
|
}
|
|
} else if (match instanceof Map<?, ?> map) {
|
|
for (var entry : map.entrySet()) {
|
|
String value = Objects.toString(entry.getValue());
|
|
if (value.matches("^_*(default_value|otherwise|default)_*$")) {
|
|
fallback = parse(entry.getKey(), output);
|
|
} else {
|
|
conditions.add(MultiExpression.entry(parse(entry.getKey(), output),
|
|
BooleanExpressionParser.parse(entry.getValue(), tagValueProducer, input)));
|
|
}
|
|
}
|
|
} else {
|
|
throw new ParseException("Invalid match block. Expected a list or map, but got: " + match);
|
|
}
|
|
return ConfigExpression.match(signature(output), MultiExpression.of(List.copyOf(conditions)), fallback);
|
|
}
|
|
|
|
private <O> Signature<I, O> signature(Class<O> outputClass) {
|
|
return new Signature<>(input, outputClass);
|
|
}
|
|
|
|
private record EvaluateInput(Contexts.Root root, Object expression, Class<?> clazz) {}
|
|
}
|