Repository: rcardin/raise4s (scala)

Issue:
Add a `values` extension function for `NonEmptyList[Raise[Error] ?=> A]` type
Add a `values` extension function for `NonEmptyList[Raise[Error] ?=> A]` type. The function must behave like the following:

```scala 3
val intermediateResults: NonEmptyList[Raise[String] ?=> Int] = NonEmptyList(1, 2, 3, 4).map(i => raise(i.toString))
val results: RaiseNel[String] ?=> NonEmptyList[Int] = intermediateResults.values
```

Pull request description:
Closes #100

Patch:
diff --git a/cats-raise4s/README.md b/cats-raise4s/README.md
index 5202983..765e55c 100644
--- a/cats-raise4s/README.md
+++ b/cats-raise4s/README.md
@@ -60,6 +60,25 @@ In general, the integration lets you use the _Cats_ type classes with the _Raise
   actual shouldBe List(2, 3, 4, 5, 6)
   ```
 
+- Use of the `NonEmptyList` data class to handle both the results and the errors in the `mapOrAccumulate` function.
+
+  ```scala 3
+  val block: NonEmptyList[Int] raises NonEmptyList[String] =
+    CatsRaise.mapOrAccumulate(NonEmptyList.of(1, 2, 3, 4, 5)) { value =>
+      if (value % 2 == 0) {
+        Raise.raise(value.toString)
+      } else {
+        value
+      }
+    }
+  val actual = Raise.fold(
+    block,
+    identity,
+    identity
+  )
+  actual shouldBe NonEmptyList.of("2", "4")
+  ```
+
 - Use the `values` extension function to accumulate errors into a `NonEmptyList[E]` from a `Iterable[A raises E]` block.
 
   ```scala 3
@@ -86,6 +105,29 @@ In general, the integration lets you use the _Cats_ type classes with the _Raise
   actual shouldBe NonEmptyList.of("2", "4")
   ```
 
+- Use the `values` extension function to accumulate errors into a `NonEmptyList[E]` from a `NonEmptyList[A raises E]` block.
+
+  ```scala 3
+  import in.rcard.raise4s.cats.CatsBind.values
+  
+  val iterableWithInnerRaise: List[Int raises String] =
+    List(1, 2, 3, 4, 5).map { value =>
+      if (value % 2 == 0) {
+        Raise.raise(value.toString)
+      } else {
+        value
+      }
+    }
+  val iterableWithOuterRaise: List[Int] raises NonEmptyList[String] =
+    iterableWithInnerRaise.values
+  val actual = Raise.fold(
+    iterableWithOuterRaise,
+    identity,
+    identity
+  )
+  actual shouldBe NonEmptyList.of("2", "4")
+  ```
+
 - Use the `Semigroup` type class to combine errors in the `zipOrAccumlateS` set of functions.
 
   ```scala 3
diff --git a/cats-raise4s/src/main/scala/in/rcard/raise4s/cats/CatsBind.scala b/cats-raise4s/src/main/scala/in/rcard/raise4s/cats/CatsBind.scala
index c55aeb0..f82b3e2 100644
--- a/cats-raise4s/src/main/scala/in/rcard/raise4s/cats/CatsBind.scala
+++ b/cats-raise4s/src/main/scala/in/rcard/raise4s/cats/CatsBind.scala
@@ -1,7 +1,7 @@
 package in.rcard.raise4s.cats
 
-import cats.data.Validated
 import cats.data.Validated.{Invalid, Valid}
+import cats.data.{NonEmptyList, Validated}
 import in.rcard.raise4s.Raise
 
 object CatsBind {
@@ -12,31 +12,64 @@ object CatsBind {
     }
 
   extension [Error, A](iterable: Iterable[Raise[Error] ?=> A])
-    /**
-     * Accumulates the errors of executions of the elements of the iterable and returns a list of
-     * the values or a non-empty list of the accumulated errors.
-     *
-     * <h2>Example</h2>
-     * {{{
-     * val iterableWithInnerRaise: List[Int raises String] =
-     *   List(1, 2, 3, 4, 5).map { value =>
-     *     if (value % 2 == 0) {
-     *       Raise.raise(value.toString)
-     *     } else {
-     *       value
-     *     }
-     *   }
-     * val iterableWithOuterRaise: List[Int] raises NonEmptyList[String] =
-     *   iterableWithInnerRaise.values
-     * val actual = Raise.fold(
-     *   iterableWithOuterRaise,
-     *   identity,
-     *   identity
-     * )
-     * actual shouldBe NonEmptyList.of("2", "4")
-     * }}}
-     * 
-     * @return A list of the values or a non-empty list of the accumulated errors
-     */
+    /** Accumulates the errors of executions of the elements of the iterable and returns a list of
+      * the values or a non-empty list of the accumulated errors.
+      *
+      * <h2>Example</h2>
+      * {{{
+      * val iterableWithInnerRaise: List[Int raises String] =
+      *   List(1, 2, 3, 4, 5).map { value =>
+      *     if (value % 2 == 0) {
+      *       Raise.raise(value.toString)
+      *     } else {
+      *       value
+      *     }
+      *   }
+      * val iterableWithOuterRaise: List[Int] raises NonEmptyList[String] =
+      *   iterableWithInnerRaise.values
+      * val actual = Raise.fold(
+      *   iterableWithOuterRaise,
+      *   identity,
+      *   identity
+      * )
+      * actual shouldBe NonEmptyList.of("2", "4")
+      * }}}
+      *
+      * @return
+      *   A list of the values or a non-empty list of the accumulated errors
+      */
     inline def values: RaiseNel[Error] ?=> List[A] = CatsRaise.mapOrAccumulate(iterable)(identity)
+
+  extension [Error, A](nonEmptyList: NonEmptyList[Raise[Error] ?=> A])
+    /** Accumulates the errors of executions of the elements of the non-empty list and returns a
+      * non-empty list of the values or a non-empty list of the accumulated errors.
+      *
+      * <h2>Example</h2>
+      * {{{
+      * val nonEmptyListWithInnerRaise: NonEmptyList[Int raises String] =
+      *   NonEmptyList.of(1, 2, 3, 4, 5).map { value =>
+      *     if (value % 2 == 0) {
+      *       Raise.raise(value.toString)
+      *     } else {
+      *       value
+      *     }
+      *   }
+      *
+      * val nonEmptyWithOuterRaise: NonEmptyList[Int] raises NonEmptyList[String] =
+      *   nonEmptyListWithInnerRaise.values
+      *
+      * val actual = Raise.fold(
+      *   nonEmptyWithOuterRaise,
+      *   identity,
+      *   identity
+      * )
+      *
+      * actual shouldBe NonEmptyList.of("2", "4")
+      * }}}
+      *
+      * @return
+      *   A non-empty list of the values or a non-empty list of the accumulated errors
+      */
+    inline def values: RaiseNel[Error] ?=> NonEmptyList[A] =
+      CatsRaise.mapOrAccumulate(nonEmptyList)(identity)
 }
diff --git a/cats-raise4s/src/main/scala/in/rcard/raise4s/cats/CatsRaise.scala b/cats-raise4s/src/main/scala/in/rcard/raise4s/cats/CatsRaise.scala
index 46ba947..ca8a7fc 100644
--- a/cats-raise4s/src/main/scala/in/rcard/raise4s/cats/CatsRaise.scala
+++ b/cats-raise4s/src/main/scala/in/rcard/raise4s/cats/CatsRaise.scala
@@ -99,6 +99,49 @@ object CatsRaise {
     )
     NonEmptyList.fromList(errors.toList).fold(results.toList)(r.raise)
 
+  /** Accumulate the errors obtained by executing the `transform` over every element of `iterable`.
+    * The error channel uses a [[NonEmptyList]] instance.
+    *
+    * <h2>Example</h2>
+    * {{{
+    * val block: NonEmptyList[Int] raises NonEmptyList[String] =
+    *   CatsRaise.mapOrAccumulate(NonEmptyList.of(1, 2, 3, 4, 5)) { value =>
+    *     if (value % 2 == 0) {
+    *       Raise.raise(value.toString)
+    *     } else {
+    *       value
+    *     }
+    *   }
+    * val actual = Raise.fold(
+    *   block,
+    *   identity,
+    *   identity
+    * )
+    * actual shouldBe NonEmptyList.of("2", "4")
+    * }}}
+    *
+    * @param nonEmptyList
+    *   The non-empty list of elements to transform
+    * @param transform
+    *   The transformation to apply to each element that can raise an error of type `Error`
+    * @param r
+    *   The Raise context
+    * @tparam Error
+    *   The type of the logical error that can be raised
+    * @tparam A
+    *   The type of the elements in the `iterable`
+    * @tparam B
+    *   The type of the transformed elements
+    * @return
+    *   A non-empty list of transformed elements
+    */
+  inline def mapOrAccumulate[Error, A, B](nonEmptyList: NonEmptyList[A])(
+      inline transform: Raise[Error] ?=> A => B
+  )(using r: RaiseNel[Error]): NonEmptyList[B] =
+    val resultAsList = CatsRaise.mapOrAccumulate(nonEmptyList.toList)(transform)
+    // We know it's safe to call get here because we started from an non-empty list
+    NonEmptyList.fromList(resultAsList).get
+
   /** Accumulate the errors from running `action1`, `action2`, `action3`, `action4`, `action5`,
     * `action6`, `action7`, `action8`, and `action9`, or accumulate all the occurred errors using
     * the [[Semigroup]] type class defined on the `Error` type. The tailing `S` in the name of the


Test patch:
diff --git a/cats-raise4s/src/test/scala/in/rcard/raise4s/cats/CatsBindSpec.scala b/cats-raise4s/src/test/scala/in/rcard/raise4s/cats/CatsBindSpec.scala
index ff6f331..c62e69f 100644
--- a/cats-raise4s/src/test/scala/in/rcard/raise4s/cats/CatsBindSpec.scala
+++ b/cats-raise4s/src/test/scala/in/rcard/raise4s/cats/CatsBindSpec.scala
@@ -82,4 +82,43 @@ class CatsBindSpec extends AnyFlatSpec with Matchers {
 
     actual shouldBe NonEmptyList.of("2", "4")
   }
+
+  "values extension function on non-empty list" should "map all the element of the non-empty list" in {
+    val nonEmptyListWithInnerRaise: NonEmptyList[Int raises String] = NonEmptyList.of(1, 2, 3, 4, 5).map { value1 =>
+      value1 + 1
+    }
+
+    val nonEmptyWithOuterRaise: NonEmptyList[Int] raises NonEmptyList[String] =
+      nonEmptyListWithInnerRaise.values
+
+    val actual = Raise.fold(
+      nonEmptyWithOuterRaise,
+      error => fail(s"An error occurred: $error"),
+      identity
+    )
+
+    actual shouldBe NonEmptyList.of(2, 3, 4, 5, 6)
+  }
+
+  it should "accumulate all the errors" in {
+    val nonEmptyListWithInnerRaise: NonEmptyList[Int raises String] =
+      NonEmptyList.of(1, 2, 3, 4, 5).map { value =>
+        if (value % 2 == 0) {
+          Raise.raise(value.toString)
+        } else {
+          value
+        }
+      }
+
+    val nonEmptyWithOuterRaise: NonEmptyList[Int] raises NonEmptyList[String] =
+      nonEmptyListWithInnerRaise.values
+
+    val actual = Raise.fold(
+      nonEmptyWithOuterRaise,
+      identity,
+      identity
+    )
+
+    actual shouldBe NonEmptyList.of("2", "4")
+  }
 }
diff --git a/cats-raise4s/src/test/scala/in/rcard/raise4s/cats/CatsRaiseSpec.scala b/cats-raise4s/src/test/scala/in/rcard/raise4s/cats/CatsRaiseSpec.scala
index 7439bc1..6065d56 100644
--- a/cats-raise4s/src/test/scala/in/rcard/raise4s/cats/CatsRaiseSpec.scala
+++ b/cats-raise4s/src/test/scala/in/rcard/raise4s/cats/CatsRaiseSpec.scala
@@ -3,7 +3,7 @@ package in.rcard.raise4s.cats
 import cats.Semigroup
 import cats.data.{NonEmptyChain, NonEmptyList, Validated}
 import in.rcard.raise4s.RaiseAnyPredef.raise
-import in.rcard.raise4s.{Raise, raises}
+import in.rcard.raise4s.{Raise, raises, Raise as value}
 import org.scalatest.flatspec.AnyFlatSpec
 import org.scalatest.matchers.should.Matchers
 
@@ -83,6 +83,40 @@ class CatsRaiseSpec extends AnyFlatSpec with Matchers {
     actual shouldBe NonEmptyList.of("2", "4")
   }
 
+  "mapOrAccumulate on a NonEmptyList to NonEmptyList" should "map all the element of the iterable" in {
+    val block: NonEmptyList[Int] raises NonEmptyList[String] =
+      CatsRaise.mapOrAccumulate(NonEmptyList.of(1, 2, 3, 4, 5)) { value1 =>
+        value1 + 1
+      }
+
+    val actual = Raise.fold(
+      block,
+      error => fail(s"An error occurred: $error"),
+      identity
+    )
+
+    actual shouldBe NonEmptyList.of(2, 3, 4, 5, 6)
+  }
+
+  it should "accumulate all the errors" in {
+    val block: NonEmptyList[Int] raises NonEmptyList[String] =
+      CatsRaise.mapOrAccumulate(NonEmptyList.of(1, 2, 3, 4, 5)) { value =>
+        if (value % 2 == 0) {
+          Raise.raise(value.toString)
+        } else {
+          value
+        }
+      }
+
+    val actual = Raise.fold(
+      block,
+      identity,
+      identity
+    )
+
+    actual shouldBe NonEmptyList.of("2", "4")
+  }
+
   "zipOrAccumulateS with combine" should "zip 9 elements" in {
     val block: List[Int] raises MyError2 =
       CatsRaise.zipOrAccumulateS({ 1 }, { 2 }, { 3 }, { 4 }, { 5 }, { 6 }, { 7 }, { 8 }, { 9 }) {