Skip to content

Commit

Permalink
Merge branch 'master' into update/scala-xml-2.0.0
Browse files Browse the repository at this point in the history
  • Loading branch information
jcazevedo committed Jun 20, 2021
2 parents 2941b11 + a8229aa commit ecc6ee2
Show file tree
Hide file tree
Showing 126 changed files with 2,037 additions and 793 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ jobs:
scala: ['2.12', '2.13', '3.0']
include:
- scala: '2.12'
scala-version: 2.12.13
scala-version: 2.12.14
- scala: '2.13'
scala-version: 2.13.5
scala-version: 2.13.6
- scala: '3.0'
scala-version: 3.0.0-RC2
scala-version: 3.0.0

steps:
- name: Checkout repository
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,6 @@ target/
.bloop/
.metals/
metals.sbt

# vscode specific
.vscode
2 changes: 1 addition & 1 deletion .scalafmt.conf
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
version = 3.0.0-RC1
version = 3.0.0-RC5

align.preset = none
maxColumn = 120
Expand Down
17 changes: 14 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
### 0.15.0 (Apr 18, 2020)
### 0.16.0 (Jun 9, 2021)

Support for Scala 3.0 was introduced.

- New features
- Added beta support for Scala 3 type class derivation. See
[documentation](https://pureconfig.github.io/v0.16.0/docs/scala-3-derivation.html) for details.

### 0.15.0 (Apr 18, 2021)

- Breaking changes
- The `Derivation` type class was removed. If you were relying on it, e.g. by using `Derivation.Successful` directly or using `Derivation` values in function signatures, you can simply use `ConfigReader` and `ConfigWriter` directly. The docs were updated to recommend [splain](https://github.com/tek/splain) as a tool to debug implicit not found errors.
- The `Derivation` type class was removed. If you were relying on it, e.g. by using `Derivation.Successful` directly
or using `Derivation` values in function signatures, you can simply use `ConfigReader` and `ConfigWriter` directly.
The docs were updated to recommend [splain](https://github.com/tek/splain) as a tool to debug implicit not found
errors.

### 0.14.1 (Feb 28, 2020)
### 0.14.1 (Feb 28, 2021)

Support for Scala 2.11 was dropped and support for Scala 3.0 (RC1) was introduced.

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ 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.15.0"
libraryDependencies += "com.github.pureconfig" %% "pureconfig" % "0.16.0"
```

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).
Expand Down
1 change: 1 addition & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ lazy val akka = module(project) in file("modules/akka")
lazy val `akka-http` = module(project) in file("modules/akka-http")
lazy val cats = module(project) in file("modules/cats")
lazy val `cats-effect` = module(project) in file("modules/cats-effect")
lazy val `cats-effect2` = module(project) in file("modules/cats-effect2")
lazy val circe = module(project) in file("modules/circe")
lazy val cron4s = module(project) in file("modules/cron4s")
lazy val enum = module(project) in file("modules/enum")
Expand Down
6 changes: 4 additions & 2 deletions core/src/main/scala-2.12-/pureconfig/FactoryCompat.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import scala.collection.mutable
/** A compatibility layer for creating `CanBuildFrom`-like generic methods that work both on Scala 2.13 and pre-2.13
* versions.
*
* @tparam A the type of elements that get added to the builder
* @tparam C the type of collection that it produces
* @tparam A
* the type of elements that get added to the builder
* @tparam C
* the type of collection that it produces
*/
trait FactoryCompat[-A, +C] {
def newBuilder(): mutable.Builder[A, C]
Expand Down
6 changes: 4 additions & 2 deletions core/src/main/scala-2.13+/pureconfig/FactoryCompat.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import scala.collection.{Factory, mutable}
/** A compatibility layer for creating `CanBuildFrom`-like generic methods that work both on Scala 2.13 and pre-2.13
* versions.
*
* @tparam A the type of elements that get added to the builder
* @tparam C the type of collection that it produces
* @tparam A
* the type of elements that get added to the builder
* @tparam C
* the type of collection that it produces
*/
trait FactoryCompat[-A, +C] {
def newBuilder(): mutable.Builder[A, C]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package pureconfig
package generic
package derivation

import scala.deriving.Mirror

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
}
}
}

object ConfigReaderDerivation {
object Default
extends ConfigReaderDerivation
with CoproductConfigReaderDerivation(ConfigFieldMapping(PascalCase, KebabCase), "type")
with ProductConfigReaderDerivation(ConfigFieldMapping(CamelCase, KebabCase))
}

val default = ConfigReaderDerivation.Default
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package pureconfig
package generic
package derivation

import scala.compiletime.{constValue, erasedValue, summonFrom, summonInline}
import scala.deriving.Mirror

import pureconfig.error.{CannotConvert, ConfigReaderFailures}
import pureconfig.generic.derivation.ConfigReaderDerivation
import pureconfig.generic.derivation.WidenType.widen

trait CoproductConfigReaderDerivation(fieldMapping: ConfigFieldMapping, optionField: String) {
self: ConfigReaderDerivation =>
inline def derivedSum[A](using m: Mirror.SumOf[A]): ConfigReader[A] =
new ConfigReader[A] {
def from(cur: ConfigCursor): ConfigReader.Result[A] =
for {
objCur <- cur.asObjectCursor
optCur <- objCur.atKey(optionField)
option <- optCur.asString
result <-
readers.get(option) match {
case Some(reader) => reader.from(cur)
case None =>
Left(
ConfigReaderFailures(
optCur.failureFor(
CannotConvert(option, constValue[m.MirroredLabel], "The value is not a valid option.")
)
)
)
}
} yield result

val readers =
Labels
.transformed[m.MirroredElemLabels](fieldMapping)
.zip(deriveForSubtypes[m.MirroredElemTypes, A])
.toMap
}

inline def deriveForSubtypes[T <: Tuple, A]: List[ConfigReader[A]] =
inline erasedValue[T] match {
case _: (h *: t) => deriveForSubtype[h, A] :: deriveForSubtypes[t, A]
case _: EmptyTuple => Nil
}

inline def deriveForSubtype[A0, A]: ConfigReader[A] =
summonFrom {
case reader: ConfigReader[A0] =>
reader.map(widen[A0, A](_))

case given Mirror.Of[A0] =>
ConfigReader.derived[A0].map(widen[A0, A](_))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package pureconfig
package generic
package derivation

import scala.compiletime.{constValue, erasedValue, error, summonInline}
import scala.deriving.Mirror

import pureconfig.error.{CannotConvert, ConfigReaderFailures}
import pureconfig.generic.derivation.WidenType.widen

type EnumConfigReader[A] = EnumConfigReaderDerivation.Default.EnumConfigReader[A]

trait EnumConfigReaderDerivation(transformName: String => String) {

trait EnumConfigReader[A] extends ConfigReader[A]

object EnumConfigReader {
inline def derived[A](using m: Mirror.SumOf[A]): EnumConfigReader[A] = {
val values = summonCases[m.MirroredElemTypes, A]
new EnumConfigReader[A] {
def from(cur: ConfigCursor): ConfigReader.Result[A] =
for {
value <- cur.asString
result <-
ordinal[A](transformName, value) match {
case Some(ord) => Right(values(ord))
case None =>
for {
v <- cur.asConfigValue
result <-
cur.failed(
CannotConvert(value, constValue[m.MirroredLabel], "The value is not a valid enum option.")
)
} yield result
}
} yield result
}
}

inline def summonCases[T <: Tuple, A]: List[A] =
inline erasedValue[T] match {
case _: (h *: t) =>
(inline summonInline[Mirror.Of[h]] match {
case m: Mirror.Singleton =>
widen[m.MirroredMonoType, A](m.fromProduct(EmptyTuple)) :: summonCases[t, A]
case _ => error("Enums cannot include parameterized cases.")
})

case _: EmptyTuple => Nil
}

inline def ordinal[A](
inline transformName: String => String,
inline value: String
)(using m: Mirror.SumOf[A]) = {
val ord = Labels.transformed[m.MirroredElemLabels](transformName).indexOf(value)
Option.when(ord >= 0)(ord)
}
}
}

object EnumConfigReaderDerivation {
object Default extends EnumConfigReaderDerivation(ConfigFieldMapping(PascalCase, KebabCase))
}
17 changes: 17 additions & 0 deletions core/src/main/scala-3/pureconfig/generic/derivation/Labels.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package pureconfig.generic
package derivation

import scala.compiletime.{constValue, erasedValue, summonFrom, summonInline}
import scala.deriving.Mirror

object Labels {
inline def transformed[T <: Tuple](
inline transform: String => String
): List[String] =
inline erasedValue[T] match {
case _: (h *: t) =>
transform(constValue[h & String]) :: transformed[t](transform)

case _: EmptyTuple => Nil
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package pureconfig
package generic
package derivation

import scala.compiletime.{constValue, constValueTuple, erasedValue, summonFrom, summonInline}
import scala.compiletime.ops.int._
import scala.deriving.Mirror
import scala.util.chaining.*

import pureconfig.error.{ConfigReaderFailures, ConvertFailure, KeyNotFound, UnknownKey, WrongSizeList}
import pureconfig.generic.derivation.WidenType.widen

trait ProductConfigReaderDerivation(fieldMapping: ConfigFieldMapping) { self: ConfigReaderDerivation =>
inline def derivedProduct[A](using m: Mirror.ProductOf[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, Nil)
} 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] =
for {
objCur <- cur.asObjectCursor
result <-
Labels
.transformed[m.MirroredElemLabels](fieldMapping)
.pipe(labels =>
readTuple[m.MirroredElemTypes, 0](
labels.map(objCur.atKeyOrUndefined(_)),
labels.map(KeyNotFound.forKeys(_, objCur.keys))
)
)
} yield m.fromProduct(result)
}
}

inline def readTuple[T <: Tuple, N <: Int](
cursors: List[ConfigCursor],
keyNotFound: List[KeyNotFound]
): Either[ConfigReaderFailures, T] =
inline erasedValue[T] match {
case _: (h *: t) =>
val reader = summonConfigReader[h]
val cursor = cursors(constValue[N])
val h =
(reader.isInstanceOf[ReadsMissingKeys], cursor.isUndefined) match {
case (true, true) | (_, false) => reader.from(cursor)
case (false, true) => cursor.failed(keyNotFound(constValue[N]))
}

h -> readTuple[t, N + 1](cursors, keyNotFound) match {
case (Right(h), Right(t)) => Right(widen[h *: t, T](h *: t))
case (Left(h), Left(t)) => Left(h ++ t)
case (_, Left(failures)) => Left(failures)
case (Left(failures), _) => Left(failures)
}

case _: EmptyTuple =>
Right(widen[EmptyTuple, T](EmptyTuple))
}

inline def summonConfigReader[A] =
summonFrom {
case reader: ConfigReader[A] => reader
case given Mirror.Of[A] => ConfigReader.derived[A]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package pureconfig.generic.derivation

object WidenType {
inline def widen[A, B](a: A): A & B =
inline a match {
case b: B => b
}
}
4 changes: 2 additions & 2 deletions core/src/main/scala/pureconfig/CollectionReaders.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import scala.reflect.ClassTag
/** A marker trait signaling that a `ConfigReader` accepts missing (undefined) values.
*
* The standard behavior of `ConfigReader`s that expect required keys in config objects is to return a `KeyNotFound`
* failure when one or more of them are missing. Mixing in this trait into the key's `ConfigReader` signals that if
* a value is missing for the key, the `ConfigReader` can be called with a cursor in the "undefined" state.
* failure when one or more of them are missing. Mixing in this trait into the key's `ConfigReader` signals that if a
* value is missing for the key, the `ConfigReader` can be called with a cursor in the "undefined" state.
*/
trait ReadsMissingKeys { this: ConfigReader[_] => }

Expand Down
4 changes: 2 additions & 2 deletions core/src/main/scala/pureconfig/CollectionWriters.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import com.typesafe.config._

/** A trait signaling that a `ConfigWriter` can write missing (undefined) values.
*
* `ConfigWriter`s always produce a valid `ConfigValue` with their `to` method. This trait adds an extra `toOpt`
* method that parent writers can use in order to decide whether or not they should write a value using this writer.
* `ConfigWriter`s always produce a valid `ConfigValue` with their `to` method. This trait adds an extra `toOpt` method
* that parent writers can use in order to decide whether or not they should write a value using this writer.
*/
trait WritesMissingKeys[A] { this: ConfigWriter[A] =>
def toOpt(a: A): Option[ConfigValue]
Expand Down

0 comments on commit ecc6ee2

Please sign in to comment.