Skip to content

Commit

Permalink
Pureconfig Toggle typeclass (#982)
Browse files Browse the repository at this point in the history
Co-authored-by: Jakub Janeček <[email protected]>
Co-authored-by: Pavel Kefurt <[email protected]>
  • Loading branch information
3 people authored Oct 3, 2022
1 parent 1a63572 commit 36f3760
Show file tree
Hide file tree
Showing 8 changed files with 459 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ object PureConfigModule {
private def formatFailure(configReaderFailure: ConfigReaderFailure): String = {
configReaderFailure match {
case convertFailure: ConvertFailure =>
s"Invalid configuration ${convertFailure.path}: ${convertFailure.description}"
s"Invalid configuration ${convertFailure.path} @ ${convertFailure.origin.map(_.description).iterator.mkString}: ${convertFailure.description}"
case configFailure =>
s"Invalid configuration : ${configFailure.description}"
s"Invalid configuration @ ${configFailure.origin.map(_.description).iterator.mkString}: ${configFailure.description}"
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.avast.sst.pureconfig

import com.typesafe.config.Config
import pureconfig.ConfigReader

/** Used to retrieve both parsed configuration object and underlying [[Config]] instance. */
final case class WithConfig[T](value: T, config: Config)

object WithConfig {
implicit def configReader[T: ConfigReader]: ConfigReader[WithConfig[T]] =
for {
config <- ConfigReader[Config]
value <- ConfigReader[T]
} yield WithConfig(value, config)

}
114 changes: 114 additions & 0 deletions pureconfig/src/main/scala/com/avast/sst/pureconfig/util/Toggle.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package com.avast.sst.pureconfig.util

import cats.{FlatMap, Functor, Monad, Monoid, Order, Semigroup}

import java.util.Collections
import com.typesafe.config.ConfigValueFactory
import pureconfig.{ConfigReader, ConfigWriter}

import scala.annotation.tailrec

sealed trait Toggle[+T] {
def toOption: Option[T]
def fold[A](empty: => A, fromValue: T => A): A
def isEmpty: Boolean
}

object Toggle {
final case class Enabled[+T](value: T) extends Toggle[T] {
override def toOption: Option[T] = Some(value)
override def fold[A](empty: => A, fromValue: T => A): A = fromValue(value)
override def isEmpty: Boolean = false
def get: T = value
}
case object Disabled extends Toggle[Nothing] {
override def toOption: Option[Nothing] = None
override def fold[A](empty: => A, fromValue: Nothing => A): A = empty
override def isEmpty: Boolean = true
}

object TogglePureConfigInstances {
implicit def toggleConfigReader[T: ConfigReader]: ConfigReader[Toggle[T]] = {
ConfigReader
.forProduct1[ConfigReader[Toggle[T]], Boolean]("enabled") { enabled =>
if (enabled) implicitly[ConfigReader[T]].map(Enabled[T])
else ConfigReader.fromCursor(_ => Right(Disabled))
}
.flatMap(identity)
}

implicit def toggleConfigWriter[T: ConfigWriter]: ConfigWriter[Toggle[T]] = {
ConfigWriter.fromFunction[Toggle[T]] {
case Enabled(value) => ConfigWriter[T].to(value).withFallback(ConfigValueFactory.fromMap(Collections.singletonMap("enabled", true)))
case Disabled => ConfigValueFactory.fromMap(Collections.singletonMap("enabled", false))
}
}
}

object ToggleStdInstances {
implicit val functorForToggle: Functor[Toggle] = new ToggleFunctor
implicit val flatMapForToggle: FlatMap[Toggle] = new ToggleFlatMap
implicit val monadForToggle: Monad[Toggle] = new ToggleMonad
implicit def monoidForToggle[A: Semigroup]: Monoid[Toggle[A]] = new ToggleMonoid[A]
implicit def orderForToggle[A: Order]: Order[Toggle[A]] = new ToggleOrder[A]
}

class ToggleFunctor extends Functor[Toggle] {
override def map[A, B](fa: Toggle[A])(f: A => B): Toggle[B] = {
fa match {
case Enabled(value) => Enabled(f(value))
case Disabled => Disabled
}
}
}

class ToggleFlatMap extends ToggleFunctor with FlatMap[Toggle] {
override def flatMap[A, B](fa: Toggle[A])(f: A => Toggle[B]): Toggle[B] = {
fa match {
case Enabled(value) => f(value)
case Disabled => Disabled
}
}

override def tailRecM[A, B](a: A)(f: A => Toggle[Either[A, B]]): Toggle[B] = tailRecMPrivate(a)(f)

@tailrec
private def tailRecMPrivate[A, B](a: A)(f: A => Toggle[Either[A, B]]): Toggle[B] = {
f(a) match {
case Enabled(Left(value)) => tailRecMPrivate(value)(f)
case Enabled(Right(value)) => Enabled(value)
case Disabled => Disabled
}
}
}

class ToggleMonad extends ToggleFlatMap with Monad[Toggle] {
override def pure[A](x: A): Toggle[A] = Enabled(x)
}

class ToggleMonoid[A](implicit A: Semigroup[A]) extends Monoid[Toggle[A]] {
override def empty: Toggle[A] = Disabled
override def combine(x: Toggle[A], y: Toggle[A]): Toggle[A] =
x match {
case Disabled => y
case Enabled(a) =>
y match {
case Disabled => x
case Enabled(b) => Enabled(A.combine(a, b))
}
}
}

class ToggleOrder[A](implicit A: Order[A]) extends Order[Toggle[A]] {
override def compare(x: Toggle[A], y: Toggle[A]): Int =
x match {
case Disabled => if (y.isEmpty) 0 else -1
case Enabled(a) =>
y match {
case Disabled => 1
case Enabled(b) => A.compare(a, b)
}
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ class PureConfigModuleTest extends AnyFunSuite {
private val source = ConfigSource.string("""|number = 123
|string = "test"""".stripMargin)

private val sourceWithMissingField = ConfigSource.string("number = 123")

private val sourceWithTypeError = ConfigSource.string("""|number = wrong_type
|string = "test"""".stripMargin)

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

implicit private val configReader: ConfigReader[TestConfig] = deriveReader[TestConfig]
Expand All @@ -20,7 +25,26 @@ class PureConfigModuleTest extends AnyFunSuite {
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'."))
NonEmptyList(
"Invalid configuration @ empty config: Key not found: 'number'.",
List("Invalid configuration @ empty config: Key not found: 'string'.")
)
)
)
assert(
PureConfigModule.make[SyncIO, TestConfig](sourceWithMissingField).unsafeRunSync() === Left(
NonEmptyList(
"Invalid configuration @ String: 1: Key not found: 'string'.",
List.empty
)
)
)
assert(
PureConfigModule.make[SyncIO, TestConfig](sourceWithTypeError).unsafeRunSync() === Left(
NonEmptyList(
"Invalid configuration number @ String: 1: Expected type NUMBER. Found STRING instead.",
List.empty
)
)
)
}
Expand All @@ -30,6 +54,12 @@ class PureConfigModuleTest extends AnyFunSuite {
assertThrows[ConfigReaderException[TestConfig]] {
PureConfigModule.makeOrRaise[SyncIO, TestConfig](ConfigSource.empty).unsafeRunSync()
}
assertThrows[ConfigReaderException[TestConfig]] {
PureConfigModule.makeOrRaise[SyncIO, TestConfig](sourceWithMissingField).unsafeRunSync()
}
assertThrows[ConfigReaderException[TestConfig]] {
PureConfigModule.makeOrRaise[SyncIO, TestConfig](sourceWithTypeError).unsafeRunSync()
}
}

}
111 changes: 111 additions & 0 deletions pureconfig/src/test/scala-2/com/avast/sst/pureconfig/ToggleTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package com.avast.sst.pureconfig

import cats.{Applicative, Eq, FlatMap, Functor, Monad, Monoid}
import com.avast.sst.pureconfig.util.Toggle
import com.avast.sst.pureconfig.util.Toggle.{Disabled, Enabled}
import org.scalatest.diagrams.Diagrams
import org.scalatest.funsuite.AnyFunSuite

class ToggleTest extends AnyFunSuite with Diagrams {

test("has Functor instance and map method works correctly") {
import com.avast.sst.pureconfig.util.Toggle.ToggleStdInstances._

val oldValue = "some value"
val newValue = "new value"
val toggle: Toggle[String] = Enabled(oldValue)
val toggle2: Toggle[String] = Disabled
val result = Functor[Toggle].map(toggle)(_ => newValue)

import cats.syntax.functor._
val result2 = toggle.map(_ => newValue)

val result3 = toggle2.map(_ => newValue)

assert(result.fold(false, value => value === newValue))
assert(result2.fold(false, value => value === newValue))
assert(result3.fold(true, _ => false))
}

test("has FlatMap instance and flatMap method works correctly") {
import com.avast.sst.pureconfig.util.Toggle.ToggleStdInstances._

val oldValue = "some value"
val newValue = "new value"
val toggle: Toggle[String] = Enabled(oldValue)
val toggle2: Toggle[String] = Disabled
val result = FlatMap[Toggle].flatMap(toggle)(_ => Enabled(newValue))

import cats.syntax.flatMap._
val result2 = toggle.flatMap(_ => Enabled(newValue))

val result3 = toggle2.flatMap(_ => Enabled(newValue))
val result4 = toggle.flatMap(_ => Disabled)

assert(result.fold(false, value => value === newValue))
assert(result2.fold(false, value => value === newValue))
assert(result3 === Disabled)
assert(result4 === Disabled)
}

test("has Applicative and Monad instance and pure method works correctly") {
import com.avast.sst.pureconfig.util.Toggle.ToggleStdInstances._
val value = "some value"
val result = Applicative[Toggle].pure(value)
val result2 = Monad[Toggle].pure(value)

import cats.syntax.applicative._
val result3 = value.pure[Toggle]

assert(result.fold(false, value => value === value))
assert(result2.fold(false, value => value === value))
assert(result3.fold(false, value => value === value))
}

test("has Monoid instance and combine method works correctly") {
import com.avast.sst.pureconfig.util.Toggle.ToggleStdInstances._

val result = Monoid[Toggle[String]].empty

val value1 = 1
val value2 = 2
val toggle1: Toggle[Int] = Enabled(value1)
val toggle2: Toggle[Int] = Enabled(value2)
val toggle3: Toggle[Int] = Disabled

import cats.syntax.monoid._
val result2 = toggle1.combine(toggle2)
val result3 = toggle1.combine(Disabled)
val result4 = toggle3.combine(toggle2)
val result5 = toggle3.combine(Disabled)

assert(result === Disabled)
assert(result2.fold(false, value => value === 3))
assert(result3.fold(false, value => value === 1))
assert(result4.fold(false, value => value === 2))
assert(result5 === Disabled)
}

test("has Order instance and compare method works correctly") {
import com.avast.sst.pureconfig.util.Toggle.ToggleStdInstances._

val value1 = 1
val value2 = 2
val toggle1: Toggle[Int] = Enabled(value1)
val toggle2: Toggle[Int] = Enabled(value2)
val toggle3: Toggle[Int] = Disabled

import cats.syntax.order._
val result1 = toggle1.compare(toggle2)
val result2 = toggle1.compare(toggle1)
val result3 = toggle2.compare(toggle1)
val result4 = toggle3.compare(toggle2)
val result5 = toggle3.compare(Disabled)

assert(result1 < 0)
assert(Eq[Int].eqv(result2, 0))
assert(result3 > 0)
assert(result4 < 0)
assert(Eq[Int].eqv(result5, 0))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ class PureConfigModuleTest extends AnyFunSuite {
private val source = ConfigSource.string("""|number = 123
|string = "test"""".stripMargin)

private val sourceWithMissingField = ConfigSource.string("number = 123")

private val sourceWithTypeError = ConfigSource.string("""|number = wrong_type
|string = "test"""".stripMargin)

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

implicit private val configReader: ConfigReader[TestConfig] = ConfigReader.derived
Expand All @@ -21,8 +26,24 @@ class PureConfigModuleTest extends AnyFunSuite {
assert(
PureConfigModule.make[SyncIO, TestConfig](ConfigSource.empty).unsafeRunSync() === Left(
NonEmptyList(
"Invalid configuration number: Key not found: 'number'.",
List("Invalid configuration string: Key not found: 'string'.")
"Invalid configuration number @ : Key not found: 'number'.",
List("Invalid configuration string @ : Key not found: 'string'.")
)
)
)
assert(
PureConfigModule.make[SyncIO, TestConfig](sourceWithMissingField).unsafeRunSync() === Left(
NonEmptyList(
"Invalid configuration string @ : Key not found: 'string'.",
List.empty
)
)
)
assert(
PureConfigModule.make[SyncIO, TestConfig](sourceWithTypeError).unsafeRunSync() === Left(
NonEmptyList(
"Invalid configuration number @ String: 1: Expected type NUMBER. Found STRING instead.",
List.empty
)
)
)
Expand All @@ -33,6 +54,12 @@ class PureConfigModuleTest extends AnyFunSuite {
assertThrows[ConfigReaderException[TestConfig]] {
PureConfigModule.makeOrRaise[SyncIO, TestConfig](ConfigSource.empty).unsafeRunSync()
}
assertThrows[ConfigReaderException[TestConfig]] {
PureConfigModule.makeOrRaise[SyncIO, TestConfig](sourceWithMissingField).unsafeRunSync()
}
assertThrows[ConfigReaderException[TestConfig]] {
PureConfigModule.makeOrRaise[SyncIO, TestConfig](sourceWithTypeError).unsafeRunSync()
}
}

}
Loading

0 comments on commit 36f3760

Please sign in to comment.