Skip to content

Commit

Permalink
#5: Add support for Optional and other types (#7)
Browse files Browse the repository at this point in the history
* Add support for URL & URI

* Fix handling "get" methods

* Add support for Optional

* Add support for date & time

* Refactoring

* Prepare release

* Fix sonar warnings

---------

Co-authored-by: kaklakariada <kaklakariada@users.noreply.github.com>
  • Loading branch information
kaklakariada and kaklakariada committed Jan 31, 2024
1 parent ae63db0 commit d6e155d
Show file tree
Hide file tree
Showing 8 changed files with 230 additions and 15 deletions.
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.6.0] - unreleased
## [0.7.0] - unreleased

## [0.6.0] - 2024-01-31

### Breaking Changes

* [#6](https://github.com/itsallcode/hamcrest-auto-matcher/pull/6): Rename packages to `org.itsallcode`
* [#5](https://github.com/itsallcode/hamcrest-auto-matcher/issues/5): Add support for `Optional` and other types
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ repositories {
}
dependencies {
testCompile 'org.itsallcode:hamcrest-auto-matcher:0.5.1'
testCompile 'org.itsallcode:hamcrest-auto-matcher:0.6.0'
}
```

Expand All @@ -41,7 +41,7 @@ dependencies {
<dependency>
<groupId>org.itsallcode</groupId>
<artifactId>hamcrest-auto-matcher</artifactId>
<version>0.5.1</version>
<version>0.6.0</version>
<scope>test</scope>
</dependency>
```
Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ dependencies {
}

group 'org.itsallcode'
version = '0.5.1'
version = '0.6.0'

java {
toolchain {
Expand Down
46 changes: 36 additions & 10 deletions src/main/java/org/itsallcode/matcher/auto/AutoConfigBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@
import java.lang.reflect.*;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.URI;
import java.net.URL;
import java.nio.file.Path;
import java.time.Instant;
import java.time.LocalDate;
import java.time.temporal.Temporal;
import java.util.*;
import java.util.Map.Entry;
Expand All @@ -30,8 +34,10 @@ class AutoConfigBuilder<T> {

private static final Set<Class<?>> SIMPLE_TYPES = Collections.unmodifiableSet(new HashSet<>(asList(String.class,
Long.class, Integer.class, Byte.class, Boolean.class, Float.class, Double.class, Character.class,
Short.class, BigInteger.class, BigDecimal.class, Calendar.class, Date.class, Temporal.class, Currency.class,
File.class, Path.class, UUID.class, Class.class, Package.class, Enum.class)));
Short.class, BigInteger.class, BigDecimal.class, Calendar.class, Date.class, java.sql.Date.class,
java.sql.Timestamp.class, Instant.class, LocalDate.class,
Temporal.class, Currency.class,
File.class, Path.class, UUID.class, Class.class, Package.class, Enum.class, URL.class, URI.class)));

private final T expected;
private final Builder<T> configBuilder;
Expand All @@ -54,18 +60,22 @@ private MatcherConfig<T> build() {
}

public static <T> Matcher<T> createEqualToMatcher(final T expected) {
if (expected.getClass().isArray()) {
final Class<? extends Object> type = expected.getClass();
if (type.isArray()) {
return createArrayMatcher(expected);
}
if (isSimpleType(expected.getClass())) {
if (isSimpleType(type)) {
return Matchers.equalTo(expected);
}
if (Map.class.isAssignableFrom(expected.getClass())) {
if (Map.class.isAssignableFrom(type)) {
return createMapContainsMatcher(expected);
}
if (Iterable.class.isAssignableFrom(expected.getClass())) {
if (Iterable.class.isAssignableFrom(type)) {
return createIterableContainsMatcher(expected);
}
if (Optional.class.isAssignableFrom(type)) {
return createOptionalMatcher(expected);
}
final MatcherConfig<T> config = new AutoConfigBuilder<>(expected).build();
return new ConfigurableMatcher<>(config);
}
Expand Down Expand Up @@ -122,6 +132,15 @@ private static <T> Matcher<T> createIterableContainsMatcher(final T expected) {
return matcher;
}

@SuppressWarnings("unchecked")
private static <T> Matcher<T> createOptionalMatcher(final T expected) {
final Optional<T> expectedOptional = (Optional<T>) expected;
if (expectedOptional.isEmpty()) {
return (Matcher<T>) OptionalMatchers.isEmpty();
}
return (Matcher<T>) OptionalMatchers.isPresentAnd(AutoMatcher.equalTo(expectedOptional.get()));
}

private boolean isNotBlackListed(final Method method) {
final Set<String> blacklist = new HashSet<>(
asList("getClass", "getProtectionDomain", "getClassLoader", "getURLs"));
Expand Down Expand Up @@ -170,16 +189,23 @@ private static boolean isSimpleType(final Class<? extends Object> type) {
return false;
}

private String getPropertyName(final String methodName) {
int prefixLength = 3;
if (methodName.startsWith("is")) {
static String getPropertyName(final String methodName) {
final int prefixLength;
if (methodName.startsWith("get")) {
prefixLength = 3;
} else if (methodName.startsWith("is")) {
prefixLength = 2;
} else {
return methodName;
}
if (methodName.length() == prefixLength) {
return methodName;
}
final String propertyName = methodName.substring(prefixLength);
return decapitalize(propertyName);
}

private String decapitalize(final String string) {
private static String decapitalize(final String string) {
return Character.toLowerCase(string.charAt(0)) + string.substring(1);
}

Expand Down
108 changes: 108 additions & 0 deletions src/main/java/org/itsallcode/matcher/auto/OptionalMatchers.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package org.itsallcode.matcher.auto;

import java.util.Optional;

import org.hamcrest.*;

/**
* Provides a set of Hamcrest matchers for {@code java.util.Optional}:
* <ul>
* <li>{@link #isEmpty()} - matches when the examined {@code Optional} contains
* no value.</li>
* <li>{@link #isPresentAnd(Matcher)} - matches when the examined
* {@code Optional} contains a value that satisfies the specified matcher.</li>
* </ul>
*
* This was copied from
* {@link https://github.com/npathai/hamcrest-optional/blob/master/src/main/java/com/github/npathai/hamcrestopt/OptionalMatchers.java}
*
* @author npathai, sweiler
*/
class OptionalMatchers {

/**
* Creates a matcher that matches when the examined {@code Optional} contains no
* value.
*
* <pre>
* Optional&lt;String&gt; optionalObject = Optional.empty();
* assertThat(optionalObject, isEmpty());
* </pre>
*
* @return a matcher that matches when the examined {@code Optional} contains no
* value.
*/
@SuppressWarnings("java:S1452") // Generic wildcard required here
static Matcher<Optional<?>> isEmpty() {
return new EmptyMatcher();
}

private static class EmptyMatcher extends TypeSafeMatcher<Optional<?>> {

public void describeTo(final Description description) {
description.appendText("is <Empty>");
}

@Override
protected boolean matchesSafely(final Optional<?> item) {
return !item.isPresent();
}

@Override
protected void describeMismatchSafely(final Optional<?> item, final Description mismatchDescription) {
mismatchDescription.appendText("had value ");
mismatchDescription.appendValue(item.orElseThrow());
}
}

/**
* Creates a matcher that matches when the examined {@code Optional} contains a
* value that satisfies the specified matcher.
*
* <pre>
* Optional&lt;String&gt; optionalObject = Optional.of("dummy value");
* assertThat(optionalObject, isPresentAnd(startsWith("dummy")));
* </pre>
*
* @param matcher a matcher for the value of the examined {@code Optional}.
* @param <T> the class of the value.
* @return a matcher that matches when the examined {@code Optional} contains a
* value that satisfies the specified matcher.
*/
public static <T> Matcher<Optional<T>> isPresentAnd(final Matcher<? super T> matcher) {
return new HasValue<>(matcher);
}

private static class HasValue<T> extends TypeSafeMatcher<Optional<T>> {
private final Matcher<? super T> matcher;

public HasValue(final Matcher<? super T> matcher) {
this.matcher = matcher;
}

@Override
public void describeTo(final Description description) {
description.appendText("has value that is ");
matcher.describeTo(description);
}

@Override
protected boolean matchesSafely(final Optional<T> item) {
return item.isPresent() && matcher.matches(item.get());
}

@Override
protected void describeMismatchSafely(final Optional<T> item, final Description mismatchDescription) {
if (item.isPresent()) {
mismatchDescription.appendText("value ");
matcher.describeMismatch(item.get(), mismatchDescription);
} else {
mismatchDescription.appendText("was <Empty>");
}
}
}

// This is an utility class that must not be instantiated.
private OptionalMatchers() {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.itsallcode.matcher.auto;

import static org.junit.jupiter.api.Assertions.assertEquals;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

class AutoConfigBuilderTest {

@ParameterizedTest
@CsvSource({ "getName,name", "getname,name", "isTrue,true", "istrue,true", "get,get", "is,is",
"otherName,otherName", "OtherName,OtherName" })
void getPropertyName(final String methodName, final String expectedPropertyName) {
assertEquals(expectedPropertyName, AutoConfigBuilder.getPropertyName(methodName));
}
}
52 changes: 51 additions & 1 deletion src/test/java/org/itsallcode/matcher/auto/AutoMatcherTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,19 @@
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.*;
import static org.itsallcode.matcher.auto.TestUtil.assertValuesDoNotMatch;
import static org.itsallcode.matcher.auto.TestUtil.assertValuesMatch;
import static org.junit.Assert.assertThrows;

import java.io.File;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.*;
import java.nio.file.Paths;
import java.sql.Date;
import java.time.Instant;
import java.time.LocalDate;
import java.util.*;

import org.hamcrest.Matcher;
Expand Down Expand Up @@ -233,11 +236,26 @@ public void testAutoMatcherWorksForSimpleTypeDate() {
assertValuesDoNotMatch(new Date(1), new Date(2));
}

@Test
public void testAutoMatcherWorksForSimpleTypeSqlDate() {
assertValuesDoNotMatch(new java.sql.Date(1), new java.sql.Date(2));
}

@Test
public void testAutoMatcherWorksForSimpleTypeSqlTimestamp() {
assertValuesDoNotMatch(new java.sql.Timestamp(1), new java.sql.Timestamp(2));
}

@Test
public void testAutoMatcherWorksForSimpleTypeInstance() {
assertValuesDoNotMatch(Instant.ofEpochMilli(1), Instant.ofEpochMilli(2));
}

@Test
public void testAutoMatcherWorksForSimpleTypeLocalDate() {
assertValuesDoNotMatch(LocalDate.parse("2024-01-31"), LocalDate.parse("2024-02-01"));
}

@Test
public void testAutoMatcherWorksForSimpleTypeFile() {
assertValuesDoNotMatch(new File("a"), new File("b"));
Expand All @@ -253,6 +271,38 @@ public void testAutoMatcherWorksForSimpleTypeUuid() {
assertValuesDoNotMatch(UUID.randomUUID(), UUID.randomUUID());
}

@Test
public void testAutoMatcherWorksForSimpleTypeUrl() throws MalformedURLException {
assertValuesDoNotMatch(new URL("http://example.com"), new URL("http://example.com/test"));
}

@Test
public void testAutoMatcherWorksForSimpleTypeUri() {
assertValuesDoNotMatch(URI.create("http://example.com"), URI.create("http://example2.com"));
}

@Test
public void testAutoMatcherWorksForSimpleTypeOptionalEmpty() {
assertValuesDoNotMatch(Optional.empty(), Optional.of("a"),
equalTo("\nExpected: has value that is \"a\"\n but: was <Empty>"));
assertValuesDoNotMatch(Optional.of("a"), Optional.empty(),
equalTo("\nExpected: is <Empty>\n but: had value \"a\""));
}

@Test
public void testAutoMatcherWorksForSimpleTypeOptionalWithString() {
assertValuesDoNotMatch(Optional.of("b"), Optional.of("a"),
equalTo("\nExpected: has value that is \"a\"\n but: value was \"b\""));
}

@Test
public void testAutoMatcherWorksForSimpleTypeOptionalWithModel() {
final DemoModel m1 = new DemoModel(1, "a", 1L, null, null, null);
final DemoModel m2 = new DemoModel(2, "a", 1L, null, null, null);
assertValuesDoNotMatch(Optional.of(m1), Optional.of(m2), containsString("but: value {id was <1>}"));
assertValuesMatch(Optional.of(value1), Optional.of(value1Equal));
}

private DemoModel model(final String name, final int id) {
return model(name, id,
asList(model(name + "-child1", id, emptyList()), model(name + "-child2", id, emptyList())));
Expand Down
12 changes: 12 additions & 0 deletions src/test/java/org/itsallcode/matcher/auto/TestUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.not;
import static org.junit.jupiter.api.Assertions.assertThrows;

import org.hamcrest.Matcher;

public class TestUtil {
private TestUtil() {
Expand All @@ -20,4 +23,13 @@ public static <T> void assertValuesDoNotMatch(final T value1, final T value2) {
assertThat("expect match", value1, AutoMatcher.equalTo(value1));
assertThat("expect match", value2, AutoMatcher.equalTo(value2));
}

public static <T> void assertValuesDoNotMatch(final T value1, final T value2,
final Matcher<String> expectedMessage) {
assertValuesDoNotMatch(value1, value2);
final Matcher<T> matcher = AutoMatcher.equalTo(value2);
final AssertionError error = assertThrows(AssertionError.class,
() -> assertThat(value1, matcher));
assertThat(error.getMessage(), expectedMessage);
}
}

0 comments on commit d6e155d

Please sign in to comment.