diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 26f34d8ab..33c0cfd14 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -12,11 +12,11 @@ jobs:
scala: ['2.12', '2.13', '3']
include:
- scala: '2.12'
- scala-version: 2.12.18
+ scala-version: 2.12.19
- scala: '2.13'
- scala-version: 2.13.12
+ scala-version: 2.13.14
- scala: '3'
- scala-version: 3.3.0
+ scala-version: 3.3.3
steps:
- name: Checkout repository
diff --git a/.scalafix.conf b/.scalafix.conf
index e7908daa0..44e7ed7f8 100644
--- a/.scalafix.conf
+++ b/.scalafix.conf
@@ -9,4 +9,5 @@ OrganizeImports {
importSelectorsOrder = Ascii
importsOrder = Ascii
removeUnused = false
+ targetDialect = Scala2
}
diff --git a/.scalafmt.conf b/.scalafmt.conf
index 4b6ade800..01f4e841f 100644
--- a/.scalafmt.conf
+++ b/.scalafmt.conf
@@ -1,4 +1,4 @@
-version = 3.7.14
+version = 3.8.2
runner.dialect = scala213
align.preset = none
@@ -8,4 +8,8 @@ fileOverride {
"glob:**/scala-3*/**" {
runner.dialect = scala3
}
+
+ "glob:**/generic-scala3/**" {
+ runner.dialect = scala3
+ }
}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2f9b9078f..489c9c93f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,19 @@
+### 0.17.7 (Jun 9, 2024)
+
+- New features
+ - Added support for Scala 3 derivation of `EnumConfigWriter` and `EnumConfigConvert` using a `derives` clause.
+ - Added a new `pureconfig-generic-scala3` module, a drop-in replacement of Scala 2's `pureconfig-generic` for semiauto derivation in Scala 3. It supports most of the types supported in `pureconfig-generic` and accepts product and coproduct hints.
+
+### 0.17.6 (Feb 22, 2024)
+
+- New features
+ - Added new `pureconfig-pekko` and `pureconfig-pekko-http` modules with relevant `ConfigReader`s and `ConfigWriter`s
+ for Pekko and Pekko HTTP types.
+
+### 0.17.5 (Jan 18, 2024)
+
+Maintenance update to update dependency versions.
+
### 0.17.4 (May 11, 2023)
Maintenance update to update dependency versions and fix issues with previously published modules.
diff --git a/README.md b/README.md
index 47b8ddabb..30882c06b 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
-[![Build Status](https://github.com/pureconfig/pureconfig/workflows/CI/badge.svg?branch=master)](https://github.com/pureconfig/pureconfig/actions?query=workflow%3ACI+branch%3Amaster)
+[![Build Status](https://github.com/pureconfig/pureconfig/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/pureconfig/pureconfig/actions?query=workflow%3ACI+branch%3Amaster)
[![Coverage Status](https://coveralls.io/repos/github/pureconfig/pureconfig/badge.svg?branch=master)](https://coveralls.io/github/pureconfig/pureconfig?branch=master)
[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.github.pureconfig/pureconfig_2.12/badge.svg)](https://search.maven.org/artifact/com.github.pureconfig/pureconfig_2.12)
[![Scaladoc](https://javadoc.io/badge/com.github.pureconfig/pureconfig-core_2.12.svg)](https://javadoc.io/page/com.github.pureconfig/pureconfig-core_2.12/latest/pureconfig/index.html)
@@ -34,9 +34,17 @@ To use PureConfig in an existing SBT project with Scala 2.12 or a later version,
`build.sbt`:
```scala
-libraryDependencies += "com.github.pureconfig" %% "pureconfig" % "0.17.4"
+libraryDependencies += "com.github.pureconfig" %% "pureconfig" % "0.17.7"
```
+For Scala 3, add the following dependency to your `build.sbt`:
+
+```scala
+libraryDependencies += "com.github.pureconfig" %% "pureconfig-core" % "0.17.7"
+```
+
+While a lot of the documentation will also apply to Scala 3, there is a specific guide for Scala 3's derivation that you can [find here](scala-3-derivation.html).
+
For a full example of `build.sbt` you can have a look at this [build.sbt](https://github.com/pureconfig/pureconfig/blob/master/example/build.sbt).
Earlier versions of Scala had bugs which can cause subtle compile-time problems in PureConfig.
diff --git a/build.sbt b/build.sbt
index cad1d6d95..552734dfd 100644
--- a/build.sbt
+++ b/build.sbt
@@ -9,7 +9,6 @@ ThisBuild / organization := "com.github.pureconfig"
// Enable the OrganizeImports Scalafix rule and semanticdb for scalafix.
ThisBuild / semanticdbEnabled := true
ThisBuild / semanticdbVersion := scalafixSemanticdb.revision
-ThisBuild / scalafixDependencies += "com.github.liancheng" %% "organize-imports" % "0.6.0"
// taken from https://github.com/scala/bug/issues/12632
ThisBuild / libraryDependencySchemes ++= Seq(
@@ -60,6 +59,7 @@ lazy val enum = module(project) in file("modules/enum")
lazy val enumeratum = module(project) in file("modules/enumeratum")
lazy val fs2 = module(project) in file("modules/fs2")
lazy val generic = genericModule(project) in file("modules/generic") dependsOn `generic-base`
+lazy val `generic-scala3` = genericModule(project) in file("modules/generic-scala3") dependsOn `generic-base`
lazy val `generic-base` = genericModule(project) in file("modules/generic-base")
lazy val hadoop = module(project) in file("modules/hadoop")
lazy val http4s = module(project) in file("modules/http4s")
@@ -68,6 +68,8 @@ lazy val ip4s = module(project) in file("modules/ip4s")
lazy val javax = module(project) in file("modules/javax")
lazy val joda = module(project) in file("modules/joda")
lazy val magnolia = module(project) in file("modules/magnolia") dependsOn `generic-base`
+lazy val pekko = module(project) in file("modules/pekko")
+lazy val `pekko-http` = module(project) in file("modules/pekko-http")
lazy val `scala-xml` = module(project) in file("modules/scala-xml")
lazy val scalaz = module(project) in file("modules/scalaz")
lazy val spark = module(project) in file("modules/spark")
@@ -170,7 +172,9 @@ lazy val lintFlags = forScalaVersions {
"-encoding",
"UTF-8", // arg for -encoding
"-feature",
- "-unchecked"
+ "-unchecked",
+ "-old-syntax",
+ "-no-indent"
)
case (maj, min) => throw new Exception(s"Unknown Scala version $maj.$min")
@@ -179,6 +183,9 @@ lazy val lintFlags = forScalaVersions {
// Use the same Scala 2.12 version in the root project as in subprojects
scalaVersion := scala212
+// Setting no cross build for the aggregating root project so that we can have proper per project exclusions.
+crossScalaVersions := Nil
+
// do not publish the root project
publish / skip := true
diff --git a/bundle/docs/README.md b/bundle/docs/README.md
index aa7258638..fc43999e8 100644
--- a/bundle/docs/README.md
+++ b/bundle/docs/README.md
@@ -2,7 +2,7 @@
-[![Build Status](https://github.com/pureconfig/pureconfig/workflows/CI/badge.svg?branch=master)](https://github.com/pureconfig/pureconfig/actions?query=workflow%3ACI+branch%3Amaster)
+[![Build Status](https://github.com/pureconfig/pureconfig/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/pureconfig/pureconfig/actions?query=workflow%3ACI+branch%3Amaster)
[![Coverage Status](https://coveralls.io/repos/github/pureconfig/pureconfig/badge.svg?branch=master)](https://coveralls.io/github/pureconfig/pureconfig?branch=master)
[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.github.pureconfig/pureconfig_2.12/badge.svg)](https://search.maven.org/artifact/com.github.pureconfig/pureconfig_2.12)
[![Scaladoc](https://javadoc.io/badge/com.github.pureconfig/pureconfig-core_2.12.svg)](https://javadoc.io/page/com.github.pureconfig/pureconfig-core_2.12/latest/pureconfig/index.html)
diff --git a/core/build.sbt b/core/build.sbt
index f0676aea4..d694bd7eb 100644
--- a/core/build.sbt
+++ b/core/build.sbt
@@ -4,4 +4,4 @@ name := "pureconfig-core"
crossScalaVersions := Seq(scala212, scala213, scala3)
-libraryDependencies += "com.typesafe" % "config" % "1.4.2"
+libraryDependencies += "com.typesafe" % "config" % "1.4.3"
diff --git a/core/src/main/scala-2/pureconfig/ReaderDerives.scala b/core/src/main/scala-2/pureconfig/ReaderDerives.scala
new file mode 100644
index 000000000..dca46c686
--- /dev/null
+++ b/core/src/main/scala-2/pureconfig/ReaderDerives.scala
@@ -0,0 +1,5 @@
+package pureconfig
+
+trait ReaderDerives {
+ // `derives` clauses are only supported in Scala 3
+}
diff --git a/core/src/main/scala-3/pureconfig/ReaderDerives.scala b/core/src/main/scala-3/pureconfig/ReaderDerives.scala
new file mode 100644
index 000000000..de2b0369d
--- /dev/null
+++ b/core/src/main/scala-3/pureconfig/ReaderDerives.scala
@@ -0,0 +1,10 @@
+package pureconfig
+
+import scala.deriving.Mirror
+
+import pureconfig.generic.derivation._
+
+trait ReaderDerives {
+ inline def derived[A](using m: Mirror.Of[A]): ConfigReader[A] =
+ ConfigReaderDerivation.Default.deriveConfigReader[A]
+}
diff --git a/core/src/main/scala-3/pureconfig/generic/derivation/ConfigReaderDerivation.scala b/core/src/main/scala-3/pureconfig/generic/derivation/ConfigReaderDerivation.scala
index 40c087fe9..808bf0e36 100644
--- a/core/src/main/scala-3/pureconfig/generic/derivation/ConfigReaderDerivation.scala
+++ b/core/src/main/scala-3/pureconfig/generic/derivation/ConfigReaderDerivation.scala
@@ -5,13 +5,29 @@ package derivation
import scala.compiletime.summonFrom
import scala.deriving.Mirror
+@deprecated(
+ "Custom derivation is deprecated in pureconfig-core. If you only need the default behavior, please use the default `derives` behavior. If you need configuration please use the `pureconfig-generic-scala3` module instead.",
+ "0.17.7"
+)
trait ConfigReaderDerivation extends CoproductConfigReaderDerivation with ProductConfigReaderDerivation {
extension (c: ConfigReader.type) {
inline def derived[A](using m: Mirror.Of[A]): ConfigReader[A] =
- inline m match {
- case given Mirror.ProductOf[A] => derivedProduct
- case given Mirror.SumOf[A] => derivedSum
- }
+ deriveConfigReader[A]
+ }
+
+ inline def deriveConfigReader[A](using m: Mirror.Of[A]): ConfigReader[A] =
+ inline m match {
+ case given Mirror.ProductOf[A] => derivedProduct
+ case given Mirror.SumOf[A] => derivedSum
+ }
+
+ /** Summons a `ConfigReader` for a given type `A`. It first tries to find an existing given instance of
+ * `ConfigReader[A]`. If none is found, it tries to derive one using this `ConfigReaderDerivation` instance. This
+ * method differs from `derived` in that the latter doesn't try to find an existing instance first.
+ */
+ protected inline def summonConfigReader[A] = summonFrom {
+ case reader: ConfigReader[A] => reader
+ case given Mirror.Of[A] => ConfigReader.derived[A]
}
/** Summons a `ConfigReader` for a given type `A`. It first tries to find an existing given instance of
@@ -24,6 +40,7 @@ trait ConfigReaderDerivation extends CoproductConfigReaderDerivation with Produc
}
}
+@deprecated("Derivation of ConfigReaders using `derives` is now supported without an import.", "0.17.7")
object ConfigReaderDerivation {
object Default
extends ConfigReaderDerivation
@@ -31,4 +48,5 @@ object ConfigReaderDerivation {
with ProductConfigReaderDerivation(ConfigFieldMapping(CamelCase, KebabCase))
}
+@deprecated("Derivation of ConfigReaders using `derives` is now supported without an import.", "0.17.7")
val default = ConfigReaderDerivation.Default
diff --git a/core/src/main/scala-3/pureconfig/generic/derivation/CoproductConfigReaderDerivation.scala b/core/src/main/scala-3/pureconfig/generic/derivation/CoproductConfigReaderDerivation.scala
index 7f3fe75c2..c978b8fde 100644
--- a/core/src/main/scala-3/pureconfig/generic/derivation/CoproductConfigReaderDerivation.scala
+++ b/core/src/main/scala-3/pureconfig/generic/derivation/CoproductConfigReaderDerivation.scala
@@ -9,6 +9,10 @@ import pureconfig.error.{CannotConvert, ConfigReaderFailures}
import pureconfig.generic.derivation.ConfigReaderDerivation
import pureconfig.generic.derivation.Utils._
+@deprecated(
+ "Custom derivation is deprecated in pureconfig-core. If you only need the default behavior, please use the default `derives` behavior. If you need configuration please use the `pureconfig-generic-scala3` module instead.",
+ "0.17.7"
+)
trait CoproductConfigReaderDerivation(fieldMapping: ConfigFieldMapping, optionField: String) {
self: ConfigReaderDerivation =>
inline def derivedSum[A](using m: Mirror.SumOf[A]): ConfigReader[A] =
diff --git a/core/src/main/scala-3/pureconfig/generic/derivation/EnumConfigConvert.scala b/core/src/main/scala-3/pureconfig/generic/derivation/EnumConfigConvert.scala
new file mode 100644
index 000000000..acdc22189
--- /dev/null
+++ b/core/src/main/scala-3/pureconfig/generic/derivation/EnumConfigConvert.scala
@@ -0,0 +1,20 @@
+package pureconfig
+package generic
+package derivation
+
+import scala.deriving.Mirror
+
+import com.typesafe.config.ConfigValue
+
+trait EnumConfigConvert[A] extends ConfigConvert[A]
+
+object EnumConfigConvert {
+ inline def derived[A: Mirror.SumOf]: EnumConfigConvert[A] =
+ new EnumConfigConvert[A] {
+ val reader = EnumConfigReaderDerivation.Default.EnumConfigReader.derived[A]
+ val writer = EnumConfigWriterDerivation.Default.EnumConfigWriter.derived[A]
+
+ def from(cur: ConfigCursor): ConfigReader.Result[A] = reader.from(cur)
+ def to(a: A): ConfigValue = writer.to(a)
+ }
+}
diff --git a/core/src/main/scala-3/pureconfig/generic/derivation/EnumConfigWriterDerivation.scala b/core/src/main/scala-3/pureconfig/generic/derivation/EnumConfigWriterDerivation.scala
new file mode 100644
index 000000000..39ec9d3e5
--- /dev/null
+++ b/core/src/main/scala-3/pureconfig/generic/derivation/EnumConfigWriterDerivation.scala
@@ -0,0 +1,43 @@
+package pureconfig
+package generic
+package derivation
+
+import scala.compiletime.{constValue, erasedValue, error, summonInline}
+import scala.deriving.Mirror
+
+import com.typesafe.config.{ConfigValue, ConfigValueFactory}
+
+import pureconfig.error.{CannotConvert, ConfigReaderFailures}
+import pureconfig.generic.derivation.Utils._
+
+type EnumConfigWriter[A] = EnumConfigWriterDerivation.Default.EnumConfigWriter[A]
+
+trait EnumConfigWriterDerivation(transformName: String => String) {
+
+ trait EnumConfigWriter[A] extends ConfigWriter[A]
+
+ object EnumConfigWriter {
+ inline def derived[A](using m: Mirror.SumOf[A]): EnumConfigWriter[A] =
+ new EnumConfigWriter[A] {
+ assertIsEnum[m.MirroredElemTypes]
+ val labels = transformedLabels[A](transformName).toVector
+
+ def to(a: A): ConfigValue =
+ ConfigValueFactory.fromAnyRef(labels(m.ordinal(a)))
+ }
+ }
+
+ private inline def assertIsEnum[T <: Tuple]: Unit =
+ inline erasedValue[T] match {
+ case _: (h *: t) =>
+ inline summonInline[Mirror.Of[h]] match {
+ case m: Mirror.Singleton => assertIsEnum[t]
+ case _ => error("Enums cannot include parameterized cases.")
+ }
+ case _: EmptyTuple => ()
+ }
+}
+
+object EnumConfigWriterDerivation {
+ object Default extends EnumConfigWriterDerivation(ConfigFieldMapping(PascalCase, KebabCase))
+}
diff --git a/core/src/main/scala-3/pureconfig/generic/derivation/ProductConfigReaderDerivation.scala b/core/src/main/scala-3/pureconfig/generic/derivation/ProductConfigReaderDerivation.scala
index ca22e5722..5b63464f1 100644
--- a/core/src/main/scala-3/pureconfig/generic/derivation/ProductConfigReaderDerivation.scala
+++ b/core/src/main/scala-3/pureconfig/generic/derivation/ProductConfigReaderDerivation.scala
@@ -5,10 +5,13 @@ package derivation
import scala.compiletime.ops.int._
import scala.compiletime.{constValue, constValueTuple, erasedValue, summonFrom, summonInline}
import scala.deriving.Mirror
-
import pureconfig.error._
import pureconfig.generic.derivation.Utils._
+@deprecated(
+ "Custom derivation is deprecated in pureconfig-core. If you only need the default behavior, please use the default `derives` behavior. If you need configuration please use the `pureconfig-generic-scala3` module instead.",
+ "0.17.7"
+)
trait ProductConfigReaderDerivation(fieldMapping: ConfigFieldMapping) { self: ConfigReaderDerivation =>
inline def derivedProduct[A](using m: Mirror.ProductOf[A]): ConfigReader[A] =
diff --git a/core/src/main/scala-3/pureconfig/generic/derivation/Utils.scala b/core/src/main/scala-3/pureconfig/generic/derivation/Utils.scala
index 8d0545fb1..687609e5d 100644
--- a/core/src/main/scala-3/pureconfig/generic/derivation/Utils.scala
+++ b/core/src/main/scala-3/pureconfig/generic/derivation/Utils.scala
@@ -1,7 +1,7 @@
package pureconfig.generic
package derivation
-import scala.compiletime.{constValue, erasedValue, summonFrom, summonInline}
+import scala.compiletime.{constValue, erasedValue}
import scala.deriving.Mirror
object Utils {
diff --git a/core/src/main/scala/pureconfig/ConfigReader.scala b/core/src/main/scala/pureconfig/ConfigReader.scala
index 22582bef3..9f065e522 100644
--- a/core/src/main/scala/pureconfig/ConfigReader.scala
+++ b/core/src/main/scala/pureconfig/ConfigReader.scala
@@ -145,7 +145,12 @@ trait ConfigReader[A] {
/** Provides methods to create [[ConfigReader]] instances.
*/
-object ConfigReader extends BasicReaders with CollectionReaders with ProductReaders with ExportedReaders {
+object ConfigReader
+ extends BasicReaders
+ with CollectionReaders
+ with ProductReaders
+ with ExportedReaders
+ with ReaderDerives {
/** The type of most config PureConfig reading methods.
*
diff --git a/docs/docs/docs/index.md b/docs/docs/docs/index.md
index af108c921..90972f85c 100644
--- a/docs/docs/docs/index.md
+++ b/docs/docs/docs/index.md
@@ -14,6 +14,15 @@ libraryDependencies += "com.github.pureconfig" %% "pureconfig" % "@VERSION@"
For a full example of `build.sbt` you can have a look at this [build.sbt](https://github.com/pureconfig/pureconfig/blob/master/example/build.sbt).
+Users of Scala 3 need to add the following dependency to their `build.sbt`:
+
+```scala
+libraryDependencies += "com.github.pureconfig" %% "pureconfig-core" % "@VERSION@"
+```
+
+While a lot of the documentation will also apply to Scala 3, there is a specific guide for Scala 3's derivation that you can [find here](scala-3-derivation.html).
+
+
Earlier versions of Scala had bugs which can cause subtle compile-time problems in PureConfig.
As a result we recommend only using the latest Scala versions within the minor series.
diff --git a/docs/docs/index.md b/docs/docs/index.md
index 9f573f9af..c2dba08cb 100644
--- a/docs/docs/index.md
+++ b/docs/docs/index.md
@@ -6,7 +6,7 @@ layout: home
-[![Build Status](https://github.com/pureconfig/pureconfig/workflows/CI/badge.svg?branch=master)](https://github.com/pureconfig/pureconfig/actions?query=workflow%3ACI+branch%3Amaster)
+[![Build Status](https://github.com/pureconfig/pureconfig/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/pureconfig/pureconfig/actions?query=workflow%3ACI+branch%3Amaster)
[![Coverage Status](https://coveralls.io/repos/github/pureconfig/pureconfig/badge.svg?branch=master)](https://coveralls.io/github/pureconfig/pureconfig?branch=master)
[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.github.pureconfig/pureconfig_2.12/badge.svg)](https://search.maven.org/artifact/com.github.pureconfig/pureconfig_2.12)
[![Scaladoc](https://javadoc.io/badge/com.github.pureconfig/pureconfig-core_2.12.svg)](https://javadoc.io/page/com.github.pureconfig/pureconfig-core_2.12/latest/pureconfig/index.html)
diff --git a/example/build.sbt b/example/build.sbt
index d634d2ae3..1c163e0c3 100644
--- a/example/build.sbt
+++ b/example/build.sbt
@@ -1,6 +1,6 @@
name := "pureconfig-example"
version := "1.0"
-scalaVersion := "2.12.18"
+scalaVersion := "2.12.19"
val VersionPattern = """ThisBuild / version := "([^"]*)"""".r
val pureconfigVersion = IO.read(file("../version.sbt")).trim match {
@@ -12,7 +12,7 @@ val pureconfigVersion = IO.read(file("../version.sbt")).trim match {
libraryDependencies += "com.github.pureconfig" %% "pureconfig" % pureconfigVersion
-crossScalaVersions := Seq("2.12.18", "2.13.12")
+crossScalaVersions := Seq("2.12.19", "2.13.14")
val versionSpecificFlags =
Def.setting {
diff --git a/modules/akka-http/README.md b/modules/akka-http/README.md
index 65b4cc119..5ab219cfb 100644
--- a/modules/akka-http/README.md
+++ b/modules/akka-http/README.md
@@ -9,7 +9,7 @@ for other classes are welcome :)
In addition to [core PureConfig](https://github.com/pureconfig/pureconfig), you'll need:
```scala
-libraryDependencies += "com.github.pureconfig" %% "pureconfig-akka-http" % "0.17.4"
+libraryDependencies += "com.github.pureconfig" %% "pureconfig-akka-http" % "0.17.7"
```
## Example
diff --git a/modules/akka/README.md b/modules/akka/README.md
index 8959bcbda..05c7b715e 100644
--- a/modules/akka/README.md
+++ b/modules/akka/README.md
@@ -7,7 +7,7 @@ Adds support for selected [Akka](http://akka.io/) classes to PureConfig.
In addition to [core PureConfig](https://github.com/pureconfig/pureconfig), you'll need:
```scala
-libraryDependencies += "com.github.pureconfig" %% "pureconfig-akka" % "0.17.4"
+libraryDependencies += "com.github.pureconfig" %% "pureconfig-akka" % "0.17.7"
```
## Example
diff --git a/modules/cats-effect/README.md b/modules/cats-effect/README.md
index a80dea455..6c1e95e0b 100644
--- a/modules/cats-effect/README.md
+++ b/modules/cats-effect/README.md
@@ -7,7 +7,7 @@ Adds support for loading configuration using [cats-effect](https://github.com/ty
In addition to [core pureconfig](https://github.com/pureconfig/pureconfig), you'll need:
```scala
-libraryDependencies += "com.github.pureconfig" %% "pureconfig-cats-effect" % "0.17.4"
+libraryDependencies += "com.github.pureconfig" %% "pureconfig-cats-effect" % "0.17.7"
```
## Example
diff --git a/modules/cats-effect/build.sbt b/modules/cats-effect/build.sbt
index ec8efcab9..9ef1b2136 100644
--- a/modules/cats-effect/build.sbt
+++ b/modules/cats-effect/build.sbt
@@ -4,7 +4,7 @@ import Utilities._
crossScalaVersions := Seq(scala212, scala213, scala3)
libraryDependencies ++= Seq(
- "org.typelevel" %% "cats-effect" % "3.5.1"
+ "org.typelevel" %% "cats-effect" % "3.5.4"
)
developers := List(Developer("keirlawson", "Keir Lawson", "keirlawson@gmail.com", url("https://github.com/keirlawson")))
diff --git a/modules/cats-effect2/README.md b/modules/cats-effect2/README.md
index 09b771f0d..cd4bc682a 100644
--- a/modules/cats-effect2/README.md
+++ b/modules/cats-effect2/README.md
@@ -8,7 +8,7 @@ This is a backport of `pureconfig-cats-effect` to the old 2.* series.
In addition to [core pureconfig](https://github.com/pureconfig/pureconfig), you'll need:
```scala
-libraryDependencies += "com.github.pureconfig" %% "pureconfig-cats-effect2" % "0.17.4"
+libraryDependencies += "com.github.pureconfig" %% "pureconfig-cats-effect2" % "0.17.7"
```
## Example
diff --git a/modules/cats/README.md b/modules/cats/README.md
index e44be3f90..3277af22c 100644
--- a/modules/cats/README.md
+++ b/modules/cats/README.md
@@ -9,7 +9,7 @@ classes.
In addition to [core pureconfig](https://github.com/pureconfig/pureconfig), you'll need:
```scala
-libraryDependencies += "com.github.pureconfig" %% "pureconfig-cats" % "0.17.4"
+libraryDependencies += "com.github.pureconfig" %% "pureconfig-cats" % "0.17.7"
```
## Example
diff --git a/modules/cats/build.sbt b/modules/cats/build.sbt
index 48e405c75..fd3a71216 100644
--- a/modules/cats/build.sbt
+++ b/modules/cats/build.sbt
@@ -4,9 +4,9 @@ import Utilities._
crossScalaVersions := Seq(scala212, scala213, scala3)
libraryDependencies ++= Seq(
- "org.typelevel" %% "cats-core" % "2.10.0",
- "org.typelevel" %% "cats-laws" % "2.10.0" % "test",
- "org.typelevel" %% "discipline-scalatest" % "2.2.0" % "test"
+ "org.typelevel" %% "cats-core" % "2.12.0",
+ "org.typelevel" %% "cats-laws" % "2.12.0" % "test",
+ "org.typelevel" %% "discipline-scalatest" % "2.3.0" % "test"
)
developers := List(
diff --git a/modules/circe/README.md b/modules/circe/README.md
index ad468ba45..81e7e8baf 100644
--- a/modules/circe/README.md
+++ b/modules/circe/README.md
@@ -7,7 +7,7 @@ Adds support for [Circe](https://circe.github.io/circe/) `Json` to PureConfig.
In addition to [core pureconfig](https://github.com/pureconfig/pureconfig), you'll need:
```scala
-libraryDependencies += "com.github.pureconfig" %% "pureconfig-circe" % "0.17.4"
+libraryDependencies += "com.github.pureconfig" %% "pureconfig-circe" % "0.17.7"
```
## Example
diff --git a/modules/circe/build.sbt b/modules/circe/build.sbt
index 8f6c87223..a8674efd6 100644
--- a/modules/circe/build.sbt
+++ b/modules/circe/build.sbt
@@ -4,9 +4,9 @@ import Utilities._
crossScalaVersions := Seq(scala212, scala213, scala3)
libraryDependencies ++= Seq(
- "io.circe" %% "circe-core" % "0.14.6",
- "io.circe" %% "circe-literal" % "0.14.6" % Test,
- "org.typelevel" %% "jawn-parser" % "1.5.1" % Test
+ "io.circe" %% "circe-core" % "0.14.8",
+ "io.circe" %% "circe-literal" % "0.14.8" % Test,
+ "org.typelevel" %% "jawn-parser" % "1.6.0" % Test
)
developers := List(
diff --git a/modules/cron4s/README.md b/modules/cron4s/README.md
index 113183b45..f5b72f3f0 100644
--- a/modules/cron4s/README.md
+++ b/modules/cron4s/README.md
@@ -8,7 +8,7 @@ Adds support for [Cron4s](https://github.com/alonsodomin/cron4s)'s CronExpr clas
In addition to [core PureConfig](https://github.com/pureconfig/pureconfig), you'll need:
```scala
-libraryDependencies += "com.github.pureconfig" %% "pureconfig-cron4s" % "0.17.4"
+libraryDependencies += "com.github.pureconfig" %% "pureconfig-cron4s" % "0.17.7"
```
## Example
diff --git a/modules/cron4s/build.sbt b/modules/cron4s/build.sbt
index 7f25cbbb6..e86fb35c6 100644
--- a/modules/cron4s/build.sbt
+++ b/modules/cron4s/build.sbt
@@ -2,7 +2,7 @@ import Dependencies.Version._
crossScalaVersions := Seq(scala212, scala213)
-libraryDependencies += "com.github.alonsodomin.cron4s" %% "cron4s-core" % "0.6.1"
+libraryDependencies += "com.github.alonsodomin.cron4s" %% "cron4s-core" % "0.7.0"
developers := List(
Developer("bardurdam", "Bárður Viberg Dam", "bardurdam@gmail.com", url("https://github.com/bardurdam"))
diff --git a/modules/enum/README.md b/modules/enum/README.md
index 701a6f094..1756255a4 100644
--- a/modules/enum/README.md
+++ b/modules/enum/README.md
@@ -11,7 +11,7 @@ Automatically create a converter to read [enum](https://github.com/julienrf/enum
In addition to [core PureConfig](https://github.com/pureconfig/pureconfig), you'll need:
```scala
-libraryDependencies += "com.github.pureconfig" %% "pureconfig-enum" % "0.17.4"
+libraryDependencies += "com.github.pureconfig" %% "pureconfig-enum" % "0.17.7"
```
## Example
diff --git a/modules/enumeratum/README.md b/modules/enumeratum/README.md
index 3867af36d..ff1d34e32 100644
--- a/modules/enumeratum/README.md
+++ b/modules/enumeratum/README.md
@@ -11,7 +11,7 @@ Automatically create a converters to read [Enumeratum](https://github.com/lloydm
In addition to [core PureConfig](https://github.com/pureconfig/pureconfig), you'll need:
```scala
-libraryDependencies += "com.github.pureconfig" %% "pureconfig-enumeratum" % "0.17.4"
+libraryDependencies += "com.github.pureconfig" %% "pureconfig-enumeratum" % "0.17.7"
```
## Example
diff --git a/modules/fs2/README.md b/modules/fs2/README.md
index 35b0124b2..5fb9c77c8 100644
--- a/modules/fs2/README.md
+++ b/modules/fs2/README.md
@@ -7,7 +7,7 @@ Adds support for loading and saving configurations from [fs2](https://github.com
In addition to [core pureconfig](https://github.com/pureconfig/pureconfig), you'll need:
```scala
-libraryDependencies += "com.github.pureconfig" %% "pureconfig-fs2" % "0.17.4"
+libraryDependencies += "com.github.pureconfig" %% "pureconfig-fs2" % "0.17.7"
```
## Example
diff --git a/modules/fs2/build.sbt b/modules/fs2/build.sbt
index 44686c7fb..7f83241cc 100644
--- a/modules/fs2/build.sbt
+++ b/modules/fs2/build.sbt
@@ -3,8 +3,8 @@ import Dependencies.Version._
crossScalaVersions := Seq(scala212, scala213, scala3)
libraryDependencies ++= Seq(
- "co.fs2" %% "fs2-core" % "3.9.2",
- "co.fs2" %% "fs2-io" % "3.9.2"
+ "co.fs2" %% "fs2-core" % "3.10.2",
+ "co.fs2" %% "fs2-io" % "3.10.2"
)
developers := List(Developer("keirlawson", "Keir Lawson", "keirlawson@gmail.com", url("https://github.com/keirlawson")))
diff --git a/modules/generic-base/build.sbt b/modules/generic-base/build.sbt
index e22a49f53..093eeade6 100644
--- a/modules/generic-base/build.sbt
+++ b/modules/generic-base/build.sbt
@@ -1,3 +1,3 @@
import Dependencies.Version._
-crossScalaVersions := Seq(scala212, scala213)
+crossScalaVersions := Seq(scala212, scala213, scala3)
diff --git a/modules/generic-scala3/build.sbt b/modules/generic-scala3/build.sbt
new file mode 100644
index 000000000..75497769e
--- /dev/null
+++ b/modules/generic-scala3/build.sbt
@@ -0,0 +1,3 @@
+import Dependencies.Version._
+
+scalaVersion := scala3
diff --git a/modules/generic-scala3/src/main/scala/pureconfig/generic/scala3/EnumDerivation.scala b/modules/generic-scala3/src/main/scala/pureconfig/generic/scala3/EnumDerivation.scala
new file mode 100644
index 000000000..306c136ed
--- /dev/null
+++ b/modules/generic-scala3/src/main/scala/pureconfig/generic/scala3/EnumDerivation.scala
@@ -0,0 +1,30 @@
+package pureconfig
+package generic
+package scala3
+
+import scala.deriving.Mirror
+
+import pureconfig.generic.derivation._
+
+private[generic] object EnumDerivation {
+ inline def deriveEnumerationReader[A: Mirror.SumOf](transformName: String => String): ConfigReader[A] =
+ (new EnumConfigReaderDerivation(transformName) {}).EnumConfigReader.derived[A]
+
+ inline def deriveEnumerationReader[A: Mirror.SumOf]: ConfigReader[A] =
+ EnumConfigReaderDerivation.Default.EnumConfigReader.derived[A]
+
+ inline def deriveEnumerationWriter[A: Mirror.SumOf](transformName: String => String): ConfigWriter[A] =
+ (new EnumConfigWriterDerivation(transformName) {}).EnumConfigWriter.derived[A]
+
+ inline def deriveEnumerationWriter[A: Mirror.SumOf]: ConfigWriter[A] =
+ EnumConfigWriterDerivation.Default.EnumConfigWriter.derived[A]
+
+ inline def deriveEnumerationConvert[A: Mirror.SumOf]: ConfigConvert[A] =
+ EnumConfigConvert.derived[A]
+
+ inline def deriveEnumerationConvert[A: Mirror.SumOf](transformName: String => String): ConfigConvert[A] =
+ ConfigConvert.fromReaderAndWriter(
+ deriveEnumerationReader(transformName),
+ deriveEnumerationWriter(transformName)
+ )
+}
diff --git a/modules/generic-scala3/src/main/scala/pureconfig/generic/scala3/HintsAwareConfigReaderDerivation.scala b/modules/generic-scala3/src/main/scala/pureconfig/generic/scala3/HintsAwareConfigReaderDerivation.scala
new file mode 100644
index 000000000..9d4f2efcb
--- /dev/null
+++ b/modules/generic-scala3/src/main/scala/pureconfig/generic/scala3/HintsAwareConfigReaderDerivation.scala
@@ -0,0 +1,24 @@
+package pureconfig
+package generic
+package scala3
+
+import scala.compiletime._
+import scala.deriving.Mirror
+
+trait HintsAwareConfigReaderDerivation
+ extends HintsAwareCoproductConfigReaderDerivation,
+ HintsAwareProductConfigReaderDerivation {
+ inline def deriveReader[A](using m: Mirror.Of[A]): ConfigReader[A] =
+ inline m match {
+ case pm: Mirror.ProductOf[A] => deriveProductReader[A](using pm, summonInline[ProductHint[A]])
+ case sm: Mirror.SumOf[A] => deriveSumReader[A](using sm, summonInline[CoproductHint[A]])
+ }
+
+ protected inline def summonConfigReader[A]: ConfigReader[A] =
+ summonFrom {
+ case reader: ConfigReader[A] => reader
+ case given Mirror.Of[A] => deriveReader[A]
+ }
+}
+
+object HintsAwareConfigReaderDerivation extends HintsAwareConfigReaderDerivation
diff --git a/modules/generic-scala3/src/main/scala/pureconfig/generic/scala3/HintsAwareConfigWriterDerivation.scala b/modules/generic-scala3/src/main/scala/pureconfig/generic/scala3/HintsAwareConfigWriterDerivation.scala
new file mode 100644
index 000000000..ff21f3e79
--- /dev/null
+++ b/modules/generic-scala3/src/main/scala/pureconfig/generic/scala3/HintsAwareConfigWriterDerivation.scala
@@ -0,0 +1,24 @@
+package pureconfig
+package generic
+package scala3
+
+import scala.compiletime._
+import scala.deriving.Mirror
+
+trait HintsAwareConfigWriterDerivation
+ extends HintsAwareCoproductConfigWriterDerivation,
+ HintsAwareProductConfigWriterDerivation {
+ inline def deriveWriter[A](using m: Mirror.Of[A]): ConfigWriter[A] =
+ inline m match {
+ case pm: Mirror.ProductOf[A] => deriveProductWriter[A](using pm, summonInline[ProductHint[A]])
+ case sm: Mirror.SumOf[A] => deriveSumWriter[A](using sm, summonInline[CoproductHint[A]])
+ }
+
+ protected inline def summonConfigWriter[A]: ConfigWriter[A] =
+ summonFrom {
+ case writer: ConfigWriter[A] => writer
+ case given Mirror.Of[A] => deriveWriter[A]
+ }
+}
+
+object HintsAwareConfigWriterDerivation extends HintsAwareConfigWriterDerivation
diff --git a/modules/generic-scala3/src/main/scala/pureconfig/generic/scala3/HintsAwareCoproductConfigReaderDerivation.scala b/modules/generic-scala3/src/main/scala/pureconfig/generic/scala3/HintsAwareCoproductConfigReaderDerivation.scala
new file mode 100644
index 000000000..69089f688
--- /dev/null
+++ b/modules/generic-scala3/src/main/scala/pureconfig/generic/scala3/HintsAwareCoproductConfigReaderDerivation.scala
@@ -0,0 +1,54 @@
+package pureconfig
+package generic
+package scala3
+
+import scala.compiletime._
+import scala.deriving.Mirror
+
+import pureconfig.error.{CannotConvert, ConfigReaderFailures}
+import pureconfig.generic.derivation.Utils
+import pureconfig.generic.error.InvalidCoproductOption
+
+trait HintsAwareCoproductConfigReaderDerivation { self: HintsAwareConfigReaderDerivation =>
+ inline def deriveSumReader[A](using cm: Mirror.SumOf[A], cph: CoproductHint[A]): ConfigReader[A] =
+ new ConfigReader[A] {
+ val labels = Utils.transformedLabels(identity)
+ val readers = labels.zip(summonAllConfigReaders[cm.MirroredElemTypes, A]).toMap
+
+ def from(cur: ConfigCursor): ConfigReader.Result[A] =
+ summon[CoproductHint[A]]
+ .from(cur, labels.sorted)
+ .flatMap {
+ case CoproductHint.Use(cursor, option) =>
+ readers.get(option) match {
+ case Some(reader) => reader.from(cursor)
+ case None => ConfigReader.Result.fail[A](cursor.failureFor(InvalidCoproductOption(option)))
+ }
+
+ case CoproductHint.Attempt(cursor, options, combineF) =>
+ val initial: Either[Vector[(String, ConfigReaderFailures)], A] = Left(Vector.empty)
+ val res = options.foldLeft(initial) { (curr, option) =>
+ curr.left.flatMap { currentFailures =>
+ readers.get(option) match {
+ case Some(reader) => reader.from(cursor).left.map(f => currentFailures :+ (option -> f))
+ case None =>
+ Left(
+ currentFailures :+
+ (option -> ConfigReaderFailures(cursor.failureFor(InvalidCoproductOption(option))))
+ )
+ }
+
+ }
+ }
+
+ res.left.map(combineF)
+ }
+ }
+
+ private inline def summonAllConfigReaders[T <: Tuple, A]: List[ConfigReader[A]] =
+ inline erasedValue[T] match {
+ case _: (h *: t) =>
+ (summonConfigReader[h] :: summonAllConfigReaders[t, A]).asInstanceOf[List[ConfigReader[A]]]
+ case _: EmptyTuple => Nil
+ }
+}
diff --git a/modules/generic-scala3/src/main/scala/pureconfig/generic/scala3/HintsAwareCoproductConfigWriterDerivation.scala b/modules/generic-scala3/src/main/scala/pureconfig/generic/scala3/HintsAwareCoproductConfigWriterDerivation.scala
new file mode 100644
index 000000000..1c2e78bc3
--- /dev/null
+++ b/modules/generic-scala3/src/main/scala/pureconfig/generic/scala3/HintsAwareCoproductConfigWriterDerivation.scala
@@ -0,0 +1,32 @@
+package pureconfig
+package generic
+package scala3
+
+import scala.compiletime._
+import scala.deriving.Mirror
+
+import com.typesafe.config.ConfigValue
+
+import pureconfig.generic.derivation.Utils
+
+trait HintsAwareCoproductConfigWriterDerivation { self: HintsAwareConfigWriterDerivation =>
+ inline def deriveSumWriter[A](using m: Mirror.SumOf[A], ch: CoproductHint[A]): ConfigWriter[A] =
+ new ConfigWriter[A] {
+ val labels = Utils.transformedLabels(identity).toVector
+ val writers = summonAllConfigWriters[m.MirroredElemTypes].toVector
+
+ def to(a: A): ConfigValue = {
+ val n = m.ordinal(a)
+ val label = labels(n)
+ val writer = writers(n).asInstanceOf[ConfigWriter[Any]]
+
+ summon[CoproductHint[A]].to(writer.to(a), label)
+ }
+ }
+
+ private inline def summonAllConfigWriters[T <: Tuple]: List[ConfigWriter[?]] =
+ inline erasedValue[T] match {
+ case _: (h *: t) => summonConfigWriter[h] :: summonAllConfigWriters[t]
+ case _: EmptyTuple => Nil
+ }
+}
diff --git a/modules/generic-scala3/src/main/scala/pureconfig/generic/scala3/HintsAwareProductConfigReaderDerivation.scala b/modules/generic-scala3/src/main/scala/pureconfig/generic/scala3/HintsAwareProductConfigReaderDerivation.scala
new file mode 100644
index 000000000..8a173a467
--- /dev/null
+++ b/modules/generic-scala3/src/main/scala/pureconfig/generic/scala3/HintsAwareProductConfigReaderDerivation.scala
@@ -0,0 +1,137 @@
+package pureconfig
+package generic
+package scala3
+
+import scala.compiletime._
+import scala.compiletime.ops.int._
+import scala.deriving.Mirror
+import scala.quoted._
+
+import pureconfig.error.{ConfigReaderFailures, KeyNotFound, WrongSizeList}
+import pureconfig.generic.ProductHint.UseOrDefault
+import pureconfig.generic.derivation.Utils
+import pureconfig.generic.derivation.Utils.widen
+
+import ProductDerivationMacros._
+
+trait HintsAwareProductConfigReaderDerivation { self: HintsAwareConfigReaderDerivation =>
+ inline def deriveProductReader[A](using pm: Mirror.ProductOf[A], ph: ProductHint[A]): ConfigReader[A] =
+ inline erasedValue[A] match {
+ case _: Tuple =>
+ new ConfigReader[A] {
+ def from(cur: ConfigCursor): ConfigReader.Result[A] =
+ for {
+ listCur <- asList(cur)
+ result <- readTuple[A & Tuple, 0](listCur.list.toVector)
+ } yield result
+
+ def asList(cur: ConfigCursor) =
+ cur.asListCursor.flatMap { listCur =>
+ if (constValue[Tuple.Size[A & Tuple]] == listCur.size)
+ Right(listCur)
+ else
+ listCur.failed(
+ WrongSizeList(constValue[Tuple.Size[A & Tuple]], listCur.size)
+ )
+ }
+ }
+
+ case _ =>
+ new ConfigReader[A] {
+ def from(cur: ConfigCursor): ConfigReader.Result[A] = {
+ val tupleSize = summonInline[ValueOf[Tuple.Size[pm.MirroredElemTypes]]]
+ val defaults = getDefaults[A](tupleSize.value)
+
+ for {
+ objCursor <- cur.asObjectCursor
+ labels = Utils.transformedLabels(identity).toVector
+ actions = labels.map { label => label -> ph.from(objCursor, label) }.toMap
+ result <- readCaseClass[pm.MirroredElemTypes, 0, A](objCursor, labels, actions, defaults)
+ } yield pm.fromProduct(result)
+ }
+ }
+ }
+
+ private inline def readCaseClass[T <: Tuple, N <: Int, A: ProductHint](
+ objCursor: ConfigObjectCursor,
+ labels: Vector[String],
+ actions: Map[String, ProductHint.Action],
+ defaults: Vector[DefaultValue]
+ ): Either[ConfigReaderFailures, T] =
+ inline erasedValue[T] match {
+ case _: (h *: t) =>
+ val n = constValue[N]
+ lazy val reader = summonConfigReader[h]
+ val default = defaults(n)
+ val label = labels(n)
+ val fieldHint = actions(label)
+
+ val head =
+ (fieldHint, default) match {
+ case (UseOrDefault(cursor, _), Some(defaultValue)) if cursor.isUndefined =>
+ Right(defaultValue().asInstanceOf[h])
+ case (action, _) if reader.isInstanceOf[ReadsMissingKeys] || !action.cursor.isUndefined =>
+ reader.from(action.cursor)
+ case _ =>
+ objCursor.failed(KeyNotFound.forKeys(fieldHint.field, objCursor.keys))
+ }
+
+ val tail = readCaseClass[t, N + 1, A](objCursor, labels, actions, defaults)
+
+ val resultTuple = ConfigReader.Result.zipWith(head, tail)((h, t) => widen[h *: t, T](h *: t))
+
+ val usedFields = actions.map(_._2.field).toSet
+ val hintFailures = summon[ProductHint[A]].bottom(objCursor, usedFields).toLeft(())
+
+ ConfigReader.Result.zipWith(resultTuple, hintFailures)((r, _) => r)
+
+ case _: EmptyTuple =>
+ Right(widen[EmptyTuple, T](EmptyTuple))
+ }
+
+ private inline def readTuple[T <: Tuple, N <: Int](cursors: Vector[ConfigCursor]): Either[ConfigReaderFailures, T] =
+ inline erasedValue[T] match {
+ case _: (h *: t) =>
+ val n = constValue[N]
+ val reader = summonConfigReader[h]
+ val cursor = cursors(n)
+
+ val head = reader.from(cursor)
+ val tail = readTuple[t, N + 1](cursors)
+
+ ConfigReader.Result.zipWith(head, tail)((h, t) => widen[h *: t, T](h *: t))
+
+ case _: EmptyTuple =>
+ Right(widen[EmptyTuple, T](EmptyTuple))
+ }
+
+}
+
+private[scala3] object ProductDerivationMacros {
+ type DefaultValue = Option[() => Any]
+
+ inline def getDefaults[T](inline size: Int): Vector[DefaultValue] = ${ getDefaultsImpl[T]('size) }
+
+ def getDefaultsImpl[T](size: Expr[Int])(using Quotes, Type[T]): Expr[Vector[DefaultValue]] = {
+ import quotes.reflect._
+
+ val n = size.valueOrAbort
+ val typeRepr = TypeRepr.of[T]
+
+ def defaultMethodAt(i: Int) =
+ typeRepr.typeSymbol.companionClass.declaredMethod(s"$$lessinit$$greater$$default$$$i").headOption
+ def callMethod(symbol: Symbol) =
+ Ref(typeRepr.typeSymbol.companionModule).select(symbol).appliedToTypes(typeRepr.typeArgs)
+
+ val expr = Expr.ofSeq {
+ (1 to n).map { i =>
+ defaultMethodAt(i) match {
+ case Some(value) => '{ Some(() => ${ callMethod(value).asExpr }) }
+ case None => Expr(None)
+ }
+ }
+ }
+
+ '{ $expr.toVector }
+ }
+}
diff --git a/modules/generic-scala3/src/main/scala/pureconfig/generic/scala3/HintsAwareProductConfigWriterDerivation.scala b/modules/generic-scala3/src/main/scala/pureconfig/generic/scala3/HintsAwareProductConfigWriterDerivation.scala
new file mode 100644
index 000000000..3bbd0bbfc
--- /dev/null
+++ b/modules/generic-scala3/src/main/scala/pureconfig/generic/scala3/HintsAwareProductConfigWriterDerivation.scala
@@ -0,0 +1,73 @@
+package pureconfig
+package generic
+package scala3
+
+import scala.compiletime._
+import scala.compiletime.ops.int._
+import scala.deriving.Mirror
+import scala.jdk.CollectionConverters.given
+import scala.quoted._
+
+import com.typesafe.config.{ConfigValue, ConfigValueFactory}
+
+import pureconfig.generic.derivation.Utils
+
+trait HintsAwareProductConfigWriterDerivation { self: HintsAwareConfigWriterDerivation =>
+
+ inline def deriveProductWriter[A](using pm: Mirror.ProductOf[A], ph: ProductHint[A]): ConfigWriter[A] =
+ inline erasedValue[A] match {
+ case _: Tuple =>
+ new ConfigWriter[A] {
+ def to(a: A): ConfigValue = {
+ val values = writeTuple[pm.MirroredElemTypes, 0](a.asInstanceOf[Product])
+
+ ConfigValueFactory.fromIterable(values.asJava)
+ }
+ }
+
+ case _ =>
+ new ConfigWriter[A] {
+ def to(a: A): ConfigValue = {
+ val labels = Utils.transformedLabels(identity).toVector
+ val values = writeCaseClass[pm.MirroredElemTypes, 0, A](a.asInstanceOf[Product], labels)
+
+ ConfigValueFactory.fromMap(values.toMap.asJava)
+ }
+ }
+ }
+
+ private inline def writeTuple[T <: Tuple, N <: Int](product: Product): List[ConfigValue] =
+ inline erasedValue[T] match {
+ case _: (h *: t) =>
+ val n = constValue[N]
+ val value = product.productElement(n).asInstanceOf[h]
+ val head = summonConfigWriter[h].to(value)
+ val tail = writeTuple[t, N + 1](product)
+
+ head :: tail
+
+ case _: EmptyTuple => Nil
+ }
+
+ private inline def writeCaseClass[T <: Tuple, N <: Int, A: ProductHint](
+ product: Product,
+ labels: Vector[String]
+ ): List[(String, ConfigValue)] =
+ inline erasedValue[T] match {
+ case _: (h *: t) =>
+ val n = constValue[N]
+ val value = product.productElement(n).asInstanceOf[h]
+
+ val valueOpt = summonConfigWriter[h] match {
+ case writer: WritesMissingKeys[`h` @unchecked] => writer.toOpt(value)
+ case writer => Some(writer.to(value))
+ }
+
+ val head = summon[ProductHint[A]].to(valueOpt, labels(n)).toList
+ val tail = writeCaseClass[t, N + 1, A](product, labels)
+
+ head ::: tail
+
+ case _: EmptyTuple => Nil
+ }
+}
diff --git a/modules/generic-scala3/src/main/scala/pureconfig/generic/semiauto.scala b/modules/generic-scala3/src/main/scala/pureconfig/generic/semiauto.scala
new file mode 100644
index 000000000..b5870bcff
--- /dev/null
+++ b/modules/generic-scala3/src/main/scala/pureconfig/generic/semiauto.scala
@@ -0,0 +1,17 @@
+package pureconfig.generic
+
+import scala.deriving.Mirror
+
+import pureconfig.ConfigConvert
+import pureconfig.generic.derivation._
+
+import scala3._
+
+object semiauto {
+ export HintsAwareConfigReaderDerivation.deriveReader
+ export HintsAwareConfigWriterDerivation.deriveWriter
+ export EnumDerivation._
+
+ inline def deriveConvert[A: Mirror.Of]: ConfigConvert[A] =
+ ConfigConvert.fromReaderAndWriter(deriveReader[A], deriveWriter[A])
+}
diff --git a/modules/generic-scala3/src/test/scala/pureconfig/generic/CoproductConvertDerivationSuite.scala b/modules/generic-scala3/src/test/scala/pureconfig/generic/CoproductConvertDerivationSuite.scala
new file mode 100644
index 000000000..96127006a
--- /dev/null
+++ b/modules/generic-scala3/src/test/scala/pureconfig/generic/CoproductConvertDerivationSuite.scala
@@ -0,0 +1,62 @@
+package pureconfig
+package generic
+
+import com.typesafe.config.{ConfigFactory, ConfigObject, ConfigValueFactory}
+import org.scalacheck.{Arbitrary, Gen}
+
+import pureconfig._
+import pureconfig.error._
+import pureconfig.error.{ConvertFailure => ConfigReaderConvertFailure}
+import pureconfig.generic._
+import pureconfig.generic.error.UnexpectedValueForFieldCoproductHint
+import pureconfig.generic.semiauto._
+
+class CoproductConvertDerivationSuite extends BaseSuite {
+ enum AnimalConfig {
+ case DogConfig(age: Int)
+ case CatConfig(age: Int)
+ case BirdConfig(canFly: Boolean)
+ }
+ given ConfigConvert[AnimalConfig] = deriveConvert
+
+ import AnimalConfig._
+
+ behavior of "ConfigConvert"
+
+ val genBirdConfig: Gen[BirdConfig] = Arbitrary.arbBool.arbitrary.map(BirdConfig.apply)
+ val genCatConfig: Gen[CatConfig] = Arbitrary.arbInt.arbitrary.map(CatConfig.apply)
+ val genDogConfig: Gen[DogConfig] = Arbitrary.arbInt.arbitrary.map(DogConfig.apply)
+ val genAnimalConfig: Gen[AnimalConfig] = Gen.oneOf(genBirdConfig, genCatConfig, genDogConfig)
+ given Arbitrary[AnimalConfig] = Arbitrary(genAnimalConfig)
+
+ checkArbitrary[AnimalConfig]
+
+ it should "read disambiguation information on sealed families by default" in {
+ val conf = ConfigFactory.parseString("{ type = dog-config, age = 2 }")
+ ConfigReader[AnimalConfig].from(conf.root()) shouldEqual Right(DogConfig(2))
+ }
+
+ it should "return a proper ConfigReaderFailure if the hint field in a coproduct is missing" in {
+ val conf = ConfigFactory.parseString("{ can-fly = true }")
+ ConfigReader[AnimalConfig].from(conf.root()) should failWithReason[KeyNotFound]
+ }
+
+ it should "return a proper ConfigReaderFailure if the hint field in a coproduct contains an invalid option" in {
+ val conf = ConfigFactory.parseString("{ can-fly = true, type = car-config }")
+ val expectedFailure = ConfigReaderConvertFailure(
+ UnexpectedValueForFieldCoproductHint(ConfigValueFactory.fromAnyRef("car-config")),
+ stringConfigOrigin(1),
+ "type"
+ )
+
+ ConfigReader[AnimalConfig].from(conf.root()) should failWith(expectedFailure)
+ }
+
+ it should "return a proper ConfigReaderFailure when a coproduct config is missing" in {
+ case class AnimalCage(animal: AnimalConfig)
+ given ConfigReader[AnimalCage] = deriveReader
+
+ ConfigReader[AnimalCage].from(ConfigFactory.empty().root()) should failWithReason[KeyNotFound]
+ }
+
+}
diff --git a/modules/generic-scala3/src/test/scala/pureconfig/generic/CoproductHintSuite.scala b/modules/generic-scala3/src/test/scala/pureconfig/generic/CoproductHintSuite.scala
new file mode 100644
index 000000000..b06622f55
--- /dev/null
+++ b/modules/generic-scala3/src/test/scala/pureconfig/generic/CoproductHintSuite.scala
@@ -0,0 +1,163 @@
+package pureconfig
+package generic
+
+import com.typesafe.config._
+
+import pureconfig.error._
+import pureconfig.generic._
+import pureconfig.generic.error.{CoproductHintException, UnexpectedValueForFieldCoproductHint}
+import pureconfig.generic.semiauto._
+import pureconfig.syntax._
+
+class CoproductHintSuite extends BaseSuite {
+ enum AnimalConfig {
+ case DogConfig(age: Int)
+ case CatConfig(age: Int)
+ case BirdConfig(canFly: Boolean)
+ }
+
+ import AnimalConfig._
+
+ behavior of "CoproductHint"
+
+ {
+ given FieldCoproductHint[AnimalConfig] = new FieldCoproductHint[AnimalConfig]("which-animal") {
+ override def fieldValue(name: String) = name.dropRight("Config".length)
+ }
+
+ it should "read values as expected when using a FieldCoproductHint" in {
+ given ConfigReader[AnimalConfig] = deriveReader
+ val conf = ConfigFactory.parseString("{ which-animal = Dog, age = 2 }")
+
+ ConfigReader[AnimalConfig].from(conf.root()) shouldEqual Right(DogConfig(2))
+ }
+
+ it should "write values as expected when using a FieldCoproductHint" in {
+ given ConfigWriter[AnimalConfig] = deriveWriter
+ val conf = ConfigWriter[AnimalConfig].to(DogConfig(2))
+
+ conf shouldBe a[ConfigObject]
+ conf.asInstanceOf[ConfigObject].get("which-animal") shouldEqual ConfigValueFactory.fromAnyRef("Dog")
+ }
+
+ it should "fail to read values that are not objects when using a FieldCoproductHint" in {
+ given ConfigReader[AnimalConfig] = deriveReader
+ val conf = ConfigValueFactory.fromAnyRef("Dog")
+
+ ConfigReader[AnimalConfig].from(conf) should failWith(
+ WrongType(ConfigValueType.STRING, Set(ConfigValueType.OBJECT))
+ )
+ }
+
+ it should "fail to read values in the discriminating field that are not strings when using a FieldCoproductHint" in {
+ given ConfigReader[AnimalConfig] = deriveReader
+ val conf = ConfigFactory.parseString("{ which-animal { type = Dog }, age = 2 }")
+
+ ConfigReader[AnimalConfig].from(conf.root()) should be(
+ Left(
+ ConfigReaderFailures(
+ ConvertFailure(
+ WrongType(ConfigValueType.OBJECT, Set(ConfigValueType.STRING)),
+ stringConfigOrigin(1),
+ "which-animal"
+ )
+ )
+ )
+ )
+ }
+
+ it should "fail with an appropriate reason if an unexpected value is found at the discriminating field when using a FieldCoproductHint" in {
+ given ConfigReader[AnimalConfig] = deriveReader
+ val conf = ConfigFactory.parseString("{ which-animal = unexpected, age = 2 }")
+
+ ConfigReader[AnimalConfig].from(conf.root()) should be(
+ Left(
+ ConfigReaderFailures(
+ ConvertFailure(
+ UnexpectedValueForFieldCoproductHint(ConfigValueFactory.fromAnyRef("unexpected")),
+ stringConfigOrigin(1),
+ "which-animal"
+ )
+ )
+ )
+ )
+ }
+
+ it should "fail to read when the hint field conflicts with a field of an option when using a FieldCoproductHint" in {
+ sealed trait Conf
+ case class AmbiguousConf(typ: String) extends Conf
+
+ given convert: ConfigConvert[Conf] = deriveConvert
+
+ given FieldCoproductHint[Conf] = FieldCoproductHint("typ")
+
+ val conf = ConfigFactory.parseString("{ typ = ambiguous-conf }")
+ convert.from(conf.root()) should failWithReason[KeyNotFound] // "typ" should not be passed to the coproduct option
+
+ val ex = the[CoproductHintException] thrownBy convert.to(AmbiguousConf("ambiguous-conf"))
+ ex.failure shouldEqual CollidingKeys("typ", ConfigValueFactory.fromAnyRef("ambiguous-conf"))
+ }
+
+ {
+ given CoproductHint[AnimalConfig] = FirstSuccessCoproductHint[AnimalConfig]
+
+ it should "read values as expected when using a FirstSuccessCoproductHint" in {
+ given ConfigReader[AnimalConfig] = deriveReader
+ val conf = ConfigFactory.parseString("{ can-fly = true }")
+
+ ConfigReader[AnimalConfig].from(conf.root()) shouldBe Right(BirdConfig(true))
+ }
+
+ it should "write values as expected when using a FirstSuccessCoproductHint" in {
+ given ConfigWriter[AnimalConfig] = deriveWriter
+ val conf = ConfigWriter[AnimalConfig].to(DogConfig(2))
+ conf shouldBe a[ConfigObject]
+ conf.asInstanceOf[ConfigObject].get("which-animal") shouldBe null
+ }
+ }
+
+ {
+ sealed trait A
+ case class AA1(a: Int) extends A
+ case class AA2(a: String) extends A
+ case class EnclosingA(values: Map[String, A])
+
+ it should "fail to read with errors relevant for coproduct derivation when using the default CoproductHint" in {
+ given ConfigReader[A] = deriveReader
+ given ConfigReader[EnclosingA] = deriveReader
+
+ val conf = ConfigFactory.parseString("""
+ {
+ values {
+ v1 {
+ type = "unexpected"
+ a = 2
+ }
+ v2 {
+ type = "aa-2"
+ a = "val"
+ }
+ v3 {
+ a = 5
+ }
+ }
+ }
+ """)
+
+ val exception = intercept[ConfigReaderException[_]] {
+ conf.root().toOrThrow[EnclosingA]
+ }
+
+ exception.failures.toList.toSet shouldBe Set(
+ ConvertFailure(
+ UnexpectedValueForFieldCoproductHint(ConfigValueFactory.fromAnyRef("unexpected")),
+ stringConfigOrigin(5),
+ "values.v1.type"
+ ),
+ ConvertFailure(KeyNotFound("type", Set()), stringConfigOrigin(12), "values.v3")
+ )
+ }
+ }
+ }
+
+}
diff --git a/modules/generic-scala3/src/test/scala/pureconfig/generic/EnumDerivationSuite.scala b/modules/generic-scala3/src/test/scala/pureconfig/generic/EnumDerivationSuite.scala
new file mode 100644
index 000000000..4795c8fab
--- /dev/null
+++ b/modules/generic-scala3/src/test/scala/pureconfig/generic/EnumDerivationSuite.scala
@@ -0,0 +1,103 @@
+package pureconfig
+package generic
+
+import scala.compiletime.testing.{typeCheckErrors, typeChecks}
+import scala.deriving.Mirror
+import scala.language.higherKinds
+
+import com.typesafe.config.{ConfigFactory, ConfigValueFactory, ConfigValueType}
+
+import pureconfig._
+import pureconfig.error.{CannotConvert, WrongType}
+import pureconfig.generic.semiauto._
+
+class EnumDerivationSuite extends BaseSuite {
+ behavior of "EnumDerivation"
+
+ it should "provide util methods to derive readers" in {
+ enum Color {
+ case RainyBlue, SunnyYellow
+ }
+
+ given ConfigReader[Color] = deriveEnumerationReader
+
+ ConfigReader[Color].from(ConfigValueFactory.fromAnyRef("rainy-blue")) shouldBe Right(Color.RainyBlue)
+ ConfigReader[Color].from(ConfigValueFactory.fromAnyRef("sunny-yellow")) shouldBe Right(Color.SunnyYellow)
+
+ val unknownValue = ConfigValueFactory.fromAnyRef("blue")
+
+ ConfigReader[Color].from(unknownValue) should failWith(
+ CannotConvert("blue", "Color", "The value is not a valid enum option."),
+ "",
+ emptyConfigOrigin
+ )
+
+ val conf = ConfigFactory.parseString("{ type: person, name: John, surname: Doe }")
+
+ ConfigReader[Color].from(conf.root()) should failWith(
+ WrongType(ConfigValueType.OBJECT, Set(ConfigValueType.STRING)),
+ "",
+ stringConfigOrigin(1)
+ )
+ }
+
+ it should "provide util methods to derive writers for enumerations encoded as enums or sealed traits" in {
+ enum Color {
+ case RainyBlue, SunnyYellow
+ }
+
+ given ConfigWriter[Color] = deriveEnumerationWriter
+
+ ConfigWriter[Color].to(Color.RainyBlue) shouldEqual ConfigValueFactory.fromAnyRef("rainy-blue")
+ ConfigWriter[Color].to(Color.SunnyYellow) shouldEqual ConfigValueFactory.fromAnyRef("sunny-yellow")
+ }
+
+ it should "provide util methods to derive full converters for enumerations encoded as enums or sealed traits" in {
+ enum Color {
+ case RainyBlue, SunnyYellow
+ }
+
+ given ConfigConvert[Color] = deriveEnumerationConvert
+
+ ConfigConvert[Color].from(ConfigValueFactory.fromAnyRef("rainy-blue")) shouldBe Right(Color.RainyBlue)
+ ConfigConvert[Color].from(ConfigValueFactory.fromAnyRef("sunny-yellow")) shouldBe Right(Color.SunnyYellow)
+ ConfigConvert[Color].to(Color.RainyBlue) shouldEqual ConfigValueFactory.fromAnyRef("rainy-blue")
+ ConfigConvert[Color].to(Color.SunnyYellow) shouldEqual ConfigValueFactory.fromAnyRef("sunny-yellow")
+ }
+
+ it should "provide customizable util methods to derive readers" in {
+ enum Color {
+ case RainyBlue, SunnyYellow
+ }
+
+ given ConfigReader[Color] = deriveEnumerationReader(ConfigFieldMapping(PascalCase, SnakeCase))
+
+ ConfigReader[Color].from(ConfigValueFactory.fromAnyRef("rainy_blue")) shouldBe Right(Color.RainyBlue)
+ ConfigReader[Color].from(ConfigValueFactory.fromAnyRef("sunny_yellow")) shouldBe Right(Color.SunnyYellow)
+ }
+
+ it should "provide customizable util methods to derive writers" in {
+ enum Color {
+ case RainyBlue, SunnyYellow
+ }
+
+ given ConfigWriter[Color] = deriveEnumerationWriter(ConfigFieldMapping(PascalCase, SnakeCase))
+
+ ConfigWriter[Color].to(Color.RainyBlue) shouldEqual ConfigValueFactory.fromAnyRef("rainy_blue")
+ ConfigWriter[Color].to(Color.SunnyYellow) shouldEqual ConfigValueFactory.fromAnyRef("sunny_yellow")
+ }
+
+ it should "provide customizable util methods to derive converters" in {
+ enum Color {
+ case RainyBlue, SunnyYellow
+ }
+
+ given ConfigConvert[Color] = deriveEnumerationConvert(ConfigFieldMapping(PascalCase, SnakeCase))
+
+ ConfigReader[Color].from(ConfigValueFactory.fromAnyRef("rainy_blue")) shouldBe Right(Color.RainyBlue)
+ ConfigReader[Color].from(ConfigValueFactory.fromAnyRef("sunny_yellow")) shouldBe Right(Color.SunnyYellow)
+ ConfigWriter[Color].to(Color.RainyBlue) shouldEqual ConfigValueFactory.fromAnyRef("rainy_blue")
+ ConfigWriter[Color].to(Color.SunnyYellow) shouldEqual ConfigValueFactory.fromAnyRef("sunny_yellow")
+ }
+
+}
diff --git a/modules/generic-scala3/src/test/scala/pureconfig/generic/ProductConvertDerivationSuite.scala b/modules/generic-scala3/src/test/scala/pureconfig/generic/ProductConvertDerivationSuite.scala
new file mode 100644
index 000000000..f79eff741
--- /dev/null
+++ b/modules/generic-scala3/src/test/scala/pureconfig/generic/ProductConvertDerivationSuite.scala
@@ -0,0 +1,291 @@
+package pureconfig
+package generic
+
+import scala.concurrent.duration._
+import scala.jdk.CollectionConverters.given
+import scala.language.higherKinds
+
+import com.typesafe.config.{ConfigFactory, ConfigRenderOptions, ConfigValueFactory}
+import org.scalacheck.Arbitrary
+
+import pureconfig.ConfigConvert.catchReadError
+import pureconfig._
+import pureconfig.error.{KeyNotFound, WrongSizeList, WrongType}
+import pureconfig.generic.semiauto._
+
+class ProductConvertDerivationSuite extends BaseSuite {
+
+ behavior of "ConfigConvert"
+
+ /* A configuration with only simple values and `Option` */
+ case class FlatConfig(b: Boolean, d: Double, f: Float, i: Int, l: Long, s: String, o: Option[String])
+ given ConfigConvert[FlatConfig] = deriveConvert
+
+ /* A configuration with a field of a type that is unknown to `ConfigConvert` */
+ class MyType(myField: String) {
+ def getMyField: String = myField
+ override def equals(obj: Any): Boolean =
+ obj match {
+ case mt: MyType => myField.equals(mt.getMyField)
+ case _ => false
+ }
+ }
+ case class ConfigWithUnknownType(d: MyType)
+ given ConfigConvert[ConfigWithUnknownType] = deriveConvert
+
+ case class RecType(ls: List[RecType])
+
+ given Arbitrary[FlatConfig] = Arbitrary {
+ Arbitrary.arbitrary[(Boolean, Double, Float, Int, Long, String, Option[String])].map((FlatConfig.apply _).tupled)
+ }
+
+ given Arbitrary[MyType] = Arbitrary {
+ Arbitrary.arbitrary[String].map(MyType(_))
+ }
+
+ given Arbitrary[ConfigWithUnknownType] = Arbitrary {
+ Arbitrary.arbitrary[MyType].map(ConfigWithUnknownType.apply)
+ }
+
+ // tests
+
+ checkArbitrary[FlatConfig]
+
+ given ConfigConvert[MyType] = ConfigConvert.viaString[MyType](catchReadError(new MyType(_)), _.getMyField)
+ checkArbitrary[ConfigWithUnknownType]
+
+ it should s"be able to override all of the ConfigReader instances used to parse the product elements" in {
+ case class FlatConfig(b: Boolean, d: Double, f: Float, i: Int, l: Long, s: String, o: Option[String])
+ given ConfigReader[FlatConfig] = deriveReader
+
+ given ConfigReader[Boolean] = ConfigReader.fromString[Boolean](catchReadError(_ => false))
+ given ConfigReader[Double] = ConfigReader.fromString[Double](catchReadError(_ => 1d))
+ given ConfigReader[Float] = ConfigReader.fromString[Float](catchReadError(_ => 2f))
+ given ConfigReader[Int] = ConfigReader.fromString[Int](catchReadError(_ => 3))
+ given ConfigReader[Long] = ConfigReader.fromString[Long](catchReadError(_ => 4L))
+ given ConfigReader[String] = ConfigReader.fromString[String](catchReadError(_ => "foobar"))
+ given ConfigConvert[Option[String]] = ConfigConvert.viaString[Option[String]](catchReadError(_ => None), _ => " ")
+
+ val cc = ConfigReader[FlatConfig]
+ val configValue =
+ ConfigValueFactory.fromMap(
+ Map(
+ "b" -> true,
+ "d" -> 2d,
+ "f" -> 4f,
+ "i" -> 6,
+ "l" -> 8L,
+ "s" -> "barfoo",
+ "o" -> "foobar"
+ ).asJava
+ )
+ cc.from(configValue) shouldBe Right(FlatConfig(false, 1d, 2f, 3, 4L, "foobar", None))
+ }
+
+ val emptyConf = ConfigFactory.empty().root()
+
+ it should s"return a ${classOf[KeyNotFound]} when a key is not in the configuration" in {
+ case class Foo(i: Int)
+ given ConfigConvert[Foo] = deriveConvert
+
+ ConfigConvert[Foo].from(emptyConf) should failWith(KeyNotFound("i"))
+ }
+
+ it should s"return a ${classOf[KeyNotFound]} when a custom convert is used and when a key is not in the configuration" in {
+ case class InnerConf(v: Int)
+ case class EnclosingConf(conf: InnerConf)
+ given ConfigConvert[EnclosingConf] = deriveConvert
+
+ given ConfigConvert[InnerConf] = new ConfigConvert[InnerConf] {
+ def from(cv: ConfigCursor) = Right(InnerConf(42))
+ def to(conf: InnerConf) = ConfigFactory.parseString(s"{ v: ${conf.v} }").root()
+ }
+
+ ConfigConvert[EnclosingConf].from(emptyConf) should failWith(KeyNotFound("conf"))
+ }
+
+ it should "allow custom ConfigWriters to handle missing keys" in {
+ case class Conf(a: Int, b: Int)
+ given ConfigWriter[Conf] = deriveWriter
+
+ ConfigWriter[Conf].to(Conf(0, 3)) shouldBe ConfigFactory.parseString("""{ a: 0, b: 3 }""").root()
+
+ {
+ given ConfigWriter[Int] = new ConfigWriter[Int] with WritesMissingKeys[Int] {
+ def to(v: Int) = ConfigValueFactory.fromAnyRef(v)
+ def toOpt(a: Int) = if (a == 0) None else Some(to(a))
+ }
+ given ConfigWriter[Conf] = deriveWriter
+
+ ConfigWriter[Conf].to(Conf(0, 3)) shouldBe ConfigFactory.parseString("""{ b: 3 }""").root()
+ }
+ }
+
+ it should "not write empty option fields" in {
+ case class Conf(a: Int, b: Option[Int])
+ given ConfigConvert[Conf] = deriveConvert
+
+ ConfigConvert[Conf].to(Conf(42, Some(1))) shouldBe ConfigFactory.parseString("""{ a: 42, b: 1 }""").root()
+ ConfigConvert[Conf].to(Conf(42, None)) shouldBe ConfigFactory.parseString("""{ a: 42 }""").root()
+ }
+
+ it should s"succeed with a correct config" in {
+ case class Foo(i: Int, s: String, bs: List[Boolean])
+ given ConfigReader[Foo] = deriveReader
+ val conf = ConfigFactory.parseString("""{ i: 1, s: "value", bs: [ true, false ] }""").root()
+ ConfigReader[Foo].from(conf) shouldBe Right(Foo(1, "value", List(true, false)))
+ }
+
+ it should s"be able to read lists as tuples" in {
+ case class Foo(values: (Boolean, Int))
+ given ConfigReader[Foo] = deriveReader
+ val conf = ConfigFactory.parseString("""{ values: [ true, 5 ] }""").root()
+ ConfigReader[Foo].from(conf) shouldBe Right(Foo(true -> 5))
+ }
+
+ it should s"return a ${classOf[WrongType]} if the types in the list do not match the tuple" in {
+ case class Foo(values: (Boolean, Int))
+ given ConfigReader[Foo] = deriveReader
+ val conf = ConfigFactory.parseString("""{ values: [ true, "value" ] }""").root()
+ ConfigReader[Foo].from(conf) should failWithReason[WrongType]
+ }
+
+ it should s"return a ${classOf[WrongSizeList]} if the list is shorter than the tuple size" in {
+ case class Foo(values: (Boolean, Int))
+ given ConfigReader[Foo] = deriveReader
+ val conf = ConfigFactory.parseString("""{ values: [ true ] }""").root()
+ ConfigReader[Foo].from(conf) should failWithReason[WrongSizeList]
+ }
+
+ it should s"return a ${classOf[WrongSizeList]} if the list is longer than the tuple size" in {
+ case class Foo(values: (Boolean, Int))
+ given ConfigReader[Foo] = deriveReader
+ val conf = ConfigFactory.parseString("""{ values: [ true, 5, "value" ] }""").root()
+ ConfigReader[Foo].from(conf) should failWithReason[WrongSizeList]
+ }
+
+ it should "allow custom ConfigReaders to handle missing keys" in {
+ case class Conf(a: Int, b: Int)
+ given ConfigReader[Conf] = deriveReader
+ val conf = ConfigFactory.parseString("""{ a: 1 }""").root()
+ ConfigReader[Conf].from(conf) should failWith(KeyNotFound("b"))
+
+ locally {
+ given ConfigReader[Int] with ReadsMissingKeys with {
+ def from(cur: ConfigCursor) =
+ cur.asConfigValue.fold(
+ _ => Right(42),
+ v => {
+ val s = v.render(ConfigRenderOptions.concise)
+ cur.scopeFailure(catchReadError(_.toInt)(implicitly)(s))
+ }
+ )
+ }
+ given ConfigReader[Conf] = deriveReader
+
+ ConfigReader[Conf].from(conf).value shouldBe Conf(1, 42)
+ }
+ }
+
+ it should "invoke defaults when a key is not in the configuration" in {
+ case class ConfA(a: Int, b: Int = 42)
+ given ConfigReader[ConfA] = deriveReader
+
+ case class ConfB[T](i: Int = 1, s: String = "a", l: List[T] = Nil)
+ given [T: ConfigReader]: ConfigReader[ConfB[T]] = deriveReader
+
+ final case class SocketConfig(
+ connectTimeout: FiniteDuration = 5.seconds,
+ readTimeout: FiniteDuration = 12.seconds,
+ keepAlive: Option[Boolean] = None,
+ reuseAddress: Option[Boolean] = None,
+ soLinger: Option[Int] = None,
+ tcpNoDelay: Option[Boolean] = Some(true),
+ receiveBufferSize: Option[Int] = None,
+ sendBufferSize: Option[Int] = None
+ )
+
+ given ConfigReader[SocketConfig] = deriveReader
+
+ val confA = ConfigFactory.parseString("""{ a: 1 }""").root()
+ ConfigReader[ConfA].from(confA).value shouldBe ConfA(1, 42)
+
+ val confB = ConfigFactory.parseString("""{ }""").root()
+ ConfigReader[ConfB[Long]].from(confB).value shouldBe ConfB[Long](1, "a", List.empty[Long])
+
+ val socketConf = ConfigFactory.parseString("""{ }""").root()
+ ConfigReader[SocketConfig]
+ .from(socketConf)
+ .value shouldBe SocketConfig(5.seconds, 12.seconds, None, None, None, Some(true), None, None)
+ }
+
+ it should "consider default arguments by default" in {
+ case class InnerConf(e: Int, g: Int)
+ given ConfigReader[InnerConf] = deriveReader
+ case class Conf(
+ a: Int,
+ b: String = "default",
+ c: Int = 42,
+ d: InnerConf = InnerConf(43, 44),
+ e: Option[Int] = Some(45)
+ )
+ given ConfigReader[Conf] = deriveReader
+
+ val conf1 = ConfigFactory.parseMap(Map("a" -> 2).asJava).root()
+ ConfigReader[Conf].from(conf1).value shouldBe Conf(2, "default", 42, InnerConf(43, 44), Some(45))
+
+ val conf2 = ConfigFactory.parseMap(Map("a" -> 2, "c" -> 50).asJava).root()
+ ConfigReader[Conf].from(conf2).value shouldBe Conf(2, "default", 50, InnerConf(43, 44), Some(45))
+
+ val conf3 = ConfigFactory.parseMap(Map("c" -> 50).asJava).root()
+ ConfigReader[Conf].from(conf3) should failWith(KeyNotFound("a"))
+
+ val conf4 = ConfigFactory.parseMap(Map("a" -> 2, "d.e" -> 5).asJava).root()
+ ConfigReader[Conf].from(conf4) should failWith(KeyNotFound("g"), "d", emptyConfigOrigin)
+
+ val conf5 = ConfigFactory.parseMap(Map("a" -> 2, "d.e" -> 5, "d.g" -> 6).asJava).root()
+ ConfigReader[Conf].from(conf5).value shouldBe Conf(2, "default", 42, InnerConf(5, 6), Some(45))
+
+ val conf6 = ConfigFactory.parseMap(Map("a" -> 2, "d" -> "notAnInnerConf").asJava).root()
+ ConfigReader[Conf].from(conf6) should failWithReason[WrongType]
+
+ val conf7 = ConfigFactory.parseMap(Map("a" -> 2, "c" -> 50, "e" -> 1).asJava).root()
+ ConfigReader[Conf].from(conf7).value shouldBe Conf(2, "default", 50, InnerConf(43, 44), Some(1))
+
+ val conf8 = ConfigFactory.parseMap(Map("a" -> 2, "c" -> 50, "e" -> null).asJava).root()
+ ConfigReader[Conf].from(conf8).value shouldBe Conf(2, "default", 50, InnerConf(43, 44), None)
+ }
+
+ it should "evaluate defaults lazily" in {
+ def throwException: Nothing = throw new RuntimeException("Should not be evaluated")
+
+ case class ConfA(foo: String = throwException)
+ case class ConfB(bar: Option[ConfA])
+ case class ConfC(baz: ConfA = ConfA(), foo: String = throwException)
+
+ given ConfigReader[ConfA] = deriveReader
+ given ConfigReader[ConfB] = deriveReader
+ given ConfigReader[ConfC] = deriveReader
+
+ ConfigSource.string("{ foo: bar }").load[ConfA] shouldBe Right(ConfA("bar"))
+ ConfigSource.string("{ }").load[ConfB] shouldBe Right(ConfB(None))
+ ConfigSource.string("{ baz: { foo: bar }, foo: bar }").load[ConfC] shouldBe Right(ConfC(ConfA("bar"), "bar"))
+ }
+
+ it should s"return a ${classOf[WrongType]} when a key has a wrong type" in {
+ case class Foo(i: Int)
+ case class Bar(foo: Foo)
+ case class FooBar(foo: Foo, bar: Bar)
+ given ConfigReader[FooBar] = deriveReader
+ val conf = ConfigFactory.parseMap(Map("foo.i" -> 1, "bar.foo" -> "").asJava).root()
+ ConfigReader[FooBar].from(conf) should failWithReason[WrongType]
+ }
+
+ it should s"work properly with recursively defined product types" in {
+ case class RecType(ls: List[RecType])
+ given ConfigReader[RecType] = deriveReader
+ val conf = ConfigFactory.parseString("ls = [{ ls = [] }, { ls = [{ ls = [] }] }]").root()
+ ConfigReader[RecType].from(conf).value shouldBe RecType(List(RecType(Nil), RecType(List(RecType(Nil)))))
+ }
+
+}
diff --git a/modules/generic-scala3/src/test/scala/pureconfig/generic/ProductHintSuite.scala b/modules/generic-scala3/src/test/scala/pureconfig/generic/ProductHintSuite.scala
new file mode 100644
index 000000000..2e2aec0c7
--- /dev/null
+++ b/modules/generic-scala3/src/test/scala/pureconfig/generic/ProductHintSuite.scala
@@ -0,0 +1,259 @@
+package pureconfig
+package generic
+
+import scala.jdk.CollectionConverters.given
+
+import com.typesafe.config.{ConfigFactory, ConfigObject, ConfigValueType}
+
+import pureconfig.error._
+import pureconfig.generic.ProductHint
+import pureconfig.generic.semiauto._
+import pureconfig.syntax._
+
+class ProductHintSuite extends BaseSuite {
+
+ behavior of "ProductHint"
+
+ case class ConfWithCamelCaseInner(thisIsAnInt: Int, thisIsAnotherInt: Int)
+ case class ConfWithCamelCase(camelCaseInt: Int, camelCaseString: String, camelCaseConf: ConfWithCamelCaseInner)
+
+ val confWithCamelCase = ConfWithCamelCase(1, "foobar", ConfWithCamelCaseInner(2, 3))
+
+ /** return all the keys in a `ConfigObject` */
+ def allKeys(configObject: ConfigObject): Set[String] =
+ configObject.toConfig().entrySet().asScala.flatMap(_.getKey.split('.')).toSet
+
+ it should "read kebab case config keys to camel case fields by default" in {
+ given ConfigReader[ConfWithCamelCase] = deriveReader
+
+ val conf = ConfigFactory.parseString("""{
+ camel-case-int = 1
+ camel-case-string = "bar"
+ camel-case-conf {
+ this-is-an-int = 3
+ this-is-another-int = 10
+ }
+ }""")
+
+ conf.to[ConfWithCamelCase] shouldBe Right(ConfWithCamelCase(1, "bar", ConfWithCamelCaseInner(3, 10)))
+ }
+
+ it should "write kebab case config keys from camel case fields by default" in {
+ given ConfigWriter[ConfWithCamelCase] = deriveWriter
+
+ val conf = confWithCamelCase.toConfig.asInstanceOf[ConfigObject]
+ allKeys(conf) should contain theSameElementsAs Seq(
+ "camel-case-int",
+ "camel-case-string",
+ "camel-case-conf",
+ "this-is-an-int",
+ "this-is-another-int"
+ )
+ }
+
+ it should "allow customizing the field mapping through a product hint" in {
+ val conf = ConfigFactory
+ .parseString("""{
+ A = 2
+ B = "two"
+ }""")
+ .root()
+
+ case class SampleConf(a: Int, b: String)
+
+ val default = deriveReader[SampleConf]
+ val customized = {
+ given ProductHint[SampleConf] = ProductHint(ConfigFieldMapping(_.toUpperCase))
+ deriveReader[SampleConf]
+ }
+
+ ConfigReader[SampleConf](using default).from(conf).left.value.toList should contain theSameElementsAs Seq(
+ ConvertFailure(KeyNotFound("a", Set("A")), stringConfigOrigin(1), ""),
+ ConvertFailure(KeyNotFound("b", Set("B")), stringConfigOrigin(1), "")
+ )
+
+ ConfigReader[SampleConf](using customized).from(conf) shouldBe Right(SampleConf(2, "two"))
+ }
+
+ it should "read camel case config keys to camel case fields when configured to do so" in {
+ given [A]: ProductHint[A] = ProductHint(ConfigFieldMapping(CamelCase, CamelCase))
+ given ConfigReader[ConfWithCamelCase] = deriveReader
+
+ val conf = ConfigFactory.parseString("""{
+ camelCaseInt = 1
+ camelCaseString = "bar"
+ camelCaseConf {
+ thisIsAnInt = 3
+ thisIsAnotherInt = 10
+ }
+ }""")
+
+ conf.to[ConfWithCamelCase] shouldBe Right(ConfWithCamelCase(1, "bar", ConfWithCamelCaseInner(3, 10)))
+ }
+
+ it should "write camel case config keys to camel case fields when configured to do so" in {
+ given [A]: ProductHint[A] = ProductHint[A](ConfigFieldMapping(CamelCase, CamelCase))
+ given ConfigWriter[ConfWithCamelCase] = deriveWriter
+
+ val conf = confWithCamelCase.toConfig.asInstanceOf[ConfigObject]
+ allKeys(conf) should contain theSameElementsAs Seq(
+ "camelCaseInt",
+ "camelCaseString",
+ "camelCaseConf",
+ "thisIsAnInt",
+ "thisIsAnotherInt"
+ )
+ }
+
+ it should "read pascal case config keys to pascal case fields when configured to do so" in {
+ given [A]: ProductHint[A] = ProductHint(ConfigFieldMapping(CamelCase, PascalCase))
+ given ConfigReader[ConfWithCamelCase] = deriveReader
+
+ val conf = ConfigFactory.parseString("""{
+ CamelCaseInt = 1
+ CamelCaseString = "bar"
+ CamelCaseConf {
+ ThisIsAnInt = 3
+ ThisIsAnotherInt = 10
+ }
+ }""")
+
+ conf.to[ConfWithCamelCase] shouldBe Right(ConfWithCamelCase(1, "bar", ConfWithCamelCaseInner(3, 10)))
+ }
+
+ it should "write pascal case config keys to pascal case fields when configured to do so" in {
+ given [A]: ProductHint[A] = ProductHint[A](ConfigFieldMapping(CamelCase, PascalCase))
+ given ConfigWriter[ConfWithCamelCase] = deriveWriter
+
+ val conf = ConfWithCamelCase(1, "foobar", ConfWithCamelCaseInner(2, 3)).toConfig.asInstanceOf[ConfigObject]
+ allKeys(conf) should contain theSameElementsAs Seq(
+ "CamelCaseInt",
+ "CamelCaseString",
+ "CamelCaseConf",
+ "ThisIsAnInt",
+ "ThisIsAnotherInt"
+ )
+ }
+
+ it should "allow customizing the field mapping only for specific types" in {
+ given ProductHint[ConfWithCamelCase] = ProductHint(ConfigFieldMapping(CamelCase, CamelCase))
+ given ConfigReader[ConfWithCamelCase] = deriveReader
+
+ val conf = ConfigFactory.parseString("""{
+ camelCaseInt = 1
+ camelCaseString = "bar"
+ camelCaseConf {
+ this-is-an-int = 3
+ this-is-another-int = 10
+ }
+ }""")
+
+ conf.to[ConfWithCamelCase] shouldBe Right(ConfWithCamelCase(1, "bar", ConfWithCamelCaseInner(3, 10)))
+ }
+
+ it should "disallow unknown keys if specified through a product hint" in {
+ given ProductHint[Conf2] = ProductHint(allowUnknownKeys = false)
+
+ case class Conf1(a: Int)
+ given ConfigReader[Conf1] = deriveReader
+ case class Conf2(a: Int)
+ given ConfigReader[Conf2] = deriveReader
+
+ val conf = ConfigFactory.parseString("""{
+ conf {
+ a = 1
+ b = 2
+ }
+ }""")
+
+ conf.getConfig("conf").to[Conf1] shouldBe Right(Conf1(1))
+ conf.getConfig("conf").to[Conf2] should failWith(UnknownKey("b"), "b", stringConfigOrigin(4))
+ }
+
+ it should "accumulate all failures if the product hint doesn't allow unknown keys" in {
+ given ProductHint[Conf] = ProductHint(allowUnknownKeys = false)
+ case class Conf(a: Int)
+ given ConfigReader[Conf] = deriveReader
+
+ val conf = ConfigFactory.parseString("""{
+ conf {
+ a = "hello"
+ b = 1
+ }
+ }""".stripMargin)
+
+ conf.getConfig("conf").to[Conf] shouldBe Left(
+ ConfigReaderFailures(
+ ConvertFailure(WrongType(ConfigValueType.STRING, Set(ConfigValueType.NUMBER)), stringConfigOrigin(3), "a"),
+ ConvertFailure(UnknownKey("b"), stringConfigOrigin(4), "b")
+ )
+ )
+ }
+
+ it should "not use default arguments if specified through a product hint" in {
+ case class InnerConf(e: Int, g: Int)
+ given ConfigReader[InnerConf] = deriveReader
+ case class Conf(
+ a: Int,
+ b: String = "default",
+ c: Int = 42,
+ d: InnerConf = InnerConf(43, 44),
+ e: Option[Int] = Some(45)
+ )
+ given ConfigReader[Conf] = deriveReader
+
+ given ProductHint[Conf] = ProductHint(useDefaultArgs = false)
+
+ val conf1 = ConfigFactory.parseMap(Map("a" -> 2).asJava)
+ conf1.to[Conf].left.value.toList should contain theSameElementsAs Seq(
+ ConvertFailure(KeyNotFound("b"), emptyConfigOrigin, ""),
+ ConvertFailure(KeyNotFound("c"), emptyConfigOrigin, ""),
+ ConvertFailure(KeyNotFound("d"), emptyConfigOrigin, "")
+ )
+ }
+
+ it should "include candidate keys in failure reasons in case of a suspected misconfigured ProductHint" in {
+ case class CamelCaseConf(camelCaseInt: Int, camelCaseString: String)
+ given ConfigReader[CamelCaseConf] = deriveReader
+ case class KebabCaseConf(kebabCaseInt: Int, kebabCaseString: String)
+ given ConfigReader[KebabCaseConf] = deriveReader
+ case class SnakeCaseConf(snakeCaseInt: Int, snakeCaseString: String)
+ given ConfigReader[SnakeCaseConf] = deriveReader
+ case class EnclosingConf(camelCaseConf: CamelCaseConf, kebabCaseConf: KebabCaseConf, snakeCaseConf: SnakeCaseConf)
+ given ConfigReader[EnclosingConf] = deriveReader
+
+ val conf = ConfigFactory.parseString("""{
+ camel-case-conf {
+ camelCaseInt = 2
+ camelCaseString = "str"
+ }
+ kebab-case-conf {
+ kebab-case-int = 2
+ kebab-case-string = "str"
+ }
+ snake-case-conf {
+ snake_case_int = 2
+ snake_case_string = "str"
+ }
+ }""")
+
+ val exception = intercept[ConfigReaderException[_]] {
+ conf.root().toOrThrow[EnclosingConf]
+ }
+
+ exception.failures.toList.toSet shouldBe Set(
+ ConvertFailure(KeyNotFound("camel-case-int", Set("camelCaseInt")), stringConfigOrigin(2), "camel-case-conf"),
+ ConvertFailure(
+ KeyNotFound("camel-case-string", Set("camelCaseString")),
+ stringConfigOrigin(2),
+ "camel-case-conf"
+ ),
+ ConvertFailure(KeyNotFound("snake-case-int", Set("snake_case_int")), stringConfigOrigin(10), "snake-case-conf"),
+ ConvertFailure(
+ KeyNotFound("snake-case-string", Set("snake_case_string")),
+ stringConfigOrigin(10),
+ "snake-case-conf"
+ )
+ )
+ }
+}
diff --git a/modules/generic/build.sbt b/modules/generic/build.sbt
index e48543a9d..3c210333c 100644
--- a/modules/generic/build.sbt
+++ b/modules/generic/build.sbt
@@ -3,6 +3,6 @@ import Dependencies.Version._
crossScalaVersions := Seq(scala212, scala213)
libraryDependencies ++= Seq(
- "com.chuusai" %% "shapeless" % "2.3.10",
+ "com.chuusai" %% "shapeless" % "2.3.12",
"org.scala-lang" % "scala-reflect" % scalaVersion.value % Provided
)
diff --git a/modules/hadoop/README.md b/modules/hadoop/README.md
index a023958ac..564ee22a8 100644
--- a/modules/hadoop/README.md
+++ b/modules/hadoop/README.md
@@ -7,7 +7,7 @@ Adds support for selected [Hadoop](http://hadoop.apache.org/) classes to PureCon
In addition to [core PureConfig](https://github.com/pureconfig/pureconfig), you'll need:
```scala
-libraryDependencies += "com.github.pureconfig" %% "pureconfig-hadoop" % "0.17.4"
+libraryDependencies += "com.github.pureconfig" %% "pureconfig-hadoop" % "0.17.7"
```
Also, `pureconfig-hadoop` depends on `hadoop-common` with `provided` scope. This means that you should explicitly add a dependency on `hadoop-common` or any other Hadoop library which depends on `hadoop-common`. Usually it would be something like this:
diff --git a/modules/hadoop/build.sbt b/modules/hadoop/build.sbt
index d2a4cf872..e13b6712a 100644
--- a/modules/hadoop/build.sbt
+++ b/modules/hadoop/build.sbt
@@ -2,7 +2,7 @@ import Dependencies.Version._
crossScalaVersions := Seq(scala212, scala213)
-libraryDependencies ++= Seq("org.apache.hadoop" % "hadoop-common" % "3.3.6" % "provided")
-mdocLibraryDependencies ++= Seq("org.apache.hadoop" % "hadoop-common" % "3.3.6")
+libraryDependencies ++= Seq("org.apache.hadoop" % "hadoop-common" % "3.4.0" % "provided")
+mdocLibraryDependencies ++= Seq("org.apache.hadoop" % "hadoop-common" % "3.4.0")
developers := List(Developer("lmnet", "Yuriy Badalyantc", "lmnet89@gmail.com", url("https://github.com/lmnet")))
diff --git a/modules/http4s/README.md b/modules/http4s/README.md
index ea999e621..820359afa 100644
--- a/modules/http4s/README.md
+++ b/modules/http4s/README.md
@@ -17,7 +17,7 @@ Support is also provided for some of the components of a `Uri`:
In addition to [core PureConfig](https://github.com/pureconfig/pureconfig), you'll need:
```scala
-libraryDependencies += "com.github.pureconfig" %% "pureconfig-http4s" % "0.17.4"
+libraryDependencies += "com.github.pureconfig" %% "pureconfig-http4s" % "0.17.7"
```
## Example
diff --git a/modules/http4s/build.sbt b/modules/http4s/build.sbt
index cda4a8bec..6d2c0437e 100644
--- a/modules/http4s/build.sbt
+++ b/modules/http4s/build.sbt
@@ -2,7 +2,7 @@ import Dependencies.Version._
crossScalaVersions := Seq(scala212, scala213, scala3)
-libraryDependencies ++= Seq("org.http4s" %% "http4s-core" % "0.23.23")
+libraryDependencies ++= Seq("org.http4s" %% "http4s-core" % "0.23.27")
developers := List(
Developer("jcranky", "Paulo Siqueira", "paulo.siqueira@gmail.com", url("https://github.com/jcranky"))
diff --git a/modules/http4s022/README.md b/modules/http4s022/README.md
index bd1752c47..397310cf3 100644
--- a/modules/http4s022/README.md
+++ b/modules/http4s022/README.md
@@ -12,7 +12,7 @@ Newer projects should use a regular [PureConfig Http4s](https://github.com/purec
In addition to [core PureConfig](https://github.com/pureconfig/pureconfig), you'll need:
```scala
-libraryDependencies += "com.github.pureconfig" %% "pureconfig-http4s022" % "0.17.4"
+libraryDependencies += "com.github.pureconfig" %% "pureconfig-http4s022" % "0.17.7"
```
## Example
diff --git a/modules/ip4s/README.md b/modules/ip4s/README.md
index f1f90fd9f..36be8cde9 100644
--- a/modules/ip4s/README.md
+++ b/modules/ip4s/README.md
@@ -18,7 +18,7 @@ PRs adding support for other classes are welcome :)
In addition to [core PureConfig](https://github.com/pureconfig/pureconfig), you'll need:
```scala
-libraryDependencies += "com.github.pureconfig" %% "pureconfig-ip4s" % "0.17.4"
+libraryDependencies += "com.github.pureconfig" %% "pureconfig-ip4s" % "0.17.7"
```
## Example
diff --git a/modules/ip4s/build.sbt b/modules/ip4s/build.sbt
index 3f70ef233..2714ab239 100644
--- a/modules/ip4s/build.sbt
+++ b/modules/ip4s/build.sbt
@@ -2,7 +2,7 @@ import Dependencies.Version._
crossScalaVersions := Seq(scala212, scala213, scala3)
-val ip4sVersion = "3.3.0"
+val ip4sVersion = "3.6.0"
libraryDependencies ++= Seq(
"com.comcast" %% "ip4s-core" % ip4sVersion,
diff --git a/modules/javax/README.md b/modules/javax/README.md
index 72c629ff3..f80e278da 100644
--- a/modules/javax/README.md
+++ b/modules/javax/README.md
@@ -7,7 +7,7 @@ Adds support for selected javax classes to PureConfig.
In addition to [core pureconfig](https://github.com/pureconfig/pureconfig), you'll need:
```scala
-libraryDependencies += "com.github.pureconfig" %% "pureconfig-javax" % "0.17.4"
+libraryDependencies += "com.github.pureconfig" %% "pureconfig-javax" % "0.17.7"
```
## Example
diff --git a/modules/joda/README.md b/modules/joda/README.md
index 6fd9e8069..a2fe911be 100644
--- a/modules/joda/README.md
+++ b/modules/joda/README.md
@@ -13,7 +13,7 @@ The converters need to be provided a `org.joda.time.format.DateTimeFormatter` to
In addition to [core pureconfig](https://github.com/pureconfig/pureconfig), you'll need:
```scala
-libraryDependencies += "com.github.pureconfig" %% "pureconfig-joda" % "0.17.4"
+libraryDependencies += "com.github.pureconfig" %% "pureconfig-joda" % "0.17.7"
```
## Example
diff --git a/modules/joda/build.sbt b/modules/joda/build.sbt
index c81aea299..e9106d762 100644
--- a/modules/joda/build.sbt
+++ b/modules/joda/build.sbt
@@ -2,7 +2,7 @@ import Dependencies.Version._
crossScalaVersions := Seq(scala212, scala213)
-libraryDependencies ++= Seq("joda-time" % "joda-time" % "2.12.5", "org.joda" % "joda-convert" % "2.2.3")
+libraryDependencies ++= Seq("joda-time" % "joda-time" % "2.12.7", "org.joda" % "joda-convert" % "2.2.3")
developers := List(
Developer("melrief", "Mario Pastorelli", "pastorelli.mario@gmail.com", url("https://github.com/melrief")),
diff --git a/modules/magnolia/README.md b/modules/magnolia/README.md
index 8a16e006d..5cb5964cf 100644
--- a/modules/magnolia/README.md
+++ b/modules/magnolia/README.md
@@ -12,7 +12,7 @@ configuration using the same [product](https://pureconfig.github.io/docs/overrid
In addition to [core pureconfig](https://github.com/pureconfig/pureconfig), you'll need:
```scala
-libraryDependencies += "com.github.pureconfig" %% "pureconfig-magnolia" % "0.17.4"
+libraryDependencies += "com.github.pureconfig" %% "pureconfig-magnolia" % "0.17.7"
```
## Example
diff --git a/modules/magnolia/build.sbt b/modules/magnolia/build.sbt
index 383e84f04..1e99cef7f 100644
--- a/modules/magnolia/build.sbt
+++ b/modules/magnolia/build.sbt
@@ -3,10 +3,10 @@ import Dependencies.Version._
crossScalaVersions := Seq(scala212, scala213)
libraryDependencies ++= Seq(
- "com.softwaremill.magnolia1_2" %% "magnolia" % "1.1.6",
+ "com.softwaremill.magnolia1_2" %% "magnolia" % "1.1.10",
"org.scala-lang" % "scala-reflect" % scalaVersion.value % Provided,
// We're using shapeless for illTyped in tests.
- "com.chuusai" %% "shapeless" % "2.3.10" % Test
+ "com.chuusai" %% "shapeless" % "2.3.12" % Test
)
developers := List(
diff --git a/modules/pekko-http/README.md b/modules/pekko-http/README.md
new file mode 100644
index 000000000..4951ba011
--- /dev/null
+++ b/modules/pekko-http/README.md
@@ -0,0 +1,36 @@
+# Pekko HTTP module for PureConfig
+
+Adds support for [Pekko-Http](https://pekko.apache.org/docs/pekko-http/current/common/http-model.html)'s Uri class to PureConfig. PRs adding support
+for other classes are welcome :)
+
+## Add pureconfig-pekko-http to your project
+
+In addition to [core PureConfig](https://github.com/pureconfig/pureconfig), you'll need:
+
+```scala
+libraryDependencies += "com.github.pureconfig" %% "pureconfig-pekko-http" % "0.17.7"
+```
+
+## Example
+
+To load an `Uri` into a configuration, create a class to hold it:
+
+```scala
+import org.apache.pekko.http.scaladsl.model.Uri
+import com.typesafe.config.ConfigFactory
+import pureconfig._
+import pureconfig.generic.auto._
+import pureconfig.module.pekkohttp._
+
+case class MyConfig(uri: Uri)
+```
+
+We can read a `MyConfig` with the following code:
+
+```scala
+val conf = ConfigFactory.parseString("""{ uri: "https://pekko.apache.org/docs/pekko-http/current/common/http-model.html" }""")
+// conf: com.typesafe.config.Config = Config(SimpleConfigObject({"uri":"https://pekko.apache.org/docs/pekko-http/current/common/http-model.html"}))
+
+ConfigSource.fromConfig(conf).load[MyConfig]
+// res0: ConfigReader.Result[MyConfig] = Right(MyConfig(https://pekko.apache.org/docs/pekko-http/current/common/http-model.html))
+```
diff --git a/modules/pekko-http/build.sbt b/modules/pekko-http/build.sbt
new file mode 100644
index 000000000..140ad248f
--- /dev/null
+++ b/modules/pekko-http/build.sbt
@@ -0,0 +1,11 @@
+import Dependencies.Version._
+
+crossScalaVersions := Seq(scala212, scala213)
+
+libraryDependencies ++= Seq(
+ "org.apache.pekko" %% "pekko-actor" % "1.0.3" % "provided",
+ "org.apache.pekko" %% "pekko-http" % "1.0.1"
+)
+mdocLibraryDependencies ++= Seq(
+ "org.apache.pekko" %% "pekko-actor" % "1.0.3"
+)
diff --git a/modules/pekko-http/docs/README.md b/modules/pekko-http/docs/README.md
new file mode 100644
index 000000000..e0b6e4457
--- /dev/null
+++ b/modules/pekko-http/docs/README.md
@@ -0,0 +1,34 @@
+# Pekko HTTP module for PureConfig
+
+Adds support for [Pekko-Http](https://pekko.apache.org/docs/pekko-http/current/common/http-model.html)'s Uri class to PureConfig. PRs adding support
+for other classes are welcome :)
+
+## Add pureconfig-pekko-http to your project
+
+In addition to [core PureConfig](https://github.com/pureconfig/pureconfig), you'll need:
+
+```scala
+libraryDependencies += "com.github.pureconfig" %% "pureconfig-pekko-http" % "@VERSION@"
+```
+
+## Example
+
+To load an `Uri` into a configuration, create a class to hold it:
+
+```scala mdoc:silent
+import org.apache.pekko.http.scaladsl.model.Uri
+import com.typesafe.config.ConfigFactory
+import pureconfig._
+import pureconfig.generic.auto._
+import pureconfig.module.pekkohttp._
+
+case class MyConfig(uri: Uri)
+```
+
+We can read a `MyConfig` with the following code:
+
+```scala mdoc:to-string
+val conf = ConfigFactory.parseString("""{ uri: "https://pekko.apache.org/docs/pekko-http/current/common/http-model.html" }""")
+
+ConfigSource.fromConfig(conf).load[MyConfig]
+```
diff --git a/modules/pekko-http/src/main/scala/pureconfig/module/pekkohttp/package.scala b/modules/pekko-http/src/main/scala/pureconfig/module/pekkohttp/package.scala
new file mode 100644
index 000000000..7582d27e1
--- /dev/null
+++ b/modules/pekko-http/src/main/scala/pureconfig/module/pekkohttp/package.scala
@@ -0,0 +1,32 @@
+package pureconfig.module
+
+import scala.util.Try
+
+import org.apache.pekko.http.scaladsl.model.Uri.ParsingMode
+import org.apache.pekko.http.scaladsl.model.{IllegalUriException, Uri}
+
+import pureconfig.error.{CannotConvert, ExceptionThrown}
+import pureconfig.{ConfigReader, ConfigWriter}
+
+package object pekkohttp {
+
+ implicit val uriReader: ConfigReader[Uri] =
+ ConfigReader.fromString(str =>
+ Try(Uri(str, ParsingMode.Strict)).toEither.left
+ .map {
+ case err: IllegalUriException => CannotConvert(str, "Uri", err.info.summary)
+ case err => ExceptionThrown(err)
+ }
+ )
+
+ implicit val uriWriter: ConfigWriter[Uri] = ConfigWriter[String].contramap(_.toString)
+
+ implicit val pathReader: ConfigReader[Uri.Path] = ConfigReader.fromString(s =>
+ Try(Uri.Path(s)).toEither.left.map {
+ case err: IllegalUriException => CannotConvert(s, "Uri.Path", err.info.summary)
+ case err => ExceptionThrown(err)
+ }
+ )
+
+ implicit val pathWriter: ConfigWriter[Uri.Path] = ConfigWriter[String].contramap(_.toString)
+}
diff --git a/modules/pekko-http/src/test/scala/pureconfig/module/pekkohttp/PekkoHttpSuite.scala b/modules/pekko-http/src/test/scala/pureconfig/module/pekkohttp/PekkoHttpSuite.scala
new file mode 100644
index 000000000..e948fe32f
--- /dev/null
+++ b/modules/pekko-http/src/test/scala/pureconfig/module/pekkohttp/PekkoHttpSuite.scala
@@ -0,0 +1,68 @@
+package pureconfig.module.pekkohttp
+
+import com.typesafe.config.ConfigFactory
+import org.apache.pekko.http.scaladsl.model.Uri
+
+import pureconfig.error.{CannotConvert, ConfigReaderFailures, ConvertFailure}
+import pureconfig.syntax._
+import pureconfig.{BaseSuite, ConfigWriter}
+
+class PekkoHttpSuite extends BaseSuite {
+
+ val uri = Uri("https://pekko.apache.org/docs/pekko-http/current/index.html")
+ val serverConf = "https://pekko.apache.org/docs/pekko-http/current/index.html"
+ val config = configString(serverConf)
+
+ val pathString = "/docs/pekko-http/current/index.html"
+ val path = Uri.Path("/docs/pekko-http/current/index.html")
+
+ behavior of "PekkoHttp module"
+
+ it should "read the uri properly" in {
+ config.to[Uri].value shouldEqual uri
+ }
+
+ it should "throw proper CannotConvert error when the uri is invalid" in {
+ val config = configString("https://pekko.apache.org/docs/pekko-http/current folder with spaces/index.html")
+ val errors = ConfigReaderFailures(
+ ConvertFailure(
+ CannotConvert(
+ "https://pekko.apache.org/docs/pekko-http/current folder with spaces/index.html",
+ "Uri",
+ "Illegal URI reference: Invalid input ' ', expected pchar, '/', '?', '#' or 'EOI' (line 1, column 49)"
+ ),
+ stringConfigOrigin(1),
+ ""
+ )
+ )
+ config.to[Uri].left.value shouldEqual errors
+ }
+
+ it should "be able to write the Uri as config" in {
+ ConfigWriter[Uri].to(uri).unwrapped shouldEqual uri.toString
+ }
+
+ it should "read the path properly" in {
+ configString(pathString).to[Uri.Path].value shouldEqual path
+ }
+
+ it should "be able to write the Uri.Path as a config" in {
+ ConfigWriter[Uri.Path].to(path).unwrapped shouldEqual path.toString()
+ }
+
+ it should " throw proper CannotConvert error when the path is invalid" in {
+ val config = configString("/docs/pekko-http/%/index.html")
+ val errors = ConfigReaderFailures(
+ ConvertFailure(
+ CannotConvert(
+ "/docs/pekko-http/%/index.html",
+ "Uri.Path",
+ "Illegal percent-encoding at pos 0"
+ ),
+ stringConfigOrigin(1),
+ ""
+ )
+ )
+ config.to[Uri.Path].left.value shouldEqual errors
+ }
+}
diff --git a/modules/pekko/README.md b/modules/pekko/README.md
new file mode 100644
index 000000000..2c83649ee
--- /dev/null
+++ b/modules/pekko/README.md
@@ -0,0 +1,39 @@
+# Pekko module for PureConfig
+
+Adds support for selected [Pekko](https://pekko.apache.org/) classes to PureConfig.
+
+## Add pureconfig-pekko to your project
+
+In addition to [core PureConfig](https://github.com/pureconfig/pureconfig), you'll need:
+
+```scala
+libraryDependencies += "com.github.pureconfig" %% "pureconfig-pekko" % "0.17.7"
+```
+
+## Example
+
+To load a `Timeout` and an `ActorPath` into a configuration, we create a class to hold our configuration:
+
+```scala
+import org.apache.pekko.actor.ActorPath
+import org.apache.pekko.util.Timeout
+import com.typesafe.config.ConfigFactory.parseString
+import pureconfig._
+import pureconfig.generic.auto._
+import pureconfig.module.pekko._
+
+case class MyConfig(timeout: Timeout, actorPath: ActorPath)
+```
+
+We can read a `MyConfig` like:
+```scala
+val conf = parseString("""{
+ timeout: 5 seconds,
+ actor-path: "pekko://my-sys/user/service-a/worker1"
+}""")
+// conf: com.typesafe.config.Config = Config(SimpleConfigObject({"actor-path":"pekko://my-sys/user/service-a/worker1","timeout":"5 seconds"}))
+ConfigSource.fromConfig(conf).load[MyConfig]
+// res0: ConfigReader.Result[MyConfig] = Right(
+// MyConfig(Timeout(5 seconds), pekko://my-sys/user/service-a/worker1)
+// )
+```
diff --git a/modules/pekko/build.sbt b/modules/pekko/build.sbt
new file mode 100644
index 000000000..3170413e1
--- /dev/null
+++ b/modules/pekko/build.sbt
@@ -0,0 +1,5 @@
+import Dependencies.Version._
+
+crossScalaVersions := Seq(scala212, scala213)
+
+libraryDependencies ++= Seq("org.apache.pekko" %% "pekko-actor" % "1.0.3")
diff --git a/modules/pekko/docs/README.md b/modules/pekko/docs/README.md
new file mode 100644
index 000000000..bd03f3b38
--- /dev/null
+++ b/modules/pekko/docs/README.md
@@ -0,0 +1,35 @@
+# Pekko module for PureConfig
+
+Adds support for selected [Pekko](https://pekko.apache.org/) classes to PureConfig.
+
+## Add pureconfig-pekko to your project
+
+In addition to [core PureConfig](https://github.com/pureconfig/pureconfig), you'll need:
+
+```scala
+libraryDependencies += "com.github.pureconfig" %% "pureconfig-pekko" % "@VERSION@"
+```
+
+## Example
+
+To load a `Timeout` and an `ActorPath` into a configuration, we create a class to hold our configuration:
+
+```scala mdoc:silent
+import org.apache.pekko.actor.ActorPath
+import org.apache.pekko.util.Timeout
+import com.typesafe.config.ConfigFactory.parseString
+import pureconfig._
+import pureconfig.generic.auto._
+import pureconfig.module.pekko._
+
+case class MyConfig(timeout: Timeout, actorPath: ActorPath)
+```
+
+We can read a `MyConfig` like:
+```scala mdoc
+val conf = parseString("""{
+ timeout: 5 seconds,
+ actor-path: "pekko://my-sys/user/service-a/worker1"
+}""")
+ConfigSource.fromConfig(conf).load[MyConfig]
+```
diff --git a/modules/pekko/src/main/scala/pureconfig/module/pekko/package.scala b/modules/pekko/src/main/scala/pureconfig/module/pekko/package.scala
new file mode 100644
index 000000000..57448e62b
--- /dev/null
+++ b/modules/pekko/src/main/scala/pureconfig/module/pekko/package.scala
@@ -0,0 +1,20 @@
+package pureconfig.module
+
+import scala.concurrent.duration.FiniteDuration
+
+import org.apache.pekko.actor.ActorPath
+import org.apache.pekko.util.Timeout
+
+import pureconfig.ConfigConvert
+import pureconfig.ConfigConvert.viaString
+import pureconfig.ConvertHelpers.catchReadError
+
+/** ConfigConvert instances for Pekko value classes.
+ */
+package object pekko {
+ implicit val timeoutCC: ConfigConvert[Timeout] =
+ ConfigConvert[FiniteDuration].xmap(new Timeout(_), _.duration)
+
+ implicit val actorPathCC: ConfigConvert[ActorPath] =
+ viaString[ActorPath](catchReadError(ActorPath.fromString), _.toSerializationFormat)
+}
diff --git a/modules/pekko/src/test/scala/pureconfig/module/pekko/PekkoSuite.scala b/modules/pekko/src/test/scala/pureconfig/module/pekko/PekkoSuite.scala
new file mode 100644
index 000000000..42bfec3cf
--- /dev/null
+++ b/modules/pekko/src/test/scala/pureconfig/module/pekko/PekkoSuite.scala
@@ -0,0 +1,27 @@
+package pureconfig.module.pekko
+
+import scala.concurrent.duration._
+
+import org.apache.pekko.actor.ActorPath
+import org.apache.pekko.util.Timeout
+
+import pureconfig.BaseSuite
+import pureconfig.syntax._
+
+class PekkoSuite extends BaseSuite {
+
+ it should "be able to read a config with a Timeout" in {
+ val expected = 5.seconds
+ configValue(s"$expected").to[Timeout].value shouldEqual Timeout(expected)
+ }
+
+ it should "load a valid ActorPath" in {
+ val str = "pekko://my-sys/user/service-a/worker1"
+ val expected = ActorPath.fromString(str)
+ configString(str).to[ActorPath].value shouldEqual expected
+ }
+
+ it should "not load invalid ActorPath" in {
+ configString("this is this the path you're looking for").to[ActorPath] should be('left)
+ }
+}
diff --git a/modules/scala-xml/README.md b/modules/scala-xml/README.md
index 298961684..cebb56bbe 100644
--- a/modules/scala-xml/README.md
+++ b/modules/scala-xml/README.md
@@ -7,7 +7,7 @@ Adds support for XML via [Scala XML](https://github.com/scala/scala-xml) to Pure
In addition to [core pureconfig](https://github.com/pureconfig/pureconfig), you'll need:
```scala
-libraryDependencies += "com.github.pureconfig" %% "pureconfig-scala-xml" % "0.17.4"
+libraryDependencies += "com.github.pureconfig" %% "pureconfig-scala-xml" % "0.17.7"
```
## Example
diff --git a/modules/scala-xml/build.sbt b/modules/scala-xml/build.sbt
index 585d434fa..059ab4412 100644
--- a/modules/scala-xml/build.sbt
+++ b/modules/scala-xml/build.sbt
@@ -5,7 +5,7 @@ crossScalaVersions := Seq(scala212, scala213)
// Scala 2.12 depends on an old version of scala-xml
libraryDependencies ++= forScalaVersions {
- case (2, 12) => Seq("org.scala-lang.modules" %% "scala-xml" % "2.2.0")
+ case (2, 12) => Seq("org.scala-lang.modules" %% "scala-xml" % "2.3.0")
case _ => Seq("org.scala-lang.modules" %% "scala-xml" % "2.0.1")
}.value
diff --git a/modules/scalaz/README.md b/modules/scalaz/README.md
index 07ed78ed2..8f30cd6f0 100644
--- a/modules/scalaz/README.md
+++ b/modules/scalaz/README.md
@@ -9,7 +9,7 @@ classes.
In addition to [core pureconfig](https://github.com/pureconfig/pureconfig), you'll need:
```scala
-libraryDependencies += "com.github.pureconfig" %% "pureconfig-scalaz" % "0.17.4"
+libraryDependencies += "com.github.pureconfig" %% "pureconfig-scalaz" % "0.17.7"
```
## Example
diff --git a/modules/scalaz/build.sbt b/modules/scalaz/build.sbt
index c1ff1ee31..dfcda76e9 100644
--- a/modules/scalaz/build.sbt
+++ b/modules/scalaz/build.sbt
@@ -3,8 +3,8 @@ import Dependencies.Version._
crossScalaVersions := Seq(scala212, scala213)
libraryDependencies ++= Seq(
- "org.scalaz" %% "scalaz-core" % "7.3.7",
- "org.scalaz" %% "scalaz-scalacheck-binding" % "7.3.7" % "test"
+ "org.scalaz" %% "scalaz-core" % "7.3.8",
+ "org.scalaz" %% "scalaz-scalacheck-binding" % "7.3.8" % "test"
)
mdocScalacOptions += "-Ypartial-unification"
diff --git a/modules/spark/README.md b/modules/spark/README.md
index b3a7501ed..193082a43 100644
--- a/modules/spark/README.md
+++ b/modules/spark/README.md
@@ -7,7 +7,7 @@ Adds support for selected [Spark](http://spark.apache.org/) classes to PureConfi
In addition to [core PureConfig](https://github.com/pureconfig/pureconfig), you'll need:
```scala
-libraryDependencies += "com.github.pureconfig" %% "pureconfig-spark" % "0.17.4"
+libraryDependencies += "com.github.pureconfig" %% "pureconfig-spark" % "0.17.7"
```
Also, `pureconfig-spark` depends on `spark-sql` with `provided` scope.
diff --git a/modules/spark/build.sbt b/modules/spark/build.sbt
index 303658dac..61725ba86 100644
--- a/modules/spark/build.sbt
+++ b/modules/spark/build.sbt
@@ -2,5 +2,5 @@ import Dependencies.Version._
crossScalaVersions := Seq(scala212, scala213)
-libraryDependencies ++= Seq("org.apache.spark" %% "spark-sql" % "3.5.0" % "provided")
-mdocLibraryDependencies ++= Seq("org.apache.spark" %% "spark-sql" % "3.5.0")
+libraryDependencies ++= Seq("org.apache.spark" %% "spark-sql" % "3.5.1" % "provided")
+mdocLibraryDependencies ++= Seq("org.apache.spark" %% "spark-sql" % "3.5.1")
diff --git a/modules/squants/README.md b/modules/squants/README.md
index 0d5c20fdd..981068277 100644
--- a/modules/squants/README.md
+++ b/modules/squants/README.md
@@ -11,7 +11,7 @@ Automatically create a converter to read [Squants](http://www.squants.com/)'s be
In addition to [core pureconfig](https://github.com/pureconfig/pureconfig), you'll need:
```scala
-libraryDependencies += "com.github.pureconfig" %% "pureconfig-squants" % "0.17.4"
+libraryDependencies += "com.github.pureconfig" %% "pureconfig-squants" % "0.17.7"
```
## Example
diff --git a/modules/sttp/README.md b/modules/sttp/README.md
index e4b2f1e31..fc447aa7e 100644
--- a/modules/sttp/README.md
+++ b/modules/sttp/README.md
@@ -7,7 +7,7 @@ Adds support for [sttp](https://github.com/softwaremill/sttp). Currently support
In addition to [core PureConfig](https://github.com/pureconfig/pureconfig), you'll need:
```scala
-libraryDependencies += "com.github.pureconfig" %% "pureconfig-sttp" % "0.17.4"
+libraryDependencies += "com.github.pureconfig" %% "pureconfig-sttp" % "0.17.7"
```
## Example
diff --git a/modules/sttp/build.sbt b/modules/sttp/build.sbt
index a4feb346c..51ef2bdd4 100644
--- a/modules/sttp/build.sbt
+++ b/modules/sttp/build.sbt
@@ -3,7 +3,7 @@ import Dependencies.Version._
crossScalaVersions := Seq(scala212, scala213, scala3)
libraryDependencies ++= Seq(
- "com.softwaremill.sttp.model" %% "core" % "1.7.2"
+ "com.softwaremill.sttp.model" %% "core" % "1.7.11"
)
developers := List(Developer("bszwej", "Bartlomiej Szwej", "bszwej@gmail.com", url("https://github.com/bszwej")))
diff --git a/modules/yaml/README.md b/modules/yaml/README.md
index 6fb52734b..3907a46c7 100644
--- a/modules/yaml/README.md
+++ b/modules/yaml/README.md
@@ -8,7 +8,7 @@ of `ConfigReader`s and hints to read configurations to domain objects without bo
In addition to the [PureConfig core](https://github.com/pureconfig/pureconfig), you'll need:
```scala
-libraryDependencies += "com.github.pureconfig" %% "pureconfig-yaml" % "0.17.4"
+libraryDependencies += "com.github.pureconfig" %% "pureconfig-yaml" % "0.17.7"
```
## Example
diff --git a/modules/zio-config/README.md b/modules/zio-config/README.md
index 73506d3a1..64b1815f7 100644
--- a/modules/zio-config/README.md
+++ b/modules/zio-config/README.md
@@ -7,7 +7,7 @@ Support for providing instances of `ConfigCovert` given instances of [ZIO Config
In addition to the [PureConfig core](https://github.com/pureconfig/pureconfig), you'll need:
```scala
-libraryDependencies += "com.github.pureconfig" %% "pureconfig-zio-config" % "0.17.4"
+libraryDependencies += "com.github.pureconfig" %% "pureconfig-zio-config" % "0.17.7"
```
## Example
diff --git a/project/Dependencies.scala b/project/Dependencies.scala
index 777c035ee..8371ca5da 100644
--- a/project/Dependencies.scala
+++ b/project/Dependencies.scala
@@ -4,13 +4,13 @@ import Utilities._
object Dependencies {
object Version {
- val scala212 = "2.12.18"
- val scala213 = "2.13.12"
- val scala3 = "3.3.1"
+ val scala212 = "2.12.19"
+ val scala213 = "2.13.14"
+ val scala3 = "3.3.3"
- val scalaTest = "3.2.17"
- val scalaTestPlusScalaCheck = "3.2.17.0"
- val scalaCheck = "1.17.0"
+ val scalaTest = "3.2.19"
+ val scalaTestPlusScalaCheck = "3.2.18.0"
+ val scalaCheck = "1.18.0"
}
// testing libraries
diff --git a/project/build.properties b/project/build.properties
index 27430827b..081fdbbc7 100644
--- a/project/build.properties
+++ b/project/build.properties
@@ -1 +1 @@
-sbt.version=1.9.6
+sbt.version=1.10.0
diff --git a/project/plugins.sbt b/project/plugins.sbt
index e1be6d1e5..99670e9f9 100644
--- a/project/plugins.sbt
+++ b/project/plugins.sbt
@@ -1,15 +1,15 @@
-addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.11.1")
-addSbtPlugin("com.47deg" % "sbt-microsites" % "1.4.3")
-addSbtPlugin("com.github.sbt" % "sbt-release" % "1.1.0")
+addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.12.1")
+addSbtPlugin("com.47deg" % "sbt-microsites" % "1.4.4")
+addSbtPlugin("com.github.sbt" % "sbt-release" % "1.4.0")
addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.2.1")
-addSbtPlugin("io.spray" % "sbt-boilerplate" % "0.6.1")
-addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.3.7")
+addSbtPlugin("com.github.sbt" % "sbt-boilerplate" % "0.7.0")
+addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.5.3")
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2")
addSbtPlugin("org.scoverage" % "sbt-coveralls" % "1.3.11")
-addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.9")
-addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.9.21")
+addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.12")
+addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.10.0")
-libraryDependencies += "org.slf4j" % "slf4j-simple" % "2.0.9"
+libraryDependencies += "org.slf4j" % "slf4j-simple" % "2.0.13"
// taken from https://github.com/scala/bug/issues/12632
ThisBuild / libraryDependencySchemes ++= Seq(
diff --git a/tests/src/test/scala-3/pureconfig/CoproductReaderDerivationSuite.scala b/tests/src/test/scala-3/pureconfig/CoproductReaderDerivationSuite.scala
index d20468a14..ff1febff0 100644
--- a/tests/src/test/scala-3/pureconfig/CoproductReaderDerivationSuite.scala
+++ b/tests/src/test/scala-3/pureconfig/CoproductReaderDerivationSuite.scala
@@ -6,8 +6,6 @@ import com.typesafe.config.ConfigFactory
import pureconfig._
import pureconfig.error._
-import pureconfig.generic._
-import pureconfig.generic.derivation.default.derived
enum AnimalConfig derives ConfigReader {
case DogConfig(age: Int)
diff --git a/tests/src/test/scala-3/pureconfig/EnumerationConvertDerivationSuite.scala b/tests/src/test/scala-3/pureconfig/EnumerationConvertDerivationSuite.scala
new file mode 100644
index 000000000..06dcceb4b
--- /dev/null
+++ b/tests/src/test/scala-3/pureconfig/EnumerationConvertDerivationSuite.scala
@@ -0,0 +1,124 @@
+package pureconfig
+
+import scala.compiletime.testing.{typeCheckErrors, typeChecks}
+import scala.deriving.Mirror
+import scala.language.higherKinds
+
+import com.typesafe.config.{ConfigFactory, ConfigValueFactory, ConfigValueType}
+
+import pureconfig._
+import pureconfig.error.{CannotConvert, WrongType}
+import pureconfig.generic.derivation._
+
+class EnumConvertDerivationSuite extends BaseSuite {
+ behavior of "EnumConfigConvert"
+
+ it should "not allow derivation of parametrized enums" in {
+ """
+ enum Color derives EnumConfigReader {
+ case RainyBlue[A](a: A)
+ case SunnyYellow
+ }
+ """ shouldNot compile
+
+ """
+ enum Color derives EnumConfigWriter {
+ case RainyBlue[A](a: A)
+ case SunnyYellow
+ }
+ """ shouldNot compile
+
+ """
+ enum Color derives EnumConfigConvert {
+ case RainyBlue[A](a: A)
+ case SunnyYellow
+ }
+ """ shouldNot compile
+ }
+
+ it should "provide methods to derive readers for enumerations encoded as enums or sealed traits" in {
+ enum Color derives EnumConfigReader {
+ case RainyBlue, SunnyYellow
+ }
+
+ sealed trait Color2 derives EnumConfigReader
+ object Color2 {
+ case object RainyBlue extends Color2
+ case object SunnyYellow extends Color2
+ }
+
+ ConfigReader[Color].from(ConfigValueFactory.fromAnyRef("rainy-blue")) shouldBe Right(Color.RainyBlue)
+ ConfigReader[Color].from(ConfigValueFactory.fromAnyRef("sunny-yellow")) shouldBe Right(Color.SunnyYellow)
+
+ ConfigReader[Color2].from(ConfigValueFactory.fromAnyRef("rainy-blue")) shouldBe Right(Color2.RainyBlue)
+ ConfigReader[Color2].from(ConfigValueFactory.fromAnyRef("sunny-yellow")) shouldBe Right(Color2.SunnyYellow)
+
+ val unknownValue = ConfigValueFactory.fromAnyRef("blue")
+
+ ConfigReader[Color].from(unknownValue) should failWith(
+ CannotConvert("blue", "Color", "The value is not a valid enum option."),
+ "",
+ emptyConfigOrigin
+ )
+
+ ConfigReader[Color2].from(unknownValue) should failWith(
+ CannotConvert("blue", "Color2", "The value is not a valid enum option."),
+ "",
+ emptyConfigOrigin
+ )
+
+ val conf = ConfigFactory.parseString("{ type: person, name: John, surname: Doe }")
+
+ ConfigReader[Color].from(conf.root()) should failWith(
+ WrongType(ConfigValueType.OBJECT, Set(ConfigValueType.STRING)),
+ "",
+ stringConfigOrigin(1)
+ )
+
+ ConfigReader[Color2].from(conf.root()) should failWith(
+ WrongType(ConfigValueType.OBJECT, Set(ConfigValueType.STRING)),
+ "",
+ stringConfigOrigin(1)
+ )
+ }
+
+ it should "provide methods to derive writers for enumerations encoded as enums or sealed traits" in {
+ enum Color derives EnumConfigWriter {
+ case RainyBlue, SunnyYellow
+ }
+
+ sealed trait Color2 derives EnumConfigWriter
+ object Color2 {
+ case object RainyBlue extends Color2
+ case object SunnyYellow extends Color2
+ }
+
+ ConfigWriter[Color].to(Color.RainyBlue) shouldEqual ConfigValueFactory.fromAnyRef("rainy-blue")
+ ConfigWriter[Color].to(Color.SunnyYellow) shouldEqual ConfigValueFactory.fromAnyRef("sunny-yellow")
+
+ ConfigWriter[Color2].to(Color2.RainyBlue) shouldEqual ConfigValueFactory.fromAnyRef("rainy-blue")
+ ConfigWriter[Color2].to(Color2.SunnyYellow) shouldEqual ConfigValueFactory.fromAnyRef("sunny-yellow")
+ }
+
+ it should "provide methods to derive full converters for enumerations encoded as enums or sealed traits" in {
+ enum Color derives EnumConfigConvert {
+ case RainyBlue, SunnyYellow
+ }
+
+ sealed trait Color2 derives EnumConfigConvert
+ object Color2 {
+ case object RainyBlue extends Color2
+ case object SunnyYellow extends Color2
+ }
+
+ ConfigConvert[Color].from(ConfigValueFactory.fromAnyRef("rainy-blue")) shouldBe Right(Color.RainyBlue)
+ ConfigConvert[Color].from(ConfigValueFactory.fromAnyRef("sunny-yellow")) shouldBe Right(Color.SunnyYellow)
+ ConfigConvert[Color].to(Color.RainyBlue) shouldEqual ConfigValueFactory.fromAnyRef("rainy-blue")
+ ConfigConvert[Color].to(Color.SunnyYellow) shouldEqual ConfigValueFactory.fromAnyRef("sunny-yellow")
+
+ ConfigConvert[Color2].from(ConfigValueFactory.fromAnyRef("rainy-blue")) shouldBe Right(Color2.RainyBlue)
+ ConfigConvert[Color2].from(ConfigValueFactory.fromAnyRef("sunny-yellow")) shouldBe Right(Color2.SunnyYellow)
+ ConfigConvert[Color2].to(Color2.RainyBlue) shouldEqual ConfigValueFactory.fromAnyRef("rainy-blue")
+ ConfigConvert[Color2].to(Color2.SunnyYellow) shouldEqual ConfigValueFactory.fromAnyRef("sunny-yellow")
+ }
+}
diff --git a/tests/src/test/scala-3/pureconfig/EnumerationReaderDerivationSuite.scala b/tests/src/test/scala-3/pureconfig/EnumerationReaderDerivationSuite.scala
deleted file mode 100644
index ca3fa68ca..000000000
--- a/tests/src/test/scala-3/pureconfig/EnumerationReaderDerivationSuite.scala
+++ /dev/null
@@ -1,41 +0,0 @@
-package pureconfig
-
-import scala.compiletime.testing.{typeCheckErrors, typeChecks}
-import scala.deriving.Mirror
-import scala.language.higherKinds
-
-import com.typesafe.config.{ConfigFactory, ConfigValueFactory, ConfigValueType}
-
-import pureconfig._
-import pureconfig.error.{CannotConvert, WrongType}
-import pureconfig.generic.derivation.{EnumConfigReader, EnumConfigReaderDerivation}
-
-enum Color derives EnumConfigReader {
- case RainyBlue, SunnyYellow
-}
-
-class EnumerationReaderDerivationSuite extends BaseSuite {
-
- import Color._
-
- behavior of "EnumConfigReader"
-
- it should "provide methods to derive readers for enumerations encoded as sealed traits or enums" in {
- ConfigReader[Color].from(ConfigValueFactory.fromAnyRef("rainy-blue")) shouldBe Right(RainyBlue)
- ConfigReader[Color].from(ConfigValueFactory.fromAnyRef("sunny-yellow")) shouldBe Right(SunnyYellow)
-
- val unknownValue = ConfigValueFactory.fromAnyRef("blue")
- ConfigReader[Color].from(unknownValue) should failWith(
- CannotConvert("blue", "Color", "The value is not a valid enum option."),
- "",
- emptyConfigOrigin
- )
-
- val conf = ConfigFactory.parseString("{ type: person, name: John, surname: Doe }")
- ConfigReader[Color].from(conf.root()) should failWith(
- WrongType(ConfigValueType.OBJECT, Set(ConfigValueType.STRING)),
- "",
- stringConfigOrigin(1)
- )
- }
-}
diff --git a/tests/src/test/scala-3/pureconfig/ProductReaderDerivationSuite.scala b/tests/src/test/scala-3/pureconfig/ProductReaderDerivationSuite.scala
index 605efc9d0..a39e37c1e 100644
--- a/tests/src/test/scala-3/pureconfig/ProductReaderDerivationSuite.scala
+++ b/tests/src/test/scala-3/pureconfig/ProductReaderDerivationSuite.scala
@@ -1,5 +1,4 @@
package pureconfig
-package generic
import scala.collection.JavaConverters.given
import scala.language.higherKinds
@@ -10,7 +9,6 @@ import org.scalacheck.Arbitrary
import pureconfig.ConfigConvert.catchReadError
import pureconfig._
import pureconfig.error.{KeyNotFound, WrongSizeList, WrongType}
-import pureconfig.generic.derivation.default.derived
class ProductReaderDerivationSuite extends BaseSuite {
diff --git a/version.sbt b/version.sbt
index 32916575d..a81d7f279 100644
--- a/version.sbt
+++ b/version.sbt
@@ -1 +1 @@
-ThisBuild / version := "0.17.5-SNAPSHOT"
+ThisBuild / version := "0.17.8-SNAPSHOT"