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 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 Input type of the expression */ public class ConfigExpressionParser { private final TagValueProducer tagValueProducer; private final ScriptEnvironment input; public ConfigExpressionParser(TagValueProducer tagValueProducer, ScriptEnvironment 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 } and * returns output of type {@code }. * * @param object a map or list of tag criteria * @param tagValueProducer a TagValueProducer * @param Input type of the expression * @param Return type of the expression */ public static ConfigExpression parse(Object object, TagValueProducer tagValueProducer, ScriptEnvironment context, Class outputClass) { return new ConfigExpressionParser<>(tagValueProducer, context).parse(object, outputClass).simplify(); } private ConfigExpression parse(Object object, Class 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("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 ConfigExpression.Match parseMatch(Object match, boolean allowElse, Class output) { List>> conditions = new ArrayList<>(); ConfigExpression 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 Signature signature(Class outputClass) { return new Signature<>(input, outputClass); } }