Skip to content

Commit

Permalink
Add Output lifts for options and lists
Browse files Browse the repository at this point in the history
Fixes #424
  • Loading branch information
pawelprazak committed Apr 9, 2024
1 parent 88ed3a9 commit 4d0a2f6
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 12 deletions.
24 changes: 12 additions & 12 deletions core/src/main/scala/besom/internal/Output.scala
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ trait OutputExtensionsFactory:
* @return
* an [[Output]] of the mapped [[Option]]
*/
def mapOption[B: Typeable](f: A => B | Output[B])(using ctx: Context): Output[Option[B]] =
def mapInner[B: Typeable](f: A => B | Output[B])(using ctx: Context): Output[Option[B]] =
output.flatMap {
case Some(a) =>
f(a) match
Expand All @@ -291,7 +291,7 @@ trait OutputExtensionsFactory:
* @return
* an [[Output]] of the flat-mapped [[Option]]
*/
def flatMapOption[B](f: A => Option[B] | Output[Option[B]])(using ctx: Context): Output[Option[B]] =
def flatMapInner[B](f: A => Option[B] | Output[Option[B]])(using ctx: Context): Output[Option[B]] =
output.flatMap {
case Some(a) =>
f(a) match
Expand All @@ -318,7 +318,7 @@ trait OutputExtensionsFactory:
* @return
* an [[Output]] of the `tail` of the [[List]] or an empty list if the list is empty
*/
def tail: Output[List[A]] = output.map {
def tailOrEmpty: Output[List[A]] = output.map {
case Nil => Nil
case list => list.tail
}
Expand All @@ -327,7 +327,7 @@ trait OutputExtensionsFactory:
* @return
* an [[Output]] of the `init` of the [[List]] or an empty list if the list is empty
*/
def init: Output[List[A]] = output.map {
def initOrEmpty: Output[List[A]] = output.map {
case Nil => Nil
case list => list.init
}
Expand All @@ -336,13 +336,13 @@ trait OutputExtensionsFactory:
* @return
* an [[Output]] of the mapped [[List]]
*/
def mapList[B](f: A => B): Output[List[B]] = output.map(_.map(f))
def mapInner[B](f: A => B): Output[List[B]] = output.map(_.map(f))

/** Calls [[List.flatMap]] on the underlying [[List]] with the given function
* @return
* an [[Output]] of the flat-mapped [[List]]
*/
def flatMapList[B](f: A => List[B]): Output[List[B]] = output.map(_.flatMap(f))
def flatMapInner[B](f: A => List[B]): Output[List[B]] = output.map(_.flatMap(f))
end OutputListOps

implicit final class OutputOptionListOps[A](output: Output[Option[List[A]]]):
Expand All @@ -363,7 +363,7 @@ trait OutputExtensionsFactory:
* @return
* an [[Output]] of the `tail` of the [[List]] or an empty list if the optional [[List]] is [[None]]
*/
def tail: Output[List[A]] = output.map {
def tailOrEmpty: Output[List[A]] = output.map {
case Some(list) => if list.isEmpty then List.empty else list.tail
case None => List.empty
}
Expand All @@ -372,30 +372,30 @@ trait OutputExtensionsFactory:
* @return
* an [[Output]] of the `init` of the [[List]] or an empty list if the optional [[List]] is [[None]]
*/
def init: Output[List[A]] = output.map {
def initOrEmpty: Output[List[A]] = output.map {
case Some(list) => if list.isEmpty then List.empty else list.init
case None => List.empty
}

/** Calls [[Option.map]] on the underlying optional [[List]] with the given function
/** Calls [[List.map]] on the underlying optional [[List]] with the given function
* @param f
* the function to apply to the value
* @return
* an [[Output]] of the mapped [[List]] or an empty list if the optional [[List]] is [[None]]
*/
def mapList[B](f: A => B): Output[List[B]] = output.map {
def mapInner[B](f: A => B): Output[List[B]] = output.map {
case Some(list) => list.map(f)
case None => List.empty
}

/** Calls [[Option.flatMap]] on the underlying optional [[List]] with the given function
/** Calls [[List.flatMap]] on the underlying optional [[List]] with the given function
*
* @param f
* the function to apply to the value
* @return
* an [[Output]] of the flat-mapped [[List]] or an empty list if the optional [[List]] is [[None]]
*/
def flatMapList[B](f: A => List[B]): Output[List[B]] = output.map {
def flatMapInner[B](f: A => List[B]): Output[List[B]] = output.map {
case Some(list) => list.flatMap(f)
case None => List.empty
}
Expand Down
92 changes: 92 additions & 0 deletions core/src/test/scala/besom/internal/OutputTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -237,12 +237,14 @@ class OutputTest extends munit.FunSuite:
optVal <- Vector(true, false)
outVal <- Vector(true, false)
do
// FIXME: the inference is not working without this
val c: Boolean | Option[Boolean] | Output[Boolean] | Output[Option[Boolean]] = (outCond, optCond) match
case (true, true) => Output(Option(cond))
case (true, false) => Output(cond)
case (false, true) => Some(cond)
case (false, false) => cond

// FIXME: the inference is not working without this
val v: String | Option[String] | Output[String] | Output[Option[String]] = (outVal, optVal) match
case (true, true) => Output(Option(value))
case (true, false) => Output(value)
Expand All @@ -257,4 +259,94 @@ class OutputTest extends munit.FunSuite:
Context().waitForAllTasks.unsafeRunSync()
}

test("getOrFail get value") {
import besom.OutputOptionOps
given Context = DummyContext().unsafeRunSync()

val result = Output(Some("value")).getOrFail(Exception("error"))
assertEquals(result.getData.unsafeRunSync(), OutputData("value"))
}

test("getOrFail throws") {
import besom.OutputOptionOps
given Context = DummyContext().unsafeRunSync()

val result = Output(None).getOrFail(Exception("error"))
interceptMessage[Exception]("error")(result.getData.unsafeRunSync())
}

Vector(
(Some("value"), "default", "value"),
(None, "default", "default")
).foreach { (value, default, expected) =>
import besom.OutputOptionOps
given Context = DummyContext().unsafeRunSync()

for outVal <- Vector(true, false)
do
val d = outVal match
case true => Output(default)
case false => default

test(s"getOrElse ${value} or ${default} (outVal: ${outVal})") {
val result = Output(value).getOrElse(d)
assertEquals(result.getData.unsafeRunSync(), OutputData(expected))
}
}

Vector(
(Some("value"), Some("default"), Some("value")),
(None, Some("default"), Some("default")),
(Some("value"), None, Some("value")),
(None, None, None)
).foreach { (value, default, expected) =>
import besom.OutputOptionOps
given Context = DummyContext().unsafeRunSync()

for outVal <- Vector(true, false)
do
val d = outVal match
case true => Output(default)
case false => default

test(s"orElse ${value} orElse ${default} (outVal: ${outVal})") {
val result = Output(value).orElse(d)
assertEquals(result.getData.unsafeRunSync(), OutputData(expected))
}
}

Vector(
(Some("value"), (v: String) => v.toUpperCase, Some("VALUE")),
(None, (v: String) => v.toUpperCase, None)
).foreach { (value, f, expected) =>
import besom.OutputOptionOps
given Context = DummyContext().unsafeRunSync()

for outVal <- Vector(true, false)
do
test(s"mapInner ${value} (outVal: ${outVal})") {
val result: Output[Option[String]] = // FIXME: the inference is not working without the explicit type
if outVal then Output(value).mapInner(f.andThen(Output(_)))
else Output(value).mapInner(f)
assertEquals(result.getData.unsafeRunSync(), OutputData(expected))
}
}

Vector(
(Some("value"), (v: String) => Some(v.toUpperCase), Some("VALUE")),
(None, (v: String) => Some(v.toUpperCase), None)
).foreach { (value, f, expected) =>
import besom.OutputOptionOps
given Context = DummyContext().unsafeRunSync()

for outVal <- Vector(true, false)
do
test(s"flatMapInner ${value} (outVal: ${outVal})") {
val result: Output[Option[String]] = // FIXME: the inference is not working without the explicit type
if outVal then Output(value).flatMapInner(f.andThen(Output(_)))
else Output(value).flatMapInner(f.andThen(Output(_)))
assertEquals(result.getData.unsafeRunSync(), OutputData(expected))
}
}

end OutputTest

0 comments on commit 4d0a2f6

Please sign in to comment.