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

exception.SkunkException.scala Maven / Gradle / Ivy

// Copyright (c) 2018-2021 by Rob Norris
// This software is licensed under the MIT License (MIT).
// For more information see LICENSE or https://opensource.org/licenses/MIT

package skunk.exception

import cats.syntax.all._
import skunk.data.Type
import skunk.Query
import skunk.util.{ CallSite, Origin, Pretty }
import natchez.Fields
import natchez.TraceValue

class SkunkException protected[skunk](
  val sql:             Option[String],
  val message:         String,
  val position:        Option[Int]                  = None,
  val detail:          Option[String]               = None,
  val hint:            Option[String]               = None,
  val history:         List[Either[Any, Any]]       = Nil,
  val arguments:       List[(Type, Option[String])] = Nil,
  val sqlOrigin:       Option[Origin]               = None,
  val argumentsOrigin: Option[Origin]               = None,
  val callSite:        Option[CallSite]             = None
) extends Exception(message) with Fields {

  override def fields: Map[String, TraceValue] = {

    var map: Map[String, TraceValue] = Map.empty

    map += "error.message" -> message

    sql     .foreach(a => map += "error.sql"      -> a)
    position.foreach(a => map += "error.position" -> a)
    detail  .foreach(a => map += "error.detail"   -> a)
    hint    .foreach(a => map += "error.hint"     -> a)

    (arguments.zipWithIndex).foreach { case ((typ, os), n) =>
      map += s"error.argument.${n + 1}.type"  -> typ.name
      map += s"error.argument.${n + 1}.value" -> os.getOrElse[String]("NULL")
    }

    sqlOrigin.foreach { o =>
      map += "error.sqlOrigin.file" -> o.file
      map += "error.sqlOrigin.line" -> o.line
    }

    argumentsOrigin.foreach { o =>
      map += "error.argumentsOrigin.file" -> o.file
      map += "error.argumentsOrigin.line" -> o.line
    }

    callSite.foreach { cs =>
      map += "error.callSite.origin.file"   -> cs.origin.file
      map += "error.callSite.origin.line"   -> cs.origin.line
      map += "error.callSite.origin.method" -> cs.methodName
    }

    map
  }

  protected def title: String =
    callSite.fold(getClass.getSimpleName) { case CallSite(name, origin) =>
      s"Skunk encountered a problem related to use of ${framed(name)}\n  at $origin"
    }

  protected def width = 80 // wrap here

  def labeled(label: String, s: String): String =
    if (s.isEmpty) "" else {
      "\n|" +
      label + Console.CYAN + Pretty.wrap(
        width - label.length,
        s,
        s"${Console.RESET}\n${Console.CYAN}" + label.map(_ => ' ')
      ) + Console.RESET
    }

  protected def header: String =
    s"""|
        |$title
        |${labeled("  Problem: ", message)}${labeled("   Detail: ", detail.orEmpty)}${labeled("     Hint: ", hint.orEmpty)}
        |
        |""".stripMargin

  protected def statement: String =
    sql.foldMap { sql =>
      val stmt = Pretty.formatMessageAtPosition(sql, message, position.getOrElse(0))
      s"""|The statement under consideration ${sqlOrigin.fold("is")(or => s"was defined\n  at $or")}
          |
          |$stmt
          |
          |""".stripMargin
    }

  // TODO: Not clear if this is useful, disabled for now
  // protected def exchanges: String =
  //   if (history.isEmpty) "" else
  //   s"""|Recent message exchanges:
  //       |
  //       |  ${history.map(_.fold(a => s"→ ${Console.BOLD}$a${Console.RESET}", "← " + _)).mkString("", "\n|  ", "")}
  //       |
  //       |""".stripMargin

  protected def args: String = {

    def formatValue(s: String) =
      s"${Console.GREEN}$s${Console.RESET}"

    if (arguments.isEmpty) "" else
    s"""|and the arguments ${argumentsOrigin.fold("were")(or => s"were provided\n  at $or")}
        |
        |  ${arguments.zipWithIndex.map { case ((t, s), n) => f"$$${n+1} $t%-10s ${s.fold("NULL")(formatValue)}" } .mkString("\n|  ") }
        |
        |""".stripMargin
  }

  protected def sections: List[String] =
    List(header, statement, args) //, exchanges)

  final override def getMessage =
    sections
      .combineAll
      .linesIterator
      .map("🔥  " + _)
      .mkString("\n", "\n", s"\n\n${getClass.getName}: $message")

}


object SkunkException {

  def fromQueryAndArguments[A](
    message:    String,
    query:      Query[A, _],
    args:       A,
    callSite:   Option[CallSite],
    hint:       Option[String] = None,
    argsOrigin: Option[Origin] = None
  ) =
    new SkunkException(
      Some(query.sql),
      message,
      sqlOrigin       = Some(query.origin),
      callSite        = callSite,
      hint            = hint,
      arguments       = query.encoder.types zip query.encoder.encode(args),
      argumentsOrigin = argsOrigin
    )

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy