com.snowplowanalytics.iglu.client.ClientError.scala Maven / Gradle / Ivy
The newest version!
/*
* Copyright (c) 2014-2023 Snowplow Analytics Ltd. All rights reserved.
*
* This program is licensed to you under the Apache License Version 2.0,
* and you may not use this file except in compliance with the Apache License Version 2.0.
* You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the Apache License Version 2.0 is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
*/
package com.snowplowanalytics.iglu.client
import java.time.Instant
import cats.Show
import cats.syntax.show._
import cats.syntax.either._
import io.circe.{Decoder, DecodingFailure, Encoder, Json, JsonObject}
import io.circe.syntax._
import validator.ValidatorError
import resolver.LookupHistory
import resolver.registries.RegistryError
import scala.collection.immutable.SortedMap
/** Common type for Resolver's and Validator's errors */
sealed trait ClientError extends Product with Serializable {
def getMessage: String =
ClientError.igluClientResolutionErrorCirceEncoder(this).noSpaces
}
object ClientError {
val SupersededByField = "supersededBy"
/** Error happened during schema resolution step */
final case class ResolutionError(value: SortedMap[String, LookupHistory]) extends ClientError {
def isNotFound: Boolean =
value.values.flatMap(_.errors).forall(_ == RegistryError.NotFound)
}
/** Error happened during schema/instance validation step */
final case class ValidationError(error: ValidatorError, supersededBy: Option[String])
extends ClientError
implicit val igluClientResolutionErrorCirceEncoder: Encoder[ClientError] =
Encoder.instance {
case ResolutionError(lookupHistory) =>
Json.obj(
"error" := Json.fromString("ResolutionError"),
"lookupHistory" := lookupHistory.toList
.map { case (repo, lookups) =>
lookups.asJson.deepMerge(Json.obj("repository" := repo.asJson))
}
)
case ValidationError(error, supersededBy) =>
val errorTypeJson = Json.obj("error" := Json.fromString("ValidationError"))
val supersededByJson = supersededBy
.map { v =>
Json.obj(SupersededByField -> v.asJson)
}
.getOrElse(JsonObject.empty.asJson)
error.asJson
.deepMerge(errorTypeJson)
.deepMerge(supersededByJson)
}
implicit val igluClientResolutionErrorCirceDecoder: Decoder[ClientError] =
Decoder.instance { cursor =>
for {
error <- cursor.downField("error").as[String]
result <- error match {
case "ResolutionError" =>
cursor
.downField("lookupHistory")
.as[List[RepoLookupHistory]]
.map { history =>
ResolutionError(SortedMap[String, LookupHistory]() ++ history.map(_.toField).toMap)
}
case "ValidationError" =>
val supersededBy = cursor.downField(SupersededByField).as[String].toOption
cursor
.as[ValidatorError]
.map { error =>
ValidationError(error, supersededBy)
}
case _ =>
DecodingFailure(
s"Error type $error cannot be recognized as Iglu Client Error",
cursor.history
).asLeft
}
} yield result
}
implicit val igluClientShowInstance: Show[ClientError] =
Show.show {
case ClientError.ValidationError(ValidatorError.InvalidData(reports), _) =>
val issues = reports.toList
.groupBy(_.path)
.map { case (path, messages) =>
s"* At ${path.getOrElse("unknown path")}:\n" ++ messages
.map(_.message)
.map(m => s" - $m")
.mkString("\n")
}
s"Instance is not valid against its schema:\n${issues.mkString("\n")}"
case ClientError.ValidationError(ValidatorError.InvalidSchema(reports), _) =>
val r = reports.toList.map(i => s"* [${i.message}] (at ${i.path})").mkString(",\n")
s"Resolved schema cannot be used to validate an instance. Following issues found:\n$r"
case ClientError.ResolutionError(lookup) =>
val attempts = (a: Int) => if (a == 1) "1 attempt" else s"$a attempts"
val errors = lookup.map { case (repo, tries) =>
s"* $repo due [${tries.errors.map(_.show).mkString(", ")}] after ${attempts(tries.attempts)}"
}
s"Schema cannot be resolved in following repositories:\n${errors.mkString("\n")}"
}
// Auxiliary entity, helping to decode Map[String, LookupHistory]
private case class RepoLookupHistory(
repository: String,
errors: Set[RegistryError],
attempts: Int,
lastAttempt: Instant
) {
def toField: (String, LookupHistory) =
(repository, LookupHistory(errors, attempts, lastAttempt))
}
private object RepoLookupHistory {
implicit val repoLookupHistoryDecoder: Decoder[RepoLookupHistory] =
Decoder.instance { cursor =>
for {
repository <- cursor.downField("repository").as[String]
errors <- cursor.downField("errors").as[Set[RegistryError]]
attempts <- cursor.downField("attempts").as[Int]
last <- cursor.downField("lastAttempt").as[Instant]
} yield RepoLookupHistory(repository, errors, attempts, last)
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy