From dbe5c37cedae1e04b6ea678bd35c30f2b2bc8f53 Mon Sep 17 00:00:00 2001 From: Adler Fleurant <2609856+AdlerFleurant@users.noreply.github.com> Date: Thu, 11 Jun 2020 09:19:55 -0400 Subject: [PATCH 1/2] Backend Quarkus History: Adding Support for History Adding history endpoint: POST /history/{period} to create. GET /history/{period} to obtain. Signed-off-by: Adler Fleurant <2609856+AdlerFleurant@users.noreply.github.com> --- openapi/openapi.yaml | 379 ++++++++++++++++++ .../src/main/resources/META-INF/openapi.yaml | 112 ++++-- .../src/main/resources/application.properties | 2 + .../services/ninja/client/PeriodClient.java | 41 ++ .../ninja/client/ScorecardClient.java | 5 + .../data/controller/AbstractResource.java | 2 +- .../ninja/data/controller/PeriodResource.java | 29 ++ .../ninja/data/service/DatabaseEngine.java | 14 +- .../data/controller/PeriodResourceTest.java | 97 +++++ .../controller/ScorecardResourceTest.java | 2 +- .../services/ninja/entity/Database.java | 9 + .../services/ninja/entity/ErrorResponse.java | 2 +- .../redhat/services/ninja/entity/Level.java | 3 +- .../redhat/services/ninja/entity/Period.java | 91 +++++ .../redhat/services/ninja/entity/Record.java | 39 ++ .../services/ninja/entity/Scorecard.java | 7 + .../redhat/services/ninja/entity/User.java | 9 - .../ninja/controller/PeriodResource.java | 68 ++++ .../ninja/controller/PeriodResourceTest.java | 86 ++++ .../redhat/services/ninja/test/database.json | 70 +++- 20 files changed, 1019 insertions(+), 48 deletions(-) create mode 100644 openapi/openapi.yaml create mode 100644 quarkus/data-client/src/main/java/com/redhat/services/ninja/client/PeriodClient.java create mode 100644 quarkus/data-service/src/main/java/com/redhat/services/ninja/data/controller/PeriodResource.java create mode 100644 quarkus/data-service/src/test/java/com/redhat/services/ninja/data/controller/PeriodResourceTest.java create mode 100644 quarkus/entities/src/main/java/com/redhat/services/ninja/entity/Period.java create mode 100644 quarkus/entities/src/main/java/com/redhat/services/ninja/entity/Record.java create mode 100644 quarkus/scorecard-service/src/main/java/com/redhat/services/ninja/controller/PeriodResource.java create mode 100644 quarkus/scorecard-service/src/test/java/com/redhat/services/ninja/controller/PeriodResourceTest.java diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml new file mode 100644 index 00000000..40a8e5a1 --- /dev/null +++ b/openapi/openapi.yaml @@ -0,0 +1,379 @@ +--- +openapi: 3.0.2 +info: + title: Giveback Ninja + version: 2.0.0 + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0 +paths: + /user: + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/User' + examples: + new-ninja: + value: + username: new_ninja + displayName: New Ninja + githubUsername: new_ninja + required: true + responses: + "201": + content: + application/json: + schema: + $ref: '#/components/schemas/User' + examples: + created-user: + value: + username: new_ninja + displayName: New Ninja + githubUsername: new_ninja + region: NA + description: Creates a user within the system. + "404": + description: When the username was not found in the backend ldap service. + operationId: create-user + summary: Registers a new user in the system. + /scorecard: + get: + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Scorecard' + description: Returns a list of all user scorecards. + summary: Get all user scorecards. + /scorecard/{username}: + get: + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Scorecard' + examples: + super_ninja: + value: + username: super_ninja + level: black + total: 248 + pointsToNextLevel: 0 + nextLevel: "" + pointMap: + githubReviewedPullRequests: 24 + githubClosedIssues: 0 + servicesSupportMergeRequests: 0 + servicesSupportClosedIssues: 0 + servicesSupportReviewedMergeRequests: 0 + githubPullRequests: 34 + gitlabMergeRequests: 43 + trelloCardsClosed: 23 + thoughtLeadershipCardsClosed: 1 + servicesSupportCardsClosed: 10 + description: Get Scorecard Info + summary: Gets Scorecard info for a specific user. + parameters: + - name: username + description: The unique name of a specific user. + schema: + type: string + in: path + required: true + /scorecard/{username}/{pool}: + post: + requestBody: + content: + text/plain: + schema: + type: integer + required: true + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Scorecard' + description: Pool for provided username was incremented successfully. + summary: Increments a scorecard for provided username and pool. + description: "" + parameters: + - name: username + description: The unique name of the user. + schema: + type: string + in: path + required: true + - name: pool + description: The pool to increment. The pool will be created if not already + defined. + schema: + type: string + in: path + required: true + /level: + get: + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Level' + examples: + level-list: + value: + - minimumPoint: 0 + name: ZERO + - minimumPoint: 5 + name: BLUE + - minimumPoint: 20 + name: GREY + - minimumPoint: 40 + name: RED + - minimumPoint: 75 + name: BLACK + description: All levels + summary: Obtains all available Levels + /history/{period}: + get: + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Period' + examples: + fy20: + value: + cumulatedOn: 2021-01-11T08:08:56.5899687 + records: + retired_ninja: + score: 120 + level: BLACK + super_ninja: + score: 80 + level: BLACK + modest_ninja: + score: 30 + level: GREY + name: FY20 + description: Period requested with path parameter + "404": + $ref: '#/components/responses/NotFoundResponse' + summary: Obtains history for requested period + post: + responses: + "201": + content: + application/json: + schema: + $ref: '#/components/schemas/Period' + examples: + fy20: + value: + cumulatedOn: 2021-06-11T08:08:56.5899687 + records: + retired_ninja: + score: 120 + level: BLACK + super_ninja: + score: 80 + level: BLACK + modest_ninja: + score: 30 + level: GREY + name: FY20 + description: Created Period + summary: Generates a new Historic period and reset existing scores + parameters: + - examples: + fy20: + value: FY20 + name: period + description: Period name + schema: + type: string + in: path + required: true +components: + schemas: + Event: + description: "" + required: + - timestamp + - type + type: object + properties: + username: + description: The user to which the event applied + type: string + type: + description: The type of event + type: string + timestamp: + format: date-time + description: The timestamp when the event occurred + type: string + description: + description: Describes the event. + type: string + example: + username: super_ninja + type: New User Registered + timestamp: 2020-03-03T17:17 + description: A new user has been registered + Record: + description: "" + required: + - level + - score + type: object + properties: + level: + description: The level attained by the user for a period + type: string + score: + description: Points cumulated by a user during a period + type: integer + Period: + description: The period for which the history was computed. Usually Fiscal Year + required: + - name + - cumulatedOn + - records + type: object + properties: + name: + description: Unique name of the period + type: string + cumulatedOn: + format: date-time + description: When the period was computed + type: string + records: + description: The records for this period + type: object + additionalProperties: + $ref: '#/components/schemas/Record' + User: + title: User + description: "" + required: + - username + type: object + properties: + displayName: + description: The name displayed to all users of the system. Typically the + users first and last name. + type: string + username: + description: The user's Red Hat Kerberos ID. + type: string + githubUsername: + description: The user's Github username. + type: string + trelloUsername: + description: The user's Trello username. + type: string + email: + description: The Red Hat email associated with the user. + type: string + example: + displayName: Super Ninja + username: super_ninja + githubUsername: super_ninja + trelloUsername: super_ninja + email: super_ninja@redhat.com + Scorecard: + title: User Scorecard + description: All stats around a user that are used to calculate Ninja Belt level. + required: + - username + type: object + properties: + username: + description: The unique user id. + type: string + level: + description: The current level of the ninja + type: string + pointsToNextLevel: + description: | + The number of points needed to achieve the next level. 0 indicates + that there is no higher level. + type: integer + nextLevel: + description: "The level that can be reached next. An empty string indicates\ + \ that \nthe current level is the highest.\n" + type: string + details: + description: Score for each pool + type: object + additionalProperties: + type: integer + score: + description: The current score + type: integer + example: + username: super_ninja + level: black + score: 248 + pointsToNextLevel: 0 + nextLevel: "" + pointMap: + githubReviewedPullRequests: 24 + githubClosedIssues: 0 + servicesSupportMergeRequests: 0 + servicesSupportClosedIssues: 0 + servicesSupportReviewedMergeRequests: 0 + githubPullRequests: 34 + gitlabMergeRequests: 43 + trelloCardsClosed: 23 + thoughtLeadershipCardsClosed: 1 + servicesSupportCardsClosed: 10 + ErrorResponse: + title: Root Type for ErrorResponse + description: An error response + type: object + properties: + message: + description: The error message + type: string + example: + message: Entity was not found + Level: + description: "" + required: + - name + - minimumScore + type: object + properties: + name: + description: The name of the level + type: string + minimumScore: + description: The minimum point needed to be at that level + type: integer + example: + name: BLACK + minimumScore: 75 + responses: + NotFoundResponse: + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + entity-not-found: + value: + message: Entity was not found + description: "" diff --git a/quarkus/all-service/src/main/resources/META-INF/openapi.yaml b/quarkus/all-service/src/main/resources/META-INF/openapi.yaml index a0148c6c..40a8e5a1 100644 --- a/quarkus/all-service/src/main/resources/META-INF/openapi.yaml +++ b/quarkus/all-service/src/main/resources/META-INF/openapi.yaml @@ -4,8 +4,8 @@ info: title: Giveback Ninja version: 2.0.0 license: - name: MIT License - url: https://opensource.org/licenses/MIT + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0 paths: /user: post: @@ -67,7 +67,7 @@ paths: total: 248 pointsToNextLevel: 0 nextLevel: "" - details: + pointMap: githubReviewedPullRequests: 24 githubClosedIssues: 0 servicesSupportMergeRequests: 0 @@ -79,7 +79,7 @@ paths: thoughtLeadershipCardsClosed: 1 servicesSupportCardsClosed: 10 description: Get Scorecard Info - summary: Get UserScorecard info for a specific user. + summary: Gets Scorecard info for a specific user. parameters: - name: username description: The unique name of a specific user. @@ -102,6 +102,8 @@ paths: schema: $ref: '#/components/schemas/Scorecard' description: Pool for provided username was incremented successfully. + summary: Increments a scorecard for provided username and pool. + description: "" parameters: - name: username description: The unique name of the user. @@ -140,7 +142,68 @@ paths: - minimumPoint: 75 name: BLACK description: All levels - summary: Obtain all available Levels + summary: Obtains all available Levels + /history/{period}: + get: + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Period' + examples: + fy20: + value: + cumulatedOn: 2021-01-11T08:08:56.5899687 + records: + retired_ninja: + score: 120 + level: BLACK + super_ninja: + score: 80 + level: BLACK + modest_ninja: + score: 30 + level: GREY + name: FY20 + description: Period requested with path parameter + "404": + $ref: '#/components/responses/NotFoundResponse' + summary: Obtains history for requested period + post: + responses: + "201": + content: + application/json: + schema: + $ref: '#/components/schemas/Period' + examples: + fy20: + value: + cumulatedOn: 2021-06-11T08:08:56.5899687 + records: + retired_ninja: + score: 120 + level: BLACK + super_ninja: + score: 80 + level: BLACK + modest_ninja: + score: 30 + level: GREY + name: FY20 + description: Created Period + summary: Generates a new Historic period and reset existing scores + parameters: + - examples: + fy20: + value: FY20 + name: period + description: Period name + schema: + type: string + in: path + required: true components: schemas: Event: @@ -168,21 +231,6 @@ components: type: New User Registered timestamp: 2020-03-03T17:17 description: A new user has been registered - Level: - description: "" - required: - - name - - minimumScore - type: object - properties: - name: - $ref: '#/components/schemas/Level' - minimumScore: - description: The minimum point needed to be at that level - type: integer - example: - name: BLACK - minimumScore: 75 Record: description: "" required: @@ -264,8 +312,8 @@ components: that there is no higher level. type: integer nextLevel: - description: | - The level that can be reached next. An empty string indicates that the current level is the highest. + description: "The level that can be reached next. An empty string indicates\ + \ that \nthe current level is the highest.\n" type: string details: description: Score for each pool @@ -281,7 +329,7 @@ components: score: 248 pointsToNextLevel: 0 nextLevel: "" - details: + pointMap: githubReviewedPullRequests: 24 githubClosedIssues: 0 servicesSupportMergeRequests: 0 @@ -302,6 +350,22 @@ components: type: string example: message: Entity was not found + Level: + description: "" + required: + - name + - minimumScore + type: object + properties: + name: + description: The name of the level + type: string + minimumScore: + description: The minimum point needed to be at that level + type: integer + example: + name: BLACK + minimumScore: 75 responses: NotFoundResponse: content: @@ -312,4 +376,4 @@ components: entity-not-found: value: message: Entity was not found - description: "" \ No newline at end of file + description: "" diff --git a/quarkus/all-service/src/main/resources/application.properties b/quarkus/all-service/src/main/resources/application.properties index 5296c25e..d26f7bf9 100644 --- a/quarkus/all-service/src/main/resources/application.properties +++ b/quarkus/all-service/src/main/resources/application.properties @@ -4,5 +4,7 @@ com.redhat.services.ninja.client.ScorecardClient/mp-rest/url=http://localhost:80 com.redhat.services.ninja.client.ScorecardClient/mp-rest/scope=javax.inject.Singleton com.redhat.services.ninja.client.LevelClient/mp-rest/url=http://localhost:8080/ com.redhat.services.ninja.client.LevelClient/mp-rest/scope=javax.inject.Singleton +com.redhat.services.ninja.client.PeriodClient/mp-rest/url=http://localhost:8080/ +com.redhat.services.ninja.client.PeriodClient/mp-rest/scope=javax.inject.Singleton quarkus.http.cors=true mp.openapi.scan.disable=true \ No newline at end of file diff --git a/quarkus/data-client/src/main/java/com/redhat/services/ninja/client/PeriodClient.java b/quarkus/data-client/src/main/java/com/redhat/services/ninja/client/PeriodClient.java new file mode 100644 index 00000000..68abfe07 --- /dev/null +++ b/quarkus/data-client/src/main/java/com/redhat/services/ninja/client/PeriodClient.java @@ -0,0 +1,41 @@ +package com.redhat.services.ninja.client; + +import com.redhat.services.ninja.entity.Period; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +import javax.enterprise.context.ApplicationScoped; +import javax.ws.rs.*; +import javax.ws.rs.core.MediaType; +import java.util.List; + +@Path("data/period") +@RegisterRestClient +@ApplicationScoped +public interface PeriodClient { + @GET + @Path("{identifier}") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + Period get(@PathParam("identifier") String identifier); + + @GET + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + List getAll(); + + @POST + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + Period create(Period period); + + @PUT + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + Period update(Period period); + + @DELETE + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + Period delete(String identifier); + +} diff --git a/quarkus/data-client/src/main/java/com/redhat/services/ninja/client/ScorecardClient.java b/quarkus/data-client/src/main/java/com/redhat/services/ninja/client/ScorecardClient.java index 6798c799..3864623f 100644 --- a/quarkus/data-client/src/main/java/com/redhat/services/ninja/client/ScorecardClient.java +++ b/quarkus/data-client/src/main/java/com/redhat/services/ninja/client/ScorecardClient.java @@ -32,4 +32,9 @@ public interface ScorecardClient { @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) Scorecard update(Scorecard scorecard); + + @DELETE + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + Scorecard delete(String identifier); } diff --git a/quarkus/data-service/src/main/java/com/redhat/services/ninja/data/controller/AbstractResource.java b/quarkus/data-service/src/main/java/com/redhat/services/ninja/data/controller/AbstractResource.java index 3a5c05a1..c396f24e 100644 --- a/quarkus/data-service/src/main/java/com/redhat/services/ninja/data/controller/AbstractResource.java +++ b/quarkus/data-service/src/main/java/com/redhat/services/ninja/data/controller/AbstractResource.java @@ -12,7 +12,7 @@ @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public abstract class AbstractResource> { - + @POST public T create(T entity) { T createdEntity = getOperations().create(entity); diff --git a/quarkus/data-service/src/main/java/com/redhat/services/ninja/data/controller/PeriodResource.java b/quarkus/data-service/src/main/java/com/redhat/services/ninja/data/controller/PeriodResource.java new file mode 100644 index 00000000..db583da7 --- /dev/null +++ b/quarkus/data-service/src/main/java/com/redhat/services/ninja/data/controller/PeriodResource.java @@ -0,0 +1,29 @@ +package com.redhat.services.ninja.data.controller; + +import com.redhat.services.ninja.data.operation.IdentifiableDatabaseOperations; +import com.redhat.services.ninja.data.service.DatabaseEngine; +import com.redhat.services.ninja.entity.Period; + +import javax.inject.Inject; +import javax.ws.rs.Consumes; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +@Path("/data/period") +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +public class PeriodResource extends AbstractIdentifiableResource> { + @Inject + DatabaseEngine engine; + + @Override + public DatabaseEngine getEngine() { + return engine; + } + + @Override + public IdentifiableDatabaseOperations getOperations() { + return engine.getPeriodOperations(); + } +} diff --git a/quarkus/data-service/src/main/java/com/redhat/services/ninja/data/service/DatabaseEngine.java b/quarkus/data-service/src/main/java/com/redhat/services/ninja/data/service/DatabaseEngine.java index 72e42aa4..d1300334 100644 --- a/quarkus/data-service/src/main/java/com/redhat/services/ninja/data/service/DatabaseEngine.java +++ b/quarkus/data-service/src/main/java/com/redhat/services/ninja/data/service/DatabaseEngine.java @@ -2,10 +2,7 @@ import com.redhat.services.ninja.data.operation.BasicDatabaseOperations; import com.redhat.services.ninja.data.operation.IdentifiableDatabaseOperations; -import com.redhat.services.ninja.entity.Database; -import com.redhat.services.ninja.entity.Event; -import com.redhat.services.ninja.entity.Scorecard; -import com.redhat.services.ninja.entity.User; +import com.redhat.services.ninja.entity.*; import org.eclipse.microprofile.config.inject.ConfigProperty; import javax.annotation.PostConstruct; @@ -37,11 +34,13 @@ public class DatabaseEngine { private Map users; private Map scorecards; private Map levels; + private Map periods; private Queue events; private Jsonb jsonb; private IdentifiableDatabaseOperations userOperations; private IdentifiableDatabaseOperations scorecardOperations; private IdentifiableDatabaseOperations levelOperations; + private IdentifiableDatabaseOperations periodOperations; private BasicDatabaseOperationsImpl eventOperations; @PostConstruct @@ -62,9 +61,11 @@ void init() throws IOException { users = database.getUsers().parallelStream().collect(Collectors.toMap(User::getUsername, Function.identity())); scorecards = database.getScorecards().parallelStream().collect(Collectors.toMap(Scorecard::getUsername, Function.identity())); levels = database.getLevels().stream().collect(Collectors.toMap(com.redhat.services.ninja.entity.Level::getIdentifier, Function.identity())); + periods = database.getHistory().stream().collect(Collectors.toMap(Period::getIdentifier, Function.identity())); events = new LinkedBlockingQueue<>(maxEvents); userOperations = new IdentifiableDatabaseOperationsImpl<>(users); levelOperations = new IdentifiableDatabaseOperationsImpl<>(levels); + periodOperations = new IdentifiableDatabaseOperationsImpl<>(periods); scorecardOperations = new IdentifiableDatabaseOperationsImpl<>(scorecards) { @Override public Scorecard create(Scorecard entity) { @@ -109,6 +110,7 @@ synchronized public void save() throws IOException { database.setScorecards(Set.copyOf(scorecards.values())); database.setLevels(new TreeSet<>(levels.values())); database.setEvents(List.copyOf(events)); + database.setHistory(new TreeSet<>(periods.values())); writeFile(database); } @@ -131,6 +133,10 @@ public IdentifiableDatabaseOperations getPeriodOperations() { + return periodOperations; + } + public IdentifiableDatabaseOperations getUserOperations() { return userOperations; } diff --git a/quarkus/data-service/src/test/java/com/redhat/services/ninja/data/controller/PeriodResourceTest.java b/quarkus/data-service/src/test/java/com/redhat/services/ninja/data/controller/PeriodResourceTest.java new file mode 100644 index 00000000..70945072 --- /dev/null +++ b/quarkus/data-service/src/test/java/com/redhat/services/ninja/data/controller/PeriodResourceTest.java @@ -0,0 +1,97 @@ +package com.redhat.services.ninja.data.controller; + +import com.data.services.ninja.test.AbstractResourceTest; +import com.redhat.services.ninja.entity.Level; +import com.redhat.services.ninja.entity.Period; +import com.redhat.services.ninja.entity.Scorecard; +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.common.mapper.TypeRef; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; +import java.util.SortedSet; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.*; + +@QuarkusTest +class PeriodResourceTest extends AbstractResourceTest { + + @Test + void create() { + Period period = new Period(); + period.setName("FY20"); + + SortedSet levels = ninjaDatabase.getLevels(); + + ninjaDatabase.getScorecards().forEach(s -> s.computeLevel(levels)); + + period.record(ninjaDatabase.getScorecards().toArray(new Scorecard[]{})); + + Period createdPeriod = given() + .contentType(ContentType.JSON) + .body(period) + .when() + .post("/data/period") + .as(Period.class); + + assertAll( + () -> assertEquals(period, createdPeriod) + ); + } + + @Test + void getAll() { + Map periods = getPeriods(); + + List allScorecards = given() + .when() + .get("/data/period") + .as(new TypeRef<>() { + }); + + assertAll( + () -> assertTrue(allScorecards.contains(periods.get("FY19"))) + ); + } + + @Test + void update() { + Map periods = getPeriods(); + + Period period = periods.get("FY18"); + + period.setName("FY17"); + + given() + .contentType(ContentType.JSON) + .body(period) + .when() + .put("/data/period") + .then() + .body("name", is("FY17")); + } + + @Test + void delete() { + Map periods = getPeriods(); + + Period period = periods.get("FY16"); + + Period scorecard = given() + .when() + .delete("/data/period/FY16") + .as(Period.class); + + assertEquals(period, scorecard); + } + + private Map getPeriods() { + return ninjaDatabase.getHistory().stream().collect(Collectors.toMap(Period::getIdentifier, Function.identity())); + } +} \ No newline at end of file diff --git a/quarkus/data-service/src/test/java/com/redhat/services/ninja/data/controller/ScorecardResourceTest.java b/quarkus/data-service/src/test/java/com/redhat/services/ninja/data/controller/ScorecardResourceTest.java index ea65c626..e0c27191 100644 --- a/quarkus/data-service/src/test/java/com/redhat/services/ninja/data/controller/ScorecardResourceTest.java +++ b/quarkus/data-service/src/test/java/com/redhat/services/ninja/data/controller/ScorecardResourceTest.java @@ -66,7 +66,7 @@ void update() { .when() .put("/data/scorecard") .then() - .body("total", equalTo(7)); + .body("total", equalTo(modestNinja.getTotal())); } @Test diff --git a/quarkus/entities/src/main/java/com/redhat/services/ninja/entity/Database.java b/quarkus/entities/src/main/java/com/redhat/services/ninja/entity/Database.java index fecb5471..23aaa559 100644 --- a/quarkus/entities/src/main/java/com/redhat/services/ninja/entity/Database.java +++ b/quarkus/entities/src/main/java/com/redhat/services/ninja/entity/Database.java @@ -9,6 +9,7 @@ public class Database { private Set scorecards = Set.of(); private SortedSet levels; private List events = List.of(); + private SortedSet history = new TreeSet<>(); public Database() { levels = new TreeSet<>(); @@ -51,6 +52,14 @@ public LocalDateTime getCreatedOn() { return createdOn; } + public SortedSet getHistory() { + return history; + } + + public void setHistory(SortedSet history) { + this.history = history; + } + public void setCreatedOn(LocalDateTime createdOn) { this.createdOn = createdOn; } diff --git a/quarkus/entities/src/main/java/com/redhat/services/ninja/entity/ErrorResponse.java b/quarkus/entities/src/main/java/com/redhat/services/ninja/entity/ErrorResponse.java index 0b6b4c02..c01126e3 100644 --- a/quarkus/entities/src/main/java/com/redhat/services/ninja/entity/ErrorResponse.java +++ b/quarkus/entities/src/main/java/com/redhat/services/ninja/entity/ErrorResponse.java @@ -4,7 +4,7 @@ import javax.json.bind.annotation.JsonbProperty; public class ErrorResponse { - private String message; + private final String message; @JsonbCreator public ErrorResponse(@JsonbProperty("message") String message) { diff --git a/quarkus/entities/src/main/java/com/redhat/services/ninja/entity/Level.java b/quarkus/entities/src/main/java/com/redhat/services/ninja/entity/Level.java index 02a7b16e..55370e95 100644 --- a/quarkus/entities/src/main/java/com/redhat/services/ninja/entity/Level.java +++ b/quarkus/entities/src/main/java/com/redhat/services/ninja/entity/Level.java @@ -49,8 +49,7 @@ public int compareTo(Level level) { } public enum KNOWN_LEVEL { - RED(40), ZERO(0), GREY(20), BLUE(5), BLACK(75); - + BLACK(75), RED(40), GREY(20), BLUE(5), ZERO(0); private final Level level; KNOWN_LEVEL(int minimumPoint) { diff --git a/quarkus/entities/src/main/java/com/redhat/services/ninja/entity/Period.java b/quarkus/entities/src/main/java/com/redhat/services/ninja/entity/Period.java new file mode 100644 index 00000000..4fe36660 --- /dev/null +++ b/quarkus/entities/src/main/java/com/redhat/services/ninja/entity/Period.java @@ -0,0 +1,91 @@ +package com.redhat.services.ninja.entity; + +import javax.json.bind.annotation.JsonbTransient; +import java.time.LocalDateTime; +import java.util.*; + +public class Period implements Comparable, Identifiable { + private String name; + private LocalDateTime cumulatedOn = LocalDateTime.now(); + + private Map records = new HashMap<>(); + + public Period() { + } + + public Period(String name, Map records) { + this.name = name; + setRecords(records); + } + + private static Map sort(Map records) { + Map newRecords = new HashMap<>(); + + records.entrySet().stream() + .sorted(Map.Entry.comparingByValue()) + .forEachOrdered(entry -> newRecords.put(entry.getKey(), entry.getValue())); + return newRecords; + } + + @Override + @JsonbTransient + public String getIdentifier() { + return name; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public LocalDateTime getCumulatedOn() { + return cumulatedOn; + } + + public void setCumulatedOn(LocalDateTime cumulatedOn) { + this.cumulatedOn = cumulatedOn; + } + + public Map getRecords() { + return Collections.unmodifiableMap(records); + } + + public void setRecords(Map records) { + this.records = sort(records); + } + + public void record(Scorecard... scorecards) { + Arrays.stream(scorecards).forEach(scorecard -> { + Record record = new Record(); + record.setLevel(scorecard.getLevel()); + record.setScore(scorecard.getTotal()); + + records.put(scorecard.getUsername(), record); + }); + + records = sort(records); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Period period = (Period) o; + return name.equals(period.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + + @Override + public int compareTo(Period period) { + Objects.requireNonNull(period, "Cannot compare with null value"); + + return -this.name.compareTo(period.name); + } +} diff --git a/quarkus/entities/src/main/java/com/redhat/services/ninja/entity/Record.java b/quarkus/entities/src/main/java/com/redhat/services/ninja/entity/Record.java new file mode 100644 index 00000000..0cf564c7 --- /dev/null +++ b/quarkus/entities/src/main/java/com/redhat/services/ninja/entity/Record.java @@ -0,0 +1,39 @@ +package com.redhat.services.ninja.entity; + +import java.util.Objects; + +public class Record implements Comparable { + private String level; + private int score; + + public Record() { + } + + public Record(String level, int score) { + this.level = level; + this.score = score; + } + + public String getLevel() { + return level; + } + + public void setLevel(String level) { + this.level = level; + } + + public int getScore() { + return score; + } + + public void setScore(int score) { + this.score = score; + } + + @Override + public int compareTo(Record record) { + Objects.requireNonNull(record, "Must not be null"); + + return -Integer.compare(this.score, record.score); + } +} diff --git a/quarkus/entities/src/main/java/com/redhat/services/ninja/entity/Scorecard.java b/quarkus/entities/src/main/java/com/redhat/services/ninja/entity/Scorecard.java index 2c799ecf..fb08c96d 100644 --- a/quarkus/entities/src/main/java/com/redhat/services/ninja/entity/Scorecard.java +++ b/quarkus/entities/src/main/java/com/redhat/services/ninja/entity/Scorecard.java @@ -10,6 +10,13 @@ public class Scorecard implements Identifiable { private int pointsToNextLevel = 0; private String nextLevel = ""; + public Scorecard() { + } + + public Scorecard(String username) { + this.username = username; + } + @Override @JsonbTransient public String getIdentifier() { diff --git a/quarkus/entities/src/main/java/com/redhat/services/ninja/entity/User.java b/quarkus/entities/src/main/java/com/redhat/services/ninja/entity/User.java index 1c0a37f5..cf4e4b85 100644 --- a/quarkus/entities/src/main/java/com/redhat/services/ninja/entity/User.java +++ b/quarkus/entities/src/main/java/com/redhat/services/ninja/entity/User.java @@ -6,7 +6,6 @@ public class User implements Identifiable{ private String username; private String displayName; - private String levelChanged; private String email; private String githubUsername; private String trelloUsername; @@ -34,14 +33,6 @@ public void setDisplayName(String displayName) { this.displayName = displayName; } - public String getLevelChanged() { - return levelChanged; - } - - public void setLevelChanged(String levelChanged) { - this.levelChanged = levelChanged; - } - public String getEmail() { return email; } diff --git a/quarkus/scorecard-service/src/main/java/com/redhat/services/ninja/controller/PeriodResource.java b/quarkus/scorecard-service/src/main/java/com/redhat/services/ninja/controller/PeriodResource.java new file mode 100644 index 00000000..b90d6a83 --- /dev/null +++ b/quarkus/scorecard-service/src/main/java/com/redhat/services/ninja/controller/PeriodResource.java @@ -0,0 +1,68 @@ +package com.redhat.services.ninja.controller; + +import com.redhat.services.ninja.client.PeriodClient; +import com.redhat.services.ninja.client.ScorecardClient; +import com.redhat.services.ninja.entity.Period; +import com.redhat.services.ninja.entity.Record; +import com.redhat.services.ninja.entity.Scorecard; +import org.eclipse.microprofile.rest.client.inject.RestClient; + +import javax.inject.Inject; +import javax.ws.rs.*; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +@Path("/history") +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +public class PeriodResource { + @Inject + @RestClient + PeriodClient periodClient; + + @Inject + @RestClient + ScorecardClient scorecardClient; + + @POST + @Path("/{periodName}") + @Consumes(MediaType.TEXT_PLAIN) + public Period create(@PathParam("periodName") String periodName) { + Optional.ofNullable(periodClient.get(periodName)).ifPresent(period -> { + throw new WebApplicationException("Period already exist", 409); + } + ); + + List scorecards = scorecardClient.getAll(); + + Map records = scorecards.stream() + .collect( + Collectors.toMap( + Scorecard::getIdentifier, + scorecard -> new Record(scorecard.getLevel(), scorecard.getTotal()))); + + Period period = new Period(periodName, records); + + periodClient.create(period); + + scorecards.stream().map(Scorecard::getIdentifier).forEach(scorecardClient::delete); + + scorecards.stream().map(Scorecard::getIdentifier).map(Scorecard::new).forEach(scorecardClient::create); + + return periodClient.create(period); + } + + @GET + @Path("/{identifier}") + public Period get(@PathParam("identifier") String identifier) { + return Optional.ofNullable(periodClient.get(identifier)) + .orElseThrow(() -> new WebApplicationException( + "Period for " + identifier + " NOT FOUND.", + Response.Status.NOT_FOUND + )); + } +} \ No newline at end of file diff --git a/quarkus/scorecard-service/src/test/java/com/redhat/services/ninja/controller/PeriodResourceTest.java b/quarkus/scorecard-service/src/test/java/com/redhat/services/ninja/controller/PeriodResourceTest.java new file mode 100644 index 00000000..ba3b1750 --- /dev/null +++ b/quarkus/scorecard-service/src/test/java/com/redhat/services/ninja/controller/PeriodResourceTest.java @@ -0,0 +1,86 @@ +package com.redhat.services.ninja.controller; + +import com.data.services.ninja.test.AbstractResourceTest; +import com.redhat.services.ninja.client.PeriodClient; +import com.redhat.services.ninja.client.ScorecardClient; +import com.redhat.services.ninja.entity.Period; +import com.redhat.services.ninja.entity.Scorecard; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.mockito.InjectMock; +import io.restassured.http.ContentType; +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; + +import static io.restassured.RestAssured.given; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@QuarkusTest +class PeriodResourceTest extends AbstractResourceTest { + @InjectMock + @RestClient + ScorecardClient scorecardClient; + + @InjectMock + @RestClient + PeriodClient periodClient; + + @BeforeEach + void initTest() { + when(scorecardClient.create(any(Scorecard.class))) + .thenAnswer(a -> a.getArgument(0)); + + when(scorecardClient.update(any(Scorecard.class))) + .thenAnswer(a -> a.getArgument(0)); + + when(scorecardClient.getAll()).thenAnswer(a -> new ArrayList<>(ninjaDatabase.getScorecards())); + + when(periodClient.create(any(Period.class))) + .thenAnswer(a -> a.getArgument(0)); + + when(periodClient.update(any(Period.class))) + .thenAnswer(a -> a.getArgument(0)); + + when(periodClient.get(any())).thenAnswer(a -> + ninjaDatabase.getHistory().stream() + .filter(p -> p.getName().equals(a.getArgument(0))).findAny().orElse(null) + ); + } + + @Test + void create() { + Period period = given() + .contentType(ContentType.TEXT) + .when() + .post("history/FY20") + .as(Period.class); + + assertAll( + () -> assertEquals("FY20", period.getIdentifier()), + () -> assertNotEquals(0, period.getRecords().size()) + ); + } + + @Test + void getExistingPeriod() { + Period period = given() + .when() + .get("history/FY19") + .as(Period.class); + + assertEquals("FY19", period.getIdentifier()); + } + + @Test + void getNonExistingScorecard() { + given() + .when() + .get("history/FY10") + .then() + .statusCode(404); + } +} \ No newline at end of file diff --git a/quarkus/test-helper/src/main/resources/com/redhat/services/ninja/test/database.json b/quarkus/test-helper/src/main/resources/com/redhat/services/ninja/test/database.json index 69562e2b..48249c2f 100644 --- a/quarkus/test-helper/src/main/resources/com/redhat/services/ninja/test/database.json +++ b/quarkus/test-helper/src/main/resources/com/redhat/services/ninja/test/database.json @@ -24,25 +24,27 @@ { "identifier": "super_ninja", "details": { - "Github": 2 + "Github": 20, + "Trello": 50, + "Merge": 10 }, - "total": 2, + "total": 80, "username": "super_ninja" }, { "identifier": "modest_ninja", "details": { - "Github": 2 + "Github": 30 }, - "total": 2, + "total": 30, "username": "modest_ninja" }, { "identifier": "retired_ninja", "details": { - "Github": 2 + "Github": 120 }, - "total": 2, + "total": 120, "username": "retired_ninja" } ], @@ -72,6 +74,62 @@ "username": "retired_ninja" } ], + "history": [ + { + "cumulatedOn": "2020-06-11T08:08:56.5899687", + "records": { + "retired_ninja": { + "score": 120, + "level": "BLACK" + }, + "super_ninja": { + "score": 80, + "level": "BLACK" + }, + "modest_ninja": { + "score": 30, + "level": "GREY" + } + }, + "name": "FY19" + }, + { + "cumulatedOn": "2019-06-11T08:08:56.5899687", + "records": { + "retired_ninja": { + "score": 120, + "level": "BLACK" + }, + "super_ninja": { + "score": 80, + "level": "BLACK" + }, + "modest_ninja": { + "score": 30, + "level": "GREY" + } + }, + "name": "FY18" + }, + { + "cumulatedOn": "2017-06-11T08:08:56.5899687", + "records": { + "retired_ninja": { + "score": 120, + "level": "BLACK" + }, + "super_ninja": { + "score": 80, + "level": "BLACK" + }, + "modest_ninja": { + "score": 30, + "level": "GREY" + } + }, + "name": "FY16" + } + ], "levels": [ { "minimumPoint": 0, From f60b1c36aabeff31f670b00a6834b264f63bb6f7 Mon Sep 17 00:00:00 2001 From: Adler Fleurant <2609856+AdlerFleurant@users.noreply.github.com> Date: Thu, 11 Jun 2020 10:30:36 -0400 Subject: [PATCH 2/2] Backend Quarkus Migration: Database Migration Helper Adding a migration path from old database. Signed-off-by: Adler Fleurant <2609856+AdlerFleurant@users.noreply.github.com> --- quarkus/migration/pom.xml | 15 +++ .../redhat/services/ninja/data/Migration.java | 95 ++++++++----------- quarkus/quarkus-pom/pom.xml | 1 + 3 files changed, 55 insertions(+), 56 deletions(-) create mode 100644 quarkus/migration/pom.xml diff --git a/quarkus/migration/pom.xml b/quarkus/migration/pom.xml new file mode 100644 index 00000000..b79d40bf --- /dev/null +++ b/quarkus/migration/pom.xml @@ -0,0 +1,15 @@ + + + 4.0.0 + + com.redhat.service.ninja + quarkus + 2.0.0-SNAPSHOT + ../quarkus-pom/pom.xml + + migration + 2.0.0-SNAPSHOT + Migration + A project to migrate from an old ninja database + \ No newline at end of file diff --git a/quarkus/migration/src/main/java/com/redhat/services/ninja/data/Migration.java b/quarkus/migration/src/main/java/com/redhat/services/ninja/data/Migration.java index f762e33d..1283c37c 100644 --- a/quarkus/migration/src/main/java/com/redhat/services/ninja/data/Migration.java +++ b/quarkus/migration/src/main/java/com/redhat/services/ninja/data/Migration.java @@ -1,13 +1,8 @@ package com.redhat.services.ninja.data; -import com.redhat.services.ninja.entity.Event; -import com.redhat.services.ninja.entity.Scorecard; -import com.redhat.services.ninja.entity.User; - -import javax.json.Json; -import javax.json.JsonNumber; -import javax.json.JsonObject; -import javax.json.JsonValue; +import com.redhat.services.ninja.entity.*; + +import javax.json.*; import javax.json.bind.Jsonb; import javax.json.bind.JsonbBuilder; import javax.json.bind.JsonbConfig; @@ -18,28 +13,25 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.time.LocalDateTime; -import java.util.List; import java.util.Map; import java.util.Set; +import java.util.TreeSet; import java.util.stream.Collectors; public class Migration { public static void main(String... args) throws IOException { + Database database = new Database(); InputStream jsonInputStream = Files.newInputStream(Path.of("old-ninja-db.json")); JsonObject oldDatabase = Json.createReader(jsonInputStream).readObject(); - Migration migration = new Migration(); - - migration.users = oldDatabase.getJsonObject("users") + var users = oldDatabase.getJsonObject("users") .values().stream() .map(JsonObject.class::cast) .map(jo -> { User user = new User(); - user.setLevel(jo.getString("level")); user.setDisplayName(jo.getString("displayName", "")); user.setGithubUsername(jo.getString("githubId", "")); - user.setLevelChanged(jo.getString("levelChanged")); user.setEmail(jo.getString("email")); user.setTrelloUsername(jo.getString("trelloId", "")); user.setUsername(jo.getString("username")); @@ -47,10 +39,13 @@ public static void main(String... args) throws IOException { return user; }).collect(Collectors.toSet()); + + database.setUsers(users); - migration.createdOn = LocalDateTime.parse(oldDatabase.getString("created")); + var createdOn = LocalDateTime.parse(oldDatabase.getString("created")); + database.setCreatedOn(createdOn); - migration.scorecards = oldDatabase.getJsonObject("scoreCards").entrySet().stream() + var scorecards = oldDatabase.getJsonObject("scoreCards").entrySet().stream() .map(e -> { JsonObject jo = (JsonObject) e.getValue(); @@ -61,11 +56,14 @@ public static void main(String... args) throws IOException { Scorecard scorecard = new Scorecard(); scorecard.setUsername(e.getKey()); - scorecard.setPointMap(scores); + scorecard.setDetails(scores); + scorecard.computeLevel(database.getLevels()); return scorecard; }).collect(Collectors.toSet()); + + database.setScorecards(scorecards); - migration.events = oldDatabase.getJsonArray("events").stream() + var events = oldDatabase.getJsonArray("events").stream() .map(JsonValue::asJsonObject) .map(jo -> { Event event = new Event(); @@ -77,49 +75,34 @@ public static void main(String... args) throws IOException { return event; }).collect(Collectors.toList()); + + database.setEvents(events); + + Set history = oldDatabase.getJsonObject("scorecardHistory").entrySet().stream() + .map(entry -> { + Map records = entry.getValue().asJsonObject().entrySet().stream() + .map(record -> + { + String recordDetail = ((JsonString) record.getValue()).getString(); + String[] splitDetail = recordDetail.split("\\|"); + + return Map.entry( + record.getKey(), + new Record(splitDetail[0], Integer.parseInt(splitDetail[1])) + ); + } + ).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + + return new Period(entry.getKey(), records); + }).collect(Collectors.toSet()); + + database.setHistory(new TreeSet<>(history)); JsonbConfig config = new JsonbConfig().withFormatting(true).withPropertyOrderStrategy(PropertyOrderStrategy.ANY); Jsonb jsonb = JsonbBuilder.newBuilder().withConfig(config).build(); Path ninjaPath = Paths.get("new-ninja-db.json"); - String content = jsonb.toJson(migration); + String content = jsonb.toJson(database); Files.writeString(ninjaPath, content); } - - private LocalDateTime createdOn = LocalDateTime.now(); - private Set users; - private Set scorecards; - private List events; - - public LocalDateTime getCreatedOn() { - return createdOn; - } - - public void setCreatedOn(LocalDateTime createdOn) { - this.createdOn = createdOn; - } - - public Set getUsers() { - return users; - } - - public void setUsers(Set users) { - this.users = users; - } - - public Set getScorecards() { - return scorecards; - } - - public void setScorecards(Set scorecards) { - this.scorecards = scorecards; - } - - public List getEvents() { - return events; - } - - public void setEvents(List events) { - this.events = events; - } } diff --git a/quarkus/quarkus-pom/pom.xml b/quarkus/quarkus-pom/pom.xml index 1f57c587..9529ea82 100644 --- a/quarkus/quarkus-pom/pom.xml +++ b/quarkus/quarkus-pom/pom.xml @@ -139,5 +139,6 @@ ../user-service ../scorecard-service ../all-service + ../migration \ No newline at end of file