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 PureConfig -[![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"