-
Notifications
You must be signed in to change notification settings - Fork 94
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
clean: Inline content of codacy-api-scala (#480)
* 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
Showing
37 changed files
with
479 additions
and
641 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
21 changes: 21 additions & 0 deletions
21
api-scala/src/main/scala/com/codacy/api/CoverageReport.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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
109
api-scala/src/main/scala/com/codacy/api/client/CodacyClient.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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}") | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
package com.codacy.api.client | ||
|
||
case class Request[T](endpoint: String, classType: Class[T], queryParameters: Map[String, String] = Map.empty) |
27 changes: 27 additions & 0 deletions
27
api-scala/src/main/scala/com/codacy/api/client/RequestResponse.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} | ||
} | ||
|
||
} |
9 changes: 9 additions & 0 deletions
9
api-scala/src/main/scala/com/codacy/api/client/RequestSuccess.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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] | ||
} |
6 changes: 6 additions & 0 deletions
6
api-scala/src/main/scala/com/codacy/api/client/RequestTimeout.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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
19
api-scala/src/main/scala/com/codacy/api/helpers/FileHelper.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} | ||
|
||
} |
41 changes: 41 additions & 0 deletions
41
api-scala/src/main/scala/com/codacy/api/helpers/vcs/GitClient.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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
143
api-scala/src/main/scala/com/codacy/api/service/CoverageServices.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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") | ||
|
||
} |
Oops, something went wrong.