Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

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

Copy link
Copy link

Description

@jayamarks
Issue body actions

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

Reactions are currently unavailable

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

    Morty Proxy This is a proxified and sanitized view of the page, visit original site.