I made a java version. do you want it?
java-implementation-ai-did-it-and-i-held-its-hand-a-little
package com.odxsolutions.decision.rex.actorsystem.util;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
public class JsonEJava {
private static final Object deleteMarker = new Object();
private static final ObjectMapper objectMapper = new ObjectMapper()
.configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true);
public static Object render(Object template, Map<String, Object> context) {
for (String key : context.keySet()) {
if (!key.matches("^[a-zA-Z_][a-zA-Z0-9_]*$")) {
throw new TemplateError("TemplateError: top level keys of context must follow /[a-zA-Z_][a-zA-Z0-9_]*/");
}
}
Map<String, Object> fullContext = new java.util.HashMap<>(context);
if (!fullContext.containsKey("now")) {
fullContext.put("now", OffsetDateTime.now(ZoneOffset.UTC).toString());
}
Builtins.getBuiltins().forEach(fullContext::putIfAbsent);
Object result = doRender(template, fullContext);
if (result == deleteMarker) {
return null;
}
if (containsFunctions(result)) {
throw new TemplateError("evaluated template contained uncalled functions");
}
return result;
}
private static Object doRender(Object template, Map<String, Object> context) {
if (TypeUtils.isNumber(template) || TypeUtils.isBool(template) || TypeUtils.isNull(template)) {
return template;
}
if (TypeUtils.isString(template)) {
return interpolate((String) template, context);
}
if (TypeUtils.isArray(template)) {
List<Object> renderedList = new java.util.ArrayList<>();
List<?> templateList = template.getClass().isArray() ? Arrays.asList((Object[]) template) : (List<?>) template;
for (int i = 0; i < templateList.size(); i++) {
Object v = templateList.get(i);
try {
Object renderedItem = doRender(v, context);
if (renderedItem != deleteMarker) {
renderedList.add(renderedItem);
}
} catch (JSONTemplateError e) {
e.add_location(String.format("[%d]", i));
throw e;
}
}
return renderedList;
}
if (template instanceof ObjectNode) {
template = objectMapper.convertValue(template, Map.class);
}
if (TypeUtils.isObject(template)) {
Map<String, Object> templateMap = (Map<String, Object>) template;
// Operator handling (e.g., $if, $map)
List<String> knownOperators = Arrays.asList(
"$if", "$eval", "$json", "$flatten", "$flattenDeep", "$fromNow", "$let",
"$map", "$reduce", "$find", "$match", "$switch", "$merge", "$mergeDeep",
"$sort", "$reverse"
);
List<String> operatorKeys = templateMap.keySet().stream()
.filter(k -> knownOperators.contains(k))
.collect(Collectors.toList());
if (operatorKeys.size() > 1) {
// Special case for $fromNow which can have a 'from' property.
if (!(operatorKeys.size() == 2 && operatorKeys.contains("$fromNow") && templateMap.containsKey("from"))) {
throw new TemplateError("only one operator allowed");
}
}
if (!operatorKeys.isEmpty()) {
String operator = operatorKeys.stream().filter(k -> !k.equals("from")).findFirst().orElse(null);
if ("$if".equals(operator)) {
return operatorsIf(templateMap, context);
}
if ("$eval".equals(operator)) {
return operatorsEval(templateMap, context);
}
if ("$json".equals(operator)) {
return operatorsJson(templateMap, context);
}
if ("$flatten".equals(operator)) {
return operatorsFlatten(templateMap, context);
}
if ("$flattenDeep".equals(operator)) {
return operatorsFlattenDeep(templateMap, context);
}
if ("$fromNow".equals(operator)) {
return operatorsFromNow(templateMap, context);
}
if ("$let".equals(operator)) {
return operatorsLet(templateMap, context);
}
if ("$map".equals(operator)) {
return operatorsMap(templateMap, context);
}
if ("$reduce".equals(operator)) {
return operatorsReduce(templateMap, context);
}
if ("$find".equals(operator)) {
return operatorsFind(templateMap, context);
}
if ("$match".equals(operator)) {
return operatorsMatch(templateMap, context);
}
if ("$switch".equals(operator)) {
return operatorsSwitch(templateMap, context);
}
if ("$merge".equals(operator)) {
return operatorsMerge(templateMap, context);
}
if ("$mergeDeep".equals(operator)) {
return operatorsMergeDeep(templateMap, context);
}
if ("$sort".equals(operator)) {
return operatorsSort(templateMap, context);
}
if ("$reverse".equals(operator)) {
return operatorsReverse(templateMap, context);
}
// Other operators would be handled here.
}
// Regular object rendering
Map<String, Object> result = new java.util.LinkedHashMap<>();
for (Map.Entry<String, Object> entry : templateMap.entrySet()) {
String key = entry.getKey();
Object value;
try {
// First render any nested templates
value = doRender(entry.getValue(), context);
} catch (JSONTemplateError e) {
if (key.matches("^[a-zA-Z][a-zA-Z0-9]*$")) {
e.add_location("." + key);
} else {
try {
e.add_location(String.format("[%s]", objectMapper.writeValueAsString(key)));
} catch (JsonProcessingException jsonProcessingException) {
// fallback for safety
e.add_location(String.format("['%s']", key));
}
}
throw e;
}
if (value != deleteMarker) {
String finalKey;
if (key.startsWith("$$")) {
finalKey = key.substring(1);
} else if (key.matches("^\\$[a-zA-Z][a-zA-Z0-9_]*$")) {
throw new TemplateError("TemplateError: $<identifier> is reserved; use $$<identifier>");
} else {
finalKey = interpolate(key, context);
if ((key.startsWith("'") && key.endsWith("'")) || (key.startsWith("\"") && key.endsWith("\""))) {
if (finalKey.length() >= 2 && finalKey.startsWith("'") && finalKey.endsWith("'")) {
finalKey = finalKey.substring(1, finalKey.length() - 1);
}
}
}
result.put(finalKey, value);
}
}
return result;
}
// If the template is of an unknown type, return it as is.
return template;
}
private static String interpolate(String string, Map<String, Object> context) {
StringBuilder result = new StringBuilder();
String remaining = string;
while (true) {
int dollarOffset = remaining.indexOf('$');
if (dollarOffset == -1) {
result.append(remaining);
break;
}
result.append(remaining, 0, dollarOffset);
remaining = remaining.substring(dollarOffset);
if (remaining.startsWith("$${")) {
result.append("${");
remaining = remaining.substring(3);
continue;
}
if (remaining.startsWith("${")) {
String expressionSource = remaining.substring(2);
ParseResult v = parseUntilTerminator(expressionSource, "}", context);
if (TypeUtils.isArray(v.result) || TypeUtils.isObject(v.result)) {
String input = expressionSource.substring(0, v.offset);
String at = TypeUtils.isObject(v.result) ? "" : " at template.message";
throw new TemplateError("TemplateError" + at + ": interpolation of '" + input + "' produced an array or object");
}
if (v.result != null) {
result.append(v.result);
}
remaining = remaining.substring(2 + v.offset + v.terminatorLength);
} else {
// Just a normal '$', not an interpolation
result.append('$');
remaining = remaining.substring(1);
}
}
return result.toString();
}
private static Object operatorsEval(Map<String, Object> template, Map<String, Object> context) {
checkUndefinedProperties(template, Arrays.asList("\\$eval"));
Object expr = template.get("$eval");
if (!TypeUtils.isString(expr)) {
throw new TemplateError("$eval must be given a string expression");
}
return parse((String) expr, context);
}
private static Object operatorsIf(Map<String, Object> template, Map<String, Object> context) {
checkUndefinedProperties(template, Arrays.asList("\\$if", "then", "else"));
if (!template.containsKey("$if")) {
// Should not happen if called correctly
return deleteMarker;
}
Object ifExpr = template.get("$if");
if (!TypeUtils.isString(ifExpr)) {
throw new TemplateError("$if can evaluate string expressions only");
}
if (template.containsKey("$then")) {
throw new TemplateError("$if Syntax error: $then: should be spelled then: (no $)");
}
Object parsedExpr = parse((String) ifExpr, context);
if (TypeUtils.isTruthy(parsedExpr)) {
return template.containsKey("then") ? doRender(template.get("then"), context) : deleteMarker;
} else {
return template.containsKey("else") ? doRender(template.get("else"), context) : deleteMarker;
}
}
private static Object operatorsJson(Map<String, Object> template, Map<String, Object> context) {
checkUndefinedProperties(template, Arrays.asList("\\$json"));
Object rendered = doRender(template.get("$json"), context);
if (containsFunctions(rendered)) {
throw new TemplateError("evaluated template contained uncalled functions");
}
return stringify(rendered);
}
private static void checkUndefinedProperties(Map<String, Object> template, List<String> allowed) {
String combinedPattern = "^(" + String.join("|", allowed) + ")$";
Pattern p = Pattern.compile(combinedPattern);
List<String> unknownKeys = new java.util.ArrayList<>();
for (String key : template.keySet()) {
if (!p.matcher(key).matches()) {
unknownKeys.add(key);
}
}
if (!unknownKeys.isEmpty()) {
java.util.Collections.sort(unknownKeys);
String allowedOperator = allowed.get(0).replace("\\", "");
throw new TemplateError("TemplateError: " + allowedOperator + " has undefined properties: " + String.join(" ", unknownKeys));
}
}
private static Object operatorsFlatten(Map<String, Object> template, Map<String, Object> context) {
checkUndefinedProperties(template, Arrays.asList("\\$flatten"));
Object value = doRender(template.get("$flatten"), context);
if (!TypeUtils.isArray(value)) {
throw new TemplateError("TemplateError: $flatten value must evaluate to an array");
}
List<Object> flattenedList = new java.util.ArrayList<>();
List<?> list = TypeUtils.objectToList(value);
for (Object item : list) {
if (TypeUtils.isArray(item)) {
flattenedList.addAll(TypeUtils.objectToList(item));
} else {
flattenedList.add(item);
}
}
return flattenedList;
}
private static Object operatorsFlattenDeep(Map<String, Object> template, Map<String, Object> context) {
checkUndefinedProperties(template, Arrays.asList("\\$flattenDeep"));
Object value = doRender(template.get("$flattenDeep"), context);
if (!TypeUtils.isArray(value)) {
throw new TemplateError("TemplateError: $flattenDeep value must evaluate to an array");
}
return flattenDeep(TypeUtils.objectToList(value));
}
private static List<Object> flattenDeep(List<?> list) {
List<Object> result = new java.util.ArrayList<>();
for (Object item : list) {
if (TypeUtils.isArray(item)) {
result.addAll(flattenDeep(TypeUtils.objectToList(item)));
} else {
result.add(item);
}
}
return result;
}
private static Object operatorsFromNow(Map<String, Object> template, Map<String, Object> context) {
checkUndefinedProperties(template, Arrays.asList("\\$fromNow", "from"));
Object value = doRender(template.get("$fromNow"), context);
if (!TypeUtils.isString(value)) {
throw new TemplateError("TemplateError: $fromNow expects a string");
}
Object referenceObj = null; // Default to null, fromNow will use context.now
if (template.containsKey("from")) {
referenceObj = doRender(template.get("from"), context);
}
return Builtins.fromNow((String) value, referenceObj, context);
}
private static Object operatorsLet(Map<String, Object> template, Map<String, Object> context) {
if (!template.containsKey("$let")) {
return deleteMarker;
}
Object letValue = template.get("$let");
if (!TypeUtils.isObject(letValue)) {
throw new TemplateError("TemplateError: $let value must be an object");
}
Object initialResult = doRender(letValue, context);
if (!TypeUtils.isObject(initialResult)) {
throw new TemplateError("TemplateError: $let value must be an object");
}
Map<String, Object> variables = new java.util.HashMap<>();
Map<String, Object> initialResultMap = (Map<String, Object>) initialResult;
for (Map.Entry<String, Object> entry : initialResultMap.entrySet()) {
String key = entry.getKey();
if (!key.matches("^[a-zA-Z_][a-zA-Z0-9_]*$")) {
throw new TemplateError("TemplateError: top level keys of $let must follow /[a-zA-Z_][a-zA-Z0-9_]*/");
}
variables.put(key, entry.getValue());
}
Map<String, Object> childContext = new java.util.HashMap<>(context);
childContext.putAll(variables);
// the only property allowed after $let is "in"
for (String k : template.keySet()) {
if (!k.equals("$let") && !k.equals("in")) {
throw new TemplateError("TemplateError: $let has undefined properties: " + k);
}
}
if (!template.containsKey("in")) {
throw new TemplateError("TemplateError: $let operator requires an `in` clause");
}
return doRender(template.get("in"), childContext);
}
private static Object operatorsMap(Map<String, Object> template, Map<String, Object> context) {
final String EACH_RE = "each\\(([a-zA-Z_][a-zA-Z0-9_]*)(,\\s*([a-zA-Z_][a-zA-Z0-9_]*))?\\)";
Object value = doRender(template.get("$map"), context);
if (!TypeUtils.isArray(value) && !TypeUtils.isObject(value)) {
// If value is invalid, we should first check for other errors like undefined properties.
checkUndefinedProperties(template, Arrays.asList("\\$map", EACH_RE));
throw new TemplateError("TemplateError: $map value must evaluate to an array or object");
}
checkUndefinedProperties(template, Arrays.asList("\\$map", EACH_RE));
String eachKey = template.keySet().stream()
.filter(k -> k.startsWith("each("))
.findFirst()
.orElse(null);
if (eachKey == null) {
// This case should be caught by checkUndefinedProperties, but as a safeguard:
throw new TemplateError("$map requires each(identifier) syntax");
}
Pattern eachPattern = Pattern.compile("^" + EACH_RE + "$");
Matcher matcher = eachPattern.matcher(eachKey);
if (!matcher.matches()) {
// This should also be caught by checkUndefinedProperties.
throw new TemplateError("$map requires each(identifier) syntax, found: " + eachKey);
}
String varName = matcher.group(1);
String indexName = matcher.group(3); // Use group 3 for the second variable name
Object eachTemplate = template.get(eachKey);
if (TypeUtils.isArray(value)) {
List<?> list = TypeUtils.objectToList(value);
List<Object> result = new java.util.ArrayList<>();
for (int i = 0; i < list.size(); i++) {
Object item = list.get(i);
Map<String, Object> loopContext = new java.util.HashMap<>(context);
loopContext.put(varName, item);
if (indexName != null) {
loopContext.put(indexName, i);
}
Object renderedItem = doRender(eachTemplate, loopContext);
if (renderedItem != deleteMarker) {
result.add(renderedItem);
}
}
return result;
} else { // isObject
Map<String, Object> map = (Map<String, Object>) value;
Map<String, Object> result = new java.util.LinkedHashMap<>();
for (Map.Entry<String, Object> entry : map.entrySet()) {
Map<String, Object> loopContext = new java.util.HashMap<>(context);
if (indexName != null) { // each(v,k)
loopContext.put(varName, entry.getValue());
loopContext.put(indexName, entry.getKey());
} else { // each(y) where y is {key:.., val:..}
Map<String, Object> y = new java.util.HashMap<>();
y.put("key", entry.getKey());
y.put("val", entry.getValue());
loopContext.put(varName, y);
}
Object renderedItem = doRender(eachTemplate, loopContext);
if (renderedItem != deleteMarker) {
if (!TypeUtils.isObject(renderedItem)) {
throw new TemplateError("$map on objects expects " + eachKey + " to evaluate to an object");
}
result.putAll((Map<String, Object>) renderedItem);
}
}
return result;
}
}
private static Object operatorsReduce(Map<String, Object> template, Map<String, Object> context) {
final String EACH_RE = "each\\(([a-zA-Z_][a-zA-Z0-9_]*),\\s*([a-zA-Z_][a-zA-Z0-9_]*)(,\\s*([a-zA-Z_][a-zA-Z0-9_]*))?\\)";
checkUndefinedProperties(template, Arrays.asList("\\$reduce", "initial", EACH_RE));
Object value = doRender(template.get("$reduce"), context);
if (!TypeUtils.isArray(value)) {
throw new TemplateError("$reduce value must evaluate to an array");
}
if (template.keySet().size() != 3) {
throw new TemplateError("$reduce must have exactly three properties");
}
String eachKey = template.keySet().stream()
.filter(k -> !k.equals("$reduce") && !k.equals("initial"))
.findFirst()
.orElse(null);
Pattern eachPattern = Pattern.compile("^" + EACH_RE + "$");
Matcher matcher = eachPattern.matcher(eachKey);
if (!matcher.matches()) {
throw new TemplateError("$reduce requires each(identifier) syntax");
}
String accName = matcher.group(1);
String varName = matcher.group(2);
String indexName = matcher.group(4);
Object eachTemplate = template.get(eachKey);
Object accValue = template.get("initial");
List<?> list = TypeUtils.objectToList(value);
for (int i = 0; i < list.size(); i++) {
Object item = list.get(i);
Map<String, Object> loopContext = new java.util.HashMap<>(context);
loopContext.put(accName, accValue);
loopContext.put(varName, item);
if (indexName != null) {
loopContext.put(indexName, i);
}
Object renderedItem = doRender(eachTemplate, loopContext);
if (renderedItem != deleteMarker) {
accValue = renderedItem;
}
}
return accValue;
}
private static Object operatorsFind(Map<String, Object> template, Map<String, Object> context) {
final String EACH_RE = "each\\(([a-zA-Z_][a-zA-Z0-9_]*)(,\\s*([a-zA-Z_][a-zA-Z0-9_]*))?\\)";
checkUndefinedProperties(template, Arrays.asList("\\$find", EACH_RE));
Object value = doRender(template.get("$find"), context);
if (!TypeUtils.isArray(value)) {
throw new TemplateError("$find value must evaluate to an array");
}
if (template.keySet().size() != 2) {
throw new TemplateError("$find must have exactly two properties");
}
String eachKey = template.keySet().stream()
.filter(k -> !k.equals("$find"))
.findFirst()
.orElse(null);
if (eachKey == null) {
// This case should be caught by checkUndefinedProperties, but as a safeguard:
throw new TemplateError("$find requires each(identifier) syntax");
}
Object eachTemplate = template.get(eachKey);
if (!TypeUtils.isString(eachTemplate)) {
throw new TemplateError("each can evaluate string expressions only");
}
String eachExpr = (String) eachTemplate;
Pattern eachPattern = Pattern.compile("^" + EACH_RE + "$");
Matcher matcher = eachPattern.matcher(eachKey);
if (!matcher.matches()) {
// This should also be caught by checkUndefinedProperties.
throw new TemplateError("$find requires each(identifier) syntax, found: " + eachKey);
}
String varName = matcher.group(1);
String indexName = matcher.group(3);
List<?> list = TypeUtils.objectToList(value);
for (int i = 0; i < list.size(); i++) {
Object item = list.get(i);
Map<String, Object> loopContext = new java.util.HashMap<>(context);
loopContext.put(varName, item);
if (indexName != null) {
loopContext.put(indexName, i);
}
Object result = parse(eachExpr, loopContext);
if (TypeUtils.isTruthy(result)) {
return item;
}
}
return deleteMarker;
}
private static Object operatorsMatch(Map<String, Object> template, Map<String, Object> context) {
Object matchValue = template.get("$match");
if (!TypeUtils.isObject(matchValue)) {
throw new TemplateError("$match can evaluate objects only");
}
Map<String, Object> conditions = (Map<String, Object>) matchValue;
List<Object> result = new java.util.ArrayList<>();
List<String> sortedKeys = new java.util.ArrayList<>(conditions.keySet());
java.util.Collections.sort(sortedKeys);
for (String condition : sortedKeys) {
if (TypeUtils.isTruthy(parse(condition, context))) {
result.add(doRender(conditions.get(condition), context));
}
}
return result;
}
private static Object operatorsSwitch(Map<String, Object> template, Map<String, Object> context) {
Object switchValue = template.get("$switch");
if (!TypeUtils.isObject(switchValue)) {
throw new TemplateError("$switch can evaluate objects only");
}
Map<String, Object> conditions = (Map<String, Object>) switchValue;
List<Object> result = new java.util.ArrayList<>();
List<String> sortedKeys = new java.util.ArrayList<>(conditions.keySet());
sortedKeys.remove("$default");
java.util.Collections.sort(sortedKeys);
for (String condition : sortedKeys) {
if (TypeUtils.isTruthy(parse(condition, context))) {
result.add(doRender(conditions.get(condition), context));
}
}
if (result.size() > 1) {
throw new TemplateError("$switch can only have one truthy condition");
}
if (result.isEmpty() && conditions.containsKey("$default")) {
result.add(doRender(conditions.get("$default"), context));
}
return result.size() > 0 ? result.get(0) : deleteMarker;
}
private static Object operatorsMerge(Map<String, Object> template, Map<String, Object> context) {
checkUndefinedProperties(template, Arrays.asList("\\$merge"));
Object value = doRender(template.get("$merge"), context);
if (!TypeUtils.isArray(value)) {
throw new TemplateError("$merge value must evaluate to an array of objects");
}
List<?> list = TypeUtils.objectToList(value);
Map<String, Object> merged = new java.util.HashMap<>();
List<String> keyOrder = new java.util.ArrayList<>();
for (Object item : list) {
if (!TypeUtils.isObject(item)) {
throw new TemplateError("$merge value must evaluate to an array of objects");
}
Map<String, Object> map = (Map<String, Object>) item;
for (String key : map.keySet()) {
if (!merged.containsKey(key)) {
keyOrder.add(key);
}
merged.put(key, map.get(key));
}
}
Map<String, Object> result = new java.util.LinkedHashMap<>();
for (String key : keyOrder) {
result.put(key, merged.get(key));
}
return result;
}
private static Object operatorsMergeDeep(Map<String, Object> template, Map<String, Object> context) {
checkUndefinedProperties(template, Arrays.asList("\\$mergeDeep"));
Object value = doRender(template.get("$mergeDeep"), context);
if (!TypeUtils.isArray(value)) {
throw new TemplateError("$mergeDeep value must evaluate to an array of objects");
}
List<?> list = TypeUtils.objectToList(value);
if (list.isEmpty()) {
return new java.util.HashMap<>();
}
for (Object item : list) {
if (!TypeUtils.isObject(item)) {
throw new TemplateError("$mergeDeep value must evaluate to an array of objects");
}
}
Object result = list.get(0);
for (int i = 1; i < list.size(); i++) {
result = mergeDeep(result, list.get(i));
}
return result;
}
private static Object mergeDeep(Object left, Object right) {
if (TypeUtils.isArray(left) && TypeUtils.isArray(right)) {
List<Object> mergedList = new java.util.ArrayList<>(TypeUtils.objectToList(left));
mergedList.addAll(TypeUtils.objectToList(right));
return mergedList;
}
if (TypeUtils.isObject(left) && TypeUtils.isObject(right)) {
Map<String, Object> leftMap = (Map<String, Object>) left;
Map<String, Object> rightMap = (Map<String, Object>) right;
Map<String, Object> result = new java.util.LinkedHashMap<>(leftMap);
for (Map.Entry<String, Object> entry : rightMap.entrySet()) {
String key = entry.getKey();
if (result.containsKey(key)) {
result.put(key, mergeDeep(result.get(key), entry.getValue()));
} else {
result.put(key, entry.getValue());
}
}
return result;
}
return right;
}
private static int compareNumbers(Number n1, Number n2) {
if (n1 instanceof java.math.BigDecimal && n2 instanceof java.math.BigDecimal) {
return ((java.math.BigDecimal) n1).compareTo((java.math.BigDecimal) n2);
}
if (n1 instanceof Double || n1 instanceof Float || n2 instanceof Double || n2 instanceof Float) {
return Double.compare(n1.doubleValue(), n2.doubleValue());
}
java.math.BigDecimal bd1 = new java.math.BigDecimal(n1.toString());
java.math.BigDecimal bd2 = new java.math.BigDecimal(n2.toString());
return bd1.compareTo(bd2);
}
private static Object operatorsSort(Map<String, Object> template, Map<String, Object> context) {
final String BY_RE = "by\\(([a-zA-Z_][a-zA-Z0-9_]*)\\)";
checkUndefinedProperties(template, Arrays.asList("\\$sort", BY_RE));
Object value = doRender(template.get("$sort"), context);
if (!TypeUtils.isArray(value)) {
throw new TemplateError("$sorted values to be sorted must have the same type");
}
List<?> list = TypeUtils.objectToList(value);
if (list.isEmpty()) {
return list;
}
String byKey = template.keySet().stream()
.filter(k -> k.startsWith("by("))
.findFirst()
.orElse(null);
java.util.function.Function<Object, Object> by;
if (byKey != null) {
Pattern byPattern = Pattern.compile("^by\\(([a-zA-Z_][a-zA-Z0-9_]*)\\)$");
Matcher matcher = byPattern.matcher(byKey);
if (!matcher.matches()) {
throw new TemplateError("$sort requires by(identifier) syntax, found: " + byKey);
}
String varName = matcher.group(1);
Object byExpr = template.get(byKey);
if (!TypeUtils.isString(byExpr)) {
throw new TemplateError("by() expression must be a string");
}
by = item -> {
Map<String, Object> byContext = new java.util.HashMap<>(context);
byContext.put(varName, item);
return parse((String) byExpr, byContext);
};
} else {
boolean needBy = list.stream().anyMatch(v -> TypeUtils.isArray(v) || TypeUtils.isObject(v));
if (needBy) {
throw new TemplateError("$sorted values to be sorted must have the same type");
}
by = item -> item;
}
List<Object[]> tagged = new java.util.ArrayList<>();
for (int i = 0; i < list.size(); i++) {
tagged.add(new Object[]{by.apply(list.get(i)), i, list.get(i)});
}
if (!tagged.isEmpty()) {
Object firstByValue = tagged.get(0)[0];
if (!(TypeUtils.isNumber(firstByValue) || TypeUtils.isString(firstByValue))) {
throw new TemplateError("$sorted values to be sorted must have the same type");
}
boolean isNumericSort = TypeUtils.isNumber(firstByValue);
for (Object[] pair : tagged) {
if (pair[0] == null) {
throw new TemplateError("$sorted values to be sorted must have the same type");
}
if (isNumericSort) {
if (!TypeUtils.isNumber(pair[0])) {
throw new TemplateError("$sorted values to be sorted must have the same type");
}
} else { // isStringSort
if (!TypeUtils.isString(pair[0])) {
throw new TemplateError("$sorted values to be sorted must have the same type");
}
}
}
}
boolean isNumericSort = !tagged.isEmpty() && TypeUtils.isNumber(tagged.get(0)[0]);
tagged.sort((a, b) -> {
Object valAObj = a[0];
Object valBObj = b[0];
int cmp;
if (isNumericSort) {
cmp = compareNumbers((Number) valAObj, (Number) valBObj);
} else {
cmp = ((String) valAObj).compareTo((String) valBObj);
}
if (cmp != 0) {
return cmp;
}
// Stable sort: if values are equal, maintain original order
return ((Integer) a[1]).compareTo((Integer) b[1]);
});
return tagged.stream().map(e -> e[2]).collect(Collectors.toList());
}
private static Object operatorsReverse(Map<String, Object> template, Map<String, Object> context) {
checkUndefinedProperties(template, Arrays.asList("\\$reverse"));
Object value = doRender(template.get("$reverse"), context);
if (!TypeUtils.isArray(value)) {
throw new TemplateError("$reverse value must evaluate to an array of objects");
}
List<?> list = TypeUtils.objectToList(value);
List<Object> reversedList = new java.util.ArrayList<>(list);
java.util.Collections.reverse(reversedList);
return reversedList;
}
private static Map<String, Long> parseTime(String str) {
Pattern timeExp = Pattern.compile(
"^(?:\\s*([+-]))?" +
"(?:\\s*(?<years>\\d+)\\s*(?:y|year|years|yr))?" +
"(?:\\s*(?<months>\\d+)\\s*(?:months|month|mo))?" +
"(?:\\s*(?<weeks>\\d+)\\s*(?:weeks|week|wk|w))?" +
"(?:\\s*(?<days>\\d+)\\s*(?:days|day|d))?" +
"(?:\\s*(?<hours>\\d+)\\s*(?:hours|hour|hr|h))?" +
"(?:\\s*(?<minutes>\\d+)\\s*(?:minutes|minute|min|m))?" +
"(?:\\s*(?<seconds>\\d+)\\s*(?:seconds|second|sec|s))?" +
"\\s*$", Pattern.CASE_INSENSITIVE);
Matcher matcher = timeExp.matcher(str == null ? "" : str);
if (!matcher.matches()) {
throw new TemplateError("String: '" + str + "' isn't a time expression");
}
long neg = "-".equals(matcher.group(1)) ? -1 : 1;
Map<String, Long> result = new java.util.HashMap<>();
String[] units = {"years", "months", "weeks", "days", "hours", "minutes", "seconds"};
for (String unit : units) {
String groupValue = matcher.group(unit);
if (groupValue != null) {
result.put(unit, Long.parseLong(groupValue) * neg);
}
}
return result;
}
private static Object parse(String source, Map<String, Object> context) {
Tokenizer tokenizer = new Tokenizer(source);
Parser parser = new Parser(tokenizer);
ASTNode tree = parser.parse();
if (parser.current_token != null) {
throw new SyntaxError("Found: " + parser.current_token.value + " token, expected one of: !=, &&, (, *, **, +, -, ., /, <, <=, ==, >, >=, [, in, ||");
}
Interpreter interpreter = new Interpreter(context);
return interpreter.interpret(tree);
}
private static String stringify(Object obj) {
try {
return objectMapper.writeValueAsString(obj);
} catch (JsonProcessingException e) {
throw new TemplateError("Failed to serialize to JSON: " + e.getMessage());
}
}
// tokenizer
static class Tokenizer {
private final Pattern pattern;
private final List<String> tokens;
private Matcher matcher;
private String source;
public Tokenizer(String source) {
this.source = source;
this.tokens = Arrays.asList(
"**", "+", "-", "*", "/", "[", "]", ".", "(", ")", "{", "}", ":", ",",
">=", "<=", "<", ">", "==", "!=", "!", "&&", "||",
"true", "false", "in", "null", "number",
"identifier", "string"
);
String[] patterns = {
"\\*\\*", "\\+", "-", "\\*", "/", "\\[", "\\]", "\\.", "\\(", "\\)", "\\{", "\\}", ":", ",",
">=", "<=", "<", ">", "==", "!=", "!", "&&", "\\|\\|",
"true(?![a-zA-Z_0-9])", "false(?![a-zA-Z_0-9])", "in(?![a-zA-Z_0-9])", "null(?![a-zA-Z_0-9])",
"[0-9]+(?:\\.[0-9]+)?",
"[a-zA-Z_][a-zA-Z_0-9]*",
"'[^']*'|\"[^\"]*\""
};
StringBuilder regex = new StringBuilder();
regex.append("\\s*(");
for (int i = 0; i < patterns.length; i++) {
regex.append("(").append(patterns[i]).append(")");
if (i < patterns.length - 1) {
regex.append("|");
}
}
regex.append(")");
this.pattern = Pattern.compile(regex.toString());
this.matcher = this.pattern.matcher(source);
}
public Token next() {
if (matcher.lookingAt()) {
String matchedValue = matcher.group(1);
String kind = null;
int groupIndex = -1;
for (int i = 2; i <= matcher.groupCount(); i++) {
if (matcher.group(i) != null) {
groupIndex = i - 2;
break;
}
}
if (groupIndex != -1) {
kind = tokens.get(groupIndex);
}
if (kind != null) {
if (matcher.end() < matcher.start() + matcher.group(0).length()) {
String unexpectedInput = source.substring(matcher.end());
throw new SyntaxError(String.format("Unexpected input for '%s' at '%s'", source, unexpectedInput));
}
Token token = new Token(kind, matchedValue, matcher.start(1), matcher.end(1));
matcher.region(matcher.end(), source.length());
return token;
}
}
if (matcher.regionEnd() > matcher.regionStart()) {
String unexpectedInput = source.substring(matcher.regionStart());
throw new SyntaxError(String.format("Unexpected input for '%s' at '%s'", source, unexpectedInput));
}
return null;
}
}
// parse
static class Parser {
Tokenizer tokenizer;
Token current_token;
List<String> unaryOpTokens = Arrays.asList("-", "+", "!");
List<String> primitivesTokens = Arrays.asList("number", "null", "true", "false", "string");
List<List<String>> operations = Arrays.asList(
Arrays.asList("||"),
Arrays.asList("&&"),
Arrays.asList("in"),
Arrays.asList("==", "!="),
Arrays.asList("<", ">", "<=", ">="),
Arrays.asList("+", "-"),
Arrays.asList("*", "/"),
Arrays.asList("**")
);
List<String> expectedTokens = Arrays.asList("!", "(", "+", "-", "[", "false", "identifier", "null", "number", "string", "true", "{");
public Parser(Tokenizer tokenizer) {
this.tokenizer = tokenizer;
this.current_token = this.tokenizer.next();
}
public void takeToken(String... kinds) {
if (current_token == null) {
throw new SyntaxError("Unexpected end of input");
}
if (kinds.length > 0 && Arrays.stream(kinds).noneMatch(k -> k.equals(current_token.kind))) {
throw new SyntaxError("Found: " + current_token.value + " token, expected one of: " + String.join(", ", kinds));
}
current_token = tokenizer.next();
}
public ASTNode parse() {
return parse(0);
}
public ASTNode parse(int level) {
ASTNode node;
if (level == operations.size() - 1) {
node = parsePropertyAccessOrFunc();
Token token = current_token;
while (token != null && operations.get(level).contains(token.kind)) {
takeToken(token.kind);
node = new BinOp(token, parse(level), node); // Corrected for right-associativity
token = current_token;
}
} else {
node = parse(level + 1);
Token token = current_token;
while (token != null && operations.get(level).contains(token.kind)) {
takeToken(token.kind);
node = new BinOp(token, node, parse(level + 1));
token = current_token;
}
}
return node;
}
public ASTNode parsePropertyAccessOrFunc() {
ASTNode node = parseUnit();
List<String> operators = Arrays.asList("[", "(", ".");
while (current_token != null && operators.contains(current_token.kind)) {
if ("[".equals(current_token.kind)) {
node = parseAccessWithBrackets(node);
} else if (".".equals(current_token.kind)) {
Token token = current_token;
takeToken(".");
if (current_token == null || !"identifier".equals(current_token.kind)) {
String found = (current_token == null) ? "null" : current_token.value;
throw new SyntaxError("Found: " + found + " token, expected one of: identifier");
}
ASTNode rightPart = new Primitive(current_token);
takeToken("identifier");
node = new BinOp(token, node, rightPart);
} else if ("(".equals(current_token.kind)) {
node = parseFunctionCall(node);
}
}
return node;
}
public ASTNode parseUnit() {
if (current_token == null) {
throw new SyntaxError("Unexpected end of input");
}
Token token = current_token;
ASTNode node;
if (unaryOpTokens.contains(token.kind)) {
takeToken(token.kind);
node = new UnaryOp(token, parseUnit());
} else if (primitivesTokens.contains(token.kind)) {
takeToken(token.kind);
node = new Primitive(token);
} else if ("identifier".equals(token.kind)) {
takeToken(token.kind);
node = new ContextValue(token);
} else if ("(".equals(token.kind)) {
takeToken("(");
node = parse();
if (node == null) {
throw new SyntaxError("Syntax error, expected one of: " + String.join(", ", expectedTokens));
}
takeToken(")");
} else if ("[".equals(token.kind)) {
node = parseList();
} else if ("{".equals(token.kind)) {
node = parseObject();
} else {
// Expected error containing: SyntaxError at template.foo: Found: } token, expected one of: !, (, +, -, [, false, identifier, null, number, string, true, {
throw new SyntaxError("Found: " + token.value + " token, expected one of: " + String.join(", ", expectedTokens));
}
return node;
}
public ASTNode parseAccessWithBrackets(ASTNode node) {
ASTNode leftArg = null, rightArg = null;
Token token = current_token;
boolean isInterval = false;
takeToken("[");
if (current_token.kind.equals("]")) {
throw new SyntaxError("Found: " + current_token.value + " token, expected one of: " + String.join(", ", expectedTokens));
}
if (!current_token.kind.equals(":")) {
leftArg = parse();
}
if (current_token.kind.equals(":")) {
isInterval = true;
takeToken(":");
}
if (!current_token.kind.equals("]")) {
rightArg = parse();
}
if (isInterval && rightArg == null && !current_token.kind.equals("]")) {
throw new SyntaxError("Syntax error, expected one of: " + String.join(", ", expectedTokens));
}
takeToken("]");
return new ValueAccess(token, node, isInterval, leftArg, rightArg);
}
public ASTNode parseFunctionCall(ASTNode name) {
Token token = current_token;
List<ASTNode> args = new java.util.ArrayList<>();
takeToken("(");
if (!")".equals(current_token.kind)) {
ASTNode node = parse();
args.add(node);
while (current_token != null && ",".equals(current_token.kind)) {
if (args.get(args.size() - 1) == null) {
throw new SyntaxError("Syntax error, expected one of: " + String.join(", ", expectedTokens));
}
takeToken(",");
node = parse();
args.add(node);
}
}
takeToken(")");
return new FunctionCall(token, name, args);
}
public ASTNode parseList() {
ASTNode node;
List<ASTNode> arr = new java.util.ArrayList<>();
Token token = current_token;
takeToken("[");
if (!"]".equals(current_token.kind)) {
node = parse();
arr.add(node);
while (",".equals(current_token.kind)) {
if (arr.get(arr.size() - 1) == null) {
throw new SyntaxError("Syntax error, expected one of: " + String.join(", ", expectedTokens));
}
takeToken(",");
node = parse();
arr.add(node);
}
}
takeToken("]");
return new ListAST(token, arr);
}
public ASTNode parseObject() {
Map<String, ASTNode> obj = new java.util.LinkedHashMap<>();
String key;
ASTNode value;
Token objToken = current_token;
takeToken("{");
Token token = current_token;
while (token != null && ("string".equals(token.kind) || "identifier".equals(token.kind))) {
key = token.value;
if ("string".equals(token.kind)) {
key = key.substring(1, key.length() - 1);
}
takeToken(token.kind);
takeToken(":");
value = parse();
if (value == null) {
throw new SyntaxError("Syntax error, expected one of: " + String.join(", ", expectedTokens));
}
obj.put(key, value);
if (current_token != null && "}".equals(current_token.kind)) {
break;
} else {
takeToken(",");
}
token = current_token;
}
takeToken("}");
return new ObjectAST(objToken, obj);
}
}
static class ParseResult {
Object result;
int offset;
int terminatorLength;
public ParseResult(Object result, int offset, int terminatorLength) {
this.result = result;
this.offset = offset;
this.terminatorLength = terminatorLength;
}
}
// parseIntilTerminator
private static ParseResult parseUntilTerminator(String source, String terminator, Map<String, Object> context) {
Tokenizer tokenizer = new Tokenizer(source);
Parser parser = new Parser(tokenizer);
ASTNode tree = parser.parse();
Token next = parser.current_token;
if (next == null) {
throw new SyntaxError("SyntaxError at template.message: unterminated ${..} expression");
} else if (!"}".equals(next.value)) { // The 'terminator' is the kind, but here we check value
throw new SyntaxError("Expected " + terminator);
}
Interpreter interpreter = new Interpreter(context);
Object result = interpreter.interpret(tree);
return new ParseResult(result, next.start, next.value.length());
}
// containsFunctions
private static boolean containsFunctions(Object rendered) {
if (TypeUtils.isFunction(rendered)) {
return true;
} else if (TypeUtils.isArray(rendered)) {
List<?> list = TypeUtils.objectToList(rendered);
for (Object item : list) {
if (containsFunctions(item)) {
return true;
}
}
} else if (TypeUtils.isObject(rendered)) {
Map<?, ?> map = (Map<?, ?>) rendered;
for (Object value : map.values()) {
if (containsFunctions(value)) {
return true;
}
}
}
return false;
}
// === AST Nodes ===
/**
* Represents a token from the tokenizer.
*/
static class Token {
String kind;
String value;
int start;
int end;
public Token(String kind, String value, int start, int end) {
this.kind = kind;
this.value = value;
this.start = start;
this.end = end;
}
}
/**
* Base class for all AST nodes.
*/
static abstract class ASTNode {
Token token;
String constructorName;
public ASTNode(Token token, String constructorName) {
this.token = token;
this.constructorName = constructorName;
}
}
static class Primitive extends ASTNode {
public Primitive(Token token) {
super(token, "Primitive");
}
}
static class BinOp extends ASTNode {
ASTNode left;
ASTNode right;
public BinOp(Token token, ASTNode left, ASTNode right) {
super(token, "BinOp");
this.left = left;
this.right = right;
}
}
static class UnaryOp extends ASTNode {
ASTNode expr;
public UnaryOp(Token token, ASTNode expr) {
super(token, "UnaryOp");
this.expr = expr;
}
}
static class FunctionCall extends ASTNode {
ASTNode name;
List<ASTNode> args;
public FunctionCall(Token token, ASTNode name, List<ASTNode> args) {
super(token, "FunctionCall");
this.name = name;
this.args = args;
}
}
static class ContextValue extends ASTNode {
public ContextValue(Token token) {
super(token, "ContextValue");
}
}
static class ListAST extends ASTNode {
List<ASTNode> list;
public ListAST(Token token, List<ASTNode> list) {
super(token, "List");
this.list = list;
}
}
static class ValueAccess extends ASTNode {
ASTNode arr;
boolean isInterval;
ASTNode left; // index or start of interval
ASTNode right; // end of interval (only for interval)
public ValueAccess(Token token, ASTNode arr, boolean isInterval, ASTNode left, ASTNode right) {
super(token, "ValueAccess");
this.arr = arr;
this.isInterval = isInterval;
this.left = left;
this.right = right;
}
}
static class ObjectAST extends ASTNode {
Map<String, ASTNode> obj;
public ObjectAST(Token token, Map<String, ASTNode> obj) {
super(token, "Object");
this.obj = obj;
}
}
// === Errors ===
static class JSONTemplateError extends RuntimeException {
protected List<String> location = new java.util.LinkedList<>();
public JSONTemplateError(String message) {
super(message);
}
public void add_location(String loc) {
location.add(0, loc);
}
@Override
public String toString() {
if (!location.isEmpty()) {
return String.format("%s at template%s: %s", this.getClass().getSimpleName(), String.join("", location), getMessage());
} else {
// The JS implementation doesn't prepend "SyntaxError: " if there's no location.
return String.format("%s: %s", this.getClass().getSimpleName(), getMessage());
}
}
}
static class SyntaxError extends JSONTemplateError {
public SyntaxError(String message) {
super(message);
}
@Override
public String toString() {
if (!location.isEmpty()) {
return String.format("%s at template%s: %s", this.getClass().getSimpleName(), String.join("", location), getMessage());
} else {
// The JS implementation doesn't prepend "SyntaxError: " if there's no location.
return String.format("%s: %s", this.getClass().getSimpleName(), getMessage());
}
}
}
static class BaseError extends JSONTemplateError {
public BaseError(String message) {
super(message);
}
}
static class InterpreterError extends BaseError {
public InterpreterError(String message) {
super(message);
}
}
static class TemplateError extends BaseError {
public TemplateError(String message) {
super(message);
}
}
static class BuiltinError extends BaseError {
public BuiltinError(String message) {
super(message);
}
}
// === Type Utils (A subset for the Interpreter) ===
static class TypeUtils {
public static boolean isString(Object expr) {
return expr instanceof String;
}
public static boolean isNumber(Object expr) {
return expr instanceof Number;
}
public static boolean isBool(Object expr) {
return expr instanceof Boolean;
}
public static boolean isNull(Object expr) {
return expr == null;
}
public static boolean isArray(Object expr) {
return expr instanceof List || (expr != null && expr.getClass().isArray());
}
public static boolean isObject(Object expr) {
// In Java, we'll simplify: it's an object if it's a Map
return expr instanceof Map && !(expr instanceof List) || expr instanceof ObjectNode;
}
public static boolean isFunction(Object expr) {
// For Java, we'd use a functional interface or a specific Function object type
return expr instanceof java.util.function.Function || expr instanceof java.util.function.BiFunction || expr instanceof Builtins.Builtin;
}
public static boolean isIntegerNumber(Object expr) {
return expr instanceof Integer || expr instanceof Long || expr instanceof Short || expr instanceof Byte;
}
public static boolean isInteger(Object expr) {
return expr instanceof Integer || expr instanceof Long;
}
public static boolean isTruthy(Object expr) {
if (expr == null) return false;
if (isBool(expr)) return (Boolean) expr;
if (isNumber(expr)) return ((Number) expr).doubleValue() != 0.0;
if (isString(expr)) return !((String) expr).isEmpty();
if (isArray(expr)) {
if (expr.getClass().isArray()) return ((Object[])expr).length > 0;
return !((List<?>) expr).isEmpty();
}
if (isObject(expr)) return !((Map<?, ?>) expr).isEmpty();
if (isFunction(expr)) return true;
return false;
}
public static List<?> objectToList(Object expr) {
if (expr instanceof List) {
return (List<?>) expr;
}
if (expr != null && expr.getClass().isArray()) {
// This is a simplification. For primitive arrays, this would fail.
// Assuming object arrays based on typical JSON structures.
return Arrays.asList((Object[]) expr);
}
// Should not be reached if isArray check is done before calling
return new java.util.ArrayList<>();
}
}
// === Interpreter ===
static class Interpreter {
private final Map<String, Object> context;
public Interpreter(Map<String, Object> context) {
this.context = context;
}
private InterpreterError expectationError(String operator, String expectation) {
return new InterpreterError(String.format("%s expects %s", operator, expectation));
}
private Object castNumber(double num) {
if (num == (long) num) {
if (num >= Integer.MIN_VALUE && num <= Integer.MAX_VALUE) {
return (int) num;
}
return (long) num;
}
return num;
}
public Object interpret(ASTNode tree) {
return visit(tree);
}
private Object visit(ASTNode node) {
if (node == null) {
// This can happen if parsing fails for an empty expression like ${}
return null;
}
switch (node.constructorName) {
case "Primitive":
return visit_Primitive((Primitive) node);
case "UnaryOp":
return visit_UnaryOp((UnaryOp) node);
case "BinOp":
return visit_BinOp((BinOp) node);
that's as many characters as I get for an issue. send me an email address? I'll send the full code and tests. :)
thanks for making json-e
I made a java version. do you want it?
java-implementation-ai-did-it-and-i-held-its-hand-a-little
that's as many characters as I get for an issue. send me an email address? I'll send the full code and tests. :)
thanks for making json-e