diff --git a/.travis.yml b/.travis.yml index ff00ce4..dd3e38e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,22 @@ language: scala +sudo: required +dist: trusty + scala: - - 2.11.0 + - 2.10.6 + - 2.11.8 + - 2.12.0 jdk: - - openjdk7 - - oraclejdk7 + - oraclejdk8 + - openjdk8 +cache: + directories: + - $HOME/.ivy2/cache + - $HOME/.sbt/boot/ install: /bin/true -script: ./sbt -J-Xss1M -J-Xms512M -J-Xmx512M -J-XX:MaxPermSize=128M ";clean;compile;test" + +script: + - sbt ++$TRAVIS_SCALA_VERSION mimaReportBinaryIssues + - sbt ++$TRAVIS_SCALA_VERSION -jvm-opts travis-jvmopts clean coverage test coverageReport + - sbt ++$TRAVIS_SCALA_VERSION coveralls diff --git a/README.md b/README.md index 694f083..fc4e4d8 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,27 @@ -# Warning: abandoned project # -I have been neglecting Ficus for several reasons: -* At work we use [knobs](http://oncue.github.io/knobs/) -* I am a bit oversubscribed on open source projects -* Ficus scratched an itch when I first wrote it, but if I were to write it again right now, I would approach it differently. - * I would represent errors explicitly in the types (currently it throws exceptions, which can be handy, but I would want an alternative). - * I would use [Shapeless](https://github.com/milessabin/shapeless) to derive readers instead of a macro. At the time, the macro was necessary to use default values on classes, but Shapeless now provides full support for this. - * I think I would end up writing a `shapeless-knobs` micro library that simply provides Shapeless-derived [Configured](https://github.com/oncue/knobs/blob/master/core/src/main/scala/knobs/Configured.scala) instances. - -Having said that, I know there are a number of people that are happily using Ficus. I'm happy to hand this project off to a good owner. The only difficulty might be in publishing credentials. Since its currently under `net.ceedubs`, the simplest option may be to have a fork and start publishing in a different namespace. I'm happy to update the repo to redirect to the new official project. Please email me at my GitHub handle at gmail if you'd like to volunteer to take over this project. +Official repo for ficus. Adopted from [ceedubs](https://github.com/ceedubs/ficus) # Ficus # Ficus is a lightweight companion to Typesafe config that makes it more Scala-friendly. Ficus adds an `as[A]` method to a normal [Typesafe Config](http://typesafehub.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. -[![Build Status](https://secure.travis-ci.org/ceedubs/ficus.png?branch=master)](http://travis-ci.org/ceedubs/ficus) +[![Build Status](https://secure.travis-ci.org/iheartradio/ficus.png?branch=master)](http://travis-ci.org/iheartradio/ficus) +[![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) +[![Coverage Status](https://coveralls.io/repos/github/iheartradio/ficus/badge.svg?branch=master)](https://coveralls.io/github/iheartradio/ficus?branch=master) +[![Latest version](https://index.scala-lang.org/iheartradio/ficus/ficus/latest.svg?color=orange)](https://index.scala-lang.org/iheartradio/ficus) # Examples # ```scala import net.ceedubs.ficus.Ficus._ +object Country extends Enumeration { + val DE = Value("DE") + val IT = Value("IT") + val NL = Value("NL") + val US = Value("US") + val GB = Value("GB") +} + case class SomeCaseClass(foo: String, bar: Int, baz: Option[FiniteDuration]) class Examples { @@ -37,6 +39,12 @@ class Examples { // something such as "15 minutes" can be converted to a FiniteDuration val retryInterval: FiniteDuration = config.as[FiniteDuration]("retryInterval") + // can extract arbitrary Enumeration types + // Note: it throws an exception at runtime, if the enumeration type cannot be instantiated or + // if a config value cannot be mapped to the enumeration value + import net.ceedubs.ficus.readers.EnumerationReader._ + val someEnumerationType: Seq[Country.Value] = config.as[Seq[Country.Value]]("countries") + // can hydrate most arbitrary types // it first tries to use an apply method on the companion object and falls back to the primary constructor // if values are not in the config, they will fall back to the default value on the class/apply method @@ -48,24 +56,26 @@ class Examples { 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). # Adding the dependency # -You most likely already have the Sonatype OSS Releases repository defined in your build, but if you don't, add this to your SBT build file (most likely build.sbt or project/build.scala): -```scala -resolvers ++= Seq( - "Sonatype OSS Releases" at "http://oss.sonatype.org/content/repositories/releases/", -) -``` + Now add the Ficus dependency to your build SBT file as well: ```scala // for Scala 2.10.x -libraryDependencies += "net.ceedubs" %% "ficus" % "1.0.1" +libraryDependencies += "com.iheart" %% "ficus" % "1.0.2" + +// for Scala 2.11.x and Java 7 +libraryDependencies += "com.iheart" %% "ficus" % "1.1.3" -// for Scala 2.11.x -libraryDependencies += "net.ceedubs" %% "ficus" % "1.1.2" +// for Scala 2.11.x, 2.12.x and Java 8 +// See the latest version in the download badge below. +libraryDependencies += "com.iheart" %% "ficus" % //see latest version in the badge below ``` + +[![Latest version](https://index.scala-lang.org/iheartradio/ficus/ficus/latest.svg?color=orange)](https://index.scala-lang.org/iheartradio/ficus) + 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. -Release notes are available on the [Ficus wiki](https://github.com/ceedubs/ficus/wiki). + # Built-in readers # Out of the box, Ficus can read most types from config: @@ -75,6 +85,7 @@ Out of the box, Ficus can read most types from config: * Collections (`List[A]`, `Set[A]`, `Map[String, A]`, `Array[A]`, etc. All types with a CanBuildFrom instance are supported) * `Config` and `ConfigValue` (Typesafe config/value) * `FiniteDuration` +* The Scala `Enumeration` type. See [Enumeration support](#enumeration-support) * Most arbitrary classes (as well as traits that have an apply method for instantiation). See [Arbitrary type support](#arbitrary-type-support) 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 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). @@ -82,18 +93,45 @@ In this context, `A` means any type for which a `ValueReader` is already defined # Imports # 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. +To enable Ficus's reading of `Enumeration` types, you can also import `net.ceedubs.ficus.readers.EnumerationReader._`. See [Enumeration support](#enumeration-support) + 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) 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. 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. -You will then need a [ValueReader](https://github.com/ceedubs/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. +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. 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. _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. +# Enumeration support # +Ficus has the ability to parse config values to Scala's `Enumeration` type. + +If you have the following enum: +```scala +object Country extends Enumeration { + val DE = Value("DE") + val IT = Value("IT") + val NL = Value("NL") + val US = Value("US") + val GB = Value("GB") +} +``` + +You can define the config like: +``` +countries = [DE, US, GB] +``` + +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. +```scala +import net.ceedubs.ficus.readers.EnumerationReader._ +val countries: Seq[Country.Value] = config.as[Seq[Country.Value]]("countries") +``` + # Arbitrary type support # ## Supported types ## @@ -107,13 +145,25 @@ If it exists, a valid apply method will be used instead of a constructor. 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. -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 arbitray type reader will still win the implicit prioritization battle unless you specifically `import MyClass.myClassReader`. +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`. + +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: + +```scala +import net.ceedubs.ficus.readers.namemappers.implicits.hyphenCase +``` # Custom extraction # 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). # Contributions # -Many thanks to all of [those who have contributed](https://github.com/ceedubs/ficus/blob/master/CONTRIBUTORS.md) to Ficus. +Many thanks to all of [those who have contributed](https://github.com/iheartradio/ficus/blob/master/CONTRIBUTORS.md) to Ficus. + +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. + + + +## Binary Compatibility -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/ceedubs/ficus/blob/master/LICENSE). Please provide unit tests along with code contributions. +[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. diff --git a/build.sbt b/build.sbt index 2130bf8..6cf5696 100644 --- a/build.sbt +++ b/build.sbt @@ -1,73 +1,53 @@ +import sbtrelease.Version + /* basic project info */ name := "ficus" -organization := "net.ceedubs" - description := "A Scala-friendly wrapper companion for Typesafe config" -homepage := Some(url("https://github.com/ceedubs/ficus")) - startYear := Some(2013) -licenses := Seq( - "MIT License" -> url("http://www.opensource.org/licenses/mit-license.html") -) - -scmInfo := Some( - ScmInfo( - url("https://github.com/ceedubs/ficus"), - "scm:git:https://github.com/ceedubs/ficus.git", - Some("scm:git:git@github.com:ceedubs/ficus.git") - ) -) - /* scala versions and options */ -scalaVersion := "2.11.0" +scalaVersion := "2.12.0" + +crossScalaVersions := Seq(scalaVersion.value, "2.10.6", "2.11.8") // These options will be used for *all* versions. scalacOptions ++= Seq( + "-feature", "-deprecation", "-unchecked", - "-encoding", "UTF-8" -) + "-encoding", "UTF-8", + "-target:jvm-1." + { + CrossVersion.partialVersion(scalaVersion.value).collect { + case (2, minor) if minor <= 10 => "7" + }.getOrElse("8") + } +) ++ (if (scalaVersion.value.startsWith("2.11") || scalaVersion.value.startsWith("2.10")) Seq("-Yclosure-elim", "-Yinline") else Seq.empty[String]) -scalacOptions ++= Seq( - "-Yclosure-elim", - "-Yinline" +javacOptions ++= Seq( + "-Xlint:unchecked", "-Xlint:deprecation" ) -// These language flags will be used only for 2.10.x. -// Uncomment those you need, or if you hate SIP-18, all of them. -scalacOptions <++= scalaVersion map { sv => - if (sv startsWith "2.10") List( - "-Xverify", - "-Ywarn-all", - "-feature", - "-language:postfixOps", - "-language:implicitConversions", - "-language:higherKinds" - ) - else Nil -} - -javacOptions ++= Seq("-Xlint:unchecked", "-Xlint:deprecation") - /* dependencies */ -libraryDependencies <++= scalaVersion { sv => - Seq( - "org.specs2" %% "specs2" % "2.3.11" % "test", - "org.scalacheck" %% "scalacheck" % "1.11.3" % "test", - "com.chuusai" %% "shapeless" % "2.0.0" % "test", - "com.typesafe" % "config" % "1.2.1", - "org.scala-lang" % "scala-reflect" % sv % "provided") -} +libraryDependencies ++= Seq( + "org.specs2" %% "specs2-core" % "3.8.6" % "test", + "org.specs2" %% "specs2-scalacheck" % "3.8.6" % "test", + "org.scalacheck" %% "scalacheck" % "1.13.4" % "test", + "com.chuusai" %% "shapeless" % "2.3.2" % "test", + "com.typesafe" % "config" % "1.3.1", + "org.scala-lang" % "scala-reflect" % scalaVersion.value % "provided", + "org.scala-lang" % "scala-compiler" % scalaVersion.value % "provided", + "org.typelevel" %% "macro-compat" % "1.1.1", + compilerPlugin("org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full) +) -/* you may need these repos */ resolvers ++= Seq( - Resolver.sonatypeRepo("snapshots") + Resolver.sonatypeRepo("snapshots"), + Resolver.bintrayRepo("iheartradio","maven"), + Resolver.jcenterRepo ) -/* testing */ parallelExecution in Test := true /* sbt behavior */ @@ -77,33 +57,22 @@ traceLevel := 5 offline := false -/* publishing */ -publishMavenStyle := true - -publishTo <<= version { (v: String) => - val nexus = "https://oss.sonatype.org/" - if (v.trim.endsWith("SNAPSHOT")) Some( - "snapshots" at nexus + "content/repositories/snapshots" - ) - else Some("releases" at nexus + "service/local/staging/deploy/maven2") -} - -mappings in (Compile, packageBin) ~= { (ms: Seq[(File, String)]) => - ms filter { case (file, toPath) => - toPath != "application.conf" +mappings in (Compile, packageBin) := { + val ms = mappings.in(Compile, packageBin).value + ms filter { case (_, toPath) => + toPath != "application.conf" } } -publishArtifact in Test := false +Publish.settings -pomIncludeRepository := { _ => false } +releaseCrossBuild := true + +mimaPreviousArtifacts := (if (scalaBinaryVersion.value != "2.10") { + Version(version.value).map { + case Version(major, subversions, _) => + val (minor :: bugfix :: _) = subversions.toList + Set(organization.value %% name.value % Seq(major, minor, bugfix - 1).mkString(".")) + }.getOrElse(Set.empty) +} else Set.empty) -pomExtra := ( - - - ceedubs - Cody Allen - ceedubs@gmail.com - - -) diff --git a/project/Publish.scala b/project/Publish.scala new file mode 100644 index 0000000..212eaba --- /dev/null +++ b/project/Publish.scala @@ -0,0 +1,73 @@ +import com.typesafe.sbt.pgp.PgpKeys +import sbt._, Keys._ +import sbtrelease.ReleasePlugin.autoImport._ +import sbtrelease.ReleaseStateTransformations._ + + +object Publish { + + pomExtra in Global := { + + + + ceedubs + Cody Allen + ceedubs@gmail.com + + + kailuowang + Kailuo Wang + kailuo.wang@gmail.com + + + } + + + val publishingSettings = Seq( + + organization in ThisBuild := "com.iheart", + publishMavenStyle := true, + licenses := Seq("MIT" -> url("http://www.opensource.org/licenses/mit-license.html")), + homepage := Some(url("http://iheartradio.github.io/ficus")), + scmInfo := Some(ScmInfo( + url("https://github.com/iheartradio/ficus"), + "git@github.com:iheartradio/ficus.git", + Some("git@github.com:iheartradio/ficus.git"))), + pomIncludeRepository := { _ => false }, + publishArtifact in Test := false, + pomExtra := ( + + + ceedubs + Cody Allen + ceedubs@gmail.com + + + kailuowang + Kailuo Wang + kailuo.wang@gmail.com + + + ), + releaseCrossBuild := true, + releasePublishArtifactsAction := PgpKeys.publishSigned.value, + + releaseProcess := Seq[ReleaseStep]( + checkSnapshotDependencies, + inquireVersions, + runClean, + runTest, + setReleaseVersion, + commitReleaseVersion, + tagRelease, + publishArtifacts, + setNextVersion, + commitNextVersion, + ReleaseStep(action = Command.process("sonatypeReleaseAll", _)), + pushChanges + ) + + ) + + val settings = publishingSettings +} diff --git a/project/build.properties b/project/build.properties index 748703f..27e88aa 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=0.13.7 +sbt.version=0.13.13 diff --git a/project/build.scala b/project/build.scala index 34bd454..1cfbbab 100644 --- a/project/build.scala +++ b/project/build.scala @@ -3,8 +3,6 @@ import Keys._ object build extends Build { - val gcsettings = Defaults.defaultSettings - val gc = TaskKey[Unit]("gc", "runs garbage collector") val gcTask = gc := { println("requesting garbage collection") @@ -13,7 +11,8 @@ object build extends Build { lazy val project = Project ( "project", - file("."), - settings = gcsettings ++ Seq(gcTask) + file(".") + ).settings( + gcTask ) } diff --git a/project/plugins.sbt b/project/plugins.sbt index 4ce4d9e..2a771ee 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1 +1,13 @@ +resolvers += "Typesafe Repository" at "https://repo.typesafe.com/typesafe/releases/" + +addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.5.0") + +addSbtPlugin("org.scoverage" % "sbt-coveralls" % "1.1.0") + +addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.3") + +addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "0.1.9") + +addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "1.1") + addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.0.0") diff --git a/sbt b/sbt deleted file mode 100755 index 040ce80..0000000 --- a/sbt +++ /dev/null @@ -1,474 +0,0 @@ -#!/usr/bin/env bash -# -# A more capable sbt runner, coincidentally also called sbt. -# Author: Paul Phillips - -# todo - make this dynamic -declare -r sbt_release_version=0.13.0 - -declare sbt_jar sbt_dir sbt_create sbt_launch_dir -declare scala_version java_home sbt_explicit_version -declare verbose debug quiet noshare batch trace_level log_level -declare sbt_saved_stty - -echoerr () { [[ -z $quiet ]] && echo "$@" >&2; } -vlog () { [[ -n "$verbose$debug" ]] && echoerr "$@"; } -dlog () { [[ -n $debug ]] && echoerr "$@"; } - -# we'd like these set before we get around to properly processing arguments -for arg in "$@"; do - case $arg in - -q|-quiet) quiet=true ;; - -d|-debug) debug=true ;; - -v|-verbose) verbose=true ;; - *) ;; - esac -done - -build_props_sbt () { - if [[ -r project/build.properties ]]; then - versionLine=$(grep ^sbt.version project/build.properties | tr -d ' \r') - versionString=${versionLine##sbt.version=} - echo "$versionString" - fi -} - -update_build_props_sbt () { - local ver="$1" - local old=$(build_props_sbt) - - if [[ $ver == $old ]]; then - return - elif [[ -r project/build.properties ]]; then - perl -pi -e "s/^sbt\.version[ ]*=.*\$/sbt.version=${ver}/" project/build.properties - grep -q '^sbt.version[ ]*=' project/build.properties || printf "\nsbt.version=${ver}\n" >> project/build.properties - - echoerr !!! - echoerr !!! Updated file project/build.properties setting sbt.version to: $ver - echoerr !!! Previous value was: $old - echoerr !!! - fi -} - -sbt_version () { - if [[ -n $sbt_explicit_version ]]; then - echo $sbt_explicit_version - else - local v=$(build_props_sbt) - if [[ -n $v ]]; then - echo $v - else - echo $sbt_release_version - fi - fi -} - -# restore stty settings (echo in particular) -onSbtRunnerExit() { - [[ -n $sbt_saved_stty ]] || return - dlog "" - dlog "restoring stty: $sbt_saved_stty" - stty $sbt_saved_stty - unset sbt_saved_stty -} - -# save stty and trap exit, to ensure echo is reenabled if we are interrupted. -trap onSbtRunnerExit EXIT -sbt_saved_stty=$(stty -g 2>/dev/null) -dlog "Saved stty: $sbt_saved_stty" - -# this seems to cover the bases on OSX, and someone will -# have to tell me about the others. -get_script_path () { - local path="$1" - [[ -L "$path" ]] || { echo "$path" ; return; } - - local target=$(readlink "$path") - if [[ "${target:0:1}" == "/" ]]; then - echo "$target" - else - echo "$(dirname $path)/$target" - fi -} - -die() { - echo "Aborting: $@" - exit 1 -} - -make_url () { - version="$1" - - echo "$sbt_launch_repo/org.scala-sbt/sbt-launch/$version/sbt-launch.jar" -} - -readarr () { - while read ; do - eval "$1+=(\"$REPLY\")" - done -} - -init_default_option_file () { - local overriding_var=${!1} - local default_file=$2 - if [[ ! -r "$default_file" && $overriding_var =~ ^@(.*)$ ]]; then - local envvar_file=${BASH_REMATCH[1]} - if [[ -r $envvar_file ]]; then - default_file=$envvar_file - fi - fi - echo $default_file -} - -declare -r cms_opts="-XX:+CMSClassUnloadingEnabled -XX:+UseConcMarkSweepGC" -declare -r jit_opts="-XX:ReservedCodeCacheSize=256m -XX:+TieredCompilation" -declare -r default_jvm_opts="-Dfile.encoding=UTF8 -XX:MaxPermSize=384m -Xms512m -Xmx1536m -Xss2m $jit_opts $cms_opts" -declare -r noshare_opts="-Dsbt.global.base=project/.sbtboot -Dsbt.boot.directory=project/.boot -Dsbt.ivy.home=project/.ivy" -declare -r latest_28="2.8.2" -declare -r latest_29="2.9.3" -declare -r latest_210="2.10.3" -declare -r latest_211="2.11.0-M5" - -declare -r script_path=$(get_script_path "$BASH_SOURCE") -declare -r script_dir="$(dirname $script_path)" -declare -r script_name="$(basename $script_path)" - -# some non-read-onlies set with defaults -declare java_cmd=java -declare sbt_opts_file=$(init_default_option_file SBT_OPTS .sbtopts) -declare jvm_opts_file=$(init_default_option_file JVM_OPTS .jvmopts) -declare sbt_launch_repo="http://typesafe.artifactoryonline.com/typesafe/ivy-releases" - -# pull -J and -D options to give to java. -declare -a residual_args -declare -a java_args -declare -a scalac_args -declare -a sbt_commands - -# args to jvm/sbt via files or environment variables -declare -a extra_jvm_opts extra_sbt_opts - -# if set, use JAVA_HOME over java found in path -[[ -e "$JAVA_HOME/bin/java" ]] && java_cmd="$JAVA_HOME/bin/java" - -# directory to store sbt launchers -declare sbt_launch_dir="$HOME/.sbt/launchers" -[[ -d "$sbt_launch_dir" ]] || mkdir -p "$sbt_launch_dir" -[[ -w "$sbt_launch_dir" ]] || sbt_launch_dir="$(mktemp -d -t sbt_extras_launchers)" - -build_props_scala () { - if [[ -r project/build.properties ]]; then - versionLine=$(grep ^build.scala.versions project/build.properties) - versionString=${versionLine##build.scala.versions=} - echo ${versionString%% .*} - fi -} - -execRunner () { - # print the arguments one to a line, quoting any containing spaces - [[ $verbose || $debug ]] && echo "# Executing command line:" && { - for arg; do - if [[ -n "$arg" ]]; then - if printf "%s\n" "$arg" | grep -q ' '; then - printf "\"%s\"\n" "$arg" - else - printf "%s\n" "$arg" - fi - fi - done - echo "" - } - - if [[ -n $batch ]]; then - exec /dev/null; then - curl --fail --silent "$url" --output "$jar" - elif which wget >/dev/null; then - wget --quiet -O "$jar" "$url" - fi - } && [[ -r "$jar" ]] -} - -acquire_sbt_jar () { - for_sbt_version="$(sbt_version)" - sbt_url="$(jar_url $for_sbt_version)" - sbt_jar="$(jar_file $for_sbt_version)" - - [[ -r "$sbt_jar" ]] || download_url "$sbt_url" "$sbt_jar" -} - -usage () { - cat < display stack traces with a max of frames (default: -1, traces suppressed) - -no-colors disable ANSI color codes - -sbt-create start sbt even if current directory contains no sbt project - -sbt-dir path to global settings/plugins directory (default: ~/.sbt/) - -sbt-boot path to shared boot directory (default: ~/.sbt/boot in 0.11+) - -ivy path to local Ivy repository (default: ~/.ivy2) - -no-share use all local caches; no sharing - -offline put sbt in offline mode - -jvm-debug Turn on JVM debugging, open at the given port. - -batch Disable interactive mode - -prompt Set the sbt prompt; in expr, 's' is the State and 'e' is Extracted - - # sbt version (default: from project/build.properties if present, else latest release) - !!! The only way to accomplish this pre-0.12.0 if there is a build.properties file which - !!! contains an sbt.version property is to update the file on disk. That's what this does. - -sbt-version use the specified version of sbt (default: $sbt_release_version) - -sbt-jar use the specified jar as the sbt launcher - -sbt-launch-dir directory to hold sbt launchers (default: $sbt_launch_dir) - -sbt-launch-repo repo url for downloading sbt launcher jar (default: $sbt_launch_repo) - - # scala version (default: as chosen by sbt) - -28 use $latest_28 - -29 use $latest_29 - -210 use $latest_210 - -211 use $latest_211 - -scala-home use the scala build at the specified directory - -scala-version use the specified version of scala - -binary-version use the specified scala version when searching for dependencies - - # java version (default: java from PATH, currently $(java -version 2>&1 | grep version)) - -java-home alternate JAVA_HOME - - # passing options to the jvm - note it does NOT use JAVA_OPTS due to pollution - # The default set is used if JVM_OPTS is unset and no -jvm-opts file is found - $default_jvm_opts - JVM_OPTS environment variable holding either the jvm args directly, or - the reference to a file containing jvm args if given path is prepended by '@' (e.g. '@/etc/jvmopts') - Note: "@"-file is overridden by local '.jvmopts' or '-jvm-opts' argument. - -jvm-opts file containing jvm args (if not given, .jvmopts in project root is used if present) - -Dkey=val pass -Dkey=val directly to the jvm - -J-X pass option -X directly to the jvm (-J is stripped) - - # passing options to sbt, OR to this runner - SBT_OPTS environment variable holding either the sbt args directly, or - the reference to a file containing sbt args if given path is prepended by '@' (e.g. '@/etc/sbtopts') - Note: "@"-file is overridden by local '.sbtopts' or '-sbt-opts' argument. - -sbt-opts file containing sbt args (if not given, .sbtopts in project root is used if present) - -S-X add -X to sbt's scalacOptions (-S is stripped) -EOM -} - -addJava () { - dlog "[addJava] arg = '$1'" - java_args=( "${java_args[@]}" "$1" ) -} -addSbt () { - dlog "[addSbt] arg = '$1'" - sbt_commands=( "${sbt_commands[@]}" "$1" ) -} -addScalac () { - dlog "[addScalac] arg = '$1'" - scalac_args=( "${scalac_args[@]}" "$1" ) -} -addResidual () { - dlog "[residual] arg = '$1'" - residual_args=( "${residual_args[@]}" "$1" ) -} -addResolver () { - addSbt "set resolvers += $1" -} -addDebugger () { - addJava "-Xdebug" - addJava "-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=$1" -} -setScalaVersion () { - [[ "$1" == *-SNAPSHOT ]] && addResolver 'Resolver.sonatypeRepo("snapshots")' - addSbt "++ $1" -} - -process_args () -{ - require_arg () { - local type="$1" - local opt="$2" - local arg="$3" - - if [[ -z "$arg" ]] || [[ "${arg:0:1}" == "-" ]]; then - die "$opt requires <$type> argument" - fi - } - while [[ $# -gt 0 ]]; do - case "$1" in - -h|-help) usage; exit 1 ;; - -v|-verbose) verbose=true && log_level=Info && shift ;; - -d|-debug) debug=true && log_level=Debug && shift ;; - -q|-quiet) quiet=true && log_level=Error && shift ;; - - -trace) require_arg integer "$1" "$2" && trace_level=$2 && shift 2 ;; - -ivy) require_arg path "$1" "$2" && addJava "-Dsbt.ivy.home=$2" && shift 2 ;; - -no-colors) addJava "-Dsbt.log.noformat=true" && shift ;; - -no-share) noshare=true && shift ;; - -sbt-boot) require_arg path "$1" "$2" && addJava "-Dsbt.boot.directory=$2" && shift 2 ;; - -sbt-dir) require_arg path "$1" "$2" && sbt_dir="$2" && shift 2 ;; - -debug-inc) addJava "-Dxsbt.inc.debug=true" && shift ;; - -offline) addSbt "set offline := true" && shift ;; - -jvm-debug) require_arg port "$1" "$2" && addDebugger $2 && shift 2 ;; - -batch) batch=true && shift ;; - -prompt) require_arg "expr" "$1" "$2" && addSbt "set shellPrompt in ThisBuild := (s => { val e = Project.extract(s) ; $2 })" && shift 2 ;; - - -sbt-create) sbt_create=true && shift ;; - -sbt-jar) require_arg path "$1" "$2" && sbt_jar="$2" && shift 2 ;; - -sbt-version) require_arg version "$1" "$2" && sbt_explicit_version="$2" && shift 2 ;; --sbt-launch-dir) require_arg path "$1" "$2" && sbt_launch_dir="$2" && shift 2 ;; --sbt-launch-repo) require_arg path "$1" "$2" && sbt_launch_repo="$2" && shift 2 ;; - -scala-version) require_arg version "$1" "$2" && setScalaVersion "$2" && shift 2 ;; --binary-version) require_arg version "$1" "$2" && addSbt "set scalaBinaryVersion in ThisBuild := \"$2\"" && shift 2 ;; - -scala-home) require_arg path "$1" "$2" && addSbt "set every scalaHome := Some(file(\"$2\"))" && shift 2 ;; - -java-home) require_arg path "$1" "$2" && java_cmd="$2/bin/java" && shift 2 ;; - -sbt-opts) require_arg path "$1" "$2" && sbt_opts_file="$2" && shift 2 ;; - -jvm-opts) require_arg path "$1" "$2" && jvm_opts_file="$2" && shift 2 ;; - - -D*) addJava "$1" && shift ;; - -J*) addJava "${1:2}" && shift ;; - -S*) addScalac "${1:2}" && shift ;; - -28) setScalaVersion $latest_28 && shift ;; - -29) setScalaVersion $latest_29 && shift ;; - -210) setScalaVersion $latest_210 && shift ;; - -211) setScalaVersion $latest_211 && shift ;; - - *) addResidual "$1" && shift ;; - esac - done -} - -# process the direct command line arguments -process_args "$@" - -# skip #-styled comments -readConfigFile() { - while read line; do echo ${line/\#*/} | grep -vE '^\s*$'; done < $1 -} - -# if there are file/environment sbt_opts, process again so we -# can supply args to this runner -if [[ -r "$sbt_opts_file" ]]; then - vlog "Using sbt options defined in file $sbt_opts_file" - readarr extra_sbt_opts < <(readConfigFile "$sbt_opts_file") -elif [[ -n "$SBT_OPTS" && !($SBT_OPTS =~ ^@.*) ]]; then - vlog "Using sbt options defined in variable \$SBT_OPTS" - extra_sbt_opts=( $SBT_OPTS ) -else - vlog "No extra sbt options have been defined" -fi - -[[ -n $extra_sbt_opts ]] && process_args "${extra_sbt_opts[@]}" - -# reset "$@" to the residual args -set -- "${residual_args[@]}" -argumentCount=$# - -# only exists in 0.12+ -setTraceLevel() { - case $(sbt_version) in - 0.{7,10,11}.*) echoerr "Cannot set trace level in sbt version $(sbt_version)" ;; - *) addSbt "set every traceLevel := $trace_level" ;; - esac -} - -# set scalacOptions if we were given any -S opts -[[ ${#scalac_args[@]} -eq 0 ]] || addSbt "set scalacOptions in ThisBuild += \"${scalac_args[@]}\"" - -# Update build.properties on disk to set explicit version - sbt gives us no choice -[[ -n "$sbt_explicit_version" ]] && update_build_props_sbt "$sbt_explicit_version" -vlog "Detected sbt version $(sbt_version)" - -[[ -n "$scala_version" ]] && echoerr "Overriding scala version to $scala_version" - -# no args - alert them there's stuff in here -(( $argumentCount > 0 )) || { - vlog "Starting $script_name: invoke with -help for other options" - residual_args=( shell ) -} - -# verify this is an sbt dir or -create was given -[[ -r ./build.sbt || -d ./project || -n "$sbt_create" ]] || { - cat < A)(implicit reader: ValueReader[Option[A]]): A = getAs[A](path).getOrElse(default) + def apply[A](key: ConfigKey[A])(implicit reader: ValueReader[A]): A = as[A](key.path) } diff --git a/src/main/scala/net/ceedubs/ficus/readers/AllValueReaderInstances.scala b/src/main/scala/net/ceedubs/ficus/readers/AllValueReaderInstances.scala index ca28f39..012060a 100644 --- a/src/main/scala/net/ceedubs/ficus/readers/AllValueReaderInstances.scala +++ b/src/main/scala/net/ceedubs/ficus/readers/AllValueReaderInstances.scala @@ -1,6 +1,6 @@ package net.ceedubs.ficus.readers -trait AllValueReaderInstances extends AnyValReaders with StringReader with OptionReader +trait AllValueReaderInstances extends AnyValReaders with StringReader with SymbolReader with OptionReader with CollectionReaders with ConfigReader with DurationReaders with ArbitraryTypeReader with TryReader with ConfigValueReader diff --git a/src/main/scala/net/ceedubs/ficus/readers/ArbitraryTypeReader.scala b/src/main/scala/net/ceedubs/ficus/readers/ArbitraryTypeReader.scala index 18f3599..d6c63f7 100644 --- a/src/main/scala/net/ceedubs/ficus/readers/ArbitraryTypeReader.scala +++ b/src/main/scala/net/ceedubs/ficus/readers/ArbitraryTypeReader.scala @@ -1,9 +1,13 @@ package net.ceedubs.ficus.readers -import net.ceedubs.ficus.util.ReflectionUtils import com.typesafe.config.Config +import macrocompat.bundle +import net.ceedubs.ficus.readers.namemappers.NameMapper +import net.ceedubs.ficus.util.ReflectionUtils + import scala.language.experimental.macros -import scala.reflect.internal.{StdNames, SymbolTable, Definitions} +import scala.reflect.internal.{Definitions, StdNames, SymbolTable} +import scala.reflect.macros.blackbox trait ArbitraryTypeReader { implicit def arbitraryTypeValueReader[T]: ValueReader[T] = macro ArbitraryTypeReaderMacros.arbitraryTypeValueReader[T] @@ -11,24 +15,22 @@ trait ArbitraryTypeReader { object ArbitraryTypeReader extends ArbitraryTypeReader -object ArbitraryTypeReaderMacros { - import scala.reflect.macros.blackbox.Context - - def arbitraryTypeValueReader[T : c.WeakTypeTag](c: Context): c.Expr[ValueReader[T]] = { - import c.universe._ +@bundle +class ArbitraryTypeReaderMacros(val c: blackbox.Context) extends ReflectionUtils { + import c.universe._ + def arbitraryTypeValueReader[T : c.WeakTypeTag]: c.Expr[ValueReader[T]] = { reify { new ValueReader[T] { - def read(config: Config, path: String): T = instantiateFromConfig[T](c)( + def read(config: Config, path: String): T = instantiateFromConfig[T]( config = c.Expr[Config](Ident(TermName("config"))), - path = c.Expr[String](Ident(TermName("path")))).splice + path = c.Expr[String](Ident(TermName("path"))), + mapper = c.Expr[NameMapper](q"""_root_.net.ceedubs.ficus.readers.namemappers.NameMapper()""")).splice } } } - def instantiateFromConfig[T : c.WeakTypeTag](c: Context)(config: c.Expr[Config], path: c.Expr[String]): c.Expr[T] = { - import c.universe._ - + def instantiateFromConfig[T : c.WeakTypeTag](config: c.Expr[Config], path: c.Expr[String], mapper: c.Expr[NameMapper]): c.Expr[T] = { val returnType = c.weakTypeOf[T] def fail(reason: String) = c.abort(c.enclosingPosition, s"Cannot generate a config value reader for type $returnType, because $reason") @@ -38,28 +40,29 @@ object ArbitraryTypeReaderMacros { case x => Some(x) } - val instantiationMethod = ReflectionUtils.instantiationMethod[T](c, fail) + val initMethod = instantiationMethod[T](fail) - val instantiationArgs = extractMethodArgsFromConfig[T](c)(method = instantiationMethod, - companionObjectMaybe = companionSymbol, config = config, path = path, fail = fail) + val instantiationArgs = extractMethodArgsFromConfig[T]( + method = initMethod, + companionObjectMaybe = companionSymbol, config = config, path = path, mapper = mapper, fail = fail + ) val instantiationObject = companionSymbol.filterNot(_ => - instantiationMethod.isConstructor + initMethod.isConstructor ).map(Ident(_)).getOrElse(New(Ident(returnType.typeSymbol))) - val instantiationCall = Select(instantiationObject, instantiationMethod.name) + val instantiationCall = Select(instantiationObject, initMethod.name) c.Expr[T](Apply(instantiationCall, instantiationArgs)) } - def extractMethodArgsFromConfig[T : c.WeakTypeTag](c: Context)(method: c.universe.MethodSymbol, companionObjectMaybe: Option[c.Symbol], - config: c.Expr[Config], path: c.Expr[String], fail: String => Nothing): List[c.Tree] = { - import c.universe._ - + def extractMethodArgsFromConfig[T : c.WeakTypeTag](method: c.universe.MethodSymbol, companionObjectMaybe: Option[c.Symbol], + config: c.Expr[Config], path: c.Expr[String], mapper: c.Expr[NameMapper], + fail: String => Nothing): List[c.Tree] = { val decodedMethodName = method.name.decodedName.toString if (!method.isPublic) fail(s"'$decodedMethodName' method is not public") method.paramLists.head.zipWithIndex map { case (param, index) => val name = param.name.decodedName.toString - val key = q"""$path + "." + $name""" + val key = q"""$path + "." + $mapper.map($name)""" val returnType: Type = param.typeSignatureIn(c.weakTypeOf[T]) companionObjectMaybe.filter(_ => param.asTerm.isParamWithDefault) map { companionObject => @@ -73,7 +76,7 @@ object ArbitraryTypeReaderMacros { Apply(Select(argValueMaybe, TermName("getOrElse")), List({ // fall back to default value for param val u = c.universe.asInstanceOf[Definitions with SymbolTable with StdNames] - val getter = u.nme.defaultGetterName(u.TermName(decodedMethodName), index + 1) + val getter = u.nme.defaultGetterName(u.newTermName(decodedMethodName), index + 1) Select(Ident(companionObject), TermName(getter.encoded)) })) } getOrElse { diff --git a/src/main/scala/net/ceedubs/ficus/readers/BigNumberReaders.scala b/src/main/scala/net/ceedubs/ficus/readers/BigNumberReaders.scala index d1356ae..7bdb3da 100644 --- a/src/main/scala/net/ceedubs/ficus/readers/BigNumberReaders.scala +++ b/src/main/scala/net/ceedubs/ficus/readers/BigNumberReaders.scala @@ -1,5 +1,7 @@ package net.ceedubs.ficus.readers +import java.math.MathContext + import com.typesafe.config.{ConfigException, Config} trait BigNumberReaders { diff --git a/src/main/scala/net/ceedubs/ficus/readers/CollectionReaders.scala b/src/main/scala/net/ceedubs/ficus/readers/CollectionReaders.scala index 7362dfa..9aad140 100644 --- a/src/main/scala/net/ceedubs/ficus/readers/CollectionReaders.scala +++ b/src/main/scala/net/ceedubs/ficus/readers/CollectionReaders.scala @@ -3,7 +3,8 @@ package net.ceedubs.ficus.readers import com.typesafe.config.{ConfigUtil, Config} import collection.JavaConverters._ import collection.generic.CanBuildFrom -import scala.reflect.ClassTag +import scala.language.postfixOps +import scala.language.higherKinds trait CollectionReaders { diff --git a/src/main/scala/net/ceedubs/ficus/readers/DurationReaders.scala b/src/main/scala/net/ceedubs/ficus/readers/DurationReaders.scala index 79b99e0..5668b8d 100644 --- a/src/main/scala/net/ceedubs/ficus/readers/DurationReaders.scala +++ b/src/main/scala/net/ceedubs/ficus/readers/DurationReaders.scala @@ -2,7 +2,7 @@ package net.ceedubs.ficus.readers import scala.concurrent.duration.FiniteDuration import com.typesafe.config.Config -import scala.concurrent.duration.MILLISECONDS +import scala.concurrent.duration.{Duration, NANOSECONDS} trait DurationReaders { @@ -13,8 +13,8 @@ trait DurationReaders { */ implicit def finiteDurationReader: ValueReader[FiniteDuration] = new ValueReader[FiniteDuration] { def read(config: Config, path: String): FiniteDuration = { - val millis = config.getDuration(path, java.util.concurrent.TimeUnit.MILLISECONDS) - FiniteDuration(millis, MILLISECONDS) + val nanos = config.getDuration(path, NANOSECONDS) + Duration.fromNanos(nanos) } } } diff --git a/src/main/scala/net/ceedubs/ficus/readers/EitherReader.scala b/src/main/scala/net/ceedubs/ficus/readers/EitherReader.scala new file mode 100644 index 0000000..82b3e27 --- /dev/null +++ b/src/main/scala/net/ceedubs/ficus/readers/EitherReader.scala @@ -0,0 +1,19 @@ +package net.ceedubs.ficus.readers +import com.typesafe.config.{Config, ConfigException} + +trait EitherReader { + implicit def eitherReader[L,R]( implicit lReader : ValueReader[L], rReader : ValueReader[R]) : ValueReader[Either[L,R]] = + new ValueReader[Either[L,R]]{ + /** Reads the value at the path `path` in the Config */ + override def read(config: Config, path: String): Either[L, R] = { + TryReader.tryValueReader(rReader).read( config, path ) + .map( Right(_) ) + .recover{ + case _ : ConfigException => Left( lReader.read(config, path)) + } + .get + } + } +} + +object EitherReader extends EitherReader \ No newline at end of file diff --git a/src/main/scala/net/ceedubs/ficus/readers/EnumerationReader.scala b/src/main/scala/net/ceedubs/ficus/readers/EnumerationReader.scala new file mode 100644 index 0000000..e2be535 --- /dev/null +++ b/src/main/scala/net/ceedubs/ficus/readers/EnumerationReader.scala @@ -0,0 +1,28 @@ +package net.ceedubs.ficus.readers + +import com.typesafe.config.ConfigException.{BadValue, Generic} +import com.typesafe.config.Config + +import scala.reflect.ClassTag +import scala.util.{Failure, Success, Try} + +trait EnumerationReader { + implicit def enumerationValueReader[T <: Enumeration : ClassTag]: ValueReader[T#Value] = new ValueReader[T#Value] { + def read(config: Config, path: String): T#Value = { + val c = implicitly[ClassTag[T]].runtimeClass + val enum = Try(c.getField("MODULE$")) match { + case Success(m) => m.get(null).asInstanceOf[T] + case Failure(e) => throw new Generic("Cannot get instance of enum: " + c.getCanonicalName + "; " + + "make sure the enum is an object and it's not contained in a class or trait", e) + } + + val value = config.getString(path) + enum.values.find(_.toString == value) + .getOrElse(throw new BadValue(config.origin(), path, value + " isn't a valid value for enum: " + + "" + c.getCanonicalName + "; allowed values: " + enum.values.mkString(", "))) + .asInstanceOf[T#Value] + } + } +} + +object EnumerationReader extends EnumerationReader diff --git a/src/main/scala/net/ceedubs/ficus/readers/ISOZonedDateTimeReader.scala b/src/main/scala/net/ceedubs/ficus/readers/ISOZonedDateTimeReader.scala new file mode 100644 index 0000000..5356200 --- /dev/null +++ b/src/main/scala/net/ceedubs/ficus/readers/ISOZonedDateTimeReader.scala @@ -0,0 +1,17 @@ +package net.ceedubs.ficus.readers + +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter + +import com.typesafe.config.Config + +trait ISOZonedDateTimeReader { + implicit val isoZonedDateTimeReader: ValueReader[ZonedDateTime] = new ValueReader[ZonedDateTime] { + override def read(config: Config, path: String): ZonedDateTime = { + val dateTimeFormatter: DateTimeFormatter = DateTimeFormatter.ISO_DATE_TIME + ZonedDateTime.parse(config.getString(path), dateTimeFormatter) + } + } +} + +object ISOZonedDateTimeReader extends ISOZonedDateTimeReader diff --git a/src/main/scala/net/ceedubs/ficus/readers/SymbolReader.scala b/src/main/scala/net/ceedubs/ficus/readers/SymbolReader.scala new file mode 100644 index 0000000..b23fcfe --- /dev/null +++ b/src/main/scala/net/ceedubs/ficus/readers/SymbolReader.scala @@ -0,0 +1,11 @@ +package net.ceedubs.ficus.readers + +import com.typesafe.config.Config + +trait SymbolReader { + implicit val symbolValueReader: ValueReader[Symbol] = new ValueReader[Symbol] { + def read(config: Config, path: String): Symbol = Symbol(config.getString(path)) + } +} + +object SymbolReader extends SymbolReader diff --git a/src/main/scala/net/ceedubs/ficus/readers/namemappers/DefaultNameMapper.scala b/src/main/scala/net/ceedubs/ficus/readers/namemappers/DefaultNameMapper.scala new file mode 100644 index 0000000..1a1d8cb --- /dev/null +++ b/src/main/scala/net/ceedubs/ficus/readers/namemappers/DefaultNameMapper.scala @@ -0,0 +1,10 @@ +package net.ceedubs.ficus.readers.namemappers + +/** + * Default implementation for name mapper, names in code equivalent to names in configuration + */ +case object DefaultNameMapper extends NameMapper { + + override def map(name: String): String = name + +} diff --git a/src/main/scala/net/ceedubs/ficus/readers/namemappers/HyphenNameMapper.scala b/src/main/scala/net/ceedubs/ficus/readers/namemappers/HyphenNameMapper.scala new file mode 100644 index 0000000..64e344a --- /dev/null +++ b/src/main/scala/net/ceedubs/ficus/readers/namemappers/HyphenNameMapper.scala @@ -0,0 +1,10 @@ +package net.ceedubs.ficus.readers.namemappers + +object HyphenNameMapper extends NameMapper { + private lazy val r = "((?<=[a-z0-9])[A-Z]|(?<=[a-zA-Z])[0-9]|(?!^)[A-Z](?=[a-z]))".r + + /** + * Maps from a camelCasedName to a hyphenated-name + */ + override def map(name: String): String = r.replaceAllIn(name, m => s"-${m.group(1)}").toLowerCase +} diff --git a/src/main/scala/net/ceedubs/ficus/readers/namemappers/NameMapper.scala b/src/main/scala/net/ceedubs/ficus/readers/namemappers/NameMapper.scala new file mode 100644 index 0000000..c50a292 --- /dev/null +++ b/src/main/scala/net/ceedubs/ficus/readers/namemappers/NameMapper.scala @@ -0,0 +1,30 @@ +package net.ceedubs.ficus.readers.namemappers + +/** + * Defines an object that knows to map between names as they found in the code + * to those who should be defined in the configuration + */ +trait NameMapper { + + /** + * Maps between the name in the code to name in configuration + * @param name The name as found in the code + */ + def map(name: String): String + +} + +/** + * Helper object to get the current name mapper + */ +object NameMapper { + + + /** + * Gets the name mapper from the implicit scope + * @param nameMapper The name mapper from the implicit scope, or the default name mapper if not found + * @return The name mapper to be used in current implicit scope + */ + def apply()(implicit nameMapper: NameMapper = DefaultNameMapper): NameMapper = nameMapper + +} diff --git a/src/main/scala/net/ceedubs/ficus/readers/namemappers/package.scala b/src/main/scala/net/ceedubs/ficus/readers/namemappers/package.scala new file mode 100644 index 0000000..7b6324b --- /dev/null +++ b/src/main/scala/net/ceedubs/ficus/readers/namemappers/package.scala @@ -0,0 +1,7 @@ +package net.ceedubs.ficus.readers + +package object namemappers { + object implicits { + implicit val hyphenCase : NameMapper = HyphenNameMapper + } +} diff --git a/src/main/scala/net/ceedubs/ficus/util/ReflectionUtils.scala b/src/main/scala/net/ceedubs/ficus/util/ReflectionUtils.scala index d3d8e55..219c827 100644 --- a/src/main/scala/net/ceedubs/ficus/util/ReflectionUtils.scala +++ b/src/main/scala/net/ceedubs/ficus/util/ReflectionUtils.scala @@ -1,10 +1,15 @@ package net.ceedubs.ficus.util -import scala.reflect.macros.blackbox.Context +import macrocompat.bundle +import scala.reflect.macros.blackbox -object ReflectionUtils { - def instantiationMethod[T : c.WeakTypeTag](c: Context, fail: String => Nothing): c.universe.MethodSymbol = { - import c.universe._ +@bundle +trait ReflectionUtils { + val c: blackbox.Context + + import c.universe._ + + def instantiationMethod[T : c.WeakTypeTag](fail: String => Nothing): c.universe.MethodSymbol = { val returnType = c.weakTypeOf[T] diff --git a/src/test/scala/net/ceedubs/ficus/ConfigSerializer.scala b/src/test/scala/net/ceedubs/ficus/ConfigSerializer.scala index 659c493..d8a3d38 100644 --- a/src/test/scala/net/ceedubs/ficus/ConfigSerializer.scala +++ b/src/test/scala/net/ceedubs/ficus/ConfigSerializer.scala @@ -1,6 +1,8 @@ package net.ceedubs.ficus -import com.typesafe.config.ConfigUtil +import com.typesafe.config.{ConfigFactory, ConfigUtil, ConfigValue} + +import scala.language.implicitConversions trait ConfigSerializer[A] { def serialize(a: A): String @@ -44,6 +46,7 @@ object ConfigSerializer { final case class ConfigSerializerOps[A](a: A, serializer: ConfigSerializer[A]) { def asConfigValue: String = serializer.serialize(a) + def toConfigValue : ConfigValue = ConfigFactory.parseString( s"dummy=$asConfigValue").root().get("dummy") } object ConfigSerializerOps { diff --git a/src/test/scala/net/ceedubs/ficus/ExampleSpec.scala b/src/test/scala/net/ceedubs/ficus/ExampleSpec.scala index 94a077c..4eecbaf 100644 --- a/src/test/scala/net/ceedubs/ficus/ExampleSpec.scala +++ b/src/test/scala/net/ceedubs/ficus/ExampleSpec.scala @@ -4,10 +4,19 @@ import org.specs2.mutable.Specification import com.typesafe.config.{Config, ConfigFactory} import Ficus._ import net.ceedubs.ficus.readers.ArbitraryTypeReader._ +import net.ceedubs.ficus.readers.EnumerationReader._ import net.ceedubs.ficus.readers.ValueReader case class ServiceConfig(urls: Set[String], maxConnections: Int, httpsRequired: Boolean = false) +object Country extends Enumeration { + val DE = Value("DE") + val IT = Value("IT") + val NL = Value("NL") + val US = Value("US") + val GB = Value("GB") +} + class ExampleSpec extends Specification { // an example config snippet for us to work with @@ -24,6 +33,7 @@ class ExampleSpec extends Specification { | maxConnections = 25 | } |} + |countries = [DE, US, GB] """.stripMargin) "Ficus config" should { @@ -37,6 +47,8 @@ class ExampleSpec extends Specification { analyticsServiceConfig.as[List[String]]("urls") must beEqualTo(List("localhost:8002", "localhost:8003")) val analyticsServiceRequiresHttps = analyticsServiceConfig.as[Option[Boolean]]("httpsRequired") getOrElse false analyticsServiceRequiresHttps must beFalse + + config.as[Seq[Country.Value]]("countries") must be equalTo Seq(Country.DE, Country.US, Country.GB) } "Automagically be able to hydrate arbitrary types from config" in { diff --git a/src/test/scala/net/ceedubs/ficus/FicusConfigSpec.scala b/src/test/scala/net/ceedubs/ficus/FicusConfigSpec.scala index a5ddbc6..69a160f 100644 --- a/src/test/scala/net/ceedubs/ficus/FicusConfigSpec.scala +++ b/src/test/scala/net/ceedubs/ficus/FicusConfigSpec.scala @@ -1,7 +1,8 @@ package net.ceedubs.ficus -import com.typesafe.config.ConfigFactory -import Ficus.{ booleanValueReader, optionValueReader, toFicusConfig } +import com.typesafe.config.{Config, ConfigFactory} +import Ficus.{ booleanValueReader, optionValueReader, stringValueReader, toFicusConfig } +import net.ceedubs.ficus.readers.ValueReader class FicusConfigSpec extends Spec { def is = s2""" A Ficus config should @@ -9,6 +10,9 @@ class FicusConfigSpec extends Spec { def is = s2""" read a value with a value reader $readAValue get an existing value as a Some $getAsSome get a missing value as a None $getAsNone + getOrElse an existing value as asked type $getOrElseFromConfig + getOrElse a missing value with default value $getOrElseFromDefault + getOrElse an existing value as asked type with customer reader $getOrElseFromConfigWithCustomValueReader accept a CongigKey and return the appropriate type $acceptAConfigKey """ @@ -32,6 +36,30 @@ class FicusConfigSpec extends Spec { def is = s2""" cfg.getAs[Boolean]("nonValue") must beNone } + def getOrElseFromConfig = { + val configString = "arealstring" + val cfg = ConfigFactory.parseString(s"myValue = $configString") + cfg.getOrElse("myValue", "notarealstring") must beEqualTo(configString) + } + + def getOrElseFromDefault = { + val cfg = ConfigFactory.parseString("myValue = arealstring") + val default = "adefaultstring" + cfg.getOrElse("nonValue", default) must beEqualTo(default) + } + + def getOrElseFromConfigWithCustomValueReader = { + val cfg = ConfigFactory.parseString("myValue = 124") + val default = 23.toByte + + implicit val byteReader = new ValueReader[Byte]{ + def read(config: Config, path: String): Byte = config.getInt(path).toByte + } + + cfg.getOrElse("myValue", default) must beEqualTo(124.toByte) + } + + def acceptAConfigKey = prop { b: Boolean => val cfg = ConfigFactory.parseString(s"myValue = $b") val key: ConfigKey[Boolean] = SimpleConfigKey("myValue") diff --git a/src/test/scala/net/ceedubs/ficus/readers/ArbitraryTypeReaderSpec.scala b/src/test/scala/net/ceedubs/ficus/readers/ArbitraryTypeReaderSpec.scala index 01793f0..70bdbea 100644 --- a/src/test/scala/net/ceedubs/ficus/readers/ArbitraryTypeReaderSpec.scala +++ b/src/test/scala/net/ceedubs/ficus/readers/ArbitraryTypeReaderSpec.scala @@ -3,6 +3,7 @@ package readers import com.typesafe.config.ConfigFactory import ConfigSerializerOps._ +import net.ceedubs.ficus.readers.namemappers.NameMapper import shapeless.test.illTyped class ArbitraryTypeReaderSpec extends Spec { def is = s2""" @@ -23,6 +24,7 @@ class ArbitraryTypeReaderSpec extends Spec { def is = s2""" allow overriding of option reader for default values $overrideOptionReaderForDefault not choose between multiple Java constructors $notChooseBetweenJavaConstructors not be prioritized over a Reader defined in a type's companion object (when Ficus._ is imported) $notTrumpCompanionReader + use name mapper $useNameMapper """ import ArbitraryTypeReaderSpec._ @@ -158,6 +160,18 @@ class ArbitraryTypeReaderSpec extends Spec { def is = s2""" val cfg = ConfigFactory.parseString("""withReaderInCompanion { foo = "bar" }""") WithReaderInCompanion("from-companion") ==== cfg.as[WithReaderInCompanion]("withReaderInCompanion") } + + def useNameMapper = prop { foo: String => + import Ficus.stringValueReader + import ArbitraryTypeReader._ + implicit val nameMapper = new NameMapper { + override def map(name: String): String = name.toUpperCase + } + + val cfg = ConfigFactory.parseString(s"singleParam { FOO = ${foo.asConfigValue} }") + val instance: ClassWithSingleParam = arbitraryTypeValueReader[ClassWithSingleParam].read(cfg, "singleParam") + instance.getFoo must_== foo + } } object ArbitraryTypeReaderSpec { diff --git a/src/test/scala/net/ceedubs/ficus/readers/BigNumberReadersSpec.scala b/src/test/scala/net/ceedubs/ficus/readers/BigNumberReadersSpec.scala index b6f4af5..681a9d8 100644 --- a/src/test/scala/net/ceedubs/ficus/readers/BigNumberReadersSpec.scala +++ b/src/test/scala/net/ceedubs/ficus/readers/BigNumberReadersSpec.scala @@ -2,14 +2,12 @@ package net.ceedubs.ficus.readers import com.typesafe.config.ConfigFactory import net.ceedubs.ficus.Spec -import org.specs2.specification.Fragments -class BigNumberReadersSpec extends Spec with BigNumberReaders {def is: Fragments = s2""" +class BigNumberReadersSpec extends Spec with BigNumberReaders { def is = s2""" The BigDecimal value reader should read a double $readDoubleAsBigDecimal read a long $readLongAsBigDecimal read an int $readIntAsBigDecimal - read a bigDecimal $readBigDecimal read a bigInt $readBigIntAsBigDecimal read a bigDecimalAsString $readBigDecimalAsStringBigDecimal read a bigIntAsString $readBigIntAsStringBigDecimal @@ -19,9 +17,9 @@ class BigNumberReadersSpec extends Spec with BigNumberReaders {def is: Fragments read a long $readLongAsBigInt read a bigInt $readBigIntAsBigInt read a bigIntAsString $readBigIntAsStringBigInt - + """ - + def readDoubleAsBigDecimal = prop { d: Double => val cfg = ConfigFactory.parseString(s"myValue = $d") bigDecimalReader.read(cfg,"myValue") must beEqualTo(BigDecimal(d)) @@ -36,13 +34,22 @@ class BigNumberReadersSpec extends Spec with BigNumberReaders {def is: Fragments val cfg = ConfigFactory.parseString(s"myValue = $i") bigDecimalReader.read(cfg,"myValue") must beEqualTo(BigDecimal(i)) } - - def readBigDecimal = prop{ b: BigDecimal => + + /* + Due to differences with BigDecimal precision handling in scala 2.10, this + test is temporarily disabled. The next test compares the string + representation of the BigDecimal and serves as a test of the actual + functionality provided by this library, which simply parses the number + as a string and calls BigDecimal's apply method. The quality of that + BigDecimal implementation is not the concern of this library. + + def readBigDecimal = prop{ b: BigDecimal => scala.util.Try(BigDecimal(b.toString)).toOption.isDefined ==> { val cfg = ConfigFactory.parseString(s"myValue = $b") bigDecimalReader.read(cfg, "myValue") must beEqualTo(b) } } + */ def readBigDecimalAsStringBigDecimal = prop{ b: BigDecimal => scala.util.Try(BigDecimal(b.toString)).toOption.isDefined ==> { @@ -58,13 +65,13 @@ class BigNumberReadersSpec extends Spec with BigNumberReaders {def is: Fragments } } - def readBigIntAsBigDecimal = prop{ b: BigInt => + def readBigIntAsBigDecimal = prop{ b: BigInt => scala.util.Try(BigDecimal(b)).toOption.isDefined ==> { val cfg = ConfigFactory.parseString(s"myValue = $b") bigDecimalReader.read(cfg, "myValue") must beEqualTo(BigDecimal(b)) } } - + def readIntAsBigInt = prop { i: Int => val cfg = ConfigFactory.parseString(s"myValue = $i") bigIntReader.read(cfg,"myValue") must beEqualTo(BigInt(i)) @@ -89,4 +96,4 @@ class BigNumberReadersSpec extends Spec with BigNumberReaders {def is: Fragments } } -} \ No newline at end of file +} diff --git a/src/test/scala/net/ceedubs/ficus/readers/CollectionReadersSpec.scala b/src/test/scala/net/ceedubs/ficus/readers/CollectionReadersSpec.scala index b528e5e..0ad0047 100644 --- a/src/test/scala/net/ceedubs/ficus/readers/CollectionReadersSpec.scala +++ b/src/test/scala/net/ceedubs/ficus/readers/CollectionReadersSpec.scala @@ -8,6 +8,7 @@ import ConfigSerializerOps._ import org.scalacheck.util.Buildable import org.scalacheck.Arbitrary import CollectionReaderSpec._ +import scala.language.higherKinds class CollectionReadersSpec extends Spec with CollectionReaders { def is = s2""" The collection value readers should @@ -33,7 +34,7 @@ class CollectionReadersSpec extends Spec with CollectionReaders { def is = s2""" mapValueReader[A].read(cfg, "myValue") must beEqualTo(map) } - reads[String] and reads[Boolean] and reads[Int] and reads[Long] and reads[Double] + reads[String] && reads[Boolean] && reads[Int] && reads[Long] && reads[Double] } def readNestedMap = { @@ -62,7 +63,7 @@ class CollectionReadersSpec extends Spec with CollectionReaders { def is = s2""" } } - reads[String] and reads[Boolean] and reads[Int] and reads[Long] and reads[Double] + reads[String] && reads[Boolean] && reads[Int] && reads[Long] && reads[Double] } def readCollectionUsedDirectly = { @@ -75,15 +76,15 @@ class CollectionReadersSpec extends Spec with CollectionReaders { def is = s2""" object CollectionReaderSpec { import scala.collection._ - implicit def buildableIndexedSeq[T]: Buildable[T, IndexedSeq] = new Buildable[T, IndexedSeq] { + implicit def buildableIndexedSeq[T]: Buildable[T, IndexedSeq[T]] = new Buildable[T, IndexedSeq[T]] { def builder = IndexedSeq.newBuilder[T] } - implicit def buildableVector[T]: Buildable[T, Vector] = new Buildable[T, Vector] { + implicit def buildableVector[T]: Buildable[T, Vector[T]] = new Buildable[T, Vector[T]] { def builder = Vector.newBuilder[T] } - implicit def buildableIterable[T]: Buildable[T, Iterable] = new Buildable[T, Iterable] { + implicit def buildableIterable[T]: Buildable[T, Iterable[T]] = new Buildable[T, Iterable[T]] { def builder = new mutable.ListBuffer[T] } } diff --git a/src/test/scala/net/ceedubs/ficus/readers/DurationReadersSpec.scala b/src/test/scala/net/ceedubs/ficus/readers/DurationReadersSpec.scala index 9d300ec..4c6253b 100644 --- a/src/test/scala/net/ceedubs/ficus/readers/DurationReadersSpec.scala +++ b/src/test/scala/net/ceedubs/ficus/readers/DurationReadersSpec.scala @@ -2,13 +2,15 @@ package net.ceedubs.ficus package readers import com.typesafe.config.ConfigFactory + import scala.concurrent.duration._ import org.scalacheck.{Gen, Prop} -class DurationReadersSpec extends Spec with DurationReaders with DeactivatedTimeConversions { def is = s2""" +class DurationReadersSpec extends Spec with DurationReaders { def is = s2""" The finite duration reader should read a millisecond value $readMillis read a minute value $readMinutes + read a days value into days $readDaysUnit """ def readMillis = prop { i: Int => @@ -16,14 +18,14 @@ class DurationReadersSpec extends Spec with DurationReaders with DeactivatedTime finiteDurationReader.read(cfg, "myValue") must beEqualTo(i millis) } - def readMinutes = Prop.forAll(Gen.choose(1.5e-8.toInt, 1.5e8.toInt)) { i: Int => + def readMinutes = Prop.forAll(Gen.choose(-1.5e8.toInt, 1.5e8.toInt)) { i: Int => val cfg = ConfigFactory.parseString("myValue = \"" + i + " minutes\"") finiteDurationReader.read(cfg, "myValue") must beEqualTo(i minutes) } -} - -/* specs2 time conversions conflict with scala.concurrent.duration time conversions */ -trait DeactivatedTimeConversions extends org.specs2.time.TimeConversions { - override def intToRichLong(v: Int) = super.intToRichLong(v) + def readDaysUnit = Prop.forAll(Gen.choose(-106580, 106580)) { i: Int => + val str = i + " day" + (if (i == 1) "" else "s") + val cfg = ConfigFactory.parseString(s"""myValue = "$str" """) + finiteDurationReader.read(cfg, "myValue").toString must beEqualTo(str) + } } diff --git a/src/test/scala/net/ceedubs/ficus/readers/EitherReadersSpec.scala b/src/test/scala/net/ceedubs/ficus/readers/EitherReadersSpec.scala new file mode 100644 index 0000000..8f91ec9 --- /dev/null +++ b/src/test/scala/net/ceedubs/ficus/readers/EitherReadersSpec.scala @@ -0,0 +1,63 @@ +package net.ceedubs.ficus.readers + +import com.typesafe.config.ConfigFactory +import net.ceedubs.ficus.{ConfigSerializer, Spec} +import net.ceedubs.ficus.ConfigSerializerOps._ +import org.scalacheck.Arbitrary + +import scala.util.{Failure, Try} +import scala.collection.JavaConverters._ + +class EitherReadersSpec extends Spec with EitherReader with OptionReader with AnyValReaders with StringReader with TryReader with CollectionReaders{ + def is=s2""" + An Either value reader should + should read right side when possible $readRightSideString + fallback to left side when key is missing $fallbackToLeftSideOnMissingKey + fallback to left when failing to read right $fallbackToLeftSideOnBadRightValue + fail when both sides fail $rightAndLeftFailure + handle a Try on the right side $rightSideTry + handle a Try on the left side $leftSideTry + handle complex types $handleComplexTypes + """ + + + def readRightSideString = prop{ a : String => + val cfg = a.toConfigValue.atKey("x") + eitherReader[String,String].read(cfg, "x") must beEqualTo(Right(a)) + } + + def fallbackToLeftSideOnMissingKey = prop{ a : String => + eitherReader[Option[String], String].read( ConfigFactory.empty(), "x" ) must beEqualTo( Left(None) ) + } + + def fallbackToLeftSideOnBadRightValue = prop{ a : Int => + val badVal = a.toString + "xx" + eitherReader[String, Int].read( badVal.toConfigValue.atKey("x"), "x" ) must beEqualTo( Left(badVal) ) + } + + def rightAndLeftFailure = prop{ a : Int => + val badVal = a.toString + "xx" + tryValueReader(eitherReader[Int, Int]).read( badVal.toConfigValue.atKey("x"), "x" ) must beAnInstanceOf[Failure[Int]] + } + + def rightSideTry = prop{ a : Int => + val badVal = a.toString + "xx" + eitherReader[Int, Try[Int]].read( a.toConfigValue.atKey("x"), "x" ) must beRight( a ) + eitherReader[Int, Try[Int]].read( badVal.toConfigValue.atKey("x"), "x" ) must beRight( beFailedTry[Int] ) + } + + def leftSideTry = prop{ a : Int => + val badVal = a.toString + "xx" + eitherReader[Try[String], Int].read( badVal.toConfigValue.atKey("x"), "x" ) must beLeft( beSuccessfulTry( badVal) ) + eitherReader[Try[Int], Int].read( badVal.toConfigValue.atKey("x"), "x" ) must beLeft( beFailedTry[Int] ) + } + + def handleComplexTypes = prop{ (a : Int, b : Int ) => + val iMap = Map( "a" -> a, "b" -> b ) + val sMap = Map( "a" -> s"${a}xx", "b" -> s"${b}xx") + + eitherReader[Map[String,String], Map[String,String]].read( sMap.toConfigValue.atKey("a"), "a" ) must beRight(sMap) + eitherReader[Map[String,String], Map[String,Int]].read( iMap.toConfigValue.atKey("a"), "a" ) must beRight(iMap) + eitherReader[Map[String,String], Map[String,Int]].read( sMap.toConfigValue.atKey("a"), "a" ) must beLeft(sMap) + } +} diff --git a/src/test/scala/net/ceedubs/ficus/readers/EnumerationReadersSpec.scala b/src/test/scala/net/ceedubs/ficus/readers/EnumerationReadersSpec.scala new file mode 100644 index 0000000..2a5cf65 --- /dev/null +++ b/src/test/scala/net/ceedubs/ficus/readers/EnumerationReadersSpec.scala @@ -0,0 +1,65 @@ +package net.ceedubs.ficus.readers + +import com.typesafe.config.{ConfigException, ConfigFactory} +import net.ceedubs.ficus.Spec +import EnumerationReadersSpec._ + +import scala.reflect.ClassTag + +class EnumerationReadersSpec extends Spec with EnumerationReader { def is = s2""" + An enumeration value reader should + map a string value to its enumeration counterpart $successStringMapping + map a int value to its enumeration counterpart $successIntMapping + throw exception if value couldn't be converted to enum value $invalidMapping + throw exception if enumeration is contained in a class or trait $notInstantiable + throw exception if enumeration is not an object $notObject + """ + + def successStringMapping = { + val cfg = ConfigFactory.parseString("myValue = SECOND") + implicit val classTag = ClassTag[StringValueEnum.type](StringValueEnum.getClass) + enumerationValueReader[StringValueEnum.type].read(cfg, "myValue") must be equalTo StringValueEnum.second + } + + def successIntMapping = { + val cfg = ConfigFactory.parseString("myValue = second") + implicit val classTag = ClassTag[IntValueEnum.type](IntValueEnum.getClass) + enumerationValueReader[IntValueEnum.type].read(cfg, "myValue") must be equalTo IntValueEnum.second + } + + def invalidMapping = { + val cfg = ConfigFactory.parseString("myValue = fourth") + implicit val classTag = ClassTag[StringValueEnum.type](StringValueEnum.getClass) + enumerationValueReader[StringValueEnum.type].read(cfg, "myValue") must throwA[ConfigException.BadValue] + } + + def notInstantiable = { + val cfg = ConfigFactory.parseString("myValue = fourth") + implicit val classTag = ClassTag[InnerEnum.type](InnerEnum.getClass) + enumerationValueReader[InnerEnum.type].read(cfg, "myValue") must throwA[ConfigException.Generic] + } + + def notObject = { + val cfg = ConfigFactory.parseString("myValue = fourth") + implicit val classTag = ClassTag[NotObject](classOf[NotObject]) + enumerationValueReader[NotObject].read(cfg, "myValue") must throwA[ConfigException.Generic] + } + + object InnerEnum extends Enumeration +} + +object EnumerationReadersSpec { + + object StringValueEnum extends Enumeration { + val first = Value("FIRST") + val second = Value("SECOND") + val third = Value("THIRD") + } + + + object IntValueEnum extends Enumeration { + val first, second, third = Value + } + + class NotObject extends Enumeration +} diff --git a/src/test/scala/net/ceedubs/ficus/readers/ISOZonedDateTimeReaderSpec.scala b/src/test/scala/net/ceedubs/ficus/readers/ISOZonedDateTimeReaderSpec.scala new file mode 100644 index 0000000..6ba2c25 --- /dev/null +++ b/src/test/scala/net/ceedubs/ficus/readers/ISOZonedDateTimeReaderSpec.scala @@ -0,0 +1,35 @@ +package net.ceedubs.ficus +package readers + +import java.time.{ZoneId, ZonedDateTime} + +import com.typesafe.config.ConfigFactory + +import Ficus.{toFicusConfig, isoZonedDateTimeReader} + +class ISOZonedDateTimeReaderSpec extends Spec { def is = s2""" + The ISOZonedDateTimeReader should + read a ZonedDateTime in ISO format $readZonedDateTime + """ + + def readZonedDateTime = { + val cfg = ConfigFactory.parseString( + s""" + | foo { + | date = "2016-02-28T11:46:26.896+01:00[Europe/Berlin]" + | } + """.stripMargin) + val date = cfg.as[ZonedDateTime]("foo.date") + val expected = ZonedDateTime.of( + 2016, + 2, + 28, + 11, + 46, + 26, + 896000000, + ZoneId.of("Europe/Berlin") + ) + date should_==(expected) + } +} diff --git a/src/test/scala/net/ceedubs/ficus/readers/SymbolReaderSpec.scala b/src/test/scala/net/ceedubs/ficus/readers/SymbolReaderSpec.scala new file mode 100644 index 0000000..f6da331 --- /dev/null +++ b/src/test/scala/net/ceedubs/ficus/readers/SymbolReaderSpec.scala @@ -0,0 +1,16 @@ +package net.ceedubs.ficus +package readers + +import com.typesafe.config.ConfigFactory +import ConfigSerializerOps._ + +class SymbolReaderSpec extends Spec with SymbolReader { def is = s2""" + The Symbol value reader should + read a Symbol $readSymbol + """ + + def readSymbol = prop { string: String => + val cfg = ConfigFactory.parseString(s"myValue = ${string.asConfigValue}") + symbolValueReader.read(cfg, "myValue") must beEqualTo(Symbol(string)) + } +} diff --git a/src/test/scala/net/ceedubs/ficus/readers/namemappers/HyphenNameMapperSpec.scala b/src/test/scala/net/ceedubs/ficus/readers/namemappers/HyphenNameMapperSpec.scala new file mode 100644 index 0000000..bb4b09c --- /dev/null +++ b/src/test/scala/net/ceedubs/ficus/readers/namemappers/HyphenNameMapperSpec.scala @@ -0,0 +1,34 @@ +package net.ceedubs.ficus +package readers.namemappers + +import org.scalacheck.Arbitrary +import org.scalacheck.Gen._ +import org.specs2.matcher.DataTables + +class HyphenNameMapperSpec extends Spec with DataTables { + def is = s2""" + A HyphenNameMapper should + hyphenate a camelCased name $hyphenateCorrectly + hyphenate a camelCased name containing digits $hyphenateWithDigits + """ + + def nonemptyStringListGen = nonEmptyListOf(alphaStr.suchThat(_.length > 1).map(_.toLowerCase)) + + implicit def nonemptyStringList = Arbitrary(nonemptyStringListGen) + + def hyphenateCorrectly = prop { foos: List[String] => + val camelCased = (foos.head +: foos.tail.map(_.capitalize)).mkString + val hyphenated = foos.mkString("-").toLowerCase + + HyphenNameMapper.map(camelCased) must_== hyphenated + } + + def hyphenateWithDigits = + "camelCased" || "hyphenated" |> + "camelCasedName67" !! "camel-cased-name-67" | + "1144StartsWithA32422" !! "1144-starts-with-a-32422" | + "get13HTML42Snippets" !! "get-13-html-42-snippets" | + "thisOneIs13InThe43Middle" !! "this-one-is-13-in-the-43-middle" | { + (camelCased, hyphenated) => HyphenNameMapper.map(camelCased) must_== hyphenated + } +} diff --git a/travis-jvmopts b/travis-jvmopts new file mode 100644 index 0000000..44bd845 --- /dev/null +++ b/travis-jvmopts @@ -0,0 +1,3 @@ +-Xss1M +-Xms512M +-Xmx512M diff --git a/version.sbt b/version.sbt index a8bd390..73ece94 100644 --- a/version.sbt +++ b/version.sbt @@ -1 +1 @@ -version in ThisBuild := "1.2.0-SNAPSHOT" +version in ThisBuild := "1.3.5-SNAPSHOT"