Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue #146 - Some changes in existing types including errors #149

Merged
merged 1 commit into from
Jan 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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