Skip to content

Commit 7a2276a

Browse files
authored
feat: add membership APIs to manage team members (#137)
* 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
1 parent 4f3214d commit 7a2276a

File tree

14 files changed

+707
-135
lines changed

14 files changed

+707
-135
lines changed

README.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,17 +67,24 @@ final RepositoryClient repositoryClient = githubClient.createRepositoryClient("m
6767
log.info(repositoryClient.getCommit("sha").get().htmlUrl());
6868
```
6969

70-
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:
71-
70+
Another example of the mirrored structure is that some of the APIs are nested under a parent API.
71+
For example, endpoints related to check runs or issues are nested under the Repository client:
7272
```java
7373
final ChecksClient checksClient = repositoryClient.createChecksApiClient();
7474
checksClient.createCheckRun(CHECK_RUN_REQUEST);
7575

7676
final IssueClient issueClient = repositoryClient.createIssueClient();
7777
issueClient.createComment(ISSUE_ID, "comment body")
7878
.thenAccept(comment -> log.info("created comment " + comment.htmlUrl()));
79+
7980
```
8081

82+
And endpoints related to teams and memberships are nested under the Organisation client:
83+
```java
84+
final TeamClient teamClient = organisationClient.createTeamClient();
85+
teamClient.getMembership("username");
86+
```
87+
8188
## Contributing
8289

8390
This project uses Maven. To run the tests locally, just run:

src/main/java/com/spotify/github/v3/clients/GitHubClient.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,13 @@
2727
import com.spotify.github.Tracer;
2828
import com.spotify.github.jackson.Json;
2929
import com.spotify.github.v3.Team;
30+
import com.spotify.github.v3.User;
3031
import com.spotify.github.v3.checks.AccessToken;
3132
import com.spotify.github.v3.comment.Comment;
3233
import com.spotify.github.v3.exceptions.ReadOnlyRepositoryException;
3334
import com.spotify.github.v3.exceptions.RequestNotOkException;
3435
import com.spotify.github.v3.git.Reference;
36+
import com.spotify.github.v3.orgs.TeamInvitation;
3537
import com.spotify.github.v3.prs.PullRequestItem;
3638
import com.spotify.github.v3.prs.Review;
3739
import com.spotify.github.v3.prs.ReviewRequests;
@@ -101,6 +103,12 @@ public class GitHubClient {
101103
static final TypeReference<List<Team>> LIST_TEAMS =
102104
new TypeReference<>() {};
103105

106+
static final TypeReference<List<User>> LIST_TEAM_MEMBERS =
107+
new TypeReference<>() {};
108+
109+
static final TypeReference<List<TeamInvitation>> LIST_PENDING_TEAM_INVITATIONS =
110+
new TypeReference<>() {};
111+
104112
private static final String GET_ACCESS_TOKEN_URL = "app/installations/%s/access_tokens";
105113

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

src/main/java/com/spotify/github/v3/clients/OrganisationClient.java

Lines changed: 4 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,7 @@
2020
//
2121
package com.spotify.github.v3.clients;
2222

23-
import static com.spotify.github.v3.clients.GitHubClient.IGNORE_RESPONSE_CONSUMER;
24-
import static com.spotify.github.v3.clients.GitHubClient.LIST_TEAMS;
25-
26-
import com.spotify.github.v3.Team;
27-
import com.spotify.github.v3.orgs.requests.TeamCreate;
2823
import java.lang.invoke.MethodHandles;
29-
import java.util.List;
30-
import java.util.concurrent.CompletableFuture;
3124
import org.slf4j.Logger;
3225
import org.slf4j.LoggerFactory;
3326

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

5548
/**
56-
* Create a team in an organisation.
57-
*
58-
* @param request create team request
59-
* @return team
60-
*/
61-
public CompletableFuture<Team> createTeam(final TeamCreate request) {
62-
final String path = String.format(TEAM_TEMPLATE, org);
63-
log.debug("Creating team in: " + path);
64-
return github.post(path, github.json().toJsonUnchecked(request), Team.class);
65-
}
66-
67-
/**
68-
* Get a specific team in an organisation.
69-
*
70-
* @param slug slug of the team name
71-
* @return team
72-
*/
73-
public CompletableFuture<Team> getTeam(final String slug) {
74-
final String path = String.format(TEAM_SLUG_TEMPLATE, org, slug);
75-
log.debug("Fetching team from " + path);
76-
return github.request(path, Team.class);
77-
}
78-
79-
/**
80-
* List teams within an organisation.
81-
*
82-
* @return list of all teams in an organisation
83-
*/
84-
public CompletableFuture<List<Team>> listTeams() {
85-
final String path = String.format(TEAM_TEMPLATE, org);
86-
log.debug("Fetching teams from " + path);
87-
return github.request(path, LIST_TEAMS);
88-
}
89-
90-
/**
91-
* Update a team in an organisation.
92-
*
93-
* @param request update team request
94-
* @param slug slug of the team name
95-
* @return team
96-
*/
97-
public CompletableFuture<Team> updateTeam(final TeamCreate request, final String slug) {
98-
final String path = String.format(TEAM_SLUG_TEMPLATE, org, slug);
99-
log.debug("Updating team in: " + path);
100-
return github.patch(path, github.json().toJsonUnchecked(request), Team.class);
101-
}
102-
103-
/**
104-
* Delete a specific team in an organisation.
49+
* Create a Teams API client.
10550
*
106-
* @param slug slug of the team name
107-
* @return team
51+
* @return Teams API client
10852
*/
109-
public CompletableFuture<Void> deleteTeam(final String slug) {
110-
final String path = String.format(TEAM_SLUG_TEMPLATE, org, slug);
111-
log.debug("Deleting team from: " + path);
112-
return github.delete(path).thenAccept(IGNORE_RESPONSE_CONSUMER);
53+
public TeamClient createTeamClient(final GitHubClient github, final String org) {
54+
return TeamClient.create(github, org);
11355
}
11456
}
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
/*-
2+
* -\-\-
3+
* github-api
4+
* --
5+
* Copyright (C) 2016 - 2020 Spotify AB
6+
* --
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
* -/-/-
19+
*/
20+
21+
22+
package com.spotify.github.v3.clients;
23+
24+
import static com.spotify.github.v3.clients.GitHubClient.IGNORE_RESPONSE_CONSUMER;
25+
import static com.spotify.github.v3.clients.GitHubClient.LIST_PENDING_TEAM_INVITATIONS;
26+
import static com.spotify.github.v3.clients.GitHubClient.LIST_TEAMS;
27+
import static com.spotify.github.v3.clients.GitHubClient.LIST_TEAM_MEMBERS;
28+
29+
import com.spotify.github.v3.Team;
30+
import com.spotify.github.v3.User;
31+
import com.spotify.github.v3.orgs.Membership;
32+
import com.spotify.github.v3.orgs.TeamInvitation;
33+
import com.spotify.github.v3.orgs.requests.MembershipCreate;
34+
import com.spotify.github.v3.orgs.requests.TeamCreate;
35+
import java.lang.invoke.MethodHandles;
36+
import java.util.List;
37+
import java.util.concurrent.CompletableFuture;
38+
import org.slf4j.Logger;
39+
import org.slf4j.LoggerFactory;
40+
41+
public class TeamClient {
42+
43+
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
44+
45+
private static final String TEAM_TEMPLATE = "/orgs/%s/teams";
46+
47+
private static final String TEAM_SLUG_TEMPLATE = "/orgs/%s/teams/%s";
48+
49+
private static final String MEMBERS_TEMPLATE = "/orgs/%s/teams/%s/members";
50+
51+
private static final String MEMBERSHIP_TEMPLATE = "/orgs/%s/teams/%s/memberships/%s";
52+
53+
private static final String INVITATIONS_TEMPLATE = "/orgs/%s/teams/%s/invitations";
54+
55+
private final GitHubClient github;
56+
57+
private final String org;
58+
59+
TeamClient(final GitHubClient github, final String org) {
60+
this.github = github;
61+
this.org = org;
62+
}
63+
64+
static TeamClient create(final GitHubClient github, final String org) {
65+
return new TeamClient(github, org);
66+
}
67+
68+
/**
69+
* Create a team in an organisation.
70+
*
71+
* @param request create team request
72+
* @return team
73+
*/
74+
public CompletableFuture<Team> createTeam(final TeamCreate request) {
75+
final String path = String.format(TEAM_TEMPLATE, org);
76+
log.debug("Creating team in: " + path);
77+
return github.post(path, github.json().toJsonUnchecked(request), Team.class);
78+
}
79+
80+
/**
81+
* Get a specific team in an organisation.
82+
*
83+
* @param slug slug of the team name
84+
* @return team
85+
*/
86+
public CompletableFuture<Team> getTeam(final String slug) {
87+
final String path = String.format(TEAM_SLUG_TEMPLATE, org, slug);
88+
log.debug("Fetching team from " + path);
89+
return github.request(path, Team.class);
90+
}
91+
92+
/**
93+
* List teams within an organisation.
94+
*
95+
* @return list of all teams in an organisation
96+
*/
97+
public CompletableFuture<List<Team>> listTeams() {
98+
final String path = String.format(TEAM_TEMPLATE, org);
99+
log.debug("Fetching teams from " + path);
100+
return github.request(path, LIST_TEAMS);
101+
}
102+
103+
/**
104+
* Update a team in an organisation.
105+
*
106+
* @param request update team request
107+
* @param slug slug of the team name
108+
* @return team
109+
*/
110+
public CompletableFuture<Team> updateTeam(final TeamCreate request, final String slug) {
111+
final String path = String.format(TEAM_SLUG_TEMPLATE, org, slug);
112+
log.debug("Updating team in: " + path);
113+
return github.patch(path, github.json().toJsonUnchecked(request), Team.class);
114+
}
115+
116+
/**
117+
* Delete a specific team in an organisation.
118+
*
119+
* @param slug slug of the team name
120+
* @return team
121+
*/
122+
public CompletableFuture<Void> deleteTeam(final String slug) {
123+
final String path = String.format(TEAM_SLUG_TEMPLATE, org, slug);
124+
log.debug("Deleting team from: " + path);
125+
return github.delete(path).thenAccept(IGNORE_RESPONSE_CONSUMER);
126+
}
127+
128+
/**
129+
* Add or update a team membership for a user.
130+
*
131+
* @param request update membership request
132+
* @return membership
133+
*/
134+
public CompletableFuture<Membership> updateMembership(final MembershipCreate request, final String slug, final String username) {
135+
final String path = String.format(MEMBERSHIP_TEMPLATE, org, slug, username);
136+
log.debug("Updating membership in: " + path);
137+
return github.put(path, github.json().toJsonUnchecked(request), Membership.class);
138+
}
139+
140+
/**
141+
* Get a team membership of a user.
142+
*
143+
* @param slug the team slug
144+
* @param username username of the team member
145+
* @return membership
146+
*/
147+
public CompletableFuture<Membership> getMembership(final String slug, final String username) {
148+
final String path = String.format(MEMBERSHIP_TEMPLATE, org, slug, username);
149+
log.debug("Fetching membership for: " + path);
150+
return github.request(path, Membership.class);
151+
}
152+
153+
/**
154+
* List members of a specific team.
155+
*
156+
* @param slug the team slug
157+
* @return list of all users in a team
158+
*/
159+
public CompletableFuture<List<User>> listTeamMembers(final String slug) {
160+
final String path = String.format(MEMBERS_TEMPLATE, org, slug);
161+
log.debug("Fetching members for: " + path);
162+
return github.request(path, LIST_TEAM_MEMBERS);
163+
}
164+
165+
/**
166+
* Delete a membership for a user.
167+
*
168+
* @param slug slug of the team name
169+
* @return membership
170+
*/
171+
public CompletableFuture<Void> deleteMembership(final String slug, final String username) {
172+
final String path = String.format(MEMBERSHIP_TEMPLATE, org, slug, username);
173+
log.debug("Deleting membership from: " + path);
174+
return github.delete(path).thenAccept(IGNORE_RESPONSE_CONSUMER);
175+
}
176+
177+
/**
178+
* List pending invitations for a team.
179+
*
180+
* @param slug the team slug
181+
* @return list of pending invitations for a team
182+
*/
183+
public CompletableFuture<List<TeamInvitation>> listPendingTeamInvitations(final String slug) {
184+
final String path = String.format(INVITATIONS_TEMPLATE, org, slug);
185+
log.debug("Fetching pending invitations for: " + path);
186+
return github.request(path, LIST_PENDING_TEAM_INVITATIONS);
187+
}
188+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*-
2+
* -\-\-
3+
* github-api
4+
* --
5+
* Copyright (C) 2016 - 2020 Spotify AB
6+
* --
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
* -/-/-
19+
*/
20+
21+
package com.spotify.github.v3.orgs;
22+
23+
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
24+
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
25+
import com.spotify.github.GithubStyle;
26+
import java.net.URI;
27+
import javax.annotation.Nullable;
28+
import org.immutables.value.Value;
29+
30+
/**
31+
* Membership resource represents data returned by a single Membership get operation.
32+
*/
33+
@Value.Immutable
34+
@GithubStyle
35+
@JsonSerialize(as = ImmutableMembership.class)
36+
@JsonDeserialize(as = ImmutableMembership.class)
37+
public interface Membership {
38+
39+
/** URL */
40+
@Nullable
41+
URI url();
42+
43+
/** ROLE */
44+
@Nullable
45+
String role();
46+
47+
/** STATE */
48+
@Nullable
49+
String state();
50+
}

0 commit comments

Comments
 (0)