Skip to content

Commit df21046

Browse files
reimaiBenFradet
andauthored
make accessToken an io to use gh app's expiring tokens (#567)
* make accessToken an io to use gh app's expiring tokens * fix format * fix tests * fix format * fix unused import * Revert "fix unused import" This reverts commit 0845bc9 * fix unused import * * redo AccessTokens making it a function so that GithubApp implementation could catch an expired token exception * unused import * * rename AccessTokens * add doc * Apply suggestions from code review Co-authored-by: Ben Fradet <benjamin.fradet@gmail.com> * review fix Co-authored-by: Ben Fradet <benjamin.fradet@gmail.com>
1 parent 965fba3 commit df21046

28 files changed

Lines changed: 174 additions & 203 deletions

github4s/src/main/scala/github4s/Github.scala

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,13 @@ package github4s
1818

1919
import cats.effect.Sync
2020
import github4s.algebras._
21+
import github4s.interpreters.StaticAccessToken
2122
import github4s.modules._
2223
import org.http4s.client.Client
2324

2425
class Github[F[_]: Sync](
2526
client: Client[F],
26-
accessToken: Option[String]
27+
accessToken: AccessToken[F]
2728
)(implicit config: GithubConfig)
2829
extends GithubAPIs[F] {
2930

@@ -48,5 +49,5 @@ object Github {
4849
client: Client[F],
4950
accessToken: Option[String] = None
5051
)(implicit config: GithubConfig): Github[F] =
51-
new Github[F](client, accessToken)
52+
new Github[F](client, new StaticAccessToken(accessToken))
5253
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package github4s.algebras
2+
3+
import github4s.GHResponse
4+
5+
/**
6+
* Source of static or expiring github tokens
7+
*
8+
* For github app authentication you'd want to create a token source
9+
* which calls github's installation authentication api with a jwt token, generated from a private key
10+
* These tokens have a 1h lifetime, so it's a good idea to handle expired tokens here as well
11+
*
12+
* @see https://docs.github.com/en/free-pro-team@latest/developers/apps/authenticating-with-github-apps
13+
*/
14+
trait AccessToken[F[_]] {
15+
16+
def withAccessToken[T](f: Option[String] => F[GHResponse[T]]): F[GHResponse[T]]
17+
}

github4s/src/main/scala/github4s/http/HttpClient.scala

Lines changed: 60 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import cats.syntax.either._
2323
import cats.syntax.functor._
2424
import github4s.GHError._
2525
import github4s._
26+
import github4s.algebras.AccessToken
2627
import github4s.domain.Pagination
2728
import github4s.http.Http4sSyntax._
2829
import io.circe.{Decoder, Encoder}
@@ -31,73 +32,83 @@ import org.http4s.circe.jsonOf
3132
import org.http4s.client.Client
3233
import org.http4s.{EntityDecoder, Request, Response, Status}
3334

34-
class HttpClient[F[_]: Sync](client: Client[F], val config: GithubConfig) {
35+
class HttpClient[F[_]: Sync](
36+
client: Client[F],
37+
val config: GithubConfig,
38+
accessTokens: AccessToken[F]
39+
) {
3540
import HttpClient._
41+
import accessTokens._
3642

3743
def get[Res: Decoder](
38-
accessToken: Option[String] = None,
3944
method: String,
4045
headers: Map[String, String] = Map.empty,
4146
params: Map[String, String] = Map.empty,
4247
pagination: Option[Pagination] = None
4348
): F[GHResponse[Res]] =
44-
run[Unit, Res](
45-
RequestBuilder(url = buildURL(method))
46-
.withAuth(accessToken)
47-
.withHeaders(headers)
48-
.withParams(
49-
params ++ pagination.fold(Map.empty[String, String])(p =>
50-
Map("page" -> p.page.toString, "per_page" -> p.per_page.toString)
49+
withAccessToken { accessToken =>
50+
run[Unit, Res](
51+
RequestBuilder(url = buildURL(method))
52+
.withAuth(accessToken)
53+
.withHeaders(headers)
54+
.withParams(
55+
params ++ pagination.fold(Map.empty[String, String])(p =>
56+
Map("page" -> p.page.toString, "per_page" -> p.per_page.toString)
57+
)
5158
)
52-
)
53-
)
59+
)
60+
}
5461

5562
def getWithoutResponse(
56-
accessToken: Option[String] = None,
5763
url: String,
5864
headers: Map[String, String] = Map.empty
5965
): F[GHResponse[Unit]] =
60-
runWithoutResponse[Unit](
61-
RequestBuilder(buildURL(url)).withHeaders(headers).withAuth(accessToken)
66+
withAccessToken(accessToken =>
67+
runWithoutResponse[Unit](
68+
RequestBuilder(buildURL(url)).withHeaders(headers).withAuth(accessToken)
69+
)
6270
)
6371

6472
def patch[Req: Encoder, Res: Decoder](
65-
accessToken: Option[String] = None,
6673
method: String,
6774
headers: Map[String, String] = Map.empty,
6875
data: Req
6976
): F[GHResponse[Res]] =
70-
run[Req, Res](
71-
RequestBuilder(buildURL(method)).patchMethod
72-
.withAuth(accessToken)
73-
.withHeaders(headers)
74-
.withData(data)
77+
withAccessToken(accessToken =>
78+
run[Req, Res](
79+
RequestBuilder(buildURL(method)).patchMethod
80+
.withAuth(accessToken)
81+
.withHeaders(headers)
82+
.withData(data)
83+
)
7584
)
7685

7786
def put[Req: Encoder, Res: Decoder](
78-
accessToken: Option[String] = None,
7987
url: String,
8088
headers: Map[String, String] = Map(),
8189
data: Req
8290
): F[GHResponse[Res]] =
83-
run[Req, Res](
84-
RequestBuilder(buildURL(url)).putMethod
85-
.withAuth(accessToken)
86-
.withHeaders(headers)
87-
.withData(data)
91+
withAccessToken(accessToken =>
92+
run[Req, Res](
93+
RequestBuilder(buildURL(url)).putMethod
94+
.withAuth(accessToken)
95+
.withHeaders(headers)
96+
.withData(data)
97+
)
8898
)
8999

90100
def post[Req: Encoder, Res: Decoder](
91-
accessToken: Option[String] = None,
92101
url: String,
93102
headers: Map[String, String] = Map.empty,
94103
data: Req
95104
): F[GHResponse[Res]] =
96-
run[Req, Res](
97-
RequestBuilder(buildURL(url)).postMethod
98-
.withAuth(accessToken)
99-
.withHeaders(headers)
100-
.withData(data)
105+
withAccessToken(accessToken =>
106+
run[Req, Res](
107+
RequestBuilder(buildURL(url)).postMethod
108+
.withAuth(accessToken)
109+
.withHeaders(headers)
110+
.withData(data)
111+
)
101112
)
102113

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

121132
def delete(
122-
accessToken: Option[String] = None,
123133
url: String,
124134
headers: Map[String, String] = Map.empty
125135
): F[GHResponse[Unit]] =
126-
run[Unit, Unit](
127-
RequestBuilder(buildURL(url)).deleteMethod.withHeaders(headers).withAuth(accessToken)
136+
withAccessToken(accessToken =>
137+
run[Unit, Unit](
138+
RequestBuilder(buildURL(url)).deleteMethod.withHeaders(headers).withAuth(accessToken)
139+
)
128140
)
129141

130142
def deleteWithResponse[Res: Decoder](
131-
accessToken: Option[String] = None,
132143
url: String,
133144
headers: Map[String, String] = Map.empty
134145
): F[GHResponse[Res]] =
135-
run[Unit, Res](
136-
RequestBuilder(buildURL(url)).deleteMethod
137-
.withAuth(accessToken)
138-
.withHeaders(headers)
146+
withAccessToken(accessToken =>
147+
run[Unit, Res](
148+
RequestBuilder(buildURL(url)).deleteMethod
149+
.withAuth(accessToken)
150+
.withHeaders(headers)
151+
)
139152
)
140153

141154
def deleteWithBody[Req: Encoder, Res: Decoder](
142-
accessToken: Option[String] = None,
143155
url: String,
144156
headers: Map[String, String] = Map.empty,
145157
data: Req
146158
): F[GHResponse[Res]] =
147-
run[Req, Res](
148-
RequestBuilder(buildURL(url)).deleteMethod
149-
.withAuth(accessToken)
150-
.withHeaders(headers)
151-
.withData(data)
159+
withAccessToken(accessToken =>
160+
run[Req, Res](
161+
RequestBuilder(buildURL(url)).deleteMethod
162+
.withAuth(accessToken)
163+
.withHeaders(headers)
164+
.withData(data)
165+
)
152166
)
153167

154168
private def buildURL(method: String): String = s"${config.baseUrl}$method"

github4s/src/main/scala/github4s/interpreters/ActivitiesInterpreter.scala

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,7 @@ import github4s.Encoders._
2323
import github4s.GHResponse
2424
import github4s.http.HttpClient
2525

26-
class ActivitiesInterpreter[F[_]](implicit client: HttpClient[F], accessToken: Option[String])
27-
extends Activities[F] {
26+
class ActivitiesInterpreter[F[_]](implicit client: HttpClient[F]) extends Activities[F] {
2827

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

@@ -35,7 +34,6 @@ class ActivitiesInterpreter[F[_]](implicit client: HttpClient[F], accessToken: O
3534
headers: Map[String, String]
3635
): F[GHResponse[Subscription]] =
3736
client.put[SubscriptionRequest, Subscription](
38-
accessToken = accessToken,
3937
url = s"notifications/threads/$id/subscription",
4038
headers = headers,
4139
data = SubscriptionRequest(subscribed, ignored)
@@ -49,7 +47,6 @@ class ActivitiesInterpreter[F[_]](implicit client: HttpClient[F], accessToken: O
4947
headers: Map[String, String]
5048
): F[GHResponse[List[Stargazer]]] =
5149
client.get[List[Stargazer]](
52-
accessToken,
5350
s"repos/$owner/$repo/stargazers",
5451
if (timeline) headers + timelineHeader else headers,
5552
pagination = pagination
@@ -64,7 +61,6 @@ class ActivitiesInterpreter[F[_]](implicit client: HttpClient[F], accessToken: O
6461
headers: Map[String, String]
6562
): F[GHResponse[List[StarredRepository]]] =
6663
client.get[List[StarredRepository]](
67-
accessToken,
6864
s"users/$username/starred",
6965
if (timeline) headers + timelineHeader else headers,
7066
Map(

github4s/src/main/scala/github4s/interpreters/GistsInterpreter.scala

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,14 @@
1616

1717
package github4s.interpreters
1818

19-
import github4s.algebras.Gists
2019
import github4s.Decoders._
21-
import github4s.domain._
2220
import github4s.Encoders._
2321
import github4s.GHResponse
22+
import github4s.algebras.Gists
23+
import github4s.domain._
2424
import github4s.http.HttpClient
2525

26-
class GistsInterpreter[F[_]](implicit client: HttpClient[F], accessToken: Option[String])
27-
extends Gists[F] {
26+
class GistsInterpreter[F[_]](implicit client: HttpClient[F]) extends Gists[F] {
2827

2928
override def newGist(
3029
description: String,
@@ -33,7 +32,6 @@ class GistsInterpreter[F[_]](implicit client: HttpClient[F], accessToken: Option
3332
headers: Map[String, String]
3433
): F[GHResponse[Gist]] =
3534
client.post[NewGistRequest, Gist](
36-
accessToken,
3735
"gists",
3836
headers,
3937
data = NewGistRequest(description, public, files)
@@ -45,7 +43,6 @@ class GistsInterpreter[F[_]](implicit client: HttpClient[F], accessToken: Option
4543
headers: Map[String, String]
4644
): F[GHResponse[Gist]] =
4745
client.get[Gist](
48-
accessToken,
4946
("gists" :: gistId :: sha.toList).mkString("/"),
5047
headers
5148
)
@@ -57,7 +54,6 @@ class GistsInterpreter[F[_]](implicit client: HttpClient[F], accessToken: Option
5754
headers: Map[String, String]
5855
): F[GHResponse[Gist]] =
5956
client.patch[EditGistRequest, Gist](
60-
accessToken,
6157
s"gists/$gistId",
6258
headers,
6359
data = EditGistRequest(description, files)

github4s/src/main/scala/github4s/interpreters/GitDataInterpreter.scala

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,15 @@
1616

1717
package github4s.interpreters
1818

19-
import github4s.http.HttpClient
20-
import github4s.algebras.GitData
2119
import cats.data.NonEmptyList
22-
import github4s.GHResponse
23-
import github4s.domain._
2420
import github4s.Decoders._
2521
import github4s.Encoders._
22+
import github4s.GHResponse
23+
import github4s.algebras.GitData
24+
import github4s.domain._
25+
import github4s.http.HttpClient
2626

27-
class GitDataInterpreter[F[_]](implicit client: HttpClient[F], accessToken: Option[String])
28-
extends GitData[F] {
27+
class GitDataInterpreter[F[_]](implicit client: HttpClient[F]) extends GitData[F] {
2928
override def getReference(
3029
owner: String,
3130
repo: String,
@@ -34,7 +33,6 @@ class GitDataInterpreter[F[_]](implicit client: HttpClient[F], accessToken: Opti
3433
headers: Map[String, String]
3534
): F[GHResponse[NonEmptyList[Ref]]] =
3635
client.get[NonEmptyList[Ref]](
37-
accessToken,
3836
s"repos/$owner/$repo/git/refs/$ref",
3937
headers,
4038
pagination = pagination
@@ -48,7 +46,6 @@ class GitDataInterpreter[F[_]](implicit client: HttpClient[F], accessToken: Opti
4846
headers: Map[String, String]
4947
): F[GHResponse[Ref]] =
5048
client.post[CreateReferenceRequest, Ref](
51-
accessToken,
5249
s"repos/$owner/$repo/git/refs",
5350
headers,
5451
CreateReferenceRequest(ref, sha)
@@ -63,7 +60,6 @@ class GitDataInterpreter[F[_]](implicit client: HttpClient[F], accessToken: Opti
6360
headers: Map[String, String]
6461
): F[GHResponse[Ref]] =
6562
client.patch[UpdateReferenceRequest, Ref](
66-
accessToken,
6763
s"repos/$owner/$repo/git/refs/$ref",
6864
headers,
6965
UpdateReferenceRequest(sha, force)
@@ -75,7 +71,7 @@ class GitDataInterpreter[F[_]](implicit client: HttpClient[F], accessToken: Opti
7571
sha: String,
7672
headers: Map[String, String]
7773
): F[GHResponse[RefCommit]] =
78-
client.get[RefCommit](accessToken, s"repos/$owner/$repo/git/commits/$sha", headers)
74+
client.get[RefCommit](s"repos/$owner/$repo/git/commits/$sha", headers)
7975

8076
override def createCommit(
8177
owner: String,
@@ -87,7 +83,6 @@ class GitDataInterpreter[F[_]](implicit client: HttpClient[F], accessToken: Opti
8783
headers: Map[String, String]
8884
): F[GHResponse[RefCommit]] =
8985
client.post[NewCommitRequest, RefCommit](
90-
accessToken,
9186
s"repos/$owner/$repo/git/commits",
9287
headers,
9388
NewCommitRequest(message, tree, parents, author)
@@ -99,7 +94,7 @@ class GitDataInterpreter[F[_]](implicit client: HttpClient[F], accessToken: Opti
9994
fileSha: String,
10095
headers: Map[String, String]
10196
): F[GHResponse[BlobContent]] =
102-
client.get[BlobContent](accessToken, s"repos/$owner/$repo/git/blobs/$fileSha", headers)
97+
client.get[BlobContent](s"repos/$owner/$repo/git/blobs/$fileSha", headers)
10398

10499
override def createBlob(
105100
owner: String,
@@ -109,7 +104,6 @@ class GitDataInterpreter[F[_]](implicit client: HttpClient[F], accessToken: Opti
109104
headers: Map[String, String]
110105
): F[GHResponse[RefInfo]] =
111106
client.post[NewBlobRequest, RefInfo](
112-
accessToken,
113107
s"repos/$owner/$repo/git/blobs",
114108
headers,
115109
NewBlobRequest(content, encoding)
@@ -123,7 +117,6 @@ class GitDataInterpreter[F[_]](implicit client: HttpClient[F], accessToken: Opti
123117
headers: Map[String, String]
124118
): F[GHResponse[TreeResult]] =
125119
client.get[TreeResult](
126-
accessToken,
127120
s"repos/$owner/$repo/git/trees/$sha",
128121
headers,
129122
(if (recursive) Map("recursive" -> "1") else Map.empty)
@@ -137,7 +130,6 @@ class GitDataInterpreter[F[_]](implicit client: HttpClient[F], accessToken: Opti
137130
headers: Map[String, String]
138131
): F[GHResponse[TreeResult]] =
139132
client.post[NewTreeRequest, TreeResult](
140-
accessToken,
141133
s"repos/$owner/$repo/git/trees",
142134
headers,
143135
NewTreeRequest(treeDataList, baseTree)
@@ -154,7 +146,6 @@ class GitDataInterpreter[F[_]](implicit client: HttpClient[F], accessToken: Opti
154146
headers: Map[String, String]
155147
): F[GHResponse[Tag]] =
156148
client.post[NewTagRequest, Tag](
157-
accessToken,
158149
s"repos/$owner/$repo/git/tags",
159150
headers,
160151
NewTagRequest(tag, message, objectSha, objectType, author)

0 commit comments

Comments
 (0)