diff --git a/build.gradle b/build.gradle index b70e6b0..e65ed59 100644 --- a/build.gradle +++ b/build.gradle @@ -14,15 +14,18 @@ plugins { id('io.github.joselion.pretty-jupiter') version '3.0.1' id('io.github.joselion.strict-null-check') version '3.0.0' id('io.github.gradle-nexus.publish-plugin') version '2.0.0-rc-1' + id('me.champeau.mrjar') version '0.1.1' id('name.remal.sonarlint') version '3.3.8' } group = 'io.github.joselion' +multiRelease { + targetVersions(11, 17, 20) +} + java { - sourceCompatibility = JavaLanguageVersion.of(17) toolchain { - languageVersion = JavaLanguageVersion.of(20) vendor = JvmVendorSpec.ORACLE } withJavadocJar() @@ -101,19 +104,23 @@ dependencies { because('version 9.0.75 has a security vulnerability') } } - - testImplementation('org.assertj:assertj-core:3.24.2') - testImplementation('org.mockito:mockito-core:5.5.0') } testing { suites { test { useJUnitJupiter('5.10.0') + dependencies { + implementation('org.assertj:assertj-core:3.24.2') + implementation('org.mockito:mockito-core:5.5.0') + } } } } +java17Test.useJUnitPlatform() +java20Test.useJUnitPlatform() + prettyJupiter { duration.threshold = 150 } diff --git a/buildscript-gradle.lockfile b/buildscript-gradle.lockfile index 9b117f6..6e6df84 100644 --- a/buildscript-gradle.lockfile +++ b/buildscript-gradle.lockfile @@ -8,6 +8,8 @@ io.github.joselion.strict-null-check:io.github.joselion.strict-null-check.gradle io.github.joselion:maybe:3.2.0=classpath io.github.joselion:pretty-jupiter:3.0.1=classpath io.github.joselion:strict-null-check:3.0.0=classpath +me.champeau.gradle.mrjar:plugin:0.1.1=classpath +me.champeau.mrjar:me.champeau.mrjar.gradle.plugin:0.1.1=classpath name.remal.gradle-plugins.sonarlint:sonarlint:3.3.8=classpath name.remal.sonarlint:name.remal.sonarlint.gradle.plugin:3.3.8=classpath empty= diff --git a/gradle.lockfile b/gradle.lockfile index bd1b858..c00a5fc 100644 --- a/gradle.lockfile +++ b/gradle.lockfile @@ -17,8 +17,8 @@ commons-codec:commons-codec:1.15=checkstyle commons-collections:commons-collections:3.2.2=checkstyle commons-io:commons-io:2.11.0=sonarlintCoreClasspath info.picocli:picocli:4.7.4=checkstyle -net.bytebuddy:byte-buddy-agent:1.14.6=testCompileClasspath,testRuntimeClasspath -net.bytebuddy:byte-buddy:1.14.6=testCompileClasspath,testRuntimeClasspath +net.bytebuddy:byte-buddy-agent:1.14.6=java17TestCompileClasspath,java17TestRuntimeClasspath,java20TestCompileClasspath,java20TestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +net.bytebuddy:byte-buddy:1.14.6=java17TestCompileClasspath,java17TestRuntimeClasspath,java20TestCompileClasspath,java20TestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath net.sf.saxon:Saxon-HE:12.3=checkstyle org.antlr:antlr4-runtime:4.11.1=checkstyle org.apache.commons:commons-compress:1.21=sonarlintCoreClasspath @@ -39,8 +39,8 @@ org.apache.tomcat.embed:tomcat-embed-el:9.0.75=sonarlintCoreClasspath org.apache.tomcat.embed:tomcat-embed-jasper:9.0.75=sonarlintCoreClasspath org.apache.tomcat:tomcat-annotations-api:9.0.80=sonarlintCoreClasspath org.apache.xbean:xbean-reflect:3.7=checkstyle -org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath -org.assertj:assertj-core:3.24.2=testCompileClasspath,testRuntimeClasspath +org.apiguardian:apiguardian-api:1.1.2=java17TestCompileClasspath,java20TestCompileClasspath,testCompileClasspath +org.assertj:assertj-core:3.24.2=java17TestCompileClasspath,java17TestRuntimeClasspath,java20TestCompileClasspath,java20TestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath org.checkerframework:checker-qual:3.27.0=checkstyle org.codehaus.plexus:plexus-classworlds:2.6.0=checkstyle org.codehaus.plexus:plexus-component-annotations:2.1.0=checkstyle @@ -48,7 +48,7 @@ org.codehaus.plexus:plexus-container-default:2.1.0=checkstyle org.codehaus.plexus:plexus-utils:3.3.0=checkstyle org.codehaus.woodstox:stax2-api:4.2.1=sonarlintCoreClasspath org.eclipse.jdt:ecj:3.33.0=sonarlintCoreClasspath -org.eclipse.jdt:org.eclipse.jdt.annotation:2.2.700=compileClasspath,testCompileClasspath +org.eclipse.jdt:org.eclipse.jdt.annotation:2.2.700=compileClasspath,java17CompileClasspath,java17TestCompileClasspath,java20CompileClasspath,java20TestCompileClasspath,testCompileClasspath org.eclipse.jdt:org.eclipse.jdt.core:3.33.0=sonarlintCoreClasspath org.eclipse.platform:org.eclipse.core.commands:3.10.300=sonarlintCoreClasspath org.eclipse.platform:org.eclipse.core.contenttype:3.8.100=sonarlintCoreClasspath @@ -64,17 +64,17 @@ org.jacoco:org.jacoco.ant:0.8.10=jacocoAnt org.jacoco:org.jacoco.core:0.8.10=jacocoAnt org.jacoco:org.jacoco.report:0.8.10=jacocoAnt org.javassist:javassist:3.28.0-GA=checkstyle -org.junit.jupiter:junit-jupiter-api:5.10.0=testCompileClasspath,testRuntimeClasspath -org.junit.jupiter:junit-jupiter-engine:5.10.0=testRuntimeClasspath -org.junit.jupiter:junit-jupiter-params:5.10.0=testCompileClasspath,testRuntimeClasspath -org.junit.jupiter:junit-jupiter:5.10.0=testCompileClasspath,testRuntimeClasspath -org.junit.platform:junit-platform-commons:1.10.0=testCompileClasspath,testRuntimeClasspath -org.junit.platform:junit-platform-engine:1.10.0=testRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:5.10.0=java17TestCompileClasspath,java17TestRuntimeClasspath,java20TestCompileClasspath,java20TestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-engine:5.10.0=java17TestRuntimeClasspath,java20TestRuntimeClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-params:5.10.0=java17TestCompileClasspath,java17TestRuntimeClasspath,java20TestCompileClasspath,java20TestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter:5.10.0=java17TestCompileClasspath,java17TestRuntimeClasspath,java20TestCompileClasspath,java20TestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-commons:1.10.0=java17TestCompileClasspath,java17TestRuntimeClasspath,java20TestCompileClasspath,java20TestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-engine:1.10.0=java17TestRuntimeClasspath,java20TestRuntimeClasspath,testRuntimeClasspath org.junit.platform:junit-platform-launcher:1.10.0=testRuntimeClasspath -org.junit:junit-bom:5.10.0=testCompileClasspath,testRuntimeClasspath -org.mockito:mockito-core:5.5.0=testCompileClasspath,testRuntimeClasspath -org.objenesis:objenesis:3.3=testRuntimeClasspath -org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath +org.junit:junit-bom:5.10.0=java17TestCompileClasspath,java17TestRuntimeClasspath,java20TestCompileClasspath,java20TestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.mockito:mockito-core:5.5.0=java17TestCompileClasspath,java17TestRuntimeClasspath,java20TestCompileClasspath,java20TestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.objenesis:objenesis:3.3=java17TestRuntimeClasspath,java20TestRuntimeClasspath,testRuntimeClasspath +org.opentest4j:opentest4j:1.3.0=java17TestCompileClasspath,java17TestRuntimeClasspath,java20TestCompileClasspath,java20TestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath org.osgi:org.osgi.service.prefs:1.1.2=sonarlintCoreClasspath org.osgi:osgi.annotation:8.0.1=sonarlintCoreClasspath org.ow2.asm:asm-commons:9.5=jacocoAnt @@ -107,4 +107,4 @@ org.sonarsource.xml:sonar-xml-plugin:2.10.0.4108=sonarlintCoreClasspath org.xmlresolver:xmlresolver:5.2.0=checkstyle xerces:xercesImpl:2.12.2=sonarlintCoreClasspath xml-apis:xml-apis:1.4.01=sonarlintCoreClasspath -empty=annotationProcessor,runtimeClasspath,testAnnotationProcessor +empty=annotationProcessor,java17AnnotationProcessor,java17RuntimeClasspath,java17TestAnnotationProcessor,java20AnnotationProcessor,java20RuntimeClasspath,java20TestAnnotationProcessor,runtimeClasspath,testAnnotationProcessor diff --git a/src/main/java/io/github/joselion/maybe/Maybe.java b/src/main/java/io/github/joselion/maybe/Maybe.java index 1849c5a..5f46ea9 100644 --- a/src/main/java/io/github/joselion/maybe/Maybe.java +++ b/src/main/java/io/github/joselion/maybe/Maybe.java @@ -134,7 +134,7 @@ public static EffectHandler fromEffect(final ThrowingRu * In other words, the following code *
    *  Optional.of(value)
-   *    .map(str -> Maybe.fromResolver(() -> decode(str)));
+   *    .map(str -> Maybe.fromResolver(() -> decode(str)));
    * 
* Is equivalent to *
@@ -166,7 +166,7 @@ public static  Function> part
    * In other words, the following code
    * 
    *  Optional.of(value)
-   *    .map(msg -> Maybe.fromEffect(() -> sendMessage(msg)));
+   *    .map(msg -> Maybe.fromEffect(() -> sendMessage(msg)));
    * 
* Is equivalent to *
@@ -341,7 +341,8 @@ public boolean equals(final Object obj) {
       return true;
     }
 
-    if (obj instanceof final Maybe other) {
+    if (obj instanceof Maybe) {
+      final var other = (Maybe) obj;
       return other.toOptional().equals(value);
     }
 
@@ -370,7 +371,7 @@ public int hashCode() {
   public String toString() {
     return value
       .map(Object::toString)
-      .map("Maybe[%s]"::formatted)
+      .map(it -> String.format("Maybe[%s]", it))
       .orElse("Maybe.nothing");
   }
 }
diff --git a/src/main/java/io/github/joselion/maybe/util/Either.java b/src/main/java/io/github/joselion/maybe/util/Either.java
index d5ff374..ee1dfab 100644
--- a/src/main/java/io/github/joselion/maybe/util/Either.java
+++ b/src/main/java/io/github/joselion/maybe/util/Either.java
@@ -21,7 +21,7 @@
  * @author Jose Luis Leon
  * @since v3.0.0
  */
-public sealed interface Either {
+public interface Either {
 
   /**
    * Factory method to create an {@code Either} instance that contains a
@@ -174,17 +174,23 @@ default Optional rightToOptional() {
    *
    * @param  the {@code Left} data type
    * @param  the {@code Right} data type
-   * @param value the left value
    */
-  record Left(L value) implements Either {
+  final class Left implements Either {
+
+    private final L value;
 
     /**
      * Compact constructor to validate the value is not null.
      *
      * @param value the value of the instance
      */
-    public Left {
+    Left(final L value) {
       Objects.requireNonNull(value, "An Either cannot be created with a null value");
+      this.value = value;
+    }
+
+    L value() {
+      return this.value;
     }
 
     @Override
@@ -242,7 +248,8 @@ public boolean equals(final Object obj) {
         return true;
       }
 
-      if (obj instanceof final Left left) {
+      if (obj instanceof Left) {
+        final var left = (Left) obj;
         return this.value.equals(left.leftOrNull());
       }
 
@@ -256,7 +263,7 @@ public int hashCode() {
 
     @Override
     public String toString() {
-      return "Either[Left: %s]".formatted(this.value);
+      return String.format("Either[Left: %s]", this.value);
     }
   }
 
@@ -265,17 +272,23 @@ public String toString() {
    *
    * @param  the {@code Left} data type
    * @param  the {@code Right} data type
-   * @param value the right value
    */
-  record Right(R value) implements Either {
+  final class Right implements Either {
+
+    private final R value;
 
     /**
      * Compact constructor to validate the value is not null.
      *
      * @param value the value of the instance
      */
-    public Right {
+    Right(final R value) {
       Objects.requireNonNull(value, "An Either cannot be created with a null value");
+      this.value = value;
+    }
+
+    R value() {
+      return this.value;
     }
 
     @Override
@@ -333,7 +346,8 @@ public boolean equals(final Object obj) {
         return true;
       }
 
-      if (obj instanceof final Right right) {
+      if (obj instanceof Right) {
+        final var right = (Right) obj;
         return this.value.equals(right.rightOrNull());
       }
 
@@ -347,7 +361,7 @@ public int hashCode() {
 
     @Override
     public String toString() {
-      return "Either[Right: %s]".formatted(this.value);
+      return String.format("Either[Right: %s]", this.value);
     }
   }
 }
diff --git a/src/main/java17/io/github/joselion/maybe/Maybe.java b/src/main/java17/io/github/joselion/maybe/Maybe.java
new file mode 100644
index 0000000..1849c5a
--- /dev/null
+++ b/src/main/java17/io/github/joselion/maybe/Maybe.java
@@ -0,0 +1,376 @@
+package io.github.joselion.maybe;
+
+import java.util.NoSuchElementException;
+import java.util.Optional;
+import java.util.function.Function;
+
+import org.eclipse.jdt.annotation.Nullable;
+
+import io.github.joselion.maybe.util.function.ThrowingConsumer;
+import io.github.joselion.maybe.util.function.ThrowingFunction;
+import io.github.joselion.maybe.util.function.ThrowingRunnable;
+import io.github.joselion.maybe.util.function.ThrowingSupplier;
+
+/**
+ * Maybe is a monadic wrapper that may contain a value. Its rich API allows to
+ * process throwing operations in a functional way leveraging {@link Optional}
+ * to unwrap the possible contained value.
+ * 
+ * @param  the type of the wrapped value
+ * 
+ * @author Jose Luis Leon
+ * @since v0.1.0
+ */
+public final class Maybe {
+
+  private final Optional value;
+
+  private Maybe(final @Nullable T value) {
+    this.value = Optional.ofNullable(value);
+  }
+
+  /**
+   * Internal use only.
+   *
+   * @return the possible wrapped value
+   */
+  Optional value() {
+    return value;
+  }
+
+  /**
+   * Creates a {@link Maybe} wrapper of the given value. If the value is
+   * {@code null}, it returns a {@link #nothing()}.
+   * 
+   * @param  the type of the value
+   * @param value the value be wrapped
+   * @return a {@code Maybe} wrapping the value if it's non-{@code null},
+   *         {@link #nothing()} otherwise
+   */
+  public static  Maybe just(final T value) {
+    return new Maybe<>(value);
+  }
+
+  /**
+   * Creates a {@link Maybe} wrapper with nothing on it. This means the wrapper
+   * does not contains a value because an exception may have occurred.
+   * 
+   * @param  the type of the value
+   * @return a {@code Maybe} with nothing
+   */
+  public static  Maybe nothing() {
+    return new Maybe<>(null);
+  }
+
+  /**
+   * Creates a {@link Maybe} wrapper of the given value if the optional is not
+   * empty. Returns a {@link #nothing()} otherwise.
+   * 

+ * This is a convenience creator that would be equivalent to: + *

+   *  Maybe.just(opt)
+   *    .resolve(Optional::get)
+   *    .toMaybe();
+   * 
+ * + * @param the type of the value + * @param value an optional value to create the wrapper from + * @return a {@code Maybe} wrapping the value if it's not empty. + * {@link #nothing()} otherwise + */ + public static Maybe fromOptional(final Optional value) { + return new Maybe<>(value.orElse(null)); + } + + /** + * Resolves the value of a throwing operation using a {@link ThrowingSupplier} + * expression. Returning then a {@link ResolveHandler} which allows to handle + * the possible error and return a safe value. + * + * @param the type of the value returned by the {@code resolver} + * @param the type of exception the {@code resolver} may throw + * @param resolver the checked supplier operation to resolve + * @return a {@link ResolveHandler} with either the value resolved or the thrown + * exception to be handled + */ + public static ResolveHandler fromResolver(final ThrowingSupplier resolver) { + try { + return ResolveHandler.ofSuccess(resolver.get()); + } catch (Throwable e) { // NOSONAR + @SuppressWarnings("unchecked") + final var error = (E) e; + return ResolveHandler.ofError(error); + } + } + + /** + * Runs an effect that may throw an exception using a {@link ThrowingRunnable} + * expression. Returning then an {@link EffectHandler} which allows to handle + * the possible error. + * + * @param the type of exception the {@code effect} may throw + * @param effect the checked runnable operation to execute + * @return an {@link EffectHandler} with either the thrown exception to be + * handled or nothing + */ + public static EffectHandler fromEffect(final ThrowingRunnable effect) { + try { + effect.run(); + return EffectHandler.empty(); + } catch (Throwable e) { // NOSONAR + @SuppressWarnings("unchecked") + final var error = (E) e; + return EffectHandler.ofError(error); + } + } + + /** + * Convenience partial application of a {@code resolver}. This method creates + * a function that receives an {@code S} value which can be used to produce a + * {@link ResolveHandler} once applied. This is specially useful when we want + * to create a {@link Maybe} from a callback argument, like on a + * {@link Optional#map(Function)} for instance. + *

+ * In other words, the following code + *

+   *  Optional.of(value)
+   *    .map(str -> Maybe.fromResolver(() -> decode(str)));
+   * 
+ * Is equivalent to + *
+   *  Optional.of(value)
+   *    .map(Maybe.partialResolver(this::decode));
+   * 
+ * + * @param the type of the value the returned function receives + * @param the type of the value to be resolved + * @param the type of the error the resolver may throw + * @param resolver a checked function that receives an {@code S} value and + * returns a {@code T} value + * @return a partially applied {@link ResolveHandler}. This means, a function + * that receives an {@code S} value, and produces a {@code ResolveHandler} + */ + public static Function> partialResolver( + final ThrowingFunction resolver + ) { + return value -> Maybe.fromResolver(() -> resolver.apply(value)); + } + + /** + * Convenience partial application of an {@code effect}. This method creates + * a function that receives an {@code S} value which can be used to produce + * an {@link EffectHandler} once applied. This is specially useful when we + * want to create a {@link Maybe} from a callback argument, like on a + * {@link Optional#map(Function)} for instance. + *

+ * In other words, the following code + *

+   *  Optional.of(value)
+   *    .map(msg -> Maybe.fromEffect(() -> sendMessage(msg)));
+   * 
+ * Is equivalent to + *
+   *  Optional.of(value)
+   *    .map(Maybe.partialEffect(this::sendMessage));
+   * 
+ * + * @param the type of the value the returned function receives + * @param the type of the error the resolver may throw + * @param effect a checked consumer that receives an {@code S} value + * @return a partially applied {@link EffectHandler}. This means, a function + * that receives an {@code S} value, and produces an {@code EffectHandler} + */ + public static Function> partialEffect( + final ThrowingConsumer effect + ) { + return value -> Maybe.fromEffect(() -> effect.accept(value)); + } + + /** + * Prepare an {@link AutoCloseable} resource to use in a resolver or effect. + * The resource will be automatically closed after the operation is finished, + * just like a common try-with-resources statement. + * + * @param the type of the resource. Extends from {@link AutoCloseable} + * @param the type of error the holder may have + * @param resource the {@link AutoCloseable} resource to prepare + * @return a {@link ResourceHolder} which let's you choose to resolve a value + * or run an effect using the prepared resource + */ + public static ResourceHolder withResource(final R resource) { + return ResourceHolder.from(resource); + } + + /** + * If present, maps the value to another using the provided mapper function. + * Otherwise, ignores the mapper and returns {@link #nothing()}. + * + * @param the type the value will be mapped to + * @param mapper the mapper function + * @return a {@code Maybe} with the mapped value if present, + * {@link #nothing()} otherwise + */ + public Maybe map(final Function mapper) { + return value.map(mapper) + .map(Maybe::just) + .orElseGet(Maybe::nothing); + } + + /** + * If present, maps the value to another using the provided mapper function. + * Otherwise, ignores the mapper and returns {@link #nothing()}. + * + * This method is similar to {@link #map(Function)}, but the mapping function is + * one whose result is already a {@code Maybe}, and if invoked, flatMap does not + * wrap it within an additional {@code Maybe}. + * + * @param the type the value will be mapped to + * @param mapper the mapper function + * @return a {@code Maybe} with the mapped value if present, + * {@link #nothing()} otherwise + */ + public Maybe flatMap(final Function> mapper) { + return value.map(mapper) + .orElseGet(Maybe::nothing); + } + + /** + * Chain the {@code Maybe} with another resolver, if and only if the previous + * operation was handled with no errors. The value of the previous operation + * is passed as argument of the {@link ThrowingFunction}. + * + * @param the type of value returned by the next operation + * @param the type of exception the new resolver may throw + * @param resolver a checked function that receives the current value and + * resolves another + * @return a {@link ResolveHandler} with either the resolved value, or the + * thrown exception to be handled + */ + @SuppressWarnings("unchecked") + public ResolveHandler resolve(final ThrowingFunction resolver) { + try { + return value + .map(Maybe.partialResolver(resolver)) + .orElseThrow(); + } catch (final NoSuchElementException error) { + return ResolveHandler.ofError((E) error); + } + } + + /** + * Chain the {@code Maybe} with another effect, if and only if the previous + * operation was handled with no errors. + * + * @param the type of exception the new effect may throw + * @param effect the checked runnable operation to execute next + * @return an {@link EffectHandler} with either the thrown exception to be + * handled or nothing + */ + @SuppressWarnings("unchecked") + public EffectHandler runEffect(final ThrowingConsumer effect) { + try { + return value + .map(Maybe.partialEffect(effect)) + .orElseThrow(); + } catch (final NoSuchElementException error) { + return EffectHandler.ofError((E) error); + } + } + + /** + * If the value is present, cast the value to another type. In case of an + * exception during the cast, a Maybe with {@link #nothing()} is returned. + * + * @param the type that the value will be cast to + * @param type the class instance of the type to cast + * @return a new {@code Maybe} with the cast value if it can be cast, + * {@link #nothing()} otherwise + */ + public Maybe cast(final Class type) { + try { + final var newValue = type.cast(value.orElseThrow()); + return Maybe.just(newValue); + } catch (final ClassCastException error) { + return nothing(); + } + } + + /** + * Checks if the {@code Maybe} has a value. + * + * @return true if the value is present, false otherwise + */ + public boolean hasValue() { + return value.isPresent(); + } + + /** + * Checks if the {@code Maybe} has nothing. That is, when no value is present. + * + * @return true if the value is NOT present, false otherwise + */ + public boolean hasNothing() { + return value.isEmpty(); + } + + /** + * Safely unbox the value as an {@link Optional} which may or may not contain + * a value. + * + * @return an optional with the value, if preset. An empty optional otherwise + */ + public Optional toOptional() { + return value; + } + + /** + * Checks if some other object is equal to this {@code Maybe}. For two objects + * to be equal they both must: + *
    + *
  • Be an instance of {@code Maybe}
  • + *
  • Contain a values equal to via {@code equals()} comparation
  • + *
+ * + * @param obj an object to be tested for equality + * @return {@code true} if the other object is "equal to" this object, + * {@code false} otherwise + */ + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + + if (obj instanceof final Maybe other) { + return other.toOptional().equals(value); + } + + return false; + } + + /** + * Returns the hash code of the value, if present, otherwise {@code 0} (zero) + * if no value is present. + * + * @return hash code value of the present value or {@code 0} if no value is present + */ + @Override + public int hashCode() { + return value.hashCode(); + } + + /** + * Returns a non-empty string representation of this {@code Maybe} suitable + * for debugging. The exact presentation format is unspecified and may vary + * between implementations and versions. + * + * @return the string representation of this instance + */ + @Override + public String toString() { + return value + .map(Object::toString) + .map("Maybe[%s]"::formatted) + .orElse("Maybe.nothing"); + } +} diff --git a/src/main/java17/io/github/joselion/maybe/util/Either.java b/src/main/java17/io/github/joselion/maybe/util/Either.java new file mode 100644 index 0000000..d5ff374 --- /dev/null +++ b/src/main/java17/io/github/joselion/maybe/util/Either.java @@ -0,0 +1,353 @@ +package io.github.joselion.maybe.util; + +import java.util.Objects; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; + +import org.eclipse.jdt.annotation.Nullable; + +/** + * Either is a monadic wrapper that contains one of two possible values which + * are represented as {@code Left} or {@code Right}. the values can be of + * different types, and the API allows to safely transform an unwrap the value. + * + * The sealed interface implementation ensures only one of the two can be + * present at the same time. + * + * @param the {@code Left} data type + * @param the {@code Right} data type + * + * @author Jose Luis Leon + * @since v3.0.0 + */ +public sealed interface Either { + + /** + * Factory method to create an {@code Either} instance that contains a + * {@code Left} value. + * + * @param the type of the left value + * @param the type of the right value + * @param value the value to use as left in the {@code Either} instance + * @return an {@code Either} instance with a left value + */ + static Either ofLeft(final L value) { + return new Left<>(value); + } + + /** + * Factory method to create an {@code Either} instance that contains a + * {@code Right} value. + * + * @param the type of the left value + * @param the type of the right value + * @param value the value to use as right in the {@code Either} instance + * @return an {@code Either} instance with a right value + */ + static Either ofRight(final R value) { + return new Right<>(value); + } + + /** + * Returns true if the {@code Left} value is present, false otherwise. + * + * @return true if left is present, false otherwise + */ + boolean isLeft(); + + /** + * Returns true if the {@code Right} value is present, false otherwise. + * + * @return true if right is present, false otherwise + */ + boolean isRight(); + + /** + * Run an effect if the {@code Left} value is present. Does nothing otherwise. + * + * @param effect a consumer function that receives the left value + * @return the same {@code Either} instance + */ + Either doOnLeft(Consumer effect); + + /** + * Run an effect if the {@code Right} value is present. Does nothing otherwise. + * + * @param effect effect a consumer function that receives the right value + * @return the same {@code Either} instance + */ + Either doOnRight(Consumer effect); + + /** + * Map the {@code Left} value to another if present. Does nothing otherwise. + * + * @param the type the left value will be mapped to + * @param mapper a function that receives the left value and returns another + * @return an {@code Either} instance with the mapped left value + */ + Either mapLeft(Function mapper); + + /** + * Map the {@code Right} value to another if present. Does nothing otherwise. + * + * @param the type the right value will be mapped to + * @param mapper a function that receives the right value and returns another + * @return an {@code Either} instance with the mapped right value + */ + Either mapRight(Function mapper); + + /** + * Terminal operator. Returns the {@code Left} value if present. Otherwise, + * it returns the provided fallback value. + * + * @param fallback the value to return if left is not present + * @return the left value or a fallback + */ + L leftOrElse(L fallback); + + /** + * Terminal operator. Returns the {@code Right} value if present. Otherwise, + * it returns the provided fallback value. + * + * @param fallback the value to return if right is not present + * @return the right value or a fallback + */ + R rightOrElse(R fallback); + + /** + * Terminal operator. Unwraps the {@code Either} to obtain the wrapped value. + * Since there's no possible way for the compiler to know which one is + * present ({@code Left} or {@code Right}), you need to provide a handler for + * both cases. Only the handler with the value present is used to unwrap and + * return the value. + * + * @param the type of the returned value + * @param onLeft a function to handle the left value if present + * @param onRight a function to handle the right value if present + * @return either the left or the right handled value + */ + T unwrap(Function onLeft, Function onRight); + + /** + * Terminal operator. Returns the {@code Left} value if present. Otherwise, + * it returns {@code null}. + * + * @return the left value or null + */ + default @Nullable L leftOrNull() { + return unwrap(Function.identity(), rigth -> null); + } + + /** + * Terminal operator. Returns the {@code Right} value if present. Otherwise, + * it returns {@code null}. + * + * @return the right value or null + */ + default @Nullable R rightOrNull() { + return unwrap(left -> null, Function.identity()); + } + + /** + * Terminal operator. Transforms the {@code Left} value to an {@link Optional}, + * which contains the value if present or is {@link Optional#empty()} otherwise. + * + * @return an {@code Optional} instance + */ + default Optional leftToOptional() { + return Optional.ofNullable(leftOrNull()); + } + + /** + * Terminal operator. Transforms the {@code Right} value to an {@link Optional}, + * which contains the value if present or is {@link Optional#empty()} otherwise. + * + * @return an {@code Optional} instance + */ + default Optional rightToOptional() { + return Optional.ofNullable(rightOrNull()); + } + + /** + * The {@code Left} implementation of {@link Either} + * + * @param the {@code Left} data type + * @param the {@code Right} data type + * @param value the left value + */ + record Left(L value) implements Either { + + /** + * Compact constructor to validate the value is not null. + * + * @param value the value of the instance + */ + public Left { + Objects.requireNonNull(value, "An Either cannot be created with a null value"); + } + + @Override + public boolean isLeft() { + return true; + } + + @Override + public boolean isRight() { + return false; + } + + @Override + public Either doOnLeft(final Consumer effect) { + effect.accept(this.value); + + return this; + } + + @Override + public Either doOnRight(final Consumer effect) { + return this; + } + + @Override + public Either mapLeft(final Function mapper) { + final var mappedLeft = mapper.apply(this.value); + + return new Left<>(mappedLeft); + } + + @Override + public Either mapRight(final Function mapper) { + return new Left<>(this.value); + } + + @Override + public L leftOrElse(final L fallback) { + return this.value; + } + + @Override + public R rightOrElse(final R fallback) { + return fallback; + } + + @Override + public T unwrap(final Function onLeft, final Function onRight) { + return onLeft.apply(this.value); + } + + @Override + public boolean equals(final Object obj) { + if (obj == this) { + return true; + } + + if (obj instanceof final Left left) { + return this.value.equals(left.leftOrNull()); + } + + return false; + } + + @Override + public int hashCode() { + return this.value.hashCode(); + } + + @Override + public String toString() { + return "Either[Left: %s]".formatted(this.value); + } + } + + /** + * The {@code Right} implementation of {@link Either} + * + * @param the {@code Left} data type + * @param the {@code Right} data type + * @param value the right value + */ + record Right(R value) implements Either { + + /** + * Compact constructor to validate the value is not null. + * + * @param value the value of the instance + */ + public Right { + Objects.requireNonNull(value, "An Either cannot be created with a null value"); + } + + @Override + public boolean isLeft() { + return false; + } + + @Override + public boolean isRight() { + return true; + } + + @Override + public Either doOnLeft(final Consumer effect) { + return this; + } + + @Override + public Either doOnRight(final Consumer effect) { + effect.accept(this.value); + + return this; + } + + @Override + public Either mapLeft(final Function mapper) { + return new Right<>(this.value); + } + + @Override + public Either mapRight(final Function mapper) { + final var mappedRight = mapper.apply(this.value); + + return new Right<>(mappedRight); + } + + @Override + public L leftOrElse(final L fallback) { + return fallback; + } + + @Override + public R rightOrElse(final R fallback) { + return this.value; + } + + @Override + public T unwrap(final Function onLeft, final Function onRight) { + return onRight.apply(this.value); + } + + @Override + public boolean equals(final Object obj) { + if (obj == this) { + return true; + } + + if (obj instanceof final Right right) { + return this.value.equals(right.rightOrNull()); + } + + return false; + } + + @Override + public int hashCode() { + return this.value.hashCode(); + } + + @Override + public String toString() { + return "Either[Right: %s]".formatted(this.value); + } + } +} diff --git a/src/test/java/io/github/joselion/maybe/util/EitherTest.java b/src/test/java/io/github/joselion/maybe/util/EitherTest.java index 53f9e0d..ccfd8b9 100644 --- a/src/test/java/io/github/joselion/maybe/util/EitherTest.java +++ b/src/test/java/io/github/joselion/maybe/util/EitherTest.java @@ -243,8 +243,8 @@ @Nested class unwrap { @Nested class when_the_left_value_is_present { @Test void returns_the_value_using_the_onLeft_handler() { - final var onLeftSpy = Spy.>lambda("The value is: %s"::formatted); - final var onRightSpy = Spy.>lambda("The value is: %s"::formatted); + final var onLeftSpy = Spy.>lambda("The value is: "::concat); + final var onRightSpy = Spy.>lambda(x -> "The value is: ".concat(x.toString())); final var value = Either.ofLeft("foo").unwrap(onLeftSpy, onRightSpy); assertThat(value).isEqualTo("The value is: foo"); @@ -256,8 +256,8 @@ @Nested class when_the_right_value_is_present { @Test void returns_the_value_using_the_onRight_handler() { - final var onLeftSpy = Spy.>lambda("The value is: %s"::formatted); - final var onRightSpy = Spy.>lambda("The value is: %s"::formatted); + final var onLeftSpy = Spy.>lambda(x -> "The value is: ".concat(x.toString())); + final var onRightSpy = Spy.>lambda("The value is: "::concat); final var value = Either.ofRight("foo").unwrap(onLeftSpy, onRightSpy); assertThat(value).isEqualTo("The value is: foo");