Compare commits

...

1 Commits

Author SHA1 Message Date
Tom Hombergs
8216c90168 first implementation of a like matcher that matches whole object hierarchies 2018-06-04 23:50:06 +02:00
9 changed files with 470 additions and 1 deletions

0
.idea/modules/spring-boot-testing.iml generated Normal file
View File

View File

@@ -3,3 +3,9 @@ userservice:
eureka:
enabled: false
listOfServers: localhost:8080
rootservice:
ribbon:
eureka:
enabled: false
listOfServers: localhost:8080

View File

@@ -22,6 +22,6 @@ dependencies {
compile("org.springframework.cloud:spring-cloud-starter-feign:1.4.1.RELEASE")
compile('com.h2database:h2:1.4.196')
testCompile('org.codehaus.groovy:groovy-all:2.4.6')
testCompile("au.com.dius:pact-jvm-consumer-junit_2.11:3.5.2")
compile("au.com.dius:pact-jvm-consumer-junit_2.11:3.5.16")
testCompile("org.springframework.boot:spring-boot-starter-test:${springboot_version}")
}

View File

@@ -0,0 +1,149 @@
package io.reflectoring.dsl;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import au.com.dius.pact.consumer.dsl.PactDslJsonBody;
import au.com.dius.pact.consumer.dsl.PactDslJsonRootValue;
public class PactDslJsonBodyLikeMapper {
private static final Set<Class<?>> SIMPLE_TYPES = new HashSet<>(Arrays.asList(
Boolean.class,
boolean.class,
Integer.class,
int.class,
Double.class,
double.class,
Float.class,
float.class,
BigDecimal.class,
Number.class,
String.class,
Long.class,
long.class
));
public static PactDslJsonBody like(Object object) {
return like(object, new PactDslJsonBody());
}
public static PactDslJsonBody like(Object object, PactDslJsonBody body) {
try {
return recursiveLike(object, body);
} catch (IllegalAccessException e) {
throw new IllegalStateException("could not create PactDslJsonBody due to exception!", e);
}
}
private static PactDslJsonBody recursiveLike(Object object, PactDslJsonBody body) throws IllegalAccessException {
Field[] fields = object.getClass().getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
Object fieldValue = field.get(object);
if (fieldValue == null) {
// fields with null values will not be mapped
continue;
}
if (isSimpleType(field.getType())) {
mapSimpleFieldWithName(field.getName(), fieldValue, body);
} else if (isCollectionType(field.getType())) {
mapCollectionField(field.getName(), (Collection) fieldValue, body);
} else {
mapComplexField(field.getName(), fieldValue, body);
}
}
return body;
}
private static void mapSimpleFieldWithName(String fieldName, Object fieldValue, PactDslJsonBody body) throws IllegalAccessException {
Class<?> type = fieldValue.getClass();
if (String.class == type) {
body.stringType(fieldName, (String) fieldValue);
} else if (Boolean.class == type || boolean.class == type) {
body.booleanType(fieldName, (Boolean) fieldValue);
} else if (Integer.class == type || int.class == type || Long.class == type || long.class == type) {
body.integerType(fieldName, (Integer) fieldValue);
} else if (Double.class == type || double.class == type) {
body.decimalType(fieldName, (Double) fieldValue);
} else if (Float.class == type || float.class == type) {
body.decimalType(fieldName, ((Float) fieldValue).doubleValue());
} else if (BigDecimal.class == type) {
body.decimalType(fieldName, (BigDecimal) fieldValue);
} else if (Number.class.isAssignableFrom(type)) {
body.numberType(fieldName, (Number) fieldValue);
} else {
throw new IllegalStateException(String.format("field '%s' of type '%s' is not a simple field", fieldName, type));
}
}
private static PactDslJsonRootValue getRootValueForType(Class<?> type) {
if (String.class == type) {
return PactDslJsonRootValue.stringType();
} else if (Boolean.class == type || boolean.class == type) {
return PactDslJsonRootValue.booleanType();
} else if (Integer.class == type || int.class == type || Long.class == type || long.class == type) {
return PactDslJsonRootValue.integerType();
} else if (Double.class == type || double.class == type) {
return PactDslJsonRootValue.decimalType();
} else if (Float.class == type || float.class == type) {
return PactDslJsonRootValue.decimalType();
} else if (BigDecimal.class == type) {
return PactDslJsonRootValue.decimalType();
} else if (Number.class.isAssignableFrom(type)) {
return PactDslJsonRootValue.numberType();
} else {
throw new IllegalStateException(String.format("unsupported type '%s'", type));
}
}
private static void mapCollectionField(String fieldName, Collection<?> collection, PactDslJsonBody body) throws IllegalAccessException {
if (collection.isEmpty()) {
throw new IllegalArgumentException("matching empty lists is not supported!");
}
Class<?> listType = collection.iterator().next().getClass();
if (isSimpleType(listType)) {
PactDslJsonRootValue rootValue = getRootValueForType(listType);
body.eachLike(fieldName, rootValue);
} else if (isCollectionType(listType)) {
throw new IllegalArgumentException("collections of collections are not supported");
} else {
PactDslJsonBody nestedBody = body.eachLike(fieldName);
for (Object complexObject : collection) {
mapComplexFieldWithoutOpeningObject(complexObject, nestedBody);
}
nestedBody.closeObject().closeArray();
}
}
private static void mapComplexField(String fieldName, Object fieldValue, PactDslJsonBody body) throws IllegalAccessException {
PactDslJsonBody nestedBody = body.object(fieldName);
mapComplexFieldWithoutOpeningObject(fieldValue, nestedBody);
}
private static void mapComplexFieldWithoutOpeningObject(Object fieldValue, PactDslJsonBody nestedBody) throws IllegalAccessException {
recursiveLike(fieldValue, nestedBody);
nestedBody.closeObject();
}
private static boolean isSimpleType(Class<?> type) {
return SIMPLE_TYPES.contains(type);
}
private static boolean isCollectionType(Class<?> type) {
return Collection.class.isAssignableFrom(type);
}
}

View File

@@ -0,0 +1,32 @@
package io.reflectoring.dsl;
public class Nested {
private String stringField = "nested string";
private Integer integerField = 42;
private String nullField = null;
public String getStringField() {
return stringField;
}
public void setStringField(String stringField) {
this.stringField = stringField;
}
public Integer getIntegerField() {
return integerField;
}
public void setIntegerField(Integer integerField) {
this.integerField = integerField;
}
public String getNullField() {
return nullField;
}
public void setNullField(String nullField) {
this.nullField = nullField;
}
}

View File

@@ -0,0 +1,72 @@
package io.reflectoring.dsl;
import au.com.dius.pact.consumer.Pact;
import au.com.dius.pact.consumer.PactProviderRuleMk2;
import au.com.dius.pact.consumer.PactVerification;
import au.com.dius.pact.consumer.dsl.PactDslJsonBody;
import au.com.dius.pact.consumer.dsl.PactDslJsonRootValue;
import au.com.dius.pact.consumer.dsl.PactDslWithProvider;
import au.com.dius.pact.model.RequestResponsePact;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest(properties = {
// overriding provider address
"rootservice.ribbon.listOfServers: localhost:8888"
})
public class PactDslJsonBodyLikeMapperConsumerTest {
@Rule
public PactProviderRuleMk2 stubProvider = new PactProviderRuleMk2("testprovider", "localhost", 8888, this);
@Autowired
private RootClient rootClient;
@Pact(state = "teststate", provider = "testprovider", consumer = "testclient")
public RequestResponsePact createPact(PactDslWithProvider builder) {
return builder
.given("teststate")
.uponReceiving("a POST request with a Root object")
.path("/root")
.method("POST")
// .body(PactDslJsonBodyLikeMapper.like(new PactDslJsonBodyLikeMapperTest.Root()))
.willRespondWith()
.status(201)
.matchHeader("Content-Type", "application/json")
.body(PactDslJsonBodyLikeMapper.like(new Root()))
.toPact();
}
@Pact(state = "teststate2", provider = "testprovider", consumer = "testclient")
public RequestResponsePact createPact2(PactDslWithProvider builder) {
return builder
.given("teststate2")
.uponReceiving("a POST request with a Root object")
.path("/root")
.method("POST")
.willRespondWith()
.status(201)
.matchHeader("Content-Type", "application/json")
.body(new PactDslJsonBody()
.eachLike("arrayField", PactDslJsonRootValue.numberType()))
.toPact();
}
@Test
@PactVerification(fragment = "createPact")
public void verifyPact() {
rootClient.createRoot(new Root());
}
@Test
@PactVerification(fragment = "createPact2")
public void verifyPact2() {
rootClient.createRoot(new Root());
}
}

View File

@@ -0,0 +1,51 @@
package io.reflectoring.dsl;
import au.com.dius.pact.consumer.dsl.PactDslJsonBody;
import com.google.common.collect.ImmutableMap;
import org.junit.Test;
import static org.junit.Assert.*;
public class PactDslJsonBodyLikeMapperTest {
@Test
public void createsMatchersForAllFields() {
Root object = new Root();
PactDslJsonBody jsonBody = PactDslJsonBodyLikeMapper.like(object);
assertNoMatcher(jsonBody, ".nullField");
assertMatcherType(jsonBody, ".stringField", "type");
assertMatcherType(jsonBody, ".booleanField", "type");
assertMatcherType(jsonBody, ".primitiveBooleanField", "type");
assertMatcherType(jsonBody, ".integerField", "integer");
assertMatcherType(jsonBody, ".primitiveIntegerField", "integer");
assertMatcherType(jsonBody, ".doubleField", "decimal");
assertMatcherType(jsonBody, ".primitiveDoubleField", "decimal");
assertMatcherType(jsonBody, ".floatField", "decimal");
assertMatcherType(jsonBody, ".primitiveFloatField", "decimal");
assertMatcherType(jsonBody, ".bigDecimalField", "decimal");
assertMatcherType(jsonBody, ".numberField", "number");
assertMatcherType(jsonBody, ".nested.stringField", "type");
assertMatcherType(jsonBody, ".nested.integerField", "integer");
assertNoMatcher(jsonBody, ".nested.nullField");
assertMatcherType(jsonBody, ".complexListField[*].stringField", "type");
assertMatcherType(jsonBody, ".complexListField[*].integerField", "integer");
assertMatcherType(jsonBody, ".simpleListField[*]", "integer");
}
private void assertMatcherType(PactDslJsonBody jsonBody, String fieldName, String expectedMatcher) {
assertMatcher(jsonBody, fieldName);
assertEquals(String.format("expected matcher for field '%s' to be of type '%s'", fieldName, expectedMatcher),
ImmutableMap.of("match", expectedMatcher),
jsonBody.getMatchers().getMatchingRules().get(fieldName).getRules().get(0).toMap());
}
private void assertMatcher(PactDslJsonBody jsonBody, String fieldName) {
assertNotNull(String.format("expected a matcher for field '%s'", fieldName),
jsonBody.getMatchers().getMatchingRules().get(fieldName));
}
private void assertNoMatcher(PactDslJsonBody jsonBody, String fieldName) {
assertNull(jsonBody.getMatchers().getMatchingRules().get(fieldName));
}
}

View File

@@ -0,0 +1,145 @@
package io.reflectoring.dsl;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.List;
public class Root {
private String nullField = null;
private String stringField = "string";
private Boolean booleanField = Boolean.TRUE;
private Integer integerField = 1;
private Double doubleField = 1d;
private Float floatField = 1f;
private BigDecimal bigDecimalField = BigDecimal.ONE;
private boolean primitiveBooleanField = true;
private int primitiveIntegerField = 1;
private double primitiveDoubleField = 1d;
private float primitiveFloatField = 1f;
private Number numberField = BigInteger.valueOf(1L);
private Nested nested = new Nested();
private List<Nested> complexListField = Arrays.asList(new Nested(), new Nested());
private List<Integer> simpleListField = Arrays.asList(1,2);
public String getNullField() {
return nullField;
}
public void setNullField(String nullField) {
this.nullField = nullField;
}
public String getStringField() {
return stringField;
}
public void setStringField(String stringField) {
this.stringField = stringField;
}
public Boolean getBooleanField() {
return booleanField;
}
public void setBooleanField(Boolean booleanField) {
this.booleanField = booleanField;
}
public Integer getIntegerField() {
return integerField;
}
public void setIntegerField(Integer integerField) {
this.integerField = integerField;
}
public Double getDoubleField() {
return doubleField;
}
public void setDoubleField(Double doubleField) {
this.doubleField = doubleField;
}
public Float getFloatField() {
return floatField;
}
public void setFloatField(Float floatField) {
this.floatField = floatField;
}
public BigDecimal getBigDecimalField() {
return bigDecimalField;
}
public void setBigDecimalField(BigDecimal bigDecimalField) {
this.bigDecimalField = bigDecimalField;
}
public boolean isPrimitiveBooleanField() {
return primitiveBooleanField;
}
public void setPrimitiveBooleanField(boolean primitiveBooleanField) {
this.primitiveBooleanField = primitiveBooleanField;
}
public int getPrimitiveIntegerField() {
return primitiveIntegerField;
}
public void setPrimitiveIntegerField(int primitiveIntegerField) {
this.primitiveIntegerField = primitiveIntegerField;
}
public double getPrimitiveDoubleField() {
return primitiveDoubleField;
}
public void setPrimitiveDoubleField(double primitiveDoubleField) {
this.primitiveDoubleField = primitiveDoubleField;
}
public float getPrimitiveFloatField() {
return primitiveFloatField;
}
public void setPrimitiveFloatField(float primitiveFloatField) {
this.primitiveFloatField = primitiveFloatField;
}
public Number getNumberField() {
return numberField;
}
public void setNumberField(Number numberField) {
this.numberField = numberField;
}
public Nested getNested() {
return nested;
}
public void setNested(Nested nested) {
this.nested = nested;
}
public List<Nested> getComplexListField() {
return complexListField;
}
public void setComplexListField(List<Nested> complexListField) {
this.complexListField = complexListField;
}
public List<Integer> getSimpleListField() {
return simpleListField;
}
public void setSimpleListField(List<Integer> simpleListField) {
this.simpleListField = simpleListField;
}
}

View File

@@ -0,0 +1,14 @@
package io.reflectoring.dsl;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@FeignClient(name = "rootservice")
public interface RootClient {
@RequestMapping(method = RequestMethod.POST, path = "/root")
Root createRoot(@RequestBody Root root);
}