diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 90a86d3..eab0e46 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - scala: [2.11.12, 2.13.6, 2.12.14] + scala: [2.11.12, 2.13.6, 2.12.14, 3.1.1] java: [adopt@1.8] runs-on: ${{ matrix.os }} steps: @@ -55,9 +55,15 @@ jobs: run: sbt ++${{ matrix.scala }} mimaReportBinaryIssues - name: Build project + if: matrix.scala == '3.1.1' + run: sbt ++${{ matrix.scala }} clean test + + - name: Build project + if: matrix.scala != '3.1.1' run: sbt ++${{ matrix.scala }} clean coverage test - name: Upload coverage data to Coveralls + if: matrix.scala != '3.1.1' env: COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} COVERALLS_FLAG_NAME: Scala ${{ matrix.scala }} diff --git a/.scalafmt.conf b/.scalafmt.conf index 0710cb2..d141fdb 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -7,3 +7,8 @@ align.tokens.add = [ {code = ":=", owner = "Term.ApplyInfix"} ] rewrite.rules = [RedundantBraces, RedundantParens] + +# TODO update scalafmt for Scala 3 support +project.excludeFilters = [ + "src/main/scala-3/net/ceedubs/ficus/util/EnumerationUtil.scala" +] diff --git a/build.sbt b/build.sbt index 02c3212..b8145e6 100644 --- a/build.sbt +++ b/build.sbt @@ -6,9 +6,20 @@ lazy val gcTask = gc := { System gc () } +def Scala3 = "3.1.1" + ThisBuild / githubWorkflowBuild := Seq( WorkflowStep.Sbt(List("mimaReportBinaryIssues"), name = Some("Report binary compatibility issues")), - WorkflowStep.Sbt(List("clean", "coverage", "test"), name = Some("Build project")) + WorkflowStep.Sbt( + List("clean", "test"), + name = Some("Build project"), + cond = Some(s"matrix.scala == '${Scala3}'") + ), + WorkflowStep.Sbt( + List("clean", "coverage", "test"), + name = Some("Build project"), + cond = Some(s"matrix.scala != '${Scala3}'") + ) ) ThisBuild / githubWorkflowBuildPostamble ++= Seq( @@ -16,6 +27,7 @@ ThisBuild / githubWorkflowBuildPostamble ++= Seq( WorkflowStep.Sbt( List("coverageReport", "coveralls"), name = Some("Upload coverage data to Coveralls"), + cond = Some(s"matrix.scala != '${Scala3}'"), env = Map( "COVERALLS_REPO_TOKEN" -> "${{ secrets.GITHUB_TOKEN }}", "COVERALLS_FLAG_NAME" -> "Scala ${{ matrix.scala }}" @@ -29,7 +41,7 @@ ThisBuild / githubWorkflowUseSbtThinClient := false ThisBuild / githubWorkflowPublishTargetBranches := Seq() -ThisBuild / crossScalaVersions := Seq("2.10.7", "2.11.12", "2.13.6", "2.12.14") +ThisBuild / crossScalaVersions := Seq("2.10.7", "2.11.12", "2.13.6", "2.12.14", Scala3) ThisBuild / scalaVersion := (ThisBuild / crossScalaVersions).value.last // Coveralls doesn't really work with Scala 2.10.7 so we are disabling it for CI @@ -67,32 +79,41 @@ lazy val root = project Compile / unmanagedSourceDirectories ++= { (Compile / unmanagedSourceDirectories).value.map { dir => CrossVersion.partialVersion(scalaVersion.value) match { - case Some((2, 13)) => file(dir.getPath ++ "-2.13+") - case _ => file(dir.getPath ++ "-2.13-") + case Some((2, 13) | (3, _)) => file(dir.getPath ++ "-2.13+") + case _ => file(dir.getPath ++ "-2.13-") } } }, Test / unmanagedSourceDirectories ++= { (Test / unmanagedSourceDirectories).value.map { dir => CrossVersion.partialVersion(scalaVersion.value) match { - case Some((2, 13)) => file(dir.getPath ++ "-2.13+") - case _ => file(dir.getPath ++ "-2.13-") + case Some((2, 13) | (3, _)) => file(dir.getPath ++ "-2.13+") + case _ => file(dir.getPath ++ "-2.13-") } } }, + libraryDependencies ++= { + if (scalaBinaryVersion.value != "3") { + Seq( + "com.chuusai" %% "shapeless" % "2.3.3" % Test, + "org.scala-lang" % "scala-reflect" % scalaVersion.value % Provided, + "org.scala-lang" % "scala-compiler" % scalaVersion.value % Provided + ) + } else { + Nil + } + }, libraryDependencies ++= (if (scalaVersion.value.startsWith("2.10")) Seq("org.specs2" %% "specs2-core" % "3.10.0" % Test, "org.specs2" %% "specs2-scalacheck" % "3.10.0" % Test) else - Seq("org.specs2" %% "specs2-core" % "4.8.3" % Test, "org.specs2" %% "specs2-scalacheck" % "4.8.3" % Test)) ++ + Seq("org.specs2" %% "specs2-core" % "4.8.3" % Test, "org.specs2" %% "specs2-scalacheck" % "4.8.3" % Test) + .map(_ cross CrossVersion.for3Use2_13)) ++ Seq( - "org.scalacheck" %% "scalacheck" % "1.14.1" % Test, - "com.chuusai" %% "shapeless" % "2.3.3" % Test, - "com.typesafe" % "config" % "1.3.4", - "org.scala-lang" % "scala-reflect" % scalaVersion.value % Provided, - "org.scala-lang" % "scala-compiler" % scalaVersion.value % Provided + "org.scalacheck" %% "scalacheck" % "1.14.1" % Test cross CrossVersion.for3Use2_13, + "com.typesafe" % "config" % "1.3.4" ) ++ - (if (!scalaVersion.value.startsWith("2.13")) + (if (Set("2.10", "2.11", "2.12").contains(scalaBinaryVersion.value)) Seq( compilerPlugin("org.scalamacros" % "paradise" % "2.1.1" cross CrossVersion.full), "org.typelevel" %% "macro-compat" % "1.1.1" @@ -115,6 +136,13 @@ lazy val root = project toPath != "application.conf" } }, + Test / sources := { + if (scalaBinaryVersion.value == "3") { + Nil // TODO + } else { + (Test / sources).value + } + }, Publish.settings, releaseCrossBuild := true, mimaPreviousArtifacts := diff --git a/src/main/scala/net/ceedubs/ficus/readers/ArbitraryTypeReader.scala b/src/main/scala-2/net/ceedubs/ficus/readers/ArbitraryTypeReader.scala similarity index 98% rename from src/main/scala/net/ceedubs/ficus/readers/ArbitraryTypeReader.scala rename to src/main/scala-2/net/ceedubs/ficus/readers/ArbitraryTypeReader.scala index 47033fa..9fdefe6 100644 --- a/src/main/scala/net/ceedubs/ficus/readers/ArbitraryTypeReader.scala +++ b/src/main/scala-2/net/ceedubs/ficus/readers/ArbitraryTypeReader.scala @@ -8,8 +8,6 @@ import scala.language.experimental.macros import scala.reflect.internal.{Definitions, StdNames, SymbolTable} import scala.reflect.macros.blackbox -case class Generated[+A](value: A) extends AnyVal - trait ArbitraryTypeReader { implicit def arbitraryTypeValueReader[T]: Generated[ValueReader[T]] = macro ArbitraryTypeReaderMacros.arbitraryTypeValueReader[T] diff --git a/src/main/scala-2/net/ceedubs/ficus/util/EnumerationUtil.scala b/src/main/scala-2/net/ceedubs/ficus/util/EnumerationUtil.scala new file mode 100644 index 0000000..838877c --- /dev/null +++ b/src/main/scala-2/net/ceedubs/ficus/util/EnumerationUtil.scala @@ -0,0 +1,5 @@ +package net.ceedubs.ficus.util + +private[ficus] object EnumerationUtil { + type EnumValue[A <: Enumeration] = A#Value +} diff --git a/src/main/scala/net/ceedubs/ficus/util/ReflectionUtils.scala b/src/main/scala-2/net/ceedubs/ficus/util/ReflectionUtils.scala similarity index 100% rename from src/main/scala/net/ceedubs/ficus/util/ReflectionUtils.scala rename to src/main/scala-2/net/ceedubs/ficus/util/ReflectionUtils.scala diff --git a/src/main/scala-3/net/ceedubs/ficus/readers/ArbitraryTypeReader.scala b/src/main/scala-3/net/ceedubs/ficus/readers/ArbitraryTypeReader.scala new file mode 100644 index 0000000..f168266 --- /dev/null +++ b/src/main/scala-3/net/ceedubs/ficus/readers/ArbitraryTypeReader.scala @@ -0,0 +1,4 @@ +package net.ceedubs.ficus.readers + +// TODO +trait ArbitraryTypeReader diff --git a/src/main/scala-3/net/ceedubs/ficus/util/EnumerationUtil.scala b/src/main/scala-3/net/ceedubs/ficus/util/EnumerationUtil.scala new file mode 100644 index 0000000..0ae472f --- /dev/null +++ b/src/main/scala-3/net/ceedubs/ficus/util/EnumerationUtil.scala @@ -0,0 +1,9 @@ +package net.ceedubs.ficus.util + +private[ficus] object EnumerationUtil { + private[this] type Aux[A] = { type Value = A } + + type EnumValue[A <: Enumeration] = A match { + case Aux[a] => a + } +} diff --git a/src/main/scala/net/ceedubs/ficus/readers/CaseInsensitiveEnumerationReader.scala b/src/main/scala/net/ceedubs/ficus/readers/CaseInsensitiveEnumerationReader.scala index 25876d7..1a0ceae 100644 --- a/src/main/scala/net/ceedubs/ficus/readers/CaseInsensitiveEnumerationReader.scala +++ b/src/main/scala/net/ceedubs/ficus/readers/CaseInsensitiveEnumerationReader.scala @@ -1,7 +1,9 @@ package net.ceedubs.ficus.readers +import net.ceedubs.ficus.util.EnumerationUtil.EnumValue + trait CaseInsensitiveEnumerationReader extends EnumerationReader { - override protected def findEnumValue[T <: Enumeration](enum: T, configValue: String): Option[T#Value] = - enum.values.find(_.toString.toLowerCase == configValue.toLowerCase) + override protected def findEnumValue[T <: Enumeration](`enum`: T, configValue: String): Option[EnumValue[T]] = + `enum`.values.find(_.toString.toLowerCase == configValue.toLowerCase) } diff --git a/src/main/scala/net/ceedubs/ficus/readers/EnumerationReader.scala b/src/main/scala/net/ceedubs/ficus/readers/EnumerationReader.scala index 27234bc..1ae8a28 100644 --- a/src/main/scala/net/ceedubs/ficus/readers/EnumerationReader.scala +++ b/src/main/scala/net/ceedubs/ficus/readers/EnumerationReader.scala @@ -2,40 +2,41 @@ package net.ceedubs.ficus.readers import com.typesafe.config.ConfigException.{BadValue, Generic} import com.typesafe.config.Config - +import net.ceedubs.ficus.util.EnumerationUtil.EnumValue import scala.reflect.ClassTag import scala.util.{Failure, Success, Try} trait EnumerationReader { - implicit def enumerationValueReader[T <: Enumeration: ClassTag]: ValueReader[T#Value] = new ValueReader[T#Value] { - def read(config: Config, path: String): T#Value = { - val c = implicitly[ClassTag[T]].runtimeClass - val enum = Try(c.getField("MODULE$")) match { - case Success(m) => m.get(null).asInstanceOf[T] - case Failure(e) => - throw new Generic( - "Cannot get instance of enum: " + c.getCanonicalName + "; " + - "make sure the enum is an object and it's not contained in a class or trait", - e - ) - } + implicit def enumerationValueReader[T <: Enumeration: ClassTag]: ValueReader[EnumValue[T]] = + new ValueReader[EnumValue[T]] { + def read(config: Config, path: String): EnumValue[T] = { + val c = implicitly[ClassTag[T]].runtimeClass + val `enum` = Try(c.getField("MODULE$")) match { + case Success(m) => m.get(null).asInstanceOf[T] + case Failure(e) => + throw new Generic( + "Cannot get instance of enum: " + c.getCanonicalName + "; " + + "make sure the enum is an object and it's not contained in a class or trait", + e + ) + } - val value = config.getString(path) - findEnumValue(enum, value) - .getOrElse( - throw new BadValue( - config.origin(), - path, - value + " isn't a valid value for enum: " + - "" + c.getCanonicalName + "; allowed values: " + enum.values.mkString(", ") + val value = config.getString(path) + findEnumValue(`enum`, value) + .getOrElse( + throw new BadValue( + config.origin(), + path, + value + " isn't a valid value for enum: " + + "" + c.getCanonicalName + "; allowed values: " + `enum`.values.mkString(", ") + ) ) - ) - .asInstanceOf[T#Value] + .asInstanceOf[EnumValue[T]] + } } - } - protected def findEnumValue[T <: Enumeration](enum: T, configValue: String): Option[T#Value] = - enum.values.find(_.toString == configValue) + protected def findEnumValue[T <: Enumeration](`enum`: T, configValue: String): Option[EnumValue[T]] = + `enum`.values.find(_.toString == configValue) } object EnumerationReader extends EnumerationReader diff --git a/src/main/scala/net/ceedubs/ficus/readers/Generated.scala b/src/main/scala/net/ceedubs/ficus/readers/Generated.scala new file mode 100644 index 0000000..ed7223f --- /dev/null +++ b/src/main/scala/net/ceedubs/ficus/readers/Generated.scala @@ -0,0 +1,3 @@ +package net.ceedubs.ficus.readers + +case class Generated[+A](value: A) extends AnyVal