Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add pureconfig for scala3 [fixes fthomas/refined#1092] #1162

Merged
merged 8 commits into from
Mar 13, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .scalafmt.conf
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
version = 3.7.2
runner.dialect = scala213
runner.dialect = Scala213Source3
maxColumn = 100
docstrings.style = keep
rewrite.rules = [PreferCurlyFors, RedundantBraces]
Expand Down
19 changes: 11 additions & 8 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ val moduleCrossPlatformMatrix: Map[String, List[Platform]] = Map(
)

val moduleCrossScalaVersionsMatrix: (String, Platform) => List[String] = {
case ("benchmark" | "eval" | "pureconfig" | "scalaz" | "scodec" | "shapeless", _) =>
case ("benchmark" | "eval" | "scalaz" | "scodec" | "shapeless", _) =>
List(Scala_2_12, Scala_2_13)
case _ =>
List(Scala_2_12, Scala_2_13, Scala_3)
Expand Down Expand Up @@ -219,9 +219,14 @@ lazy val pureconfig = myCrossProject("pureconfig")
.dependsOn(core % "compile->compile;test->test")
.settings(
libraryDependencies ++= macroParadise(Test).value ++ Seq(
"com.github.pureconfig" %% "pureconfig-core" % pureconfigVersion,
"com.github.pureconfig" %% "pureconfig-generic" % pureconfigVersion % Test
)
"com.github.pureconfig" %% "pureconfig-core" % pureconfigVersion
) ++ (CrossVersion.partialVersion(scalaVersion.value) match {
case Some((2, 12 | 13)) =>
Seq(
"com.github.pureconfig" %% "pureconfig-generic" % pureconfigVersion % Test
)
case _ => Seq.empty
})
ivan-klass marked this conversation as resolved.
Show resolved Hide resolved
)

lazy val pureconfigJVM = pureconfig.jvm
Expand Down Expand Up @@ -450,10 +455,8 @@ lazy val compileSettings = Def.settings(
if (dir.getName != "scala") Seq(dir)
else
CrossVersion.partialVersion(scalaVersion.value) match {
case Some((2, 12)) => Seq(file(dir.getPath + "-3.0-"))
case Some((2, 13)) => Seq(file(dir.getPath + "-3.0-"))
case Some((0, _)) => Seq(file(dir.getPath + "-3.0+"))
case Some((3, _)) => Seq(file(dir.getPath + "-3.0+"))
case Some((2, 12 | 13)) => Seq(file(dir.getPath + "-3.0-"))
case Some((0 | 3, _)) => Seq(file(dir.getPath + "-3.0+"))
case other => sys.error(s"unmanagedSourceDirectories for scalaVersion $other not set")
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package eu.timepit.refined.pureconfig

import scala.quoted.{Expr, Quotes, Type, quotes}

final class DescribeType[T](val repr: String)

object DescribeType:

private def macroImpl[T: Type](using Quotes): Expr[DescribeType[T]] = {
import quotes.reflect.*
'{ new DescribeType[T](${ Expr(TypeRepr.of[T].show) }) }
}

inline given describeType[T]: DescribeType[T] = ${ macroImpl[T]}

private[pureconfig] trait TypeDescribeVersionSpecific {

protected def getDescription[A](d: DescribeType[A]): String = d.repr
}
fthomas marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package eu.timepit.refined.pureconfig

private[pureconfig] trait TypeDescribeVersionSpecific {
final type DescribeType[A] = scala.reflect.runtime.universe.WeakTypeTag[A]
ivan-klass marked this conversation as resolved.
Show resolved Hide resolved

protected def getDescription[A](d: DescribeType[A]): String = d.tpe.toString
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package eu.timepit.refined.pureconfig

import _root_.pureconfig.{ConfigCursor, ConfigReader, ConfigWriter}
import _root_.pureconfig.error.{CannotConvert, ConvertFailure, ConfigReaderFailures}
import eu.timepit.refined.api.{RefType, Validate}
import com.typesafe.config.ConfigValue

trait BaseInstances extends TypeDescribeVersionSpecific {

implicit def refTypeConfigReader[F[_, _], T, P](implicit
configReader: ConfigReader[T],
refType: RefType[F],
validate: Validate[T, P],
describeType: DescribeType[F[T, P]]
): ConfigReader[F[T, P]] = new ConfigReader[F[T, P]] {

override def from(cur: ConfigCursor): Either[ConfigReaderFailures, F[T, P]] =
configReader.from(cur) match {
case Right(t) =>
refType.refine[P](t) match {
case Left(because) =>
Left(
ConfigReaderFailures(
ConvertFailure(
reason = CannotConvert(
value = cur.valueOpt.map(_.render()).getOrElse("none"),
toType = getDescription(describeType),
because = because
),
cur = cur
)
)
)

case Right(refined) =>
Right(refined)
}

case Left(configReaderFailures) =>
Left(configReaderFailures)
}
}

implicit def refTypeConfigWriter[F[_, _], T, P](implicit
configWriter: ConfigWriter[T],
refType: RefType[F]
): ConfigWriter[F[T, P]] =
new ConfigWriter[F[T, P]] {
override def to(a: F[T, P]): ConfigValue = configWriter.to(refType.unwrap(a))
}

}
Original file line number Diff line number Diff line change
@@ -1,47 +1,15 @@
package eu.timepit.refined

import _root_.pureconfig.{ConfigConvert, ConfigCursor}
import _root_.pureconfig.error.{CannotConvert, ConfigReaderFailures, ConvertFailure}
import com.typesafe.config.ConfigValue
import _root_.pureconfig.ConfigConvert
import eu.timepit.refined.api.{RefType, Validate}
import scala.reflect.runtime.universe.WeakTypeTag

package object pureconfig {
package object pureconfig extends BaseInstances {

implicit def refTypeConfigConvert[F[_, _], T, P](implicit
configConvert: ConfigConvert[T],
refType: RefType[F],
validate: Validate[T, P],
typeTag: WeakTypeTag[F[T, P]]
typeTag: DescribeType[F[T, P]]
): ConfigConvert[F[T, P]] =
new ConfigConvert[F[T, P]] {
override def from(cur: ConfigCursor): Either[ConfigReaderFailures, F[T, P]] =
configConvert.from(cur) match {
case Right(t) =>
refType.refine[P](t) match {
case Left(because) =>
Left(
ConfigReaderFailures(
ConvertFailure(
reason = CannotConvert(
value = cur.valueOpt.map(_.render()).getOrElse("none"),
toType = typeTag.tpe.toString,
because = because
),
cur = cur
)
)
)

case Right(refined) =>
Right(refined)
}

case Left(configReaderFailures) =>
Left(configReaderFailures)
}

override def to(t: F[T, P]): ConfigValue =
configConvert.to(refType.unwrap(t))
}
ConfigConvert.fromReaderAndWriter(refTypeConfigReader, refTypeConfigWriter)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package eu.timepit.refined.pureconfig

import _root_.pureconfig.{ConfigReader, ConfigWriter}
import com.typesafe.config.ConfigValueFactory
import _root_.pureconfig.generic.derivation.default.derived
import eu.timepit.refined.types.numeric.PosInt

import java.util.{Map as JMap}

trait SpecDerivedInstances {

given ConfigReader[Config] = ConfigReader.derived
// we only care that this part of config writer (refined type) compiles
private val posIntConfWriter: ConfigWriter[PosInt] = summon
// there's no builtin generic derivation of ConfigWriter[ADT] for scala 3 yet
given ConfigWriter[Config] = posIntConfWriter.contramap[Config](_.value).mapConfig(cfg =>
ConfigValueFactory.fromMap(JMap.of("value", cfg))
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package eu.timepit.refined.pureconfig

import _root_.pureconfig.{ConfigReader, ConfigWriter}
import _root_.pureconfig.generic.auto

trait SpecDerivedInstances {

implicit val configReader: ConfigReader[Config] = auto.exportReader.instance
implicit val configWriter: ConfigWriter[Config] = auto.exportWriter.instance
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package eu.timepit.refined.pureconfig

import eu.timepit.refined.types.numeric.PosInt
case class Config(value: PosInt)
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,24 @@ import org.scalacheck.Prop._
import org.scalacheck.Properties
import pureconfig._
import pureconfig.error.{CannotConvert, ConfigReaderFailures, ConvertFailure, WrongType}
import pureconfig.generic.auto._

class RefTypeConfigConvertSpec extends Properties("RefTypeConfigConvert") {

case class Config(value: PosInt)
class RefTypeConfigConvertSpec
extends Properties("RefTypeConfigConvert")
with SpecDerivedInstances {

property("load success") = secure {
loadConfigWithValue("1") ?=
Right(Config(PosInt.unsafeFrom(1)))
}

property("load failure (predicate)") = secure {
val expected1 = Left(

def expectedFailure(toTypeMsg: String) = Left(
ConfigReaderFailures(
ConvertFailure(
reason = CannotConvert(
value = "0",
toType = "eu.timepit.refined.api.Refined[Int,eu.timepit.refined.numeric.Positive]",
toType = toTypeMsg,
because = "Predicate failed: (0 > 0)."
),
origin = Some(ConfigOriginFactory.newSimple("String").withLineNumber(1)),
Expand All @@ -32,26 +32,25 @@ class RefTypeConfigConvertSpec extends Properties("RefTypeConfigConvert") {
)
)

val expected1 = expectedFailure(
"eu.timepit.refined.api.Refined[Int,eu.timepit.refined.numeric.Positive]"
)

// Allow "scala.Int" instead of just "Int" in the toType parameter.
// For some reason Scala 2.12 with sbt 1.1.2 uses the former.
val expected2 = Left(
ConfigReaderFailures(
ConvertFailure(
reason = CannotConvert(
value = "0",
toType =
"eu.timepit.refined.api.Refined[scala.Int,eu.timepit.refined.numeric.Positive]",
because = "Predicate failed: (0 > 0)."
),
origin = Some(ConfigOriginFactory.newSimple("String").withLineNumber(1)),
path = "value"
)
)
val expected2 = expectedFailure(
"eu.timepit.refined.api.Refined[scala.Int,eu.timepit.refined.numeric.Positive]"
)

// scala 3 macro provide type representation in this way
val expected3 = expectedFailure(
"eu.timepit.refined.api.Refined[scala.Int, eu.timepit.refined.numeric.Greater[shapeless.nat._0]]"
)

val actual = loadConfigWithValue("0")
(actual ?= expected1) ||
(actual ?= expected2)
(actual ?= expected2) ||
(actual ?= expected3)
}

property("load failure (wrong type)") = secure {
Expand Down