Repository: rcardin/raise4s (scala)

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

Pull request description:
Closes #98

Patch:
diff --git a/cats-raise4s/README.md b/cats-raise4s/README.md
index 4a4cb7f..5202983 100644
--- a/cats-raise4s/README.md
+++ b/cats-raise4s/README.md
@@ -60,6 +60,32 @@ In general, the integration lets you use the _Cats_ type classes with the _Raise
   actual shouldBe List(2, 3, 4, 5, 6)
   ```
 
+- Use the `values` extension function to accumulate errors into a `NonEmptyList[E]` from a `Iterable[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 3f27a84..c55aeb0 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
@@ -10,4 +10,33 @@ object CatsBind {
       case Valid(value)   => value
       case Invalid(error) => Raise.raise(error)
     }
+
+  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
+     */
+    inline def values: RaiseNel[Error] ?=> List[A] = CatsRaise.mapOrAccumulate(iterable)(identity)
 }


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 a0a4d01..ff6f331 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
@@ -1,8 +1,8 @@
 package in.rcard.raise4s.cats
 
 import cats.data.*
-import in.rcard.raise4s.Raise
-import in.rcard.raise4s.cats.CatsBind.value
+import in.rcard.raise4s.cats.CatsBind.{value, values}
+import in.rcard.raise4s.{Raise, raises}
 import org.scalatest.flatspec.AnyFlatSpec
 import org.scalatest.matchers.should.Matchers
 
@@ -43,4 +43,43 @@ class CatsBindSpec extends AnyFlatSpec with Matchers {
 
     actual should be(2)
   }
+
+  "values extension function" should "map all the element of the iterable" in {
+    val iterableWithInnerRaise: List[Int raises String] = List(1, 2, 3, 4, 5).map { value1 =>
+      value1 + 1
+    }
+
+    val iterableWithOuterRaise: List[Int] raises NonEmptyList[String] =
+      iterableWithInnerRaise.values
+
+    val actual = Raise.fold(
+      iterableWithOuterRaise,
+      error => fail(s"An error occurred: $error"),
+      identity
+    )
+
+    actual shouldBe List(2, 3, 4, 5, 6)
+  }
+
+  it should "accumulate all the errors" in {
+    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")
+  }
 }