github4s.http.HttpClient.scala Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of github4s_2.12 Show documentation
Show all versions of github4s_2.12 Show documentation
Github API wrapper written in Scala
The newest version!
/*
* Copyright 2016-2024 47 Degrees Open Source
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package github4s.http
import cats.data.EitherT
import cats.effect.Resource
import cats.effect.kernel.Concurrent
import cats.syntax.all._
import github4s.GHError._
import github4s._
import github4s.algebras.{AccessHeader, AccessToken}
import github4s.domain.Pagination
import github4s.http.Http4sSyntax._
import io.circe.{Decoder, Encoder}
import org.http4s.circe.CirceEntityDecoder._
import org.http4s.circe.jsonOf
import org.http4s.client.Client
import org.http4s._
class HttpClient[F[_]: Concurrent] private (
client: Client[F],
val config: GithubConfig,
accessHeader: AccessHeader[F]
) {
import HttpClient._
import accessHeader._
def get[Res: Decoder](
method: String,
headers: Map[String, String] = Map.empty,
params: Map[String, String] = Map.empty,
pagination: Option[Pagination] = None
): F[GHResponse[Res]] =
withAccessHeader { authHeader =>
run[Unit, Res](
RequestBuilder(url = buildURL(method))
.withAuthHeader(authHeader)
.withHeaders(headers)
.withParams(
params ++ pagination.fold(Map.empty[String, String])(p =>
Map("page" -> p.page.toString, "per_page" -> p.per_page.toString)
)
)
)
}
def getWithoutResponse(
url: String,
headers: Map[String, String] = Map.empty
): F[GHResponse[Unit]] =
withAccessHeader(authHeader =>
runWithoutResponse[Unit](
RequestBuilder(buildURL(url)).withHeaders(headers).withAuthHeader(authHeader)
)
)
def patch[Req: Encoder, Res: Decoder](
method: String,
headers: Map[String, String] = Map.empty,
data: Req
): F[GHResponse[Res]] =
withAccessHeader(authHeader =>
run[Req, Res](
RequestBuilder(buildURL(method)).patchMethod
.withAuthHeader(authHeader)
.withHeaders(headers)
.withData(data)
)
)
def put[Req: Encoder, Res: Decoder](
url: String,
headers: Map[String, String] = Map(),
data: Req
): F[GHResponse[Res]] =
withAccessHeader(authHeader =>
run[Req, Res](
RequestBuilder(buildURL(url)).putMethod
.withAuthHeader(authHeader)
.withHeaders(headers)
.withData(data)
)
)
def post[Req: Encoder, Res: Decoder](
url: String,
headers: Map[String, String] = Map.empty,
data: Req
): F[GHResponse[Res]] =
withAccessHeader(authHeader =>
run[Req, Res](
RequestBuilder(buildURL(url)).postMethod
.withAuthHeader(authHeader)
.withHeaders(headers)
.withData(data)
)
)
def postAuth[Req: Encoder, Res: Decoder](
method: String,
headers: Map[String, String] = Map.empty,
data: Req
): F[GHResponse[Res]] =
run[Req, Res](RequestBuilder(buildURL(method)).postMethod.withHeaders(headers).withData(data))
def postOAuth[Res: Decoder](
url: String,
headers: Map[String, String] = Map.empty,
params: Map[String, String] = Map.empty
): F[GHResponse[Res]] =
run[Unit, Res](
RequestBuilder(url).postMethod
.withHeaders(Map("Accept" -> "application/json") ++ headers)
.withParams(params)
)
def delete(
url: String,
headers: Map[String, String] = Map.empty
): F[GHResponse[Unit]] =
withAccessHeader(authHeader =>
run[Unit, Unit](
RequestBuilder(buildURL(url)).deleteMethod.withHeaders(headers).withAuthHeader(authHeader)
)
)
def deleteWithResponse[Res: Decoder](
url: String,
headers: Map[String, String] = Map.empty
): F[GHResponse[Res]] =
withAccessHeader(authHeader =>
run[Unit, Res](
RequestBuilder(buildURL(url)).deleteMethod
.withAuthHeader(authHeader)
.withHeaders(headers)
)
)
def deleteWithBody[Req: Encoder, Res: Decoder](
url: String,
headers: Map[String, String] = Map.empty,
data: Req
): F[GHResponse[Res]] =
withAccessHeader(authHeader =>
run[Req, Res](
RequestBuilder(buildURL(url)).deleteMethod
.withAuthHeader(authHeader)
.withHeaders(headers)
.withData(data)
)
)
private def buildURL(method: String): String = s"${config.baseUrl}$method"
private def run[Req: Encoder, Res: Decoder](request: RequestBuilder[Req]): F[GHResponse[Res]] =
runRequest(request)
.use { response =>
buildResponse(response).map(GHResponse(_, response.status.code, response.headers.toMap))
}
private def runWithoutResponse[Req: Encoder](request: RequestBuilder[Req]): F[GHResponse[Unit]] =
runRequest(
request
).use { response =>
buildResponseFromEmpty(response).map(
GHResponse(_, response.status.code, response.headers.toMap)
)
}
private def runRequest[Req: Encoder](request: RequestBuilder[Req]): Resource[F, Response[F]] =
client
.run(
Request[F]()
.withMethod(request.httpVerb)
.withUri(request.toUri(config))
.withHeaders(Headers(config.toHeaderList) ++ Headers(request.toHeaderList))
.withJsonBody(request.data)
)
}
object HttpClient {
// the GitHub API sometimes returns [[BasicError]] when 404.
private[github4s] val notFoundDecoder: Decoder[GHError] =
implicitly[Decoder[NotFoundError]].widen.or(BasicError.basicErrorDecoder.widen)
private def notFoundEntityDecoder[F[_]: Concurrent]: EntityDecoder[F, GHError] =
jsonOf(implicitly, notFoundDecoder)
private[github4s] def buildResponse[F[_]: Concurrent, A: Decoder](
response: Response[F]
): F[Either[GHError, A]] =
(response.status.code match {
case i if Status.fromInt(i).exists(_.isSuccess) => response.attemptAs[A].map(_.asRight)
case 400 => response.attemptAs[BadRequestError].map(_.asLeft)
case 401 => response.attemptAs[UnauthorizedError].map(_.asLeft)
case 403 => response.attemptAs[ForbiddenError].map(_.asLeft)
case 404 => response.attemptAs[GHError](notFoundEntityDecoder).map(_.asLeft)
case 422 => response.attemptAs[UnprocessableEntityError].map(_.asLeft)
case 423 => response.attemptAs[RateLimitExceededError].map(_.asLeft)
case _ =>
EitherT
.right[DecodeFailure](responseBody(response))
.map(s =>
UnhandledResponseError(s"Unhandled status code ${response.status.code}", s).asLeft
)
}).fold(
e => (JsonParsingError(e): GHError).asLeft,
_.leftMap[GHError](identity)
)
private[github4s] def buildResponseFromEmpty[F[_]: Concurrent](
response: Response[F]
): F[Either[GHError, Unit]] =
if (response.status.isSuccess)
Either.unit[GHError].pure[F]
else
buildResponse[F, Unit](response)
private def responseBody[F[_]: Concurrent](response: Response[F]): F[String] =
response.bodyText.compile.foldMonoid
def apply[F[_]: Concurrent](
client: Client[F],
config: GithubConfig,
accessToken: AccessToken[F]
): HttpClient[F] =
new HttpClient[F](client, config, AccessHeader.from(accessToken))
def apply[F[_]: Concurrent](
client: Client[F],
config: GithubConfig,
accessHeader: AccessHeader[F]
): HttpClient[F] =
new HttpClient[F](client, config, accessHeader)
}