Skip to content

I made a java version. do you want it? #559

@jayamarks

Description

@jayamarks

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions