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

make accessToken an io to use gh app's expiring tokens #567

Merged
merged 13 commits into from Nov 17, 2020
5 changes: 3 additions & 2 deletions github4s/src/main/scala/github4s/Github.scala
Expand Up @@ -18,12 +18,13 @@ package github4s

import cats.effect.Sync
import github4s.algebras._
import github4s.interpreters.StaticAccessToken
import github4s.modules._
import org.http4s.client.Client

class Github[F[_]: Sync](
client: Client[F],
accessToken: Option[String]
accessToken: AccessToken[F]
)(implicit config: GithubConfig)
extends GithubAPIs[F] {

Expand All @@ -48,5 +49,5 @@ object Github {
client: Client[F],
accessToken: Option[String] = None
)(implicit config: GithubConfig): Github[F] =
new Github[F](client, accessToken)
new Github[F](client, new StaticAccessToken(accessToken))
}
17 changes: 17 additions & 0 deletions github4s/src/main/scala/github4s/algebras/AccessToken.scala
@@ -0,0 +1,17 @@
package github4s.algebras

import github4s.GHResponse

/**
* Source of static or expiring github tokens
*
* For github app authentication you'd want to create a token source
* which calls github's installation authentication api with a jwt token, generated from a private key
* These tokens have a 1h lifetime, so it's a good idea to handle expired tokens here as well
*
* @see https://docs.github.com/en/free-pro-team@latest/developers/apps/authenticating-with-github-apps
*/
trait AccessToken[F[_]] {

def withAccessToken[T](f: Option[String] => F[GHResponse[T]]): F[GHResponse[T]]
}
106 changes: 60 additions & 46 deletions github4s/src/main/scala/github4s/http/HttpClient.scala
Expand Up @@ -23,6 +23,7 @@ import cats.syntax.either._
import cats.syntax.functor._
import github4s.GHError._
import github4s._
import github4s.algebras.AccessToken
import github4s.domain.Pagination
import github4s.http.Http4sSyntax._
import io.circe.{Decoder, Encoder}
Expand All @@ -31,73 +32,83 @@ import org.http4s.circe.jsonOf
import org.http4s.client.Client
import org.http4s.{EntityDecoder, Request, Response, Status}

class HttpClient[F[_]: Sync](client: Client[F], val config: GithubConfig) {
class HttpClient[F[_]: Sync](
client: Client[F],
val config: GithubConfig,
accessTokens: AccessToken[F]
) {
import HttpClient._
import accessTokens._

def get[Res: Decoder](
accessToken: Option[String] = None,
method: String,
headers: Map[String, String] = Map.empty,
params: Map[String, String] = Map.empty,
pagination: Option[Pagination] = None
): F[GHResponse[Res]] =
run[Unit, Res](
RequestBuilder(url = buildURL(method))
.withAuth(accessToken)
.withHeaders(headers)
.withParams(
params ++ pagination.fold(Map.empty[String, String])(p =>
Map("page" -> p.page.toString, "per_page" -> p.per_page.toString)
withAccessToken { accessToken =>
run[Unit, Res](
RequestBuilder(url = buildURL(method))
.withAuth(accessToken)
.withHeaders(headers)
.withParams(
params ++ pagination.fold(Map.empty[String, String])(p =>
Map("page" -> p.page.toString, "per_page" -> p.per_page.toString)
)
)
)
)
)
}

def getWithoutResponse(
accessToken: Option[String] = None,
url: String,
headers: Map[String, String] = Map.empty
): F[GHResponse[Unit]] =
runWithoutResponse[Unit](
RequestBuilder(buildURL(url)).withHeaders(headers).withAuth(accessToken)
withAccessToken(accessToken =>
runWithoutResponse[Unit](
RequestBuilder(buildURL(url)).withHeaders(headers).withAuth(accessToken)
)
)

def patch[Req: Encoder, Res: Decoder](
accessToken: Option[String] = None,
method: String,
headers: Map[String, String] = Map.empty,
data: Req
): F[GHResponse[Res]] =
run[Req, Res](
RequestBuilder(buildURL(method)).patchMethod
.withAuth(accessToken)
.withHeaders(headers)
.withData(data)
withAccessToken(accessToken =>
run[Req, Res](
RequestBuilder(buildURL(method)).patchMethod
.withAuth(accessToken)
.withHeaders(headers)
.withData(data)
)
)

def put[Req: Encoder, Res: Decoder](
accessToken: Option[String] = None,
url: String,
headers: Map[String, String] = Map(),
data: Req
): F[GHResponse[Res]] =
run[Req, Res](
RequestBuilder(buildURL(url)).putMethod
.withAuth(accessToken)
.withHeaders(headers)
.withData(data)
withAccessToken(accessToken =>
run[Req, Res](
RequestBuilder(buildURL(url)).putMethod
.withAuth(accessToken)
.withHeaders(headers)
.withData(data)
)
)

def post[Req: Encoder, Res: Decoder](
accessToken: Option[String] = None,
url: String,
headers: Map[String, String] = Map.empty,
data: Req
): F[GHResponse[Res]] =
run[Req, Res](
RequestBuilder(buildURL(url)).postMethod
.withAuth(accessToken)
.withHeaders(headers)
.withData(data)
withAccessToken(accessToken =>
run[Req, Res](
RequestBuilder(buildURL(url)).postMethod
.withAuth(accessToken)
.withHeaders(headers)
.withData(data)
)
)

def postAuth[Req: Encoder, Res: Decoder](
Expand All @@ -119,36 +130,39 @@ class HttpClient[F[_]: Sync](client: Client[F], val config: GithubConfig) {
)

def delete(
accessToken: Option[String] = None,
url: String,
headers: Map[String, String] = Map.empty
): F[GHResponse[Unit]] =
run[Unit, Unit](
RequestBuilder(buildURL(url)).deleteMethod.withHeaders(headers).withAuth(accessToken)
withAccessToken(accessToken =>
run[Unit, Unit](
RequestBuilder(buildURL(url)).deleteMethod.withHeaders(headers).withAuth(accessToken)
)
)

def deleteWithResponse[Res: Decoder](
accessToken: Option[String] = None,
url: String,
headers: Map[String, String] = Map.empty
): F[GHResponse[Res]] =
run[Unit, Res](
RequestBuilder(buildURL(url)).deleteMethod
.withAuth(accessToken)
.withHeaders(headers)
withAccessToken(accessToken =>
run[Unit, Res](
RequestBuilder(buildURL(url)).deleteMethod
.withAuth(accessToken)
.withHeaders(headers)
)
)

def deleteWithBody[Req: Encoder, Res: Decoder](
accessToken: Option[String] = None,
url: String,
headers: Map[String, String] = Map.empty,
data: Req
): F[GHResponse[Res]] =
run[Req, Res](
RequestBuilder(buildURL(url)).deleteMethod
.withAuth(accessToken)
.withHeaders(headers)
.withData(data)
withAccessToken(accessToken =>
run[Req, Res](
RequestBuilder(buildURL(url)).deleteMethod
.withAuth(accessToken)
.withHeaders(headers)
.withData(data)
)
)

private def buildURL(method: String): String = s"${config.baseUrl}$method"
Expand Down
Expand Up @@ -23,8 +23,7 @@ import github4s.Encoders._
import github4s.GHResponse
import github4s.http.HttpClient

class ActivitiesInterpreter[F[_]](implicit client: HttpClient[F], accessToken: Option[String])
extends Activities[F] {
class ActivitiesInterpreter[F[_]](implicit client: HttpClient[F]) extends Activities[F] {

private val timelineHeader = ("Accept" -> "application/vnd.github.v3.star+json")

Expand All @@ -35,7 +34,6 @@ class ActivitiesInterpreter[F[_]](implicit client: HttpClient[F], accessToken: O
headers: Map[String, String]
): F[GHResponse[Subscription]] =
client.put[SubscriptionRequest, Subscription](
accessToken = accessToken,
url = s"notifications/threads/$id/subscription",
headers = headers,
data = SubscriptionRequest(subscribed, ignored)
Expand All @@ -49,7 +47,6 @@ class ActivitiesInterpreter[F[_]](implicit client: HttpClient[F], accessToken: O
headers: Map[String, String]
): F[GHResponse[List[Stargazer]]] =
client.get[List[Stargazer]](
accessToken,
s"repos/$owner/$repo/stargazers",
if (timeline) headers + timelineHeader else headers,
pagination = pagination
Expand All @@ -64,7 +61,6 @@ class ActivitiesInterpreter[F[_]](implicit client: HttpClient[F], accessToken: O
headers: Map[String, String]
): F[GHResponse[List[StarredRepository]]] =
client.get[List[StarredRepository]](
accessToken,
s"users/$username/starred",
if (timeline) headers + timelineHeader else headers,
Map(
Expand Down
Expand Up @@ -16,15 +16,14 @@

package github4s.interpreters

import github4s.algebras.Gists
import github4s.Decoders._
import github4s.domain._
import github4s.Encoders._
import github4s.GHResponse
import github4s.algebras.Gists
import github4s.domain._
import github4s.http.HttpClient

class GistsInterpreter[F[_]](implicit client: HttpClient[F], accessToken: Option[String])
extends Gists[F] {
class GistsInterpreter[F[_]](implicit client: HttpClient[F]) extends Gists[F] {

override def newGist(
description: String,
Expand All @@ -33,7 +32,6 @@ class GistsInterpreter[F[_]](implicit client: HttpClient[F], accessToken: Option
headers: Map[String, String]
): F[GHResponse[Gist]] =
client.post[NewGistRequest, Gist](
accessToken,
"gists",
headers,
data = NewGistRequest(description, public, files)
Expand All @@ -45,7 +43,6 @@ class GistsInterpreter[F[_]](implicit client: HttpClient[F], accessToken: Option
headers: Map[String, String]
): F[GHResponse[Gist]] =
client.get[Gist](
accessToken,
("gists" :: gistId :: sha.toList).mkString("/"),
headers
)
Expand All @@ -57,7 +54,6 @@ class GistsInterpreter[F[_]](implicit client: HttpClient[F], accessToken: Option
headers: Map[String, String]
): F[GHResponse[Gist]] =
client.patch[EditGistRequest, Gist](
accessToken,
s"gists/$gistId",
headers,
data = EditGistRequest(description, files)
Expand Down
Expand Up @@ -16,16 +16,15 @@

package github4s.interpreters

import github4s.http.HttpClient
import github4s.algebras.GitData
import cats.data.NonEmptyList
import github4s.GHResponse
import github4s.domain._
import github4s.Decoders._
import github4s.Encoders._
import github4s.GHResponse
import github4s.algebras.GitData
import github4s.domain._
import github4s.http.HttpClient

class GitDataInterpreter[F[_]](implicit client: HttpClient[F], accessToken: Option[String])
extends GitData[F] {
class GitDataInterpreter[F[_]](implicit client: HttpClient[F]) extends GitData[F] {
override def getReference(
owner: String,
repo: String,
Expand All @@ -34,7 +33,6 @@ class GitDataInterpreter[F[_]](implicit client: HttpClient[F], accessToken: Opti
headers: Map[String, String]
): F[GHResponse[NonEmptyList[Ref]]] =
client.get[NonEmptyList[Ref]](
accessToken,
s"repos/$owner/$repo/git/refs/$ref",
headers,
pagination = pagination
Expand All @@ -48,7 +46,6 @@ class GitDataInterpreter[F[_]](implicit client: HttpClient[F], accessToken: Opti
headers: Map[String, String]
): F[GHResponse[Ref]] =
client.post[CreateReferenceRequest, Ref](
accessToken,
s"repos/$owner/$repo/git/refs",
headers,
CreateReferenceRequest(ref, sha)
Expand All @@ -63,7 +60,6 @@ class GitDataInterpreter[F[_]](implicit client: HttpClient[F], accessToken: Opti
headers: Map[String, String]
): F[GHResponse[Ref]] =
client.patch[UpdateReferenceRequest, Ref](
accessToken,
s"repos/$owner/$repo/git/refs/$ref",
headers,
UpdateReferenceRequest(sha, force)
Expand All @@ -75,7 +71,7 @@ class GitDataInterpreter[F[_]](implicit client: HttpClient[F], accessToken: Opti
sha: String,
headers: Map[String, String]
): F[GHResponse[RefCommit]] =
client.get[RefCommit](accessToken, s"repos/$owner/$repo/git/commits/$sha", headers)
client.get[RefCommit](s"repos/$owner/$repo/git/commits/$sha", headers)

override def createCommit(
owner: String,
Expand All @@ -87,7 +83,6 @@ class GitDataInterpreter[F[_]](implicit client: HttpClient[F], accessToken: Opti
headers: Map[String, String]
): F[GHResponse[RefCommit]] =
client.post[NewCommitRequest, RefCommit](
accessToken,
s"repos/$owner/$repo/git/commits",
headers,
NewCommitRequest(message, tree, parents, author)
Expand All @@ -99,7 +94,7 @@ class GitDataInterpreter[F[_]](implicit client: HttpClient[F], accessToken: Opti
fileSha: String,
headers: Map[String, String]
): F[GHResponse[BlobContent]] =
client.get[BlobContent](accessToken, s"repos/$owner/$repo/git/blobs/$fileSha", headers)
client.get[BlobContent](s"repos/$owner/$repo/git/blobs/$fileSha", headers)

override def createBlob(
owner: String,
Expand All @@ -109,7 +104,6 @@ class GitDataInterpreter[F[_]](implicit client: HttpClient[F], accessToken: Opti
headers: Map[String, String]
): F[GHResponse[RefInfo]] =
client.post[NewBlobRequest, RefInfo](
accessToken,
s"repos/$owner/$repo/git/blobs",
headers,
NewBlobRequest(content, encoding)
Expand All @@ -123,7 +117,6 @@ class GitDataInterpreter[F[_]](implicit client: HttpClient[F], accessToken: Opti
headers: Map[String, String]
): F[GHResponse[TreeResult]] =
client.get[TreeResult](
accessToken,
s"repos/$owner/$repo/git/trees/$sha",
headers,
(if (recursive) Map("recursive" -> "1") else Map.empty)
Expand All @@ -137,7 +130,6 @@ class GitDataInterpreter[F[_]](implicit client: HttpClient[F], accessToken: Opti
headers: Map[String, String]
): F[GHResponse[TreeResult]] =
client.post[NewTreeRequest, TreeResult](
accessToken,
s"repos/$owner/$repo/git/trees",
headers,
NewTreeRequest(treeDataList, baseTree)
Expand All @@ -154,7 +146,6 @@ class GitDataInterpreter[F[_]](implicit client: HttpClient[F], accessToken: Opti
headers: Map[String, String]
): F[GHResponse[Tag]] =
client.post[NewTagRequest, Tag](
accessToken,
s"repos/$owner/$repo/git/tags",
headers,
NewTagRequest(tag, message, objectSha, objectType, author)
Expand Down