Skip to content

Commit f8ea0a0

Browse files
authored
feat: add a github client (#2747)
In this PR: - Add a github client to retrieve pull request status from a repository. - Add unit test.
1 parent 2170bc0 commit f8ea0a0

File tree

8 files changed

+251
-3
lines changed

8 files changed

+251
-3
lines changed

java-shared-dependencies/dependency-analyzer/pom.xml

+12
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,18 @@
3838
</arguments>
3939
</configuration>
4040
</plugin>
41+
<plugin>
42+
<groupId>org.apache.maven.plugins</groupId>
43+
<artifactId>maven-surefire-plugin</artifactId>
44+
<version>3.2.5</version>
45+
<configuration>
46+
<environmentVariables>
47+
<!--this environment variable is used to set token when construct
48+
a mock http request in unit test-->
49+
<GITHUB_TOKEN>fake_value</GITHUB_TOKEN>
50+
</environmentVariables>
51+
</configuration>
52+
</plugin>
4153
</plugins>
4254
</build>
4355

java-shared-dependencies/dependency-analyzer/src/main/java/com/google/cloud/external/DepsDevClient.java

+12-3
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,24 @@
2626
import java.util.List;
2727
import java.util.stream.Collectors;
2828

29+
/**
30+
* DepsDevClient is a class that sends HTTP requests to the Deps.dev RESTful API.
31+
*
32+
* <p>This class simplifies the process of making API calls by handling authentication, request
33+
* construction, and response parsing. It uses the {@link java.net.http.HttpClient} for sending
34+
* requests and {@link com.google.gson.Gson} for handling JSON serialization/deserialization.
35+
*/
2936
public class DepsDevClient {
3037

3138
private final HttpClient client;
32-
public final Gson gson;
39+
private final Gson gson;
3340
private final static String ADVISORY_URL_BASE = "https://api.deps.dev/v3/advisories/%s";
3441

35-
private final static String DEPENDENCY_URLBASE = "https://api.deps.dev/v3/systems/%s/packages/%s/versions/%s:dependencies";
42+
private final static String DEPENDENCY_URLBASE =
43+
"https://api.deps.dev/v3/systems/%s/packages/%s/versions/%s:dependencies";
3644

37-
public final static String QUERY_URL_BASE = "https://api.deps.dev/v3/query?versionKey.system=%s&versionKey.name=%s&versionKey.version=%s";
45+
public final static String QUERY_URL_BASE =
46+
"https://api.deps.dev/v3/query?versionKey.system=%s&versionKey.name=%s&versionKey.version=%s";
3847

3948
public DepsDevClient(HttpClient client) {
4049
this.client = client;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
package com.google.cloud.external;
2+
3+
import com.google.cloud.model.Interval;
4+
import com.google.cloud.model.PullRequest;
5+
import com.google.cloud.model.PullRequestStatistics;
6+
import com.google.gson.Gson;
7+
import com.google.gson.GsonBuilder;
8+
import com.google.gson.reflect.TypeToken;
9+
import java.io.IOException;
10+
import java.net.URI;
11+
import java.net.URISyntaxException;
12+
import java.net.http.HttpClient;
13+
import java.net.http.HttpRequest;
14+
import java.net.http.HttpResponse;
15+
import java.net.http.HttpResponse.BodyHandlers;
16+
import java.time.Instant;
17+
import java.time.ZoneId;
18+
import java.time.ZonedDateTime;
19+
import java.util.ArrayList;
20+
import java.util.List;
21+
import java.util.Objects;
22+
23+
/**
24+
* GitHubClient is a class that sends HTTP requests to the GitHub RESTful API. It provides methods
25+
* for interacting with various GitHub resources such as repositories, issues, users, etc.
26+
*
27+
* <p>This class simplifies the process of making API calls by handling authentication, request
28+
* construction, and response parsing. It uses the {@link java.net.http.HttpClient} for sending
29+
* requests and {@link com.google.gson.Gson} for handling JSON serialization/deserialization.
30+
*/
31+
public class GitHubClient {
32+
private final HttpClient client;
33+
private final Gson gson;
34+
private static final String PULL_REQUESTS_BASE =
35+
"https://api.github.com/repos/%s/%s/pulls?state=all&per_page=100&page=%s";
36+
private static final int MAX_PULL_REQUEST_NUM = 1000;
37+
private static final String OPEN_STATE = "open";
38+
39+
public GitHubClient(HttpClient client) {
40+
this.client = client;
41+
this.gson = new GsonBuilder().create();
42+
}
43+
44+
public PullRequestStatistics listMonthlyPullRequestStatusOf(String organization, String repo)
45+
throws URISyntaxException, IOException, InterruptedException {
46+
return listPullRequestStatus(organization, repo, Interval.MONTHLY);
47+
}
48+
49+
private PullRequestStatistics listPullRequestStatus(
50+
String organization, String repo, Interval interval)
51+
throws URISyntaxException, IOException, InterruptedException {
52+
List<PullRequest> pullRequests = listPullRequests(organization, repo);
53+
ZonedDateTime now = ZonedDateTime.now();
54+
long created =
55+
pullRequests.stream()
56+
.distinct()
57+
.filter(pullRequest -> pullRequest.state().equals(OPEN_STATE))
58+
.filter(
59+
pullRequest -> {
60+
ZonedDateTime createdAt = utcTimeFrom(pullRequest.createdAt());
61+
return now.minusDays(interval.getDays()).isBefore(createdAt);
62+
})
63+
.count();
64+
65+
long merged =
66+
pullRequests.stream()
67+
.distinct()
68+
.filter(pullRequest -> Objects.nonNull(pullRequest.mergedAt()))
69+
.filter(
70+
pullRequest -> {
71+
ZonedDateTime createdAt = utcTimeFrom(pullRequest.mergedAt());
72+
return now.minusDays(interval.getDays()).isBefore(createdAt);
73+
})
74+
.count();
75+
76+
return new PullRequestStatistics(created, merged, interval);
77+
}
78+
79+
private List<PullRequest> listPullRequests(String organization, String repo)
80+
throws URISyntaxException, IOException, InterruptedException {
81+
List<PullRequest> pullRequests = new ArrayList<>();
82+
int page = 1;
83+
while (pullRequests.size() < MAX_PULL_REQUEST_NUM) {
84+
HttpResponse<String> response = getResponse(getPullRequestsUrl(organization, repo, page));
85+
pullRequests.addAll(
86+
gson.fromJson(response.body(), new TypeToken<List<PullRequest>>() {}.getType()));
87+
page++;
88+
}
89+
90+
return pullRequests;
91+
}
92+
93+
private String getPullRequestsUrl(String organization, String repo, int page) {
94+
return String.format(PULL_REQUESTS_BASE, organization, repo, page);
95+
}
96+
97+
private ZonedDateTime utcTimeFrom(String time) {
98+
ZoneId zoneIdUTC = ZoneId.of("UTC");
99+
Instant instant = Instant.parse(time);
100+
return instant.atZone(zoneIdUTC);
101+
}
102+
103+
private HttpResponse<String> getResponse(String endpoint)
104+
throws URISyntaxException, IOException, InterruptedException {
105+
HttpRequest request =
106+
HttpRequest.newBuilder()
107+
.header("Authorization", System.getenv("GITHUB_TOKEN"))
108+
.uri(new URI(endpoint))
109+
.GET()
110+
.build();
111+
return client.send(request, BodyHandlers.ofString());
112+
}
113+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.google.cloud.model;
2+
3+
public enum Interval {
4+
WEEKLY(7),
5+
MONTHLY(30);
6+
7+
private final int days;
8+
9+
Interval(int days) {
10+
this.days = days;
11+
}
12+
13+
public int getDays() {
14+
return days;
15+
}
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.google.cloud.model;
2+
3+
import com.google.gson.annotations.SerializedName;
4+
5+
/**
6+
* A record that represents a GitHub pull request.
7+
*
8+
* @param url The url of the pull request.
9+
* @param state The state of the pull request, e.g., open, merged.
10+
* @param createdAt The creation time of the pull request.
11+
* @param mergedAt The merged time of the pull request; null if not merged.
12+
*/
13+
public record PullRequest(
14+
String url,
15+
String state,
16+
@SerializedName("created_at") String createdAt,
17+
@SerializedName("merged_at") String mergedAt) {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.google.cloud.model;
2+
3+
/**
4+
* A record that represents statistics about pull requests within a specified time interval.
5+
*
6+
* <p>The pull request statistics is used to show pull request freshness in the package information
7+
* report.
8+
*
9+
* <p>For example, x pull requests are created and y pull requests are merged in the last 30 days.
10+
*
11+
* @param created The number of pull requests created within the interval.
12+
* @param merged The number of pull requests merged within the interval.
13+
* @param interval The time interval over which the statistics were collected.
14+
*/
15+
public record PullRequestStatistics(long created, long merged, Interval interval) {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package com.google.cloud.external;
2+
3+
import static org.junit.Assert.assertEquals;
4+
import static org.mockito.ArgumentMatchers.any;
5+
import static org.mockito.Mockito.mock;
6+
import static org.mockito.Mockito.when;
7+
8+
import com.google.cloud.model.Interval;
9+
import com.google.cloud.model.PullRequestStatistics;
10+
import java.io.IOException;
11+
import java.net.URISyntaxException;
12+
import java.net.http.HttpClient;
13+
import java.net.http.HttpRequest;
14+
import java.net.http.HttpResponse;
15+
import java.net.http.HttpResponse.BodyHandler;
16+
import java.nio.file.Files;
17+
import java.nio.file.Path;
18+
import java.time.Instant;
19+
import java.time.ZoneId;
20+
import java.time.ZonedDateTime;
21+
import org.junit.Before;
22+
import org.junit.Test;
23+
import org.mockito.MockedStatic;
24+
import org.mockito.Mockito;
25+
26+
public class GitHubClientTest {
27+
28+
private HttpResponse<String> response;
29+
private GitHubClient client;
30+
31+
@Before
32+
public void setUp() throws IOException, InterruptedException {
33+
HttpClient httpClient = mock(HttpClient.class);
34+
client = new GitHubClient(httpClient);
35+
response = mock(HttpResponse.class);
36+
when(httpClient.send(any(HttpRequest.class), any(BodyHandler.class))).thenReturn(response);
37+
}
38+
39+
@Test
40+
public void testListMonthlyPullRequestStatusSucceeds()
41+
throws URISyntaxException, IOException, InterruptedException {
42+
ZonedDateTime fixedNow = ZonedDateTime.parse("2024-05-22T09:33:52Z");
43+
ZonedDateTime lastMonth = ZonedDateTime.parse("2024-04-22T09:33:52Z");
44+
Instant prInstant = Instant.parse("2024-05-10T09:33:52Z");
45+
ZonedDateTime prTime = ZonedDateTime.parse("2024-05-10T09:33:52Z");
46+
String responseBody =
47+
Files.readString(Path.of("src/test/resources/pull_request_sample_response.txt"));
48+
49+
try (MockedStatic<ZonedDateTime> mockedLocalDateTime = Mockito.mockStatic(ZonedDateTime.class);
50+
MockedStatic<Instant> mockedInstant = Mockito.mockStatic(Instant.class)) {
51+
mockedLocalDateTime.when(ZonedDateTime::now).thenReturn(fixedNow);
52+
mockedInstant.when(() -> Instant.parse(Mockito.anyString())).thenReturn(prInstant);
53+
when(fixedNow.minusDays(30)).thenReturn(lastMonth);
54+
when(prInstant.atZone(ZoneId.of("UTC"))).thenReturn(prTime);
55+
when(response.body()).thenReturn(responseBody);
56+
String org = "";
57+
String repo = "";
58+
PullRequestStatistics status = client.listMonthlyPullRequestStatusOf(org, repo);
59+
60+
assertEquals(Interval.MONTHLY, status.interval());
61+
assertEquals(3, status.created());
62+
assertEquals(7, status.merged());
63+
}
64+
}
65+
}

java-shared-dependencies/dependency-analyzer/src/test/resources/pull_request_sample_response.txt

+1
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)