diff --git a/.gitignore b/.gitignore index a2653f2..1835283 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,7 @@ target *.iws .idea/ -*.gpg \ No newline at end of file +# VS Code +.vscode + +*.gpg diff --git a/pom.xml b/pom.xml index 7ded3b9..0960f3e 100644 --- a/pom.xml +++ b/pom.xml @@ -40,6 +40,8 @@ 2.0.1.Final 3.1.1 5.13.4 + 1.3 + 4.11.0 @@ -102,7 +104,25 @@ junit-jupiter-api test + + org.junit.jupiter + junit-jupiter-params + test + + + org.hamcrest + hamcrest-all + ${hamcrest.version} + test + + + org.mockito + mockito-core + ${mockito.version} + test + + @@ -126,6 +146,32 @@ ${java.version} + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.4 + + + false + 3.0 + false + true + true + true + + + false + UTF-8 + false + + + false + false + true + true + + + org.apache.maven.plugins maven-jar-plugin diff --git a/src/main/java/org/openapitools/jackson/nullable/JsonNullable.java b/src/main/java/org/openapitools/jackson/nullable/JsonNullable.java index 43a521b..53c4173 100644 --- a/src/main/java/org/openapitools/jackson/nullable/JsonNullable.java +++ b/src/main/java/org/openapitools/jackson/nullable/JsonNullable.java @@ -4,6 +4,10 @@ import java.util.NoSuchElementException; import java.util.Objects; import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Stream; public class JsonNullable implements Serializable { @@ -66,15 +70,86 @@ public T orElse(T other) { return this.isPresent ? this.value : other; } + /** + * If a value is present, returns the value, otherwise returns the result + * produced by the supplying function. + * + * @param supplier the supplying function that produces a value to be returned + * @return the value, if present, otherwise the result produced by the supplying function + * @throws NullPointerException if no value is present and the supplying function is null + * + * @since 0.2.8 + */ + public T orElseGet(Supplier supplier) { + return this.isPresent ? this.value : supplier.get(); + } + + /** + * If a value is present, returns the value, otherwise throws + * NoSuchElementException. + * + * @return the value of this JsonNullable + * @throws NoSuchElementException if no value if present + * + * @since 0.2.8 + */ + public T orElseThrow() { + if (!isPresent) { + throw new NoSuchElementException("Value is undefined"); + } + return value; + } + + /** + * If a value is present, returns the value, otherwise throws an exception + * produced by the exception supplying function. + * + * @param type of the exception to be thrown + * @param supplier the supplying function that produces an exception to be + * thrown + * @return the value, if present + * @throws X if no value is present + * @throws NullPointerException if no value is present and the exception + * supplying function is {@code null} + * + * @since 0.2.8 + */ + public T orElseThrow(Supplier supplier) + throws X + { + if( this.isPresent ) { + return this.value; + } + throw supplier.get(); + } + + /** + * If a value is present, returns true, otherwise false. + * + * @return true if a value is present, otherwise false + */ public boolean isPresent() { return isPresent; } + /** + * If a value is not present, returns true, otherwise false. + * + * @return true if a value is not present, otherwise false + * + * @since 0.2.8 + */ + public boolean isUndefined() { + return !isPresent; + } + /** * If a value is present, performs the given action with the value, * otherwise does nothing. * * @param action the action to be performed, if a value is present + * @throws NullPointerException if a value is present and the given action + * is null */ public void ifPresent( Consumer action) { @@ -84,6 +159,153 @@ public void ifPresent( } } + /** + * If a value is present, performs the given action with the value, + * otherwise performs the given empty-based action. + * + * @param action the action to be performed, if a value is present + * @param undefinedAction the empty-based action to be performed, if no + * value is present + * @throws NullPointerException if a value is present and the given action + * is null, or no value is present and the given empty-based action + * is null + * + * @since 0.2.8 + */ + public void ifPresentOrElse( Consumer action, Runnable undefinedAction ) { + if (this.isPresent) { + action.accept(value); + } + else { + undefinedAction.run(); + } + } + + /** + * If a value is present, and the value matches the given predicate, returns + * a JsonNullable describing the value, otherwise returns an undefined + * JsonNullable. + * + * @param predicate the predicate to apply to a value, if present + * @return a JsonNullable describing the value of this JsonNullable, + * if a value is present and the value matches the given predicate, + * otherwise an undefined JsonNullable + * @throws NullPointerException if the predicate is null + * + * @since 0.2.8 + */ + public JsonNullable filter( Predicate predicate ) { + if (predicate == null) { + throw new NullPointerException("filter predicate is null"); + } + if (this.isPresent && predicate.test(value)) { + return this; + } + else { + return undefined(); + } + } + + /** + * If a value is present, returns a JsonNullable describing the result of + * applying the given mapping function to the value, otherwise returns an + * undeined JsonNullable. + * + * @param the type of the value returned from the mapping function + * @param mapper the mapping function to apply to a value, if present + * @return a JsonNullable describing the result of applying a mapping + * function to the value of this JsonNullable, if a value is + * present, otherwise an undefined JsonNullable + * @throws NullPointerException if the mapping function is null + * + * @since 0.2.8 + */ + public JsonNullable map( Function mapper) { + if (mapper == null) { + throw new NullPointerException("mapping function is null"); + } + if (this.isPresent) { + return new JsonNullable(mapper.apply(value), true); + } + return undefined(); + } + + /** + * If a value is present, returns the result of applying the given + * JsonNullable-bearing mapping function to the value, otherwise returns an + * undefined JsonNullable. + * + * @param the type of value of the JsonNullable returned by the mapping + * function + * @param mapper the mapping function to apply to a value, if present + * @return the result of applying a JsonNullable-bearing mapping function to + * the value of this JsonNullable, if a value is present, otherwise + * an undefined JsonNullable + * @throws NullPointerException if the mapping function is null or returns a + * null result + * + * @since 0.2.8 + */ + @SuppressWarnings("unchecked") + public JsonNullable flatMap( Function> mapper ) { + if (mapper == null) { + throw new NullPointerException("mapping function is null"); + } + if (!this.isPresent) { + return undefined(); + } + + JsonNullable mapped = (JsonNullable)mapper.apply(value); + if (mapped == null) { + throw new NullPointerException("mapped value is null"); + } + return mapped; + } + + /** + * If a value is present, returns a JsonNullable describing the value, + * otherwise returns a JsonNullable produced by the supplying function. + * + * @param supplier the supplying function that produces a JsonNullable to be + * returned + * @return returns a JsonNullable describing the value of this JsonNullable, + * if a value is present, otherwise a JsonNullable produced by the + * supplying function. + * @throws NullPointerException if the supplying function is null or + * produces a null result + * + * @since 0.2.8 + */ + @SuppressWarnings("unchecked") + public JsonNullable or( Supplier> supplier ) { + if( supplier == null ) { + throw new NullPointerException("or supplier is null"); + } + if (this.isPresent) { + return this; + } + JsonNullable supplied = (JsonNullable)supplier.get(); + if (supplied == null) { + throw new NullPointerException("supplied value is null"); + } + return supplied; + } + + /** + * If a value is present, returns a sequential Stream containing only that + * value, otherwise returns an empty Stream. + * + * @return the JsonNullable value as a Stream + * + * @since 0.2.8 + */ + public Stream stream() { + if (this.isPresent) { + return Stream.of(value); + } + return Stream.empty(); + } + @Override public boolean equals(Object obj) { if (this == obj) { diff --git a/src/test/java/org/openapitools/jackson/nullable/StreamingApiTest.java b/src/test/java/org/openapitools/jackson/nullable/StreamingApiTest.java new file mode 100644 index 0000000..f4b7a73 --- /dev/null +++ b/src/test/java/org/openapitools/jackson/nullable/StreamingApiTest.java @@ -0,0 +1,1023 @@ +/* + * Copyright 2025 Christian Trimble + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openapitools.jackson.nullable; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.concurrent.Callable; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.hamcrest.Matcher; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.AdditionalAnswers; + +import static org.openapitools.jackson.nullable.JsonNullable.of; +import static org.openapitools.jackson.nullable.JsonNullable.undefined; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.nullValue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.empty; + +/** + * Tests for Stream API methods patterned after java.util.Optional. + * + * @author Christian Trimble + */ +//@DisplayName("Streaming API Tests") +public class StreamingApiTest { + + static ReflectiveService>> OPTIONAL = new ReflectiveService>>(Optional.class); + + static String VALUE = "value"; + static String OTHER = "other"; + static String NULL = null; + static JsonNullable JSON_VALUE = of(VALUE); + static JsonNullable JSON_OTHER = of(OTHER); + static JsonNullable JSON_NULL = of(null); + static JsonNullable UNDEFINED = undefined(); + static Class NPE = NullPointerException.class; + static Class TE = TestException.class; + + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + public abstract class BaseTest { + + public abstract Stream baseArgs(); + public abstract Callable createCall(Object[] row); + public abstract Callable createEquivalentCall(Object[] row); + + @ParameterizedTest + @MethodSource("args") + public void standardCall(String name, Callable> callable, Matcher matcher) { + Assumptions.assumeTrue(callable!=null); + Object actual = null; + try { + actual = callable.call(); + } catch (Throwable t) { + assertThat(t, matcher); + return; + } + assertThat(actual, matcher); + } + + public Stream args() { + return baseArgs() + .map(args->new Object[] { args[0], createCall(args), createMatcher(args[args.length - 1])}) + .map(args->Arguments.argumentSet((String)args[0], args)); + } + + @ParameterizedTest + @MethodSource("equivalentArgs") + public void equivalentCall(String name, Callable>> callable, Matcher matcher) { + Assumptions.assumeTrue(callable!=null); + Object actual = null; + try { + actual = callable.call(); + } catch (Throwable t) { + assertThat(t, matcher); + return; + } + assertThat(actual, matcher); + } + + public Stream equivalentArgs() { + return baseArgs() + .map(args->new Object[] { + args[0], + createEquivalentCall(args), + createMatcher(args[args.length - 1]) + }) + .map(args->Arguments.argumentSet("equivalent "+(String)args[0], args)); + } + } + + @Nested + @DisplayName("Streaming API Tests or(Supplier)") + public class OrTest extends BaseTest { + public Stream baseArgs() { + Supplier NULL_SUPPLIER = null; + return Arrays.asList(new Object[][] { + { "or value supplied on value" , JSON_VALUE, supplier(JSON_OTHER), JSON_VALUE }, + { "or value supplied on null" , JSON_NULL , supplier(JSON_OTHER), JSON_NULL }, + { "or value supplied on undefined", UNDEFINED , supplier(JSON_OTHER), JSON_OTHER }, + { "or null supplied on value" , JSON_VALUE, supplier(NULL) , JSON_VALUE }, + { "or null supplied on null" , JSON_NULL , supplier(NULL) , JSON_NULL }, + { "or null supplied on undefined" , UNDEFINED , supplier(NULL) , NPE }, + { "or null supplier on value" , JSON_VALUE, NULL_SUPPLIER , NPE }, + { "or null supplier on null" , JSON_NULL , NULL_SUPPLIER , NPE }, + { "or null supplier on undefined" , UNDEFINED , NULL_SUPPLIER , NPE }, + }).stream(); + } + + @Override + public Callable createCall(Object[] row) { + return biFunctionCall( + JsonNullable::or, + asJson(row[1]), + asSupplier(row[2]) + ); + } + + @Override + public Callable createEquivalentCall(Object[] row) { + return biFunctionCall( + OPTIONAL.forName("or") + .>>>argument(Supplier.class) + .>>getBiFunction() + .map(f->f.andThen(StreamingApiTest::optionalToNullable)) + .orElse(null), + nullableToOptional(row[1]), + nullableSupplierToOptionalSupplier(asSupplier(row[2])) + ); + } + } + + @Nested + @DisplayName("Streaming API Tests orElse(Object)") + public class OrElseTest extends BaseTest { + public Stream baseArgs() { + return Arrays.asList(new Object[][] { + { "orElse value on value" , JSON_VALUE, OTHER, VALUE }, + { "orElse value on null" , JSON_NULL , OTHER, NULL }, + { "orElse value on undefined", UNDEFINED , OTHER, OTHER }, + { "orElse null on value" , JSON_VALUE, NULL , VALUE }, + { "orElse null on null" , JSON_NULL , NULL , NULL }, + { "orElse null on undefined" , UNDEFINED , NULL , NULL }, + }).stream(); + } + + @Override + public Callable createCall(Object[] row) { + return biFunctionCall( + JsonNullable::orElse, + asJson(row[1]), + (String)row[2] + ); + } + + @Override + public Callable createEquivalentCall(Object[] row) { + return biFunctionCall( + OPTIONAL.forName("orElse") + .>argument(Object.class) + .>getBiFunction() + .map(f->f.andThen(StreamingApiTest::optionalValueToNullableValue)) + .orElse(null), + nullableToOptional(row[1]), + nullableValueToOptionalValue(row[2]) + ); + } + } + + @Nested + @DisplayName("Streaming API Tests orElseGet(Supplier)") + public class OrElseGetTest extends BaseTest { + public Stream baseArgs() { + Supplier NULL_SUPPLIER = null; + return Arrays.asList(new Object[][] { + { "orElseGet value supplied on value" , JSON_VALUE, supplier(OTHER), VALUE }, + { "orElseGet value supplied on null" , JSON_NULL , supplier(OTHER), NULL }, + { "orElseGet value supplied on undefined", UNDEFINED , supplier(OTHER), OTHER }, + { "orElseGet null supplier on value" , JSON_VALUE, NULL_SUPPLIER , VALUE }, + { "orElseGet null supplier on null" , JSON_NULL , NULL_SUPPLIER , NULL }, + { "orElseGet null supplier on undefined" , UNDEFINED , NULL_SUPPLIER , NPE }, + }).stream(); + } + + @Override + public Callable createCall(Object[] row) { + return biFunctionCall( + JsonNullable::orElseGet, + asJson(row[1]), + asSupplier(row[2]) + ); + } + + @Override + public Callable createEquivalentCall(Object[] row) { + return biFunctionCall( + OPTIONAL.forName("orElseGet") + .>>argument(Supplier.class) + .>getBiFunction() + .map(f->f.andThen(StreamingApiTest::optionalValueToNullableValue)) + .orElse(null), + nullableToOptional(row[1]), + nullableValueSupplierToOptionalValueSupplier(asSupplier(row[2])) + ); + } + } + + @Nested + @DisplayName("Streaming API Tests orElseThrow()") + public class OrElseThrowTest extends BaseTest { + public Stream baseArgs() { + Class NSEE = NoSuchElementException.class; + return Arrays.asList(new Object[][] { + { "orElseThrow on value" , JSON_VALUE, VALUE }, + { "orElseThrow on null" , JSON_NULL , NULL }, + { "orElseThrow on undefined", UNDEFINED , NSEE, }, + }).stream(); + } + + @Override + public Callable createCall(Object[] row) { + return functionCall( + JsonNullable::orElseThrow, + asJson(row[1]) + ); + } + + @Override + public Callable createEquivalentCall(Object[] row) { + return functionCall( + OPTIONAL.forName("orElseThrow") + .>getFunction() + .map(f->f.andThen(StreamingApiTest::optionalValueToNullableValue)) + .orElse(null), + nullableToOptional(row[1]) + ); + } + } + + @Nested + @DisplayName("Streaming API Tests orElseThrow(Supplier)") + public class OrElseThrowWithSupplierTest extends BaseTest { + public Stream baseArgs() { + TestSupplier NULL_SUPPLIER = null; + TestSupplier NULL_SUPPLIED = ()->null; + TestSupplier TEST_EXCEPTION = TestException::new; + return Arrays.asList(new Object[][] { + { "orElseThrow with supplier on value" , JSON_VALUE, TEST_EXCEPTION, VALUE }, + { "orElseThrow with supplier on null" , JSON_NULL , TEST_EXCEPTION, NULL }, + { "orElseThrow with supplier on undefined", UNDEFINED , TEST_EXCEPTION, TE }, + { "orElseThrow null supplier on value" , JSON_VALUE, NULL_SUPPLIER , VALUE }, + { "orElseThrow null supplier on null" , JSON_NULL , NULL_SUPPLIER , NULL }, + { "orElseThrow null supplier on undefined", UNDEFINED , NULL_SUPPLIER , NPE }, + { "orElseThrow null supplied on value" , JSON_VALUE, NULL_SUPPLIED , VALUE }, + { "orElseThrow null supplied on null" , JSON_NULL , NULL_SUPPLIED , NULL }, + { "orElseThrow null supplied on undefined", UNDEFINED , NULL_SUPPLIED , NPE }, + }).stream(); + } + + @Override + public Callable createCall(Object[] row) { + return biFunctionWithThrowsCall( + JsonNullable::orElseThrow, + asJson(row[1]), + asExceptionSupplier(row[2]) + ); + } + + @Override + public Callable createEquivalentCall(Object[] row) { + return biFunctionWithThrowsCall( + OPTIONAL.forName("orElseThrow") + .>argument(Supplier.class) + .throwing(Exception.class) + .>getBiFunctionWithThrows() + .map(f->f.andThen(StreamingApiTest::optionalValueToNullableValue)) + .orElse(null), + nullableToOptional(row[1]), + asExceptionSupplier(row[2]) + ); + } + } + + @Nested + @DisplayName("Streaming API Tests map(Function)") + public class MapTest extends BaseTest { + public Stream baseArgs() { + Function MAPPING = value->value!=null?value+" mapped":"null mapped"; + Function NULL_MAPPING = null; + return Arrays.asList(new Object[][] { + { "map on value" , JSON_VALUE, MAPPING , of("value mapped") }, + { "map on null" , JSON_NULL , MAPPING , of("null mapped") }, + { "map on undefined" , UNDEFINED , MAPPING , UNDEFINED }, + { "map null mapping on value" , JSON_VALUE, NULL_MAPPING, NPE }, + { "map null mapping on null" , JSON_NULL , NULL_MAPPING, NPE }, + { "map null mapping on undefined", UNDEFINED , NULL_MAPPING, NPE }, + }).stream(); + } + + @SuppressWarnings("unchecked") + @Override + public Callable createCall(Object[] row) { + return biFunctionCall( + JsonNullable::map, + asJson(row[1]), + (Function)row[2] + ); + } + + @Override + public Callable createEquivalentCall(Object[] row) { + return biFunctionCall( + OPTIONAL.forName("map") + ., Optional>>argument(Function.class) + .>>getBiFunction() + .map(f->f.andThen(StreamingApiTest::optionalToNullable)) + .orElse(null), + nullableToOptional(asJson(row[1])), + nullableValueToOptionalValue(asFunction(row[2])) + ); + } + } + + @Nested + @DisplayName("Streaming API Tests filter(Predicate)") + public class FilterTest extends BaseTest { + public Stream baseArgs() { + TestPredicate KEEP_ALL = value->true; + TestPredicate KEEP_NULL = value->value==null; + TestPredicate KEEP_VALUE = value->value!=null; + TestPredicate KEEP_NONE = value->false; + TestPredicate NULL_PREDICATE = null; + return Arrays.asList(new Object[][] { + { "filter keep all on value" , JSON_VALUE, KEEP_ALL , JSON_VALUE }, + { "filter keep all on null" , JSON_NULL , KEEP_ALL , JSON_NULL }, + { "filter keep all on undefined" , UNDEFINED , KEEP_ALL , UNDEFINED }, + { "filter keep value on value" , JSON_VALUE, KEEP_VALUE , JSON_VALUE }, + { "filter keep value on null" , JSON_NULL , KEEP_VALUE , UNDEFINED }, + { "filter keep value on undefined" , UNDEFINED , KEEP_VALUE , UNDEFINED }, + { "filter keep null on value" , JSON_VALUE, KEEP_NULL , UNDEFINED }, + { "filter keep null on null" , JSON_NULL , KEEP_NULL , JSON_NULL }, + { "filter keep null on undefined" , UNDEFINED , KEEP_NULL , UNDEFINED }, + { "filter remove on value" , JSON_VALUE, KEEP_NONE , UNDEFINED }, + { "filter remove on null" , JSON_NULL , KEEP_NONE , UNDEFINED }, + { "filter remove on undefined" , UNDEFINED , KEEP_NONE , UNDEFINED }, + { "filter null predicate on value" , JSON_VALUE, NULL_PREDICATE, NPE }, + { "filter null predicate on null" , JSON_NULL , NULL_PREDICATE, NPE }, + { "filter null predicate on undefined", UNDEFINED , NULL_PREDICATE, NPE }, + }).stream(); + } + + @SuppressWarnings("unchecked") + @Override + public Callable createCall(Object[] row) { + return biFunctionCall( + JsonNullable::filter, + asJson(row[1]), + (Predicate)row[2] + ); + } + + @Override + public Callable createEquivalentCall(Object[] row) { + return biFunctionCall( + OPTIONAL.forName("filter") + .>>argument(Predicate.class) + .>>getBiFunction() + .map(f->f.andThen(StreamingApiTest::optionalToNullable)) + .orElse(null), + nullableToOptional(row[1]), + nullableValueToOptionalValue(asPredicate(row[2])) + ); + } + } + + @Nested + @DisplayName("Streaming API Tests flatMap(Function)") + public class FlatMapTest extends BaseTest { + public Stream baseArgs() { + Function> ANY_TO_UNDEFINED = value->UNDEFINED; + Function> NULL_TO_UNDEFINED = value->value!=null?of(value):UNDEFINED; + Function> ANY_TO_OTHER = value->JSON_OTHER; + Function> NULL_MAPPING = null; + Function> NULL_RESULT = value->null; + return Arrays.asList(new Object[][] { + { "flatMap any to undefined on value" , JSON_VALUE, ANY_TO_UNDEFINED , UNDEFINED }, + { "flatMap any to undefined on null" , JSON_NULL , ANY_TO_UNDEFINED , UNDEFINED }, + { "flatMap any to undefined on undefined" , UNDEFINED , ANY_TO_UNDEFINED , UNDEFINED }, + { "flatMap null to undefined on value" , JSON_VALUE, NULL_TO_UNDEFINED, JSON_VALUE }, + { "flatMap null to undefined on null" , JSON_NULL , NULL_TO_UNDEFINED, UNDEFINED }, + { "flatMap null to undefined on undefined", UNDEFINED , NULL_TO_UNDEFINED, UNDEFINED }, + { "flatMap any to other on value" , JSON_VALUE, ANY_TO_OTHER , JSON_OTHER }, + { "flatMap any to other on null" , JSON_NULL , ANY_TO_OTHER , JSON_OTHER }, + { "flatMap any to other on undefined" , UNDEFINED , ANY_TO_OTHER , UNDEFINED }, + { "flatMap null mapping on value" , JSON_VALUE, NULL_MAPPING , NPE }, + { "flatMap null mapping on null" , JSON_NULL , NULL_MAPPING , NPE }, + { "flatMap null mapping on undefined" , UNDEFINED , NULL_MAPPING , NPE }, + { "flatMap null result on value" , JSON_VALUE, NULL_RESULT , NPE }, + { "flatMap null result on null" , JSON_NULL , NULL_RESULT , NPE }, + { "flatMap null result on undefined" , UNDEFINED , NULL_RESULT , UNDEFINED }, + }).stream(); + } + + @SuppressWarnings("unchecked") + @Override + public Stream args() { + return baseArgs() + .map(args->new Object[] { args[0], biFunctionCall(JsonNullable::flatMap, asJson(args[1]), (Function>)args[2]), createMatcher(args[3])}) + .map(args->Arguments.argumentSet((String)args[0], args)); + } + + @SuppressWarnings("unchecked") + @Override + public Callable createCall(Object[] row) { + return biFunctionCall( + JsonNullable::flatMap, + asJson(row[1]), + (Function>)row[2] + ); + } + + @Override + public Callable createEquivalentCall(Object[] row) { + return biFunctionCall( + OPTIONAL.forName("flatMap") + ., Optional>>>argument(Function.class) + .>>getBiFunction() + .map(f->f.andThen(StreamingApiTest::optionalToNullable)) + .orElse(null), + nullableToOptional(row[1]), + Optional.ofNullable(asFunction(row[2])) + .map(f->f + .compose(StreamingApiTest::optionalValueToNullableValue) + .andThen(StreamingApiTest::nullableToOptional)) + .orElse(null) + ); + } + } + + @Nested + @DisplayName("Streaming API Tests isUndefined()") + public class IsUndefinedTest extends BaseTest { + public Stream baseArgs() { + return Arrays.asList(new Object[][] { + { "isUndefined on value" , JSON_VALUE, false }, + { "isUndefined on null" , JSON_NULL , false }, + { "isUndefined on undefined", UNDEFINED , true }, + }).stream(); + } + + @Override + public Callable createCall(Object[] row) { + return functionCall( + JsonNullable::isUndefined, + asJson(row[1]) + ); + } + + @Override + public Callable createEquivalentCall(Object[] row) { + return functionCall( + OPTIONAL.forName("isEmpty") + .getFunction() + .orElse(null), + nullableToOptional(row[1]) + ); + } + } + + @Nested + @DisplayName("Streaming API Tests stream()") + public class StreamTest extends BaseTest { + public Stream baseArgs() { + return Arrays.asList(new Object[][] { + { "stream on value" , JSON_VALUE, new Object[] {VALUE} }, + { "stream on null" , JSON_NULL , new Object[] {NULL} }, + { "stream on undefined", UNDEFINED , new Object[] {} } + }).stream(); + } + + @Override + public Callable createCall(Object[] row) { + return functionCall( + ((Function, Stream>)JsonNullable::stream) + .andThen(s->s.collect(Collectors.toList())), + asJson(row[1]) + ); + } + + @Override + public Callable createEquivalentCall(Object[] row) { + return functionCall( + OPTIONAL.forName("stream") + .>>getFunction() + .map(f->f + .andThen(s->s.map(StreamingApiTest::optionalValueToNullableValue)) + .andThen(s->s.collect(Collectors.toList()))) + .orElse(null), + nullableToOptional(row[1]) + ); + } + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + @DisplayName("Streaming API Tests ifPresentOrElse(Consumer, Runnable)") + public class IfPresentOrElseTest { + + public Stream baseArgs() { + TestConsumer ACTION = (value)->{}; + TestConsumer NULL_ACTION = null; + Runnable UNDEFINED_ACTION = ()->{}; + Runnable NULL_UNDEFINED_ACTION = null; + Function, Runnable>> ACTION_CALLED = (value)->(c, r)->{verify(c).accept(value);}; + BiConsumer, Runnable> UNDEFINED_ACTION_CALLED = (c, r)->{verify(r).run();}; + BiConsumer, Runnable> NOTHING_CALLED = (c, r)->{}; + return Arrays.asList(new Object[][] { + { "ifPresentOrElse on value with action and undefined action" , JSON_VALUE, ACTION , UNDEFINED_ACTION , null, ACTION_CALLED.apply(VALUE) }, + { "ifPresentOrElse on null with action and undefined action" , JSON_NULL , ACTION , UNDEFINED_ACTION , null, ACTION_CALLED.apply(NULL) }, + { "ifPresentOrElse on undefined with action and undefined action" , UNDEFINED , ACTION , UNDEFINED_ACTION , null, UNDEFINED_ACTION_CALLED }, + { "ifPresentOrElse on value with null action and undefined action" , JSON_VALUE, NULL_ACTION, UNDEFINED_ACTION , NPE , NOTHING_CALLED }, + { "ifPresentOrElse on null with null action and undefined action" , JSON_NULL , NULL_ACTION, UNDEFINED_ACTION , NPE , NOTHING_CALLED }, + { "ifPresentOrElse on undefined with null action and undefined action" , UNDEFINED , NULL_ACTION, UNDEFINED_ACTION , null, UNDEFINED_ACTION_CALLED }, + { "ifPresentOrElse on value with action and null undefined action" , JSON_VALUE, ACTION , NULL_UNDEFINED_ACTION, null, ACTION_CALLED.apply(VALUE) }, + { "ifPresentOrElse on null with action and null undefined action" , JSON_NULL , ACTION , NULL_UNDEFINED_ACTION, null, ACTION_CALLED.apply(NULL) }, + { "ifPresentOrElse on undefined with action and null undefined action" , UNDEFINED , ACTION , NULL_UNDEFINED_ACTION, NPE , NOTHING_CALLED }, + { "ifPresentOrElse on value with null action and null undefined action" , JSON_VALUE, NULL_ACTION, NULL_UNDEFINED_ACTION, NPE , NOTHING_CALLED }, + { "ifPresentOrElse on null with null action and null undefined action" , JSON_NULL , NULL_ACTION, NULL_UNDEFINED_ACTION, NPE , NOTHING_CALLED }, + { "ifPresentOrElse on undefined null with action and null undefined action", UNDEFINED , NULL_ACTION, NULL_UNDEFINED_ACTION, NPE , NOTHING_CALLED }, + }).stream(); + } + + public Stream args() { + return baseArgs() + .map(args->new Object[] { + args[0], + args[1], + args[2] != null ? mock(TestConsumer.class, AdditionalAnswers.delegatesTo((asConsumer(args[2])))) : null, + args[3] != null ? mock(Runnable.class, AdditionalAnswers.delegatesTo((Runnable)args[3])) : null, + args[4], + args[5] + }) + .map(args->new Object[] { + args[0], + triConsumerCall(JsonNullable::ifPresentOrElse, asJson(args[1]), asConsumer(args[2]), (Runnable)args[3]), + createMatcher(args[4]), + new Runnable() { + public void run() { + asBiConsumer(args[5]).accept(asConsumer(args[2]), (Runnable)args[3]); + if ( args[2] != null ) verifyNoMoreInteractions(args[2]); + if ( args[3] != null ) verifyNoMoreInteractions(args[3]); + } + } + }) + .map(args->Arguments.argumentSet((String)args[0], args)); + } + + public Stream equivalentArgs() { + return baseArgs() + .map(args->new Object[] { + args[0], + args[1], + args[2] != null ? mock(TestConsumer.class, AdditionalAnswers.delegatesTo((asConsumer(args[2])))) : null, + args[3] != null ? mock(Runnable.class, AdditionalAnswers.delegatesTo((Runnable)args[3])) : null, + args[4], + args[5] + }) + .map(args->new Object[] { + args[0], + triConsumerCall( + OPTIONAL.forName("ifPresentOrElse") + .>>argument(Consumer.class) + .argument(Runnable.class) + .getTriConsumer() + .orElse(null), + nullableToOptional(args[1]), + (TestConsumer>)(value)->(asConsumer(args[2])).accept(optionalValueToNullableValue(value)), + (Runnable)args[3] + ), + createMatcher(args[4]), + new Runnable() { + public void run() { + asBiConsumer(args[5]).accept(asConsumer(args[2]), (Runnable)args[3]); + if ( args[2] != null ) verifyNoMoreInteractions(args[2]); + if ( args[3] != null ) verifyNoMoreInteractions(args[3]); + } + } + }) + .map(args->Arguments.argumentSet("equivalent "+(String)args[0], args)); + } + + @ParameterizedTest + @MethodSource("args") + public void standardCall(String name, Callable> callable, Matcher matcher, Runnable verify) { + Assumptions.assumeTrue(callable!=null); + Object actual = null; + try { + actual = callable.call(); + } catch (Throwable t) { + assertThat(t, matcher); + verify.run(); + return; + } + assertThat(actual, matcher); + verify.run(); + } + + @ParameterizedTest + @MethodSource("equivalentArgs") + public void equivalentCall(String name, Callable>> callable, Matcher matcher, Runnable verify) { + Assumptions.assumeTrue(callable!=null); + Object actual = null; + try { + actual = callable.call(); + } catch (Throwable t) { + assertThat(t, matcher); + verify.run(); + return; + } + assertThat(actual, matcher); + verify.run(); + } + } + + // + // These static methods form a two-way coorespondance between JsonNullable and + // Optional> + // + static Optional> nullableToOptional(JsonNullable value) { + if ( value.isUndefined() ) { + return Optional.empty(); + } else { + return Optional.of(nullableValueToOptionalValue(value.get())); + } + } + + @SuppressWarnings("unchecked") + static Optional> nullableToOptional(Object value) { + if( value != null && !(value instanceof JsonNullable) ) throw new IllegalArgumentException(); + return nullableToOptional((JsonNullable)value); + } + + static Optional nullableValueToOptionalValue(String value) { + return Optional.ofNullable(value); + } + + static Optional nullableValueToOptionalValue(Object value) { + if( value != null && !(value instanceof String)) throw new IllegalArgumentException(); + return Optional.ofNullable((String)value); + } + + static JsonNullable optionalToNullable(Optional> value) { + if( !value.isPresent() ) { + return JsonNullable.undefined(); + } + return JsonNullable.of(optionalValueToNullableValue(value.get())); + } + + static String optionalValueToNullableValue( Optional value ) { + return value.orElse(null); + } + + public static TestPredicate> nullableValueToOptionalValue( TestPredicate test ) { + if( test == null ) return null; + return test.compose(StreamingApiTest::optionalValueToNullableValue); + } + + public static Function, Optional> nullableValueToOptionalValue(Function f) { + if( f == null ) return null; + return f + .compose(StreamingApiTest::optionalValueToNullableValue) + .andThen(StreamingApiTest::nullableValueToOptionalValue); + } + + public Supplier>> nullableSupplierToOptionalSupplier(Supplier> supplier) { + if( supplier == null ) return null; + return ()->{ + return nullableToOptional(supplier.get()); + }; + } + + public static Supplier> nullableValueSupplierToOptionalValueSupplier(Supplier supplier) { + if( supplier == null ) return null; + return ()->{ + return nullableValueToOptionalValue(supplier.get()); + }; + } + + // + // This service provides reflective method calls using functional interfaces. + // In the case that a method is not defined in the current JDK version, + // Optional.empty() is returned. + // + public static class ReflectiveService { + Class clazz; + public ReflectiveService(Class clazz) { + this.clazz = clazz; + } + + public Optional getMethod(String methodName, Class... parameterTypes) { + try { + return Optional.of(clazz.getMethod(methodName, parameterTypes)); + } catch ( Exception e ) { + return Optional.empty(); + } + } + + public NoArguments forName(String name) { + return new NoArguments(name); + } + + public class NoArguments { + String methodName; + public NoArguments(String methodName) { + this.methodName = methodName; + } + @SuppressWarnings("unchecked") + public Optional> getFunction() { + return getMethod(methodName) + .map(method->{ + return optional->{ + try { + return (R)method.invoke(optional); + } catch ( InvocationTargetException ite ) { + if( ite.getCause() instanceof RuntimeException ) { + throw (RuntimeException)ite.getCause(); + } else if ( ite.getCause() instanceof Error ) { + throw (Error)ite.getCause(); + } else { + throw new RuntimeException(ite.getCause()); + } + } + catch ( IllegalAccessException iae ) { + throw new RuntimeException(iae); + } + }; + }); + } + + public OneArgument argument(Class firstType) { + return new OneArgument(firstType); + } + + public class OneArgument { + private Class firstType; + + public OneArgument(Class firstType) { + this.firstType = firstType; + } + + public OneArgumentWithThrows throwing(Class thrownType) { + return new OneArgumentWithThrows(firstType); + } + + public TwoArguments argument(Class secondType) { + return new TwoArguments(firstType, secondType); + } + + @SuppressWarnings("unchecked") + public Optional> getBiFunction() { + return getMethod(methodName, firstType) + .map(method->{ + return (optional, argument)->{ + try { + return (R)method.invoke(optional, argument); + } catch ( InvocationTargetException ite ) { + if( ite.getCause() instanceof RuntimeException ) { + throw (RuntimeException)ite.getCause(); + } else if ( ite.getCause() instanceof Error ) { + throw (Error)ite.getCause(); + } else { + throw new RuntimeException(ite.getCause()); + } + } + catch ( IllegalAccessException iae ) { + throw new RuntimeException(iae); + } + }; + }); + } + } + + public class OneArgumentWithThrows { + private Class firstType; + + public OneArgumentWithThrows(Class firstType) { + this.firstType = firstType; + } + + @SuppressWarnings("unchecked") + public Optional> getBiFunctionWithThrows() { + return getMethod(methodName, firstType) + .map(method->{ + return (optional, argument)->{ + try { + return (R)method.invoke(optional, argument); + } catch ( InvocationTargetException ite ) { + if( ite.getCause() instanceof RuntimeException ) { + throw (RuntimeException)ite.getCause(); + } else if ( ite.getCause() instanceof Error ) { + throw (Error)ite.getCause(); + } else if ( ite.getCause() instanceof Exception ) { + throw (E)ite.getCause(); + } else { + throw new RuntimeException(ite.getCause()); + } + } + catch ( IllegalAccessException iae ) { + throw new RuntimeException(iae); + } + }; + }); + } + } + + public class TwoArguments { + private Class firstType; + private Class secondType; + + public TwoArguments( Class firstType, Class secondType) { + this.firstType = firstType; + this.secondType = secondType; + } + public Optional> getTriConsumer() { + return getMethod(methodName, firstType, secondType) + .map(method->{ + return (optional, argumentA, argumentB)->{ + try { + method.invoke(optional, argumentA, argumentB); + } catch ( InvocationTargetException ite ) { + if( ite.getCause() instanceof RuntimeException ) { + throw (RuntimeException)ite.getCause(); + } else if ( ite.getCause() instanceof Error ) { + throw (Error)ite.getCause(); + } else { + throw new RuntimeException(ite.getCause()); + } + } + catch ( IllegalAccessException iae ) { + throw new RuntimeException(iae); + } + }; + }); + } + } + } + } + + /* + * Methods that aid in casting objects + */ + @SuppressWarnings("unchecked") + public static JsonNullable asJson(Object value) { + return (JsonNullable)value; + } + + @SuppressWarnings("unchecked") + public static TestConsumer asConsumer(Object o) { + return (TestConsumer)o; + } + + @SuppressWarnings("unchecked") + public static BiConsumer asBiConsumer(Object o) { + return (BiConsumer)o; + } + + @SuppressWarnings("unchecked") + public static Function asFunction(Object o) { + return (Function)o; + } + + @SuppressWarnings("unchecked") + public static TestSupplier asSupplier(Object o) { + return (TestSupplier)o; + } + + @SuppressWarnings("unchecked") + public static TestPredicate asPredicate(Object o) { + return (TestPredicate)o; + } + + @SuppressWarnings("unchecked") + public static TestSupplier asExceptionSupplier(Object value) { + return (TestSupplier)value; + } + + /* + * These functions create calls for different method signatures. + */ + public static Callable functionCall(Function f, A a) { + if(f == null) return null; + return ()->{ + return f.apply((A)a); + }; + } + + public static Callable biFunctionCall(BiFunction f, A a, B b) { + if(f == null) return null; + return ()->{ + return f.apply((A)a, (B)b); + }; + } + + public static Callable biFunctionWithThrowsCall(BiFunctionWithThrows f, A a, B b) { + if(f == null) return null; + return ()->{ + try { + return f.apply((A)a, (B)b); + } catch (RuntimeException | Error e) { + throw e; + } catch (Exception e) { + throw e; + } catch (Throwable t) { + throw new IllegalStateException(t); + } + }; + } + + public static Callable triConsumerCall(TriConsumer f, A a, B b, C c) { + if(f == null) return null; + return ()->{ + f.accept(a, b, c); + return null; + }; + } + + public static Matcher createMatcher(Object value) { + if ( value == null ) { + return nullValue(); + } + if ( value instanceof Class ) { + return instanceOf((Class)value); + } + if ( value.getClass().isArray() ) { + Object[] array = (Object[])value; + return array.length > 0 ? contains(array) : empty(); + } + return equalTo(value); + } + + public static TestSupplier supplier(T value) { + return ()->value; + } + + // + // Interfaces and classes created to aid testing. + // + public static interface TestPredicate extends Predicate { + public default TestPredicate compose( Function f ) { + return (value)->{ + return test(f.apply(value)); + }; + } + } + + public static interface TestSupplier extends Supplier { + public default TestSupplier andThen(Function mapping) { + return ()->{ + return mapping.apply(get()); + }; + } + } + + public static interface TestConsumer extends Consumer { + public default TestConsumer compose( Function f) { + return (value)->{ + accept(f.apply(value)); + }; + } + } + + public static interface TriConsumer { + public void accept(A a, B b, C c); + } + + public static interface BiFunctionWithThrows { + public R apply(T t, U u) + throws X; + + public default BiFunctionWithThrows andThen(Function f) { + return (T t, U u)->{ + return f.apply(apply(t, u)); + }; + } + } + + public static class TestException extends Exception {} +}