From 4134c007a035f68744ef54b8d05ac2f8fe965967 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Wed, 15 Mar 2023 11:14:58 -0400 Subject: [PATCH 1/7] Add filter which converts a map with camelCase attributes into one that also accepts snake_case keys --- .../lib/filter/AllowSnakeCaseFilter.java | 43 +++++++++++++++++++ .../jinjava/lib/filter/FilterLibrary.java | 1 + .../collections/SnakeCaseAccessibleMap.java | 43 +++++++++++++++++++ .../lib/filter/AllowSnakeCaseFilterTest.java | 28 ++++++++++++ 4 files changed, 115 insertions(+) create mode 100644 src/main/java/com/hubspot/jinjava/lib/filter/AllowSnakeCaseFilter.java create mode 100644 src/main/java/com/hubspot/jinjava/objects/collections/SnakeCaseAccessibleMap.java create mode 100644 src/test/java/com/hubspot/jinjava/lib/filter/AllowSnakeCaseFilterTest.java diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/AllowSnakeCaseFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/AllowSnakeCaseFilter.java new file mode 100644 index 000000000..3b9868968 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/lib/filter/AllowSnakeCaseFilter.java @@ -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 map = (Map) var; + if (map instanceof PyMap) { + map = ((PyMap) map).toMap(); + } + return new SnakeCaseAccessibleMap( + new SizeLimitingPyMap(map, interpreter.getConfig().getMaxMapSize()) + ); + } +} diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/FilterLibrary.java b/src/main/java/com/hubspot/jinjava/lib/filter/FilterLibrary.java index 216a1d2bd..f5dfc30e3 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/FilterLibrary.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/FilterLibrary.java @@ -32,6 +32,7 @@ protected void registerDefaults() { registerClasses( AbsFilter.class, AddFilter.class, + AllowSnakeCaseFilter.class, AttrFilter.class, Base64DecodeFilter.class, Base64EncodeFilter.class, diff --git a/src/main/java/com/hubspot/jinjava/objects/collections/SnakeCaseAccessibleMap.java b/src/main/java/com/hubspot/jinjava/objects/collections/SnakeCaseAccessibleMap.java new file mode 100644 index 000000000..1c4b68c90 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/objects/collections/SnakeCaseAccessibleMap.java @@ -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 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 appendPyishString(T appendable) + throws IOException { + return (T) appendable + .append(PyishSerializable.writeValueAsString(toMap())) + .append('|') + .append(AllowSnakeCaseFilter.NAME); + } +} diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/AllowSnakeCaseFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/AllowSnakeCaseFilterTest.java new file mode 100644 index 000000000..5db6305b5 --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/lib/filter/AllowSnakeCaseFilterTest.java @@ -0,0 +1,28 @@ +package com.hubspot.jinjava.lib.filter; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.hubspot.jinjava.BaseInterpretingTest; +import com.hubspot.jinjava.objects.serialization.PyishObjectMapper; +import org.junit.Test; + +public class AllowSnakeCaseFilterTest extends BaseInterpretingTest { + + @Test + public void itDoesNotChangeNonMaps() { + assertThat(interpreter.render("{{ 'fooBar'|allow_snake_case }}")).isEqualTo("fooBar"); + } + + @Test + public void itMakesMapKeysAccessibleWithSnakeCase() { + assertThat(interpreter.render("{{ ({'fooBar': 'foo'}|allow_snake_case).foo_bar }}")) + .isEqualTo("foo"); + } + + @Test + public void itReserializesAsSnakeCaseAccessibleMap() { + interpreter.render("{% set map = {'fooBar': 'foo'}|allow_snake_case %}"); + assertThat(PyishObjectMapper.getAsPyishString(interpreter.getContext().get("map"))) + .isEqualTo("{'fooBar': 'foo'} |allow_snake_case"); + } +} From 0f535bae37ae7446d826ca6997e03dfde104aebd Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Wed, 15 Mar 2023 11:15:22 -0400 Subject: [PATCH 2/7] When executing in eager execution, reconstruct beans to be accessible with snake case --- .../BothCasingBeanSerializer.java | 24 +++++++++++++ .../PyishBeanSerializerModifier.java | 9 +++++ .../serialization/PyishObjectMapper.java | 11 +++++- .../serialization/PyishObjectMapperTest.java | 36 +++++++++++++++++++ 4 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/hubspot/jinjava/objects/serialization/BothCasingBeanSerializer.java diff --git a/src/main/java/com/hubspot/jinjava/objects/serialization/BothCasingBeanSerializer.java b/src/main/java/com/hubspot/jinjava/objects/serialization/BothCasingBeanSerializer.java new file mode 100644 index 000000000..2552f296f --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/BothCasingBeanSerializer.java @@ -0,0 +1,24 @@ +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 extends JsonSerializer { + public static final BothCasingBeanSerializer INSTANCE = new BothCasingBeanSerializer(); + + private BothCasingBeanSerializer() {} + + @Override + public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) + throws IOException { + StringBuilder sb = new StringBuilder(); + sb + .append(PyishSerializable.writeValueAsString(value)) + .append('|') + .append(AllowSnakeCaseFilter.NAME); + gen.writeRawValue(sb.toString()); + } +} diff --git a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishBeanSerializerModifier.java b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishBeanSerializerModifier.java index a55f0bf36..600173439 100644 --- a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishBeanSerializerModifier.java +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishBeanSerializerModifier.java @@ -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; @@ -23,6 +24,14 @@ public JsonSerializer modifySerializer( if (Map.Entry.class.isAssignableFrom(beanDesc.getBeanClass())) { return MapEntrySerializer.INSTANCE; } + if ( + serializer instanceof BeanSerializer && + Boolean.TRUE.equals( + config.getAttributes().getAttribute(PyishObjectMapper.EAGER_EXECUTION_ATTRIBUTE) + ) + ) { + return BothCasingBeanSerializer.INSTANCE; + } return serializer; } else { return PyishSerializer.INSTANCE; diff --git a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java index 14da54606..055168903 100644 --- a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java @@ -19,6 +19,7 @@ public class PyishObjectMapper { public static final ObjectWriter PYISH_OBJECT_WRITER; + public static final String EAGER_EXECUTION_ATTRIBUTE = "eagerExecution"; static { ObjectMapper mapper = new ObjectMapper( @@ -75,7 +76,15 @@ public static String getAsPyishStringOrThrow(Object val) throws IOException { } else { writer = new CharArrayWriter(); } - objectWriter.writeValue(writer, val); + objectWriter + .withAttribute( + EAGER_EXECUTION_ATTRIBUTE, + JinjavaInterpreter + .getCurrentMaybe() + .map(interpreter -> interpreter.getConfig().getExecutionMode().useEagerParser()) + .orElse(false) + ) + .writeValue(writer, val); return writer.toString(); } diff --git a/src/test/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapperTest.java b/src/test/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapperTest.java index cadcb8be9..b5e1cd604 100644 --- a/src/test/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapperTest.java +++ b/src/test/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapperTest.java @@ -8,6 +8,7 @@ import com.hubspot.jinjava.Jinjava; import com.hubspot.jinjava.JinjavaConfig; import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.mode.EagerExecutionMode; import com.hubspot.jinjava.objects.collections.SizeLimitingPyMap; import java.util.ArrayList; import java.util.HashMap; @@ -88,4 +89,39 @@ public void itLimitsDepth() { JinjavaInterpreter.popCurrent(); } } + + @Test + public void itSerializesToSnakeCaseAccessibleMap() { + try { + Jinjava jinjava = new Jinjava( + JinjavaConfig + .newBuilder() + .withExecutionMode(EagerExecutionMode.instance()) + .build() + ); + JinjavaInterpreter.pushCurrent(jinjava.newInterpreter()); + assertThat(PyishObjectMapper.getAsPyishString(new Foo("bar"))) + .isEqualTo("{'fooBar': 'bar'} |allow_snake_case"); + } finally { + JinjavaInterpreter.popCurrent(); + } + } + + @Test + public void itDoesNotConvertToSnakeCaseMapInDefaultExecutionMode() { + assertThat(PyishObjectMapper.getAsPyishString(new Foo("bar")).trim()) + .isEqualTo("{'fooBar': 'bar'}"); + } + + static class Foo { + private final String bar; + + public Foo(String bar) { + this.bar = bar; + } + + public String getFooBar() { + return bar; + } + } } From dfde72a5626aad275996e277ee688327833f2f6f Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Thu, 16 Mar 2023 13:42:48 -0400 Subject: [PATCH 3/7] Change where eager execution attribute is checked to make use of caching serializers --- .../BothCasingBeanSerializer.java | 40 ++++++++++++++----- .../PyishBeanSerializerModifier.java | 9 +---- 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/objects/serialization/BothCasingBeanSerializer.java b/src/main/java/com/hubspot/jinjava/objects/serialization/BothCasingBeanSerializer.java index 2552f296f..d25119d30 100644 --- a/src/main/java/com/hubspot/jinjava/objects/serialization/BothCasingBeanSerializer.java +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/BothCasingBeanSerializer.java @@ -6,19 +6,39 @@ import com.hubspot.jinjava.lib.filter.AllowSnakeCaseFilter; import java.io.IOException; -public class BothCasingBeanSerializer extends JsonSerializer { - public static final BothCasingBeanSerializer INSTANCE = new BothCasingBeanSerializer(); +public class BothCasingBeanSerializer extends JsonSerializer { + private final JsonSerializer orignalSerializer; - private BothCasingBeanSerializer() {} + private BothCasingBeanSerializer(JsonSerializer jsonSerializer) { + this.orignalSerializer = jsonSerializer; + } + + public static BothCasingBeanSerializer wrapping( + JsonSerializer jsonSerializer + ) { + return new BothCasingBeanSerializer<>(jsonSerializer); + } @Override - public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) + public void serialize( + T value, + JsonGenerator gen, + SerializerProvider serializerProvider + ) throws IOException { - StringBuilder sb = new StringBuilder(); - sb - .append(PyishSerializable.writeValueAsString(value)) - .append('|') - .append(AllowSnakeCaseFilter.NAME); - gen.writeRawValue(sb.toString()); + if ( + Boolean.TRUE.equals( + serializerProvider.getAttribute(PyishObjectMapper.EAGER_EXECUTION_ATTRIBUTE) + ) + ) { + StringBuilder sb = new StringBuilder(); + sb + .append(PyishSerializable.writeValueAsString(value)) + .append('|') + .append(AllowSnakeCaseFilter.NAME); + gen.writeRawValue(sb.toString()); + } else { + orignalSerializer.serialize(value, gen, serializerProvider); + } } } diff --git a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishBeanSerializerModifier.java b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishBeanSerializerModifier.java index 600173439..a3d84cdb1 100644 --- a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishBeanSerializerModifier.java +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishBeanSerializerModifier.java @@ -24,13 +24,8 @@ public JsonSerializer modifySerializer( if (Map.Entry.class.isAssignableFrom(beanDesc.getBeanClass())) { return MapEntrySerializer.INSTANCE; } - if ( - serializer instanceof BeanSerializer && - Boolean.TRUE.equals( - config.getAttributes().getAttribute(PyishObjectMapper.EAGER_EXECUTION_ATTRIBUTE) - ) - ) { - return BothCasingBeanSerializer.INSTANCE; + if (serializer instanceof BeanSerializer) { + return BothCasingBeanSerializer.wrapping(serializer); } return serializer; } else { From c9f612817dceab05695786605c09a7264e98eced Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Thu, 16 Mar 2023 14:00:35 -0400 Subject: [PATCH 4/7] Change to only use allow_snake_case filter when the serialization isn't directly for output --- .../BothCasingBeanSerializer.java | 4 ++- .../serialization/PyishObjectMapper.java | 23 +++++++------ .../serialization/PyishObjectMapperTest.java | 32 +++++++++---------- 3 files changed, 31 insertions(+), 28 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/objects/serialization/BothCasingBeanSerializer.java b/src/main/java/com/hubspot/jinjava/objects/serialization/BothCasingBeanSerializer.java index d25119d30..f966065be 100644 --- a/src/main/java/com/hubspot/jinjava/objects/serialization/BothCasingBeanSerializer.java +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/BothCasingBeanSerializer.java @@ -28,9 +28,11 @@ public void serialize( throws IOException { if ( Boolean.TRUE.equals( - serializerProvider.getAttribute(PyishObjectMapper.EAGER_EXECUTION_ATTRIBUTE) + 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)) diff --git a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java index 055168903..950842f66 100644 --- a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java @@ -19,7 +19,7 @@ public class PyishObjectMapper { public static final ObjectWriter PYISH_OBJECT_WRITER; - public static final String EAGER_EXECUTION_ATTRIBUTE = "eagerExecution"; + public static final String ALLOW_SNAKE_CASE_ATTRIBUTE = "allowSnakeCase"; static { ObjectMapper mapper = new ObjectMapper( @@ -37,14 +37,18 @@ public class PyishObjectMapper { public static String getAsUnquotedPyishString(Object val) { if (val != null) { - return WhitespaceUtils.unquoteAndUnescape(getAsPyishString(val)); + return WhitespaceUtils.unquoteAndUnescape(getAsPyishString(val, true)); } return ""; } public static String getAsPyishString(Object val) { + return getAsPyishString(val, false); + } + + private static String getAsPyishString(Object val, boolean forOutput) { try { - return getAsPyishStringOrThrow(val); + return getAsPyishStringOrThrow(val, forOutput); } catch (IOException e) { if (e instanceof LengthLimitingJsonProcessingException) { throw new OutputTooBigException( @@ -57,6 +61,11 @@ public static String getAsPyishString(Object val) { } public static String getAsPyishStringOrThrow(Object val) throws IOException { + return getAsPyishStringOrThrow(val, false); + } + + public static String getAsPyishStringOrThrow(Object val, boolean forOutput) + throws IOException { ObjectWriter objectWriter = PYISH_OBJECT_WRITER; Writer writer; Optional maxOutputSize = JinjavaInterpreter @@ -77,13 +86,7 @@ public static String getAsPyishStringOrThrow(Object val) throws IOException { writer = new CharArrayWriter(); } objectWriter - .withAttribute( - EAGER_EXECUTION_ATTRIBUTE, - JinjavaInterpreter - .getCurrentMaybe() - .map(interpreter -> interpreter.getConfig().getExecutionMode().useEagerParser()) - .orElse(false) - ) + .withAttribute(ALLOW_SNAKE_CASE_ATTRIBUTE, !forOutput) .writeValue(writer, val); return writer.toString(); } diff --git a/src/test/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapperTest.java b/src/test/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapperTest.java index b5e1cd604..65b937cad 100644 --- a/src/test/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapperTest.java +++ b/src/test/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapperTest.java @@ -7,8 +7,8 @@ import com.google.common.collect.ImmutableMap; import com.hubspot.jinjava.Jinjava; import com.hubspot.jinjava.JinjavaConfig; +import com.hubspot.jinjava.LegacyOverrides; import com.hubspot.jinjava.interpret.JinjavaInterpreter; -import com.hubspot.jinjava.mode.EagerExecutionMode; import com.hubspot.jinjava.objects.collections.SizeLimitingPyMap; import java.util.ArrayList; import java.util.HashMap; @@ -92,25 +92,23 @@ public void itLimitsDepth() { @Test public void itSerializesToSnakeCaseAccessibleMap() { - try { - Jinjava jinjava = new Jinjava( - JinjavaConfig - .newBuilder() - .withExecutionMode(EagerExecutionMode.instance()) - .build() - ); - JinjavaInterpreter.pushCurrent(jinjava.newInterpreter()); - assertThat(PyishObjectMapper.getAsPyishString(new Foo("bar"))) - .isEqualTo("{'fooBar': 'bar'} |allow_snake_case"); - } finally { - JinjavaInterpreter.popCurrent(); - } + assertThat(PyishObjectMapper.getAsPyishString(new Foo("bar"))) + .isEqualTo("{'fooBar': 'bar'} |allow_snake_case"); } @Test - public void itDoesNotConvertToSnakeCaseMapInDefaultExecutionMode() { - assertThat(PyishObjectMapper.getAsPyishString(new Foo("bar")).trim()) - .isEqualTo("{'fooBar': 'bar'}"); + public void itDoesNotConvertToSnakeCaseMapWhenResultIsForOutput() { + Jinjava jinjava = new Jinjava( + JinjavaConfig + .newBuilder() + .withLegacyOverrides( + LegacyOverrides.newBuilder().withUsePyishObjectMapper(true).build() + ) + .build() + ); + JinjavaInterpreter interpreter = jinjava.newInterpreter(); + interpreter.getContext().put("foo", new Foo("bar")); + assertThat(interpreter.render("{{ foo }}")).isEqualTo("{'fooBar': 'bar'}"); } static class Foo { From 47fd189b72518d92c42ec715b510e4beb2200412 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Thu, 16 Mar 2023 14:29:17 -0400 Subject: [PATCH 5/7] Add legacy override to pyish serialize using snake case by default --- .../com/hubspot/jinjava/LegacyOverrides.java | 13 +++++++ .../serialization/PyishObjectMapper.java | 37 ++++++++++++++++--- .../serialization/PyishObjectMapperTest.java | 24 ++++++++++++ 3 files changed, 68 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/LegacyOverrides.java b/src/main/java/com/hubspot/jinjava/LegacyOverrides.java index 9fb46b03d..5ff01314f 100644 --- a/src/main/java/com/hubspot/jinjava/LegacyOverrides.java +++ b/src/main/java/com/hubspot/jinjava/LegacyOverrides.java @@ -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; @@ -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; @@ -38,6 +40,10 @@ public boolean isUsePyishObjectMapper() { return usePyishObjectMapper; } + public boolean isUseSnakeCasePropertyNaming() { + return useSnakeCasePropertyNaming; + } + public boolean isWhitespaceRequiredWithinTokens() { return whitespaceRequiredWithinTokens; } @@ -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; @@ -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 ) @@ -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 ) { diff --git a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java index 950842f66..c7628aaf6 100644 --- a/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapper.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.module.SimpleModule; import com.hubspot.jinjava.interpret.JinjavaInterpreter; @@ -19,9 +20,23 @@ public class PyishObjectMapper { public static final ObjectWriter PYISH_OBJECT_WRITER; + public static final ObjectWriter SNAKE_CASE_PYISH_OBJECT_WRITER; public static final String ALLOW_SNAKE_CASE_ATTRIBUTE = "allowSnakeCase"; static { + PYISH_OBJECT_WRITER = + getPyishObjectMapper() + .writer(PyishPrettyPrinter.INSTANCE) + .with(PyishCharacterEscapes.INSTANCE); + + SNAKE_CASE_PYISH_OBJECT_WRITER = + getPyishObjectMapper() + .setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE) + .writer(PyishPrettyPrinter.INSTANCE) + .with(PyishCharacterEscapes.INSTANCE); + } + + private static ObjectMapper getPyishObjectMapper() { ObjectMapper mapper = new ObjectMapper( new JsonFactoryBuilder().quoteChar('\'').build() ) @@ -31,8 +46,7 @@ public class PyishObjectMapper { .addSerializer(PyishSerializable.class, PyishSerializer.INSTANCE) ); mapper.getSerializerProvider().setNullKeySerializer(new NullKeySerializer()); - PYISH_OBJECT_WRITER = - mapper.writer(PyishPrettyPrinter.INSTANCE).with(PyishCharacterEscapes.INSTANCE); + return mapper; } public static String getAsUnquotedPyishString(Object val) { @@ -66,7 +80,16 @@ public static String getAsPyishStringOrThrow(Object val) throws IOException { public static String getAsPyishStringOrThrow(Object val, boolean forOutput) throws IOException { - ObjectWriter objectWriter = PYISH_OBJECT_WRITER; + boolean useSnakeCaseMappingOverride = JinjavaInterpreter + .getCurrentMaybe() + .map( + interpreter -> + interpreter.getConfig().getLegacyOverrides().isUseSnakeCasePropertyNaming() + ) + .orElse(false); + ObjectWriter objectWriter = useSnakeCaseMappingOverride + ? SNAKE_CASE_PYISH_OBJECT_WRITER + : PYISH_OBJECT_WRITER; Writer writer; Optional maxOutputSize = JinjavaInterpreter .getCurrentMaybe() @@ -85,9 +108,11 @@ public static String getAsPyishStringOrThrow(Object val, boolean forOutput) } else { writer = new CharArrayWriter(); } - objectWriter - .withAttribute(ALLOW_SNAKE_CASE_ATTRIBUTE, !forOutput) - .writeValue(writer, val); + if (!useSnakeCaseMappingOverride) { + objectWriter = objectWriter.withAttribute(ALLOW_SNAKE_CASE_ATTRIBUTE, !forOutput); + } + objectWriter.writeValue(writer, val); + return writer.toString(); } diff --git a/src/test/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapperTest.java b/src/test/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapperTest.java index 65b937cad..a96277dd3 100644 --- a/src/test/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapperTest.java +++ b/src/test/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapperTest.java @@ -111,6 +111,30 @@ public void itDoesNotConvertToSnakeCaseMapWhenResultIsForOutput() { assertThat(interpreter.render("{{ foo }}")).isEqualTo("{'fooBar': 'bar'}"); } + @Test + public void itSerializesToSnakeCaseWhenLegacyOverrideIsSet() { + Jinjava jinjava = new Jinjava( + JinjavaConfig + .newBuilder() + .withLegacyOverrides( + LegacyOverrides + .newBuilder() + .withUsePyishObjectMapper(true) + .withUseSnakeCasePropertyNaming(true) + .build() + ) + .build() + ); + JinjavaInterpreter interpreter = jinjava.newInterpreter(); + try { + JinjavaInterpreter.pushCurrent(interpreter); + interpreter.getContext().put("foo", new Foo("bar")); + assertThat(interpreter.render("{{ foo }}")).isEqualTo("{'foo_bar': 'bar'}"); + } finally { + JinjavaInterpreter.popCurrent(); + } + } + static class Foo { private final String bar; From 5cb42f51f7af1d3e4fe617c86a266f066b9d031c Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Thu, 16 Mar 2023 14:35:06 -0400 Subject: [PATCH 6/7] Also make default ObjectMapper for ToJsonFilter use snake_properties if legacy override is set --- .../java/com/hubspot/jinjava/JinjavaConfig.java | 16 ++++++++++++++-- .../jinjava/lib/filter/FromJsonFilter.java | 7 ++++--- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/JinjavaConfig.java b/src/main/java/com/hubspot/jinjava/JinjavaConfig.java index 47d60a6d2..c37574897 100644 --- a/src/main/java/com/hubspot/jinjava/JinjavaConfig.java +++ b/src/main/java/com/hubspot/jinjava/JinjavaConfig.java @@ -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; @@ -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 { @@ -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; } @@ -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 nodePreProcessor = new JinjavaNodePreProcessor(); diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/FromJsonFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/FromJsonFilter.java index fbea6fa8d..bacee747d 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/FromJsonFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/FromJsonFilter.java @@ -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; @@ -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) { @@ -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); } From b28c7dcb07a6bd6c37608ef97d3de9b0201c4e7e Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Fri, 17 Mar 2023 14:06:24 -0400 Subject: [PATCH 7/7] Also convert to snake case accessible map when serializing map entries --- .../serialization/MapEntrySerializer.java | 17 +++++++++++------ .../serialization/PyishObjectMapperTest.java | 11 +++++++++++ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/objects/serialization/MapEntrySerializer.java b/src/main/java/com/hubspot/jinjava/objects/serialization/MapEntrySerializer.java index dd354bb46..e67cfd32a 100644 --- a/src/main/java/com/hubspot/jinjava/objects/serialization/MapEntrySerializer.java +++ b/src/main/java/com/hubspot/jinjava/objects/serialization/MapEntrySerializer.java @@ -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(), @@ -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)); } diff --git a/src/test/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapperTest.java b/src/test/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapperTest.java index a96277dd3..39b03e3c5 100644 --- a/src/test/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapperTest.java +++ b/src/test/java/com/hubspot/jinjava/objects/serialization/PyishObjectMapperTest.java @@ -10,6 +10,7 @@ import com.hubspot.jinjava.LegacyOverrides; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.objects.collections.SizeLimitingPyMap; +import java.util.AbstractMap; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -96,6 +97,16 @@ public void itSerializesToSnakeCaseAccessibleMap() { .isEqualTo("{'fooBar': 'bar'} |allow_snake_case"); } + @Test + public void itSerializesToSnakeCaseAccessibleMapWhenInMapEntry() { + assertThat( + PyishObjectMapper.getAsPyishString( + new AbstractMap.SimpleImmutableEntry<>("foo", new Foo("bar")) + ) + ) + .isEqualTo("fn:map_entry('foo', {'fooBar': 'bar'} |allow_snake_case)"); + } + @Test public void itDoesNotConvertToSnakeCaseMapWhenResultIsForOutput() { Jinjava jinjava = new Jinjava(