├── 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 <