├── project
├── build.properties
├── plugins.sbt
└── build.scala
├── version.sbt
├── .gitignore
├── src
├── main
│ └── scala
│ │ └── net
│ │ └── ceedubs
│ │ └── ficus
│ │ ├── ConfigKey.scala
│ │ ├── readers
│ │ ├── StringReader.scala
│ │ ├── AllValueReaderInstances.scala
│ │ ├── TryReader.scala
│ │ ├── ConfigValueReader.scala
│ │ ├── ConfigReader.scala
│ │ ├── OptionReader.scala
│ │ ├── AnyValReaders.scala
│ │ ├── DurationReaders.scala
│ │ ├── BigNumberReaders.scala
│ │ ├── CollectionReaders.scala
│ │ ├── ValueReader.scala
│ │ └── ArbitraryTypeReader.scala
│ │ ├── Ficus.scala
│ │ ├── FicusConfig.scala
│ │ └── util
│ │ └── ReflectionUtils.scala
└── test
│ └── scala
│ └── net
│ └── ceedubs
│ └── ficus
│ ├── Spec.scala
│ ├── readers
│ ├── StringReaderSpec.scala
│ ├── OptionReadersSpec.scala
│ ├── ValueReaderSpec.scala
│ ├── DurationReadersSpec.scala
│ ├── ConfigReaderSpec.scala
│ ├── TryReaderSpec.scala
│ ├── AnyValReadersSpec.scala
│ ├── ConfigValueReaderSpec.scala
│ ├── BigNumberReadersSpec.scala
│ ├── CollectionReadersSpec.scala
│ ├── CaseClassReadersSpec.scala
│ └── ArbitraryTypeReaderSpec.scala
│ ├── FicusConfigSpec.scala
│ ├── Examples.scala
│ ├── ConfigSerializer.scala
│ └── ExampleSpec.scala
├── CONTRIBUTORS.md
├── LICENSE
├── README.md
└── sbt
/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version=0.13.7
2 |
--------------------------------------------------------------------------------
/version.sbt:
--------------------------------------------------------------------------------
1 | version in ThisBuild := "1.2.0-SNAPSHOT"
2 |
--------------------------------------------------------------------------------
/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.0.0")
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | .idea_modules
3 | target
4 | *.swp
5 | *~
6 | .DS_Store
7 | .lib
8 |
--------------------------------------------------------------------------------
/src/main/scala/net/ceedubs/ficus/ConfigKey.scala:
--------------------------------------------------------------------------------
1 | package net.ceedubs.ficus
2 |
3 | trait ConfigKey[A] {
4 | def path: String
5 | }
6 |
7 | final case class SimpleConfigKey[A](path: String) extends ConfigKey[A]
8 |
--------------------------------------------------------------------------------
/src/test/scala/net/ceedubs/ficus/Spec.scala:
--------------------------------------------------------------------------------
1 | package net.ceedubs.ficus
2 |
3 | import org.specs2.matcher.MustMatchers
4 | import org.specs2.{ScalaCheck, Specification}
5 |
6 | trait Spec extends Specification with MustMatchers with ScalaCheck
--------------------------------------------------------------------------------
/CONTRIBUTORS.md:
--------------------------------------------------------------------------------
1 | # Contributors #
2 |
3 | Many thanks to those who have contributed to Ficus.
4 |
5 | * Kelsey Gilmore-Innis: unit tests
6 | * Thomas Dufour: value reader for `ConfigValue` and unit tests
7 | * Cody Allen: core maintainer
8 |
--------------------------------------------------------------------------------
/src/main/scala/net/ceedubs/ficus/readers/StringReader.scala:
--------------------------------------------------------------------------------
1 | package net.ceedubs.ficus.readers
2 |
3 | import com.typesafe.config.Config
4 |
5 | trait StringReader {
6 | implicit val stringValueReader: ValueReader[String] = new ValueReader[String] {
7 | def read(config: Config, path: String): String = config.getString(path)
8 | }
9 | }
10 |
11 | object StringReader extends StringReader
12 |
--------------------------------------------------------------------------------
/src/main/scala/net/ceedubs/ficus/readers/AllValueReaderInstances.scala:
--------------------------------------------------------------------------------
1 | package net.ceedubs.ficus.readers
2 |
3 | trait AllValueReaderInstances extends AnyValReaders with StringReader with OptionReader
4 | with CollectionReaders with ConfigReader with DurationReaders with ArbitraryTypeReader
5 | with TryReader with ConfigValueReader
6 |
7 | object AllValueReaderInstances extends AllValueReaderInstances
8 |
--------------------------------------------------------------------------------
/project/build.scala:
--------------------------------------------------------------------------------
1 | import sbt._
2 | import Keys._
3 |
4 | object build extends Build {
5 |
6 | val gcsettings = Defaults.defaultSettings
7 |
8 | val gc = TaskKey[Unit]("gc", "runs garbage collector")
9 | val gcTask = gc := {
10 | println("requesting garbage collection")
11 | System gc()
12 | }
13 |
14 | lazy val project = Project (
15 | "project",
16 | file("."),
17 | settings = gcsettings ++ Seq(gcTask)
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/src/main/scala/net/ceedubs/ficus/readers/TryReader.scala:
--------------------------------------------------------------------------------
1 | package net.ceedubs.ficus.readers
2 |
3 | import scala.util.Try
4 | import com.typesafe.config.Config
5 |
6 | trait TryReader {
7 | implicit def tryValueReader[A](implicit valueReader: ValueReader[A]): ValueReader[Try[A]] = new ValueReader[Try[A]] {
8 | def read(config: Config, path: String): Try[A] = Try(valueReader.read(config, path))
9 | }
10 | }
11 |
12 | object TryReader extends TryReader
13 |
--------------------------------------------------------------------------------
/src/main/scala/net/ceedubs/ficus/readers/ConfigValueReader.scala:
--------------------------------------------------------------------------------
1 | package net.ceedubs.ficus.readers
2 |
3 | import com.typesafe.config.ConfigValue
4 | import com.typesafe.config.Config
5 |
6 | trait ConfigValueReader {
7 | implicit val configValueValueReader: ValueReader[ConfigValue] = new ValueReader[ConfigValue] {
8 | override def read(config: Config, path: String): ConfigValue = config.getValue(path)
9 | }
10 | }
11 |
12 | object ConfigValueReader extends ConfigValueReader
--------------------------------------------------------------------------------
/src/main/scala/net/ceedubs/ficus/Ficus.scala:
--------------------------------------------------------------------------------
1 | package net.ceedubs.ficus
2 |
3 | import com.typesafe.config.Config
4 | import net.ceedubs.ficus.readers._
5 |
6 | trait FicusInstances extends AnyValReaders with StringReader with OptionReader
7 | with CollectionReaders with ConfigReader with DurationReaders
8 | with TryReader with ConfigValueReader with BigNumberReaders
9 |
10 | object Ficus extends FicusInstances {
11 | implicit def toFicusConfig(config: Config): FicusConfig = SimpleFicusConfig(config)
12 | }
13 |
--------------------------------------------------------------------------------
/src/main/scala/net/ceedubs/ficus/readers/ConfigReader.scala:
--------------------------------------------------------------------------------
1 | package net.ceedubs.ficus.readers
2 |
3 | import com.typesafe.config.Config
4 | import net.ceedubs.ficus.{SimpleFicusConfig, FicusConfig}
5 |
6 | trait ConfigReader {
7 | implicit val configValueReader: ValueReader[Config] = new ValueReader[Config] {
8 | def read(config: Config, path: String): Config = config.getConfig(path)
9 | }
10 |
11 | implicit val ficusConfigValueReader: ValueReader[FicusConfig] = configValueReader.map(SimpleFicusConfig)
12 | }
13 |
14 | object ConfigReader extends ConfigReader
15 |
--------------------------------------------------------------------------------
/src/main/scala/net/ceedubs/ficus/readers/OptionReader.scala:
--------------------------------------------------------------------------------
1 | package net.ceedubs.ficus.readers
2 |
3 | import com.typesafe.config.Config
4 |
5 | trait OptionReader {
6 | implicit def optionValueReader[A](implicit valueReader: ValueReader[A]): ValueReader[Option[A]] = new ValueReader[Option[A]] {
7 | def read(config: Config, path: String): Option[A] = {
8 | if (config.hasPath(path)) {
9 | Some(valueReader.read(config, path))
10 | } else {
11 | None
12 | }
13 | }
14 | }
15 | }
16 |
17 | object OptionReader extends OptionReader
18 |
--------------------------------------------------------------------------------
/src/test/scala/net/ceedubs/ficus/readers/StringReaderSpec.scala:
--------------------------------------------------------------------------------
1 | package net.ceedubs.ficus
2 | package readers
3 |
4 | import com.typesafe.config.ConfigFactory
5 | import org.scalacheck.Prop
6 | import ConfigSerializerOps._
7 |
8 | class StringReaderSpec extends Spec with StringReader { def is = s2"""
9 | The String value reader should
10 | read a String $readString
11 | """
12 |
13 | def readString = prop { string: String =>
14 | val cfg = ConfigFactory.parseString(s"myValue = ${string.asConfigValue}")
15 | stringValueReader.read(cfg, "myValue") must beEqualTo(string)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/test/scala/net/ceedubs/ficus/readers/OptionReadersSpec.scala:
--------------------------------------------------------------------------------
1 | package net.ceedubs.ficus
2 | package readers
3 |
4 | import com.typesafe.config.ConfigFactory
5 |
6 | class OptionReadersSpec extends Spec with OptionReader with AnyValReaders { def is = s2"""
7 | An option value reader should
8 | wrap an existing value in a Some $optionSome
9 | return a None for a non-existing value $optionNone
10 | """
11 |
12 | def optionSome = prop { i: Int =>
13 | val cfg = ConfigFactory.parseString(s"myValue = $i")
14 | optionValueReader[Int].read(cfg, "myValue") must beSome(i)
15 | }
16 |
17 | def optionNone = {
18 | val cfg = ConfigFactory.parseString("")
19 | optionValueReader[Boolean].read(cfg, "myValue") must beNone
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/test/scala/net/ceedubs/ficus/readers/ValueReaderSpec.scala:
--------------------------------------------------------------------------------
1 | package net.ceedubs.ficus
2 | package readers
3 |
4 | import com.typesafe.config.ConfigFactory
5 | import Ficus.intValueReader
6 |
7 | class ValueReaderSpec extends Spec { def is = s2"""
8 | A value reader should
9 | be able to be fetched from implicit scope via the companion apply method $fromCompanionApply
10 | be a functor $transformAsFunctor
11 | """
12 |
13 | def fromCompanionApply = {
14 | ValueReader[Int] must beEqualTo(implicitly[ValueReader[Int]])
15 | }
16 |
17 | def transformAsFunctor = {
18 | val plusOneReader = ValueReader[Int].map(_ + 1)
19 | prop { i: Int =>
20 | val cfg = ConfigFactory.parseString(s"myValue = $i")
21 | plusOneReader.read(cfg, "myValue") must beEqualTo(i + 1)
22 | }
23 | }
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/src/main/scala/net/ceedubs/ficus/readers/AnyValReaders.scala:
--------------------------------------------------------------------------------
1 | package net.ceedubs.ficus.readers
2 |
3 | import com.typesafe.config.Config
4 |
5 | trait AnyValReaders {
6 | implicit val booleanValueReader: ValueReader[Boolean] = new ValueReader[Boolean] {
7 | def read(config: Config, path: String): Boolean = config.getBoolean(path)
8 | }
9 |
10 | implicit val intValueReader: ValueReader[Int] = new ValueReader[Int] {
11 | def read(config: Config, path: String): Int = config.getInt(path)
12 | }
13 |
14 | implicit val longValueReader: ValueReader[Long] = new ValueReader[Long] {
15 | def read(config: Config, path: String): Long = config.getLong(path)
16 | }
17 |
18 | implicit val doubleValueReader: ValueReader[Double] = new ValueReader[Double] {
19 | def read(config: Config, path: String): Double = config.getDouble(path)
20 | }
21 |
22 | }
23 |
24 | object AnyValReaders extends AnyValReaders
25 |
--------------------------------------------------------------------------------
/src/main/scala/net/ceedubs/ficus/readers/DurationReaders.scala:
--------------------------------------------------------------------------------
1 | package net.ceedubs.ficus.readers
2 |
3 | import scala.concurrent.duration.FiniteDuration
4 | import com.typesafe.config.Config
5 | import scala.concurrent.duration.MILLISECONDS
6 |
7 | trait DurationReaders {
8 |
9 | /**
10 | * A reader for for a scala.concurrent.duration.FiniteDuration. This reader should be able to read any valid duration
11 | * format as defined by the HOCON spec.
12 | * For example, it can read "15 minutes" or "1 day".
13 | */
14 | implicit def finiteDurationReader: ValueReader[FiniteDuration] = new ValueReader[FiniteDuration] {
15 | def read(config: Config, path: String): FiniteDuration = {
16 | val millis = config.getDuration(path, java.util.concurrent.TimeUnit.MILLISECONDS)
17 | FiniteDuration(millis, MILLISECONDS)
18 | }
19 | }
20 | }
21 |
22 | object DurationReaders extends DurationReaders
23 |
--------------------------------------------------------------------------------
/src/main/scala/net/ceedubs/ficus/FicusConfig.scala:
--------------------------------------------------------------------------------
1 | package net.ceedubs.ficus
2 |
3 | import com.typesafe.config.Config
4 | import net.ceedubs.ficus.readers.{AllValueReaderInstances, ValueReader}
5 |
6 | trait FicusConfig {
7 | def config: Config
8 |
9 | def as[A](path: String)(implicit reader: ValueReader[A]): A = reader.read(config, path)
10 |
11 | def getAs[A](path: String)(implicit reader: ValueReader[Option[A]]): Option[A] = reader.read(config, path)
12 |
13 | def apply[A](key: ConfigKey[A])(implicit reader: ValueReader[A]): A = as[A](key.path)
14 | }
15 |
16 | final case class SimpleFicusConfig(config: Config) extends FicusConfig
17 |
18 | @deprecated(
19 | "For implicits, use Ficus._ instead of FicusConfig._. Separately use ArbitraryTypeReader._ for macro-based derived reader instances. See https://github.com/ceedubs/ficus/issues/5",
20 | since = "1.0.1/1.1.1")
21 | object FicusConfig extends AllValueReaderInstances {
22 | implicit def toFicusConfig(config: Config): FicusConfig = SimpleFicusConfig(config)
23 | }
24 |
--------------------------------------------------------------------------------
/src/main/scala/net/ceedubs/ficus/readers/BigNumberReaders.scala:
--------------------------------------------------------------------------------
1 | package net.ceedubs.ficus.readers
2 |
3 | import com.typesafe.config.{ConfigException, Config}
4 |
5 | trait BigNumberReaders {
6 | implicit val bigDecimalReader: ValueReader[BigDecimal] = new ValueReader[BigDecimal] {
7 | def read(config: Config, path: String): BigDecimal = {
8 | val s = config.getString(path)
9 | try {
10 | BigDecimal(s)
11 | } catch {
12 | case e: NumberFormatException => throw new ConfigException.WrongType(config.origin(),path,"scala.math.BigDecimal","String",e)
13 | }
14 | }
15 | }
16 |
17 | implicit val bigIntReader: ValueReader[BigInt] = new ValueReader[BigInt] {
18 | def read(config: Config, path: String): BigInt = {
19 | val s = config.getString(path)
20 | try {
21 | BigInt(s)
22 | } catch {
23 | case e: NumberFormatException => throw new ConfigException.WrongType(config.origin(),path,"scala.math.BigInt","String",e)
24 | }
25 | }
26 | }
27 | }
28 |
29 | object BigNumberReaders extends BigNumberReaders
30 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) [2014] [Cody Allen]
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/test/scala/net/ceedubs/ficus/readers/DurationReadersSpec.scala:
--------------------------------------------------------------------------------
1 | package net.ceedubs.ficus
2 | package readers
3 |
4 | import com.typesafe.config.ConfigFactory
5 | import scala.concurrent.duration._
6 | import org.scalacheck.{Gen, Prop}
7 |
8 | class DurationReadersSpec extends Spec with DurationReaders with DeactivatedTimeConversions { def is = s2"""
9 | The finite duration reader should
10 | read a millisecond value $readMillis
11 | read a minute value $readMinutes
12 | """
13 |
14 | def readMillis = prop { i: Int =>
15 | val cfg = ConfigFactory.parseString(s"myValue = $i")
16 | finiteDurationReader.read(cfg, "myValue") must beEqualTo(i millis)
17 | }
18 |
19 | def readMinutes = Prop.forAll(Gen.choose(1.5e-8.toInt, 1.5e8.toInt)) { i: Int =>
20 | val cfg = ConfigFactory.parseString("myValue = \"" + i + " minutes\"")
21 | finiteDurationReader.read(cfg, "myValue") must beEqualTo(i minutes)
22 | }
23 |
24 | }
25 |
26 | /* specs2 time conversions conflict with scala.concurrent.duration time conversions */
27 | trait DeactivatedTimeConversions extends org.specs2.time.TimeConversions {
28 | override def intToRichLong(v: Int) = super.intToRichLong(v)
29 | }
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Project has moved
2 |
3 | The official repository is now [iheartradio/ficus](https://github.com/iheartradio/ficus), since the kind folks at [iHeartRadio](http://www.iheartradio.com/) have adopted this library.
4 |
5 | ## Reasons for handing over ownership
6 | I had been neglecting Ficus for several reasons:
7 | * At work we use [knobs](http://oncue.github.io/knobs/)
8 | * I am a bit oversubscribed on open source projects
9 | * Ficus scratched an itch when I first wrote it, but if I were to write it again right now, I would approach it differently.
10 | * I would represent errors explicitly in the types (currently it throws exceptions, which can be handy, but I would want an alternative).
11 | * I would use [Shapeless](https://github.com/milessabin/shapeless) to derive readers instead of a macro. At the time, the macro was necessary to use default values on classes, but Shapeless now provides full support for this.
12 | * I think I would end up writing a `shapeless-knobs` micro library that simply provides Shapeless-derived [Configured](https://github.com/oncue/knobs/blob/master/core/src/main/scala/knobs/Configured.scala) instances.
13 |
14 | Having said that, I know there are a number of people that are happily using Ficus. So I'm quite thankful and happy that a team at iHeartRadio has adopted it.
15 |
--------------------------------------------------------------------------------
/src/test/scala/net/ceedubs/ficus/FicusConfigSpec.scala:
--------------------------------------------------------------------------------
1 | package net.ceedubs.ficus
2 |
3 | import com.typesafe.config.ConfigFactory
4 | import Ficus.{ booleanValueReader, optionValueReader, toFicusConfig }
5 |
6 | class FicusConfigSpec extends Spec { def is = s2"""
7 | A Ficus config should
8 | be implicitly converted from a Typesafe config $implicitlyConverted
9 | read a value with a value reader $readAValue
10 | get an existing value as a Some $getAsSome
11 | get a missing value as a None $getAsNone
12 | accept a CongigKey and return the appropriate type $acceptAConfigKey
13 | """
14 |
15 | def implicitlyConverted = {
16 | val cfg = ConfigFactory.parseString("myValue = true")
17 | cfg.as[Boolean]("myValue") must beTrue
18 | }
19 |
20 | def readAValue = prop { b: Boolean =>
21 | val cfg = ConfigFactory.parseString(s"myValue = $b")
22 | cfg.as[Boolean]("myValue") must beEqualTo(b)
23 | }
24 |
25 | def getAsSome = prop { b: Boolean =>
26 | val cfg = ConfigFactory.parseString(s"myValue = $b")
27 | cfg.getAs[Boolean]("myValue") must beSome(b)
28 | }
29 |
30 | def getAsNone = {
31 | val cfg = ConfigFactory.parseString("myValue = true")
32 | cfg.getAs[Boolean]("nonValue") must beNone
33 | }
34 |
35 | def acceptAConfigKey = prop { b: Boolean =>
36 | val cfg = ConfigFactory.parseString(s"myValue = $b")
37 | val key: ConfigKey[Boolean] = SimpleConfigKey("myValue")
38 | cfg(key) must beEqualTo(b)
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/main/scala/net/ceedubs/ficus/readers/CollectionReaders.scala:
--------------------------------------------------------------------------------
1 | package net.ceedubs.ficus.readers
2 |
3 | import com.typesafe.config.{ConfigUtil, Config}
4 | import collection.JavaConverters._
5 | import collection.generic.CanBuildFrom
6 | import scala.reflect.ClassTag
7 |
8 | trait CollectionReaders {
9 |
10 | private[this] val DummyPathValue: String = "collection-entry-path"
11 |
12 | implicit def traversableReader[C[_], A](implicit entryReader: ValueReader[A], cbf: CanBuildFrom[Nothing, A, C[A]]): ValueReader[C[A]] = new ValueReader[C[A]] {
13 | def read(config: Config, path: String): C[A] = {
14 | val list = config.getList(path).asScala
15 | val builder = cbf()
16 | builder.sizeHint(list.size)
17 | list foreach { entry =>
18 | val entryConfig = entry.atPath(DummyPathValue)
19 | builder += entryReader.read(entryConfig, DummyPathValue)
20 | }
21 | builder.result
22 | }
23 | }
24 |
25 | implicit def mapValueReader[A](implicit entryReader: ValueReader[A]): ValueReader[Map[String, A]] = new ValueReader[Map[String, A]] {
26 | def read(config: Config, path: String): Map[String, A] = {
27 | val relativeConfig = config.getConfig(path)
28 | relativeConfig.root().entrySet().asScala map { entry =>
29 | val key = entry.getKey
30 | key -> entryReader.read(relativeConfig, ConfigUtil.quoteString(key))
31 | } toMap
32 | }
33 | }
34 |
35 | }
36 |
37 | object CollectionReaders extends CollectionReaders
38 |
--------------------------------------------------------------------------------
/src/test/scala/net/ceedubs/ficus/Examples.scala:
--------------------------------------------------------------------------------
1 | package net.ceedubs.ficus
2 |
3 | import com.typesafe.config.{Config, ConfigFactory}
4 | import net.ceedubs.ficus.Ficus._
5 | import net.ceedubs.ficus.readers.ArbitraryTypeReader._
6 | import scala.concurrent.duration.FiniteDuration
7 |
8 | case class SomeCaseClass(foo: String, bar: Int, baz: Option[FiniteDuration])
9 |
10 | class Examples {
11 | val config: Config = ConfigFactory.load() // standard Typesafe Config
12 |
13 | // Note: explicit typing isn't necessary. It's just in these examples to make it clear what the return types are.
14 | // This line could instead be: val appName = config.as[String]("app.name")
15 | val appName: String = config.as[String]("app.name") // equivalent to config.getString("app.name")
16 |
17 | // config.as[Option[Boolean]]("preloadCache") will return None if preloadCache isn't defined in the config
18 | val preloadCache: Boolean = config.as[Option[Boolean]]("preloadCache").getOrElse(false)
19 |
20 | val adminUserIds: Set[Long] = config.as[Set[Long]]("adminIds")
21 |
22 | // something such as "15 minutes" can be converted to a FiniteDuration
23 | val retryInterval: FiniteDuration = config.as[FiniteDuration]("retryInterval")
24 |
25 | // can hydrate most arbitrary types
26 | // it first tries to use an apply method on the companion object and falls back to the primary constructor
27 | // if values are not in the config, they will fall back to the default value on the class/apply method
28 | val someCaseClass: SomeCaseClass = config.as[SomeCaseClass]("someCaseClass")
29 | }
30 |
--------------------------------------------------------------------------------
/src/test/scala/net/ceedubs/ficus/readers/ConfigReaderSpec.scala:
--------------------------------------------------------------------------------
1 | package net.ceedubs.ficus
2 | package readers
3 |
4 | import com.typesafe.config.{Config, ConfigFactory}
5 | import Ficus._
6 |
7 | class ConfigReaderSpec extends Spec { def is = s2"""
8 | The Config value reader should
9 | read a config $readConfig
10 | implicitly read a config $implicitlyReadConfig
11 | read a ficus config $readFicusConfig
12 | implicitly read a ficus config $implicitlyReadFicusConfig
13 | """
14 |
15 | def readConfig = prop { i: Int =>
16 | val cfg = ConfigFactory.parseString(
17 | s"""
18 | |myConfig {
19 | | myValue = $i
20 | |}
21 | """.stripMargin)
22 | configValueReader.read(cfg, "myConfig").getInt("myValue") must beEqualTo(i)
23 | }
24 |
25 | def implicitlyReadConfig = prop { i: Int =>
26 | val cfg = ConfigFactory.parseString(
27 | s"""
28 | |myConfig {
29 | | myValue = $i
30 | |}
31 | """.stripMargin)
32 | cfg.as[Config]("myConfig").getInt("myValue") must beEqualTo(i)
33 | }
34 |
35 | def readFicusConfig = prop { i: Int =>
36 | val cfg = ConfigFactory.parseString(
37 | s"""
38 | |myConfig {
39 | | myValue = $i
40 | |}
41 | """.stripMargin)
42 | ficusConfigValueReader.read(cfg, "myConfig").as[Int]("myValue") must beEqualTo(i)
43 | }
44 |
45 | def implicitlyReadFicusConfig = prop { i: Int =>
46 | val cfg = ConfigFactory.parseString(
47 | s"""
48 | |myConfig {
49 | | myValue = $i
50 | |}
51 | """.stripMargin)
52 | cfg.as[FicusConfig]("myConfig").as[Int]("myValue") must beEqualTo(i)
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/test/scala/net/ceedubs/ficus/readers/TryReaderSpec.scala:
--------------------------------------------------------------------------------
1 | package net.ceedubs.ficus.readers
2 |
3 | import com.typesafe.config.{ConfigException, Config, ConfigFactory}
4 | import net.ceedubs.ficus.Spec
5 | import org.scalacheck.Prop
6 | import scala.util.Failure
7 |
8 | class TryReaderSpec extends Spec with TryReader with AnyValReaders { def is = s2"""
9 | A try value reader should
10 | return a success when a value can be read $successWhenPresent
11 | return a failure when a value cannot be read $cannotBeRead
12 | handle an unexpected exception type $unexpectedExceptionType
13 | handle an unexpected exception $unexpectedException
14 | """
15 |
16 | def successWhenPresent = prop { i: Int =>
17 | val cfg = ConfigFactory.parseString(s"myValue = $i")
18 | tryValueReader[Int].read(cfg, "myValue") must beSuccessfulTry[Int].withValue(i)
19 | }
20 |
21 | def cannotBeRead = {
22 | val cfg = ConfigFactory.parseString("myValue = true")
23 | tryValueReader[Boolean].read(cfg, "wrongKey") must beFailedTry[Boolean].withThrowable[ConfigException.Missing]
24 | }
25 |
26 | def unexpectedExceptionType = {
27 | val cfg = ConfigFactory.parseString("myValue = true")
28 | implicit val stringValueReader: ValueReader[String] = new ValueReader[String] {
29 | def read(config: Config, path: String): String = throw new NullPointerException("oops")
30 | }
31 | tryValueReader[String].read(cfg, "myValue") must beFailedTry[String].withThrowable[NullPointerException]("oops")
32 | }
33 |
34 | def unexpectedException = prop { up: Throwable =>
35 | val cfg = ConfigFactory.parseString("myValue = true")
36 | implicit val stringValueReader: ValueReader[String] = new ValueReader[String] {
37 | def read(config: Config, path: String): String = throw up
38 | }
39 | val expectedMessage = Option(up).map(_.getMessage).orNull
40 | tryValueReader[String].read(cfg, "myValue") must beFailedTry[String] and
41 | (up.getMessage == expectedMessage must beTrue)
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/main/scala/net/ceedubs/ficus/readers/ValueReader.scala:
--------------------------------------------------------------------------------
1 | package net.ceedubs.ficus.readers
2 |
3 | import com.typesafe.config.Config
4 |
5 | /** Reads a value of type A that is located at a provided `path` in a Config. */
6 | trait ValueReader[A] { self =>
7 |
8 | /** Reads the value at the path `path` in the Config */
9 | def read(config: Config, path: String): A
10 |
11 | /**
12 | * Turns a ValueReader[A] into a ValueReader[B] by applying the provided transformation `f` on the item of type A
13 | * that is read from config
14 | */
15 | def map[B](f: A => B): ValueReader[B] = new ValueReader[B] {
16 | def read(config: Config, path: String): B = f(self.read(config, path))
17 | }
18 | }
19 |
20 | object ValueReader {
21 |
22 | /** Returns the implicit ValueReader[A] in scope.
23 | * `ValueReader[A]` is equivalent to `implicitly[ValueReader[A]]`
24 | */
25 | def apply[A](implicit reader: ValueReader[A]): ValueReader[A] = reader
26 |
27 | /** ValueReader that receives a Config whose root is the path being read.
28 | *
29 | * This is generally the most concise way to implement a ValueReader that doesn't depend on the path of the value
30 | * being read.
31 | *
32 | * For example to read a `case class FooBar(foo: Foo, bar: Bar)`, instead of
33 | * {{{
34 | * new ValueReader[FooBar] {
35 | * def read(config: Config, path: String): FooBar = {
36 | * val localizedConfig = config.getConfig(path)
37 | * FooBar(
38 | * foo = localizedConfig.as[Foo]("foo"),
39 | * bar = localizedConfig.as[Bar]("bar"))
40 | * }
41 | * }
42 | * }}}
43 | * you could do
44 | * {{{
45 | * ValueReader.relative[FooBar] { config =>
46 | * FooBar(
47 | * foo = config.as[Foo]("foo"),
48 | * bar = config.as[Bar]("bar))
49 | * }
50 | * }}}
51 | */
52 | def relative[A](f: Config => A): ValueReader[A] = new ValueReader[A] {
53 | def read(config: Config, path: String): A = f(config.getConfig(path))
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/test/scala/net/ceedubs/ficus/readers/AnyValReadersSpec.scala:
--------------------------------------------------------------------------------
1 | package net.ceedubs.ficus.readers
2 |
3 | import com.typesafe.config.ConfigFactory
4 | import net.ceedubs.ficus.Spec
5 |
6 | class AnyValReadersSpec extends Spec with AnyValReaders { def is = s2"""
7 | The Boolean value reader should
8 | read a boolean $readBoolean
9 |
10 | The Int value reader should
11 | read an int $readInt
12 | read a double as an int $readDoubleAsInt
13 |
14 | The Long value reader should
15 | read a long $readLong
16 | read an int as a long $readIntAsLong
17 |
18 | The Double value reader should
19 | read a double $readDouble
20 | read an int as a double $readIntAsDouble
21 | """
22 |
23 | def readBoolean = prop { b: Boolean =>
24 | val cfg = ConfigFactory.parseString(s"myValue = $b")
25 | booleanValueReader.read(cfg, "myValue") must beEqualTo(b)
26 | }
27 |
28 | def readInt = prop { i: Int =>
29 | val cfg = ConfigFactory.parseString(s"myValue = $i")
30 | intValueReader.read(cfg, "myValue") must beEqualTo(i)
31 | }
32 |
33 | def readDoubleAsInt = prop { d: Double =>
34 | (d >= Int.MinValue && d <= Int.MaxValue) ==> {
35 | val cfg = ConfigFactory.parseString(s"myValue = $d")
36 | intValueReader.read(cfg, "myValue") must beEqualTo(d.toInt)
37 | }
38 | }
39 |
40 | def readLong = prop { l: Long =>
41 | val cfg = ConfigFactory.parseString(s"myValue = $l")
42 | longValueReader.read(cfg, "myValue") must beEqualTo(l)
43 | }
44 |
45 | def readIntAsLong = prop { i: Int =>
46 | val cfg = ConfigFactory.parseString(s"myValue = $i")
47 | longValueReader.read(cfg, "myValue") must beEqualTo(i.toLong)
48 | }
49 |
50 | def readDouble = prop { d: Double =>
51 | val cfg = ConfigFactory.parseString(s"myValue = $d")
52 | doubleValueReader.read(cfg, "myValue") must beEqualTo(d)
53 | }
54 |
55 | def readIntAsDouble = prop { i: Int =>
56 | val cfg = ConfigFactory.parseString(s"myValue = $i")
57 | doubleValueReader.read(cfg, "myValue") must beEqualTo(i.toDouble)
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/main/scala/net/ceedubs/ficus/util/ReflectionUtils.scala:
--------------------------------------------------------------------------------
1 | package net.ceedubs.ficus.util
2 |
3 | import scala.reflect.macros.blackbox.Context
4 |
5 | object ReflectionUtils {
6 | def instantiationMethod[T : c.WeakTypeTag](c: Context, fail: String => Nothing): c.universe.MethodSymbol = {
7 | import c.universe._
8 |
9 | val returnType = c.weakTypeOf[T]
10 |
11 | val returnTypeTypeArgs = returnType match {
12 | case TypeRef(_, _, args) => args
13 | case _ => Nil
14 | }
15 |
16 | if (returnTypeTypeArgs.nonEmpty) fail(s"value readers cannot be auto-generated for types with type parameters. Consider defining your own ValueReader[$returnType]")
17 |
18 | val companionSymbol = returnType.typeSymbol.companion match {
19 | case NoSymbol => None
20 | case x => Some(x)
21 | }
22 |
23 | val applyMethods = companionSymbol.toList.flatMap(_.typeSignatureIn(returnType).members collect {
24 | case m: MethodSymbol if m.name.decodedName.toString == "apply" && m.returnType <:< returnType => m
25 | })
26 |
27 | val applyMethod = applyMethods match {
28 | case Nil => None
29 | case (head :: Nil) => Some(head)
30 | case _ => fail(s"its companion object has multiple apply methods that return type $returnType")
31 | }
32 |
33 | applyMethod getOrElse {
34 | val primaryConstructor = returnType.decl(termNames.CONSTRUCTOR) match {
35 | case t: TermSymbol => {
36 | val constructors = t.alternatives collect {
37 | case m: MethodSymbol if m.isConstructor => m
38 | }
39 | val primaryScalaConstructor = constructors.find(m => m.isPrimaryConstructor && !m.isJava)
40 | primaryScalaConstructor orElse {
41 | if (constructors.length == 1) constructors.headOption else None
42 | }
43 | }
44 | case _ => None
45 | }
46 | primaryConstructor getOrElse {
47 | fail(s"it has no apply method in a companion object that returns type $returnType, and it doesn't have a primary constructor")
48 | }
49 | }
50 | }
51 |
52 | }
53 |
--------------------------------------------------------------------------------
/src/test/scala/net/ceedubs/ficus/readers/ConfigValueReaderSpec.scala:
--------------------------------------------------------------------------------
1 | package net.ceedubs.ficus.readers
2 |
3 | import com.typesafe.config.ConfigFactory
4 | import net.ceedubs.ficus.Spec
5 | import com.typesafe.config.ConfigValueType
6 | import net.ceedubs.ficus.ConfigSerializerOps._
7 |
8 | class ConfigValueReaderSpec extends Spec with ConfigValueReader {
9 | def is = s2"""
10 | The ConfigValue value reader should
11 | read a boolean $readBoolean
12 | read an int $readInt
13 | read a double $readDouble
14 | read a string $readString
15 | read an object $readObject
16 | """
17 |
18 | def readBoolean = prop { b: Boolean =>
19 | val cfg = ConfigFactory.parseString(s"myValue = $b")
20 | val read = configValueValueReader.read(cfg, "myValue")
21 | read.valueType must beEqualTo(ConfigValueType.BOOLEAN)
22 | read.unwrapped() must beEqualTo(b)
23 | }
24 |
25 | def readInt = prop { i: Int =>
26 | val cfg = ConfigFactory.parseString(s"myValue = $i")
27 | val read = configValueValueReader.read(cfg, "myValue")
28 | read.valueType must beEqualTo(ConfigValueType.NUMBER)
29 | read.unwrapped() must beEqualTo(int2Integer(i))
30 | }
31 |
32 | def readDouble = prop { d: Double =>
33 | val cfg = ConfigFactory.parseString(s"myValue = $d")
34 | val read = configValueValueReader.read(cfg, "myValue")
35 | read.valueType must beEqualTo(ConfigValueType.NUMBER)
36 | read.unwrapped() must beEqualTo(double2Double(d))
37 | }
38 |
39 | def readString = prop { s: String =>
40 | val cfg = ConfigFactory.parseString(s"myValue = ${s.asConfigValue}")
41 | val read = configValueValueReader.read(cfg, "myValue")
42 | read.valueType must beEqualTo(ConfigValueType.STRING)
43 | read.unwrapped() must beEqualTo(s)
44 | }
45 |
46 | def readObject = prop { i: Int =>
47 | val cfg = ConfigFactory.parseString(s"myValue = { i = $i }")
48 | val read = configValueValueReader.read(cfg, "myValue")
49 | read.valueType must beEqualTo(ConfigValueType.OBJECT)
50 | read.unwrapped() must beEqualTo(cfg.getValue("myValue").unwrapped())
51 | }
52 |
53 | def readList = prop { i: Int =>
54 | val cfg = ConfigFactory.parseString(s"myValue = [ $i ]")
55 | val read = configValueValueReader.read(cfg, "myValue")
56 | read.valueType must beEqualTo(ConfigValueType.LIST)
57 | read.unwrapped() must beEqualTo(cfg.getValue("myValue").unwrapped())
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/test/scala/net/ceedubs/ficus/ConfigSerializer.scala:
--------------------------------------------------------------------------------
1 | package net.ceedubs.ficus
2 |
3 | import com.typesafe.config.ConfigUtil
4 |
5 | trait ConfigSerializer[A] {
6 | def serialize(a: A): String
7 | }
8 |
9 | object ConfigSerializer {
10 | def apply[A](f: A => String): ConfigSerializer[A] = new ConfigSerializer[A] {
11 | def serialize(a: A): String = f(a)
12 | }
13 |
14 | def fromToString[A]: ConfigSerializer[A] = apply[A](_.toString)
15 |
16 | protected def serializeIterable[A](iterable: Iterable[A])(implicit serializer: ConfigSerializer[A]): String = {
17 | val elements = iterable.map(a => serializer.serialize(a))
18 | s"[${elements.mkString(", ")}]"
19 | }
20 |
21 | implicit val stringSerializer = apply[String](ConfigUtil.quoteString)
22 | implicit val booleanSerializer = fromToString[Boolean]
23 | implicit val intSerializer = fromToString[Int]
24 | implicit val longSerializer = fromToString[Long]
25 | implicit val doubleSerializer = fromToString[Double]
26 |
27 | implicit def listSerializer[A : ConfigSerializer]: ConfigSerializer[List[A]] = apply[List[A]](serializeIterable)
28 | implicit def serializerForSets[A : ConfigSerializer]: ConfigSerializer[Set[A]] = apply[Set[A]](serializeIterable)
29 | implicit def indexedSeqSerializer[A : ConfigSerializer]: ConfigSerializer[IndexedSeq[A]] = apply[IndexedSeq[A]](serializeIterable)
30 | implicit def vectorSerializer[A : ConfigSerializer]: ConfigSerializer[Vector[A]] = apply[Vector[A]](serializeIterable)
31 | implicit def arraySerializer[A : ConfigSerializer]: ConfigSerializer[Array[A]] = apply[Array[A]] { array =>
32 | serializeIterable(array.toIterable)
33 | }
34 | def iterableSerializer[A: ConfigSerializer]: ConfigSerializer[Iterable[A]] = apply[Iterable[A]](serializeIterable)
35 |
36 | implicit def stringKeyMapSerializer[A](implicit valueSerializer: ConfigSerializer[A]) = new ConfigSerializer[Map[String, A]] {
37 | def serialize(map: Map[String, A]): String = {
38 | val lines = map.toIterable.map(Function.tupled((key, value) => s"${stringSerializer.serialize(key)} = ${valueSerializer.serialize(value)}"))
39 | s"{\n ${lines.mkString("\n ")}\n}"
40 | }
41 | }
42 |
43 | }
44 |
45 | final case class ConfigSerializerOps[A](a: A, serializer: ConfigSerializer[A]) {
46 | def asConfigValue: String = serializer.serialize(a)
47 | }
48 |
49 | object ConfigSerializerOps {
50 | implicit def toConfigSerializerOps[A](a: A)(implicit serializer: ConfigSerializer[A]): ConfigSerializerOps[A] =
51 | ConfigSerializerOps[A](a, serializer)
52 | }
53 |
--------------------------------------------------------------------------------
/src/test/scala/net/ceedubs/ficus/ExampleSpec.scala:
--------------------------------------------------------------------------------
1 | package net.ceedubs.ficus
2 |
3 | import org.specs2.mutable.Specification
4 | import com.typesafe.config.{Config, ConfigFactory}
5 | import Ficus._
6 | import net.ceedubs.ficus.readers.ArbitraryTypeReader._
7 | import net.ceedubs.ficus.readers.ValueReader
8 |
9 | case class ServiceConfig(urls: Set[String], maxConnections: Int, httpsRequired: Boolean = false)
10 |
11 | class ExampleSpec extends Specification {
12 |
13 | // an example config snippet for us to work with
14 | val config = ConfigFactory.parseString(
15 | """
16 | |services {
17 | | users {
18 | | urls = ["localhost:8001"]
19 | | maxConnections = 100
20 | | httpsRequired = true
21 | | }
22 | | analytics {
23 | | urls = ["localhost:8002", "localhost:8003"]
24 | | maxConnections = 25
25 | | }
26 | |}
27 | """.stripMargin)
28 |
29 | "Ficus config" should {
30 | "make Typesafe config more Scala-friendly" in {
31 | val userServiceConfig = config.as[Config]("services.users")
32 | userServiceConfig.as[Set[String]]("urls") must beEqualTo(Set("localhost:8001"))
33 | userServiceConfig.as[Int]("maxConnections") must beEqualTo(100)
34 | userServiceConfig.as[Option[Boolean]]("httpsRequired") must beSome(true)
35 |
36 | val analyticsServiceConfig = config.as[Config]("services.analytics")
37 | analyticsServiceConfig.as[List[String]]("urls") must beEqualTo(List("localhost:8002", "localhost:8003"))
38 | val analyticsServiceRequiresHttps = analyticsServiceConfig.as[Option[Boolean]]("httpsRequired") getOrElse false
39 | analyticsServiceRequiresHttps must beFalse
40 | }
41 |
42 | "Automagically be able to hydrate arbitrary types from config" in {
43 | // Tkere are a few restrictions on types that can be read. See README file in root of project
44 | val analyticsConfig = config.as[ServiceConfig]("services.analytics")
45 | analyticsConfig.maxConnections must beEqualTo(25)
46 | // since this value isn't in the config, it will fall back to the default for the case class
47 | analyticsConfig.httpsRequired must beFalse
48 | }
49 |
50 | "Be easily extensible" in {
51 | // If we want a value reader that defaults httpsRequired to true instead of false (the default on the case
52 | // class) we can define a custom value reader for ServiceConfig
53 | implicit val serviceConfigReader: ValueReader[ServiceConfig] = ValueReader.relative { serviceConfig =>
54 | ServiceConfig(
55 | urls = serviceConfig.as[Set[String]]("urls"),
56 | maxConnections = serviceConfig.getInt("maxConnections"), // the old-fashioned way is fine too!
57 | httpsRequired = serviceConfig.as[Option[Boolean]]("httpsRequired") getOrElse true
58 | )
59 | }
60 |
61 | // so we don't have to add a "services." prefix for each service
62 | val servicesConfig = config.as[Config]("services")
63 |
64 | val analyticsServiceConfig: ServiceConfig = servicesConfig.as[ServiceConfig]("analytics")
65 | // the analytics service config doesn't define an "httpsRequired" value, but the serviceConfigReader defaults
66 | // to true if it is empty with its 'getOrElse true' on the extracted Option
67 | analyticsServiceConfig.httpsRequired must beTrue
68 |
69 | val userServiceConfig: ServiceConfig = servicesConfig.as[ServiceConfig]("users")
70 |
71 | val servicesMap = config.as[Map[String,ServiceConfig]]("services")
72 | servicesMap must beEqualTo(Map("users" -> userServiceConfig, "analytics" -> analyticsServiceConfig))
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/test/scala/net/ceedubs/ficus/readers/BigNumberReadersSpec.scala:
--------------------------------------------------------------------------------
1 | package net.ceedubs.ficus.readers
2 |
3 | import com.typesafe.config.ConfigFactory
4 | import net.ceedubs.ficus.Spec
5 | import org.specs2.specification.Fragments
6 |
7 | class BigNumberReadersSpec extends Spec with BigNumberReaders {def is: Fragments = s2"""
8 | The BigDecimal value reader should
9 | read a double $readDoubleAsBigDecimal
10 | read a long $readLongAsBigDecimal
11 | read an int $readIntAsBigDecimal
12 | read a bigDecimal $readBigDecimal
13 | read a bigInt $readBigIntAsBigDecimal
14 | read a bigDecimalAsString $readBigDecimalAsStringBigDecimal
15 | read a bigIntAsString $readBigIntAsStringBigDecimal
16 |
17 | The BigInt value reader should
18 | read an int $readIntAsBigInt
19 | read a long $readLongAsBigInt
20 | read a bigInt $readBigIntAsBigInt
21 | read a bigIntAsString $readBigIntAsStringBigInt
22 |
23 | """
24 |
25 | def readDoubleAsBigDecimal = prop { d: Double =>
26 | val cfg = ConfigFactory.parseString(s"myValue = $d")
27 | bigDecimalReader.read(cfg,"myValue") must beEqualTo(BigDecimal(d))
28 | }
29 |
30 | def readLongAsBigDecimal = prop{ l: Long =>
31 | val cfg = ConfigFactory.parseString(s"myValue = $l")
32 | bigDecimalReader.read(cfg,"myValue") must beEqualTo(BigDecimal(l))
33 | }
34 |
35 | def readIntAsBigDecimal = prop{ i: Int =>
36 | val cfg = ConfigFactory.parseString(s"myValue = $i")
37 | bigDecimalReader.read(cfg,"myValue") must beEqualTo(BigDecimal(i))
38 | }
39 |
40 | def readBigDecimal = prop{ b: BigDecimal =>
41 | scala.util.Try(BigDecimal(b.toString)).toOption.isDefined ==> {
42 | val cfg = ConfigFactory.parseString(s"myValue = $b")
43 | bigDecimalReader.read(cfg, "myValue") must beEqualTo(b)
44 | }
45 | }
46 |
47 | def readBigDecimalAsStringBigDecimal = prop{ b: BigDecimal =>
48 | scala.util.Try(BigDecimal(b.toString)).toOption.isDefined ==> {
49 | val cfg = ConfigFactory.parseString(s"myValue = ${b.toString}")
50 | bigDecimalReader.read(cfg, "myValue") must beEqualTo(BigDecimal(b.toString))
51 | }
52 | }
53 |
54 | def readBigIntAsStringBigDecimal = prop{ b: BigInt =>
55 | scala.util.Try(BigDecimal(b.toString)).toOption.isDefined ==> {
56 | val cfg = ConfigFactory.parseString(s"myValue = ${b.toString}")
57 | bigDecimalReader.read(cfg, "myValue") must beEqualTo(BigDecimal(b.toString))
58 | }
59 | }
60 |
61 | def readBigIntAsBigDecimal = prop{ b: BigInt =>
62 | scala.util.Try(BigDecimal(b)).toOption.isDefined ==> {
63 | val cfg = ConfigFactory.parseString(s"myValue = $b")
64 | bigDecimalReader.read(cfg, "myValue") must beEqualTo(BigDecimal(b))
65 | }
66 | }
67 |
68 | def readIntAsBigInt = prop { i: Int =>
69 | val cfg = ConfigFactory.parseString(s"myValue = $i")
70 | bigIntReader.read(cfg,"myValue") must beEqualTo(BigInt(i))
71 | }
72 |
73 | def readLongAsBigInt = prop { l: Long =>
74 | val cfg = ConfigFactory.parseString(s"myValue = $l")
75 | bigIntReader.read(cfg,"myValue") must beEqualTo(BigInt(l))
76 | }
77 |
78 | def readBigIntAsBigInt = prop{ b: BigInt =>
79 | scala.util.Try(BigInt(b.toString)).toOption.isDefined ==> {
80 | val cfg = ConfigFactory.parseString(s"myValue = $b")
81 | bigIntReader.read(cfg, "myValue") must beEqualTo(BigInt(b.toString))
82 | }
83 | }
84 |
85 | def readBigIntAsStringBigInt = prop{ b: BigInt =>
86 | scala.util.Try(BigInt(b.toString)).toOption.isDefined ==> {
87 | val cfg = ConfigFactory.parseString(s"myValue = ${b.toString}")
88 | bigIntReader.read(cfg, "myValue") must beEqualTo(BigInt(b.toString))
89 | }
90 | }
91 |
92 | }
--------------------------------------------------------------------------------
/src/test/scala/net/ceedubs/ficus/readers/CollectionReadersSpec.scala:
--------------------------------------------------------------------------------
1 | package net.ceedubs.ficus
2 | package readers
3 |
4 | import com.typesafe.config.ConfigFactory
5 | import AnyValReaders.{booleanValueReader, doubleValueReader, intValueReader, longValueReader}
6 | import StringReader.stringValueReader
7 | import ConfigSerializerOps._
8 | import org.scalacheck.util.Buildable
9 | import org.scalacheck.Arbitrary
10 | import CollectionReaderSpec._
11 |
12 | class CollectionReadersSpec extends Spec with CollectionReaders { def is = s2"""
13 | The collection value readers should
14 | read a list ${readCollection[List]}
15 | read a set ${readCollection[Set]}
16 | read an array ${readCollection[Array]}
17 | read an indexed sequence ${readCollection[IndexedSeq]}
18 | read a vector ${readCollection[Vector]}
19 | read an iterable $readIterable
20 | read a map with strings as keys $readStringMap
21 | read a map nested in another object $readNestedMap
22 | read a collection when used directly $readCollectionUsedDirectly
23 | """
24 |
25 | def readIterable = {
26 | implicit def iterableSerializer[A: ConfigSerializer]: ConfigSerializer[Iterable[A]] = ConfigSerializer.iterableSerializer
27 | readCollection[Iterable]
28 | }
29 |
30 | def readStringMap = {
31 | def reads[A: Arbitrary : ValueReader: ConfigSerializer] = prop { map: Map[String, A] =>
32 | val cfg = ConfigFactory.parseString(s"myValue = ${map.asConfigValue}")
33 | mapValueReader[A].read(cfg, "myValue") must beEqualTo(map)
34 | }
35 |
36 | reads[String] and reads[Boolean] and reads[Int] and reads[Long] and reads[Double]
37 | }
38 |
39 | def readNestedMap = {
40 | val cfg = ConfigFactory.parseString(
41 | """
42 | |wrapper {
43 | | myValue {
44 | | item1 = "value1"
45 | | item2 = "value2"
46 | | }
47 | |}
48 | """.stripMargin)
49 | mapValueReader[String].read(cfg, "wrapper.myValue") must beEqualTo(Map("item1" -> "value1", "item2" -> "value2"))
50 | }
51 |
52 | protected def readCollection[C[_]](implicit AS: Arbitrary[C[String]], SS: ConfigSerializer[C[String]], RS: ValueReader[C[String]],
53 | AB: Arbitrary[C[Boolean]], SB: ConfigSerializer[C[Boolean]], RB: ValueReader[C[Boolean]],
54 | AI: Arbitrary[C[Int]], SI: ConfigSerializer[C[Int]], RI: ValueReader[C[Int]],
55 | AL: Arbitrary[C[Long]], SL: ConfigSerializer[C[Long]], RL: ValueReader[C[Long]],
56 | AD: Arbitrary[C[Double]], SD: ConfigSerializer[C[Double]], RD: ValueReader[C[Double]]) = {
57 |
58 | def reads[V](implicit arb: Arbitrary[C[V]], serializer: ConfigSerializer[C[V]], reader: ValueReader[C[V]]) = {
59 | prop { values: C[V] =>
60 | val cfg = ConfigFactory.parseString(s"myValue = ${values.asConfigValue}")
61 | reader.read(cfg, "myValue") must beEqualTo(values)
62 | }
63 | }
64 |
65 | reads[String] and reads[Boolean] and reads[Int] and reads[Long] and reads[Double]
66 | }
67 |
68 | def readCollectionUsedDirectly = {
69 | val cfg = ConfigFactory.parseString("set: [1, 2, 2, 3]")
70 | traversableReader[Set, Int].read(cfg, "set") must beEqualTo(Set(1, 2, 3))
71 | }
72 |
73 | }
74 |
75 | object CollectionReaderSpec {
76 | import scala.collection._
77 |
78 | implicit def buildableIndexedSeq[T]: Buildable[T, IndexedSeq] = new Buildable[T, IndexedSeq] {
79 | def builder = IndexedSeq.newBuilder[T]
80 | }
81 |
82 | implicit def buildableVector[T]: Buildable[T, Vector] = new Buildable[T, Vector] {
83 | def builder = Vector.newBuilder[T]
84 | }
85 |
86 | implicit def buildableIterable[T]: Buildable[T, Iterable] = new Buildable[T, Iterable] {
87 | def builder = new mutable.ListBuffer[T]
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/main/scala/net/ceedubs/ficus/readers/ArbitraryTypeReader.scala:
--------------------------------------------------------------------------------
1 | package net.ceedubs.ficus.readers
2 |
3 | import net.ceedubs.ficus.util.ReflectionUtils
4 | import com.typesafe.config.Config
5 | import scala.language.experimental.macros
6 | import scala.reflect.internal.{StdNames, SymbolTable, Definitions}
7 |
8 | trait ArbitraryTypeReader {
9 | implicit def arbitraryTypeValueReader[T]: ValueReader[T] = macro ArbitraryTypeReaderMacros.arbitraryTypeValueReader[T]
10 | }
11 |
12 | object ArbitraryTypeReader extends ArbitraryTypeReader
13 |
14 | object ArbitraryTypeReaderMacros {
15 | import scala.reflect.macros.blackbox.Context
16 |
17 | def arbitraryTypeValueReader[T : c.WeakTypeTag](c: Context): c.Expr[ValueReader[T]] = {
18 | import c.universe._
19 |
20 | reify {
21 | new ValueReader[T] {
22 | def read(config: Config, path: String): T = instantiateFromConfig[T](c)(
23 | config = c.Expr[Config](Ident(TermName("config"))),
24 | path = c.Expr[String](Ident(TermName("path")))).splice
25 | }
26 | }
27 | }
28 |
29 | def instantiateFromConfig[T : c.WeakTypeTag](c: Context)(config: c.Expr[Config], path: c.Expr[String]): c.Expr[T] = {
30 | import c.universe._
31 |
32 | val returnType = c.weakTypeOf[T]
33 |
34 | def fail(reason: String) = c.abort(c.enclosingPosition, s"Cannot generate a config value reader for type $returnType, because $reason")
35 |
36 | val companionSymbol = returnType.typeSymbol.companion match {
37 | case NoSymbol => None
38 | case x => Some(x)
39 | }
40 |
41 | val instantiationMethod = ReflectionUtils.instantiationMethod[T](c, fail)
42 |
43 | val instantiationArgs = extractMethodArgsFromConfig[T](c)(method = instantiationMethod,
44 | companionObjectMaybe = companionSymbol, config = config, path = path, fail = fail)
45 | val instantiationObject = companionSymbol.filterNot(_ =>
46 | instantiationMethod.isConstructor
47 | ).map(Ident(_)).getOrElse(New(Ident(returnType.typeSymbol)))
48 | val instantiationCall = Select(instantiationObject, instantiationMethod.name)
49 | c.Expr[T](Apply(instantiationCall, instantiationArgs))
50 | }
51 |
52 | def extractMethodArgsFromConfig[T : c.WeakTypeTag](c: Context)(method: c.universe.MethodSymbol, companionObjectMaybe: Option[c.Symbol],
53 | config: c.Expr[Config], path: c.Expr[String], fail: String => Nothing): List[c.Tree] = {
54 | import c.universe._
55 |
56 | val decodedMethodName = method.name.decodedName.toString
57 |
58 | if (!method.isPublic) fail(s"'$decodedMethodName' method is not public")
59 |
60 | method.paramLists.head.zipWithIndex map { case (param, index) =>
61 | val name = param.name.decodedName.toString
62 | val key = q"""$path + "." + $name"""
63 | val returnType: Type = param.typeSignatureIn(c.weakTypeOf[T])
64 |
65 | companionObjectMaybe.filter(_ => param.asTerm.isParamWithDefault) map { companionObject =>
66 | val optionType = appliedType(weakTypeOf[Option[_]].typeConstructor, List(returnType))
67 | val optionReaderType = appliedType(weakTypeOf[ValueReader[_]].typeConstructor, List(optionType))
68 | val optionReader = c.inferImplicitValue(optionReaderType, silent = true) match {
69 | case EmptyTree => fail(s"an implicit value reader of type $optionReaderType must be in scope to read parameter '$name' on '$decodedMethodName' method since '$name' has a default value")
70 | case x => x
71 | }
72 | val argValueMaybe = q"$optionReader.read($config, $key)"
73 | Apply(Select(argValueMaybe, TermName("getOrElse")), List({
74 | // fall back to default value for param
75 | val u = c.universe.asInstanceOf[Definitions with SymbolTable with StdNames]
76 | val getter = u.nme.defaultGetterName(u.TermName(decodedMethodName), index + 1)
77 | Select(Ident(companionObject), TermName(getter.encoded))
78 | }))
79 | } getOrElse {
80 | val readerType = appliedType(weakTypeOf[ValueReader[_]].typeConstructor, List(returnType))
81 | val reader = c.inferImplicitValue(readerType, silent = true) match {
82 | case EmptyTree => fail(s"an implicit value reader of type $readerType must be in scope to read parameter '$name' on '$decodedMethodName' method")
83 | case x => x
84 | }
85 | q"$reader.read($config, $key)"
86 | }
87 | }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/test/scala/net/ceedubs/ficus/readers/CaseClassReadersSpec.scala:
--------------------------------------------------------------------------------
1 | package net.ceedubs.ficus
2 | package readers
3 |
4 | import com.typesafe.config.ConfigFactory
5 | import net.ceedubs.ficus.Ficus._
6 | import net.ceedubs.ficus.readers.ArbitraryTypeReader._
7 | import ConfigSerializerOps._
8 |
9 | object CaseClassReadersSpec {
10 | case class SimpleCaseClass(bool: Boolean)
11 | case class MultipleFields(string: String, long: Long)
12 | case class WithOption(option: Option[String])
13 | case class WithNestedCaseClass(simple: SimpleCaseClass)
14 | case class ValueClass(int: Int) extends AnyVal
15 | case class WithNestedValueClass(valueClass: ValueClass)
16 | case class WithDefault(string: String = "bar")
17 | case class Foo(bool: Boolean, intOpt: Option[Int], withNestedCaseClass: WithNestedCaseClass,
18 | withNestedValueClass: WithNestedValueClass)
19 | }
20 |
21 | class CaseClassReadersSpec extends Spec { def is = s2"""
22 | A case class reader should
23 | be able to be used implicitly $useImplicitly
24 | hydrate a simple case class $hydrateSimpleCaseClass
25 | hydrate a case class with multiple fields $multipleFields
26 | use another implicit value reader for a field $withOptionField
27 | read a nested case class $withNestedCaseClass
28 | read a top-level value class $topLevelValueClass
29 | read a nested value class $nestedValueClass
30 | fall back to a default value $fallbackToDefault
31 | do a combination of these things $combination
32 | """
33 |
34 | import CaseClassReadersSpec._
35 |
36 | def useImplicitly = {
37 | val cfg = ConfigFactory.parseString("simple { bool = false }")
38 | cfg.as[SimpleCaseClass]("simple") must_== SimpleCaseClass(bool = false)
39 | }
40 |
41 | def hydrateSimpleCaseClass = prop { bool: Boolean =>
42 | val cfg = ConfigFactory.parseString(s"simple { bool = $bool }")
43 | cfg.as[SimpleCaseClass]("simple") must_== SimpleCaseClass(bool = bool)
44 | }
45 |
46 | def multipleFields = prop { (foo: String, long: Long) =>
47 | val cfg = ConfigFactory.parseString(
48 | s"""
49 | |multipleFields {
50 | | string = ${foo.asConfigValue}
51 | | long = $long
52 | |}
53 | """.stripMargin)
54 | cfg.as[MultipleFields]("multipleFields") must_== MultipleFields(string = foo, long = long)
55 | }
56 |
57 | def withOptionField = prop { s: String =>
58 | val cfg = ConfigFactory.parseString(s"""withOption { option = ${s.asConfigValue} }""")
59 | cfg.as[WithOption]("withOption") must_== WithOption(Some(s))
60 | }
61 |
62 | def withNestedCaseClass = prop { bool: Boolean =>
63 | val cfg = ConfigFactory.parseString(
64 | s"""
65 | |withNested {
66 | | simple {
67 | | bool = $bool
68 | | }
69 | |}
70 | """.stripMargin)
71 | cfg.as[WithNestedCaseClass]("withNested") must_== WithNestedCaseClass(
72 | simple = SimpleCaseClass(bool = bool))
73 | }
74 |
75 | def topLevelValueClass = prop { int: Int =>
76 | val cfg = ConfigFactory.parseString(s"valueClass { int = $int }")
77 | cfg.as[ValueClass]("valueClass") must_== ValueClass(int)
78 | }
79 |
80 | def nestedValueClass = prop { int: Int =>
81 | val cfg = ConfigFactory.parseString(
82 | s"""
83 | |withNestedValueClass {
84 | | valueClass {
85 | | int = $int
86 | | }
87 | |}
88 | """.stripMargin)
89 | cfg.as[WithNestedValueClass]("withNestedValueClass") must_== WithNestedValueClass(
90 | valueClass = ValueClass(int = int))
91 | }
92 |
93 | def fallbackToDefault = {
94 | val cfg = ConfigFactory.parseString("""withDefault { }""")
95 | cfg.as[WithDefault]("withDefault") must_== WithDefault()
96 | }
97 |
98 | def combination = prop { (fooBool: Boolean, simpleBool: Boolean, valueClassInt: Int) =>
99 | val cfg = ConfigFactory.parseString(
100 | s"""
101 | |foo {
102 | | bool = $fooBool
103 | | withNestedCaseClass {
104 | | simple {
105 | | bool = $simpleBool
106 | | }
107 | | }
108 | | withNestedValueClass = {
109 | | valueClass {
110 | | int = $valueClassInt
111 | | }
112 | | }
113 | |}
114 | """.stripMargin)
115 | cfg.as[Foo]("foo") must_== Foo(
116 | bool = fooBool,
117 | intOpt = None,
118 | withNestedCaseClass = WithNestedCaseClass(simple = SimpleCaseClass(bool = simpleBool)),
119 | withNestedValueClass = WithNestedValueClass(ValueClass(int = valueClassInt))
120 | )
121 | }
122 |
123 | }
124 |
--------------------------------------------------------------------------------
/src/test/scala/net/ceedubs/ficus/readers/ArbitraryTypeReaderSpec.scala:
--------------------------------------------------------------------------------
1 | package net.ceedubs.ficus
2 | package readers
3 |
4 | import com.typesafe.config.ConfigFactory
5 | import ConfigSerializerOps._
6 | import shapeless.test.illTyped
7 |
8 | class ArbitraryTypeReaderSpec extends Spec { def is = s2"""
9 | An arbitrary type reader should
10 | instantiate with a single-param apply method $instantiateSingleParamApply
11 | instantiate with no apply method but a single constructor with a single param $instantiateSingleParamConstructor
12 | instantiate with a multi-param apply method $instantiateMultiParamApply
13 | instantiate with no apply method but a single constructor with multiple params $instantiateMultiParamConstructor
14 | instantiate with multiple apply methods if only one returns the correct type $multipleApply
15 | instantiate with primary constructor when no apply methods and multiple constructors $multipleConstructors
16 | use another implicit value reader for a field $withOptionField
17 | fall back to a default value on an apply method $fallBackToApplyMethodDefaultValue
18 | fall back to default values on an apply method if base key isn't in config $fallBackToApplyMethodDefaultValueNoKey
19 | fall back to a default value on a constructor arg $fallBackToConstructorDefaultValue
20 | fall back to a default values on a constructor if base key isn't in config $fallBackToConstructorDefaultValueNoKey
21 | ignore a default value on an apply method if a value is in config $ignoreApplyParamDefault
22 | ignore a default value in a constructor if a value is in config $ignoreConstructorParamDefault
23 | allow overriding of option reader for default values $overrideOptionReaderForDefault
24 | not choose between multiple Java constructors $notChooseBetweenJavaConstructors
25 | not be prioritized over a Reader defined in a type's companion object (when Ficus._ is imported) $notTrumpCompanionReader
26 | """
27 |
28 | import ArbitraryTypeReaderSpec._
29 |
30 | def instantiateSingleParamApply = prop { foo2: String =>
31 | import Ficus.stringValueReader
32 | import ArbitraryTypeReader._
33 | val cfg = ConfigFactory.parseString(s"simple { foo2 = ${foo2.asConfigValue} }")
34 | val instance: WithSimpleCompanionApply = arbitraryTypeValueReader[WithSimpleCompanionApply].read(cfg, "simple")
35 | instance.foo must_== foo2
36 | }
37 |
38 | def instantiateSingleParamConstructor = prop { foo: String =>
39 | import Ficus.stringValueReader
40 | import ArbitraryTypeReader._
41 | val cfg = ConfigFactory.parseString(s"singleParam { foo = ${foo.asConfigValue} }")
42 | val instance: ClassWithSingleParam = arbitraryTypeValueReader[ClassWithSingleParam].read(cfg, "singleParam")
43 | instance.getFoo must_== foo
44 | }
45 |
46 | def instantiateMultiParamApply = prop { (foo: String, bar: Int) =>
47 | import Ficus.{intValueReader, stringValueReader}
48 | import ArbitraryTypeReader._
49 | val cfg = ConfigFactory.parseString(
50 | s"""
51 | |multi {
52 | | foo = ${foo.asConfigValue}
53 | | bar = $bar
54 | |}""".stripMargin)
55 | val instance: WithMultiCompanionApply = arbitraryTypeValueReader[WithMultiCompanionApply].read(cfg, "multi")
56 | (instance.foo must_== foo) and (instance.bar must_== bar)
57 | }
58 |
59 | def instantiateMultiParamConstructor = prop { (foo: String, bar: Int) =>
60 | import Ficus.{intValueReader, stringValueReader}
61 | import ArbitraryTypeReader._
62 | val cfg = ConfigFactory.parseString(
63 | s"""
64 | |multi {
65 | | foo = ${foo.asConfigValue}
66 | | bar = $bar
67 | |}""".stripMargin)
68 | val instance: ClassWithMultipleParams = arbitraryTypeValueReader[ClassWithMultipleParams].read(cfg, "multi")
69 | (instance.foo must_== foo) and (instance.bar must_== bar)
70 | }
71 |
72 | def multipleApply = prop { foo: String =>
73 | import Ficus.stringValueReader
74 | import ArbitraryTypeReader._
75 | val cfg = ConfigFactory.parseString(s"withMultipleApply { foo = ${foo.asConfigValue} }")
76 | val instance: WithMultipleApplyMethods = arbitraryTypeValueReader[WithMultipleApplyMethods].read(cfg, "withMultipleApply")
77 | instance.foo must_== foo
78 | }
79 |
80 | def multipleConstructors = prop { foo: String =>
81 | import Ficus.stringValueReader
82 | import ArbitraryTypeReader._
83 | val cfg = ConfigFactory.parseString(s"withMultipleConstructors { foo = ${foo.asConfigValue} }")
84 | val instance: ClassWithMultipleConstructors = arbitraryTypeValueReader[ClassWithMultipleConstructors].read(cfg, "withMultipleConstructors")
85 | instance.foo must_== foo
86 | }
87 |
88 | def fallBackToApplyMethodDefaultValue = {
89 | import Ficus.{optionValueReader, stringValueReader}
90 | import ArbitraryTypeReader._
91 | val cfg = ConfigFactory.parseString("withDefault { }")
92 | arbitraryTypeValueReader[WithDefault].read(cfg, "withDefault").foo must_== "defaultFoo"
93 | }
94 |
95 | def fallBackToApplyMethodDefaultValueNoKey = {
96 | import Ficus.{optionValueReader, stringValueReader}
97 | import ArbitraryTypeReader._
98 | val cfg = ConfigFactory.parseString("")
99 | arbitraryTypeValueReader[WithDefault].read(cfg, "withDefault").foo must_== "defaultFoo"
100 | }
101 |
102 | def fallBackToConstructorDefaultValue = {
103 | import Ficus.{optionValueReader, stringValueReader}
104 | import ArbitraryTypeReader._
105 | val cfg = ConfigFactory.parseString("withDefault { }")
106 | arbitraryTypeValueReader[ClassWithDefault].read(cfg, "withDefault").foo must_== "defaultFoo"
107 | }
108 |
109 | def fallBackToConstructorDefaultValueNoKey = {
110 | import Ficus.{optionValueReader, stringValueReader}
111 | import ArbitraryTypeReader._
112 | val cfg = ConfigFactory.parseString("")
113 | arbitraryTypeValueReader[ClassWithDefault].read(cfg, "withDefault").foo must_== "defaultFoo"
114 | }
115 |
116 | def withOptionField = {
117 | import Ficus.{optionValueReader, stringValueReader}
118 | import ArbitraryTypeReader._
119 | val cfg = ConfigFactory.parseString("""withOption { option = "here" }""")
120 | arbitraryTypeValueReader[WithOption].read(cfg, "withOption").option must_== Some("here")
121 | }
122 |
123 | def ignoreApplyParamDefault = prop { foo: String =>
124 | import Ficus.{optionValueReader, stringValueReader}
125 | import ArbitraryTypeReader._
126 | val cfg = ConfigFactory.parseString(s"withDefault { foo = ${foo.asConfigValue} }")
127 | arbitraryTypeValueReader[WithDefault].read(cfg, "withDefault").foo must_== foo
128 | }
129 |
130 | def ignoreConstructorParamDefault = prop { foo: String =>
131 | import Ficus.{optionValueReader, stringValueReader}
132 | import ArbitraryTypeReader._
133 | val cfg = ConfigFactory.parseString(s"withDefault { foo = ${foo.asConfigValue} }")
134 | arbitraryTypeValueReader[ClassWithDefault].read(cfg, "withDefault").foo must_== foo
135 | }
136 |
137 | def overrideOptionReaderForDefault = {
138 | import ArbitraryTypeReader._
139 | implicit val stringOptionReader: ValueReader[Option[String]] = Ficus.stringValueReader map { s =>
140 | if (s.isEmpty) None else Some(s)
141 | }
142 | val cfg = ConfigFactory.parseString("""withDefault { foo = "" }""")
143 | arbitraryTypeValueReader[ClassWithDefault].read(cfg, "withDefault").foo must beEqualTo("defaultFoo")
144 | }
145 |
146 | def notChooseBetweenJavaConstructors = {
147 | illTyped("implicitly[ValueReader[String]]")
148 | illTyped("implicitly[ValueReader[Long]]")
149 | illTyped("implicitly[ValueReader[Int]]")
150 | illTyped("implicitly[ValueReader[Float]]")
151 | illTyped("implicitly[ValueReader[Double]]")
152 | illTyped("implicitly[ValueReader[Char]]")
153 | success // failure would result in compile error
154 | }
155 |
156 | def notTrumpCompanionReader = {
157 | import Ficus._
158 | val cfg = ConfigFactory.parseString("""withReaderInCompanion { foo = "bar" }""")
159 | WithReaderInCompanion("from-companion") ==== cfg.as[WithReaderInCompanion]("withReaderInCompanion")
160 | }
161 | }
162 |
163 | object ArbitraryTypeReaderSpec {
164 | trait WithSimpleCompanionApply {
165 | def foo: String
166 | }
167 |
168 | object WithSimpleCompanionApply {
169 | def apply(foo2: String): WithSimpleCompanionApply = new WithSimpleCompanionApply {
170 | val foo = foo2
171 | }
172 | }
173 |
174 | trait WithMultiCompanionApply {
175 | def foo: String
176 | def bar: Int
177 | }
178 |
179 | object WithMultiCompanionApply {
180 | def apply(foo: String, bar: Int): WithMultiCompanionApply = {
181 | val (_foo, _bar) = (foo, bar)
182 | new WithMultiCompanionApply {
183 | val foo = _foo
184 | val bar = _bar
185 | }
186 | }
187 | }
188 |
189 | trait WithDefault {
190 | def foo: String
191 | }
192 |
193 | object WithDefault {
194 | def apply(foo: String = "defaultFoo"): WithDefault = {
195 | val _foo = foo
196 | new WithDefault {
197 | val foo = _foo
198 | }
199 | }
200 | }
201 |
202 | trait WithOption {
203 | def option: Option[String]
204 | }
205 |
206 | object WithOption {
207 | def apply(option: Option[String]): WithOption = {
208 | val _option = option
209 | new WithOption {
210 | val option = _option
211 | }
212 | }
213 | }
214 |
215 | class ClassWithSingleParam(foo: String) {
216 | def getFoo = foo
217 | }
218 |
219 | class ClassWithMultipleParams(val foo: String, val bar: Int)
220 |
221 | class ClassWithDefault(val foo: String = "defaultFoo")
222 |
223 | trait WithMultipleApplyMethods {
224 | def foo: String
225 | }
226 |
227 | object WithMultipleApplyMethods {
228 |
229 | def apply(foo: Option[String]): Option[WithMultipleApplyMethods] = foo map { f =>
230 | new WithMultipleApplyMethods {
231 | def foo: String = f
232 | }
233 | }
234 |
235 | def apply(foo: String): WithMultipleApplyMethods = {
236 | val _foo = foo
237 | new WithMultipleApplyMethods {
238 | def foo: String = _foo
239 | }
240 | }
241 | }
242 |
243 | class ClassWithMultipleConstructors(val foo: String) {
244 | def this(fooInt: Int) = this(fooInt.toString)
245 | }
246 |
247 | case class WithReaderInCompanion(foo: String)
248 |
249 | object WithReaderInCompanion {
250 | implicit val reader: ValueReader[WithReaderInCompanion] =
251 | ValueReader.relative(_ => WithReaderInCompanion("from-companion"))
252 | }
253 |
254 | }
255 |
--------------------------------------------------------------------------------
/sbt:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | #
3 | # A more capable sbt runner, coincidentally also called sbt.
4 | # Author: Paul Phillips
5 |
6 | # todo - make this dynamic
7 | declare -r sbt_release_version=0.13.0
8 |
9 | declare sbt_jar sbt_dir sbt_create sbt_launch_dir
10 | declare scala_version java_home sbt_explicit_version
11 | declare verbose debug quiet noshare batch trace_level log_level
12 | declare sbt_saved_stty
13 |
14 | echoerr () { [[ -z $quiet ]] && echo "$@" >&2; }
15 | vlog () { [[ -n "$verbose$debug" ]] && echoerr "$@"; }
16 | dlog () { [[ -n $debug ]] && echoerr "$@"; }
17 |
18 | # we'd like these set before we get around to properly processing arguments
19 | for arg in "$@"; do
20 | case $arg in
21 | -q|-quiet) quiet=true ;;
22 | -d|-debug) debug=true ;;
23 | -v|-verbose) verbose=true ;;
24 | *) ;;
25 | esac
26 | done
27 |
28 | build_props_sbt () {
29 | if [[ -r project/build.properties ]]; then
30 | versionLine=$(grep ^sbt.version project/build.properties | tr -d ' \r')
31 | versionString=${versionLine##sbt.version=}
32 | echo "$versionString"
33 | fi
34 | }
35 |
36 | update_build_props_sbt () {
37 | local ver="$1"
38 | local old=$(build_props_sbt)
39 |
40 | if [[ $ver == $old ]]; then
41 | return
42 | elif [[ -r project/build.properties ]]; then
43 | perl -pi -e "s/^sbt\.version[ ]*=.*\$/sbt.version=${ver}/" project/build.properties
44 | grep -q '^sbt.version[ ]*=' project/build.properties || printf "\nsbt.version=${ver}\n" >> project/build.properties
45 |
46 | echoerr !!!
47 | echoerr !!! Updated file project/build.properties setting sbt.version to: $ver
48 | echoerr !!! Previous value was: $old
49 | echoerr !!!
50 | fi
51 | }
52 |
53 | sbt_version () {
54 | if [[ -n $sbt_explicit_version ]]; then
55 | echo $sbt_explicit_version
56 | else
57 | local v=$(build_props_sbt)
58 | if [[ -n $v ]]; then
59 | echo $v
60 | else
61 | echo $sbt_release_version
62 | fi
63 | fi
64 | }
65 |
66 | # restore stty settings (echo in particular)
67 | onSbtRunnerExit() {
68 | [[ -n $sbt_saved_stty ]] || return
69 | dlog ""
70 | dlog "restoring stty: $sbt_saved_stty"
71 | stty $sbt_saved_stty
72 | unset sbt_saved_stty
73 | }
74 |
75 | # save stty and trap exit, to ensure echo is reenabled if we are interrupted.
76 | trap onSbtRunnerExit EXIT
77 | sbt_saved_stty=$(stty -g 2>/dev/null)
78 | dlog "Saved stty: $sbt_saved_stty"
79 |
80 | # this seems to cover the bases on OSX, and someone will
81 | # have to tell me about the others.
82 | get_script_path () {
83 | local path="$1"
84 | [[ -L "$path" ]] || { echo "$path" ; return; }
85 |
86 | local target=$(readlink "$path")
87 | if [[ "${target:0:1}" == "/" ]]; then
88 | echo "$target"
89 | else
90 | echo "$(dirname $path)/$target"
91 | fi
92 | }
93 |
94 | die() {
95 | echo "Aborting: $@"
96 | exit 1
97 | }
98 |
99 | make_url () {
100 | version="$1"
101 |
102 | echo "$sbt_launch_repo/org.scala-sbt/sbt-launch/$version/sbt-launch.jar"
103 | }
104 |
105 | readarr () {
106 | while read ; do
107 | eval "$1+=(\"$REPLY\")"
108 | done
109 | }
110 |
111 | init_default_option_file () {
112 | local overriding_var=${!1}
113 | local default_file=$2
114 | if [[ ! -r "$default_file" && $overriding_var =~ ^@(.*)$ ]]; then
115 | local envvar_file=${BASH_REMATCH[1]}
116 | if [[ -r $envvar_file ]]; then
117 | default_file=$envvar_file
118 | fi
119 | fi
120 | echo $default_file
121 | }
122 |
123 | declare -r cms_opts="-XX:+CMSClassUnloadingEnabled -XX:+UseConcMarkSweepGC"
124 | declare -r jit_opts="-XX:ReservedCodeCacheSize=256m -XX:+TieredCompilation"
125 | declare -r default_jvm_opts="-Dfile.encoding=UTF8 -XX:MaxPermSize=384m -Xms512m -Xmx1536m -Xss2m $jit_opts $cms_opts"
126 | declare -r noshare_opts="-Dsbt.global.base=project/.sbtboot -Dsbt.boot.directory=project/.boot -Dsbt.ivy.home=project/.ivy"
127 | declare -r latest_28="2.8.2"
128 | declare -r latest_29="2.9.3"
129 | declare -r latest_210="2.10.3"
130 | declare -r latest_211="2.11.0-M5"
131 |
132 | declare -r script_path=$(get_script_path "$BASH_SOURCE")
133 | declare -r script_dir="$(dirname $script_path)"
134 | declare -r script_name="$(basename $script_path)"
135 |
136 | # some non-read-onlies set with defaults
137 | declare java_cmd=java
138 | declare sbt_opts_file=$(init_default_option_file SBT_OPTS .sbtopts)
139 | declare jvm_opts_file=$(init_default_option_file JVM_OPTS .jvmopts)
140 | declare sbt_launch_repo="http://typesafe.artifactoryonline.com/typesafe/ivy-releases"
141 |
142 | # pull -J and -D options to give to java.
143 | declare -a residual_args
144 | declare -a java_args
145 | declare -a scalac_args
146 | declare -a sbt_commands
147 |
148 | # args to jvm/sbt via files or environment variables
149 | declare -a extra_jvm_opts extra_sbt_opts
150 |
151 | # if set, use JAVA_HOME over java found in path
152 | [[ -e "$JAVA_HOME/bin/java" ]] && java_cmd="$JAVA_HOME/bin/java"
153 |
154 | # directory to store sbt launchers
155 | declare sbt_launch_dir="$HOME/.sbt/launchers"
156 | [[ -d "$sbt_launch_dir" ]] || mkdir -p "$sbt_launch_dir"
157 | [[ -w "$sbt_launch_dir" ]] || sbt_launch_dir="$(mktemp -d -t sbt_extras_launchers)"
158 |
159 | build_props_scala () {
160 | if [[ -r project/build.properties ]]; then
161 | versionLine=$(grep ^build.scala.versions project/build.properties)
162 | versionString=${versionLine##build.scala.versions=}
163 | echo ${versionString%% .*}
164 | fi
165 | }
166 |
167 | execRunner () {
168 | # print the arguments one to a line, quoting any containing spaces
169 | [[ $verbose || $debug ]] && echo "# Executing command line:" && {
170 | for arg; do
171 | if [[ -n "$arg" ]]; then
172 | if printf "%s\n" "$arg" | grep -q ' '; then
173 | printf "\"%s\"\n" "$arg"
174 | else
175 | printf "%s\n" "$arg"
176 | fi
177 | fi
178 | done
179 | echo ""
180 | }
181 |
182 | if [[ -n $batch ]]; then
183 | exec /dev/null; then
212 | curl --fail --silent "$url" --output "$jar"
213 | elif which wget >/dev/null; then
214 | wget --quiet -O "$jar" "$url"
215 | fi
216 | } && [[ -r "$jar" ]]
217 | }
218 |
219 | acquire_sbt_jar () {
220 | for_sbt_version="$(sbt_version)"
221 | sbt_url="$(jar_url $for_sbt_version)"
222 | sbt_jar="$(jar_file $for_sbt_version)"
223 |
224 | [[ -r "$sbt_jar" ]] || download_url "$sbt_url" "$sbt_jar"
225 | }
226 |
227 | usage () {
228 | cat < display stack traces with a max of frames (default: -1, traces suppressed)
236 | -no-colors disable ANSI color codes
237 | -sbt-create start sbt even if current directory contains no sbt project
238 | -sbt-dir path to global settings/plugins directory (default: ~/.sbt/)
239 | -sbt-boot path to shared boot directory (default: ~/.sbt/boot in 0.11+)
240 | -ivy path to local Ivy repository (default: ~/.ivy2)
241 | -no-share use all local caches; no sharing
242 | -offline put sbt in offline mode
243 | -jvm-debug Turn on JVM debugging, open at the given port.
244 | -batch Disable interactive mode
245 | -prompt Set the sbt prompt; in expr, 's' is the State and 'e' is Extracted
246 |
247 | # sbt version (default: from project/build.properties if present, else latest release)
248 | !!! The only way to accomplish this pre-0.12.0 if there is a build.properties file which
249 | !!! contains an sbt.version property is to update the file on disk. That's what this does.
250 | -sbt-version use the specified version of sbt (default: $sbt_release_version)
251 | -sbt-jar use the specified jar as the sbt launcher
252 | -sbt-launch-dir directory to hold sbt launchers (default: $sbt_launch_dir)
253 | -sbt-launch-repo repo url for downloading sbt launcher jar (default: $sbt_launch_repo)
254 |
255 | # scala version (default: as chosen by sbt)
256 | -28 use $latest_28
257 | -29 use $latest_29
258 | -210 use $latest_210
259 | -211 use $latest_211
260 | -scala-home use the scala build at the specified directory
261 | -scala-version use the specified version of scala
262 | -binary-version use the specified scala version when searching for dependencies
263 |
264 | # java version (default: java from PATH, currently $(java -version 2>&1 | grep version))
265 | -java-home alternate JAVA_HOME
266 |
267 | # passing options to the jvm - note it does NOT use JAVA_OPTS due to pollution
268 | # The default set is used if JVM_OPTS is unset and no -jvm-opts file is found
269 | $default_jvm_opts
270 | JVM_OPTS environment variable holding either the jvm args directly, or
271 | the reference to a file containing jvm args if given path is prepended by '@' (e.g. '@/etc/jvmopts')
272 | Note: "@"-file is overridden by local '.jvmopts' or '-jvm-opts' argument.
273 | -jvm-opts file containing jvm args (if not given, .jvmopts in project root is used if present)
274 | -Dkey=val pass -Dkey=val directly to the jvm
275 | -J-X pass option -X directly to the jvm (-J is stripped)
276 |
277 | # passing options to sbt, OR to this runner
278 | SBT_OPTS environment variable holding either the sbt args directly, or
279 | the reference to a file containing sbt args if given path is prepended by '@' (e.g. '@/etc/sbtopts')
280 | Note: "@"-file is overridden by local '.sbtopts' or '-sbt-opts' argument.
281 | -sbt-opts file containing sbt args (if not given, .sbtopts in project root is used if present)
282 | -S-X add -X to sbt's scalacOptions (-S is stripped)
283 | EOM
284 | }
285 |
286 | addJava () {
287 | dlog "[addJava] arg = '$1'"
288 | java_args=( "${java_args[@]}" "$1" )
289 | }
290 | addSbt () {
291 | dlog "[addSbt] arg = '$1'"
292 | sbt_commands=( "${sbt_commands[@]}" "$1" )
293 | }
294 | addScalac () {
295 | dlog "[addScalac] arg = '$1'"
296 | scalac_args=( "${scalac_args[@]}" "$1" )
297 | }
298 | addResidual () {
299 | dlog "[residual] arg = '$1'"
300 | residual_args=( "${residual_args[@]}" "$1" )
301 | }
302 | addResolver () {
303 | addSbt "set resolvers += $1"
304 | }
305 | addDebugger () {
306 | addJava "-Xdebug"
307 | addJava "-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=$1"
308 | }
309 | setScalaVersion () {
310 | [[ "$1" == *-SNAPSHOT ]] && addResolver 'Resolver.sonatypeRepo("snapshots")'
311 | addSbt "++ $1"
312 | }
313 |
314 | process_args ()
315 | {
316 | require_arg () {
317 | local type="$1"
318 | local opt="$2"
319 | local arg="$3"
320 |
321 | if [[ -z "$arg" ]] || [[ "${arg:0:1}" == "-" ]]; then
322 | die "$opt requires <$type> argument"
323 | fi
324 | }
325 | while [[ $# -gt 0 ]]; do
326 | case "$1" in
327 | -h|-help) usage; exit 1 ;;
328 | -v|-verbose) verbose=true && log_level=Info && shift ;;
329 | -d|-debug) debug=true && log_level=Debug && shift ;;
330 | -q|-quiet) quiet=true && log_level=Error && shift ;;
331 |
332 | -trace) require_arg integer "$1" "$2" && trace_level=$2 && shift 2 ;;
333 | -ivy) require_arg path "$1" "$2" && addJava "-Dsbt.ivy.home=$2" && shift 2 ;;
334 | -no-colors) addJava "-Dsbt.log.noformat=true" && shift ;;
335 | -no-share) noshare=true && shift ;;
336 | -sbt-boot) require_arg path "$1" "$2" && addJava "-Dsbt.boot.directory=$2" && shift 2 ;;
337 | -sbt-dir) require_arg path "$1" "$2" && sbt_dir="$2" && shift 2 ;;
338 | -debug-inc) addJava "-Dxsbt.inc.debug=true" && shift ;;
339 | -offline) addSbt "set offline := true" && shift ;;
340 | -jvm-debug) require_arg port "$1" "$2" && addDebugger $2 && shift 2 ;;
341 | -batch) batch=true && shift ;;
342 | -prompt) require_arg "expr" "$1" "$2" && addSbt "set shellPrompt in ThisBuild := (s => { val e = Project.extract(s) ; $2 })" && shift 2 ;;
343 |
344 | -sbt-create) sbt_create=true && shift ;;
345 | -sbt-jar) require_arg path "$1" "$2" && sbt_jar="$2" && shift 2 ;;
346 | -sbt-version) require_arg version "$1" "$2" && sbt_explicit_version="$2" && shift 2 ;;
347 | -sbt-launch-dir) require_arg path "$1" "$2" && sbt_launch_dir="$2" && shift 2 ;;
348 | -sbt-launch-repo) require_arg path "$1" "$2" && sbt_launch_repo="$2" && shift 2 ;;
349 | -scala-version) require_arg version "$1" "$2" && setScalaVersion "$2" && shift 2 ;;
350 | -binary-version) require_arg version "$1" "$2" && addSbt "set scalaBinaryVersion in ThisBuild := \"$2\"" && shift 2 ;;
351 | -scala-home) require_arg path "$1" "$2" && addSbt "set every scalaHome := Some(file(\"$2\"))" && shift 2 ;;
352 | -java-home) require_arg path "$1" "$2" && java_cmd="$2/bin/java" && shift 2 ;;
353 | -sbt-opts) require_arg path "$1" "$2" && sbt_opts_file="$2" && shift 2 ;;
354 | -jvm-opts) require_arg path "$1" "$2" && jvm_opts_file="$2" && shift 2 ;;
355 |
356 | -D*) addJava "$1" && shift ;;
357 | -J*) addJava "${1:2}" && shift ;;
358 | -S*) addScalac "${1:2}" && shift ;;
359 | -28) setScalaVersion $latest_28 && shift ;;
360 | -29) setScalaVersion $latest_29 && shift ;;
361 | -210) setScalaVersion $latest_210 && shift ;;
362 | -211) setScalaVersion $latest_211 && shift ;;
363 |
364 | *) addResidual "$1" && shift ;;
365 | esac
366 | done
367 | }
368 |
369 | # process the direct command line arguments
370 | process_args "$@"
371 |
372 | # skip #-styled comments
373 | readConfigFile() {
374 | while read line; do echo ${line/\#*/} | grep -vE '^\s*$'; done < $1
375 | }
376 |
377 | # if there are file/environment sbt_opts, process again so we
378 | # can supply args to this runner
379 | if [[ -r "$sbt_opts_file" ]]; then
380 | vlog "Using sbt options defined in file $sbt_opts_file"
381 | readarr extra_sbt_opts < <(readConfigFile "$sbt_opts_file")
382 | elif [[ -n "$SBT_OPTS" && !($SBT_OPTS =~ ^@.*) ]]; then
383 | vlog "Using sbt options defined in variable \$SBT_OPTS"
384 | extra_sbt_opts=( $SBT_OPTS )
385 | else
386 | vlog "No extra sbt options have been defined"
387 | fi
388 |
389 | [[ -n $extra_sbt_opts ]] && process_args "${extra_sbt_opts[@]}"
390 |
391 | # reset "$@" to the residual args
392 | set -- "${residual_args[@]}"
393 | argumentCount=$#
394 |
395 | # only exists in 0.12+
396 | setTraceLevel() {
397 | case $(sbt_version) in
398 | 0.{7,10,11}.*) echoerr "Cannot set trace level in sbt version $(sbt_version)" ;;
399 | *) addSbt "set every traceLevel := $trace_level" ;;
400 | esac
401 | }
402 |
403 | # set scalacOptions if we were given any -S opts
404 | [[ ${#scalac_args[@]} -eq 0 ]] || addSbt "set scalacOptions in ThisBuild += \"${scalac_args[@]}\""
405 |
406 | # Update build.properties on disk to set explicit version - sbt gives us no choice
407 | [[ -n "$sbt_explicit_version" ]] && update_build_props_sbt "$sbt_explicit_version"
408 | vlog "Detected sbt version $(sbt_version)"
409 |
410 | [[ -n "$scala_version" ]] && echoerr "Overriding scala version to $scala_version"
411 |
412 | # no args - alert them there's stuff in here
413 | (( $argumentCount > 0 )) || {
414 | vlog "Starting $script_name: invoke with -help for other options"
415 | residual_args=( shell )
416 | }
417 |
418 | # verify this is an sbt dir or -create was given
419 | [[ -r ./build.sbt || -d ./project || -n "$sbt_create" ]] || {
420 | cat <