Skip to content

Commit a714e61

Browse files
author
Javier de Silóniz Sandino
authored
Merge pull request #38 from 47deg/g4s-10-support-for-scalajs
Compatibility with scala-js
2 parents 3d82dcf + 544652b commit a714e61

54 files changed

Lines changed: 1254 additions & 420 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ target/
33
.idea
44
docs/src/jekyll/_site/
55
docs/src/jekyll/*.md
6+
.DS_Store
67

78
# PGP keys
89
*.gpg

build.sbt

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ lazy val buildSettings = Seq(
2424
"scala" -> MIT("2016", "47 Degrees, LLC. <http://www.47deg.com>")
2525
)
2626
) ++ reformatOnCompileSettings ++
27+
sharedCommonSettings ++
2728
miscSettings ++
2829
sharedReleaseProcess ++
2930
credentialSettings ++
@@ -39,32 +40,55 @@ lazy val micrositeSettings = Seq(
3940
includeFilter in makeSite := "*.html" | "*.css" | "*.png" | "*.jpg" | "*.gif" | "*.js" | "*.swf" | "*.md"
4041
)
4142

42-
lazy val dependencies = addLibs(vAll,
43+
lazy val commonDeps = addLibs(vAll,
4344
"cats-free",
4445
"circe-core",
4546
"circe-generic",
4647
"circe-parser",
4748
"simulacrum") ++
48-
addTestLibs(vAll, "scalatest") ++
4949
addCompilerPlugins(vAll, "paradise") ++
50-
Seq(
50+
Seq(libraryDependencies ++= Seq(
51+
"org.scalatest" %%% "scalatest" % "3.0.0" % "test",
52+
"com.github.marklister" %%% "base64" % "0.2.2"
53+
))
54+
55+
lazy val jvmDeps = Seq(
5156
libraryDependencies ++= Seq(
5257
"org.scalaj" %% "scalaj-http" % "2.2.1",
5358
"org.mock-server" % "mockserver-netty" % "3.10.4" % "test"
5459
))
5560

61+
lazy val jsDeps = Seq(
62+
libraryDependencies ++= Seq(
63+
"fr.hmil" %%% "roshttp" % "2.0.0-RC1"
64+
)
65+
)
66+
5667
lazy val docsDependencies = libraryDependencies ++= Seq(
5768
"com.ironcorelabs" %% "cats-scalatest" % "1.1.2" % "test",
5869
"org.mock-server" % "mockserver-netty" % "3.10.4" % "test"
5970
)
6071

6172
lazy val scalazDependencies = addLibs(vAll, "scalaz-concurrent")
6273

63-
lazy val github4s = (project in file("."))
74+
/** github4s - cross project that provides cross platform support.*/
75+
lazy val github4s = (crossProject in file("github4s"))
6476
.settings(moduleName := "github4s")
65-
.settings(buildSettings: _*)
66-
.settings(dependencies: _*)
6777
.enablePlugins(AutomateHeaderPlugin)
78+
.enablePlugins(BuildInfoPlugin).
79+
settings(
80+
buildInfoKeys := Seq[BuildInfoKey](name, version, "token" -> Option(sys.props("token")).getOrElse("")),
81+
buildInfoPackage := "github4s"
82+
)
83+
.settings(buildSettings: _*)
84+
.settings(commonDeps: _*)
85+
.jvmSettings(jvmDeps: _*)
86+
.jsSettings(sharedJsSettings: _*)
87+
.jsSettings(testSettings: _*)
88+
.jsSettings(jsDeps: _*)
89+
90+
lazy val github4sJVM = github4s.jvm
91+
lazy val github4sJS = github4s.js
6892

6993
lazy val docs = (project in file("docs"))
7094
.dependsOn(scalaz)
@@ -79,5 +103,9 @@ lazy val scalaz = (project in file("scalaz"))
79103
.settings(moduleName := "github4s-scalaz")
80104
.settings(buildSettings: _*)
81105
.settings(scalazDependencies: _*)
82-
.dependsOn(github4s)
106+
.dependsOn(github4sJVM)
83107
.enablePlugins(AutomateHeaderPlugin)
108+
109+
lazy val testSettings = Seq(
110+
fork in Test := false
111+
)

docs/src/main/tut/docs.md

Lines changed: 50 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,38 +3,51 @@ layout: docs
33
title: Getting Started
44
---
55

6-
# Get started
6+
# Getting started
77

88
WIP: Import
99

1010
```tut:silent
1111
import github4s.Github
1212
```
1313

14+
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:
15+
16+
```tut:silent
17+
import github4s.jvm.Implicits._
18+
```
19+
20+
```tut:silent
21+
// import github4s.js.Implicits._
22+
```
23+
1424
```tut:invisible
1525
val accessToken = sys.props.get("token")
1626
```
1727

18-
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:
28+
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:
1929

2030
```scala
2131
case class GHResult[A](result: A, statusCode: Int, headers: Map[String, IndexedSeq[String]])
2232
```
2333

24-
For geting an user
34+
For getting an user
2535

2636
```tut:silent
2737
val user1 = Github(accessToken).users.get("rafaparadela")
2838
```
2939

30-
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`.
40+
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[_]`:
3141

3242
```tut:silent
3343
import cats.Eval
3444
import github4s.Github._
35-
import github4s.implicits._
45+
import scalaj.http._
46+
47+
object ProgramEval {
48+
val u1 = user1.exec[Eval, HttpResponse[String]].value
49+
}
3650
37-
val u1 = user1.exec[Eval].value
3851
```
3952

4053
WIP: As mentioned above `u1` should have an `GHResult[User]` in the right.
@@ -45,7 +58,7 @@ import github4s.GithubResponses.GHResult
4558
```
4659

4760
```tut:book
48-
u1 match {
61+
ProgramEval.u1 match {
4962
case Right(GHResult(result, status, headers)) => result.login
5063
case Left(e) => e.getMessage
5164
}
@@ -55,31 +68,41 @@ WIP: With `Id`
5568

5669
```tut:silent
5770
import cats.Id
71+
import scalaj.http._
5872
59-
val u2 = Github(accessToken).users.get("raulraja").exec[Id]
73+
object ProgramId {
74+
val u2 = Github(accessToken).users.get("raulraja").exec[Id, HttpResponse[String]]
75+
}
6076
```
6177

6278
WIP: With `Future`
6379

6480
```tut:silent
65-
import github4s.implicits._
81+
import cats.Id
6682
import scala.concurrent.Future
6783
import scala.concurrent.ExecutionContext.Implicits.global
6884
import scala.concurrent.duration._
6985
import scala.concurrent.Await
86+
import scalaj.http._
7087
71-
val u3 = Github(accessToken).users.get("dialelo").exec[Future]
72-
Await.result(u3, 2.seconds)
88+
object ProgramFuture {
89+
val u3 = Github(accessToken).users.get("dialelo").exec[Future, HttpResponse[String]]
90+
Await.result(u3, 2.seconds)
91+
}
7392
```
7493

7594
WIP: With `scalaz.Task`
7695

7796
```tut:silent
7897
import scalaz.concurrent.Task
7998
import github4s.scalaz.implicits._
99+
import scalaj.http._
100+
import github4s.jvm.Implicits._
80101
81-
val u4 = Github(accessToken).users.get("franciscodr").exec[Task]
82-
u4.attemptRun
102+
object ProgramTask {
103+
val u4 = Github(accessToken).users.get("franciscodr").exec[Task, HttpResponse[String]]
104+
u4.attemptRun
105+
}
83106
```
84107

85108
```tut:invisible
@@ -89,14 +112,24 @@ import cats.Eval
89112
import cats.implicits._
90113
import github4s.Github
91114
import github4s.Github._
92-
import github4s.implicits._
115+
import github4s.jvm.Implicits._
116+
import scalaj.http._
93117
94118
val accessToken = sys.props.get("token")
95119
```
96120

97121
```tut:book
98-
val user1 = Github(accessToken).users.get("rafaparadela").exec[Eval].value
122+
object ProgramEval {
123+
val user1 = Github(accessToken).users.get("rafaparadela").exec[Eval, HttpResponse[String]].value
124+
}
99125
100-
user1 should be ('right)
101-
user1.toOption map (_.result.login shouldBe "rafaparadela")
126+
ProgramEval.user1 should be ('right)
127+
ProgramEval.user1.toOption map (_.result.login shouldBe "rafaparadela")
102128
```
129+
130+
# Test credentials
131+
132+
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":
133+
134+
sbt -Dtoken=ACCESS_TOKEN_STRING
135+
```
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/*
2+
* Copyright (c) 2016 47 Degrees, LLC. <http://www.47deg.com>
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a copy of
5+
* this software and associated documentation files (the "Software"), to deal in
6+
* the Software without restriction, including without limitation the rights to
7+
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
8+
* the Software, and to permit persons to whom the Software is furnished to do so,
9+
* subject to the following conditions:
10+
*
11+
* The above copyright notice and this permission notice shall be included in all
12+
* copies or substantial portions of the Software.
13+
*
14+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
16+
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
17+
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
18+
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
19+
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20+
*/
21+
22+
package github4s
23+
24+
import scala.concurrent.Future
25+
import fr.hmil.roshttp._
26+
import fr.hmil.roshttp.body.{BodyPart, BulkBodyPart}
27+
import java.nio.ByteBuffer
28+
29+
import cats.implicits._
30+
import fr.hmil.roshttp.response.SimpleHttpResponse
31+
import fr.hmil.roshttp.util.HeaderMap
32+
import fr.hmil.roshttp.body.Implicits._
33+
import fr.hmil.roshttp.exceptions.HttpException
34+
35+
import scala.concurrent.ExecutionContext.Implicits.global
36+
import github4s.GithubResponses.{GHResponse, GHResult, JsonParsingException, UnexpectedException}
37+
import github4s.GithubDefaultUrls._
38+
import github4s.Decoders._
39+
import github4s.HttpClient.HttpCode400
40+
import io.circe.Decoder
41+
import io.circe.parser._
42+
import monix.reactive.Observable
43+
44+
import scala.util.{Failure, Success}
45+
46+
case class CirceJSONBody(value: String) extends BulkBodyPart {
47+
override def contentType: String = s"application/json; charset=utf-8"
48+
override def contentData: ByteBuffer = ByteBuffer.wrap(value.getBytes("utf-8"))
49+
}
50+
51+
trait HttpRequestBuilderExtensionJS {
52+
53+
import monix.execution.Scheduler.Implicits.global
54+
55+
val userAgent = {
56+
val name = github4s.BuildInfo.name
57+
val version = github4s.BuildInfo.version
58+
s"$name/$version"
59+
}
60+
61+
implicit def extensionJS: HttpRequestBuilderExtension[SimpleHttpResponse, Future] =
62+
new HttpRequestBuilderExtension[SimpleHttpResponse, Future] {
63+
def run[A](rb: HttpRequestBuilder[SimpleHttpResponse, Future])(
64+
implicit D: Decoder[A]): Future[GHResponse[A]] = {
65+
val request = HttpRequest(rb.url)
66+
.withMethod(Method(rb.httpVerb.verb))
67+
.withQueryParameters(rb.params.toSeq: _*)
68+
.withHeader("content-type", "application/json")
69+
.withHeader("user-agent", userAgent)
70+
.withHeaders(rb.authHeader.toList: _*)
71+
.withHeaders(rb.headers.toList: _*)
72+
73+
rb.data
74+
.map(d => request.send(CirceJSONBody(d)))
75+
.getOrElse(request.send())
76+
.map(toEntity[A])
77+
.recoverWith {
78+
case e => Future.successful(Either.left(UnexpectedException(e.getMessage)))
79+
}
80+
}
81+
}
82+
83+
def toEntity[A](response: SimpleHttpResponse)(implicit D: Decoder[A]): GHResponse[A] =
84+
response match {
85+
case r if r.statusCode < HttpCode400.statusCode
86+
decode[A](r.body).fold(
87+
e Either.left(JsonParsingException(e.getMessage, r.body)),
88+
result
89+
Either.right(
90+
GHResult(result, r.statusCode, rosHeaderMapToRegularMap(r.headers))
91+
)
92+
)
93+
case r
94+
Either.left(
95+
UnexpectedException(
96+
s"Failed invoking get with status : ${r.statusCode}, body : \n ${r.body}"))
97+
}
98+
99+
private def rosHeaderMapToRegularMap(
100+
headers: HeaderMap[String]): Map[String, IndexedSeq[String]] =
101+
headers.flatMap(m => Map(m._1.toLowerCase -> IndexedSeq(m._2)))
102+
103+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright (c) 2016 47 Degrees, LLC. <http://www.47deg.com>
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a copy of
5+
* this software and associated documentation files (the "Software"), to deal in
6+
* the Software without restriction, including without limitation the rights to
7+
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
8+
* the Software, and to permit persons to whom the Software is furnished to do so,
9+
* subject to the following conditions:
10+
*
11+
* The above copyright notice and this permission notice shall be included in all
12+
* copies or substantial portions of the Software.
13+
*
14+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
16+
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
17+
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
18+
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
19+
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20+
*/
21+
22+
package github4s.js
23+
24+
object Implicits extends ImplicitsJS

0 commit comments

Comments
 (0)