Skip to content

feat: Add module pureconfig with tests and basic documentation #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Sep 20, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .scalafmt.conf
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ continuationIndent.defnSite = 2
align.arrowEnumeratorGenerator = true
align.openParenCallSite = true
align.openParenDefnSite = true
rewrite.rules = [RedundantParens, RedundantBraces, SortImports, SortModifiers, PreferCurlyFors]
rewrite.rules = [RedundantParens, SortImports, SortModifiers, PreferCurlyFors]
includeNoParensInSelectChains = true
lineEndings = preserve
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
# Scala Server Toolkit

[![Build Status](https://travis-ci.org/avast/scala-server-toolkit.svg?branch=master)](https://travis-ci.org/avast/scala-server-toolkit)
![GitHub tag (latest SemVer)](https://img.shields.io/github/v/tag/avast/scala-server-toolkit?color=white&label=version&sort=semver)
[![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)

This project is a culmination of years of Scala development at Avast and tries to represent the best practices of Scala server development
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
together well and allow you to build reliable server applications.

## [Documentation](./docs/index.md)

Or you can [deep dive into example code](example/src/main/scala/com/avast/server/toolkit/example/Main.scala) if you like that more.

## Design

There are certain design decisions and constraints that are put in place to guide the development of the toolkit and recommended for
Expand Down
34 changes: 31 additions & 3 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,47 @@ ThisBuild / scmInfo := Some(
ScmInfo(url("https://github.com/avast/scala-server-toolkit"), "scm:git:[email protected]:avast/scala-server-toolkit.git")
)

ThisBuild / scalaVersion := "2.13.1"
ThisBuild / scalaVersion := "2.13.0"
ThisBuild / scalacOptions := ScalacOptions.default

ThisBuild / turbo := true

lazy val commonSettings = Seq(
libraryDependencies ++= Seq(
compilerPlugin("org.typelevel" %% "kind-projector" % "0.10.3")
compilerPlugin(Dependencies.kindProjector),
Dependencies.catsEffect,
Dependencies.scalaTest
),
Test / publishArtifact := false
)

lazy val root = (project in file("."))
.aggregate(example, pureconfig)
.settings(
commonSettings,
name := "scala-server-toolkit",
publish / skip := true
)

lazy val example = project
.dependsOn(pureconfig)
.enablePlugins(MdocPlugin)
.settings(
commonSettings,
name := "scala-server-toolkit-example",
publish / skip := true,
run / fork := true,
Global / cancelable := true,
mdocIn := baseDirectory.value / "src" / "main" / "mdoc",
mdocOut := baseDirectory.value / ".." / "docs",
libraryDependencies ++= Seq(
Dependencies.zio,
Dependencies.zioInteropCats
)
)

lazy val pureconfig = project
.settings(
commonSettings,
name := "scala-server-toolkit-pureconfig",
libraryDependencies += Dependencies.pureConfig
)
14 changes: 14 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Scala Server Toolkit Documentation

* [Getting Started](#getting-started)
* [Module PureConfig](pureconfig.md)

## Getting Started

Creating a simple HTTP server is as easy as this:

#### build.sbt

![GitHub tag (latest SemVer)](https://img.shields.io/github/v/tag/avast/scala-server-toolkit?label=version&sort=semver)

`libraryDependencies += "com.avast.server.toolkit" %% "scala-server-toolkit-pureconfig" % "<VERSION>"`
27 changes: 27 additions & 0 deletions docs/pureconfig.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Module PureConfig

![GitHub tag (latest SemVer)](https://img.shields.io/github/v/tag/avast/scala-server-toolkit?label=version&sort=semver)

`libraryDependencies += "com.avast.server.toolkit" %% "scala-server-toolkit-pureconfig" % "<VERSION>"`

This module allows you to load your application's configuration file according to a case class provided to it. It uses
[PureConfig](https://pureconfig.github.io) library to do so which uses [Lightbend Config](https://github.com/lightbend/config) which means
that your application's configuration will be in [HOCON](https://github.com/lightbend/config/blob/master/HOCON.md) format.


```scala
import com.avast.server.toolkit.pureconfig._
import pureconfig.ConfigReader
import pureconfig.generic.semiauto.deriveReader
import zio.interop.catz._
import zio.Task

final case class ServerConfiguration(listenAddress: String, listenPort: Int)

implicit val serverConfigurationReader: ConfigReader[ServerConfiguration] = deriveReader
// serverConfigurationReader: ConfigReader[ServerConfiguration] = pureconfig.generic.DerivedConfigReader1$$anon$3@662e5590

val maybeConfiguration = PureConfigModule.make[Task, ServerConfiguration]
// maybeConfiguration: Task[Either[cats.data.NonEmptyList[String], ServerConfiguration]] = zio.ZIO$EffectPartial@606f0f70
```

14 changes: 14 additions & 0 deletions example/src/main/mdoc/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Scala Server Toolkit Documentation

* [Getting Started](#getting-started)
* [Module PureConfig](pureconfig.md)

## Getting Started

Creating a simple HTTP server is as easy as this:

#### build.sbt

![GitHub tag (latest SemVer)](https://img.shields.io/github/v/tag/avast/scala-server-toolkit?label=version&sort=semver)

`libraryDependencies += "com.avast.server.toolkit" %% "scala-server-toolkit-pureconfig" % "<VERSION>"`
25 changes: 25 additions & 0 deletions example/src/main/mdoc/pureconfig.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Module PureConfig

![GitHub tag (latest SemVer)](https://img.shields.io/github/v/tag/avast/scala-server-toolkit?label=version&sort=semver)

`libraryDependencies += "com.avast.server.toolkit" %% "scala-server-toolkit-pureconfig" % "<VERSION>"`

This module allows you to load your application's configuration file according to a case class provided to it. It uses
[PureConfig](https://pureconfig.github.io) library to do so which uses [Lightbend Config](https://github.com/lightbend/config) which means
that your application's configuration will be in [HOCON](https://github.com/lightbend/config/blob/master/HOCON.md) format.

Loading of configuration is side-effectful so it is wrapped in `F` which is `Sync`. This module also tweaks the error messages a little.

```scala mdoc
import com.avast.server.toolkit.pureconfig._
import pureconfig.ConfigReader
import pureconfig.generic.semiauto.deriveReader
import zio.interop.catz._
import zio.Task

final case class ServerConfiguration(listenAddress: String, listenPort: Int)

implicit val serverConfigurationReader: ConfigReader[ServerConfiguration] = deriveReader

val maybeConfiguration = PureConfigModule.make[Task, ServerConfiguration]
```
Empty file.
26 changes: 26 additions & 0 deletions example/src/main/scala/com/avast/server/toolkit/example/Main.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.avast.server.toolkit.example

import cats.effect.Resource
import com.avast.server.toolkit.example.config.Configuration
import com.avast.server.toolkit.pureconfig.PureConfigModule
import zio.interop.catz._
import zio.{Task, ZIO}

object Main extends CatsApp {

def program: Resource[Task, Unit] = {
for {
configuration <- Resource.liftF(PureConfigModule.makeOrRaise[Task, Configuration])
} yield ()
}

override def run(args: List[String]): ZIO[Environment, Nothing, Int] = {
program
.use(_ => Task.never)
.fold(
_ => 1,
_ => 0
)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.avast.server.toolkit.example.config

import pureconfig.ConfigReader
import pureconfig.generic.semiauto._

final case class Configuration()

object Configuration {

implicit val reader: ConfigReader[Configuration] = deriveReader

}
12 changes: 12 additions & 0 deletions project/Dependencies.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import sbt._

object Dependencies {

val catsEffect = "org.typelevel" %% "cats-effect" % "2.0.0"
val kindProjector = "org.typelevel" %% "kind-projector" % "0.10.3"
val pureConfig = "com.github.pureconfig" %% "pureconfig" % "0.12.0"
val scalaTest = "org.scalatest" %% "scalatest" % "3.0.8" % Test
val zio = "dev.zio" %% "zio" % "1.0.0-RC12-1"
val zioInteropCats = "dev.zio" %% "zio-interop-cats" % "2.0.0.0-RC3"

}
2 changes: 0 additions & 2 deletions project/ScalacOptions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ object ScalacOptions {
"-Xlint",
"-Xfatal-warnings",
"-Xcheckinit",
"-Ywarn-dead-code",
"-Ywarn-value-discard",
"-Ypartial-unification",
"-Ywarn-macros:after",
"-Ybackend-parallelism",
"4"
Expand Down
1 change: 0 additions & 1 deletion project/plugins.sbt
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.0.5")
addSbtPlugin("org.scalameta" % "sbt-mdoc" % "1.3.2")
addSbtPlugin("com.github.cb372" % "sbt-explicit-dependencies" % "0.2.10")
addSbtPlugin("com.geirsson" % "sbt-ci-release" % "1.3.2")
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.avast.server.toolkit.pureconfig

import cats.data.NonEmptyList
import cats.effect.Sync
import cats.syntax.either._
import pureconfig.error.{ConfigReaderFailure, ConfigReaderFailures, ConvertFailure}
import pureconfig.{ConfigReader, ConfigSource}

import scala.language.higherKinds
import scala.reflect.ClassTag

/** Provides loading of configuration into case class via PureConfig. */
object PureConfigModule {

/** Loads the case class `A` using Lightbend Config's standard behavior. */
def make[F[_]: Sync, A: ConfigReader]: F[Either[NonEmptyList[String], A]] = make(ConfigSource.default)

/** Loads the case class `A` using provided [[pureconfig.ConfigSource]]. */
def make[F[_]: Sync, A: ConfigReader](source: ConfigSource): F[Either[NonEmptyList[String], A]] = {
Sync[F].delay(source.load[A].leftMap(convertFailures))
}

/** Loads the case class `A` using Lightbend Config's standard behavior or raises an exception. */
def makeOrRaise[F[_]: Sync, A: ConfigReader: ClassTag]: F[A] = makeOrRaise(ConfigSource.default)

/** Loads the case class `A` using provided [[pureconfig.ConfigSource]] or raises an exception. */
def makeOrRaise[F[_]: Sync, A: ConfigReader: ClassTag](source: ConfigSource): F[A] = Sync[F].delay(source.loadOrThrow[A])

private def convertFailures(failures: ConfigReaderFailures): NonEmptyList[String] = {
NonEmptyList(failures.head, failures.tail).map(formatFailure)
}

private def formatFailure(configReaderFailure: ConfigReaderFailure): String = {
configReaderFailure match {
case convertFailure: ConvertFailure =>
s"Invalid configuration ${convertFailure.path}: ${convertFailure.description}"
case configFailure =>
s"Invalid configuration : ${configFailure.description}"
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.avast.server.toolkit.pureconfig

import cats.data.NonEmptyList
import cats.effect.SyncIO
import org.scalatest.FunSuite
import pureconfig.error.ConfigReaderException
import pureconfig.generic.semiauto.deriveReader
import pureconfig.{ConfigReader, ConfigSource}

class PureConfigModuleTest extends FunSuite {

private val source = ConfigSource.string("""|number = 123
|string = "test"""".stripMargin)

private case class TestConfig(number: Int, string: String)

implicit private val configReader: ConfigReader[TestConfig] = deriveReader

test("Simple configuration loading") {
assert(PureConfigModule.make[SyncIO, TestConfig](source).unsafeRunSync() === Right(TestConfig(123, "test")))
assert(
PureConfigModule.make[SyncIO, TestConfig](ConfigSource.empty).unsafeRunSync() === Left(
NonEmptyList("Invalid configuration : Key not found: 'number'.", List("Invalid configuration : Key not found: 'string'."))
)
)
}

test("Configuration loading with exceptions") {
assert(PureConfigModule.makeOrRaise[SyncIO, TestConfig](source).unsafeRunSync() === TestConfig(123, "test"))
assertThrows[ConfigReaderException[TestConfig]] {
PureConfigModule.makeOrRaise[SyncIO, TestConfig](ConfigSource.empty).unsafeRunSync()
}
}

}