Skip to content

Commit

Permalink
Add services responsible for fetching data from GitHub API. Add domai…
Browse files Browse the repository at this point in the history
…n classes. Add tests for same layer.
  • Loading branch information
homer committed Jan 28, 2020
1 parent d1edcc5 commit c64e3bf
Show file tree
Hide file tree
Showing 19 changed files with 927 additions and 13 deletions.
33 changes: 33 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
HELP.md
github.token
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**
!**/src/test/**

### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache

### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
out/

### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/

### VS Code ###
.vscode/
11 changes: 11 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,20 @@ repositories {

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation "org.assertj:assertj-vavr:0.2.0"

compile 'javax.inject:javax.inject:1'
compile 'io.vavr:vavr:0.10.2'

compileOnly 'org.projectlombok:lombok:1.18.10'

annotationProcessor 'org.projectlombok:lombok:1.18.10'

testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}

testCompile("org.assertj:assertj-core:3.11.1")
}

test {
Expand Down
1 change: 1 addition & 0 deletions lombok.config
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
lombok.anyConstructor.addConstructorProperties = true
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,17 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication
public class ContributorsJavaApplication {

@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}

public static void main(String[] args) {
SpringApplication.run(ContributorsJavaApplication.class, args);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.gosiewski.contributorsjava.dto.incoming;

import lombok.Value;

@Value
public final class ContributorRequestDto {
private final String login;
private final int contributions;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.gosiewski.contributorsjava.dto.incoming;

import lombok.Value;

@Value
public final class RepositoryRequestDto {
private String name;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.gosiewski.contributorsjava.dto.outgoing;

import lombok.Value;

@Value
public class ContributorDto {
private String name;
private int contributions;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.gosiewski.contributorsjava.error;

public final class ApiCallError extends Error {
public ApiCallError() {
super("External HTTP response was different than expected.");
}
}
10 changes: 10 additions & 0 deletions src/main/java/com/gosiewski/contributorsjava/error/Error.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.gosiewski.contributorsjava.error;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public abstract class Error {
private final String reason;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.gosiewski.contributorsjava.error;

public final class IllegalArgumentError extends Error {
public IllegalArgumentError(String reason) {
super(reason);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.gosiewski.contributorsjava.error;

public final class NotFoundError extends Error {
public NotFoundError() {
super("Not found");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package com.gosiewski.contributorsjava.service;

import com.gosiewski.contributorsjava.dto.incoming.ContributorRequestDto;
import com.gosiewski.contributorsjava.dto.incoming.RepositoryRequestDto;
import com.gosiewski.contributorsjava.error.ApiCallError;
import com.gosiewski.contributorsjava.error.Error;
import com.gosiewski.contributorsjava.error.IllegalArgumentError;
import com.gosiewski.contributorsjava.service.domain.Contributor;
import com.gosiewski.contributorsjava.service.domain.Repository;
import io.vavr.collection.List;
import io.vavr.collection.Seq;
import io.vavr.control.Either;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;

import java.util.ArrayList;

@Service
@RequiredArgsConstructor
public class GitHubAPIService {
final static Logger logger = LoggerFactory.getLogger(GitHubAPIService.class);

private final static String REPOS_URL = "https://api.github.com/orgs/%1$s/repos";
private final static String CONTRIBUTORS_URL = "https://api.github.com/repos/%1$s/%2$s/contributors";

private final HttpClient httpClient;

final Either<Error, Seq<Repository>> getOrganizationRepos(final String organizationName) {
if (organizationName.isBlank()) {
return Either.left(new IllegalArgumentError("Organization name cannot be blank."));
}

final String url = String.format(REPOS_URL, organizationName);

return getFullGitHubResource(url, RepositoryRequestDto.class).map(this::mapRepositoryDtos);
}

final Either<Error, Seq<Contributor>> getRepoContributors(final String ownerName,
final String repoName) {
if (ownerName.isBlank() || repoName.isBlank()) {
return Either.left(new IllegalArgumentError("Owner name or repo name cannot be blank."));
}

final String url = String.format(CONTRIBUTORS_URL, ownerName, repoName);

return getFullGitHubResource(url, ContributorRequestDto.class).map(this::mapContributorDtos);
}

private <T> Either<Error, Seq<T>> getFullGitHubResource(final String url, final Class<T> clazz) {
Either<Error, ResponseEntity<java.util.List<T>>> result = httpClient.fetchFirstPage(url, clazz);

if (result.isLeft()) {
return Either.left(result.getLeft());
}

ResponseEntity<java.util.List<T>> response = result.get();

if (response.getBody() == null) {
logEmptyBodyError(response.toString());

return Either.left(new ApiCallError());
}

final java.util.List<T> fullResult = new ArrayList<>(response.getBody());

while(httpClient.hasNextPage(response.getHeaders())) {
result = httpClient.fetchNextPage(response, clazz);

if (result.isLeft()) {
return Either.left(result.getLeft());
}

response = result.get();

if (response.getBody() == null) {
logEmptyBodyError(response.toString());

return Either.left(new ApiCallError());
}

fullResult.addAll(response.getBody());
}

return Either.right(List.ofAll(fullResult));
}

private Seq<Contributor> mapContributorDtos(final Seq<ContributorRequestDto> dtos) {
return dtos.map(contributorDto -> new Contributor(contributorDto.getLogin(), contributorDto.getContributions()));
}

private Seq<Repository> mapRepositoryDtos(final Seq<RepositoryRequestDto> dtos) {
return dtos.map(repositoryDto -> new Repository(repositoryDto.getName()));
}

private void logEmptyBodyError(final String response) {
logger.error("GitHub responded with empty body:");
logger.error(response);
}
}
Loading

0 comments on commit c64e3bf

Please sign in to comment.