Skip to content

Commit aba35c6

Browse files
Merge pull request #1 from avast/feat/pureconfig
feat: Add module pureconfig with tests and basic documentation
2 parents 3d16fb3 + b2a3f44 commit aba35c6

File tree

15 files changed

+247
-7
lines changed

15 files changed

+247
-7
lines changed

.scalafmt.conf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@ continuationIndent.defnSite = 2
66
align.arrowEnumeratorGenerator = true
77
align.openParenCallSite = true
88
align.openParenDefnSite = true
9-
rewrite.rules = [RedundantParens, RedundantBraces, SortImports, SortModifiers, PreferCurlyFors]
9+
rewrite.rules = [RedundantParens, SortImports, SortModifiers, PreferCurlyFors]
1010
includeNoParensInSelectChains = true
1111
lineEndings = preserve

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
11
# Scala Server Toolkit
22

3+
[![Build Status](https://travis-ci.org/avast/scala-server-toolkit.svg?branch=master)](https://travis-ci.org/avast/scala-server-toolkit)
4+
![GitHub tag (latest SemVer)](https://img.shields.io/github/v/tag/avast/scala-server-toolkit?color=white&label=version&sort=semver)
5+
[![Scala Steward badge](https://img.shields.io/badge/Scala_Steward-helping-brightgreen.svg?style=flat&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAQCAMAAAARSr4IAAAAVFBMVEUAAACHjojlOy5NWlrKzcYRKjGFjIbp293YycuLa3pYY2LSqql4f3pCUFTgSjNodYRmcXUsPD/NTTbjRS+2jomhgnzNc223cGvZS0HaSD0XLjbaSjElhIr+AAAAAXRSTlMAQObYZgAAAHlJREFUCNdNyosOwyAIhWHAQS1Vt7a77/3fcxxdmv0xwmckutAR1nkm4ggbyEcg/wWmlGLDAA3oL50xi6fk5ffZ3E2E3QfZDCcCN2YtbEWZt+Drc6u6rlqv7Uk0LdKqqr5rk2UCRXOk0vmQKGfc94nOJyQjouF9H/wCc9gECEYfONoAAAAASUVORK5CYII=)](https://scala-steward.org)
6+
37
This project is a culmination of years of Scala development at Avast and tries to represent the best practices of Scala server development
48
we have gained together with tools that allow us to be effective. It is a set of small, flexible and cohesive building blocks that fit
59
together well and allow you to build reliable server applications.
610

11+
## [Documentation](./docs/index.md)
12+
13+
Or you can [deep dive into example code](example/src/main/scala/com/avast/server/toolkit/example/Main.scala) if you like that more.
14+
715
## Design
816

917
There are certain design decisions and constraints that are put in place to guide the development of the toolkit and recommended for

build.sbt

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,47 @@ ThisBuild / scmInfo := Some(
77
ScmInfo(url("https://github.com/avast/scala-server-toolkit"), "scm:git:[email protected]:avast/scala-server-toolkit.git")
88
)
99

10-
ThisBuild / scalaVersion := "2.13.1"
10+
ThisBuild / scalaVersion := "2.13.0"
1111
ThisBuild / scalacOptions := ScalacOptions.default
1212

13+
ThisBuild / turbo := true
14+
1315
lazy val commonSettings = Seq(
1416
libraryDependencies ++= Seq(
15-
compilerPlugin("org.typelevel" %% "kind-projector" % "0.10.3")
17+
compilerPlugin(Dependencies.kindProjector),
18+
Dependencies.catsEffect,
19+
Dependencies.scalaTest
1620
),
1721
Test / publishArtifact := false
1822
)
1923

2024
lazy val root = (project in file("."))
25+
.aggregate(example, pureconfig)
2126
.settings(
22-
commonSettings,
2327
name := "scala-server-toolkit",
2428
publish / skip := true
2529
)
30+
31+
lazy val example = project
32+
.dependsOn(pureconfig)
33+
.enablePlugins(MdocPlugin)
34+
.settings(
35+
commonSettings,
36+
name := "scala-server-toolkit-example",
37+
publish / skip := true,
38+
run / fork := true,
39+
Global / cancelable := true,
40+
mdocIn := baseDirectory.value / "src" / "main" / "mdoc",
41+
mdocOut := baseDirectory.value / ".." / "docs",
42+
libraryDependencies ++= Seq(
43+
Dependencies.zio,
44+
Dependencies.zioInteropCats
45+
)
46+
)
47+
48+
lazy val pureconfig = project
49+
.settings(
50+
commonSettings,
51+
name := "scala-server-toolkit-pureconfig",
52+
libraryDependencies += Dependencies.pureConfig
53+
)

docs/index.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Scala Server Toolkit Documentation
2+
3+
* [Getting Started](#getting-started)
4+
* [Module PureConfig](pureconfig.md)
5+
6+
## Getting Started
7+
8+
Creating a simple HTTP server is as easy as this:
9+
10+
#### build.sbt
11+
12+
![GitHub tag (latest SemVer)](https://img.shields.io/github/v/tag/avast/scala-server-toolkit?label=version&sort=semver)
13+
14+
`libraryDependencies += "com.avast.server.toolkit" %% "scala-server-toolkit-pureconfig" % "<VERSION>"`

docs/pureconfig.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Module PureConfig
2+
3+
![GitHub tag (latest SemVer)](https://img.shields.io/github/v/tag/avast/scala-server-toolkit?label=version&sort=semver)
4+
5+
`libraryDependencies += "com.avast.server.toolkit" %% "scala-server-toolkit-pureconfig" % "<VERSION>"`
6+
7+
This module allows you to load your application's configuration file according to a case class provided to it. It uses
8+
[PureConfig](https://pureconfig.github.io) library to do so which uses [Lightbend Config](https://github.com/lightbend/config) which means
9+
that your application's configuration will be in [HOCON](https://github.com/lightbend/config/blob/master/HOCON.md) format.
10+
11+
12+
```scala
13+
import com.avast.server.toolkit.pureconfig._
14+
import pureconfig.ConfigReader
15+
import pureconfig.generic.semiauto.deriveReader
16+
import zio.interop.catz._
17+
import zio.Task
18+
19+
final case class ServerConfiguration(listenAddress: String, listenPort: Int)
20+
21+
implicit val serverConfigurationReader: ConfigReader[ServerConfiguration] = deriveReader
22+
// serverConfigurationReader: ConfigReader[ServerConfiguration] = pureconfig.generic.DerivedConfigReader1$$anon$3@662e5590
23+
24+
val maybeConfiguration = PureConfigModule.make[Task, ServerConfiguration]
25+
// maybeConfiguration: Task[Either[cats.data.NonEmptyList[String], ServerConfiguration]] = zio.ZIO$EffectPartial@606f0f70
26+
```
27+

example/src/main/mdoc/index.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Scala Server Toolkit Documentation
2+
3+
* [Getting Started](#getting-started)
4+
* [Module PureConfig](pureconfig.md)
5+
6+
## Getting Started
7+
8+
Creating a simple HTTP server is as easy as this:
9+
10+
#### build.sbt
11+
12+
![GitHub tag (latest SemVer)](https://img.shields.io/github/v/tag/avast/scala-server-toolkit?label=version&sort=semver)
13+
14+
`libraryDependencies += "com.avast.server.toolkit" %% "scala-server-toolkit-pureconfig" % "<VERSION>"`

example/src/main/mdoc/pureconfig.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Module PureConfig
2+
3+
![GitHub tag (latest SemVer)](https://img.shields.io/github/v/tag/avast/scala-server-toolkit?label=version&sort=semver)
4+
5+
`libraryDependencies += "com.avast.server.toolkit" %% "scala-server-toolkit-pureconfig" % "<VERSION>"`
6+
7+
This module allows you to load your application's configuration file according to a case class provided to it. It uses
8+
[PureConfig](https://pureconfig.github.io) library to do so which uses [Lightbend Config](https://github.com/lightbend/config) which means
9+
that your application's configuration will be in [HOCON](https://github.com/lightbend/config/blob/master/HOCON.md) format.
10+
11+
Loading of configuration is side-effectful so it is wrapped in `F` which is `Sync`. This module also tweaks the error messages a little.
12+
13+
```scala mdoc
14+
import com.avast.server.toolkit.pureconfig._
15+
import pureconfig.ConfigReader
16+
import pureconfig.generic.semiauto.deriveReader
17+
import zio.interop.catz._
18+
import zio.Task
19+
20+
final case class ServerConfiguration(listenAddress: String, listenPort: Int)
21+
22+
implicit val serverConfigurationReader: ConfigReader[ServerConfiguration] = deriveReader
23+
24+
val maybeConfiguration = PureConfigModule.make[Task, ServerConfiguration]
25+
```

example/src/main/resources/reference.conf

Whitespace-only changes.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.avast.server.toolkit.example
2+
3+
import cats.effect.Resource
4+
import com.avast.server.toolkit.example.config.Configuration
5+
import com.avast.server.toolkit.pureconfig.PureConfigModule
6+
import zio.interop.catz._
7+
import zio.{Task, ZIO}
8+
9+
object Main extends CatsApp {
10+
11+
def program: Resource[Task, Unit] = {
12+
for {
13+
configuration <- Resource.liftF(PureConfigModule.makeOrRaise[Task, Configuration])
14+
} yield ()
15+
}
16+
17+
override def run(args: List[String]): ZIO[Environment, Nothing, Int] = {
18+
program
19+
.use(_ => Task.never)
20+
.fold(
21+
_ => 1,
22+
_ => 0
23+
)
24+
}
25+
26+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.avast.server.toolkit.example.config
2+
3+
import pureconfig.ConfigReader
4+
import pureconfig.generic.semiauto._
5+
6+
final case class Configuration()
7+
8+
object Configuration {
9+
10+
implicit val reader: ConfigReader[Configuration] = deriveReader
11+
12+
}

project/Dependencies.scala

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import sbt._
2+
3+
object Dependencies {
4+
5+
val catsEffect = "org.typelevel" %% "cats-effect" % "2.0.0"
6+
val kindProjector = "org.typelevel" %% "kind-projector" % "0.10.3"
7+
val pureConfig = "com.github.pureconfig" %% "pureconfig" % "0.12.0"
8+
val scalaTest = "org.scalatest" %% "scalatest" % "3.0.8" % Test
9+
val zio = "dev.zio" %% "zio" % "1.0.0-RC12-1"
10+
val zioInteropCats = "dev.zio" %% "zio-interop-cats" % "2.0.0.0-RC3"
11+
12+
}

project/ScalacOptions.scala

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,7 @@ object ScalacOptions {
1010
"-Xlint",
1111
"-Xfatal-warnings",
1212
"-Xcheckinit",
13-
"-Ywarn-dead-code",
1413
"-Ywarn-value-discard",
15-
"-Ypartial-unification",
1614
"-Ywarn-macros:after",
1715
"-Ybackend-parallelism",
1816
"4"

project/plugins.sbt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.0.5")
22
addSbtPlugin("org.scalameta" % "sbt-mdoc" % "1.3.2")
3-
addSbtPlugin("com.github.cb372" % "sbt-explicit-dependencies" % "0.2.10")
43
addSbtPlugin("com.geirsson" % "sbt-ci-release" % "1.3.2")
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package com.avast.server.toolkit.pureconfig
2+
3+
import cats.data.NonEmptyList
4+
import cats.effect.Sync
5+
import cats.syntax.either._
6+
import pureconfig.error.{ConfigReaderFailure, ConfigReaderFailures, ConvertFailure}
7+
import pureconfig.{ConfigReader, ConfigSource}
8+
9+
import scala.language.higherKinds
10+
import scala.reflect.ClassTag
11+
12+
/** Provides loading of configuration into case class via PureConfig. */
13+
object PureConfigModule {
14+
15+
/** Loads the case class `A` using Lightbend Config's standard behavior. */
16+
def make[F[_]: Sync, A: ConfigReader]: F[Either[NonEmptyList[String], A]] = make(ConfigSource.default)
17+
18+
/** Loads the case class `A` using provided [[pureconfig.ConfigSource]]. */
19+
def make[F[_]: Sync, A: ConfigReader](source: ConfigSource): F[Either[NonEmptyList[String], A]] = {
20+
Sync[F].delay(source.load[A].leftMap(convertFailures))
21+
}
22+
23+
/** Loads the case class `A` using Lightbend Config's standard behavior or raises an exception. */
24+
def makeOrRaise[F[_]: Sync, A: ConfigReader: ClassTag]: F[A] = makeOrRaise(ConfigSource.default)
25+
26+
/** Loads the case class `A` using provided [[pureconfig.ConfigSource]] or raises an exception. */
27+
def makeOrRaise[F[_]: Sync, A: ConfigReader: ClassTag](source: ConfigSource): F[A] = Sync[F].delay(source.loadOrThrow[A])
28+
29+
private def convertFailures(failures: ConfigReaderFailures): NonEmptyList[String] = {
30+
NonEmptyList(failures.head, failures.tail).map(formatFailure)
31+
}
32+
33+
private def formatFailure(configReaderFailure: ConfigReaderFailure): String = {
34+
configReaderFailure match {
35+
case convertFailure: ConvertFailure =>
36+
s"Invalid configuration ${convertFailure.path}: ${convertFailure.description}"
37+
case configFailure =>
38+
s"Invalid configuration : ${configFailure.description}"
39+
}
40+
}
41+
42+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.avast.server.toolkit.pureconfig
2+
3+
import cats.data.NonEmptyList
4+
import cats.effect.SyncIO
5+
import org.scalatest.FunSuite
6+
import pureconfig.error.ConfigReaderException
7+
import pureconfig.generic.semiauto.deriveReader
8+
import pureconfig.{ConfigReader, ConfigSource}
9+
10+
class PureConfigModuleTest extends FunSuite {
11+
12+
private val source = ConfigSource.string("""|number = 123
13+
|string = "test"""".stripMargin)
14+
15+
private case class TestConfig(number: Int, string: String)
16+
17+
implicit private val configReader: ConfigReader[TestConfig] = deriveReader
18+
19+
test("Simple configuration loading") {
20+
assert(PureConfigModule.make[SyncIO, TestConfig](source).unsafeRunSync() === Right(TestConfig(123, "test")))
21+
assert(
22+
PureConfigModule.make[SyncIO, TestConfig](ConfigSource.empty).unsafeRunSync() === Left(
23+
NonEmptyList("Invalid configuration : Key not found: 'number'.", List("Invalid configuration : Key not found: 'string'."))
24+
)
25+
)
26+
}
27+
28+
test("Configuration loading with exceptions") {
29+
assert(PureConfigModule.makeOrRaise[SyncIO, TestConfig](source).unsafeRunSync() === TestConfig(123, "test"))
30+
assertThrows[ConfigReaderException[TestConfig]] {
31+
PureConfigModule.makeOrRaise[SyncIO, TestConfig](ConfigSource.empty).unsafeRunSync()
32+
}
33+
}
34+
35+
}

0 commit comments

Comments
 (0)