diff --git a/core/src/main/scala/pureconfig/BasicReaders.scala b/core/src/main/scala/pureconfig/BasicReaders.scala index d7ffe1160..251a0d8a8 100644 --- a/core/src/main/scala/pureconfig/BasicReaders.scala +++ b/core/src/main/scala/pureconfig/BasicReaders.scala @@ -24,22 +24,39 @@ import pureconfig.error._ trait PrimitiveReaders { implicit val stringConfigReader = ConfigReader.fromCursor(_.asString) + implicit val charConfigReader = ConfigReader.fromNonEmptyString[Char](s => s.size match { case 1 => Right(s.charAt(0)) case len => Left(WrongSizeString(1, len)) }) + implicit val booleanConfigReader = ConfigReader.fromCursor(_.asBoolean) - implicit val doubleConfigReader = ConfigReader.fromNonEmptyString[Double](catchReadError({ - case v if v.last == '%' => v.dropRight(1).toDouble / 100d - case v => v.toDouble - })) - implicit val floatConfigReader = ConfigReader.fromNonEmptyString[Float](catchReadError({ - case v if v.last == '%' => v.dropRight(1).toFloat / 100f - case v => v.toFloat - })) + + implicit val doubleConfigReader = ConfigReader.fromCursor({ cur => + val asStringReader = catchReadError({ + case v if v.last == '%' => v.dropRight(1).toDouble / 100f + case v => v.toDouble + }) + + cur.asString.right.flatMap(s => cur.scopeFailure(asStringReader(s))) + .left.flatMap(_ => cur.asDouble) + }) + + implicit val floatConfigReader = ConfigReader.fromCursor({ cur => + val asStringReader = catchReadError({ + case v if v.last == '%' => v.dropRight(1).toFloat / 100f + case v => v.toFloat + }) + + cur.asString.right.flatMap(s => cur.scopeFailure(asStringReader(s))) + .left.flatMap(_ => cur.asFloat) + }) + implicit val intConfigReader = ConfigReader.fromCursor(_.asInt) + implicit val longConfigReader = ConfigReader.fromCursor(_.asLong) + implicit val shortConfigReader = ConfigReader.fromCursor(_.asShort) } diff --git a/core/src/main/scala/pureconfig/ConfigConvert.scala b/core/src/main/scala/pureconfig/ConfigConvert.scala index 85737380b..9464e40ae 100644 --- a/core/src/main/scala/pureconfig/ConfigConvert.scala +++ b/core/src/main/scala/pureconfig/ConfigConvert.scala @@ -9,9 +9,8 @@ package pureconfig import scala.reflect.ClassTag import scala.util.Try -import com.typesafe.config.{ ConfigValue, ConfigValueFactory } import pureconfig.ConvertHelpers._ -import pureconfig.error.{ ConfigReaderFailures, FailureReason } +import pureconfig.error.FailureReason /** * Trait for objects capable of reading and writing objects of a given type from and to `ConfigValues`. @@ -49,10 +48,10 @@ object ConfigConvert extends ConvertHelpers { def to(t: T) = writer.value.to(t) } - def viaString[T](fromF: String => Either[FailureReason, T], toF: T => String): ConfigConvert[T] = new ConfigConvert[T] { - override def from(cur: ConfigCursor): Either[ConfigReaderFailures, T] = stringToEitherConvert(fromF)(cur) - override def to(t: T): ConfigValue = ConfigValueFactory.fromAnyRef(toF(t)) - } + def viaString[T](fromF: String => Either[FailureReason, T], toF: T => String): ConfigConvert[T] = + fromReaderAndWriter( + Derivation.Successful(ConfigReader.fromString(fromF)), + Derivation.Successful(ConfigWriter.toString(toF))) def viaStringTry[T: ClassTag](fromF: String => Try[T], toF: T => String): ConfigConvert[T] = { viaString[T](tryF(fromF), toF) diff --git a/core/src/main/scala/pureconfig/ConfigReader.scala b/core/src/main/scala/pureconfig/ConfigReader.scala index 287541682..434bc37cb 100644 --- a/core/src/main/scala/pureconfig/ConfigReader.scala +++ b/core/src/main/scala/pureconfig/ConfigReader.scala @@ -143,9 +143,8 @@ object ConfigReader extends BasicReaders with CollectionReaders with ExportedRea def fromFunction[A](fromF: ConfigValue => Either[ConfigReaderFailures, A]) = fromCursor(fromF.compose(_.value)) - def fromString[A](fromF: String => Either[FailureReason, A]): ConfigReader[A] = new ConfigReader[A] { - override def from(cur: ConfigCursor): Either[ConfigReaderFailures, A] = stringToEitherConvert(fromF)(cur) - } + def fromString[A](fromF: String => Either[FailureReason, A]): ConfigReader[A] = + ConfigReader.fromCursor(_.asString).emap(fromF) def fromStringTry[A](fromF: String => Try[A])(implicit ct: ClassTag[A]): ConfigReader[A] = { fromString[A](tryF(fromF)) diff --git a/core/src/main/scala/pureconfig/ConvertHelpers.scala b/core/src/main/scala/pureconfig/ConvertHelpers.scala index e627405d3..c45053d7a 100644 --- a/core/src/main/scala/pureconfig/ConvertHelpers.scala +++ b/core/src/main/scala/pureconfig/ConvertHelpers.scala @@ -1,10 +1,9 @@ package pureconfig import scala.reflect.ClassTag -import scala.util.{ Failure, Success, Try } import scala.util.control.NonFatal +import scala.util.{ Failure, Success, Try } -import com.typesafe.config._ import pureconfig.error._ /** @@ -30,18 +29,6 @@ trait ConvertHelpers { case Failure(e) => Left(ExceptionThrown(e)) } - private[pureconfig] def stringToEitherConvert[T](fromF: String => Either[FailureReason, T]): ConfigCursor => Either[ConfigReaderFailures, T] = { cur => - // Because we can't trust Typesafe Config not to throw, we wrap the - // evaluation into a `try-catch` to prevent an unintentional exception from escaping. - try { - cur.scopeFailure { - fromF(cur.asString.fold(_ => cur.value.render(ConfigRenderOptions.concise), identity)) - } - } catch { - case NonFatal(t) => cur.failed(ExceptionThrown(t)) - } - } - private[pureconfig] def ensureNonEmpty[T](implicit ct: ClassTag[T]): String => Either[FailureReason, String] = { case "" => Left(EmptyStringFound(ct.toString)) case x => Right(x) diff --git a/core/src/main/scala/pureconfig/DurationUtils.scala b/core/src/main/scala/pureconfig/DurationUtils.scala index 3455f49b7..86f44e1a6 100644 --- a/core/src/main/scala/pureconfig/DurationUtils.scala +++ b/core/src/main/scala/pureconfig/DurationUtils.scala @@ -6,8 +6,9 @@ package pureconfig import scala.concurrent.duration.Duration.{ Inf, MinusInf } import scala.concurrent.duration.{ DAYS, Duration, FiniteDuration, HOURS, MICROSECONDS, MILLISECONDS, MINUTES, NANOSECONDS, SECONDS, TimeUnit } import scala.util.Try +import scala.util.control.NonFatal -import pureconfig.error.{ CannotConvert, FailureReason } +import pureconfig.error.{ CannotConvert, ExceptionThrown, FailureReason } /** * Utility functions for converting a `String` to a `Duration` and vice versa. The parser accepts the HOCON unit @@ -25,6 +26,8 @@ private[pureconfig] object DurationUtils { case ex: NumberFormatException => val err = s"${ex.getMessage}. (try a number followed by any of ns, us, ms, s, m, h, d)" Left(CannotConvert(string, "Duration", err)) + case NonFatal(t) => + Left(ExceptionThrown(t)) } } diff --git a/tests/src/test/scala/pureconfig/BasicConvertersSuite.scala b/tests/src/test/scala/pureconfig/BasicConvertersSuite.scala index 95e822641..f174c7417 100644 --- a/tests/src/test/scala/pureconfig/BasicConvertersSuite.scala +++ b/tests/src/test/scala/pureconfig/BasicConvertersSuite.scala @@ -27,16 +27,22 @@ class BasicConvertersSuite extends BaseSuite { checkArbitrary[Duration] checkFailure[Duration, EmptyStringFound](ConfigValueFactory.fromAnyRef("")) - checkFailure[Duration, CannotConvert](ConfigValueFactory.fromIterable(List(1).asJava)) + checkFailures[Duration]( + ConfigValueFactory.fromIterable(List(1).asJava) -> ConfigReaderFailures( + ConvertFailure(WrongType(ConfigValueType.LIST, Set(ConfigValueType.STRING)), None, ""))) checkArbitrary[JavaDuration] checkFailure[JavaDuration, EmptyStringFound](ConfigValueFactory.fromAnyRef("")) - checkFailure[JavaDuration, CannotConvert](ConfigValueFactory.fromIterable(List(1).asJava)) + checkFailures[JavaDuration]( + ConfigValueFactory.fromIterable(List(1).asJava) -> ConfigReaderFailures( + ConvertFailure(WrongType(ConfigValueType.LIST, Set(ConfigValueType.STRING)), None, ""))) checkArbitrary[FiniteDuration] checkFailure[FiniteDuration, EmptyStringFound](ConfigValueFactory.fromAnyRef("")) + checkFailures[FiniteDuration]( + ConfigValueFactory.fromIterable(List(1).asJava) -> ConfigReaderFailures( + ConvertFailure(WrongType(ConfigValueType.LIST, Set(ConfigValueType.STRING)), None, ""))) checkFailure[FiniteDuration, CannotConvert]( - ConfigValueFactory.fromIterable(List(1).asJava), ConfigConvert[Duration].to(Duration.MinusInf), ConfigConvert[Duration].to(Duration.Inf)) @@ -90,11 +96,19 @@ class BasicConvertersSuite extends BaseSuite { checkArbitrary[Double] checkArbitrary2[Double, Percentage](_.toDoubleFraction) - checkFailure[Double, EmptyStringFound](ConfigValueFactory.fromAnyRef("")) - checkFailure[Double, CannotConvert](ConfigValueFactory.fromIterable(List(1, 2, 3, 4).asJava)) + checkFailures[Double]( + ConfigValueFactory.fromAnyRef("") -> ConfigReaderFailures( + ConvertFailure(WrongType(ConfigValueType.STRING, Set(ConfigValueType.NUMBER)), None, "")), + ConfigValueFactory.fromIterable(List(1, 2, 3, 4).asJava) -> ConfigReaderFailures( + ConvertFailure(WrongType(ConfigValueType.LIST, Set(ConfigValueType.NUMBER)), None, ""))) checkArbitrary[Float] checkArbitrary2[Float, Percentage](_.toFloatFraction) + checkFailures[Float]( + ConfigValueFactory.fromAnyRef("") -> ConfigReaderFailures( + ConvertFailure(WrongType(ConfigValueType.STRING, Set(ConfigValueType.NUMBER)), None, "")), + ConfigValueFactory.fromIterable(List(1, 2, 3, 4).asJava) -> ConfigReaderFailures( + ConvertFailure(WrongType(ConfigValueType.LIST, Set(ConfigValueType.NUMBER)), None, ""))) checkArbitrary[Int] @@ -128,9 +142,11 @@ class BasicConvertersSuite extends BaseSuite { ConfigValueFactory.fromMap(Map("3" -> 2, "1" -> 4).asJava) -> List(4, 2), ConfigValueFactory.fromMap(Map("1" -> 1, "a" -> 2).asJava) -> List(1)) - checkFailure[immutable.List[Int], WrongType]( - ConfigValueFactory.fromMap(Map("b" -> 1, "a" -> 2).asJava), - ConfigValueFactory.fromMap(Map().asJava)) + checkFailures[immutable.List[Int]]( + ConfigValueFactory.fromMap(Map("b" -> 1, "a" -> 2).asJava) -> ConfigReaderFailures( + ConvertFailure(WrongType(ConfigValueType.OBJECT, Set(ConfigValueType.LIST)), None, "")), + ConfigValueFactory.fromMap(Map().asJava) -> ConfigReaderFailures( + ConvertFailure(WrongType(ConfigValueType.OBJECT, Set(ConfigValueType.LIST)), None, ""))) checkArbitrary[immutable.ListSet[Int]]