diff --git a/README.md b/README.md index e84e1dd9..67b4821d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,26 @@ # github-stats +## Available commands + +`github-stats` runs in an interactive shell where you can run commands that fetch Github data. + +Command definition: + +``` +login - Log in to the Github API before making any requests. +Usage: login --token + +team - Get data from a specified team and its sub teams. +Usage: team -n --from --to + FROM_DATE: month and year in MM/yy format (starting Jan 2000) + TO_DATE: same format as FROM_DATE, accepts until current_month - 1 + +user - Get data from a specified user. +Usage: user -n --from --to + FROM_DATE: month and year in MM/yy format (starting Jan 2000) + TO_DATE: same format as FROM_DATE, accepts until current_month - 1 +``` + ## Software stack - Java 17 - Spring Boot 2.7.5 diff --git a/src/main/java/io/pakland/mdas/githubstats/infrastructure/shell/components/CommitComponent.java b/src/main/java/io/pakland/mdas/githubstats/infrastructure/shell/components/CommitComponent.java deleted file mode 100644 index 921abbf8..00000000 --- a/src/main/java/io/pakland/mdas/githubstats/infrastructure/shell/components/CommitComponent.java +++ /dev/null @@ -1,32 +0,0 @@ -package io.pakland.mdas.githubstats.infrastructure.shell.components; - -import org.springframework.context.annotation.Bean; -import org.springframework.shell.command.CommandRegistration; -import org.springframework.shell.standard.ShellComponent; - -@ShellComponent -public class CommitComponent { - - public void commits(String user) { - System.out.println("commits"); - } - - @Bean - CommandRegistration commitCommandRegistration() { - CommitComponent commitComponent = new CommitComponent(); - - return CommandRegistration.builder() - .command("commits") - .description("Number of commits by user") - .withTarget() - .method(commitComponent, "commits") - .and() - .withOption() - .shortNames('u') - .required() - .type(String.class) - .and() - .build(); - } - -} diff --git a/src/main/java/io/pakland/mdas/githubstats/infrastructure/shell/components/LinesOfCodeComponent.java b/src/main/java/io/pakland/mdas/githubstats/infrastructure/shell/components/LinesOfCodeComponent.java deleted file mode 100644 index 0e04971f..00000000 --- a/src/main/java/io/pakland/mdas/githubstats/infrastructure/shell/components/LinesOfCodeComponent.java +++ /dev/null @@ -1,31 +0,0 @@ -package io.pakland.mdas.githubstats.infrastructure.shell.components; - -import org.springframework.context.annotation.Bean; -import org.springframework.shell.command.CommandRegistration; -import org.springframework.shell.standard.ShellComponent; - -@ShellComponent -public class LinesOfCodeComponent { - - public void locChanges(String user) { - System.out.println("locChanges"); - } - - @Bean - CommandRegistration linesOfCodeCommandRegistration() { - LinesOfCodeComponent linesOfCodeComponent = new LinesOfCodeComponent(); - - return CommandRegistration.builder() - .command("locChanges") - .description("Lines of code deleted and added") - .withTarget() - .method(linesOfCodeComponent, "locChanges") - .and() - .withOption() - .shortNames('u') - .required() - .type(String.class) - .and() - .build(); - } -} diff --git a/src/main/java/io/pakland/mdas/githubstats/infrastructure/shell/components/LoginComponent.java b/src/main/java/io/pakland/mdas/githubstats/infrastructure/shell/components/LoginComponent.java new file mode 100644 index 00000000..e9efb161 --- /dev/null +++ b/src/main/java/io/pakland/mdas/githubstats/infrastructure/shell/components/LoginComponent.java @@ -0,0 +1,13 @@ +package io.pakland.mdas.githubstats.infrastructure.shell.components; + +import org.springframework.shell.standard.ShellComponent; +import org.springframework.shell.standard.ShellOption; + +@ShellComponent +public class LoginComponent { + + private boolean login(@ShellOption(value = {"token"}) String token) { + return token != null; + } + +} diff --git a/src/main/java/io/pakland/mdas/githubstats/infrastructure/shell/components/PullRequestComponent.java b/src/main/java/io/pakland/mdas/githubstats/infrastructure/shell/components/PullRequestComponent.java deleted file mode 100644 index c0e016e8..00000000 --- a/src/main/java/io/pakland/mdas/githubstats/infrastructure/shell/components/PullRequestComponent.java +++ /dev/null @@ -1,41 +0,0 @@ -package io.pakland.mdas.githubstats.infrastructure.shell.components; - -import org.springframework.context.annotation.Bean; -import org.springframework.shell.command.CommandRegistration; -import org.springframework.shell.standard.ShellComponent; - -@ShellComponent -public class PullRequestComponent { - - private void prExecuted(String user) { - System.out.println("prExecuted"); - } - - private void prReviewed(String user) { - System.out.println("prReviewed"); - } - - private void prCommentLength(String user) { - System.out.println("prCommentLength"); - } - - - @Bean - CommandRegistration pullRequestCommandRegistration() { - PullRequestComponent pullRequestComponent = new PullRequestComponent(); - - return CommandRegistration.builder() - .command("prExecuted") - .description("Number of pull requests executed") - .withTarget() - .method(pullRequestComponent, "prExecuted") - .and() - .withOption() - .shortNames('u') - .required() - .type(String.class) - .and() - .build(); - } - -} diff --git a/src/main/java/io/pakland/mdas/githubstats/infrastructure/shell/components/TeamComponent.java b/src/main/java/io/pakland/mdas/githubstats/infrastructure/shell/components/TeamComponent.java new file mode 100644 index 00000000..5acc6ab8 --- /dev/null +++ b/src/main/java/io/pakland/mdas/githubstats/infrastructure/shell/components/TeamComponent.java @@ -0,0 +1,33 @@ +package io.pakland.mdas.githubstats.infrastructure.shell.components; + +import io.pakland.mdas.githubstats.infrastructure.shell.validation.DateValidator; +import io.pakland.mdas.githubstats.infrastructure.shell.validation.TeamNameValidator; +import org.springframework.shell.standard.ShellComponent; +import org.springframework.shell.standard.ShellOption; + +@ShellComponent +public class TeamComponent { + + private boolean team( + @ShellOption(value = {"n"}) String teamName, + @ShellOption(value = {"from"}) String fromDate, + @ShellOption(value = {"to"}) String toDate + ) { + DateValidator dateValidator = new DateValidator(); + TeamNameValidator teamNameValidator = new TeamNameValidator(); + + boolean isInputValid = dateValidator.validate(fromDate) + && dateValidator.validate(toDate) + && teamNameValidator.validate(teamName); + + if (!isInputValid) { + // Alert user, and halt command + return false; + } + + // ... Perform request ... + + return true; + } + +} diff --git a/src/main/java/io/pakland/mdas/githubstats/infrastructure/shell/components/UserComponent.java b/src/main/java/io/pakland/mdas/githubstats/infrastructure/shell/components/UserComponent.java new file mode 100644 index 00000000..814a59a0 --- /dev/null +++ b/src/main/java/io/pakland/mdas/githubstats/infrastructure/shell/components/UserComponent.java @@ -0,0 +1,33 @@ +package io.pakland.mdas.githubstats.infrastructure.shell.components; + +import io.pakland.mdas.githubstats.infrastructure.shell.validation.DateValidator; +import io.pakland.mdas.githubstats.infrastructure.shell.validation.UserNameValidator; +import org.springframework.shell.standard.ShellComponent; +import org.springframework.shell.standard.ShellOption; + +@ShellComponent +public class UserComponent { + + private boolean user( + @ShellOption(value = {"n"}) String userName, + @ShellOption(value = {"from"}) String fromDate, + @ShellOption(value = {"to"}) String toDate + ) { + DateValidator dateValidator = new DateValidator(); + UserNameValidator userNameValidator = new UserNameValidator(); + + boolean isInputValid = dateValidator.validate(fromDate) + && dateValidator.validate(toDate) + && userNameValidator.validate(userName); + + if (!isInputValid) { + // Alert user, and halt command + return false; + } + + // ... Perform request ... + + return true; + } + +} diff --git a/src/main/java/io/pakland/mdas/githubstats/infrastructure/shell/configuration/CommandConfiguration.java b/src/main/java/io/pakland/mdas/githubstats/infrastructure/shell/configuration/CommandConfiguration.java new file mode 100644 index 00000000..1092eb91 --- /dev/null +++ b/src/main/java/io/pakland/mdas/githubstats/infrastructure/shell/configuration/CommandConfiguration.java @@ -0,0 +1,102 @@ +package io.pakland.mdas.githubstats.infrastructure.shell.configuration; + +import io.pakland.mdas.githubstats.infrastructure.shell.components.LoginComponent; +import io.pakland.mdas.githubstats.infrastructure.shell.components.TeamComponent; +import io.pakland.mdas.githubstats.infrastructure.shell.components.UserComponent; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.shell.command.CommandRegistration; + +@Configuration +public class CommandConfiguration { + + @Bean + public CommandRegistration buildUserCommand() { + UserComponent userComponent = new UserComponent(); + + return CommandRegistration.builder() + .command("user") + .description("Get data from a specified user.") + .withTarget() + .method(userComponent, "user") + .and() + .withOption() + .shortNames('n') + .longNames("name") + .label("USER_NAME") + .arity(CommandRegistration.OptionArity.EXACTLY_ONE) + .type(String.class) + .required() + .and() + .withOption() + .longNames("from") + .label("FROM_DATE") + .arity(CommandRegistration.OptionArity.EXACTLY_ONE) + .type(String.class) + .required() + .and() + .withOption() + .longNames("to") + .label("TO_DATE") + .arity(CommandRegistration.OptionArity.EXACTLY_ONE) + .type(String.class) + .required() + .and() + .build(); + } + + @Bean + public CommandRegistration buildTeamCommand() { + TeamComponent teamComponent = new TeamComponent(); + + return CommandRegistration.builder() + .command("team") + .description("Get data from a specified team and its sub teams.") + .withTarget() + .method(teamComponent, "team") + .and() + .withOption() + .shortNames('n') + .longNames("name") + .label("TEAM_NAME") + .arity(CommandRegistration.OptionArity.EXACTLY_ONE) + .type(String.class) + .required() + .and() + .withOption() + .longNames("from") + .label("FROM_DATE") + .arity(CommandRegistration.OptionArity.EXACTLY_ONE) + .type(String.class) + .required() + .and() + .withOption() + .longNames("to") + .label("TO_DATE") + .arity(CommandRegistration.OptionArity.EXACTLY_ONE) + .type(String.class) + .required() + .and() + .build(); + } + + @Bean + public CommandRegistration buildLoginCommand() { + LoginComponent loginComponent = new LoginComponent(); + + return CommandRegistration.builder() + .command("login") + .description("Log in to the Github API before making any requests.") + .withTarget() + .method(loginComponent, "login") + .and() + .withOption() + .longNames("token") + .label("TOKEN") + .type(String.class) + .required() + .and() + .build(); + } + +} diff --git a/src/main/java/io/pakland/mdas/githubstats/infrastructure/shell/validation/DateValidator.java b/src/main/java/io/pakland/mdas/githubstats/infrastructure/shell/validation/DateValidator.java new file mode 100644 index 00000000..de9f0da4 --- /dev/null +++ b/src/main/java/io/pakland/mdas/githubstats/infrastructure/shell/validation/DateValidator.java @@ -0,0 +1,37 @@ +package io.pakland.mdas.githubstats.infrastructure.shell.validation; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; + +public class DateValidator implements InputValidator { + + private final DateTimeFormatter formatter; + + public DateValidator() { + String datePattern = "dd/MM/yy HH:mm"; + formatter = DateTimeFormatter.ofPattern(datePattern); + } + + /** + * @param input Must be MM/yy, and prior to the current month. Starting year: 2000 (01/01/99 parses to 01/01/2099) + */ + @Override + public boolean validate(String input) { + if (input == null || input.isBlank()) return false; + + try { + LocalDateTime date = LocalDateTime.parse(getInitialDateFromMMYY(input), formatter); + LocalDateTime now = LocalDateTime.now(); + return date.isBefore(now.minusMonths(1)); + } catch (DateTimeParseException e) { + // Input was not properly formatted + return false; + } + } + + private String getInitialDateFromMMYY(String input) { + return "01/" + input + " 00:00"; + } + +} diff --git a/src/main/java/io/pakland/mdas/githubstats/infrastructure/shell/validation/InputValidator.java b/src/main/java/io/pakland/mdas/githubstats/infrastructure/shell/validation/InputValidator.java new file mode 100644 index 00000000..9d0373f0 --- /dev/null +++ b/src/main/java/io/pakland/mdas/githubstats/infrastructure/shell/validation/InputValidator.java @@ -0,0 +1,7 @@ +package io.pakland.mdas.githubstats.infrastructure.shell.validation; + +public interface InputValidator { + + boolean validate(T input); + +} diff --git a/src/main/java/io/pakland/mdas/githubstats/infrastructure/shell/validation/TeamNameValidator.java b/src/main/java/io/pakland/mdas/githubstats/infrastructure/shell/validation/TeamNameValidator.java new file mode 100644 index 00000000..d1fc51a2 --- /dev/null +++ b/src/main/java/io/pakland/mdas/githubstats/infrastructure/shell/validation/TeamNameValidator.java @@ -0,0 +1,23 @@ +package io.pakland.mdas.githubstats.infrastructure.shell.validation; + +public class TeamNameValidator implements InputValidator { + + public TeamNameValidator() {} + + /** + * @param input Must pass the Github team name creation constraints: + * - Team name may only contain alphanumeric characters or hyphens. + * - Team name cannot have multiple consecutive hyphens. + * - Team name cannot begin or end with a hyphen. + * - Maximum is 39 characters. + */ + @Override + public boolean validate(String input) { + if (input == null || input.isBlank()) return false; + return !input.matches("^-.*") + && !input.matches(".*-$") + && input.matches("[a-zA-Z0-9\\-]{0,39}") + && !input.matches(".*--.*"); + } + +} diff --git a/src/main/java/io/pakland/mdas/githubstats/infrastructure/shell/validation/UserNameValidator.java b/src/main/java/io/pakland/mdas/githubstats/infrastructure/shell/validation/UserNameValidator.java new file mode 100644 index 00000000..da72a423 --- /dev/null +++ b/src/main/java/io/pakland/mdas/githubstats/infrastructure/shell/validation/UserNameValidator.java @@ -0,0 +1,22 @@ +package io.pakland.mdas.githubstats.infrastructure.shell.validation; + +public class UserNameValidator implements InputValidator { + + public UserNameValidator() {} + + /** + * @param input Must pass the Github username creation constraints: + * - Username may only contain alphanumeric characters or hyphens. + * - Username cannot have multiple consecutive hyphens. + * - Username cannot begin or end with a hyphen. + * - Maximum is 39 characters. + */ + @Override + public boolean validate(String input) { + if (input == null || input.isBlank()) return false; + return !input.matches("^-.*") + && !input.matches(".*-$") + && input.matches("[a-zA-Z0-9\\-]{0,39}") + && !input.matches(".*--.*"); + } +} diff --git a/src/test/java/io/pakland/mdas/githubstats/infrastructure/shell/validation/DateValidatorTest.java b/src/test/java/io/pakland/mdas/githubstats/infrastructure/shell/validation/DateValidatorTest.java new file mode 100644 index 00000000..9c025959 --- /dev/null +++ b/src/test/java/io/pakland/mdas/githubstats/infrastructure/shell/validation/DateValidatorTest.java @@ -0,0 +1,30 @@ +package io.pakland.mdas.githubstats.infrastructure.shell.validation; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class DateValidatorTest { + DateValidator dateValidator = new DateValidator(); + + @Test + void testValidationShouldPass() { + assertTrue(dateValidator.validate("12/01")); + assertTrue(dateValidator.validate("03/22")); + assertTrue(dateValidator.validate("09/22")); + assertTrue(dateValidator.validate("10/20")); + assertTrue(dateValidator.validate("07/15")); + } + + @Test + void testValidationShouldFail() { + assertFalse(dateValidator.validate("aa")); + assertFalse(dateValidator.validate("")); + assertFalse(dateValidator.validate("01-02")); + assertFalse(dateValidator.validate("13/9")); + assertFalse(dateValidator.validate("00/00")); + assertFalse(dateValidator.validate("1/4444444444444")); + assertFalse(dateValidator.validate("2/22")); + assertFalse(dateValidator.validate(" / ")); + } +} diff --git a/src/test/java/io/pakland/mdas/githubstats/infrastructure/shell/validation/TeamNameValidatorTest.java b/src/test/java/io/pakland/mdas/githubstats/infrastructure/shell/validation/TeamNameValidatorTest.java new file mode 100644 index 00000000..7d2afd3a --- /dev/null +++ b/src/test/java/io/pakland/mdas/githubstats/infrastructure/shell/validation/TeamNameValidatorTest.java @@ -0,0 +1,31 @@ +package io.pakland.mdas.githubstats.infrastructure.shell.validation; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class TeamNameValidatorTest { + TeamNameValidator teamNameValidator = new TeamNameValidator(); + + @Test + void testValidationShouldPass() { + assertTrue(teamNameValidator.validate("name")); + assertTrue(teamNameValidator.validate("test-hello-world")); + assertTrue(teamNameValidator.validate("22312")); + assertTrue(teamNameValidator.validate("alphanumeric-and-hyphens-23")); + assertTrue(teamNameValidator.validate("UPPERcase")); + } + + @Test + void testValidationShouldFail() { + assertFalse(teamNameValidator.validate("name with spaces")); + assertFalse(teamNameValidator.validate("very-very-very-very-very-very-very-very-very-very-long-name")); + assertFalse(teamNameValidator.validate("-name")); + assertFalse(teamNameValidator.validate("name-")); + assertFalse(teamNameValidator.validate("i-have--2-hyphens")); + assertFalse(teamNameValidator.validate("")); + assertFalse(teamNameValidator.validate(" ")); + assertFalse(teamNameValidator.validate(" - ")); + } +} diff --git a/src/test/java/io/pakland/mdas/githubstats/infrastructure/shell/validation/UserNameValidatorTest.java b/src/test/java/io/pakland/mdas/githubstats/infrastructure/shell/validation/UserNameValidatorTest.java new file mode 100644 index 00000000..aca3d582 --- /dev/null +++ b/src/test/java/io/pakland/mdas/githubstats/infrastructure/shell/validation/UserNameValidatorTest.java @@ -0,0 +1,31 @@ +package io.pakland.mdas.githubstats.infrastructure.shell.validation; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class UserNameValidatorTest { + UserNameValidator userNameValidator = new UserNameValidator(); + + @Test + void testValidationShouldPass() { + assertTrue(userNameValidator.validate("name")); + assertTrue(userNameValidator.validate("test-hello-world")); + assertTrue(userNameValidator.validate("22312")); + assertTrue(userNameValidator.validate("alphanumeric-and-hyphens-23")); + assertTrue(userNameValidator.validate("UPPERcase")); + } + + @Test + void testValidationShouldFail() { + assertFalse(userNameValidator.validate("name with spaces")); + assertFalse(userNameValidator.validate("very-very-very-very-very-very-very-very-very-very-long-name")); + assertFalse(userNameValidator.validate("-name")); + assertFalse(userNameValidator.validate("name-")); + assertFalse(userNameValidator.validate("i-have--2-hyphens")); + assertFalse(userNameValidator.validate("")); + assertFalse(userNameValidator.validate(" ")); + assertFalse(userNameValidator.validate(" - ")); + } +}