Skip to content

Commit

Permalink
Require an http4s client to provide more flexibility, fixes #371
Browse files Browse the repository at this point in the history
  • Loading branch information
BenFradet committed Mar 17, 2020
1 parent 9e28ba8 commit ea964fd
Show file tree
Hide file tree
Showing 19 changed files with 738 additions and 473 deletions.
23 changes: 11 additions & 12 deletions github4s/src/main/scala/github4s/Github.scala
Expand Up @@ -16,19 +16,18 @@

package github4s

import java.util.concurrent.TimeUnit.MILLISECONDS
import cats.effect.ConcurrentEffect
import cats.effect.Sync
import github4s.algebras._
import github4s.modules._
import scala.concurrent.ExecutionContext
import scala.concurrent.duration.Duration
import org.http4s.client.Client

class Github[F[_]: ConcurrentEffect](accessToken: Option[String], timeout: Option[Duration])(
implicit ec: ExecutionContext
class Github[F[_]: Sync](
client: Client[F],
accessToken: Option[String]
) {

private lazy val module: GithubAPIs[F] =
new GithubAPIv3[F](accessToken, timeout.getOrElse(Duration(1000L, MILLISECONDS)))
new GithubAPIv3[F](client, accessToken)

lazy val users: Users[F] = module.users
lazy val repos: Repositories[F] = module.repos
Expand All @@ -45,10 +44,10 @@ class Github[F[_]: ConcurrentEffect](accessToken: Option[String], timeout: Optio

object Github {

def apply[F[_]: ConcurrentEffect](
accessToken: Option[String] = None,
timeout: Option[Duration] = None
)(implicit ec: ExecutionContext): Github[F] =
new Github[F](accessToken, timeout)
def apply[F[_]: Sync](
client: Client[F],
accessToken: Option[String] = None
): Github[F] =
new Github[F](client, accessToken)

}
3 changes: 1 addition & 2 deletions github4s/src/main/scala/github4s/http/Http4sSyntax.scala
Expand Up @@ -16,7 +16,6 @@

package github4s.http

import cats.effect.ConcurrentEffect
import org.http4s._
import org.http4s.MediaType
import org.http4s.Headers
Expand All @@ -26,7 +25,7 @@ import org.http4s.headers.`Content-Type`

object Http4sSyntax {

implicit class RequestOps[F[_]: ConcurrentEffect](self: Request[F]) {
implicit class RequestOps[F[_]](self: Request[F]) {
def withJsonBody[T](maybeData: Option[T])(implicit enc: Encoder[T]): Request[F] =
maybeData.fold(self)(data =>
self
Expand Down
63 changes: 28 additions & 35 deletions github4s/src/main/scala/github4s/http/HttpClient.scala
Expand Up @@ -16,21 +16,18 @@

package github4s.http

import cats.effect.Sync
import cats.syntax.either._
import cats.syntax.functor._
import io.circe.{Decoder, Encoder}
import github4s.GithubResponses.{GHResponse, JsonParsingException}
import github4s.domain.Pagination
import github4s.http.Http4sSyntax._
import org.http4s.Request
import org.http4s.client.blaze.BlazeClientBuilder
import cats.effect.{ConcurrentEffect, Resource}
import github4s.GithubResponses.{GHResponse, JsonParsingException}
import cats.implicits._
import io.circe.{Decoder, Encoder}
import org.http4s.circe.CirceEntityDecoder._
import github4s.http.Http4sSyntax._
import org.http4s.client.Client

import scala.concurrent.ExecutionContext
import scala.concurrent.duration.Duration

class HttpClient[F[_]: ConcurrentEffect](connTimeout: Duration)(implicit ec: ExecutionContext) {
class HttpClient[F[_]: Sync](client: Client[F]) {

val urls: GithubAPIv3Config = GithubAPIv3Config()

Expand Down Expand Up @@ -132,33 +129,29 @@ class HttpClient[F[_]: ConcurrentEffect](connTimeout: Duration)(implicit ec: Exe
val defaultPagination = Pagination(1, 1000)
val defaultPage: Int = 1
val defaultPerPage: Int = 30
val resource: Resource[F, Client[F]] = BlazeClientBuilder[F](ec)
.withConnectTimeout(connTimeout)
.resource

private def buildURL(method: String): String = urls.baseUrl + method

private def run[Req: Encoder, Res: Decoder](request: RequestBuilder[Req]): F[GHResponse[Res]] =
resource
.use(
_.run(
Request[F]()
.withMethod(request.httpVerb)
.withUri(request.toUri(urls))
.withHeaders(request.toHeaderList: _*)
.withJsonBody(request.data)
).use(response =>
response
.attemptAs[Res]
.value
.map { e =>
GHResponse(
e.leftMap(e => JsonParsingException(e.message, request.data.toString)),
response.status.code,
response.headers.toMap
)
}
)
private def run[Req: Encoder, Res: Decoder](request: RequestBuilder[Req]): F[GHResponse[Res]] = {
client
.run(
Request[F]()
.withMethod(request.httpVerb)
.withUri(request.toUri(urls))
.withHeaders(request.toHeaderList: _*)
.withJsonBody(request.data)
)

.use { response =>
response
.attemptAs[Res]
.value
.map { e =>
GHResponse(
e.leftMap(e => JsonParsingException(e.message, request.data.toString)),
response.status.code,
response.headers.toMap
)
}
}
}
}
Expand Up @@ -18,7 +18,7 @@ package github4s.interpreters

import github4s.GithubResponses.GHResponse
import github4s.algebras.Projects
import github4s.domain.{Column, Card, Pagination, Project}
import github4s.domain.{Card, Column, Pagination, Project}
import github4s.http.HttpClient
import github4s.Decoders._

Expand Down
15 changes: 6 additions & 9 deletions github4s/src/main/scala/github4s/modules/GithubAPIs.scala
Expand Up @@ -16,13 +16,11 @@

package github4s.modules

import cats.effect.ConcurrentEffect
import cats.effect.Sync
import github4s.algebras._
import github4s.http.HttpClient
import github4s.interpreters._

import scala.concurrent.ExecutionContext
import scala.concurrent.duration.Duration
import org.http4s.client.Client

sealed trait GithubAPIs[F[_]] {
def users: Users[F]
Expand All @@ -38,12 +36,11 @@ sealed trait GithubAPIs[F[_]] {
def projects: Projects[F]
}

class GithubAPIv3[F[_]: ConcurrentEffect](accessToken: Option[String] = None, timeout: Duration)(
implicit ec: ExecutionContext
) extends GithubAPIs[F] {
class GithubAPIv3[F[_]: Sync](client: Client[F], accessToken: Option[String] = None)
extends GithubAPIs[F] {

implicit val client = new HttpClient[F](timeout)
implicit val at = accessToken
implicit val httpClient = new HttpClient[F](client)
implicit val at = accessToken

override val users: Users[F] = new UsersInterpreter[F]
override val repos: Repositories[F] = new RepositoriesInterpreter[F]
Expand Down
81 changes: 48 additions & 33 deletions github4s/src/test/scala/github4s/integration/GHActivitiesSpec.scala
Expand Up @@ -19,36 +19,41 @@ package github4s.integration
import cats.effect.IO
import github4s.Github
import github4s.domain._

import github4s.utils.{BaseIntegrationSpec, Integration}

trait GHActivitiesSpec extends BaseIntegrationSpec {

"Activity >> Set a thread subscription" should "return expected response when a valid thread id is provided" taggedAs Integration in {
val response =
Github[IO](accessToken).activities
.setThreadSub(validThreadId, true, false, headerUserAgent)
.unsafeRunSync()
val response = clientResource
.use { client =>
Github[IO](client, accessToken).activities
.setThreadSub(validThreadId, true, false, headerUserAgent)
}
.unsafeRunSync()

testIsRight[Subscription](response)
response.statusCode shouldBe okStatusCode
}

it should "return error when an invalid thread id is passed" taggedAs Integration in {
val response =
Github[IO](accessToken).activities
.setThreadSub(invalidThreadId, true, false, headerUserAgent)
.unsafeRunSync()
val response = clientResource
.use { client =>
Github[IO](client, accessToken).activities
.setThreadSub(invalidThreadId, true, false, headerUserAgent)
}
.unsafeRunSync()

testIsLeft(response)
response.statusCode shouldBe notFoundStatusCode
}

"Activity >> ListStargazers" should "return the expected list of starrers for valid data" taggedAs Integration in {
val response =
Github[IO](accessToken).activities
.listStargazers(validRepoOwner, validRepoName, false, None, headerUserAgent)
.unsafeRunSync()
val response = clientResource
.use { client =>
Github[IO](client, accessToken).activities
.listStargazers(validRepoOwner, validRepoName, false, None, headerUserAgent)
}
.unsafeRunSync()

testIsRight[List[Stargazer]](response, { r =>
r.nonEmpty shouldBe true
Expand All @@ -58,10 +63,12 @@ trait GHActivitiesSpec extends BaseIntegrationSpec {
}

it should "return the expected list of starrers for valid data with dates if timeline" taggedAs Integration in {
val response =
Github[IO](accessToken).activities
.listStargazers(validRepoOwner, validRepoName, true, None, headerUserAgent)
.unsafeRunSync()
val response = clientResource
.use { client =>
Github[IO](client, accessToken).activities
.listStargazers(validRepoOwner, validRepoName, true, None, headerUserAgent)
}
.unsafeRunSync()

testIsRight[List[Stargazer]](response, { r =>
r.nonEmpty shouldBe true
Expand All @@ -71,20 +78,24 @@ trait GHActivitiesSpec extends BaseIntegrationSpec {
}

it should "return error for invalid repo name" taggedAs Integration in {
val response =
Github[IO](accessToken).activities
.listStargazers(invalidRepoName, validRepoName, false, None, headerUserAgent)
.unsafeRunSync()
val response = clientResource
.use { client =>
Github[IO](client, accessToken).activities
.listStargazers(invalidRepoName, validRepoName, false, None, headerUserAgent)
}
.unsafeRunSync()

testIsLeft(response)
response.statusCode shouldBe notFoundStatusCode
}

"Activity >> ListStarredRepositories" should "return the expected list of starred repos" taggedAs Integration in {
val response =
Github[IO](accessToken).activities
.listStarredRepositories(validUsername, false, headers = headerUserAgent)
.unsafeRunSync()
val response = clientResource
.use { client =>
Github[IO](client, accessToken).activities
.listStarredRepositories(validUsername, false, headers = headerUserAgent)
}
.unsafeRunSync()

testIsRight[List[StarredRepository]](response, { r =>
r.nonEmpty shouldBe true
Expand All @@ -94,10 +105,12 @@ trait GHActivitiesSpec extends BaseIntegrationSpec {
}

it should "return the expected list of starred repos with dates if timeline" taggedAs Integration in {
val response =
Github[IO](accessToken).activities
.listStarredRepositories(validUsername, true, headers = headerUserAgent)
.unsafeRunSync()
val response = clientResource
.use { client =>
Github[IO](client, accessToken).activities
.listStarredRepositories(validUsername, true, headers = headerUserAgent)
}
.unsafeRunSync()

testIsRight[List[StarredRepository]](response, { r =>
r.nonEmpty shouldBe true
Expand All @@ -107,10 +120,12 @@ trait GHActivitiesSpec extends BaseIntegrationSpec {
}

it should "return error for invalid username" taggedAs Integration in {
val response =
Github[IO](accessToken).activities
.listStarredRepositories(invalidUsername, false, headers = headerUserAgent)
.unsafeRunSync()
val response = clientResource
.use { client =>
Github[IO](client, accessToken).activities
.listStarredRepositories(invalidUsername, false, headers = headerUserAgent)
}
.unsafeRunSync()

testIsLeft(response)
response.statusCode shouldBe notFoundStatusCode
Expand Down
47 changes: 31 additions & 16 deletions github4s/src/test/scala/github4s/integration/GHAuthSpec.scala
Expand Up @@ -25,34 +25,49 @@ trait GHAuthSpec extends BaseIntegrationSpec {

"Auth >> NewAuth" should "return error on Left when invalid credential is provided" taggedAs Integration in {

val response = Github[IO]().auth
.newAuth(
validUsername,
invalidPassword,
validScopes,
validNote,
validClientId,
invalidClientSecret,
headerUserAgent
)
val response = clientResource
.use { client =>
Github[IO](client).auth
.newAuth(
validUsername,
invalidPassword,
validScopes,
validNote,
validClientId,
invalidClientSecret,
headerUserAgent
)
}
.unsafeRunSync()

testIsLeft(response)
}

"Auth >> AuthorizeUrl" should "return the expected URL for valid username" taggedAs Integration in {
val response =
Github[IO]().auth
.authorizeUrl(validClientId, validRedirectUri, validScopes)
.unsafeRunSync()
val response = clientResource
.use { client =>
Github[IO](client).auth
.authorizeUrl(validClientId, validRedirectUri, validScopes)
}
.unsafeRunSync()

testIsRight[Authorize](response, r => r.url.contains(validRedirectUri) shouldBe true)
response.statusCode shouldBe okStatusCode
}

"Auth >> GetAccessToken" should "return error on Left for invalid code value" taggedAs Integration in {
val response = Github[IO]().auth
.getAccessToken(validClientId, invalidClientSecret, "", validRedirectUri, "", headerUserAgent)
val response = clientResource
.use { client =>
Github[IO](client).auth
.getAccessToken(
validClientId,
invalidClientSecret,
"",
validRedirectUri,
"",
headerUserAgent
)
}
.unsafeRunSync()

testIsLeft(response)
Expand Down

0 comments on commit ea964fd

Please sign in to comment.