├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── build.sbt ├── project ├── Dependencies.scala ├── build.properties └── plugins.sbt └── src ├── main └── scala │ └── com │ └── github │ └── andr83 │ └── scalaconfig │ ├── Reader.scala │ ├── instances │ ├── DefaultReader.scala │ ├── GenericReader.scala │ └── RecordToMap.scala │ └── package.scala └── test └── scala └── com └── github └── andr83 └── scalaconfig └── ReaderSpec.scala /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | 4 | # sbt specific 5 | .cache 6 | .history 7 | .lib/ 8 | dist/* 9 | target/ 10 | lib_managed/ 11 | src_managed/ 12 | project/boot/ 13 | project/plugins/project/ 14 | 15 | # Scala-IDE specific 16 | .scala_dependencies 17 | .worksheet 18 | .idea 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: scala 2 | jdk: 3 | - oraclejdk12 4 | sudo: false 5 | scala: 6 | - 2.11.8 7 | - 2.12.0 8 | - 2.13.0 9 | script: 10 | - sbt clean coverage test 11 | after_success: 12 | - sbt coverageReport coveralls -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Andrei Tupitcyn 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # scalaconfig 2 | 3 | [![Build Status](https://travis-ci.org/andr83/scalaconfig.svg?branch=master)](https://travis-ci.org/andr83/scalaconfig) 4 | [![codecov](https://codecov.io/gh/andr83/scalaconfig/branch/master/graph/badge.svg)](https://codecov.io/gh/andr83/scalaconfig) 5 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.github.andr83/scalaconfig_2.11/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.github.andr83/scalaconfig_2.11) 6 | 7 | ScalaConfig is a lightweight wrapper over ~~Typesafe~~ Lightbend Config library provides scala friendly access. 8 | It is implemented with type classes pattern and use shapeless for reading case classes. 9 | 10 | > Current documentation is a actual for 0.7 version. 11 | 12 | ScalaConfig adds additional metods: 13 | 14 | * `as[A](path)` - return `Either[Seq[Throwable], A]` by path in config object 15 | * `as[A]` - convert config object to `Either[Seq[Throwable], A]` 16 | * `asUnsafe[A](path)` - return value of type `A` by path in config object. On fail a first exception will thrown. 17 | * `asUnsafe[A]` - convert config object to value of type `A`. On fail a first exception will thrown. 18 | 19 | ## Supported types 20 | 21 | * Primitive (`Int`, `Long`, `Float`, `Double`, `Boolean`) 22 | * `String`, `Symbol` 23 | * Typesafe `Config` and `ConfigValue` 24 | * `FiniteDuration` 25 | * `Properties` 26 | * Collections (`List[A]`, `Set[A]`, `Map[String, A]`, `Map[String, AnyRef]`, `Array[A]`, etc. All types with a CanBuildFrom instance are supported) 27 | * `Option[A]` 28 | * Case classes 29 | 30 | ## Examples 31 | 32 | ```scala 33 | import com.github.andr83.scalaconfig._ 34 | 35 | val config: Config = ConfigFactory.load() 36 | 37 | val host = config.asUnsafe[String]("host") 38 | val port = config.asUnsafe[Int]("port") 39 | val path = config.asUnsafe[Option[String]]("path") 40 | val users = config.asUnsafe[List[String]]("access.users") 41 | 42 | case class DbConfig(host: String, port: Int, user: Option[String] = None, passwd: Option[String] = None) 43 | 44 | val dbConfig: Reader.Result[DbConfig] = config.as[DbConfig]("db") 45 | val dbConfig2: Reader.Result[DbConfig] = config.as[DbConfig] // Direct `config` mapping to case class 46 | val dbConfig3: Reader.Result[Map[String, String]] = config.as[Map[String, String]] 47 | val dbConfig3: Reader.Result[Map[String, AnyRef]]] = config.as[Map[String, AnyRef]] 48 | 49 | // Custom reader 50 | class User(name: String, password: String) 51 | 52 | implicit def userReader: Reader[User] = Reader.pure((config: Config, path: String) => { 53 | val userConfig = config.getConfig(path) 54 | new User( 55 | user = userConfig.asUnsafe[String]("name"), 56 | password = userConfig.asUnsafe[String]("password") 57 | ) 58 | } 59 | }) 60 | 61 | // OR 62 | implicit def userReader: Reader[User] = Reader.pureV((config: Config, path: String) => { 63 | val userConfig = config.getConfig(path) 64 | 65 | val userE = userConfig.as[String]("name") 66 | val passwordE = userConfig.as[String]("password") 67 | 68 | // with Cats or Scalaz it can be of course more elegant! 69 | (userE, passwordE) match { 70 | case (Right(user), Right(password)) => Right(new User(user, password)) 71 | case ( Left(errors1), Left(errors2)) => Left(errors1 ++ errors2) 72 | case (Left(errors), _) => Left(errors) 73 | case (_, Left(errors)) => Left(errors) 74 | } 75 | } 76 | }) 77 | 78 | ``` 79 | 80 | ## Implementation details 81 | https://andr83.io/en/1384/ 82 | 83 | ## Usage 84 | 85 | ### Latest release. 86 | 87 | ```scala 88 | // for >= Scala 2.11.x, 2.12.x, 2.13.x 89 | libraryDependencies += "com.github.andr83" %% "scalaconfig" % "0.7" 90 | ``` 91 | 92 | If you want scala 2.10 support please use 0.4 version. 93 | 94 | ### Develop branch. 95 | 96 | ```scala 97 | resolvers += Resolver.sonatypeRepo("snapshots") 98 | 99 | libraryDependencies += "com.github.andr83" %% "scalaconfig" % "0.8-SNAPSHOT" 100 | ``` 101 | 102 | ## License 103 | 104 | MIT License 105 | 106 | Copyright (c) 2016 Andrei Tupitcyn 107 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | 3 | 4 | val scalaconfig = project 5 | .in(file(".")) 6 | .settings( 7 | organization := "com.github.andr83", 8 | name := "scalaconfig", 9 | version := "0.8-SNAPSHOT", 10 | scalaVersion := "2.13.3", 11 | scalacOptions += "-Xlog-implicits", 12 | crossScalaVersions := Seq("2.11.12", "2.12.12", "2.13.3"), 13 | isSnapshot := version.value.endsWith("-SNAPSHOT"), 14 | scalacOptions := Seq("-unchecked", "-deprecation", "-encoding", "utf8"), 15 | resolvers ++= Seq( 16 | Resolver.sonatypeRepo("releases"), 17 | Resolver.sonatypeRepo("snapshots") 18 | ), 19 | publishMavenStyle := true, 20 | publishTo := { 21 | val nexus = "https://oss.sonatype.org/" 22 | if (isSnapshot.value) 23 | Some("snapshots" at nexus + "content/repositories/snapshots") 24 | else 25 | Some("releases" at nexus + "service/local/staging/deploy/maven2") 26 | }, 27 | publishTo := { 28 | val nexus = "https://oss.sonatype.org/" 29 | if (isSnapshot.value) 30 | Some("snapshots" at nexus + "content/repositories/snapshots") 31 | else 32 | Some("releases" at nexus + "service/local/staging/deploy/maven2") 33 | }, 34 | publishArtifact in Test := false, 35 | pomIncludeRepository := { _ => false }, 36 | libraryDependencies ++= Seq( 37 | Library.typesafeConfig % "provided", 38 | Library.scalaCollectionCompat, 39 | Library.scalaTest % "test", 40 | Library.shapeless 41 | ), 42 | Compile / scalacOptions ++= { 43 | CrossVersion.partialVersion(scalaVersion.value) match { 44 | case Some((2, n)) if n >= 13 => "-Ymacro-annotations" :: Nil 45 | case _ => Nil 46 | } 47 | }, 48 | libraryDependencies ++= { 49 | CrossVersion.partialVersion(scalaVersion.value) match { 50 | case Some((2, n)) if n >= 13 => Nil 51 | case _ => compilerPlugin(Library.scalaMacrosParadise cross CrossVersion.full) :: Nil 52 | } 53 | }, 54 | pomExtra := { 55 | https://github.com/andr83/scalaconfig 56 | 57 | 58 | MIT License 59 | http://www.opensource.org/licenses/mit-license.php 60 | 61 | 62 | 63 | scm:git:github.com/andr83/scalaconfig 64 | scm:git:git@github.com/andr83/scalaconfig 65 | github.com/andr83/scalaconfig 66 | 67 | 68 | 69 | andr83 70 | Andrei Tupitcyn 71 | andrew.tupitsin@gmail.com 72 | 73 | 74 | } 75 | ) 76 | -------------------------------------------------------------------------------- /project/Dependencies.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | 3 | 4 | object Version { 5 | val typesafeConfig = "1.4.0" 6 | val scalaCollectionCompat = "2.1.6" 7 | val scalaTest = "3.2.0" 8 | val shapeless = "2.3.3" 9 | val scalaMacrosParadise = "2.1.1" 10 | } 11 | 12 | object Library { 13 | val typesafeConfig = "com.typesafe" % "config" % Version.typesafeConfig 14 | val scalaTest = "org.scalatest" %% "scalatest" % Version.scalaTest 15 | val shapeless = "com.chuusai" %% "shapeless" % Version.shapeless 16 | val scalaCollectionCompat = "org.scala-lang.modules" %% "scala-collection-compat" % Version.scalaCollectionCompat 17 | val scalaMacrosParadise = "org.scalamacros" % "paradise" % Version.scalaMacrosParadise 18 | } -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version = 1.3.13 -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | logLevel := Level.Warn 2 | addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.6.1") 3 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.9.1") 4 | addSbtPlugin("com.jsuereth" % "sbt-pgp" % "2.0.0-M2") -------------------------------------------------------------------------------- /src/main/scala/com/github/andr83/scalaconfig/Reader.scala: -------------------------------------------------------------------------------- 1 | package com.github.andr83.scalaconfig 2 | 3 | import com.typesafe.config._ 4 | 5 | import scala.language.higherKinds 6 | import scala.util.{Failure, Success, Try} 7 | 8 | /** 9 | * @author andr83 10 | */ 11 | trait Reader[A] { 12 | def apply(config: Config, path: String): Reader.Result[A] 13 | } 14 | 15 | object Reader { 16 | type Result[A] = Either[Seq[Throwable], A] 17 | 18 | def pure[A](f: (Config, String)=> A): Reader[A] with Object = new Reader[A] { 19 | override def apply(config: Config, path: String): Result[A] = Try(f(config, path)) match { 20 | case Success(a) => Right(a) 21 | case Failure(e) => Left(Seq(e)) 22 | } 23 | } 24 | 25 | def pureV[A](f: (Config, String)=> Result[A]): Reader[A] with Object = new Reader[A] { 26 | override def apply(config: Config, path: String): Result[A] = Try(f(config, path)) match { 27 | case Success(a) => a 28 | case Failure(e) => Left(Seq(e)) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/scala/com/github/andr83/scalaconfig/instances/DefaultReader.scala: -------------------------------------------------------------------------------- 1 | package com.github.andr83.scalaconfig.instances 2 | 3 | import java.util.Properties 4 | 5 | import com.github.andr83.scalaconfig.{FakePath, Reader} 6 | import com.typesafe.config.{Config, ConfigValue} 7 | 8 | import scala.collection.JavaConverters._ 9 | import scala.collection.compat._ 10 | import scala.concurrent.duration.{FiniteDuration, NANOSECONDS} 11 | import scala.language.higherKinds 12 | 13 | /** 14 | * @author andr83 15 | */ 16 | trait DefaultReader { 17 | implicit val stringReader: Reader[String] = Reader.pure((config: Config, path: String) => config.getString(path)) 18 | 19 | implicit val symbolReader: Reader[Symbol] = Reader.pure((config: Config, path: String) => Symbol(config.getString(path))) 20 | 21 | implicit val intReader: Reader[Int] = Reader.pure((config: Config, path: String) => config.getInt(path)) 22 | 23 | implicit val longReader: Reader[Long] = Reader.pure((config: Config, path: String) => config.getLong(path)) 24 | 25 | implicit val floatReader: Reader[Float] = Reader.pure((config: Config, path: String) => config.getNumber(path).floatValue()) 26 | 27 | implicit val doubleReader: Reader[Double] = Reader.pure((config: Config, path: String) => config.getDouble(path)) 28 | 29 | implicit val booleanReader: Reader[Boolean] = Reader.pure((config: Config, path: String) => config.getBoolean(path)) 30 | 31 | implicit val finiteDurationReader: Reader[FiniteDuration] = Reader.pure((config: Config, path: String) => { 32 | val length = config.getDuration(path, java.util.concurrent.TimeUnit.NANOSECONDS) 33 | FiniteDuration(length, NANOSECONDS) 34 | }) 35 | 36 | implicit val configValueReader: Reader[ConfigValue] = Reader.pure((config: Config, path: String) => config.getValue(path)) 37 | 38 | implicit val configReader: Reader[Config] = Reader.pure((config: Config, path: String) => config.getConfig(path)) 39 | 40 | implicit def optReader[A: Reader]: Reader[Option[A]] = Reader.pureV((config: Config, path: String) => { 41 | if (config.hasPath(path)) { 42 | implicitly[Reader[A]].apply(config, path) match { 43 | case Right(a) => Right(Some(a)) 44 | case Left(errors) => Left(errors) 45 | } 46 | } else { 47 | Right(None) 48 | } 49 | }) 50 | 51 | implicit def traversableReader[A: Reader, C[_]](implicit factory: Factory[A, C[A]]): Reader[C[A]] = Reader.pureV((config: Config, path: String) => { 52 | val reader = implicitly[Reader[A]] 53 | val list = config.getList(path).asScala 54 | 55 | val (errors, res) = list map (item => { 56 | val entryConfig = item.atPath(FakePath) 57 | reader(entryConfig, FakePath) 58 | }) partition (_.isLeft) 59 | 60 | if (errors.nonEmpty) { 61 | Left(errors.flatMap(_.left.get).toSeq) 62 | } else { 63 | val builder = factory.newBuilder 64 | builder.sizeHint(list.size) 65 | res.foreach { 66 | case Right(a) => builder += a 67 | case _ => 68 | } 69 | Right(builder.result()) 70 | } 71 | }) 72 | 73 | def mapReader[A: Reader](collapsed: Boolean): Reader[Map[String, A]] = Reader.pureV((config: Config, path: String) => { 74 | val reader = implicitly[Reader[A]] 75 | val obj = config.getConfig(path) 76 | val entries = if (collapsed) obj.entrySet() else obj.root().entrySet() 77 | val (errors, res) = entries.asScala.map(e => { 78 | val entryConfig = e.getValue.atPath(FakePath) 79 | val key = if (collapsed) e.getKey.stripPrefix("\"").stripSuffix("\"") else e.getKey 80 | key -> reader(entryConfig, FakePath) 81 | }).partition(_._2.isLeft) 82 | if (errors.nonEmpty) { 83 | Left(errors.flatMap(_._2.left.get).toSeq) 84 | } else { 85 | Right(res.map { 86 | case (k, Right(a)) => k -> a 87 | case _ => throw new IllegalStateException 88 | }.toMap) 89 | } 90 | }) 91 | 92 | implicit def mapReaderGeneric[A: Reader]: Reader[Map[String, A]] = mapReader(collapsed = false) 93 | implicit def mapReaderAnyVal[A <: AnyVal : Reader]: Reader[Map[String, A]] = mapReader(collapsed = true) 94 | implicit def mapReaderString: Reader[Map[String, String]] = mapReader(collapsed = true) 95 | implicit def mapReaderInt: Reader[Map[String, Int]] = mapReader(collapsed = true) 96 | 97 | implicit val mapStringAnyReader: Reader[Map[String, AnyRef]] = Reader.pure((config: Config, path: String) => { 98 | val obj = config.getConfig(path) 99 | obj.root().unwrapped().asScala.toMap 100 | }) 101 | 102 | implicit val propertiesReader: Reader[Properties] = Reader.pureV((config: Config, path: String) => { 103 | mapStringAnyReader(config, path) match { 104 | case Right(map) => 105 | val props = new Properties() 106 | map.foreach { case (k, v)=> props.put(k, v)} 107 | Right(props) 108 | case Left(errors) => Left(errors) 109 | } 110 | }) 111 | } 112 | 113 | object DefaultReader extends DefaultReader 114 | -------------------------------------------------------------------------------- /src/main/scala/com/github/andr83/scalaconfig/instances/GenericReader.scala: -------------------------------------------------------------------------------- 1 | package com.github.andr83.scalaconfig.instances 2 | 3 | import com.github.andr83.scalaconfig.Reader 4 | import com.typesafe.config._ 5 | import shapeless._ 6 | import shapeless.labelled._ 7 | 8 | /** 9 | * @author andr83 10 | */ 11 | trait HReader[L <: HList] { 12 | def apply(config: Config, defaults: Map[String, Any]): Reader.Result[L] 13 | } 14 | 15 | object HReader { 16 | implicit val hnil: HReader[HNil] = new HReader[HNil] { 17 | override def apply(config: Config, defaults: Map[String, Any]): Reader.Result[HNil] = Right(HNil) 18 | } 19 | 20 | implicit def hconsHReader1[K <: Symbol, V, T <: HList](implicit 21 | witness: Witness.Aux[K], 22 | hr: Reader[V], 23 | tr: HReader[T] 24 | ): HReader[FieldType[K, V] :: T] = new HReader[FieldType[K, V] :: T] { 25 | override def apply(config: Config, defaults: Map[String, Any]): Reader.Result[FieldType[K, V] :: T] = { 26 | val key = witness.value.name 27 | val valueRes: Reader.Result[V] = if (config.hasPath(key)) { 28 | hr(config, key) 29 | } else { 30 | defaults 31 | .get(key) 32 | .map(v => Right(v.asInstanceOf[V])) 33 | .getOrElse(hr(config, key)) //trying to resolve non existing key in reader, e.g. for Option 34 | } 35 | val trRes = tr(config, defaults) 36 | 37 | (valueRes, trRes) match { 38 | case (Right(head), Right(tail)) => Right(field[K](head) :: tail) 39 | case (Left(errors1), Left(errors2)) => Left(errors1 ++ errors2) 40 | case (Left(errors), _) => Left(errors) 41 | case (_, Left(errors)) => Left(errors) 42 | } 43 | } 44 | } 45 | } 46 | 47 | 48 | trait GenericReader { 49 | 50 | implicit def anyValReader[T <: AnyVal, U](implicit 51 | unwrapped: Unwrapped.Aux[T, U], 52 | reader: Reader[U] 53 | ): Reader[T] = Reader.pureV((config: Config, path: String) => { 54 | reader(config, path).right.map(unwrapped.wrap) 55 | }) 56 | 57 | implicit def genericReader[H, T <: HList, D <: HList](implicit 58 | gen: LabelledGeneric.Aux[H, T], 59 | tr: Lazy[HReader[T]], 60 | defaults: Default.AsRecord.Aux[H, D], 61 | defaultMapper: RecordToMap[D] 62 | ): Reader[H] = new Reader[H] { 63 | def apply(config: Config, path: String): Reader.Result[H] = { 64 | tr.value(config.getConfig(path), defaultMapper(defaults())) match { 65 | case Right(res) => Right(gen.from(res)) 66 | case Left(errors) => Left(errors) 67 | } 68 | } 69 | } 70 | 71 | } 72 | 73 | object GenericReader extends GenericReader -------------------------------------------------------------------------------- /src/main/scala/com/github/andr83/scalaconfig/instances/RecordToMap.scala: -------------------------------------------------------------------------------- 1 | package com.github.andr83.scalaconfig.instances 2 | 3 | import shapeless.labelled.FieldType 4 | import shapeless.{::, HList, HNil, Witness} 5 | 6 | import scala.collection.immutable.Map 7 | 8 | abstract class RecordToMap[R <: HList] { 9 | def apply(r: R): Map[String, Any] 10 | } 11 | 12 | object RecordToMap { 13 | implicit val hnilRecordToMap: RecordToMap[HNil] = new RecordToMap[HNil] { 14 | def apply(r: HNil): Map[String, Any] = Map.empty 15 | } 16 | 17 | implicit def hconsRecordToMap[K <: Symbol, V, T <: HList](implicit 18 | wit: Witness.Aux[K], 19 | rtmT: RecordToMap[T] 20 | ): RecordToMap[FieldType[K, V] :: T] = new RecordToMap[FieldType[K, V] :: T] { 21 | def apply(r: FieldType[K, V] :: T): Map[String, Any] = rtmT(r.tail) + ((wit.value.name, r.head)) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/scala/com/github/andr83/scalaconfig/package.scala: -------------------------------------------------------------------------------- 1 | package com.github.andr83 2 | 3 | import com.github.andr83.scalaconfig.instances.{DefaultReader, GenericReader} 4 | import com.typesafe.config.Config 5 | 6 | /** 7 | * @author andr83 8 | */ 9 | package object scalaconfig extends GenericReader with DefaultReader { 10 | private[scalaconfig] val FakePath: String = "fakePath" 11 | 12 | implicit class ScalaConfig(val config: Config) extends AnyVal { 13 | def as[A: Reader]: Reader.Result[A] = implicitly[Reader[A]].apply(config.atKey(FakePath), FakePath) 14 | 15 | def as[A: Reader](path: String): Reader.Result[A] = implicitly[Reader[A]].apply(config, path) 16 | 17 | def asUnsafe[A: Reader]: A = as[A].fold(errors=> throw errors.head, identity) 18 | 19 | def asUnsafe[A: Reader](path: String): A = as[A](path).fold(errors=> throw errors.head, identity) 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/test/scala/com/github/andr83/scalaconfig/ReaderSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.andr83.scalaconfig 2 | 3 | import java.util.Properties 4 | 5 | import com.typesafe.config.{Config, ConfigFactory, ConfigValue, ConfigValueType} 6 | import org.scalatest.Inside 7 | import org.scalatest.flatspec.AnyFlatSpec 8 | import org.scalatest.matchers.should.Matchers 9 | 10 | import scala.concurrent.duration._ 11 | 12 | /** 13 | * @author andr83 14 | */ 15 | class ReaderSpec extends AnyFlatSpec with Matchers with Inside { 16 | 17 | "String value reader" should "read string" in { 18 | val config = ConfigFactory.parseString(s"stringField = SomeString") 19 | 20 | config.asUnsafe[String]("stringField") should be("SomeString") 21 | } 22 | 23 | "String value" should "be read as symbol also" in { 24 | val config = ConfigFactory.parseString(s"stringField = SomeString") 25 | 26 | config.asUnsafe[Symbol]("stringField") should equal(Symbol("SomeString")) 27 | } 28 | 29 | "Int value reader" should "read int" in { 30 | val config = ConfigFactory.parseString(s"intField = 42") 31 | 32 | config.asUnsafe[Int]("intField") should be(42) 33 | } 34 | 35 | "Long value reader" should "read long" in { 36 | val config = ConfigFactory.parseString(s"longField = 42") 37 | 38 | config.asUnsafe[Long]("longField") should be(42) 39 | } 40 | 41 | "Float value reader" should "read float" in { 42 | val config = ConfigFactory.parseString(s"floatField = 42.6") 43 | 44 | config.asUnsafe[Float]("floatField") should be(42.6f) 45 | } 46 | 47 | "Double value reader" should "read double" in { 48 | val config = ConfigFactory.parseString(s"doubleField = 42.6") 49 | 50 | config.asUnsafe[Double]("doubleField") should be(42.6) 51 | } 52 | 53 | "Boolean value reader" should "read boolean" in { 54 | val config = ConfigFactory.parseString(s"flagField = true") 55 | 56 | config.asUnsafe[Boolean]("flagField") should be(true) 57 | } 58 | 59 | "Duration value reader" should "read duration values according HOCON spec" in { 60 | val config = ConfigFactory.parseString( 61 | """ 62 | |d10s = 10 seconds 63 | |d1m = 1 minute 64 | |d12d = 12d 65 | """.stripMargin) 66 | 67 | config.asUnsafe[FiniteDuration]("d10s") should be(10.seconds) 68 | config.asUnsafe[FiniteDuration]("d1m") should be(1.minute) 69 | config.asUnsafe[FiniteDuration]("d12d") should be(12.days) 70 | } 71 | 72 | "ConfigValue value reader" should "read Typesafe ConfigValue" in { 73 | val config = ConfigFactory.parseString(s"someField = someValue") 74 | 75 | val configValue = config.asUnsafe[ConfigValue]("someField") 76 | configValue.valueType() should be(ConfigValueType.STRING) 77 | configValue.unwrapped() should be("someValue") 78 | } 79 | 80 | "Config value reader" should "read Typesafe Config" in { 81 | val innerConfigStr = 82 | """ 83 | |{ 84 | | field1 = value1 85 | | field2 = value2 86 | |} 87 | """.stripMargin 88 | val config = ConfigFactory.parseString( 89 | s""" 90 | |innerConfig = $innerConfigStr 91 | """.stripMargin) 92 | 93 | config.asUnsafe[Config]("innerConfig") should be(ConfigFactory.parseString(innerConfigStr)) 94 | } 95 | 96 | "Option value reader" should "wrap existing value in a Some or return a None" in { 97 | val config = ConfigFactory.parseString(s"stringField = SomeString") 98 | 99 | config.asUnsafe[Option[String]]("stringField") should be(Some("SomeString")) 100 | config.asUnsafe[Option[String]]("emptyField") should be(None) 101 | } 102 | 103 | "Traversable reader" should "return any collection which have s CanBuildFrom instance " in { 104 | val config = ConfigFactory.parseString( 105 | """ 106 | |stringItems: ["a","b","c"] 107 | |intItems: [1,2,3] 108 | """.stripMargin) 109 | 110 | config.asUnsafe[Seq[String]]("stringItems") should be(Seq("a", "b", "c")) 111 | config.asUnsafe[List[String]]("stringItems") should be(List("a", "b", "c")) 112 | config.asUnsafe[Array[String]]("stringItems") should be(Array("a", "b", "c")) 113 | 114 | config.asUnsafe[Seq[Option[String]]]("stringItems") should be(Seq(Some("a"), Some("b"), Some("c"))) 115 | 116 | config.asUnsafe[Seq[Int]]("intItems") should be(Seq(1, 2, 3)) 117 | config.asUnsafe[List[Int]]("intItems") should be(List(1, 2, 3)) 118 | config.asUnsafe[Array[Int]]("intItems") should be(Array(1, 2, 3)) 119 | } 120 | 121 | "Map reader" should "return Map[String, A]" in { 122 | val config = ConfigFactory.parseString( 123 | """ 124 | |mapField = { 125 | | key1 = value1 126 | | key2 = value2 127 | |} 128 | """.stripMargin) 129 | 130 | config.asUnsafe[Map[String, String]]("mapField") should be(Map("key1" -> "value1", "key2" -> "value2")) 131 | } 132 | 133 | "Map reader" should "return also nested value" in { 134 | val config = ConfigFactory.parseString( 135 | """ 136 | |parent { 137 | | mapField = { 138 | | key1 = value1 139 | | key2 = value2 140 | | } 141 | |} 142 | """.stripMargin) 143 | 144 | config.asUnsafe[Map[String, String]]("parent.mapField") should be(Map("key1" -> "value1", "key2" -> "value2")) 145 | } 146 | 147 | "Map reader" should "return Map[String, String] also for keys with dots" in { 148 | val config = ConfigFactory.parseString( 149 | """ 150 | |mapField = { 151 | | "10.11" = value1 152 | | "12.13" = value2 153 | | key3.subkey = value3 154 | |} 155 | """.stripMargin) 156 | 157 | config.asUnsafe[Map[String, String]]("mapField") should be(Map("10.11" -> "value1", "12.13" -> "value2", "key3.subkey" -> "value3")) 158 | } 159 | 160 | // TODO support for collapsed maps with objects 161 | "Map reader" should "return Map[String, A] also for keys with dots" ignore { 162 | val config = ConfigFactory.parseString( 163 | """ 164 | |mapField = { 165 | | "10.11" = { 166 | | key1 = value1 167 | | key2 = 11 168 | | } 169 | | "12.13" = { 170 | | key1 = value2 171 | | key2 = 22 172 | | } 173 | | key3.subkey = { 174 | | key1 = value3 175 | | key2 = 33 176 | | } 177 | |} 178 | """.stripMargin) 179 | 180 | case class Test(key1: String, key2: Int) 181 | val t1 = Test("value1", 11) 182 | val t2 = Test("value2", 22) 183 | val t3 = Test("value3", 33) 184 | 185 | config.asUnsafe[Map[String, Test]]("mapField") should be(Map("10.11" -> t1, "12.13" -> t2, "key3.subkey" -> t3)) 186 | } 187 | 188 | "Map reader" should "be able to read nested collapsed Map with Strings" in { 189 | val config = ConfigFactory.parseString( 190 | """ 191 | |users = { 192 | | user1 { 193 | | key11.subkey1 = value1 194 | | key12.subkey2 = value2 195 | | } 196 | | user2 { 197 | | key21.subkey1 = value1 198 | | key22.subkey2 = value2 199 | | } 200 | |} 201 | """.stripMargin) 202 | 203 | val user1 = Map("key11.subkey1" -> "value1", "key12.subkey2" -> "value2") 204 | val user2 = Map("key21.subkey1" -> "value1", "key22.subkey2" -> "value2") 205 | 206 | type Users = Map[String, Map[String, String]] 207 | 208 | config.asUnsafe[Users]("users") should be (Map("user1" -> user1, "user2" -> user2)) 209 | } 210 | 211 | "Map reader" should "be able to read nested collapsed Map with Ints" in { 212 | val config = ConfigFactory.parseString( 213 | """ 214 | |users = { 215 | | user1 { 216 | | key11.subkey1 = 11 217 | | key12.subkey2 = 12 218 | | } 219 | | user2 { 220 | | key21.subkey1 = 21 221 | | key22.subkey2 = 22 222 | | } 223 | |} 224 | """.stripMargin) 225 | 226 | val user1 = Map("key11.subkey1" -> 11, "key12.subkey2" -> 12) 227 | val user2 = Map("key21.subkey1" -> 21, "key22.subkey2" -> 22) 228 | 229 | type Users = Map[String, Map[String, Int]] 230 | 231 | config.asUnsafe[Users]("users") should be (Map("user1" -> user1, "user2" -> user2)) 232 | } 233 | 234 | "Map reader" should "be able to read nested collapsed Map with AnyVals" in { 235 | val config = ConfigFactory.parseString( 236 | """ 237 | |users = { 238 | | user1 { 239 | | key11.subkey1 = value1 240 | | key12.subkey2 = value2 241 | | } 242 | | user2 { 243 | | key21.subkey1 = value1 244 | | key22.subkey2 = value2 245 | | } 246 | |} 247 | """.stripMargin) 248 | 249 | val sv = StringValue 250 | val user1 = Map("key11.subkey1" -> sv("value1"), "key12.subkey2" -> sv("value2")) 251 | val user2 = Map("key21.subkey1" -> sv("value1"), "key22.subkey2" -> sv("value2")) 252 | 253 | type Users = Map[String, Map[String, StringValue]] 254 | 255 | config.asUnsafe[Users]("users") should be (Map("user1" -> user1, "user2" -> user2)) 256 | } 257 | 258 | "Config object" should "be directly convert to Map" in { 259 | val config = ConfigFactory.parseString( 260 | """ 261 | |{ 262 | | key1 = value1 263 | | key2 = value2 264 | |} 265 | """.stripMargin) 266 | 267 | config.asUnsafe[Map[String, String]] should be(Map("key1" -> "value1", "key2" -> "value2")) 268 | } 269 | 270 | "Config object" should "be able to convert to Map[String, AnyRef]" in { 271 | val config = ConfigFactory.parseString( 272 | """ 273 | |{ 274 | | key1 = value1 275 | | key2 = 42 276 | |} 277 | """.stripMargin) 278 | 279 | config.asUnsafe[Map[String, AnyRef]] should be(Map("key1" -> "value1", "key2" -> 42)) 280 | } 281 | 282 | "Config object" should "be able to convert java Properties" in { 283 | val config = ConfigFactory.parseString( 284 | """ 285 | |{ 286 | | key1 = value1 287 | | key2 = 42 288 | |} 289 | """.stripMargin) 290 | 291 | val expected = new Properties() 292 | expected.put("key1", "value1") 293 | expected.put("key2", Int.box(42)) 294 | config.asUnsafe[Properties] should be (expected) 295 | } 296 | 297 | "Value classes" should "read raw value" in { 298 | val config = ConfigFactory.parseString( 299 | """ 300 | |{ 301 | | key1 = value1 302 | | key2 = 42 303 | |} 304 | """.stripMargin) 305 | 306 | case class Foo(key1: StringValue, key2: IntValue) 307 | 308 | config.asUnsafe[IntValue]("key2") shouldBe IntValue(42) 309 | config.asUnsafe[StringValue]("key1") shouldBe StringValue("value1") 310 | } 311 | 312 | "Config object" should "be able to be converted to Case Class instance" in { 313 | val config = ConfigFactory.parseString( 314 | """ 315 | |test = { 316 | | key1 = value1 317 | | key2 = 42 318 | |} 319 | """.stripMargin) 320 | case class Test(key1: String, key2: Int) 321 | 322 | config.asUnsafe[Test]("test") should be(Test(key1 = "value1", key2 = 42)) 323 | } 324 | 325 | "Generic reader" should "be able to read nested Case Classes" in { 326 | val config = ConfigFactory.parseString( 327 | """ 328 | |test = { 329 | | user = { 330 | | name = user 331 | | password = pswd 332 | | } 333 | |} 334 | """.stripMargin) 335 | 336 | case class User(name: String, password: String) 337 | case class Settings(user: User) 338 | 339 | 340 | config.asUnsafe[Settings]("test") should be (Settings(User("user", "pswd"))) 341 | } 342 | 343 | "Case class with default Option value to Some(...)" should "be correctly instantiated" in { 344 | val config = ConfigFactory.parseString( 345 | """ 346 | |test = { 347 | | key1 = value1 348 | |} 349 | """.stripMargin) 350 | case class Test(key1: String, key2: Option[Int] = Some(42) ) 351 | val c = config.asUnsafe[Test]("test") 352 | c.key1 should be ("value1") 353 | c.key2 should be (Some(42)) // the default value of the case class should be Some(42) and not None 354 | } 355 | 356 | "Option value in Case class" should "map to None" in { 357 | val config = ConfigFactory.parseString( 358 | """ 359 | |test = { 360 | | key1 = value1 361 | |} 362 | """.stripMargin) 363 | case class Test(key1: String, key2: Option[Int]) 364 | val c = config.asUnsafe[Test]("test") 365 | c.key1 should be ("value1") 366 | c.key2 should be (None) 367 | } 368 | 369 | "Custom reader" should "be used" in { 370 | val config = ConfigFactory.parseString( 371 | """ 372 | |test = [user, 123] 373 | """.stripMargin) 374 | 375 | case class Test(key1: String, key2: Int) 376 | 377 | implicit val testReader = Reader.pure[Test]((config: Config, path: String) => { 378 | val list = config.getList(path) 379 | Test(list.get(0).unwrapped().asInstanceOf[String], list.get(1).unwrapped().asInstanceOf[Int]) 380 | }) 381 | 382 | config.asUnsafe[Test]("test") should be(Test(key1 = "user", key2 = 123)) 383 | } 384 | 385 | "Reader" should "return all errors" in { 386 | val config = ConfigFactory.parseString( 387 | """ 388 | |test = { 389 | | key1 = value1 390 | | key2 = value2 391 | |} 392 | """.stripMargin) 393 | case class Test(key1: Int, key2: Option[Float] = Some(42f)) 394 | 395 | val res = config.as[Test]("test") 396 | inside(res) { 397 | case Left(errors) => errors should have size 2 398 | } 399 | } 400 | } 401 | 402 | case class StringValue(value: String) extends AnyVal 403 | case class IntValue(value: Int) extends AnyVal 404 | --------------------------------------------------------------------------------