Skip to content

Commit

Permalink
Add ADR support to CalmHub (#798)
Browse files Browse the repository at this point in the history
* Add ADR counter, get ADRs and create ADR endpoints (#716)

* Add get ADR revisions endpoint (#716)

* Add get ADR Revision endpoint (#716)

* Add Get ADR endpoint (#716)

* Add update ADR endpoint

* Add ADR model (#716)

* Use ADR object when getting latest revision (#716)

* Refactor ADRStore (#716
)

* Add endpoint to change ADR status. Added missing tests. Refactored. (#716)

* Rename paramatarized test (#716)

* Refactoring to create exception handler. Also creating adr domain package, (#716)

* Rename ADR types (#716)
  • Loading branch information
grahampacker-ms authored Jan 21, 2025
1 parent 1d418fd commit c73a1fe
Show file tree
Hide file tree
Showing 23 changed files with 1,906 additions and 3 deletions.
4 changes: 2 additions & 2 deletions calm-hub/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
*.iml
# *.ipr

# CMake
Expand Down Expand Up @@ -316,4 +316,4 @@ thumb
sketch


# End of https://www.toptal.com/developers/gitignore/api/intellij,react,node,java,maven
# End of https://www.toptal.com/developers/gitignore/api/intellij,react,node,java,maven
11 changes: 11 additions & 0 deletions calm-hub/mongo/init-mongo.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,17 @@ if (db.counters.countDocuments({ _id: "architectureStoreCounter" }) === 1) {
print("architectureStoreCounter already exists, no initialization needed");
}

if (db.counters.countDocuments({ _id: "adrStoreCounter" }) === 1) {
db.counters.insertOne({
_id: "adrStoreCounter",
sequence_value: 1
});
print("Initialized adrStoreCounter with sequence_value 1");
} else {
print("adrStoreCounter already exists, no initialization needed");
}


if (db.counters.countDocuments({ _id: "flowStoreCounter" }) === 1) {
db.counters.insertOne({
_id: "flowStoreCounter",
Expand Down
30 changes: 30 additions & 0 deletions calm-hub/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,16 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-mongodb-client</artifactId>
</dependency>
<dependency>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder-core</artifactId>
<version>44</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>

<!-- Testing -->
<dependency>
Expand Down Expand Up @@ -165,6 +175,11 @@
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.12</version>
<configuration>
<excludes>
<exclude>**/*Builder.*</exclude>
</excludes>
</configuration>
<executions>
<execution>
<id>default-prepare-agent</id>
Expand Down Expand Up @@ -200,6 +215,21 @@
</execution>
</executions>
</plugin>

<!-- Record Builder Plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder-processor</artifactId>
<version>44</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
<profiles>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
package integration;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import com.mongodb.client.MongoDatabase;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.TestProfile;
import org.bson.Document;
import org.eclipse.microprofile.config.ConfigProvider;
import org.finos.calm.domain.adr.Adr;
import org.finos.calm.domain.adr.AdrMeta;
import org.finos.calm.domain.adr.AdrMetaBuilder;
import org.finos.calm.domain.adr.Decision;
import org.finos.calm.domain.adr.DecisionBuilder;
import org.finos.calm.domain.adr.LinkBuilder;
import org.finos.calm.domain.adr.NewAdrRequest;
import org.finos.calm.domain.adr.NewAdrRequestBuilder;
import org.finos.calm.domain.adr.Option;
import org.finos.calm.domain.adr.OptionBuilder;
import org.finos.calm.domain.adr.Status;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.List;

import com.fasterxml.jackson.databind.ObjectMapper;
import static integration.MongoSetup.counterSetup;
import static integration.MongoSetup.namespaceSetup;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;
import static org.junit.jupiter.api.Assertions.assertEquals;

@QuarkusTest
@TestProfile(IntegrationTestProfile.class)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class MongoAdrIntegration {

private ObjectMapper objectMapper;

private static final Logger logger = LoggerFactory.getLogger(MongoAdrIntegration.class);

private final String TITLE = "My ADR";
private final String PROBLEM_STATEMENT = "My problem is...";
private final List<String> DECISION_DRIVERS = List.of("a", "b", "c");
private final Option OPTION_A = OptionBuilder.builder().name("Option 1").description("optionDescription")
.positiveConsequences(List.of("a")).negativeConsequences(List.of("b")).build();
private final Option OPTION_B = OptionBuilder.builder().name("Option 2").description("optionDescription")
.positiveConsequences(List.of("c")).negativeConsequences(List.of("d")).build();
private final List<Option> CONSIDERED_OPTIONS = List.of(OPTION_A, OPTION_B);
private final String RATIONALE = "This is the best option";
private final Decision DECISION_OUTCOME = DecisionBuilder.builder()
.rationale(RATIONALE)
.chosenOption(OPTION_A)
.build();

private final NewAdrRequest newAdr = NewAdrRequestBuilder.builder()
.title(TITLE)
.contextAndProblemStatement(PROBLEM_STATEMENT)
.decisionDrivers(DECISION_DRIVERS)
.consideredOptions(CONSIDERED_OPTIONS)
.decisionOutcome(DECISION_OUTCOME)
.links(List.of(LinkBuilder.builder().rel("abc").href("http://abc.com").build()))
.build();

private final Adr adr = Adr.builderFromNewAdr(newAdr).status(Status.draft).build();

@BeforeEach
public void setupAdrs() {
String mongoUri = ConfigProvider.getConfig().getValue("quarkus.mongodb.connection-string", String.class);

// Safeguard: Fail fast if URI is not set
if(mongoUri == null || mongoUri.isBlank()) {
logger.error("MongoDB URI is not set. Check the EndToEndResource configuration.");
throw new IllegalStateException("MongoDB URI is not set. Check the EndToEndResource configuration.");
}

try(MongoClient mongoClient = MongoClients.create(mongoUri)) {
MongoDatabase database = mongoClient.getDatabase("calmSchemas");

if(!database.listCollectionNames().into(new ArrayList<>()).contains("adrs")) {
database.createCollection("adrs");
database.getCollection("adrs").insertOne(
new Document("namespace", "finos").append("adrs", new ArrayList<>())
);
}

counterSetup(database);
namespaceSetup(database);
}
this.objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
}

@Test
@Order(1)
void end_to_end_verify_get_with_no_architecture() {
given()
.when().get("/calm/namespaces/finos/adrs")
.then()
.statusCode(200)
.body("values", empty());
}

@Test
@Order(2)
void end_to_end_verify_create_an_adr() throws JsonProcessingException {
given()
.body(objectMapper.writeValueAsString(newAdr))
.header("Content-Type", "application/json")
.when().post("/calm/namespaces/finos/adrs")
.then()
.statusCode(201)
.header("Location", containsString("calm/namespaces/finos/adrs/1"));
}

@Test
@Order(3)
void end_to_end_verify_get_adr_revision() throws JsonProcessingException {
AdrMeta expectedAdrMeta = AdrMetaBuilder.builder()
.namespace("finos")
.id(1)
.revision(1)
.adrContent(adr)
.build();

AdrMeta actualAdrMeta = given()
.when().get("/calm/namespaces/finos/adrs/1/revisions/1")
.then()
.statusCode(200)
.extract()
.body()
.as(AdrMeta.class);
assertEquals(expectedAdrMeta, actualAdrMeta);
}

@Test
@Order(4)
void end_to_end_verify_get_adr() {
AdrMeta actualAdrMeta = given()
.when().get("/calm/namespaces/finos/adrs/1")
.then()
.statusCode(200)
.extract()
.body()
.as(AdrMeta.class);

AdrMeta expectedAdrMeta = AdrMetaBuilder.builder()
.namespace("finos")
.id(1)
.revision(1)
.adrContent(adr)
.build();
assertEquals(expectedAdrMeta, actualAdrMeta);
}

@Test
@Order(5)
void end_to_end_verify_update_an_adr() throws JsonProcessingException {
given()
.body(objectMapper.writeValueAsString(newAdr))
.header("Content-Type", "application/json")
.when().post("/calm/namespaces/finos/adrs/1")
.then()
.statusCode(201)
.header("Location", containsString("calm/namespaces/finos/adrs/1"));
}

@Test
@Order(6)
void end_to_end_verify_get_revisions() {
given()
.when().get("/calm/namespaces/finos/adrs/1/revisions")
.then()
.statusCode(200)
.body("values", hasSize(2))
.body("values[0]", equalTo(1))
.body("values[1]", equalTo(2));

}

@Test
@Order(7)
void end_to_end_verify_update_an_adr_status() throws JsonProcessingException {
given()
.when().post("/calm/namespaces/finos/adrs/1/status/proposed")
.then()
.statusCode(201)
.header("Location", containsString("calm/namespaces/finos/adrs/1"));
}

@Test
@Order(8)
void end_to_end_verify_status_changed() throws JsonProcessingException {

given()
.when().get("/calm/namespaces/finos/adrs/1/revisions/3")
.then()
.statusCode(200)
.body("adrContent.status", equalTo("proposed"));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import com.mongodb.client.MongoDatabase;
import io.quarkus.test.common.QuarkusTestResource;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.TestProfile;
import org.bson.Document;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@ public static void counterSetup(MongoDatabase database) {
database.createCollection("counters");
Document patternStoreCounter = new Document("_id", "patternStoreCounter").append("sequence_value", 0);
Document architectureStoreCounter = new Document("_id", "architectureStoreCounter").append("sequence_value", 0);
Document adrStoreCounter = new Document("_id", "adrStoreCounter").append("sequence_value", 0);
database.getCollection("counters").insertOne(patternStoreCounter);
database.getCollection("counters").insertOne(architectureStoreCounter);
database.getCollection("counters").insertOne(adrStoreCounter);
}
}
}
61 changes: 61 additions & 0 deletions calm-hub/src/main/java/org/finos/calm/domain/adr/Adr.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package org.finos.calm.domain.adr;

import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import io.soabase.recordbuilder.core.RecordBuilder;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;

@RecordBuilder.Options(enableWither = false)
@RecordBuilder
public record Adr(
String title,
Status status,
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@JsonSerialize(using = LocalDateTimeSerializer.class)
LocalDateTime creationDateTime,
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@JsonSerialize(using = LocalDateTimeSerializer.class)
LocalDateTime updateDateTime,
String contextAndProblemStatement,
List<String> decisionDrivers,
List<Option> consideredOptions,
Decision decisionOutcome,
List<Link> links

) {

public static AdrBuilder builderFromNewAdr(NewAdrRequest newAdrRequest) {
return AdrBuilder.builder()
.title(newAdrRequest.title())
.contextAndProblemStatement(newAdrRequest.contextAndProblemStatement())
.decisionDrivers(newAdrRequest.decisionDrivers())
.consideredOptions(newAdrRequest.consideredOptions())
.decisionOutcome(newAdrRequest.decisionOutcome())
.links(newAdrRequest.links());
}

// does not include datetimes in equals
@Override
public boolean equals(Object o) {
if(this == o) return true;
if(o == null || getClass() != o.getClass()) return false;
Adr that = (Adr) o;
return Objects.equals(title, that.title) &&
status == that.status &&
Objects.equals(contextAndProblemStatement, that.contextAndProblemStatement) &&
Objects.equals(decisionDrivers, that.decisionDrivers) &&
Objects.equals(consideredOptions, that.consideredOptions) &&
Objects.equals(decisionOutcome, that.decisionOutcome) &&
Objects.equals(links, that.links);
}

@Override
public int hashCode() {
return Objects.hash(title, status, contextAndProblemStatement, decisionDrivers, consideredOptions, decisionOutcome, links);
}
}
13 changes: 13 additions & 0 deletions calm-hub/src/main/java/org/finos/calm/domain/adr/AdrMeta.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.finos.calm.domain.adr;

import io.soabase.recordbuilder.core.RecordBuilder;

/**
* Represents an ADR and the associated namespace, id, and revision.
* The ADR is represented as a String in JSON format.
*/
@RecordBuilder.Options(enableWither = false)
@RecordBuilder
public record AdrMeta(String namespace, int id, int revision, Adr adrContent) {

}
Loading

0 comments on commit c73a1fe

Please sign in to comment.