Skip to content

Commit

Permalink
feat: add membership APIs to manage team members (#137)
Browse files Browse the repository at this point in the history
* feat: create a membership client

* feat: refactor into teamclient

* feat: add ability to list team members

* docs: update readme to address nested APIs

* feat: add update membership functionality

* feat: add delete membership functionality

* feat: add list pending team invitations logic

* chore: cleanup

* feat: cleanup

* docs: update wording in readme

* fix: naming

* fix: update wording

* chore: formatting

* feat: add test to test team client creation
  • Loading branch information
ebk45 authored Jul 13, 2023
1 parent 4f3214d commit 7a2276a
Show file tree
Hide file tree
Showing 14 changed files with 707 additions and 135 deletions.
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,17 +67,24 @@ final RepositoryClient repositoryClient = githubClient.createRepositoryClient("m
log.info(repositoryClient.getCommit("sha").get().htmlUrl());
```

Some APIs, such as Checks API are nested in the Repository API. Endpoints such as `POST /repos/:owner/:repo/check-runs` live in the ChecksClient:

Another example of the mirrored structure is that some of the APIs are nested under a parent API.
For example, endpoints related to check runs or issues are nested under the Repository client:
```java
final ChecksClient checksClient = repositoryClient.createChecksApiClient();
checksClient.createCheckRun(CHECK_RUN_REQUEST);

final IssueClient issueClient = repositoryClient.createIssueClient();
issueClient.createComment(ISSUE_ID, "comment body")
.thenAccept(comment -> log.info("created comment " + comment.htmlUrl()));

```

And endpoints related to teams and memberships are nested under the Organisation client:
```java
final TeamClient teamClient = organisationClient.createTeamClient();
teamClient.getMembership("username");
```

## Contributing

This project uses Maven. To run the tests locally, just run:
Expand Down
8 changes: 8 additions & 0 deletions src/main/java/com/spotify/github/v3/clients/GitHubClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,13 @@
import com.spotify.github.Tracer;
import com.spotify.github.jackson.Json;
import com.spotify.github.v3.Team;
import com.spotify.github.v3.User;
import com.spotify.github.v3.checks.AccessToken;
import com.spotify.github.v3.comment.Comment;
import com.spotify.github.v3.exceptions.ReadOnlyRepositoryException;
import com.spotify.github.v3.exceptions.RequestNotOkException;
import com.spotify.github.v3.git.Reference;
import com.spotify.github.v3.orgs.TeamInvitation;
import com.spotify.github.v3.prs.PullRequestItem;
import com.spotify.github.v3.prs.Review;
import com.spotify.github.v3.prs.ReviewRequests;
Expand Down Expand Up @@ -101,6 +103,12 @@ public class GitHubClient {
static final TypeReference<List<Team>> LIST_TEAMS =
new TypeReference<>() {};

static final TypeReference<List<User>> LIST_TEAM_MEMBERS =
new TypeReference<>() {};

static final TypeReference<List<TeamInvitation>> LIST_PENDING_TEAM_INVITATIONS =
new TypeReference<>() {};

private static final String GET_ACCESS_TOKEN_URL = "app/installations/%s/access_tokens";

private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,7 @@
//
package com.spotify.github.v3.clients;

import static com.spotify.github.v3.clients.GitHubClient.IGNORE_RESPONSE_CONSUMER;
import static com.spotify.github.v3.clients.GitHubClient.LIST_TEAMS;

import com.spotify.github.v3.Team;
import com.spotify.github.v3.orgs.requests.TeamCreate;
import java.lang.invoke.MethodHandles;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -53,62 +46,11 @@ static OrganisationClient create(final GitHubClient github, final String org) {
}

/**
* Create a team in an organisation.
*
* @param request create team request
* @return team
*/
public CompletableFuture<Team> createTeam(final TeamCreate request) {
final String path = String.format(TEAM_TEMPLATE, org);
log.debug("Creating team in: " + path);
return github.post(path, github.json().toJsonUnchecked(request), Team.class);
}

/**
* Get a specific team in an organisation.
*
* @param slug slug of the team name
* @return team
*/
public CompletableFuture<Team> getTeam(final String slug) {
final String path = String.format(TEAM_SLUG_TEMPLATE, org, slug);
log.debug("Fetching team from " + path);
return github.request(path, Team.class);
}

/**
* List teams within an organisation.
*
* @return list of all teams in an organisation
*/
public CompletableFuture<List<Team>> listTeams() {
final String path = String.format(TEAM_TEMPLATE, org);
log.debug("Fetching teams from " + path);
return github.request(path, LIST_TEAMS);
}

/**
* Update a team in an organisation.
*
* @param request update team request
* @param slug slug of the team name
* @return team
*/
public CompletableFuture<Team> updateTeam(final TeamCreate request, final String slug) {
final String path = String.format(TEAM_SLUG_TEMPLATE, org, slug);
log.debug("Updating team in: " + path);
return github.patch(path, github.json().toJsonUnchecked(request), Team.class);
}

/**
* Delete a specific team in an organisation.
* Create a Teams API client.
*
* @param slug slug of the team name
* @return team
* @return Teams API client
*/
public CompletableFuture<Void> deleteTeam(final String slug) {
final String path = String.format(TEAM_SLUG_TEMPLATE, org, slug);
log.debug("Deleting team from: " + path);
return github.delete(path).thenAccept(IGNORE_RESPONSE_CONSUMER);
public TeamClient createTeamClient(final GitHubClient github, final String org) {
return TeamClient.create(github, org);
}
}
188 changes: 188 additions & 0 deletions src/main/java/com/spotify/github/v3/clients/TeamClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
/*-
* -\-\-
* github-api
* --
* Copyright (C) 2016 - 2020 Spotify AB
* --
* 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 com.spotify.github.v3.clients;

import static com.spotify.github.v3.clients.GitHubClient.IGNORE_RESPONSE_CONSUMER;
import static com.spotify.github.v3.clients.GitHubClient.LIST_PENDING_TEAM_INVITATIONS;
import static com.spotify.github.v3.clients.GitHubClient.LIST_TEAMS;
import static com.spotify.github.v3.clients.GitHubClient.LIST_TEAM_MEMBERS;

import com.spotify.github.v3.Team;
import com.spotify.github.v3.User;
import com.spotify.github.v3.orgs.Membership;
import com.spotify.github.v3.orgs.TeamInvitation;
import com.spotify.github.v3.orgs.requests.MembershipCreate;
import com.spotify.github.v3.orgs.requests.TeamCreate;
import java.lang.invoke.MethodHandles;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class TeamClient {

private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

private static final String TEAM_TEMPLATE = "/orgs/%s/teams";

private static final String TEAM_SLUG_TEMPLATE = "/orgs/%s/teams/%s";

private static final String MEMBERS_TEMPLATE = "/orgs/%s/teams/%s/members";

private static final String MEMBERSHIP_TEMPLATE = "/orgs/%s/teams/%s/memberships/%s";

private static final String INVITATIONS_TEMPLATE = "/orgs/%s/teams/%s/invitations";

private final GitHubClient github;

private final String org;

TeamClient(final GitHubClient github, final String org) {
this.github = github;
this.org = org;
}

static TeamClient create(final GitHubClient github, final String org) {
return new TeamClient(github, org);
}

/**
* Create a team in an organisation.
*
* @param request create team request
* @return team
*/
public CompletableFuture<Team> createTeam(final TeamCreate request) {
final String path = String.format(TEAM_TEMPLATE, org);
log.debug("Creating team in: " + path);
return github.post(path, github.json().toJsonUnchecked(request), Team.class);
}

/**
* Get a specific team in an organisation.
*
* @param slug slug of the team name
* @return team
*/
public CompletableFuture<Team> getTeam(final String slug) {
final String path = String.format(TEAM_SLUG_TEMPLATE, org, slug);
log.debug("Fetching team from " + path);
return github.request(path, Team.class);
}

/**
* List teams within an organisation.
*
* @return list of all teams in an organisation
*/
public CompletableFuture<List<Team>> listTeams() {
final String path = String.format(TEAM_TEMPLATE, org);
log.debug("Fetching teams from " + path);
return github.request(path, LIST_TEAMS);
}

/**
* Update a team in an organisation.
*
* @param request update team request
* @param slug slug of the team name
* @return team
*/
public CompletableFuture<Team> updateTeam(final TeamCreate request, final String slug) {
final String path = String.format(TEAM_SLUG_TEMPLATE, org, slug);
log.debug("Updating team in: " + path);
return github.patch(path, github.json().toJsonUnchecked(request), Team.class);
}

/**
* Delete a specific team in an organisation.
*
* @param slug slug of the team name
* @return team
*/
public CompletableFuture<Void> deleteTeam(final String slug) {
final String path = String.format(TEAM_SLUG_TEMPLATE, org, slug);
log.debug("Deleting team from: " + path);
return github.delete(path).thenAccept(IGNORE_RESPONSE_CONSUMER);
}

/**
* Add or update a team membership for a user.
*
* @param request update membership request
* @return membership
*/
public CompletableFuture<Membership> updateMembership(final MembershipCreate request, final String slug, final String username) {
final String path = String.format(MEMBERSHIP_TEMPLATE, org, slug, username);
log.debug("Updating membership in: " + path);
return github.put(path, github.json().toJsonUnchecked(request), Membership.class);
}

/**
* Get a team membership of a user.
*
* @param slug the team slug
* @param username username of the team member
* @return membership
*/
public CompletableFuture<Membership> getMembership(final String slug, final String username) {
final String path = String.format(MEMBERSHIP_TEMPLATE, org, slug, username);
log.debug("Fetching membership for: " + path);
return github.request(path, Membership.class);
}

/**
* List members of a specific team.
*
* @param slug the team slug
* @return list of all users in a team
*/
public CompletableFuture<List<User>> listTeamMembers(final String slug) {
final String path = String.format(MEMBERS_TEMPLATE, org, slug);
log.debug("Fetching members for: " + path);
return github.request(path, LIST_TEAM_MEMBERS);
}

/**
* Delete a membership for a user.
*
* @param slug slug of the team name
* @return membership
*/
public CompletableFuture<Void> deleteMembership(final String slug, final String username) {
final String path = String.format(MEMBERSHIP_TEMPLATE, org, slug, username);
log.debug("Deleting membership from: " + path);
return github.delete(path).thenAccept(IGNORE_RESPONSE_CONSUMER);
}

/**
* List pending invitations for a team.
*
* @param slug the team slug
* @return list of pending invitations for a team
*/
public CompletableFuture<List<TeamInvitation>> listPendingTeamInvitations(final String slug) {
final String path = String.format(INVITATIONS_TEMPLATE, org, slug);
log.debug("Fetching pending invitations for: " + path);
return github.request(path, LIST_PENDING_TEAM_INVITATIONS);
}
}
50 changes: 50 additions & 0 deletions src/main/java/com/spotify/github/v3/orgs/Membership.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*-
* -\-\-
* github-api
* --
* Copyright (C) 2016 - 2020 Spotify AB
* --
* 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 com.spotify.github.v3.orgs;

import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.spotify.github.GithubStyle;
import java.net.URI;
import javax.annotation.Nullable;
import org.immutables.value.Value;

/**
* Membership resource represents data returned by a single Membership get operation.
*/
@Value.Immutable
@GithubStyle
@JsonSerialize(as = ImmutableMembership.class)
@JsonDeserialize(as = ImmutableMembership.class)
public interface Membership {

/** URL */
@Nullable
URI url();

/** ROLE */
@Nullable
String role();

/** STATE */
@Nullable
String state();
}
Loading

0 comments on commit 7a2276a

Please sign in to comment.