commit 3602e8dce1ecb05791ac80e5047c0af028e8ca62 Author: Michael Bull Date: Sat Oct 21 03:51:30 2017 +0100 Initial commit diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..17ac45a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{kt}] +indent_style = tab +indent_size = 4 +continuation_indent_size = 4 + +[*.{gradle}] +indent_style = space +indent_size = 4 +continuation_indent_size = 4 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d6abef9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Hidden files +.* + +# Temporary files +*~ + +# Git +!.git* + +# EditorConfig +!.editorconfig + +# IntelliJ Idea +out/ +*.iml +*.ipr +*.iws + +# Gradle +build/ + +# JVM error logs +hs_err_pid*.log +replay_pid*.log diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b2a3e02 --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ +Copyright (c) 2017 Michael Bull (https://www.michael-bull.com) + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..b86c882 --- /dev/null +++ b/build.gradle @@ -0,0 +1,62 @@ +buildscript { + repositories { + mavenCentral() + jcenter() + } + + dependencies { + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" + classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokkaVersion" + classpath "org.junit.platform:junit-platform-gradle-plugin:$junitPlatformVersion" + } +} + +apply plugin: 'kotlin' +apply plugin: 'maven-publish' +apply plugin: 'org.jetbrains.dokka' +apply plugin: 'org.junit.platform.gradle.plugin' + +repositories { + mavenCentral() +} + +dependencies { + compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlinVersion" + testCompile "com.natpryce:hamkrest:$hamkrestVersion" + testCompile "org.junit.jupiter:junit-jupiter-params:$junitVersion" + testCompile "org.junit.jupiter:junit-jupiter-api:$junitVersion" + testRuntime "org.junit.jupiter:junit-jupiter-engine:$junitVersion" +} + +compileKotlin { + kotlinOptions.jvmTarget = "1.8" +} + +compileTestKotlin { + kotlinOptions.jvmTarget = "1.8" +} + +dokka { + outputFormat = 'html' + outputDirectory = "$buildDir/docs" +} + +task sourcesJar(type: Jar) { + from sourceSets.main.allSource + classifier = 'sources' +} + +task kdocJar(type: Jar, dependsOn: dokka) { + from dokka.outputDirectory + classifier = 'docs' +} + +publishing { + publications { + mavenJava(MavenPublication) { + from components.java + artifact sourcesJar + artifact kdocJar + } + } +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..a6cb907 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,8 @@ +group=com.mikebull94.kotlin-result +version=1.0-SNAPSHOT + +dokkaVersion=0.9.15 +hamkrestVersion=1.4.2.0 +kotlinVersion=1.1.51 +junitVersion=5.0.1 +junitPlatformVersion=1.0.0 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..ee30f58 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..7053476 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Oct 20 21:56:00 BST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-3.5-rc-2-all.zip diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..4453cce --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# 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 + ;; + 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" + which java >/dev/null 2>&1 || 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 + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save ( ) { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..f955316 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@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=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@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= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +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 init + +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 + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +: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 %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="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! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..6e074d7 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'kotlin-result' + diff --git a/src/main/kotlin/com/mikebull94/result/And.kt b/src/main/kotlin/com/mikebull94/result/And.kt new file mode 100644 index 0000000..40b18d2 --- /dev/null +++ b/src/main/kotlin/com/mikebull94/result/And.kt @@ -0,0 +1,11 @@ +package com.mikebull94.result + +/** + * - Elm: [Result.andThen](http://package.elm-lang.org/packages/elm-lang/core/latest/Result#andThen) + */ +inline fun Result.andThen(transform: (V) -> Result): Result { + return when (this) { + is Ok -> transform(value) + is Error -> error(error) + } +} diff --git a/src/main/kotlin/com/mikebull94/result/Factory.kt b/src/main/kotlin/com/mikebull94/result/Factory.kt new file mode 100644 index 0000000..63fe9e6 --- /dev/null +++ b/src/main/kotlin/com/mikebull94/result/Factory.kt @@ -0,0 +1,4 @@ +package com.mikebull94.result + +fun ok(value: V) = Ok(value) +fun error(error: E) = Error(error) diff --git a/src/main/kotlin/com/mikebull94/result/Get.kt b/src/main/kotlin/com/mikebull94/result/Get.kt new file mode 100644 index 0000000..3134c4d --- /dev/null +++ b/src/main/kotlin/com/mikebull94/result/Get.kt @@ -0,0 +1,21 @@ +package com.mikebull94.result + +/** + * - Elm: [Result.toMaybe](http://package.elm-lang.org/packages/elm-lang/core/latest/Result#toMaybe) + */ +fun Result.get(): V? { + return when (this) { + is Ok -> value + is Error -> null + } +} + +/** + * - Elm: [Result.withDefault](http://package.elm-lang.org/packages/elm-lang/core/latest/Result#withDefault) + */ +infix fun Result.getOrElse(default: V): V { + return when (this) { + is Ok -> value + is Error -> default + } +} diff --git a/src/main/kotlin/com/mikebull94/result/Iterable.kt b/src/main/kotlin/com/mikebull94/result/Iterable.kt new file mode 100644 index 0000000..e29b201 --- /dev/null +++ b/src/main/kotlin/com/mikebull94/result/Iterable.kt @@ -0,0 +1,104 @@ +package com.mikebull94.result + +inline fun Iterable.fold( + initial: R, + operation: (acc: R, T) -> Result +): Result { + var accumulator = initial + + forEach { element -> + val operationResult = operation(accumulator, element) + + when (operationResult) { + is Ok -> { + accumulator = operationResult.value + } + is Error -> return error(operationResult.error) + } + } + + return ok(accumulator) +} + +inline fun List.foldRight(initial: R, operation: (T, acc: R) -> Result): Result { + var accumulator = initial + + if (!isEmpty()) { + val iterator = listIterator(size) + while (iterator.hasPrevious()) { + val operationResult = operation(iterator.previous(), accumulator) + + when (operationResult) { + is Ok -> { + accumulator = operationResult.value + } + is Error -> return error(operationResult.error) + } + } + } + + return ok(accumulator) +} + +/** + * - Elm: [Result.Extra.combine](http://package.elm-lang.org/packages/circuithub/elm-result-extra/1.4.0/Result-Extra#combine) + */ +fun combine(vararg results: Result) = results.asIterable().combine() + +/** + * - Elm: [Result.Extra.combine](http://package.elm-lang.org/packages/circuithub/elm-result-extra/1.4.0/Result-Extra#combine) + */ +fun Iterable>.combine(): Result, E> { + return ok(map { + when (it) { + is Ok -> it.value + is Error -> return error(it.error) + } + }) +} + +/** + * - Haskell: [Data.Either.lefts](https://hackage.haskell.org/package/base-4.10.0.0/docs/Data-Either.html#v:lefts) + */ +fun getAll(vararg results: Result) = results.asIterable().getAll() + +/** + * - Haskell: [Data.Either.lefts](https://hackage.haskell.org/package/base-4.10.0.0/docs/Data-Either.html#v:lefts) + */ +fun Iterable>.getAll(): List { + return filter { it is Ok }.map { (it as Ok).value } +} + +/** + * - Haskell: [Data.Either.rights](https://hackage.haskell.org/package/base-4.10.0.0/docs/Data-Either.html#v:rights) + */ +fun getAllErrors(vararg results: Result) = results.asIterable().getAllErrors() + +/** + * - Haskell: [Data.Either.rights](https://hackage.haskell.org/package/base-4.10.0.0/docs/Data-Either.html#v:rights) + */ +fun Iterable>.getAllErrors(): List { + return filter { it is Error }.map { (it as Error).error } +} + +/** + * - Haskell: [Data.Either.partitionEithers](https://hackage.haskell.org/package/base-4.10.0.0/docs/Data-Either.html#v:partitionEithers) + */ +fun partition(vararg results: Result) = results.asIterable().partition() + +/** + * - Haskell: [Data.Either.partitionEithers](https://hackage.haskell.org/package/base-4.10.0.0/docs/Data-Either.html#v:partitionEithers) + */ +fun Iterable>.partition(): Pair, List> { + val values = mutableListOf() + val errors = mutableListOf() + + forEach { result -> + when (result) { + is Ok -> values.add(result.value) + is Error -> errors.add(result.error) + } + } + + return Pair(values, errors) +} diff --git a/src/main/kotlin/com/mikebull94/result/Map.kt b/src/main/kotlin/com/mikebull94/result/Map.kt new file mode 100644 index 0000000..8c9fcbb --- /dev/null +++ b/src/main/kotlin/com/mikebull94/result/Map.kt @@ -0,0 +1,48 @@ +package com.mikebull94.result + +/** + * - Elm: [Result.map](http://package.elm-lang.org/packages/elm-lang/core/latest/Result#map) + * - Haskell: [Data.Bifunctor.first](https://hackage.haskell.org/package/base-4.10.0.0/docs/Data-Bifunctor.html#v:first) + */ +inline fun Result.map(transform: (V) -> U): Result { + return when (this) { + is Ok -> ok(transform(value)) + is Error -> error(error) + } +} + +/** + * - Elm: [Result.mapError](http://package.elm-lang.org/packages/elm-lang/core/latest/Result#mapError) + * - Haskell: [Data.Bifunctor.right](https://hackage.haskell.org/package/base-4.10.0.0/docs/Data-Bifunctor.html#v:second) + */ +inline fun Result.mapError(transform: (E) -> U): Result { + return when (this) { + is Ok -> ok(value) + is Error -> error(transform(error)) + } +} + +/** + * - Elm: [Result.Extra.mapBoth](http://package.elm-lang.org/packages/circuithub/elm-result-extra/1.4.0/Result-Extra#mapBoth) + * - Haskell: [Data.Either.either](https://hackage.haskell.org/package/base-4.10.0.0/docs/Data-Either.html#v:either) + */ +inline fun Result.mapBoth(success: (V) -> U, failure: (E) -> U): U { + return when (this) { + is Ok -> success(value) + is Error -> failure(error) + } +} + +// TODO: better name? +/** + * - Haskell: [Data.Bifunctor.Bimap](https://hackage.haskell.org/package/base-4.10.0.0/docs/Data-Bifunctor.html#v:bimap) + */ +inline fun Result.mapEither( + okTransform: (V1) -> V2, + errorTransform: (E1) -> E2 +): Result { + return when (this) { + is Ok -> ok(okTransform(value)) + is Error -> error(errorTransform(error)) + } +} diff --git a/src/main/kotlin/com/mikebull94/result/On.kt b/src/main/kotlin/com/mikebull94/result/On.kt new file mode 100644 index 0000000..5042111 --- /dev/null +++ b/src/main/kotlin/com/mikebull94/result/On.kt @@ -0,0 +1,4 @@ +package com.mikebull94.result + +fun Result.onSuccess(callback: (V) -> Unit) = mapBoth(callback, {}) +fun Result.onFailure(callback: (E) -> Unit) = mapBoth({}, callback) diff --git a/src/main/kotlin/com/mikebull94/result/Or.kt b/src/main/kotlin/com/mikebull94/result/Or.kt new file mode 100644 index 0000000..c5fee17 --- /dev/null +++ b/src/main/kotlin/com/mikebull94/result/Or.kt @@ -0,0 +1,18 @@ +package com.mikebull94.result + +infix fun Result.or(default: V): Result { + return when (this) { + is Ok -> this + is Error -> ok(default) + } +} + +/** + * - Elm: [Result.extract](http://package.elm-lang.org/packages/circuithub/elm-result-extra/1.4.0/Result-Extra#extract) + */ +inline fun Result.extract(transform: (E) -> V): V { + return when (this) { + is Ok -> value + is Error -> transform(error) + } +} diff --git a/src/main/kotlin/com/mikebull94/result/Result.kt b/src/main/kotlin/com/mikebull94/result/Result.kt new file mode 100644 index 0000000..cb64e65 --- /dev/null +++ b/src/main/kotlin/com/mikebull94/result/Result.kt @@ -0,0 +1,39 @@ +package com.mikebull94.result + +/** + * - Elm: [Result](http://package.elm-lang.org/packages/elm-lang/core/5.1.1/Result) + * - Haskell: [Data.Either](https://hackage.haskell.org/package/base-4.10.0.0/docs/Data-Either.html) + */ +sealed class Result + +class Ok internal constructor(val value: V) : Result() { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Ok<*, *> + + if (value != other.value) return false + + return true + } + + override fun hashCode() = value?.hashCode() ?: 0 + override fun toString() = "Result.Ok($value)" +} + +class Error internal constructor(val error: E) : Result() { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Error<*, *> + + if (error != other.error) return false + + return true + } + + override fun hashCode() = error?.hashCode() ?: 0 + override fun toString() = "Result.Error($error)" +} diff --git a/src/test/kotlin/com/mikebull94/result/AndTest.kt b/src/test/kotlin/com/mikebull94/result/AndTest.kt new file mode 100644 index 0000000..cde55ed --- /dev/null +++ b/src/test/kotlin/com/mikebull94/result/AndTest.kt @@ -0,0 +1,25 @@ +package com.mikebull94.result + +import com.natpryce.hamkrest.assertion.assertThat +import com.natpryce.hamkrest.equalTo +import com.natpryce.hamkrest.sameInstance +import org.junit.jupiter.api.Test + +internal class AndTest { + private object AndError + + @Test + internal fun `andThen should return the transformed result value if ok`() { + val value = ok(5).andThen { ok(it + 7) }.get() + assertThat(value, equalTo(12)) + } + + @Test + internal fun `andThen should return the result error if not ok`() { + val result = ok(20).andThen { ok(it + 43) }.andThen { error(AndError) } + + result as Error + + assertThat(result.error, sameInstance(AndError)) + } +} diff --git a/src/test/kotlin/com/mikebull94/result/GetTest.kt b/src/test/kotlin/com/mikebull94/result/GetTest.kt new file mode 100644 index 0000000..b1bbed2 --- /dev/null +++ b/src/test/kotlin/com/mikebull94/result/GetTest.kt @@ -0,0 +1,33 @@ +package com.mikebull94.result + +import com.natpryce.hamkrest.assertion.assertThat +import com.natpryce.hamkrest.equalTo +import org.junit.jupiter.api.Test + +internal class GetTest { + private object GetError + + @Test + internal fun `get should return the result value if ok`() { + val value = ok(12).get() + assertThat(value, equalTo(12)) + } + + @Test + internal fun `get should return null if not ok`() { + val value = error(GetError).get() + assertThat(value, equalTo(null)) + } + + @Test + internal fun `getOrElse should return the result value if ok`() { + val value = ok("hello").getOrElse("world") + assertThat(value, equalTo("hello")) + } + + @Test + internal fun `getOrElse should return default value if not ok`() { + val value = error(GetError).getOrElse("default") + assertThat(value, equalTo("default")) + } +} diff --git a/src/test/kotlin/com/mikebull94/result/IterableTest.kt b/src/test/kotlin/com/mikebull94/result/IterableTest.kt new file mode 100644 index 0000000..778d90b --- /dev/null +++ b/src/test/kotlin/com/mikebull94/result/IterableTest.kt @@ -0,0 +1,179 @@ +package com.mikebull94.result + +import com.natpryce.hamkrest.Matcher +import com.natpryce.hamkrest.assertion.assertThat +import com.natpryce.hamkrest.equalTo +import com.natpryce.hamkrest.sameInstance +import org.junit.jupiter.api.Test + +internal class IterableTest { + private sealed class IterableError { + object IterableError1 : IterableError() + object IterableError2 : IterableError() + } + + private fun sameError(error: IterableError): Matcher { + return sameInstance(error) + } + + @Test + internal fun `fold should return the accumulated value if ok`() { + val result = listOf(20, 30, 40, 50).fold( + initial = 10, + operation = { a, b -> ok(a + b) } + ) + + result as Ok + + assertThat(result.value, equalTo(150)) + } + + @Test + internal fun `fold should return the first error if not ok`() { + val result: Result = listOf(5, 10, 15, 20, 25).fold( + initial = 1, + operation = { a, b -> + when (b) { + (5 + 10) -> error(IterableError.IterableError1) + (5 + 10 + 15 + 20) -> error(IterableError.IterableError2) + else -> ok(a * b) + } + } + ) + + result as Error + + val matcher: Matcher = sameInstance(IterableError.IterableError1) + assertThat(result.error, matcher) + } + + @Test + internal fun `foldRight should return the accumulated value if ok`() { + val result = listOf(2, 5, 10, 20).foldRight( + initial = 100, + operation = { a, b -> ok(b - a) } + ) + + result as Ok + + assertThat(result.value, equalTo(63)) + } + + @Test + internal fun `foldRight should return the last error if not ok`() { + val result = listOf(2, 5, 10, 20, 40).foldRight( + initial = 38500, + operation = { a, b -> + when (b) { + (((38500 / 40) / 20) / 10) -> error(IterableError.IterableError1) + ((38500 / 40) / 20) -> error(IterableError.IterableError2) + else -> ok(b / a) + } + } + ) + + result as Error + + assertThat(result.error, sameError(IterableError.IterableError2)) + } + + @Test + internal fun `combine should return the combined list of values if results are ok`() { + val values = combine( + ok(10), + ok(20), + ok(30) + ).get()!! + + assertThat(values.size, equalTo(3)) + assertThat(values[0], equalTo(10)) + assertThat(values[1], equalTo(20)) + assertThat(values[2], equalTo(30)) + } + + @Test + internal fun `combine should return the first error if results are not ok`() { + val result = combine( + ok(20), + ok(40), + error(IterableError.IterableError1), + ok(60), + error(IterableError.IterableError2), + ok(80) + ) + + result as Error + + assertThat(result.error, sameError(IterableError.IterableError1)) + } + + @Test + internal fun `getAll should return all of the result values`() { + val values = getAll( + ok("hello"), + ok("big"), + error(IterableError.IterableError2), + ok("wide"), + error(IterableError.IterableError1), + ok("world") + ) + + assertThat(values.size, equalTo(4)) + assertThat(values[0], equalTo("hello")) + assertThat(values[1], equalTo("big")) + assertThat(values[2], equalTo("wide")) + assertThat(values[3], equalTo("world")) + } + + @Test + internal fun `getAllErrors should return all of the result errors`() { + val errors = getAllErrors( + error(IterableError.IterableError2), + ok("haskell"), + error(IterableError.IterableError2), + ok("f#"), + error(IterableError.IterableError1), + ok("elm"), + error(IterableError.IterableError1), + ok("clojure"), + error(IterableError.IterableError2) + ) + + assertThat(errors.size, equalTo(5)) + assertThat(errors[0], sameError(IterableError.IterableError2)) + assertThat(errors[1], sameError(IterableError.IterableError2)) + assertThat(errors[2], sameError(IterableError.IterableError1)) + assertThat(errors[3], sameError(IterableError.IterableError1)) + assertThat(errors[4], sameError(IterableError.IterableError2)) + } + + @Test + internal fun `partition should return a pair of all the result values and errors`() { + val pairs = partition( + error(IterableError.IterableError2), + ok("haskell"), + error(IterableError.IterableError2), + ok("f#"), + error(IterableError.IterableError1), + ok("elm"), + error(IterableError.IterableError1), + ok("clojure"), + error(IterableError.IterableError2) + ) + + val values = pairs.first + assertThat(values.size, equalTo(4)) + assertThat(values[0], equalTo("haskell")) + assertThat(values[1], equalTo("f#")) + assertThat(values[2], equalTo("elm")) + assertThat(values[3], equalTo("clojure")) + + val errors = pairs.second + assertThat(errors.size, equalTo(5)) + assertThat(errors[0], sameError(IterableError.IterableError2)) + assertThat(errors[1], sameError(IterableError.IterableError2)) + assertThat(errors[2], sameError(IterableError.IterableError1)) + assertThat(errors[3], sameError(IterableError.IterableError1)) + assertThat(errors[4], sameError(IterableError.IterableError2)) + } +} diff --git a/src/test/kotlin/com/mikebull94/result/MapTest.kt b/src/test/kotlin/com/mikebull94/result/MapTest.kt new file mode 100644 index 0000000..322c0fb --- /dev/null +++ b/src/test/kotlin/com/mikebull94/result/MapTest.kt @@ -0,0 +1,101 @@ +package com.mikebull94.result + +import com.natpryce.hamkrest.Matcher +import com.natpryce.hamkrest.assertion.assertThat +import com.natpryce.hamkrest.equalTo +import com.natpryce.hamkrest.sameInstance +import org.junit.jupiter.api.Test + +internal class MapTest { + private sealed class MapError(val reason: String) { + object HelloError : MapError("hello") + object WorldError : MapError("world") + class CustomError(reason: String) : MapError(reason) + } + + private fun sameError(error: MapError): Matcher { + return sameInstance(error) + } + + @Test + internal fun `map should return the transformed result value if ok`() { + val value = ok(10).map { it + 20 }.get() + assertThat(value, equalTo(30)) + } + + @Test + internal fun `map should return the result error if not ok`() { + val result = error(MapError.HelloError).map { "hello $it" } + + result as Error + + assertThat(result.error, sameError(MapError.HelloError)) + } + + @Test + internal fun `mapError should return the result value if ok`() { + val value = ok(55).map { it + 15 }.mapError { MapError.WorldError }.get() + assertThat(value, equalTo(70)) + } + + @Test + internal fun `mapError should return the transformed result error if not ok`() { + val result: Result = ok("let") + .map { "$it me" } + .andThen { + when (it) { + "let me" -> error(MapError.CustomError("$it $it")) + else -> ok("$it get") + } + } + .mapError { MapError.CustomError("${it.reason} get what i want") } + + result as Error + + assertThat(result.error.reason, equalTo("let me let me get what i want")) + } + + @Test + internal fun `mapBoth should return the transformed result value if ok`() { + val value = ok("there is").mapBoth( + { "$it a light" }, + { MapError.CustomError("$it that never") } + ) as String + + assertThat(value, equalTo("there is a light")) + } + + @Test + internal fun `mapBoth should return the transformed result error if not ok`() { + val error = error(MapError.CustomError("this")).mapBoth( + { "$it charming" }, + { MapError.CustomError("${it.reason} man") } + ) as MapError.CustomError + + assertThat(error.reason, equalTo("this man")) + } + + @Test + internal fun `mapEither should return the transformed result value if ok`() { + val result = ok(500).mapEither( + { it + 500 }, + { MapError.CustomError(it) } + ) + + result as Ok + + assertThat(result.value, equalTo(1000)) + } + + @Test + internal fun `mapEither should return the transformed result error if not ok`() { + val result = error("the reckless").mapEither( + { "the wild youth" }, + { MapError.CustomError("the truth") } + ) + + result as Error + + assertThat(result.error.reason, equalTo("the truth")) + } +} diff --git a/src/test/kotlin/com/mikebull94/result/OnTest.kt b/src/test/kotlin/com/mikebull94/result/OnTest.kt new file mode 100644 index 0000000..2bf6215 --- /dev/null +++ b/src/test/kotlin/com/mikebull94/result/OnTest.kt @@ -0,0 +1,38 @@ +package com.mikebull94.result + +import com.natpryce.hamkrest.assertion.assertThat +import com.natpryce.hamkrest.equalTo +import org.junit.jupiter.api.Test + +internal class OnTest { + object CounterError + class Counter(var count: Int) + + @Test + internal fun `onSuccess should invoke the callback when result is ok`() { + val counter = Counter(50) + ok(counter).onSuccess { it.count += 50 } + assertThat(counter.count, equalTo(100)) + } + + @Test + internal fun `onSuccess should not invoke the callback when result is not ok`() { + val counter = Counter(200) + error(CounterError).onSuccess { counter.count -= 50 } + assertThat(counter.count, equalTo(200)) + } + + @Test + internal fun `onFailure should invoke the callback when result is not ok`() { + val counter = Counter(555) + error(CounterError).onFailure { counter.count += 100 } + assertThat(counter.count, equalTo(655)) + } + + @Test + internal fun `onFailure should not invoke the callback when result is ok`() { + val counter = Counter(1020) + ok("hello").onFailure { counter.count = 1030 } + assertThat(counter.count, equalTo(1020)) + } +} diff --git a/src/test/kotlin/com/mikebull94/result/OrTest.kt b/src/test/kotlin/com/mikebull94/result/OrTest.kt new file mode 100644 index 0000000..e238a50 --- /dev/null +++ b/src/test/kotlin/com/mikebull94/result/OrTest.kt @@ -0,0 +1,33 @@ +package com.mikebull94.result + +import com.natpryce.hamkrest.assertion.assertThat +import com.natpryce.hamkrest.equalTo +import org.junit.jupiter.api.Test + +internal class OrTest { + private object OrError + + @Test + internal fun `or should return the result value if ok`() { + val value = ok(500).or(1000).get() + assertThat(value, equalTo(500)) + } + + @Test + internal fun `or should return the default value if not ok`() { + val error = error(OrError).or(5000).get() + assertThat(error, equalTo(5000)) + } + + @Test + internal fun `extract should return the result value if ok`() { + val value = ok("hello").extract { OrError } as String + assertThat(value, equalTo("hello")) + } + + @Test + internal fun `extract should return the transformed result error if not ok`() { + val error = error("hello").extract { "$it darkness" } + assertThat(error, equalTo("hello darkness")) + } +}