Skip to content

Commit

Permalink
Issue #146 - Some changes in existing types including errors
Browse files Browse the repository at this point in the history
  • Loading branch information
kevin-lee committed Jan 21, 2021
1 parent 66b88d7 commit edb158d
Show file tree
Hide file tree
Showing 4 changed files with 254 additions and 40 deletions.
50 changes: 34 additions & 16 deletions src/main/scala/kevinlee/git/Git.scala
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
package kevinlee.git

import java.io.File

import cats._
import cats.data._
import cats.implicits._

import effectie.cats._
import effectie.cats.Effectful._
import effectie.cats._
import io.circe.{Decoder, Encoder}
import io.estatico.newtype.macros.newtype

import java.io.File

/** @author Kevin Lee
* @since 2019-01-01
Expand Down Expand Up @@ -80,6 +81,14 @@ trait Git[F[_]] {

}

@SuppressWarnings(
Array(
"org.wartremover.warts.ExplicitImplicitTypes",
"org.wartremover.warts.ImplicitConversion",
"org.wartremover.warts.ImplicitParameter",
"org.wartremover.warts.PublicInference",
)
)
object Git {
// $COVERAGE-OFF$

Expand All @@ -89,13 +98,20 @@ object Git {

type CmdResult[F[_], A] = EitherT[CmdHistoryWriter[F, *], GitCommandError, A]

final case class BranchName(value: String) extends AnyVal
final case class TagName(value: String) extends AnyVal
final case class Repository(value: String) extends AnyVal
@newtype case class BranchName(value: String)
@newtype case class TagName(value: String)
object TagName {
implicit val encoder: Encoder[TagName] = deriving
implicit val decoder: Decoder[TagName] = deriving
}
final case class Repository(value: String) extends AnyVal
final case class RemoteName(remoteName: String) extends AnyVal
final case class RepoUrl(repoUrl: String) extends AnyVal
final case class Description(value: String) extends AnyVal

@newtype case class TagMessage(tagMessage: String)
@newtype case class HashObject(hashObject: String)

def apply[F[_]: Git]: Git[F] = implicitly[Git[F]]

implicit def gitF[F[_]: EffectConstructor: Monad]: Git[F] = new GitF[F]
Expand Down Expand Up @@ -170,15 +186,17 @@ object Git {
): CmdResult[F, A] =
EitherT {
val fOf = r.map { eth =>
val w: CmdHistory = eth match {
case Left(error) =>
List.empty[GitCmdAndResult]
case Right((cmdResult, a)) =>
List(GitCmdAndResult(gitCmd, cmdResult))
}
val eth2: Either[GitCommandError, A] = eth.map {
case (_, a) => a
val w: CmdHistory =
eth match {
case Left(error) =>
List.empty[GitCmdAndResult]
case Right((cmdResult, a)) =>
List(GitCmdAndResult(gitCmd, cmdResult))
}
val eth2: Either[GitCommandError, A] = eth.map {
case (_, a) =>
a
}
(w, eth2)
}
WriterT(fOf)
Expand All @@ -188,7 +206,7 @@ object Git {
gitCmdSimpleWithWriter[BranchName](
baseDir,
GitCmd.currentBranchName,
xs => BranchName(xs.mkString.trim)
xs => BranchName(xs.mkString.trim),
)

override def checkIfCurrentBranchIsSame(
Expand Down
8 changes: 4 additions & 4 deletions src/main/scala/kevinlee/github/OldGitHubApi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ trait OldGitHubApi[F[_]] {
tagName: TagName,
changelog: Changelog,
assets: Seq[File]
): F[Either[GitHubError, GitHubRelease]]
): F[Either[GitHubError, OldGitHubRelease]]
}

object OldGitHubApi {
Expand Down Expand Up @@ -127,16 +127,16 @@ object OldGitHubApi {
tagName: TagName,
changelog: Changelog,
assets: Seq[File]
): F[Either[GitHubError, GitHubRelease]] = (for {
): F[Either[GitHubError, OldGitHubRelease]] = (for {
exists <- EitherT(releaseExists(gitHub, repo, tagName))
gitHubRelease <-
if (exists) {
eitherTLeftPure[GitHubRelease](GitHubError.releaseAlreadyExists(tagName))
eitherTLeftPure[OldGitHubRelease](GitHubError.releaseAlreadyExists(tagName))
} else {
for {
gHRepository <- EitherT(this.getRepo(gitHub, repo))
release <- EitherT(createGHRelease(gHRepository, tagName, changelog))
} yield GitHubRelease(
} yield OldGitHubRelease(
tagName,
changelog,
assets.map { file =>
Expand Down
183 changes: 173 additions & 10 deletions src/main/scala/kevinlee/github/data/GitHubError.scala
Original file line number Diff line number Diff line change
@@ -1,24 +1,50 @@
package kevinlee.github.data

import cats.Show
import cats.syntax.all._
import io.circe.parser._
import io.circe.{Decoder, Encoder, Json}
import kevinlee.git.Git.{RepoUrl, TagName}
import kevinlee.git.GitCommandError
import kevinlee.http.{HttpError, HttpRequest, HttpResponse}

import java.time.Instant

/** @author Kevin Lee
* @since 2019-03-09
*/
sealed trait GitHubError

object GitHubError {
final case object NoCredential extends GitHubError
final case object InvalidCredential extends GitHubError
final case class MalformedURL(url: String, errorMessage: Option[String]) extends GitHubError
final case class ConnectionFailure(error: String) extends GitHubError
final case class GitHubServerError(error: String) extends GitHubError
final case class ReleaseAlreadyExists(tagName: TagName) extends GitHubError
final case class ReleaseCreationError(message: String) extends GitHubError
final case class InvalidGitHubRepoUrl(repoUrl: RepoUrl) extends GitHubError
final case class ChangelogNotFound(changelogLocation: String, tagName: TagName) extends GitHubError
final case class CausedByGitCommandError(cause: GitCommandError) extends GitHubError
final case object NoCredential extends GitHubError
final case object InvalidCredential extends GitHubError
final case class MalformedURL(url: String, errorMessage: Option[String]) extends GitHubError
final case class ConnectionFailure(error: String) extends GitHubError
final case class GitHubServerError(error: String) extends GitHubError
final case class ReleaseAlreadyExists(tagName: TagName) extends GitHubError
final case class ReleaseCreationError(message: String) extends GitHubError
final case class InvalidGitHubRepoUrl(repoUrl: RepoUrl) extends GitHubError
final case class ChangelogNotFound(changelogLocation: String, tagName: TagName) extends GitHubError
final case class CausedByGitCommandError(cause: GitCommandError) extends GitHubError
case object NoReleaseCreated extends GitHubError
final case class AbuseRateLimits(message: String, documentationUrl: String) extends GitHubError
final case class RateLimitExceeded(
// "X-RateLimit-Limit"
rateLimit: Option[Int],
// X-RateLimit-Remaining
remaining: Option[Int],
// X-RateLimit-Reset
reset: Option[Instant],
message: String,
docUrl: String,
) extends GitHubError
final case class ForbiddenRequest(httpRequest: HttpRequest, httpResponse: HttpResponse) extends GitHubError
final case class UnprocessableEntity(
httpRequest: HttpRequest,
httpResponse: HttpResponse,
responseBodyJson: Option[ResponseBodyJson],
) extends GitHubError
final case class UnexpectedFailure(httpError: HttpError) extends GitHubError

def noCredential: GitHubError = NoCredential

Expand Down Expand Up @@ -46,6 +72,32 @@ object GitHubError {
def causedByGitCommandError(cause: GitCommandError): GitHubError =
CausedByGitCommandError(cause)

def noReleaseCreated: GitHubError = NoReleaseCreated

def abuseRateLimits(message: String, documentationUrl: String): GitHubError =
AbuseRateLimits(message, documentationUrl)

def rateLimitExceeded(
rateLimit: Option[Int],
remaining: Option[Int],
reset: Option[Instant],
message: String,
docUrl: String,
): GitHubError = RateLimitExceeded(rateLimit, remaining, reset, message, docUrl)

def forbiddenRequest(httpRequest: HttpRequest, httpResponse: HttpResponse): GitHubError =
ForbiddenRequest(httpRequest, httpResponse)

def unprocessableEntity(
httpRequest: HttpRequest,
httpResponse: HttpResponse,
responseBodyJson: Option[ResponseBodyJson],
): GitHubError =
UnprocessableEntity(httpRequest: HttpRequest, httpResponse: HttpResponse, responseBodyJson)

def unexpectedFailure(httpError: HttpError): GitHubError =
UnexpectedFailure(httpError)

def render(gitHubError: GitHubError): String = gitHubError match {
case NoCredential =>
"No GitHub access credential found - Check out the document for GitHub Auth Token"
Expand Down Expand Up @@ -79,5 +131,116 @@ object GitHubError {
| ${GitCommandError.render(cause)}
|""".stripMargin

case NoReleaseCreated =>
"No GitHub release has been created"

case AbuseRateLimits(message, docUrl) =>
s"""$message
|For more details, visit $docUrl
|""".stripMargin

case RateLimitExceeded(rateLimit, remaining, reset, message, docUrl) =>
s"""$message
|The maximum number of requests per hour: $rateLimit
|The number of requests remaining in the current rate limit window: $remaining
|The rate limit window resets at ${reset.fold("\"No reset info provided by GitHub\"")(_.toString)}
|For more details, visit $docUrl
|""".stripMargin

case ForbiddenRequest(httpRequest, httpResponse) =>
s"""The request has been forbidden by GitHub API.
| Request: ${httpRequest.show}
|Response: ${httpResponse.show}
|""".stripMargin

case UnprocessableEntity(httpRequest, httpResponse, responseBodyJson) =>
s"""Unprocessable Entity:
|responseBody: ${responseBodyJson.fold("")(_.show)}
|---
|Request: ${httpRequest.show}
|Response: ${httpResponse.show}
|""".stripMargin

case UnexpectedFailure(httpError) =>
s"""Unexpected failure:
|${httpError.show}
|""".stripMargin

}

final case class ResponseBodyJson(message: String, documentationUrl: Option[String])
object ResponseBodyJson {
implicit val encoder: Encoder[ResponseBodyJson] =
responseBodyJson =>
Json.obj(
(List("message" -> Json.fromString(responseBodyJson.message)) ++
responseBodyJson
.documentationUrl
.toList
.map(documentationUrl => "documentation_url" -> Json.fromString(documentationUrl))): _*
)

implicit val decoder: Decoder[ResponseBodyJson] =
c =>
for {
message <- c.downField("message").as[String]
documentationUrl <- c.downField("documentation_url").as[Option[String]]
} yield ResponseBodyJson(message, documentationUrl)

implicit val show: Show[ResponseBodyJson] = encoder.apply(_).spaces2
}

def fromHttpError(httpError: HttpError): GitHubError = httpError match {
case HttpError.Forbidden(httpRequest, httpResponse @ HttpResponse(_, headers, Some(body))) =>
decode[ResponseBodyJson](body.body) match {
case Right(ResponseBodyJson(message, Some(docUrl))) =>
if (
message.contains("You have triggered an abuse detection mechanism") ||
docUrl.contains("abuse-rate-limits")
) {
GitHubError.abuseRateLimits(message, docUrl)
} else if (
message.contains("API rate limit exceeded") ||
docUrl.contains("rate-limiting")
) {
val rateLimit = httpResponse.findHeaderValueByName(_.equalsIgnoreCase("X-RateLimit-Limit")).map(_.toInt)
val remaining = httpResponse.findHeaderValueByName(_.equalsIgnoreCase("X-RateLimit-Remaining")).map(_.toInt)
val reset = httpResponse
.findHeaderValueByName(_.equalsIgnoreCase("X-RateLimit-Reset"))
.map(x => Instant.ofEpochSecond(x.toLong))

GitHubError.rateLimitExceeded(
rateLimit,
remaining,
reset,
message,
docUrl,
)
} else {
GitHubError.forbiddenRequest(httpRequest, httpResponse)
}
case Right(ResponseBodyJson(message, None)) =>
GitHubError.forbiddenRequest(httpRequest, httpResponse)

case Left(_) =>
GitHubError.forbiddenRequest(httpRequest, httpResponse)
}

case HttpError.UnprocessableEntity(request, response) =>
val responseBodyJson = response
.body
.flatMap(body =>
decode[ResponseBodyJson](body.body) match {
case Right(responseBodyJson) =>
responseBodyJson.some
case Left(err) =>
none[ResponseBodyJson]
}
)
GitHubError.unprocessableEntity(request, response, responseBodyJson)

case error =>
GitHubError.unexpectedFailure(error)
}

}
Loading

0 comments on commit edb158d

Please sign in to comment.