Skip to content

Commit

Permalink
Issue #146 - Add upload assets to GitHub release
Browse files Browse the repository at this point in the history
  • Loading branch information
kevin-lee committed Jan 28, 2021
1 parent a5d52aa commit 04b73cd
Show file tree
Hide file tree
Showing 5 changed files with 342 additions and 66 deletions.
46 changes: 43 additions & 3 deletions src/main/scala/kevinlee/github/GitHubApi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ trait GitHubApi[F[_]] {
repo: GitHubRepoWithAuth,
): F[Either[GitHubError, Option[GitHubRelease.Response]]]

def uploadAssetToRelease(
params: GitHubRelease.UploadAssetParams,
repo: GitHubRepoWithAuth,
): F[Either[GitHubError, Option[GitHubRelease.Asset]]]

}

object GitHubApi {
Expand All @@ -34,7 +39,8 @@ object GitHubApi {

final class GitHubApiF[F[_]: Monad](val httpClient: HttpClient[F]) extends GitHubApi[F] {
// TODO: make it configurable
val baseUrl: String = "https://api.github.com"
val baseUrl: String = "https://api.github.com"
val baseUploadUrl: String = "https://uploads.github.com"

val DefaultAccept: String = "application/vnd.github.v3+json"

Expand Down Expand Up @@ -66,7 +72,7 @@ object GitHubApi {
): F[Either[GitHubError, Option[GitHubRelease.Response]]] = {
val url = s"$baseUrl/repos/${repo.gitHubRepo.org.org}/${repo.gitHubRepo.repo.repo}/releases"
val httpRequest = HttpRequest
.withHeadersAndBody[GitHubRelease.CreateRequestParams](
.withHeadersAndJsonBody[GitHubRelease.CreateRequestParams](
HttpRequest.Method.post,
HttpRequest.Uri(url),
HttpRequest.Header("accept" -> DefaultAccept) ::
Expand All @@ -91,7 +97,7 @@ object GitHubApi {
val url =
s"$baseUrl/repos/${repo.gitHubRepo.org.org}/${repo.gitHubRepo.repo.repo}/releases/${params.releaseId.releaseId}"
val httpRequest = HttpRequest
.withHeadersAndBody[GitHubRelease.UpdateRequestParams](
.withHeadersAndJsonBody[GitHubRelease.UpdateRequestParams](
HttpRequest.Method.patch,
HttpRequest.Uri(url),
HttpRequest.Header("accept" -> DefaultAccept) ::
Expand All @@ -109,6 +115,40 @@ object GitHubApi {
)
}

override def uploadAssetToRelease(
params: GitHubRelease.UploadAssetParams,
repo: GitHubRepoWithAuth,
): F[Either[GitHubError, Option[GitHubRelease.Asset]]] = {
val url =
s"$baseUploadUrl/repos/${repo.gitHubRepo.org.org}/${repo.gitHubRepo.repo.repo}/releases/${params.releaseId.releaseId}/assets"
val httpRequest = HttpRequest
.withHeadersParamsAndMultipartBody(
HttpRequest.Method.post,
HttpRequest.Uri(url),
repo
.accessToken
.toHeaderList,
List(
HttpRequest.Param(
"name" -> params.name.assetName
)
) ++ params.label
.map(assetLabel =>
HttpRequest.Param(
"label" -> assetLabel.assetLabel
)
)
.toList,
params.multipartData,
)
httpClient
.request[Option[GitHubRelease.Asset]](httpRequest)
.map(
_.toOptionIfNotFound
.leftMap(GitHubError.fromHttpError)
.flatMap(res => res.asRight[GitHubError])
)
}
}

}
32 changes: 32 additions & 0 deletions src/main/scala/kevinlee/github/data/GitHubRelease.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import io.circe.syntax._
import io.circe.{Decoder, Encoder, Json}
import io.estatico.newtype.macros.{newsubtype, newtype}
import kevinlee.git.Git
import kevinlee.http.HttpRequest

import java.time.Instant

Expand Down Expand Up @@ -50,6 +51,7 @@ object GitHubRelease {
} yield CreateRequestParams(tagName, name, body, draft, prerelease)

}

final case class UpdateRequestParams(
tagName: Git.TagName,
releaseId: ReleaseId,
Expand Down Expand Up @@ -87,6 +89,18 @@ object GitHubRelease {

}

final case class UploadAssetParams(
tagName: Git.TagName,
releaseId: ReleaseId,
name: UploadAssetParams.AssetName,
label: Option[UploadAssetParams.AssetLabel],
multipartData: HttpRequest.MultipartData,
)
object UploadAssetParams {
@newtype case class AssetName(assetName: String)
@newtype case class AssetLabel(assetLabel: String)
}

@newtype case class Accept(accept: String)
object Accept {
implicit val encoder: Encoder[Accept] = deriving
Expand Down Expand Up @@ -285,6 +299,8 @@ object GitHubRelease {
final case class Response(
id: Response.Id,
uri: Response.Url,
assetsUrl: Response.AssetsUrl,
uploadUrl: Response.UploadUrl,
author: User,
tagName: Git.TagName,
name: ReleaseName,
Expand All @@ -308,6 +324,16 @@ object GitHubRelease {
implicit val encoder: Encoder[Url] = deriving
implicit val decoder: Decoder[Url] = deriving
}
@newtype case class AssetsUrl(assetsUrl: String)
object AssetsUrl {
implicit val encoder: Encoder[AssetsUrl] = deriving
implicit val decoder: Decoder[AssetsUrl] = deriving
}
@newtype case class UploadUrl(uploadUrl: String)
object UploadUrl {
implicit val encoder: Encoder[UploadUrl] = deriving
implicit val decoder: Decoder[UploadUrl] = deriving
}
@newtype case class CreatedAt(createdAt: Instant)
object CreatedAt {
implicit val encoder: Encoder[CreatedAt] = deriving
Expand All @@ -324,6 +350,8 @@ object GitHubRelease {
Json.obj(
"id" -> response.id.id.asJson,
"url" -> response.uri.url.asJson,
"assets_url" -> response.assetsUrl.assetsUrl.asJson,
"upload_url" -> response.uploadUrl.uploadUrl.asJson,
"author" -> response.author.asJson,
"tag_name" -> response.tagName.asJson,
"name" -> response.name.asJson,
Expand All @@ -340,6 +368,8 @@ object GitHubRelease {
for {
id <- c.downField("id").as[Id]
url <- c.downField("url").as[Url]
assetsUrl <- c.downField("assets_url").as[AssetsUrl]
uploadUrl <- c.downField("upload_url").as[UploadUrl]
author <- c.downField("author").as[User]
tagName <- c.downField("tag_name").as[Git.TagName]
name <- c.downField("name").as[ReleaseName]
Expand All @@ -352,6 +382,8 @@ object GitHubRelease {
} yield Response(
id,
url,
assetsUrl,
uploadUrl,
author,
tagName,
name,
Expand Down
26 changes: 18 additions & 8 deletions src/main/scala/kevinlee/http/HttpClient.scala
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,15 @@ trait HttpClient[F[_]] {

object HttpClient {

def apply[F[_]: Monad: EffectConstructor: ConcurrentEffect: Log](client: Client[F]): HttpClient[F] =
def apply[
F[_]: Monad: EffectConstructor: ConcurrentEffect: ContextShift: Log
](client: Client[F]): HttpClient[F] =
new HttpClientF[F](client)

@SuppressWarnings(Array("org.wartremover.warts.Any", "org.wartremover.warts.Nothing"))
final class HttpClientF[F[_]: Monad: EffectConstructor: ConcurrentEffect: Log](
final class HttpClientF[
F[_]: Monad: EffectConstructor: ConcurrentEffect: ContextShift: Log
](
client: Client[F]
) extends HttpClient[F] {

Expand All @@ -47,18 +51,24 @@ object HttpClient {
sendRequest[A](httpRequest).value

private[this] def sendRequest[A](
httpRequest: HttpRequest,
httpRequest: HttpRequest
)(
implicit entityDecoderA: Decoder[A]
): EitherT[F, HttpError, A] =
for {
request <- EitherT.fromEither(
HttpRequest.toHttp4s(
httpRequest,
)
httpRequest.toHttp4s[F]
)

postProcessedReq <- eitherTRightF[HttpError](postProcessRequest(request, implicitly[EntityDecoder[F, A]].consumes))
postProcessedReq <- eitherTRightF[HttpError](
postProcessRequest(
request,
if (httpRequest.isBodyMultipart)
Set.empty[MediaRange]
else
implicitly[EntityDecoder[F, A]].consumes,
)
)

res <-
log(
Expand Down Expand Up @@ -87,7 +97,7 @@ object HttpClient {
private[this] def responseHandler[A](
httpRequest: HttpRequest
)(
implicit decoderA: Decoder[A],
implicit decoderA: Decoder[A]
): Response[F] => F[Either[HttpError, A]] = {
case Successful(successResponse) =>
implicitly[EntityDecoder[F, A]]
Expand Down
9 changes: 8 additions & 1 deletion src/main/scala/kevinlee/http/HttpError.scala
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ object HttpError {
httpRequest: HttpRequest,
httpResponse: HttpResponse,
) extends HttpError
final case class MethodUnsupportedForMultipart(
httpRequest: HttpRequest
) extends HttpError

def invalidUri(uriString: String, errorMessage: String): HttpError = InvalidUri(uriString, errorMessage)

Expand Down Expand Up @@ -77,11 +80,15 @@ object HttpError {
error.asLeft[Option[A]]
}

def forbidden(httpRequest: HttpRequest, httpResponse: HttpResponse): HttpError = Forbidden(httpRequest, httpResponse)
def forbidden(httpRequest: HttpRequest, httpResponse: HttpResponse): HttpError =
Forbidden(httpRequest, httpResponse)

def unprocessableEntity(httpRequest: HttpRequest, httpResponse: HttpResponse): HttpError =
UnprocessableEntity(httpRequest, httpResponse)

def methodUnsupportedForMultipart(httpRequest: HttpRequest): HttpError =
MethodUnsupportedForMultipart(httpRequest)

@SuppressWarnings(Array("org.wartremover.warts.ToString"))
implicit final val show: Show[HttpError] = _.toString

Expand Down
Loading

0 comments on commit 04b73cd

Please sign in to comment.