Skip to content

Commit 36f3760

Browse files
nb-ceffajakubjanecekPavel Kefurt
authored
Pureconfig Toggle typeclass (#982)
Co-authored-by: Jakub Janeček <[email protected]> Co-authored-by: Pavel Kefurt <[email protected]>
1 parent 1a63572 commit 36f3760

File tree

8 files changed

+459
-5
lines changed

8 files changed

+459
-5
lines changed

pureconfig/src/main/scala/com/avast/sst/pureconfig/PureConfigModule.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@ object PureConfigModule {
3232
private def formatFailure(configReaderFailure: ConfigReaderFailure): String = {
3333
configReaderFailure match {
3434
case convertFailure: ConvertFailure =>
35-
s"Invalid configuration ${convertFailure.path}: ${convertFailure.description}"
35+
s"Invalid configuration ${convertFailure.path} @ ${convertFailure.origin.map(_.description).iterator.mkString}: ${convertFailure.description}"
3636
case configFailure =>
37-
s"Invalid configuration : ${configFailure.description}"
37+
s"Invalid configuration @ ${configFailure.origin.map(_.description).iterator.mkString}: ${configFailure.description}"
3838
}
3939
}
4040

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.avast.sst.pureconfig
2+
3+
import com.typesafe.config.Config
4+
import pureconfig.ConfigReader
5+
6+
/** Used to retrieve both parsed configuration object and underlying [[Config]] instance. */
7+
final case class WithConfig[T](value: T, config: Config)
8+
9+
object WithConfig {
10+
implicit def configReader[T: ConfigReader]: ConfigReader[WithConfig[T]] =
11+
for {
12+
config <- ConfigReader[Config]
13+
value <- ConfigReader[T]
14+
} yield WithConfig(value, config)
15+
16+
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package com.avast.sst.pureconfig.util
2+
3+
import cats.{FlatMap, Functor, Monad, Monoid, Order, Semigroup}
4+
5+
import java.util.Collections
6+
import com.typesafe.config.ConfigValueFactory
7+
import pureconfig.{ConfigReader, ConfigWriter}
8+
9+
import scala.annotation.tailrec
10+
11+
sealed trait Toggle[+T] {
12+
def toOption: Option[T]
13+
def fold[A](empty: => A, fromValue: T => A): A
14+
def isEmpty: Boolean
15+
}
16+
17+
object Toggle {
18+
final case class Enabled[+T](value: T) extends Toggle[T] {
19+
override def toOption: Option[T] = Some(value)
20+
override def fold[A](empty: => A, fromValue: T => A): A = fromValue(value)
21+
override def isEmpty: Boolean = false
22+
def get: T = value
23+
}
24+
case object Disabled extends Toggle[Nothing] {
25+
override def toOption: Option[Nothing] = None
26+
override def fold[A](empty: => A, fromValue: Nothing => A): A = empty
27+
override def isEmpty: Boolean = true
28+
}
29+
30+
object TogglePureConfigInstances {
31+
implicit def toggleConfigReader[T: ConfigReader]: ConfigReader[Toggle[T]] = {
32+
ConfigReader
33+
.forProduct1[ConfigReader[Toggle[T]], Boolean]("enabled") { enabled =>
34+
if (enabled) implicitly[ConfigReader[T]].map(Enabled[T])
35+
else ConfigReader.fromCursor(_ => Right(Disabled))
36+
}
37+
.flatMap(identity)
38+
}
39+
40+
implicit def toggleConfigWriter[T: ConfigWriter]: ConfigWriter[Toggle[T]] = {
41+
ConfigWriter.fromFunction[Toggle[T]] {
42+
case Enabled(value) => ConfigWriter[T].to(value).withFallback(ConfigValueFactory.fromMap(Collections.singletonMap("enabled", true)))
43+
case Disabled => ConfigValueFactory.fromMap(Collections.singletonMap("enabled", false))
44+
}
45+
}
46+
}
47+
48+
object ToggleStdInstances {
49+
implicit val functorForToggle: Functor[Toggle] = new ToggleFunctor
50+
implicit val flatMapForToggle: FlatMap[Toggle] = new ToggleFlatMap
51+
implicit val monadForToggle: Monad[Toggle] = new ToggleMonad
52+
implicit def monoidForToggle[A: Semigroup]: Monoid[Toggle[A]] = new ToggleMonoid[A]
53+
implicit def orderForToggle[A: Order]: Order[Toggle[A]] = new ToggleOrder[A]
54+
}
55+
56+
class ToggleFunctor extends Functor[Toggle] {
57+
override def map[A, B](fa: Toggle[A])(f: A => B): Toggle[B] = {
58+
fa match {
59+
case Enabled(value) => Enabled(f(value))
60+
case Disabled => Disabled
61+
}
62+
}
63+
}
64+
65+
class ToggleFlatMap extends ToggleFunctor with FlatMap[Toggle] {
66+
override def flatMap[A, B](fa: Toggle[A])(f: A => Toggle[B]): Toggle[B] = {
67+
fa match {
68+
case Enabled(value) => f(value)
69+
case Disabled => Disabled
70+
}
71+
}
72+
73+
override def tailRecM[A, B](a: A)(f: A => Toggle[Either[A, B]]): Toggle[B] = tailRecMPrivate(a)(f)
74+
75+
@tailrec
76+
private def tailRecMPrivate[A, B](a: A)(f: A => Toggle[Either[A, B]]): Toggle[B] = {
77+
f(a) match {
78+
case Enabled(Left(value)) => tailRecMPrivate(value)(f)
79+
case Enabled(Right(value)) => Enabled(value)
80+
case Disabled => Disabled
81+
}
82+
}
83+
}
84+
85+
class ToggleMonad extends ToggleFlatMap with Monad[Toggle] {
86+
override def pure[A](x: A): Toggle[A] = Enabled(x)
87+
}
88+
89+
class ToggleMonoid[A](implicit A: Semigroup[A]) extends Monoid[Toggle[A]] {
90+
override def empty: Toggle[A] = Disabled
91+
override def combine(x: Toggle[A], y: Toggle[A]): Toggle[A] =
92+
x match {
93+
case Disabled => y
94+
case Enabled(a) =>
95+
y match {
96+
case Disabled => x
97+
case Enabled(b) => Enabled(A.combine(a, b))
98+
}
99+
}
100+
}
101+
102+
class ToggleOrder[A](implicit A: Order[A]) extends Order[Toggle[A]] {
103+
override def compare(x: Toggle[A], y: Toggle[A]): Int =
104+
x match {
105+
case Disabled => if (y.isEmpty) 0 else -1
106+
case Enabled(a) =>
107+
y match {
108+
case Disabled => 1
109+
case Enabled(b) => A.compare(a, b)
110+
}
111+
}
112+
}
113+
114+
}

pureconfig/src/test/scala-2/com/avast/sst/pureconfig/PureConfigModuleTest.scala

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ class PureConfigModuleTest extends AnyFunSuite {
1212
private val source = ConfigSource.string("""|number = 123
1313
|string = "test"""".stripMargin)
1414

15+
private val sourceWithMissingField = ConfigSource.string("number = 123")
16+
17+
private val sourceWithTypeError = ConfigSource.string("""|number = wrong_type
18+
|string = "test"""".stripMargin)
19+
1520
private case class TestConfig(number: Int, string: String)
1621

1722
implicit private val configReader: ConfigReader[TestConfig] = deriveReader[TestConfig]
@@ -20,7 +25,26 @@ class PureConfigModuleTest extends AnyFunSuite {
2025
assert(PureConfigModule.make[SyncIO, TestConfig](source).unsafeRunSync() === Right(TestConfig(123, "test")))
2126
assert(
2227
PureConfigModule.make[SyncIO, TestConfig](ConfigSource.empty).unsafeRunSync() === Left(
23-
NonEmptyList("Invalid configuration : Key not found: 'number'.", List("Invalid configuration : Key not found: 'string'."))
28+
NonEmptyList(
29+
"Invalid configuration @ empty config: Key not found: 'number'.",
30+
List("Invalid configuration @ empty config: Key not found: 'string'.")
31+
)
32+
)
33+
)
34+
assert(
35+
PureConfigModule.make[SyncIO, TestConfig](sourceWithMissingField).unsafeRunSync() === Left(
36+
NonEmptyList(
37+
"Invalid configuration @ String: 1: Key not found: 'string'.",
38+
List.empty
39+
)
40+
)
41+
)
42+
assert(
43+
PureConfigModule.make[SyncIO, TestConfig](sourceWithTypeError).unsafeRunSync() === Left(
44+
NonEmptyList(
45+
"Invalid configuration number @ String: 1: Expected type NUMBER. Found STRING instead.",
46+
List.empty
47+
)
2448
)
2549
)
2650
}
@@ -30,6 +54,12 @@ class PureConfigModuleTest extends AnyFunSuite {
3054
assertThrows[ConfigReaderException[TestConfig]] {
3155
PureConfigModule.makeOrRaise[SyncIO, TestConfig](ConfigSource.empty).unsafeRunSync()
3256
}
57+
assertThrows[ConfigReaderException[TestConfig]] {
58+
PureConfigModule.makeOrRaise[SyncIO, TestConfig](sourceWithMissingField).unsafeRunSync()
59+
}
60+
assertThrows[ConfigReaderException[TestConfig]] {
61+
PureConfigModule.makeOrRaise[SyncIO, TestConfig](sourceWithTypeError).unsafeRunSync()
62+
}
3363
}
3464

3565
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package com.avast.sst.pureconfig
2+
3+
import cats.{Applicative, Eq, FlatMap, Functor, Monad, Monoid}
4+
import com.avast.sst.pureconfig.util.Toggle
5+
import com.avast.sst.pureconfig.util.Toggle.{Disabled, Enabled}
6+
import org.scalatest.diagrams.Diagrams
7+
import org.scalatest.funsuite.AnyFunSuite
8+
9+
class ToggleTest extends AnyFunSuite with Diagrams {
10+
11+
test("has Functor instance and map method works correctly") {
12+
import com.avast.sst.pureconfig.util.Toggle.ToggleStdInstances._
13+
14+
val oldValue = "some value"
15+
val newValue = "new value"
16+
val toggle: Toggle[String] = Enabled(oldValue)
17+
val toggle2: Toggle[String] = Disabled
18+
val result = Functor[Toggle].map(toggle)(_ => newValue)
19+
20+
import cats.syntax.functor._
21+
val result2 = toggle.map(_ => newValue)
22+
23+
val result3 = toggle2.map(_ => newValue)
24+
25+
assert(result.fold(false, value => value === newValue))
26+
assert(result2.fold(false, value => value === newValue))
27+
assert(result3.fold(true, _ => false))
28+
}
29+
30+
test("has FlatMap instance and flatMap method works correctly") {
31+
import com.avast.sst.pureconfig.util.Toggle.ToggleStdInstances._
32+
33+
val oldValue = "some value"
34+
val newValue = "new value"
35+
val toggle: Toggle[String] = Enabled(oldValue)
36+
val toggle2: Toggle[String] = Disabled
37+
val result = FlatMap[Toggle].flatMap(toggle)(_ => Enabled(newValue))
38+
39+
import cats.syntax.flatMap._
40+
val result2 = toggle.flatMap(_ => Enabled(newValue))
41+
42+
val result3 = toggle2.flatMap(_ => Enabled(newValue))
43+
val result4 = toggle.flatMap(_ => Disabled)
44+
45+
assert(result.fold(false, value => value === newValue))
46+
assert(result2.fold(false, value => value === newValue))
47+
assert(result3 === Disabled)
48+
assert(result4 === Disabled)
49+
}
50+
51+
test("has Applicative and Monad instance and pure method works correctly") {
52+
import com.avast.sst.pureconfig.util.Toggle.ToggleStdInstances._
53+
val value = "some value"
54+
val result = Applicative[Toggle].pure(value)
55+
val result2 = Monad[Toggle].pure(value)
56+
57+
import cats.syntax.applicative._
58+
val result3 = value.pure[Toggle]
59+
60+
assert(result.fold(false, value => value === value))
61+
assert(result2.fold(false, value => value === value))
62+
assert(result3.fold(false, value => value === value))
63+
}
64+
65+
test("has Monoid instance and combine method works correctly") {
66+
import com.avast.sst.pureconfig.util.Toggle.ToggleStdInstances._
67+
68+
val result = Monoid[Toggle[String]].empty
69+
70+
val value1 = 1
71+
val value2 = 2
72+
val toggle1: Toggle[Int] = Enabled(value1)
73+
val toggle2: Toggle[Int] = Enabled(value2)
74+
val toggle3: Toggle[Int] = Disabled
75+
76+
import cats.syntax.monoid._
77+
val result2 = toggle1.combine(toggle2)
78+
val result3 = toggle1.combine(Disabled)
79+
val result4 = toggle3.combine(toggle2)
80+
val result5 = toggle3.combine(Disabled)
81+
82+
assert(result === Disabled)
83+
assert(result2.fold(false, value => value === 3))
84+
assert(result3.fold(false, value => value === 1))
85+
assert(result4.fold(false, value => value === 2))
86+
assert(result5 === Disabled)
87+
}
88+
89+
test("has Order instance and compare method works correctly") {
90+
import com.avast.sst.pureconfig.util.Toggle.ToggleStdInstances._
91+
92+
val value1 = 1
93+
val value2 = 2
94+
val toggle1: Toggle[Int] = Enabled(value1)
95+
val toggle2: Toggle[Int] = Enabled(value2)
96+
val toggle3: Toggle[Int] = Disabled
97+
98+
import cats.syntax.order._
99+
val result1 = toggle1.compare(toggle2)
100+
val result2 = toggle1.compare(toggle1)
101+
val result3 = toggle2.compare(toggle1)
102+
val result4 = toggle3.compare(toggle2)
103+
val result5 = toggle3.compare(Disabled)
104+
105+
assert(result1 < 0)
106+
assert(Eq[Int].eqv(result2, 0))
107+
assert(result3 > 0)
108+
assert(result4 < 0)
109+
assert(Eq[Int].eqv(result5, 0))
110+
}
111+
}

pureconfig/src/test/scala-3/com/avast/sst/pureconfig/PureConfigModuleTest.scala

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ class PureConfigModuleTest extends AnyFunSuite {
1212
private val source = ConfigSource.string("""|number = 123
1313
|string = "test"""".stripMargin)
1414

15+
private val sourceWithMissingField = ConfigSource.string("number = 123")
16+
17+
private val sourceWithTypeError = ConfigSource.string("""|number = wrong_type
18+
|string = "test"""".stripMargin)
19+
1520
private case class TestConfig(number: Int, string: String)
1621

1722
implicit private val configReader: ConfigReader[TestConfig] = ConfigReader.derived
@@ -21,8 +26,24 @@ class PureConfigModuleTest extends AnyFunSuite {
2126
assert(
2227
PureConfigModule.make[SyncIO, TestConfig](ConfigSource.empty).unsafeRunSync() === Left(
2328
NonEmptyList(
24-
"Invalid configuration number: Key not found: 'number'.",
25-
List("Invalid configuration string: Key not found: 'string'.")
29+
"Invalid configuration number @ : Key not found: 'number'.",
30+
List("Invalid configuration string @ : Key not found: 'string'.")
31+
)
32+
)
33+
)
34+
assert(
35+
PureConfigModule.make[SyncIO, TestConfig](sourceWithMissingField).unsafeRunSync() === Left(
36+
NonEmptyList(
37+
"Invalid configuration string @ : Key not found: 'string'.",
38+
List.empty
39+
)
40+
)
41+
)
42+
assert(
43+
PureConfigModule.make[SyncIO, TestConfig](sourceWithTypeError).unsafeRunSync() === Left(
44+
NonEmptyList(
45+
"Invalid configuration number @ String: 1: Expected type NUMBER. Found STRING instead.",
46+
List.empty
2647
)
2748
)
2849
)
@@ -33,6 +54,12 @@ class PureConfigModuleTest extends AnyFunSuite {
3354
assertThrows[ConfigReaderException[TestConfig]] {
3455
PureConfigModule.makeOrRaise[SyncIO, TestConfig](ConfigSource.empty).unsafeRunSync()
3556
}
57+
assertThrows[ConfigReaderException[TestConfig]] {
58+
PureConfigModule.makeOrRaise[SyncIO, TestConfig](sourceWithMissingField).unsafeRunSync()
59+
}
60+
assertThrows[ConfigReaderException[TestConfig]] {
61+
PureConfigModule.makeOrRaise[SyncIO, TestConfig](sourceWithTypeError).unsafeRunSync()
62+
}
3663
}
3764

3865
}

0 commit comments

Comments
 (0)