diff --git a/build.sbt b/build.sbt index 98c49137..dc6825cf 100644 --- a/build.sbt +++ b/build.sbt @@ -91,11 +91,20 @@ lazy val compat = new MultiScalaCrossProject( exclude[ReversedMissingMethodProblem]("scala.collection.compat.PackageShared.*"), // it's package-private exclude[Problem]("scala.collection.compat.*PreservingBuilder*") ) - }, + } ) .jvmSettings( Test / unmanagedSourceDirectories += (ThisBuild / baseDirectory).value / "compat/src/test/scala-jvm", - junit, + Compile / unmanagedSourceDirectories += { + val jvmParent = (ThisBuild / baseDirectory).value / "compat/jvm/src/main" + CrossVersion.partialVersion(scalaVersion.value) match { + case Some((3, _) | (2, 13)) => + jvmParent / "scala-2.13" + case _ => + jvmParent / "scala-2.11_2.12" + } + }, + junit ) .disablePlugins(ScalafixPlugin), _.jsSettings( @@ -111,6 +120,16 @@ lazy val compat = new MultiScalaCrossProject( } Seq(s"$opt:$x->$y/") }, + Test / unmanagedSourceDirectories += (ThisBuild / baseDirectory).value / "compat/src/test/scala-js", + Compile / unmanagedSourceDirectories += { + val jsAndNativeSourcesParent = (ThisBuild / baseDirectory).value / "compat/jsNative/src/main" + CrossVersion.partialVersion(scalaVersion.value) match { + case Some((3, _) | (2, 13)) => + jsAndNativeSourcesParent / "scala-2.13" + case _ => + jsAndNativeSourcesParent / "scala-2.11_2.12" + } + }, Test / fork := false // Scala.js cannot run forked tests ).jsEnablePlugins(ScalaJSJUnitPlugin), _.nativeSettings( @@ -122,6 +141,15 @@ lazy val compat = new MultiScalaCrossProject( case Some((3, 1)) => mimaPreviousArtifacts.value.filter(_.revision != "2.6.0") case _ => mimaPreviousArtifacts.value }), + Compile / unmanagedSourceDirectories += { + val jsAndNativeSourcesParent = (ThisBuild / baseDirectory).value / "compat/jsNative/src/main" + CrossVersion.partialVersion(scalaVersion.value) match { + case Some((3, _) | (2, 13)) => + jsAndNativeSourcesParent / "scala-2.13" + case _ => + jsAndNativeSourcesParent / "scala-2.11_2.12" + } + }, libraryDependencies += "org.scala-native" %%% "junit-runtime" % nativeVersion, Test / fork := false // Scala Native cannot run forked tests ) diff --git a/compat/jsNative/src/main/scala-2.11_2.12/scala/jdk/OptionConverters.scala b/compat/jsNative/src/main/scala-2.11_2.12/scala/jdk/OptionConverters.scala new file mode 100644 index 00000000..9a179fc1 --- /dev/null +++ b/compat/jsNative/src/main/scala-2.11_2.12/scala/jdk/OptionConverters.scala @@ -0,0 +1,65 @@ +/* + * Scala (https://www.scala-lang.org) + * + * Copyright EPFL and Lightbend, Inc. + * + * Licensed under Apache License 2.0 + * (http://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + +package scala.jdk + +import java.util.Optional + +/** This object provides extension methods that convert between Scala `Option` and Java `Optional` + * types. + * + * It differs from the JVM version as in it does not provide any conversions for the Optional primitive type + * wrappers which are available in the JDK but not in Scala-JS or Scala-Native. + * + * Scala `Option` is extended with a `toJava` method that creates a corresponding `Optional`. + * + * Java `Optional` is extended with a `toScala` method. + * + * + * Example usage: + * + * {{{ + * import scala.jdk.OptionConverters._ + * val a = Option("example").toJava // Creates java.util.Optional[String] containing "example" + * val b = (None: Option[String]).toJava // Creates an empty java.util.Optional[String] + * val c = a.toScala // Back to Option("example") + * val d = b.toScala // Back to None typed as Option[String] + * }}} + */ +object OptionConverters { + + /** Provides conversions from Java `Optional` to Scala `Option` and specialized `Optional` types */ + implicit class RichOptional[A](private val o: java.util.Optional[A]) extends AnyVal { + + /** Convert a Java `Optional` to a Scala `Option` */ + def toScala: Option[A] = if (o.isPresent) Some(o.get) else None + + /** Convert a Java `Optional` to a Scala `Option` */ + @deprecated("Use `toScala` instead", "2.13.0") + def asScala: Option[A] = if (o.isPresent) Some(o.get) else None + } + + /** Provides conversions from Scala `Option` to Java `Optional` types */ + implicit class RichOption[A](private val o: Option[A]) extends AnyVal { + + /** Convert a Scala `Option` to a generic Java `Optional` */ + def toJava: Optional[A] = o match { + case Some(a) => Optional.ofNullable(a); case _ => Optional.empty[A] + } + + /** Convert a Scala `Option` to a generic Java `Optional` */ + @deprecated("Use `toJava` instead", "2.13.0") + def asJava: Optional[A] = o match { + case Some(a) => Optional.ofNullable(a); case _ => Optional.empty[A] + } + } +} diff --git a/compat/jvm/src/main/scala-2.11_2.12/scala/jdk/OptionConverters.scala b/compat/jvm/src/main/scala-2.11_2.12/scala/jdk/OptionConverters.scala new file mode 100644 index 00000000..b85e43b8 --- /dev/null +++ b/compat/jvm/src/main/scala-2.11_2.12/scala/jdk/OptionConverters.scala @@ -0,0 +1,121 @@ +/* + * Scala (https://www.scala-lang.org) + * + * Copyright EPFL and Lightbend, Inc. + * + * Licensed under Apache License 2.0 + * (http://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + +package scala.jdk + +import java.util.{Optional, OptionalDouble, OptionalInt, OptionalLong} + +/** This object provides extension methods that convert between Scala `Option` and Java `Optional` + * types. + * + * Scala `Option` is extended with a `toJava` method that creates a corresponding `Optional`, and + * a `toJavaPrimitive` method that creates a specialized variant (e.g., `OptionalInt`) if + * applicable. + * + * Java `Optional` is extended with a `toScala` method and a `toJavaPrimitive` method. + * + * Finally, specialized `Optional` types are extended with `toScala` and `toJavaGeneric` methods. + * + * Example usage: + * + * {{{ + * import scala.jdk.OptionConverters._ + * val a = Option("example").toJava // Creates java.util.Optional[String] containing "example" + * val b = (None: Option[String]).toJava // Creates an empty java.util.Optional[String] + * val c = a.toScala // Back to Option("example") + * val d = b.toScala // Back to None typed as Option[String] + * val e = Option(2.7).toJava // java.util.Optional[Double] containing boxed 2.7 + * val f = Option(2.7).toJavaPrimitive // java.util.OptionalDouble containing 2.7 (not boxed) + * val g = f.toScala // Back to Option(2.7) + * val h = f.toJavaGeneric // Same as e + * val i = e.toJavaPrimitive // Same as f + * }}} + */ +object OptionConverters { + + /** Provides conversions from Java `Optional` to Scala `Option` and specialized `Optional` types */ + implicit class RichOptional[A](private val o: java.util.Optional[A]) extends AnyVal { + + /** Convert a Java `Optional` to a Scala `Option` */ + def toScala: Option[A] = if (o.isPresent) Some(o.get) else None + + /** Convert a Java `Optional` to a Scala `Option` */ + @deprecated("Use `toScala` instead", "2.13.0") + def asScala: Option[A] = if (o.isPresent) Some(o.get) else None + + /** Convert a generic Java `Optional` to a specialized variant */ + def toJavaPrimitive[O](implicit shape: OptionShape[A, O]): O = shape.fromJava(o) + } + + /** Provides conversions from Scala `Option` to Java `Optional` types */ + implicit class RichOption[A](private val o: Option[A]) extends AnyVal { + + /** Convert a Scala `Option` to a generic Java `Optional` */ + def toJava: Optional[A] = o match { + case Some(a) => Optional.ofNullable(a); case _ => Optional.empty[A] + } + + /** Convert a Scala `Option` to a generic Java `Optional` */ + @deprecated("Use `toJava` instead", "2.13.0") + def asJava: Optional[A] = o match { + case Some(a) => Optional.ofNullable(a); case _ => Optional.empty[A] + } + + /** Convert a Scala `Option` to a specialized Java `Optional` */ + def toJavaPrimitive[O](implicit shape: OptionShape[A, O]): O = shape.fromScala(o) + } + + /** Provides conversions from `OptionalDouble` to Scala `Option` and the generic `Optional` */ + implicit class RichOptionalDouble(private val o: OptionalDouble) extends AnyVal { + + /** Convert a Java `OptionalDouble` to a Scala `Option` */ + def toScala: Option[Double] = if (o.isPresent) Some(o.getAsDouble) else None + + /** Convert a Java `OptionalDouble` to a Scala `Option` */ + @deprecated("Use `toScala` instead", "2.13.0") + def asScala: Option[Double] = if (o.isPresent) Some(o.getAsDouble) else None + + /** Convert a Java `OptionalDouble` to a generic Java `Optional` */ + def toJavaGeneric: Optional[Double] = + if (o.isPresent) Optional.of(o.getAsDouble) else Optional.empty[Double] + } + + /** Provides conversions from `OptionalInt` to Scala `Option` and the generic `Optional` */ + implicit class RichOptionalInt(private val o: OptionalInt) extends AnyVal { + + /** Convert a Java `OptionalInt` to a Scala `Option` */ + def toScala: Option[Int] = if (o.isPresent) Some(o.getAsInt) else None + + /** Convert a Java `OptionalInt` to a Scala `Option` */ + @deprecated("Use `toScala` instead", "2.13.0") + def asScala: Option[Int] = if (o.isPresent) Some(o.getAsInt) else None + + /** Convert a Java `OptionalInt` to a generic Java `Optional` */ + def toJavaGeneric: Optional[Int] = + if (o.isPresent) Optional.of(o.getAsInt) else Optional.empty[Int] + } + + /** Provides conversions from `OptionalLong` to Scala `Option` and the generic `Optional` */ + implicit class RichOptionalLong(private val o: OptionalLong) extends AnyVal { + + /** Convert a Java `OptionalLong` to a Scala `Option` */ + def toScala: Option[Long] = if (o.isPresent) Some(o.getAsLong) else None + + /** Convert a Java `OptionalLong` to a Scala `Option` */ + @deprecated("Use `toScala` instead", "2.13.0") + def asScala: Option[Long] = if (o.isPresent) Some(o.getAsLong) else None + + /** Convert a Java `OptionalLong` to a generic Java `Optional` */ + def toJavaGeneric: Optional[Long] = + if (o.isPresent) Optional.of(o.getAsLong) else Optional.empty[Long] + } +} diff --git a/compat/jvm/src/main/scala-2.11_2.12/scala/jdk/OptionShape.scala b/compat/jvm/src/main/scala-2.11_2.12/scala/jdk/OptionShape.scala new file mode 100644 index 00000000..aa98c891 --- /dev/null +++ b/compat/jvm/src/main/scala-2.11_2.12/scala/jdk/OptionShape.scala @@ -0,0 +1,74 @@ +/* + * Scala (https://www.scala-lang.org) + * + * Copyright EPFL and Lightbend, Inc. + * + * Licensed under Apache License 2.0 + * (http://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + +package scala.jdk + +import java.util.{Optional, OptionalDouble, OptionalInt, OptionalLong} +import java.{lang => jl} + +import scala.annotation.implicitNotFound + +/** A type class implementing conversions from a generic Scala `Option` or Java `Optional` to + * a specialized Java variant (for `Double`, `Int` and `Long`). + * + * @tparam A the primitive type wrapped in an option + * @tparam O the specialized Java `Optional` wrapping an element of type `A` + */ +@implicitNotFound("No specialized Optional type exists for elements of type ${A}") +sealed abstract class OptionShape[A, O] { + + /** Converts from `Optional` to the specialized variant `O` */ + def fromJava(o: Optional[A]): O + + /** Converts from `Option` to the specialized variant `O` */ + def fromScala(o: Option[A]): O +} + +object OptionShape { + implicit val doubleOptionShape: OptionShape[Double, OptionalDouble] = + new OptionShape[Double, OptionalDouble] { + def fromJava(o: Optional[Double]): OptionalDouble = + if (o.isPresent) OptionalDouble.of(o.get) else OptionalDouble.empty + + def fromScala(o: Option[Double]): OptionalDouble = o match { + case Some(d) => OptionalDouble.of(d) + case _ => OptionalDouble.empty + } + } + implicit val jDoubleOptionShape: OptionShape[jl.Double, OptionalDouble] = + doubleOptionShape.asInstanceOf[OptionShape[jl.Double, OptionalDouble]] + + implicit val intOptionShape: OptionShape[Int, OptionalInt] = new OptionShape[Int, OptionalInt] { + def fromJava(o: Optional[Int]): OptionalInt = + if (o.isPresent) OptionalInt.of(o.get) else OptionalInt.empty + + def fromScala(o: Option[Int]): OptionalInt = o match { + case Some(d) => OptionalInt.of(d) + case _ => OptionalInt.empty + } + } + implicit val jIntegerOptionShape: OptionShape[jl.Integer, OptionalInt] = + intOptionShape.asInstanceOf[OptionShape[jl.Integer, OptionalInt]] + + implicit val longOptionShape: OptionShape[Long, OptionalLong] = + new OptionShape[Long, OptionalLong] { + def fromJava(o: Optional[Long]): OptionalLong = + if (o.isPresent) OptionalLong.of(o.get) else OptionalLong.empty + + def fromScala(o: Option[Long]): OptionalLong = o match { + case Some(d) => OptionalLong.of(d) + case _ => OptionalLong.empty + } + } + implicit val jLongOptionShape: OptionShape[jl.Long, OptionalLong] = + longOptionShape.asInstanceOf[OptionShape[jl.Long, OptionalLong]] +} diff --git a/compat/src/test/scala-js/test/scala/jdk/OptionConvertersTest.scala b/compat/src/test/scala-js/test/scala/jdk/OptionConvertersTest.scala new file mode 100644 index 00000000..12a3c9f4 --- /dev/null +++ b/compat/src/test/scala-js/test/scala/jdk/OptionConvertersTest.scala @@ -0,0 +1,55 @@ +package test.scala.jdk + +import org.junit.Assert.assertEquals +import org.junit.Test + +import java.util.Optional +import scala.jdk.OptionConverters._ + +/** + * The tests were copied from the Scala 2.13 Standard Library. `scala.jdk.javaapi` stuff has been omitted and + * everything concerning `OptionalInt`, `OptionalDouble` and `OptionalLong` is only available in the jvm tests. + * + * See https://github.com/scala/scala/blob/2.13.x/test/junit/scala/jdk/OptionConvertersTest.scala. + */ +class OptionConvertersTest { + @Test + def scalaToEverything(): Unit = { + val o = Option("fish") + val n = None: Option[String] + val od = Option(2.7) + val nd = None: Option[Double] + val oi = Option(4) + val ni = None: Option[Int] + val ol = Option(-1L) + val nl = None: Option[Long] + assertEquals(o.toJava, Optional.of(o.get)) + assertEquals(n.toJava, Optional.empty[String]) + assertEquals(od.toJava.get: Double, Optional.of(od.get).get: Double, 0) + assertEquals(nd.toJava, Optional.empty[Double]) + assertEquals(oi.toJava.get: Int, Optional.of(oi.get).get: Int) + assertEquals(ni.toJava, Optional.empty[Int]) + assertEquals(ol.toJava.get: Long, Optional.of(ol.get).get: Long) + assertEquals(nl.toJava, Optional.empty[Long]) + } + + @Test + def javaGenericToEverything(): Unit = { + val o = Optional.of("fish") + val n = Optional.empty[String] + val od = Optional.of(2.7) + val nd = Optional.empty[Double] + val oi = Optional.of(4) + val ni = Optional.empty[Int] + val ol = Optional.of(-1L) + val nl = Optional.empty[Long] + assertEquals(o.toScala, Option(o.get)) + assertEquals(n.toScala, Option.empty[String]) + assertEquals(od.toScala, Option(od.get)) + assertEquals(nd.toScala, Option.empty[Double]) + assertEquals(oi.toScala, Option(oi.get)) + assertEquals(ni.toScala, Option.empty[Int]) + assertEquals(ol.toScala, Option(ol.get)) + assertEquals(nl.toScala, Option.empty[Long]) + } +} diff --git a/compat/src/test/scala-jvm/test/scala/jdk/OptionConvertersJVMTest.scala b/compat/src/test/scala-jvm/test/scala/jdk/OptionConvertersJVMTest.scala new file mode 100644 index 00000000..b973d256 --- /dev/null +++ b/compat/src/test/scala-jvm/test/scala/jdk/OptionConvertersJVMTest.scala @@ -0,0 +1,97 @@ +package test.scala.jdk + +import org.junit.Assert.assertEquals +import org.junit.Test + +import java.util._ +import scala.jdk.OptionConverters._ + +/** + * The tests were copied from the Scala 2.13 Standard Library. `scala.jdk.javaapi` stuff has been omitted. + * + * See https://github.com/scala/scala/blob/2.13.x/test/junit/scala/jdk/OptionConvertersTest.scala. + */ +class OptionConvertersJVMTest { + @Test + def scalaToEverything(): Unit = { + val o = Option("fish") + val n = (None: Option[String]) + val od = Option(2.7) + val nd = (None: Option[Double]) + + val oi = Option(4) + val ni = (None: Option[Int]) + val ol = Option(-1L) + val nl = (None: Option[Long]) + assertEquals(o.toJava, Optional.of(o.get)) + assertEquals(n.toJava, Optional.empty[String]) + assertEquals(od.toJava.get: Double, Optional.of(od.get).get: Double, 0) + assertEquals(nd.toJava, Optional.empty[Double]) + assertEquals(od.toJavaPrimitive, OptionalDouble.of(od.get)) + assertEquals(nd.toJavaPrimitive, OptionalDouble.empty) + assertEquals(oi.toJava.get: Int, Optional.of(oi.get).get: Int) + assertEquals(ni.toJava, Optional.empty[Int]) + assertEquals(oi.toJavaPrimitive, OptionalInt.of(oi.get)) + assertEquals(ni.toJavaPrimitive, OptionalInt.empty) + assertEquals(ol.toJava.get: Long, Optional.of(ol.get).get: Long) + assertEquals(nl.toJava, Optional.empty[Long]) + assertEquals(ol.toJavaPrimitive, OptionalLong.of(ol.get)) + assertEquals(nl.toJavaPrimitive, OptionalLong.empty) + } + + @Test + def javaGenericToEverything(): Unit = { + val o = Optional.of("fish") + val n = Optional.empty[String] + val od = Optional.of(2.7) + val nd = Optional.empty[Double] + val oi = Optional.of(4) + val ni = Optional.empty[Int] + val ol = Optional.of(-1L) + val nl = Optional.empty[Long] + assertEquals(o.toScala, Option(o.get)) + assertEquals(n.toScala, Option.empty[String]) + assertEquals(od.toScala, Option(od.get)) + assertEquals(nd.toScala, Option.empty[Double]) + assertEquals(od.toJavaPrimitive, OptionalDouble.of(od.get)) + assertEquals(nd.toJavaPrimitive, OptionalDouble.empty) + assertEquals(oi.toScala, Option(oi.get)) + assertEquals(ni.toScala, Option.empty[Int]) + assertEquals(oi.toJavaPrimitive, OptionalInt.of(oi.get)) + assertEquals(ni.toJavaPrimitive, OptionalInt.empty) + assertEquals(ol.toScala, Option(ol.get)) + assertEquals(nl.toScala, Option.empty[Long]) + assertEquals(ol.toJavaPrimitive, OptionalLong.of(ol.get)) + assertEquals(nl.toJavaPrimitive, OptionalLong.empty) + } + + @Test + def javaOptionalDoubleToEverything(): Unit = { + val o = OptionalDouble.of(2.7) + val n = OptionalDouble.empty + assertEquals(o.toScala, Option(o.getAsDouble)) + assertEquals(o.toJavaGeneric.get: Double, Optional.of(o.getAsDouble).get: Double, 0) + assertEquals(n.toScala, None: Option[Double]) + assertEquals(n.toJavaGeneric, Optional.empty[Double]) + } + + @Test + def javaOptionalIntToEverything(): Unit = { + val o = OptionalInt.of(4) + val n = OptionalInt.empty + assertEquals(o.toScala, Option(o.getAsInt)) + assertEquals(o.toJavaGeneric.get: Int, Optional.of(o.getAsInt).get: Int) + assertEquals(n.toScala, None: Option[Int]) + assertEquals(n.toJavaGeneric, Optional.empty[Int]) + } + + @Test + def javaOptionalLongToEverything(): Unit = { + val o = OptionalLong.of(-1) + val n = OptionalLong.empty + assertEquals(o.toScala, Option(o.getAsLong)) + assertEquals(o.toJavaGeneric.get: Long, Optional.of(o.getAsLong).get: Long) + assertEquals(n.toScala, None: Option[Long]) + assertEquals(n.toJavaGeneric, Optional.empty[Long]) + } +} diff --git a/project/plugins.sbt b/project/plugins.sbt index fcf4ebbf..ab76cec6 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,6 +1,6 @@ addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.10.0") addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.2.0") -addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.4") +addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.5") addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.2.0") addSbtPlugin("org.scala-lang.modules" % "sbt-scala-module" % "3.0.1") addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.9.34")