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

busymachines.pureharm.internals.rest.decodeFailures.scala Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2019 BusyMachines
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package busymachines.pureharm.internals.rest

import busymachines.pureharm.anomaly._
import io.circe.{CursorOp, DecodingFailure}

/** @author Lorand Szakacs, https://github.com/lorandszakacs
  * @since 15 Jul 2020
  */

final private[pureharm] case class CirceDecodingAnomaly(
  failure: DecodingFailure
) extends InvalidInputAnomaly(
    "Failed to decode JSON. See 'parameters.path' for a history of decoding failure. And 'parameters' for details.",
    Option(failure),
  ) {

  override val id: AnomalyID = CirceDecodingAnomaly.ID

  override val parameters: Anomaly.Parameters = {
    val reverseHistory = failure.history.reverse
    reverseHistory match {
      //this indicates a missing field
      case CursorOp.DownField(fn) :: Nil if failure.message.contains(CirceDecodingAnomaly.MissingFieldIndicator) =>
        Anomaly.Parameters(
          "missingField" -> fn,
          "path"         -> (reverseHistory.map(lensOpToString) ++
            countArrayFailureIndex(failure.history).map(idx => s"failed to decode array element index [$idx]").toList),
        ) ++ super.parameters

      case _ =>
        Anomaly.Parameters(
          "expectedType" -> failure.message,
          "path"         -> (reverseHistory.map(lensOpToString) ++
            countArrayFailureIndex(failure.history).map(idx => s"failed to decode array element index [$idx]").toList),
        ) ++ super.parameters
    }

  }

  /** For instance, for a list of 4 elements, we expect to see such a history:
    * {{{
    *   List(MoveRight, MoveRight, MoveRight, DownArray, DownField(f4))
    * }}}
    * So we count all contiguous MoveRight from head to tail.
    *   - Since going down into nested arrays requires a DownArray
    * TODO: test to see if this heuristic works properly for:
    *  - arrays in arrays
    *  - arrays containing objects containing arrays (and failure is in these nested arrays)
    * @return
    */
  private def countArrayFailureIndex(o: List[CursorOp]): Option[Int] = {
    val rightMoves = o.takeWhile(_.isInstanceOf[CursorOp.MoveRight.type])
    if (rightMoves.isEmpty)
      Option.empty
    else Option(rightMoves.size)
  }

  private def lensOpToString(o: CursorOp): String =
    o match {
      case s: CursorOp.Field     => s"field: ${s.k}"
      case s: CursorOp.DownField => s"decoding field: ${s.k}"
      case CursorOp.DownArray => s"decoding array"
      case CursorOp.MoveRight => s"decoding array element"
      case s                  => s.toString
    }
}

object CirceDecodingAnomaly {
  /** When a field is missing, circe usually answers with this message:
    * DecodingFailure(Attempt to decode value on failed cursor, List(DownField(fl)))
    *
    * So we are kinda left to interpret strings, which sucks and is brittle...
    */
  private val MissingFieldIndicator: String = "Attempt to decode value on failed cursor"
  case object ID extends AnomalyID { override val name: String = "rest_json_0" }

}

//--------------------------------------------
final private[pureharm] case class CirceParsingAnomaly(
  failure: io.circe.ParsingFailure
) extends InvalidInputAnomaly(
    "Failed to parse JSON. Double check your syntax",
    Option(failure),
  ) {
  override val id: AnomalyID = CirceParsingAnomaly.ID
}

object CirceParsingAnomaly {
  case object ID extends AnomalyID { override val name: String = "rest_json_1" }
}

//--------------------------------------------
abstract private[pureharm] class MissingRequestPart(
  tpe:          String,
  tapirMessage: String,
) extends InvalidInputAnomaly(
    s"Missing required part: '$tpe'"
  ) {
  override val id: AnomalyID = MissingRequestPart.ID

  override def parameters: Anomaly.Parameters = super.parameters ++ Anomaly.Parameters(
    "diagnostic" -> tapirMessage
  )
}

object MissingRequestPart {
  case object ID extends AnomalyID { override val name: String = "rest_missing" }
}

//--------------------------------------------
final private[pureharm] case class MissingRequestPartGeneric(
  tpe:          String,
  tapirMessage: String,
) extends InvalidInputAnomaly(
    s"Missing required: $tpe >> $tapirMessage"
  ) {
  override val id: AnomalyID = MissingRequestPartGeneric.ID

  override def parameters: Anomaly.Parameters = super.parameters ++ Anomaly.Parameters(
    "diagnostic" -> tapirMessage
  )
}

object MissingRequestPartGeneric {
  case object ID extends AnomalyID { override val name: String = "rest_missing" }
}

//--------------------------------------------
final private[pureharm] case class MissingQueryParam(
  paramName:    String,
  tapirMessage: String,
) extends MissingRequestPart("Query parameter", tapirMessage) {
  override val id: AnomalyID = MissingQueryParam.ID

  override val parameters: Anomaly.Parameters = Anomaly.Parameters(
    "missingParam" -> paramName
  ) ++ super.parameters
}

object MissingQueryParam {
  case object ID extends AnomalyID { override val name: String = "rest_missing_qp" }
}

//--------------------------------------------
final private[pureharm] case class MissingHeader(
  headerName:   String,
  tapirMessage: String,
) extends MissingRequestPart("Header", tapirMessage) {
  override val id: AnomalyID = MissingHeader.ID

  override val parameters: Anomaly.Parameters = Anomaly.Parameters(
    "missingHeader" -> headerName
  ) ++ super.parameters
}

object MissingHeader {
  case object ID extends AnomalyID { override val name: String = "rest_missing_header" }
}

//--------------------------------------------
final private[pureharm] case class InvalidMultiple(
  tapirMessage: String,
  validations:  Seq[String],
) extends InvalidInputAnomaly(tapirMessage) {
  override val id: AnomalyID = InvalidMultiple.ID

  override val parameters: Anomaly.Parameters = super.parameters ++ Anomaly.Parameters(
    "validations" -> validations
  )

}

object InvalidMultiple {
  case object ID extends AnomalyID { override val name: String = "rest_invalid_multiple" }
}

//--------------------------------------------
abstract private[pureharm] class InvalidRequestPart(
  tpe:          String,
  tapirMessage: String,
  error:        String,
) extends InvalidInputAnomaly(
    s"Request part: '$tpe' is present, but has an invalid value. Please check that it's of the proper type, e.g. a number, UUID, proper date format, etc."
  ) {
  override val id: AnomalyID = InvalidRequestPart.ID

  override def parameters: Anomaly.Parameters = super.parameters ++ Anomaly.Parameters(
    "partType"   -> tpe,
    "diagnostic" -> tapirMessage,
    "error"      -> error,
  )
}

object InvalidRequestPart {
  case object ID extends AnomalyID { override val name: String = "rest_invalid" }
}

//--------------------------------------------
final private[pureharm] case class InvalidRequestPartGeneric(
  tpe:          String,
  givenValue:   String,
  tapirMessage: String,
  error:        String,
) extends InvalidRequestPart(
    tpe          = tpe,
    tapirMessage = tapirMessage,
    error        = error,
  ) {
  override val id: AnomalyID = InvalidRequestPartGeneric.ID

  override def parameters: Anomaly.Parameters = super.parameters ++ Anomaly.Parameters(
    "givenValue" -> givenValue
  )
}

object InvalidRequestPartGeneric {
  case object ID extends AnomalyID { override val name: String = "rest_invalid" }
}

//--------------------------------------------
final private[pureharm] case class InvalidQueryParam(
  paramName:    String,
  givenValue:   String,
  tapirMessage: String,
  error:        String,
) extends InvalidRequestPart(
    tpe          = "Query parameter",
    tapirMessage = tapirMessage,
    error        = error,
  ) {
  override val id: AnomalyID = InvalidQueryParam.ID

  override val parameters: Anomaly.Parameters = Anomaly.Parameters(
    "paramName"  -> paramName,
    "givenValue" -> givenValue,
  ) ++ super.parameters
}

object InvalidQueryParam {
  case object ID extends AnomalyID { override val name: String = "rest_invalid_qp" }
}

//--------------------------------------------
final private[pureharm] case class InvalidHeader(
  headerName:   String,
  givenValue:   String,
  tapirMessage: String,
  error:        String,
) extends InvalidRequestPart(
    tpe          = "Header",
    tapirMessage = tapirMessage,
    error        = error,
  ) {
  override val id: AnomalyID = InvalidHeader.ID

  override val parameters: Anomaly.Parameters = Anomaly.Parameters(
    "headerName" -> headerName,
    "givenValue" -> givenValue,
  ) ++ super.parameters
}

object InvalidHeader {
  case object ID extends AnomalyID { override val name: String = "rest_invalid_hd" }
}

//--------------------------------------------
final private[pureharm] case class MissingAuthHeader(
  headerName: String
) extends UnauthorizedAnomaly(
    s"Missing required header used for authentication: $headerName"
  ) {
  override val id: AnomalyID = MissingAuthHeader.ID

  override val parameters: Anomaly.Parameters = Anomaly.Parameters(
    "headerName" -> headerName
  ) ++ super.parameters
}

object MissingAuthHeader {
  case object ID extends AnomalyID { override val name: String = "auth_missing_header" }
}

//--------------------------------------------
final private[pureharm] case class InvalidAuthHeader(
  headerName: String,
  givenValue: String,
  error:      String,
) extends UnauthorizedAnomaly(
    s"Header value used for authentication: $headerName >> has incorect syntax/format. Please make sure it's of the proper type or format."
  ) {
  override val id: AnomalyID = InvalidAuthHeader.ID

  override val parameters: Anomaly.Parameters = Anomaly.Parameters(
    "headerName" -> headerName,
    "givenValue" -> givenValue,
    "error"      -> error,
  ) ++ super.parameters
}

object InvalidAuthHeader {
  case object ID extends AnomalyID { override val name: String = "auth_invalid_header" }
}

//--------------------------------------------
final private[pureharm] case class MismatchAnomaly(
  tpe:          String,
  expected:     String,
  given:        String,
  tapirMessage: String,
) extends InvalidInputAnomaly(
    s"Mismatched input of type '$tpe' >> expected=$expected, given=$given."
  ) {
  override val id: AnomalyID = MismatchAnomaly.ID

  override val parameters: Anomaly.Parameters = Anomaly.Parameters(
    "type"       -> tpe,
    "expected"   -> expected,
    "given"      -> given,
    "diagnostic" -> tapirMessage,
  ) ++ super.parameters
}

object MismatchAnomaly {
  case object ID extends AnomalyID { override val name: String = "rest_mismatch" }
}

//--------------------------------------------
final private[pureharm] case class ValidationAnomaly(
  tapirMessage: String,
  errors:       List[String],
) extends InvalidInputAnomaly(
    s"Invalid input"
  ) {
  override val id: AnomalyID = ValidationAnomaly.ID

  override val parameters: Anomaly.Parameters = Anomaly.Parameters(
    "diagnostic" -> tapirMessage,
    "errors"     -> errors,
  ) ++ super.parameters
}

object ValidationAnomaly {
  case object ID extends AnomalyID { override val name: String = "rest_validation" }
}
//--------------------------------------------




© 2015 - 2025 Weber Informatics LLC | Privacy Policy