diff --git a/.gitignore b/.gitignore index baa018580..b590bff11 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ target/ .idea docs/src/jekyll/_site/ docs/src/jekyll/*.md +.DS_Store # PGP keys *.gpg diff --git a/build.sbt b/build.sbt index 6c3525952..3a9e7923b 100644 --- a/build.sbt +++ b/build.sbt @@ -24,6 +24,7 @@ lazy val buildSettings = Seq( "scala" -> MIT("2016", "47 Degrees, LLC. ") ) ) ++ reformatOnCompileSettings ++ + sharedCommonSettings ++ miscSettings ++ sharedReleaseProcess ++ credentialSettings ++ @@ -39,20 +40,30 @@ lazy val micrositeSettings = Seq( includeFilter in makeSite := "*.html" | "*.css" | "*.png" | "*.jpg" | "*.gif" | "*.js" | "*.swf" | "*.md" ) -lazy val dependencies = addLibs(vAll, +lazy val commonDeps = addLibs(vAll, "cats-free", "circe-core", "circe-generic", "circe-parser", "simulacrum") ++ - addTestLibs(vAll, "scalatest") ++ addCompilerPlugins(vAll, "paradise") ++ - Seq( +Seq(libraryDependencies ++= Seq( + "org.scalatest" %%% "scalatest" % "3.0.0" % "test", + "com.github.marklister" %%% "base64" % "0.2.2" +)) + +lazy val jvmDeps = Seq( libraryDependencies ++= Seq( "org.scalaj" %% "scalaj-http" % "2.2.1", "org.mock-server" % "mockserver-netty" % "3.10.4" % "test" )) +lazy val jsDeps = Seq( + libraryDependencies ++= Seq( + "fr.hmil" %%% "roshttp" % "2.0.0-RC1" + ) +) + lazy val docsDependencies = libraryDependencies ++= Seq( "com.ironcorelabs" %% "cats-scalatest" % "1.1.2" % "test", "org.mock-server" % "mockserver-netty" % "3.10.4" % "test" @@ -60,11 +71,24 @@ lazy val docsDependencies = libraryDependencies ++= Seq( lazy val scalazDependencies = addLibs(vAll, "scalaz-concurrent") -lazy val github4s = (project in file(".")) +/** github4s - cross project that provides cross platform support.*/ +lazy val github4s = (crossProject in file("github4s")) .settings(moduleName := "github4s") - .settings(buildSettings: _*) - .settings(dependencies: _*) .enablePlugins(AutomateHeaderPlugin) + .enablePlugins(BuildInfoPlugin). + settings( + buildInfoKeys := Seq[BuildInfoKey](name, version, "token" -> Option(sys.props("token")).getOrElse("")), + buildInfoPackage := "github4s" + ) + .settings(buildSettings: _*) + .settings(commonDeps: _*) + .jvmSettings(jvmDeps: _*) + .jsSettings(sharedJsSettings: _*) + .jsSettings(testSettings: _*) + .jsSettings(jsDeps: _*) + +lazy val github4sJVM = github4s.jvm +lazy val github4sJS = github4s.js lazy val docs = (project in file("docs")) .dependsOn(scalaz) @@ -79,5 +103,9 @@ lazy val scalaz = (project in file("scalaz")) .settings(moduleName := "github4s-scalaz") .settings(buildSettings: _*) .settings(scalazDependencies: _*) - .dependsOn(github4s) + .dependsOn(github4sJVM) .enablePlugins(AutomateHeaderPlugin) + +lazy val testSettings = Seq( + fork in Test := false +) \ No newline at end of file diff --git a/docs/src/main/tut/docs.md b/docs/src/main/tut/docs.md index 376d30662..5e38b5d38 100755 --- a/docs/src/main/tut/docs.md +++ b/docs/src/main/tut/docs.md @@ -3,7 +3,7 @@ layout: docs title: Getting Started --- -# Get started +# Getting started WIP: Import @@ -11,30 +11,43 @@ WIP: Import import github4s.Github ``` +In order for github4s to work in both JVM and scala-js environments, you'll need to place different implicits in your scope, depending on your needs: + +```tut:silent +import github4s.jvm.Implicits._ +``` + +```tut:silent +// import github4s.js.Implicits._ +``` + ```tut:invisible val accessToken = sys.props.get("token") ``` -WIP: Every Github4s api returns a `Free[GHResponse[A], A]` where `GHResonse[A]` is a type alias for `Either[GHException, GHResult[A]]`. GHResult contains the result `[A]` given by Github, but also the status code of the response and headers: +WIP: Every Github4s api returns a `Free[GHResponse[A], A]` where `GHResponse[A]` is a type alias for `Either[GHException, GHResult[A]]`. GHResult contains the result `[A]` given by GitHub, but also the status code of the response and headers: ```scala case class GHResult[A](result: A, statusCode: Int, headers: Map[String, IndexedSeq[String]]) ``` -For geting an user +For getting an user ```tut:silent val user1 = Github(accessToken).users.get("rafaparadela") ``` -user1 in this case `Free[GHException Xor GHResult[User], User]` and we can run (`foldMap`) with `exec[M[_]]` where `M[_]` represent any type container that implements `MonadError[M, Throwable]`, for instance `cats.Eval`. +user1 in this case `Free[GHException Xor GHResult[User], User]` and we can run (`foldMap`) with `exec[M[_], C]` where `M[_]` represent any type container that implements `MonadError[M, Throwable]`, for instance `cats.Eval`; and C represents a valid implementation of an HttpClient. The previously mentioned implicit classes carry already set up instances for working with `scalaj` (for JVM-compatible apps) and `roshttp` (for scala-js-compatible apps). Take into account that in the latter case, you can only use `Future` in the place of `M[_]`: ```tut:silent import cats.Eval import github4s.Github._ -import github4s.implicits._ +import scalaj.http._ + +object ProgramEval { + val u1 = user1.exec[Eval, HttpResponse[String]].value +} -val u1 = user1.exec[Eval].value ``` WIP: As mentioned above `u1` should have an `GHResult[User]` in the right. @@ -45,7 +58,7 @@ import github4s.GithubResponses.GHResult ``` ```tut:book -u1 match { +ProgramEval.u1 match { case Right(GHResult(result, status, headers)) => result.login case Left(e) => e.getMessage } @@ -55,21 +68,27 @@ WIP: With `Id` ```tut:silent import cats.Id +import scalaj.http._ -val u2 = Github(accessToken).users.get("raulraja").exec[Id] +object ProgramId { + val u2 = Github(accessToken).users.get("raulraja").exec[Id, HttpResponse[String]] +} ``` WIP: With `Future` ```tut:silent -import github4s.implicits._ +import cats.Id import scala.concurrent.Future import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration._ import scala.concurrent.Await +import scalaj.http._ -val u3 = Github(accessToken).users.get("dialelo").exec[Future] -Await.result(u3, 2.seconds) +object ProgramFuture { + val u3 = Github(accessToken).users.get("dialelo").exec[Future, HttpResponse[String]] + Await.result(u3, 2.seconds) +} ``` WIP: With `scalaz.Task` @@ -77,9 +96,13 @@ WIP: With `scalaz.Task` ```tut:silent import scalaz.concurrent.Task import github4s.scalaz.implicits._ +import scalaj.http._ +import github4s.jvm.Implicits._ -val u4 = Github(accessToken).users.get("franciscodr").exec[Task] -u4.attemptRun +object ProgramTask { + val u4 = Github(accessToken).users.get("franciscodr").exec[Task, HttpResponse[String]] + u4.attemptRun +} ``` ```tut:invisible @@ -89,14 +112,24 @@ import cats.Eval import cats.implicits._ import github4s.Github import github4s.Github._ -import github4s.implicits._ +import github4s.jvm.Implicits._ +import scalaj.http._ val accessToken = sys.props.get("token") ``` ```tut:book -val user1 = Github(accessToken).users.get("rafaparadela").exec[Eval].value +object ProgramEval { + val user1 = Github(accessToken).users.get("rafaparadela").exec[Eval, HttpResponse[String]].value +} -user1 should be ('right) -user1.toOption map (_.result.login shouldBe "rafaparadela") +ProgramEval.user1 should be ('right) +ProgramEval.user1.toOption map (_.result.login shouldBe "rafaparadela") ``` + +# Test credentials + +Note that for github4s to have access to the GitHub API during the test phases, you need to provide a valid access token with the right credentials (i.e.: users + gists scopes), through the sbt configuration variable "token": + +sbt -Dtoken=ACCESS_TOKEN_STRING +``` \ No newline at end of file diff --git a/github4s/js/src/main/scala/github4s/HttpRequestBuilderExtensionJS.scala b/github4s/js/src/main/scala/github4s/HttpRequestBuilderExtensionJS.scala new file mode 100644 index 000000000..c1cc079d3 --- /dev/null +++ b/github4s/js/src/main/scala/github4s/HttpRequestBuilderExtensionJS.scala @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2016 47 Degrees, LLC. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package github4s + +import scala.concurrent.Future +import fr.hmil.roshttp._ +import fr.hmil.roshttp.body.{BodyPart, BulkBodyPart} +import java.nio.ByteBuffer + +import cats.implicits._ +import fr.hmil.roshttp.response.SimpleHttpResponse +import fr.hmil.roshttp.util.HeaderMap +import fr.hmil.roshttp.body.Implicits._ +import fr.hmil.roshttp.exceptions.HttpException + +import scala.concurrent.ExecutionContext.Implicits.global +import github4s.GithubResponses.{GHResponse, GHResult, JsonParsingException, UnexpectedException} +import github4s.GithubDefaultUrls._ +import github4s.Decoders._ +import github4s.HttpClient.HttpCode400 +import io.circe.Decoder +import io.circe.parser._ +import monix.reactive.Observable + +import scala.util.{Failure, Success} + +case class CirceJSONBody(value: String) extends BulkBodyPart { + override def contentType: String = s"application/json; charset=utf-8" + override def contentData: ByteBuffer = ByteBuffer.wrap(value.getBytes("utf-8")) +} + +trait HttpRequestBuilderExtensionJS { + + import monix.execution.Scheduler.Implicits.global + + val userAgent = { + val name = github4s.BuildInfo.name + val version = github4s.BuildInfo.version + s"$name/$version" + } + + implicit def extensionJS: HttpRequestBuilderExtension[SimpleHttpResponse, Future] = + new HttpRequestBuilderExtension[SimpleHttpResponse, Future] { + def run[A](rb: HttpRequestBuilder[SimpleHttpResponse, Future])( + implicit D: Decoder[A]): Future[GHResponse[A]] = { + val request = HttpRequest(rb.url) + .withMethod(Method(rb.httpVerb.verb)) + .withQueryParameters(rb.params.toSeq: _*) + .withHeader("content-type", "application/json") + .withHeader("user-agent", userAgent) + .withHeaders(rb.authHeader.toList: _*) + .withHeaders(rb.headers.toList: _*) + + rb.data + .map(d => request.send(CirceJSONBody(d))) + .getOrElse(request.send()) + .map(toEntity[A]) + .recoverWith { + case e => Future.successful(Either.left(UnexpectedException(e.getMessage))) + } + } + } + + def toEntity[A](response: SimpleHttpResponse)(implicit D: Decoder[A]): GHResponse[A] = + response match { + case r if r.statusCode < HttpCode400.statusCode ⇒ + decode[A](r.body).fold( + e ⇒ Either.left(JsonParsingException(e.getMessage, r.body)), + result ⇒ + Either.right( + GHResult(result, r.statusCode, rosHeaderMapToRegularMap(r.headers)) + ) + ) + case r ⇒ + Either.left( + UnexpectedException( + s"Failed invoking get with status : ${r.statusCode}, body : \n ${r.body}")) + } + + private def rosHeaderMapToRegularMap( + headers: HeaderMap[String]): Map[String, IndexedSeq[String]] = + headers.flatMap(m => Map(m._1.toLowerCase -> IndexedSeq(m._2))) + +} diff --git a/github4s/js/src/main/scala/github4s/js/Implicits.scala b/github4s/js/src/main/scala/github4s/js/Implicits.scala new file mode 100644 index 000000000..9def7594b --- /dev/null +++ b/github4s/js/src/main/scala/github4s/js/Implicits.scala @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2016 47 Degrees, LLC. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package github4s.js + +object Implicits extends ImplicitsJS diff --git a/github4s/js/src/main/scala/github4s/js/ImplicitsJS.scala b/github4s/js/src/main/scala/github4s/js/ImplicitsJS.scala new file mode 100644 index 000000000..d421021f0 --- /dev/null +++ b/github4s/js/src/main/scala/github4s/js/ImplicitsJS.scala @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2016 47 Degrees, LLC. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package github4s.js + +import cats.instances.FutureInstances +import fr.hmil.roshttp.response.SimpleHttpResponse +import github4s.HttpRequestBuilderExtensionJS +import github4s.free.interpreters.Interpreters +import github4s.implicits._ +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.Future + +trait ImplicitsJS extends FutureInstances with HttpRequestBuilderExtensionJS { + + implicit val intInstanceFutureRosHttp = new Interpreters[Future, SimpleHttpResponse] + +} diff --git a/github4s/js/src/test/scala/github4s/utils/integration/GHAuthSpec.scala b/github4s/js/src/test/scala/github4s/utils/integration/GHAuthSpec.scala new file mode 100644 index 000000000..34a46b051 --- /dev/null +++ b/github4s/js/src/test/scala/github4s/utils/integration/GHAuthSpec.scala @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2016 47 Degrees, LLC. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package github4s.integration + +import github4s.Github._ +import github4s.Github +import github4s.utils.TestUtils +import org.scalatest._ +import fr.hmil.roshttp.response.SimpleHttpResponse +import github4s.free.domain.Authorize +import github4s.js.Implicits._ +import scala.concurrent.Future + +class GHAuthSpec extends AsyncFlatSpec with Matchers with TestUtils { + + override implicit val executionContext = scala.concurrent.ExecutionContext.Implicits.global + + "Auth >> NewAuth" should "return error on Left when invalid credential is provided" in { + + val response = Github().auth + .newAuth(validUsername, + invalidPassword, + validScopes, + validNote, + validClientId, + invalidClientSecret) + .exec[Future, SimpleHttpResponse] + + testFutureIsLeft(response) + } + + "Auth >> AuthorizeUrl" should "return the expected URL for valid username" in { + val response = + Github().auth + .authorizeUrl(validClientId, validRedirectUri, validScopes) + .exec[Future, SimpleHttpResponse] + + testFutureIsRight[Authorize](response, { r => + r.result.url.contains(validRedirectUri) shouldBe true + r.statusCode shouldBe okStatusCode + }) + } + + "Auth >> GetAccessToken" should "return error on Left for invalid code value" in { + val response = Github().auth + .getAccessToken(validClientId, invalidClientSecret, "", validRedirectUri, "") + .exec[Future, SimpleHttpResponse] + + testFutureIsLeft(response) + } + +} diff --git a/github4s/js/src/test/scala/github4s/utils/integration/GHGistsSpec.scala b/github4s/js/src/test/scala/github4s/utils/integration/GHGistsSpec.scala new file mode 100644 index 000000000..0c1ef0baa --- /dev/null +++ b/github4s/js/src/test/scala/github4s/utils/integration/GHGistsSpec.scala @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2016 47 Degrees, LLC. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package github4s.integration + +import github4s.Github._ +import github4s.Github +import github4s.utils.TestUtils +import org.scalatest._ +import fr.hmil.roshttp.response.SimpleHttpResponse +import github4s.free.domain.{Gist, GistFile} +import github4s.js.Implicits._ +import scala.concurrent.Future + +class GHGistsSpec extends AsyncFlatSpec with Matchers with TestUtils { + + override implicit val executionContext = scala.concurrent.ExecutionContext.Implicits.global + + "Gists >> Post" should "return the provided gist" in { + val response = Github(accessToken).gists + .newGist(validGistDescription, + validGistPublic, + Map(validGistFilename -> GistFile(validGistFileContent))) + .exec[Future, SimpleHttpResponse] + + testFutureIsRight[Gist](response, { r => + r.result.description shouldBe validGistDescription + r.statusCode shouldBe createdStatusCode + }) + } +} diff --git a/github4s/js/src/test/scala/github4s/utils/integration/GHReposSpec.scala b/github4s/js/src/test/scala/github4s/utils/integration/GHReposSpec.scala new file mode 100644 index 000000000..1c3824794 --- /dev/null +++ b/github4s/js/src/test/scala/github4s/utils/integration/GHReposSpec.scala @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2016 47 Degrees, LLC. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package github4s.integration + +import github4s.Github._ +import github4s.GithubResponses._ +import github4s.Github +import github4s.utils.TestUtils +import org.scalatest.{AsyncFlatSpec, FlatSpec, Matchers} +import fr.hmil.roshttp.response.SimpleHttpResponse +import github4s.free.domain.{Commit, Repository, User} +import github4s.js.Implicits._ +import scala.concurrent.Future + +class GHReposSpec extends AsyncFlatSpec with Matchers with TestUtils { + + override implicit val executionContext = scala.concurrent.ExecutionContext.Implicits.global + + "Repos >> Get" should "return the expected name when valid repo is provided" in { + + val response = + Github(accessToken).repos.get(validRepoOwner, validRepoName).exec[Future, SimpleHttpResponse] + + testFutureIsRight[Repository](response, { r => + r.result.name shouldBe validRepoName + r.statusCode shouldBe okStatusCode + }) + } + + it should "return error when an invalid repo name is passed" in { + val response = + Github(accessToken).repos + .get(validRepoOwner, invalidRepoName) + .exec[Future, SimpleHttpResponse] + + testFutureIsLeft(response) + } + + "Repos >> ListCommits" should "return the expected list of commits for valid data" in { + val response = + Github(accessToken).repos + .listCommits(validRepoOwner, validRepoName) + .exec[Future, SimpleHttpResponse] + + testFutureIsRight[List[Commit]](response, { r => + r.result.nonEmpty shouldBe true + r.statusCode shouldBe okStatusCode + }) + } + + it should "return error for invalid repo name" in { + val response = + Github(accessToken).repos + .listCommits(invalidRepoName, validRepoName) + .exec[Future, SimpleHttpResponse] + + testFutureIsLeft(response) + } + + "Repos >> ListContributors" should "return the expected list of contributors for valid data" in { + val response = + Github(accessToken).repos + .listContributors(validRepoOwner, validRepoName) + .exec[Future, SimpleHttpResponse] + + testFutureIsRight[List[User]](response, { r => + r.result shouldNot be(empty) + r.statusCode shouldBe okStatusCode + }) + } + + it should "return error for invalid repo name" in { + val response = + Github(accessToken).repos + .listContributors(invalidRepoName, validRepoName) + .exec[Future, SimpleHttpResponse] + + testFutureIsLeft(response) + } + +} diff --git a/github4s/js/src/test/scala/github4s/utils/integration/GHUsersSpec.scala b/github4s/js/src/test/scala/github4s/utils/integration/GHUsersSpec.scala new file mode 100644 index 000000000..3601176dc --- /dev/null +++ b/github4s/js/src/test/scala/github4s/utils/integration/GHUsersSpec.scala @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2016 47 Degrees, LLC. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package github4s.integration + +import github4s.Github._ +import github4s.Github +import github4s.utils.TestUtils +import org.scalatest._ +import fr.hmil.roshttp.response.SimpleHttpResponse +import scala.concurrent.Future +import github4s.free.domain.User +import github4s.js.Implicits._ + +class GHUsersSpec extends AsyncFlatSpec with Matchers with TestUtils { + + override implicit val executionContext = scala.concurrent.ExecutionContext.Implicits.global + + "Users >> Get" should "return the expected login for a valid username" in { + val response = Github(accessToken).users.get(validUsername).exec[Future, SimpleHttpResponse] + + testFutureIsRight[User](response, { r => + r.result.login shouldBe validUsername + r.statusCode shouldBe okStatusCode + }) + } + + it should "return error on Left for invalid username" in { + val response = Github(accessToken).users.get(invalidUsername).exec[Future, SimpleHttpResponse] + testFutureIsLeft(response) + } + + "Users >> GetAuth" should "return error on Left when no accessToken is provided" in { + val response = Github().users.getAuth.exec[Future, SimpleHttpResponse] + testFutureIsLeft(response) + } + + "Users >> GetUsers" should "return users for a valid since value" in { + val response = + Github(accessToken).users.getUsers(validSinceInt).exec[Future, SimpleHttpResponse] + + testFutureIsRight[List[User]](response, { r => + r.result.nonEmpty shouldBe true + r.statusCode shouldBe okStatusCode + }) + } + + it should "return an empty list when a invalid since value is provided" in { + val response = + Github(accessToken).users.getUsers(invalidSinceInt).exec[Future, SimpleHttpResponse] + + testFutureIsRight[List[User]](response, { r => + r.result.isEmpty shouldBe true + r.statusCode shouldBe okStatusCode + }) + } + +} diff --git a/github4s/js/src/test/scala/github4s/utils/utils/TestUtils.scala b/github4s/js/src/test/scala/github4s/utils/utils/TestUtils.scala new file mode 100644 index 000000000..cfe4311dc --- /dev/null +++ b/github4s/js/src/test/scala/github4s/utils/utils/TestUtils.scala @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2016 47 Degrees, LLC. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package github4s.utils + +import com.github.marklister.base64.Base64._ +import org.scalatest.{Assertion, Matchers} +import cats.implicits._ +import github4s.GithubResponses.{GHResponse, GHResult} + +import scala.concurrent.Future + +trait TestUtils extends Matchers { + def testFutureIsLeft[A](response: Future[GHResponse[A]])( + implicit ec: scala.concurrent.ExecutionContext) = { + response map { r => + r.isLeft should be(true) + } + } + + def testFutureIsRight[A](response: Future[GHResponse[A]], f: (GHResult[A]) => Assertion)( + implicit ec: scala.concurrent.ExecutionContext) = { + response map { r ⇒ + { + r.isRight should be(true) + + r.toOption map { rr => + f(rr) + } match { + case _ => succeed + } + } + } + } + + val accessToken = Option(github4s.BuildInfo.token) + def tokenHeader = "token " + accessToken.getOrElse("") + + val validUsername = "rafaparadela" + val invalidUsername = "GHInvalidaUserName" + val invalidPassword = "invalidPassword" + + def validBasicAuth = s"Basic ${s"$validUsername:".getBytes.toBase64}" + def invalidBasicAuth = s"Basic ${"$validUsername:$invalidPassword".getBytes.toBase64}" + + val validScopes = List("public_repo") + val validNote = "New access token" + val validClientId = "e8e39175648c9db8c280" + val invalidClientSecret = "1234567890" + val validCode = "code" + val invalidCode = "invalid-code" + + val validRepoOwner = "47deg" + val validRepoName = "github4s" + val invalidRepoName = "GHInvalidRepoName" + val validRedirectUri = "http://localhost:9000/_oauth-callback" + val validPage = 1 + val invalidPage = 999 + val validPerPage = 100 + + val validSinceInt = 100 + val invalidSinceInt = 999999999 + + val okStatusCode = 200 + val createdStatusCode = 201 + val unauthorizedStatusCode = 401 + val notFoundStatusCode = 404 + + val validAnonParameter = "true" + val invalidAnonParameter = "X" + + val validGistDescription = "A Gist" + val validGistPublic = false + val validGistFileContent = "val meaningOfLife = 42" + val validGistFilename = "test.scala" +} diff --git a/github4s/jvm/src/main/scala/github4s/HttpRequestBuilderExtensionJVM.scala b/github4s/jvm/src/main/scala/github4s/HttpRequestBuilderExtensionJVM.scala new file mode 100644 index 000000000..6a4383641 --- /dev/null +++ b/github4s/jvm/src/main/scala/github4s/HttpRequestBuilderExtensionJVM.scala @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2016 47 Degrees, LLC. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package github4s + +import github4s.GithubResponses.{GHResult, JsonParsingException, UnexpectedException} +import io.circe.Decoder +import io.circe.parser._ +import io.circe.generic.auto._ +import github4s.GithubResponses.GHResponse + +import scalaj.http._ +import cats.implicits._ +import github4s.GithubDefaultUrls._ +import github4s.free.interpreters.Capture + +trait HttpRequestBuilderExtensionJVM { + + implicit def extensionJVM[M[_]]( + implicit C: Capture[M]): HttpRequestBuilderExtension[HttpResponse[String], M] = + new HttpRequestBuilderExtension[HttpResponse[String], M] { + + def run[A](rb: HttpRequestBuilder[HttpResponse[String], M])( + implicit D: Decoder[A]): M[GHResponse[A]] = { + + val connTimeoutMs: Int = 1000 + val readTimeoutMs: Int = 5000 + + val request = Http(rb.url) + .method(rb.httpVerb.verb) + .option(HttpOptions.connTimeout(connTimeoutMs)) + .option(HttpOptions.readTimeout(readTimeoutMs)) + .params(rb.params) + .headers(rb.authHeader) + .headers(rb.headers) + + rb.data match { + case Some(d) ⇒ + C.capture( + toEntity[A](request.postData(d).header("content-type", "application/json").asString, + request.url)) + case _ ⇒ C.capture(toEntity[A](request.asString, request.url)) + } + } + + } + + def toEntity[A](response: HttpResponse[String], url: String)( + implicit D: Decoder[A]): GHResponse[A] = response match { + case r if r.isSuccess ⇒ + decode[A](r.body).fold( + e ⇒ Either.left(JsonParsingException(e.getMessage, r.body)), + result ⇒ Either.right(GHResult(result, r.code, toLowerCase(r.headers))) + ) + case r ⇒ + Either.left( + UnexpectedException(s"Failed invoking get with status : ${r.code}, body : \n ${r.body}")) + } + + private def toLowerCase( + headers: Map[String, IndexedSeq[String]]): Map[String, IndexedSeq[String]] = + headers.map(e ⇒ (e._1.toLowerCase, e._2)) +} diff --git a/github4s/jvm/src/main/scala/github4s/jvm/Implicits.scala b/github4s/jvm/src/main/scala/github4s/jvm/Implicits.scala new file mode 100644 index 000000000..1a90fa416 --- /dev/null +++ b/github4s/jvm/src/main/scala/github4s/jvm/Implicits.scala @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2016 47 Degrees, LLC. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package github4s.jvm + +import github4s.FutureCaptureInstance + +object Implicits extends ImplicitsJVM with FutureCaptureInstance diff --git a/github4s/jvm/src/main/scala/github4s/jvm/ImplicitsJVM.scala b/github4s/jvm/src/main/scala/github4s/jvm/ImplicitsJVM.scala new file mode 100644 index 000000000..8bccb9133 --- /dev/null +++ b/github4s/jvm/src/main/scala/github4s/jvm/ImplicitsJVM.scala @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2016 47 Degrees, LLC. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package github4s.jvm + +import cats.instances.FutureInstances +import cats.{Eval, _} +import github4s.free.interpreters.Interpreters +import github4s.{EvalInstances, HttpRequestBuilderExtensionJVM, IdInstances} +import scala.concurrent.Future +import scalaj.http.HttpResponse +import scala.concurrent.ExecutionContext.Implicits.global +import github4s.implicits._ + +trait ImplicitsJVM + extends IdInstances + with EvalInstances + with FutureInstances + with HttpRequestBuilderExtensionJVM { + + implicit val intInstanceIdScalaJ = new Interpreters[Id, HttpResponse[String]] + implicit val intInstanceEvalScalaJ = new Interpreters[Eval, HttpResponse[String]] + implicit val intInstanceFutureScalaJ = new Interpreters[Future, HttpResponse[String]] + +} diff --git a/src/test/scala/github4s/integration/GHAuthSpec.scala b/github4s/jvm/src/test/scala/github4s/integration/GHAuthSpec.scala similarity index 89% rename from src/test/scala/github4s/integration/GHAuthSpec.scala rename to github4s/jvm/src/test/scala/github4s/integration/GHAuthSpec.scala index 2ae285f30..4b7dc8b05 100644 --- a/src/test/scala/github4s/integration/GHAuthSpec.scala +++ b/github4s/jvm/src/test/scala/github4s/integration/GHAuthSpec.scala @@ -25,9 +25,10 @@ import cats.Id import cats.implicits._ import github4s.Github._ import github4s.Github -import github4s.implicits._ import github4s.utils.TestUtils import org.scalatest._ +import github4s.jvm.Implicits._ +import scalaj.http._ class GHAuthSpec extends FlatSpec with Matchers with TestUtils { @@ -39,13 +40,15 @@ class GHAuthSpec extends FlatSpec with Matchers with TestUtils { validNote, validClientId, invalidClientSecret) - .exec[Id] + .exec[Id, HttpResponse[String]] response should be('left) } "Auth >> AuthorizeUrl" should "return the expected URL for valid username" in { val response = - Github().auth.authorizeUrl(validClientId, validRedirectUri, validScopes).exec[Id] + Github().auth + .authorizeUrl(validClientId, validRedirectUri, validScopes) + .exec[Id, HttpResponse[String]] response should be('right) response.toOption map { r ⇒ @@ -58,7 +61,7 @@ class GHAuthSpec extends FlatSpec with Matchers with TestUtils { "Auth >> GetAccessToken" should "return error on Left for invalid code value" in { val response = Github().auth .getAccessToken(validClientId, invalidClientSecret, "", validRedirectUri, "") - .exec[Id] + .exec[Id, HttpResponse[String]] response should be('left) } diff --git a/src/test/scala/github4s/integration/GHGistsSpec.scala b/github4s/jvm/src/test/scala/github4s/integration/GHGistsSpec.scala similarity index 85% rename from src/test/scala/github4s/integration/GHGistsSpec.scala rename to github4s/jvm/src/test/scala/github4s/integration/GHGistsSpec.scala index ab85b964c..edc8c139c 100644 --- a/src/test/scala/github4s/integration/GHGistsSpec.scala +++ b/github4s/jvm/src/test/scala/github4s/integration/GHGistsSpec.scala @@ -24,16 +24,21 @@ package github4s.integration import cats.Id import cats.implicits._ import github4s.Github._ -import github4s.implicits._ +import github4s.free.domain.GistFile import github4s.Github import github4s.utils.TestUtils +import github4s.jvm.Implicits._ import org.scalatest._ +import scalaj.http.HttpResponse + class GHGistsSpec extends FlatSpec with Matchers with TestUtils { "Gists >> Post" should "return the provided gist" in { val response = Github(accessToken).gists - .newGist(validGistDescription, validGistPublic, validGistFiles) - .exec[Id] + .newGist(validGistDescription, + validGistPublic, + Map(validGistFilename -> GistFile(validGistFileContent))) + .exec[Id, HttpResponse[String]] response should be('right) response.toOption map { r ⇒ r.result.description shouldBe validGistDescription diff --git a/src/test/scala/github4s/integration/GHReposSpec.scala b/github4s/jvm/src/test/scala/github4s/integration/GHReposSpec.scala similarity index 73% rename from src/test/scala/github4s/integration/GHReposSpec.scala rename to github4s/jvm/src/test/scala/github4s/integration/GHReposSpec.scala index 67dc76673..be23fc836 100644 --- a/src/test/scala/github4s/integration/GHReposSpec.scala +++ b/github4s/jvm/src/test/scala/github4s/integration/GHReposSpec.scala @@ -25,16 +25,19 @@ import cats.Id import cats.implicits._ import github4s.Github._ import github4s.GithubResponses._ -import github4s.implicits._ import github4s.Github +import github4s.jvm.Implicits._ import github4s.utils.TestUtils -import org.scalatest.{Matchers, FlatSpec} +import org.scalatest.{FlatSpec, Matchers} + +import scalaj.http.HttpResponse class GHReposSpec extends FlatSpec with Matchers with TestUtils { "Repos >> Get" should "return the expected name when valid repo is provided" in { - val response = Github(accessToken).repos.get(validRepoOwner, validRepoName).exec[Id] + val response = + Github(accessToken).repos.get(validRepoOwner, validRepoName).exec[Id, HttpResponse[String]] response should be('right) response.toOption map { r ⇒ r.result.name shouldBe validRepoName @@ -43,12 +46,15 @@ class GHReposSpec extends FlatSpec with Matchers with TestUtils { } it should "return error when an invalid repo name is passed" in { - val response = Github(accessToken).repos.get(validRepoOwner, invalidRepoName).exec[Id] + val response = + Github(accessToken).repos.get(validRepoOwner, invalidRepoName).exec[Id, HttpResponse[String]] response should be('left) } "Repos >> ListCommits" should "return the expected list of commits for valid data" in { - val response = Github(accessToken).repos.listCommits(validRepoOwner, validRepoName).exec[Id] + val response = Github(accessToken).repos + .listCommits(validRepoOwner, validRepoName) + .exec[Id, HttpResponse[String]] response should be('right) response.toOption map { r ⇒ @@ -58,13 +64,17 @@ class GHReposSpec extends FlatSpec with Matchers with TestUtils { } it should "return error for invalid repo name" in { - val response = Github(accessToken).repos.listCommits(invalidRepoName, validRepoName).exec[Id] + val response = Github(accessToken).repos + .listCommits(invalidRepoName, validRepoName) + .exec[Id, HttpResponse[String]] response should be('left) } "Repos >> ListContributors" should "return the expected list of contributors for valid data" in { val response = - Github(accessToken).repos.listContributors(validRepoOwner, validRepoName).exec[Id] + Github(accessToken).repos + .listContributors(validRepoOwner, validRepoName) + .exec[Id, HttpResponse[String]] response should be('right) response.toOption map { r ⇒ @@ -76,7 +86,9 @@ class GHReposSpec extends FlatSpec with Matchers with TestUtils { it should "return error for invalid repo name" in { val response = - Github(accessToken).repos.listContributors(invalidRepoName, validRepoName).exec[Id] + Github(accessToken).repos + .listContributors(invalidRepoName, validRepoName) + .exec[Id, HttpResponse[String]] response should be('left) } diff --git a/src/test/scala/github4s/integration/GHUsersSpec.scala b/github4s/jvm/src/test/scala/github4s/integration/GHUsersSpec.scala similarity index 87% rename from src/test/scala/github4s/integration/GHUsersSpec.scala rename to github4s/jvm/src/test/scala/github4s/integration/GHUsersSpec.scala index 9d959979d..4bd37302f 100644 --- a/src/test/scala/github4s/integration/GHUsersSpec.scala +++ b/github4s/jvm/src/test/scala/github4s/integration/GHUsersSpec.scala @@ -24,15 +24,17 @@ package github4s.integration import cats.Id import cats.implicits._ import github4s.Github._ -import github4s.implicits._ import github4s.Github +import github4s.jvm.Implicits._ import github4s.utils.TestUtils import org.scalatest._ +import scalaj.http.HttpResponse + class GHUsersSpec extends FlatSpec with Matchers with TestUtils { "Users >> Get" should "return the expected login for a valid username" in { - val response = Github(accessToken).users.get(validUsername).exec[Id] + val response = Github(accessToken).users.get(validUsername).exec[Id, HttpResponse[String]] response should be('right) response.toOption map { r ⇒ r.result.login shouldBe validUsername @@ -41,17 +43,17 @@ class GHUsersSpec extends FlatSpec with Matchers with TestUtils { } it should "return error on Left for invalid username" in { - val response = Github(accessToken).users.get(invalidUsername).exec[Id] + val response = Github(accessToken).users.get(invalidUsername).exec[Id, HttpResponse[String]] response should be('left) } "Users >> GetAuth" should "return error on Left when no accessToken is provided" in { - val response = Github().users.getAuth.exec[Id] + val response = Github().users.getAuth.exec[Id, HttpResponse[String]] response should be('left) } "Users >> GetUsers" should "return users for a valid since value" in { - val response = Github(accessToken).users.getUsers(validSinceInt).exec[Id] + val response = Github(accessToken).users.getUsers(validSinceInt).exec[Id, HttpResponse[String]] response should be('right) response.toOption map { r ⇒ @@ -61,7 +63,8 @@ class GHUsersSpec extends FlatSpec with Matchers with TestUtils { } it should "return an empty list when a invalid since value is provided" in { - val response = Github(accessToken).users.getUsers(invalidSinceInt).exec[Id] + val response = + Github(accessToken).users.getUsers(invalidSinceInt).exec[Id, HttpResponse[String]] response should be('right) response.toOption map { r ⇒ diff --git a/src/test/scala/github4s/unit/ApiSpec.scala b/github4s/jvm/src/test/scala/github4s/unit/ApiSpec.scala similarity index 93% rename from src/test/scala/github4s/unit/ApiSpec.scala rename to github4s/jvm/src/test/scala/github4s/unit/ApiSpec.scala index d8d71dd29..4f4094acf 100644 --- a/src/test/scala/github4s/unit/ApiSpec.scala +++ b/github4s/jvm/src/test/scala/github4s/unit/ApiSpec.scala @@ -22,22 +22,27 @@ package github4s.unit import github4s.api.{Auth, Gists, Repos, Users} -import github4s.free.domain.Pagination +import github4s.free.domain.{GistFile, Pagination} import github4s.utils.{DummyGithubUrls, MockGithubApiServer, TestUtils} import org.scalatest._ import cats.implicits._ +import scalaj.http._ +import cats.Id +import github4s.jvm.ImplicitsJVM + class ApiSpec extends FlatSpec with Matchers with TestUtils with MockGithubApiServer - with DummyGithubUrls { + with DummyGithubUrls + with ImplicitsJVM { - val auth = new Auth - val repos = new Repos - val users = new Users - val gists = new Gists + val auth = new Auth[HttpResponse[String], Id] + val repos = new Repos[HttpResponse[String], Id] + val users = new Users[HttpResponse[String], Id] + val gists = new Gists[HttpResponse[String], Id] "Auth >> NewAuth" should "return a valid token when valid credential is provided" in { val response = auth.newAuth(validUsername, "", validScopes, validNote, validClientId, "") @@ -245,7 +250,10 @@ class ApiSpec "Gists >> PostGist" should "return the provided gist for a valid request" in { val response = - gists.newGist(validGistDescription, validGistPublic, validGistFiles, accessToken) + gists.newGist(validGistDescription, + validGistPublic, + Map(validGistFilename -> GistFile(validGistFileContent)), + accessToken) response should be('right) response.toOption map { r ⇒ diff --git a/src/test/scala/github4s/unit/DecodersSpec.scala b/github4s/jvm/src/test/scala/github4s/unit/DecodersSpec.scala similarity index 100% rename from src/test/scala/github4s/unit/DecodersSpec.scala rename to github4s/jvm/src/test/scala/github4s/unit/DecodersSpec.scala diff --git a/src/test/scala/github4s/utils/DummyGithubUrls.scala b/github4s/jvm/src/test/scala/github4s/utils/DummyGithubUrls.scala similarity index 100% rename from src/test/scala/github4s/utils/DummyGithubUrls.scala rename to github4s/jvm/src/test/scala/github4s/utils/DummyGithubUrls.scala diff --git a/src/test/scala/github4s/utils/FakeResponses.scala b/github4s/jvm/src/test/scala/github4s/utils/FakeResponses.scala similarity index 100% rename from src/test/scala/github4s/utils/FakeResponses.scala rename to github4s/jvm/src/test/scala/github4s/utils/FakeResponses.scala diff --git a/src/test/scala/github4s/utils/MockGithubApiServer.scala b/github4s/jvm/src/test/scala/github4s/utils/MockGithubApiServer.scala similarity index 99% rename from src/test/scala/github4s/utils/MockGithubApiServer.scala rename to github4s/jvm/src/test/scala/github4s/utils/MockGithubApiServer.scala index 941b9feee..5fd8a4187 100644 --- a/src/test/scala/github4s/utils/MockGithubApiServer.scala +++ b/github4s/jvm/src/test/scala/github4s/utils/MockGithubApiServer.scala @@ -23,9 +23,8 @@ package github4s.utils import org.mockserver.model.HttpRequest._ import org.mockserver.model.HttpResponse._ -import org.mockserver.model.NottableString._ -import org.mockserver.model.{Parameter, ParameterBody} import org.mockserver.model.JsonBody._ +import org.mockserver.model.NottableString._ trait MockGithubApiServer extends MockServerService with FakeResponses with TestUtils { diff --git a/src/test/scala/github4s/utils/MockServerService.scala b/github4s/jvm/src/test/scala/github4s/utils/MockServerService.scala similarity index 100% rename from src/test/scala/github4s/utils/MockServerService.scala rename to github4s/jvm/src/test/scala/github4s/utils/MockServerService.scala diff --git a/src/test/scala/github4s/utils/TestUtils.scala b/github4s/jvm/src/test/scala/github4s/utils/TestUtils.scala similarity index 86% rename from src/test/scala/github4s/utils/TestUtils.scala rename to github4s/jvm/src/test/scala/github4s/utils/TestUtils.scala index ad7cc33f7..012307c9c 100644 --- a/src/test/scala/github4s/utils/TestUtils.scala +++ b/github4s/jvm/src/test/scala/github4s/utils/TestUtils.scala @@ -21,9 +21,7 @@ package github4s.utils -import github4s.free.domain.GistFile - -import scalaj.http.HttpConstants._ +import com.github.marklister.base64.Base64.Encoder trait TestUtils { @@ -34,8 +32,8 @@ trait TestUtils { val invalidUsername = "GHInvalidaUserName" val invalidPassword = "invalidPassword" - def validBasicAuth = s"Basic ${base64(s"$validUsername:")}" - def invalidBasicAuth = s"Basic ${base64(s"$validUsername:$invalidPassword")}" + def validBasicAuth = s"Basic ${s"$validUsername:".getBytes.toBase64}" + def invalidBasicAuth = s"Basic ${s"$validUsername:$invalidPassword".getBytes.toBase64}" val validScopes = List("public_repo") val validNote = "New access token" @@ -65,6 +63,6 @@ trait TestUtils { val validGistDescription = "A Gist" val validGistPublic = true - val validGistFile = GistFile("val meaningOfLife = 42") - val validGistFiles = Map("file1.scala" → validGistFile) + val validGistFileContent = "val meaningOfLife = 42" + val validGistFilename = "test.scala" } diff --git a/src/main/resources/application.conf b/github4s/shared/src/main/resources/application.conf similarity index 100% rename from src/main/resources/application.conf rename to github4s/shared/src/main/resources/application.conf diff --git a/src/main/scala/github4s/Decoders.scala b/github4s/shared/src/main/scala/github4s/Decoders.scala similarity index 99% rename from src/main/scala/github4s/Decoders.scala rename to github4s/shared/src/main/scala/github4s/Decoders.scala index fa7bece2a..4c52dac54 100644 --- a/src/main/scala/github4s/Decoders.scala +++ b/github4s/shared/src/main/scala/github4s/Decoders.scala @@ -22,7 +22,7 @@ package github4s import github4s.free.domain._ -import io.circe._, io.circe.generic.auto._, io.circe.jawn._, io.circe.syntax._ +import io.circe._, io.circe.generic.auto._, io.circe.syntax._ /** Implicit circe decoders of domains objects */ object Decoders { diff --git a/src/main/scala/github4s/Github.scala b/github4s/shared/src/main/scala/github4s/Github.scala similarity index 82% rename from src/main/scala/github4s/Github.scala rename to github4s/shared/src/main/scala/github4s/Github.scala index 247849d24..bbf24f113 100644 --- a/src/main/scala/github4s/Github.scala +++ b/github4s/shared/src/main/scala/github4s/Github.scala @@ -21,11 +21,12 @@ package github4s -import cats.data.{OptionT, EitherT} -import cats.{MonadError, ~>, RecursiveTailRecM} +import cats.data.{EitherT, OptionT} +import cats.{MonadError, RecursiveTailRecM, ~>} import cats.implicits._ import github4s.GithubResponses._ import github4s.app._ +import github4s.free.interpreters.Interpreters /** * Represent the Github API wrapper @@ -47,9 +48,11 @@ object Github { implicit class GithubIOSyntaxEither[A](gio: GHIO[GHResponse[A]]) { - def exec[M[_]](implicit I: (GitHub4s ~> M), - A: MonadError[M, Throwable], - TR: RecursiveTailRecM[M]): M[GHResponse[A]] = gio foldMap I + def exec[M[_], C](implicit I: Interpreters[M, C], + A: MonadError[M, Throwable], + TR: RecursiveTailRecM[M], + H: HttpRequestBuilderExtension[C, M]): M[GHResponse[A]] = + gio foldMap I.interpreters def liftGH: EitherT[GHIO, GHException, GHResult[A]] = EitherT[GHIO, GHException, GHResult[A]](gio) diff --git a/src/main/scala/github4s/GithubAPIs.scala b/github4s/shared/src/main/scala/github4s/GithubAPIs.scala similarity index 100% rename from src/main/scala/github4s/GithubAPIs.scala rename to github4s/shared/src/main/scala/github4s/GithubAPIs.scala diff --git a/src/main/scala/github4s/GithubDefaultUrls.scala b/github4s/shared/src/main/scala/github4s/GithubDefaultUrls.scala similarity index 100% rename from src/main/scala/github4s/GithubDefaultUrls.scala rename to github4s/shared/src/main/scala/github4s/GithubDefaultUrls.scala diff --git a/src/main/scala/github4s/GithubResponses.scala b/github4s/shared/src/main/scala/github4s/GithubResponses.scala similarity index 61% rename from src/main/scala/github4s/GithubResponses.scala rename to github4s/shared/src/main/scala/github4s/GithubResponses.scala index 06e8e34f9..d6454a177 100644 --- a/src/main/scala/github4s/GithubResponses.scala +++ b/github4s/shared/src/main/scala/github4s/GithubResponses.scala @@ -22,12 +22,7 @@ package github4s import cats.free.Free -import cats.implicits._ import github4s.app.GitHub4s -import io.circe.Decoder -import io.circe.parser._ -import io.circe.generic.auto._ -import scalaj.http.HttpResponse object GithubResponses { @@ -49,27 +44,4 @@ object GithubResponses { case class UnexpectedException(msg: String) extends GHException(msg) - def toEntity[A](response: HttpResponse[String])(implicit D: Decoder[A]): GHResponse[A] = - response match { - case r if r.isSuccess ⇒ - decode[A](r.body).fold( - e ⇒ Either.left(JsonParsingException(e.getMessage, r.body)), - result ⇒ Either.right(GHResult(result, r.code, toLowerCase(r.headers))) - ) - case r ⇒ - Either.left( - UnexpectedException(s"Failed invoking get with status : ${r.code}, body : \n ${r.body}")) - } - - def toEmpty(response: HttpResponse[String]): GHResponse[Unit] = response match { - case r if r.isSuccess ⇒ Either.right(GHResult(Unit, r.code, toLowerCase(r.headers))) - case r ⇒ - Either.left( - UnexpectedException(s"Failed invoking get with status : ${r.code}, body : \n ${r.body}")) - } - - private def toLowerCase( - headers: Map[String, IndexedSeq[String]]): Map[String, IndexedSeq[String]] = - headers.map(e ⇒ (e._1.toLowerCase, e._2)) - } diff --git a/github4s/shared/src/main/scala/github4s/HttpClient.scala b/github4s/shared/src/main/scala/github4s/HttpClient.scala new file mode 100644 index 000000000..090026add --- /dev/null +++ b/github4s/shared/src/main/scala/github4s/HttpClient.scala @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2016 47 Degrees, LLC. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package github4s + +import github4s.free.domain.Pagination +import io.circe.Decoder +import github4s.GithubResponses.GHResponse +import github4s.HttpClient._ +import github4s.free.interpreters.Capture + +object HttpClient { + type Headers = Map[String, String] + + sealed trait HttpVerb { + def verb: String + } + + case object Get extends HttpVerb { + def verb = "GET" + } + + case object Post extends HttpVerb { + def verb = "POST" + } + + case object Put extends HttpVerb { + def verb = "PUT" + } + + case object Delete extends HttpVerb { + def verb = "DELETE" + } + + case object Patch extends HttpVerb { + def verb = "PATCH" + } + + sealed trait HttpStatus { + def statusCode: Int + } + + case object HttpCode400 extends HttpStatus { + def statusCode = 400 + } +} + +class HttpRequestBuilder[C, M[_]]( + val url: String, + val httpVerb: HttpVerb = Get, + val authHeader: Map[String, String] = Map.empty[String, String], + val data: Option[String] = None, + val params: Map[String, String] = Map.empty[String, String], + val headers: Map[String, String] = Map.empty[String, String] +) { + + def postMethod = new HttpRequestBuilder[C, M](url, Post, authHeader, data, params, headers) + + def patchMethod = new HttpRequestBuilder[C, M](url, Patch, authHeader, data, params, headers) + + def putMethod = new HttpRequestBuilder[C, M](url, Put, authHeader, data, params, headers) + + def deleteMethod = new HttpRequestBuilder[C, M](url, Delete, authHeader, data, params, headers) + + def withAuth(accessToken: Option[String] = None) = { + val authHeader = accessToken match { + case Some(token) ⇒ Map("Authorization" → s"token $token") + case _ ⇒ Map.empty[String, String] + } + new HttpRequestBuilder[C, M](url, httpVerb, authHeader, data, params, headers) + } + + def withHeaders(headers: Map[String, String]) = + new HttpRequestBuilder[C, M](url, httpVerb, authHeader, data, params, headers) + + def withParams(params: Map[String, String]) = + new HttpRequestBuilder[C, M](url, httpVerb, authHeader, data, params, headers) + + def withData(data: String) = + new HttpRequestBuilder[C, M](url, httpVerb, authHeader, Option(data), params, headers) +} + +object HttpRequestBuilder { + def httpRequestBuilder[C, M[_]]( + url: String, + httpVerb: HttpVerb = Get, + authHeader: Map[String, String] = Map.empty[String, String], + data: Option[String] = None, + params: Map[String, String] = Map.empty[String, String], + headers: Map[String, String] = Map.empty[String, String] + ) = new HttpRequestBuilder[C, M](url, httpVerb, authHeader, data, params, headers) +} + +class HttpClient[C, M[_]](implicit urls: GithubApiUrls, + httpRbImpl: HttpRequestBuilderExtension[C, M]) { + import HttpRequestBuilder._ + + val defaultPagination = Pagination(1, 1000) + + def get[A]( + accessToken: Option[String] = None, + method: String, + params: Map[String, String] = Map.empty, + pagination: Option[Pagination] = None + )(implicit D: Decoder[A]): M[GHResponse[A]] = + httpRbImpl.run[A]( + httpRequestBuilder(buildURL(method)) + .withAuth(accessToken) + .withParams(params ++ pagination.fold(Map.empty[String, String])(p ⇒ + Map("page" → p.page.toString, "per_page" → p.per_page.toString))) + ) + + def patch[A](accessToken: Option[String] = None, method: String, data: String)( + implicit D: Decoder[A]): M[GHResponse[A]] = + httpRbImpl.run[A]( + httpRequestBuilder(buildURL(method)).patchMethod.withAuth(accessToken).withData(data)) + + def put[A](accessToken: Option[String] = None, method: String)( + implicit D: Decoder[A]): M[GHResponse[A]] = + httpRbImpl.run[A]( + httpRequestBuilder(buildURL(method)).putMethod + .withAuth(accessToken) + .withHeaders(Map("Content-Length" → "0"))) + + def post[A]( + accessToken: Option[String] = None, + method: String, + headers: Map[String, String] = Map.empty, + data: String + )(implicit D: Decoder[A]): M[GHResponse[A]] = + httpRbImpl.run[A]( + httpRequestBuilder(buildURL(method)).postMethod + .withAuth(accessToken) + .withHeaders(headers) + .withData(data)) + + def postAuth[A]( + method: String, + headers: Map[String, String] = Map.empty, + data: String + )(implicit D: Decoder[A]): M[GHResponse[A]] = + httpRbImpl.run[A]( + httpRequestBuilder(buildURL(method)).postMethod.withHeaders(headers).withData(data)) + + def postOAuth[A]( + url: String, + data: String + )(implicit D: Decoder[A]): M[GHResponse[A]] = + httpRbImpl.run[A]( + httpRequestBuilder(url).postMethod + .withHeaders(Map("Accept" → "application/json")) + .withData(data)) + + def delete[A](accessToken: Option[String] = None, method: String)( + implicit D: Decoder[A]): M[GHResponse[A]] = + httpRbImpl.run[A](httpRequestBuilder(buildURL(method)).deleteMethod.withAuth(accessToken)) + + private def buildURL(method: String) = urls.baseUrl + method + + val defaultPage: Int = 1 + val defaultPerPage: Int = 30 +} diff --git a/github4s/shared/src/main/scala/github4s/HttpClientExtension.scala b/github4s/shared/src/main/scala/github4s/HttpClientExtension.scala new file mode 100644 index 000000000..d90c683d0 --- /dev/null +++ b/github4s/shared/src/main/scala/github4s/HttpClientExtension.scala @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2016 47 Degrees, LLC. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package github4s + +import github4s.GithubResponses.GHResponse +import io.circe.Decoder + +import scala.language.higherKinds + +trait HttpRequestBuilderExtension[C, M[_]] { + def run[A](rb: HttpRequestBuilder[C, M])(implicit D: Decoder[A]): M[GHResponse[A]] +} diff --git a/src/main/scala/github4s/Implicits.scala b/github4s/shared/src/main/scala/github4s/Implicits.scala similarity index 95% rename from src/main/scala/github4s/Implicits.scala rename to github4s/shared/src/main/scala/github4s/Implicits.scala index 83dbb8372..2ffa289dd 100644 --- a/src/main/scala/github4s/Implicits.scala +++ b/github4s/shared/src/main/scala/github4s/Implicits.scala @@ -27,13 +27,17 @@ import cats.{Monad, Id, Eval, MonadError, FlatMap} import github4s.free.interpreters._ import scala.concurrent.{ExecutionContext, Future} -object implicits extends Interpreters with EvalInstances with IdInstances with FutureInstances { +object implicits + extends FutureCaptureInstance + with EvalInstances + with IdInstances + with FutureInstances +trait FutureCaptureInstance { //Future Capture evidence: implicit val futureCaptureInstance = new Capture[Future] { override def capture[A](a: ⇒ A): Future[A] = Future.successful(a) } - } trait EvalInstances { diff --git a/src/main/scala/github4s/api/Auth.scala b/github4s/shared/src/main/scala/github4s/api/Auth.scala similarity index 80% rename from src/main/scala/github4s/api/Auth.scala rename to github4s/shared/src/main/scala/github4s/api/Auth.scala index f4b4f3328..7ad13704b 100644 --- a/src/main/scala/github4s/api/Auth.scala +++ b/github4s/shared/src/main/scala/github4s/api/Auth.scala @@ -22,18 +22,22 @@ package github4s.api import java.util.UUID -import github4s.GithubResponses.{GHResult, GHResponse} + +import github4s.GithubResponses.{GHResponse, GHResult} import github4s.free.domain._ -import github4s.{GithubApiUrls, HttpClient} +import github4s.{GithubApiUrls, HttpClient, HttpRequestBuilderExtension} import io.circe.generic.auto._ import io.circe.syntax._ import cats.implicits._ -import scalaj.http.HttpConstants._ +import github4s.free.interpreters.Capture +import com.github.marklister.base64.Base64.Encoder /** Factory to encapsulate calls related to Auth operations */ -class Auth(implicit urls: GithubApiUrls) { +class Auth[C, M[_]](implicit urls: GithubApiUrls, + C: Capture[M], + httpClientImpl: HttpRequestBuilderExtension[C, M]) { - val httpClient = new HttpClient + val httpClient = new HttpClient[C, M] val authorizeUrl = urls.authorizeUrl val accessTokenUrl = urls.accessTokenUrl @@ -57,10 +61,10 @@ class Auth(implicit urls: GithubApiUrls) { note: String, client_id: String, client_secret: String - ): GHResponse[Authorization] = + ): M[GHResponse[Authorization]] = httpClient.postAuth[Authorization]( method = "authorizations", - headers = Map("Authorization" → s"Basic ${base64(s"$username:$password")}"), + headers = Map("Authorization" → s"Basic ${s"$username:$password".getBytes.toBase64}"), data = NewAuthRequest(scopes, note, client_id, client_secret).asJson.noSpaces ) @@ -76,15 +80,17 @@ class Auth(implicit urls: GithubApiUrls) { client_id: String, redirect_uri: String, scopes: List[String] - ): GHResponse[Authorize] = { + ): M[GHResponse[Authorize]] = { val state = UUID.randomUUID().toString - Either.right( - GHResult( - result = - Authorize(authorizeUrl.format(client_id, redirect_uri, scopes.mkString(","), state), - state), - statusCode = 200, - headers = Map.empty + C.capture( + Either.right( + GHResult( + result = + Authorize(authorizeUrl.format(client_id, redirect_uri, scopes.mkString(","), state), + state), + statusCode = 200, + headers = Map.empty + ) ) ) } @@ -105,7 +111,7 @@ class Auth(implicit urls: GithubApiUrls) { code: String, redirect_uri: String, state: String - ): GHResponse[OAuthToken] = httpClient.postOAuth[OAuthToken]( + ): M[GHResponse[OAuthToken]] = httpClient.postOAuth[OAuthToken]( url = accessTokenUrl, data = NewOAuthRequest(client_id, client_secret, code, redirect_uri, state).asJson.noSpaces ) diff --git a/src/main/scala/github4s/api/Gists.scala b/github4s/shared/src/main/scala/github4s/api/Gists.scala similarity index 83% rename from src/main/scala/github4s/api/Gists.scala rename to github4s/shared/src/main/scala/github4s/api/Gists.scala index 6546865c8..b92d089a0 100644 --- a/src/main/scala/github4s/api/Gists.scala +++ b/github4s/shared/src/main/scala/github4s/api/Gists.scala @@ -22,16 +22,20 @@ package github4s.api import github4s.free.domain._ -import github4s.{Decoders, GithubApiUrls, HttpClient} +import github4s._ import io.circe.generic.auto._ import io.circe.syntax._ import cats.implicits._ +import github4s.GithubResponses.GHResponse +import github4s.free.interpreters.Capture /** Factory to encapsulate calls related to Repositories operations */ -class Gists(implicit urls: GithubApiUrls) { +class Gists[C, M[_]](implicit urls: GithubApiUrls, + C: Capture[M], + httpClientImpl: HttpRequestBuilderExtension[C, M]) { import Decoders._ - val httpClient = new HttpClient + val httpClient = new HttpClient[C, M] /** * Create a new gist @@ -44,7 +48,7 @@ class Gists(implicit urls: GithubApiUrls) { def newGist(description: String, public: Boolean, files: Map[String, GistFile], - accessToken: Option[String] = None) = + accessToken: Option[String] = None): M[GHResponse[Gist]] = httpClient.post[Gist]( accessToken, "gists", diff --git a/src/main/scala/github4s/api/Repos.scala b/github4s/shared/src/main/scala/github4s/api/Repos.scala similarity index 90% rename from src/main/scala/github4s/api/Repos.scala rename to github4s/shared/src/main/scala/github4s/api/Repos.scala index a8fa8361c..cdf30fba7 100644 --- a/src/main/scala/github4s/api/Repos.scala +++ b/github4s/shared/src/main/scala/github4s/api/Repos.scala @@ -22,16 +22,19 @@ package github4s.api import github4s.GithubResponses.GHResponse -import github4s.free.domain.{Pagination, Commit, Repository, User} -import github4s.{GithubApiUrls, Decoders, HttpClient} +import github4s.free.domain.{Commit, Pagination, Repository, User} +import github4s.free.interpreters.Capture +import github4s._ import io.circe.generic.auto._ /** Factory to encapsulate calls related to Repositories operations */ -class Repos(implicit urls: GithubApiUrls) { +class Repos[C, M[_]](implicit urls: GithubApiUrls, + C: Capture[M], + httpClientImpl: HttpRequestBuilderExtension[C, M]) { import Decoders._ - val httpClient = new HttpClient + val httpClient = new HttpClient[C, M] /** * Get information of a particular repository @@ -43,7 +46,7 @@ class Repos(implicit urls: GithubApiUrls) { */ def get(accessToken: Option[String] = None, owner: String, - repo: String): GHResponse[Repository] = + repo: String): M[GHResponse[Repository]] = httpClient.get[Repository](accessToken, s"repos/$owner/$repo") /** @@ -70,7 +73,7 @@ class Repos(implicit urls: GithubApiUrls) { since: Option[String] = None, until: Option[String] = None, pagination: Option[Pagination] = Some(httpClient.defaultPagination) - ): GHResponse[List[Commit]] = + ): M[GHResponse[List[Commit]]] = httpClient.get[List[Commit]](accessToken, s"repos/$owner/$repo/commits", Map( @@ -99,7 +102,7 @@ class Repos(implicit urls: GithubApiUrls) { owner: String, repo: String, anon: Option[String] = None - ): GHResponse[List[User]] = + ): M[GHResponse[List[User]]] = httpClient.get[List[User]](accessToken, s"repos/$owner/$repo/contributors", Map( diff --git a/src/main/scala/github4s/api/Users.scala b/github4s/shared/src/main/scala/github4s/api/Users.scala similarity index 84% rename from src/main/scala/github4s/api/Users.scala rename to github4s/shared/src/main/scala/github4s/api/Users.scala index ff74aa4c7..da54d814a 100644 --- a/src/main/scala/github4s/api/Users.scala +++ b/github4s/shared/src/main/scala/github4s/api/Users.scala @@ -22,14 +22,17 @@ package github4s.api import github4s.GithubResponses.GHResponse -import github4s.{GithubApiUrls, HttpClient} +import github4s.{GithubApiUrls, HttpClient, HttpRequestBuilderExtension} import github4s.free.domain.{Pagination, User} +import github4s.free.interpreters.Capture import io.circe.generic.auto._ /** Factory to encapsulate calls related to Users operations */ -class Users(implicit urls: GithubApiUrls) { +class Users[C, M[_]](implicit urls: GithubApiUrls, + C: Capture[M], + httpClientImpl: HttpRequestBuilderExtension[C, M]) { - val httpClient = new HttpClient + val httpClient = new HttpClient[C, M] /** * Get information for a particular user @@ -38,7 +41,7 @@ class Users(implicit urls: GithubApiUrls) { * @param username of the user to retrieve * @return GHResponse[User] User details */ - def get(accessToken: Option[String] = None, username: String): GHResponse[User] = + def get(accessToken: Option[String] = None, username: String): M[GHResponse[User]] = httpClient.get[User](accessToken, s"users/$username") /** @@ -46,7 +49,7 @@ class Users(implicit urls: GithubApiUrls) { * @param accessToken to identify the authenticated user * @return GHResponse[User] User details */ - def getAuth(accessToken: Option[String] = None): GHResponse[User] = + def getAuth(accessToken: Option[String] = None): M[GHResponse[User]] = httpClient.get[User](accessToken, "user") /** @@ -61,7 +64,7 @@ class Users(implicit urls: GithubApiUrls) { accessToken: Option[String] = None, since: Int, pagination: Option[Pagination] = None - ): GHResponse[List[User]] = + ): M[GHResponse[List[User]]] = httpClient.get[List[User]](accessToken, "users", Map("since" → since.toString), pagination) } diff --git a/src/main/scala/github4s/app.scala b/github4s/shared/src/main/scala/github4s/app.scala similarity index 100% rename from src/main/scala/github4s/app.scala rename to github4s/shared/src/main/scala/github4s/app.scala diff --git a/src/main/scala/github4s/free/algebra/AuthOps.scala b/github4s/shared/src/main/scala/github4s/free/algebra/AuthOps.scala similarity index 100% rename from src/main/scala/github4s/free/algebra/AuthOps.scala rename to github4s/shared/src/main/scala/github4s/free/algebra/AuthOps.scala diff --git a/src/main/scala/github4s/free/algebra/GistOps.scala b/github4s/shared/src/main/scala/github4s/free/algebra/GistOps.scala similarity index 100% rename from src/main/scala/github4s/free/algebra/GistOps.scala rename to github4s/shared/src/main/scala/github4s/free/algebra/GistOps.scala diff --git a/src/main/scala/github4s/free/algebra/RepositoryOps.scala b/github4s/shared/src/main/scala/github4s/free/algebra/RepositoryOps.scala similarity index 100% rename from src/main/scala/github4s/free/algebra/RepositoryOps.scala rename to github4s/shared/src/main/scala/github4s/free/algebra/RepositoryOps.scala diff --git a/src/main/scala/github4s/free/algebra/UserOps.scala b/github4s/shared/src/main/scala/github4s/free/algebra/UserOps.scala similarity index 100% rename from src/main/scala/github4s/free/algebra/UserOps.scala rename to github4s/shared/src/main/scala/github4s/free/algebra/UserOps.scala diff --git a/src/main/scala/github4s/free/domain/Authorization.scala b/github4s/shared/src/main/scala/github4s/free/domain/Authorization.scala similarity index 100% rename from src/main/scala/github4s/free/domain/Authorization.scala rename to github4s/shared/src/main/scala/github4s/free/domain/Authorization.scala diff --git a/src/main/scala/github4s/free/domain/Commit.scala b/github4s/shared/src/main/scala/github4s/free/domain/Commit.scala similarity index 100% rename from src/main/scala/github4s/free/domain/Commit.scala rename to github4s/shared/src/main/scala/github4s/free/domain/Commit.scala diff --git a/src/main/scala/github4s/free/domain/Gist.scala b/github4s/shared/src/main/scala/github4s/free/domain/Gist.scala similarity index 100% rename from src/main/scala/github4s/free/domain/Gist.scala rename to github4s/shared/src/main/scala/github4s/free/domain/Gist.scala diff --git a/src/main/scala/github4s/free/domain/Pagination.scala b/github4s/shared/src/main/scala/github4s/free/domain/Pagination.scala similarity index 100% rename from src/main/scala/github4s/free/domain/Pagination.scala rename to github4s/shared/src/main/scala/github4s/free/domain/Pagination.scala diff --git a/src/main/scala/github4s/free/domain/Repository.scala b/github4s/shared/src/main/scala/github4s/free/domain/Repository.scala similarity index 100% rename from src/main/scala/github4s/free/domain/Repository.scala rename to github4s/shared/src/main/scala/github4s/free/domain/Repository.scala diff --git a/src/main/scala/github4s/free/domain/User.scala b/github4s/shared/src/main/scala/github4s/free/domain/User.scala similarity index 100% rename from src/main/scala/github4s/free/domain/User.scala rename to github4s/shared/src/main/scala/github4s/free/domain/User.scala diff --git a/github4s/shared/src/main/scala/github4s/free/interpreters/Interpreters.scala b/github4s/shared/src/main/scala/github4s/free/interpreters/Interpreters.scala new file mode 100644 index 000000000..8057878ed --- /dev/null +++ b/github4s/shared/src/main/scala/github4s/free/interpreters/Interpreters.scala @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2016 47 Degrees, LLC. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package github4s.free.interpreters + +import cats.implicits._ +import cats.{ApplicativeError, Eval, MonadError, ~>} +import github4s.GithubDefaultUrls._ +import github4s.HttpRequestBuilderExtension +import github4s.api.{Auth, Gists, Repos, Users} +import github4s.app.{COGH01, COGH02, GitHub4s} +import github4s.free.algebra._ +import io.circe.Decoder +import simulacrum.typeclass + +@typeclass +trait Capture[M[_]] { + def capture[A](a: ⇒ A): M[A] +} + +class Interpreters[M[_], C](implicit A: ApplicativeError[M, Throwable], + C: Capture[M], + httpClientImpl: HttpRequestBuilderExtension[C, M]) { + + implicit def interpreters( + implicit A: MonadError[M, Throwable] + ): GitHub4s ~> M = { + val c01interpreter: COGH01 ~> M = repositoryOpsInterpreter or userOpsInterpreter + val c02interpreter: COGH02 ~> M = gistOpsInterpreter or c01interpreter + val all: GitHub4s ~> M = authOpsInterpreter or c02interpreter + all + } + + /** + * Lifts Repository Ops to an effect capturing Monad such as Task via natural transformations + */ + def repositoryOpsInterpreter: RepositoryOp ~> M = new (RepositoryOp ~> M) { + + val repos = new Repos() + + def apply[A](fa: RepositoryOp[A]): M[A] = fa match { + case GetRepo(owner, repo, accessToken) ⇒ repos.get(accessToken, owner, repo) + case ListCommits(owner, repo, sha, path, author, since, until, pagination, accessToken) ⇒ + repos.listCommits(accessToken, owner, repo, sha, path, author, since, until, pagination) + case ListContributors(owner, repo, anon, accessToken) ⇒ + repos.listContributors(accessToken, owner, repo, anon) + } + } + + /** + * Lifts User Ops to an effect capturing Monad such as Task via natural transformations + */ + def userOpsInterpreter: UserOp ~> M = + new (UserOp ~> M) { + + val users = new Users() + + def apply[A](fa: UserOp[A]): M[A] = fa match { + case GetUser(username, accessToken) ⇒ users.get(accessToken, username) + case GetAuthUser(accessToken) ⇒ users.getAuth(accessToken) + case GetUsers(since, pagination, accessToken) ⇒ + users.getUsers(accessToken, since, pagination) + } + } + + /** + * Lifts Auth Ops to an effect capturing Monad such as Task via natural transformations + */ + def authOpsInterpreter: AuthOp ~> M = + new (AuthOp ~> M) { + + val auth = new Auth() + + def apply[A](fa: AuthOp[A]): M[A] = fa match { + case NewAuth(username, password, scopes, note, client_id, client_secret) ⇒ + auth.newAuth(username, password, scopes, note, client_id, client_secret) + case AuthorizeUrl(client_id, redirect_uri, scopes) ⇒ + auth.authorizeUrl(client_id, redirect_uri, scopes) + case GetAccessToken(client_id, client_secret, code, redirect_uri, state) ⇒ + auth.getAccessToken(client_id, client_secret, code, redirect_uri, state) + } + } + + /** + * Lifts Gist Ops to an effect capturing Monad such as Task via natural transformations + */ + def gistOpsInterpreter: GistOp ~> M = + new (GistOp ~> M) { + + val gists = new Gists() + + def apply[A](fa: GistOp[A]): M[A] = fa match { + case NewGist(description, public, files, accessToken) ⇒ + gists.newGist(description, public, files, accessToken) + } + } + +} diff --git a/project/plugins.sbt b/project/plugins.sbt index b16c9dc2e..612b7a634 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,3 +1,4 @@ addSbtPlugin("com.fortysevendeg" % "sbt-catalysts-extras" % "0.0.4") addSbtPlugin("com.geirsson" % "sbt-scalafmt" % "0.4.7") addSbtPlugin("com.fortysevendeg" % "sbt-microsites" % "0.2.6") +addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.4.0") \ No newline at end of file diff --git a/scalaz/src/main/scala/github4s/scalaz/Implicits.scala b/scalaz/src/main/scala/github4s/scalaz/Implicits.scala index 4ff99174a..28066ae1b 100644 --- a/scalaz/src/main/scala/github4s/scalaz/Implicits.scala +++ b/scalaz/src/main/scala/github4s/scalaz/Implicits.scala @@ -24,11 +24,15 @@ package github4s.scalaz import cats.implicits._ import github4s.Github._ import cats.{MonadError, RecursiveTailRecM} +import github4s.HttpRequestBuilderExtensionJVM + import scalaz.concurrent.Task -import github4s.free.interpreters.Capture +import github4s.free.interpreters.{Capture, Interpreters} + +import scalaj.http.HttpResponse import scalaz._ -object implicits { +object implicits extends HttpRequestBuilderExtensionJVM { implicit val taskCaptureInstance = new Capture[Task] { override def capture[A](a: ⇒ A): Task[A] = Task.now(a) @@ -55,6 +59,8 @@ object implicits { } + implicit val intInstanceTaskScalaJ = new Interpreters[Task, HttpResponse[String]] + private[this] def toScalazDisjunction[A, B](disj: Either[A, B]): A \/ B = disj.fold(l ⇒ -\/(l), r ⇒ \/-(r)) } diff --git a/src/main/scala/github4s/HttpClient.scala b/src/main/scala/github4s/HttpClient.scala deleted file mode 100644 index 7013c5377..000000000 --- a/src/main/scala/github4s/HttpClient.scala +++ /dev/null @@ -1,172 +0,0 @@ -/* - * Copyright (c) 2016 47 Degrees, LLC. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to - * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of - * the Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER - * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -package github4s - -import github4s.GithubResponses.GHResponse -import github4s.free.domain.Pagination -import io.circe.Decoder -import scalaj.http._ - -class HttpClient(implicit urls: GithubApiUrls) { - - val defaultPagination = Pagination(1, 1000) - - sealed trait HttpVerb { - def verb: String - } - - case object Get extends HttpVerb { - def verb = "GET" - } - - case object Post extends HttpVerb { - def verb = "POST" - } - - case object Put extends HttpVerb { - def verb = "PUT" - } - - case object Delete extends HttpVerb { - def verb = "DELETE" - } - - case object Patch extends HttpVerb { - def verb = "PATCH" - } - - case class HttpRequestBuilder( - url: String, - httVerb: HttpVerb = Get, - connectionTimeout: Int = connTimeoutMs, - readTimeOut: Int = readTimeoutMs, - authHeader: Map[String, String] = Map.empty[String, String], - data: Option[String] = None, - params: Map[String, String] = Map.empty[String, String], - headers: Map[String, String] = Map.empty[String, String] - ) { - - def postMethod = copy(httVerb = Post) - - def patchMethod = copy(httVerb = Patch) - - def putMethod = copy(httVerb = Put) - - def deleteMethod = copy(httVerb = Delete) - - def withAuth(accessToken: Option[String] = None) = - copy(authHeader = accessToken match { - case Some(token) ⇒ Map("Authorization" → s"token $token") - case _ ⇒ Map.empty[String, String] - }) - - def withHeaders(headers: Map[String, String]) = copy(headers = headers) - - def withParams(params: Map[String, String]) = copy(params = params) - - def withData(data: String) = copy(data = Option(data)) - - def run = { - val request = Http(url) - .method(httVerb.verb) - .option(HttpOptions.connTimeout(connectionTimeout)) - .option(HttpOptions.readTimeout(readTimeOut)) - .params(params) - .headers(authHeader) - .headers(headers) - - data match { - case Some(d) ⇒ request.postData(d).header("content-type", "application/json").asString - case _ ⇒ request.asString - } - } - - } - - def get[A]( - accessToken: Option[String] = None, - method: String, - params: Map[String, String] = Map.empty, - pagination: Option[Pagination] = None - )(implicit D: Decoder[A]): GHResponse[A] = - GithubResponses.toEntity( - HttpRequestBuilder(buildURL(method)) - .withAuth(accessToken) - .withParams(params ++ pagination.fold(Map.empty[String, String])(p ⇒ - Map("page" → p.page.toString, "per_page" → p.per_page.toString))) - .run) - - def patch[A](accessToken: Option[String] = None, method: String, data: String)( - implicit D: Decoder[A]): GHResponse[A] = - GithubResponses.toEntity( - HttpRequestBuilder(buildURL(method)).patchMethod.withAuth(accessToken).withData(data).run) - - def put(accessToken: Option[String] = None, method: String): GHResponse[Unit] = - GithubResponses.toEmpty( - HttpRequestBuilder(buildURL(method)).putMethod - .withAuth(accessToken) - .withHeaders(Map("Content-Length" → "0")) - .run) - - def post[A]( - accessToken: Option[String] = None, - method: String, - headers: Map[String, String] = Map.empty, - data: String - )(implicit D: Decoder[A]): GHResponse[A] = - GithubResponses.toEntity( - HttpRequestBuilder(buildURL(method)) - .withAuth(accessToken) - .withHeaders(headers) - .withData(data) - .run) - - def postAuth[A]( - method: String, - headers: Map[String, String] = Map.empty, - data: String - )(implicit D: Decoder[A]): GHResponse[A] = - GithubResponses.toEntity( - HttpRequestBuilder(buildURL(method)).withHeaders(headers).withData(data).run) - - def postOAuth[A]( - url: String, - data: String - )(implicit D: Decoder[A]): GHResponse[A] = - GithubResponses.toEntity( - HttpRequestBuilder(url).postMethod - .withHeaders(Map("Accept" → "application/json")) - .withData(data) - .run) - - def delete(accessToken: Option[String] = None, method: String): GHResponse[Unit] = - GithubResponses.toEmpty( - HttpRequestBuilder(buildURL(method)).deleteMethod.withAuth(accessToken).run) - - private val connTimeoutMs: Int = 1000 - private val readTimeoutMs: Int = 5000 - val defaultPage: Int = 1 - val defaultPerPage: Int = 30 - - private def buildURL(method: String) = urls.baseUrl + method - -} diff --git a/src/main/scala/github4s/free/interpreters/Interpreters.scala b/src/main/scala/github4s/free/interpreters/Interpreters.scala deleted file mode 100644 index c69575633..000000000 --- a/src/main/scala/github4s/free/interpreters/Interpreters.scala +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright (c) 2016 47 Degrees, LLC. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to - * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of - * the Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER - * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -package github4s.free.interpreters - -import cats.implicits._ -import cats.{ApplicativeError, Eval, MonadError, ~>} -import github4s.GithubDefaultUrls._ -import github4s.api.{Auth, Gists, Repos, Users} -import github4s.app.{COGH01, COGH02, GitHub4s} -import github4s.free.algebra._ -import io.circe.Decoder -import simulacrum.typeclass - -@typeclass -trait Capture[M[_]] { - def capture[A](a: ⇒ A): M[A] -} - -trait Interpreters { - - implicit def interpreters[M[_]]( - implicit A: MonadError[M, Throwable], - C: Capture[M] - ): GitHub4s ~> M = { - val c01interpreter: COGH01 ~> M = repositoryOpsInterpreter[M] or userOpsInterpreter[M] - val c02interpreter: COGH02 ~> M = gistOpsInterpreter[M] or c01interpreter - val all: GitHub4s ~> M = authOpsInterpreter[M] or c02interpreter - all - } - - /** - * Lifts Repository Ops to an effect capturing Monad such as Task via natural transformations - */ - def repositoryOpsInterpreter[M[_]](implicit A: ApplicativeError[M, Throwable], - C: Capture[M]): RepositoryOp ~> M = new (RepositoryOp ~> M) { - - val repos = new Repos() - - def apply[A](fa: RepositoryOp[A]): M[A] = fa match { - case GetRepo(owner, repo, accessToken) ⇒ C.capture(repos.get(accessToken, owner, repo)) - case ListCommits(owner, repo, sha, path, author, since, until, pagination, accessToken) ⇒ - C.capture( - repos.listCommits(accessToken, owner, repo, sha, path, author, since, until, pagination)) - case ListContributors(owner, repo, anon, accessToken) ⇒ - C.capture(repos.listContributors(accessToken, owner, repo, anon)) - } - } - - /** - * Lifts User Ops to an effect capturing Monad such as Task via natural transformations - */ - def userOpsInterpreter[M[_]](implicit A: ApplicativeError[M, Throwable], - C: Capture[M]): UserOp ~> M = new (UserOp ~> M) { - - val users = new Users() - - def apply[A](fa: UserOp[A]): M[A] = fa match { - case GetUser(username, accessToken) ⇒ C.capture(users.get(accessToken, username)) - case GetAuthUser(accessToken) ⇒ C.capture(users.getAuth(accessToken)) - case GetUsers(since, pagination, accessToken) ⇒ - C.capture(users.getUsers(accessToken, since, pagination)) - } - } - - /** - * Lifts Auth Ops to an effect capturing Monad such as Task via natural transformations - */ - def authOpsInterpreter[M[_]](implicit A: ApplicativeError[M, Throwable], - C: Capture[M]): AuthOp ~> M = new (AuthOp ~> M) { - - val auth = new Auth() - - def apply[A](fa: AuthOp[A]): M[A] = fa match { - case NewAuth(username, password, scopes, note, client_id, client_secret) ⇒ - C.capture(auth.newAuth(username, password, scopes, note, client_id, client_secret)) - case AuthorizeUrl(client_id, redirect_uri, scopes) ⇒ - C.capture(auth.authorizeUrl(client_id, redirect_uri, scopes)) - case GetAccessToken(client_id, client_secret, code, redirect_uri, state) ⇒ - C.capture(auth.getAccessToken(client_id, client_secret, code, redirect_uri, state)) - } - } - - /** - * Lifts Gist Ops to an effect capturing Monad such as Task via natural transformations - */ - def gistOpsInterpreter[M[_]](implicit A: ApplicativeError[M, Throwable], - C: Capture[M]): GistOp ~> M = new (GistOp ~> M) { - - val gists = new Gists() - - def apply[A](fa: GistOp[A]): M[A] = fa match { - case NewGist(description, public, files, accessToken) ⇒ - C.capture(gists.newGist(description, public, files, accessToken)) - } - } - -}