diff --git a/.gitignore b/.gitignore index f83e8cf..6943b56 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ .idea target *.iml +/.gradle/ +/build/ diff --git a/README.md b/README.md index 8d838af..c029c20 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,8 @@ -# co.unruly.control +# AJ's control + +This repo is for studying and documenting behavior, as well as updating and changing a few various parts + +## co.unruly.control [![Build Status](https://travis-ci.org/unruly/control.svg?branch=master)](https://travis-ci.org/unruly/control) [![Release Version](https://img.shields.io/maven-central/v/co.unruly/control.svg)](https://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22co.unruly%22%20AND%20a%3A%22control%22) @@ -185,8 +189,12 @@ and failure types, and both functions must return the same type. `Result.then` takes a function from a `Result` to a value, and then applies that function to the result. The following lines of code are (other than generics inference issues) equivalent: ```java -ifFailed(x -> "Hello World").apply(result); -result.then(ifFailed(x -> "Hello World")); +class example { + example() { + ifFailed(x -> "Hello World").apply(result); + result.then(ifFailed(x -> "Hello World")); + } +} ``` By structuring the API like this, instead of having a fixed set of methods available, we can @@ -224,10 +232,14 @@ Piper pipe = Piper.pipe(42); You can then chain functions on the pipe: ```java -Piper.pipe(42) // yields a Pipe containing 42 - .then(x -> x + 10) // yields a Pipe containing 52 - .then(x -> x * 2) // yields a Pipe containing 104 - .resolve() // returns 104 +class example { + example() { + Piper.pipe(42) // yields a Pipe containing 42 + .then(x -> x + 10) // yields a Pipe containing 52 + .then(x -> x * 2) // yields a Pipe containing 104 + .resolve(); // returns 104 + } +} ``` This allows us to rewrite our failing-to-compile example from above as: diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..29ab009 --- /dev/null +++ b/build.gradle @@ -0,0 +1,46 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * This project uses @Incubating APIs which are subject to change. + */ + +plugins { + id 'java-library' + id 'maven-publish' +} + +repositories { + mavenLocal() + maven { + url = uri('https://repo.maven.apache.org/maven2/') + } +} + +dependencies { + api 'org.hamcrest:java-hamcrest:2.0.0.0' + api 'org.hamcrest:hamcrest-junit:2.0.0.0' + api 'org.jetbrains:annotations:RELEASE' + implementation 'org.jetbrains:annotations:16.0.2' + testImplementation 'junit:junit:4.13.1' + testImplementation 'org.mockito:mockito-core:5.4.0' +} + +group = 'co.unruly' +version = '0.8.14-SNAPSHOT' +description = 'control' + +java { + withSourcesJar() + withJavadocJar() + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +tasks.withType(JavaCompile).configureEach { + options.encoding = 'UTF-8' +} + +tasks.withType(Javadoc).configureEach { + options.encoding = 'UTF-8' +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..7f93135 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..3fa8f86 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..1aa94a4 --- /dev/null +++ b/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# 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 +# +# https://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. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..93e3f59 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/pom.xml b/pom.xml deleted file mode 100644 index a6a5003..0000000 --- a/pom.xml +++ /dev/null @@ -1,178 +0,0 @@ - - 4.0.0 - - co.unruly - control - 0.8.14-SNAPSHOT - jar - - control - - A collection of functional utilities around error handling, wrapping a successfully returned value or - error - - - https://github.com/unruly/control - - - 1.8 - 1.8 - 1.8 - UTF-8 - -Xdoclint:none - - - - - Unruly Developers - oss@unrulymedia.com - - - - - - MIT License - http://www.opensource.org/licenses/mit-license.php - - - - - scm:git:git@github.com:unruly/${project.artifactId}.git - scm:git:git@github.com:unruly/${project.artifactId}.git - git@github.com:unruly/${project.artifactId}.git - HEAD - - - - - - - org.hamcrest - java-hamcrest - 2.0.0.0 - - - - - - junit - junit - 4.12 - test - - - org.mockito - mockito-core - 2.6.2 - test - - - org.hamcrest - hamcrest-junit - 2.0.0.0 - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.1 - - ${java.version} - ${java.version} - - - - org.apache.maven.plugins - maven-source-plugin - 2.2.1 - - - attach-sources - - jar-no-fork - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - 2.10.4 - - - attach-javadocs - - jar - - - - - - org.apache.maven.plugins - maven-release-plugin - 2.5 - - true - false - release - deploy - forked-path - - - - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.5 - true - - ossrh - https://oss.sonatype.org/ - true - - - - - - - - release - - - - org.apache.maven.plugins - maven-gpg-plugin - 1.6 - - - sign-artifacts - verify - - sign - - - false - gpg - - - - - - - - - - - - ossrh - https://oss.sonatype.org/content/repositories/snapshots - - - ossrh - https://oss.sonatype.org/service/local/staging/deploy/maven2/ - - - - diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..f6746b1 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,12 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * This project uses @Incubating APIs which are subject to change. + */ + +pluginManagement { + // Include 'plugins build' to define convention plugins. +// includeBuild('build-logic') +} + +rootProject.name = 'control' diff --git a/src/main/java/co/unruly/control/ErrorThrowingLambdas.java b/src/main/java/co/unruly/control/ErrorThrowingLambdas.java index c3536ff..bb5dc9e 100644 --- a/src/main/java/co/unruly/control/ErrorThrowingLambdas.java +++ b/src/main/java/co/unruly/control/ErrorThrowingLambdas.java @@ -1,5 +1,8 @@ package co.unruly.control; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Function; @@ -9,11 +12,12 @@ * A collection of functional interfaces which throw, and convenience functions to wrap them * so any thrown Throwables are converted to RuntimeExceptions so they can be used where * non-throwing functional interfaces are required - * + *

* Catching errors in the general case is not recommended, but there are specific errors * which are contextually reasonable to catch. Therefore, this wider capability exists * separately and should be used judiciously. */ +@SuppressWarnings("unused") public interface ErrorThrowingLambdas { /** @@ -21,21 +25,48 @@ public interface ErrorThrowingLambdas { */ @FunctionalInterface interface ThrowingFunction { + /** + * @param input input variable + * @return output of type O + * @throws X the exception type + */ O apply(I input) throws X; + /** + * @param nextFunction the next function to pass our input into + * @param outcome type + * @return dF(x -> y) where x is given to next function, + * and either T is return or an exception is thrown + */ default ThrowingFunction andThen(Function nextFunction) { return x -> nextFunction.apply(apply(x)); } - default ThrowingFunction compose(Function nextFunction) { + /** + * @param nextFunction next function to + * @param input type + * @return a function that can throw a runtime exception + * when provided with an input X, it will pass X into nextFunction, and then + * call the throwing function on the result + */ + default ThrowingFunction + compose(Function nextFunction) { return x -> apply(nextFunction.apply(x)); } /** * Converts the provided function into a regular Function, where any thrown exceptions are * wrapped in a RuntimeException. + * @param f function that throws + * @param input type + * @param output type + * @param exception type + * @return a function that expects an input X. X will be applied to f, and the result is + * wrapped in a try-catch block */ - static Function throwingRuntime(ThrowingFunction f) { + @Contract(pure = true) + static @NotNull Function + throwingRuntime(ThrowingFunction f) { return x -> { try { return f.apply(x); @@ -51,13 +82,24 @@ static Function throwingRuntime(ThrowingFuncti */ @FunctionalInterface interface ThrowingConsumer { + /** + * @param item the item to accept + * @throws X exception type + */ void accept(T item) throws X; /** * Converts the provided consumer into a regular Consumer, where any thrown exceptions are * wrapped in a RuntimeException. + * @param p consumer that throws + * @param input type + * @param exception type + * @return a function that expects an input X. X is passed to p, and the + * result is wrapped in a try-catch block */ - static Consumer throwingRuntime(ThrowingConsumer p) { + @Contract(pure = true) + static @NotNull Consumer + throwingRuntime(ThrowingConsumer p) { return x -> { try { p.accept(x); @@ -73,13 +115,27 @@ static Consumer throwingRuntime(ThrowingConsumer { + /** + * @param first left value + * @param second right value + * @return the result of working with first and second + * @throws X exception type + */ R apply(A first, B second) throws X; /** * Converts the provided bifunction into a regular BiFunction, where any thrown exceptions * are wrapped in a RuntimeException + * @param f the bifunction that might throw + * @param left type + * @param right type + * @param result type + * @param exception type + * @return a bifunction that applies the two inputs to f, wraps the result in a try-catch */ - static BiFunction throwingRuntime(ThrowingBiFunction f) { + @Contract(pure = true) + static @NotNull BiFunction + throwingRuntime(ThrowingBiFunction f) { return (a, b) -> { try { return f.apply(a, b); @@ -95,13 +151,26 @@ static BiFunction throwingRuntime(Throwi */ @FunctionalInterface interface ThrowingPredicate { + /** + * @param item input value + * @return T/f outcome of testing item + * @throws X exception type + */ boolean test(T item) throws X; + /** * Converts the provided predicate into a regular Predicate, where any thrown exceptions * are wrapped in a RuntimeException + * @param p predicate function that throws + * @param input type + * @param exception type + * @return function that takes an input, tests it against p, and throws a contained try/catch + * exception depending on the result */ - static Predicate throwingRuntime(ThrowingPredicate p) { + @Contract(pure = true) + static @NotNull Predicate + throwingRuntime(ThrowingPredicate p) { return x -> { try { return p.test(x); diff --git a/src/main/java/co/unruly/control/HigherOrderFunctions.java b/src/main/java/co/unruly/control/HigherOrderFunctions.java index 4e5cefd..33912a6 100644 --- a/src/main/java/co/unruly/control/HigherOrderFunctions.java +++ b/src/main/java/co/unruly/control/HigherOrderFunctions.java @@ -1,6 +1,8 @@ package co.unruly.control; import co.unruly.control.pair.Pair; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; import java.util.*; import java.util.function.BiFunction; @@ -14,19 +16,33 @@ import static java.util.stream.Collectors.toList; import static java.util.stream.Stream.iterate; +/** + * interfaces for working with higher level functions + */ +@SuppressWarnings("unused") public interface HigherOrderFunctions { /** * Takes a BiFunction, and reverses the order of the arguments + * @param f function to use + * @param left type + * @param right type + * @param result type + * @return dF where f(a, b) now becomes f(b, a) */ - static BiFunction flip(BiFunction f) { + @Contract(pure = true) + static @NotNull BiFunction flip(BiFunction f) { return (a, b) -> f.apply(b, a); } /** * Takes a list of functions (which take and return the same type) and composes * them into a single function, applying the provided functions in order + * @param functions an arbitrary number of T -> T functions + * @param function type + * @return a single function representing the composition of each composite function */ + @SafeVarargs static Function compose(Function... functions) { return compose(Stream.of(functions)); } @@ -34,46 +50,80 @@ static Function compose(Function... functions) { /** * Takes a Stream of functions (which take and return the same type) and composes * them into a single function, applying the provided functions in order + * @param functions functions to compose + * @param input type + * @return a single function that is the combination of each function */ - static Function compose(Stream> functions) { + static Function compose(@NotNull Stream> functions) { return functions.reduce(identity(), Function::andThen); } /** * Takes a list of predicates and composes them into a single predicate, which * passes when all passed-in predicates pass + * @param functions functions to compose + * @param input type + * @return predicate function that is the composition of all passed in predicates */ + @SafeVarargs static Predicate compose(Predicate... functions) { return Stream.of(functions).reduce(__ -> true, Predicate::and); } /** * Turns a Consumer into a Function which applies the consumer and returns the input + * @param action consumer to act + * @param input type + * @return dF (x -> x) where x is passed to action to execute */ - static Function peek(Consumer action) { + @Contract(pure = true) + static @NotNull Function peek(Consumer action) { return t -> { action.accept(t); return t; }; } - static Stream> withIndices(Stream items) { + /** + * @param items item stream + * @param stream type + * @return a stream of KV pairs where k is the index and T is the value + */ + static @NotNull Stream> + withIndices(Stream items) { return zip(iterate(0, x -> x + 1), items); } - static Stream> zip(Stream a, Stream b) { + /** + * @param a left stream + * @param b right stream + * @param left type + * @param right type + * @return zips the two streams into a pair + */ + static @NotNull Stream> + zip(Stream a, Stream b) { return zip(a, b, Pair::of); } /** * Zips two streams together using the zipper function, resulting in a single stream of * items from each stream combined using the provided function. - * + *

* The resultant stream will have the length of the shorter of the two input streams. - * - * Sourced from https://stackoverflow.com/questions/17640754/zipping-streams-using-jdk8-with-lambda-java-util-stream-streams-zip + *

+ * Sourced from + * here + * @param a left stream + * @param b right stream + * @param zipper zipping function + * @param left type + * @param right type + * @param result type + * @return a stream representing the merging of streams a and b */ - static Stream zip(Stream a, Stream b, BiFunction zipper) { + static @NotNull Stream + zip(Stream a, Stream b, BiFunction zipper) { Objects.requireNonNull(zipper); Spliterator aSpliterator = Objects.requireNonNull(a).spliterator(); Spliterator bSpliterator = Objects.requireNonNull(b).spliterator(); @@ -88,7 +138,7 @@ static Stream> zip(Stream a, Stream b) { Iterator aIterator = Spliterators.iterator(aSpliterator); Iterator bIterator = Spliterators.iterator(bSpliterator); - Iterator cIterator = new Iterator() { + Iterator cIterator = new Iterator<>() { @Override public boolean hasNext() { return aIterator.hasNext() && bIterator.hasNext(); @@ -108,13 +158,23 @@ public C next() { /** * Takes two lists, and returns a list of pairs forming the Cartesian product of those lists. + * @param as left list + * @param bs right list + * @param left type + * @param right type + * @return list of pairs in the form L[P(A, B)...] */ - static List> pairs(List as, List bs) { + static List> + pairs(@NotNull List as, List bs) { return as.stream().flatMap(a -> bs.stream().map(b -> Pair.of(a, b))).collect(toList()); } /** * Takes a value, and returns that same value, upcast to a suitable type. Inference is our friend here. + * @param fv value to upcast + * @param type to upcast to + * @param value type + * @return fv upcasted from T to R */ static R upcast(T fv) { return fv; diff --git a/src/main/java/co/unruly/control/Lazy.java b/src/main/java/co/unruly/control/Lazy.java index ae9580e..750ee26 100644 --- a/src/main/java/co/unruly/control/Lazy.java +++ b/src/main/java/co/unruly/control/Lazy.java @@ -8,15 +8,22 @@ * Will only calculate the value once. * @param the type of object we're lazily instantiating */ +@SuppressWarnings({"unused", "optional", "OptionalUsedAsFieldOrParameterType"}) public final class Lazy { private final Supplier source; private Optional value = Optional.empty(); + /** + * @param source . + */ public Lazy(Supplier source) { this.source = source; } + /** + * @return value, calculates it if it hasn't been calculated yet + */ public synchronized T get() { return value.orElseGet(this::calculateAndStore); } diff --git a/src/main/java/co/unruly/control/Lists.java b/src/main/java/co/unruly/control/Lists.java index 9773fa3..ffde24f 100644 --- a/src/main/java/co/unruly/control/Lists.java +++ b/src/main/java/co/unruly/control/Lists.java @@ -2,19 +2,29 @@ import co.unruly.control.pair.Pair; import co.unruly.control.result.Result; +import org.jetbrains.annotations.NotNull; import java.util.List; import static co.unruly.control.result.Resolvers.split; +/** + * interface to allow us to work with results and lists + */ public interface Lists { - static Result, List> successesOrFailures(List> results) { + /** + * @param results a list of result types + * @param success type + * @param fail type + * @return a result with success as a list of the successes or a list of failures as the failures + */ + static Result, List> successesOrFailures(@NotNull List> results) { Pair, List> successesAndFailures = results.stream().collect(split()); - if(successesAndFailures.right.isEmpty()) { - return Result.success(successesAndFailures.left); + if(successesAndFailures.right().isEmpty()) { + return Result.success(successesAndFailures.left()); } else { - return Result.failure(successesAndFailures.right); + return Result.failure(successesAndFailures.right()); } } } diff --git a/src/main/java/co/unruly/control/Optionals.java b/src/main/java/co/unruly/control/Optionals.java index 0daff78..1d41a5b 100644 --- a/src/main/java/co/unruly/control/Optionals.java +++ b/src/main/java/co/unruly/control/Optionals.java @@ -1,20 +1,29 @@ package co.unruly.control; +import org.jetbrains.annotations.NotNull; + import java.util.Optional; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Stream; +/** + * Interface for working with optionals + */ +@SuppressWarnings({"unused", "optional", "OptionalUsedAsFieldOrParameterType"}) public interface Optionals { /** * Converts an Optional to a Stream. * If the Optional was present, you get a Stream of one item. * If it was absent, you get an empty stream. - * - * Flatmapping a Stream of Optionals over this method will return a Stream of the + *

+ * Flat mapping a Stream of Optionals over this method will return a Stream of the * contents of the present Optionals in the input stream. + * @param item optional item + * @param item type + * @return stream of T's or empty's */ static Stream stream(Optional item) { return either(item, Stream::of, Stream::empty); @@ -23,27 +32,39 @@ static Stream stream(Optional item) { /** * If the provided optional is present, applies the first function to the wrapped value and returns it. * Otherwise, returns the value supplied by the second function + * @param optional optional value + * @param onPresent action on present + * @param onAbsent action on absence + * @param optional type + * @param result type + * @return result type from either onPresent on onAbsent */ - static R either(Optional optional, Function onPresent, Supplier onAbsent) { + static R either(@NotNull Optional optional, Function onPresent, Supplier onAbsent) { return optional.map(onPresent).orElseGet(onAbsent); } /** * If the provided Optional is present, pass the wrapped value to the provided consumer - * + *

* This simply invokes Optional.ifPresent(), and exists for cases where side-effects are required * in both the present and absent cases on an Optional. The Optional API doesn't cover the latter case, * so this provides a mimicking calling convention to permit consistency. + * @param optional optional value + * @param consume consuming function + * @param value type */ - static void ifPresent(Optional optional, Consumer consume) { + static void ifPresent(@NotNull Optional optional, Consumer consume) { optional.ifPresent(consume); } /** * If the provided Optional is empty, invoke the provided Runnable + * @param optional optional value + * @param action absent action + * @param value type */ - static void ifAbsent(Optional optional, Runnable action) { - if(!optional.isPresent()) { + static void ifAbsent(@NotNull Optional optional, Runnable action) { + if(optional.isEmpty()) { action.run(); } } diff --git a/src/main/java/co/unruly/control/PartialApplication.java b/src/main/java/co/unruly/control/PartialApplication.java index b880760..de9cd03 100644 --- a/src/main/java/co/unruly/control/PartialApplication.java +++ b/src/main/java/co/unruly/control/PartialApplication.java @@ -1,6 +1,8 @@ package co.unruly.control; import co.unruly.control.pair.Triple.TriFunction; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; import java.util.function.BiFunction; import java.util.function.Function; @@ -10,50 +12,94 @@ * A collection of functions to partially apply arguments to functions, to simplify usage * in streams, optionals etc. */ +@SuppressWarnings("unused") public interface PartialApplication { /** * Binds the provided argument to the function, and returns a Supplier with that argument applied. - * + *

* bind(f, a) is equivalent to () -> f.apply(a) + * @param f binding function + * @param input to bind to f + * @param input type + * @param output type + * @return supplier of O */ - static Supplier bind(Function f, I input) { + @Contract(pure = true) + static @NotNull Supplier + bind(Function f, I input) { return () -> f.apply(input); } /** * Binds the provided argument to the function, and returns a new Function with that argument already applied. - * + *

* bind(f, a) is equivalent to b -> f.apply(a, b) + * @param f binding function + * @param firstParam to bind + * @param left type + * @param right type + * @param result type + * @return function that asks for B, and gives R */ - static Function bind(BiFunction f, A firstParam) { + @Contract(pure = true) + static @NotNull Function + bind(BiFunction f, A firstParam) { return secondParam -> f.apply(firstParam, secondParam); } /** * Binds the provided arguments to the function, and returns a new Supplier with those arguments already applied. - * + *

* bind(f, a, b) is equivalent to () -> f.apply(a, b) + * @param f binding function + * @param firstParam to bind + * @param secondParam to bind + * @param left type + * @param right type + * @param result type + * @return supplier of R */ - static Supplier bind(BiFunction f, A firstParam, B secondParam) { + @Contract(pure = true) + static @NotNull Supplier + bind(BiFunction f, A firstParam, B secondParam) { return () -> f.apply(firstParam, secondParam); } /** * Binds the provided argument to the function, and returns a new BiFunction with that argument already applied. - * + *

* bind(f, a) is equivalent to (b, c) -> f.apply(a, b, c) + * @param f function to bind + * @param firstParam to bind + * @param firstParam type + * @param left type of closure + * @param right type of closure + * @param result type + * @return bifunction that, when executed, provides R */ - static BiFunction bind(TriFunction f, A firstParam) { + @Contract(pure = true) + static @NotNull BiFunction + bind(TriFunction f, A firstParam) { return (secondParam, thirdParam) -> f.apply(firstParam, secondParam, thirdParam); } /** * Binds the provided arguments to the function, and returns a new Function with those arguments already applied. - * + *

* bind(f, a, b) is equivalent to c -> f.apply(a, b, c) + * @param f binding function + * @param firstParam to bind + * @param secondParam to bind + * @param firstParam type + * @param secondParam type + * @param final type argument + * @param result type + * @return function that asks for third argument, returns R */ - static Function bind(TriFunction f, A firstParam, B secondParam) { + @Contract(pure = true) + static @NotNull Function + bind(TriFunction f, A firstParam, B secondParam) { return thirdParam -> f.apply(firstParam, secondParam, thirdParam); } } diff --git a/src/main/java/co/unruly/control/Piper.java b/src/main/java/co/unruly/control/Piper.java index e49f9fc..fd9f000 100644 --- a/src/main/java/co/unruly/control/Piper.java +++ b/src/main/java/co/unruly/control/Piper.java @@ -1,34 +1,46 @@ package co.unruly.control; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + import java.util.function.Consumer; import java.util.function.Function; /** * Wraps a value in a Piper, allowing chaining of operations and segueing into other applicable types * (such as Result) cleanly. - * + *

* This is most useful when not streaming over values, starting off with a value which is not a Result. * * @param the type of wrapped value */ +@SuppressWarnings("unused") public class Piper { private final T element; + /** + * @param element . + */ public Piper(T element) { this.element = element; } /** * Applies the function to the piped value, returning a new pipe containing that value. + * @param function piping function + * @param result type + * @return new pipe segment of R */ - public Piper then(Function function) { + public Piper then(@NotNull Function function) { return new Piper<>(function.apply(element)); } /** * Applies the consumer to the current value of the piped value, returning a pipe containing * that value. + * @param consumer consuming function + * @return pipe containing the value */ public Piper peek(Consumer consumer) { return then(HigherOrderFunctions.peek(consumer)); @@ -36,6 +48,7 @@ public Piper peek(Consumer consumer) { /** * Returns the final result of the piped value, with all the piped functions applied. + * @return T */ public T resolve() { return element; @@ -43,16 +56,23 @@ public T resolve() { /** * Returns the final result of the piped value, with all the piped functions applied. + * @param f piping function + * @param result type + * @return R */ - public R resolveWith(Function f) { + public R resolveWith(@NotNull Function f) { return f.apply(element); } /** * Creates a new Piper wrapping the provided element. + * @param element value + * @param value type + * @return a piper around T */ - public static Piper pipe(T element) { - return new Piper(element); + @Contract(value = "_ -> new", pure = true) + public static @NotNull Piper pipe(T element) { + return new Piper<>(element); } } diff --git a/src/main/java/co/unruly/control/Predicates.java b/src/main/java/co/unruly/control/Predicates.java index 32a96b4..4c81ff5 100644 --- a/src/main/java/co/unruly/control/Predicates.java +++ b/src/main/java/co/unruly/control/Predicates.java @@ -1,18 +1,26 @@ package co.unruly.control; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + import java.util.function.Predicate; /** * Created by tomj on 31/03/2017. */ +@SuppressWarnings("unused") public interface Predicates { /** * Negates a predicate: mostly useful when our predicate is a method reference or lambda where we can't * call negate() on it directly, or where the code reads better by having the negation at the beginning * rather than the end. + * @param test predicate function + * @param input type + * @return reverse of the predicate function (negation) */ - static Predicate not(Predicate test) { + @Contract(pure = true) + static @NotNull Predicate not(@NotNull Predicate test) { return test.negate(); } } diff --git a/src/main/java/co/unruly/control/ThrowingLambdas.java b/src/main/java/co/unruly/control/ThrowingLambdas.java index 71c1f3b..380a38d 100644 --- a/src/main/java/co/unruly/control/ThrowingLambdas.java +++ b/src/main/java/co/unruly/control/ThrowingLambdas.java @@ -1,5 +1,8 @@ package co.unruly.control; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Function; @@ -10,19 +13,38 @@ * so any thrown exceptions are converted to RuntimeExceptions so they can be used where * non-throwing functional interfaces are required */ +@SuppressWarnings("ALL") public interface ThrowingLambdas { /** * A Function which may throw a checked exception */ + @SuppressWarnings("unused") @FunctionalInterface interface ThrowingFunction { + /** + * @param input value + * @return O + * @throws X exception type + */ O apply(I input) throws X; + /** + * @param nextFunction to use in pipe chain + * @param output type + * @return dF(x -> y) where x is piped into outer throwing function + * and then passed to the next function and y is T or it throws + */ default ThrowingFunction andThen(Function nextFunction) { return x -> nextFunction.apply(apply(x)); } + /** + * @param nextFunction to use in pipe chain + * @param input type + * @return dF(x -> y) where x is piped into the next function + * and y is O or it throws + */ default ThrowingFunction compose(Function nextFunction) { return x -> apply(nextFunction.apply(x)); } @@ -30,8 +52,16 @@ default ThrowingFunction compose(Function nextFunction) { /** * Converts the provided function into a regular Function, where any thrown exceptions are * wrapped in a RuntimeException. + * @param f I/O function that may throw X + * @param input type + * @param output type + * @param exception type + * @return dF(x -> y) where f is passed x and wrapped in a try-catch that + * throws a runtime */ - static Function throwingRuntime(ThrowingFunction f) { + @Contract(pure = true) + static @NotNull Function + throwingRuntime(ThrowingFunction f) { return x -> { try { return f.apply(x); @@ -45,15 +75,27 @@ static Function throwingRuntime(ThrowingFuncti /** * A Consumer which may throw a checked exception */ + @SuppressWarnings("unused") @FunctionalInterface interface ThrowingConsumer { + /** + * @param item value + * @throws X exception type + */ void accept(T item) throws X; /** * Converts the provided consumer into a regular Consumer, where any thrown exceptions are * wrapped in a RuntimeException. + * @param p consumer that throws + * @param input type + * @param exception type + * @return dF(x -> y) where x is passed to p and p + * is wrapped in a try catch that throws a runtime exception */ - static Consumer throwingRuntime(ThrowingConsumer p) { + @Contract(pure = true) + static @NotNull Consumer + throwingRuntime(ThrowingConsumer p) { return x -> { try { p.accept(x); @@ -66,16 +108,36 @@ static Consumer throwingRuntime(ThrowingConsumer left type + * @param right type + * @param result type + * @param exception type */ + @SuppressWarnings("unused") @FunctionalInterface interface ThrowingBiFunction { + /** + * @param first . + * @param second . + * @return R + * @throws X exception type + */ R apply(A first, B second) throws X; /** * Converts the provided bifunction into a regular BiFunction, where any thrown exceptions * are wrapped in a RuntimeException + * @param f bifunction that may throw + * @param left type + * @param right type + * @param result type + * @param exception type + * @return dF((a, b) -> y) where a and b are passed to f + * and f is wrapped in a try-catch that throws a runtime exception */ - static BiFunction throwingRuntime(ThrowingBiFunction f) { + @Contract(pure = true) + static @NotNull BiFunction + throwingRuntime(ThrowingBiFunction f) { return (a, b) -> { try { return f.apply(a, b); @@ -88,16 +150,29 @@ static BiFunction throwingRuntime(Throwi /** * A Predicate which may throw a checked exception + * @param input type + * @param exception type */ @FunctionalInterface interface ThrowingPredicate { + /** + * @param item to test + * @return boolean result of test + * @throws X throw type + */ boolean test(T item) throws X; /** * Converts the provided predicate into a regular Predicate, where any thrown exceptions * are wrapped in a RuntimeException + * @param p predicate that may throw + * @param input type + * @param output type + * @return dF(x -> y) where p is wrapped in a try catch that throws a runtime */ - static Predicate throwingRuntime(ThrowingPredicate p) { + @Contract(pure = true) + static @NotNull Predicate + throwingRuntime(ThrowingPredicate p) { return x -> { try { return p.test(x); @@ -108,7 +183,17 @@ static Predicate throwingRuntime(ThrowingPredicate Predicate throwsWhen(ThrowingConsumer consumer) { + /** + * @param consumer that may throw + * @param input type + * @param exception type + * @return dF(x -> y) where x is passed to the consumer + * and the consumer may or may not throw + * and y is a boolean that represents if the consumer failed + */ + @Contract(pure = true) + static @NotNull Predicate + throwsWhen(ThrowingConsumer consumer) { return t -> { try { consumer.accept(t); @@ -119,7 +204,18 @@ static Predicate throwsWhen(ThrowingConsumer c }; } - static Predicate doesntThrow(ThrowingConsumer consumer) { + /** + * @param consumer which may throw + * @param input type + * @param exception type + * @return dF(x -> y) where the consumer may throw + * and is thus wrapped in a try-catch that returns + * true/false based on whether or not the consumer + * threw + */ + @Contract(pure = true) + static @NotNull Predicate + doesntThrow(ThrowingConsumer consumer) { return t -> { try { consumer.accept(t); diff --git a/src/main/java/co/unruly/control/Unit.java b/src/main/java/co/unruly/control/Unit.java index 44a4481..cda2f5c 100644 --- a/src/main/java/co/unruly/control/Unit.java +++ b/src/main/java/co/unruly/control/Unit.java @@ -1,5 +1,8 @@ package co.unruly.control; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + import java.util.function.Consumer; import java.util.function.Function; @@ -9,14 +12,22 @@ * This exists to offer a bridge between void and regular functions, providing * convenience methods to convert between them. */ +@SuppressWarnings("unused") public enum Unit { + /** + * enum value + */ UNIT; /** - * Converts a Consumer to a Function, which returns Unit.UNIT + * Converts a Consumer to a Function, which returns Unit. UNIT + * @param toVoid function to use as consumer + * @param input type + * @return UNIT */ - public static Function functify(Consumer toVoid) { + @Contract(pure = true) + public static @NotNull Function functify(Consumer toVoid) { return x -> { toVoid.accept(x); return Unit.UNIT; @@ -25,13 +36,20 @@ public static Function functify(Consumer toVoid) { /** * Converts a Function to a Consumer, throwing away the return value + * @param function to convert + * @param input type + * @return consumer that throws away the return */ - public static Consumer voidify(Function function) { + @Contract(pure = true) + public static @NotNull Consumer voidify(@NotNull Function function) { return function::apply; } /** - * A no-op function which takes any argument, does nothing, and returns Unit.UNIT + * A no-op function which takes any argument, does nothing, and returns Unit. UNIT + * @param __ nothin + * @param type + * @return UNIT */ public static Unit noOp(T __) { return UNIT; @@ -39,6 +57,8 @@ public static Unit noOp(T __) { /** * A no-op consumer which takes any argument and does nothing + * @param __ nothin + * @param type */ public static void noOpConsumer(T __) { // do nothing diff --git a/src/main/java/co/unruly/control/casts/Equality.java b/src/main/java/co/unruly/control/casts/Equality.java index 739fc2b..c526a3d 100644 --- a/src/main/java/co/unruly/control/casts/Equality.java +++ b/src/main/java/co/unruly/control/casts/Equality.java @@ -7,8 +7,22 @@ import static co.unruly.control.result.Resolvers.ifFailed; import static co.unruly.control.result.Transformers.onSuccess; +/** + * Equality testing interface + */ public interface Equality { + /** + * @param self the object checking + * @param other the object to be checked + * @param equalityChecker the predicate function + * @param the types of both + * @return whether both are equal in terms of + * 1) exact cast + * 2) the equality checker + * 3) do not result in failure + */ + @SuppressWarnings("unchecked") static boolean areEqual(T self, Object other, BiPredicate equalityChecker) { if(self==other) { return true; diff --git a/src/main/java/co/unruly/control/matchers/FailureMatcher.java b/src/main/java/co/unruly/control/matchers/FailureMatcher.java index 4f2cb5c..91c9b7b 100644 --- a/src/main/java/co/unruly/control/matchers/FailureMatcher.java +++ b/src/main/java/co/unruly/control/matchers/FailureMatcher.java @@ -4,17 +4,25 @@ import org.hamcrest.Description; import org.hamcrest.Matcher; import org.hamcrest.TypeSafeDiagnosingMatcher; +import org.jetbrains.annotations.NotNull; +/** A helper class for failure matching + * @param success + * @param failure + */ public class FailureMatcher extends TypeSafeDiagnosingMatcher> { private final Matcher innerMatcher; + /** + * @param innerMatcher . + */ public FailureMatcher(Matcher innerMatcher) { this.innerMatcher = innerMatcher; } @Override - protected boolean matchesSafely(Result result, Description description) { + protected boolean matchesSafely(@NotNull Result result, Description description) { Boolean matches = result.either( success -> false, innerMatcher::matches @@ -28,7 +36,7 @@ protected boolean matchesSafely(Result result, Description description) { } @Override - public void describeTo(Description description) { + public void describeTo(@NotNull Description description) { description.appendText("A Failure containing "); innerMatcher.describeTo(description); } diff --git a/src/main/java/co/unruly/control/matchers/ResultMatchers.java b/src/main/java/co/unruly/control/matchers/ResultMatchers.java index d1413b7..55bc69c 100644 --- a/src/main/java/co/unruly/control/matchers/ResultMatchers.java +++ b/src/main/java/co/unruly/control/matchers/ResultMatchers.java @@ -3,6 +3,8 @@ import co.unruly.control.result.Result; import org.hamcrest.Description; import org.hamcrest.Matcher; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; import static org.hamcrest.CoreMatchers.equalTo; @@ -13,33 +15,57 @@ public class ResultMatchers { /** * Matches if the received value is a Success containing the specified value + * @param expectedValue success value to test against + * @param success + * @param failure + * @return matcher testing if input is a success containing expected value */ - public static Matcher> isSuccessOf(S expectedValue) { + @Contract("_ -> new") + public static @NotNull Matcher> + isSuccessOf(S expectedValue) { return isSuccessThat(equalTo(expectedValue)); } /** * Matches if the received value is a Success matching the specified value + * @param expectedSuccess success to test against + * @param success + * @param failure + * @return matcher testing if input matches success value */ - public static Matcher> isSuccessThat(Matcher expectedSuccess) { + @Contract("_ -> new") + public static @NotNull Matcher> + isSuccessThat(Matcher expectedSuccess) { return new SuccessMatcher<>(expectedSuccess); } /** * Matches if the received value is a Failure containing the specified value + * @param expectedValue value to test failure against + * @param success + * @param failure + * @return matcher that matches input against failure value */ - public static Matcher> isFailureOf(F expectedValue) { + @Contract("_ -> new") + public static @NotNull Matcher> + isFailureOf(F expectedValue) { return isFailureThat(equalTo(expectedValue)); } /** * Matches if the received value is a Failure matching the specified value + * @param expectedFailure failure to test against + * @param success + * @param failure + * @return a matcher for the expected failure */ - public static Matcher> isFailureThat(Matcher expectedFailure) { + @Contract("_ -> new") + public static @NotNull Matcher> + isFailureThat(Matcher expectedFailure) { return new FailureMatcher<>(expectedFailure); } - static void describeTo(Result result, Description description) { + static void describeTo(@NotNull Result result, Description description) { result.either( success -> description.appendText("A Success containing " + success), failure -> description.appendText("A Failure containing " + failure) diff --git a/src/main/java/co/unruly/control/matchers/SuccessMatcher.java b/src/main/java/co/unruly/control/matchers/SuccessMatcher.java index dfde335..ff8aee8 100644 --- a/src/main/java/co/unruly/control/matchers/SuccessMatcher.java +++ b/src/main/java/co/unruly/control/matchers/SuccessMatcher.java @@ -4,17 +4,26 @@ import org.hamcrest.Description; import org.hamcrest.Matcher; import org.hamcrest.TypeSafeDiagnosingMatcher; +import org.jetbrains.annotations.NotNull; +/** + * @param success + * @param failure + */ +@SuppressWarnings("unused") public class SuccessMatcher extends TypeSafeDiagnosingMatcher> { private final Matcher innerMatcher; + /** + * @param innerMatcher . + */ public SuccessMatcher(Matcher innerMatcher) { this.innerMatcher = innerMatcher; } @Override - protected boolean matchesSafely(Result result, Description description) { + protected boolean matchesSafely(@NotNull Result result, Description description) { Boolean matches = result.either( innerMatcher::matches, failure -> false @@ -28,12 +37,12 @@ protected boolean matchesSafely(Result result, Description description) { } @Override - public void describeTo(Description description) { + public void describeTo(@NotNull Description description) { description.appendText("A Success containing "); innerMatcher.describeTo(description); } - private void describe(Result result, Description description) { + private void describe(@NotNull Result result, Description description) { result.either( success -> description.appendText("A Success containing " + success), failure -> description.appendText("A Failure containing " + failure) diff --git a/src/main/java/co/unruly/control/pair/Comprehensions.java b/src/main/java/co/unruly/control/pair/Comprehensions.java index 5d53014..d4e4239 100644 --- a/src/main/java/co/unruly/control/pair/Comprehensions.java +++ b/src/main/java/co/unruly/control/pair/Comprehensions.java @@ -3,6 +3,8 @@ import co.unruly.control.pair.Quad.QuadFunction; import co.unruly.control.pair.Triple.TriFunction; import co.unruly.control.result.Result; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; import java.util.Optional; import java.util.function.BiFunction; @@ -11,78 +13,201 @@ import static co.unruly.control.result.Transformers.attempt; import static co.unruly.control.result.Transformers.onSuccess; +/** + * Comprehensions seem to be a set of abstract functions that + * define ways to apply various kinds of function to tuples + */ +@SuppressWarnings({"optional", "OptionalUsedAsFieldOrParameterType"}) public interface Comprehensions { - static Function, T> onAll(BiFunction f) { + /** + * @param f function to apply pair to + * @param left + * @param right + * @param return type + * @return function that applies bifunction to the provided pair + */ + @Contract(pure = true) + static @NotNull Function, T> + onAll(BiFunction f) { return pair -> pair.then(f); } - static Function, T> onAll(TriFunction f) { + /** + * @param f the tri function to use + * @param . + * @param . + * @param . + * @param return type + * @return a function that will apply f to the passed in triple + */ + @Contract(pure = true) + static @NotNull Function, T> + onAll(TriFunction f) { return triple -> triple.then(f); } - static Function, T> onAll(QuadFunction f) { + /** + * @param f the quad function to use + * @param . + * @param . + * @param . + * @param . + * @param return type + * @return a function that takes a quad and applies the quad function to that quad + */ + @Contract(pure = true) + static @NotNull Function, T> + onAll(QuadFunction f) { return quad -> quad.then(f); } - static Optional> allOf(Optional maybeLeft, Optional maybeRight) { + /** + * @param maybeLeft left val + * @param maybeRight right val + * @param type l + * @param type r + * @return pair of left and right exists + */ + static Optional> + allOf(@NotNull Optional maybeLeft, Optional maybeRight) { return maybeLeft.flatMap(left -> maybeRight.map(right -> Pair.of(left, right))); } - static Optional> allOf(Optional maybeFirst, Optional maybeSecond, Optional maybeThird) { - return maybeFirst.flatMap(first -> maybeSecond.flatMap(second -> maybeThird.map(third -> Triple.of(first, second, third)))); + /** + * @param maybeFirst . + * @param maybeSecond . + * @param maybeThird . + * @param . + * @param . + * @param . + * @return tuple of values of three optionals + */ + static Optional> + allOf(@NotNull Optional maybeFirst, Optional maybeSecond, Optional maybeThird) { + return maybeFirst.flatMap(first -> + maybeSecond.flatMap(second -> + maybeThird.map(third -> + Triple.of(first, second, third)))); } - static Optional> allOf(Optional maybeFirst, Optional maybeSecond, Optional maybeThird, Optional maybeFourth) { - return maybeFirst.flatMap(first -> maybeSecond.flatMap(second -> maybeThird.flatMap(third -> maybeFourth.map(fourth -> Quad.of(first, second, third, fourth))))); + /** + * @param maybeFirst optional first + * @param maybeSecond second + * @param maybeThird third + * @param maybeFourth fourth + * @param type a + * @param type b + * @param type c + * @param type d + * @return quad tuple + */ + static Optional> + allOf(@NotNull Optional maybeFirst, Optional maybeSecond, Optional maybeThird, Optional maybeFourth) { + return maybeFirst.flatMap(first -> + maybeSecond.flatMap(second -> + maybeThird.flatMap(third -> + maybeFourth.map(fourth -> + Quad.of(first, second, third, fourth))))); } - static Result, F> allOf(Result left, Result right) { + /** + * @param left left result + * @param right right result + * @param failure + * @param left side of pair + * @param right side of pair + * @return pair of left and right success + */ + static Result, F> + allOf(@NotNull Result left, Result right) { return left.then(attempt(l -> right.then(onSuccess(r -> Pair.of(l, r))))); } - static Result, F> allOf(Result first, Result second, Result third) { + /** + * @param first | + * @param second | + * @param third | + * @param | + * @param | + * @param | + * @param | + * @return tuple of three successes + */ + static Result, F> + allOf(@NotNull Result first, Result second, Result third) { return first.then(attempt(firstValue -> second.then(attempt(secondValue -> third.then(onSuccess(thirdValue -> - Triple.of(firstValue, secondValue, thirdValue) - )) - )) - )); + Triple.of(firstValue, secondValue, thirdValue))))))); } - static Result, F> allOf(Result first, Result second, Result third, Result fourth) { + /** + * @param first . + * @param second . + * @param third . + * @param fourth . + * @param . + * @param . + * @param . + * @param . + * @param . + * @return collapses success states of four results to one + */ + static Result, F> + allOf(@NotNull Result first, Result second, Result third, Result fourth) { return first.then(attempt(firstValue -> second.then(attempt(secondValue -> third.then(attempt(thirdValue -> fourth.then(onSuccess(fourthValue -> - Quad.of(firstValue, secondValue, thirdValue, fourthValue) - )) - )) - )) - )); + Quad.of(firstValue, secondValue, thirdValue, fourthValue))))))))); } - static Function, F>, Result> ifAllSucceeded( - BiFunction f - ) { + /** + * @param f test function + * @param failure type + * @param left input + * @param right input + * @param success output + * @return a function that when passed a result with a pair-fail setup, returns + * applying that result to f + */ + static @NotNull Function, F>, Result> + ifAllSucceeded(BiFunction f) { return onSuccess(onAll(f)); } - static Function, F>, Result> ifAllSucceeded( - TriFunction f - ) { + /** + * @param f tri function collapsor + * @param failure type + * @param . + * @param . + * @param . + * @param result type + * @return dF(x -> y) where X is a result with success type triple and y is a result with a collapsed success type + */ + static @NotNull Function, F>, Result> + ifAllSucceeded(TriFunction f) { return onSuccess(onAll(f)); } - static Function, F>, Result> ifAllSucceeded( - QuadFunction f - ) { + /** + * @param f quad function to collapse + * @param fail type + * @param . + * @param . + * @param . + * @param . + * @param collapsed type + * @return dF(x -> y) where x is a result with a success quad type and y is a result with a collapsed success type + */ + static @NotNull Function, F>, Result> + ifAllSucceeded(QuadFunction f) { return onSuccess(onAll(f)); } diff --git a/src/main/java/co/unruly/control/pair/Maps.java b/src/main/java/co/unruly/control/pair/Maps.java index f639599..19a4c4d 100644 --- a/src/main/java/co/unruly/control/pair/Maps.java +++ b/src/main/java/co/unruly/control/pair/Maps.java @@ -1,5 +1,8 @@ package co.unruly.control.pair; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + import java.util.Map; import java.util.stream.Collector; import java.util.stream.Collectors; @@ -36,21 +39,23 @@ static Map mapOf(Pair ...entries) { * @param the right type of the pair, interpreted as the value type * @return a Collector which collects a Stream of Pairs into a Map */ - static Collector, ?, Map> toMap() { + @Contract(value = " -> new", pure = true) + static @NotNull Collector, ?, Map> toMap() { return Collectors.toMap(Pair::left, Pair::right); } /** * Creates a key-value pair. - * - * This is just an alias for Pair.of, that makes more sense in a map-initialisation context. + *

+ * This is just an alias for Pair. Of, that makes more sense in a map-initialisation context. * @param key the key * @param value the value * @param the key type * @param the value type * @return a key-value pair */ - static Pair entry(K key, V value) { + @Contract(value = "_, _ -> new", pure = true) + static @NotNull Pair entry(K key, V value) { return Pair.of(key, value); } } diff --git a/src/main/java/co/unruly/control/pair/Pair.java b/src/main/java/co/unruly/control/pair/Pair.java index e45346d..03ba3d7 100644 --- a/src/main/java/co/unruly/control/pair/Pair.java +++ b/src/main/java/co/unruly/control/pair/Pair.java @@ -1,5 +1,8 @@ package co.unruly.control.pair; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + import java.util.Objects; import java.util.function.BiFunction; import java.util.function.Function; @@ -7,24 +10,34 @@ /** * A basic tuple type */ -public class Pair { - - public final L left; - public final R right; +public record Pair(L left, R right) { - public Pair(L left, R right) { - this.left = left; - this.right = right; + /** + * @param left left value + * @param right right value + */ + public Pair { } - public static Pair of(L left, R right) { + /** + * @param left . + * @param right . + * @param left type + * @param right type + * @return new pair from left and right + */ + @Contract(value = "_, _ -> new", pure = true) + public static @NotNull Pair of(L left, R right) { return new Pair<>(left, right); } /** * Gets the left element. Note that Pair also supports direct member access, but this is useful when you need * a method reference to extract one side of the pair. + * + * @return left */ + @Override public L left() { return left; } @@ -32,7 +45,10 @@ public L left() { /** * Gets the right element. Note that Pair also supports direct member access, but this is useful when you need * a method reference to extract one side of the pair. + * + * @return right */ + @Override public R right() { return right; @@ -40,15 +56,23 @@ public R right() { /** * Applies the given function to this pair. + * + * @param function action + * @param result type + * @return result of applying function to this pair */ - public T then(Function, T> function) { + public T then(@NotNull Function, T> function) { return function.apply(this); } /** * Applies the given bifunction to this pair, using left for the first argument and right for the second + * + * @param function bifunction pair + * @param output type + * @return output type */ - public T then(BiFunction function) { + public T then(@NotNull BiFunction function) { return function.apply(this.left, this.right); } @@ -66,8 +90,9 @@ public int hashCode() { return Objects.hash(left, right); } + @Contract(pure = true) @Override - public String toString() { + public @NotNull String toString() { return "Pair{" + "left=" + left + ", right=" + right + diff --git a/src/main/java/co/unruly/control/pair/PairListCollector.java b/src/main/java/co/unruly/control/pair/PairListCollector.java index 0980eaf..766bc6f 100644 --- a/src/main/java/co/unruly/control/pair/PairListCollector.java +++ b/src/main/java/co/unruly/control/pair/PairListCollector.java @@ -18,6 +18,10 @@ public class PairListCollector implements Collector, Pa private final Function, FL> leftFinisher; private final Function, FR> rightFinisher; + /** + * @param leftFinisher . + * @param rightFinisher . + */ public PairListCollector(Function, FL> leftFinisher, Function, FR> rightFinisher) { this.leftFinisher = leftFinisher; this.rightFinisher = rightFinisher; @@ -31,23 +35,23 @@ public Supplier, List>> supplier() { @Override public BiConsumer, List>, Pair> accumulator() { return (pairs, pair) -> { - pairs.left.add(pair.left); - pairs.right.add(pair.right); + pairs.left().add(pair.left()); + pairs.right().add(pair.right()); }; } @Override public BinaryOperator, List>> combiner() { return (first, second) -> { - first.left.addAll(second.left); - first.right.addAll(second.right); + first.left().addAll(second.left()); + first.right().addAll(second.right()); return first; }; } @Override public Function, List>, Pair> finisher() { - return pair -> Pair.of(leftFinisher.apply(pair.left), rightFinisher.apply(pair.right)); + return pair -> Pair.of(leftFinisher.apply(pair.left()), rightFinisher.apply(pair.right())); } @Override diff --git a/src/main/java/co/unruly/control/pair/PairReducingCollector.java b/src/main/java/co/unruly/control/pair/PairReducingCollector.java index 738d01f..bf9fb7a 100644 --- a/src/main/java/co/unruly/control/pair/PairReducingCollector.java +++ b/src/main/java/co/unruly/control/pair/PairReducingCollector.java @@ -20,7 +20,16 @@ public class PairReducingCollector implements Collector, PairRe private final BinaryOperator leftReducer; private final BinaryOperator rightReducer; - public PairReducingCollector(L leftIdentity, R rightIdentity, BinaryOperator leftReducer, BinaryOperator rightReducer) { + /** + * @param leftIdentity . + * @param rightIdentity . + * @param leftReducer . + * @param rightReducer . + */ + public PairReducingCollector( + L leftIdentity, R rightIdentity, + BinaryOperator leftReducer, + BinaryOperator rightReducer) { this.leftIdentity = leftIdentity; this.rightIdentity = rightIdentity; this.leftReducer = leftReducer; @@ -35,8 +44,8 @@ public Supplier> supplier() { @Override public BiConsumer, Pair> accumulator() { return (acc, item) -> { - acc.left = leftReducer.apply(acc.left, item.left); - acc.right = rightReducer.apply(acc.right, item.right); + acc.left = leftReducer.apply(acc.left, item.left()); + acc.right = rightReducer.apply(acc.right, item.right()); }; } @@ -59,7 +68,11 @@ public Set characteristics() { return EnumSet.noneOf(Characteristics.class); } - static class MutablePair { + /** + * @param left type + * @param right type + */ + public static class MutablePair { L left; R right; diff --git a/src/main/java/co/unruly/control/pair/Pairs.java b/src/main/java/co/unruly/control/pair/Pairs.java index f965832..fe5987b 100644 --- a/src/main/java/co/unruly/control/pair/Pairs.java +++ b/src/main/java/co/unruly/control/pair/Pairs.java @@ -1,16 +1,16 @@ package co.unruly.control.pair; import co.unruly.control.result.Result; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; import java.util.Collections; import java.util.List; -import java.util.Optional; import java.util.function.BiFunction; import java.util.function.BinaryOperator; import java.util.function.Function; import java.util.function.IntFunction; import java.util.stream.Collector; -import java.util.stream.Collectors; import java.util.stream.Stream; import static co.unruly.control.result.Result.failure; @@ -20,94 +20,167 @@ /** * Convenience functions on Pairs */ +@SuppressWarnings("unused") public interface Pairs { /** * Applies the given function to the left element of a Pair, returning a new Pair with the result of that * function as the left element and the original right element untouched + * @param leftMapper mapping function for left value + * @param

    old left value type + * @param new left value type + * @param right type + * @return function that takes a pair, and converts the left type to a new type */ - static Function, Pair> onLeft(Function leftMapper) { - return pair -> Pair.of(leftMapper.apply(pair.left), pair.right); + @Contract(pure = true) + static @NotNull Function, Pair> + onLeft(Function leftMapper) { + return pair -> Pair.of(leftMapper.apply(pair.left()), pair.right()); } /** * Applies the given function to the right element of a Pair, returning a new Pair with the result of that * function as the right element and the original left element untouched + * @param rightMapper mapping function for right value + * @param old right value type + * @param new right value type + * @param right type + * @return function that takes a pair, and converts the right type to a new type */ - static Function, Pair> onRight(Function rightMapper) { - return pair -> Pair.of(pair.left, rightMapper.apply(pair.right)); + @Contract(pure = true) + static @NotNull Function, Pair> + onRight(Function rightMapper) { + return pair -> Pair.of(pair.left(), rightMapper.apply(pair.right())); } /** - * Applies the given function to both elements off a Pair, assuming that both elements are of the + * Applies the given function to both elements of a Pair, assuming that both elements are of the * same type + * @param f mutating function + * @param old type + * @param new type + * @return function that asks for a pair in the form P(T, T) and returns P(R, R) */ - static Function, Pair> onBoth(Function f) { - return pair -> Pair.of(f.apply(pair.left), f.apply(pair.right)); + @Contract(pure = true) + static @NotNull Function, Pair> + onBoth(Function f) { + return pair -> Pair.of(f.apply(pair.left()), f.apply(pair.right())); } /** * Applies the given function to both elements off a Pair, yielding a non-Pair value + * @param f merging function + * @param left type + * @param right type + * @param output type + * @return function that asks for a pair and returns T */ - static Function, T> merge(BiFunction f) { + @Contract(pure = true) + static @NotNull Function, T> + merge(BiFunction f) { return pair -> pair.then(f); } /** * Merges a Pair of Lists of T into a single List of T, with the left items at the front of the list. + * @param type + * @return list with left before right, merged */ - static Function, List>, List> mergeLists() { - return pair -> Stream.of(pair.left, pair.right).flatMap(List::stream).collect(toList()); + @Contract(pure = true) + static @NotNull Function, List>, List> mergeLists() { + return pair -> Stream.of(pair.left(), pair.right()).flatMap(List::stream).collect(toList()); } /** * Collects a Stream of Pairs into a single Pair of lists, where a given index can be used to access the left * and right parts of the input pairs respectively. + * @param left type + * @param right type + * @return a collector representing a single list of pairs that is indexable */ - static Collector, Pair, List>, Pair, List>> toParallelLists() { + @Contract(value = " -> new", pure = true) + static @NotNull Collector, Pair, List>, Pair, List>> + toParallelLists() { return using(Collections::unmodifiableList, Collections::unmodifiableList); } /** * Collects a Stream of Pairs into a single Pair of arrays, where a given index can be used to access the left * and right parts of the input pairs respectively. + * @param leftArrayConstructor left constructing function + * @param rightArrayConstructor right constructing function + * @param left type + * @param right type + * @return collector representing a single pair of arrays, where an index can be used to access the + * left and right parts */ - static Collector, Pair, List>, Pair> toArrays(IntFunction leftArrayConstructor, IntFunction rightArrayConstructor) { + @Contract(value = "_, _ -> new", pure = true) + static @NotNull Collector, Pair, List>, Pair> + toArrays(IntFunction leftArrayConstructor, IntFunction rightArrayConstructor) { return using( - left -> left.stream().toArray(leftArrayConstructor), - right -> right.stream().toArray(rightArrayConstructor) + left -> left.toArray(leftArrayConstructor), + right -> right.toArray(rightArrayConstructor) ); } /** * Reduces a stream of pairs to a single pair, using the provided identities and reducer functions + * @param leftIdentity left input + * @param leftReducer left reducing function + * @param rightIdentity right input + * @param rightReducer right reducing function + * @param left type + * @param right type + * @return collector that represents a single pair, representing the reduced pairs + * based on the identities and reducing mechanisms */ - static PairReducingCollector reducing( - L leftIdentity, BinaryOperator leftReducer, - R rightIdentity, BinaryOperator rightReducer) { + @Contract(value = "_, _, _, _ -> new", pure = true) + static @NotNull PairReducingCollector + reducing(L leftIdentity, BinaryOperator leftReducer, + R rightIdentity, BinaryOperator rightReducer) { return new PairReducingCollector<>(leftIdentity, rightIdentity, leftReducer, rightReducer); } - static Collector, Pair, List>, Pair> using( - Function, FL> leftFinisher, - Function, FR> rightFinisher) { + /** + * @param leftFinisher left collection mechanism + * @param rightFinisher right collection mechanism + * @param left type + * @param right type + * @param new left finisher + * @param new right finisher + * @return pair of lists + */ + @Contract(value = "_, _ -> new", pure = true) + static @NotNull Collector, Pair, List>, Pair> + using(Function, FL> leftFinisher, Function, FR> rightFinisher) { return new PairListCollector<>(leftFinisher, rightFinisher); } /** - * If there are any elements in the right side of the Pair, return a failure of + * If there are any elements on the right side of the Pair, return a failure of * the right side, otherwise return a success of the left. + * @param sides pair of lists + * @param left type + * @param right type + * @return result representing success if right side has pairs, otherwise returns + * list of left side */ - static Result, List> anyFailures(Pair, List> sides) { - return sides.right.isEmpty() ? success(sides.left) : failure(sides.right); + static Result, List> + anyFailures(@NotNull Pair, List> sides) { + return sides.right().isEmpty() ? success(sides.left()) : failure(sides.right()); } /** - * If there are any elements in the left side of the Pair, return a success of + * If there are any elements on the left side of the Pair, return a success of * the left side, otherwise return a failure of the left. + * @param sides pair of lists + * @param left type + * @param right type + * @return result where success is if left side has elements, failure otherwise */ - static Result, List> anySuccesses(Pair, List> sides) { - return sides.left.isEmpty() ? failure(sides.right) : success(sides.left); + static Result, List> + anySuccesses(@NotNull Pair, List> sides) { + return sides.left().isEmpty() ? failure(sides.right()) : success(sides.left()); } } diff --git a/src/main/java/co/unruly/control/pair/Quad.java b/src/main/java/co/unruly/control/pair/Quad.java index 34108de..f97a10d 100644 --- a/src/main/java/co/unruly/control/pair/Quad.java +++ b/src/main/java/co/unruly/control/pair/Quad.java @@ -1,53 +1,73 @@ package co.unruly.control.pair; -import java.util.Objects; -import java.util.function.Function; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; -public class Quad { +import java.util.function.Function; +import java.util.Objects; +/** + * @param first . + * @param second . + * @param third . + * @param fourth . + * @param . + * @param . + * @param . + * @param . + */ +public record Quad(A first, B second, C third, D fourth) { + + /** + * @param . + * @param . + * @param . + * @param . + * @param output type + */ @FunctionalInterface public interface QuadFunction { + /** + * @param a . + * @param b . + * @param c . + * @param d . + * @return T + */ T apply(A a, B b, C c, D d); } - public final A first; - public final B second; - public final C third; - public final D fourth; - - - public Quad(A first, B second, C third, D fourth) { - this.first = first; - this.second = second; - this.third = third; - this.fourth = fourth; - } - - public static Quad of(A first, B second, C third, D fourth) { - return new Quad(first, second, third, fourth); - } - - public A first() { - return first; - } - - public B second() { - return second; - } - - public C third() { - return third; - } - - public D fourth() { - return fourth; + /** + * @param first . + * @param second . + * @param third . + * @param fourth . + * @param . + * @param . + * @param . + * @param . + * @return new quad of first-fourth params + */ + @Contract("_, _, _, _ -> new") + public static @NotNull Quad of(A first, B second, C third, D fourth) { + return new Quad<>(first, second, third, fourth); } - public T then(Function, T> function) { + /** + * @param function quad function to use + * @param return type + * @return T given this + */ + public T then(@NotNull Function, T> function) { return function.apply(this); } - public T then(QuadFunction function) { + /** + * @param function quad function to use + * @param return type + * @return T + */ + public T then(@NotNull QuadFunction function) { return function.apply(first, second, third, fourth); } @@ -67,8 +87,9 @@ public int hashCode() { return Objects.hash(first, second, third, fourth); } + @Contract(pure = true) @Override - public String toString() { + public @NotNull String toString() { return "Quad{" + "first=" + first + ", second=" + second + diff --git a/src/main/java/co/unruly/control/pair/Triple.java b/src/main/java/co/unruly/control/pair/Triple.java index 537549a..a45b6b5 100644 --- a/src/main/java/co/unruly/control/pair/Triple.java +++ b/src/main/java/co/unruly/control/pair/Triple.java @@ -1,46 +1,69 @@ package co.unruly.control.pair; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + import java.util.Objects; import java.util.function.Function; -public class Triple { +/** + * @param first . + * @param second . + * @param third . + * @param . + * @param . + * @param . + */ +@SuppressWarnings("unused") +public record Triple(A first, B second, C third) { + /** + * @param . + * @param . + * @param . + * @param result type + */ @FunctionalInterface public interface TriFunction { + /** + * @param a . + * @param b . + * @param c . + * @return R + */ R apply(A a, B b, C c); } - public final A first; - public final B second; - public final C third; - - public Triple(A first, B second, C third) { - this.first = first; - this.second = second; - this.third = third; - } - - public static Triple of(A first, B second, C third) { + /** + * @param first . + * @param second . + * @param third . + * @param . + * @param . + * @param . + * @return triple if inputs + */ + @Contract(value = "_, _, _ -> new", pure = true) + public static @NotNull Triple + of(A first, B second, C third) { return new Triple<>(first, second, third); } - public A first() { - return first; - } - - public B second() { - return second; - } - - public C third() { - return third; - } - - public T then(Function, T> function) { + /** + * @param function triple function + * @param output type + * @return T from collapsing triple function + */ + public T then(@NotNull Function, T> function) { return function.apply(this); } - public T then(TriFunction function) { + /** + * @param function tri function + * @param output type + * @return T from collapsing tri types + */ + public T then(@NotNull TriFunction function) { return function.apply(first, second, third); } @@ -59,8 +82,9 @@ public int hashCode() { return Objects.hash(first, second, third); } + @Contract(pure = true) @Override - public String toString() { + public @NotNull String toString() { return "Triple{" + "first=" + first + ", second=" + second + diff --git a/src/main/java/co/unruly/control/result/Combiners.java b/src/main/java/co/unruly/control/result/Combiners.java index d61cacd..5984f09 100644 --- a/src/main/java/co/unruly/control/result/Combiners.java +++ b/src/main/java/co/unruly/control/result/Combiners.java @@ -5,33 +5,58 @@ import static co.unruly.control.result.Result.success; +/** + * interface for combining different data structures + */ public interface Combiners { /** * Combines two Results into a single Result. If both arguments are a Success, then * it applies the given function to their values and returns a Success of it. - * + * @param a + * @param b + * @param f + * @param secondArgument result to merge + * @return first failure * If either or both arguments are Failures, then this returns the first failure * it encountered. */ - static Function, MergeableResults> combineWith(Result secondArgument) { + @org.jetbrains.annotations.NotNull + @org.jetbrains.annotations.Contract(pure = true) + static Function, MergeableResults> + combineWith(Result secondArgument) { // ugh ugh ugh we need an abstract class because otherwise it can't infer generics properly can i be sick now? ta - return result -> new MergeableResults() { + return result -> new MergeableResults<>() { + /** + * @param combiner combining mechanism + * @param combiner + * @return an either + */ @Override public Result using(BiFunction combiner) { return result.either( - s1 -> secondArgument.either( - s2 -> success(combiner.apply(s1, s2)), + s1 -> secondArgument.either( + s2 -> success(combiner.apply(s1, s2)), + Result::failure + ), Result::failure - ), - Result::failure ); } }; } + /** + * @param input 1 + * @param input 2 + * @param failure + */ @FunctionalInterface interface MergeableResults { + /** + * @param combiner combining function + * @param success combiner + * @return result with the combined result as the success and fail as fail + */ Result using(BiFunction combiner); } } diff --git a/src/main/java/co/unruly/control/result/Introducers.java b/src/main/java/co/unruly/control/result/Introducers.java index be47a7a..d9e8248 100644 --- a/src/main/java/co/unruly/control/result/Introducers.java +++ b/src/main/java/co/unruly/control/result/Introducers.java @@ -1,6 +1,8 @@ package co.unruly.control.result; import co.unruly.control.ThrowingLambdas; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; import java.util.Map; import java.util.Optional; @@ -9,61 +11,97 @@ import java.util.function.Supplier; import java.util.stream.Stream; -import static co.unruly.control.result.Result.failure; -import static co.unruly.control.result.Result.success; import static co.unruly.control.result.Transformers.unwrapSuccesses; import static java.util.function.Function.identity; /** - * A collection of sample functions which take regular values and output a Result. + * A collection of sample functions which take regular values and output a Result. */ +@SuppressWarnings("unused") public interface Introducers { + /** * Returns a Function which creates a new Success wrapping the provided value + * @param success type + * @param fail type + * @return success wrapper */ - static Function> success() { + @Contract(pure = true) + static @NotNull Function> + success() { return Result::success; } /** * Returns a Function which creates a new Failure wrapping the provided value + * @param success type + * @param fail type + * @return failure wrapper */ - static Function> failure() { + @Contract(pure = true) + static @NotNull Function> failure() { return Result::failure; } /** * Returns a function which takes an Optional value, and returns a success of the * wrapped value if it was present, otherwise returns a failure using the provided Supplier + * @param onEmpty failure supplier + * @param success type + * @param failure type + * @return result type */ - static Function, Result> fromOptional(Supplier onEmpty) { + @Contract(pure = true) + static @NotNull Function, Result> + fromOptional(Supplier onEmpty) { return maybe -> maybe.map(Result::success).orElseGet(() -> Result.failure(onEmpty.get())); } /** * Returns a function which takes a value and checks a predicate on it: if the predicate passes, then * return a success of that value, otherwise apply the failure mapper to it + * @param test a predicate testing function + * @param fail + * @param success + * @param failureMapper func to map to + * @return dF(x -> y) where x is S and y is a result type determined by testing x against test */ - static Function> ifFalse(Predicate test, Function failureMapper) { + @Contract(pure = true) + static @NotNull Function> ifFalse(Predicate test, Function failureMapper) { return val -> test.test(val) ? Result.success(val) : Result.failure(failureMapper.apply(val)); } /** * Returns a function which takes a value and checks a predicate on it: if the predicate passes, then * return a success of that value, otherwise return a failure of the provided value + * @param test testing function + * @param failureValue fail value + * @param success type + * @param fail type + * @return result */ - static Function> ifFalse(Predicate test, F failureValue) { + @Contract(pure = true) + static @NotNull Function> + ifFalse(Predicate test, F failureValue) { return val -> test.test(val) ? Result.success(val) : Result.failure(failureValue); } /** * Returns a function which takes a value, applies the given function to it, and returns a * success of the returned value, unless it's null, when we return the given failure value + * @param mapper mapping function + * @param failure failure type + * @param input type + * @param output type + * @param failure type + * @return failure if null, success of output otherwise */ - static Function> ifNull(Function mapper, F failure) { + @Contract(pure = true) + static @NotNull Function> + ifNull(Function mapper, F failure) { return input -> { - final S1 output = mapper.apply(input); + final Output output = mapper.apply(input); return output == null ? Result.failure(failure) : Result.success(output); }; } @@ -72,10 +110,18 @@ static Function> ifNull(Function mapper, F fa * Returns a function which takes a value, applies the given function to it, and returns a * success of the returned value, unless it's null, when we return a failure of the given * function to the input value. + * @param mapper mapping function + * @param failureMapper mapping (to failure) funcntion + * @param input type + * @param mapped type + * @param fail type + * @return returns result with either success w/ output or failure with input */ - static Function> ifNull(Function mapper, Function failureMapper) { + @Contract(pure = true) + static @NotNull Function> + ifNull(Function mapper, Function failureMapper) { return input -> { - final S1 output = mapper.apply(input); + final Output output = mapper.apply(input); return output == null ? Result.failure(failureMapper.apply(input)) : Result.success(output); }; } @@ -84,12 +130,21 @@ static Function> ifNull(Function mapper, Func * Returns a function which takes a value, applies the given function to it, and returns a success of * the input unless the returned value matches the provided value. Otherwise, it returns a failure of the * failure value provided. - * + *

    * Note that the success path returns a success of the original value, not the result of applying this * function. This can be used to build more complex predicates, or to check the return value of a * consumer-with-return-code. + * @param checker maps input type to value type + * @param value value to check against + * @param failure failure value + * @param success type + * @param failure type + * @param value type + * @return dF(x->y) where x is S and returns a result of type R(S, F) based on comparing S to V */ - static Function> ifYields(Function checker, V value, F failure) { + @Contract(pure = true) + static @NotNull Function> + ifYields(Function checker, V value, F failure) { return input -> checker.apply(input) == value ? Result.failure(failure) : Result.success(input); } @@ -97,13 +152,24 @@ static Function> ifYields(Function checker, V va * Returns a function which takes a value, applies the given function to it, and returns a success of * the input unless the returned value matches the provided value. Otherwise, it returns a failure of the * input value applied to the failure mapping function. - * + *

    * Note that the success path returns a success of the original value, not the result of applying this * function. This can be used to build more complex predicates, or to check the return value of a * consumer-with-return-code. + * @param checker maps S to V + * @param value type to compare against + * @param failureMapper maps success to failure outcome + * @param success type + * @param fail type + * @param value comparison type + * @return dF(x -> y) where */ - static Function> ifYields(Function checker, V value, Function failureMapper) { - return input -> checker.apply(input) == value ? Result.failure(failureMapper.apply(input)) : Result.success(input); + @Contract(pure = true) + static @NotNull Function> + ifYields(Function checker, V value, Function failureMapper) { + return input -> checker.apply(input) == value ? + Result.failure(failureMapper.apply(input)) : + Result.success(input); } /** @@ -111,23 +177,32 @@ static Function> ifYields(Function checker, V va * a success of the output of that function. In the case where the function throws an exception, * that exception is passed to the provided exception-mapper, and the output of that call is the * failure value. - * + *

    * Whilst we take a ThrowingFunction which throws a specific checked exception type X, our * eventual Result is of the more general type Exception. That's because it's also possible for the * function to throw other types of RuntimeException, and we have two choices: don't catch (or rethrow) * RuntimeException, or have a more general failure type. Rethrowing exceptions goes against the whole * point of constraining the error path, so we opt for the latter. - * + *

    * If the provided function throws an Error, we don't catch that. Errors in general are not * intended to be caught. - * + *

    * Note that idiomatic handling of Exceptions as failure type does allow specialised catch blocks * on specific exception types. + * @param throwingFunction function that may fail + * @param exceptionMapper maps the exception to the failure type + * @param input type + * @param output type + * @param exception type + * @param failure type + * @return df(IS -> R(OS, F)) where x is the input type passed to throwingFunction + * and the output wrapped in a try-catch that returns the exception mapped to a failure + * via the exceptionMapper */ - static Function> tryTo( - ThrowingLambdas.ThrowingFunction throwingFunction, - Function exceptionMapper - ) { + @Contract(pure = true) + static @NotNull Function> + tryTo(ThrowingLambdas.ThrowingFunction throwingFunction, + Function exceptionMapper) { return input -> { try { return Result.success(throwingFunction.apply(input)); @@ -141,22 +216,28 @@ static Function> tryTo( * Returns a function which takes a value, applies the provided function to it, and returns * a success of the output of that function, or a failure of the exception thrown by that function * if it threw an exception. - * + *

    * Whilst we take a ThrowingFunction which throws a specific checked exception type X, our * eventual Result is of the more general type Exception. That's because it's also possible for the * function to throw other types of RuntimeException, and we have two choices: don't catch (or rethrow) * RuntimeException, or have a more general failure type. Rethrowing exceptions goes against the whole * point of constraining the error path, so we opt for the latter. - * + *

    * If the provided function throws an Error, we don't catch that. Errors in general are not * intended to be caught. - * + *

    * Note that idiomatic handling of Exceptions as failure type does allow specialised catch blocks * on specific exception types. + * @param throwingFunction function that throws type X + * @param input type + * @param output type + * @param exception type + * @return dF(IS -> R(OS, Exception)) + * input is passed to the throwing function, + * output is a result with either the OS or F(Exception) */ - static Function> tryTo( - ThrowingLambdas.ThrowingFunction throwingFunction - ) { + static @NotNull Function> + tryTo(ThrowingLambdas.ThrowingFunction throwingFunction) { return tryTo(throwingFunction, identity()); } @@ -164,23 +245,29 @@ static Function> tryTo( * Returns a function which takes a value, applies the provided function to it, and returns * a success of the output of that function. If an exception is thrown, return a failure of * the specified failure case value. - * + *

    * Whilst we take a ThrowingFunction which throws a specific checked exception type X, our * eventual Result is of the more general type Exception. That's because it's also possible for the * function to throw other types of RuntimeException, and we have two choices: don't catch (or rethrow) * RuntimeException, or have a more general failure type. Rethrowing exceptions goes against the whole * point of constraining the error path, so we opt for the latter. - * + *

    * If the provided function throws an Error, we don't catch that. Errors in general are not * intended to be caught. - * + *

    * Note that idiomatic handling of Exceptions as failure type does allow specialised catch blocks * on specific exception types. + * @param throwingFunction function that takes an input type IS, returns an output type OS, or throws X + * @param failureCase function mapping exception to failure? + * @param input type + * @param output type + * @param exception type + * @param failure value + * @return dF(x -> y) where x is of type IS and y is a result of type R(OS, F) */ - static Function> tryTo( - ThrowingLambdas.ThrowingFunction throwingFunction, - F failureCase - ) { + @Contract(pure = true) + static @NotNull Function> + tryTo(ThrowingLambdas.ThrowingFunction throwingFunction, F failureCase) { return tryTo(throwingFunction, __ -> failureCase); } @@ -189,8 +276,15 @@ static Function> tryTo( * Returns a function which takes a value, applies the provided stream-returning function to it, * and return a stream which is the stream returned by the function, with each element wrapped in * a success, or a single failure of the exception thrown by that function if it threw an exception. + * @param f function that may throw an exception + * @param input type + * @param output type + * @param exception type + * @return dF(x -> y) where x is provided to f and returns a stream of success results or a single failure result + * of the exception type X if f failed */ - static Function>> tryAndUnwrap(ThrowingLambdas.ThrowingFunction, X> f) { + static @NotNull Function>> + tryAndUnwrap(ThrowingLambdas.ThrowingFunction, X> f) { return tryTo(f).andThen(unwrapSuccesses()); } @@ -198,12 +292,18 @@ static Function>> * Takes a class and returns a function which takes a value, attempts to cast it to that class, and returns * a Success of the provided type if it's a member of it, and a Failure of the known type otherwise, in both * cases containing the input value. - * + *

    * This differs from exactCastTo in that exactCastTo will only return a Success if the given value is exactly * the target type, whereas this will also return a Success if it is a subtype of that type. + * @param targetClass class to cast to + * @param input type + * @param output type + * @return result with either cast type or failure */ + @Contract(pure = true) @SuppressWarnings("unchecked") - static Function> castTo(Class targetClass) { + static @NotNull Function> + castTo(Class targetClass) { return input -> targetClass.isAssignableFrom(input.getClass()) ? Result.success((OS)input) : Result.failure(input); @@ -213,12 +313,18 @@ static Function> castTo(Class targetC * Takes a class and returns a function which takes a value, attempts to cast it to that class, and returns * a Success of the provided type if it's the same type as it, and a Failure of the known type otherwise, in both * cases containing the input value. - * + *

    * This differs from castTo in that castTo will return a Success if the given value is a subtype of the target * type, whereas this will only return a Success if it is exactly that type. + * @param targetClass class to cast to + * @param input type + * @param success output type + * @return result with either cast type or fail tye */ + @Contract(pure = true) @SuppressWarnings("unchecked") - static Function> exactCastTo(Class targetClass) { + static @NotNull Function> + exactCastTo(Class targetClass) { return input -> targetClass.equals(input.getClass()) ? Result.success((OS)input) : Result.failure(input); @@ -229,8 +335,16 @@ static Function> exactCastTo(Class ta * Takes a java.util.Map and a failure function, and returns a function which takes a key and returns * a success of the associated value in the Map, if present, or applies the failure function to the * key otherwise. + * @param map map with K -> S relationship + * @param failureProvider function to call with K in fail case + * @param key + * @param success type + * @param failure type + * @return result based on whether K is in map */ - static Function> fromMap(Map map, Function failureProvider) { + @Contract(pure = true) + static @NotNull Function> + fromMap(Map map, Function failureProvider) { return key -> { if(map.containsKey(key)) { return Result.success(map.get(key)); diff --git a/src/main/java/co/unruly/control/result/Match.java b/src/main/java/co/unruly/control/result/Match.java index 5aff1b9..b08eb18 100644 --- a/src/main/java/co/unruly/control/result/Match.java +++ b/src/main/java/co/unruly/control/result/Match.java @@ -1,5 +1,8 @@ package co.unruly.control.result; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + import java.util.function.Function; import java.util.stream.Stream; @@ -10,7 +13,7 @@ /** * A small DSL for building compact dispatch tables: better than if-expressions, worse than * proper pattern matching. But hey, it's Java, what do you expect? - * + *

    * This models a match attempt as a sequence of operations on a Result, starting with a Failure * and continuously trying to use flatMapFailure to convert that Result into a Success. */ @@ -21,9 +24,15 @@ public class Match { * a function, the otherwise() method must be called on the result of this function: * as there's no way to determine if the dispatch table is complete, a base case is * required. + * @param potentialMatchers an arbitrary list of matching functions + * @param input type + * @param success output type + * @return a match attempt type representing the outcome when given an input */ + @Contract(pure = true) @SafeVarargs - public static MatchAttempt match(Function>... potentialMatchers) { + public static @NotNull MatchAttempt + match(Function>... potentialMatchers) { return f -> attemptMatch(potentialMatchers).andThen(ifFailed(f)); } @@ -31,8 +40,14 @@ public static MatchAttempt match(Function>... poten * Builds a dispatch function from the provided matchers. Note that this returns a Result, * as there's no way to determine if the dispatch table is complete: if no match is found, * returns a Failure of the input value. + * @param potentialMatchers an arbitrary number of matching functions + * @param input type + * @param success output type + * @return a matching function that maps an input I to a result output */ - public static Function> attemptMatch(Function>... potentialMatchers) { + @SafeVarargs + public static @NotNull Function> + attemptMatch(Function>... potentialMatchers) { return compose(Stream.of(potentialMatchers).map(Transformers::recover)).compose(Result::failure); } @@ -41,22 +56,45 @@ public static Function> attemptMatch(Function input type + * @param output type + * @return match attempt result based on passed in failure value */ + @Contract(pure = true) @SafeVarargs - public static BoundMatchAttempt matchValue(I inputValue, Function>... potentialMatchers) { + public static @NotNull BoundMatchAttempt + matchValue(I inputValue, Function>... potentialMatchers) { return f -> pipe(inputValue) .then(attemptMatch(potentialMatchers)) .then(ifFailed(f)) .resolve(); } + /** + * @param input + * @param output + */ @FunctionalInterface public interface MatchAttempt { + /** + * @param baseCase . + * @return basecase + */ Function otherwise(Function baseCase); } + /** + * @param input + * @param output + */ @FunctionalInterface public interface BoundMatchAttempt { + /** + * @param baseCase . + * @return output + */ O otherwise(Function baseCase); } } diff --git a/src/main/java/co/unruly/control/result/MonadicAliases.java b/src/main/java/co/unruly/control/result/MonadicAliases.java index 8438dcb..7df5499 100644 --- a/src/main/java/co/unruly/control/result/MonadicAliases.java +++ b/src/main/java/co/unruly/control/result/MonadicAliases.java @@ -1,5 +1,8 @@ package co.unruly.control.result; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + import java.util.function.Function; import static co.unruly.control.result.Introducers.success; @@ -9,20 +12,33 @@ * Aliases for standard functions on Results which use names more familiar * to users of Haskell */ +@SuppressWarnings("unused") public interface MonadicAliases { /** * Returns a function which converts a regular value into a Result (as a Success) + * @param success + * @param failure + * @return result type */ - static Function> pure() { + @Contract(pure = true) + static @NotNull Function> + pure() { return success(); } /** * Returns a function which, when applied to a Result, applies the provided function to * the wrapped value if it's a Success, otherwise perpetuates the existing failure + * @param f function to map success to + * @param input type + * @param success output type + * @param fail type + * @return function that, given a result type, maps the success input + * through the provided function and returns a new result with success output type */ - static Function, Result> map(Function f) { + static @NotNull Function, Result> + map(Function f) { return onSuccess(f); } @@ -30,29 +46,51 @@ static Function, Result> map(Function f) { * Returns a function which, when applied to a Result, applies the provided function to * the wrapped value, returning that Result, if it's a Success. This can turn a Success into a * Failure. - * + *

    * If the result was already a failure, it perpetuates the existing failure. + * @param f the mapping function + * @param input type + * @param output type + * @param failure type + * @return a function that takes an input and maps it to a new output for success */ - static Function, Result> flatMap(Function> f) { + @Contract(pure = true) + static @NotNull Function, Result> + flatMap(Function> f) { return attempt(f); } + /** * Returns a function which, when applied to a Result, applies the provided function to * the wrapped value, returning that Result, if it's a Success. This can turn a Success into a * Failure. - * + *

    * If the result was already a failure, it perpetuates the existing failure. + * @param f the mapping function + * @param input type + * @param output type + * @param fail type + * @return a function that takes an input type and maps it to an output type + * inside a result */ - static Function, Result> bind(Function> f) { + @Contract(pure = true) + static @NotNull Function, Result> + bind(Function> f) { return attempt(f); } /** * Returns a function which, when applied to a Result, applies the provided function to * the wrapped value if it's a failure, otherwise perpetuates the existing success + * @param f the mapping function + * @param success type + * @param input type + * @param output type + * @return a function that maps a failure input to a new failure output */ - static Function, Result> mapFailure(Function f) { + static @NotNull Function, Result> + mapFailure(Function f) { return onFailure(f); } @@ -60,10 +98,17 @@ static Function, Result> mapFailure(Function * If the result was already a success, it perpetuates the existing success. + * @param f the mapping function + * @param success type + * @param input type + * @param output type + * @return a function that maps a failure input to a new failure output */ - static Function, Result> flatMapFailure(Function> f) { + @Contract(pure = true) + static @NotNull Function, Result> + flatMapFailure(Function> f) { return recover(f); } @@ -72,10 +117,17 @@ static Function, Result> flatMapFailure(Function< * Returns a function which, when applied to a Result, applies the provided function to * the wrapped value, returning that Result, if it's a Failure. This can turn a Failure into a * Success. - * + *

    * If the result was already a success, it perpetuates the existing success. + * @param f the mapping function + * @param success type + * @param input type + * @param output type + * @return a function that maps a failure input to a new failure output */ - static Function, Result> bindFailure(Function> f) { + @Contract(pure = true) + static @NotNull Function, Result> + bindFailure(Function> f) { return recover(f); } } diff --git a/src/main/java/co/unruly/control/result/Recover.java b/src/main/java/co/unruly/control/result/Recover.java index 8e505a1..b339b6e 100644 --- a/src/main/java/co/unruly/control/result/Recover.java +++ b/src/main/java/co/unruly/control/result/Recover.java @@ -1,5 +1,8 @@ package co.unruly.control.result; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + import java.util.Optional; import java.util.function.Function; import java.util.function.Predicate; @@ -8,13 +11,21 @@ /** * A collection of functions to (conditionally) recover a failure into a success. */ +@SuppressWarnings("unused") public interface Recover { /** * Returns a function which takes an Optional value, and returns a failure of the * wrapped value if it was present, otherwise returns a success using the provided Supplier + * @param onEmpty supplier function + * @param success type + * @param fail type + * @return dF(x -> y) where x is an optional such that if x is a + * failure it gets wrapped, otherwise the supplier is called */ - static Function, Result> whenAbsent(Supplier onEmpty) { + @Contract(pure = true) + static @NotNull Function, Result> + whenAbsent(Supplier onEmpty) { return maybe -> maybe.map(Result::failure).orElseGet(() -> Result.success(onEmpty.get())); } @@ -22,17 +33,36 @@ static Function, Result> whenAbsent(Supplier onEmpty * Takes a class and a mapping function and returns a function which takes a value and, if it's of the * provided class, applies the mapping function to it and returns it as a Success, otherwise returning * the input value as a Failure. + * @param targetClass to map to + * @param mapper maps from NarrowFailType to S + * @param success type + * @param broad fail type + * @param narrow fail type + * @return dF(BroadFailType -> R(S, BroadFailType)) where + * NarrowFailType !extends BroadFailType -> R(BroadFailType) + * NarrowFailType extends BroadFailType -> mapper(BroadFailType as NarrowFailType) -> R(S) */ - static Function> ifType(Class targetClass, Function mapper) { - return Introducers.castTo(targetClass).andThen(Transformers.onSuccess(mapper)); + static @NotNull + Function> + ifType(Class targetClass, Function mapper) { + return Introducers.castTo(targetClass) + .andThen(Transformers.onSuccess(mapper)); } /** * Takes a predicate and a mapping function and returns a function which takes a value and, if it satisfies * the predicate, applies the mapping function to it and returns it as a Success, otherwise returning * the input value as a Failure. + * @param test predicate function + * @param mapper mapping function + * @param success type + * @param fail type + * @return dF(x -> y) where if x satisfies test, return mapper(x) as a success or + * failure otherwise */ - static Function> ifIs(Predicate test, Function mapper) { + @Contract(pure = true) + static @NotNull Function> + ifIs(Predicate test, Function mapper) { return input -> test.test(input) ? Result.success(mapper.apply(input)) : Result.failure(input); @@ -42,8 +72,15 @@ static Function> ifIs(Predicate test, Function m * Takes a predicate and a mapping function and returns a function which takes a value and, if it doesn't * satisfy the predicate, applies the mapping function to it and returns it as a Success, otherwise returning * the input value as a Failure. + * @param test testing function + * @param mapper mapping function + * @param success type + * @param fail type + * @return dF(x -> y) where if x doesn't satisfy test, x is passed to mapper and put into a success + * and is otherwise a failure */ - static Function> ifNot(Predicate test, Function mapper) { + static @NotNull Function> + ifNot(@NotNull Predicate test, Function mapper) { return ifIs(test.negate(), mapper); } @@ -51,8 +88,15 @@ static Function> ifNot(Predicate test, Function * Takes a value and a mapping function and returns a function which takes a value and, if it is equal to * the provided value, applies the mapping function to it and returns it as a Success, otherwise returning * the input value as a Failure. + * @param expectedValue value to check against input + * @param mapper mapping function for success case + * @param success type + * @param fail type + * @return dF(x -> y) where y is a result whose success applies the mapper to x */ - static Function> ifEquals(F expectedValue, Function mapper) { + @Contract(pure = true) + static @NotNull Function> + ifEquals(F expectedValue, Function mapper) { return input -> expectedValue.equals(input) ? Result.success(mapper.apply(input)) : Result.failure(input); @@ -61,8 +105,14 @@ static Function> ifEquals(F expectedValue, Function /** * Matches the value if the provided function yields an Optional whose value is * present, returning the value in that Optional. + * @param successProvider function providing an optional success + * @param success type + * @param fail type + * @return function that, given an input, pipes it through success provider into a result */ - static Function> ifPresent(Function> successProvider) { + @Contract(pure = true) + static @NotNull Function> + ifPresent(Function> successProvider) { return value -> successProvider .apply(value) .map(Result::success) diff --git a/src/main/java/co/unruly/control/result/Resolvers.java b/src/main/java/co/unruly/control/result/Resolvers.java index a5bec4f..114a284 100644 --- a/src/main/java/co/unruly/control/result/Resolvers.java +++ b/src/main/java/co/unruly/control/result/Resolvers.java @@ -2,6 +2,8 @@ import co.unruly.control.pair.Pair; import co.unruly.control.pair.Pairs; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; import java.util.List; import java.util.Optional; @@ -17,13 +19,18 @@ * A set of common functions to convert from a Result to * an unwrapped value. */ +@SuppressWarnings("unused") public interface Resolvers { /** * Takes a Result where both success and failure types are the same, and returns * either the success or failure value as appropriate + * @param input type + * @return T */ - static Function, T> collapse() { + @Contract(pure = true) + static @NotNull Function, T> + collapse() { return r -> r.either(identity(), identity()); } @@ -31,8 +38,16 @@ static Function, T> collapse() { * Takes a Result and returns the success value if it is a success, or if it's * a failure, returns the result of applying the recovery function to the * failure value. + * @param recoveryFunction function to recover from F + * @param output success + * @param input success + * @param failure to recovery type + * @param failure type + * @return output success or recovered failure */ - static Function, OS> ifFailed(Function recoveryFunction) { + @Contract(pure = true) + static @NotNull Function, OS> + ifFailed(Function recoveryFunction) { return r -> r.either(identity(), recoveryFunction); } @@ -40,8 +55,13 @@ static Function, OS> ifFaile * Takes a Result for which the failure type is an Exception, and returns the * success value if it's a success, or throws the failure exception, wrapped in a * RuntimeException. + * @param success type + * @param exception type + * @return success value or throws an exception */ - static Function, S> getOrThrow() { + @Contract(pure = true) + static @NotNull Function, S> + getOrThrow() { return r -> r.either(identity(), ex -> { throw new RuntimeException(ex); }); } @@ -49,8 +69,14 @@ static Function, S> getOrThrow() { * Takes a Result and returns the success value if it is a success, or if it's * a failure, throws the result of applying the exception converter to the * failure value. + * @param exceptionConverter converts F to an exception + * @param success type + * @param fail type + * @return success or throws a runtime exception */ - static Function, S> getOrThrow(Function exceptionConverter) { + @Contract(pure = true) + static @NotNull Function, S> + getOrThrow(Function exceptionConverter) { return r -> r.either(identity(), failure -> { throw exceptionConverter.apply(failure); }); } @@ -58,8 +84,13 @@ static Function, S> getOrThrow(Function * Returns a Stream of successes: a stream of a single value if this is a success, * or an empty stream if this is a failure. This is intended to be used to flat-map * over a stream of Results to extract a stream of just the successes. + * @param success type + * @param success type + * @return function that, given a result, provides a stream of successes + * or an empty stream on failure */ - static Function, Stream> successes() { + @Contract(pure = true) + static @NotNull Function, Stream> successes() { return r -> r.either(Stream::of, __ -> empty()); } @@ -67,48 +98,80 @@ static Function, Stream> successes() { * Returns a Stream of failures: a stream of a single value if this is a failure, * or an empty stream if this is a success. This is intended to be used to flat-map * over a stream of Results to extract a stream of just the failures. + * @param success type + * @param fail type + * @return function that provides a stream of failures + * or an empty stream on success, given a result */ - static Function, Stream> failures() { + @Contract(pure = true) + static @NotNull Function, Stream> failures() { return r -> r.either(__ -> empty(), Stream::of); } /** * Returns an Optional success value, which is present if this result was a failure * and empty if it was a failure. + * @param success type + * @param fail type + * @return function that, given result, gives an optional if there was + * a success, or None otherwise */ - static Function, Optional> toOptional() { + @Contract(pure = true) + static @NotNull Function, Optional> + toOptional() { return r -> r.either(Optional::of, __ -> Optional.empty()); } /** * Returns an Optional failure value, which is present if this result was a failure * and empty if it was a success. + * @param success type + * @param fail type + * @return function that, given result, gives an optional if there was + * a failure, or None otherwise */ - static Function, Optional> toOptionalFailure() { + @Contract(pure = true) + static @NotNull Function, Optional> + toOptionalFailure() { return r -> r.either(__ -> Optional.empty(), Optional::of); } /** * Collects a Stream of Results into a Pair of Lists, the left containing the unwrapped * success values, the right containing the unwrapped failures. + * @param success type + * @param fail type + * @return pair of lists with unwrapped successes and failures */ - static Collector, Pair, List>, Pair, List>> split() { - return new ResultCollector<>(pair -> Pair.of(unmodifiableList(pair.left), unmodifiableList(pair.right))); + @Contract(value = " -> new", pure = true) + static @NotNull Collector, Pair, List>, Pair, List>> + split() { + return new ResultCollector<>(pair -> Pair.of(unmodifiableList(pair.left()), unmodifiableList(pair.right()))); } /** * Collects a Stream of Results into a Result which contains a List of Successes, if all results in * the stream were successful, or a list of Failures if any failed. + * @param success type + * @param fail type + * @return collector with list of all successes or list of failures */ - static Collector, Pair, List>, Result, List>> allSucceeded() { + @Contract(value = " -> new", pure = true) + static @NotNull Collector, Pair, List>, Result, List>> + allSucceeded() { return new ResultCollector<>(Pairs::anyFailures); } /** * Collects a Stream of Results into a Result which contains a List of Successes, if any results in * the stream were successful, or a list of Failures if all failed. + * @param success type + * @param fail type + * @return collector with list of successes, or list of all failures */ - static Collector, Pair, List>, Result, List>> anySucceeded() { + @Contract(value = " -> new", pure = true) + static @NotNull Collector, Pair, List>, Result, List>> + anySucceeded() { return new ResultCollector<>(Pairs::anySuccesses); } } diff --git a/src/main/java/co/unruly/control/result/Result.java b/src/main/java/co/unruly/control/result/Result.java index b41b8ab..ee2c0c9 100644 --- a/src/main/java/co/unruly/control/result/Result.java +++ b/src/main/java/co/unruly/control/result/Result.java @@ -1,5 +1,8 @@ package co.unruly.control.result; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + import java.io.Serializable; import java.util.Objects; import java.util.function.Function; @@ -11,12 +14,13 @@ *

    * The interface for Result is minimal: many common sample operations are implemented * with static methods on Introducers, Transformers, and Resolvers. - * + *

    * These can be composed upon a Result by passing them as arguments to then(). * - * @param The type of a success - * @param The type of a failure + * @param The type of success + * @param The type of failure */ +@SuppressWarnings("unused") public abstract class Result implements Serializable { private Result() { @@ -24,29 +28,53 @@ private Result() { /** * Creates a new Success + * @param value to wrap + * @param success type + * @param fail type + * @return success wrapper of S */ - public static Result success(S value) { + @Contract("_ -> new") + public static @NotNull Result success(S value) { return new Success<>(value); } /** * Creates a new Success, taking the failure type for contexts where it can't be inferred. + * @param value to wrap + * @param failureType class type + * @param success type + * @param failure type + * @return S wrapped in success */ - public static Result success(S value, Class failureType) { + @Contract("_, _ -> new") + public static @NotNull Result + success(S value, Class failureType) { return new Success<>(value); } /** * Creates a new Failure + * @param error to wrap + * @param success type + * @param fail type + * @return F wrapped in failure */ - public static Result failure(F error) { + @Contract("_ -> new") + public static @NotNull Result failure(F error) { return new Failure<>(error); } /** * Creates a new Failure, taking the success type for contexts where it can't be inferred. + * @param error to wrap + * @param successType for context + * @param success type + * @param fail type + * @return F wrapped in failure */ - public static Result failure(F error, Class successType) { + @Contract("_, _ -> new") + public static @NotNull Result + failure(F error, Class successType) { return new Failure<>(error); } @@ -59,8 +87,11 @@ public static Result failure(F error, Class successType) { * @param onFailure the function to process the failure value, if this is a Failure * @param the type of the end result * @return The result of executing onSuccess if this result is a Success, or onFailure if it's a failure + * @param success fail type + * @param failure fail type */ - public abstract R either(Function onSuccess, Function onFailure); + public abstract R + either(Function onSuccess, Function onFailure); /** * Applies a function to this Result. This permits inverting the calling convention, so that instead of the following: @@ -95,8 +126,13 @@ public static Result failure(F error, Class successType) { * .then(map(Shop::purchaseHat)); * } * + * @param biMapper function to apply result to + * @param parent type + * @param child type + * @return parent type of applying the mapper of result to WideType */ - public T then(Function, T2> biMapper) { + public WideType + then(@NotNull Function, NarrowType> biMapper) { return biMapper.apply(this); } @@ -108,12 +144,13 @@ private Success(L value) { } @Override - public S either(Function onSuccess, Function onFailure) { + public S either(@NotNull Function onSuccess, Function onFailure) { return onSuccess.apply(value); } + @Contract(pure = true) @Override - public String toString() { + public @NotNull String toString() { return "Success{" + value + '}'; } @@ -139,12 +176,13 @@ private Failure(R value) { } @Override - public S either(Function onSuccess, Function onFailure) { + public S either(Function onSuccess, @NotNull Function onFailure) { return onFailure.apply(value); } + @Contract(pure = true) @Override - public String toString() { + public @NotNull String toString() { return "Failure{" + value + '}'; } diff --git a/src/main/java/co/unruly/control/result/ResultCollector.java b/src/main/java/co/unruly/control/result/ResultCollector.java index fea78b8..ea8ee5d 100644 --- a/src/main/java/co/unruly/control/result/ResultCollector.java +++ b/src/main/java/co/unruly/control/result/ResultCollector.java @@ -1,6 +1,8 @@ package co.unruly.control.result; import co.unruly.control.pair.Pair; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; import java.util.ArrayList; import java.util.Collections; @@ -13,47 +15,40 @@ import java.util.stream.Collector; import java.util.stream.Stream; -import static java.util.Collections.unmodifiableList; import static java.util.stream.Collectors.toList; /** * Collects a Stream of Results into a Pair, with the left being a list of success values * and the right being a list of failure values. */ -class ResultCollector implements Collector, Pair, List>, T> { - - private final Function, List>, T> finisher; - - ResultCollector(Function, List>, T> finisher) { - this.finisher = finisher; - } +record ResultCollector( + Function, List>, T> finisher) implements Collector, Pair, List>, T> { + @Contract(pure = true) @Override - public Supplier, List>> supplier() { + public @NotNull Supplier, List>> supplier() { return () -> new Pair<>(new ArrayList<>(), new ArrayList<>()); } + @Contract(pure = true) @Override - public BiConsumer, List>, Result> accumulator() { - return (accumulator, Result) -> Result.either(accumulator.left::add, accumulator.right::add); + public @NotNull BiConsumer, List>, Result> accumulator() { + return (accumulator, Result) -> Result.either(accumulator.left()::add, accumulator.right()::add); } + @Contract(pure = true) @Override - public BinaryOperator, List>> combiner() { + public @NotNull BinaryOperator, List>> combiner() { return (x, y) -> Pair.of( - Stream.of(x, y).flatMap(l -> l.left.stream()).collect(toList()), - Stream.of(x, y).flatMap(r -> r.right.stream()).collect(toList()) + Stream.of(x, y).flatMap(l -> l.left().stream()).collect(toList()), + Stream.of(x, y).flatMap(r -> r.right().stream()).collect(toList()) ); } - @Override - public Function, List>, T> finisher() { - return finisher; - } - + @Contract(pure = true) @Override - public Set characteristics() { + public @NotNull Set characteristics() { return Collections.emptySet(); } } diff --git a/src/main/java/co/unruly/control/result/Transformers.java b/src/main/java/co/unruly/control/result/Transformers.java index 8cb1ce5..8acefa3 100644 --- a/src/main/java/co/unruly/control/result/Transformers.java +++ b/src/main/java/co/unruly/control/result/Transformers.java @@ -3,6 +3,8 @@ import co.unruly.control.ConsumableFunction; import co.unruly.control.HigherOrderFunctions; import co.unruly.control.ThrowingLambdas; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; import java.util.function.Consumer; import java.util.function.Function; @@ -14,20 +16,34 @@ /** * A collection of functions which take a Result and return a Result. */ +@SuppressWarnings("unused") public interface Transformers { /** * Returns a function which takes a Result and, if it's a success, applies the mapping value to that * success, otherwise returning the original failure. + * @param mappingFunction to pipe the success + * @param input success + * @param output success + * @param failure type + * @return dF(R(IS, F) -> R(OS, F)) IS -> mappingFunction -> OS */ - static Function, Result> onSuccess(Function mappingFunction) { + static @NotNull Function, Result> + onSuccess(@NotNull Function mappingFunction) { return attempt(mappingFunction.andThen(Result::success)); } /** * Returns a Consumer which takes a Result and, if it's a Success, passes it to the provided consumer. + * @param consumer to use on success + * @param success + * @param failure type + * @return dF(x -> x) where x is consumed by the + * consumer to produce a side effect if it's a success */ - static ConsumableFunction> onSuccessDo(Consumer consumer) { + @Contract(pure = true) + static @NotNull ConsumableFunction> + onSuccessDo(Consumer consumer) { return r -> r.then(onSuccess(peek(consumer))); } @@ -35,10 +51,20 @@ static ConsumableFunction> onSuccessDo(Consumer consumer) * Returns a function which takes a Result with an Exception failure type and, if it's a success, applies * the mapping value to that success, returning a new success unless an exception is thrown, when it * returns a failure of that exception. If the input was a failure, it returns that failure. + * @param throwingFunction to try to map the success but may throw + * @param input success + * @param output success + * @param exception type + * @return dF(R(IS, Exception) -> R(OS, Exception)) + *

    + * if input success -> R(IS -> OS) + *

    + * if input failure -> R(Exception) + *

    + * if throwingFunction failure -> R(X) */ - static Function, Result> onSuccessTry( - ThrowingLambdas.ThrowingFunction throwingFunction - ) { + static @NotNull Function, Result> + onSuccessTry(ThrowingLambdas.ThrowingFunction throwingFunction) { return attempt(tryTo(throwingFunction)); } @@ -48,11 +74,19 @@ static Function, Result input success + * @param output success + * @param fail type + * @param exception type + * @return dF(x -> y) where y uses the throwing function if + * x is a success AND if the throwing function throws instead + * uses the exception mapper to fail gracefully into a failure type */ - static Function, Result> onSuccessTry( - ThrowingLambdas.ThrowingFunction throwingFunction, - Function exceptionMapper - ) { + static @NotNull Function, Result> + onSuccessTry(ThrowingLambdas.ThrowingFunction throwingFunction, + Function exceptionMapper) { return attempt(tryTo(throwingFunction).andThen(onFailure(exceptionMapper))); } @@ -60,34 +94,66 @@ static Function, Result> o * Returns a function which takes a Result and, if it's a success, applies the provided function * to that success - generating a new Result - and returns that Result. Otherwise, returns the * original failure. + * @param mappingFunction to use on success + * @param input success + * @param output success + * @param parent failure type + * @param child failure type + * @return dF(R(InputSuccess, WF) -> R(OutputSuccess, WF)) */ - static Function, Result> attempt(Function> mappingFunction) { - return r -> r.either(mappingFunction.andThen(onFailure((Function) (fv) -> HigherOrderFunctions.upcast(fv))), Result::failure); + @Contract(pure = true) + static @NotNull + Function, Result> + attempt(Function> mappingFunction) { + Function upcaster = HigherOrderFunctions::upcast; + Function, Result> upcastOnFailure + = onFailure(upcaster); + return r -> r.either(mappingFunction.andThen(upcastOnFailure), Result::failure); } /** * Returns a function which takes a Result and, if it's a failure, applies the provided function * to that failure. Otherwise, returns the original success. + * @param mappingFunction to map the failure against + * @param success type + * @param input failure type + * @param output failure type + * @return dF(R(S, IF) -> R(S, OF)) where the output uses + * the mapping function if x is a failure */ - static Function, Result> onFailure(Function mappingFunction) { + static @NotNull + Function, Result> + onFailure(@NotNull Function mappingFunction) { return recover(mappingFunction.andThen(Result::failure)); } /** * Returns a consumer which takes a Result and, if it's a failure, passes it to the provided consumer + * @param consumer to use on failure + * @param success type + * @param failure type + * @return dF(x -> y) where if x is a failure it passes it to the consumer */ - static ConsumableFunction> onFailureDo(Consumer consumer) { + @Contract(pure = true) + static @NotNull ConsumableFunction> + onFailureDo(Consumer consumer) { return r -> r.then(onFailure(peek(consumer))); } /** * Takes a Result of a Stream as a success or a single failure, and returns a Stream of Results * containing all the successes, or the single failure. - * + *

    * The main use-case for this is to follow mapping over tryTo() on a function which was designed to * be flat-mapped over. + * @param success type + * @param fail type + * @return dF(x -> y) where y is either a stream of success results + * or a failure */ - static Function, F>, Stream>> unwrapSuccesses() { + @Contract(pure = true) + static @NotNull Function, F>, Stream>> + unwrapSuccesses() { return r -> r.either( successes -> successes.map(Result::success), failure -> Stream.of(Result.failure(failure)) @@ -98,53 +164,83 @@ static Function, F>, Stream>> unwrapSuccess * Returns a function which takes a Result and, if it's a failure, applies the provided function to * that failure - generating a new Result - and returns that Result. Otherwise, return the original * success. + * @param recoveryFunction function to generate recovery result if input is a failure + * @param success type + * @param input fail type + * @param output fail type + * @param output success type + * @return dF(R(S, IF) -> R(S/OS, OF)) where + * R = success -> R(S) + * R = failure -> recoveryFunction(IF) -> R(OS, OF) */ - static Function, Result> recover(Function> recoveryFunction) { - return r -> r.either(Result::success, recoveryFunction.andThen(onSuccess((Function) (fv) -> HigherOrderFunctions.upcast(fv)))); + @Contract(pure = true) + static @NotNull Function, Result> + recover(Function> recoveryFunction) { + return r -> r.either( + Result::success, + recoveryFunction.andThen(onSuccess((Function) HigherOrderFunctions::upcast))); } /** * Returns a function which takes a Result, and converts failures to successes and vice versa. + * @param success type + * @param fail type + * @return dF(x -> y) where if x is R(S, F) then y is R(F, S) */ - static Function, Result> invert() { + @Contract(pure = true) + static @NotNull Function, Result> invert() { return r -> r.either(Result::failure, Result::success); } /** - * Returns a function which takes a Result whose success type is itself a Result, and merges the failure cases + * Returns a function which takes a Result whose success type is itself a Result, and merges the failure cases, * so we have a flat Result. - * + *

    * Note that *most of the time* this shouldn't be required, and indicates using onSuccess() when attempt() would * be more appropriate. - * + *

    * There are some situations though where we do end up with constructs like this: one example * is when a function which returns a Result can throw exceptions (eg, when a Result-returning handler is passed * into a database context). Passing that call into a tryTo() will yield a success type of the inner Result, wrapped * in an outer Result for the tryTo(). - * + *

    * If the failure types of the inner and outer failure types do not match, you'll need to either first convert the * failures of the outer Result or use the overload which maps the failures of the inner Result. + * @param success type + * @param fail type + * @return dF(x -> y) where x of the form R(R(S, FInner) FOuter) + * y is the result of merging x's failures into R(S, FMerged) */ - static Function, F>, Result> mergeFailures() { + @Contract(pure = true) + static @NotNull Function, F>, Result> + mergeFailures() { return attempt(i -> i); } /** - * Returns a function which takes a Result whose success type is itself a Result, and merges the failure cases + * Returns a function which takes a Result whose success type is itself a Result, and merges the failure cases, * so we have a flat Result. - * + *

    * Note that *most of the time* this shouldn't be required, and indicates using onSuccess() when attempt() would * be more appropriate. - * + *

    * There are some situations though where we do end up with constructs like this: one example * is when a function which returns a Result can throw exceptions (eg, when a Result-returning handler is passed * into a database context). Passing that call into a tryTo() will yield a success type of the inner Result, wrapped * in an outer Result for the tryTo(). - * + *

    * In these cases, it's more likely the inner failure type is domain-specific, so the default approach is to map the * outer failure to the inner failure and then merge. + * @param failureMapper f(F2 -> F1) merges f2 into f1 + * @param success type + * @param left fail type + * @param right fail type + * @return dF(x -> y) where R is a result function yielding F2 + * y is a result where the failure is merged if necessary */ - static Function, F2>, Result> mergeFailures(Function failureMapper) { + @Contract(pure = true) + static @NotNull Function, F2>, Result> + mergeFailures(Function failureMapper) { return r -> r.then(onFailure(failureMapper)).then(mergeFailures()); } } diff --git a/src/main/java/co/unruly/control/result/TypeOf.java b/src/main/java/co/unruly/control/result/TypeOf.java index b3ea090..76016d5 100644 --- a/src/main/java/co/unruly/control/result/TypeOf.java +++ b/src/main/java/co/unruly/control/result/TypeOf.java @@ -1,6 +1,9 @@ package co.unruly.control.result; import co.unruly.control.HigherOrderFunctions; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.function.Function; @@ -8,47 +11,84 @@ /** * Some syntax-fu in order to get nice, readable up-casting operations on Results. - * + *

    * Usage isn't totally obvious from the implementation: to upcast a success to an Animal, for example, you need: * - * using(TypeOf.forSuccesses()) + * using(TypeOf.{@literal ()}forSuccesses()) * - * - * That'll give you a Function, Function> (inferring + *

    + * That'll give you a {@code (Function, Function>)} (inferring * the types Animal and String from context), which you can then use for mapping a Stream or use in * a Result then-operation chain. */ -public interface TypeOf { +@SuppressWarnings("unused") +public interface TypeOf { /** * Generalises the success type for a Result to an appropriate superclass. + * @param dummy class for overload differentiation + * @param success parent type + * @param failure type + * @param success child type + * @return f(R(NS, F) -> R(WS, F)) */ - static Function, Result> using(ForSuccesses dummy) { - return result -> result.then(onSuccess(HigherOrderFunctions::upcast)); + @Contract(pure = true) + static @NotNull + Function, Result> + using(ForSuccesses dummy) { + Function, Result> upcastSuccess + = onSuccess(HigherOrderFunctions::upcast); + return result -> result.then(upcastSuccess); } /** * Generalises the failure type for a Result to an appropriate superclass. + * @param dummy class for overload differentiation + * @param success type + * @param > failure parent type + * @param failure child type + * @return result with failure upcasted */ - static Function, Result> using(ForFailures dummy) { - return result -> result.then(Transformers.onFailure(HigherOrderFunctions::upcast)); + @Contract(pure = true) + static @NotNull + Function, Result> + using(ForFailures dummy) { + Function, Result> upcastFailure + = Transformers.onFailure(HigherOrderFunctions::upcast); + return result -> result.then(upcastFailure); } + /** + * @param input type + * @return null + */ // we don't use the return value - all this does is provide type context - static ForSuccesses forSuccesses() { + @Contract(pure = true) + static @Nullable ForSuccesses forSuccesses() { return null; } + /** + * @param input type + * @return null + */ // we don't use the return value - all this does is provide type context - static ForFailures forFailures() { + @Contract(pure = true) + static @Nullable ForFailures forFailures() { return null; } - // this class only exists so we can differentiate the overloads of using() + /** + * @param input type + */ + // this class only exists, so we can differentiate the overloads of using() // we don't even instantiate it class ForSuccesses { } - // this class only exists so we can differentiate the overloads of using() + /** + * @param input type + */ + // this class only exists, so we can differentiate the overloads of using() // we don't even instantiate it class ForFailures { } } diff --git a/src/main/java/co/unruly/control/validation/FailedValidation.java b/src/main/java/co/unruly/control/validation/FailedValidation.java index 609f24c..daf3862 100644 --- a/src/main/java/co/unruly/control/validation/FailedValidation.java +++ b/src/main/java/co/unruly/control/validation/FailedValidation.java @@ -1,20 +1,22 @@ package co.unruly.control.validation; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + import java.util.List; import java.util.Objects; -public final class FailedValidation implements ForwardingList { - - public final T value; - public final List errors; - - public FailedValidation(T value, List errors) { - this.value = value; - this.errors = errors; - } +/** + * @param value value under consideration + * @param errors errors associated with value + * @param value type + * @param error type + */ +public record FailedValidation(T value, List errors) implements ForwardingList { + @Contract(pure = true) @Override - public String toString() { + public @NotNull String toString() { return "FailedValidation{" + "value=" + value + ", errors=" + errors + @@ -27,7 +29,7 @@ public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; FailedValidation that = (FailedValidation) o; return Objects.equals(value, that.value) && - Objects.equals(errors, that.errors); + Objects.equals(errors, that.errors); } @Override @@ -35,6 +37,8 @@ public int hashCode() { return Objects.hash(value, errors); } + + @Override public List delegate() { return errors; diff --git a/src/main/java/co/unruly/control/validation/ForwardingList.java b/src/main/java/co/unruly/control/validation/ForwardingList.java index 3f045d5..4acc744 100644 --- a/src/main/java/co/unruly/control/validation/ForwardingList.java +++ b/src/main/java/co/unruly/control/validation/ForwardingList.java @@ -1,13 +1,24 @@ package co.unruly.control.validation; +import org.jetbrains.annotations.NotNull; + import java.util.*; import java.util.function.Consumer; import java.util.function.Predicate; import java.util.function.UnaryOperator; import java.util.stream.Stream; +/** + * @param list type + * here is a link to forward lists in C++ + * link + * It /should/ be the same principle + */ public interface ForwardingList extends List { + /** + * @return delegate list + */ List delegate(); default int size() { @@ -22,15 +33,15 @@ default boolean contains(Object o) { return delegate().contains(o); } - default Iterator iterator() { + default @NotNull Iterator iterator() { return delegate().iterator(); } - default Object[] toArray() { + default Object @NotNull [] toArray() { return delegate().toArray(); } - default T1[] toArray(T1[] a) { + default T1 @NotNull [] toArray(T1 @NotNull [] a) { return delegate().toArray(a); } @@ -42,23 +53,23 @@ default boolean remove(Object o) { return delegate().remove(o); } - default boolean containsAll(Collection c) { - return delegate().containsAll(c); + default boolean containsAll(@NotNull Collection c) { + return new HashSet<>(delegate()).containsAll(c); } - default boolean addAll(Collection c) { + default boolean addAll(@NotNull Collection c) { return delegate().addAll(c); } - default boolean addAll(int index, Collection c) { + default boolean addAll(int index, @NotNull Collection c) { return delegate().addAll(index, c); } - default boolean removeAll(Collection c) { + default boolean removeAll(@NotNull Collection c) { return delegate().removeAll(c); } - default boolean retainAll(Collection c) { + default boolean retainAll(@NotNull Collection c) { return delegate().retainAll(c); } @@ -98,15 +109,15 @@ default int lastIndexOf(Object o) { return delegate().lastIndexOf(o); } - default ListIterator listIterator() { + default @NotNull ListIterator listIterator() { return delegate().listIterator(); } - default ListIterator listIterator(int index) { + default @NotNull ListIterator listIterator(int index) { return delegate().listIterator(index); } - default List subList(int fromIndex, int toIndex) { + default @NotNull List subList(int fromIndex, int toIndex) { return delegate().subList(fromIndex, toIndex); } diff --git a/src/main/java/co/unruly/control/validation/Validator.java b/src/main/java/co/unruly/control/validation/Validator.java index e69cac1..c55d725 100644 --- a/src/main/java/co/unruly/control/validation/Validator.java +++ b/src/main/java/co/unruly/control/validation/Validator.java @@ -8,6 +8,10 @@ import static java.util.stream.Collectors.toList; +/** + * @param input + * @param errors + */ @FunctionalInterface public interface Validator extends Function>> { @@ -15,9 +19,13 @@ default Result> apply(T item) { List errors = validate(item).collect(toList()); return errors.isEmpty() ? Result.success(item) - : Result.failure(new FailedValidation(item, errors)); + : Result.failure(new FailedValidation<>(item, errors)); } + /** + * @param item input + * @return stream of errors E + */ Stream validate(T item); } diff --git a/src/main/java/co/unruly/control/validation/Validators.java b/src/main/java/co/unruly/control/validation/Validators.java index 9372fb7..8dc8121 100644 --- a/src/main/java/co/unruly/control/validation/Validators.java +++ b/src/main/java/co/unruly/control/validation/Validators.java @@ -4,6 +4,8 @@ import co.unruly.control.Optionals; import co.unruly.control.ThrowingLambdas.ThrowingFunction; import co.unruly.control.result.Result; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; import java.util.Arrays; import java.util.function.BiFunction; @@ -18,46 +20,146 @@ import static co.unruly.control.result.Transformers.onFailure; import static co.unruly.control.result.Transformers.recover; +/** + * interface for working with validation + */ +@SuppressWarnings("unused") public interface Validators { + /** + * @param validators arbitrary set of validators + * @param input type + * @param error type + * @return dF(x -> y) where dF represents the composition of the validators + * and x is the input that is threaded through all of them + */ + @Contract(pure = true) @SafeVarargs - public static Validator compose(Validator... validators) { + static @NotNull Validator + compose(Validator... validators) { return t -> Arrays.stream(validators).flatMap(v -> v.validate(t)); } - public static Validator rejectIf(Predicate test, E error) { + /** + * @param test predicate function + * @param error error type + * @param input type + * @param error type + * @return dF(x -> y) where x yields error if passing test + */ + static @NotNull Validator + rejectIf(@NotNull Predicate test, E error) { return acceptIf(test.negate(), error); } - public static Validator rejectIf(Predicate test, Function errorGenerator) { + /** + * @param test predicate function + * @param errorGenerator f(T -> E) + * @param input type + * @param error type + * @return dF(x -> y) where x yields output from errorGenerator + * if passing test + */ + static @NotNull Validator + rejectIf(@NotNull Predicate test, Function errorGenerator) { return acceptIf(test.negate(), errorGenerator); } - public static Validator acceptIf(Predicate test, E error) { + /** + * @param test predicate function + * @param error error type + * @param input type + * @param error type + * @return dF(x -> y) where x yields error if failing test + */ + @Contract(pure = true) + static @NotNull Validator + acceptIf(Predicate test, E error) { return acceptIf(test, t -> error); } - public static Validator acceptIf(Predicate test, Function errorGenerator) { + /** + * @param test predicate function + * @param errorGenerator generates errors from T + * @param input type + * @param error type + * @return dF(x -> y) where if x fails test, it returns + * a stream of E from errorGenerator + */ + @Contract(pure = true) + static @NotNull Validator + acceptIf(Predicate test, Function errorGenerator) { return t -> test.test(t) ? Stream.empty() : Stream.of(errorGenerator.apply(t)); } - public static Validator firstOf(Validator validator) { + /** + * @param validator validation function + * @param input type + * @param output type + * @return optional stream getting the first result of the stream validation + */ + @Contract(pure = true) + static @NotNull Validator firstOf(Validator validator) { return t -> Optionals.stream(validator.validate(t).findFirst()); } - public static Validator onlyIf(Predicate test, Validator validator) { + /** + * @param test predicate function + * @param validator validation function + * @param input type + * @param error type + * @return dF(x -> y) where x is tested and, if passed, is then validated + * otherwise it returns an empty stream + */ + @Contract(pure = true) + static @NotNull Validator onlyIf(Predicate test, Validator validator) { return t -> test.test(t) ? validator.validate(t) : Stream.empty(); } - public static Validator mappingErrors(Validator validator, BiFunction errorMapper) { + /** + * @param validator maps input to validation outcome + * @param errorMapper f((T, E) -> E1) maps input and error outcome to new error type + * @param input type + * @param input error type + * @param output error type + * @return dF(x -> y) where x is validated and mapped with any yielded errors into + * the mapper + */ + @Contract(pure = true) + static @NotNull Validator + mappingErrors(Validator validator, BiFunction errorMapper) { return t -> validator.validate(t).map(e -> errorMapper.apply(t, e)); } - public static Validator on(Function accessor, Validator innerValidator) { + /** + * @param accessor f(T -> T1) + * @param innerValidator validator for T1 + * @param input type + * @param output type + * @param error type + * @return dF(x -> y) where x converted to T1 and validated + */ + @Contract(pure = true) + static @NotNull Validator + on(Function accessor, Validator innerValidator) { return t -> innerValidator.validate(accessor.apply(t)); } - public static Validator tryOn(ThrowingFunction accessor, Function onException, Validator innerValidator) { + /** + * @param accessor function that converts T -> T1 or throws + * @param onException f(exception -> E), called when accessor throws + * @param innerValidator validates T1 or yields E + * @param input type + * @param output type + * @param error type + * @param exception type + * @return dF(x -> y) where x is provided to accessor and then validated + * y is the output of the validation wrapped in a try-catch where + * the failure maps the exception to an error state + */ + @Contract(pure = true) + static @NotNull Validator + tryOn(ThrowingFunction accessor, Function onException, Validator innerValidator) { return t -> { try { return innerValidator.validate(accessor.apply(t)); @@ -67,11 +169,33 @@ public static Validator tryOn(ThrowingFunc }; } - public static Validator onEach(Function> iterator, Validator innerValidator) { - return t -> StreamSupport.stream(iterator.apply(t).spliterator(), false).flatMap(innerValidator::validate); + /** + * @param iterator f(x1 -> Iterable(x2)) + * @param innerValidator f(x2 -> E) + * @param input type + * @param output type + * @param error type + * @return validator outcome + */ + @Contract(pure = true) + static @NotNull Validator + onEach(Function> iterator, Validator innerValidator) { + return t -> StreamSupport.stream(iterator.apply(t).spliterator(), false) + .flatMap(innerValidator::validate); } - public static Validator tryTo(Validator validatorWhichThrowsRuntimeExceptions, Function errorMapper) { + /** + * @param validatorWhichThrowsRuntimeExceptions validator function that throws + * @param errorMapper f(exception -> error type) + * @param success type + * @param error type + * @return dF(x -> y) where x is an input to our validator function + * and y is the result of the validation or a stream of the error mapper + * if an exception is thrown + */ + @Contract(pure = true) + static @NotNull Validator + tryTo(Validator validatorWhichThrowsRuntimeExceptions, Function errorMapper) { return t -> { try { return validatorWhichThrowsRuntimeExceptions.validate(t); @@ -81,10 +205,25 @@ public static Validator tryTo(Validator validatorWhichThrowsR }; } - public static Function>, Result>> ignoreWhen(Predicate filterCondition) { - return result -> result - .then(onFailure(fv -> new FailedValidation<>(fv.value, fv.errors.stream().filter(filterCondition.negate()).collect(Collectors.toList())))) - .then(recover(fv -> fv.errors.isEmpty() ? success(fv.value) : failure(fv))); + /** + * @param filterCondition predicate function to filter the failure validation + * @param success + * @param errors(?) + * @return a function that expects a result, and does some filtering + * based on the failure outcome + */ + @Contract(pure = true) + static @NotNull Function>, Result>> + ignoreWhen(Predicate filterCondition) { + Function, FailedValidation> filteredFailedValidation = fv -> { + var t = fv.errors().stream() + .filter(filterCondition.negate()) + .collect(Collectors.toList()); + return new FailedValidation<>(fv.value(), t); + }; + Function, Result>> isEmptyResult = + fv -> fv.errors().isEmpty() ? success(fv.value()) : failure(fv); + return result -> result.then(onFailure(filteredFailedValidation)).then(recover(isEmptyResult)); } } diff --git a/src/test/java/co/unruly/control/ThrowingLambdasTest.java b/src/test/java/co/unruly/control/ThrowingLambdasTest.java index e9c87c1..1e12ef8 100644 --- a/src/test/java/co/unruly/control/ThrowingLambdasTest.java +++ b/src/test/java/co/unruly/control/ThrowingLambdasTest.java @@ -1,5 +1,6 @@ package co.unruly.control; +import org.jetbrains.annotations.NotNull; import org.junit.Test; import java.util.function.Predicate; @@ -7,6 +8,7 @@ import static co.unruly.control.ThrowingLambdas.ThrowingPredicate.throwingRuntime; import static org.junit.Assert.assertTrue; +@SuppressWarnings("RedundantThrows") public class ThrowingLambdasTest { // @Test @@ -17,6 +19,7 @@ public class ThrowingLambdasTest { @Test public void canHandleThrowingMethodsWithAppropriateFunctionalInterfaceType() { assertTrue(tryToTest(2, ThrowingLambdasTest::dodgyIsEven)); + assertTrue(tryToTest(4, ThrowingLambdasTest::dodgyIsEven)); } @Test @@ -32,9 +35,10 @@ public void canConvertThrowingLambdasToNonThrowingLambdas() throws Exception { @Test public void canConvertMultiThrowingLambdasToNonThrowingLambdas() throws Exception { assertTrue(test(2, throwingRuntime(ThrowingLambdasTest::veryDodgyIsEven))); + assertTrue(test(4, throwingRuntime(ThrowingLambdasTest::veryDodgyIsEven))); } - private static boolean test(T item, Predicate test) { + private static boolean test(T item, @NotNull Predicate test) { return test.test(item); } diff --git a/src/test/java/co/unruly/control/ZipTest.java b/src/test/java/co/unruly/control/ZipTest.java index 9857dd9..1f66c49 100644 --- a/src/test/java/co/unruly/control/ZipTest.java +++ b/src/test/java/co/unruly/control/ZipTest.java @@ -11,6 +11,7 @@ import static co.unruly.control.HigherOrderFunctions.zip; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; +import static org.junit.Assert.assertEquals; public class ZipTest { @@ -20,8 +21,10 @@ public void zipsItemsTogether() { Stream.of("hello", "goodbye"), Stream.of("world", "cruel world")) .collect(Collectors.toList()); - - assertThat(pairs, contains(Pair.of("hello", "world"), Pair.of("goodbye", "cruel world"))); + var listPairs = List.of( + Pair.of("hello", "world"), + Pair.of("goodbye", "cruel world")); + assertEquals(listPairs, pairs); } @Test @@ -50,7 +53,11 @@ public void generatedStreamIsShorterOfInputStreams() { public void buildsIndexedList() { List> indexed = withIndices(Stream.of("zero", "one", "two", "three")) .collect(Collectors.toList()); - - assertThat(indexed, contains(Pair.of(0, "zero"), Pair.of(1, "one"), Pair.of(2, "two"), Pair.of(3, "three"))); + var comparison = List.of( + Pair.of(0, "zero"), + Pair.of(1, "one"), + Pair.of(2, "two"), + Pair.of(3, "three")); + assertEquals(indexed, comparison); } } diff --git a/src/test/java/co/unruly/control/pair/PairTest.java b/src/test/java/co/unruly/control/pair/PairTest.java index a168203..cd140eb 100644 --- a/src/test/java/co/unruly/control/pair/PairTest.java +++ b/src/test/java/co/unruly/control/pair/PairTest.java @@ -54,15 +54,15 @@ public void canCollectToParallelArrays() { Pair parallelArrays = Stream.of(Pair.of(2, "hello"), Pair.of(4, "goodbye")) .collect(toArrays(Integer[]::new, String[]::new)); - assertThat(asList(parallelArrays.left), is(asList(2, 4))); - assertThat(asList(parallelArrays.right), is(asList("hello", "goodbye"))); + assertThat(asList(parallelArrays.left()), is(asList(2, 4))); + assertThat(asList(parallelArrays.right()), is(asList("hello", "goodbye"))); } @Test public void canReduceAStreamOfPairs() { Pair reduced = Stream.of(Pair.of(2, "hello"), Pair.of(4, "goodbye")) .collect(reducing( - 0, (x, y) -> x + y, + 0, Integer::sum, "", String::concat )); diff --git a/src/test/java/co/unruly/control/result/CastsTest.java b/src/test/java/co/unruly/control/result/CastsTest.java index a5205ff..16e3306 100644 --- a/src/test/java/co/unruly/control/result/CastsTest.java +++ b/src/test/java/co/unruly/control/result/CastsTest.java @@ -6,7 +6,7 @@ import static co.unruly.control.matchers.ResultMatchers.isFailureOf; import static co.unruly.control.matchers.ResultMatchers.isSuccessOf; import static co.unruly.control.result.Introducers.castTo; -import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; public class CastsTest { @@ -17,7 +17,7 @@ public void castingToCorrectTypeYieldsSuccess() { Result cast = pipe(helloWorld) .resolveWith(castTo(String.class)); - assertThat(cast, isSuccessOf("Hello World")); + assertTrue(isSuccessOf("Hello World").matches(cast)); } @Test @@ -27,6 +27,6 @@ public void castingToIncorrectTypeYieldsFailure() { Result cast = pipe(helloWorld) .resolveWith(castTo(Integer.class)); - assertThat(cast, isFailureOf("Hello World")); + assertTrue(isFailureOf("Hello World").matches(cast)); } } diff --git a/src/test/java/co/unruly/control/result/MatchTest.java b/src/test/java/co/unruly/control/result/MatchTest.java index cf19983..2028862 100644 --- a/src/test/java/co/unruly/control/result/MatchTest.java +++ b/src/test/java/co/unruly/control/result/MatchTest.java @@ -9,7 +9,7 @@ import static co.unruly.control.result.Match.matchValue; import static co.unruly.control.result.Recover.*; import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; public class MatchTest { @@ -19,10 +19,10 @@ public void canMatchOnTypeWithFlowTyping() { ifType(B.class, B::messageForB), ifType(C.class, C::messageForC) ).otherwise(A::message); - - assertThat(matchByType.apply(new A("Cheese")), is("Cheese")); - assertThat(matchByType.apply(new B("Ketchup")), is("I'm a B and I say Ketchup")); - assertThat(matchByType.apply(new C("Pickles")), is("I'm a C and I say Pickles")); + var bMatch = matchByType.apply(new B("Ketchup")); + assertTrue(is("Cheese").matches(matchByType.apply(new A("Cheese")))); + assertTrue(is("I'm a B and I say Ketchup").matches(bMatch)); + assertTrue(is("I'm a C and I say Pickles").matches(matchByType.apply(new C("Pickles")))); } @Test @@ -31,11 +31,11 @@ public void canMatchOnValue() { ifEquals(4, x -> x + " sure looks like a 4 to me!"), ifEquals(7, x -> x + " looks like one of them gosh-darned 7s?") ).otherwise(x -> "I have no idea what a " + x + " is though..."); - - assertThat(matchByType.apply(3), is("I have no idea what a 3 is though...")); - assertThat(matchByType.apply(4), is("4 sure looks like a 4 to me!")); - assertThat(matchByType.apply(6), is("I have no idea what a 6 is though...")); - assertThat(matchByType.apply(7), is("7 looks like one of them gosh-darned 7s?")); + var nineMatch = matchByType.apply(7); + assertTrue(is("I have no idea what a 3 is though...").matches(matchByType.apply(3))); + assertTrue(is("4 sure looks like a 4 to me!").matches(matchByType.apply(4))); + assertTrue(is("I have no idea what a 6 is though...").matches(matchByType.apply(6))); + assertTrue(is("7 looks like one of them gosh-darned 7s?").matches(nineMatch)); } @Test @@ -45,10 +45,10 @@ public void canMatchOnTest() { ifIs(x -> x < 0, x -> x + " is one of those banker's negative number thingies") ).otherwise(x -> x + " is a regular, god-fearing number for god-fearing folks"); - assertThat(matchByType.apply(2), is("2, well, that's one of those even numbers")); - assertThat(matchByType.apply(-6), is("-6, well, that's one of those even numbers")); - assertThat(matchByType.apply(3), is("3 is a regular, god-fearing number for god-fearing folks")); - assertThat(matchByType.apply(-9), is("-9 is one of those banker's negative number thingies")); + assertTrue(is("2, well, that's one of those even numbers").matches(matchByType.apply(2))); + assertTrue(is("-6, well, that's one of those even numbers").matches(matchByType.apply(-6))); + assertTrue(is("3 is a regular, god-fearing number for god-fearing folks").matches(matchByType.apply(3))); + assertTrue(is("-9 is one of those banker's negative number thingies").matches(matchByType.apply(-9))); } @Test @@ -58,7 +58,7 @@ public void canMatchOnTestPassingArgument() { ifIs(x -> x < 0, x -> x + " is one of those banker's negative number thingies") ).otherwise(x -> x + " is a regular, god-fearing number for god-fearing folks"); - assertThat(matchByResult, is("4, well, that's one of those even numbers")); + assertTrue(is("4, well, that's one of those even numbers").matches(matchByResult)); } @Test @@ -69,7 +69,7 @@ public void canOperateOverAListOfOptionalProviders() { ifPresent(Things::c) ).otherwise(__ -> "Ketchup!"); - assertThat(cheese, is("Cheese!")); + assertTrue(is("Cheese!").matches(cheese)); } @@ -81,14 +81,14 @@ public void usesDefaultIfNoOptionalProvidersProvideAValue() { ifPresent(Things::c) ).otherwise(__ -> "Ketchup!"); - assertThat(cheese, is("Ketchup!")); + assertTrue(is("Ketchup!").matches(cheese)); } @Test public void useMatchToCalculateFactorial() { - assertThat(factorial(0), is(1)); - assertThat(factorial(1), is(1)); - assertThat(factorial(6), is(720)); + assertTrue(is(1).matches(factorial(0))); + assertTrue(is(1).matches(factorial(1))); + assertTrue(is(720).matches(factorial(6))); } @Test(expected = IllegalArgumentException.class) diff --git a/src/test/java/co/unruly/control/result/PiperTest.java b/src/test/java/co/unruly/control/result/PiperTest.java index 0def996..5c90da5 100644 --- a/src/test/java/co/unruly/control/result/PiperTest.java +++ b/src/test/java/co/unruly/control/result/PiperTest.java @@ -12,7 +12,7 @@ import static co.unruly.control.result.Introducers.tryTo; import static co.unruly.control.result.Transformers.attempt; import static co.unruly.control.result.Transformers.onFailure; -import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; public class PiperTest { @@ -24,10 +24,10 @@ public void canChainSeveralOperationsBeforeOneWhichMayFail() { .then(pattern::matcher) .then(ifIs(Matcher::find, m -> m.group(1))) .then(onFailure(__ -> "Could not find group to match")) - .then(attempt(tryTo(Integer::parseInt, ex -> ex.getMessage()))) + .then(attempt(tryTo(Integer::parseInt, Throwable::getMessage))) .resolve(); - assertThat(result, isSuccessOf(1234)); + assertTrue(isSuccessOf(1234).matches(result)); } @Test @@ -38,10 +38,10 @@ public void canChainSeveralOperationsBeforeOneWhichMayFail2() { .then(pattern::matcher) .then(ifIs(Matcher::find, m -> m.group(1))) .then(onFailure(__ -> "Could not find group to match")) - .then(attempt(tryTo(Integer::parseInt, ex -> ex.getMessage()))) + .then(attempt(tryTo(Integer::parseInt, Throwable::getMessage))) .resolve(); - assertThat(result, isFailureOf("Could not find group to match")); + assertTrue(isFailureOf("Could not find group to match").matches(result)); } @Test @@ -55,6 +55,6 @@ public void canChainSeveralOperationsBeforeOneWhichMayFail3() { .then(attempt(tryTo(Integer::parseInt, ex -> "Parse failure: " + ex.getMessage()))) .resolve(); - assertThat(result, isFailureOf("Parse failure: For input string: \"a\"")); + assertTrue(isFailureOf("Parse failure: For input string: \"a\"").matches(result)); } } diff --git a/src/test/java/co/unruly/control/result/ResultsTest.java b/src/test/java/co/unruly/control/result/ResultsTest.java index c02c787..53fac7e 100644 --- a/src/test/java/co/unruly/control/result/ResultsTest.java +++ b/src/test/java/co/unruly/control/result/ResultsTest.java @@ -1,5 +1,6 @@ package co.unruly.control.result; +import co.unruly.control.ConsumableFunction; import co.unruly.control.Lists; import co.unruly.control.pair.Comprehensions; import co.unruly.control.pair.Pair; @@ -28,46 +29,43 @@ import static java.util.stream.Collectors.toList; import static org.hamcrest.CoreMatchers.hasItems; import static org.hamcrest.core.Is.is; -import static org.junit.Assert.assertThat; +import static org.junit.Assert.*; import static org.mockito.Mockito.*; - +@SuppressWarnings("unchecked") public class ResultsTest { @Test public void canCreateSuccess() { Result shouldBeSuccess = success(5); - - assertThat(shouldBeSuccess.either(success -> true, failure -> false), is(true)); + Boolean isSuccess = shouldBeSuccess.either(success -> true, failure -> false); + assertTrue(isSuccess); } @Test public void canCreateFailure() { Result shouldFail = failure("oh poop"); - - assertThat(shouldFail.either(success -> true, failure -> false), is(false)); + assertFalse(shouldFail.either(success -> true, failure -> false)); } @Test public void canReduceResultToValue() { Result success = success(5); Result failure = failure("i blew up"); - - assertThat(success.either( + String successString = success.either( succ -> String.format("I got %d out of this Result", succ), - err -> err), - is("I got 5 out of this Result")); - - assertThat(failure.either( + err -> err); + String failString = failure.either( succ -> String.format("I got %d out of this Result", succ), - err -> err), - is("i blew up")); + err -> err); + assertEquals(successString, "I got 5 out of this Result"); + assertEquals(failString, "i blew up"); } @Test public void canDoSideEffectsOnCorrectSideForSuccess() { - final Consumer successCallback = mock(Consumer.class); - final Consumer failureCallback = mock(Consumer.class); + final Consumer successCallback = (Consumer) mock(Consumer.class); + final Consumer failureCallback = (Consumer) mock(Consumer.class); Result success = success(5); @@ -76,22 +74,22 @@ public void canDoSideEffectsOnCorrectSideForSuccess() { .then(onFailureDo(failureCallback)); verify(successCallback).accept(5); - verifyZeroInteractions(failureCallback); + verifyNoInteractions(failureCallback); } @Test public void canDoSideEffectsOnCorrectSideForFailure() { - final Consumer successCallback = mock(Consumer.class); - final Consumer failureCallback = mock(Consumer.class); + final Consumer successCallback = (Consumer) mock(Consumer.class); + final Consumer failureCallback = (Consumer) mock(Consumer.class); Result failure = failure("oops"); - + ConsumableFunction> failureDo = onFailureDo(failureCallback); failure .then(onSuccessDo(successCallback)) - .then(onFailureDo(failureCallback)); + .then(failureDo); verify(failureCallback).accept("oops"); - verifyZeroInteractions(successCallback); + verifyNoInteractions(successCallback); } @Test @@ -104,9 +102,10 @@ public void flatMapsSuccessesIntoAppropriateValues() { final Result failure = failure("Cannot parse number"); - assertThat(six.then(attempt(halve)), isSuccessOf(3)); - assertThat(five.then(attempt(halve)), isFailureOf("Cannot halve an odd number into an integer")); - assertThat(failure.then(attempt(halve)), isFailureOf("Cannot parse number")); + assertTrue(isSuccessOf(3).matches(six.then(attempt(halve)))); + assertTrue(isFailureOf("Cannot halve an odd number into an integer") + .matches(five.then(attempt(halve)))); + assertTrue(isFailureOf("Cannot parse number").matches(failure.then(attempt(halve)))); } @Test @@ -117,8 +116,8 @@ public void canMapSuccesses() { final Result twelve = six.then(onSuccess(x -> x * 2)); final Result stillFailure = failure.then(onSuccess(x -> x * 2)); - assertThat(twelve, isSuccessOf(12)); - assertThat(stillFailure, is(failure)); + assertTrue(isSuccessOf(12).matches(twelve)); + assertTrue(is(failure).matches(stillFailure)); } @Test @@ -129,8 +128,8 @@ public void canMapFailures() { final Result stillSix = six.then(onFailure(String::toLowerCase)); final Result lowerCaseFailure = failure.then(onFailure(String::toLowerCase)); - assertThat(stillSix, Is.is(success(6))); - assertThat(lowerCaseFailure, Is.is(failure("cannot parse number"))); + assertTrue(Is.is(success(6)).matches(stillSix)); + assertTrue(Is.is(failure("cannot parse number")).matches(lowerCaseFailure)); } @Test @@ -141,18 +140,19 @@ public void canMapOverExceptionThrowingMethods() { Result parsedSix = six.then(onSuccessTry(Long::parseLong, Exception::toString)); Result parsedNaN = notANumber.then(onSuccessTry(Long::parseLong, Exception::toString)); - assertThat(parsedSix, Is.is(success(6L))); - assertThat(parsedNaN, Is.is(failure("java.lang.NumberFormatException: For input string: \"NaN\""))); + assertTrue(Is.is(success(6L)).matches(parsedSix)); + assertTrue(Is.is(failure("java.lang.NumberFormatException: For input string: \"NaN\"")).matches(parsedNaN)); } @Test public void canStreamSuccesses() { - Stream> results = Stream.of(success(6), success(5), failure("darnit")); + Stream> results + = Stream.of(success(6), success(5), failure("darnit")); Stream resultStream = results.flatMap(successes()); List successes = resultStream.collect(toList()); - assertThat(successes, hasItems(6, 5)); + assertTrue(hasItems(6, 5).matches(successes)); } @Test @@ -163,45 +163,55 @@ public void canMergeOperationsOnTwoResults() { Result oddFive = Result.failure("Five is odd"); Result oddSeven = Result.failure("Seven is odd"); - assertThat(evenSix.then(combineWith(evenTwo)).using((x, y) -> x * y), isSuccessOf(12)); - assertThat(evenSix.then(combineWith(oddSeven)).using((x, y) -> x * y), isFailureOf("Seven is odd")); - assertThat(oddFive.then(combineWith(evenTwo)).using((x, y) -> x * y), isFailureOf("Five is odd")); - assertThat(oddFive.then(combineWith(oddSeven)).using((x, y) -> x * y), isFailureOf("Five is odd")); + assertTrue(isSuccessOf(12) + .matches(evenSix.then(combineWith(evenTwo)).using((x, y) -> x * y))); + assertTrue(isFailureOf("Seven is odd") + .matches(evenSix.then(combineWith(oddSeven)).using((x, y) -> x * y))); + assertTrue(isFailureOf("Five is odd") + .matches(oddFive.then(combineWith(evenTwo)).using((x, y) -> x * y))); + assertTrue(isFailureOf("Five is odd") + .matches(oddFive.then(combineWith(oddSeven)).using((x, y) -> x * y))); } @Test public void canStreamFailures() { - Stream> results = Stream.of(success(6), success(5), failure("darnit")); + Stream> results + = Stream.of(success(6), success(5), failure("darnit")); - List failures = results.flatMap(failures()).collect(toList()); + List failures = results.flatMap(failures()).toList(); - assertThat(failures, hasItems("darnit")); + assertTrue(failures.contains("darnit")); } @Test public void canExtractValuesFromMap() { Map frenchNumberNames = mapOf(entry("un", 1), entry("deux", 2), entry("trois", 3)); - Function> extractor = fromMap(frenchNumberNames, word -> String.format("%s is not a french number", word)); - - assertThat(extractor.apply("deux"), isSuccessOf(2)); - assertThat(extractor.apply("quattro"), isFailureOf("quattro is not a french number")); + Function> extractor + = fromMap(frenchNumberNames, word -> String.format("%s is not a french number", word)); + var outcome = extractor.apply("deux"); + var successOfTwo = isSuccessOf(2); + assertTrue(successOfTwo.matches(outcome)); + assertTrue(isFailureOf("quattro is not a french number") + .matches(extractor.apply("quattro"))); } @Test public void canConvertListOfResultsIntoResultOfList() { - List> results = asList(success(1), success(42), success(69)); + List> results + = asList(success(1), success(42), success(69)); Result, List> unwrapped = Lists.successesOrFailures(results); - assertThat(unwrapped, isSuccessOf(asList(1, 42, 69))); + assertTrue(isSuccessOf(asList(1, 42, 69)).matches(unwrapped)); } @Test public void canConvertListOfResultsIntoFailureOfListOfReasons() { - List> results = asList(success(1), failure("cheese"), success(69), failure("hotdog")); + List> results + = asList(success(1), failure("cheese"), success(69), failure("hotdog")); Result, List> unwrapped = Lists.successesOrFailures(results); - assertThat(unwrapped, isFailureOf(asList("cheese", "hotdog"))); + assertTrue(isFailureOf(asList("cheese", "hotdog")).matches(unwrapped)); } @Test @@ -217,7 +227,7 @@ public void exampleParseAndHalveNumbers() { .flatMap(successes()) .collect(toList()); - assertThat(halvedNumbers, hasItems(3L)); + assertTrue(hasItems(3L).matches(halvedNumbers)); verify(failureCallback).accept("java.lang.NumberFormatException: For input string: \"NaN\""); verify(failureCallback).accept("5 is odd"); @@ -225,7 +235,6 @@ public void exampleParseAndHalveNumbers() { } @Test - @SuppressWarnings("unchecked") public void exampleSplitResults() { Stream inputs = Stream.of("6", "5", "NaN"); @@ -234,8 +243,9 @@ public void exampleSplitResults() { .map(attempt(x -> x % 2 == 0 ? success(x / 2) : failure(x + " is odd"))) .collect(split()); - assertThat(halvedNumbers.left, hasItems(3L)); - assertThat(halvedNumbers.right, hasItems("java.lang.NumberFormatException: For input string: \"NaN\"", "5 is odd")); + assertTrue(hasItems(3L).matches(halvedNumbers.left())); + assertTrue(hasItems("java.lang.NumberFormatException: For input string: \"NaN\"", "5 is odd") + .matches(halvedNumbers.right())); } @Test @@ -247,7 +257,7 @@ public void shouldAggregateResults_BothSuccessful() { ) .then(ifAllSucceeded((x, y) -> x + " = " + y)); - assertThat(actualResult, isSuccessOf("Yay! = 123")); + assertTrue(isSuccessOf("Yay! = 123").matches(actualResult)); } @Test @@ -257,7 +267,7 @@ public void shouldAggregateResults_OneFailure(){ failure("No!") ).then(ifAllSucceeded((x, y) -> x + " = " + y)); - assertThat(result, isFailureOf("No!")); + assertTrue(isFailureOf("No!").matches(result)); } @Test @@ -268,7 +278,7 @@ public void shouldAggregateResults_AllThreeSuccessful() { success("boo") ).then(ifAllSucceeded((x, y, z) -> x + " " + y + " " + z)); - assertThat(song, isSuccessOf("bibbidy bobbidy boo")); + assertTrue(isSuccessOf("bibbidy bobbidy boo").matches(song)); } @Test @@ -279,7 +289,7 @@ public void shouldAggregateResults_AllThreeFailed() { failure("nooo") ).then(ifAllSucceeded((x, y, z) -> x + " " + y + " " + z)); - assertThat(uhoh, isFailureOf("no")); + assertTrue(isFailureOf("no").matches(uhoh)); } @Test @@ -291,7 +301,7 @@ public void shouldAggregateResults_AllFourPassed() { success("YesYesYesYes") ).then(ifAllSucceeded((a, b, c, d) -> a + b + c + d)); - assertThat(result, isSuccessOf("YesYesYesYesYesYesYesYesYesYes")); + assertTrue(isSuccessOf("YesYesYesYesYesYesYesYesYesYes").matches(result)); } @Test @@ -303,6 +313,6 @@ public void shouldAggregateResults_AllFourFailures() { failure("NoNo") // There's no limit ).then(ifAllSucceeded((a, b, c, d) -> a + b + c + d)); - assertThat(result, isFailureOf("NoNo")); + assertTrue(isFailureOf("NoNo").matches(result)); } } diff --git a/src/test/java/co/unruly/control/result/TryTest.java b/src/test/java/co/unruly/control/result/TryTest.java index 429c812..e5f907c 100644 --- a/src/test/java/co/unruly/control/result/TryTest.java +++ b/src/test/java/co/unruly/control/result/TryTest.java @@ -1,5 +1,6 @@ package co.unruly.control.result; +import org.jetbrains.annotations.NotNull; import org.junit.Test; import java.util.List; @@ -18,9 +19,6 @@ import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.containsInAnyOrder; - -//import static co.unruly.control.result.Results.ifFailed; public class TryTest { @@ -79,14 +77,14 @@ public void canHandleStreamFunctionsUsingTryToAndUnwrap() { )); } - private static String throwsRuntimeException(String instruction) { + private static @NotNull String throwsRuntimeException(String instruction) { if("throw".equals(instruction)) { throw new RuntimeException("This is a naughty method"); } return "Today, I was good"; } - private static String throwsCheckedException(String instruction) throws CustomCheckedException { + private static @NotNull String throwsCheckedException(String instruction) throws CustomCheckedException { if("throw".equals(instruction)) { throw new CustomCheckedException("This is a naughty method"); } @@ -101,6 +99,7 @@ private static Stream throwsAndMakesStream(String possiblyNumber) { return IntStream.range(1, Integer.parseInt(possiblyNumber) + 1).boxed(); } + @SuppressWarnings("unused") static class CustomCheckedException extends Exception { public CustomCheckedException(String message) { diff --git a/src/test/java/co/unruly/control/validation/ValidatorTest.java b/src/test/java/co/unruly/control/validation/ValidatorTest.java index b54b15f..563e61a 100644 --- a/src/test/java/co/unruly/control/validation/ValidatorTest.java +++ b/src/test/java/co/unruly/control/validation/ValidatorTest.java @@ -5,28 +5,29 @@ import co.unruly.control.pair.Pair; import co.unruly.control.result.Result; import org.hamcrest.Matcher; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; import org.junit.Test; import java.util.List; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Predicate; -import java.util.stream.Collectors; import java.util.stream.Stream; import static co.unruly.control.matchers.ResultMatchers.isSuccessOf; import static co.unruly.control.result.Resolvers.*; import static co.unruly.control.result.Result.failure; -import static co.unruly.control.result.Result.success; import static co.unruly.control.result.Transformers.onFailureDo; import static co.unruly.control.result.Transformers.onSuccessDo; import static co.unruly.control.validation.Validators.*; import static java.util.Arrays.asList; import static java.util.stream.Collectors.toList; import static org.hamcrest.CoreMatchers.hasItems; -import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.*; +@SuppressWarnings("unchecked") public class ValidatorTest { @Test @@ -36,22 +37,23 @@ public void canCreateValidatorsWithFixedErrorMessages() { Result> validate4 = isEven.apply(4); Result> validate5 = isEven.apply(5); - assertThat(validate4, isSuccessOf(4)); + assertTrue(isSuccessOf(4).matches(validate4)); - assertThat(validate5, isFailedValidationOf(5, "odd")); + assertTrue(isFailedValidationOf(5, "odd").matches(validate5)); } @Test public void canCreateValidatorsWithDynamicErrorMessages() { - Validator isEven = acceptIf(divisibleBy(2), x -> String.format("%d is odd", x)); + Validator isEven = + acceptIf(divisibleBy(2), x -> String.format("%d is odd", x)); Result> validate4 = isEven.apply(4); Result> validate5 = isEven.apply(5); - assertThat(validate4, isSuccessOf(4)); + assertTrue(isSuccessOf(4).matches(validate4)); - assertThat(validate5, isFailedValidationOf(5, "5 is odd")); + assertTrue(isFailedValidationOf(5, "5 is odd").matches(validate5)); } @Test @@ -63,9 +65,9 @@ public void canComposeValidators() { Result> validate4 = fizzbuzz.apply(4); Result> validate5 = fizzbuzz.apply(15); - assertThat(validate4, isSuccessOf(4)); + assertTrue(isSuccessOf(4).matches(validate4)); - assertThat(validate5, isFailedValidationOf(15, "fizz", "15 is a buzz")); + assertTrue(isFailedValidationOf(15, "fizz", "15 is a buzz").matches(validate5)); } @@ -78,25 +80,27 @@ public void canComposeValidatorsForFirstError() { Result> validate5 = fizzbuzz.apply(5); Result> validate15 = fizzbuzz.apply(15); - assertThat(validate5, isFailedValidationOf(5, "5 is a buzz")); - assertThat(validate15, isFailedValidationOf(15, "fizz")); + assertTrue(isFailedValidationOf(5, "5 is a buzz").matches(validate5)); + assertTrue(isFailedValidationOf(15, "fizz").matches(validate15)); } @Test public void doesNotExecuteValidatorsIfAlreadyFailedAndOnlyReportingFirst() { Validator fizzbuzz = firstOf(compose( rejectIf(divisibleBy(3), "fizz"), - rejectIf(divisibleBy(5), x -> { throw new AssertionError("should not exercise this method"); }))); + rejectIf(divisibleBy(5), x -> { + throw new AssertionError("should not exercise this method"); }))); Validator biglittle = firstOf(compose( rejectIf(x -> x > 10, "big"), - rejectIf(x -> x < 3, x -> { throw new AssertionError("should not exercise this method"); }))); + rejectIf(x -> x < 3, x -> { + throw new AssertionError("should not exercise this method"); }))); Validator combined = compose(fizzbuzz, biglittle); Result> validate15 = combined.apply(15); - assertThat(validate15, isFailedValidationOf(15, "fizz", "big")); + assertTrue(isFailedValidationOf(15, "fizz", "big").matches(validate15)); } @Test @@ -104,28 +108,29 @@ public void canStreamSuccesses() { Validator isEven = acceptIf(divisibleBy(2), "odd"); List evens = Stream.of(1,2,3,4,5,6,7,8,9) - .map((item) -> isEven.apply(item)) + .map(isEven) .flatMap(successes()) .collect(toList()); - assertThat(evens, hasItems(2,4,6,8)); + assertTrue(hasItems(2,4,6,8).matches(evens)); } @Test public void canStreamFailures() { Validator isEven = acceptIf(divisibleBy(2), "odd"); - List> odds = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9) - .map((item) -> isEven.apply(item)) + List> odds = + Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9) + .map(isEven) .flatMap(failures()) .collect(toList()); - assertThat(odds, hasItems( + assertTrue(hasItems( validationFailure(1, "odd"), validationFailure(3, "odd"), validationFailure(5, "odd"), validationFailure(7, "odd"), - validationFailure(9, "odd"))); + validationFailure(9, "odd")).matches(odds)); } @Test @@ -139,7 +144,7 @@ public void canConsumeSuccesses() { rejectIf(multipleOf(7), x -> x + " divides by 7") ); - Stream.of(1,2,3,4,5,6,7,8,9).map((item) -> isPrime.apply(item)).forEach(onSuccessDo(log)); + Stream.of(1,2,3,4,5,6,7,8,9).map(isPrime).forEach(onSuccessDo(log)); verify(log).accept(1); verify(log).accept(2); @@ -160,7 +165,7 @@ public void canConsumeFailures() { rejectIf(multipleOf(7), x -> x + " divides by 7") ); - Stream.of(1,2,3,4,5,6,7,8,9).map((item) -> isPrime.apply(item)).forEach(onFailureDo(log)); + Stream.of(1,2,3,4,5,6,7,8,9).map(isPrime).forEach(onFailureDo(log)); verify(log).accept(validationFailure(4, "4 divides by 2")); verify(log).accept(validationFailure(6, "6 divides by 2", "6 divides by 3")); @@ -180,7 +185,7 @@ public void canFireFirstErrorForEachFailure() { rejectIf(multipleOf(7), x -> x + " divides by 7") )); - Stream.of(1,2,3,4,5,6,7,8,9).map((item) -> isPrime.apply(item)).forEach(onFailureDo(log)); + Stream.of(1,2,3,4,5,6,7,8,9).map(isPrime).forEach(onFailureDo(log)); verify(log).accept(validationFailure(4, "4 divides by 2")); verify(log).accept(validationFailure(6, "6 divides by 2")); @@ -192,7 +197,7 @@ public void canFireFirstErrorForEachFailure() { @Test public void canCreateConditionalValidator() { Validator, String> containsEvens = acceptIf( - list -> list.stream().filter(x -> x % 2 == 0).collect(Collectors.toList()).isEmpty(), + list -> list.stream().noneMatch(x -> x % 2 == 0), "List contains even numbers"); Validator, String> onlyChecksEvenLengthLists = onlyIf( @@ -200,11 +205,14 @@ public void canCreateConditionalValidator() { containsEvens ); - Result, FailedValidation, String>> ofFiveNumbers = onlyChecksEvenLengthLists.apply(asList(1, 2, 3, 4, 5)); - Result, FailedValidation, String>> ofSixNumbers = onlyChecksEvenLengthLists.apply(asList(1, 2, 3, 4, 5, 6)); + Result, FailedValidation, String>> ofFiveNumbers + = onlyChecksEvenLengthLists.apply(asList(1, 2, 3, 4, 5)); + Result, FailedValidation, String>> ofSixNumbers + = onlyChecksEvenLengthLists.apply(asList(1, 2, 3, 4, 5, 6)); - assertThat(ofFiveNumbers, isSuccessOf(asList(1,2,3,4,5))); - assertThat(ofSixNumbers, isFailedValidationOf(asList(1,2,3,4,5,6), "List contains even numbers")); + assertTrue(isSuccessOf(asList(1,2,3,4,5)).matches(ofFiveNumbers)); + assertTrue(isFailedValidationOf(asList(1,2,3,4,5,6), "List contains even numbers") + .matches(ofSixNumbers)); } @Test @@ -218,7 +226,8 @@ public void canFireAllErrorsForEachFailure() { rejectIf(multipleOf(7), x -> x + " divides by 7") ); - Stream.of(1,2,3,4,5,6,7,8,9).map((item) -> isPrime.apply(item)).forEach(onFailureDo(v -> v.errors.forEach(e -> log.accept(v.value, e)))); + Stream.of(1,2,3,4,5,6,7,8,9).map(isPrime) + .forEach(onFailureDo(v -> v.errors().forEach(e -> log.accept(v.value(), e)))); verify(log).accept(4, "4 divides by 2"); verify(log).accept(6, "6 divides by 2"); @@ -239,7 +248,8 @@ public void canMapErrors() { rejectIf(multipleOf(7), x -> x + " divides by 7") ), (num, msg) -> msg + ", oh boy"); - Stream.of(1,2,3,4,5,6,7,8,9).map((item) -> isPrime.apply(item)).forEach(onFailureDo(v -> v.errors.forEach(e -> log.accept(v.value, e)))); + Stream.of(1,2,3,4,5,6,7,8,9).map(isPrime) + .forEach(onFailureDo(v -> v.errors().forEach(e -> log.accept(v.value(), e)))); verify(log).accept(4, "4 divides by 2, oh boy"); verify(log).accept(6, "6 divides by 2, oh boy"); @@ -260,69 +270,71 @@ public void canSplitResults() { Pair, List>> results = Stream .of(4,5,6,7,8) - .map((item) -> isPrime.apply(item)) + .map(isPrime) .collect(split()); - assertThat(results.left, hasItems(5, 7)); - assertThat(results.right, hasItems( + assertTrue(hasItems(5, 7).matches(results.left())); + assertTrue(hasItems( validationFailure(4, "4 divides by 2"), validationFailure(6, "6 divides by 2", "6 divides by 3"), validationFailure(8, "8 divides by 2") - )); + ).matches(results.right())); } @Test public void canIgnoreErrors() { - Result> failedValidation = failure(new FailedValidation(42, asList("fail1", "fail2", "error1"))); + Result> failedValidation + = failure(new FailedValidation<>(42, asList("fail1", "fail2", "error1"))); Result> filteredValidation = failedValidation .then(ignoreWhen(error -> error.startsWith("fail"))); - assertThat(filteredValidation, isFailedValidationOf(42, "error1")); + assertTrue(isFailedValidationOf(42, "error1").matches(filteredValidation)); } @Test public void convertsToSuccessWhenAllErrorsIgnored() { - Result> failedValidation = failure(new FailedValidation(42, asList("fail1", "fail2", "fail3"))); + Result> failedValidation + = failure(new FailedValidation<>(42, asList("fail1", "fail2", "fail3"))); Result> filteredValidation = failedValidation .then(ignoreWhen(error -> error.startsWith("fail"))); - assertThat(filteredValidation, isSuccessOf(42)); + assertTrue(isSuccessOf(42).matches(filteredValidation)); } @Test public void blammo() { - safelyDoSomethingDodgy(x -> { throw new Exception("hello"); }, "cheese"); + safelyDoSomethingDodgy(x -> { throw new Exception("hello"); }); } - private static void safelyDoSomethingDodgy(ThrowingLambdas.ThrowingConsumer consumer, String message) { + private static void safelyDoSomethingDodgy(ThrowingLambdas.ThrowingConsumer consumer) { try { - consumer.accept(message); + consumer.accept("cheese"); } catch (Exception ex) { // do nothing cos that's how I roll } } - private static void doSomethingDodgy(String message) throws Exception { - throw new Exception(message); - } - - private static Predicate divisibleBy(int factor) { + @Contract(pure = true) + private static @NotNull Predicate divisibleBy(int factor) { return x -> x % factor == 0; } - private static Predicate multipleOf(int factor) { + @Contract(pure = true) + private static @NotNull Predicate multipleOf(int factor) { return x -> x != factor && x % factor == 0; } + @Contract("_, _ -> new") @SafeVarargs - private final FailedValidation validationFailure(T value, E... errors) { - return new FailedValidation(value, asList(errors)); + private @NotNull FailedValidation validationFailure(T value, E... errors) { + return new FailedValidation<>(value, asList(errors)); } - private Matcher>> isFailedValidationOf(T value, E... errors) { + private @NotNull Matcher>> + isFailedValidationOf(T value, E... errors) { FailedValidation failedValidation = validationFailure(value, errors); return ResultMatchers.isFailureOf(failedValidation); } diff --git a/src/test/java/examples/ConciseEquals.java b/src/test/java/examples/ConciseEquals.java index 62dede5..d19fd6d 100644 --- a/src/test/java/examples/ConciseEquals.java +++ b/src/test/java/examples/ConciseEquals.java @@ -8,6 +8,7 @@ * Just a demonstration of how we can build a cleaner way to check equality * using Result-based casts under the hood. */ +@SuppressWarnings("unused") public class ConciseEquals { private final int number; @@ -30,7 +31,8 @@ public ConciseEquals(int number, String text) { @Override public boolean equals(Object o) { - return areEqual(this, o, (a, b) -> + return getClass() == o.getClass() && + areEqual(this, o, (a, b) -> a.number == b.number && Objects.equals(a.text, b.text) ); diff --git a/src/test/java/examples/ExceptionsInStreamsHandling.java b/src/test/java/examples/ExceptionsInStreamsHandling.java index cb449f9..6ed76b2 100644 --- a/src/test/java/examples/ExceptionsInStreamsHandling.java +++ b/src/test/java/examples/ExceptionsInStreamsHandling.java @@ -10,11 +10,10 @@ import static co.unruly.control.result.Introducers.tryTo; import static co.unruly.control.result.Resolvers.ifFailed; import static co.unruly.control.result.Transformers.*; -import static java.util.stream.Collectors.toList; +@SuppressWarnings({"unused", "NewClassNamingConvention"}) public class ExceptionsInStreamsHandling { - @Test public void handling_exceptions_with_result_example() { List customerAges = Stream.of("Bob", "Bill") @@ -27,7 +26,7 @@ public void handling_exceptions_with_result_example() { }))) .map(recover(ifType(IOException.class, error -> -2))) .map(ifFailed(__ -> -127)) - .collect(toList()); + .toList(); } @Test @@ -43,22 +42,22 @@ public void handling_multiple_exceptions_with_result_example() { }))) .map(recover(ifType(IOException.class, error -> -2))) .map(ifFailed(__ -> -127)) - .collect(toList()); + .toList(); } static class CustomerNotFound extends Exception {} static class NoCustomerWithThatName extends CustomerNotFound {} - public Customer findCustomerByName(String name) throws CustomerNotFound { + public Customer findCustomerByName(String name) { return customer; } private void sendEmailUpdateTo(Customer potentialCustomer) { - email(customer.emailAddress(), customer.name(), "Blah blah blah"); + email(customer.emailAddress(), customer.name()); } - private void email(String email, String name, String message) { + private void email(String email, String name) { } @@ -71,7 +70,7 @@ public interface Customer { default String name() { return ""; } default int age() { return 0; } - default int calculateValue() throws CostUnknown { return 0; } + default int calculateValue() { return 0; } class CostUnknown extends Exception {} } diff --git a/src/test/java/examples/FlatMapVariance.java b/src/test/java/examples/FlatMapVariance.java index 6b53e5e..94ae671 100644 --- a/src/test/java/examples/FlatMapVariance.java +++ b/src/test/java/examples/FlatMapVariance.java @@ -4,6 +4,8 @@ import co.unruly.control.result.TypeOf; import co.unruly.control.validation.Validator; import co.unruly.control.validation.Validators; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; import java.util.List; import java.util.stream.Collectors; @@ -15,18 +17,19 @@ import static co.unruly.control.result.TypeOf.using; import static co.unruly.control.validation.Validators.rejectIf; +@SuppressWarnings("unused") public class FlatMapVariance { - private static Validator fizzbuzz = Validators.compose( + private static final Validator fizzbuzz = Validators.compose( rejectIf(n -> n % 3 == 0, "fizz"), rejectIf(n -> n % 5 == 0, "buzz")); - private static Validator under100 = Validators.compose( + private static final Validator under100 = Validators.compose( rejectIf(s -> s.length() > 2, s -> s + " is too damn high") ); public void canFlatmapErrorTypeOfStringIntoErrorTypeOfString() { - divideExactlyByTwo(3) + divideExactlyByTwo() .then(attempt(this::isPrime)); } @@ -64,13 +67,12 @@ public void canFlatmapErrorTypeOfFailedValidationIntoErrorTypeOfListOfString() { public void canFlatmapErrorTypeOfListOfStringIntoErrorTypeOfFailedValidation() { Result> foo = listFactors(5) - .then(attempt((item) -> fizzbuzz.apply(item))); + .then(attempt(fizzbuzz)); } - private Result divideExactlyByTwo(int number) { - return number % 2 == 0 - ? Result.success(number / 2) - : Result.failure(number + " is odd: cannot divide exactly by two"); + @Contract(" -> new") + private @NotNull Result divideExactlyByTwo() { + return Result.failure(3 + " is odd: cannot divide exactly by two"); } private Result isPrime(int number) { diff --git a/src/test/java/examples/FunctionalErrorHandling.java b/src/test/java/examples/FunctionalErrorHandling.java index a57dfc6..fe48bb2 100644 --- a/src/test/java/examples/FunctionalErrorHandling.java +++ b/src/test/java/examples/FunctionalErrorHandling.java @@ -1,6 +1,8 @@ package examples; import co.unruly.control.result.Result; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; import org.junit.Test; import static co.unruly.control.result.Combiners.combineWith; @@ -10,6 +12,7 @@ import static co.unruly.control.result.Transformers.attempt; import static co.unruly.control.result.Transformers.onSuccess; +@SuppressWarnings({"unused", "NewClassNamingConvention"}) public class FunctionalErrorHandling { @Test @@ -37,6 +40,7 @@ public void howToMakeBreakfast() { // I am however good enough to put the eggs on toast Result eggsOnToast = scrambledEggs.then(combineWith(toast)).using(ScrambledEggsOnToast::new); + @SuppressWarnings("unused") Breakfast breakfast = eggsOnToast.then(ifFailed(__ -> new BowlOfCornflakes())); } @@ -46,14 +50,16 @@ public boolean areEggsOff() { return false; } - public Eggs getEggs() { + @Contract(value = " -> new", pure = true) + public @NotNull Eggs getEggs() { return new Eggs(); } } private static class Bread { - public Toast toast() { + @Contract(value = " -> new", pure = true) + public @NotNull Toast toast() { return new Toast(); } } @@ -75,7 +81,8 @@ private static class ScrambledEggs { private static class Eggs { - public Result scramble() { + @Contract(" -> new") + public @NotNull Result scramble() { return success(new ScrambledEggs()); } } @@ -86,14 +93,7 @@ public static ScrambledEggs salt(ScrambledEggs unsalted) { } } - private static class ScrambledEggsOnToast implements Breakfast { - private final ScrambledEggs eggs; - private final Toast toast; - - private ScrambledEggsOnToast(ScrambledEggs eggs, Toast toast) { - this.eggs = eggs; - this.toast = toast; - } + private record ScrambledEggsOnToast(ScrambledEggs eggs, Toast toast) implements Breakfast { } private static class BowlOfCornflakes implements Breakfast { diff --git a/src/test/java/examples/NovelErrorHandling.java b/src/test/java/examples/NovelErrorHandling.java index c9248a8..46d1963 100644 --- a/src/test/java/examples/NovelErrorHandling.java +++ b/src/test/java/examples/NovelErrorHandling.java @@ -1,6 +1,8 @@ package examples; import co.unruly.control.result.Result; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; import static co.unruly.control.result.Resolvers.collapse; import static co.unruly.control.result.Result.failure; @@ -9,9 +11,14 @@ import static co.unruly.control.result.Transformers.onSuccess; import static java.lang.String.format; +@SuppressWarnings("unused") public class NovelErrorHandling { - public static String novelSales(Author author, Publisher publisher, Editor editor, Retailer retailer) { + public static String + novelSales(@NotNull Author author, + @NotNull Publisher publisher, + @NotNull Editor editor, + @NotNull Retailer retailer) { return author.getIdea() .then(attempt(publisher::getAdvance)) .then(attempt(author::writeNovel)) @@ -23,7 +30,7 @@ public static String novelSales(Author author, Publisher publisher, Editor edito } public static class Author { - private Result idea; + private final Result idea; private final int skill; private final int lifestyleCosts; @@ -37,7 +44,7 @@ public Result getIdea() { return idea; } - public Result writeNovel(Advance advance) { + public Result writeNovel(@NotNull Advance advance) { if(advance.amount > lifestyleCosts) { int happiness = advance.amount - lifestyleCosts; return success(new Manuscript(advance.idea.title, happiness * skill)); @@ -47,31 +54,24 @@ public Result writeNovel(Advance advance) { } } - public static class Publisher { + public record Publisher(int qualityThreshold, int generosity) { - public final int qualityThreshold; - public final int generosity; - - public Publisher(int qualityThreshold, int generosity) { - this.qualityThreshold = qualityThreshold; - this.generosity = generosity; - } - - public Result getAdvance(Idea idea) { - if(idea.appeal >= qualityThreshold) { - return success(new Advance(idea.appeal * generosity, idea)); - } else { - return failure("This novel wouldn't sell"); + public Result getAdvance(@NotNull Idea idea) { + if (idea.appeal >= qualityThreshold) { + return success(new Advance(idea.appeal * generosity, idea)); + } else { + return failure("This novel wouldn't sell"); + } } - } - public Novel publishNovel(Manuscript manuscript) { - return new Novel(manuscript.title, manuscript.quality); + @Contract("_ -> new") + public @NotNull Novel publishNovel(@NotNull Manuscript manuscript) { + return new Novel(manuscript.title, manuscript.quality); + } } - } public static class Editor { - public Manuscript editNovel(Manuscript manuscript) { + public Manuscript editNovel(@NotNull Manuscript manuscript) { return new Manuscript(manuscript.title, manuscript.quality + 3); } } @@ -88,44 +88,16 @@ public Sales sellNovel(Novel novel) { } } - public static class Idea { - public final String title; - public final int appeal; - - public Idea(String title, int appeal) { - this.title = title; - this.appeal = appeal; - } + public record Idea(String title, int appeal) { } - public static class Advance { - public final int amount; - public final Idea idea; - - public Advance(int amount, Idea idea) { - this.amount = amount; - this.idea = idea; - } + public record Advance(int amount, Idea idea) { } - public static class Manuscript { - public final String title; - public final int quality; - - public Manuscript(String title, int quality) { - this.title = title; - this.quality = quality; - } + public record Manuscript(String title, int quality) { } - public static class Novel { - public final String title; - public final int quality; - - public Novel(String title, int quality) { - this.title = title; - this.quality = quality; - } + public record Novel(String title, int quality) { } public static class Sales {