Skip to content

Commit

Permalink
Merge pull request #119 from rcardin/88-add-tracing-capability
Browse files Browse the repository at this point in the history
Added tracing capability
  • Loading branch information
rcardin authored Dec 30, 2024
2 parents 48bc7d9 + e0b2034 commit f4388c6
Show file tree
Hide file tree
Showing 4 changed files with 144 additions and 7 deletions.
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,28 @@ val usdAmount: Double = Raise.recoverable {
The strategy lets us define how to recover from a typed error in a dedicated place, leaving the happy logic in the main
block.

### Tracing

When a logical error is raised, the library will not print any information about the error by default. Sometimes, it's useful for debugging purposes to have a trace of the error. The library allows you to define a strategy called `TraceWith` that processes the error together with a stack trace that identifies where the error was raised.

To enable tracing for a specific code block, wrap it inside the `trace` DSL and provide a `TraceWith` strategy:

```scala 3
import in.rcard.raise4s.Raise.{raise, traced}
import in.rcard.raise4s.Strategies.TraceWith

given TraceWith[String] = (trace: Traced) => {
trace.printStackTrace()
}
val lambda: Int raises String = traced {
raise("Oops!")
}
val actual: String | Int = Raise.run(lambda)
actual shouldBe "Oops!"
```

The `Traced` exception the tracing engine uses contains the original typed error.

## Contributing

If you want to contribute to the project, please do it! Any help is welcome.
Expand Down
42 changes: 41 additions & 1 deletion core/src/main/scala/in/rcard/raise4s/Raise.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package in.rcard.raise4s

import in.rcard.raise4s
import in.rcard.raise4s.Strategies.RecoverWith
import in.rcard.raise4s.Strategies.{RecoverWith, TraceWith, Traced, TracedRaise}

import scala.annotation.targetName
import scala.util.Try
Expand Down Expand Up @@ -1768,4 +1768,44 @@ object Raise {
*/
inline def withDefault[Error, A](default: A)(inline block: Raise[Error] ?=> A): A =
Raise.recover(block)(error => default)

/** Add tracing to a block of code that can raise a logical error. In detail, the logical error is
* wrapped inside a [[Traced]] exception and processed by the [[TraceWith]] strategy instance.
* Please, be aware that adding tracing to an error can have a performance impact since a fat
* exception with a full stack trace is created.
*
* <h2>Example</h2>
* {{{
* given TraceWith[String] = trace => {
* trace.printStackTrace()
* }
* val lambda: Int raises String = traced {
* raise("Oops!")
* }
* val actual: String | Int = Raise.run(lambda)
* actual shouldBe "Oops!"
* }}}
*
* @param block
* The block of code to execute that can raise an error
* @param tracing
* The strategy to process the traced error
* @tparam Error
* The type of the logical error that can be raised by the `block` lambda
* @tparam A
* The type of the result of the execution of `block` lambda
* @return
* The original block wrapped into a traced block
*/
inline def traced[Error, A](
inline block: Raise[Error] ?=> A
)(using inline tracing: TraceWith[Error]): Raise[Error] ?=> A = {
try {
given tracedRaise: Raise[Error] = new TracedRaise
block
} catch
case traced: Traced[Error] =>
tracing.trace(traced)
Raise.raise(traced.original)
}
}
49 changes: 45 additions & 4 deletions core/src/main/scala/in/rcard/raise4s/Strategies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,53 @@ object Strategies {
* actual should be(43)
* }}}
*
* @tparam Error The type of the error to recover from
* @tparam A The type of the value to return if the recovery is successful
*
* @see [[Raise.recoverable]]
* @tparam Error
* The type of the error to recover from
* @tparam A
* The type of the value to return if the recovery is successful
*
* @see
* [[Raise.recoverable]]
*/
trait RecoverWith[Error, A] {
def recover(error: Error): A
}

/** Implement the `raise` method to throw a [[Traced]] exception with the error to trace instead
* of a [[Raised]] exception.
*/
private[raise4s] class TracedRaise extends Raise[Any]:
def raise(e: Any): Nothing = throw Traced(e)

/** The exception that wraps the original error in case of tracing. The difference with the [[Raised]] exception is that
* the exception contains a full stack trace.
* @param original
* The original error to trace
* @tparam Error
* The type of the error to trace
*/
case class Traced[Error](original: Error) extends Exception

/** A strategy that allows to trace an error and return it. The [[trace]] method represent the
* behavior to trace the error. As a strategy, it should be used as a `given` instance. Use the
* type class instance with the [[Raise.traced]] DSL.
*
* <h2>Example</h2>
* {{{
* given TraceWith[String] = trace => {
* trace.printStackTrace()
* }
* val lambda: Int raises String = traced {
* raise("Oops!")
* }
* val actual: String | Int = Raise.run(lambda)
* actual shouldBe "Oops!"
* }}}
*
* @tparam Error
* The type of the error to trace
*/
trait TraceWith[Error] {
def trace(traced: Traced[Error]): Unit
}
}
38 changes: 36 additions & 2 deletions core/src/test/scala/in/rcard/raise4s/StrategiesSpec.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package in.rcard.raise4s

import in.rcard.raise4s.Raise.raise
import in.rcard.raise4s.Strategies.{MapError, anyRaised}
import in.rcard.raise4s.Raise.{raise, traced}
import in.rcard.raise4s.Strategies.{MapError, TraceWith, anyRaised}
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers

Expand Down Expand Up @@ -39,4 +39,38 @@ class StrategiesSpec extends AnyFlatSpec with Matchers {
val result: Int | String = Raise.run(finalLambda)
result shouldBe "Hello"
}

"TraceWith" should "allow defining a strategy that trace the error and return it" in {
val queue = collection.mutable.ListBuffer.empty[String]
given TraceWith[String] = trace => {
queue += trace.original
trace.printStackTrace()
}

val lambda: Int raises String = traced {
raise("Oops!")
}

val actual: String | Int = Raise.run(lambda)

actual shouldBe "Oops!"
queue should contain("Oops!")
}

it should "return the happy path value if no error is raised" in {
val queue = collection.mutable.ListBuffer.empty[String]
given TraceWith[String] = trace => {
queue += trace.original
trace.printStackTrace()
}

val lambda: Int raises String = traced {
42
}

val actual: Int | String = Raise.run(lambda)

actual shouldBe 42
queue shouldBe empty
}
}

0 comments on commit f4388c6

Please sign in to comment.