Skip to content

Commit

Permalink
Merge pull request #38 from 47deg/g4s-10-support-for-scalajs
Browse files Browse the repository at this point in the history
Compatibility with scala-js
  • Loading branch information
Javier de Silóniz Sandino authored Nov 2, 2016
2 parents 3d82dcf + 544652b commit a714e61
Show file tree
Hide file tree
Showing 54 changed files with 1,254 additions and 420 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ target/
.idea
docs/src/jekyll/_site/
docs/src/jekyll/*.md
.DS_Store

# PGP keys
*.gpg
42 changes: 35 additions & 7 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ lazy val buildSettings = Seq(
"scala" -> MIT("2016", "47 Degrees, LLC. <http://www.47deg.com>")
)
) ++ reformatOnCompileSettings ++
sharedCommonSettings ++
miscSettings ++
sharedReleaseProcess ++
credentialSettings ++
Expand All @@ -39,32 +40,55 @@ 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"
)

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)
Expand All @@ -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
)
67 changes: 50 additions & 17 deletions docs/src/main/tut/docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,51 @@ layout: docs
title: Getting Started
---

# Get started
# Getting started

WIP: Import

```tut:silent
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.
Expand All @@ -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
}
Expand All @@ -55,31 +68,41 @@ 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`

```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
Expand All @@ -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
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* Copyright (c) 2016 47 Degrees, LLC. <http://www.47deg.com>
*
* 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)))

}
24 changes: 24 additions & 0 deletions github4s/js/src/main/scala/github4s/js/Implicits.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright (c) 2016 47 Degrees, LLC. <http://www.47deg.com>
*
* 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
Loading

0 comments on commit a714e61

Please sign in to comment.