Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Project API #354

Merged
merged 18 commits into from
Mar 2, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions docs/docs/project.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
---
layout: docs
title: Project API
permalink: project
---

# Project API

Note: The Projects API is currently available for developers to preview. During the preview period,
the API may change without advance notice. Please see the blog post for full details. To access the
API during the preview period, you must provide a custom media type in the `Accept` header:
`application/vnd.github.inertia-preview+json`

Github4s supports the [Project API](https://developer.github.com/v3/projects/). As a result,
with Github4s, you can interact with:

- [Project](#project)
- [List projects](#list-project)
- [Columns](#columns)
- [List project columns](#list-projects-columns)

The following examples assume the following imports and token:

```scala mdoc:silent
import github4s.Github
import github4s.GithubIOSyntax._
import cats.effect.IO
import scala.concurrent.ExecutionContext.Implicits.global

implicit val IOContextShift = IO.contextShift(global)
val accessToken = sys.env.get("GITHUB4S_ACCESS_TOKEN")
```

They also make use of `cats.effect.IO`, but any type container `F` implementing `ConcurrentEffect` will do.

LiftIO syntax for `cats.Id` and `Future` are provided in `GithubIOSyntax`.

## Project

### List projects

You can list the project for a particular organization with `listProjects`; it takes as arguments:

- `org`: name of the organization for which we want to retrieve the projects.
- `state`: filter projects returned by their state. Can be either `open`, `closed`, `all`. Default: `open`, optional
- `pagination`: Limit and Offset for pagination, optional.
- `header`: headers to include in the request, optional.

To list the projects for organization `47deg`:

```scala mdoc:compile-only
val listProjects = Github[IO](accessToken).projects.listProjects(
org = "47deg",
headers = Map("Accept" -> "application/vnd.github.inertia-preview+json"))
listProjects.unsafeRunSync() match {
case Left(e) => println(s"Something went wrong: ${e.getMessage}")
case Right(r) => println(r.result)
}
```

The `result` on the right is the corresponding [List[Project]][project-scala].

See [the API doc](https://developer.github.com/v3/projects/#list-organization-projects) for full reference.

[project-scala]: https://github.com/47deg/github4s/blob/master/github4s/src/main/scala/github4s/domain/Project.scala

### Columns

#### List project columns

You can list the columns for a particular project with `listColumns`; it takes as arguments:

- `project_id`: project id for which we want to retrieve the columns.
- `pagination`: Limit and Offset for pagination, optional.
- `header`: headers to include in the request, optional.

To list the columns for project_id `1910444`:

```scala mdoc:compile-only
val listColumns = Github[IO](accessToken).projects.listColumns(
project_id = 1910444,
headers = Map("Accept" -> "application/vnd.github.inertia-preview+json"))
listColumns.unsafeRunSync match {
case Left(e) => println(s"Something went wrong: ${e.getMessage}")
case Right(r) => println(r.result)
}
```

The `result` on the right is the corresponding [List[Column]][column-scala].

See [the API doc](https://developer.github.com/v3/projects/columns/#list-project-columns) for full reference.

[column-scala]: https://github.com/47deg/github4s/blob/master/github4s/src/main/scala/github4s/domain/Column.scala
3 changes: 3 additions & 0 deletions docs/src/main/resources/microsite/data/menu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,6 @@ options:

- title: Team API
url: team

- title: Project API
url: project
5 changes: 4 additions & 1 deletion github4s/src/main/scala/github4s/Decoders.scala
Original file line number Diff line number Diff line change
Expand Up @@ -276,5 +276,8 @@ object Decoders {
)
)

implicit val decodeTeam: Decoder[Team] = deriveDecoder[Team]
implicit val decodeTeam: Decoder[Team] = deriveDecoder[Team]
implicit val decodeProject: Decoder[Project] = deriveDecoder[Project]
implicit val decodeColumn: Decoder[Column] = deriveDecoder[Column]

}
1 change: 1 addition & 0 deletions github4s/src/main/scala/github4s/Github.scala
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class Github[F[_]: ConcurrentEffect](accessToken: Option[String], timeout: Optio
lazy val pullRequests: PullRequests[F] = module.pullRequests
lazy val organizations: Organizations[F] = module.organizations
lazy val teams: Teams[F] = module.teams
lazy val projects: Projects[F] = module.projects
}

object Github {
Expand Down
55 changes: 55 additions & 0 deletions github4s/src/main/scala/github4s/algebras/Projects.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright 2016-2020 47 Degrees, LLC. <http://www.47deg.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package github4s.algebras

import github4s.GithubResponses.GHResponse
import github4s.domain._

trait Projects[F[_]] {

/**
* List the projects belonging to a specific organization
*
* @param org Organization for which we want to retrieve the projects
* @param state Filter projects returned by their state. Can be either `open`, `closed`, `all`.
* Default: `open`
* @param pagination Limit and Offset for pagination
* @param headers Optional user headers to include in the request
* @return GHResponse with the list of projects belonging to this organization
*/
def listProjects(
org: String,
state: Option[String] = None,
pagination: Option[Pagination] = None,
headers: Map[String, String] = Map()
): F[GHResponse[List[Project]]]

/**
* List the columns belonging to a specific project id
*
* @param project_id Project id for which we want to retrieve the columns
* @param pagination Limit and Offset for pagination
* @param headers Optional user headers to include in the request
* @return GHResponse with the list of columns belonging to this project id
*/
def listColumns(
project_id: Int,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
project_id: Int,
projectId: Int,

wdyt?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my opinion, I think we should leave it at that and to have the same structure in the whole project.
For instance:
client_id
commit_id

pagination: Option[Pagination] = None,
headers: Map[String, String] = Map()
): F[GHResponse[List[Column]]]

}
66 changes: 66 additions & 0 deletions github4s/src/main/scala/github4s/domain/Project.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Copyright 2016-2020 47 Degrees, LLC. <http://www.47deg.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package github4s.domain

final case class Project(
owner_url: String,
url: String,
html_url: String,
columns_url: String,
id: Int,
node_id: String,
name: String,
body: Option[String],
number: Int,
creator: Creator,
created_at: String,
updated_at: String,
organization_permission: Option[String],
`private`: Option[Boolean]
)

final case class Creator(
login: String,
id: Int,
node_id: String,
avatar_url: String,
gravatar_id: Option[String],
url: String,
html_url: String,
followers_url: String,
following_url: String,
gists_url: String,
starred_url: String,
subscriptions_url: String,
organizations_url: String,
repos_url: String,
events_url: String,
received_events_url: String,
`type`: String,
site_admin: Boolean
)

final case class Column(
url: String,
project_url: String,
cards_url: String,
id: Int,
node_id: String,
name: String,
created_at: String,
updated_at: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright 2016-2020 47 Degrees, LLC. <http://www.47deg.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package github4s.interpreters

import cats.Applicative
import github4s.GithubResponses.GHResponse
import github4s.algebras.Projects
import github4s.domain.{Column, Pagination, Project}
import github4s.http.HttpClient
import github4s.Decoders._

class ProjectsInterpreter[F[_]](
implicit client: HttpClient[F],
accessToken: Option[String]
) extends Projects[F] {

override def listProjects(
org: String,
state: Option[String],
pagination: Option[Pagination] = None,
headers: Map[String, String] = Map()
): F[GHResponse[List[Project]]] =
client.get[List[Project]](
accessToken,
s"orgs/$org/projects",
headers,
state.fold(Map.empty[String, String])(s => Map("state" -> s)),
pagination
)

override def listColumns(
project_id: Int,
pagination: Option[Pagination],
headers: Map[String, String]
): F[GHResponse[List[Column]]] = client.get[List[Column]](
accessToken,
s"projects/$project_id/columns",
headers,
Map(),
pagination
)
}
4 changes: 3 additions & 1 deletion github4s/src/main/scala/github4s/modules/GithubAPIs.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ package github4s.modules

import cats.effect.ConcurrentEffect
import github4s.algebras._
import github4s.interpreters._
import github4s.http.HttpClient
import github4s.interpreters._

import scala.concurrent.ExecutionContext
import scala.concurrent.duration.Duration
Expand All @@ -35,6 +35,7 @@ sealed trait GithubAPIs[F[_]] {
def pullRequests: PullRequests[F]
def organizations: Organizations[F]
def teams: Teams[F]
def projects: Projects[F]
}

class GithubAPIv3[F[_]: ConcurrentEffect](accessToken: Option[String] = None, timeout: Duration)(
Expand All @@ -54,5 +55,6 @@ class GithubAPIv3[F[_]: ConcurrentEffect](accessToken: Option[String] = None, ti
override val pullRequests: PullRequests[F] = new PullRequestsInterpreter[F]
override val organizations: Organizations[F] = new OrganizationsInterpreter[F]
override val teams: Teams[F] = new TeamsInterpreter[F]
override val projects: Projects[F] = new ProjectsInterpreter[F]

}
68 changes: 68 additions & 0 deletions github4s/src/test/scala/github4s/integration/GHProjectsSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* Copyright 2016-2020 47 Degrees, LLC. <http://www.47deg.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package github4s.integration

import cats.effect.IO
import github4s.Github
import github4s.domain.{Column, Project}
import github4s.utils.{BaseIntegrationSpec, Integration}

trait GHProjectsSpec extends BaseIntegrationSpec {

"Project >> ListProjects" should "return the expected projects when a valid org is provided" taggedAs Integration in {
val response =
Github[IO](accessToken).projects
.listProjects(validRepoOwner, headers = headerUserAgent ++ headerAccept)
.unsafeRunSync()

testIsRight[List[Project]](response, { r =>
r.result.nonEmpty shouldBe true
r.statusCode shouldBe okStatusCode
})
}

it should "return error when an invalid org is passed" taggedAs Integration in {
val response =
Github[IO](accessToken).projects
.listProjects(invalidRepoName, headers = headerUserAgent ++ headerAccept)
.unsafeRunSync()

testIsLeft(response)
}

"Project >> ListColumns" should "return the expected column when a valid project id is provided" taggedAs Integration in {
val response =
Github[IO](accessToken).projects
.listColumns(validProjectId, headers = headerUserAgent ++ headerAccept)
.unsafeRunSync()

testIsRight[List[Column]](response, { r =>
r.result.nonEmpty shouldBe true
r.statusCode shouldBe okStatusCode
})
}

it should "return error when an invalid project id is passed" taggedAs Integration in {
val response =
Github[IO](accessToken).projects
.listColumns(invalidProjectId, headers = headerUserAgent ++ headerAccept)
.unsafeRunSync()

testIsLeft(response)
}

}
Loading