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

ciris.ConfigError.scala Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2017-2021 Viktor Lövgren
 *
 * SPDX-License-Identifier: MIT
 */

package ciris

import cats.{Eq, Show}
import cats.data.Chain
import cats.implicits._
import ciris.ConfigError._

/**
  * Error which occurred while loading or decoding configuration values.
  *
  * Configuration errors can be created using [[ConfigError.apply]], or
  * with [[ConfigError.sensitive]] if the error might contain sensitive
  * details. When writing [[ConfigDecoder]]s, [[ConfigError.decode]]
  * can be useful for creating decoding errors.
  *
  * Errors for a single configuration value, which might be retrieved
  * from one of multiple sources, can be combined and accumulated with
  * [[ConfigError#or]]. Errors for multiple configuration values can
  * similarly be accumulated using [[ConfigError#and]].
  *
  * Error messages can be retrieved using [[ConfigError#messages]]. If
  * the error relates to a value which might contain sensitive details,
  * [[ConfigError#redacted]] can be used to redact such details. When
  * [[ConfigValue#secret]] is used, sensitive details are redacted and
  * the value is wrapped in [[Secret]] to prevent it from being shown.
  *
  * A `Throwable` representation of a [[ConfigError]] can be retrieved
  * using [[ConfigError#throwable]].
  *
  * @example {{{
  * scala> val error = ConfigError("error")
  * error: ConfigError = ConfigError(error)
  *
  * scala> val sensitive = ConfigError.sensitive("error", "redacted")
  * sensitive: ConfigError = Sensitive(error, redacted)
  *
  * scala> error.or(sensitive).messages
  * res0: cats.data.Chain[String] = Chain(Error and error)
  *
  * scala> error.and(sensitive).redacted.messages
  * res1: cats.data.Chain[String] = Chain(error, redacted)
  * }}}
  */
sealed abstract class ConfigError {

  /**
    * Returns the error messages of the contained errors.
    */
  def messages: Chain[String]

  /**
    * Returns a new [[ConfigError]] with sensitive details redacted.
    */
  def redacted: ConfigError

  /**
    * Returns a new [[ConfigError]] combining errors for
    * separate configuration values.
    */
  final def and(that: ConfigError): ConfigError =
    (this, that) match {
      case (_, Empty)         => this
      case (Empty, _)         => that
      case (And(as), And(bs)) => normalize(as ++ bs, And(_))
      case (And(as), _)       => normalize(as :+ that, And(_))
      case (_, And(bs))       => normalize(this +: bs, And(_))
      case (_, _)             => normalize(Chain(this, that), And(_))
    }

  /**
    * Returns `true` if the error is due to no value
    * being available; otherwise `false`.
    *
    * If the error is a combination of multiple errors,
    * returns `true` if all errors are due to no value
    * being available; otherwise `false`.
    */
  private[ciris] final def isMissing: Boolean =
    this match {
      case And(errors)     => errors.forall(_.isMissing)
      case Apply(_)        => false
      case Empty           => false
      case Loaded          => false
      case Missing(_)      => true
      case Or(errors)      => errors.forall(_.isMissing)
      case Sensitive(_, _) => false
    }

  /**
    * Returns a new [[ConfigError]] combining errors for
    * a single configuration value.
    */
  final def or(that: ConfigError): ConfigError =
    (this, that) match {
      case (_, Empty)       => this
      case (Empty, _)       => that
      case (Or(as), Or(bs)) => normalize(as ++ bs, Or(_))
      case (Or(as), _)      => normalize(as :+ that, Or(_))
      case (_, Or(bs))      => normalize(this +: bs, Or(_))
      case (_, _)           => normalize(Chain(this, that), Or(_))
    }

  /**
    * Returns a new `Throwable` including the contained
    * error messages.
    */
  final def throwable: Throwable =
    ConfigException(this)
}

/**
  * @groupname Create Creating Instances
  * @groupprio Create 0
  *
  * @groupname Instances Type Class Instances
  * @groupprio Instances 1
  */
object ConfigError {

  /**
    * Returns a new [[ConfigError]] using the specified message.
    *
    * If the specified message might contain sensitive details,
    * then use [[ConfigError.sensitive]] instead to create an
    * error which is capable of redacting sensitive details.
    *
    * @group Create
    */
  final def apply(message: => String): ConfigError =
    Apply(() => message)

  /**
    * Returns a new [[ConfigError]] using the specified message and
    * redacted message.
    *
    * The specified message is used in the returned [[ConfigError]].
    * Whenever [[ConfigError#redacted]] is invoked on the returned
    * instance, a new [[ConfigError]] is returned which instead
    * uses the redacted message.
    *
    * @group Create
    */
  final def sensitive(message: => String, redactedMessage: => String): ConfigError =
    Sensitive(() => message, () => redactedMessage)

  /**
    * Returns a new [[ConfigError]] for when the specified value
    * could not be decoded.
    *
    * @group Create
    */
  final def decode[A](
    typeName: String,
    key: Option[ConfigKey],
    value: A
  )(implicit show: Show[A]): ConfigError = {
    def message(valueShown: Option[String]): String =
      (key, valueShown) match {
        case (Some(key), Some(value)) =>
          s"${key.description.capitalize} with value $value cannot be converted to $typeName"
        case (Some(key), None) =>
          s"${key.description.capitalize} cannot be converted to $typeName"
        case (None, Some(value)) =>
          s"Unable to convert value $value to $typeName"
        case (None, None) =>
          s"Unable to convert value to $typeName"
      }

    ConfigError.sensitive(
      message = message(Some(value.show)),
      redactedMessage = message(None)
    )
  }

  private[ciris] final case class And(errors: Chain[ConfigError]) extends ConfigError {
    override final def messages: Chain[String] =
      errors.flatMap(_.messages)

    override final def redacted: ConfigError =
      And(errors.map(_.redacted))

    override final def toString: String =
      s"And(${errors.toList.mkString(", ")})"
  }

  private[ciris] final case class Apply(message: () => String) extends ConfigError {
    override final def messages: Chain[String] =
      Chain.one(message())

    override final val redacted: ConfigError =
      this

    override final def hashCode: Int =
      message().hashCode

    override final def equals(that: Any): Boolean =
      that match {
        case Apply(message2) => message() == message2()
        case _               => false
      }

    override final def toString: String =
      s"ConfigError(${message()})"
  }

  private[ciris] case object Empty extends ConfigError {
    override final val messages: Chain[String] =
      Chain.empty

    override final val redacted: ConfigError =
      this

    override final def toString: String =
      "Empty"
  }

  private[ciris] case object Loaded extends ConfigError {
    override final val messages: Chain[String] =
      Chain.empty

    override final val redacted: ConfigError =
      this

    override final def toString: String =
      "Loaded"
  }

  private[ciris] final case class Missing(key: ConfigKey) extends ConfigError {
    override final def messages: Chain[String] =
      Chain.one(s"Missing ${uncapitalize(key.description)}")

    override final val redacted: ConfigError =
      this

    override final def toString: String =
      s"Missing($key)"
  }

  private[ciris] final case class Or(errors: Chain[ConfigError]) extends ConfigError {
    override final def messages: Chain[String] = {
      val messages =
        errors
          .flatMap(_.messages)
          .toVector
          .zipWithIndex
          .map {
            case (m, 0) => m.capitalize
            case (m, _) => uncapitalize(m)
          }

      Chain.one {
        if (messages.size < 3) {
          messages.mkString(" and ")
        } else {
          messages.init.mkString("", ", ", s", and ${messages.last}")
        }
      }
    }

    override final def redacted: ConfigError =
      Or(errors.map(_.redacted))

    override final def toString: String =
      s"Or(${errors.toList.mkString(", ")})"
  }

  private[ciris] final case class Sensitive(
    message: () => String,
    redactedMessage: () => String
  ) extends ConfigError {
    override final def messages: Chain[String] =
      Chain.one(message())

    override final def redacted: ConfigError =
      Apply(redactedMessage)

    override final def hashCode: Int =
      (message(), redactedMessage()).hashCode

    override final def equals(that: Any): Boolean =
      that match {
        case Sensitive(message2, redactedMessage2) =>
          message() == message2() && redactedMessage() == redactedMessage2()
        case _ =>
          false
      }

    override final def toString: String =
      s"Sensitive(${message()}, ${redactedMessage()})"
  }

  private[ciris] final def normalize(
    errors: Chain[ConfigError],
    create: Chain[ConfigError] => ConfigError
  ): ConfigError =
    if (errors.contains(Loaded)) {
      val rest = errors.filter(_ != Loaded)
      if (rest.isEmpty) {
        Loaded
      } else {
        create(rest.prepend(Loaded))
      }
    } else {
      create(errors)
    }

  private[ciris] final def uncapitalize(s: String): String =
    if (s.length == 0 || s.charAt(0).isLower) s
    else {
      val chars = s.toCharArray
      chars(0) = chars(0).toLower
      new String(chars)
    }

  /**
    * @group Instances
    */
  implicit final val configErrorEq: Eq[ConfigError] = {
    def eqv(e1: Chain[ConfigError], e2: Chain[ConfigError]) =
      Eq[Chain[ConfigError]].eqv(e1, e2)

    Eq.instance {
      case (Empty, Empty)                         => true
      case (Empty, _)                             => false
      case (Loaded, Loaded)                       => true
      case (Loaded, _)                            => false
      case (Missing(k1), Missing(k2))             => k1 === k2
      case (Missing(_), _)                        => false
      case (And(e1), And(e2))                     => eqv(e1, e2)
      case (And(_), _)                            => false
      case (Or(e1), Or(e2))                       => eqv(e1, e2)
      case (Or(_), _)                             => false
      case (Apply(m1), Apply(m2))                 => m1() === m2()
      case (Apply(_), _)                          => false
      case (Sensitive(m1, r1), Sensitive(m2, r2)) => m1() === m2() && r1() === r2()
      case (Sensitive(_, _), _)                   => false
    }
  }

  /**
    * @group Instances
    */
  implicit final val configErrorShow: Show[ConfigError] =
    Show.fromToString
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy