Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add eager deferred macro logic #547

Merged
merged 12 commits into from
Nov 30, 2020
22 changes: 22 additions & 0 deletions src/main/java/com/hubspot/jinjava/interpret/Context.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.Stack;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -223,6 +224,27 @@ public boolean isGlobalMacro(String identifier) {
return getGlobalMacro(identifier) != null;
}

public Optional<MacroFunction> getLocalMacro(String fullName) {
String[] nameArray = fullName.split("\\.", 2);
if (nameArray.length != 2) {
return Optional.empty();
}
String localKey = nameArray[0];
String macroName = nameArray[1];
Object localValue = get(localKey);
if (localValue instanceof DeferredValue) {
localValue = ((DeferredValue) localValue).getOriginalValue();
}
if (!(localValue instanceof Map)) {
return Optional.empty();
}
Object possibleMacroFunction = ((Map<String, Object>) localValue).get(macroName);
if (possibleMacroFunction instanceof MacroFunction) {
return Optional.of((MacroFunction) possibleMacroFunction);
}
return Optional.empty();
}

public boolean isAutoEscape() {
if (autoEscape != null) {
return autoEscape;
Expand Down
100 changes: 62 additions & 38 deletions src/main/java/com/hubspot/jinjava/lib/fn/MacroFunction.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,45 @@ public MacroFunction(
this.deferred = false;
}

public MacroFunction(MacroFunction source, String name) {
super(name, (LinkedHashMap<String, Object>) source.getDefaults());
this.content = source.content;
this.caller = source.caller;
this.localContextScope = source.localContextScope;
this.definitionLineNumber = source.definitionLineNumber;
this.definitionStartPosition = source.definitionStartPosition;
this.deferred = source.deferred;
}

@Override
public Object doEvaluate(
Map<String, Object> argMap,
Map<String, Object> kwargMap,
List<Object> varArgs
) {
JinjavaInterpreter interpreter = JinjavaInterpreter.getCurrent();
Optional<String> importFile = getImportFile(interpreter);
try (InterpreterScopeClosable c = interpreter.enterScope()) {
String result = getEvaluationResult(argMap, kwargMap, varArgs, interpreter);

if (
!interpreter.getContext().getDeferredNodes().isEmpty() ||
!interpreter.getContext().getEagerTokens().isEmpty()
) {
throw new DeferredValueException(
getName(),
interpreter.getLineNumber(),
interpreter.getPosition()
);
}

return result;
} finally {
importFile.ifPresent(path -> interpreter.getContext().getCurrentPathStack().pop());
}
}

public Optional<String> getImportFile(JinjavaInterpreter interpreter) {
Optional<String> importFile = Optional.ofNullable(
(String) localContextScope.get(Context.IMPORT_RESOURCE_PATH_KEY)
);
Expand All @@ -72,50 +104,42 @@ public Object doEvaluate(
interpreter.getPosition()
)
);
return importFile;
}

try (InterpreterScopeClosable c = interpreter.enterScope()) {
interpreter.setLineNumber(definitionLineNumber);
interpreter.setPosition(definitionStartPosition);

for (Map.Entry<String, Object> scopeEntry : localContextScope
.getScope()
.entrySet()) {
if (scopeEntry.getValue() instanceof MacroFunction) {
interpreter.getContext().addGlobalMacro((MacroFunction) scopeEntry.getValue());
} else {
interpreter.getContext().put(scopeEntry.getKey(), scopeEntry.getValue());
}
}

// named parameters
for (Map.Entry<String, Object> argEntry : argMap.entrySet()) {
interpreter.getContext().put(argEntry.getKey(), argEntry.getValue());
public String getEvaluationResult(
Map<String, Object> argMap,
Map<String, Object> kwargMap,
List<Object> varArgs,
JinjavaInterpreter interpreter
) {
interpreter.setLineNumber(definitionLineNumber);
interpreter.setPosition(definitionStartPosition);
for (Map.Entry<String, Object> scopeEntry : localContextScope.getScope().entrySet()) {
if (scopeEntry.getValue() instanceof MacroFunction) {
interpreter.getContext().addGlobalMacro((MacroFunction) scopeEntry.getValue());
} else {
interpreter.getContext().put(scopeEntry.getKey(), scopeEntry.getValue());
}
// parameter map
interpreter.getContext().put("kwargs", kwargMap);
// varargs list
interpreter.getContext().put("varargs", varArgs);

LengthLimitingStringBuilder result = new LengthLimitingStringBuilder(
interpreter.getConfig().getMaxOutputSize()
);
}

for (Node node : content) {
result.append(node.render(interpreter));
}
// named parameters
for (Map.Entry<String, Object> argEntry : argMap.entrySet()) {
interpreter.getContext().put(argEntry.getKey(), argEntry.getValue());
}
// parameter map
interpreter.getContext().put("kwargs", kwargMap);
// varargs list
interpreter.getContext().put("varargs", varArgs);

if (!interpreter.getContext().getDeferredNodes().isEmpty()) {
throw new DeferredValueException(
getName(),
interpreter.getLineNumber(),
interpreter.getPosition()
);
}
LengthLimitingStringBuilder result = new LengthLimitingStringBuilder(
interpreter.getConfig().getMaxOutputSize()
);

return result.toString();
} finally {
importFile.ifPresent(path -> interpreter.getContext().getCurrentPathStack().pop());
for (Node node : content) {
result.append(node.render(interpreter));
}
return result.toString();
}

public void setDeferred(boolean deferred) {
Expand Down
121 changes: 121 additions & 0 deletions src/main/java/com/hubspot/jinjava/lib/fn/eager/EagerMacroFunction.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package com.hubspot.jinjava.lib.fn.eager;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.hubspot.jinjava.el.ext.AbstractCallableMethod;
import com.hubspot.jinjava.interpret.DeferredValue;
import com.hubspot.jinjava.interpret.DeferredValueException;
import com.hubspot.jinjava.interpret.JinjavaInterpreter;
import com.hubspot.jinjava.interpret.JinjavaInterpreter.InterpreterScopeClosable;
import com.hubspot.jinjava.lib.fn.MacroFunction;
import com.hubspot.jinjava.lib.tag.MacroTag;
import com.hubspot.jinjava.util.ChunkResolver;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.StringJoiner;

public class EagerMacroFunction extends AbstractCallableMethod {
private String fullName;
private MacroFunction macroFunction;
private JinjavaInterpreter interpreter;

public EagerMacroFunction(
String fullName,
MacroFunction macroFunction,
JinjavaInterpreter interpreter
) {
super(
macroFunction.getName(),
getLinkedHashmap(macroFunction.getArguments(), macroFunction.getDefaults())
);
this.fullName = fullName;
this.macroFunction = macroFunction;
this.interpreter = interpreter;
}

private static LinkedHashMap<String, Object> getLinkedHashmap(
List<String> args,
Map<String, Object> defaults
) {
LinkedHashMap<String, Object> linkedHashMap = new LinkedHashMap<>();
for (String arg : args) {
linkedHashMap.put(arg, defaults.get(arg));
}
return linkedHashMap;
}

public Object doEvaluate(
Map<String, Object> argMap,
Map<String, Object> kwargMap,
List<Object> varArgs
) {
Optional<String> importFile = macroFunction.getImportFile(interpreter);
try (InterpreterScopeClosable c = interpreter.enterScope()) {
return macroFunction.getEvaluationResult(argMap, kwargMap, varArgs, interpreter);
} finally {
importFile.ifPresent(path -> interpreter.getContext().getCurrentPathStack().pop());
}
}

public String getStartTag(JinjavaInterpreter interpreter) {
StringJoiner argJoiner = new StringJoiner(", ");
for (String arg : macroFunction.getArguments()) {
try {
if (macroFunction.getDefaults().get(arg) != null) {
argJoiner.add(
String.format(
"%s=%s",
arg,
ChunkResolver.getValueAsJinjavaString(macroFunction.getDefaults().get(arg))
)
);
continue;
}
} catch (JsonProcessingException ignored) {}
argJoiner.add(arg);
}
return new StringJoiner(" ")
.add(interpreter.getConfig().getTokenScannerSymbols().getExpressionStartWithTag())
.add(MacroTag.TAG_NAME)
.add(String.format("%s(%s)", fullName, argJoiner.toString()))
.add(interpreter.getConfig().getTokenScannerSymbols().getExpressionEndWithTag())
.toString();
}

public String getEndTag(JinjavaInterpreter interpreter) {
return new StringJoiner(" ")
.add(interpreter.getConfig().getTokenScannerSymbols().getExpressionStartWithTag())
.add(String.format("end%s", MacroTag.TAG_NAME))
.add(interpreter.getConfig().getTokenScannerSymbols().getExpressionEndWithTag())
.toString();
}

/**
* Reconstruct the image of the macro function, @see MacroFunction#reconstructImage()
* This image, however, may be partially or fully resolved depending on the
* usage of the arguments, which are filled in as deferred values, and any values on
* this interpreter's context.
* @return An image of the macro function that's body is resolved as much as possible.
* This image allows for the macro function to be recreated during a later
* rendering pass.
*/
public String reconstructImage() {
String result;
try {
result =
(String) evaluate(
macroFunction
.getArguments()
.stream()
.map(arg -> DeferredValue.instance())
.toArray()
);
} catch (DeferredValueException e) {
// In case something not eager-supported encountered a deferred value
return macroFunction.reconstructImage();
}

return (getStartTag(interpreter) + result + getEndTag(interpreter));
}
}
Loading