Skip to content

Commit

Permalink
clean: Inline content of codacy-api-scala (#480)
Browse files Browse the repository at this point in the history
* clean: Inline content of codacy-api-scala

- Remove unused code
- Update sbt and plugins

* style: Scalafmt

* fix: Fix tests

* fix: Fix test

* clean: Remove non-existent it:test

* Add fork / run for local testing

* fix: Mock server requirement
  • Loading branch information
lolgab committed Nov 15, 2023
1 parent 9c1b866 commit a26475b
Show file tree
Hide file tree
Showing 37 changed files with 479 additions and 641 deletions.
3 changes: 0 additions & 3 deletions .circleci/config.yml
Expand Up @@ -224,9 +224,6 @@ workflows:
- run:
name: Unit testing
command: sbt test
- run:
name: It testing
command: sbt it:test
- run:
name: Aggregate coverage reports
command: sbt coverageAggregate
Expand Down
21 changes: 21 additions & 0 deletions api-scala/src/main/scala/com/codacy/api/CoverageReport.scala
@@ -0,0 +1,21 @@
package com.codacy.api

import play.api.libs.json.{JsNumber, JsObject, Json, Writes}

case class CoverageFileReport(filename: String, coverage: Map[Int, Int])

case class CoverageReport(fileReports: Seq[CoverageFileReport])

object CoverageReport {
implicit val mapWrites: Writes[Map[Int, Int]] = Writes[Map[Int, Int]] { map: Map[Int, Int] =>
JsObject(map.map {
case (key, value) => (key.toString, JsNumber(value))
}(collection.breakOut))
}
implicit val coverageFileReportWrites: Writes[CoverageFileReport] = Json.writes[CoverageFileReport]
implicit val coverageReportWrites: Writes[CoverageReport] = Json.writes[CoverageReport]
}

object OrganizationProvider extends Enumeration {
val manual, gh, bb, ghe, bbe, gl, gle = Value
}
109 changes: 109 additions & 0 deletions api-scala/src/main/scala/com/codacy/api/client/CodacyClient.scala
@@ -0,0 +1,109 @@
package com.codacy.api.client

import play.api.libs.json._
import com.codacy.api.util.JsonOps
import scalaj.http.Http

import java.net.URL
import scala.util.{Failure, Success, Try}
import scala.util.control.NonFatal

class CodacyClient(
apiUrl: Option[String] = None,
apiToken: Option[String] = None,
projectToken: Option[String] = None
) {

private case class ErrorJson(error: String)
private case class PaginatedResult[T](next: Option[String], values: Seq[T])

private implicit val errorJsonFormat: Reads[ErrorJson] = Json.reads[ErrorJson]

private val tokens = Map.empty[String, String] ++
apiToken.map(t => "api-token" -> t) ++
projectToken.map(t => "project-token" -> t) ++
// This is deprecated and is kept for backward compatibility. It will removed in the context of CY-1272
apiToken.map(t => "api_token" -> t) ++
projectToken.map(t => "project_token" -> t)

private val remoteUrl = new URL(new URL(apiUrl.getOrElse("https://api.codacy.com")), "/2.0").toString()

/*
* Does an API post
*/
def post[T](
request: Request[T],
value: String,
timeoutOpt: Option[RequestTimeout] = None,
sleepTime: Option[Int],
numRetries: Option[Int]
)(implicit reads: Reads[T]): RequestResponse[T] = {
val url = s"$remoteUrl/${request.endpoint}"
try {
val headers = tokens ++ Map("Content-Type" -> "application/json")

val httpRequest = timeoutOpt match {
case Some(timeout) =>
Http(url).timeout(connTimeoutMs = timeout.connTimeoutMs, readTimeoutMs = timeout.readTimeoutMs)
case None => Http(url)
}

val body = httpRequest
.params(request.queryParameters)
.headers(headers)
.postData(value)
.asString
.body

parseJsonAs[T](body) match {
case failure: FailedResponse =>
retryPost(request, value, timeoutOpt, sleepTime, numRetries.map(x => x - 1), failure.message)
case success => success
}
} catch {
case NonFatal(ex) => retryPost(request, value, timeoutOpt, sleepTime, numRetries.map(x => x - 1), ex.getMessage)
}
}

private def retryPost[T](
request: Request[T],
value: String,
timeoutOpt: Option[RequestTimeout],
sleepTime: Option[Int],
numRetries: Option[Int],
failureMessage: String
)(implicit reads: Reads[T]): RequestResponse[T] = {
if (numRetries.exists(x => x > 0)) {
sleepTime.map(x => Thread.sleep(x))
post(request, value, timeoutOpt, sleepTime, numRetries.map(x => x - 1))
} else {
RequestResponse.failure(
s"Error doing a post to $remoteUrl/${request.endpoint}: exhausted retries due to $failureMessage"
)
}
}

private def parseJsonAs[T](input: String)(implicit reads: Reads[T]): RequestResponse[T] = {
parseJson(input) match {
case failure: FailedResponse => failure
case SuccessfulResponse(json) =>
json
.validate[T]
.fold(
errors => FailedResponse(JsonOps.handleConversionFailure(errors)),
converted => SuccessfulResponse(converted)
)
}
}

private def parseJson(input: String): RequestResponse[JsValue] = {
Try(Json.parse(input)) match {
case Success(json) =>
json
.validate[ErrorJson]
.fold(_ => SuccessfulResponse(json), apiError => FailedResponse(s"API Error: ${apiError.error}"))
case Failure(exception) =>
FailedResponse(s"Failed to parse API response as JSON: $input\nUnderlying exception - ${exception.getMessage}")
}
}
}
3 changes: 3 additions & 0 deletions api-scala/src/main/scala/com/codacy/api/client/Request.scala
@@ -0,0 +1,3 @@
package com.codacy.api.client

case class Request[T](endpoint: String, classType: Class[T], queryParameters: Map[String, String] = Map.empty)
@@ -0,0 +1,27 @@
package com.codacy.api.client

sealed trait RequestResponse[+A]

case class SuccessfulResponse[A](value: A) extends RequestResponse[A]

case class FailedResponse(message: String) extends RequestResponse[Nothing]

object RequestResponse {

def success[A](a: A): RequestResponse[A] = SuccessfulResponse(a)

def failure[A](message: String): RequestResponse[A] = FailedResponse(message: String)

def apply[A](r1: RequestResponse[Seq[A]], r2: RequestResponse[Seq[A]]): RequestResponse[Seq[A]] = {
r1 match {
case SuccessfulResponse(v1) =>
r2 match {
case SuccessfulResponse(v2) =>
SuccessfulResponse(v1 ++ v2)
case f @ FailedResponse(_) => f
}
case f @ FailedResponse(_) => f
}
}

}
@@ -0,0 +1,9 @@
package com.codacy.api.client

import play.api.libs.json.{Json, Reads}

case class RequestSuccess(success: String)

object RequestSuccess {
implicit val requestSuccessReads: Reads[RequestSuccess] = Json.reads[RequestSuccess]
}
@@ -0,0 +1,6 @@
package com.codacy.api.client

/**
* The socket connection and read timeouts in milliseconds.
*/
case class RequestTimeout(connTimeoutMs: Int, readTimeoutMs: Int)
19 changes: 19 additions & 0 deletions api-scala/src/main/scala/com/codacy/api/helpers/FileHelper.scala
@@ -0,0 +1,19 @@
package com.codacy.api.helpers

import java.io.{File, PrintWriter}

import play.api.libs.json._

import scala.util.Try

object FileHelper {

def writeJsonToFile[A](file: File, value: A)(implicit writes: Writes[A]): Boolean = {
val reportJson = Json.stringify(Json.toJson(value))
val printWriter = new PrintWriter(file)
val result = Try(printWriter.write(reportJson)).isSuccess
printWriter.close()
result
}

}
@@ -0,0 +1,41 @@
package com.codacy.api.helpers.vcs

import java.io.File
import java.util.Date

import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.{Repository, RepositoryBuilder}

import scala.collection.JavaConverters._
import scala.util.Try

case class CommitInfo(uuid: String, authorName: String, authorEmail: String, date: Date)

class GitClient(workDirectory: File) {

val repositoryTry: Try[Repository] = Try(new RepositoryBuilder().findGitDir(workDirectory).readEnvironment().build())

val repository: Option[Repository] = repositoryTry.toOption

def latestCommitUuid(): Option[String] = {
repositoryTry
.map { rep =>
val git = new Git(rep)
val headRev = git.log().setMaxCount(1).call().asScala.head
headRev.getName
}
.toOption
.filter(_.trim.nonEmpty)
}

def latestCommitInfo: Try[CommitInfo] = {
repositoryTry.map { rep =>
val git = new Git(rep)
val headRev = git.log().setMaxCount(1).call().asScala.head
val authorIdent = headRev.getAuthorIdent

CommitInfo(headRev.getName, authorIdent.getName, authorIdent.getEmailAddress, authorIdent.getWhen)
}
}

}
143 changes: 143 additions & 0 deletions api-scala/src/main/scala/com/codacy/api/service/CoverageServices.scala
@@ -0,0 +1,143 @@
package com.codacy.api.service

import com.codacy.api.client.{CodacyClient, Request, RequestResponse, RequestSuccess, RequestTimeout}
import com.codacy.api.{CoverageReport, OrganizationProvider}
import play.api.libs.json.Json

class CoverageServices(client: CodacyClient) {

/**
* Send coverage report to Codacy endpoint.
* This endpoint requires a project token to authenticate the request and identify the project.
* Therefore, the client must be initialized with a valid project token.
* @param commitUuid commit unique identifier
* @param language programing language
* @param coverageReport coverage report being sent to Codacy
* @param partial flag that signals if the report operation will be broken in multiple operations
* @param timeoutOpt socket connection and read timeouts in milliseconds
* @return Request response
*/
def sendReport(
commitUuid: String,
language: String,
coverageReport: CoverageReport,
partial: Boolean = false,
timeoutOpt: Option[RequestTimeout] = None,
sleepTime: Option[Int] = None,
numRetries: Option[Int] = None,
): RequestResponse[RequestSuccess] = {
val endpoint = s"coverage/$commitUuid/${encodePathSegment(language.toLowerCase)}"

postRequest(endpoint, coverageReport, partial, timeoutOpt, sleepTime, numRetries)
}

/**
* Send final notification signaling the end of the report operation.
* This endpoint requires an account token to authenticate the request and identify the project.
* Therefore, the client must be initialized with a valid account token.
* @param commitUuid commit unique identifier
* @param timeoutOpt socket connection and read timeouts in milliseconds
* @return Request Response
*/
def sendFinalNotification(
commitUuid: String,
timeoutOpt: Option[RequestTimeout] = None,
sleepTime: Option[Int] = None,
numRetries: Option[Int] = None,
): RequestResponse[RequestSuccess] = {
val endpoint = s"commit/$commitUuid/coverageFinal"

postEmptyRequest(endpoint, timeoutOpt, sleepTime, numRetries)
}

/**
* Send coverage report with a project name to Codacy endpoint.
* This endpoint requires an account token to authenticate the request.
* Therefore, the client must be initialized with a valid account token.
* @param username reporter's username
* @param projectName name of the project the report pertains
* @param commitUuid commit unique identifier
* @param language programing language
* @param coverageReport coverage report being reported
* @param partial flag that signals if the report operation will be broken in multiple operations
* @param timeoutOpt socket connection and read timeouts in milliseconds
* @return Request Response
*/
def sendReportWithProjectName(
provider: OrganizationProvider.Value,
username: String,
projectName: String,
commitUuid: String,
language: String,
coverageReport: CoverageReport,
partial: Boolean = false,
timeoutOpt: Option[RequestTimeout] = None,
sleepTime: Option[Int] = None,
numRetries: Option[Int] = None,
): RequestResponse[RequestSuccess] = {
val endpoint =
s"${provider.toString}/$username/$projectName/commit/$commitUuid/coverage/${encodePathSegment(language.toLowerCase)}"
postRequest(endpoint, coverageReport, partial, timeoutOpt, sleepTime, numRetries)
}

/**
* Send final notification with a project name, signaling the end of the report operation.
* This endpoint requires an account token to authenticate the request.
* Therefore, the client must be initialized with a valid account token.
* @param username reporter's username
* @param projectName name of the project the report pertains
* @param commitUuid commit unique identifier
* @param timeoutOpt socket connection and read timeouts in milliseconds
* @return Request Response
*/
def sendFinalWithProjectName(
provider: OrganizationProvider.Value,
username: String,
projectName: String,
commitUuid: String,
timeoutOpt: Option[RequestTimeout] = None,
sleepTime: Option[Int] = None,
numRetries: Option[Int] = None
): RequestResponse[RequestSuccess] = {
val endpoint = s"${provider.toString}/$username/$projectName/commit/$commitUuid/coverageFinal"

postEmptyRequest(endpoint, timeoutOpt, sleepTime, numRetries)
}

private def postRequest(
endpoint: String,
coverageReport: CoverageReport,
partial: Boolean,
timeoutOpt: Option[RequestTimeout],
sleepTime: Option[Int],
numRetries: Option[Int]
) = {
val queryParams = getQueryParameters(partial)

val jsonString = serializeCoverageReport(coverageReport)

client.post(Request(endpoint, classOf[RequestSuccess], queryParams), jsonString, timeoutOpt, sleepTime, numRetries)
}

private def postEmptyRequest(
endpoint: String,
timeoutOpt: Option[RequestTimeout],
sleepTime: Option[Int],
numRetries: Option[Int]
) =
client.post(Request(endpoint, classOf[RequestSuccess]), "{}", timeoutOpt, sleepTime, numRetries)

private def getQueryParameters(partial: Boolean) = {
Map("partial" -> partial.toString)
}
private def serializeCoverageReport(coverageReport: CoverageReport) =
Json.stringify(Json.toJson(coverageReport))

/**
* Any encoding that we do here, needs to have the same output
* of play.utils.UriEncoding.encodePathSegment for our languages.
* https://github.com/playframework/playframework/blob/316fbd61c9fc6a6081a3aeef7e773c8bbccd0b6b/core/play/src/main/scala/play/utils/UriEncoding.scala#L50
*/
private def encodePathSegment(segment: String): String = segment.replaceAll(" ", "%20")

}

0 comments on commit a26475b

Please sign in to comment.