Skip to content

Commit

Permalink
Merge pull request #1 from Nosto/improvement/detectCamelCasePropertie…
Browse files Browse the repository at this point in the history
…sWhenSnakeCaseConfigured

Detect when camel case properties are used for a class that's configured to use snake case
  • Loading branch information
gary-nosto authored Oct 28, 2021
2 parents 69f9157 + 5a34968 commit 80bb7d3
Show file tree
Hide file tree
Showing 8 changed files with 234 additions and 50 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -19,7 +20,7 @@
* A base class for Jackson-mapped beans.
* <p>
* 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.
* <p>
* Ensures that all beans that extend this can be serialised, then deserialised
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<T, U extends T> {

Expand All @@ -59,12 +59,12 @@ public abstract class AbstractJacksonBeanTest<T, U extends T> {
private final Class<? extends T> deserClass;
private final Class<? extends U> concreteClass;

@SuppressWarnings({"ConstructorNotProtectedInAbstractClass", "JUnitTestCaseWithNonTrivialConstructors"})
@SuppressWarnings({"JUnitTestCaseWithNonTrivialConstructors"})
public AbstractJacksonBeanTest(Class<? extends U> clazz) {
this(clazz, clazz);
}

@SuppressWarnings({"ConstructorNotProtectedInAbstractClass", "JUnitTestCaseWithNonTrivialConstructors"})
@SuppressWarnings({"JUnitTestCaseWithNonTrivialConstructors"})
public AbstractJacksonBeanTest(Class<? extends T> deserClass, Class<? extends U> concreteClass) {
this.deserClass = deserClass;
this.concreteClass = concreteClass;
Expand All @@ -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() {
Expand Down Expand Up @@ -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<PropertyNamingStrategy, List<String>> 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());
}

/**
Expand Down
10 changes: 5 additions & 5 deletions beanie-core/src/main/java/com/nosto/beanie/BeanieProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -30,7 +30,7 @@ default String toPrettyJSON(@Nullable Object out) {
}
}

default <T> T fromJSON(String json, Class<T> type) throws IOException {
default <T> T fromJSON(String json, Class<T> type) throws JsonProcessingException {
return getMapper().readValue(json, type);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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<InvalidSnakeCasePropertyNamingStrategyTest.TestBean, InvalidSnakeCasePropertyNamingStrategyTest.TestBean> {
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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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());
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ValidCamelCasePropertyNamingStrategyTest.TestBean, ValidCamelCasePropertyNamingStrategyTest.TestBean> {
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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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<ValidSnakeCasePropertyNamingStrategyTest.TestBean, ValidSnakeCasePropertyNamingStrategyTest.TestBean> {
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;
}
}
}
Loading

0 comments on commit 80bb7d3

Please sign in to comment.