Skip to content

Commit

Permalink
Merge pull request #1118 from HubSpot/sharing-alias-name
Browse files Browse the repository at this point in the history
Allow imported files to have variables sharing alias of import name
  • Loading branch information
jasmith-hs authored Oct 11, 2023
2 parents 963ad10 + c9a1165 commit 1963975
Show file tree
Hide file tree
Showing 30 changed files with 864 additions and 651 deletions.
424 changes: 18 additions & 406 deletions src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerImportTag.java

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.google.common.annotations.Beta;
import com.hubspot.jinjava.interpret.JinjavaInterpreter;
import com.hubspot.jinjava.lib.tag.IncludeTag;
import com.hubspot.jinjava.lib.tag.eager.importing.EagerImportingStrategyFactory;
import com.hubspot.jinjava.loader.RelativePathResolver;
import com.hubspot.jinjava.tree.TagNode;
import com.hubspot.jinjava.util.EagerReconstructionUtils;
Expand All @@ -29,7 +30,7 @@ public String innerInterpret(TagNode tagNode, JinjavaInterpreter interpreter) {
tagNode.getStartPosition()
);
templateFile = interpreter.resolveResourceLocation(templateFile);
final String initialPathSetter = EagerImportTag.getSetTagForCurrentPath(
final String initialPathSetter = EagerImportingStrategyFactory.getSetTagForCurrentPath(
interpreter
);
final String newPathSetter = EagerReconstructionUtils.buildBlockOrInlineSetTag(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.google.common.annotations.Beta;
import com.hubspot.jinjava.interpret.JinjavaInterpreter;
import com.hubspot.jinjava.lib.tag.SetTag;
import com.hubspot.jinjava.lib.tag.eager.importing.AliasedEagerImportingStrategy;
import com.hubspot.jinjava.tree.TagNode;
import com.hubspot.jinjava.util.EagerReconstructionUtils;
import com.hubspot.jinjava.util.PrefixToPreserveState;
Expand Down Expand Up @@ -149,35 +150,34 @@ protected PrefixToPreserveState getPrefixToPreserveState(
return prefixToPreserveState;
}

protected String getSuffixToPreserveState(
public static String getSuffixToPreserveState(
String variables,
JinjavaInterpreter interpreter
) {
if (variables.isEmpty()) {
return "";
}
StringBuilder suffixToPreserveState = new StringBuilder();
Optional<String> maybeFullImportAlias = interpreter
.getContext()
.getImportResourceAlias();
if (maybeFullImportAlias.isPresent()) {
String currentImportAlias = maybeFullImportAlias
.get()
.substring(maybeFullImportAlias.get().lastIndexOf(".") + 1);
String filteredVariables = Arrays
.stream(variables.split(","))
.filter(var -> !var.equals(currentImportAlias))
.collect(Collectors.joining(","));
if (!filteredVariables.isEmpty()) {
String updateString = getUpdateString(filteredVariables);
suffixToPreserveState.append(
interpreter.render(
EagerReconstructionUtils.buildDoUpdateTag(
currentImportAlias,
updateString,
interpreter
)
)
);
}
Optional<String> maybeTemporaryImportAlias = AliasedEagerImportingStrategy.getTemporaryImportAlias(
interpreter.getContext()
);
if (
maybeTemporaryImportAlias.isPresent() &&
!AliasedEagerImportingStrategy.isTemporaryImportAlias(variables) &&
!interpreter.getContext().getMetaContextVariables().contains(variables)
) {
String updateString = getUpdateString(variables);

// Don't need to render because the temporary import alias's value is always deferred, and rendering will do nothing
suffixToPreserveState.append(
EagerReconstructionUtils.buildDoUpdateTag(
maybeTemporaryImportAlias.get(),
updateString,
interpreter
)
);
}

return suffixToPreserveState.toString();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
package com.hubspot.jinjava.lib.tag.eager.importing;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import com.hubspot.jinjava.interpret.Context;
import com.hubspot.jinjava.interpret.DeferredValue;
import com.hubspot.jinjava.interpret.DeferredValueException;
import com.hubspot.jinjava.interpret.InterpretException;
import com.hubspot.jinjava.interpret.JinjavaInterpreter;
import com.hubspot.jinjava.lib.fn.MacroFunction;
import com.hubspot.jinjava.lib.tag.eager.DeferredToken;
import com.hubspot.jinjava.objects.collections.PyMap;
import com.hubspot.jinjava.objects.serialization.PyishObjectMapper;
import com.hubspot.jinjava.util.EagerReconstructionUtils;
import com.hubspot.jinjava.util.PrefixToPreserveState;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.StringJoiner;
import java.util.stream.Stream;

public class AliasedEagerImportingStrategy implements EagerImportingStrategy {
private static final String TEMPORARY_IMPORT_ALIAS_PREFIX = "__temp_import_alias_";
private static final String TEMPORARY_IMPORT_ALIAS_FORMAT =
TEMPORARY_IMPORT_ALIAS_PREFIX + "%d__";

public static Optional<String> getTemporaryImportAlias(Context context) {
return context
.getImportResourceAlias()
.map(AliasedEagerImportingStrategy::getTemporaryImportAlias);
}

public static boolean isTemporaryImportAlias(String varName) {
// This is just faster than checking a regex
return varName.startsWith(TEMPORARY_IMPORT_ALIAS_PREFIX);
}

private static String getTemporaryImportAlias(String fullAlias) {
return String.format(
TEMPORARY_IMPORT_ALIAS_FORMAT,
Math.abs(Objects.hashCode(fullAlias))
);
}

private final ImportingData importingData;
private final String currentImportAlias;
private final String fullImportAlias;

@VisibleForTesting
public AliasedEagerImportingStrategy(
ImportingData importingData,
String currentImportAlias
) {
this.importingData = importingData;
this.currentImportAlias = currentImportAlias;
Optional<String> maybeParentImportAlias = importingData
.getOriginalInterpreter()
.getContext()
.getImportResourceAlias();
if (maybeParentImportAlias.isPresent()) {
fullImportAlias =
String.format("%s.%s", maybeParentImportAlias.get(), currentImportAlias);
} else {
fullImportAlias = currentImportAlias;
}
}

@Override
public String handleDeferredTemplateFile(DeferredValueException e) {
return (
importingData.getInitialPathSetter() +
new PrefixToPreserveState(
EagerReconstructionUtils.handleDeferredTokenAndReconstructReferences(
importingData.getOriginalInterpreter(),
DeferredToken
.builderFromToken(importingData.getTagToken())
.addUsedDeferredWords(Stream.of(importingData.getHelpers().get(0)))
.addSetDeferredWords(Stream.of(currentImportAlias))
.build()
)
) +
importingData.getTagToken().getImage()
);
}

@Override
public void setup(JinjavaInterpreter child) {
child.getContext().getScope().put(Context.IMPORT_RESOURCE_ALIAS_KEY, fullImportAlias);
child.getContext().put(Context.IMPORT_RESOURCE_ALIAS_KEY, fullImportAlias);
constructFullAliasPathMap(currentImportAlias, child);
getMapForCurrentContextAlias(currentImportAlias, child);
importingData
.getOriginalInterpreter()
.getContext()
.put(getTemporaryImportAlias(fullImportAlias), DeferredValue.instance());
}

@Override
public void integrateChild(JinjavaInterpreter child) {
JinjavaInterpreter parent = importingData.getOriginalInterpreter();
for (MacroFunction macro : child.getContext().getGlobalMacros().values()) {
if (parent.getContext().isDeferredExecutionMode()) {
macro.setDeferred(true);
}
}
Map<String, Object> childBindings = child.getContext().getSessionBindings();
childBindings.putAll(child.getContext().getGlobalMacros());
String temporaryImportAlias = getTemporaryImportAlias(fullImportAlias);
Map<String, Object> mapForCurrentContextAlias = getMapForCurrentContextAlias(
currentImportAlias,
child
);
childBindings.remove(temporaryImportAlias);
importingData.getOriginalInterpreter().getContext().remove(temporaryImportAlias);
// Remove meta keys
childBindings.remove(Context.GLOBAL_MACROS_SCOPE_KEY);
childBindings.remove(Context.IMPORT_RESOURCE_ALIAS_KEY);
mapForCurrentContextAlias.putAll(childBindings);
}

@Override
public String getFinalOutput(
String newPathSetter,
String output,
JinjavaInterpreter child
) {
String temporaryImportAlias = getTemporaryImportAlias(fullImportAlias);
return (
newPathSetter +
EagerReconstructionUtils.buildBlockOrInlineSetTag(
temporaryImportAlias,
Collections.emptyMap(),
importingData.getOriginalInterpreter()
) +
wrapInChildScope(
EagerImportingStrategy.getSetTagForDeferredChildBindings(
child,
currentImportAlias,
child.getContext()
) +
output,
child
) +
EagerReconstructionUtils.buildSetTag(
ImmutableMap.of(currentImportAlias, temporaryImportAlias),
importingData.getOriginalInterpreter(),
true
) +
importingData.getInitialPathSetter()
);
}

@SuppressWarnings("unchecked")
private static void constructFullAliasPathMap(
String currentImportAlias,
JinjavaInterpreter child
) {
String fullImportAlias = child
.getContext()
.getImportResourceAlias()
.orElse(currentImportAlias);
String[] allAliases = fullImportAlias.split("\\.");
Map<String, Object> currentMap = child.getContext().getParent();
for (int i = 0; i < allAliases.length - 1; i++) {
Object maybeNextMap = currentMap.get(allAliases[i]);
if (maybeNextMap instanceof Map) {
currentMap = (Map<String, Object>) maybeNextMap;
} else if (
maybeNextMap instanceof DeferredValue &&
((DeferredValue) maybeNextMap).getOriginalValue() instanceof Map
) {
currentMap =
(Map<String, Object>) ((DeferredValue) maybeNextMap).getOriginalValue();
} else {
throw new InterpretException("Encountered a problem with import alias maps");
}
}
currentMap.put(
allAliases[allAliases.length - 1],
child.getContext().isDeferredExecutionMode()
? DeferredValue.instance(new PyMap(new HashMap<>()))
: new PyMap(new HashMap<>())
);
}

@SuppressWarnings("unchecked")
private static Map<String, Object> getMapForCurrentContextAlias(
String currentImportAlias,
JinjavaInterpreter child
) {
Object parentValueForChild = child
.getContext()
.getParent()
.getSessionBindings()
.get(currentImportAlias);
if (parentValueForChild instanceof Map) {
return (Map<String, Object>) parentValueForChild;
} else if (parentValueForChild instanceof DeferredValue) {
if (((DeferredValue) parentValueForChild).getOriginalValue() instanceof Map) {
return (Map<String, Object>) (
(DeferredValue) parentValueForChild
).getOriginalValue();
}
Map<String, Object> newMap = new PyMap(new HashMap<>());
child
.getContext()
.getParent()
.put(currentImportAlias, DeferredValue.instance(newMap));
return newMap;
} else {
Map<String, Object> newMap = new PyMap(new HashMap<>());
child
.getContext()
.getParent()
.put(
currentImportAlias,
child.getContext().isDeferredExecutionMode()
? DeferredValue.instance(newMap)
: newMap
);
return newMap;
}
}

private String wrapInChildScope(String output, JinjavaInterpreter child) {
String combined =
output + getDoTagToPreserve(importingData.getOriginalInterpreter(), child);
// So that any set variables other than the alias won't exist outside the child's scope
return EagerReconstructionUtils.wrapInChildScope(
combined,
importingData.getOriginalInterpreter()
);
}

private String getDoTagToPreserve(
JinjavaInterpreter interpreter,
JinjavaInterpreter child
) {
StringJoiner keyValueJoiner = new StringJoiner(",");
String temporaryImportAlias = getTemporaryImportAlias(fullImportAlias);
Map<String, Object> currentAliasMap = getMapForCurrentContextAlias(
currentImportAlias,
child
);
for (Map.Entry<String, Object> entry : currentAliasMap.entrySet()) {
if (entry.getKey().equals(temporaryImportAlias)) {
continue;
}
if (entry.getValue() instanceof DeferredValue) {
keyValueJoiner.add(String.format("'%s': %s", entry.getKey(), entry.getKey()));
} else if (!(entry.getValue() instanceof MacroFunction)) {
keyValueJoiner.add(
String.format(
"'%s': %s",
entry.getKey(),
PyishObjectMapper.getAsPyishString(entry.getValue())
)
);
}
}
if (keyValueJoiner.length() > 0) {
return EagerReconstructionUtils.buildDoUpdateTag(
temporaryImportAlias,
"{" + keyValueJoiner.toString() + "}",
interpreter
);
}
return "";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.hubspot.jinjava.lib.tag.eager.importing;

import com.hubspot.jinjava.interpret.DeferredValue;
import com.hubspot.jinjava.interpret.DeferredValueException;
import com.hubspot.jinjava.interpret.JinjavaInterpreter;
import com.hubspot.jinjava.util.EagerReconstructionUtils;
import java.util.Map;
import java.util.stream.Collectors;

public interface EagerImportingStrategy {
String handleDeferredTemplateFile(DeferredValueException e);
void setup(JinjavaInterpreter child);

void integrateChild(JinjavaInterpreter child);
String getFinalOutput(String newPathSetter, String output, JinjavaInterpreter child);

static String getSetTagForDeferredChildBindings(
JinjavaInterpreter interpreter,
String currentImportAlias,
Map<String, Object> childBindings
) {
return childBindings
.entrySet()
.stream()
.filter(
entry ->
entry.getValue() instanceof DeferredValue &&
((DeferredValue) entry.getValue()).getOriginalValue() != null
)
.filter(entry -> !interpreter.getContext().containsKey(entry.getKey()))
.filter(entry -> !entry.getKey().equals(currentImportAlias))
.map(
entry ->
EagerReconstructionUtils.buildBlockOrInlineSetTag( // don't register deferred token so that we don't defer them on higher context scopes; they only exist in the child scope
entry.getKey(),
((DeferredValue) entry.getValue()).getOriginalValue(),
interpreter
)
)
.collect(Collectors.joining());
}
}
Loading

0 comments on commit 1963975

Please sign in to comment.