From 474fff59a65064f0155908e10a6089844b01f12b Mon Sep 17 00:00:00 2001 From: Timo Schmid Date: Wed, 2 Oct 2019 22:16:28 +0200 Subject: [PATCH 1/5] Add Search.repository endpoint --- .../github/data/SearchResult.scala | 54 +++++++++++++++++++ .../github/endpoints/Search.scala | 32 +++++++++++ 2 files changed, 86 insertions(+) create mode 100644 core/src/main/scala/io/chrisdavenport/github/data/SearchResult.scala create mode 100644 core/src/main/scala/io/chrisdavenport/github/endpoints/Search.scala diff --git a/core/src/main/scala/io/chrisdavenport/github/data/SearchResult.scala b/core/src/main/scala/io/chrisdavenport/github/data/SearchResult.scala new file mode 100644 index 0000000..ba209ea --- /dev/null +++ b/core/src/main/scala/io/chrisdavenport/github/data/SearchResult.scala @@ -0,0 +1,54 @@ +package io.chrisdavenport.github.data + +import io.circe.Decoder +import cats.implicits._ +import io.chrisdavenport.github.data.SearchResult.Order.{Ascending, Descending} + +case class SearchResult[A](totalCount: Int, incompleteResults: Boolean, items: List[A]) + +object SearchResult { + + implicit def searchResultDecoder[A](implicit aDecoder: Decoder[A]): Decoder[SearchResult[A]] = + cursor => ( + cursor.downField("total_count").as[Int], + cursor.downField("incomplete_results").as[Boolean], + cursor.downField("items").as[List[A]] + ).mapN(SearchResult.apply) + + sealed trait Sort + + object Sort { + + case object Stars extends Sort + case object Forks extends Sort + case object HelpWantedIssues extends Sort + case object Updated extends Sort + case object BestMatch extends Sort + + def toOptionalParam(sort: Sort): Option[String] = + sort match { + case Stars => Some("stars") + case Forks => Some("forks") + case HelpWantedIssues => Some("help-wanted-issues") + case Updated => Some("updated") + case BestMatch => None + } + + } + + sealed trait Order + + object Order { + + case object Ascending extends Order + case object Descending extends Order + + def toOptionalParam(order: Order): Option[String] = + order match { + case Ascending => Some("asc") + case Descending => Some("desc") + } + + } + +} diff --git a/core/src/main/scala/io/chrisdavenport/github/endpoints/Search.scala b/core/src/main/scala/io/chrisdavenport/github/endpoints/Search.scala new file mode 100644 index 0000000..6e69009 --- /dev/null +++ b/core/src/main/scala/io/chrisdavenport/github/endpoints/Search.scala @@ -0,0 +1,32 @@ +package io.chrisdavenport.github.endpoints + +import cats.data._ +import cats.effect._ +import io.chrisdavenport.github.data.Repositories._ +import org.http4s._ +import org.http4s.implicits._ +import org.http4s.client.Client +import io.chrisdavenport.github.Auth +import io.chrisdavenport.github.data.SearchResult +import io.chrisdavenport.github.data.SearchResult.{Order, Sort} +import io.chrisdavenport.github.internals.GithubMedia._ +import io.chrisdavenport.github.internals.RequestConstructor + +object Search { + + def repository[F[_]: Sync]( + q: String, + sort: Option[Sort], + order: Option[Order], + auth: Option[Auth] + ): Kleisli[F, Client[F], SearchResult[Repo]] = + RequestConstructor.runRequestWithNoBody[F, SearchResult[Repo]]( + auth, + Method.GET, + (uri"/search" / "repositories") + .withQueryParam("q", q) + .withOptionQueryParam("sort", sort.flatMap(Sort.toOptionalParam)) + .withOptionQueryParam("order", order.flatMap(Order.toOptionalParam)) + ) + +} From 5aaaa7f7cebfd2664a0e5f2091f0a7d8e1b885b2 Mon Sep 17 00:00:00 2001 From: Timo Schmid Date: Wed, 2 Oct 2019 22:18:15 +0200 Subject: [PATCH 2/5] Remove unused import --- .../main/scala/io/chrisdavenport/github/data/SearchResult.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/core/src/main/scala/io/chrisdavenport/github/data/SearchResult.scala b/core/src/main/scala/io/chrisdavenport/github/data/SearchResult.scala index ba209ea..cc07296 100644 --- a/core/src/main/scala/io/chrisdavenport/github/data/SearchResult.scala +++ b/core/src/main/scala/io/chrisdavenport/github/data/SearchResult.scala @@ -2,7 +2,6 @@ package io.chrisdavenport.github.data import io.circe.Decoder import cats.implicits._ -import io.chrisdavenport.github.data.SearchResult.Order.{Ascending, Descending} case class SearchResult[A](totalCount: Int, incompleteResults: Boolean, items: List[A]) From 726ab416c48a7b8d1c6c73aab219c23cd5da3ec5 Mon Sep 17 00:00:00 2001 From: Timo Schmid Date: Wed, 2 Oct 2019 22:25:04 +0200 Subject: [PATCH 3/5] Remove leading slash Co-Authored-By: Christopher Davenport --- .../main/scala/io/chrisdavenport/github/endpoints/Search.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/scala/io/chrisdavenport/github/endpoints/Search.scala b/core/src/main/scala/io/chrisdavenport/github/endpoints/Search.scala index 6e69009..a00c660 100644 --- a/core/src/main/scala/io/chrisdavenport/github/endpoints/Search.scala +++ b/core/src/main/scala/io/chrisdavenport/github/endpoints/Search.scala @@ -23,7 +23,7 @@ object Search { RequestConstructor.runRequestWithNoBody[F, SearchResult[Repo]]( auth, Method.GET, - (uri"/search" / "repositories") + (uri"search" / "repositories") .withQueryParam("q", q) .withOptionQueryParam("sort", sort.flatMap(Sort.toOptionalParam)) .withOptionQueryParam("order", order.flatMap(Order.toOptionalParam)) From b6b752103cf88bab64b3a51ac09e6fc910114ca8 Mon Sep 17 00:00:00 2001 From: Timo Schmid Date: Wed, 2 Oct 2019 22:57:24 +0200 Subject: [PATCH 4/5] Address issues from code-review --- .../github/endpoints/Search.scala | 4 + .../github/endpoints/SearchSpec.scala | 143 ++++++++++++++++++ 2 files changed, 147 insertions(+) create mode 100644 core/src/test/scala/io/chrisdavenport/github/endpoints/SearchSpec.scala diff --git a/core/src/main/scala/io/chrisdavenport/github/endpoints/Search.scala b/core/src/main/scala/io/chrisdavenport/github/endpoints/Search.scala index a00c660..28bfe8c 100644 --- a/core/src/main/scala/io/chrisdavenport/github/endpoints/Search.scala +++ b/core/src/main/scala/io/chrisdavenport/github/endpoints/Search.scala @@ -14,6 +14,10 @@ import io.chrisdavenport.github.internals.RequestConstructor object Search { + /** + * Repository Search Endpoint + * https://developer.github.com/v3/search/#search-repositories + */ def repository[F[_]: Sync]( q: String, sort: Option[Sort], diff --git a/core/src/test/scala/io/chrisdavenport/github/endpoints/SearchSpec.scala b/core/src/test/scala/io/chrisdavenport/github/endpoints/SearchSpec.scala new file mode 100644 index 0000000..cf7f6ae --- /dev/null +++ b/core/src/test/scala/io/chrisdavenport/github/endpoints/SearchSpec.scala @@ -0,0 +1,143 @@ +package io.chrisdavenport.github.endpoints + +import org.specs2.mutable.Specification + +import cats.effect._ +import cats.effect.specs2.CatsEffect + + +import io.circe.literal._ +import org.http4s._ +import org.http4s.implicits._ +import org.http4s.client._ +import org.http4s.circe._ +import org.http4s.dsl.io._ + +class SearchSpec extends Specification with CatsEffect { + + "Search" should { + + "return a valid search result" in { + Search.repository[IO]("anything", None, None, None) + .run(Client.fromHttpApp(searchRepositories.orNotFound)) + .attempt + .map{_ must beRight} + } + + } + + val searchRepositories : HttpRoutes[IO] = HttpRoutes.of { + case GET -> Root / "search" / "repositories" => + Ok( + json""" +{ + "total_count" : 1, + "incomplete_results" : false, + "items" : [ + { + "id" : 56091117, + "node_id" : "MDEwOlJlcG9zaXRvcnk1NjA5MTExNw==", + "name" : "svalidate", + "full_name" : "timo-schmid/svalidate", + "private" : false, + "owner" : { + "login" : "timo-schmid", + "id" : 1415715, + "node_id" : "MDQ6VXNlcjE0MTU3MTU=", + "avatar_url" : "https://avatars1.githubusercontent.com/u/1415715?v=4", + "gravatar_id" : "", + "url" : "https://api.github.com/users/timo-schmid", + "html_url" : "https://github.com/timo-schmid", + "followers_url" : "https://api.github.com/users/timo-schmid/followers", + "following_url" : "https://api.github.com/users/timo-schmid/following{/other_user}", + "gists_url" : "https://api.github.com/users/timo-schmid/gists{/gist_id}", + "starred_url" : "https://api.github.com/users/timo-schmid/starred{/owner}{/repo}", + "subscriptions_url" : "https://api.github.com/users/timo-schmid/subscriptions", + "organizations_url" : "https://api.github.com/users/timo-schmid/orgs", + "repos_url" : "https://api.github.com/users/timo-schmid/repos", + "events_url" : "https://api.github.com/users/timo-schmid/events{/privacy}", + "received_events_url" : "https://api.github.com/users/timo-schmid/received_events", + "type" : "User", + "site_admin" : false + }, + "html_url" : "https://github.com/timo-schmid/svalidate", + "description" : "lightweight validation for scala", + "fork" : false, + "url" : "https://api.github.com/repos/timo-schmid/svalidate", + "forks_url" : "https://api.github.com/repos/timo-schmid/svalidate/forks", + "keys_url" : "https://api.github.com/repos/timo-schmid/svalidate/keys{/key_id}", + "collaborators_url" : "https://api.github.com/repos/timo-schmid/svalidate/collaborators{/collaborator}", + "teams_url" : "https://api.github.com/repos/timo-schmid/svalidate/teams", + "hooks_url" : "https://api.github.com/repos/timo-schmid/svalidate/hooks", + "issue_events_url" : "https://api.github.com/repos/timo-schmid/svalidate/issues/events{/number}", + "events_url" : "https://api.github.com/repos/timo-schmid/svalidate/events", + "assignees_url" : "https://api.github.com/repos/timo-schmid/svalidate/assignees{/user}", + "branches_url" : "https://api.github.com/repos/timo-schmid/svalidate/branches{/branch}", + "tags_url" : "https://api.github.com/repos/timo-schmid/svalidate/tags", + "blobs_url" : "https://api.github.com/repos/timo-schmid/svalidate/git/blobs{/sha}", + "git_tags_url" : "https://api.github.com/repos/timo-schmid/svalidate/git/tags{/sha}", + "git_refs_url" : "https://api.github.com/repos/timo-schmid/svalidate/git/refs{/sha}", + "trees_url" : "https://api.github.com/repos/timo-schmid/svalidate/git/trees{/sha}", + "statuses_url" : "https://api.github.com/repos/timo-schmid/svalidate/statuses/{sha}", + "languages_url" : "https://api.github.com/repos/timo-schmid/svalidate/languages", + "stargazers_url" : "https://api.github.com/repos/timo-schmid/svalidate/stargazers", + "contributors_url" : "https://api.github.com/repos/timo-schmid/svalidate/contributors", + "subscribers_url" : "https://api.github.com/repos/timo-schmid/svalidate/subscribers", + "subscription_url" : "https://api.github.com/repos/timo-schmid/svalidate/subscription", + "commits_url" : "https://api.github.com/repos/timo-schmid/svalidate/commits{/sha}", + "git_commits_url" : "https://api.github.com/repos/timo-schmid/svalidate/git/commits{/sha}", + "comments_url" : "https://api.github.com/repos/timo-schmid/svalidate/comments{/number}", + "issue_comment_url" : "https://api.github.com/repos/timo-schmid/svalidate/issues/comments{/number}", + "contents_url" : "https://api.github.com/repos/timo-schmid/svalidate/contents/{+path}", + "compare_url" : "https://api.github.com/repos/timo-schmid/svalidate/compare/{base}...{head}", + "merges_url" : "https://api.github.com/repos/timo-schmid/svalidate/merges", + "archive_url" : "https://api.github.com/repos/timo-schmid/svalidate/{archive_format}{/ref}", + "downloads_url" : "https://api.github.com/repos/timo-schmid/svalidate/downloads", + "issues_url" : "https://api.github.com/repos/timo-schmid/svalidate/issues{/number}", + "pulls_url" : "https://api.github.com/repos/timo-schmid/svalidate/pulls{/number}", + "milestones_url" : "https://api.github.com/repos/timo-schmid/svalidate/milestones{/number}", + "notifications_url" : "https://api.github.com/repos/timo-schmid/svalidate/notifications{?since,all,participating}", + "labels_url" : "https://api.github.com/repos/timo-schmid/svalidate/labels{/name}", + "releases_url" : "https://api.github.com/repos/timo-schmid/svalidate/releases{/id}", + "deployments_url" : "https://api.github.com/repos/timo-schmid/svalidate/deployments", + "created_at" : "2016-04-12T19:17:42Z", + "updated_at" : "2016-11-03T17:31:23Z", + "pushed_at" : "2017-03-14T16:46:34Z", + "git_url" : "git://github.com/timo-schmid/svalidate.git", + "ssh_url" : "git@github.com:timo-schmid/svalidate.git", + "clone_url" : "https://github.com/timo-schmid/svalidate.git", + "svn_url" : "https://github.com/timo-schmid/svalidate", + "homepage" : "http://svalidate.readthedocs.org/en/latest/", + "size" : 45, + "stargazers_count" : 1, + "watchers_count" : 1, + "language" : "Scala", + "has_issues" : true, + "has_projects" : true, + "has_downloads" : true, + "has_wiki" : true, + "has_pages" : false, + "forks_count" : 0, + "mirror_url" : null, + "archived" : false, + "disabled" : false, + "open_issues_count" : 0, + "license" : null, + "forks" : 0, + "open_issues" : 0, + "watchers" : 1, + "default_branch" : "master", + "permissions" : { + "admin" : true, + "push" : true, + "pull" : true + }, + "score" : 21.219961 + } + ] +} + """ + ) + } + +} From edc29e1d561f628385053f762a50688a0cd2c002 Mon Sep 17 00:00:00 2001 From: Timo Schmid Date: Wed, 2 Oct 2019 22:57:54 +0200 Subject: [PATCH 5/5] Fix uri for github.com for relative uris --- .../io/chrisdavenport/github/internals/RequestConstructor.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/scala/io/chrisdavenport/github/internals/RequestConstructor.scala b/core/src/main/scala/io/chrisdavenport/github/internals/RequestConstructor.scala index 247f3fd..9053db8 100644 --- a/core/src/main/scala/io/chrisdavenport/github/internals/RequestConstructor.scala +++ b/core/src/main/scala/io/chrisdavenport/github/internals/RequestConstructor.scala @@ -89,7 +89,7 @@ object RequestConstructor { .void .stream - private val GITHUB_URI: Uri = uri"https://api.github.com" + private val GITHUB_URI: Uri = uri"https://api.github.com/" private def baseUrl(auth: Option[Auth]): Uri = auth match { case Some(EnterpriseOAuth(apiEndpoint, _)) => apiEndpoint