Skip to content

Commit

Permalink
GitHub challenge solution
Browse files Browse the repository at this point in the history
  • Loading branch information
sergeda committed Jun 3, 2020
0 parents commit 04b59b2
Show file tree
Hide file tree
Showing 20 changed files with 1,136 additions and 0 deletions.
20 changes: 20 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
*.iml
*.class
*.log
target/
project/target/
project/boot/
dist/
boot/
logs/
out/
tmp/
.history/
.idea/
.idea_modules/
.DS_STORE
.cache
.settings
.project
.classpath
version.properties
12 changes: 12 additions & 0 deletions .scalafmt.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
style = IntelliJ
align = more
maxColumn = 150
align.openParenCallSite = false
align.openParenDefnSite = false
danglingParentheses = true
continuationIndent.defnSite = 13
newlines.alwaysBeforeTopLevelStatements = true
spaces.afterKeywordBeforeParen = true
includeCurlyBraceInSelectChains = false
newlines.alwaysBeforeElseAfterCurlyIf = false
optIn.breakChainOnFirstMethodDot = false
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#GithubRank challenge

Write a server that will return all contributors to a specific organization on GitHub ordered by their contributions.
There are can be multiple pages with results. And you have to consider rate limiting of GitHub.

###To run locally
Set Github token as an environment variable of name GH_TOKEN

Run server with command:
sbt run

###How to test
Server listens on port 8080.
Call server with url /org/ORGANIZATION/contributors specifying any organization that exist on GitHub.
For example: http://localhost:8080/org/akka/contributors
39 changes: 39 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@

val Http4sVersion = "0.20.0"
val CirceVersion = "0.13.0"
val SttpVersion = "2.0.7"
val ScalaCacheVersion = "0.28.0"


lazy val root = (project in file("."))
.settings(
organization := "serhii",
name := "task",
version := "0.0.1-SNAPSHOT",
scalaVersion := "2.12.11",
scalacOptions ++= Seq(
"-deprecation",
"-encoding", "UTF-8",
"-language:higherKinds",
"-language:postfixOps",
"-feature",
"-Ypartial-unification",
),
libraryDependencies ++= Seq(
"org.http4s" %% "http4s-blaze-server" % Http4sVersion,
"org.http4s" %% "http4s-blaze-client" % Http4sVersion,
"org.http4s" %% "http4s-circe" % Http4sVersion,
"org.http4s" %% "http4s-dsl" % Http4sVersion,
"io.circe" %% "circe-generic" % CirceVersion,
"com.softwaremill.sttp.client" %% "core" % SttpVersion,
"com.softwaremill.sttp.client" %% "circe" % SttpVersion,
"ch.qos.logback" % "logback-classic" % "1.2.3",
"com.typesafe.scala-logging" %% "scala-logging" % "3.9.0",
"com.softwaremill.sttp.client" %% "async-http-client-backend-cats" % SttpVersion,
"org.scalatest" %% "scalatest" % "3.1.1" % Test,
"org.scalamock" %% "scalamock" % "4.4.0" % Test,
"com.typesafe" % "config" % "1.3.3"
)
)

scalafmtOnCompile in ThisBuild := true
1 change: 1 addition & 0 deletions project/build.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sbt.version=1.3.8
1 change: 1 addition & 0 deletions project/plugins.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
addSbtPlugin("com.lucidchart" % "sbt-scalafmt" % "1.15")
11 changes: 11 additions & 0 deletions src/main/resources/application.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
server {
host = "0.0.0.0"
port = 8080
}

github.key = ${GH_TOKEN}

client {
concurrent-requests = 10
request-timeout = 10
}
16 changes: 16 additions & 0 deletions src/main/resources/logback.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>




<root level="INFO">
<appender-ref ref="STDOUT" />
</root>

<logger name="org.http4s"/>
</configuration>
31 changes: 31 additions & 0 deletions src/main/scala/serhii/Main.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package serhii

import cats.effect.IO._
import cats.effect.{ExitCode, IO, IOApp}
import com.typesafe.config.ConfigFactory
import com.typesafe.scalalogging.LazyLogging
import serhii.http.HttpServer
import serhii.service.GitHubClientImpl
import sttp.client.asynchttpclient.cats.AsyncHttpClientCatsBackend

object Main extends IOApp with LazyLogging {

private val config = ConfigFactory.load()

override def run(args: List[String]): IO[ExitCode] = {
AsyncHttpClientCatsBackend.resource().use { implicit backend =>
new HttpServer(
config.getString("server.host"),
config.getInt("server.port"),
gitHub = new GitHubClientImpl[IO](
config.getString("github.key"),
config.getInt("client.concurrent-requests"),
config.getInt("client.request-timeout")
)
).server.redeemWith(ex => {
logger.error(ex.getMessage)
IO.raiseError(ex)
}, _ => IO.pure(ExitCode.Success))
}
}
}
30 changes: 30 additions & 0 deletions src/main/scala/serhii/http/Http4sRoutes.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package serhii.http

import cats.effect.Sync
import cats.implicits._
import com.typesafe.scalalogging.LazyLogging
import org.http4s.HttpRoutes
import org.http4s.circe.CirceEntityEncoder._
import org.http4s.dsl.Http4sDsl
import org.http4s.headers.`Retry-After`
import serhii.service.GitHubClient
import serhii.utils.Errors.{RateLimited, UnknownGitHubError}

class Http4sRoutes[F[_]: Sync](gitHub: GitHubClient[F]) extends Http4sDsl[F] with LazyLogging {

def gitHubRoutes: HttpRoutes[F] = HttpRoutes.of[F] {
case GET -> Root / "org" / organization / "contributors" =>
val escapedOrganization = java.net.URLEncoder.encode(organization, "UTF-8")
(for {
repositories <- gitHub.getRepos(escapedOrganization)
contributors <- gitHub.getContributors(repositories)
} yield Ok(contributors)).flatten.recoverWith({
case RateLimited(time) =>
ServiceUnavailable(`Retry-After`.unsafeFromLong(time))
case UnknownGitHubError(error) =>
logger.error("Unknown error happened during calling GitHub: " + error)
InternalServerError()
})
}

}
15 changes: 15 additions & 0 deletions src/main/scala/serhii/http/HttpApi.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package serhii.http

import cats.effect.Sync
import org.http4s.implicits._
import org.http4s.{HttpApp, HttpRoutes}
import serhii.service.GitHubClient

final class HttpApi[F[_]: Sync](gitHub: GitHubClient[F]) {

private val httpRoutes: HttpRoutes[F] =
new Http4sRoutes(gitHub).gitHubRoutes

val httpApp: HttpApp[F] = httpRoutes.orNotFound

}
11 changes: 11 additions & 0 deletions src/main/scala/serhii/http/HttpServer.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package serhii.http

import cats.effect.{ConcurrentEffect, Sync, Timer}
import org.http4s.server.blaze.BlazeServerBuilder
import serhii.service.GitHubClient

class HttpServer[F[_]: Sync: ConcurrentEffect: Timer](host: String, port: Int, gitHub: GitHubClient[F]) {
val httpApi = new HttpApi(gitHub)

val server: F[Unit] = BlazeServerBuilder[F].bindHttp(port, host).withHttpApp(httpApi.httpApp).serve.compile.drain
}
12 changes: 12 additions & 0 deletions src/main/scala/serhii/model/Contributor.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package serhii.model

import io.circe._
import io.circe.generic.semiauto._

case class Contributor(login: String, contributions: Int)

object Contributor {
implicit val jsonDecoder: Decoder[Contributor] = deriveDecoder[Contributor]
implicit val jsonEncoder: Encoder[Contributor] = (c: Contributor) =>
Json.obj(("contributor_login", Json.fromString(c.login)), ("contributions", Json.fromInt(c.contributions)))
}
14 changes: 14 additions & 0 deletions src/main/scala/serhii/model/GitHubRepository.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package serhii.model

import io.circe.{Decoder, HCursor}

case class GitHubRepository(contributorsUrl: String)
case object GitHubRepository {
implicit def jsonDecoder: Decoder[GitHubRepository] = (c: HCursor) => {
for {
url <- c.downField("contributors_url").as[String]
} yield {
new GitHubRepository(url)
}
}
}
16 changes: 16 additions & 0 deletions src/main/scala/serhii/service/GitHubClient.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package serhii.service

import serhii.model.{Contributor, GitHubRepository}
import sttp.client.{NothingT, SttpBackend}

trait GitHubClient[F[_]] {

implicit val backend: SttpBackend[F, Nothing, NothingT]

val token: String
val concurrency: Int
val timeout: Int

def getRepos(organization: String): F[Vector[GitHubRepository]]
def getContributors(repositories: Vector[GitHubRepository]): F[Vector[Contributor]]
}
Loading

0 comments on commit 04b59b2

Please sign in to comment.