diff --git a/CHANGELOG b/CHANGELOG index 123b8df..bbde639 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,12 @@ CHANGELOG ========= +0.4.0 +----- + +* Restore public access for `TypeAlias.getValue()` +* Add `Maybe`, `Just`, `Nothing` + 0.3.0 ----- diff --git a/README.md b/README.md index dd342cd..1a03777 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ TypeAlias for Java net.kemitix mon - 0.3.0 + 0.4.0 ``` @@ -41,3 +41,37 @@ void foo(final Goal goal) { System.out.println("The goal is " + goal.getValue()); } ``` + +### Maybe (Just & Nothing) + +```java +assertThat(Maybe.maybe(null)).isEqualTo(Maybe.nothing()); +assertThat(Maybe.maybe(1)).isEqualTo(Maybe.just(1)); +assertThat(Maybe.nothing() + .orElseGet(() -> 1)).isEqualTo(1); +assertThat(Maybe.just(1) + .orElseGet(() -> 2)).isEqualTo(1); +assertThat(Maybe.nothing() + .orElse(1)).isEqualTo(1); +assertThat(Maybe.just(1) + .orElse(2)).isEqualTo(1); +assertThat(Maybe.just(1) + .filter(v -> v > 2)).isEqualTo(Maybe.nothing()); +assertThat(Maybe.just(3) + .filter(v -> v > 2)).isEqualTo(Maybe.just(3)); +assertThat(Maybe.just(1) + .toOptional()).isEqualTo(Optional.of(1)); +assertThat(Maybe.nothing() + .toOptional()).isEqualTo(Optional.empty()); +assertThat(Maybe.fromOptional(Optional.of(1))).isEqualTo(Maybe.just(1)); +assertThat(Maybe.fromOptional(Optional.empty())).isEqualTo(Maybe.nothing()); +final AtomicInteger reference = new AtomicInteger(0); +assertThat(Maybe.just(1).peek(reference::set)).isEqualTo(Maybe.just(1)); +assertThat(reference).hasValue(1); +assertThat(Maybe.nothing().peek(v -> reference.incrementAndGet())).isEqualTo(Maybe.nothing()); +assertThat(reference).hasValue(1); +assertThatCode(() -> Maybe.just(1).orElseThrow(IllegalStateException::new)) + .doesNotThrowAnyException(); +assertThatThrownBy(() -> Maybe.nothing().orElseThrow(IllegalStateException::new)) + .isInstanceOf(IllegalStateException.class); +``` diff --git a/pom.xml b/pom.xml index 58bac15..ccf634f 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ 3.2.4 mon - 0.3.0 + 0.4.0 4.12 diff --git a/src/main/java/net/kemitix/mon/Functor.java b/src/main/java/net/kemitix/mon/Functor.java index 0111ca1..7dc4fa1 100644 --- a/src/main/java/net/kemitix/mon/Functor.java +++ b/src/main/java/net/kemitix/mon/Functor.java @@ -34,18 +34,19 @@ import java.util.function.Function; * * * @param the type of the Functor + * @param the type of the mapped Functor * * @author Tomasz Nurkiewicz (?@?.?) */ -public interface Functor { +public interface Functor> { /** * Applies the function to the value within the Functor, returning the result within a Functor. * * @param f the function to apply - * @param the type of the result of the function + * @param the type of the content of the mapped functor * * @return a Functor containing the result of the function {@code f} applied to the value */ - Functor map(Function f); + F map(Function f); } diff --git a/src/main/java/net/kemitix/mon/Identity.java b/src/main/java/net/kemitix/mon/Identity.java index 1300d7d..4b4df37 100644 --- a/src/main/java/net/kemitix/mon/Identity.java +++ b/src/main/java/net/kemitix/mon/Identity.java @@ -33,7 +33,7 @@ import java.util.function.Function; * @author Paul Campbell (pcampbell@kemitix.net) */ @RequiredArgsConstructor -class Identity implements Functor { +class Identity implements Functor> { private final T value; diff --git a/src/main/java/net/kemitix/mon/Just.java b/src/main/java/net/kemitix/mon/Just.java new file mode 100644 index 0000000..9437bc7 --- /dev/null +++ b/src/main/java/net/kemitix/mon/Just.java @@ -0,0 +1,94 @@ +/** + * The MIT License (MIT) + * + * Copyright (c) 2017 Paul Campbell + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software + * and associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies + * or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE + * AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package net.kemitix.mon; + +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; + +import java.util.Objects; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; + +/** + * A Maybe where a value is present. + * + * @param the type of the content + * + * @author Paul Campbell (pcampbell@kemitix.net) + */ +@RequiredArgsConstructor(access = AccessLevel.PROTECTED) +public final class Just implements Maybe { + + private final T value; + + @Override + public Maybe map(final Function f) { + return new Just<>(f.apply(value)); + } + + @Override + public boolean equals(final Object other) { + return other instanceof Just && Objects.equals(this.value, ((Just) other).value); + } + + @Override + public int hashCode() { + return Objects.hashCode(value); + } + + @Override + public T orElseGet(final Supplier supplier) { + return value; + } + + @Override + public T orElse(final T otherValue) { + return this.value; + } + + @Override + public Maybe filter(final Predicate predicate) { + if (predicate.test(value)) { + return this; + } + return Maybe.nothing(); + } + + @Override + public Optional toOptional() { + return Optional.of(value); + } + + @Override + public Maybe peek(final Consumer consumer) { + consumer.accept(value); + return this; + } + + @Override + public void orElseThrow(final Supplier e) { + // do not throw + } +} diff --git a/src/main/java/net/kemitix/mon/Maybe.java b/src/main/java/net/kemitix/mon/Maybe.java new file mode 100644 index 0000000..3211d0c --- /dev/null +++ b/src/main/java/net/kemitix/mon/Maybe.java @@ -0,0 +1,145 @@ +/** + * The MIT License (MIT) + * + * Copyright (c) 2017 Paul Campbell + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software + * and associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies + * or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE + * AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package net.kemitix.mon; + +import lombok.NonNull; + +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.function.Supplier; + +/** + * A value that may or may not be present. + * + * @param the type of the content of the Just + * + * @author Paul Campbell (pcampbell@kemitix.net) + */ +public interface Maybe extends Functor> { + + /** + * Create a Maybe for the value that is present. + * + * @param value the value, not null + * @param the type of the value + * + * @return a Maybe of the value + */ + static Maybe just(@NonNull final T value) { + return new Just<>(value); + } + + /** + * Create a Maybe for a lack of a value. + * + * @param the type of the missing value + * + * @return an empty Maybe + */ + @SuppressWarnings("unchecked") + static Maybe nothing() { + return (Maybe) Nothing.INSTANCE; + } + + /** + * Create a Maybe for the value that may or may not be present. + * + * @param value the value, may be null + * @param the type of the value + * + * @return a Maybe, either a Just, or Nothing if value is null + */ + static Maybe maybe(final T value) { + if (value == null) { + return nothing(); + } + return just(value); + } + + /** + * Create a Maybe from an {@link Optional}. + * + * @param optional the Optional + * @param the type of the Optional + * + * @return a Maybe + */ + static Maybe fromOptional(final Optional optional) { + return optional.map(Maybe::maybe) + .orElse(nothing()); + } + + /** + * Provide a value to use when Maybe is Nothing. + * + * @param supplier supplier for an alternate value + * + * @return a Maybe + */ + T orElseGet(Supplier supplier); + + /** + * A value to use when Maybe is Nothing. + * + * @param otherValue an alternate value + * + * @return a Maybe + */ + T orElse(T otherValue); + + /** + * Filter a Maybe by the predicate, replacing with Nothing when it fails. + * + * @param predicate the test + * + * @return the Maybe, or Nothing if the test returns false + */ + Maybe filter(Predicate predicate); + + /** + * Convert the Maybe to an {@link Optional}. + * + * @return an Optional containing a value for a Just, or empty for a Nothing + */ + Optional toOptional(); + + /** + * Provide the value within the Maybe, if it exists, to the Supplier, and returns the Maybe. + * + * @param consumer the Consumer to the value if present + * + * @return the Maybe + */ + Maybe peek(Consumer consumer); + + /** + * Throw the exception if the Maybe is a Nothing. + * + * @param e the exception to throw + * + * @throws Exception if the Maybe is a Nothing + */ + @SuppressWarnings("illegalthrows") + void orElseThrow(Supplier e) throws Exception; + +} diff --git a/src/main/java/net/kemitix/mon/Mon.java b/src/main/java/net/kemitix/mon/Mon.java index 8abe82d..b591044 100644 --- a/src/main/java/net/kemitix/mon/Mon.java +++ b/src/main/java/net/kemitix/mon/Mon.java @@ -37,7 +37,7 @@ import java.util.function.Supplier; * @author Paul Campbell (pcampbell@kemitix.net) */ @RequiredArgsConstructor(access = AccessLevel.PROTECTED) -public class Mon implements Functor { +public class Mon implements Functor> { /** * The value. diff --git a/src/main/java/net/kemitix/mon/Nothing.java b/src/main/java/net/kemitix/mon/Nothing.java new file mode 100644 index 0000000..f484b1f --- /dev/null +++ b/src/main/java/net/kemitix/mon/Nothing.java @@ -0,0 +1,75 @@ +/** + * The MIT License (MIT) + * + * Copyright (c) 2017 Paul Campbell + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software + * and associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies + * or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE + * AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package net.kemitix.mon; + +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; + +/** + * A Maybe where no value is present. + * + * @param the type of the missing content + * + * @author Paul Campbell (pcampbell@kemitix.net) + */ +public final class Nothing implements Maybe { + + protected static final Maybe INSTANCE = new Nothing<>(); + + @Override + public Maybe map(final Function f) { + return this; + } + + @Override + public T orElseGet(final Supplier supplier) { + return supplier.get(); + } + + @Override + public T orElse(final T otherValue) { + return otherValue; + } + + @Override + public Maybe filter(final Predicate predicate) { + return this; + } + + @Override + public Optional toOptional() { + return Optional.empty(); + } + + @Override + public Maybe peek(final Consumer consumer) { + return this; + } + + @Override + public void orElseThrow(final Supplier e) throws Exception { + throw e.get(); + } +} diff --git a/src/main/java/net/kemitix/mon/TypeAlias.java b/src/main/java/net/kemitix/mon/TypeAlias.java index f5dc0c5..7676736 100644 --- a/src/main/java/net/kemitix/mon/TypeAlias.java +++ b/src/main/java/net/kemitix/mon/TypeAlias.java @@ -26,6 +26,9 @@ import java.util.function.Function; /** * Type Alias for other types. * + *

{@link #toString()}, {@link #equals(Object)} and {@link #hashCode()} are all transparent, returning the value + * for the type being aliased.

+ * * @param the type of the alias * * @author Paul Campbell (pcampbell@kemitix.net) @@ -38,20 +41,13 @@ public abstract class TypeAlias { */ private final T value; - private final Class type; - /** * Constructor. * * @param value the value - * @param type the type of the value */ - protected TypeAlias( - final T value, - final Class type - ) { + protected TypeAlias(final T value) { this.value = value; - this.type = type; } /** @@ -72,14 +68,13 @@ public abstract class TypeAlias { } @Override - @SuppressWarnings("unchecked") public final boolean equals(final Object o) { if (o instanceof TypeAlias) { - if (((TypeAlias) o).type.equals(type)) { - return ((TypeAlias) o).map(getValue()::equals); - } else { - return false; - } + final TypeAlias other = (TypeAlias) o; + final Object otherValue = other.getValue(); + final Class otherValueClass = otherValue.getClass(); + return otherValueClass.equals(getValue().getClass()) + && otherValue.equals(getValue()); } return map(o::equals); } @@ -94,7 +89,7 @@ public abstract class TypeAlias { * * @return the value */ - private T getValue() { + public T getValue() { return value; } } diff --git a/src/test/java/net/kemitix/mon/IdentityTest.java b/src/test/java/net/kemitix/mon/IdentityTest.java index 513a979..db95452 100644 --- a/src/test/java/net/kemitix/mon/IdentityTest.java +++ b/src/test/java/net/kemitix/mon/IdentityTest.java @@ -10,15 +10,6 @@ import org.junit.Test; */ public class IdentityTest implements WithAssertions { - @Test - public void functorLawMapIdEqualsId() { - //given - final String id = "id"; - //when - - //then - } - @Test public void canMapIdentityFromStringToInteger() { //given diff --git a/src/test/java/net/kemitix/mon/MaybeTest.java b/src/test/java/net/kemitix/mon/MaybeTest.java new file mode 100644 index 0000000..ba68b3c --- /dev/null +++ b/src/test/java/net/kemitix/mon/MaybeTest.java @@ -0,0 +1,114 @@ +package net.kemitix.mon; + +import org.junit.Test; + +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Predicate; + +import static net.kemitix.mon.Maybe.just; +import static net.kemitix.mon.Maybe.maybe; +import static net.kemitix.mon.Maybe.nothing; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class MaybeTest { + + private static Predicate eq(final T value) { + return v -> Objects.equals(value, v); + } + + @Test + public void justMustBeNonNull() { + assertThatNullPointerException().isThrownBy(() -> just(null)) + .withMessage("value"); + } + + @Test + public void nothingReusesTheSameInstance() { + assertThat(nothing()).isSameAs(nothing()); + } + + @Test + public void equality() { + assertThat(just(1)).isEqualTo(just(1)); + assertThat(just(1)).isNotEqualTo(just(2)); + assertThat(just(1)).isNotEqualTo(nothing()); + assertThat(nothing()).isEqualTo(nothing()); + assertThat(just(1).equals("1")).isFalse(); + } + + @Test + public void maybeAllowsNull() { + assertThat(just(1)).isEqualTo(maybe(1)); + assertThat(nothing()).isEqualTo(maybe(null)); + } + + @Test + public void map() { + assertThat(just(1).map(v -> v + 1)).isEqualTo(just(2)); + assertThat(nothing().map(v -> v)).isEqualTo(nothing()); + } + + @Test + public void testHashCode() { + assertThat(just(1).hashCode()).isEqualTo(Objects.hashCode(1)); + } + + @Test + public void orElseGet() { + assertThat(just(1).orElseGet(() -> -1)).isEqualTo(1); + assertThat(nothing().orElseGet(() -> -1)).isEqualTo(-1); + } + + @Test + public void orElse() { + assertThat(just(1).orElse(-1)).isEqualTo(1); + assertThat(nothing().orElse(-1)).isEqualTo(-1); + } + + @Test + public void filter() { + assertThat(just(1).filter(eq(1))).isEqualTo(just(1)); + assertThat(just(1).filter(eq(0))).isEqualTo(nothing()); + assertThat(nothing().filter(eq(1))).isEqualTo(nothing()); + } + + @Test + public void toOptional() { + assertThat(just(1).toOptional()).isEqualTo(Optional.of(1)); + assertThat(nothing() + .toOptional()).isEqualTo(Optional.empty()); + } + + @Test + public void fromOptional() { + assertThat(Maybe.fromOptional(Optional.of(1))).isEqualTo(just(1)); + assertThat(Maybe.fromOptional(Optional.empty())).isEqualTo(nothing()); + } + + + @Test + public void peek() { + final AtomicInteger ref = new AtomicInteger(0); + assertThat(just(1).peek(x -> ref.incrementAndGet())).isEqualTo(just(1)); + assertThat(ref.get()).isEqualTo(1); + + assertThat(nothing().peek(x -> ref.incrementAndGet())).isEqualTo(nothing()); + assertThat(ref.get()).isEqualTo(1); + } + + @Test + public void justOrThrow() { + assertThatCode(() -> just(1).orElseThrow(IllegalStateException::new)).doesNotThrowAnyException(); + } + + @Test + public void nothingOrThrow() { + assertThatThrownBy(() -> nothing().orElseThrow(IllegalStateException::new)).isInstanceOf( + IllegalStateException.class); + } +} diff --git a/src/test/java/net/kemitix/mon/TypeAliasTest.java b/src/test/java/net/kemitix/mon/TypeAliasTest.java index 41c4222..08a5448 100644 --- a/src/test/java/net/kemitix/mon/TypeAliasTest.java +++ b/src/test/java/net/kemitix/mon/TypeAliasTest.java @@ -1,22 +1,29 @@ package net.kemitix.mon; +import org.assertj.core.util.Strings; import org.junit.Test; +import java.util.Arrays; import java.util.Collections; -import java.util.function.Function; +import java.util.List; import static org.assertj.core.api.Assertions.assertThat; public class TypeAliasTest { @Test - public void shouldCreateATypeAliasAndGetTheValue() throws Exception { + public void shouldCreateATypeAliasAndGetTheValue() { //given final String value = "value"; //when final TypeAlias typeAlias = givenTypeAlias(value); //then - assertThat(typeAlias.map(value::equals)).isTrue(); + assertThat(typeAlias.getValue()).isSameAs(value); + } + + private TypeAlias givenTypeAlias(final String value) { + return new TypeAlias(value) { + }; } @Test @@ -25,38 +32,34 @@ public class TypeAliasTest { final Iterable iterable = Collections.emptyList(); //when final TypeAlias> typeAlias = - new TypeAlias>(iterable, Iterable.class) { + new TypeAlias>(iterable) { }; //then - assertThat(typeAlias.map(iterable::equals)).isTrue(); - } - - private TypeAlias givenTypeAlias(final String value) { - return new TypeAlias(value, String.class) { - }; + assertThat(typeAlias.getValue()).isSameAs(iterable); } @Test - public void shouldCreateAnAliasedTypeAndGetTheValue() throws Exception { + public void shouldCreateATypeAliasSubclassAndGetTheValue() { //given final String value = "value"; //when final AnAlias anAlias = AnAlias.of(value); //then - assertThat(anAlias.map(value::equals)).isTrue(); + assertThat(anAlias.getValue()).isSameAs(value); } @Test public void shouldNotBeEqualWhenValueTypesAreDifferent() { //given final TypeAlias stringTypeAlias = givenTypeAlias("1"); - final TypeAlias integerTypeAlias = new TypeAlias(1, Integer.class){}; + final TypeAlias integerTypeAlias = new TypeAlias(1) { + }; //then assertThat(stringTypeAlias).isNotEqualTo(integerTypeAlias); } @Test - public void shouldBeEqualWhenValuesAreTheSame() throws Exception { + public void shouldBeEqualWhenValuesAreTheSame() { //given final String value = "value"; final AnAlias anAlias1 = AnAlias.of(value); @@ -66,7 +69,16 @@ public class TypeAliasTest { } @Test - public void shouldBeEqualToUnAliasedValue() throws Exception { + public void shouldNotBeEqualWhenValuesAreNotTheSame() { + //given + final AnAlias valueA = AnAlias.of("value a"); + final AnAlias valueB = AnAlias.of("value b"); + //then + assertThat(valueA).isNotEqualTo(valueB); + } + + @Test + public void shouldBeEqualToRawValue() { //given final String value = "value"; final AnAlias anAlias = AnAlias.of(value); @@ -75,7 +87,7 @@ public class TypeAliasTest { } @Test - public void shouldHaveHashCodeOfValue() throws Exception { + public void shouldHaveHashCodeOfValue() { //given final String value = "value"; final AnAlias anAlias = AnAlias.of(value); @@ -84,24 +96,24 @@ public class TypeAliasTest { } @Test - public void shouldHaveSameToStringAsAliasedType() throws Exception { + public void shouldHaveSameToStringAsAliasedType() { //given - final String value = "value"; + final List value = Arrays.asList(1, 2, 3); //when - final AnAlias anAlias = AnAlias.of(value); + final TypeAlias> anAlias = new TypeAlias>(value) { + }; //then - assertThat(anAlias.toString()).isEqualTo(value); + assertThat(anAlias.toString()).isEqualTo(value.toString()); } @Test public void shouldMapTypeAlias() { //given final AnAlias anAlias = AnAlias.of("text"); - final Function function = v -> v; //when - final String value = anAlias.map(function); + final String value = anAlias.map(Strings::quote); //then - assertThat(value).isEqualTo("text"); + assertThat(value).isEqualTo("'text'"); } private static class AnAlias extends TypeAlias { @@ -112,7 +124,7 @@ public class TypeAliasTest { * @param value the value */ protected AnAlias(final String value) { - super(value, String.class); + super(value); } protected static AnAlias of(final String value) {