Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide interpreters with several monad instances #18

Merged
merged 9 commits into from
Jun 6, 2016
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
21 changes: 17 additions & 4 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,10 @@ lazy val buildSettings = Seq(

lazy val dependencies = libraryDependencies ++= Seq(
"org.typelevel" %% "cats" % "0.4.0",
"org.scalaz" %% "scalaz-concurrent" % "7.1.4",
"org.scalaj" %% "scalaj-http" % "2.2.1",
"io.circe" %% "circe-core" % "0.3.0",
"io.circe" %% "circe-generic" % "0.3.0",
"io.circe" %% "circe-parser" % "0.3.0",
"com.typesafe" % "config" % "1.3.0",
"org.scalatest" %% "scalatest" % "2.2.6" % "test",
"com.ironcorelabs" %% "cats-scalatest" % "1.1.2" % "test",
"org.mock-server" % "mockserver-netty" % "3.10.4" % "test"
Expand Down Expand Up @@ -65,7 +63,12 @@ lazy val github4sSettings = buildSettings ++ dependencies ++ scalariformSettings

lazy val ghpagesSettings = ghpages.settings ++ Seq(git.remoteRepo := "git@github.com:47deg/github4s.git")

lazy val docsSettings = buildSettings ++ noPublishSettings ++ tutSettings ++ tutDirectoriesSettings ++ ghpagesSettings
lazy val docsSettings = buildSettings ++ docsDependencies ++ noPublishSettings ++ tutSettings ++ tutDirectoriesSettings ++ ghpagesSettings

lazy val docsDependencies = libraryDependencies ++= Seq(
"com.ironcorelabs" %% "cats-scalatest" % "1.1.2" % "test",
"org.mock-server" % "mockserver-netty" % "3.10.4" % "test"
)

lazy val github4s = (project in file("."))
.settings(moduleName := "github4s")
Expand All @@ -75,5 +78,15 @@ lazy val docs = (project in file("docs"))
.settings(moduleName := "github4s-docs")
.settings(docsSettings: _*)
.enablePlugins(JekyllPlugin)
.dependsOn(github4s)
.dependsOn(scalaz)

lazy val scalazDependencies = libraryDependencies ++= Seq(
"org.scalaz" %% "scalaz-concurrent" % "7.1.4"
)

lazy val scalazSettings = buildSettings ++ scalazDependencies ++ scalariformSettings

lazy val scalaz = (project in file("scalaz"))
.settings(moduleName := "github4s-scalaz")
.settings(scalazSettings: _*)
.dependsOn(github4s)
1 change: 1 addition & 0 deletions docs/src/jekyll/_config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ description: "A GitHub API wrapper written in Scala"
github_owner: 47deg
baseurl: /github4s
style: github4s
highlight_theme: tomorrow
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

docs: true

markdown: redcarpet
Expand Down
78 changes: 75 additions & 3 deletions docs/src/tut/docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,78 @@
layout: docs
---

```tut
2 + 2
```
# Get started

WIP: Import

```tut:silent
import github4s.Github
```

WIP: Every Github4s api returns a `Free[GHResponse[A], A]` where `GHResonse[A]` is a type alias for `GHException Xor 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

```tut:silent
val user1 = Github().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`.

```tut:silent
import cats.Eval
import github4s.Github._
import github4s.implicits._

val u1 = user1.exec[Eval].value
```

WIP: As mentioned above `u1` should have an `GHResult[User]` in the right.

```tut:invisible
import cats.data.Xor
import github4s.GithubResponses.GHResult
```

```tut:book
u1 match {
case Xor.Right(GHResult(result, status, headers)) => result.login
case Xor.Left(e) => e.getMessage
}
```

WIP: With `Id`

```tut:silent
import cats.Id

val u2 = Github().users.get("raulraja").exec[Id]
```

WIP: With `Future`

```tut:silent
import cats.std.future._
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
import scala.concurrent.Await

val u3 = Github().users.get("dialelo").exec[Future]
Await.result(u3, 2.seconds)
```

WIP: With `scalaz.Task`

```tut:silent
import scalaz.concurrent.Task
import github4s.scalaz.implicits._

val u4 = Github().users.get("franciscodr").exec[Task]
u4.attemptRun
```

19 changes: 17 additions & 2 deletions docs/src/tut/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,22 @@ technologies:
- database: ["Database", "Lorem ipsum dolor sit amet, conse ctetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolo…"]
---

```tut
1 + 1
```tut:invisible
import org.scalatest._
import Matchers._
import cats.scalatest.XorMatchers.right
import cats.scalatest.XorValues
import cats.scalatest.XorValues._
import cats.Eval
import github4s.Github
import github4s.Github._
import github4s.implicits._
```

```tut:book
val user1 = Github().users.get("rafaparadela").exec[Eval].value

user1 shouldBe right
user1.value.result.login shouldBe "rafaparadela"
```

21 changes: 21 additions & 0 deletions scalaz/src/main/scala/github4s/scalaz/Implicits.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package github4s.scalaz

import cats.MonadError
import scalaz.concurrent.Task

object implicits {

implicit val g4sTaskMonadError: MonadError[Task, Throwable] = new MonadError[Task, Throwable] {

override def pure[A](x: A): Task[A] = Task.now(x)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd overwrite pureEval too for using Task.now when the value has been already evaluated, like they do in Cats with Future. This avoids introducing an unneeded asynchronous boundary.


override def map[A, B](fa: Task[A])(f: A ⇒ B): Task[B] = fa.map(f)

override def flatMap[A, B](fa: Task[A])(ff: A ⇒ Task[B]): Task[B] = fa.flatMap(ff)

override def raiseError[A](e: Throwable): Task[A] = Task.fail(e)

override def handleErrorWith[A](fa: Task[A])(f: Throwable ⇒ Task[A]): Task[A] = fa.handleWith({ case x ⇒ f(x) })
}

}
15 changes: 0 additions & 15 deletions src/main/scala/github4s/GithubApiConfig.scala

This file was deleted.

16 changes: 16 additions & 0 deletions src/main/scala/github4s/GithubDefaultUrls.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package github4s

case class GithubApiUrls(
baseUrl: String,
authorizeUrl: String,
accessTokenUrl: String
)

object GithubDefaultUrls {

implicit val defaultUrls: GithubApiUrls = GithubApiUrls(
"https://api.github.com/",
"https://github.com/login/oauth/authorize?client_id=%s&redirect_uri=%s&scope=%s&state=%s",
"https://github.com/login/oauth/access_token"
)
}
2 changes: 1 addition & 1 deletion src/main/scala/github4s/GithubResponses.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ object GithubResponses {

type GHResponse[A] = GHException Xor GHResult[A]

case class GHResult[A](value: A, statusCode: Int, headers: Map[String, IndexedSeq[String]])
case class GHResult[A](result: A, statusCode: Int, headers: Map[String, IndexedSeq[String]])

sealed abstract class GHException(msg: String, cause: Option[Throwable] = None) extends Throwable(msg) {
cause foreach initCause
Expand Down
4 changes: 2 additions & 2 deletions src/main/scala/github4s/HttpClient.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import github4s.free.domain.Pagination
import io.circe.Decoder
import scalaj.http._

class HttpClient(implicit config: GithubApiConfig) {
class HttpClient(implicit urls: GithubApiUrls) {

val defaultPagination = Pagination(1, 1000)

Expand Down Expand Up @@ -148,6 +148,6 @@ class HttpClient(implicit config: GithubApiConfig) {
val defaultPage: Int = 1
val defaultPerPage: Int = 30

private def buildURL(method: String) = config.getString("github.baseUrl") + method
private def buildURL(method: String) = urls.baseUrl + method

}
62 changes: 62 additions & 0 deletions src/main/scala/github4s/Implicits.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package github4s

import cats.std.FutureInstances
import cats.std.future._
import cats.{ Monad, Id, Eval, MonadError }
import github4s.free.interpreters.Interpreters
import scala.concurrent.{ ExecutionContext, Future }

object implicits extends Interpreters with EvalInstances with IdInstances with FutureInstances {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can drop the braces here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense


}

trait EvalInstances {

implicit val evalMonadError: MonadError[Eval, Throwable] = new MonadError[Eval, Throwable] {

override def pure[A](x: A): Eval[A] = Eval.now(x)

override def map[A, B](fa: Eval[A])(f: A ⇒ B): Eval[B] = fa.map(f)

override def flatMap[A, B](fa: Eval[A])(ff: A ⇒ Eval[B]): Eval[B] =
fa.flatMap(ff)

override def raiseError[A](e: Throwable): Eval[A] =
Eval.later({ throw e })

override def handleErrorWith[A](fa: Eval[A])(f: Throwable ⇒ Eval[A]): Eval[A] =
Eval.later({
try {
fa.value
} catch {
case e: Throwable ⇒ f(e).value
}
})
}

}

trait IdInstances {

implicit def idMonadError(implicit I: Monad[Id]): MonadError[Id, Throwable] = new MonadError[Id, Throwable] {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that the instance of MonadError[Id, Throwable] doesn't fullfill the laws of MonadError since exceptions cannot be captured. Do we want to provide it as part of the library knowing this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you tested I'd and task in a file where either one was required as implícits. @dialelo and I have experienced implícits ambiguity presumably because Task also conforms to Id given I'd is applicable to anything. Not sure we should provide this instance here. We may have to look into a LowerPriorityImplicits trait stack to avoid ambiguity. See also cats for examples of this.


override def pure[A](x: A): Id[A] = I.pure(x)

override def ap[A, B](ff: Id[A ⇒ B])(fa: Id[A]): Id[B] = I.ap(ff)(fa)

override def map[A, B](fa: Id[A])(f: Id[A ⇒ B]): Id[B] = I.map(fa)(f)

override def flatMap[A, B](fa: Id[A])(f: A ⇒ Id[B]): Id[B] = I.flatMap(fa)(f)

override def raiseError[A](e: Throwable): Id[A] = throw e

override def handleErrorWith[A](fa: Id[A])(f: Throwable ⇒ Id[A]): Id[A] = {
try {
fa
} catch {
case e: Exception ⇒ f(e)
}
}
}

}
10 changes: 5 additions & 5 deletions src/main/scala/github4s/api/Auth.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@ import java.util.UUID
import cats.data.Xor
import github4s.GithubResponses.{ GHResult, GHResponse }
import github4s.free.domain._
import github4s.{ GithubApiConfig, HttpClient }
import github4s.{ GithubApiUrls, HttpClient }
import io.circe.generic.auto._
import io.circe.syntax._
import scalaj.http.HttpConstants._

/** Factory to encapsulate calls related to Auth operations */
class Auth(implicit config: GithubApiConfig) {
class Auth(implicit urls: GithubApiUrls) {

val httpClient = new HttpClient

val authorizeUrl = config.getString("github.authorizeUrl")
val accessTokenUrl = config.getString("github.accessTokenUrl")
val authorizeUrl = urls.authorizeUrl
val accessTokenUrl = urls.accessTokenUrl

/**
* Call to request a new authorization given a basic authentication, the returned object Authorization includes an
Expand Down Expand Up @@ -59,7 +59,7 @@ class Auth(implicit config: GithubApiConfig) {
val state = UUID.randomUUID().toString
Xor.Right(
GHResult(
value = Authorize(authorizeUrl.format(client_id, redirect_uri, scopes.mkString(","), state), state),
result = Authorize(authorizeUrl.format(client_id, redirect_uri, scopes.mkString(","), state), state),
statusCode = 200,
headers = Map.empty
)
Expand Down
4 changes: 2 additions & 2 deletions src/main/scala/github4s/api/Repos.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ package github4s.api

import github4s.GithubResponses.GHResponse
import github4s.free.domain.{ Pagination, Commit, Repository, User }
import github4s.{ GithubApiConfig, Decoders, HttpClient }
import github4s.{ GithubApiUrls, Decoders, HttpClient }
import io.circe.generic.auto._

/** Factory to encapsulate calls related to Repositories operations */
class Repos(implicit config: GithubApiConfig) {
class Repos(implicit urls: GithubApiUrls) {

import Decoders._

Expand Down
4 changes: 2 additions & 2 deletions src/main/scala/github4s/api/Users.scala
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package github4s.api

import github4s.GithubResponses.GHResponse
import github4s.{ GithubApiConfig, HttpClient }
import github4s.{ GithubApiUrls, HttpClient }
import github4s.free.domain.{ Pagination, User }
import io.circe.generic.auto._

/** Factory to encapsulate calls related to Users operations */
class Users(implicit config: GithubApiConfig) {
class Users(implicit urls: GithubApiUrls) {

val httpClient = new HttpClient

Expand Down