diff --git a/beanie-annotations/src/main/java/com/nosto/beanie/JacksonBean.java b/beanie-annotations/src/main/java/com/nosto/beanie/JacksonBean.java index cece57f..94d4f17 100644 --- a/beanie-annotations/src/main/java/com/nosto/beanie/JacksonBean.java +++ b/beanie-annotations/src/main/java/com/nosto/beanie/JacksonBean.java @@ -10,6 +10,7 @@ package com.nosto.beanie; +import com.fasterxml.jackson.annotation.JsonCreator; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.apache.commons.lang3.builder.ToStringBuilder; @@ -19,7 +20,7 @@ * A base class for Jackson-mapped beans. *

* All sub-classes are automatically unit-tested and they should have - * a constructor with {@link com.fasterxml.jackson.annotation.JsonCreator} + * a constructor with {@link JsonCreator} * that takes all serialization properties. *

* Ensures that all beans that extend this can be serialised, then deserialised diff --git a/beanie-core/src/main/java/com/nosto/beanie/AbstractJacksonBeanTest.java b/beanie-core/src/main/java/com/nosto/beanie/AbstractJacksonBeanTest.java index fe419cf..d701f62 100644 --- a/beanie-core/src/main/java/com/nosto/beanie/AbstractJacksonBeanTest.java +++ b/beanie-core/src/main/java/com/nosto/beanie/AbstractJacksonBeanTest.java @@ -10,38 +10,38 @@ package com.nosto.beanie; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.databind.BeanDescription; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.fasterxml.jackson.databind.introspect.AnnotatedField; +import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition; +import com.nosto.beanie.jeasy.ExcludedMapAndCollectionsAsEmptyRandomizerRegistry; +import com.nosto.beanie.jeasy.ForceAllNonPrimitivesAsNullRandomizerRegistry; +import org.jeasy.random.EasyRandom; +import org.jeasy.random.EasyRandomParameters; +import org.jeasy.random.api.RandomizerRegistry; +import org.jeasy.random.randomizers.registry.CustomRandomizerRegistry; +import org.junit.Assert; +import org.junit.Test; + +import javax.annotation.Nullable; import java.io.IOException; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Modifier; import java.security.SecureRandom; import java.time.Instant; import java.time.OffsetDateTime; import java.time.ZoneOffset; import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.Random; import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.IntStream; -import javax.annotation.Nullable; - -import org.jeasy.random.EasyRandom; -import org.jeasy.random.EasyRandomParameters; -import org.jeasy.random.api.RandomizerRegistry; -import org.jeasy.random.randomizers.registry.CustomRandomizerRegistry; -import org.junit.Assert; -import org.junit.Test; - -import com.fasterxml.jackson.databind.BeanDescription; -import com.fasterxml.jackson.databind.JavaType; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.PropertyNamingStrategy; -import com.fasterxml.jackson.databind.introspect.AnnotatedField; -import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition; -import com.nosto.beanie.jeasy.ExcludedMapAndCollectionsAsEmptyRandomizerRegistry; -import com.nosto.beanie.jeasy.ForceAllNonPrimitivesAsNullRandomizerRegistry; - @SuppressWarnings("UseOfObsoleteDateTimeApi") public abstract class AbstractJacksonBeanTest { @@ -59,12 +59,12 @@ public abstract class AbstractJacksonBeanTest { private final Class deserClass; private final Class concreteClass; - @SuppressWarnings({"ConstructorNotProtectedInAbstractClass", "JUnitTestCaseWithNonTrivialConstructors"}) + @SuppressWarnings({"JUnitTestCaseWithNonTrivialConstructors"}) public AbstractJacksonBeanTest(Class clazz) { this(clazz, clazz); } - @SuppressWarnings({"ConstructorNotProtectedInAbstractClass", "JUnitTestCaseWithNonTrivialConstructors"}) + @SuppressWarnings({"JUnitTestCaseWithNonTrivialConstructors"}) public AbstractJacksonBeanTest(Class deserClass, Class concreteClass) { this.deserClass = deserClass; this.concreteClass = concreteClass; @@ -75,9 +75,7 @@ protected RandomizerRegistry getRandomizerRegistry() { } /** - * Assert all properties have an equivalent - * {@link com.fasterxml.jackson.annotation.JsonCreator} - * parameter + * Assert all properties have an equivalent {@link JsonCreator} parameter */ @Test public void constructorParameters() { @@ -117,20 +115,44 @@ public void serde() { /** * Test that all properties of a bean are named with a consistent naming strategy. + * If the bean is configured to use a specific naming strategy, property names should be consistent with that strategy. */ @Test public void namingStrategy() { - Map> cases = getDescription().findProperties() - .stream() - .map(BeanPropertyDefinition::getName) - .collect(Collectors.groupingBy(name -> { - if (name.contains("_") && !name.toLowerCase().equals(name)) { - return PropertyNamingStrategy.SNAKE_CASE; + BeanDescription description = getDescription(); + getDescription().findProperties() + .forEach(property -> verifyPropertyName(description, property)); + } + + private void verifyPropertyName(BeanDescription bean, BeanPropertyDefinition property) { + PropertyNamingStrategy.PropertyNamingStrategyBase namingStrategy = Optional.ofNullable(bean.getClassAnnotations().get(JsonNaming.class)) + .map(JsonNaming::value) + .filter(PropertyNamingStrategy.PropertyNamingStrategyBase.class::isAssignableFrom) + .map(c -> { + try { + return c.getConstructor().newInstance(); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new RuntimeException("Cannot construct naming strategy.", e); + } + }) + .map(PropertyNamingStrategy.PropertyNamingStrategyBase.class::cast) + .orElseGet(() -> { + // try to detect the naming strategy + String name = property.getName(); + if (name.contains("_")) { + return (PropertyNamingStrategy.PropertyNamingStrategyBase) PropertyNamingStrategy.SNAKE_CASE; } else { - return PropertyNamingStrategy.LOWER_CAMEL_CASE; + return new PropertyNamingStrategy.PropertyNamingStrategyBase() { + @Override + public String translate(String propertyName) { + return propertyName; + } + }; } - })); - Assert.assertEquals(cases.toString(), 1, cases.size()); + }); + + Assert.assertEquals(String.format("Property %s does not use strategy %s", property.getName(), namingStrategy.getClass()), + namingStrategy.translate(property.getName()), property.getName()); } /** diff --git a/beanie-core/src/main/java/com/nosto/beanie/BeanieProvider.java b/beanie-core/src/main/java/com/nosto/beanie/BeanieProvider.java index 8fd06b1..4ef647b 100644 --- a/beanie-core/src/main/java/com/nosto/beanie/BeanieProvider.java +++ b/beanie-core/src/main/java/com/nosto/beanie/BeanieProvider.java @@ -10,14 +10,14 @@ package com.nosto.beanie; -import java.io.IOException; - -import javax.annotation.Nullable; - +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.BeanDescription; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; +import javax.annotation.Nullable; +import java.io.IOException; + public interface BeanieProvider { ObjectMapper getMapper(); @@ -30,7 +30,7 @@ default String toPrettyJSON(@Nullable Object out) { } } - default T fromJSON(String json, Class type) throws IOException { + default T fromJSON(String json, Class type) throws JsonProcessingException { return getMapper().readValue(json, type); } diff --git a/beanie-core/src/test/java/com/nosto/beanie/InvalidSnakeCasePropertyNamingStrategyTest.java b/beanie-core/src/test/java/com/nosto/beanie/InvalidSnakeCasePropertyNamingStrategyTest.java new file mode 100644 index 0000000..60f44b1 --- /dev/null +++ b/beanie-core/src/test/java/com/nosto/beanie/InvalidSnakeCasePropertyNamingStrategyTest.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2021 Nosto Solutions Ltd All Rights Reserved. + * + * This software is the confidential and proprietary information of + * Nosto Solutions Ltd ("Confidential Information"). You shall not + * disclose such Confidential Information and shall use it only in + * accordance with the terms of the agreement you entered into with + * Nosto Solutions Ltd. + */ + +package com.nosto.beanie; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import org.junit.Test; + +public class InvalidSnakeCasePropertyNamingStrategyTest extends AbstractJacksonBeanTest { + public InvalidSnakeCasePropertyNamingStrategyTest() { + super(TestBean.class); + } + + @Test(expected = AssertionError.class) + @Override + public void namingStrategy() { + super.namingStrategy(); + } + + @Override + protected BeanieProvider getBeanieProvider() { + return new DefaultBeanieProvider(); + } + + @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) + public static class TestBean extends AbstractTestBean { + + private final String propertyA; + private final String propertyB; + + @JsonCreator + public TestBean(@JsonProperty("propertyA") String propertyA, @JsonProperty("propertyB") String propertyB) { + this.propertyA = propertyA; + this.propertyB = propertyB; + } + + @SuppressWarnings("unused") + public String getPropertyA() { + return propertyA; + } + + @SuppressWarnings("unused") + public String getPropertyB() { + return propertyB; + } + } +} diff --git a/beanie-core/src/test/java/com/nosto/beanie/PolymorphicJacksonBeanTest.java b/beanie-core/src/test/java/com/nosto/beanie/PolymorphicJacksonBeanTest.java index 208044e..71bb82f 100644 --- a/beanie-core/src/test/java/com/nosto/beanie/PolymorphicJacksonBeanTest.java +++ b/beanie-core/src/test/java/com/nosto/beanie/PolymorphicJacksonBeanTest.java @@ -9,17 +9,11 @@ */ package com.nosto.beanie; -import static org.junit.Assert.assertEquals; - -import java.io.IOException; - +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.core.JsonProcessingException; import org.junit.Test; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonTypeInfo; -import com.fasterxml.jackson.annotation.JsonTypeName; +import static org.junit.Assert.assertEquals; /** * @author ollik1 @@ -31,7 +25,7 @@ public PolymorphicJacksonBeanTest() { } @Test - public void polymorphicDeserialization() throws IOException { + public void polymorphicDeserialization() throws JsonProcessingException { Base value = getMapper().readValue("{\"type\":\"t1\",\"x\":\"foo\"}", Base.class); assertEquals(Concrete1.class, value.getClass()); assertEquals("foo", value.getX()); diff --git a/beanie-core/src/test/java/com/nosto/beanie/ValidCamelCasePropertyNamingStrategyTest.java b/beanie-core/src/test/java/com/nosto/beanie/ValidCamelCasePropertyNamingStrategyTest.java new file mode 100644 index 0000000..8730623 --- /dev/null +++ b/beanie-core/src/test/java/com/nosto/beanie/ValidCamelCasePropertyNamingStrategyTest.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2021 Nosto Solutions Ltd All Rights Reserved. + * + * This software is the confidential and proprietary information of + * Nosto Solutions Ltd ("Confidential Information"). You shall not + * disclose such Confidential Information and shall use it only in + * accordance with the terms of the agreement you entered into with + * Nosto Solutions Ltd. + */ + +package com.nosto.beanie; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class ValidCamelCasePropertyNamingStrategyTest extends AbstractJacksonBeanTest { + public ValidCamelCasePropertyNamingStrategyTest() { + super(TestBean.class); + } + + @Override + protected BeanieProvider getBeanieProvider() { + return new DefaultBeanieProvider(); + } + + public static class TestBean extends AbstractTestBean { + + private final String propertyA; + private final String propertyB; + + @JsonCreator + public TestBean(@JsonProperty("propertyA") String propertyA, @JsonProperty("propertyB") String propertyB) { + this.propertyA = propertyA; + this.propertyB = propertyB; + } + + @SuppressWarnings("unused") + public String getPropertyA() { + return propertyA; + } + + @SuppressWarnings("unused") + public String getPropertyB() { + return propertyB; + } + } +} diff --git a/beanie-core/src/test/java/com/nosto/beanie/ValidSnakeCasePropertyNamingStrategyTest.java b/beanie-core/src/test/java/com/nosto/beanie/ValidSnakeCasePropertyNamingStrategyTest.java new file mode 100644 index 0000000..aa0f0bc --- /dev/null +++ b/beanie-core/src/test/java/com/nosto/beanie/ValidSnakeCasePropertyNamingStrategyTest.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2021 Nosto Solutions Ltd All Rights Reserved. + * + * This software is the confidential and proprietary information of + * Nosto Solutions Ltd ("Confidential Information"). You shall not + * disclose such Confidential Information and shall use it only in + * accordance with the terms of the agreement you entered into with + * Nosto Solutions Ltd. + */ + +package com.nosto.beanie; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +public class ValidSnakeCasePropertyNamingStrategyTest extends AbstractJacksonBeanTest { + public ValidSnakeCasePropertyNamingStrategyTest() { + super(TestBean.class); + } + + @Override + protected BeanieProvider getBeanieProvider() { + return new DefaultBeanieProvider(); + } + + @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) + public static class TestBean extends AbstractTestBean { + + private final String propertyA; + private final String propertyB; + + @JsonCreator + public TestBean(@JsonProperty("property_a") String propertyA, @JsonProperty("property_b") String propertyB) { + this.propertyA = propertyA; + this.propertyB = propertyB; + } + + @SuppressWarnings("unused") + public String getPropertyA() { + return propertyA; + } + + @SuppressWarnings("unused") + public String getPropertyB() { + return propertyB; + } + } +} diff --git a/build.gradle b/build.gradle index 724932c..a4456d7 100644 --- a/build.gradle +++ b/build.gradle @@ -32,7 +32,7 @@ allprojects { project -> apply plugin: "jacoco" group 'com.nosto' - version '1.0.5' + version '1.0.6' repositories { mavenCentral() @@ -168,29 +168,42 @@ allprojects { project -> pom { //noinspection GroovyAssignabilityCheck name = 'Beanie' + //noinspection GroovyAssignabilityCheck description = 'A simple library to sanity-check your bean ser-deser' + //noinspection GroovyAssignabilityCheck url = 'https://github.com/nosto/beanie' licenses { license { + //noinspection GroovyAssignabilityCheck name = 'The Apache License, Version 2.0' + //noinspection GroovyAssignabilityCheck url = 'https://www.apache.org/licenses/LICENSE-2.0.txt' } } developers { developer { + //noinspection GroovyAssignabilityCheck id = 'ollik1' + //noinspection GroovyAssignabilityCheck name = 'Olli Kuonanoja' + //noinspection GroovyAssignabilityCheck email = 'olli@nosto.com' } developer { + //noinspection GroovyAssignabilityCheck id = 'mridang' + //noinspection GroovyAssignabilityCheck name = 'Mridang Agarwalla' + //noinspection GroovyAssignabilityCheck email = 'mridang@nosto.com' } } scm { + //noinspection GroovyAssignabilityCheck connection = 'scm:git:git://github.com/nosto/beanie.git' + //noinspection GroovyAssignabilityCheck developerConnection = 'scm:git:ssh://github.com/nosto/beanie.git' + //noinspection GroovyAssignabilityCheck url = 'https://github.com/nosto/beanie' } }