Skip to content

Commit

Permalink
feat: add a github client (#2747)
Browse files Browse the repository at this point in the history
In this PR:
- Add a github client to retrieve pull request status from a repository.
- Add unit test.
  • Loading branch information
JoeWang1127 authored and lqiu96 committed May 22, 2024
1 parent d63af40 commit 80cc1f7
Show file tree
Hide file tree
Showing 8 changed files with 251 additions and 3 deletions.
12 changes: 12 additions & 0 deletions java-shared-dependencies/dependency-analyzer/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,18 @@
</arguments>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
<configuration>
<environmentVariables>
<!--this environment variable is used to set token when construct
a mock http request in unit test-->
<GITHUB_TOKEN>fake_value</GITHUB_TOKEN>
</environmentVariables>
</configuration>
</plugin>
</plugins>
</build>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,24 @@
import java.util.List;
import java.util.stream.Collectors;

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

private final HttpClient client;
public final Gson gson;
private final Gson gson;
private final static String ADVISORY_URL_BASE = "https://api.deps.dev/v3/advisories/%s";

private final static String DEPENDENCY_URLBASE = "https://api.deps.dev/v3/systems/%s/packages/%s/versions/%s:dependencies";
private final static String DEPENDENCY_URLBASE =
"https://api.deps.dev/v3/systems/%s/packages/%s/versions/%s:dependencies";

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

public DepsDevClient(HttpClient client) {
this.client = client;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package com.google.cloud.external;

import com.google.cloud.model.Interval;
import com.google.cloud.model.PullRequest;
import com.google.cloud.model.PullRequestStatistics;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
* GitHubClient is a class that sends HTTP requests to the GitHub RESTful API. It provides methods
* for interacting with various GitHub resources such as repositories, issues, users, etc.
*
* <p>This class simplifies the process of making API calls by handling authentication, request
* construction, and response parsing. It uses the {@link java.net.http.HttpClient} for sending
* requests and {@link com.google.gson.Gson} for handling JSON serialization/deserialization.
*/
public class GitHubClient {
private final HttpClient client;
private final Gson gson;
private static final String PULL_REQUESTS_BASE =
"https://api.github.com/repos/%s/%s/pulls?state=all&per_page=100&page=%s";
private static final int MAX_PULL_REQUEST_NUM = 1000;
private static final String OPEN_STATE = "open";

public GitHubClient(HttpClient client) {
this.client = client;
this.gson = new GsonBuilder().create();
}

public PullRequestStatistics listMonthlyPullRequestStatusOf(String organization, String repo)
throws URISyntaxException, IOException, InterruptedException {
return listPullRequestStatus(organization, repo, Interval.MONTHLY);
}

private PullRequestStatistics listPullRequestStatus(
String organization, String repo, Interval interval)
throws URISyntaxException, IOException, InterruptedException {
List<PullRequest> pullRequests = listPullRequests(organization, repo);
ZonedDateTime now = ZonedDateTime.now();
long created =
pullRequests.stream()
.distinct()
.filter(pullRequest -> pullRequest.state().equals(OPEN_STATE))
.filter(
pullRequest -> {
ZonedDateTime createdAt = utcTimeFrom(pullRequest.createdAt());
return now.minusDays(interval.getDays()).isBefore(createdAt);
})
.count();

long merged =
pullRequests.stream()
.distinct()
.filter(pullRequest -> Objects.nonNull(pullRequest.mergedAt()))
.filter(
pullRequest -> {
ZonedDateTime createdAt = utcTimeFrom(pullRequest.mergedAt());
return now.minusDays(interval.getDays()).isBefore(createdAt);
})
.count();

return new PullRequestStatistics(created, merged, interval);
}

private List<PullRequest> listPullRequests(String organization, String repo)
throws URISyntaxException, IOException, InterruptedException {
List<PullRequest> pullRequests = new ArrayList<>();
int page = 1;
while (pullRequests.size() < MAX_PULL_REQUEST_NUM) {
HttpResponse<String> response = getResponse(getPullRequestsUrl(organization, repo, page));
pullRequests.addAll(
gson.fromJson(response.body(), new TypeToken<List<PullRequest>>() {}.getType()));
page++;
}

return pullRequests;
}

private String getPullRequestsUrl(String organization, String repo, int page) {
return String.format(PULL_REQUESTS_BASE, organization, repo, page);
}

private ZonedDateTime utcTimeFrom(String time) {
ZoneId zoneIdUTC = ZoneId.of("UTC");
Instant instant = Instant.parse(time);
return instant.atZone(zoneIdUTC);
}

private HttpResponse<String> getResponse(String endpoint)
throws URISyntaxException, IOException, InterruptedException {
HttpRequest request =
HttpRequest.newBuilder()
.header("Authorization", System.getenv("GITHUB_TOKEN"))
.uri(new URI(endpoint))
.GET()
.build();
return client.send(request, BodyHandlers.ofString());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.google.cloud.model;

public enum Interval {
WEEKLY(7),
MONTHLY(30);

private final int days;

Interval(int days) {
this.days = days;
}

public int getDays() {
return days;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.google.cloud.model;

import com.google.gson.annotations.SerializedName;

/**
* A record that represents a GitHub pull request.
*
* @param url The url of the pull request.
* @param state The state of the pull request, e.g., open, merged.
* @param createdAt The creation time of the pull request.
* @param mergedAt The merged time of the pull request; null if not merged.
*/
public record PullRequest(
String url,
String state,
@SerializedName("created_at") String createdAt,
@SerializedName("merged_at") String mergedAt) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.google.cloud.model;

/**
* A record that represents statistics about pull requests within a specified time interval.
*
* <p>The pull request statistics is used to show pull request freshness in the package information
* report.
*
* <p>For example, x pull requests are created and y pull requests are merged in the last 30 days.
*
* @param created The number of pull requests created within the interval.
* @param merged The number of pull requests merged within the interval.
* @param interval The time interval over which the statistics were collected.
*/
public record PullRequestStatistics(long created, long merged, Interval interval) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.google.cloud.external;

import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import com.google.cloud.model.Interval;
import com.google.cloud.model.PullRequestStatistics;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandler;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import org.junit.Before;
import org.junit.Test;
import org.mockito.MockedStatic;
import org.mockito.Mockito;

public class GitHubClientTest {

private HttpResponse<String> response;
private GitHubClient client;

@Before
public void setUp() throws IOException, InterruptedException {
HttpClient httpClient = mock(HttpClient.class);
client = new GitHubClient(httpClient);
response = mock(HttpResponse.class);
when(httpClient.send(any(HttpRequest.class), any(BodyHandler.class))).thenReturn(response);
}

@Test
public void testListMonthlyPullRequestStatusSucceeds()
throws URISyntaxException, IOException, InterruptedException {
ZonedDateTime fixedNow = ZonedDateTime.parse("2024-05-22T09:33:52Z");
ZonedDateTime lastMonth = ZonedDateTime.parse("2024-04-22T09:33:52Z");
Instant prInstant = Instant.parse("2024-05-10T09:33:52Z");
ZonedDateTime prTime = ZonedDateTime.parse("2024-05-10T09:33:52Z");
String responseBody =
Files.readString(Path.of("src/test/resources/pull_request_sample_response.txt"));

try (MockedStatic<ZonedDateTime> mockedLocalDateTime = Mockito.mockStatic(ZonedDateTime.class);
MockedStatic<Instant> mockedInstant = Mockito.mockStatic(Instant.class)) {
mockedLocalDateTime.when(ZonedDateTime::now).thenReturn(fixedNow);
mockedInstant.when(() -> Instant.parse(Mockito.anyString())).thenReturn(prInstant);
when(fixedNow.minusDays(30)).thenReturn(lastMonth);
when(prInstant.atZone(ZoneId.of("UTC"))).thenReturn(prTime);
when(response.body()).thenReturn(responseBody);
String org = "";
String repo = "";
PullRequestStatistics status = client.listMonthlyPullRequestStatusOf(org, repo);

assertEquals(Interval.MONTHLY, status.interval());
assertEquals(3, status.created());
assertEquals(7, status.merged());
}
}
}

Large diffs are not rendered by default.

0 comments on commit 80cc1f7

Please sign in to comment.