Skip to content

Commit

Permalink
Merge a9ae9d2 into 01e16ff
Browse files Browse the repository at this point in the history
  • Loading branch information
ruippeixotog committed Sep 29, 2018
2 parents 01e16ff + a9ae9d2 commit b720948
Show file tree
Hide file tree
Showing 7 changed files with 40 additions and 33 deletions.
24 changes: 12 additions & 12 deletions core/src/main/scala/pureconfig/CollectionReaders.scala
Expand Up @@ -8,26 +8,26 @@ import pureconfig.ConvertHelpers._
import pureconfig.error._

/**
* The default behavior of ConfigReaders that are implicitly derived in PureConfig is to raise a
* KeyNotFoundException when a required key is missing. Mixing in this trait to a ConfigReader
* allows customizing this behavior. When a key is missing, but the ConfigReader of the given
* type extends this trait, the `from` method of the ConfigReader is called with null.
* 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 a
* value is missing for the key, the `ConfigReader` can be called with an cursor in the "undefined" state.
*/
trait AllowMissingKey { self: ConfigReader[_] => }
trait ReadsMissingKeys { this: ConfigReader[_] => }

/**
* Trait containing `ConfigReader` instances for collection types.
*/
trait CollectionReaders {

implicit def optionReader[T](implicit conv: Derivation[ConfigReader[T]]) = new OptionConfigReader[T]

class OptionConfigReader[T](implicit conv: Derivation[ConfigReader[T]]) extends ConfigReader[Option[T]] with AllowMissingKey {
override def from(cur: ConfigCursor): Either[ConfigReaderFailures, Option[T]] = {
if (cur.isUndefined || cur.isNull) Right(None)
else conv.value.from(cur).right.map(Some(_))
implicit def optionReader[T](implicit conv: Derivation[ConfigReader[T]]): ConfigReader[Option[T]] =
new ConfigReader[Option[T]] with ReadsMissingKeys {
override def from(cur: ConfigCursor): Either[ConfigReaderFailures, Option[T]] = {
if (cur.isUndefined || cur.isNull) Right(None)
else conv.value.from(cur).right.map(Some(_))
}
}
}

implicit def traversableReader[T, F[T] <: TraversableOnce[T]](
implicit
Expand Down
33 changes: 20 additions & 13 deletions core/src/main/scala/pureconfig/CollectionWriters.scala
Expand Up @@ -5,12 +5,30 @@ import scala.language.higherKinds

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

/**
* Trait containing `ConfigWriter` instances for collection types.
*/
trait CollectionWriters {

implicit def optionWriter[T](implicit conv: Derivation[ConfigWriter[T]]) = new CollectionWriters.OptionConfigWriter[T]
implicit def optionWriter[T](implicit conv: Derivation[ConfigWriter[T]]): ConfigWriter[Option[T]] =
new ConfigWriter[Option[T]] with WritesMissingKeys[Option[T]] {
override def to(t: Option[T]): ConfigValue = t match {
case Some(v) => conv.value.to(v)
case None => ConfigValueFactory.fromAnyRef(null)
}

def toOpt(t: Option[T]): Option[ConfigValue] = t.map(conv.value.to)
}

implicit def traversableWriter[T, F[T] <: TraversableOnce[T]](
implicit
Expand All @@ -28,15 +46,4 @@ trait CollectionWriters {
}
}

object CollectionWriters extends CollectionWriters {

// TODO: change this to an `AllowMissingKey`-like trait for better extensibility
class OptionConfigWriter[T](implicit conv: Derivation[ConfigWriter[T]]) extends ConfigWriter[Option[T]] {
override def to(t: Option[T]): ConfigValue = t match {
case Some(v) => conv.value.to(v)
case None => ConfigValueFactory.fromAnyRef(null)
}

def toOption(t: Option[T]): Option[ConfigValue] = t.map(conv.value.to)
}
}
object CollectionWriters extends CollectionWriters
2 changes: 1 addition & 1 deletion core/src/main/scala/pureconfig/package.scala
Expand Up @@ -40,7 +40,7 @@ package object pureconfig {
// loads a value from a config in a given namespace. All `loadConfig` methods _must_ use this method to get correct
// namespace handling, both in the values to load and in the error messages.
private[this] def loadValue[A](conf: TypesafeConfig, namespace: String)(implicit reader: Derivation[ConfigReader[A]]): Either[ConfigReaderFailures, A] = {
getValue(conf, namespace, reader.value.isInstanceOf[AllowMissingKey]).right.flatMap(reader.value.from)
getValue(conf, namespace, reader.value.isInstanceOf[ReadsMissingKeys]).right.flatMap(reader.value.from)
}

/**
Expand Down
Expand Up @@ -175,14 +175,14 @@ loadConfig[Foo](ConfigFactory.empty)
loadConfig[FooOpt](ConfigFactory.empty)
```

However, if you want to allow your custom `ConfigReader`s to handle missing keys, you can extend the `AllowMissingKey`
trait. For `ConfigReader`s extending `AllowMissingKey`, a missing key will issue a call to the `from` method of the
However, if you want to allow your custom `ConfigReader`s to handle missing keys, you can extend the `ReadsMissingKeys`
trait. For `ConfigReader`s extending `ReadsMissingKeys`, a missing key will issue a call to the `from` method of the
available `ConfigReader` for that type with a [cursor](config-cursors.html) to an undefined value.

Under this setup:

```tut:silent
implicit val maybeIntReader = new ConfigReader[Int] with AllowMissingKey {
implicit val maybeIntReader = new ConfigReader[Int] with ReadsMissingKeys {
override def from(cur: ConfigCursor) =
if (cur.isUndefined) Right(42) else ConfigReader[Int].from(cur)
}
Expand Down
Expand Up @@ -56,7 +56,7 @@ object MapShapedReader {
case keyCur if keyCur.isUndefined =>
default.head match {
case Some(defaultValue) if hint.useDefaultArgs => Right(defaultValue)
case _ if headReader.isInstanceOf[AllowMissingKey] => headReader.from(keyCur)
case _ if headReader.isInstanceOf[ReadsMissingKeys] => headReader.from(keyCur)
case _ => cur.failed(KeyNotFound.forKeys(keyStr, cur.keys))
}
case keyCur => headReader.from(keyCur)
Expand Down
Expand Up @@ -34,8 +34,8 @@ object MapShapedWriter {
val rem = tConfigWriter.value.to(t.tail)
// TODO check that all keys are unique
vFieldConvert.value.value match {
case f: CollectionWriters.OptionConfigWriter[_] =>
f.toOption(t.head) match {
case f: WritesMissingKeys[V @unchecked] =>
f.toOpt(t.head) match {
case Some(v) =>
rem.asInstanceOf[ConfigObject].withValue(keyStr, v)
case None =>
Expand Down
Expand Up @@ -73,7 +73,7 @@ class ProductConvertersSuite extends BaseSuite {
val conf = ConfigFactory.parseString("""{ a: 1 }""").root()
ConfigConvert[Conf].from(conf) should failWith(KeyNotFound("b"))

implicit val defaultInt = new ConfigConvert[Int] with AllowMissingKey {
implicit val defaultInt = new ConfigConvert[Int] with ReadsMissingKeys {
def from(cur: ConfigCursor) =
if (cur.isUndefined) Right(42) else {
val s = cur.value.render(ConfigRenderOptions.concise)
Expand Down

0 comments on commit b720948

Please sign in to comment.