├── project ├── build.properties ├── plugins.sbt └── Publish.scala ├── version.sbt ├── .gitignore ├── src ├── test │ ├── scala-2 │ │ └── net │ │ │ └── ceedubs │ │ │ └── ficus │ │ │ ├── Scala3Compat.scala │ │ │ ├── readers │ │ │ ├── LocalDateReaderSpec.scala │ │ │ ├── CaseInsensitiveEnumerationReadersSpec.scala │ │ │ ├── ISOZonedDateTimeReaderSpec.scala │ │ │ ├── PeriodReaderSpec.scala │ │ │ ├── ChronoUnitReaderSpec.scala │ │ │ ├── ConfigReaderSpec.scala │ │ │ ├── CaseClassReadersSpec.scala │ │ │ └── ArbitraryTypeReaderSpec.scala │ │ │ ├── Examples.scala │ │ │ ├── FicusConfigSpec.scala │ │ │ └── ExampleSpec.scala │ ├── scala │ │ └── net │ │ │ └── ceedubs │ │ │ └── ficus │ │ │ ├── Spec.scala │ │ │ ├── readers │ │ │ ├── SymbolReaderSpec.scala │ │ │ ├── StringReaderSpec.scala │ │ │ ├── OptionReadersSpec.scala │ │ │ ├── ValueReaderSpec.scala │ │ │ ├── URIReaderSpec.scala │ │ │ ├── URLReaderSpec.scala │ │ │ ├── HyphenNameMapperSpec.scala │ │ │ ├── HyphenNameMapperNoDigitsSpec.scala │ │ │ ├── AnyValReadersSpec.scala │ │ │ ├── TryReaderSpec.scala │ │ │ ├── DurationReadersSpec.scala │ │ │ ├── ConfigValueReaderSpec.scala │ │ │ ├── EnumerationReadersSpec.scala │ │ │ ├── EitherReadersSpec.scala │ │ │ ├── InetSocketAddressReadersSpec.scala │ │ │ ├── CollectionReadersSpec.scala │ │ │ └── BigNumberReadersSpec.scala │ │ │ └── ConfigSerializer.scala │ ├── scala-3 │ │ └── net │ │ │ └── ceedubs │ │ │ └── ficus │ │ │ └── Scala3Compat.scala │ └── scala-2.13 │ │ └── net │ │ └── ceedubs │ │ └── ficus │ │ └── Issue82Spec.scala └── main │ ├── scala-2.13+ │ ├── macrocompat │ │ └── bundle.scala │ └── net │ │ └── ceedubs │ │ └── ficus │ │ └── readers │ │ └── CollectionReaders.scala │ ├── scala-3 │ └── net │ │ └── ceedubs │ │ └── ficus │ │ ├── readers │ │ └── ArbitraryTypeReader.scala │ │ └── util │ │ └── EnumerationUtil.scala │ ├── scala │ └── net │ │ └── ceedubs │ │ └── ficus │ │ ├── readers │ │ ├── Generated.scala │ │ ├── namemappers │ │ │ ├── package.scala │ │ │ ├── HyphenNameMapper.scala │ │ │ └── HyphenNameMapperNoDigits.scala │ │ ├── StringReader.scala │ │ ├── SymbolReader.scala │ │ ├── CaseInsensitiveEnumerationReader.scala │ │ ├── PeriodReader.scala │ │ ├── TryReader.scala │ │ ├── ConfigValueReader.scala │ │ ├── LocalDateReader.scala │ │ ├── OptionReader.scala │ │ ├── AllValueReaderInstances.scala │ │ ├── ChronoUnitReader.scala │ │ ├── ConfigReader.scala │ │ ├── URLReader.scala │ │ ├── URIReaders.scala │ │ ├── ISOZonedDateTimeReader.scala │ │ ├── EitherReader.scala │ │ ├── AnyValReaders.scala │ │ ├── BigNumberReaders.scala │ │ ├── NameMapper.scala │ │ ├── EnumerationReader.scala │ │ ├── DurationReaders.scala │ │ ├── InetSocketAddressReaders.scala │ │ └── ValueReader.scala │ │ ├── ConfigKey.scala │ │ ├── Ficus.scala │ │ └── FicusConfig.scala │ ├── scala-2 │ └── net │ │ └── ceedubs │ │ └── ficus │ │ ├── util │ │ ├── EnumerationUtil.scala │ │ └── ReflectionUtils.scala │ │ └── readers │ │ └── ArbitraryTypeReader.scala │ └── scala-2.13- │ └── net │ └── ceedubs │ └── ficus │ └── readers │ └── CollectionReaders.scala ├── CONTRIBUTORS.md ├── .scalafmt.conf ├── .github └── workflows │ ├── format.yml │ ├── ci.yml │ └── clean.yml ├── LICENSE └── README.md /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.5.4 2 | -------------------------------------------------------------------------------- /version.sbt: -------------------------------------------------------------------------------- 1 | ThisBuild / version := "1.5.3-SNAPSHOT" 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .idea_modules 3 | target 4 | *.swp 5 | *~ 6 | .DS_Store 7 | .lib 8 | -------------------------------------------------------------------------------- /src/test/scala-2/net/ceedubs/ficus/Scala3Compat.scala: -------------------------------------------------------------------------------- 1 | package net.ceedubs.ficus 2 | 3 | trait Scala3Compat 4 | -------------------------------------------------------------------------------- /src/main/scala-2.13+/macrocompat/bundle.scala: -------------------------------------------------------------------------------- 1 | package macrocompat 2 | 3 | class bundle extends scala.annotation.Annotation 4 | -------------------------------------------------------------------------------- /src/main/scala-3/net/ceedubs/ficus/readers/ArbitraryTypeReader.scala: -------------------------------------------------------------------------------- 1 | package net.ceedubs.ficus.readers 2 | 3 | // TODO 4 | trait ArbitraryTypeReader 5 | -------------------------------------------------------------------------------- /src/main/scala/net/ceedubs/ficus/readers/Generated.scala: -------------------------------------------------------------------------------- 1 | package net.ceedubs.ficus.readers 2 | 3 | case class Generated[+A](value: A) extends AnyVal 4 | -------------------------------------------------------------------------------- /src/main/scala-2/net/ceedubs/ficus/util/EnumerationUtil.scala: -------------------------------------------------------------------------------- 1 | package net.ceedubs.ficus.util 2 | 3 | private[ficus] object EnumerationUtil { 4 | type EnumValue[A <: Enumeration] = A#Value 5 | } 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 with Scala3Compat 7 | -------------------------------------------------------------------------------- /src/test/scala-3/net/ceedubs/ficus/Scala3Compat.scala: -------------------------------------------------------------------------------- 1 | package net.ceedubs.ficus 2 | 3 | import org.specs2._ 4 | 5 | trait Scala3Compat extends Specification { 6 | implicit final class MustEqualExtension[A](a1: A) { 7 | def must_==(a2: A) = a1 must beEqualTo(a2) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/scala-3/net/ceedubs/ficus/util/EnumerationUtil.scala: -------------------------------------------------------------------------------- 1 | package net.ceedubs.ficus.util 2 | 3 | private[ficus] object EnumerationUtil { 4 | private[this] type Aux[A] = { type Value = A } 5 | 6 | type EnumValue[A <: Enumeration] = A match { 7 | case Aux[a] => a 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/scala/net/ceedubs/ficus/readers/namemappers/package.scala: -------------------------------------------------------------------------------- 1 | package net.ceedubs.ficus.readers 2 | 3 | package object namemappers { 4 | object implicits { 5 | implicit val hyphenCase: NameMapper = HyphenNameMapper 6 | implicit val hyphenCaseNoDigits: NameMapper = HyphenNameMapperNoDigits 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /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/SymbolReader.scala: -------------------------------------------------------------------------------- 1 | package net.ceedubs.ficus.readers 2 | 3 | import com.typesafe.config.Config 4 | 5 | trait SymbolReader { 6 | implicit val symbolValueReader: ValueReader[Symbol] = new ValueReader[Symbol] { 7 | def read(config: Config, path: String): Symbol = Symbol(config.getString(path)) 8 | } 9 | } 10 | 11 | object SymbolReader extends SymbolReader 12 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = 3.4.3 2 | runner.dialect = scala213 3 | preset = default 4 | align.preset = most 5 | maxColumn = 120 6 | project.git = true 7 | docstrings.style = AsteriskSpace 8 | align.tokens."+" = [ 9 | {code = ":=", owner = "Term.ApplyInfix"} 10 | ] 11 | rewrite.rules = [RedundantBraces, RedundantParens] 12 | 13 | fileOverride { 14 | "glob:**/src/main/scala-3/**" { 15 | runner.dialect = scala3 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/scala/net/ceedubs/ficus/readers/CaseInsensitiveEnumerationReader.scala: -------------------------------------------------------------------------------- 1 | package net.ceedubs.ficus.readers 2 | 3 | import net.ceedubs.ficus.util.EnumerationUtil.EnumValue 4 | 5 | trait CaseInsensitiveEnumerationReader extends EnumerationReader { 6 | 7 | override protected def findEnumValue[T <: Enumeration](`enum`: T, configValue: String): Option[EnumValue[T]] = 8 | `enum`.values.find(_.toString.toLowerCase == configValue.toLowerCase) 9 | } 10 | -------------------------------------------------------------------------------- /src/main/scala/net/ceedubs/ficus/readers/PeriodReader.scala: -------------------------------------------------------------------------------- 1 | package net.ceedubs.ficus.readers 2 | 3 | import java.time.Period 4 | 5 | import com.typesafe.config.Config 6 | 7 | trait PeriodReader { 8 | implicit val periodReader: ValueReader[Period] = new ValueReader[Period] { 9 | override def read(config: Config, path: String): Period = 10 | Period.parse(config.getString(path)) 11 | } 12 | } 13 | 14 | object PeriodReader extends PeriodReader 15 | -------------------------------------------------------------------------------- /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 13 | -------------------------------------------------------------------------------- /src/main/scala/net/ceedubs/ficus/readers/LocalDateReader.scala: -------------------------------------------------------------------------------- 1 | package net.ceedubs.ficus.readers 2 | 3 | import java.time.LocalDate 4 | 5 | import com.typesafe.config.Config 6 | 7 | trait LocalDateReader { 8 | implicit val localDateReader: ValueReader[LocalDate] = new ValueReader[LocalDate] { 9 | override def read(config: Config, path: String): LocalDate = 10 | LocalDate.parse(config.getString(path)) 11 | } 12 | } 13 | 14 | object LocalDateReader extends LocalDateReader 15 | -------------------------------------------------------------------------------- /src/main/scala/net/ceedubs/ficus/readers/namemappers/HyphenNameMapper.scala: -------------------------------------------------------------------------------- 1 | package net.ceedubs.ficus.readers.namemappers 2 | 3 | import net.ceedubs.ficus.readers.NameMapper 4 | 5 | object HyphenNameMapper extends NameMapper { 6 | private lazy val r = "((?<=[a-z0-9])[A-Z]|(?<=[a-zA-Z])[0-9]|(?!^)[A-Z](?=[a-z]))".r 7 | 8 | /** Maps from a camelCasedName to a hyphenated-name 9 | */ 10 | override def map(name: String): String = r.replaceAllIn(name, m => s"-${m.group(1)}").toLowerCase 11 | } 12 | -------------------------------------------------------------------------------- /src/main/scala/net/ceedubs/ficus/readers/namemappers/HyphenNameMapperNoDigits.scala: -------------------------------------------------------------------------------- 1 | package net.ceedubs.ficus.readers.namemappers 2 | 3 | import net.ceedubs.ficus.readers.NameMapper 4 | 5 | object HyphenNameMapperNoDigits extends NameMapper { 6 | private lazy val r = "((?<=[a-z0-9])[A-Z]|(?!^)[A-Z](?=[a-z]))".r 7 | 8 | /** Maps from a camelCasedName to a hyphenated-name 9 | */ 10 | override def map(name: String): String = r.replaceAllIn(name, m => s"-${m.group(1)}").toLowerCase 11 | } 12 | -------------------------------------------------------------------------------- /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]] = 7 | new ValueReader[Option[A]] { 8 | def read(config: Config, path: String): Option[A] = 9 | if (config.hasPath(path)) { 10 | Some(valueReader.read(config, path)) 11 | } else { 12 | None 13 | } 14 | } 15 | } 16 | 17 | object OptionReader extends OptionReader 18 | -------------------------------------------------------------------------------- /src/main/scala/net/ceedubs/ficus/readers/AllValueReaderInstances.scala: -------------------------------------------------------------------------------- 1 | package net.ceedubs.ficus.readers 2 | 3 | trait AllValueReaderInstances 4 | extends AnyValReaders 5 | with StringReader 6 | with SymbolReader 7 | with OptionReader 8 | with CollectionReaders 9 | with ConfigReader 10 | with DurationReaders 11 | with ArbitraryTypeReader 12 | with TryReader 13 | with ConfigValueReader 14 | with PeriodReader 15 | with ChronoUnitReader 16 | with LocalDateReader 17 | 18 | object AllValueReaderInstances extends AllValueReaderInstances 19 | -------------------------------------------------------------------------------- /src/main/scala/net/ceedubs/ficus/readers/ChronoUnitReader.scala: -------------------------------------------------------------------------------- 1 | package net.ceedubs.ficus.readers 2 | 3 | import com.typesafe.config.Config 4 | 5 | import java.time.temporal.ChronoUnit 6 | 7 | trait ChronoUnitReader { 8 | implicit val chronoUnitReader: ValueReader[ChronoUnit] = new ValueReader[ChronoUnit] { 9 | 10 | /** Reads the value at the path `path` in the Config */ 11 | override def read(config: Config, path: String): ChronoUnit = 12 | ChronoUnit.valueOf(config.getString(path).toUpperCase) 13 | } 14 | } 15 | 16 | object ChronoUnitReader extends ChronoUnitReader 17 | -------------------------------------------------------------------------------- /src/test/scala/net/ceedubs/ficus/readers/SymbolReaderSpec.scala: -------------------------------------------------------------------------------- 1 | package net.ceedubs.ficus 2 | package readers 3 | 4 | import com.typesafe.config.ConfigFactory 5 | import ConfigSerializerOps._ 6 | 7 | class SymbolReaderSpec extends Spec with SymbolReader { 8 | def is = s2""" 9 | The Symbol value reader should 10 | read a Symbol $readSymbol 11 | """ 12 | 13 | def readSymbol = prop { (string: String) => 14 | val cfg = ConfigFactory.parseString(s"myValue = ${string.asConfigValue}") 15 | symbolValueReader.read(cfg, "myValue") must beEqualTo(Symbol(string)) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /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 = if (path == ".") config else config.getConfig(path) 9 | } 10 | 11 | implicit val ficusConfigValueReader: ValueReader[FicusConfig] = configValueReader.map(SimpleFicusConfig) 12 | } 13 | 14 | object ConfigReader extends ConfigReader 15 | -------------------------------------------------------------------------------- /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 { 9 | def is = s2""" 10 | The String value reader should 11 | read a String $readString 12 | """ 13 | 14 | def readString = prop { (string: String) => 15 | val cfg = ConfigFactory.parseString(s"myValue = ${string.asConfigValue}") 16 | stringValueReader.read(cfg, "myValue") must beEqualTo(string) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/format.yml: -------------------------------------------------------------------------------- 1 | name: Scalafmt 2 | 3 | on: 4 | pull_request: 5 | branches: ['**'] 6 | 7 | env: 8 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 9 | 10 | jobs: 11 | build: 12 | name: Code is formatted 13 | strategy: 14 | matrix: 15 | os: [ubuntu-latest] 16 | runs-on: ${{ matrix.os }} 17 | steps: 18 | - name: Checkout current branch (full) 19 | uses: actions/checkout@v2 20 | with: 21 | fetch-depth: 0 22 | 23 | - name: Check project is formatted 24 | uses: jrouly/scalafmt-native-action@v1 25 | with: 26 | version: '3.4.3' 27 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | resolvers += "Typesafe Repository" at "https://repo.typesafe.com/typesafe/releases/" 2 | 3 | addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.8.2") 4 | addSbtPlugin("org.scoverage" % "sbt-coveralls" % "1.3.1") 5 | addSbtPlugin("com.github.sbt" % "sbt-release" % "1.1.0") 6 | addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "0.9.2") 7 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.9.7") 8 | addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.1.2") 9 | addSbtPlugin("com.codecommit" % "sbt-github-actions" % "0.12.0") 10 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.3") 11 | -------------------------------------------------------------------------------- /src/main/scala/net/ceedubs/ficus/readers/URLReader.scala: -------------------------------------------------------------------------------- 1 | package net.ceedubs.ficus.readers 2 | 3 | import java.net.{URL, MalformedURLException} 4 | 5 | import com.typesafe.config.{Config, ConfigException} 6 | 7 | trait URLReader { 8 | implicit val javaURLReader: ValueReader[URL] = new ValueReader[URL] { 9 | def read(config: Config, path: String): URL = { 10 | val s = config.getString(path) 11 | try new URL(s) 12 | catch { 13 | case e: MalformedURLException => 14 | throw new ConfigException.WrongType(config.origin(), path, "java.net.URL", "String", e) 15 | } 16 | } 17 | } 18 | } 19 | 20 | object URLReader extends URLReader 21 | -------------------------------------------------------------------------------- /src/main/scala/net/ceedubs/ficus/readers/URIReaders.scala: -------------------------------------------------------------------------------- 1 | package net.ceedubs.ficus.readers 2 | 3 | import java.net.{URI, URISyntaxException} 4 | 5 | import com.typesafe.config.{Config, ConfigException} 6 | 7 | trait URIReaders { 8 | implicit val javaURIReader: ValueReader[URI] = new ValueReader[URI] { 9 | def read(config: Config, path: String): URI = { 10 | val s = config.getString(path) 11 | try new URI(s) 12 | catch { 13 | case e: URISyntaxException => 14 | throw new ConfigException.WrongType(config.origin(), path, "java.net.URI", "String", e) 15 | } 16 | } 17 | } 18 | 19 | } 20 | 21 | object URIReaders extends URIReaders 22 | -------------------------------------------------------------------------------- /src/main/scala/net/ceedubs/ficus/readers/ISOZonedDateTimeReader.scala: -------------------------------------------------------------------------------- 1 | package net.ceedubs.ficus.readers 2 | 3 | import java.time.ZonedDateTime 4 | import java.time.format.DateTimeFormatter 5 | 6 | import com.typesafe.config.Config 7 | 8 | trait ISOZonedDateTimeReader { 9 | implicit val isoZonedDateTimeReader: ValueReader[ZonedDateTime] = new ValueReader[ZonedDateTime] { 10 | override def read(config: Config, path: String): ZonedDateTime = { 11 | val dateTimeFormatter: DateTimeFormatter = DateTimeFormatter.ISO_DATE_TIME 12 | ZonedDateTime.parse(config.getString(path), dateTimeFormatter) 13 | } 14 | } 15 | } 16 | 17 | object ISOZonedDateTimeReader extends ISOZonedDateTimeReader 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 { 7 | def is = s2""" 8 | An option value reader should 9 | wrap an existing value in a Some $optionSome 10 | return a None for a non-existing value $optionNone 11 | """ 12 | 13 | def optionSome = prop { (i: Int) => 14 | val cfg = ConfigFactory.parseString(s"myValue = $i") 15 | optionValueReader[Int].read(cfg, "myValue") must beSome(i) 16 | } 17 | 18 | def optionNone = { 19 | val cfg = ConfigFactory.parseString("") 20 | optionValueReader[Boolean].read(cfg, "myValue") must beNone 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/test/scala-2/net/ceedubs/ficus/readers/LocalDateReaderSpec.scala: -------------------------------------------------------------------------------- 1 | package net.ceedubs.ficus 2 | package readers 3 | 4 | import java.time.LocalDate 5 | import com.typesafe.config.ConfigFactory 6 | import Ficus.{toFicusConfig, localDateReader} 7 | 8 | class LocalDateReaderSpec extends Spec { 9 | def is = s2""" 10 | The LocalDateReader should 11 | read a LocalDate in ISO format without a time-zone: $readLocalDate 12 | """ 13 | 14 | def readLocalDate = { 15 | val cfg = ConfigFactory.parseString(s""" 16 | | foo { 17 | | date = "2003-01-03" 18 | | } 19 | """.stripMargin) 20 | val localDate = cfg.as[LocalDate]("foo.date") 21 | val expected = LocalDate.of(2003, 1, 3) 22 | localDate should_== expected 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /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 { 8 | def is = s2""" 9 | A value reader should 10 | be able to be fetched from implicit scope via the companion apply method $fromCompanionApply 11 | be a functor $transformAsFunctor 12 | """ 13 | 14 | def fromCompanionApply = 15 | ValueReader[Int] must beEqualTo(implicitly[ValueReader[Int]]) 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/EitherReader.scala: -------------------------------------------------------------------------------- 1 | package net.ceedubs.ficus.readers 2 | import com.typesafe.config.{Config, ConfigException} 3 | 4 | trait EitherReader { 5 | implicit def eitherReader[L, R](implicit 6 | lReader: ValueReader[L], 7 | rReader: ValueReader[R] 8 | ): ValueReader[Either[L, R]] = 9 | new ValueReader[Either[L, R]] { 10 | 11 | /** Reads the value at the path `path` in the Config */ 12 | override def read(config: Config, path: String): Either[L, R] = 13 | TryReader 14 | .tryValueReader(rReader) 15 | .read(config, path) 16 | .map(Right(_)) 17 | .recover { case _: ConfigException => 18 | Left(lReader.read(config, path)) 19 | } 20 | .get 21 | } 22 | } 23 | 24 | object EitherReader extends EitherReader 25 | -------------------------------------------------------------------------------- /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 | import scala.language.implicitConversions 6 | 7 | trait FicusInstances 8 | extends AnyValReaders 9 | with StringReader 10 | with SymbolReader 11 | with OptionReader 12 | with CollectionReaders 13 | with ConfigReader 14 | with DurationReaders 15 | with TryReader 16 | with ConfigValueReader 17 | with BigNumberReaders 18 | with ISOZonedDateTimeReader 19 | with PeriodReader 20 | with LocalDateReader 21 | with ChronoUnitReader 22 | with URIReaders 23 | with URLReader 24 | with InetSocketAddressReaders 25 | 26 | object Ficus extends FicusInstances { 27 | implicit def toFicusConfig(config: Config): FicusConfig = SimpleFicusConfig(config) 28 | } 29 | -------------------------------------------------------------------------------- /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/test/scala-2/net/ceedubs/ficus/readers/CaseInsensitiveEnumerationReadersSpec.scala: -------------------------------------------------------------------------------- 1 | package net.ceedubs.ficus.readers 2 | 3 | import com.typesafe.config.ConfigFactory 4 | import net.ceedubs.ficus.readers.EnumerationReadersSpec._ 5 | 6 | import scala.reflect.ClassTag 7 | 8 | class CaseInsensitiveEnumerationReadersSpec extends EnumerationReadersSpec with CaseInsensitiveEnumerationReader { 9 | override def is = super.is.append(s2""" 10 | A case insensitive enumeration value reader should 11 | map a string value with different case to its enumeration counterpart $successMixedCaseMapping 12 | """) 13 | 14 | def successMixedCaseMapping = { 15 | val cfg = ConfigFactory.parseString("myValue = secOND") 16 | implicit val classTag = ClassTag[StringValueEnum.type](StringValueEnum.getClass) 17 | enumerationValueReader[StringValueEnum.type].read(cfg, "myValue") must be equalTo StringValueEnum.second 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/test/scala/net/ceedubs/ficus/readers/URIReaderSpec.scala: -------------------------------------------------------------------------------- 1 | package net.ceedubs.ficus.readers 2 | 3 | import java.net.URI 4 | 5 | import com.typesafe.config.ConfigException.WrongType 6 | import com.typesafe.config.ConfigFactory 7 | import net.ceedubs.ficus.Spec 8 | 9 | class URIReaderSpec extends Spec with URIReaders { 10 | def is = s2""" 11 | The URI value reader should 12 | read a valid URI $readValidURI 13 | detect wrong type on malformed URI $readMalformedURI 14 | """ 15 | 16 | def readValidURI = { 17 | val uri = """https://www.google.com""" 18 | val cfg = ConfigFactory.parseString(s"myValue = ${"\"" + uri + "\""}") 19 | javaURIReader.read(cfg, "myValue") must beEqualTo(new URI(uri)) 20 | } 21 | 22 | def readMalformedURI = { 23 | val malformedUri = """foo://{bar}.com""" 24 | val cfg = ConfigFactory.parseString(s"myValue = ${"\"" + malformedUri + "\""}") 25 | javaURIReader.read(cfg, "myValue") must throwA[WrongType] 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/test/scala-2/net/ceedubs/ficus/readers/ISOZonedDateTimeReaderSpec.scala: -------------------------------------------------------------------------------- 1 | package net.ceedubs.ficus 2 | package readers 3 | 4 | import java.time.{ZoneId, ZonedDateTime} 5 | 6 | import com.typesafe.config.ConfigFactory 7 | 8 | import Ficus.{toFicusConfig, isoZonedDateTimeReader} 9 | 10 | class ISOZonedDateTimeReaderSpec extends Spec { 11 | def is = s2""" 12 | The ISOZonedDateTimeReader should 13 | read a ZonedDateTime in ISO format $readZonedDateTime 14 | """ 15 | 16 | def readZonedDateTime = { 17 | val cfg = ConfigFactory.parseString(s""" 18 | | foo { 19 | | date = "2016-02-28T11:46:26.896+01:00[Europe/Berlin]" 20 | | } 21 | """.stripMargin) 22 | val date = cfg.as[ZonedDateTime]("foo.date") 23 | val expected = ZonedDateTime.of( 24 | 2016, 25 | 2, 26 | 28, 27 | 11, 28 | 46, 29 | 26, 30 | 896000000, 31 | ZoneId.of("Europe/Berlin") 32 | ) 33 | date should_== expected 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/test/scala-2.13/net/ceedubs/ficus/Issue82Spec.scala: -------------------------------------------------------------------------------- 1 | package net.ceedubs.ficus 2 | 3 | import net.ceedubs.ficus.Ficus._ 4 | import net.ceedubs.ficus.readers.ArbitraryTypeReader._ 5 | import com.typesafe.config._ 6 | import org.specs2.mutable.Specification 7 | 8 | class Issue82Spec extends Specification { 9 | "Ficus config" should { 10 | "not throw `java.lang.ClassCastException`" in { 11 | case class TestSettings(val `foo-bar`: Long, `foo`: String) 12 | val config = ConfigFactory.parseString("""{ foo-bar: 3, foo: "4" }""") 13 | config.as[TestSettings] must not(throwA[java.lang.ClassCastException]) 14 | } 15 | 16 | """should not assign "foo-bar" to "foo"""" in { 17 | case class TestSettings(val `foo-bar`: String, `foo`: String) 18 | val config = ConfigFactory.parseString("""{ foo-bar: "foo-bar", foo: "foo" }""") 19 | val settings = config.as[TestSettings] 20 | (settings.`foo-bar` mustEqual "foo-bar") and (settings.`foo` mustEqual "foo") 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/test/scala/net/ceedubs/ficus/readers/URLReaderSpec.scala: -------------------------------------------------------------------------------- 1 | package net.ceedubs.ficus.readers 2 | 3 | import java.net.URL 4 | 5 | import com.typesafe.config.ConfigException.WrongType 6 | import com.typesafe.config.ConfigFactory 7 | import net.ceedubs.ficus.Spec 8 | 9 | class URLReaderSpec extends Spec with URLReader with TryReader { 10 | def is = s2""" 11 | The URL value reader should 12 | read a valid URL $readValidURL 13 | detect wrong type on malformed URL (with an unsupported protocol) $readMalformedURL 14 | """ 15 | 16 | def readValidURL = { 17 | val url = """https://www.google.com""" 18 | val cfg = ConfigFactory.parseString(s"myValue = ${"\"" + url + "\""}") 19 | javaURLReader.read(cfg, "myValue") must beEqualTo(new URL(url)) 20 | } 21 | 22 | def readMalformedURL = { 23 | val malformedUrl = """foo://bar.com""" 24 | val cfg = ConfigFactory.parseString(s"myValue = ${"\"" + malformedUrl + "\""}") 25 | javaURLReader.read(cfg, "myValue") must throwA[WrongType] 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/test/scala-2/net/ceedubs/ficus/readers/PeriodReaderSpec.scala: -------------------------------------------------------------------------------- 1 | package net.ceedubs.ficus 2 | package readers 3 | 4 | import java.time.Period 5 | import com.typesafe.config.ConfigFactory 6 | import Ficus.{toFicusConfig, periodReader} 7 | 8 | class PeriodReaderSpec extends Spec { 9 | def is = s2""" 10 | The PeriodReader should 11 | read a Period in ISO-8601 format $readPeriod 12 | read a negative Period $readNegativePeriod 13 | """ 14 | 15 | def readPeriod = { 16 | val cfg = ConfigFactory.parseString(s""" 17 | | foo { 18 | | interval = "P1Y3M10D" 19 | | } 20 | """.stripMargin) 21 | val period = cfg.as[Period]("foo.interval") 22 | val expected = Period.of(1, 3, 10) 23 | period should_== expected 24 | } 25 | 26 | def readNegativePeriod = { 27 | val cfg = ConfigFactory.parseString(s""" 28 | | foo { 29 | | interval = "P-1Y10M3D" 30 | | } 31 | """.stripMargin) 32 | val period = cfg.as[Period]("foo.interval") 33 | val expected = Period.of(-1, 10, 3) 34 | period should_== expected 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/scala/net/ceedubs/ficus/readers/BigNumberReaders.scala: -------------------------------------------------------------------------------- 1 | package net.ceedubs.ficus.readers 2 | 3 | import java.math.MathContext 4 | 5 | import com.typesafe.config.{ConfigException, Config} 6 | 7 | trait BigNumberReaders { 8 | implicit val bigDecimalReader: ValueReader[BigDecimal] = new ValueReader[BigDecimal] { 9 | def read(config: Config, path: String): BigDecimal = { 10 | val s = config.getString(path) 11 | try BigDecimal(s) 12 | catch { 13 | case e: NumberFormatException => 14 | throw new ConfigException.WrongType(config.origin(), path, "scala.math.BigDecimal", "String", e) 15 | } 16 | } 17 | } 18 | 19 | implicit val bigIntReader: ValueReader[BigInt] = new ValueReader[BigInt] { 20 | def read(config: Config, path: String): BigInt = { 21 | val s = config.getString(path) 22 | try BigInt(s) 23 | catch { 24 | case e: NumberFormatException => 25 | throw new ConfigException.WrongType(config.origin(), path, "scala.math.BigInt", "String", e) 26 | } 27 | } 28 | } 29 | } 30 | 31 | object BigNumberReaders extends BigNumberReaders 32 | -------------------------------------------------------------------------------- /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/main/scala/net/ceedubs/ficus/readers/NameMapper.scala: -------------------------------------------------------------------------------- 1 | package net.ceedubs.ficus.readers 2 | 3 | /** Defines an object that knows to map between names as they found in the code to those who should be defined in the 4 | * configuration 5 | */ 6 | trait NameMapper { 7 | 8 | /** Maps between the name in the code to name in configuration 9 | * @param name 10 | * The name as found in the code 11 | */ 12 | def map(name: String): String 13 | 14 | } 15 | 16 | /** Helper object to get the current name mapper 17 | */ 18 | object NameMapper { 19 | 20 | /** Gets the name mapper from the implicit scope 21 | * @param nameMapper 22 | * The name mapper from the implicit scope, or the default name mapper if not found 23 | * @return 24 | * The name mapper to be used in current implicit scope 25 | */ 26 | def apply()(implicit nameMapper: NameMapper = DefaultNameMapper): NameMapper = nameMapper 27 | 28 | } 29 | 30 | /** Default implementation for name mapper, names in code equivalent to names in configuration 31 | */ 32 | case object DefaultNameMapper extends NameMapper { 33 | 34 | override def map(name: String): String = name 35 | 36 | } 37 | -------------------------------------------------------------------------------- /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 | import scala.language.implicitConversions 6 | 7 | trait FicusConfig { 8 | def config: Config 9 | 10 | def as[A](path: String)(implicit reader: ValueReader[A]): A = reader.read(config, path) 11 | 12 | def as[A](implicit reader: ValueReader[A]): A = as(".") 13 | 14 | def getAs[A](path: String)(implicit reader: ValueReader[Option[A]]): Option[A] = reader.read(config, path) 15 | 16 | def getOrElse[A](path: String, default: => A)(implicit reader: ValueReader[Option[A]]): A = 17 | getAs[A](path).getOrElse(default) 18 | 19 | def apply[A](key: ConfigKey[A])(implicit reader: ValueReader[A]): A = as[A](key.path) 20 | } 21 | 22 | final case class SimpleFicusConfig(config: Config) extends FicusConfig 23 | 24 | @deprecated( 25 | "For implicits, use Ficus._ instead of FicusConfig._. Separately use ArbitraryTypeReader._ for macro-based derived reader instances. See https://github.com/ceedubs/ficus/issues/5", 26 | since = "1.0.1/1.1.1" 27 | ) 28 | object FicusConfig extends AllValueReaderInstances { 29 | implicit def toFicusConfig(config: Config): FicusConfig = SimpleFicusConfig(config) 30 | } 31 | -------------------------------------------------------------------------------- /src/test/scala-2/net/ceedubs/ficus/readers/ChronoUnitReaderSpec.scala: -------------------------------------------------------------------------------- 1 | package net.ceedubs.ficus 2 | package readers 3 | 4 | import java.time.temporal.ChronoUnit 5 | 6 | import com.typesafe.config.ConfigFactory 7 | import Ficus.{chronoUnitReader, toFicusConfig} 8 | 9 | class ChronoUnitReaderSpec extends Spec { 10 | def is = s2""" 11 | The ChronoUnitReader should 12 | read a ChronoUnit $readChronoUnit 13 | read a lower case ChronoUnit $readChronoUnitLowerCase 14 | """ 15 | 16 | def readChronoUnit = { 17 | val cfg = ConfigFactory.parseString(s""" 18 | | foo { 19 | | chrono-unit = "MILLIS" 20 | | } 21 | """.stripMargin) 22 | val chronoUnit = cfg.as[ChronoUnit]("foo.chrono-unit") 23 | val expected = ChronoUnit.MILLIS 24 | chronoUnit should_== expected 25 | } 26 | 27 | def readChronoUnitLowerCase = { 28 | val cfg = ConfigFactory.parseString(s""" 29 | | foo { 30 | | chrono-unit = "millis" 31 | | } 32 | """.stripMargin) 33 | val chronoUnit = cfg.as[ChronoUnit]("foo.chrono-unit") 34 | val expected = ChronoUnit.MILLIS 35 | chronoUnit should_== expected 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/test/scala/net/ceedubs/ficus/readers/HyphenNameMapperSpec.scala: -------------------------------------------------------------------------------- 1 | package net.ceedubs.ficus.readers 2 | 3 | import net.ceedubs.ficus.Spec 4 | import net.ceedubs.ficus.readers.namemappers.HyphenNameMapper 5 | import org.scalacheck.Arbitrary 6 | import org.scalacheck.Gen._ 7 | import org.specs2.matcher.DataTables 8 | 9 | class HyphenNameMapperSpec extends Spec with DataTables { 10 | def is = s2""" 11 | A HyphenNameMapper should 12 | hyphenate a camelCased name $hyphenateCorrectly 13 | hyphenate a camelCased name containing digits $hyphenateWithDigits 14 | """ 15 | 16 | def nonemptyStringListGen = nonEmptyListOf(alphaStr.suchThat(_.length > 1).map(_.toLowerCase)) 17 | 18 | implicit def nonemptyStringList: Arbitrary[List[String]] = Arbitrary(nonemptyStringListGen) 19 | 20 | def hyphenateCorrectly = prop { (foos: List[String]) => 21 | val camelCased = (foos.head +: foos.tail.map(_.capitalize)).mkString 22 | val hyphenated = foos.mkString("-").toLowerCase 23 | 24 | HyphenNameMapper.map(camelCased) must_== hyphenated 25 | } 26 | 27 | def hyphenateWithDigits = 28 | "camelCased" || "hyphenated" |> 29 | "camelCasedName67" !! "camel-cased-name-67" | 30 | "1144StartsWithA32422" !! "1144-starts-with-a-32422" | 31 | "get13HTML42Snippets" !! "get-13-html-42-snippets" | 32 | "thisOneIs13InThe43Middle" !! "this-one-is-13-in-the-43-middle" | { (camelCased, hyphenated) => 33 | HyphenNameMapper.map(camelCased) must_== hyphenated 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/test/scala/net/ceedubs/ficus/readers/HyphenNameMapperNoDigitsSpec.scala: -------------------------------------------------------------------------------- 1 | package net.ceedubs.ficus.readers 2 | 3 | import net.ceedubs.ficus.Spec 4 | import net.ceedubs.ficus.readers.namemappers.{HyphenNameMapper, HyphenNameMapperNoDigits} 5 | import org.scalacheck.Arbitrary 6 | import org.scalacheck.Gen._ 7 | import org.specs2.matcher.DataTables 8 | 9 | class HyphenNameMapperNoDigitsSpec extends Spec with DataTables { 10 | def is = s2""" 11 | A HyphenNameMapper should 12 | hyphenate a camelCased name $hyphenateCorrectly 13 | hyphenate a camelCased name containing digits without hyphen before digit $hyphenateWithDigits 14 | """ 15 | 16 | def nonemptyStringListGen = nonEmptyListOf(alphaStr.suchThat(_.length > 1).map(_.toLowerCase)) 17 | 18 | implicit def nonemptyStringList: Arbitrary[List[String]] = Arbitrary(nonemptyStringListGen) 19 | 20 | def hyphenateCorrectly = prop { (foos: List[String]) => 21 | val camelCased = (foos.head +: foos.tail.map(_.capitalize)).mkString 22 | val hyphenated = foos.mkString("-").toLowerCase 23 | 24 | HyphenNameMapper.map(camelCased) must_== hyphenated 25 | } 26 | 27 | def hyphenateWithDigits = 28 | "camelCased" || "hyphenated" |> 29 | "camelCasedName67" !! "camel-cased-name67" | 30 | "1144StartsWithA32422" !! "1144-starts-with-a32422" | 31 | "get13HTML42Snippets" !! "get13-html42-snippets" | 32 | "thisOneIs13InThe43Middle" !! "this-one-is13-in-the43-middle" | { (camelCased, hyphenated) => 33 | HyphenNameMapperNoDigits.map(camelCased) must_== hyphenated 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/test/scala-2/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/main/scala-2.13+/net/ceedubs/ficus/readers/CollectionReaders.scala: -------------------------------------------------------------------------------- 1 | package net.ceedubs.ficus.readers 2 | 3 | import com.typesafe.config.{Config, ConfigUtil} 4 | 5 | import scala.collection.Factory 6 | import scala.jdk.CollectionConverters._ 7 | import scala.language.postfixOps 8 | import scala.language.higherKinds 9 | 10 | trait CollectionReaders { 11 | 12 | private[this] val DummyPathValue: String = "collection-entry-path" 13 | 14 | implicit def traversableReader[C[_], A](implicit 15 | entryReader: ValueReader[A], 16 | cbf: Factory[A, C[A]] 17 | ): ValueReader[C[A]] = new ValueReader[C[A]] { 18 | def read(config: Config, path: String): C[A] = { 19 | val list = config.getList(path).asScala 20 | val builder = cbf.newBuilder 21 | builder.sizeHint(list.size) 22 | list foreach { entry => 23 | val entryConfig = entry.atPath(DummyPathValue) 24 | builder += entryReader.read(entryConfig, DummyPathValue) 25 | } 26 | builder.result() 27 | } 28 | } 29 | 30 | implicit def mapValueReader[A](implicit entryReader: ValueReader[A]): ValueReader[Map[String, A]] = 31 | new ValueReader[Map[String, A]] { 32 | def read(config: Config, path: String): Map[String, A] = { 33 | val relativeConfig = config.getConfig(path) 34 | relativeConfig.root().entrySet().asScala map { entry => 35 | val key = entry.getKey 36 | key -> entryReader.read(relativeConfig, ConfigUtil.quoteString(key)) 37 | } toMap 38 | } 39 | } 40 | 41 | } 42 | 43 | object CollectionReaders extends CollectionReaders 44 | -------------------------------------------------------------------------------- /src/main/scala-2.13-/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.language.postfixOps 7 | import scala.language.higherKinds 8 | 9 | trait CollectionReaders { 10 | 11 | private[this] val DummyPathValue: String = "collection-entry-path" 12 | 13 | implicit def traversableReader[C[_], A](implicit 14 | entryReader: ValueReader[A], 15 | cbf: CanBuildFrom[Nothing, A, C[A]] 16 | ): ValueReader[C[A]] = new ValueReader[C[A]] { 17 | def read(config: Config, path: String): C[A] = { 18 | val list = config.getList(path).asScala 19 | val builder = cbf() 20 | builder.sizeHint(list.size) 21 | list foreach { entry => 22 | val entryConfig = entry.atPath(DummyPathValue) 23 | builder += entryReader.read(entryConfig, DummyPathValue) 24 | } 25 | builder.result() 26 | } 27 | } 28 | 29 | implicit def mapValueReader[A](implicit entryReader: ValueReader[A]): ValueReader[Map[String, A]] = 30 | new ValueReader[Map[String, A]] { 31 | def read(config: Config, path: String): Map[String, A] = { 32 | val relativeConfig = config.getConfig(path) 33 | relativeConfig.root().entrySet().asScala map { entry => 34 | val key = entry.getKey 35 | key -> entryReader.read(relativeConfig, ConfigUtil.quoteString(key)) 36 | } toMap 37 | } 38 | } 39 | 40 | } 41 | 42 | object CollectionReaders extends CollectionReaders 43 | -------------------------------------------------------------------------------- /src/main/scala/net/ceedubs/ficus/readers/EnumerationReader.scala: -------------------------------------------------------------------------------- 1 | package net.ceedubs.ficus.readers 2 | 3 | import com.typesafe.config.ConfigException.{BadValue, Generic} 4 | import com.typesafe.config.Config 5 | import net.ceedubs.ficus.util.EnumerationUtil.EnumValue 6 | import scala.reflect.ClassTag 7 | import scala.util.{Failure, Success, Try} 8 | 9 | trait EnumerationReader { 10 | implicit def enumerationValueReader[T <: Enumeration: ClassTag]: ValueReader[EnumValue[T]] = 11 | new ValueReader[EnumValue[T]] { 12 | def read(config: Config, path: String): EnumValue[T] = { 13 | val c = implicitly[ClassTag[T]].runtimeClass 14 | val `enum` = Try(c.getField("MODULE$")) match { 15 | case Success(m) => m.get(null).asInstanceOf[T] 16 | case Failure(e) => 17 | throw new Generic( 18 | "Cannot get instance of enum: " + c.getCanonicalName + "; " + 19 | "make sure the enum is an object and it's not contained in a class or trait", 20 | e 21 | ) 22 | } 23 | 24 | val value = config.getString(path) 25 | findEnumValue(`enum`, value) 26 | .getOrElse( 27 | throw new BadValue( 28 | config.origin(), 29 | path, 30 | value + " isn't a valid value for enum: " + 31 | "" + c.getCanonicalName + "; allowed values: " + `enum`.values.mkString(", ") 32 | ) 33 | ) 34 | .asInstanceOf[EnumValue[T]] 35 | } 36 | } 37 | 38 | protected def findEnumValue[T <: Enumeration](`enum`: T, configValue: String): Option[EnumValue[T]] = 39 | `enum`.values.find(_.toString == configValue) 40 | } 41 | 42 | object EnumerationReader extends EnumerationReader 43 | -------------------------------------------------------------------------------- /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, ConfigException} 5 | import scala.concurrent.duration.{Duration, NANOSECONDS} 6 | import scala.util.Try 7 | 8 | trait DurationReaders { 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. For 12 | * 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 nanos = config.getDuration(path, NANOSECONDS) 17 | Duration.fromNanos(nanos) 18 | } 19 | } 20 | 21 | /** A reader for for a scala.concurrent.duration.Duration. This reader should be able to read any valid duration 22 | * format as defined by the HOCON spec and 23 | * positive and negative infinite values supported by Duration's apply method. For 26 | * example, it can read "15 minutes", "1 day", "-Inf", or "PlusInf". 27 | */ 28 | implicit def durationReader: ValueReader[Duration] = new ValueReader[Duration] { 29 | def read(config: Config, path: String): Duration = 30 | (Try { 31 | finiteDurationReader.read(config, path) 32 | } recover { case _: ConfigException.BadValue => 33 | val nonFinite = config.getString(path) 34 | Duration(nonFinite) 35 | }).get 36 | } 37 | } 38 | 39 | object DurationReaders extends DurationReaders 40 | -------------------------------------------------------------------------------- /src/test/scala-2/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 { 8 | def is = s2""" 9 | The Config value reader should 10 | read a config $readConfig 11 | implicitly read a config $implicitlyReadConfig 12 | read a ficus config $readFicusConfig 13 | implicitly read a ficus config $implicitlyReadFicusConfig 14 | implicitly read a ficus config itself $implicitlyReadFicusConfigFromSelf 15 | """ 16 | 17 | def readConfig = prop { (i: Int) => 18 | val cfg = ConfigFactory.parseString(s""" 19 | |myConfig { 20 | | myValue = $i 21 | |} 22 | """.stripMargin) 23 | configValueReader.read(cfg, "myConfig").getInt("myValue") must beEqualTo(i) 24 | } 25 | 26 | def implicitlyReadConfig = prop { (i: Int) => 27 | val cfg = ConfigFactory.parseString(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(s""" 37 | |myConfig { 38 | | myValue = $i 39 | |} 40 | """.stripMargin) 41 | ficusConfigValueReader.read(cfg, "myConfig").as[Int]("myValue") must beEqualTo(i) 42 | } 43 | 44 | def implicitlyReadFicusConfig = prop { (i: Int) => 45 | val cfg = ConfigFactory.parseString(s""" 46 | |myConfig { 47 | | myValue = $i 48 | |} 49 | """.stripMargin) 50 | cfg.as[FicusConfig]("myConfig").as[Int]("myValue") must beEqualTo(i) 51 | } 52 | 53 | def implicitlyReadFicusConfigFromSelf = prop { (i: Int) => 54 | val cfg = ConfigFactory.parseString(s""" 55 | |myConfig { 56 | | myValue = $i 57 | |} 58 | """.stripMargin) 59 | cfg.getConfig("myConfig").as[FicusConfig].as[Int]("myValue") must beEqualTo(i) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /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 { 7 | def is = s2""" 8 | The Boolean value reader should 9 | read a boolean $readBoolean 10 | 11 | The Int value reader should 12 | read an int $readInt 13 | read a double as an int $readDoubleAsInt 14 | 15 | The Long value reader should 16 | read a long $readLong 17 | read an int as a long $readIntAsLong 18 | 19 | The Double value reader should 20 | read a double $readDouble 21 | read an int as a double $readIntAsDouble 22 | """ 23 | 24 | def readBoolean = prop { (b: Boolean) => 25 | val cfg = ConfigFactory.parseString(s"myValue = $b") 26 | booleanValueReader.read(cfg, "myValue") must beEqualTo(b) 27 | } 28 | 29 | def readInt = prop { (i: Int) => 30 | val cfg = ConfigFactory.parseString(s"myValue = $i") 31 | intValueReader.read(cfg, "myValue") must beEqualTo(i) 32 | } 33 | 34 | def readDoubleAsInt = prop { (d: Double) => 35 | (d >= Int.MinValue && d <= Int.MaxValue) ==> { 36 | val cfg = ConfigFactory.parseString(s"myValue = $d") 37 | intValueReader.read(cfg, "myValue") must beEqualTo(d.toInt) 38 | } 39 | } 40 | 41 | def readLong = prop { (l: Long) => 42 | val cfg = ConfigFactory.parseString(s"myValue = $l") 43 | longValueReader.read(cfg, "myValue") must beEqualTo(l) 44 | } 45 | 46 | def readIntAsLong = prop { (i: Int) => 47 | val cfg = ConfigFactory.parseString(s"myValue = $i") 48 | longValueReader.read(cfg, "myValue") must beEqualTo(i.toLong) 49 | } 50 | 51 | def readDouble = prop { (d: Double) => 52 | val cfg = ConfigFactory.parseString(s"myValue = $d") 53 | doubleValueReader.read(cfg, "myValue") must beEqualTo(d) 54 | } 55 | 56 | def readIntAsDouble = prop { (i: Int) => 57 | val cfg = ConfigFactory.parseString(s"myValue = $i") 58 | doubleValueReader.read(cfg, "myValue") must beEqualTo(i.toDouble) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/scala/net/ceedubs/ficus/readers/InetSocketAddressReaders.scala: -------------------------------------------------------------------------------- 1 | package net.ceedubs.ficus.readers 2 | 3 | import java.net.InetSocketAddress 4 | 5 | import com.typesafe.config.{Config, ConfigException} 6 | 7 | trait InetSocketAddressReaders { 8 | private def parseHostAndPort(unparsedHostAndPort: String): Option[InetSocketAddress] = { 9 | val hostAndPort = """([a-zA-Z0-9\.\-]+)\s*:\s*(\d+)""".r 10 | unparsedHostAndPort match { 11 | case hostAndPort(host, port) => 12 | Some(new InetSocketAddress(host, port.toInt)) 13 | case _ => 14 | None 15 | } 16 | } 17 | 18 | implicit val inetSocketAddressListReader: ValueReader[List[InetSocketAddress]] = 19 | new ValueReader[List[InetSocketAddress]] { 20 | def read(config: Config, path: String): List[InetSocketAddress] = 21 | try 22 | config 23 | .getString(path) 24 | .split(", *") 25 | .toList 26 | .map(parseHostAndPort) 27 | .partition(_.isEmpty) match { 28 | case (errors, ok) if errors.isEmpty => 29 | ok.flatten 30 | case _ => 31 | throw new IllegalArgumentException("Cannot parse string into hosts and ports") 32 | } 33 | catch { 34 | case e: Exception => 35 | throw new ConfigException.WrongType(config.origin(), path, "java.net.InetSocketAddress", "String", e) 36 | } 37 | } 38 | 39 | implicit val inetSocketAddressReader: ValueReader[InetSocketAddress] = new ValueReader[InetSocketAddress] { 40 | def read(config: Config, path: String): InetSocketAddress = 41 | try 42 | parseHostAndPort(config.getString(path)) 43 | .getOrElse(throw new IllegalArgumentException("Cannot parse string into host and port")) 44 | catch { 45 | case e: Exception => 46 | throw new ConfigException.WrongType(config.origin(), path, "java.net.InetSocketAddress", "String", e) 47 | } 48 | } 49 | } 50 | 51 | object InetSocketAddressReaders extends InetSocketAddressReaders 52 | -------------------------------------------------------------------------------- /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 | /** Turns a ValueReader[A] into a ValueReader[B] by applying the provided transformation `f` on the item of type A 12 | * that is read from config 13 | */ 14 | def map[B](f: A => B): ValueReader[B] = new ValueReader[B] { 15 | def read(config: Config, path: String): B = f(self.read(config, path)) 16 | } 17 | } 18 | 19 | object ValueReader { 20 | implicit def generatedReader[A](implicit generated: Generated[ValueReader[A]]): ValueReader[A] = generated.value 21 | 22 | /** Returns the implicit ValueReader[A] in scope. `ValueReader[A]` is equivalent to `implicitly[ValueReader[A]]` 23 | */ 24 | def apply[A](implicit reader: ValueReader[A]): ValueReader[A] = reader 25 | 26 | /** ValueReader that receives a Config whose root is the path being read. 27 | * 28 | * This is generally the most concise way to implement a ValueReader that doesn't depend on the path of the value 29 | * being read. 30 | * 31 | * For example to read a `case class FooBar(foo: Foo, bar: Bar)`, instead of 32 | * {{{ 33 | * new ValueReader[FooBar] { 34 | * def read(config: Config, path: String): FooBar = { 35 | * val localizedConfig = config.getConfig(path) 36 | * FooBar( 37 | * foo = localizedConfig.as[Foo]("foo"), 38 | * bar = localizedConfig.as[Bar]("bar")) 39 | * } 40 | * } 41 | * }}} 42 | * you could do 43 | * {{{ 44 | * ValueReader.relative[FooBar] { config => 45 | * FooBar( 46 | * foo = config.as[Foo]("foo"), 47 | * bar = config.as[Bar]("bar)) 48 | * } 49 | * }}} 50 | */ 51 | def relative[A](f: Config => A): ValueReader[A] = new ValueReader[A] { 52 | def read(config: Config, path: String): A = f(config.getConfig(path)) 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 { 9 | def is = s2""" 10 | A try value reader should 11 | return a success when a value can be read $successWhenPresent 12 | return a failure when a value cannot be read $cannotBeRead 13 | handle an unexpected exception type $unexpectedExceptionType 14 | handle an unexpected exception $unexpectedException 15 | """ 16 | 17 | def successWhenPresent = prop { (i: Int) => 18 | val cfg = ConfigFactory.parseString(s"myValue = $i") 19 | tryValueReader[Int].read(cfg, "myValue") must beSuccessfulTry[Int].withValue(i) 20 | } 21 | 22 | def cannotBeRead = { 23 | val cfg = ConfigFactory.parseString("myValue = true") 24 | tryValueReader[Boolean].read(cfg, "wrongKey") must beFailedTry[Boolean].withThrowable[ConfigException.Missing] 25 | } 26 | 27 | def unexpectedExceptionType = { 28 | val cfg = ConfigFactory.parseString("myValue = true") 29 | implicit val stringValueReader: ValueReader[String] = new ValueReader[String] { 30 | def read(config: Config, path: String): String = throw new NullPointerException("oops") 31 | } 32 | tryValueReader[String].read(cfg, "myValue") must beFailedTry[String].withThrowable[NullPointerException]("oops") 33 | } 34 | 35 | def unexpectedException = prop { (up: Throwable) => 36 | val cfg = ConfigFactory.parseString("myValue = true") 37 | implicit val stringValueReader: ValueReader[String] = new ValueReader[String] { 38 | def read(config: Config, path: String): String = throw up 39 | } 40 | val expectedMessage = Option(up).map(_.getMessage).orNull 41 | tryValueReader[String].read(cfg, "myValue") must beFailedTry[String] and 42 | (up.getMessage == expectedMessage must beTrue) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /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 | 6 | import scala.concurrent.duration._ 7 | import org.scalacheck.{Gen, Prop} 8 | 9 | class DurationReadersSpec extends Spec with DurationReaders { 10 | def is = s2""" 11 | The finite duration reader should 12 | read a millisecond value ${readMillis(finiteDurationReader)} 13 | read a minute value ${readMinutes(finiteDurationReader)} 14 | read a days value into days ${readDaysUnit(finiteDurationReader)} 15 | 16 | The duration reader should 17 | read a millisecond value ${readMillis(durationReader)} 18 | read a minute value ${readMinutes(durationReader)} 19 | read a days value into days ${readDaysUnit(durationReader)} 20 | read positive infinite values $readPositiveInf 21 | read negative infinite values $readNegativeInf 22 | """ 23 | 24 | def readMillis[T](reader: ValueReader[T]) = prop { (i: Int) => 25 | val cfg = ConfigFactory.parseString(s"myValue = $i") 26 | reader.read(cfg, "myValue") must beEqualTo(i.millis) 27 | } 28 | 29 | def readMinutes[T](reader: ValueReader[T]) = Prop.forAll(Gen.choose(-1.5e8.toInt, 1.5e8.toInt)) { (i: Int) => 30 | val cfg = ConfigFactory.parseString("myValue = \"" + i + " minutes\"") 31 | reader.read(cfg, "myValue") must_== i.minutes 32 | } 33 | 34 | def readDaysUnit[T](reader: ValueReader[T]) = Prop.forAll(Gen.choose(-106580, 106580)) { (i: Int) => 35 | val str = i.toString + " day" + (if (i == 1) "" else "s") 36 | val cfg = ConfigFactory.parseString(s"""myValue = "$str" """) 37 | reader.read(cfg, "myValue").toString == str 38 | } 39 | 40 | def readPositiveInf = { 41 | val positiveInf = List("Inf", "PlusInf", "\"+Inf\"") 42 | positiveInf.forall { (s: String) => 43 | val cfg = ConfigFactory.parseString(s"myValue = $s") 44 | durationReader.read(cfg, "myValue") == Duration.Inf 45 | } 46 | } 47 | 48 | def readNegativeInf = { 49 | val negativeInf = List("-Inf", "MinusInf") 50 | negativeInf.forall { (s: String) => 51 | val cfg = ConfigFactory.parseString(s"myValue = $s") 52 | durationReader.read(cfg, "myValue") == Duration.MinusInf 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/scala-2/net/ceedubs/ficus/util/ReflectionUtils.scala: -------------------------------------------------------------------------------- 1 | package net.ceedubs.ficus.util 2 | 3 | import macrocompat.bundle 4 | import scala.reflect.macros.blackbox 5 | 6 | @bundle 7 | trait ReflectionUtils { 8 | val c: blackbox.Context 9 | 10 | import c.universe._ 11 | 12 | def instantiationMethod[T: c.WeakTypeTag](fail: String => Nothing): c.universe.MethodSymbol = { 13 | 14 | val returnType = c.weakTypeOf[T] 15 | 16 | val returnTypeTypeArgs = returnType match { 17 | case TypeRef(_, _, args) => args 18 | case _ => Nil 19 | } 20 | 21 | if (returnTypeTypeArgs.nonEmpty) 22 | fail( 23 | s"value readers cannot be auto-generated for types with type parameters. Consider defining your own ValueReader[$returnType]" 24 | ) 25 | 26 | val companionSymbol = returnType.typeSymbol.companion match { 27 | case NoSymbol => None 28 | case x => Some(x) 29 | } 30 | 31 | val applyMethods = companionSymbol.toList.flatMap(_.typeSignatureIn(returnType).members collect { 32 | case m: MethodSymbol if m.name.decodedName.toString == "apply" && m.returnType <:< returnType => m 33 | }) 34 | 35 | val applyMethod = applyMethods match { 36 | case Nil => None 37 | case (head :: Nil) => Some(head) 38 | case _ => fail(s"its companion object has multiple apply methods that return type $returnType") 39 | } 40 | 41 | applyMethod getOrElse { 42 | val primaryConstructor = returnType.decl(termNames.CONSTRUCTOR) match { 43 | case t: TermSymbol => 44 | val constructors = t.alternatives collect { 45 | case m: MethodSymbol if m.isConstructor => m 46 | } 47 | val primaryScalaConstructor = constructors.find(m => m.isPrimaryConstructor && !m.isJava) 48 | primaryScalaConstructor orElse { 49 | if (constructors.length == 1) constructors.headOption else None 50 | } 51 | case _ => None 52 | } 53 | primaryConstructor getOrElse { 54 | fail( 55 | s"it has no apply method in a companion object that returns type $returnType, and it doesn't have a primary constructor" 56 | ) 57 | } 58 | } 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This file was automatically generated by sbt-github-actions using the 2 | # githubWorkflowGenerate task. You should add and commit this file to 3 | # your git repository. It goes without saying that you shouldn't edit 4 | # this file by hand! Instead, if you wish to make changes, you should 5 | # change your sbt build configuration to revise the workflow description 6 | # to meet your needs, then regenerate this file. 7 | 8 | name: Continuous Integration 9 | 10 | on: 11 | pull_request: 12 | branches: ['**'] 13 | push: 14 | branches: ['**'] 15 | 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | 19 | jobs: 20 | build: 21 | name: Build and Test 22 | strategy: 23 | matrix: 24 | os: [ubuntu-latest] 25 | scala: [2.11.12, 2.13.6, 2.12.14, 3.1.1] 26 | java: [adopt@1.8] 27 | runs-on: ${{ matrix.os }} 28 | steps: 29 | - name: Checkout current branch (full) 30 | uses: actions/checkout@v2 31 | with: 32 | fetch-depth: 0 33 | 34 | - name: Setup Java and Scala 35 | uses: olafurpg/setup-scala@v12 36 | with: 37 | java-version: ${{ matrix.java }} 38 | 39 | - name: Cache sbt 40 | uses: actions/cache@v2 41 | with: 42 | path: | 43 | ~/.sbt 44 | ~/.ivy2/cache 45 | ~/.coursier/cache/v1 46 | ~/.cache/coursier/v1 47 | ~/AppData/Local/Coursier/Cache/v1 48 | ~/Library/Caches/Coursier/v1 49 | key: ${{ runner.os }}-sbt-cache-v2-${{ hashFiles('**/*.sbt') }}-${{ hashFiles('project/build.properties') }} 50 | 51 | - name: Check that workflows are up to date 52 | run: sbt ++${{ matrix.scala }} githubWorkflowCheck 53 | 54 | - name: Report binary compatibility issues 55 | run: sbt ++${{ matrix.scala }} mimaReportBinaryIssues 56 | 57 | - name: Build project 58 | if: matrix.scala == '3.1.1' 59 | run: sbt ++${{ matrix.scala }} clean test 60 | 61 | - name: Build project 62 | if: matrix.scala != '3.1.1' 63 | run: sbt ++${{ matrix.scala }} clean coverage test 64 | 65 | - name: Upload coverage data to Coveralls 66 | if: matrix.scala != '3.1.1' 67 | env: 68 | COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} 69 | COVERALLS_FLAG_NAME: Scala ${{ matrix.scala }} 70 | run: sbt ++${{ matrix.scala }} coverageReport coveralls -------------------------------------------------------------------------------- /.github/workflows/clean.yml: -------------------------------------------------------------------------------- 1 | # This file was automatically generated by sbt-github-actions using the 2 | # githubWorkflowGenerate task. You should add and commit this file to 3 | # your git repository. It goes without saying that you shouldn't edit 4 | # this file by hand! Instead, if you wish to make changes, you should 5 | # change your sbt build configuration to revise the workflow description 6 | # to meet your needs, then regenerate this file. 7 | 8 | name: Clean 9 | 10 | on: push 11 | 12 | jobs: 13 | delete-artifacts: 14 | name: Delete Artifacts 15 | runs-on: ubuntu-latest 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | steps: 19 | - name: Delete artifacts 20 | run: | 21 | # Customize those three lines with your repository and credentials: 22 | REPO=${GITHUB_API_URL}/repos/${{ github.repository }} 23 | 24 | # A shortcut to call GitHub API. 25 | ghapi() { curl --silent --location --user _:$GITHUB_TOKEN "$@"; } 26 | 27 | # A temporary file which receives HTTP response headers. 28 | TMPFILE=/tmp/tmp.$$ 29 | 30 | # An associative array, key: artifact name, value: number of artifacts of that name. 31 | declare -A ARTCOUNT 32 | 33 | # Process all artifacts on this repository, loop on returned "pages". 34 | URL=$REPO/actions/artifacts 35 | while [[ -n "$URL" ]]; do 36 | 37 | # Get current page, get response headers in a temporary file. 38 | JSON=$(ghapi --dump-header $TMPFILE "$URL") 39 | 40 | # Get URL of next page. Will be empty if we are at the last page. 41 | URL=$(grep '^Link:' "$TMPFILE" | tr ',' '\n' | grep 'rel="next"' | head -1 | sed -e 's/.*.*//') 42 | rm -f $TMPFILE 43 | 44 | # Number of artifacts on this page: 45 | COUNT=$(( $(jq <<<$JSON -r '.artifacts | length') )) 46 | 47 | # Loop on all artifacts on this page. 48 | for ((i=0; $i < $COUNT; i++)); do 49 | 50 | # Get name of artifact and count instances of this name. 51 | name=$(jq <<<$JSON -r ".artifacts[$i].name?") 52 | ARTCOUNT[$name]=$(( $(( ${ARTCOUNT[$name]} )) + 1)) 53 | 54 | id=$(jq <<<$JSON -r ".artifacts[$i].id?") 55 | size=$(( $(jq <<<$JSON -r ".artifacts[$i].size_in_bytes?") )) 56 | printf "Deleting '%s' #%d, %'d bytes\n" $name ${ARTCOUNT[$name]} $size 57 | ghapi -X DELETE $REPO/actions/artifacts/$id 58 | done 59 | done -------------------------------------------------------------------------------- /project/Publish.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | import Keys._ 3 | import com.jsuereth.sbtpgp.PgpKeys 4 | import sbtrelease.ReleasePlugin.autoImport._ 5 | import sbtrelease.ReleaseStateTransformations._ 6 | 7 | object Publish { 8 | 9 | pomExtra in Global := { 10 | 11 | 12 | 13 | ceedubs 14 | Cody Allen 15 | ceedubs@gmail.com 16 | 17 | 18 | kailuowang 19 | Kailuo Wang 20 | kailuo.wang@gmail.com 21 | 22 | 23 | } 24 | 25 | val publishingSettings = Seq( 26 | ThisBuild / organization := "com.iheart", 27 | publishMavenStyle := true, 28 | licenses := Seq("MIT" -> url("http://www.opensource.org/licenses/mit-license.html")), 29 | homepage := Some(url("http://iheartradio.github.io/ficus")), 30 | scmInfo := Some( 31 | ScmInfo( 32 | url("https://github.com/iheartradio/ficus"), 33 | "git@github.com:iheartradio/ficus.git", 34 | Some("git@github.com:iheartradio/ficus.git") 35 | ) 36 | ), 37 | pomIncludeRepository := { _ => false }, 38 | Test / publishArtifact := false, 39 | publishTo := { 40 | val nexus = "https://oss.sonatype.org/" 41 | if (isSnapshot.value) 42 | Some("Snapshots" at nexus + "content/repositories/snapshots") 43 | else 44 | Some("Releases" at nexus + "service/local/staging/deploy/maven2") 45 | }, 46 | pomExtra := ( 47 | 48 | 49 | ceedubs 50 | Cody Allen 51 | ceedubs@gmail.com 52 | 53 | 54 | kailuowang 55 | Kailuo Wang 56 | kailuo.wang@gmail.com 57 | 58 | 59 | ), 60 | releaseCrossBuild := true, 61 | releasePublishArtifactsAction := PgpKeys.publishSigned.value, 62 | releaseProcess := Seq[ReleaseStep]( 63 | checkSnapshotDependencies, 64 | inquireVersions, 65 | runClean, 66 | runTest, 67 | setReleaseVersion, 68 | commitReleaseVersion, 69 | tagRelease, 70 | publishArtifacts, 71 | setNextVersion, 72 | commitNextVersion, 73 | ReleaseStep(action = Command.process("sonatypeReleaseAll", _)), 74 | pushChanges 75 | ) 76 | ) 77 | 78 | val settings = publishingSettings 79 | } 80 | -------------------------------------------------------------------------------- /src/test/scala-2/net/ceedubs/ficus/FicusConfigSpec.scala: -------------------------------------------------------------------------------- 1 | package net.ceedubs.ficus 2 | 3 | import com.typesafe.config.{Config, ConfigFactory} 4 | import Ficus.{booleanValueReader, optionValueReader, stringValueReader, toFicusConfig} 5 | import net.ceedubs.ficus.readers.ValueReader 6 | 7 | class FicusConfigSpec extends Spec { 8 | def is = s2""" 9 | A Ficus config should 10 | be implicitly converted from a Typesafe config $implicitlyConverted 11 | read a value with a value reader $readAValue 12 | get an existing value as a Some $getAsSome 13 | get a missing value as a None $getAsNone 14 | getOrElse an existing value as asked type $getOrElseFromConfig 15 | getOrElse a missing value with default value $getOrElseFromDefault 16 | getOrElse an existing value as asked type with customer reader $getOrElseFromConfigWithCustomValueReader 17 | accept a CongigKey and return the appropriate type $acceptAConfigKey 18 | """ 19 | 20 | def implicitlyConverted = { 21 | val cfg = ConfigFactory.parseString("myValue = true") 22 | cfg.as[Boolean]("myValue") must beTrue 23 | } 24 | 25 | def readAValue = prop { (b: Boolean) => 26 | val cfg = ConfigFactory.parseString(s"myValue = $b") 27 | cfg.as[Boolean]("myValue") must beEqualTo(b) 28 | } 29 | 30 | def getAsSome = prop { (b: Boolean) => 31 | val cfg = ConfigFactory.parseString(s"myValue = $b") 32 | cfg.getAs[Boolean]("myValue") must beSome(b) 33 | } 34 | 35 | def getAsNone = { 36 | val cfg = ConfigFactory.parseString("myValue = true") 37 | cfg.getAs[Boolean]("nonValue") must beNone 38 | } 39 | 40 | def getOrElseFromConfig = { 41 | val configString = "arealstring" 42 | val cfg = ConfigFactory.parseString(s"myValue = $configString") 43 | cfg.getOrElse("myValue", "notarealstring") must beEqualTo(configString) 44 | } 45 | 46 | def getOrElseFromDefault = { 47 | val cfg = ConfigFactory.parseString("myValue = arealstring") 48 | val default = "adefaultstring" 49 | cfg.getOrElse("nonValue", default) must beEqualTo(default) 50 | } 51 | 52 | def getOrElseFromConfigWithCustomValueReader = { 53 | val cfg = ConfigFactory.parseString("myValue = 124") 54 | val default = 23.toByte 55 | 56 | implicit val byteReader = new ValueReader[Byte] { 57 | def read(config: Config, path: String): Byte = config.getInt(path).toByte 58 | } 59 | 60 | cfg.getOrElse("myValue", default) must beEqualTo(124.toByte) 61 | } 62 | 63 | def acceptAConfigKey = prop { (b: Boolean) => 64 | val cfg = ConfigFactory.parseString(s"myValue = $b") 65 | val key: ConfigKey[Boolean] = SimpleConfigKey("myValue") 66 | cfg(key) must beEqualTo(b) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/test/scala/net/ceedubs/ficus/readers/EnumerationReadersSpec.scala: -------------------------------------------------------------------------------- 1 | package net.ceedubs.ficus.readers 2 | 3 | import com.typesafe.config.{ConfigException, ConfigFactory} 4 | import net.ceedubs.ficus.Spec 5 | import EnumerationReadersSpec._ 6 | 7 | import scala.reflect.ClassTag 8 | 9 | class EnumerationReadersSpec extends Spec with EnumerationReader { 10 | def is = s2""" 11 | An enumeration value reader should 12 | map a string value to its enumeration counterpart $successStringMapping 13 | map a int value to its enumeration counterpart $successIntMapping 14 | throw exception if value couldn't be converted to enum value $invalidMapping 15 | throw exception if enumeration is contained in a class or trait $notInstantiable 16 | throw exception if enumeration is not an object $notObject 17 | """ 18 | 19 | def successStringMapping = { 20 | val cfg = ConfigFactory.parseString("myValue = SECOND") 21 | implicit val classTag = ClassTag[StringValueEnum.type](StringValueEnum.getClass) 22 | enumerationValueReader[StringValueEnum.type].read(cfg, "myValue") must_== StringValueEnum.second 23 | } 24 | 25 | def successIntMapping = { 26 | val cfg = ConfigFactory.parseString("myValue = second") 27 | implicit val classTag = ClassTag[IntValueEnum.type](IntValueEnum.getClass) 28 | enumerationValueReader[IntValueEnum.type].read(cfg, "myValue") must_== IntValueEnum.second 29 | } 30 | 31 | def invalidMapping = { 32 | val cfg = ConfigFactory.parseString("myValue = fourth") 33 | implicit val classTag = ClassTag[StringValueEnum.type](StringValueEnum.getClass) 34 | enumerationValueReader[StringValueEnum.type].read(cfg, "myValue") must throwA[ConfigException.BadValue] 35 | } 36 | 37 | def notInstantiable = { 38 | val cfg = ConfigFactory.parseString("myValue = fourth") 39 | implicit val classTag = ClassTag[InnerEnum.type](InnerEnum.getClass) 40 | enumerationValueReader[InnerEnum.type].read(cfg, "myValue") must throwA[ConfigException.Generic] 41 | } 42 | 43 | def notObject = { 44 | val cfg = ConfigFactory.parseString("myValue = fourth") 45 | implicit val classTag = ClassTag[NotObject](classOf[NotObject]) 46 | enumerationValueReader[NotObject].read(cfg, "myValue") must throwA[ConfigException.Generic] 47 | } 48 | 49 | object InnerEnum extends Enumeration 50 | } 51 | 52 | object EnumerationReadersSpec { 53 | 54 | object StringValueEnum extends Enumeration { 55 | val first = Value("FIRST") 56 | val second = Value("SECOND") 57 | val third = Value("THIRD") 58 | } 59 | 60 | object IntValueEnum extends Enumeration { 61 | val first, second, third = Value 62 | } 63 | 64 | class NotObject extends Enumeration 65 | } 66 | -------------------------------------------------------------------------------- /src/test/scala/net/ceedubs/ficus/ConfigSerializer.scala: -------------------------------------------------------------------------------- 1 | package net.ceedubs.ficus 2 | 3 | import com.typesafe.config.{ConfigFactory, ConfigUtil, ConfigValue} 4 | 5 | import scala.language.implicitConversions 6 | 7 | trait ConfigSerializer[A] { 8 | def serialize(a: A): String 9 | } 10 | 11 | object ConfigSerializer { 12 | def apply[A](f: A => String): ConfigSerializer[A] = new ConfigSerializer[A] { 13 | def serialize(a: A): String = f(a) 14 | } 15 | 16 | def fromToString[A]: ConfigSerializer[A] = apply[A](_.toString) 17 | 18 | protected def serializeIterable[A](iterable: Iterable[A])(implicit serializer: ConfigSerializer[A]): String = { 19 | val elements = iterable.map(a => serializer.serialize(a)) 20 | s"[${elements.mkString(", ")}]" 21 | } 22 | 23 | implicit val stringSerializer: ConfigSerializer[String] = apply[String](ConfigUtil.quoteString) 24 | implicit val booleanSerializer: ConfigSerializer[Boolean] = fromToString[Boolean] 25 | implicit val intSerializer: ConfigSerializer[Int] = fromToString[Int] 26 | implicit val longSerializer: ConfigSerializer[Long] = fromToString[Long] 27 | implicit val doubleSerializer: ConfigSerializer[Double] = fromToString[Double] 28 | 29 | implicit def listSerializer[A: ConfigSerializer]: ConfigSerializer[List[A]] = apply[List[A]](serializeIterable) 30 | implicit def serializerForSets[A: ConfigSerializer]: ConfigSerializer[Set[A]] = apply[Set[A]](serializeIterable) 31 | implicit def indexedSeqSerializer[A: ConfigSerializer]: ConfigSerializer[IndexedSeq[A]] = 32 | apply[IndexedSeq[A]](serializeIterable) 33 | implicit def vectorSerializer[A: ConfigSerializer]: ConfigSerializer[Vector[A]] = apply[Vector[A]](serializeIterable) 34 | implicit def arraySerializer[A: ConfigSerializer]: ConfigSerializer[Array[A]] = apply[Array[A]] { array => 35 | serializeIterable(array.toIterable) 36 | } 37 | def iterableSerializer[A: ConfigSerializer]: ConfigSerializer[Iterable[A]] = apply[Iterable[A]](serializeIterable) 38 | 39 | implicit def stringKeyMapSerializer[A](implicit 40 | valueSerializer: ConfigSerializer[A] 41 | ): ConfigSerializer[Map[String, A]] = 42 | new ConfigSerializer[Map[String, A]] { 43 | def serialize(map: Map[String, A]): String = { 44 | val lines = map.toIterable.map( 45 | Function.tupled((key, value) => s"${stringSerializer.serialize(key)} = ${valueSerializer.serialize(value)}") 46 | ) 47 | s"{\n ${lines.mkString("\n ")}\n}" 48 | } 49 | } 50 | 51 | } 52 | 53 | final case class ConfigSerializerOps[A](a: A, serializer: ConfigSerializer[A]) { 54 | def asConfigValue: String = serializer.serialize(a) 55 | def toConfigValue: ConfigValue = ConfigFactory.parseString(s"dummy=$asConfigValue").root().get("dummy") 56 | } 57 | 58 | object ConfigSerializerOps { 59 | implicit def toConfigSerializerOps[A](a: A)(implicit serializer: ConfigSerializer[A]): ConfigSerializerOps[A] = 60 | ConfigSerializerOps[A](a, serializer) 61 | } 62 | -------------------------------------------------------------------------------- /src/test/scala/net/ceedubs/ficus/readers/EitherReadersSpec.scala: -------------------------------------------------------------------------------- 1 | package net.ceedubs.ficus.readers 2 | 3 | import com.typesafe.config.ConfigFactory 4 | import net.ceedubs.ficus.{ConfigSerializer, Spec} 5 | import net.ceedubs.ficus.ConfigSerializerOps._ 6 | import org.scalacheck.Arbitrary 7 | 8 | import scala.util.{Failure, Try} 9 | import scala.collection.JavaConverters._ 10 | 11 | class EitherReadersSpec 12 | extends Spec 13 | with EitherReader 14 | with OptionReader 15 | with AnyValReaders 16 | with StringReader 17 | with TryReader 18 | with CollectionReaders { 19 | def is = s2""" 20 | An Either value reader should 21 | should read right side when possible $readRightSideString 22 | fallback to left side when key is missing $fallbackToLeftSideOnMissingKey 23 | fallback to left when failing to read right $fallbackToLeftSideOnBadRightValue 24 | fail when both sides fail $rightAndLeftFailure 25 | handle a Try on the right side $rightSideTry 26 | handle a Try on the left side $leftSideTry 27 | handle complex types $handleComplexTypes 28 | """ 29 | 30 | def readRightSideString = prop { (a: String) => 31 | val cfg = a.toConfigValue.atKey("x") 32 | eitherReader[String, String].read(cfg, "x") must beEqualTo(Right(a)) 33 | } 34 | 35 | def fallbackToLeftSideOnMissingKey = prop { (a: String) => 36 | eitherReader[Option[String], String].read(ConfigFactory.empty(), "x") must beEqualTo(Left(None)) 37 | } 38 | 39 | def fallbackToLeftSideOnBadRightValue = prop { (a: Int) => 40 | val badVal = a.toString + "xx" 41 | eitherReader[String, Int].read(badVal.toConfigValue.atKey("x"), "x") must beEqualTo(Left(badVal)) 42 | } 43 | 44 | def rightAndLeftFailure = prop { (a: Int) => 45 | val badVal = a.toString + "xx" 46 | tryValueReader(eitherReader[Int, Int]).read(badVal.toConfigValue.atKey("x"), "x") must beAnInstanceOf[Failure[Int]] 47 | } 48 | 49 | def rightSideTry = prop { (a: Int) => 50 | val badVal = a.toString + "xx" 51 | eitherReader[Int, Try[Int]].read(a.toConfigValue.atKey("x"), "x") must beRight(a) 52 | eitherReader[Int, Try[Int]].read(badVal.toConfigValue.atKey("x"), "x") must beRight(beFailedTry[Int]) 53 | } 54 | 55 | def leftSideTry = prop { (a: Int) => 56 | val badVal = a.toString + "xx" 57 | eitherReader[Try[String], Int].read(badVal.toConfigValue.atKey("x"), "x") must beLeft( 58 | beSuccessfulTry[String](badVal) 59 | ) 60 | eitherReader[Try[Int], Int].read(badVal.toConfigValue.atKey("x"), "x") must beLeft(beFailedTry[Int]) 61 | } 62 | 63 | def handleComplexTypes = prop { (a: Int, b: Int) => 64 | val iMap = Map("a" -> a, "b" -> b) 65 | val sMap = Map("a" -> s"${a}xx", "b" -> s"${b}xx") 66 | 67 | eitherReader[Map[String, String], Map[String, String]].read(sMap.toConfigValue.atKey("a"), "a") must beRight(sMap) 68 | eitherReader[Map[String, String], Map[String, Int]].read(iMap.toConfigValue.atKey("a"), "a") must beRight(iMap) 69 | eitherReader[Map[String, String], Map[String, Int]].read(sMap.toConfigValue.atKey("a"), "a") must beLeft(sMap) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/test/scala/net/ceedubs/ficus/readers/InetSocketAddressReadersSpec.scala: -------------------------------------------------------------------------------- 1 | package net.ceedubs.ficus.readers 2 | 3 | import java.net.InetSocketAddress 4 | 5 | import com.typesafe.config.ConfigException.WrongType 6 | import com.typesafe.config.ConfigFactory 7 | import net.ceedubs.ficus.Spec 8 | 9 | class InetSocketAddressReadersSpec extends Spec with InetSocketAddressReaders { 10 | def is = s2""" 11 | The InetSocketAddress value readers should 12 | read a valid named InetSocketAddress $readValidNamedInetSocketAddress 13 | read a valid raw InetSocketAddress $readValidRawInetSocketAddress 14 | detect wrong type on a malformed InetSocketAddress $readMalformedInetSocketAddress 15 | read a valid comma-separated list of InetSocketAddresses $readValidInetSocketAddresses 16 | read a valid comma-separated list of InetSocketAddresses surrounded by whitespace $readValidInetSocketAddressesWithWhiteSpace 17 | detect wrong type on malformed InetSocketAddresses $readMalformedInetSocketAddresses 18 | be able to read a single InetSocketAddress as a list of InetSocketAddresses $readSingleInetSocketAddress 19 | """ 20 | 21 | def readValidNamedInetSocketAddress = { 22 | val inetSocketAddress = """localhost:65535""" 23 | val cfg = ConfigFactory.parseString(s"myValue = ${"\"" + inetSocketAddress + "\""}") 24 | inetSocketAddressReader.read(cfg, "myValue") must beEqualTo(new InetSocketAddress("localhost", 65535)) 25 | } 26 | 27 | def readValidRawInetSocketAddress = { 28 | val inetSocketAddress = """127.0.0.1:65535""" 29 | val cfg = ConfigFactory.parseString(s"myValue = ${"\"" + inetSocketAddress + "\""}") 30 | inetSocketAddressReader.read(cfg, "myValue") must beEqualTo(new InetSocketAddress("127.0.0.1", 65535)) 31 | } 32 | 33 | def readMalformedInetSocketAddress = { 34 | val malformedInetSocketAddress = """localhost123""" 35 | val cfg = ConfigFactory.parseString(s"myValue = ${"\"" + malformedInetSocketAddress + "\""}") 36 | inetSocketAddressReader.read(cfg, "myValue") must throwA[WrongType] 37 | } 38 | 39 | def readValidInetSocketAddresses = { 40 | val inetSocketAddresses = """localhost:65535,localhost:80,localhost:443""" 41 | val cfg = ConfigFactory.parseString(s"myValue = ${"\"" + inetSocketAddresses + "\""}") 42 | inetSocketAddressListReader.read(cfg, "myValue") must beEqualTo( 43 | List( 44 | new InetSocketAddress("localhost", 65535), 45 | new InetSocketAddress("localhost", 80), 46 | new InetSocketAddress("localhost", 443) 47 | ) 48 | ) 49 | } 50 | 51 | def readValidInetSocketAddressesWithWhiteSpace = { 52 | val inetSocketAddresses = """localhost: 65535, localhost: 80, localhost: 443""" 53 | val cfg = ConfigFactory.parseString(s"myValue = ${"\"" + inetSocketAddresses + "\""}") 54 | inetSocketAddressListReader.read(cfg, "myValue") must beEqualTo( 55 | List( 56 | new InetSocketAddress("localhost", 65535), 57 | new InetSocketAddress("localhost", 80), 58 | new InetSocketAddress("localhost", 443) 59 | ) 60 | ) 61 | } 62 | 63 | def readMalformedInetSocketAddresses = { 64 | val malformedInetSocketAddresses = """localhost:65535 + localhost:80""" 65 | val cfg = ConfigFactory.parseString(s"myValue = ${"\"" + malformedInetSocketAddresses + "\""}") 66 | inetSocketAddressListReader.read(cfg, "myValue") must throwA[WrongType] 67 | } 68 | 69 | def readSingleInetSocketAddress = { 70 | val inetSocketAddress = """localhost:65535""" 71 | val cfg = ConfigFactory.parseString(s"myValue = ${"\"" + inetSocketAddress + "\""}") 72 | inetSocketAddressListReader.read(cfg, "myValue") must beEqualTo( 73 | List(new InetSocketAddress("localhost", 65535)) 74 | ) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /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 | import scala.language.higherKinds 12 | 13 | class CollectionReadersSpec extends Spec with CollectionReaders { 14 | def is = s2""" 15 | The collection value readers should 16 | read a list ${readCollection[List]} 17 | read a set ${readCollection[Set]} 18 | read an array ${readCollection[Array]} 19 | read an indexed sequence ${readCollection[IndexedSeq]} 20 | read a vector ${readCollection[Vector]} 21 | read an iterable $readIterable 22 | read a map with strings as keys $readStringMap 23 | read a map nested in another object $readNestedMap 24 | read a collection when used directly $readCollectionUsedDirectly 25 | """ 26 | 27 | def readIterable = { 28 | implicit def iterableSerializer[A: ConfigSerializer]: ConfigSerializer[Iterable[A]] = 29 | ConfigSerializer.iterableSerializer 30 | readCollection[Iterable] 31 | } 32 | 33 | def readStringMap = { 34 | def reads[A: Arbitrary: ValueReader: ConfigSerializer] = prop { (map: Map[String, A]) => 35 | val cfg = ConfigFactory.parseString(s"myValue = ${map.asConfigValue}") 36 | mapValueReader[A].read(cfg, "myValue") must beEqualTo(map) 37 | } 38 | 39 | reads[String] && reads[Boolean] && reads[Int] && reads[Long] && reads[Double] 40 | } 41 | 42 | def readNestedMap = { 43 | val cfg = ConfigFactory.parseString(""" 44 | |wrapper { 45 | | myValue { 46 | | item1 = "value1" 47 | | item2 = "value2" 48 | | } 49 | |} 50 | """.stripMargin) 51 | mapValueReader[String].read(cfg, "wrapper.myValue") must beEqualTo(Map("item1" -> "value1", "item2" -> "value2")) 52 | } 53 | 54 | protected def readCollection[C[_]](implicit 55 | AS: Arbitrary[C[String]], 56 | SS: ConfigSerializer[C[String]], 57 | RS: ValueReader[C[String]], 58 | AB: Arbitrary[C[Boolean]], 59 | SB: ConfigSerializer[C[Boolean]], 60 | RB: ValueReader[C[Boolean]], 61 | AI: Arbitrary[C[Int]], 62 | SI: ConfigSerializer[C[Int]], 63 | RI: ValueReader[C[Int]], 64 | AL: Arbitrary[C[Long]], 65 | SL: ConfigSerializer[C[Long]], 66 | RL: ValueReader[C[Long]], 67 | AD: Arbitrary[C[Double]], 68 | SD: ConfigSerializer[C[Double]], 69 | RD: ValueReader[C[Double]] 70 | ) = { 71 | 72 | def reads[V](implicit arb: Arbitrary[C[V]], serializer: ConfigSerializer[C[V]], reader: ValueReader[C[V]]) = 73 | prop { (values: C[V]) => 74 | val cfg = ConfigFactory.parseString(s"myValue = ${values.asConfigValue}") 75 | reader.read(cfg, "myValue") must beEqualTo(values) 76 | } 77 | 78 | reads[String] && reads[Boolean] && reads[Int] && reads[Long] && reads[Double] 79 | } 80 | 81 | def readCollectionUsedDirectly = { 82 | val cfg = ConfigFactory.parseString("set: [1, 2, 2, 3]") 83 | traversableReader[Set, Int].read(cfg, "set") must beEqualTo(Set(1, 2, 3)) 84 | } 85 | 86 | } 87 | 88 | object CollectionReaderSpec { 89 | import scala.collection._ 90 | 91 | implicit def buildableIndexedSeq[T]: Buildable[T, IndexedSeq[T]] = new Buildable[T, IndexedSeq[T]] { 92 | def builder = IndexedSeq.newBuilder[T] 93 | } 94 | 95 | implicit def buildableVector[T]: Buildable[T, Vector[T]] = new Buildable[T, Vector[T]] { 96 | def builder = Vector.newBuilder[T] 97 | } 98 | 99 | implicit def buildableIterable[T]: Buildable[T, Iterable[T]] = new Buildable[T, Iterable[T]] { 100 | def builder = new mutable.ListBuffer[T] 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/test/scala-2/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.EnumerationReader._ 8 | import net.ceedubs.ficus.readers.ValueReader 9 | 10 | case class ServiceConfig(urls: Set[String], maxConnections: Int, httpsRequired: Boolean = false) 11 | 12 | object Country extends Enumeration { 13 | val DE = Value("DE") 14 | val IT = Value("IT") 15 | val NL = Value("NL") 16 | val US = Value("US") 17 | val GB = Value("GB") 18 | } 19 | 20 | class ExampleSpec extends Specification { 21 | 22 | // an example config snippet for us to work with 23 | val config = ConfigFactory.parseString(""" 24 | |services { 25 | | users { 26 | | urls = ["localhost:8001"] 27 | | maxConnections = 100 28 | | httpsRequired = true 29 | | } 30 | | analytics { 31 | | urls = ["localhost:8002", "localhost:8003"] 32 | | maxConnections = 25 33 | | } 34 | |} 35 | |countries = [DE, US, GB] 36 | """.stripMargin) 37 | 38 | "Ficus config" should { 39 | "make Typesafe config more Scala-friendly" in { 40 | val userServiceConfig = config.as[Config]("services.users") 41 | userServiceConfig.as[Set[String]]("urls") must beEqualTo(Set("localhost:8001")) 42 | userServiceConfig.as[Int]("maxConnections") must beEqualTo(100) 43 | userServiceConfig.as[Option[Boolean]]("httpsRequired") must beSome(true) 44 | 45 | val analyticsServiceConfig = config.as[Config]("services.analytics") 46 | analyticsServiceConfig.as[List[String]]("urls") must beEqualTo(List("localhost:8002", "localhost:8003")) 47 | val analyticsServiceRequiresHttps = analyticsServiceConfig.as[Option[Boolean]]("httpsRequired") getOrElse false 48 | analyticsServiceRequiresHttps must beFalse 49 | 50 | config.as[Seq[Country.Value]]("countries") must be equalTo Seq(Country.DE, Country.US, Country.GB) 51 | } 52 | 53 | "Automagically be able to hydrate arbitrary types from config" in { 54 | // Tkere are a few restrictions on types that can be read. See README file in root of project 55 | val analyticsConfig = config.as[ServiceConfig]("services.analytics") 56 | analyticsConfig.maxConnections must beEqualTo(25) 57 | // since this value isn't in the config, it will fall back to the default for the case class 58 | analyticsConfig.httpsRequired must beFalse 59 | } 60 | 61 | "Be easily extensible" in { 62 | // If we want a value reader that defaults httpsRequired to true instead of false (the default on the case 63 | // class) we can define a custom value reader for ServiceConfig 64 | implicit val serviceConfigReader: ValueReader[ServiceConfig] = ValueReader.relative { serviceConfig => 65 | ServiceConfig( 66 | urls = serviceConfig.as[Set[String]]("urls"), 67 | maxConnections = serviceConfig.getInt("maxConnections"), // the old-fashioned way is fine too! 68 | httpsRequired = serviceConfig.as[Option[Boolean]]("httpsRequired") getOrElse true 69 | ) 70 | } 71 | 72 | // so we don't have to add a "services." prefix for each service 73 | val servicesConfig = config.as[Config]("services") 74 | 75 | val analyticsServiceConfig: ServiceConfig = servicesConfig.as[ServiceConfig]("analytics") 76 | // the analytics service config doesn't define an "httpsRequired" value, but the serviceConfigReader defaults 77 | // to true if it is empty with its 'getOrElse true' on the extracted Option 78 | analyticsServiceConfig.httpsRequired must beTrue 79 | 80 | val userServiceConfig: ServiceConfig = servicesConfig.as[ServiceConfig]("users") 81 | 82 | val servicesMap = config.as[Map[String, ServiceConfig]]("services") 83 | servicesMap must beEqualTo(Map("users" -> userServiceConfig, "analytics" -> analyticsServiceConfig)) 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/test/scala/net/ceedubs/ficus/readers/BigNumberReadersSpec.scala: -------------------------------------------------------------------------------- 1 | package net.ceedubs.ficus.readers 2 | 3 | import com.typesafe.config.ConfigException.WrongType 4 | import com.typesafe.config.ConfigFactory 5 | import net.ceedubs.ficus.Spec 6 | 7 | class BigNumberReadersSpec extends Spec with BigNumberReaders { 8 | def is = s2""" 9 | The BigDecimal value reader should 10 | read a double $readDoubleAsBigDecimal 11 | read a long $readLongAsBigDecimal 12 | read an int $readIntAsBigDecimal 13 | read a bigInt $readBigIntAsBigDecimal 14 | read a bigDecimalAsString $readBigDecimalAsStringBigDecimal 15 | read a bigIntAsString $readBigIntAsStringBigDecimal 16 | detect wrong type on malformed BigDecimal $readMalformedBigDecimal 17 | 18 | The BigInt value reader should 19 | read an int $readIntAsBigInt 20 | read a long $readLongAsBigInt 21 | read a bigInt $readBigIntAsBigInt 22 | read a bigIntAsString $readBigIntAsStringBigInt 23 | detect wrong type on malformed BigInt $readMalformedBigInt 24 | """ 25 | 26 | def readDoubleAsBigDecimal = prop { (d: Double) => 27 | val cfg = ConfigFactory.parseString(s"myValue = $d") 28 | bigDecimalReader.read(cfg, "myValue") must beEqualTo(BigDecimal(d)) 29 | } 30 | 31 | def readLongAsBigDecimal = prop { (l: Long) => 32 | val cfg = ConfigFactory.parseString(s"myValue = $l") 33 | bigDecimalReader.read(cfg, "myValue") must beEqualTo(BigDecimal(l)) 34 | } 35 | 36 | def readIntAsBigDecimal = prop { (i: Int) => 37 | val cfg = ConfigFactory.parseString(s"myValue = $i") 38 | bigDecimalReader.read(cfg, "myValue") must beEqualTo(BigDecimal(i)) 39 | } 40 | 41 | /* 42 | Due to differences with BigDecimal precision handling in scala 2.10, this 43 | test is temporarily disabled. The next test compares the string 44 | representation of the BigDecimal and serves as a test of the actual 45 | functionality provided by this library, which simply parses the number 46 | as a string and calls BigDecimal's apply method. The quality of that 47 | BigDecimal implementation is not the concern of this library. 48 | 49 | def readBigDecimal = prop{ b: BigDecimal => 50 | scala.util.Try(BigDecimal(b.toString)).toOption.isDefined ==> { 51 | val cfg = ConfigFactory.parseString(s"myValue = $b") 52 | bigDecimalReader.read(cfg, "myValue") must beEqualTo(b) 53 | } 54 | } 55 | */ 56 | 57 | def readBigDecimalAsStringBigDecimal = prop { (b: BigDecimal) => 58 | scala.util.Try(BigDecimal(b.toString)).toOption.isDefined ==> { 59 | val cfg = ConfigFactory.parseString(s"myValue = ${b.toString}") 60 | bigDecimalReader.read(cfg, "myValue") must beEqualTo(BigDecimal(b.toString)) 61 | } 62 | } 63 | 64 | def readBigIntAsStringBigDecimal = prop { (b: BigInt) => 65 | scala.util.Try(BigDecimal(b.toString)).toOption.isDefined ==> { 66 | val cfg = ConfigFactory.parseString(s"myValue = ${b.toString}") 67 | bigDecimalReader.read(cfg, "myValue") must beEqualTo(BigDecimal(b.toString)) 68 | } 69 | } 70 | 71 | def readMalformedBigDecimal = { 72 | val malformedBigDecimal = "foo" 73 | val cfg = ConfigFactory.parseString(s"myValue = ${"\"" + malformedBigDecimal + "\""}") 74 | bigDecimalReader.read(cfg, "myValue") must throwA[WrongType] 75 | } 76 | 77 | def readBigIntAsBigDecimal = prop { (b: BigInt) => 78 | scala.util.Try(BigDecimal(b)).toOption.isDefined ==> { 79 | val cfg = ConfigFactory.parseString(s"myValue = $b") 80 | bigDecimalReader.read(cfg, "myValue") must beEqualTo(BigDecimal(b)) 81 | } 82 | } 83 | 84 | def readIntAsBigInt = prop { (i: Int) => 85 | val cfg = ConfigFactory.parseString(s"myValue = $i") 86 | bigIntReader.read(cfg, "myValue") must beEqualTo(BigInt(i)) 87 | } 88 | 89 | def readLongAsBigInt = prop { (l: Long) => 90 | val cfg = ConfigFactory.parseString(s"myValue = $l") 91 | bigIntReader.read(cfg, "myValue") must beEqualTo(BigInt(l)) 92 | } 93 | 94 | def readBigIntAsBigInt = prop { (b: BigInt) => 95 | scala.util.Try(BigInt(b.toString)).toOption.isDefined ==> { 96 | val cfg = ConfigFactory.parseString(s"myValue = $b") 97 | bigIntReader.read(cfg, "myValue") must beEqualTo(BigInt(b.toString)) 98 | } 99 | } 100 | 101 | def readBigIntAsStringBigInt = prop { (b: BigInt) => 102 | scala.util.Try(BigInt(b.toString)).toOption.isDefined ==> { 103 | val cfg = ConfigFactory.parseString(s"myValue = ${b.toString}") 104 | bigIntReader.read(cfg, "myValue") must beEqualTo(BigInt(b.toString)) 105 | } 106 | } 107 | 108 | def readMalformedBigInt = { 109 | val malformedBigInt = "foo" 110 | val cfg = ConfigFactory.parseString(s"myValue = ${"\"" + malformedBigInt + "\""}") 111 | bigIntReader.read(cfg, "myValue") must throwA[WrongType] 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/main/scala-2/net/ceedubs/ficus/readers/ArbitraryTypeReader.scala: -------------------------------------------------------------------------------- 1 | package net.ceedubs.ficus.readers 2 | 3 | import com.typesafe.config.Config 4 | import macrocompat.bundle 5 | import net.ceedubs.ficus.util.ReflectionUtils 6 | 7 | import scala.language.experimental.macros 8 | import scala.reflect.internal.{Definitions, StdNames, SymbolTable} 9 | import scala.reflect.macros.blackbox 10 | 11 | trait ArbitraryTypeReader { 12 | implicit def arbitraryTypeValueReader[T]: Generated[ValueReader[T]] = 13 | macro ArbitraryTypeReaderMacros.arbitraryTypeValueReader[T] 14 | } 15 | 16 | object ArbitraryTypeReader extends ArbitraryTypeReader 17 | 18 | @bundle 19 | class ArbitraryTypeReaderMacros(val c: blackbox.Context) extends ReflectionUtils { 20 | import c.universe._ 21 | 22 | def arbitraryTypeValueReader[T: c.WeakTypeTag]: c.Expr[Generated[ValueReader[T]]] = 23 | reify { 24 | Generated(new ValueReader[T] { 25 | def read(config: Config, path: String): T = instantiateFromConfig[T]( 26 | config = c.Expr[Config](Ident(TermName("config"))), 27 | path = c.Expr[String](Ident(TermName("path"))), 28 | mapper = c.Expr[NameMapper](q"""_root_.net.ceedubs.ficus.readers.NameMapper()""") 29 | ).splice 30 | }) 31 | } 32 | 33 | def instantiateFromConfig[T: c.WeakTypeTag]( 34 | config: c.Expr[Config], 35 | path: c.Expr[String], 36 | mapper: c.Expr[NameMapper] 37 | ): c.Expr[T] = { 38 | val returnType = c.weakTypeOf[T] 39 | 40 | def fail(reason: String) = 41 | c.abort(c.enclosingPosition, s"Cannot generate a config value reader for type $returnType, because $reason") 42 | 43 | val companionSymbol = returnType.typeSymbol.companion match { 44 | case NoSymbol => None 45 | case x => Some(x) 46 | } 47 | 48 | val initMethod = instantiationMethod[T](fail) 49 | 50 | val instantiationArgs = extractMethodArgsFromConfig[T]( 51 | method = initMethod, 52 | companionObjectMaybe = companionSymbol, 53 | config = config, 54 | path = path, 55 | mapper = mapper, 56 | fail = fail 57 | ) 58 | val instantiationObject = companionSymbol 59 | .filterNot(_ => initMethod.isConstructor) 60 | .map(Ident(_)) 61 | .getOrElse(New(Ident(returnType.typeSymbol))) 62 | val instantiationCall = Select(instantiationObject, initMethod.name) 63 | c.Expr[T](Apply(instantiationCall, instantiationArgs)) 64 | } 65 | 66 | def extractMethodArgsFromConfig[T: c.WeakTypeTag]( 67 | method: c.universe.MethodSymbol, 68 | companionObjectMaybe: Option[c.Symbol], 69 | config: c.Expr[Config], 70 | path: c.Expr[String], 71 | mapper: c.Expr[NameMapper], 72 | fail: String => Nothing 73 | ): List[c.Tree] = { 74 | val decodedMethodName = method.name.decodedName.toString 75 | 76 | if (!method.isPublic) fail(s"'$decodedMethodName' method is not public") 77 | 78 | method.paramLists.head.zipWithIndex map { case (param, index) => 79 | val name = param.name.decodedName.toString 80 | val key = q"""if ($path == ".") $mapper.map($name) else $path + "." + $mapper.map($name)""" 81 | val returnType: Type = param.typeSignatureIn(c.weakTypeOf[T]) 82 | 83 | companionObjectMaybe.filter(_ => param.asTerm.isParamWithDefault) map { companionObject => 84 | val optionType = appliedType(weakTypeOf[Option[_]].typeConstructor, List(returnType)) 85 | val optionReaderType = appliedType(weakTypeOf[ValueReader[_]].typeConstructor, List(optionType)) 86 | val optionReader = c.inferImplicitValue(optionReaderType, silent = true) match { 87 | case EmptyTree => 88 | fail( 89 | 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" 90 | ) 91 | case x => x 92 | } 93 | val argValueMaybe = q"$optionReader.read($config, $key)" 94 | Apply( 95 | Select(argValueMaybe, TermName("getOrElse")), 96 | List { 97 | // fall back to default value for param 98 | val u = c.universe.asInstanceOf[Definitions with SymbolTable with StdNames] 99 | val getter = u.nme.defaultGetterName(u.newTermName(decodedMethodName), index + 1) 100 | Select(Ident(companionObject), TermName(getter.encoded)) 101 | } 102 | ) 103 | } getOrElse { 104 | val readerType = appliedType(weakTypeOf[ValueReader[_]].typeConstructor, List(returnType)) 105 | val reader = c.inferImplicitValue(readerType, silent = true) match { 106 | case EmptyTree => 107 | fail( 108 | s"an implicit value reader of type $readerType must be in scope to read parameter '$name' on '$decodedMethodName' method" 109 | ) 110 | case x => x 111 | } 112 | q"$reader.read($config, $key)" 113 | } 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/test/scala-2/net/ceedubs/ficus/readers/CaseClassReadersSpec.scala: -------------------------------------------------------------------------------- 1 | package net.ceedubs.ficus 2 | package readers 3 | 4 | import com.typesafe.config.ConfigFactory 5 | import com.typesafe.config.Config 6 | import net.ceedubs.ficus.Ficus._ 7 | import net.ceedubs.ficus.readers.ArbitraryTypeReader._ 8 | import ConfigSerializerOps._ 9 | 10 | object CaseClassReadersSpec { 11 | case class SimpleCaseClass(bool: Boolean) 12 | case class MultipleFields(string: String, long: Long) 13 | case class WithOption(option: Option[String]) 14 | case class WithNestedCaseClass(simple: SimpleCaseClass) 15 | case class ValueClass(int: Int) extends AnyVal 16 | case class CompanionImplicit(value: Int) 17 | object CompanionImplicit { 18 | implicit val reader: ValueReader[CompanionImplicit] = 19 | implicitly[ValueReader[Int]].map(CompanionImplicit.apply) 20 | } 21 | case class WithNestedCompanionImplicit(value: CompanionImplicit) 22 | 23 | case class WithNestedValueClass(valueClass: ValueClass) 24 | case class WithDefault(string: String = "bar") 25 | case class Foo( 26 | bool: Boolean, 27 | intOpt: Option[Int], 28 | withNestedCaseClass: WithNestedCaseClass, 29 | withNestedValueClass: WithNestedValueClass 30 | ) 31 | } 32 | 33 | class CaseClassReadersSpec extends Spec { 34 | def is = s2""" 35 | A case class reader should 36 | be able to be used implicitly $useImplicitly 37 | hydrate a simple case class $hydrateSimpleCaseClass 38 | hydrate a simple case class from config itself $hydrateSimpleCaseClassFromSelf 39 | hydrate a case class with multiple fields $multipleFields 40 | use another implicit value reader for a field $withOptionField 41 | read a nested case class $withNestedCaseClass 42 | read a top-level value class $topLevelValueClass 43 | read a nested value class $nestedValueClass 44 | fall back to a default value $fallbackToDefault 45 | do a combination of these things $combination 46 | allow providing custom reader in companion $companionImplicitTopLevel 47 | allow providing custom reader in companion $nestedCompanionImplicit 48 | """ 49 | 50 | import CaseClassReadersSpec._ 51 | 52 | def useImplicitly = { 53 | val cfg = ConfigFactory.parseString("simple { bool = false }") 54 | cfg.as[SimpleCaseClass]("simple") must_== SimpleCaseClass(bool = false) 55 | } 56 | 57 | def hydrateSimpleCaseClass = prop { (bool: Boolean) => 58 | val cfg = ConfigFactory.parseString(s"simple { bool = $bool }") 59 | cfg.as[SimpleCaseClass]("simple") must_== SimpleCaseClass(bool = bool) 60 | } 61 | 62 | def hydrateSimpleCaseClassFromSelf = prop { (bool: Boolean) => 63 | val cfg = ConfigFactory.parseString(s"simple { bool = $bool }") 64 | cfg.getConfig("simple").as[SimpleCaseClass] must_== SimpleCaseClass(bool = bool) 65 | } 66 | 67 | def multipleFields = prop { (foo: String, long: Long) => 68 | val cfg = ConfigFactory.parseString(s""" 69 | |multipleFields { 70 | | string = ${foo.asConfigValue} 71 | | long = $long 72 | |} 73 | """.stripMargin) 74 | cfg.as[MultipleFields]("multipleFields") must_== MultipleFields(string = foo, long = long) 75 | } 76 | 77 | def withOptionField = prop { (s: String) => 78 | val cfg = ConfigFactory.parseString(s"""withOption { option = ${s.asConfigValue} }""") 79 | cfg.as[WithOption]("withOption") must_== WithOption(Some(s)) 80 | } 81 | 82 | def withNestedCaseClass = prop { (bool: Boolean) => 83 | val cfg = ConfigFactory.parseString(s""" 84 | |withNested { 85 | | simple { 86 | | bool = $bool 87 | | } 88 | |} 89 | """.stripMargin) 90 | cfg.as[WithNestedCaseClass]("withNested") must_== WithNestedCaseClass(simple = SimpleCaseClass(bool = bool)) 91 | } 92 | 93 | def topLevelValueClass = prop { (int: Int) => 94 | val cfg = ConfigFactory.parseString(s"valueClass { int = $int }") 95 | cfg.as[ValueClass]("valueClass") must_== ValueClass(int) 96 | } 97 | 98 | def nestedValueClass = prop { (int: Int) => 99 | val cfg = ConfigFactory.parseString(s""" 100 | |withNestedValueClass { 101 | | valueClass = { 102 | | int = $int 103 | | } 104 | |} 105 | """.stripMargin) 106 | cfg.as[WithNestedValueClass]("withNestedValueClass") must_== WithNestedValueClass( 107 | valueClass = ValueClass(int = int) 108 | ) 109 | } 110 | 111 | def companionImplicitTopLevel = prop { (int: Int) => 112 | val cfg = ConfigFactory.parseString(s"value = $int ") 113 | cfg.as[CompanionImplicit]("value") must_== CompanionImplicit(int) 114 | } 115 | 116 | def nestedCompanionImplicit = prop { (int: Int) => 117 | val cfg = ConfigFactory.parseString(s""" 118 | |withNestedCompanionImplicit { 119 | | value = $int 120 | |} 121 | """.stripMargin) 122 | cfg.as[WithNestedCompanionImplicit]("withNestedCompanionImplicit") must_== WithNestedCompanionImplicit( 123 | value = CompanionImplicit(value = int) 124 | ) 125 | } 126 | 127 | def fallbackToDefault = { 128 | val cfg = ConfigFactory.parseString("""withDefault { }""") 129 | cfg.as[WithDefault]("withDefault") must_== WithDefault() 130 | } 131 | 132 | def combination = prop { (fooBool: Boolean, simpleBool: Boolean, valueClassInt: Int) => 133 | val cfg = ConfigFactory.parseString(s""" 134 | |foo { 135 | | bool = $fooBool 136 | | withNestedCaseClass { 137 | | simple { 138 | | bool = $simpleBool 139 | | } 140 | | } 141 | | withNestedValueClass = { 142 | | valueClass = { 143 | | int = $valueClassInt 144 | | } 145 | | } 146 | |} 147 | """.stripMargin) 148 | cfg.as[Foo]("foo") must_== Foo( 149 | bool = fooBool, 150 | intOpt = None, 151 | withNestedCaseClass = WithNestedCaseClass(simple = SimpleCaseClass(bool = simpleBool)), 152 | withNestedValueClass = WithNestedValueClass(ValueClass(int = valueClassInt)) 153 | ) 154 | } 155 | 156 | } 157 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Official repo for ficus. Adopted from [ceedubs](https://github.com/ceedubs/ficus) 2 | 3 | # Ficus # 4 | Ficus is a lightweight companion to Typesafe config that makes it more Scala-friendly. 5 | 6 | Ficus adds an `as[A]` method to a normal [Typesafe Config](http://lightbend.github.io/config/latest/api/com/typesafe/config/Config.html) so you can do things like `config.as[Option[Int]]`, `config.as[List[String]]`, or even `config.as[MyClass]`. It is implemented with type classes so that it is easily extensible and many silly mistakes can be caught by the compiler. 7 | 8 | [![Build Status](https://github.com/iheartradio/ficus/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/iheartradio/ficus/actions/workflows/ci.yml?query=branch%3Amaster) 9 | [![Join the chat at https://gitter.im/iheartradio/ficus](https://badges.gitter.im/iheartradio/ficus.svg)](https://gitter.im/iheartradio/ficus?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 10 | [![Coverage Status](https://coveralls.io/repos/github/iheartradio/ficus/badge.svg?branch=master)](https://coveralls.io/github/iheartradio/ficus?branch=master) 11 | [![Latest version](https://index.scala-lang.org/iheartradio/ficus/ficus/latest.svg?color=orange)](https://index.scala-lang.org/iheartradio/ficus) 12 | 13 | # Examples # 14 | ```scala 15 | import net.ceedubs.ficus.Ficus._ 16 | import com.typesafe.config.ConfigFactory 17 | 18 | object Country extends Enumeration { 19 | val DE = Value("DE") 20 | val IT = Value("IT") 21 | val NL = Value("NL") 22 | val US = Value("US") 23 | val GB = Value("GB") 24 | } 25 | 26 | case class SomeCaseClass(foo: String, bar: Int, baz: Option[FiniteDuration]) 27 | 28 | class Examples { 29 | val config: Config = ConfigFactory.load() // standard Typesafe Config 30 | 31 | // Note: explicit typing isn't necessary. It's just in these examples to make it clear what the return types are. 32 | // This line could instead be: val appName = config.as[String]("app.name") 33 | val appName: String = config.as[String]("app.name") // equivalent to config.getString("app.name") 34 | 35 | // config.as[Option[Boolean]]("preloadCache") will return None if preloadCache isn't defined in the config 36 | val preloadCache: Boolean = config.as[Option[Boolean]]("preloadCache").getOrElse(false) 37 | 38 | val adminUserIds: Set[Long] = config.as[Set[Long]]("adminIds") 39 | 40 | // something such as "15 minutes" can be converted to a FiniteDuration 41 | val retryInterval: FiniteDuration = config.as[FiniteDuration]("retryInterval") 42 | 43 | // can extract arbitrary Enumeration types 44 | // Note: it throws an exception at runtime, if the enumeration type cannot be instantiated or 45 | // if a config value cannot be mapped to the enumeration value 46 | import net.ceedubs.ficus.readers.EnumerationReader._ 47 | val someEnumerationType: Seq[Country.Value] = config.as[Seq[Country.Value]]("countries") 48 | 49 | // can hydrate most arbitrary types 50 | // it first tries to use an apply method on the companion object and falls back to the primary constructor 51 | // if values are not in the config, they will fall back to the default value on the class/apply method 52 | import net.ceedubs.ficus.readers.ArbitraryTypeReader._ 53 | val someCaseClass: SomeCaseClass = config.as[SomeCaseClass]("someCaseClass") 54 | } 55 | ``` 56 | 57 | For more detailed examples and how they match up with what's defined in a config file, see [the example spec](https://github.com/ceedubs/ficus/blob/master/src/test/scala/net/ceedubs/ficus/ExampleSpec.scala). 58 | 59 | # Adding the dependency # 60 | 61 | 62 | Now add the Ficus dependency to your build SBT file as well: 63 | ```scala 64 | // for Scala 2.10.x 65 | libraryDependencies += "com.iheart" %% "ficus" % "1.0.2" 66 | 67 | // for Scala 2.11.x and Java 7 68 | libraryDependencies += "com.iheart" %% "ficus" % "1.1.3" 69 | 70 | // for Scala 2.11.x, 2.12.x, 2.13.x and Java 8 71 | // See the latest version in the download badge below. 72 | libraryDependencies += "com.iheart" %% "ficus" % //see latest version in the badge below 73 | ``` 74 | 75 | [![Latest version](https://index.scala-lang.org/iheartradio/ficus/ficus/latest.svg?color=orange)](https://index.scala-lang.org/iheartradio/ficus) 76 | 77 | If you want to take advantage of Ficus's ability to automatically hydrate arbitrary traits and classes from configuration, you need to be on Scala version 2.10.2 or higer, because this functionality depends on implicit macros. 78 | 79 | 80 | 81 | # Built-in readers # 82 | Out of the box, Ficus can read most types from config: 83 | * Primitives (`Boolean`, `Int`, `Long`, `Double`) 84 | * `String` 85 | * `Option[A]` 86 | * Collections (`List[A]`, `Set[A]`, `Map[String, A]`, `Array[A]`, etc. All types with a CanBuildFrom instance are supported) 87 | * `Config` and `ConfigValue` (Typesafe config/value) 88 | * `FiniteDuration` and `Duration` 89 | * The Scala `Enumeration` type. See [Enumeration support](#enumeration-support) 90 | * Most arbitrary classes (as well as traits that have an apply method for instantiation). See [Arbitrary type support](#arbitrary-type-support) 91 | 92 | In this context, `A` means any type for which a `ValueReader` is already defined. For example, `Option[String]` is supported out of the box because `String` is. If you want to be able to extract an `Option[Foo[A]]` for some type `Foo` that doesn't meet the supported type requirements (for example, this `Foo` has a type parameter), the option part is taken care of, but you will need to provide the implementation for extracting a `Foo[A]` from config. See [Custom extraction](#custom-extraction). 93 | 94 | # Imports # 95 | The easiest way to start using Ficus config is to just `import net.ceedubs.ficus.Ficus._` as was done in the Examples section. This will import all of the implicit values you need to start easily grabbing most basic types out of config using the `as` method that will become available on Typesafe `Config` objects. 96 | 97 | To enable Ficus's reading of `Enumeration` types, you can also import `net.ceedubs.ficus.readers.EnumerationReader._`. See [Enumeration support](#enumeration-support) 98 | 99 | To enable Ficus's macro-based reading of case classes and other types, you can also import `net.ceedubs.ficus.readers.ArbitraryTypeReader._`. See [Arbitrary type support](#arbitrary-type-support) 100 | 101 | If you would like to be more judicial about what you import (either to prevent namespace pollution or to potentially speed up compile times), you are free to specify which imports you need. 102 | 103 | You will probably want to `import net.ceedubs.ficus.Ficus.toFicusConfig`, which will provide an implicit conversion from Typesafe `Config` to `FicusConfig`, giving you the `as` method. 104 | 105 | You will then need a [ValueReader](https://github.com/iheartradio/ficus/blob/master/src/main/scala/net/ceedubs/ficus/readers/ValueReader.scala) for each type that you want to grab using `as`. You can choose whether you would like to get the reader via an import or a mixin Trait. For example, if you want to be able to call `as[String]`, you can either `import net.ceedubs.ficus.FicusConfig.stringValueReader` or you can add `with net.ceedubs.ficus.readers.StringReader` to your class definition. 106 | 107 | If instead you want to be able to call `as[Option[String]]`, you would need to bring an implicit `ValueReader` for `Option` into scope (via `import net.ceedubs.ficus.FicusConfig.optionValueReader` for example), but then you would also need to bring the `String` value reader into scope as described above, since the `Option` value reader delegates through to the relevant value reader after checking that a config value exists at the given path. 108 | 109 | _Don't worry_. It will be obvious if you forgot to bring the right value reader into scope, because the compiler will give you an error. 110 | 111 | # Enumeration support # 112 | Ficus has the ability to parse config values to Scala's `Enumeration` type. 113 | 114 | If you have the following enum: 115 | ```scala 116 | object Country extends Enumeration { 117 | val DE = Value("DE") 118 | val IT = Value("IT") 119 | val NL = Value("NL") 120 | val US = Value("US") 121 | val GB = Value("GB") 122 | } 123 | ``` 124 | 125 | You can define the config like: 126 | ``` 127 | countries = [DE, US, GB] 128 | ``` 129 | 130 | To get an `Enumeration` type from your config you must import the `EnumerationReader` into your code. Then you can fetch it with the `as` method that Ficus provides on Typesafe `Config` objects. 131 | ```scala 132 | import net.ceedubs.ficus.readers.EnumerationReader._ 133 | val countries: Seq[Country.Value] = config.as[Seq[Country.Value]]("countries") 134 | ``` 135 | 136 | # Arbitrary type support # 137 | 138 | ## Supported types ## 139 | * Traits or classes whose companion object has an appropriate apply method. This includes **case classes** (and even nested case classes). 140 | - The apply method must not take type parameters and its return type must match the trait or class 141 | * Classes that have a primary constructor with no type parameters 142 | 143 | If the apply method or constructor used has default arguments, Ficus will fall back to those for values not in the configuration. 144 | 145 | If it exists, a valid apply method will be used instead of a constructor. 146 | 147 | If Ficus doesn't know how to read an arbitrary type, it will provide a helpful **compile-time** error message explaining why. It won't risk guessing incorrectly. 148 | 149 | Arbitrary type support requires Scala 2.10.2 or higher, because it takes advantage of implicit macros. To enable it, import `net.ceedubs.ficus.readers.ArbitraryTypeReader._`. Note that having the arbitrary type reader in scope can cause some implicit shadowing that you might not expect. If you define `MyClass` and define an `implicit val myClassReader: ValueReader[MyClass]` in the `MyClass` companion object, the arbitrary type reader will still win the implicit prioritization battle unless you specifically `import MyClass.myClassReader`. 150 | 151 | By default the config keys has to match exactly the field name in the class, which by java convention is camel cased. To enable hyphen cased mapping, i.e. hyphen cased config keys, you can import a hyphen cased name mapper into the scope, such as: 152 | 153 | ```scala 154 | import net.ceedubs.ficus.readers.namemappers.implicits.hyphenCase 155 | ``` 156 | 157 | # Custom extraction # 158 | When you call `as[String]("somePath")`, Ficus config knows how to extract a String because there is an implicit `ValueReader[String]` in scope. If you would like, you can even teach it how to extract a `Foo` from the config using `as[Foo]("fooPath")` if you create your own `ValueReader[Foo]`. You could pass this Foo extractor explicitly to the `as` method, but most likely you just want to make it implicit. For an example of a custom value reader, see the `ValueReader[ServiceConfig]` defined in [ExampleSpec](https://github.com/ceedubs/ficus/blob/master/src/test/scala/net/ceedubs/ficus/ExampleSpec.scala). 159 | 160 | # Contributions # 161 | 162 | Many thanks to all of [those who have contributed](https://github.com/iheartradio/ficus/blob/master/CONTRIBUTORS.md) to Ficus. 163 | 164 | Would you like to contribute to Ficus? Pull requests are welcome and encouraged! Please note that contributions will be under the [MIT license](https://github.com/iheartradio/ficus/blob/master/LICENSE). Please provide unit tests along with code contributions. 165 | 166 | 167 | 168 | ## Binary Compatibility 169 | 170 | [MiMa](https://github.com/typesafehub/migration-manager) can be used to check the binary compatibility between two versions of a library.T To check for binary incompatibilities, run `mimaReportBinaryIssues` in the sbt repl. The build is configured to compare the current version against the last released version (It does this naïvely at the moment by merely decrementing bugfix version). If any binary compatibility issues are detected, you may wish to adjust your code to maintain binary compatibility, if that is the goal, or modify the minor version to indicate to consumers that the new version should not be considered binary compatible. 171 | -------------------------------------------------------------------------------- /src/test/scala-2/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 { 9 | def is = s2""" 10 | An arbitrary type reader should 11 | instantiate with a single-param apply method $instantiateSingleParamApply 12 | instantiate with a single-param apply method from config itself $instantiateSingleParamApplyFromSelf 13 | instantiate with no apply method but a single constructor with a single param $instantiateSingleParamConstructor 14 | instantiate with a multi-param apply method $instantiateMultiParamApply 15 | instantiate with no apply method but a single constructor with multiple params $instantiateMultiParamConstructor 16 | instantiate with multiple apply methods if only one returns the correct type $multipleApply 17 | instantiate with primary constructor when no apply methods and multiple constructors $multipleConstructors 18 | use another implicit value reader for a field $withOptionField 19 | fall back to a default value on an apply method $fallBackToApplyMethodDefaultValue 20 | fall back to default values on an apply method if base key isn't in config $fallBackToApplyMethodDefaultValueNoKey 21 | fall back to a default value on a constructor arg $fallBackToConstructorDefaultValue 22 | fall back to a default values on a constructor if base key isn't in config $fallBackToConstructorDefaultValueNoKey 23 | ignore a default value on an apply method if a value is in config $ignoreApplyParamDefault 24 | ignore a default value in a constructor if a value is in config $ignoreConstructorParamDefault 25 | allow overriding of option reader for default values $overrideOptionReaderForDefault 26 | not choose between multiple Java constructors $notChooseBetweenJavaConstructors 27 | not be prioritized over a Reader defined in a type's companion object (when Ficus._ is imported) $notTrumpCompanionReader 28 | use name mapper $useNameMapper 29 | """ 30 | 31 | import ArbitraryTypeReaderSpec._ 32 | 33 | def instantiateSingleParamApply = prop { (foo2: String) => 34 | import Ficus.stringValueReader 35 | import ArbitraryTypeReader._ 36 | val cfg = ConfigFactory.parseString(s"simple { foo2 = ${foo2.asConfigValue} }") 37 | val instance: WithSimpleCompanionApply = 38 | arbitraryTypeValueReader[WithSimpleCompanionApply].value.read(cfg, "simple") 39 | instance.foo must_== foo2 40 | } 41 | 42 | def instantiateSingleParamApplyFromSelf = prop { (foo2: String) => 43 | import Ficus.stringValueReader 44 | import ArbitraryTypeReader._ 45 | val cfg = ConfigFactory.parseString(s"simple { foo2 = ${foo2.asConfigValue} }") 46 | val instance: WithSimpleCompanionApply = 47 | arbitraryTypeValueReader[WithSimpleCompanionApply].value.read(cfg.getConfig("simple"), ".") 48 | instance.foo must_== foo2 49 | } 50 | 51 | def instantiateSingleParamConstructor = prop { (foo: String) => 52 | import Ficus.stringValueReader 53 | import ArbitraryTypeReader._ 54 | val cfg = ConfigFactory.parseString(s"singleParam { foo = ${foo.asConfigValue} }") 55 | val instance: ClassWithSingleParam = arbitraryTypeValueReader[ClassWithSingleParam].value.read(cfg, "singleParam") 56 | instance.getFoo must_== foo 57 | } 58 | 59 | def instantiateMultiParamApply = prop { (foo: String, bar: Int) => 60 | import Ficus.{intValueReader, stringValueReader} 61 | import ArbitraryTypeReader._ 62 | val cfg = ConfigFactory.parseString(s""" 63 | |multi { 64 | | foo = ${foo.asConfigValue} 65 | | bar = $bar 66 | |}""".stripMargin) 67 | val instance: WithMultiCompanionApply = arbitraryTypeValueReader[WithMultiCompanionApply].value.read(cfg, "multi") 68 | (instance.foo must_== foo) and (instance.bar must_== bar) 69 | } 70 | 71 | def instantiateMultiParamConstructor = prop { (foo: String, bar: Int) => 72 | import Ficus.{intValueReader, stringValueReader} 73 | import ArbitraryTypeReader._ 74 | val cfg = ConfigFactory.parseString(s""" 75 | |multi { 76 | | foo = ${foo.asConfigValue} 77 | | bar = $bar 78 | |}""".stripMargin) 79 | val instance: ClassWithMultipleParams = arbitraryTypeValueReader[ClassWithMultipleParams].value.read(cfg, "multi") 80 | (instance.foo must_== foo) and (instance.bar must_== bar) 81 | } 82 | 83 | def multipleApply = prop { (foo: String) => 84 | import Ficus.stringValueReader 85 | import ArbitraryTypeReader._ 86 | val cfg = ConfigFactory.parseString(s"withMultipleApply { foo = ${foo.asConfigValue} }") 87 | val instance: WithMultipleApplyMethods = 88 | arbitraryTypeValueReader[WithMultipleApplyMethods].value.read(cfg, "withMultipleApply") 89 | instance.foo must_== foo 90 | } 91 | 92 | def multipleConstructors = prop { (foo: String) => 93 | import Ficus.stringValueReader 94 | import ArbitraryTypeReader._ 95 | val cfg = ConfigFactory.parseString(s"withMultipleConstructors { foo = ${foo.asConfigValue} }") 96 | val instance: ClassWithMultipleConstructors = 97 | arbitraryTypeValueReader[ClassWithMultipleConstructors].value.read(cfg, "withMultipleConstructors") 98 | instance.foo must_== foo 99 | } 100 | 101 | def fallBackToApplyMethodDefaultValue = { 102 | import Ficus.{optionValueReader, stringValueReader} 103 | import ArbitraryTypeReader._ 104 | val cfg = ConfigFactory.parseString("withDefault { }") 105 | arbitraryTypeValueReader[WithDefault].value.read(cfg, "withDefault").foo must_== "defaultFoo" 106 | } 107 | 108 | def fallBackToApplyMethodDefaultValueNoKey = { 109 | import Ficus.{optionValueReader, stringValueReader} 110 | import ArbitraryTypeReader._ 111 | val cfg = ConfigFactory.parseString("") 112 | arbitraryTypeValueReader[WithDefault].value.read(cfg, "withDefault").foo must_== "defaultFoo" 113 | } 114 | 115 | def fallBackToConstructorDefaultValue = { 116 | import Ficus.{optionValueReader, stringValueReader} 117 | import ArbitraryTypeReader._ 118 | val cfg = ConfigFactory.parseString("withDefault { }") 119 | arbitraryTypeValueReader[ClassWithDefault].value.read(cfg, "withDefault").foo must_== "defaultFoo" 120 | } 121 | 122 | def fallBackToConstructorDefaultValueNoKey = { 123 | import Ficus.{optionValueReader, stringValueReader} 124 | import ArbitraryTypeReader._ 125 | val cfg = ConfigFactory.parseString("") 126 | arbitraryTypeValueReader[ClassWithDefault].value.read(cfg, "withDefault").foo must_== "defaultFoo" 127 | } 128 | 129 | def withOptionField = { 130 | import Ficus.{optionValueReader, stringValueReader} 131 | import ArbitraryTypeReader._ 132 | val cfg = ConfigFactory.parseString("""withOption { option = "here" }""") 133 | arbitraryTypeValueReader[WithOption].value.read(cfg, "withOption").option must_== Some("here") 134 | } 135 | 136 | def ignoreApplyParamDefault = prop { (foo: String) => 137 | import Ficus.{optionValueReader, stringValueReader} 138 | import ArbitraryTypeReader._ 139 | val cfg = ConfigFactory.parseString(s"withDefault { foo = ${foo.asConfigValue} }") 140 | arbitraryTypeValueReader[WithDefault].value.read(cfg, "withDefault").foo must_== foo 141 | } 142 | 143 | def ignoreConstructorParamDefault = prop { (foo: String) => 144 | import Ficus.{optionValueReader, stringValueReader} 145 | import ArbitraryTypeReader._ 146 | val cfg = ConfigFactory.parseString(s"withDefault { foo = ${foo.asConfigValue} }") 147 | arbitraryTypeValueReader[ClassWithDefault].value.read(cfg, "withDefault").foo must_== foo 148 | } 149 | 150 | def overrideOptionReaderForDefault = { 151 | import ArbitraryTypeReader._ 152 | implicit val stringOptionReader: ValueReader[Option[String]] = Ficus.stringValueReader map { s => 153 | if (s.isEmpty) None else Some(s) 154 | } 155 | val cfg = ConfigFactory.parseString("""withDefault { foo = "" }""") 156 | arbitraryTypeValueReader[ClassWithDefault].value.read(cfg, "withDefault").foo must beEqualTo("defaultFoo") 157 | } 158 | 159 | def notChooseBetweenJavaConstructors = { 160 | illTyped("implicitly[ValueReader[String]]") 161 | illTyped("implicitly[ValueReader[Long]]") 162 | illTyped("implicitly[ValueReader[Int]]") 163 | illTyped("implicitly[ValueReader[Float]]") 164 | illTyped("implicitly[ValueReader[Double]]") 165 | illTyped("implicitly[ValueReader[Char]]") 166 | success // failure would result in compile error 167 | } 168 | 169 | def notTrumpCompanionReader = { 170 | import Ficus._ 171 | val cfg = ConfigFactory.parseString("""withReaderInCompanion { foo = "bar" }""") 172 | WithReaderInCompanion("from-companion") ==== cfg.as[WithReaderInCompanion]("withReaderInCompanion") 173 | } 174 | 175 | def useNameMapper = prop { (foo: String) => 176 | import Ficus.stringValueReader 177 | import ArbitraryTypeReader._ 178 | implicit val nameMapper: NameMapper = new NameMapper { 179 | override def map(name: String): String = name.toUpperCase 180 | } 181 | 182 | val cfg = ConfigFactory.parseString(s"singleParam { FOO = ${foo.asConfigValue} }") 183 | val instance: ClassWithSingleParam = arbitraryTypeValueReader[ClassWithSingleParam].value.read(cfg, "singleParam") 184 | instance.getFoo must_== foo 185 | } 186 | } 187 | 188 | object ArbitraryTypeReaderSpec { 189 | trait WithSimpleCompanionApply { 190 | def foo: String 191 | } 192 | 193 | object WithSimpleCompanionApply { 194 | def apply(foo2: String): WithSimpleCompanionApply = new WithSimpleCompanionApply { 195 | val foo = foo2 196 | } 197 | } 198 | 199 | trait WithMultiCompanionApply { 200 | def foo: String 201 | def bar: Int 202 | } 203 | 204 | object WithMultiCompanionApply { 205 | def apply(foo: String, bar: Int): WithMultiCompanionApply = { 206 | val (_foo, _bar) = (foo, bar) 207 | new WithMultiCompanionApply { 208 | val foo = _foo 209 | val bar = _bar 210 | } 211 | } 212 | } 213 | 214 | trait WithDefault { 215 | def foo: String 216 | } 217 | 218 | object WithDefault { 219 | def apply(foo: String = "defaultFoo"): WithDefault = { 220 | val _foo = foo 221 | new WithDefault { 222 | val foo = _foo 223 | } 224 | } 225 | } 226 | 227 | trait WithOption { 228 | def option: Option[String] 229 | } 230 | 231 | object WithOption { 232 | def apply(option: Option[String]): WithOption = { 233 | val _option = option 234 | new WithOption { 235 | val option = _option 236 | } 237 | } 238 | } 239 | 240 | class ClassWithSingleParam(foo: String) { 241 | def getFoo = foo 242 | } 243 | 244 | class ClassWithMultipleParams(val foo: String, val bar: Int) 245 | 246 | class ClassWithDefault(val foo: String = "defaultFoo") 247 | 248 | trait WithMultipleApplyMethods { 249 | def foo: String 250 | } 251 | 252 | object WithMultipleApplyMethods { 253 | 254 | def apply(foo: Option[String]): Option[WithMultipleApplyMethods] = foo map { f => 255 | new WithMultipleApplyMethods { 256 | def foo: String = f 257 | } 258 | } 259 | 260 | def apply(foo: String): WithMultipleApplyMethods = { 261 | val _foo = foo 262 | new WithMultipleApplyMethods { 263 | def foo: String = _foo 264 | } 265 | } 266 | } 267 | 268 | class ClassWithMultipleConstructors(val foo: String) { 269 | def this(fooInt: Int) = this(fooInt.toString) 270 | } 271 | 272 | case class WithReaderInCompanion(foo: String) 273 | 274 | object WithReaderInCompanion { 275 | implicit val reader: ValueReader[WithReaderInCompanion] = 276 | ValueReader.relative(_ => WithReaderInCompanion("from-companion")) 277 | } 278 | 279 | } 280 | --------------------------------------------------------------------------------