From c7e4c5720d40708d11b39783abd94b6d184dc5a8 Mon Sep 17 00:00:00 2001 From: JoseLion Date: Wed, 13 Sep 2023 00:57:15 -0500 Subject: [PATCH] =?UTF-8?q?=EF=BB=BFfeat(compat):=20Provide=20JDK11=20comp?= =?UTF-8?q?atibilty=20through=20Multi-Release=20JAR?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 82 ++-- buildscript-gradle.lockfile | 11 +- config/checkstyle/checkstyle.xml | 18 + config/checkstyle/suppressions.xml | 5 +- gradle.lockfile | 84 +++- .../java/io/github/joselion/maybe/Maybe.java | 9 +- .../io/github/joselion/maybe/util/Either.java | 36 +- .../io/github/joselion/maybe/Maybe.java | 376 ++++++++++++++++++ .../io/github/joselion/maybe/util/Either.java | 353 ++++++++++++++++ 9 files changed, 908 insertions(+), 66 deletions(-) create mode 100644 src/main/java17/io/github/joselion/maybe/Maybe.java create mode 100644 src/main/java17/io/github/joselion/maybe/util/Either.java diff --git a/build.gradle b/build.gradle index 2d0c9d8..5342abd 100644 --- a/build.gradle +++ b/build.gradle @@ -5,24 +5,27 @@ buildscript { } plugins { - id 'checkstyle' - id 'jacoco' - id 'java-library' - id 'maven-publish' - id 'signing' + id('checkstyle') + id('jacoco') + id('java-library') + id('maven-publish') + id('signing') - 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 'se.solrike.sonarlint' version '1.0.0-beta.15' + 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() @@ -42,21 +45,32 @@ jar { from(sourceSets.main.allSource) manifest { attributes( + 'Build-Jdk': "${System.properties['java.version']} (${System.properties['java.vendor']} ${System.properties['java.vm.version']})", + 'Build-Jdk-Spec': java.sourceCompatibility, + 'Build-Timestamp': LocalDateTime.now().toString(), + 'Created-By': "Gradle ${gradle.gradleVersion}", 'Implementation-Title': project.name, 'Implementation-Version': project.version, + 'Package': "${project.group}.${project.name}", ) } } -sonarlint { - excludeRules = [ - 'java:S107', // Allow constructors with more than 7 parameters - 'java:S3776', // Allow methods with more than 15 lines - 'java:S4032', // Allow packages only containing `package-info.java` - ] - includeRules = [ - 'java:S4266', // "Stream.collect()" calls should not be redundant - ] +sonarLint { + setToolVersion('9.2.0.74516') + languages { + include('java') + } + rules { + enable( + 'java:S4266', // "Stream.collect()" calls should not be redundant + ) + disable( + 'java:S107', // Allow constructors with more than 7 parameters + 'java:S3776', // Allow methods with more than 15 lines + 'java:S4032', // Allow packages only containing `package-info.java` + ) + } } strictNullCheck { @@ -83,20 +97,38 @@ repositories { } dependencies { - sonarlintPlugins('org.sonarsource.java:sonar-java-plugin:7.20.0.31692') - - testImplementation('org.assertj:assertj-core:3.24.2') - testImplementation('org.mockito:mockito-core:5.5.0') + sonarlintCorePlugins('org.sonarsource.java:sonar-java-plugin:7.24.0.32100') } testing { suites { - test { + configureEach { useJUnitJupiter('5.10.0') + + dependencies { + implementation('org.assertj:assertj-core:3.24.2') + implementation('org.mockito:mockito-core:5.5.0') + } + + targets { + all { + testTask.configure { + javaLauncher = javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(20) + } + } + } + } } } } +tasks.withType(JavaCompile).matching { it.name.contains('Test') }.configureEach { + javaCompiler = javaToolchains.compilerFor { + languageVersion = JavaLanguageVersion.of(20) + } +} + prettyJupiter { duration.threshold = 150 } diff --git a/buildscript-gradle.lockfile b/buildscript-gradle.lockfile index 28914e4..6e6df84 100644 --- a/buildscript-gradle.lockfile +++ b/buildscript-gradle.lockfile @@ -1,7 +1,6 @@ # This is a Gradle generated file for dependency locking. # Manual edits can break the build and are not advised. # This file is expected to be part of source control. -io.github.furstenheim:copy_down:1.1=classpath io.github.gradle-nexus.publish-plugin:io.github.gradle-nexus.publish-plugin.gradle.plugin:2.0.0-rc-1=classpath io.github.gradle-nexus:publish-plugin:2.0.0-rc-1=classpath io.github.joselion.pretty-jupiter:io.github.joselion.pretty-jupiter.gradle.plugin:3.0.1=classpath @@ -9,10 +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 -org.apache.commons:commons-lang3:3.11=classpath -org.apache.commons:commons-text:1.9=classpath -org.jsoup:jsoup:1.15.2=classpath -org.sonarsource.sonarlint.core:sonarlint-core:8.0.2.42487=classpath -se.solrike.sonarlint:se.solrike.sonarlint.gradle.plugin:1.0.0-beta.15=classpath -se.solrike.sonarlint:sonarlint-gradle-plugin:1.0.0-beta.15=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/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index f3cd8bb..075f411 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -84,7 +84,25 @@ + + + + + + + + + + + + + + + + + + diff --git a/config/checkstyle/suppressions.xml b/config/checkstyle/suppressions.xml index 43f940c..9dacd2f 100644 --- a/config/checkstyle/suppressions.xml +++ b/config/checkstyle/suppressions.xml @@ -3,5 +3,8 @@ - + diff --git a/gradle.lockfile b/gradle.lockfile index 98026d9..06c95de 100644 --- a/gradle.lockfile +++ b/gradle.lockfile @@ -1,7 +1,10 @@ # This is a Gradle generated file for dependency locking. # Manual edits can break the build and are not advised. # This file is expected to be part of source control. -com.google.code.findbugs:jsr305:3.0.2=checkstyle +com.fasterxml.staxmate:staxmate:2.4.0=sonarlintCoreClasspath +com.fasterxml.woodstox:woodstox-core:6.4.0=sonarlintCoreClasspath +com.google.code.findbugs:jsr305:3.0.2=checkstyle,sonarlintCoreClasspath +com.google.code.gson:gson:2.8.9=sonarlintCoreClasspath com.google.collections:google-collections:1.0=checkstyle com.google.errorprone:error_prone_annotations:2.18.0=checkstyle com.google.guava:failureaccess:1.0.1=checkstyle @@ -12,11 +15,14 @@ com.puppycrawl.tools:checkstyle:10.12.3=checkstyle commons-beanutils:commons-beanutils:1.9.4=checkstyle 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 +org.apache.commons:commons-lang3:3.12.0=sonarlintCoreClasspath org.apache.commons:commons-lang3:3.8.1=checkstyle org.apache.commons:commons-text:1.3=checkstyle org.apache.httpcomponents.client5:httpclient5:5.1.3=checkstyle @@ -28,35 +34,77 @@ org.apache.maven.doxia:doxia-core:1.12.0=checkstyle org.apache.maven.doxia:doxia-logging-api:1.12.0=checkstyle org.apache.maven.doxia:doxia-module-xdoc:1.12.0=checkstyle org.apache.maven.doxia:doxia-sink-api:1.12.0=checkstyle +org.apache.tomcat.embed:tomcat-embed-core:9.0.75=sonarlintCoreClasspath +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.75=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 org.codehaus.plexus:plexus-container-default:2.1.0=checkstyle org.codehaus.plexus:plexus-utils:3.3.0=checkstyle -org.eclipse.jdt:org.eclipse.jdt.annotation:2.2.700=compileClasspath,testCompileClasspath +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,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 +org.eclipse.platform:org.eclipse.core.jobs:3.13.200=sonarlintCoreClasspath +org.eclipse.platform:org.eclipse.core.resources:3.18.200=sonarlintCoreClasspath +org.eclipse.platform:org.eclipse.core.runtime:3.26.100=sonarlintCoreClasspath +org.eclipse.platform:org.eclipse.equinox.common:3.17.100=sonarlintCoreClasspath +org.eclipse.platform:org.eclipse.equinox.preferences:3.10.200=sonarlintCoreClasspath +org.eclipse.platform:org.eclipse.osgi:3.18.300=sonarlintCoreClasspath +org.eclipse.platform:org.eclipse.text:3.12.300=sonarlintCoreClasspath org.jacoco:org.jacoco.agent:0.8.10=jacocoAgent,jacocoAnt 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 org.ow2.asm:asm-tree:9.5=jacocoAnt +org.ow2.asm:asm:9.0=sonarlintCoreClasspath org.ow2.asm:asm:9.5=jacocoAnt org.reflections:reflections:0.10.2=checkstyle -org.sonarsource.java:sonar-java-plugin:7.20.0.31692=sonarlintPlugins +org.sonarsource.analyzer-commons:sonar-analyzer-commons:2.7.0.1482=sonarlintCoreClasspath +org.sonarsource.analyzer-commons:sonar-analyzer-recognizers:2.7.0.1482=sonarlintCoreClasspath +org.sonarsource.analyzer-commons:sonar-performance-measure:2.7.0.1482=sonarlintCoreClasspath +org.sonarsource.analyzer-commons:sonar-regex-parsing:2.7.0.1482=sonarlintCoreClasspath +org.sonarsource.analyzer-commons:sonar-xml-parsing:2.7.0.1482=sonarlintCoreClasspath +org.sonarsource.java:external-reports:7.24.0.32100=sonarlintCoreClasspath +org.sonarsource.java:java-checks:7.24.0.32100=sonarlintCoreClasspath +org.sonarsource.java:java-frontend:7.24.0.32100=sonarlintCoreClasspath +org.sonarsource.java:java-jsp:7.24.0.32100=sonarlintCoreClasspath +org.sonarsource.java:java-surefire:7.24.0.32100=sonarlintCoreClasspath +org.sonarsource.java:java-symbolic-execution:7.24.0.32100=sonarlintCoreClasspath +org.sonarsource.java:jdt:7.24.0.32100=sonarlintCoreClasspath +org.sonarsource.java:sonar-java-plugin:7.24.0.32100=sonarlintCoreClasspath +org.sonarsource.javascript:css:10.5.1.22382=sonarlintCoreClasspath +org.sonarsource.javascript:javascript-checks:10.5.1.22382=sonarlintCoreClasspath +org.sonarsource.javascript:sonar-javascript-plugin:10.5.1.22382=sonarlintCoreClasspath +org.sonarsource.kotlin:sonar-kotlin-plugin:2.17.0.2902=sonarlintCoreClasspath +org.sonarsource.slang:sonar-ruby-plugin:1.14.0.4481=sonarlintCoreClasspath +org.sonarsource.slang:sonar-scala-plugin:1.14.0.4481=sonarlintCoreClasspath +org.sonarsource.sonarlint.core:sonarlint-core:9.2.0.74516=sonarlintCore,sonarlintCoreClasspath +org.sonarsource.sslr:sslr-core:1.24.0.633=sonarlintCoreClasspath +org.sonarsource.xml:sonar-xml-plugin:2.10.0.4108=sonarlintCoreClasspath org.xmlresolver:xmlresolver:5.2.0=checkstyle -empty=annotationProcessor,runtimeClasspath,signatures,sonarlint,testAnnotationProcessor +xerces:xercesImpl:2.12.2=sonarlintCoreClasspath +xml-apis:xml-apis:1.4.01=sonarlintCoreClasspath +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..a405590 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) { // NOSONAR
+      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..b60b657 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 {
+  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 {
+    public 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) { // NOSONAR
+        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 {
+  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 {
+    public 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) { // NOSONAR
+        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); + } + } +}