├── .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 | [](https://travis-ci.org/andr83/scalaconfig)
4 | [](https://codecov.io/gh/andr83/scalaconfig)
5 | [](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 |
--------------------------------------------------------------------------------