• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

snowplow / iglu-scala-client / 19097678019

05 Nov 2025 09:40AM UTC coverage: 81.375% (-0.6%) from 81.96%
19097678019

push

github

oguzhanunlu
Refactor CI (#265)

568 of 698 relevant lines covered (81.38%)

0.83 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

65.96
/modules/data/src/main/scala/com.snowplowanalytics.iglu/client/ClientError.scala
1
/*
2
 * Copyright (c) 2014-2023 Snowplow Analytics Ltd. All rights reserved.
3
 *
4
 * This program is licensed to you under the Apache License Version 2.0,
5
 * and you may not use this file except in compliance with the Apache License Version 2.0.
6
 * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
7
 *
8
 * Unless required by applicable law or agreed to in writing,
9
 * software distributed under the Apache License Version 2.0 is distributed on an
10
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
 * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
12
 */
13
package com.snowplowanalytics.iglu.client
14

15
import java.time.Instant
16

17
import cats.Show
18
import cats.syntax.show._
19
import cats.syntax.either._
20
import io.circe.{Decoder, DecodingFailure, Encoder, Json, JsonObject}
21
import io.circe.syntax._
22
import validator.ValidatorError
23
import resolver.LookupHistory
24
import resolver.registries.RegistryError
25

26
import scala.collection.immutable.SortedMap
27

28
/** Common type for Resolver's and Validator's errors */
29
sealed trait ClientError extends Product with Serializable {
30
  def getMessage: String =
31
    ClientError.igluClientResolutionErrorCirceEncoder(this).noSpaces
×
32
}
33

34
object ClientError {
35

36
  val SupersededByField = "supersededBy"
1✔
37

38
  /** Error happened during schema resolution step */
39
  final case class ResolutionError(value: SortedMap[String, LookupHistory]) extends ClientError {
40
    def isNotFound: Boolean =
41
      value.values.flatMap(_.errors).forall(_ == RegistryError.NotFound)
×
42
  }
43

44
  /** Error happened during schema/instance validation step */
45
  final case class ValidationError(error: ValidatorError, supersededBy: Option[String])
46
      extends ClientError
47

48
  implicit val igluClientResolutionErrorCirceEncoder: Encoder[ClientError] =
49
    Encoder.instance {
1✔
50
      case ResolutionError(lookupHistory) =>
51
        Json.obj(
1✔
52
          "error" := Json.fromString("ResolutionError"),
1✔
53
          "lookupHistory" := lookupHistory.toList
1✔
54
            .map { case (repo, lookups) =>
1✔
55
              lookups.asJson.deepMerge(Json.obj("repository" := repo.asJson))
1✔
56
            }
57
        )
58
      case ValidationError(error, supersededBy) =>
1✔
59
        val errorTypeJson = Json.obj("error" := Json.fromString("ValidationError"))
1✔
60
        val supersededByJson = supersededBy
61
          .map { v =>
62
            Json.obj(SupersededByField -> v.asJson)
1✔
63
          }
64
          .getOrElse(JsonObject.empty.asJson)
1✔
65
        error.asJson
66
          .deepMerge(errorTypeJson)
67
          .deepMerge(supersededByJson)
1✔
68
    }
69

70
  implicit val igluClientResolutionErrorCirceDecoder: Decoder[ClientError] =
71
    Decoder.instance { cursor =>
1✔
72
      for {
73
        error <- cursor.downField("error").as[String]
1✔
74
        result <- error match {
75
          case "ResolutionError" =>
76
            cursor
77
              .downField("lookupHistory")
1✔
78
              .as[List[RepoLookupHistory]]
1✔
79
              .map { history =>
1✔
80
                ResolutionError(SortedMap[String, LookupHistory]() ++ history.map(_.toField).toMap)
1✔
81
              }
82
          case "ValidationError" =>
1✔
83
            val supersededBy = cursor.downField(SupersededByField).as[String].toOption
1✔
84
            cursor
85
              .as[ValidatorError]
1✔
86
              .map { error =>
1✔
87
                ValidationError(error, supersededBy)
1✔
88
              }
89
          case _ =>
90
            DecodingFailure(
×
91
              s"Error type $error cannot be recognized as Iglu Client Error",
92
              cursor.history
×
93
            ).asLeft
×
94
        }
95

96
      } yield result
97

98
    }
99

100
  implicit val igluClientShowInstance: Show[ClientError] =
101
    Show.show {
1✔
102
      case ClientError.ValidationError(ValidatorError.InvalidData(reports), _) =>
×
103
        val issues = reports.toList
104
          .groupBy(_.path)
×
105
          .map { case (path, messages) =>
×
106
            s"* At ${path.getOrElse("unknown path")}:\n" ++ messages
×
107
              .map(_.message)
108
              .map(m => s"  - $m")
109
              .mkString("\n")
×
110
          }
111
        s"Instance is not valid against its schema:\n${issues.mkString("\n")}"
112
      case ClientError.ValidationError(ValidatorError.InvalidSchema(reports), _) =>
×
113
        val r = reports.toList.map(i => s"* [${i.message}] (at ${i.path})").mkString(",\n")
×
114
        s"Resolved schema cannot be used to validate an instance. Following issues found:\n$r"
115
      case ClientError.ResolutionError(lookup) =>
×
116
        val attempts = (a: Int) => if (a == 1) "1 attempt" else s"$a attempts"
×
117
        val errors = lookup.map { case (repo, tries) =>
×
118
          s"* $repo due [${tries.errors.map(_.show).mkString(", ")}] after ${attempts(tries.attempts)}"
×
119
        }
120
        s"Schema cannot be resolved in following repositories:\n${errors.mkString("\n")}"
121
    }
122

123
  // Auxiliary entity, helping to decode Map[String, LookupHistory]
124
  private case class RepoLookupHistory(
125
    repository: String,
126
    errors: Set[RegistryError],
127
    attempts: Int,
128
    lastAttempt: Instant
129
  ) {
130
    def toField: (String, LookupHistory) =
131
      (repository, LookupHistory(errors, attempts, lastAttempt))
1✔
132
  }
133

134
  private object RepoLookupHistory {
135
    implicit val repoLookupHistoryDecoder: Decoder[RepoLookupHistory] =
136
      Decoder.instance { cursor =>
1✔
137
        for {
138
          repository <- cursor.downField("repository").as[String]
1✔
139
          errors     <- cursor.downField("errors").as[Set[RegistryError]]
1✔
140
          attempts   <- cursor.downField("attempts").as[Int]
1✔
141
          last       <- cursor.downField("lastAttempt").as[Instant]
1✔
142
        } yield RepoLookupHistory(repository, errors, attempts, last)
1✔
143
      }
144
  }
145
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc