Skip to content

Commit

Permalink
refactor(analyzer): rewrite monadic version with tasks
Browse files Browse the repository at this point in the history
  • Loading branch information
tassiluca committed Feb 21, 2024
1 parent c26f655 commit f0b94f7
Show file tree
Hide file tree
Showing 20 changed files with 143 additions and 267 deletions.
2 changes: 2 additions & 0 deletions .idea/modules.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,20 @@ class AnalyzerGUI(controller: AppController) extends AnalyzerView:
gui.setVisible(true)

override def update(result: OrganizationReport): Unit = SwingUtilities.invokeLater(() =>
gui.contributionsModel.setDataVector(
result._1.toSeq.sortBy(_._2)(using Ordering.Long.reverse).map(e => Array[Any](e._1, e._2)).toArray,
gui.contributionsCols,
)
gui.repoDetailsModel.setDataVector(
result._2.map(e => Array[Any](e.name, e.issues, e.stars, e.lastRelease)).toArray,
gui.repoDetailsCols,
)
gui.contributionsModel.setDataVector(
result._1.toSeq.sortBy(_._2)(using Ordering.Long.reverse).map(e => Array[Any](e._1, e._2)).toArray,
gui.contributionsCols,
)
gui.repoDetailsModel.setDataVector(
result._2.map(e => Array[Any](e.name, e.issues, e.stars, e.lastRelease)).toArray,
gui.repoDetailsCols,
),
)

override def error(errorMessage: String): Unit =
SwingUtilities.invokeLater(() => gui.showError(errorMessage))

override def endComputation(): Unit =
SwingUtilities.invokeLater(() => gui.endSession())
SwingUtilities.invokeLater(() => gui.ended())

override def cancelled(): Unit = SwingUtilities.invokeLater(() => gui.cancelled())
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ class MainFrame(controller: AppController) extends JFrame:
getContentPane.add(mainPanel)

def showError(errorMsg: String): Unit =
endSession()
ended()
JOptionPane.showMessageDialog(this, errorMsg, "Error!", JOptionPane.ERROR_MESSAGE)

def endSession(): Unit = stateText.setText("Computation ended.")
def ended(): Unit = stateText.setText("Computation ended.")

def cancelled(): Unit = stateText.setText("Computation canceled.")
def cancelled(): Unit = stateText.setText("Computation canceled.")
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class AnalyzerGUI(controller: AppController) : AnalyzerView {
)
}

override fun endComputation() = SwingUtilities.invokeLater { gui.endSession() }
override fun endComputation() = SwingUtilities.invokeLater { gui.ended() }

override fun error(errorMessage: String) = SwingUtilities.invokeLater { gui.showError(errorMessage) }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,29 @@ package io.github.tassiLuca.analyzer.client
import gears.async.{Async, Future}
import io.github.tassiLuca.analyzer.commons.client.{AnalyzerView, AppController, OrganizationReport}
import io.github.tassiLuca.analyzer.commons.lib.RepositoryReport
import io.github.tassiLuca.analyzer.lib.Analyzer
import io.github.tassiLuca.analyzer.lib.{Analyzer, GitHubService}

object AppController:
def direct(using Async): AppController = DirectAppController()

private class DirectAppController(using Async) extends AppController:
private val view = AnalyzerView.gui(this)
private val analyzer = Analyzer.ofGitHub
private val analyzer = Analyzer.of(GitHubService())
private var currentComputation: Option[Future[Unit]] = None

view.run()

override def stopSession(): Unit = currentComputation.foreach(_.cancel())

override def runSession(organizationName: String): Unit =
var organizationReport: OrganizationReport = (Map(), Set())
val f = Future:
analyzer.analyze(organizationName) { report =>
organizationReport = (organizationReport._1.aggregatedTo(report), organizationReport._2 + report)
view.update(organizationReport)
} match { case Left(e) => view.error(e); case Right(_) => view.endComputation() }
} match { case Left(e) => view.error(e); case _ => view.endComputation() }
currentComputation = Some(f)

extension (m: Map[String, Long])
private def aggregatedTo(report: RepositoryReport): Map[String, Long] =
m ++ report.contributions.map(c => c.user -> (m.getOrElse(c.user, 0L) + c.contributions))
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ import gears.async.Async
@main def directAnalyzerLauncher(): Unit =
Async.blocking:
AppController.direct
Thread.sleep(Long.MaxValue)
Thread.sleep(Long.MaxValue)
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,20 @@ import io.github.tassiLuca.analyzer.commons.lib.{Repository, RepositoryReport}
import io.github.tassiLuca.boundaries.EitherConversions.given
import io.github.tassiLuca.boundaries.either
import io.github.tassiLuca.boundaries.either.?
import io.github.tassiLuca.utils.ChannelClosedConverter.tryable
import io.github.tassiLuca.utils.ChannelsPimping.tryable

trait Analyzer:
def analyze(organizationName: String)(
updateResults: Async ?=> RepositoryReport => Unit,
updateResults: RepositoryReport => Unit,
)(using Async): Either[String, Seq[RepositoryReport]]

object Analyzer:
def ofGitHub: Analyzer = GitHubAnalyzer()
def of(service: GitHubService): Analyzer = GitHubAnalyzer(service)

private class GitHubAnalyzer extends Analyzer:
private val gitHubService = GitHubService()
private class GitHubAnalyzer(gitHubService: GitHubService) extends Analyzer:

override def analyze(organizationName: String)(
updateResults: Async ?=> RepositoryReport => Unit,
updateResults: RepositoryReport => Unit,
)(using Async): Either[String, Seq[RepositoryReport]] = either:
val reposInfo = gitHubService
.repositoriesOf(organizationName).?
Expand All @@ -34,4 +33,4 @@ object Analyzer:
private def performAnalysis(using Async): Future[RepositoryReport] = Future:
val contributions = Future { gitHubService.contributorsOf(r.organization, r.name) }
val release = Future { gitHubService.lastReleaseOf(r.organization, r.name) }
lib.RepositoryReport(r.name, r.issues, r.stars, contributions.await.getOrElse(Seq()), release.await.toOption)
RepositoryReport(r.name, r.issues, r.stars, contributions.await.getOrElse(Seq()), release.await.toOption)
2 changes: 2 additions & 0 deletions analyzer-monadic/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ dependencies {
implementation(libs.sttp)
implementation(libs.sttp.upickle)
implementation(libs.cats.core)
implementation("com.softwaremill.sttp.client3:async-http-client-backend-monix_3:3.9.3")
implementation("io.monix:monix_3:3.4.1")
api(project(":analyzer-commons"))
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package io.github.tassiLuca.analyzer.client

import io.github.tassiLuca.analyzer.commons.client.{AnalyzerView, AppController, OrganizationReport}
import io.github.tassiLuca.analyzer.commons.lib.RepositoryReport
import io.github.tassiLuca.analyzer.lib.Analyzer
import monix.execution.CancelableFuture

object AppController:
def monadic: AppController = MonadicAppController()

private class MonadicAppController extends AppController:

import monix.execution.Scheduler.Implicits.global
private val view = AnalyzerView.gui(this)
private val analyzer = Analyzer.ofGitHub()
private var currentComputation: Option[CancelableFuture[Unit]] = None

view.run()

override def runSession(organizationName: String): Unit =
var organizationReport: OrganizationReport = (Map(), Set())
val f = analyzer.analyze(organizationName) { report =>
organizationReport = (organizationReport._1.aggregatedTo(report), organizationReport._2 + report)
view.update(organizationReport)
}.value.runToFuture.map { case Left(value) => view.error(value); case Right(_) => view.endComputation() }
currentComputation = Some(f)

override def stopSession(): Unit = currentComputation foreach (_.cancel())

extension (m: Map[String, Long])
private def aggregatedTo(report: RepositoryReport): Map[String, Long] =
m ++ report.contributions.map(c => c.user -> (m.getOrElse(c.user, 0L) + c.contributions))
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package io.github.tassiLuca.analyzer.client

@main def monadicAnalyzerLauncher(): Unit =
AppController.monadic
Original file line number Diff line number Diff line change
@@ -1,70 +1,36 @@
package io.github.tassiLuca.analyzer.lib

import cats.data.EitherT
import cats.implicits.toTraverseOps
import io.github.tassiLuca.analyzer.commons.lib
import io.github.tassiLuca.analyzer.commons.lib.{Repository, RepositoryReport}

import scala.concurrent.{Await, ExecutionContext, Future}
import monix.eval.Task

trait Analyzer:
def analyze(organizationName: String)(
updateResult: RepositoryReport => Unit,
)(using ExecutionContext): Future[Either[String, Seq[RepositoryReport]]]

def analyze2(organizationName: String)(
updateResult: RepositoryReport => Unit,
)(using ExecutionContext): EitherT[Future, String, Seq[RepositoryReport]]
): EitherT[Task, String, Seq[RepositoryReport]]

object Analyzer:
def ofGitHub(): Analyzer = new AnalyzerImpl()
def ofGitHub(): Analyzer = AnalyzerImpl()

private class AnalyzerImpl extends Analyzer:
private val gitHubService = GitHubService()

override def analyze(organizationName: String)(
updateResult: RepositoryReport => Unit,
)(using ExecutionContext): Future[Either[String, Seq[RepositoryReport]]] =
gitHubService.repositoriesOf(organizationName).flatMap {
case Left(error) => Future.successful(Left(error))
case Right(repos) =>
val futuresReports: Seq[Future[RepositoryReport]] = repos.map(performAnalysis)
val futureSeqOfReports: Future[Seq[RepositoryReport]] = Future.sequence(futuresReports)
futureSeqOfReports.map(Right(_))
}

override def analyze2(organizationName: String)(
updateResult: RepositoryReport => Unit,
)(using ExecutionContext): EitherT[Future, String, Seq[RepositoryReport]] =
import cats.implicits.toTraverseOps
): EitherT[Task, String, Seq[RepositoryReport]] =
for
repositories <- EitherT(gitHubService.repositoriesOf(organizationName))
reports <- repositories.traverse(r => EitherT.right(r.idiomaticAnalysis))
repositories <- gitHubService.repositoriesOf(organizationName)
reports <- repositories.traverse(r => EitherT.right(r.performAnalysis(updateResult)))
yield reports

extension (r: Repository)
private def performAnalysis(using ExecutionContext): Future[RepositoryReport] =
val contributionsTask = gitHubService.contributorsOf(r.organization, r.name)
val releaseTask = gitHubService.lastReleaseOf(r.organization, r.name)
private def performAnalysis(updateResult: RepositoryReport => Unit): Task[RepositoryReport] =
val contributorsTask = gitHubService.contributorsOf(r.organization, r.name).value
val releaseTask = gitHubService.lastReleaseOf(r.organization, r.name).value
for
contributions <- contributionsTask
lastRelease <- releaseTask
yield lib.RepositoryReport(r.name, r.issues, r.stars, contributions.getOrElse(Seq.empty), lastRelease.toOption)

private def idiomaticAnalysis(using ExecutionContext): Future[RepositoryReport] =
import cats.implicits.catsSyntaxTuple2Semigroupal
(gitHubService.contributorsOf(r.organization, r.name), gitHubService.lastReleaseOf(r.organization, r.name))
.mapN { case (contributions, lastRelease) =>
lib.RepositoryReport(r.name, r.issues, r.stars, contributions.getOrElse(Seq.empty), lastRelease.toOption)
}

@main def testAnalyzer(): Unit =
given ExecutionContext = ExecutionContext.global
val result = Analyzer.ofGitHub().analyze("unibo-spe")(report => println(report))
Await.ready(result, scala.concurrent.duration.Duration.Inf)
println(s">> $result")

@main def testAnalyzerWithCats(): Unit =
given ExecutionContext = ExecutionContext.global
val result = Analyzer.ofGitHub().analyze2("unibo-spe")(report => println(report)).value
Await.ready(result, scala.concurrent.duration.Duration.Inf)
println(s">> $result")
result <- Task.parZip2(contributorsTask, releaseTask)
report = RepositoryReport(r.name, r.issues, r.stars, result._1.getOrElse(Seq.empty), result._2.toOption)
_ <- Task(updateResult(report))
yield report
Original file line number Diff line number Diff line change
@@ -1,66 +1,44 @@
package io.github.tassiLuca.analyzer.lib

import cats.data.EitherT
import io.github.tassiLuca.analyzer.commons.lib.{Contribution, Release, Repository}
import sttp.client3.HttpClientFutureBackend
import sttp.model.Uri

import scala.concurrent.{Await, ExecutionContext, Future}
import monix.eval.Task

trait GitHubService:
def repositoriesOf(organizationName: String)(using ExecutionContext): Future[Either[String, Seq[Repository]]]

def contributorsOf(
organizationName: String,
repositoryName: String,
)(using ExecutionContext): Future[Either[String, Seq[Contribution]]]

def lastReleaseOf(
organizationName: String,
repositoryName: String,
)(using ExecutionContext): Future[Either[String, Release]]
def repositoriesOf(organizationName: String): EitherT[Task, String, Seq[Repository]]
def contributorsOf(organizationName: String, repositoryName: String): EitherT[Task, String, Seq[Contribution]]
def lastReleaseOf(organizationName: String, repositoryName: String): EitherT[Task, String, Release]

object GitHubService:
def apply(): GitHubService = GitHubServiceImpl()

private class GitHubServiceImpl extends GitHubService:
import sttp.client3.httpclient.monix.HttpClientMonixBackend
import sttp.client3.{UriContext, basicRequest}
import upickle.default.{read, Reader}
import sttp.model.Uri
import upickle.default.{Reader, read}

private val apiUrl = "https://api.github.com"
private val request = basicRequest.auth.bearer(System.getenv("GH_TOKEN"))

override def repositoriesOf(
organizationName: String,
)(using ExecutionContext): Future[Either[String, Seq[Repository]]] =
performRequest[Seq[Repository]](uri"$apiUrl/orgs/$organizationName/repos?per_page=100")
override def repositoriesOf(organizationName: String): EitherT[Task, String, Seq[Repository]] =
performRequest[Seq[Repository]](uri"$apiUrl/orgs/$organizationName/repos")

override def contributorsOf(
organizationName: String,
repositoryName: String,
)(using ExecutionContext): Future[Either[String, Seq[Contribution]]] =
performRequest[Seq[Contribution]](uri"$apiUrl/repos/$organizationName/$repositoryName/contributors?per_page=100")
): EitherT[Task, String, Seq[Contribution]] =
performRequest[Seq[Contribution]](uri"$apiUrl/repos/$organizationName/$repositoryName/contributors")

override def lastReleaseOf(
organizationName: String,
repositoryName: String,
)(using ExecutionContext): Future[Either[String, Release]] =
): EitherT[Task, String, Release] =
performRequest[Release](uri"$apiUrl/repos/$organizationName/$repositoryName/releases/latest")

private def performRequest[T](endpoint: Uri)(using Reader[T], ExecutionContext): Future[Either[String, T]] =
private def performRequest[T](endpoint: Uri)(using Reader[T]): EitherT[Task, String, T] = EitherT:
for
response <- HttpClientFutureBackend().send(request.get(endpoint))
backend <- HttpClientMonixBackend()
response <- request.get(endpoint).send(backend)
result = response.body.map(r => read[T](r))
yield result

@main def main(): Unit =
given ExecutionContext = ExecutionContext.global
val service = GitHubService()
val result = service.repositoriesOf("unibo-spe")
Await.ready(result, scala.concurrent.duration.Duration.Inf)
println(result.value)
val result2 = service.contributorsOf("unibo-spe", "spe-slides")
Await.ready(result2, scala.concurrent.duration.Duration.Inf)
println(result2.value)
val result3 = service.lastReleaseOf("unibo-spe", "spe-slides")
Await.ready(result3, scala.concurrent.duration.Duration.Inf)
println(result3.value)
25 changes: 0 additions & 25 deletions commons/src/main/scala/io/github/tassiLuca/UseTasks.scala

This file was deleted.

Loading

0 comments on commit f0b94f7

Please sign in to comment.