diff --git a/src/main/scala/codecheck/github/api/GitHubAPI.scala b/src/main/scala/codecheck/github/api/GitHubAPI.scala index 7117afb..0fc1f37 100644 --- a/src/main/scala/codecheck/github/api/GitHubAPI.scala +++ b/src/main/scala/codecheck/github/api/GitHubAPI.scala @@ -28,6 +28,7 @@ class GitHubAPI(token: String, client: Transport, tokenType: String = "token", d with WebhookOp with CollaboratorOp with BranchOp + with SearchOp { private val endpoint = "https://api.github.com" diff --git a/src/main/scala/codecheck/github/models/Repository.scala b/src/main/scala/codecheck/github/models/Repository.scala index 6ec3bc4..c90e5cb 100644 --- a/src/main/scala/codecheck/github/models/Repository.scala +++ b/src/main/scala/codecheck/github/models/Repository.scala @@ -66,9 +66,12 @@ 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").toLong def description = opt("description") - def open_issues_count = get("open_issues_count").toInt + def open_issues_count = get("open_issues_count").toLong + def `private` = boolean("private") lazy val permissions = Permissions(value \ "permissions") lazy val owner = User(value \ "owner") 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..c5e2d12 --- /dev/null +++ b/src/main/scala/codecheck/github/models/Search.scala @@ -0,0 +1,133 @@ +package codecheck.github.models + +import org.json4s.JValue +import org.json4s.JArray + +sealed trait SearchSort { + def name: String + override def toString = name +} + +sealed abstract class SearchRepositorySort(val name: String) extends SearchSort + +object SearchRepositorySort { + case object stars extends SearchRepositorySort("stars") + case object forks extends SearchRepositorySort("forks") + case object updated extends SearchRepositorySort("updated") + + val values = Array(stars, forks, updated) + + def fromString(str: String) = values.filter(_.name == str).head +} + +sealed abstract class SearchCodeSort(val name: String) extends SearchSort + +object SearchCodeSort { + case object indexed extends SearchCodeSort("indexed") + + val values = Array(indexed) + + def fromString(str: String) = values.filter(_.name == str).head +} + +sealed abstract class SearchIssueSort(val name: String) extends SearchSort + +object SearchIssueSort { + case object created extends SearchIssueSort("created") + case object updated extends SearchIssueSort("updated") + case object comments extends SearchIssueSort("comments") + + val values = Array(created, updated, comments) + + def fromString(str: String) = values.filter(_.name == str).head +} + +sealed abstract class SearchUserSort(val name: String) extends SearchSort + +object SearchUserSort { + case object followers extends SearchUserSort("followers") + case object repositories extends SearchUserSort("repositories") + case object joined extends SearchUserSort("joined") + + val values = Array(followers, repositories, joined) + + def fromString(str: String) = values.filter(_.name == str).head +} + +sealed trait SearchInput extends AbstractInput { + def q: String + def sort: Option[SearchSort] + def order: SortDirection + def query = s"?q=$q" + sort.map(sortBy => s"&sort=$sortBy&order=$order").getOrElse("") +} + +case class SearchRepositoryInput ( + val q: String, + val sort: Option[SearchRepositorySort] = None, + val order: SortDirection = SortDirection.desc +) extends SearchInput + +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(Repository(_)) + case _ => Nil + } +} + +case class SearchCodeInput ( + q: String, + sort: Option[SearchCodeSort] = None, + order: SortDirection = SortDirection.desc +) extends SearchInput + +case class SearchCodeItem(value: JValue) extends AbstractJson(value) { + def name: String = get("name") + def path: String = get("path") + def sha: String = get("sha") + def url: String = get("url") + def git_url: String = get("git_url") + def html_url: String = get("html_url") + def score: Double = get("score").toDouble + lazy val repository = 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(SearchCodeItem(_)) + case _ => Nil + } +} + +case class SearchIssueInput ( + q: String, + sort: Option[SearchIssueSort] = None, + order: SortDirection = SortDirection.desc +) extends SearchInput + +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(Issue(_)) + case _ => Nil + } +} + +case class SearchUserInput ( + q: String, + sort: Option[SearchUserSort] = None, + order: SortDirection = SortDirection.desc +) extends SearchInput + +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(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..9c81930 --- /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${input.query}" + exec("GET", path ).map { res => + SearchRepositoryResult(res.body) + } + } + + def searchCode(input: SearchInput): Future[SearchCodeResult] = { + val path = s"/search/code${input.query}" + exec("GET", path ).map { res => + SearchCodeResult(res.body) + } + } + + def searchIssues(input: SearchInput): Future[SearchIssueResult] = { + val path = s"/search/issues${input.query}" + exec("GET", path ).map { res => + SearchIssueResult(res.body) + } + } + + def searchUser(input: SearchInput): Future[SearchUserResult] = { + val path = s"/search/users${input.query}" + 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..3ce9538 --- /dev/null +++ b/src/test/scala/SearchOpSpec.scala @@ -0,0 +1,93 @@ +import org.scalatest.path.FunSpec +import scala.concurrent.Await +import scala.concurrent.ExecutionContext.Implicits.global +import codecheck.github.models.SortDirection +import codecheck.github.models.SearchRepositoryInput +import codecheck.github.models.SearchCodeInput +import codecheck.github.models.SearchIssueInput +import codecheck.github.models.SearchUserInput +import codecheck.github.models.SearchRepositorySort +import codecheck.github.models.SearchCodeSort +import codecheck.github.models.SearchIssueSort +import codecheck.github.models.SearchUserSort +import codecheck.github.models.SearchRepositoryResult +import codecheck.github.models.SearchCodeResult +import codecheck.github.exceptions.GitHubAPIException + +class SearchOpSpec extends FunSpec + with Constants +{ + + describe("searchRepositories") { + it("with valid SearchInput should succeed") { + val q = "tetris language:assembly".trim.replaceAll(" ","+") + val input = SearchRepositoryInput(q,sort=Some(SearchRepositorySort.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") { + val q = "jquery in:name,description".trim.replaceAll(" ","+") + val input = SearchRepositoryInput(q,sort=Some(SearchRepositorySort.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") { + val q = "addClass in:file language:js repo:jquery/jquery".trim.replaceAll(" ","+") + val input = SearchCodeInput(q,sort=None,order=SortDirection.desc) + val res = Await.result(api.searchCode(input), TIMEOUT) + assert(res.total_count >= 1) + assert(res.items(0).repository.id >= 1 ) + assert(res.items(0).sha.length >= 40) + assert(res.items(0).score >= 0d) + assert(res.items(0).repository.full_name == "jquery/jquery") + } + it("with valid SearchInput it should succeed") { + val q = "function size:10000 language:python".trim.replaceAll(" ","+") + val input = SearchCodeInput(q,sort=Some(SearchCodeSort.indexed),order=SortDirection.asc) + val res = Await.result(api.searchCode(input), TIMEOUT) + assert(res.total_count >= 1) + assert(res.items(0).repository.id >= 1 ) + assert(res.items(0).path.endsWith(".py")) + assert(res.items(0).sha.length >= 40) + assert(res.items(0).score >= 0d) + assert(res.items(0).repository.`private` == false) + } + } + describe("searchIssues") { + it("with valid SearchInput should succeed") { + val q = "windows label:bug language:python state:open".trim.replaceAll(" ","+") + val input = SearchIssueInput(q,sort=Some(SearchIssueSort.created),order=SortDirection.desc) + 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") { + val q = "tom repos:>42 followers:>1000" + .trim.replaceAll(" ","+") + .replaceAll(">","%3E") + val input = SearchUserInput(q,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) + } + } +}