Skip to content
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

Pureconfig Toggle #982

Merged
merged 15 commits into from
Oct 3, 2022
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)
jakubjanecek marked this conversation as resolved.
Show resolved Hide resolved

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