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

[DAY-1] First commit with basic implementation #1

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,12 @@
*.class

*.log

*.class
.idea
target/

project/target
.bsp/
.bloop/
.vscode/

4 changes: 4 additions & 0 deletions .scalafmt.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
version = 2.5.0

align.preset = more // For pretty alignment.
maxColumn = 120 // For my wide 30" display.
36 changes: 36 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@

lazy val akkaVersion = "2.6.15"
lazy val akkaHttpVersion = "10.2.5"
lazy val akkaHttpJsonVersion = "1.37.0"
lazy val akkaPersistenceJdbc = "5.0.1"
lazy val circeVersion = "0.14.1"
lazy val slickVersion = "3.3.3"
lazy val postgresqlVersion = "42.2.23"
lazy val tapirAkkaHttpVersion = "0.19.0-M4"
lazy val logbackClassicVersion = "1.2.5"

lazy val scalaTestVersion = "3.2.9"

lazy val root = (project in file(".")).
settings(
inThisBuild(List(
organization := "com.igobrilhante",
scalaVersion := "2.13.4"
)),
name := "github-challenge",
libraryDependencies ++= Seq(
"com.typesafe.akka" %% "akka-http" % akkaHttpVersion,
"de.heikoseeberger" %% "akka-http-circe" % akkaHttpJsonVersion,
"com.typesafe.akka" %% "akka-actor-typed" % akkaVersion,
"com.typesafe.akka" %% "akka-stream" % akkaVersion,
"ch.qos.logback" % "logback-classic" % logbackClassicVersion,

"io.circe" %% "circe-core" % circeVersion,
"io.circe" %% "circe-generic" % circeVersion,
"io.circe" %% "circe-parser" % circeVersion,

"com.typesafe.akka" %% "akka-http-testkit" % akkaHttpVersion % Test,
"com.typesafe.akka" %% "akka-actor-testkit-typed" % akkaVersion % Test,
"org.scalatest" %% "scalatest" % "3.1.4" % Test
)
)
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.5.1
1 change: 1 addition & 0 deletions project/plugins.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
addSbtPlugin("io.spray" % "sbt-revolver" % "0.9.1")
13 changes: 13 additions & 0 deletions src/main/resources/application.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
app {
routes {
# If ask takes more time than this to complete the request is failed
ask-timeout = 5s
}
}

github {
username = "username"
username = ${?GH_USERNAME}
token = ""
token = ${?GH_TOKEN}
}
20 changes: 20 additions & 0 deletions src/main/resources/logback.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<configuration>
<!-- This is a development logging configuration that logs to standard out, for an example of a production
logging config, see the Akka docs: https://doc.akka.io/docs/akka/2.6/typed/logging.html#logback -->
<appender name="STDOUT" target="System.out" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>[%date{ISO8601}] [%level] [%logger] [%thread] [%X{akkaSource}] - %msg%n</pattern>
</encoder>
</appender>

<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
<queueSize>1024</queueSize>
<neverBlock>true</neverBlock>
<appender-ref ref="STDOUT" />
</appender>

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

</configuration>
12 changes: 12 additions & 0 deletions src/main/scala/com/igobrilhante/GHSystem.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.igobrilhante

import akka.actor.typed.Behavior
import akka.actor.typed.scaladsl.Behaviors

object GHSystem {

trait Command

def apply(): Behavior[GHSystem.Command] = Behaviors.empty

}
19 changes: 19 additions & 0 deletions src/main/scala/com/igobrilhante/github/api/GHService.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.igobrilhante.github.api

import scala.concurrent.Future

import com.igobrilhante.github.models.{GHContributor, GHOrganization, GHRepository}

trait GHService {

def getOrganization(organizationId: String): Future[GHOrganization]

def getRepositories(organizationId: String, page: Int): Future[List[GHRepository]]

def getAllRepositories(organizationId: String): Future[List[GHRepository]]

def getContributors(organizationId: String, repositoryId: String): Future[List[GHContributor]]

def getRankedContributors(organizationId: String): Future[List[GHContributor]]

}
7 changes: 7 additions & 0 deletions src/main/scala/com/igobrilhante/github/commons/Logging.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.igobrilhante.github.commons

import org.slf4j.{Logger, LoggerFactory}

trait Logging {
val logger: Logger = LoggerFactory.getLogger(this.getClass)
}
139 changes: 139 additions & 0 deletions src/main/scala/com/igobrilhante/github/impl/GHServiceImpl.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package com.igobrilhante.github.impl

import scala.concurrent.{ExecutionContext, Future}

import akka.actor.typed.ActorSystem
import akka.http.javadsl.unmarshalling.Unmarshaller
import akka.http.scaladsl.Http
import akka.http.scaladsl.model.{HttpHeader, HttpRequest, HttpResponse}
import akka.http.scaladsl.unmarshalling.{Unmarshal, Unmarshaller}
import akka.stream.Materializer
import akka.http.scaladsl.model._
import HttpMethods._

import com.igobrilhante.GHSystem
import com.igobrilhante.github.api.GHService
import com.igobrilhante.github.models.{GHCommit, GHContributor, GHOrganization, GHRepository}
import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport._
import HttpProtocols._
import MediaTypes._
import HttpCharsets._
import akka.http.scaladsl.model.headers.BasicHttpCredentials

import com.igobrilhante.github.commons.Logging

class GHServiceImpl()(implicit val system: ActorSystem[_], val ec: ExecutionContext) extends GHService with Logging {

private val mat = Materializer.matFromSystem
private val BaseApi = "https://api.github.com"
private val http = Http(system)
private val MaxReposPerPage = 100
private val config = system.settings.config
private val githubConfig = config.getConfig("github")
private val GitHubUser = githubConfig.getString("username")
private val GitHubToken = githubConfig.getString("token")
private val GitHubCredentials = BasicHttpCredentials(GitHubUser, GitHubToken)

override def getOrganization(organizationId: String): Future[GHOrganization] = {
val uri = s"$BaseApi/orgs/${organizationId}"
for {
response <- request(uri)
list <- Unmarshal(response).to[GHOrganization]
} yield list
}

override def getRepositories(organizationId: String, page: Int = 1): Future[List[GHRepository]] = {
val uri = s"$BaseApi/orgs/${organizationId}/repos?page=$page&per_page=$MaxReposPerPage"
for {
response <- request(uri)
list <- Unmarshal(response).to[List[GHRepository]]
} yield list

}

override def getContributors(organizationId: String, repositoryId: String): Future[List[GHContributor]] = {
val uri = s"$BaseApi/repos/${organizationId}/${repositoryId}/contributors"
for {
response <- request(uri)
list <- Unmarshal(response).to[List[GHContributor]]
} yield list
}

def getRankedContributors(organizationId: String): Future[List[GHContributor]] = {

def rankContributors(list: List[(GHRepository, List[GHContributor])]) = {

list
.flatMap { case (repo, contributors) => contributors.map((_, repo)) }
.groupBy(_._1.login)
.map {
case (_, contributorRepositories) =>
val totalContributions = contributorRepositories.map(_._1.contributions).sum
val contributor = contributorRepositories.head._1
contributor.copy(contributions = totalContributions)
}
.toList
.sortBy(_.contributions)(Ordering[Int].reverse)

}

for {
allRepositories <- getAllRepositories(organizationId)
reposAndContributors <-
Future.sequence(allRepositories.map(repo => getContributors(organizationId, repo.name).map(c => (repo, c))))
result = rankContributors(reposAndContributors)
} yield result
}

def getAllRepositories(organizationId: String): Future[List[GHRepository]] = {
getAllRepositoriesStrategy(organizationId)
}

private def getAllRepositoriesStrategy(organizationId: String): Future[List[GHRepository]] = {

def test(buckets: Int) = {
(1 to buckets)
.map(page => getRepositories(organizationId, page))
.foldLeft(Future.successful(List.empty[GHRepository])) {
case (result, future) =>
for {
currentList <- result
next <- future
} yield (currentList ++ next)
}
}

for {
org <- getOrganization(organizationId)
total = org.total
buckets = math.ceil(total / MaxReposPerPage.toDouble).toInt
result <- test(buckets)
} yield result
}

private def getAllRepositoriesRecursive(organizationId: String): Future[List[GHRepository]] = {
def getRepos(page: Int): Future[List[GHRepository]] = {
for {
repos <- getRepositories(organizationId, page)
result <- {
if (repos.length < MaxReposPerPage) Future.successful(repos)
else getRepos(page + 1).map(_ ++ repos)
}
} yield result
}
getRepos(page = 1)
}

private def request(uri: String) =
http.singleRequest(
HttpRequest(uri = uri).withHeaders(
headers.Authorization(GitHubCredentials)
)
)

// private def request2[A](uri: String)(implicit um: Unmarshaller[HttpResponse, A]) =
// http
// .singleRequest(HttpRequest(uri = uri))
// .flatMap(response => Unmarshal(response).to[A](um, ec, mat))

}
11 changes: 11 additions & 0 deletions src/main/scala/com/igobrilhante/github/models/GHCommit.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.igobrilhante.github.models

import io.circe.{Decoder, Encoder}
import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder}

case class GHCommit()

object GHCommit {
implicit val decoder: Decoder[GHCommit] = deriveDecoder
implicit val encoder: Encoder[GHCommit] = deriveEncoder
}
13 changes: 13 additions & 0 deletions src/main/scala/com/igobrilhante/github/models/GHContributor.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.igobrilhante.github.models

import io.circe.{Decoder, Encoder}
import io.circe.generic.semiauto._

case class GHContributor(id: Long, login: String, contributions: Int)

object GHContributor {

implicit val decoder: Decoder[GHContributor] = deriveDecoder
implicit val encoder: Encoder[GHContributor] = deriveEncoder

}
15 changes: 15 additions & 0 deletions src/main/scala/com/igobrilhante/github/models/GHOrganization.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.igobrilhante.github.models

import io.circe.{Decoder, Encoder}
import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder}

case class GHOrganization(id: Long, login: String, name: String, public_repos: Int, total_private_repos: Option[Int]) {

def total: Int = public_repos + total_private_repos.getOrElse(0)

}

object GHOrganization {
implicit val decoder: Decoder[GHOrganization] = deriveDecoder
implicit val encoder: Encoder[GHOrganization] = deriveEncoder
}
13 changes: 13 additions & 0 deletions src/main/scala/com/igobrilhante/github/models/GHRepository.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.igobrilhante.github.models

import io.circe.{Decoder, Encoder}
import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder}

case class GHRepository(id: Long, name: String, `private`: Boolean)

object GHRepository {

implicit val decoder: Decoder[GHRepository] = deriveDecoder
implicit val encoder: Encoder[GHRepository] = deriveEncoder

}
3 changes: 3 additions & 0 deletions src/test/resources/application-test.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
include "application"

# default config for tests, we just import the regular conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.igobrilhante.github.tests

import com.igobrilhante.github.api.GHService
import com.igobrilhante.github.impl.GHServiceImpl

class GHServicePublicSpec extends GHSpec {

val service: GHService = new GHServiceImpl()

val organizationId = "ScalaConsultants"
val publicRepo = "lift-rest-demo"

"GHService Public Spec" should "get public repositories for ScalaConsultants" in {

val list = service.getRepositories("ScalaConsultants", page = 1).futureValue(timeout)

list should not be empty

}

it should s"get the organization '$organizationId'" in {
val org = service.getOrganization(organizationId).futureValue(timeout)

org.login shouldEqual organizationId

}

it should "get a list of contributors for public repo 'lift-rest-demo'" in {
val list = service.getContributors(organizationId, publicRepo).futureValue(timeout)

list should not be empty
}

it should s"get all repositories for $organizationId" in {
val org = service.getOrganization(organizationId).futureValue(timeout)
val list = service.getAllRepositories(organizationId).futureValue(timeout)

list should have size org.total
list.filter(_.`private`) should have size org.total_private_repos.getOrElse(0).toLong
list.filterNot(_.`private`) should have size org.public_repos
}

}
Loading