diff --git a/src/main/scala/codecheck/github/api/GitHubAPI.scala b/src/main/scala/codecheck/github/api/GitHubAPI.scala index af9ca43..366af99 100644 --- a/src/main/scala/codecheck/github/api/GitHubAPI.scala +++ b/src/main/scala/codecheck/github/api/GitHubAPI.scala @@ -27,6 +27,7 @@ class GitHubAPI(token: String, client: AsyncHttpClient, tokenType: String = "tok with MilestoneOp with WebhookOp with CollaboratorOp + with SearchOp { private val endpoint = "https://api.github.com" diff --git a/src/main/scala/codecheck/github/models/Issue.scala b/src/main/scala/codecheck/github/models/Issue.scala index 325ed32..5769b58 100644 --- a/src/main/scala/codecheck/github/models/Issue.scala +++ b/src/main/scala/codecheck/github/models/Issue.scala @@ -104,6 +104,7 @@ case class Issue(value: JValue) extends AbstractJson(value) { lazy val assignee = objectOpt("assignee")(v => User(v)) lazy val milestone = objectOpt("milestone")(v => Milestone(v)) + val state = get("state") lazy val user = new User(value \ "user") lazy val labels = (value \ "labels") match { case JArray(arr) => arr.map(new Label(_)) diff --git a/src/main/scala/codecheck/github/models/Repository.scala b/src/main/scala/codecheck/github/models/Repository.scala index 93f3570..852423f 100644 --- a/src/main/scala/codecheck/github/models/Repository.scala +++ b/src/main/scala/codecheck/github/models/Repository.scala @@ -53,6 +53,8 @@ case class Repository(value: JValue) extends AbstractJson(value) { def name = get("name") def full_name = get("full_name") def url = get("url") + def language = get("language") + def stargazers_count = get("stargazers_count") def description = opt("description") def open_issues_count = get("open_issues_count").toInt @@ -65,4 +67,4 @@ case class Permissions(value: JValue) extends AbstractJson(value) { def admin = boolean("admin") def push = boolean("push") def pull = boolean("pull") -} \ No newline at end of file +} diff --git a/src/main/scala/codecheck/github/models/Search.scala b/src/main/scala/codecheck/github/models/Search.scala new file mode 100644 index 0000000..081479b --- /dev/null +++ b/src/main/scala/codecheck/github/models/Search.scala @@ -0,0 +1,74 @@ +package codecheck.github.models + +import org.json4s.JValue +import org.json4s.JArray + +sealed abstract class SearchSort(val name: String) { + override def toString = name +} +object SearchSort { + //for serachRepositories + case object stars extends SearchSort("stars") + case object forks extends SearchSort("forks") + case object updated extends SearchSort("updated") + + //for searchCode + case object indexed extends SearchSort("indexed") + + //for searchIssues + case object comments extends SearchSort("comments") + case object created extends SearchSort("created") + //case object updated extends SearchSort("updated") + + //for searchUser + case object followers extends SearchSort("followers") + case object repositories extends SearchSort("repositories") + case object joined extends SearchSort("joined") +} + +case class SearchInput ( + q: String, + sort: Option[SearchSort] = None, + order: SortDirection = SortDirection.desc +) extends AbstractInput + +case class SearchRepositoryResult(value: JValue) extends AbstractJson(value) { + def total_count: Long = get("total_count").toLong + def incomplete_results: Boolean = boolean("incomplete_results") + lazy val items = (value \ "items") match { + case JArray(arr) => arr.map(new Repository(_)) + case _ => Nil + } +} + +case class searchCodeItems (value: JValue) extends AbstractJson(value){ + def name: String = get("name") + lazy val Repo = new Repository(value \ "repository") +} + +case class SearchCodeResult(value: JValue) extends AbstractJson(value) { + def total_count: Long = get("total_count").toLong + def incomplete_results: Boolean = boolean("incomplete_results") + lazy val items = (value \ "items") match { + case JArray(arr) => arr.map(new searchCodeItems(_)) + case _ => Nil + } +} + +case class SearchIssueResult(value: JValue) extends AbstractJson(value) { + def total_count: Long = get("total_count").toLong + def incomplete_results: Boolean = boolean("incomplete_results") + lazy val items = (value \ "items") match { + case JArray(arr) => arr.map(new Issue(_)) + case _ => Nil + } +} + +case class SearchUserResult(value: JValue) extends AbstractJson(value) { + def total_count: Long = get("total_count").toLong + def incomplete_results: Boolean = boolean("incomplete_results") + lazy val items = (value \ "items") match { + case JArray(arr) => arr.map(new User(_)) + case _ => Nil + } +} diff --git a/src/main/scala/codecheck/github/operations/SearchOp.scala b/src/main/scala/codecheck/github/operations/SearchOp.scala new file mode 100644 index 0000000..8519a69 --- /dev/null +++ b/src/main/scala/codecheck/github/operations/SearchOp.scala @@ -0,0 +1,43 @@ +package codecheck.github.operations + +import scala.concurrent.Future +import scala.concurrent.ExecutionContext.Implicits.global + +import codecheck.github.api.GitHubAPI +import codecheck.github.models.SearchInput +import codecheck.github.models.SearchRepositoryResult +import codecheck.github.models.SearchCodeResult +import codecheck.github.models.SearchIssueResult +import codecheck.github.models.SearchUserResult + +trait SearchOp { + self: GitHubAPI => + + def searchRepositories(input: SearchInput): Future[SearchRepositoryResult] = { + val path = s"/search/repositories?q=${input.q}&sort=${input.sort}&order=${input.order}" + exec("GET", path ).map { res => + SearchRepositoryResult(res.body) + } + } + + def searchCode(input: SearchInput): Future[SearchCodeResult] = { + val path = s"/search/code?q=${input.q}&sort=${input.sort}&order=${input.order}" + exec("GET", path ).map { res => + SearchCodeResult(res.body) + } + } + + def searchIssues(input: SearchInput): Future[SearchIssueResult] = { + val path = s"/search/issues?q=${input.q}&sort=${input.sort}&order=${input.order}" + exec("GET", path ).map { res => + SearchIssueResult(res.body) + } + } + + def searchUser(input: SearchInput): Future[SearchUserResult] = { + val path = s"/search/users?q=${input.q}&sort=${input.sort}&order=${input.order}" + exec("GET", path ).map { res => + SearchUserResult(res.body) + } + } +} diff --git a/src/test/scala/SearchOpSpec.scala b/src/test/scala/SearchOpSpec.scala new file mode 100644 index 0000000..90ead5f --- /dev/null +++ b/src/test/scala/SearchOpSpec.scala @@ -0,0 +1,96 @@ +import org.scalatest.path.FunSpec +import scala.concurrent.Await +import scala.concurrent.ExecutionContext.Implicits.global +import codecheck.github.models.SortDirection +import codecheck.github.models.SearchInput +import codecheck.github.models.SearchSort +import codecheck.github.models.SearchRepositoryResult +import codecheck.github.models.SearchCodeResult +import codecheck.github.models.searchCodeItems +import codecheck.github.exceptions.GitHubAPIException + +class SearchOpSpec extends FunSpec + with Constants +{ + + describe("searchRepositories") { + it("with valid SearchInput should succeed") { + var q = "tetris language:assembly" + val q1 = q.trim.replaceAll(" ","+"); + val input = SearchInput(q1,sort=Some(SearchSort.stars),order=SortDirection.desc) + val res = Await.result(api.searchRepositories(input), TIMEOUT) + assert(res.total_count >= 1) + assert(res.items(0).id >= 1 ) + assert(res.items(0).name.length >= 1) + assert(res.items(0).full_name.length >= 1) + assert(res.items(0).description.isDefined) + assert(res.items(0).open_issues_count >= 0) + assert(res.items(0).language == "Assembly") + assert(res.items(0).stargazers_count > res.items(1).stargazers_count) + } + it("with valid changed query(q) SearchInput should succeed") { + var q = "jquery in:name,description" + val q1 = q.trim.replaceAll(" ","+"); + val input = SearchInput(q1,sort=Some(SearchSort.stars),order=SortDirection.desc) + val res = Await.result(api.searchRepositories(input), TIMEOUT) + assert(res.total_count >= 1) + assert(res.items(0).id >= 1 ) + assert(res.items(0).name.length >= 1) + assert(res.items(0).full_name.length >= 1) + assert(res.items(0).description.isDefined) + assert(res.items(0).open_issues_count >= 0) + } + } + describe("searchCode") { + it("with valid SearchInput q,no SortOrder should succeed") { + var q = "addClass in:file language:js repo:jquery/jquery" + val q1 = q.trim.replaceAll(" ","+"); + val input = SearchInput(q1,sort=None,order=SortDirection.desc) + val res = Await.result(api.searchCode(input), TIMEOUT) + assert(res.total_count >= 1) + assert(res.items(0).Repo.id >= 1 ) + assert(res.items(0).Repo.full_name == "jquery/jquery") + } + //Following test results in error: + // "message" : "Validation Failed", + // "errors" : [ { + // "message" : "Must include at least one user, organization, or repository" + it("with valid SearchInput it should succeed") { + var q = "function size:10000 language:python" + val q1 = q.trim.replaceAll(" ","+"); + val input = SearchInput(q1,sort=Some(SearchSort.indexed),order=SortDirection.desc) + try { + val res = Await.result(api.searchCode(input), TIMEOUT) + fail + } catch { + case e: GitHubAPIException => + assert(e.error.errors.length == 1) + assert(e.error.message == "Validation Failed") + } + } + } + describe("searchIssues") { + it("with valid SearchInput should succeed") { + var q = "windows label:bug language:python state:open" + val q1 = q.trim.replaceAll(" ","+"); + val input = SearchInput(q1,sort=Some(SearchSort.created),order=SortDirection.asc) + val res = Await.result(api.searchIssues(input), TIMEOUT) + assert(res.total_count >= 1) + assert(res.items(0).labels(0).name == "bug" ) + assert(res.items(0).state == "open") + assert(((res.items(0).created_at).compareTo(res.items(1).created_at)) > 0) + } + } + describe("searchUser") { + it("with valid SearchInput should succeed") { + var q = "tom repos:>42 followers:>1000" + q = q.trim.replaceAll(" ","+") + val q1 = q.replaceAll(">","%3E") + val input = SearchInput(q1,sort=None,order=SortDirection.desc) + val res = Await.result(api.searchUser(input), TIMEOUT) + assert(res.total_count >= 0) + assert(res.items(0).login.length >= 0) + assert(res.items(0).id >= 0) + } + } +}