Skip to content

Commit

Permalink
Merge pull request #1031 from HubSpot/allow-snake-case
Browse files Browse the repository at this point in the history
Add `allow_snake_case` filter and legacy override to serialize with snake_case
  • Loading branch information
jasmith-hs authored Mar 17, 2023
2 parents 189046b + b28c7dc commit 5124456
Show file tree
Hide file tree
Showing 12 changed files with 318 additions and 16 deletions.
16 changes: 14 additions & 2 deletions src/main/java/com/hubspot/jinjava/JinjavaConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import static com.hubspot.jinjava.lib.fn.Functions.DEFAULT_RANGE_LIMIT;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.hubspot.jinjava.el.JinjavaInterpreterResolver;
import com.hubspot.jinjava.el.JinjavaNodePreProcessor;
import com.hubspot.jinjava.el.JinjavaObjectUnwrapper;
Expand All @@ -44,6 +45,7 @@
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;
import javax.annotation.Nullable;
import javax.el.ELResolver;

public class JinjavaConfig {
Expand Down Expand Up @@ -135,11 +137,21 @@ private JinjavaConfig(Builder builder) {
legacyOverrides = builder.legacyOverrides;
dateTimeProvider = builder.dateTimeProvider;
enablePreciseDivideFilter = builder.enablePreciseDivideFilter;
objectMapper = builder.objectMapper;
objectMapper = setupObjectMapper(builder.objectMapper);
objectUnwrapper = builder.objectUnwrapper;
nodePreProcessor = builder.nodePreProcessor;
}

private ObjectMapper setupObjectMapper(@Nullable ObjectMapper objectMapper) {
if (objectMapper == null) {
objectMapper = new ObjectMapper();
if (legacyOverrides.isUseSnakeCasePropertyNaming()) {
objectMapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
}
}
return objectMapper;
}

public Charset getCharset() {
return charset;
}
Expand Down Expand Up @@ -298,7 +310,7 @@ public static class Builder {
private ExecutionMode executionMode = DefaultExecutionMode.instance();
private LegacyOverrides legacyOverrides = LegacyOverrides.NONE;
private boolean enablePreciseDivideFilter = false;
private ObjectMapper objectMapper = new ObjectMapper();
private ObjectMapper objectMapper = null;

private ObjectUnwrapper objectUnwrapper = new JinjavaObjectUnwrapper();
private BiConsumer<Node, JinjavaInterpreter> nodePreProcessor = new JinjavaNodePreProcessor();
Expand Down
13 changes: 13 additions & 0 deletions src/main/java/com/hubspot/jinjava/LegacyOverrides.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public class LegacyOverrides {
private final boolean evaluateMapKeys;
private final boolean iterateOverMapKeys;
private final boolean usePyishObjectMapper;
private final boolean useSnakeCasePropertyNaming;
private final boolean whitespaceRequiredWithinTokens;
private final boolean useNaturalOperatorPrecedence;
private final boolean parseWhitespaceControlStrictly;
Expand All @@ -17,6 +18,7 @@ private LegacyOverrides(Builder builder) {
evaluateMapKeys = builder.evaluateMapKeys;
iterateOverMapKeys = builder.iterateOverMapKeys;
usePyishObjectMapper = builder.usePyishObjectMapper;
useSnakeCasePropertyNaming = builder.useSnakeCasePropertyNaming;
whitespaceRequiredWithinTokens = builder.whitespaceRequiredWithinTokens;
useNaturalOperatorPrecedence = builder.useNaturalOperatorPrecedence;
parseWhitespaceControlStrictly = builder.parseWhitespaceControlStrictly;
Expand All @@ -38,6 +40,10 @@ public boolean isUsePyishObjectMapper() {
return usePyishObjectMapper;
}

public boolean isUseSnakeCasePropertyNaming() {
return useSnakeCasePropertyNaming;
}

public boolean isWhitespaceRequiredWithinTokens() {
return whitespaceRequiredWithinTokens;
}
Expand All @@ -54,6 +60,7 @@ public static class Builder {
private boolean evaluateMapKeys = false;
private boolean iterateOverMapKeys = false;
private boolean usePyishObjectMapper = false;
private boolean useSnakeCasePropertyNaming = false;
private boolean whitespaceRequiredWithinTokens = false;
private boolean useNaturalOperatorPrecedence = false;
private boolean parseWhitespaceControlStrictly = false;
Expand All @@ -69,6 +76,7 @@ public static Builder from(LegacyOverrides legacyOverrides) {
.withEvaluateMapKeys(legacyOverrides.evaluateMapKeys)
.withIterateOverMapKeys(legacyOverrides.iterateOverMapKeys)
.withUsePyishObjectMapper(legacyOverrides.usePyishObjectMapper)
.withUseSnakeCasePropertyNaming(legacyOverrides.useSnakeCasePropertyNaming)
.withWhitespaceRequiredWithinTokens(
legacyOverrides.whitespaceRequiredWithinTokens
)
Expand All @@ -93,6 +101,11 @@ public Builder withUsePyishObjectMapper(boolean usePyishObjectMapper) {
return this;
}

public Builder withUseSnakeCasePropertyNaming(boolean useSnakeCasePropertyNaming) {
this.useSnakeCasePropertyNaming = useSnakeCasePropertyNaming;
return this;
}

public Builder withWhitespaceRequiredWithinTokens(
boolean whitespaceRequiredWithinTokens
) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.hubspot.jinjava.lib.filter;

import com.hubspot.jinjava.doc.annotations.JinjavaDoc;
import com.hubspot.jinjava.doc.annotations.JinjavaParam;
import com.hubspot.jinjava.doc.annotations.JinjavaSnippet;
import com.hubspot.jinjava.interpret.JinjavaInterpreter;
import com.hubspot.jinjava.objects.collections.PyMap;
import com.hubspot.jinjava.objects.collections.SizeLimitingPyMap;
import com.hubspot.jinjava.objects.collections.SnakeCaseAccessibleMap;
import java.util.Map;

@JinjavaDoc(
value = "Allow keys on the provided camelCase map to be accessed using snake_case",
input = @JinjavaParam(
value = "map",
type = "dict",
desc = "The dict to make keys accessible using snake_case",
required = true
),
snippets = { @JinjavaSnippet(code = "{{ {'fooBar': 'baz'}|allow_snake_case }}") }
)
public class AllowSnakeCaseFilter implements Filter {
public static final String NAME = "allow_snake_case";

@Override
public String getName() {
return NAME;
}

@Override
public Object filter(Object var, JinjavaInterpreter interpreter, String... args) {
if (!(var instanceof Map)) {
return var;
}
Map<String, Object> map = (Map<String, Object>) var;
if (map instanceof PyMap) {
map = ((PyMap) map).toMap();
}
return new SnakeCaseAccessibleMap(
new SizeLimitingPyMap(map, interpreter.getConfig().getMaxMapSize())
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ protected void registerDefaults() {
registerClasses(
AbsFilter.class,
AddFilter.class,
AllowSnakeCaseFilter.class,
AttrFilter.class,
Base64DecodeFilter.class,
Base64EncodeFilter.class,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.hubspot.jinjava.lib.filter;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.hubspot.jinjava.doc.annotations.JinjavaDoc;
import com.hubspot.jinjava.doc.annotations.JinjavaParam;
import com.hubspot.jinjava.doc.annotations.JinjavaSnippet;
Expand All @@ -19,7 +18,6 @@
snippets = { @JinjavaSnippet(code = "{{object|fromJson}}") }
)
public class FromJsonFilter implements Filter {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

@Override
public Object filter(Object var, JinjavaInterpreter interpreter, String... args) {
Expand All @@ -31,7 +29,10 @@ public Object filter(Object var, JinjavaInterpreter interpreter, String... args)
throw new InvalidInputException(interpreter, this, InvalidReason.STRING);
}
try {
return OBJECT_MAPPER.readValue((String) var, Object.class);
return interpreter
.getConfig()
.getObjectMapper()
.readValue((String) var, Object.class);
} catch (IOException e) {
throw new InvalidInputException(interpreter, this, InvalidReason.JSON_READ);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.hubspot.jinjava.objects.collections;

import com.google.common.base.CaseFormat;
import com.hubspot.jinjava.lib.filter.AllowSnakeCaseFilter;
import com.hubspot.jinjava.objects.serialization.PyishSerializable;
import java.io.IOException;
import java.util.Map;

public class SnakeCaseAccessibleMap extends PyMap implements PyishSerializable {

public SnakeCaseAccessibleMap(Map<String, Object> map) {
super(map);
}

@Override
public Object get(Object key) {
Object result = super.get(key);
if (result == null && key instanceof String) {
return getWithCamelCase((String) key);
}
return result;
}

private Object getWithCamelCase(String key) {
if (key == null) {
return null;
}
if (key.indexOf('_') == -1) {
return null;
}
return super.get(CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, key));
}

@SuppressWarnings("unchecked")
@Override
public <T extends Appendable & CharSequence> T appendPyishString(T appendable)
throws IOException {
return (T) appendable
.append(PyishSerializable.writeValueAsString(toMap()))
.append('|')
.append(AllowSnakeCaseFilter.NAME);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.hubspot.jinjava.objects.serialization;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.hubspot.jinjava.lib.filter.AllowSnakeCaseFilter;
import java.io.IOException;

public class BothCasingBeanSerializer<T> extends JsonSerializer<T> {
private final JsonSerializer<T> orignalSerializer;

private BothCasingBeanSerializer(JsonSerializer<T> jsonSerializer) {
this.orignalSerializer = jsonSerializer;
}

public static <T> BothCasingBeanSerializer<T> wrapping(
JsonSerializer<T> jsonSerializer
) {
return new BothCasingBeanSerializer<>(jsonSerializer);
}

@Override
public void serialize(
T value,
JsonGenerator gen,
SerializerProvider serializerProvider
)
throws IOException {
if (
Boolean.TRUE.equals(
serializerProvider.getAttribute(PyishObjectMapper.ALLOW_SNAKE_CASE_ATTRIBUTE)
)
) {
// if it's directly for output, then we don't want to add the additional filter characters,
// as doing so would make the "|allow_snake_case" appear in the final output.
StringBuilder sb = new StringBuilder();
sb
.append(PyishSerializable.writeValueAsString(value))
.append('|')
.append(AllowSnakeCaseFilter.NAME);
gen.writeRawValue(sb.toString());
} else {
orignalSerializer.serialize(value, gen, serializerProvider);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,16 @@ public void serialize(
);
String key;
String value;
ObjectWriter objectWriter = PyishObjectMapper.PYISH_OBJECT_WRITER.withAttribute(
PyishObjectMapper.ALLOW_SNAKE_CASE_ATTRIBUTE,
serializerProvider.getAttribute(PyishObjectMapper.ALLOW_SNAKE_CASE_ATTRIBUTE)
);
if (remainingLength != null) {
ObjectWriter objectWriter = PyishObjectMapper.PYISH_OBJECT_WRITER.withAttribute(
LengthLimitingWriter.REMAINING_LENGTH_ATTRIBUTE,
remainingLength
);
objectWriter =
objectWriter.withAttribute(
LengthLimitingWriter.REMAINING_LENGTH_ATTRIBUTE,
remainingLength
);
key = objectWriter.writeValueAsString(entry.getKey());
LengthLimitingWriter lengthLimitingWriter = new LengthLimitingWriter(
new CharArrayWriter(),
Expand All @@ -39,8 +44,8 @@ public void serialize(
objectWriter.writeValue(lengthLimitingWriter, entry.getValue());
value = lengthLimitingWriter.toString();
} else {
key = PyishObjectMapper.PYISH_OBJECT_WRITER.writeValueAsString(entry.getKey());
value = PyishObjectMapper.PYISH_OBJECT_WRITER.writeValueAsString(entry.getValue());
key = objectWriter.writeValueAsString(entry.getKey());
value = objectWriter.writeValueAsString(entry.getValue());
}
jsonGenerator.writeRawValue(String.format("fn:map_entry(%s, %s)", key, value));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializationConfig;
import com.fasterxml.jackson.databind.ser.BeanSerializer;
import com.fasterxml.jackson.databind.ser.BeanSerializerModifier;
import java.util.Map;

Expand All @@ -23,6 +24,9 @@ public JsonSerializer<?> modifySerializer(
if (Map.Entry.class.isAssignableFrom(beanDesc.getBeanClass())) {
return MapEntrySerializer.INSTANCE;
}
if (serializer instanceof BeanSerializer) {
return BothCasingBeanSerializer.wrapping(serializer);
}
return serializer;
} else {
return PyishSerializer.INSTANCE;
Expand Down
Loading

0 comments on commit 5124456

Please sign in to comment.