All Downloads are FREE. Search and download functionalities are using the official Maven repository.

harness.core.HError.scala Maven / Gradle / Ivy

package harness.core

import cats.data.NonEmptyList
import cats.syntax.option.*
import cats.syntax.traverse.*
import harness.core.RunMode.{Dev, Prod}
import scala.annotation.targetName

sealed abstract class HError(
    final val userMessage: HError.UserMessage,
    final val internalMessage: String,
    final val causes: List[Throwable],
) extends Throwable {

  // TODO (KR) : Tweak these (`baseUserMessage`, `userMessage` = s"${baseUserMessage}\n${causes.flatMap(_.userMessage.toOption}" ...
  //           : and the same for internalMessage
  //           : turn current `fullInternalMessage` into a sort of `verboseInternalMessage`

  final lazy val fullInternalMessage: String =
    HError.throwableMessage(this, false)

  final lazy val fullInternalMessageWithTrace: String =
    HError.throwableMessage(this, true)

  override final def getMessage: String = userMessage.show(HError.UserMessage.IfHidden.default)

  override final def toString: String = fullInternalMessage

  final def toNel: NonEmptyList[HError.Single] = HError.unwrap(this)

  final def formatMessage(runMode: RunMode, ifHidden: HError.UserMessage.IfHidden): String =
    runMode match {
      case Prod => userMessage.show(ifHidden)
      case Dev  => fullInternalMessage // TODO (KR) :
    }

}
object HError {

  def fromThrowable(throwable: Throwable): HError =
    throwable match {
      case hError: HError => hError
      case _              => HError.Wrapped(throwable)
    }

  def unwrap(err: HError): NonEmptyList[HError.Single] =
    err match {
      case HError.Multiple(children) => children
      case err: HError.Single        => NonEmptyList.one(err)
    }

  def apply(err: NonEmptyList[HError]): HError =
    err match {
      case NonEmptyList(err, Nil) => err
      case _                      => HError.Multiple(err.flatMap(HError.unwrap))
    }
  inline def apply(err0: HError, errN: HError*): HError =
    HError(NonEmptyList(err0, errN.toList))

  // =====|  |=====

  trait UserMessage {

    def show(ifHidden: UserMessage.IfHidden): String

    final def toOption: Option[String] =
      this match {
        case UserMessage.Const(msg) => msg.some
        case _                      => None
      }

  }
  object UserMessage {

    final case class Const(msg: String) extends UserMessage {
      override def show(ifHidden: IfHidden): String = msg
    }
    val hidden: UserMessage = _.hiddenMsg

    def fromOption(msg: Option[String]): UserMessage =
      msg match {
        case Some(msg) => UserMessage.Const(msg)
        case None      => UserMessage.hidden
      }

    final case class IfHidden(hiddenMsg: String)
    object IfHidden {
      val default: IfHidden = IfHidden("There was an internal system error")
    }

  }

  // =====| Direct Children of HError |=====

  abstract class Single(
      userMessage: HError.UserMessage,
      internalMessage: String,
      causes: List[Throwable],
  ) extends HError(userMessage, internalMessage, causes)

  final case class Multiple private[HError] (children: NonEmptyList[HError.Single])
      extends HError(
        ifHidden => children.toList.map(e => e.userMessage.show(ifHidden).split("\n").mkString("- ", "  ", "")).mkString("\n"),
        children.toList.map(e => e.internalMessage.split("\n").mkString("- ", "  ", "")).mkString("\n"),
        children.toList.flatMap(_.causes),
      )

  // =====| HError.Single Descendants |=====

  /**
    * Only purpose is to lift a non-HError Throwable into an HError.
    * If another HError uses this as a cause, the wrapping will be removed.
    */
  final case class Wrapped private[HError] (wrapped: Throwable)
      extends HError.Single(
        HError.UserMessage.hidden,
        Option(wrapped.getMessage).getOrElse(wrapped.toString), // TODO (KR) :
        wrapped :: Nil,
      )

  /**
    * Used when the user of the program provides invalid input.
    */
  final class UserError private (userMessage: String, internalMessage: String, causes: List[Throwable])
      extends HError.Single(
        HError.UserMessage.Const(userMessage),
        internalMessage,
        causes,
      )
  object UserError extends ErrorBuilder3[UserError](new UserError(_, _, _))

  /**
    * Used when there is an error with the system.
    * - IO
    * - Database
    * - Environment
    */
  final class SystemFailure private (internalMessage: String, causes: List[Throwable])
      extends HError.Single(
        HError.UserMessage.hidden,
        internalMessage,
        causes,
      )
  object SystemFailure extends ErrorBuilder2[SystemFailure](new SystemFailure(_, _))

  /**
    * Used when you want to start the error message with "WTF, why is this happening"
    * - Map#get(_).thisShouldAlwaysBeThere
    * - List#toNel.thisShouldAlwaysBeNonEmpty
    */
  final class InternalDefect private (internalMessage: String, causes: List[Throwable])
      extends HError.Single(
        HError.UserMessage.hidden,
        s"Internal Defect - unexpected failure:\n$internalMessage",
        causes,
      )
  object InternalDefect extends ErrorBuilder2[InternalDefect](new InternalDefect(_, _))

  /**
    * Analogous to the scala predef '???', except instead of throwing, it gives you a concrete error type.
    */
  final class ??? private (functionality: String)
      extends HError.Single(
        HError.UserMessage.Const(s"This feature is not yet supported: '$functionality'"),
        s"Encountered an unimplemented block of code: '$functionality'",
        Nil,
      )
  object ??? {
    def apply(functionality: String): ??? = new ???(functionality)
  }

  // =====| Helpers |=====

  private def throwableMessage(throwable: Throwable, stackTrace: Boolean): String = {
    val parts: (String, List[List[(String, String)]]) =
      throwable match {
        case hError: HError =>
          (
            s"HError[${hError.getClass.getSimpleName}]",
            List(
              hError.userMessage match {
                case UserMessage.Const(msg) if msg == hError.internalMessage => ("Message", msg) :: Nil
                case UserMessage.Const(msg)                                  => ("User Message", msg) :: ("Internal Message", hError.internalMessage) :: Nil
                case _                                                       => ("Internal Message", hError.internalMessage) :: Nil
              },
              hError.causes.zipWithIndex.map { (c, i) => (s"Cause[$i]", throwableMessage(c, stackTrace)) },
              Option.when(stackTrace)(("Stack Trace", formatExceptionTrace(hError.getStackTrace))).toList,
            ),
          )
        case throwable =>
          (
            throwable.getClass.getSimpleName,
            List(
              Option(throwable.getMessage).map(("Message", _)).toList,
              Option(throwable.getCause).map { c => ("Cause", throwableMessage(c, stackTrace)) }.toList,
              Option.when(stackTrace)(("Stack Trace", formatExceptionTrace(throwable.getStackTrace))).toList,
            ),
          )
      }

    parts._2.flatten
      .map { (label, message) => message.split("\n").mkString(s"\n  $label:\n      > ", "\n        ", "") }
      .mkString(s"--- ${parts._1} ---", "", "")
  }

  private def formatExceptionTrace(trace: Array[StackTraceElement]): String = trace.mkString("\n")

  private def unwrapThrowable(throwable: Throwable): List[Throwable] =
    throwable match {
      case HError.Multiple(children) => children.toList.flatMap(unwrapThrowable)
      case HError.Wrapped(throwable) => throwable :: Nil
      case _                         => throwable :: Nil
    }

  // =====| Builders |=====

  abstract class ErrorBuilder2[+E](build: (String, List[Throwable]) => E) {
    final def apply(message: String, causes: Throwable*): E = build(message, causes.toList.flatMap(HError.unwrapThrowable))
    final def apply(message: String, causes: Iterable[Throwable]): E = build(message, causes.toList.flatMap(HError.unwrapThrowable))
  }

  abstract class ErrorBuilder3[+E](build: (String, String, List[Throwable]) => E) extends ErrorBuilder2[E]((msg, causes) => build(msg, msg, causes)) {
    final def apply(userMessage: String, internalMessage: String, causes: Throwable*): E = build(userMessage, internalMessage, causes.toList.flatMap(HError.unwrapThrowable))
    final def apply(userMessage: String, internalMessage: String, causes: Iterable[Throwable]): E = build(userMessage, internalMessage, causes.toList.flatMap(HError.unwrapThrowable))
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy