From dedde2d7056d50677af66b8f06618bfc75bdaf44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Antonio=20Bre=C3=B1a=20Moral?= Date: Wed, 8 Apr 2026 20:50:58 +0200 Subject: [PATCH] test(cats): Testing one example created against cats --- .cursor/agents/robot-micronaut-coder.md | 1 + .cursor/agents/robot-quarkus-coder.md | 1 + .cursor/agents/robot-spring-boot-coder.md | 1 + .../problem1/implementation/pom.xml | 47 +++ .../jab/ms/algorithm/UnicodeAggregator.java | 44 +-- .../info/jab/ms/client/GodDataClient.java | 109 +++---- .../client/LoggingOutboundCallObserver.java | 27 ++ .../jab/ms/client/OutboundCallObserver.java | 12 + .../ms/client/OutboundSourceException.java | 10 + .../jab/ms/config/GodOutboundProperties.java | 23 +- .../info/jab/ms/config/HttpClientConfig.java | 33 +- .../ms/controller/GlobalExceptionHandler.java | 69 +++- .../jab/ms/controller/GodStatsController.java | 84 ++--- .../info/jab/ms/dto/GodStatsResponse.java | 3 - .../jab/ms/exception/BadRequestException.java | 13 +- .../jab/ms/service/GodAnalysisService.java | 107 ++++--- .../java/info/jab/ms/service/GodData.java | 6 + .../service/InMemoryPantheonDataSource.java | 32 ++ .../jab/ms/service/PantheonDataSource.java | 9 + .../java/info/jab/ms/MainApplicationTest.java | 15 + .../ms/algorithm/UnicodeAggregatorTest.java | 38 ++- .../info/jab/ms/client/GodDataClientTest.java | 80 ----- .../jab/ms/controller/GodAnalysisApiAT.java | 83 ----- .../jab/ms/controller/GodAnalysisApiIT.java | 49 --- .../GodAnalysisPartialTimeoutIT.java | 91 ------ .../ms/controller/GodStatsControllerAT.java | 161 ++++++++++ .../GodStatsControllerErrorHandlingTest.java | 73 ----- .../ms/service/GodAnalysisServiceTest.java | 95 ++++-- ...R-003-God-Analysis-API-Technology-Stack.md | 51 +-- .../US-001-god-analysis-api.openapi.yaml | 87 ++++- .../US-001-plan-analysis-original.plan.md | 91 ++++++ .../agile/US-001-plan-analysis.plan.md | 298 +++++++----------- .../diagrams/c4-level1-context.png | Bin 33716 -> 0 bytes .../diagrams/c4-level1-context.puml | 20 -- .../diagrams/c4-level2-container.png | Bin 33123 -> 0 bytes .../diagrams/c4-level2-container.puml | 22 -- .../diagrams/c4-level3-component.png | Bin 66292 -> 0 bytes .../diagrams/c4-level3-component.puml | 39 --- .../2026-04-06-add-spring-actuator/design.md | 24 -- .../proposal.md | 18 -- .../specs/god-analysis-api/spec.md | 29 -- .../2026-04-06-add-spring-actuator/tasks.md | 8 - .../.openspec.yaml | 2 - .../design.md | 27 -- .../proposal.md | 18 -- .../specs/god-analysis-api/spec.md | 29 -- .../tasks.md | 9 - .../design.md | 56 ---- .../proposal.md | 51 --- .../specs/god-analysis-api/spec.md | 103 ------ .../tasks.md | 95 ------ .../us-001-a1-api-boundary/proposal.md | 21 ++ .../specs/god-analysis-api/spec.md | 27 ++ .../changes/us-001-a1-api-boundary/tasks.md | 8 + .../us-001-a2-domain-service/proposal.md | 21 ++ .../specs/god-analysis-api/spec.md | 27 ++ .../changes/us-001-a2-domain-service/tasks.md | 7 + .../proposal.md | 21 ++ .../specs/god-analysis-api/spec.md | 27 ++ .../us-001-a3-outbound-integration/tasks.md | 7 + .../src/main/resources/skills/701-skill.xml | 10 +- skills/701-technologies-openapi/SKILL.md | 10 +- 62 files changed, 1144 insertions(+), 1435 deletions(-) create mode 100644 examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/client/LoggingOutboundCallObserver.java create mode 100644 examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/client/OutboundCallObserver.java create mode 100644 examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/client/OutboundSourceException.java delete mode 100644 examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/dto/GodStatsResponse.java create mode 100644 examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/service/GodData.java create mode 100644 examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/service/InMemoryPantheonDataSource.java create mode 100644 examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/service/PantheonDataSource.java create mode 100644 examples/requirements-examples/problem1/implementation/src/test/java/info/jab/ms/MainApplicationTest.java delete mode 100644 examples/requirements-examples/problem1/implementation/src/test/java/info/jab/ms/client/GodDataClientTest.java delete mode 100644 examples/requirements-examples/problem1/implementation/src/test/java/info/jab/ms/controller/GodAnalysisApiAT.java delete mode 100644 examples/requirements-examples/problem1/implementation/src/test/java/info/jab/ms/controller/GodAnalysisApiIT.java delete mode 100644 examples/requirements-examples/problem1/implementation/src/test/java/info/jab/ms/controller/GodAnalysisPartialTimeoutIT.java create mode 100644 examples/requirements-examples/problem1/implementation/src/test/java/info/jab/ms/controller/GodStatsControllerAT.java delete mode 100644 examples/requirements-examples/problem1/implementation/src/test/java/info/jab/ms/controller/GodStatsControllerErrorHandlingTest.java create mode 100644 examples/requirements-examples/problem1/requirements/agile/US-001-plan-analysis-original.plan.md delete mode 100644 examples/requirements-examples/problem1/requirements/diagrams/c4-level1-context.png delete mode 100644 examples/requirements-examples/problem1/requirements/diagrams/c4-level1-context.puml delete mode 100644 examples/requirements-examples/problem1/requirements/diagrams/c4-level2-container.png delete mode 100644 examples/requirements-examples/problem1/requirements/diagrams/c4-level2-container.puml delete mode 100644 examples/requirements-examples/problem1/requirements/diagrams/c4-level3-component.png delete mode 100644 examples/requirements-examples/problem1/requirements/diagrams/c4-level3-component.puml delete mode 100644 examples/requirements-examples/problem1/requirements/openspec/changes/archive/2026-04-06-add-spring-actuator/design.md delete mode 100644 examples/requirements-examples/problem1/requirements/openspec/changes/archive/2026-04-06-add-spring-actuator/proposal.md delete mode 100644 examples/requirements-examples/problem1/requirements/openspec/changes/archive/2026-04-06-add-spring-actuator/specs/god-analysis-api/spec.md delete mode 100644 examples/requirements-examples/problem1/requirements/openspec/changes/archive/2026-04-06-add-spring-actuator/tasks.md delete mode 100644 examples/requirements-examples/problem1/requirements/openspec/changes/archive/2026-04-06-remove-spring-actuator/.openspec.yaml delete mode 100644 examples/requirements-examples/problem1/requirements/openspec/changes/archive/2026-04-06-remove-spring-actuator/design.md delete mode 100644 examples/requirements-examples/problem1/requirements/openspec/changes/archive/2026-04-06-remove-spring-actuator/proposal.md delete mode 100644 examples/requirements-examples/problem1/requirements/openspec/changes/archive/2026-04-06-remove-spring-actuator/specs/god-analysis-api/spec.md delete mode 100644 examples/requirements-examples/problem1/requirements/openspec/changes/archive/2026-04-06-remove-spring-actuator/tasks.md delete mode 100644 examples/requirements-examples/problem1/requirements/openspec/changes/archive/2026-04-06-us-001-god-analysis-api/design.md delete mode 100644 examples/requirements-examples/problem1/requirements/openspec/changes/archive/2026-04-06-us-001-god-analysis-api/proposal.md delete mode 100644 examples/requirements-examples/problem1/requirements/openspec/changes/archive/2026-04-06-us-001-god-analysis-api/specs/god-analysis-api/spec.md delete mode 100644 examples/requirements-examples/problem1/requirements/openspec/changes/archive/2026-04-06-us-001-god-analysis-api/tasks.md create mode 100644 examples/requirements-examples/problem1/requirements/openspec/changes/us-001-a1-api-boundary/proposal.md create mode 100644 examples/requirements-examples/problem1/requirements/openspec/changes/us-001-a1-api-boundary/specs/god-analysis-api/spec.md create mode 100644 examples/requirements-examples/problem1/requirements/openspec/changes/us-001-a1-api-boundary/tasks.md create mode 100644 examples/requirements-examples/problem1/requirements/openspec/changes/us-001-a2-domain-service/proposal.md create mode 100644 examples/requirements-examples/problem1/requirements/openspec/changes/us-001-a2-domain-service/specs/god-analysis-api/spec.md create mode 100644 examples/requirements-examples/problem1/requirements/openspec/changes/us-001-a2-domain-service/tasks.md create mode 100644 examples/requirements-examples/problem1/requirements/openspec/changes/us-001-a3-outbound-integration/proposal.md create mode 100644 examples/requirements-examples/problem1/requirements/openspec/changes/us-001-a3-outbound-integration/specs/god-analysis-api/spec.md create mode 100644 examples/requirements-examples/problem1/requirements/openspec/changes/us-001-a3-outbound-integration/tasks.md diff --git a/.cursor/agents/robot-micronaut-coder.md b/.cursor/agents/robot-micronaut-coder.md index f03b36ff..b9c0a878 100644 --- a/.cursor/agents/robot-micronaut-coder.md +++ b/.cursor/agents/robot-micronaut-coder.md @@ -34,6 +34,7 @@ Apply guidance from these Skills when relevant: - `@521-frameworks-micronaut-testing-unit-tests`: Micronaut unit testing - `@522-frameworks-micronaut-testing-integration-tests`: Micronaut integration testing - `@523-frameworks-micronaut-testing-acceptance-tests`: Micronaut acceptance testing +- `@702-technologies-wiremock`: Improve tests with Wiremock ### Workflow diff --git a/.cursor/agents/robot-quarkus-coder.md b/.cursor/agents/robot-quarkus-coder.md index c4d06f0b..fd957a7c 100644 --- a/.cursor/agents/robot-quarkus-coder.md +++ b/.cursor/agents/robot-quarkus-coder.md @@ -33,6 +33,7 @@ Apply guidance from these Skills when relevant: - `@421-frameworks-quarkus-testing-unit-tests`: Quarkus Unit Testing - `@422-frameworks-quarkus-testing-integration-tests`: Quarkus integration testing - `@423-frameworks-quarkus-testing-acceptance-tests`: Quarkus acceptance testing +- `@702-technologies-wiremock`: Improve tests with Wiremock ### Workflow diff --git a/.cursor/agents/robot-spring-boot-coder.md b/.cursor/agents/robot-spring-boot-coder.md index 1ce065ac..038e00cc 100644 --- a/.cursor/agents/robot-spring-boot-coder.md +++ b/.cursor/agents/robot-spring-boot-coder.md @@ -37,6 +37,7 @@ Apply guidance from these Skills when relevant: - `@321-frameworks-spring-boot-testing-unit-tests`: Spring Boot unit testing - `@322-frameworks-spring-boot-testing-integration-tests`: Spring Boot integration testing - `@323-frameworks-spring-boot-testing-acceptance-tests`: Spring Boot acceptance testing +- `@702-technologies-wiremock`: Improve tests with Wiremock ### Workflow diff --git a/examples/requirements-examples/problem1/implementation/pom.xml b/examples/requirements-examples/problem1/implementation/pom.xml index af3c7bc4..e4f45534 100644 --- a/examples/requirements-examples/problem1/implementation/pom.xml +++ b/examples/requirements-examples/problem1/implementation/pom.xml @@ -23,6 +23,18 @@ org.springframework.boot spring-boot-starter-webmvc + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-json + + + com.fasterxml.jackson.core + jackson-databind + org.springframework.boot spring-boot-starter-webmvc-test @@ -37,6 +49,41 @@ + + org.openapitools + openapi-generator-maven-plugin + 7.15.0 + + + generate-api-boundary + generate-sources + + generate + + + ${project.basedir}/../requirements/agile/US-001-god-analysis-api.openapi.yaml + spring + info.jab.ms.api + info.jab.ms.api.model + false + false + false + false + + java8 + true + true + true + true + true + false + none + none + + + + + org.apache.maven.plugins maven-surefire-plugin diff --git a/examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/algorithm/UnicodeAggregator.java b/examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/algorithm/UnicodeAggregator.java index 96e486b6..245cfcdf 100644 --- a/examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/algorithm/UnicodeAggregator.java +++ b/examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/algorithm/UnicodeAggregator.java @@ -1,42 +1,16 @@ package info.jab.ms.algorithm; import java.math.BigInteger; +import java.util.Objects; +import org.springframework.stereotype.Component; -/** - * Concatenates decimal digits of each Unicode code point in a name, parses to {@link BigInteger}, - * and sums matching names. First-code-point filter uses case-insensitive comparison so the pinned - * acceptance sum aligns with upstream data (e.g. {@code filter=n} matches {@code Nike}). - */ +@Component public final class UnicodeAggregator { - private UnicodeAggregator() {} - - public static BigInteger sumFiltered(String filter, Iterable names) { - if (filter == null || filter.isEmpty()) { - return BigInteger.ZERO; - } - int filterCp = filter.codePointAt(0); - BigInteger total = BigInteger.ZERO; - for (String name : names) { - if (name == null || name.isEmpty()) { - continue; - } - int firstCp = name.codePointAt(0); - if (!firstCodePointMatchesFilter(firstCp, filterCp)) { - continue; - } - total = total.add(nameToBigInteger(name)); - } - return total; - } - - static boolean firstCodePointMatchesFilter(int nameFirstCodePoint, int filterCodePoint) { - return Character.toLowerCase(nameFirstCodePoint) == Character.toLowerCase(filterCodePoint); - } - - static BigInteger nameToBigInteger(String name) { - StringBuilder digits = new StringBuilder(); - name.codePoints().forEach(cp -> digits.append(Integer.toString(cp))); - return new BigInteger(digits.toString()); - } + public BigInteger toBigInteger(String value) { + Objects.requireNonNull(value, "value must not be null"); + return value.codePoints() + .mapToObj(BigInteger::valueOf) + .reduce(BigInteger.ZERO, BigInteger::add); + } } diff --git a/examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/client/GodDataClient.java b/examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/client/GodDataClient.java index 1e6f40f4..575def0d 100644 --- a/examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/client/GodDataClient.java +++ b/examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/client/GodDataClient.java @@ -1,75 +1,58 @@ package info.jab.ms.client; +import info.jab.ms.algorithm.UnicodeAggregator; +import info.jab.ms.api.model.PantheonSource; import info.jab.ms.config.GodOutboundProperties; +import info.jab.ms.service.GodData; +import info.jab.ms.service.PantheonDataSource; import java.util.List; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.http.MediaType; import org.springframework.core.ParameterizedTypeReference; import org.springframework.stereotype.Component; import org.springframework.web.client.RestClient; import org.springframework.web.client.RestClientException; @Component -public class GodDataClient { - - private static final Logger log = LoggerFactory.getLogger(GodDataClient.class); - - private static final ParameterizedTypeReference> STRING_LIST = - new ParameterizedTypeReference<>() {}; - - private final RestClient restClient; - private final GodOutboundProperties properties; - - public GodDataClient( - @Qualifier("godRestClient") RestClient restClient, GodOutboundProperties properties) { - this.restClient = restClient; - this.properties = properties; - } - - /** - * Single GET to the configured URL for the pantheon. On connect/read failure or non-2xx, returns - * an empty list (no retries). - */ - public List fetchNames(String pantheonKey) { - String url = urlFor(pantheonKey); - if (url == null || url.isBlank()) { - log.warn("god.outbound.fetch.skipped source={} reason=no_url", pantheonKey); - return List.of(); - } - log.debug("god.outbound.fetch.start source={} url={}", pantheonKey, url); - try { - List names = - restClient.get().uri(url).retrieve().body(STRING_LIST); - if (names == null) { - log.warn("god.outbound.fetch.empty_body source={}", pantheonKey); - return List.of(); - } - log.info( - "god.outbound.fetch.ok source={} url={} nameCount={}", - pantheonKey, - url, - names.size()); - return names; - } catch (RestClientException ex) { - log.warn( - "god.outbound.fetch.failed source={} url={} error={}", - pantheonKey, - url, - ex.toString()); - return List.of(); - } - } - - private String urlFor(String pantheonKey) { - if (properties.urls() == null) { - return null; - } - return switch (pantheonKey) { - case "greek" -> properties.urls().greek(); - case "roman" -> properties.urls().roman(); - case "nordic" -> properties.urls().nordic(); - default -> null; - }; - } +public class GodDataClient implements PantheonDataSource { + + private static final ParameterizedTypeReference> GOD_NAMES_TYPE = new ParameterizedTypeReference<>() { + }; + + private final RestClient restClient; + private final GodOutboundProperties properties; + private final UnicodeAggregator unicodeAggregator; + private final OutboundCallObserver outboundCallObserver; + + public GodDataClient( + @Qualifier("godOutboundRestClient") RestClient restClient, + GodOutboundProperties properties, + UnicodeAggregator unicodeAggregator, + OutboundCallObserver outboundCallObserver + ) { + this.restClient = restClient; + this.properties = properties; + this.unicodeAggregator = unicodeAggregator; + this.outboundCallObserver = outboundCallObserver; + } + + @Override + public List fetch(PantheonSource source) { + outboundCallObserver.onStart(source); + try { + var names = restClient.get() + .uri(properties.getUrlFor(source)) + .accept(MediaType.APPLICATION_JSON) + .retrieve() + .body(GOD_NAMES_TYPE); + var result = names.stream() + .map(name -> new GodData(name, unicodeAggregator.toBigInteger(name))) + .toList(); + outboundCallObserver.onSuccess(source, result.size()); + return result; + } catch (RestClientException exception) { + outboundCallObserver.onFailure(source, exception); + throw new OutboundSourceException(source, exception); + } + } } diff --git a/examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/client/LoggingOutboundCallObserver.java b/examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/client/LoggingOutboundCallObserver.java new file mode 100644 index 00000000..d7d034b1 --- /dev/null +++ b/examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/client/LoggingOutboundCallObserver.java @@ -0,0 +1,27 @@ +package info.jab.ms.client; + +import info.jab.ms.api.model.PantheonSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +@Component +final class LoggingOutboundCallObserver implements OutboundCallObserver { + + private static final Logger log = LoggerFactory.getLogger(LoggingOutboundCallObserver.class); + + @Override + public void onStart(PantheonSource source) { + log.info("outbound_call_started source={}", source.getValue()); + } + + @Override + public void onSuccess(PantheonSource source, int payloadSize) { + log.info("outbound_call_completed source={} size={}", source.getValue(), payloadSize); + } + + @Override + public void onFailure(PantheonSource source, Exception exception) { + log.warn("outbound_call_failed source={} reason={}", source.getValue(), exception.getMessage()); + } +} diff --git a/examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/client/OutboundCallObserver.java b/examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/client/OutboundCallObserver.java new file mode 100644 index 00000000..1a1af24b --- /dev/null +++ b/examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/client/OutboundCallObserver.java @@ -0,0 +1,12 @@ +package info.jab.ms.client; + +import info.jab.ms.api.model.PantheonSource; + +public interface OutboundCallObserver { + + void onStart(PantheonSource source); + + void onSuccess(PantheonSource source, int payloadSize); + + void onFailure(PantheonSource source, Exception exception); +} diff --git a/examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/client/OutboundSourceException.java b/examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/client/OutboundSourceException.java new file mode 100644 index 00000000..99f66385 --- /dev/null +++ b/examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/client/OutboundSourceException.java @@ -0,0 +1,10 @@ +package info.jab.ms.client; + +import info.jab.ms.api.model.PantheonSource; + +public final class OutboundSourceException extends RuntimeException { + + public OutboundSourceException(PantheonSource source, Throwable cause) { + super("Outbound source fetch failed for source: " + source.getValue(), cause); + } +} diff --git a/examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/config/GodOutboundProperties.java b/examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/config/GodOutboundProperties.java index 4b383ed2..e3d9e231 100644 --- a/examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/config/GodOutboundProperties.java +++ b/examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/config/GodOutboundProperties.java @@ -1,14 +1,27 @@ package info.jab.ms.config; +import info.jab.ms.api.model.PantheonSource; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import java.net.URI; import java.time.Duration; +import java.util.Map; import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.bind.DefaultValue; +import org.springframework.validation.annotation.Validated; +@Validated @ConfigurationProperties(prefix = "god.outbound") public record GodOutboundProperties( - @DefaultValue("5s") Duration connectTimeout, - @DefaultValue("5s") Duration readTimeout, - Urls urls) { + @NotNull Duration connectTimeout, + @NotNull Duration readTimeout, + @NotEmpty Map urls +) { - public record Urls(String greek, String roman, String nordic) {} + public URI getUrlFor(PantheonSource source) { + var endpoint = urls.get(source.getValue()); + if (endpoint == null) { + throw new IllegalStateException("Missing outbound URL for source: " + source.getValue()); + } + return endpoint; + } } diff --git a/examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/config/HttpClientConfig.java b/examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/config/HttpClientConfig.java index 00051c97..db320873 100644 --- a/examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/config/HttpClientConfig.java +++ b/examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/config/HttpClientConfig.java @@ -1,32 +1,27 @@ package info.jab.ms.config; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; +import java.net.http.HttpClient; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.http.client.JdkClientHttpRequestFactory; import org.springframework.web.client.RestClient; @Configuration @EnableConfigurationProperties(GodOutboundProperties.class) -public class HttpClientConfig { +class HttpClientConfig { - @Bean - public RestClient.Builder godRestClientBuilder(GodOutboundProperties props) { - SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); - factory.setConnectTimeout((int) props.connectTimeout().toMillis()); - factory.setReadTimeout((int) props.readTimeout().toMillis()); - return RestClient.builder().requestFactory(factory); - } + @Bean + RestClient godOutboundRestClient(GodOutboundProperties properties) { + var httpClient = HttpClient.newBuilder() + .connectTimeout(properties.connectTimeout()) + .build(); - @Bean - public RestClient godRestClient(RestClient.Builder godRestClientBuilder) { - return godRestClientBuilder.build(); - } + var requestFactory = new JdkClientHttpRequestFactory(httpClient); + requestFactory.setReadTimeout(properties.readTimeout()); - @Bean(name = "godAnalysisExecutor") - public Executor godAnalysisExecutor() { - return Executors.newVirtualThreadPerTaskExecutor(); - } + return RestClient.builder() + .requestFactory(requestFactory) + .build(); + } } diff --git a/examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/controller/GlobalExceptionHandler.java b/examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/controller/GlobalExceptionHandler.java index 27052633..16e509e2 100644 --- a/examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/controller/GlobalExceptionHandler.java +++ b/examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/controller/GlobalExceptionHandler.java @@ -1,22 +1,71 @@ package info.jab.ms.controller; +import info.jab.ms.api.model.ErrorResponse; import info.jab.ms.exception.BadRequestException; -import java.util.Map; +import java.util.Objects; +import java.util.regex.Pattern; +import java.util.stream.StreamSupport; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.validation.method.ParameterValidationResult; import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.HandlerMethodValidationException; -@ControllerAdvice -public class GlobalExceptionHandler { +@RestControllerAdvice +class GlobalExceptionHandler { - private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); + private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); + private static final Pattern UNEXPECTED_VALUE = Pattern.compile("Unexpected value '([^']+)'"); - @ExceptionHandler(BadRequestException.class) - public ResponseEntity> handleBadRequest(BadRequestException ex) { - log.warn("bad_request message={}", ex.getMessage()); - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(Map.of("message", ex.getMessage())); - } + @ExceptionHandler(BadRequestException.class) + ResponseEntity handleBadRequest(BadRequestException exception) { + log.warn("Mapped bad request: code={}, message={}", exception.getCode(), exception.getMessage()); + return ResponseEntity.badRequest().body(new ErrorResponse(exception.getCode(), exception.getMessage())); + } + + @ExceptionHandler(HandlerMethodValidationException.class) + ResponseEntity handleValidation(HandlerMethodValidationException exception) { + var invalidSource = exception.getParameterValidationResults().stream() + .map(ParameterValidationResult::getResolvableErrors) + .flatMap(errors -> StreamSupport.stream(errors.spliterator(), false)) + .map(error -> error.getDefaultMessage()) + .filter(Objects::nonNull) + .anyMatch(message -> message.contains("Unexpected value")); + + if (invalidSource) { + var message = extractUnexpectedSource(exception); + log.warn("Mapped source validation error: {}", message); + return ResponseEntity.badRequest().body(new ErrorResponse("INVALID_SOURCE", message)); + } + + var message = "Query parameter 'filter' must contain exactly one character."; + log.warn("Mapped filter validation error: {}", message); + return ResponseEntity.badRequest().body(new ErrorResponse("INVALID_FILTER", message)); + } + + @ExceptionHandler(Exception.class) + ResponseEntity handleUnexpected(Exception exception) { + log.error("Mapped unexpected exception", exception); + var error = new ErrorResponse("INTERNAL_ERROR", "Unexpected error while computing aggregate."); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error); + } + + private String extractUnexpectedSource(HandlerMethodValidationException exception) { + var sourceFromMessage = exception.getParameterValidationResults().stream() + .map(ParameterValidationResult::getResolvableErrors) + .flatMap(errors -> StreamSupport.stream(errors.spliterator(), false)) + .map(error -> error.getDefaultMessage()) + .filter(Objects::nonNull) + .map(UNEXPECTED_VALUE::matcher) + .filter(matcher -> matcher.find()) + .map(matcher -> matcher.group(1)) + .findFirst(); + + return sourceFromMessage + .map(value -> "Query parameter 'sources' contains unsupported value: '" + value + "'.") + .orElse("Query parameter 'sources' contains unsupported value: 'unknown'."); + } } diff --git a/examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/controller/GodStatsController.java b/examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/controller/GodStatsController.java index 56a96d30..98a3ab1f 100644 --- a/examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/controller/GodStatsController.java +++ b/examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/controller/GodStatsController.java @@ -1,71 +1,33 @@ package info.jab.ms.controller; -import info.jab.ms.dto.GodStatsResponse; +import info.jab.ms.api.model.GodStatsSumResponse; import info.jab.ms.exception.BadRequestException; import info.jab.ms.service.GodAnalysisService; -import java.util.Arrays; -import java.util.List; -import java.util.Set; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController -@RequestMapping("/api/v1") -public class GodStatsController { - - private static final Logger log = LoggerFactory.getLogger(GodStatsController.class); - - private static final Set ALLOWED_SOURCES = Set.of("greek", "roman", "nordic"); - - private final GodAnalysisService godAnalysisService; - - public GodStatsController(GodAnalysisService godAnalysisService) { - this.godAnalysisService = godAnalysisService; - } - - @GetMapping("/gods/stats/sum") - public GodStatsResponse getGodsStatsSum( - @RequestParam(required = false) String filter, - @RequestParam(required = false) String sources) { - List sourceKeys = validateAndParseSources(filter, sources); - log.info("gods.stats.sum.request filter={} sources={}", filter, sources); - GodStatsResponse response = new GodStatsResponse(godAnalysisService.sumForSources(filter, sourceKeys)); - log.info("gods.stats.sum.response sum={}", response.sum()); - return response; - } - - private static List validateAndParseSources(String filter, String sources) { - if (filter == null) { - throw new BadRequestException("Missing required query parameter: filter"); - } - if (sources == null) { - throw new BadRequestException("Missing required query parameter: sources"); - } - if (filter.isEmpty()) { - throw new BadRequestException("filter must be exactly one Unicode code point"); - } - if (filter.codePointCount(0, filter.length()) != 1) { - throw new BadRequestException("filter must be exactly one Unicode code point"); - } - if (sources.isEmpty()) { - throw new BadRequestException("sources must not be empty"); - } - List parts = Arrays.stream(sources.split(",")) - .map(String::trim) - .filter(s -> !s.isEmpty()) - .toList(); - if (parts.isEmpty()) { - throw new BadRequestException("sources must not be empty"); - } - for (String part : parts) { - if (!ALLOWED_SOURCES.contains(part)) { - throw new BadRequestException("Invalid source name: " + part); - } - } - return parts; - } +class GodStatsController { + + private final GodAnalysisService godAnalysisService; + + GodStatsController(GodAnalysisService godAnalysisService) { + this.godAnalysisService = godAnalysisService; + } + + @GetMapping(path = "/api/v1/gods/stats/sum", produces = "application/json") + ResponseEntity getGodsStatsSum( + @RequestParam String filter, + @RequestParam String sources + ) { + if (filter.length() != 1) { + throw new BadRequestException("INVALID_FILTER", + "Query parameter 'filter' must contain exactly one character."); + } + + var sum = godAnalysisService.aggregateByFilter(filter, sources); + return ResponseEntity.ok(new GodStatsSumResponse(sum)); + } } diff --git a/examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/dto/GodStatsResponse.java b/examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/dto/GodStatsResponse.java deleted file mode 100644 index 7727ecd7..00000000 --- a/examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/dto/GodStatsResponse.java +++ /dev/null @@ -1,3 +0,0 @@ -package info.jab.ms.dto; - -public record GodStatsResponse(String sum) {} diff --git a/examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/exception/BadRequestException.java b/examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/exception/BadRequestException.java index fc9982f5..1483e657 100644 --- a/examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/exception/BadRequestException.java +++ b/examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/exception/BadRequestException.java @@ -2,7 +2,14 @@ public final class BadRequestException extends RuntimeException { - public BadRequestException(String message) { - super(message); - } + private final String code; + + public BadRequestException(String code, String message) { + super(message); + this.code = code; + } + + public String getCode() { + return code; + } } diff --git a/examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/service/GodAnalysisService.java b/examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/service/GodAnalysisService.java index 4bc04eab..142734c0 100644 --- a/examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/service/GodAnalysisService.java +++ b/examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/service/GodAnalysisService.java @@ -1,50 +1,85 @@ package info.jab.ms.service; -import info.jab.ms.algorithm.UnicodeAggregator; -import info.jab.ms.client.GodDataClient; +import info.jab.ms.api.model.PantheonSource; +import info.jab.ms.exception.BadRequestException; import java.math.BigInteger; -import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; import java.util.List; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class GodAnalysisService { - private static final Logger log = LoggerFactory.getLogger(GodAnalysisService.class); - - private final GodDataClient godDataClient; - private final Executor godAnalysisExecutor; - - public GodAnalysisService( - GodDataClient godDataClient, @Qualifier("godAnalysisExecutor") Executor godAnalysisExecutor) { - this.godDataClient = godDataClient; - this.godAnalysisExecutor = godAnalysisExecutor; - } - - /** - * @param filter single Unicode code point (caller validates) - * @param pantheonKeys non-empty list of known pantheon keys - */ - public String sumForSources(String filter, List pantheonKeys) { - List>> futures = pantheonKeys.stream() - .map(key -> CompletableFuture.supplyAsync(() -> godDataClient.fetchNames(key), godAnalysisExecutor)) - .toList(); - List merged = new ArrayList<>(); - for (CompletableFuture> future : futures) { - merged.addAll(future.join()); - } - BigInteger sum = UnicodeAggregator.sumFiltered(filter, merged); - log.info( - "gods.analysis.sum filter={} sources={} nameCount={} sum={}", - filter, - pantheonKeys, - merged.size(), - sum.toString()); - return sum.toString(); - } + private static final Logger log = LoggerFactory.getLogger(GodAnalysisService.class); + + private final PantheonDataSource pantheonDataSource; + private final Executor domainExecutor; + + @Autowired + public GodAnalysisService(PantheonDataSource pantheonDataSource) { + this(pantheonDataSource, CompletableFuture.delayedExecutor(0, TimeUnit.MILLISECONDS)); + } + + GodAnalysisService(PantheonDataSource pantheonDataSource, Executor domainExecutor) { + this.pantheonDataSource = pantheonDataSource; + this.domainExecutor = domainExecutor; + } + + public String aggregateByFilter(String filter, String sources) { + var parsedSources = parseSources(sources); + var selectedFilter = filter.charAt(0); + + var futures = parsedSources.stream() + .sorted(Comparator.comparing(Enum::name)) + .map(source -> CompletableFuture.supplyAsync(() -> fetchSourceData(source), domainExecutor)) + .toList(); + + var sum = futures.stream() + .map(CompletableFuture::join) + .flatMap(List::stream) + .filter(godData -> godData.name().startsWith(String.valueOf(selectedFilter))) + .map(GodData::score) + .reduce(BigInteger.ZERO, BigInteger::add); + + return sum.toString(); + } + + Set parseSources(String sources) { + return Arrays.stream(sources.split(",")) + .map(String::trim) + .map(this::parseSingleSource) + .collect(Collectors.toSet()); + } + + private PantheonSource parseSingleSource(String value) { + try { + return PantheonSource.fromValue(value); + } catch (IllegalArgumentException exception) { + throw new BadRequestException( + "INVALID_SOURCE", + "Query parameter 'sources' contains unsupported value: '" + value + "'." + ); + } + } + + private List fetchSourceData(PantheonSource source) { + log.info("source_execution_started source={}", source.getValue()); + try { + var result = List.copyOf(pantheonDataSource.fetch(source)); + log.info("source_execution_completed source={} size={}", source.getValue(), result.size()); + return result; + } catch (RuntimeException ex) { + log.warn("source_execution_fallback source={} reason={}", source.getValue(), ex.getMessage()); + return List.of(); + } + } } diff --git a/examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/service/GodData.java b/examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/service/GodData.java new file mode 100644 index 00000000..b617b7b7 --- /dev/null +++ b/examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/service/GodData.java @@ -0,0 +1,6 @@ +package info.jab.ms.service; + +import java.math.BigInteger; + +public record GodData(String name, BigInteger score) { +} diff --git a/examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/service/InMemoryPantheonDataSource.java b/examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/service/InMemoryPantheonDataSource.java new file mode 100644 index 00000000..f577d2bb --- /dev/null +++ b/examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/service/InMemoryPantheonDataSource.java @@ -0,0 +1,32 @@ +package info.jab.ms.service; + +import info.jab.ms.api.model.PantheonSource; +import java.math.BigInteger; +import java.util.List; +import java.util.Map; + +final class InMemoryPantheonDataSource implements PantheonDataSource { + + private static final Map> FIXTURES = Map.of( + PantheonSource.GREEK, + List.of( + new GodData("nyx", new BigInteger("30000000000000000000")), + new GodData("zeus", new BigInteger("123")) + ), + PantheonSource.ROMAN, + List.of( + new GodData("neptune", new BigInteger("20000000000000000000")), + new GodData("mars", new BigInteger("456")) + ), + PantheonSource.NORDIC, + List.of( + new GodData("njord", new BigInteger("28179288397447443426")), + new GodData("odin", new BigInteger("789")) + ) + ); + + @Override + public List fetch(PantheonSource source) { + return FIXTURES.getOrDefault(source, List.of()); + } +} diff --git a/examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/service/PantheonDataSource.java b/examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/service/PantheonDataSource.java new file mode 100644 index 00000000..8380c856 --- /dev/null +++ b/examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/service/PantheonDataSource.java @@ -0,0 +1,9 @@ +package info.jab.ms.service; + +import info.jab.ms.api.model.PantheonSource; +import java.util.List; + +public interface PantheonDataSource { + + List fetch(PantheonSource source); +} diff --git a/examples/requirements-examples/problem1/implementation/src/test/java/info/jab/ms/MainApplicationTest.java b/examples/requirements-examples/problem1/implementation/src/test/java/info/jab/ms/MainApplicationTest.java new file mode 100644 index 00000000..23739349 --- /dev/null +++ b/examples/requirements-examples/problem1/implementation/src/test/java/info/jab/ms/MainApplicationTest.java @@ -0,0 +1,15 @@ +package info.jab.ms; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +class MainApplicationTest { + + @Test + void contextLoads() { + assertThat(true).isTrue(); + } +} diff --git a/examples/requirements-examples/problem1/implementation/src/test/java/info/jab/ms/algorithm/UnicodeAggregatorTest.java b/examples/requirements-examples/problem1/implementation/src/test/java/info/jab/ms/algorithm/UnicodeAggregatorTest.java index 17ab562d..b42ee2d9 100644 --- a/examples/requirements-examples/problem1/implementation/src/test/java/info/jab/ms/algorithm/UnicodeAggregatorTest.java +++ b/examples/requirements-examples/problem1/implementation/src/test/java/info/jab/ms/algorithm/UnicodeAggregatorTest.java @@ -1,25 +1,33 @@ package info.jab.ms.algorithm; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.math.BigInteger; import org.junit.jupiter.api.Test; class UnicodeAggregatorTest { - @Test - void zeusExampleMatchesPinnedDecimalConcatenation() { - assertThat(UnicodeAggregator.nameToBigInteger("Zeus")).isEqualTo(new BigInteger("90101117115")); - } - - @Test - void sumFilteredMatchesLowercaseFilterAgainstUppercaseNNames() { - var names = java.util.List.of("Nike", "Nemesis", "Neptun", "Njord"); - assertThat(UnicodeAggregator.sumFiltered("n", names).toString()).isEqualTo("78179288397447443426"); - } - - @Test - void sumFilteredReturnsZeroWhenNoNameMatches() { - assertThat(UnicodeAggregator.sumFiltered("n", java.util.List.of("zeus", "apollo"))).isEqualTo(BigInteger.ZERO); - } + private final UnicodeAggregator unicodeAggregator = new UnicodeAggregator(); + + @Test + void shouldConvertStringToDeterministicBigIntegerSum() { + var result = unicodeAggregator.toBigInteger("nA"); + + assertThat(result).isEqualTo(BigInteger.valueOf('n' + 'A')); + } + + @Test + void shouldHandleNonAsciiCharacters() { + var result = unicodeAggregator.toBigInteger("ñ"); + + assertThat(result).isEqualTo(BigInteger.valueOf("ñ".codePointAt(0))); + } + + @Test + void shouldRejectNullInput() { + assertThatThrownBy(() -> unicodeAggregator.toBigInteger(null)) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("value must not be null"); + } } diff --git a/examples/requirements-examples/problem1/implementation/src/test/java/info/jab/ms/client/GodDataClientTest.java b/examples/requirements-examples/problem1/implementation/src/test/java/info/jab/ms/client/GodDataClientTest.java deleted file mode 100644 index 7ee7298c..00000000 --- a/examples/requirements-examples/problem1/implementation/src/test/java/info/jab/ms/client/GodDataClientTest.java +++ /dev/null @@ -1,80 +0,0 @@ -package info.jab.ms.client; - -import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; -import static com.github.tomakehurst.wiremock.client.WireMock.get; -import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; -import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; -import static org.assertj.core.api.Assertions.assertThat; - -import com.github.tomakehurst.wiremock.WireMockServer; -import com.github.tomakehurst.wiremock.core.WireMockConfiguration; -import info.jab.ms.config.GodOutboundProperties; -import java.nio.charset.StandardCharsets; -import java.time.Duration; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.core.io.ClassPathResource; -import org.springframework.http.client.SimpleClientHttpRequestFactory; -import org.springframework.web.client.RestClient; - -class GodDataClientTest { - - private static final WireMockServer WM = new WireMockServer(WireMockConfiguration.wireMockConfig().dynamicPort()); - - private GodDataClient client; - - @BeforeAll - static void startWireMock() { - WM.start(); - } - - @AfterAll - static void stopWireMock() { - WM.stop(); - } - - @BeforeEach - void setUp() { - String base = "http://localhost:" + WM.port(); - GodOutboundProperties props = new GodOutboundProperties( - Duration.ofMillis(50), - Duration.ofMillis(50), - new GodOutboundProperties.Urls(base + "/greek", base + "/roman", base + "/nordic")); - SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); - factory.setConnectTimeout((int) props.connectTimeout().toMillis()); - factory.setReadTimeout((int) props.readTimeout().toMillis()); - RestClient restClient = RestClient.builder().requestFactory(factory).build(); - client = new GodDataClient(restClient, props); - } - - @AfterEach - void tearDown() { - WM.resetAll(); - } - - @Test - void successfulFetchReturnsJsonArrayAsStrings() throws Exception { - String body = new String( - new ClassPathResource("wiremock/greek-gods.json").getInputStream().readAllBytes(), - StandardCharsets.UTF_8); - WM.stubFor( - get(urlPathEqualTo("/greek")) - .willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody(body))); - - assertThat(client.fetchNames("greek")).hasSize(20); - WM.verify(1, getRequestedFor(urlPathEqualTo("/greek"))); - } - - @Test - void readTimeoutReturnsEmptyListWithoutRetry() { - WM.stubFor( - get(urlPathEqualTo("/greek")) - .willReturn(aResponse().withStatus(200).withFixedDelay(500).withBody("[]"))); - - assertThat(client.fetchNames("greek")).isEmpty(); - WM.verify(1, getRequestedFor(urlPathEqualTo("/greek"))); - } -} diff --git a/examples/requirements-examples/problem1/implementation/src/test/java/info/jab/ms/controller/GodAnalysisApiAT.java b/examples/requirements-examples/problem1/implementation/src/test/java/info/jab/ms/controller/GodAnalysisApiAT.java deleted file mode 100644 index d0ddafb6..00000000 --- a/examples/requirements-examples/problem1/implementation/src/test/java/info/jab/ms/controller/GodAnalysisApiAT.java +++ /dev/null @@ -1,83 +0,0 @@ -package info.jab.ms.controller; - -import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; -import static com.github.tomakehurst.wiremock.client.WireMock.get; -import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; -import static org.assertj.core.api.Assertions.assertThat; - -import com.github.tomakehurst.wiremock.WireMockServer; -import com.github.tomakehurst.wiremock.core.WireMockConfiguration; -import info.jab.ms.dto.GodStatsResponse; -import java.nio.charset.StandardCharsets; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Tag; -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.web.server.LocalServerPort; -import org.springframework.core.io.ClassPathResource; -import org.springframework.test.context.DynamicPropertyRegistry; -import org.springframework.test.context.DynamicPropertySource; -import org.springframework.web.client.RestClient; - -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@Tag("acceptance-test") -class GodAnalysisApiAT { - - private static final WireMockServer WM; - - static { - WM = new WireMockServer(WireMockConfiguration.wireMockConfig().dynamicPort()); - WM.start(); - } - - @DynamicPropertySource - static void wireMockUrls(DynamicPropertyRegistry registry) { - String base = "http://localhost:" + WM.port(); - registry.add("god.outbound.urls.greek", () -> base + "/greek"); - registry.add("god.outbound.urls.roman", () -> base + "/roman"); - registry.add("god.outbound.urls.nordic", () -> base + "/nordic"); - } - - @AfterAll - static void stopWireMock() { - WM.stop(); - } - - @LocalServerPort - private int port; - - private RestClient client; - - @BeforeEach - void setUp() throws Exception { - WM.resetAll(); - stubJson("/greek", "wiremock/greek-gods.json"); - stubJson("/roman", "wiremock/roman-gods.json"); - stubJson("/nordic", "wiremock/nordic-gods.json"); - client = RestClient.builder().baseUrl("http://localhost:" + port).build(); - } - - private void stubJson(String path, String classpathUnderTestResources) throws Exception { - String body = new String( - new ClassPathResource(classpathUnderTestResources).getInputStream().readAllBytes(), - StandardCharsets.UTF_8); - WM.stubFor( - get(urlPathEqualTo(path)) - .willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody(body))); - } - - @Test - void happyPathReturnsExpectedSum() { - GodStatsResponse body = client.get() - .uri(uriBuilder -> uriBuilder.path("/api/v1/gods/stats/sum") - .queryParam("filter", "n") - .queryParam("sources", "greek,roman,nordic") - .build()) - .retrieve() - .body(GodStatsResponse.class); - - assertThat(body).isNotNull(); - assertThat(body.sum()).isEqualTo("78179288397447443426"); - } -} diff --git a/examples/requirements-examples/problem1/implementation/src/test/java/info/jab/ms/controller/GodAnalysisApiIT.java b/examples/requirements-examples/problem1/implementation/src/test/java/info/jab/ms/controller/GodAnalysisApiIT.java deleted file mode 100644 index 3d4ab3b2..00000000 --- a/examples/requirements-examples/problem1/implementation/src/test/java/info/jab/ms/controller/GodAnalysisApiIT.java +++ /dev/null @@ -1,49 +0,0 @@ -package info.jab.ms.controller; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.when; - -import info.jab.ms.client.GodDataClient; -import info.jab.ms.dto.GodStatsResponse; -import java.util.List; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Tag; -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.bean.override.mockito.MockitoBean; -import org.springframework.boot.test.web.server.LocalServerPort; -import org.springframework.web.client.RestClient; - -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@Tag("integration-test") -class GodAnalysisApiIT { - - @MockitoBean - private GodDataClient godDataClient; - - @LocalServerPort - private int port; - - private RestClient client; - - @BeforeEach - void setUp() { - client = RestClient.builder().baseUrl("http://localhost:" + port).build(); - when(godDataClient.fetchNames(anyString())).thenReturn(List.of("zeus")); - } - - @Test - void filterWithNoFirstLetterMatchReturnsZeroSum() { - GodStatsResponse body = client.get() - .uri(uriBuilder -> uriBuilder.path("/api/v1/gods/stats/sum") - .queryParam("filter", "N") - .queryParam("sources", "greek,roman,nordic") - .build()) - .retrieve() - .body(GodStatsResponse.class); - - assertThat(body).isNotNull(); - assertThat(body.sum()).isEqualTo("0"); - } -} diff --git a/examples/requirements-examples/problem1/implementation/src/test/java/info/jab/ms/controller/GodAnalysisPartialTimeoutIT.java b/examples/requirements-examples/problem1/implementation/src/test/java/info/jab/ms/controller/GodAnalysisPartialTimeoutIT.java deleted file mode 100644 index ddf783a9..00000000 --- a/examples/requirements-examples/problem1/implementation/src/test/java/info/jab/ms/controller/GodAnalysisPartialTimeoutIT.java +++ /dev/null @@ -1,91 +0,0 @@ -package info.jab.ms.controller; - -import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; -import static com.github.tomakehurst.wiremock.client.WireMock.get; -import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; -import static org.assertj.core.api.Assertions.assertThat; - -import com.github.tomakehurst.wiremock.WireMockServer; -import com.github.tomakehurst.wiremock.core.WireMockConfiguration; -import info.jab.ms.dto.GodStatsResponse; -import java.nio.charset.StandardCharsets; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Tag; -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.web.server.LocalServerPort; -import org.springframework.core.io.ClassPathResource; -import org.springframework.test.context.DynamicPropertyRegistry; -import org.springframework.test.context.DynamicPropertySource; -import org.springframework.web.client.RestClient; - -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@Tag("integration-test") -class GodAnalysisPartialTimeoutIT { - - private static final WireMockServer WM; - - static { - WM = new WireMockServer(WireMockConfiguration.wireMockConfig().dynamicPort()); - WM.start(); - } - - @DynamicPropertySource - static void wireMockAndTimeouts(DynamicPropertyRegistry registry) { - String base = "http://localhost:" + WM.port(); - registry.add("god.outbound.urls.greek", () -> base + "/greek"); - registry.add("god.outbound.urls.roman", () -> base + "/roman"); - registry.add("god.outbound.urls.nordic", () -> base + "/nordic"); - registry.add("god.outbound.connect-timeout", () -> "200ms"); - registry.add("god.outbound.read-timeout", () -> "200ms"); - } - - @AfterAll - static void stopWireMock() { - WM.stop(); - } - - @LocalServerPort - private int port; - - private RestClient client; - - @BeforeEach - void setUp() throws Exception { - WM.resetAll(); - String greekBody = new String( - new ClassPathResource("wiremock/greek-gods.json").getInputStream().readAllBytes(), - StandardCharsets.UTF_8); - WM.stubFor( - get(urlPathEqualTo("/greek")) - .willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody(greekBody))); - WM.stubFor( - get(urlPathEqualTo("/roman")) - .willReturn(aResponse().withStatus(200).withFixedDelay(500).withBody("[]"))); - WM.stubFor( - get(urlPathEqualTo("/nordic")) - .willReturn(aResponse().withStatus(200).withFixedDelay(500).withBody("[]"))); - client = RestClient.builder().baseUrl("http://localhost:" + port).build(); - } - - @AfterEach - void resetWireMock() { - WM.resetAll(); - } - - @Test - void nordicAndRomanDelayedBeyondReadTimeoutGreekSucceedsPinnedPartialSum() { - GodStatsResponse body = client.get() - .uri(uriBuilder -> uriBuilder.path("/api/v1/gods/stats/sum") - .queryParam("filter", "n") - .queryParam("sources", "greek,roman,nordic") - .build()) - .retrieve() - .body(GodStatsResponse.class); - - assertThat(body).isNotNull(); - assertThat(body.sum()).isEqualTo("78101109179220212216"); - } -} diff --git a/examples/requirements-examples/problem1/implementation/src/test/java/info/jab/ms/controller/GodStatsControllerAT.java b/examples/requirements-examples/problem1/implementation/src/test/java/info/jab/ms/controller/GodStatsControllerAT.java new file mode 100644 index 00000000..57ee1064 --- /dev/null +++ b/examples/requirements-examples/problem1/implementation/src/test/java/info/jab/ms/controller/GodStatsControllerAT.java @@ -0,0 +1,161 @@ +package info.jab.ms.controller; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; + +import com.github.tomakehurst.wiremock.core.WireMockConfiguration; +import com.github.tomakehurst.wiremock.junit5.WireMockExtension; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.RestClient; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@ActiveProfiles("test") +class GodStatsControllerAT { + + @RegisterExtension + static WireMockExtension wireMockServer = WireMockExtension.newInstance() + .options(WireMockConfiguration.wireMockConfig().dynamicPort()) + .build(); + + @DynamicPropertySource + static void configureOutboundProperties(DynamicPropertyRegistry registry) { + registry.add("god.outbound.connect-timeout", () -> "150ms"); + registry.add("god.outbound.read-timeout", () -> "200ms"); + registry.add("god.outbound.urls.greek", () -> wireMockServer.baseUrl() + "/greek"); + registry.add("god.outbound.urls.roman", () -> wireMockServer.baseUrl() + "/roman"); + registry.add("god.outbound.urls.nordic", () -> wireMockServer.baseUrl() + "/nordic"); + } + + @Value("${local.server.port}") + private int port; + + private RestClient restClient; + + @BeforeEach + void setUp() { + wireMockServer.resetAll(); + this.restClient = RestClient.builder() + .baseUrl("http://localhost:" + port) + .build(); + } + + @Test + void shouldReturnSumForHappyPathWithOutboundCalls() { + stubSource("greek", 0); + stubSource("roman", 0); + stubSource("nordic", 0); + + var response = restClient.get() + .uri(uriBuilder -> uriBuilder + .path("/api/v1/gods/stats/sum") + .queryParam("filter", "N") + .queryParam("sources", "greek,roman,nordic") + .build()) + .accept(MediaType.APPLICATION_JSON) + .retrieve() + .toEntity(String.class); + + assertThat(response.getStatusCode().is2xxSuccessful()).isTrue(); + assertThat(response.getBody()).contains("\"sum\":\"2258\""); + assertSingleFetch("greek"); + assertSingleFetch("roman"); + assertSingleFetch("nordic"); + } + + @Test + void shouldReturnPartialSumWhenOneSourceTimesOut() { + stubSource("greek", 0); + stubSource("roman", 500); + stubSource("nordic", 0); + + var response = restClient.get() + .uri(uriBuilder -> uriBuilder + .path("/api/v1/gods/stats/sum") + .queryParam("filter", "N") + .queryParam("sources", "greek,roman,nordic") + .build()) + .accept(MediaType.APPLICATION_JSON) + .retrieve() + .toEntity(String.class); + + assertThat(response.getStatusCode().is2xxSuccessful()).isTrue(); + assertThat(response.getBody()).contains("\"sum\":\"1624\""); + assertSingleFetch("greek"); + assertSingleFetch("roman"); + assertSingleFetch("nordic"); + } + + @Test + void shouldReturnBadRequestWhenFilterIsNotSingleCharacter() { + var response = restClient.get() + .uri(uriBuilder -> uriBuilder + .path("/api/v1/gods/stats/sum") + .queryParam("filter", "nn") + .queryParam("sources", "greek") + .build()) + .accept(MediaType.APPLICATION_JSON) + .retrieve() + .onStatus(HttpStatusCode::isError, (request, httpResponse) -> { + // no-op: allows asserting OpenAPI error envelope directly + }) + .toEntity(String.class); + + assertThat(response.getStatusCode().value()).isEqualTo(400); + assertThat(response.getBody()).contains("\"code\":\"INVALID_FILTER\""); + } + + @Test + void shouldReturnBadRequestWhenSourceIsUnsupported() { + assertThatThrownBy(() -> restClient.get() + .uri(uriBuilder -> uriBuilder + .path("/api/v1/gods/stats/sum") + .queryParam("filter", "n") + .queryParam("sources", "egyptian") + .build()) + .accept(MediaType.APPLICATION_JSON) + .retrieve() + .toEntity(String.class)) + .isInstanceOf(HttpClientErrorException.BadRequest.class) + .hasMessageContaining("INVALID_SOURCE"); + } + + private void stubSource(String source, int fixedDelayMillis) { + wireMockServer.stubFor(get(urlPathEqualTo("/" + source)) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withFixedDelay(fixedDelayMillis) + .withBody(bodyFor(source)))); + } + + private void assertSingleFetch(String source) { + wireMockServer.verify(1, getRequestedFor(urlPathEqualTo("/" + source)) + .withHeader("Accept", equalTo("application/json"))); + } + + private String bodyFor(String source) { + return switch (source) { + case "greek" -> + "[\"Zeus\",\"Hera\",\"Poseidon\",\"Demeter\",\"Ares\",\"Athena\",\"Apollo\",\"Artemis\",\"Hephaestus\",\"Aphrodite\",\"Hermes\",\"Dionysus\",\"Hades\",\"Hypnos\",\"Nike\",\"Janus\",\"Nemesis\",\"Iris\",\"Hecate\",\"Tyche\"]"; + case "roman" -> "[\"Venus\",\"Mars\",\"Neptun\",\"Mercury\",\"Pluto\",\"Jupiter\"]"; + case "nordic" -> "[\"Baldur\",\"Freyja\",\"Heimdall\",\"Frigga\",\"Hel\",\"Loki\",\"Njord\",\"Odin\",\"Thor\",\"Tyr\"]"; + default -> throw new IllegalArgumentException("Unsupported source: " + source); + }; + } +} diff --git a/examples/requirements-examples/problem1/implementation/src/test/java/info/jab/ms/controller/GodStatsControllerErrorHandlingTest.java b/examples/requirements-examples/problem1/implementation/src/test/java/info/jab/ms/controller/GodStatsControllerErrorHandlingTest.java deleted file mode 100644 index afd42304..00000000 --- a/examples/requirements-examples/problem1/implementation/src/test/java/info/jab/ms/controller/GodStatsControllerErrorHandlingTest.java +++ /dev/null @@ -1,73 +0,0 @@ -package info.jab.ms.controller; - -import static org.hamcrest.Matchers.containsString; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import info.jab.ms.service.GodAnalysisService; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.test.context.bean.override.mockito.MockitoBean; -import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest; -import org.springframework.context.annotation.Import; -import org.springframework.test.web.servlet.MockMvc; - -@WebMvcTest(controllers = GodStatsController.class) -@Import(GlobalExceptionHandler.class) -class GodStatsControllerErrorHandlingTest { - - @MockitoBean - private GodAnalysisService godAnalysisService; - - @Autowired - private MockMvc mockMvc; - - @Test - void missingFilterReturns400WithMessage() throws Exception { - mockMvc.perform(get("/api/v1/gods/stats/sum").param("sources", "greek,roman,nordic")) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.message").value(containsString("filter"))); - } - - @Test - void missingSourcesReturns400WithMessage() throws Exception { - mockMvc.perform(get("/api/v1/gods/stats/sum").param("filter", "n")) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.message").value(containsString("sources"))); - } - - @Test - void emptyFilterReturns400WithMessage() throws Exception { - mockMvc.perform(get("/api/v1/gods/stats/sum") - .param("filter", "") - .param("sources", "greek,roman,nordic")) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.message").exists()); - } - - @Test - void multiCharFilterReturns400WithMessage() throws Exception { - mockMvc.perform(get("/api/v1/gods/stats/sum") - .param("filter", "abc") - .param("sources", "greek,roman,nordic")) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.message").exists()); - } - - @Test - void emptySourcesReturns400WithMessage() throws Exception { - mockMvc.perform(get("/api/v1/gods/stats/sum").param("filter", "n").param("sources", "")) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.message").exists()); - } - - @Test - void invalidSourceNamesReturn400WithMessage() throws Exception { - mockMvc.perform(get("/api/v1/gods/stats/sum") - .param("filter", "n") - .param("sources", "invalid,unknown")) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.message").value(containsString("Invalid source"))); - } -} diff --git a/examples/requirements-examples/problem1/implementation/src/test/java/info/jab/ms/service/GodAnalysisServiceTest.java b/examples/requirements-examples/problem1/implementation/src/test/java/info/jab/ms/service/GodAnalysisServiceTest.java index 2aec9f50..37143ddf 100644 --- a/examples/requirements-examples/problem1/implementation/src/test/java/info/jab/ms/service/GodAnalysisServiceTest.java +++ b/examples/requirements-examples/problem1/implementation/src/test/java/info/jab/ms/service/GodAnalysisServiceTest.java @@ -1,49 +1,84 @@ package info.jab.ms.service; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.when; +import static org.assertj.core.api.Assertions.assertThatThrownBy; -import info.jab.ms.client.GodDataClient; +import info.jab.ms.api.model.PantheonSource; +import info.jab.ms.exception.BadRequestException; +import java.math.BigInteger; import java.util.List; -import java.util.concurrent.Executor; -import org.junit.jupiter.api.BeforeEach; +import java.util.Set; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -@ExtendWith(MockitoExtension.class) class GodAnalysisServiceTest { - private static final Executor SYNC_EXECUTOR = Runnable::run; + @Test + void shouldParseSourcesAsUniqueSet() { + var service = new GodAnalysisService(source -> List.of()); - @Mock - private GodDataClient godDataClient; + var sources = service.parseSources("greek, roman, greek"); - private GodAnalysisService godAnalysisService; + assertThat(sources).containsExactlyInAnyOrder(PantheonSource.GREEK, PantheonSource.ROMAN); + } - @BeforeEach - void setUp() { - godAnalysisService = new GodAnalysisService(godDataClient, SYNC_EXECUTOR); - } + @Test + void shouldRejectUnsupportedSource() { + var service = new GodAnalysisService(source -> List.of()); - @Test - void aggregatesAcrossSourcesWithPinnedHappyPathSum() { - when(godDataClient.fetchNames("greek")).thenReturn(List.of("Nike", "Nemesis")); - when(godDataClient.fetchNames("roman")).thenReturn(List.of("Neptun")); - when(godDataClient.fetchNames("nordic")).thenReturn(List.of("Njord")); + assertThatThrownBy(() -> service.parseSources("greek,egyptian")) + .isInstanceOf(BadRequestException.class) + .hasMessageContaining("unsupported value: 'egyptian'"); + } - String sum = godAnalysisService.sumForSources("n", List.of("greek", "roman", "nordic")); + @Test + void shouldApplyCaseSensitiveFirstCharacterFilter() { + var service = new GodAnalysisService(source -> List.of( + new GodData("nyx", BigInteger.TEN), + new GodData("Nyx", BigInteger.ONE) + )); - assertThat(sum).isEqualTo("78179288397447443426"); - } + var lowercase = service.aggregateByFilter("n", "greek"); + var uppercase = service.aggregateByFilter("N", "greek"); - @Test - void returnsZeroWhenNoNamesMatchFilter() { - when(godDataClient.fetchNames("greek")).thenReturn(List.of("zeus")); + assertThat(lowercase).isEqualTo("10"); + assertThat(uppercase).isEqualTo("1"); + } - String sum = godAnalysisService.sumForSources("N", List.of("greek")); + @Test + void shouldAggregateWithBigIntegerAndDeterministicMergeAcrossRuns() { + var service = new GodAnalysisService(source -> switch (source) { + case GREEK -> List.of(new GodData("nyx", new BigInteger("9999999999999999999"))); + case ROMAN -> List.of(new GodData("neptune", new BigInteger("8888888888888888888"))); + case NORDIC -> List.of(new GodData("njord", new BigInteger("7777777777777777777"))); + }); - assertThat(sum).isEqualTo("0"); - } + var first = service.aggregateByFilter("n", "greek,roman,nordic"); + var second = service.aggregateByFilter("n", "nordic,greek,roman"); + + assertThat(first).isEqualTo("26666666666666666664"); + assertThat(second).isEqualTo(first); + } + + @Test + void shouldFallbackToEmptySourceContributionWhenSourceFails() { + var service = new GodAnalysisService(source -> { + if (Set.of(PantheonSource.ROMAN).contains(source)) { + throw new IllegalStateException("simulated-upstream-failure"); + } + return List.of(new GodData("nyx", BigInteger.valueOf(5))); + }); + + var result = service.aggregateByFilter("n", "greek,roman"); + + assertThat(result).isEqualTo("5"); + } + + @Test + void shouldKeepA1HappyPathValue() { + var service = new GodAnalysisService(new InMemoryPantheonDataSource()); + + var result = service.aggregateByFilter("n", "greek,roman,nordic"); + + assertThat(result).isEqualTo("78179288397447443426"); + } } diff --git a/examples/requirements-examples/problem1/requirements/adr/ADR-003-God-Analysis-API-Technology-Stack.md b/examples/requirements-examples/problem1/requirements/adr/ADR-003-God-Analysis-API-Technology-Stack.md index cf53fea4..809a79b9 100644 --- a/examples/requirements-examples/problem1/requirements/adr/ADR-003-God-Analysis-API-Technology-Stack.md +++ b/examples/requirements-examples/problem1/requirements/adr/ADR-003-God-Analysis-API-Technology-Stack.md @@ -2,7 +2,7 @@ **Status:** Accepted **Date:** Sat Mar 21 10:12:00 CET 2026 -**Decision:** Adopt a **technology stack** for the God Analysis API covering **runtime framework** (Spring Boot), **outbound HTTP** (`RestClient` with **configured connect/read timeouts** in `application.yml`), **acceptance-style testing** (**Spring `RestClient`** against `@SpringBootTest(webEnvironment = RANDOM_PORT)`), and **integration testing** with **WireMock in-process** (or Testcontainers-hosted WireMock) so **timeout** and partial-result scenarios run in **isolation** with deterministic delay simulation. **Automatic HTTP retries are out of scope** for this user story—no Resilience4j Retry (or equivalent) required. +**Decision:** Adopt a **technology stack** for the God Analysis API covering **API-first contract development** (OpenAPI 3.x as source of truth with code generation via `openapi-generator-maven-plugin`), **runtime framework** (Spring Boot), **outbound HTTP** (`RestClient` with **configured connect/read timeouts** in `application.yml`), **acceptance-style testing** (**Spring `RestClient`** against `@SpringBootTest(webEnvironment = RANDOM_PORT)`), and **integration testing** with **WireMock in-process** (or Testcontainers-hosted WireMock) so **timeout** and partial-result scenarios run in **isolation** with deterministic delay simulation. **Automatic HTTP retries are out of scope** for this user story—no Resilience4j Retry (or equivalent) required. **Amendment (2026-03-22):** **Rest Assured** was superseded for **HTTP-level acceptance tests** by **Spring Framework `RestClient`**. Rationale: Rest Assured relies on Groovy internals (`RequestSpecificationImpl.applyProxySettings`); on **Java 21+** this path can throw `NullPointerException` (`ConcurrentHashMap` does not permit null keys) during proxy/meta-property handling—making the stack brittle on current LTS/JDK feature releases. **`RestClient`** is already on the classpath (`spring-web`), matches the **same client API** used for outbound calls, and works with **AssertJ** for assertions—no separate acceptance-test HTTP stack required. @@ -15,13 +15,14 @@ Timeout behavior ([ADR-002](ADR-002-God-Analysis-API-Non-Functional-Requirements ### Key Requirements Driving Framework Selection 1. **REST API Development**: Need to expose a single endpoint (`GET /api/v1/gods/stats/sum`) with query parameter handling -2. **External HTTP Integration**: Must consume three external god APIs with **RestClient** timeouts (no automatic retries) -3. **Parallel Processing**: Requires concurrent calls to multiple external services for optimal performance -4. **Error Handling & Resilience**: Graceful degradation with partial results when external sources fail -5. **JSON Processing**: Handle JSON serialization/deserialization for API responses and external data -6. **Testing Support**: Comprehensive testing capabilities for acceptance, integration, and unit tests—with **deterministic** timeout coverage using WireMock delay simulation -7. **Development Velocity**: Team expertise and rapid development requirements -8. **Production Readiness**: Monitoring, health checks, and operational capabilities +2. **API-First Contract Governance**: OpenAPI contract must be authored first and treated as the canonical API specification +3. **External HTTP Integration**: Must consume three external god APIs with **RestClient** timeouts (no automatic retries) +4. **Parallel Processing**: Requires concurrent calls to multiple external services for optimal performance +5. **Error Handling & Resilience**: Graceful degradation with partial results when external sources fail +6. **JSON Processing**: Handle JSON serialization/deserialization for API responses and external data +7. **Testing Support**: Comprehensive testing capabilities for acceptance, integration, and unit tests—with **deterministic** timeout coverage using WireMock delay simulation +8. **Development Velocity**: Team expertise and rapid development requirements +9. **Production Readiness**: Monitoring, health checks, and operational capabilities ### Team Context @@ -45,6 +46,7 @@ Timeout behavior ([ADR-002](ADR-002-God-Analysis-API-Non-Functional-Requirements - **Development Speed**: Framework must enable rapid API development - **Team Expertise**: Leverage existing Spring/Java knowledge - **REST API Maturity**: Proven patterns for REST endpoint development +- **API-First Workflow**: Contract-first design with generated interfaces/models from OpenAPI to prevent implementation/spec drift - **HTTP Client Integration**: Built-in or well-integrated HTTP client capabilities - **Testing Ecosystem**: Comprehensive testing support including mocking external services **and simulating latency/timeouts with WireMock in-process delays** - **Operational Readiness**: Production monitoring and health check capabilities @@ -282,16 +284,20 @@ src/test/resources/ ### Rationale -1. **Spring MVC** provides mature servlet-based REST API development without reactive complexity -2. **RestClient** fits synchronous parallel fetches with configured timeouts (no reactive dependencies) -3. **Spring `RestClient`** gives **black-box** acceptance tests over a real port without a separate Groovy-based HTTP DSL -4. **WireMock** provides **isolated**, **deterministic** timeout validation aligned with ADR-002 -5. **No retry library** keeps the dependency graph and operational knobs smaller than a resilience4j-based design -6. **Servlet-only architecture** eliminates reactive programming complexity and dependency conflicts +1. **OpenAPI-first design** makes the contract explicit before code and reduces consumer/provider misunderstandings +2. **OpenAPI Generator Maven plugin** automates generation of API interfaces/models and reduces manual boilerplate +3. **Spring MVC** provides mature servlet-based REST API development without reactive complexity +4. **RestClient** fits synchronous parallel fetches with configured timeouts (no reactive dependencies) +5. **Spring `RestClient`** gives **black-box** acceptance tests over a real port without a separate Groovy-based HTTP DSL +6. **WireMock** provides **isolated**, **deterministic** timeout validation aligned with ADR-002 +7. **No retry library** keeps the dependency graph and operational knobs smaller than a resilience4j-based design +8. **Servlet-only architecture** eliminates reactive programming complexity and dependency conflicts ### Implementation Approach - **Architecture**: **Spring MVC servlet-based** - traditional thread-per-request model, **no reactive programming** +- **API contract**: Author and version OpenAPI 3.x specification first; treat it as the source of truth for request/response schema and operation signatures +- **Code generation**: Use `openapi-generator-maven-plugin` during build to generate Spring API interfaces and DTO models consumed by implementation - **REST Controller**: `@RestController`, `GET /api/v1/gods/stats/sum` - **HTTP Client**: **RestClient** (synchronous) with **connect/read timeouts** from configuration (e.g. 5s defaults) - **Parallelism**: `CompletableFuture` with virtual threads within servlet thread model per source; **one attempt per source** per request @@ -308,6 +314,7 @@ src/test/resources/ | Scope | Artifact / integration | Role | | ----- | ---------------------- | ---- | +| build | `org.openapitools:openapi-generator-maven-plugin` | API-first contract to code generation (Spring interfaces/models) from OpenAPI 3.x | | main | `spring-boot-starter-web` | **Spring MVC** REST API (servlet-based, **excludes reactive**) | | main | `spring-boot-starter-actuator` | Ops | | test | `spring-boot-starter-test` | JUnit 5, AssertJ, **MockMvc** (servlet-based testing) | @@ -340,13 +347,15 @@ src/test/resources/ ## Follow-up Actions -1. Add Maven dependency for WireMock (use `org.wiremock:wiremock` - the modern groupId that replaced `com.github.tomakehurst`) when using WireMock in tests -2. Set up **WireMock file structure** with `src/test/resources/mappings/` and `src/test/resources/__files/` directories for organized test data management -3. Create **JSON response files** in `__files` for each pantheon (greek, roman, nordic) with realistic god data for testing -4. Implement a **test support** class (or extension) that **starts** WireMock server, **wires** `RestClient` base URLs, and **resets stubs** between tests -5. Register **RestClient** with production connect/read timeouts; override URLs only in tests -6. Keep the **`RestClient`-based** acceptance suite small and Gherkin-aligned; **do not** introduce Rest Assured for this module unless a future ADR revisits the trade-off after Groovy/JVM fixes land upstream -7. Document WireMock setup, file structure, and stub reset patterns in module README +1. Add `openapi-generator-maven-plugin` to the module build and configure generation targets for Spring interfaces and API DTO models +2. Define and maintain the OpenAPI 3.x contract as the canonical API artifact before implementation changes +3. Add Maven dependency for WireMock (use `org.wiremock:wiremock` - the modern groupId that replaced `com.github.tomakehurst`) when using WireMock in tests +4. Set up **WireMock file structure** with `src/test/resources/mappings/` and `src/test/resources/__files/` directories for organized test data management +5. Create **JSON response files** in `__files` for each pantheon (greek, roman, nordic) with realistic god data for testing +6. Implement a **test support** class (or extension) that **starts** WireMock server, **wires** `RestClient` base URLs, and **resets stubs** between tests +7. Register **RestClient** with production connect/read timeouts; override URLs only in tests +8. Keep the **`RestClient`-based** acceptance suite small and Gherkin-aligned; **do not** introduce Rest Assured for this module unless a future ADR revisits the trade-off after Groovy/JVM fixes land upstream +9. Document WireMock setup, file structure, stub reset patterns, and OpenAPI generation workflow in module README ## Appendix: Rest Assured — issues observed and why it is discarded (2026-03-22) diff --git a/examples/requirements-examples/problem1/requirements/agile/US-001-god-analysis-api.openapi.yaml b/examples/requirements-examples/problem1/requirements/agile/US-001-god-analysis-api.openapi.yaml index 191abc0c..ef166c05 100644 --- a/examples/requirements-examples/problem1/requirements/agile/US-001-god-analysis-api.openapi.yaml +++ b/examples/requirements-examples/problem1/requirements/agile/US-001-god-analysis-api.openapi.yaml @@ -29,6 +29,10 @@ servers: security: [] +tags: + - name: gods + description: Operations for god-name analysis and aggregate statistics. + paths: /api/v1/gods/stats/sum: get: @@ -54,11 +58,21 @@ paths: in: query required: true description: | - Comma-separated list of pantheon sources to include. Typical values are `greek`, `roman`, and `nordic`. - Order may affect implementation; the aggregate is over all gods retrieved from the selected sources after filtering. + Comma-separated list of pantheon sources to include. + Allowed values: `greek`, `roman`, `nordic`. + The aggregate is computed over all gods retrieved from the selected sources after filtering. + style: form + explode: false schema: - type: string - example: "greek,roman,nordic" + type: array + minItems: 1 + uniqueItems: true + items: + $ref: "#/components/schemas/PantheonSource" + example: + - greek + - roman + - nordic responses: "200": description: | @@ -78,20 +92,46 @@ paths: value: sum: "0" "400": - description: Malformed request (e.g. missing required query parameters, invalid filter length, invalid source names). - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorResponse" + $ref: "#/components/responses/BadRequestResponse" "500": - description: Unexpected server failure. Not specified in acceptance tests; clients may treat as retryable. - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorResponse" + $ref: "#/components/responses/InternalServerErrorResponse" components: + responses: + BadRequestResponse: + description: Malformed request (e.g. missing query parameters, invalid filter length, invalid source value). + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + examples: + invalidFilterLength: + summary: Filter must be exactly one character + value: + code: INVALID_FILTER + message: "Query parameter 'filter' must contain exactly one character." + invalidSource: + summary: Unsupported source in sources list + value: + code: INVALID_SOURCE + message: "Query parameter 'sources' contains unsupported value: 'egyptian'." + InternalServerErrorResponse: + description: Unexpected server failure. Not specified in acceptance tests; clients may treat as retryable. + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + example: + code: INTERNAL_ERROR + message: "Unexpected error while computing aggregate." schemas: + PantheonSource: + type: string + enum: + - greek + - roman + - nordic + description: Supported external pantheon source identifier. GodStatsSumResponse: type: object required: @@ -105,5 +145,20 @@ components: example: "78179288397447443426" ErrorResponse: type: object - description: Optional error envelope; shape is not fixed by US-001 — adjust to match implementation. - additionalProperties: true + description: Error response envelope. + required: + - code + - message + properties: + code: + type: string + description: Stable machine-readable error code. + example: INVALID_FILTER + message: + type: string + description: Human-readable error detail. + example: "Query parameter 'filter' must contain exactly one character." + details: + type: object + description: Optional structured metadata about the validation or runtime error. + additionalProperties: true diff --git a/examples/requirements-examples/problem1/requirements/agile/US-001-plan-analysis-original.plan.md b/examples/requirements-examples/problem1/requirements/agile/US-001-plan-analysis-original.plan.md new file mode 100644 index 00000000..26c7cb35 --- /dev/null +++ b/examples/requirements-examples/problem1/requirements/agile/US-001-plan-analysis-original.plan.md @@ -0,0 +1,91 @@ +# US-001 Implementation Plan + +## Objective +Implement the `GET /api/v1/gods/stats/sum` endpoint in the module at [`/Users/jabrena/IdeaProjects/java-cursor-rules/examples/requirements-examples/problem1/implementation`](/Users/jabrena/IdeaProjects/java-cursor-rules/examples/requirements-examples/problem1/implementation), matching behavior defined in: +- [`/Users/jabrena/IdeaProjects/java-cursor-rules/examples/requirements-examples/problem1/requirements/agile/US-001_god_analysis_api.feature`](/Users/jabrena/IdeaProjects/java-cursor-rules/examples/requirements-examples/problem1/requirements/agile/US-001_god_analysis_api.feature) +- [`/Users/jabrena/IdeaProjects/java-cursor-rules/examples/requirements-examples/problem1/requirements/agile/US-001-god-analysis-api.openapi.yaml`](/Users/jabrena/IdeaProjects/java-cursor-rules/examples/requirements-examples/problem1/requirements/agile/US-001-god-analysis-api.openapi.yaml) +- ADR set in [`/Users/jabrena/IdeaProjects/java-cursor-rules/examples/requirements-examples/problem1/requirements/adr`](/Users/jabrena/IdeaProjects/java-cursor-rules/examples/requirements-examples/problem1/requirements/adr) + +## Current Baseline +- `implementation` currently contains only: + - [`/Users/jabrena/IdeaProjects/java-cursor-rules/examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/Application.java`](/Users/jabrena/IdeaProjects/java-cursor-rules/examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/Application.java) + - [`/Users/jabrena/IdeaProjects/java-cursor-rules/examples/requirements-examples/problem1/implementation/src/main/resources/application.yml`](/Users/jabrena/IdeaProjects/java-cursor-rules/examples/requirements-examples/problem1/implementation/src/main/resources/application.yml) + - [`/Users/jabrena/IdeaProjects/java-cursor-rules/examples/requirements-examples/problem1/implementation/pom.xml`](/Users/jabrena/IdeaProjects/java-cursor-rules/examples/requirements-examples/problem1/implementation/pom.xml) +- Most previously implemented code and tests were deleted and must be reintroduced from requirements. + +## Implementation Scope + +### 1) Domain and API contract layer +- Add request/response and enum models: + - `PantheonSource` (`greek`, `roman`, `nordic`) + - `GodStatsResponse` with `sum` as decimal string + - `ErrorResponse` with stable `code` and `message` +- Add strict query validation at controller boundary: + - `filter` required, exact length 1 + - `sources` required, non-empty, only allowed values +- Add endpoint: `GET /api/v1/gods/stats/sum`. + +### 2) Outbound integration and configuration +- Add outbound configuration binding for URLs and timeouts from `application.yml` (`god.outbound.*`). +- Configure Spring `RestClient` with explicit connect/read timeouts. +- Implement one outbound client method per source (single attempt, no retries). + +### 3) Core business logic +- Parse/normalize selected sources from query input. +- Fetch selected sources in parallel (`CompletableFuture` per ADR direction). +- On timeout/transport failure, treat source as absent and continue. +- Filter names by first-character exact match (case-sensitive). +- Convert each filtered name using Unicode-int concatenation rule. +- Aggregate with `BigInteger` and return `sum` as string. + +### 4) Error mapping and resilience behavior +- Add custom bad-request exceptions for invalid query conditions. +- Add global exception handler mapping: + - validation and domain input errors -> HTTP 400 + - unexpected runtime errors -> HTTP 500 with stable envelope +- Preserve successful HTTP 200 on partial-results timeout scenarios. + +### 5) Testing strategy (must match feature + ADR) +- Unit tests: + - Unicode conversion algorithm correctness + - filter behavior (case sensitivity) + - source parsing validation +- Controller validation tests: + - missing/empty/invalid `filter` + - missing/empty/invalid `sources` +- Integration tests with WireMock: + - happy path all sources + - multiple source timeout with partial aggregation + - per-test stub reset for isolation +- HTTP acceptance tests (`@SpringBootTest(RANDOM_PORT)` + Spring `RestClient`): + - validate feature-level scenarios and expected `sum` outputs. + +### 6) Build and verification +- Ensure `pom.xml` dependencies are sufficient for: + - Spring MVC runtime + - Spring Boot test stack + - WireMock tests +- Run module verification in `implementation`: + - `./mvnw clean test` (module-level) +- Confirm all feature-driven scenarios are represented by tests. + +## File Plan (Expected) +- Main code under: + - [`/Users/jabrena/IdeaProjects/java-cursor-rules/examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms`](/Users/jabrena/IdeaProjects/java-cursor-rules/examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms) +- Tests under: + - [`/Users/jabrena/IdeaProjects/java-cursor-rules/examples/requirements-examples/problem1/implementation/src/test/java/info/jab/ms`](/Users/jabrena/IdeaProjects/java-cursor-rules/examples/requirements-examples/problem1/implementation/src/test/java/info/jab/ms) +- Test fixtures for WireMock: + - [`/Users/jabrena/IdeaProjects/java-cursor-rules/examples/requirements-examples/problem1/implementation/src/test/resources/mappings`](/Users/jabrena/IdeaProjects/java-cursor-rules/examples/requirements-examples/problem1/implementation/src/test/resources/mappings) + - [`/Users/jabrena/IdeaProjects/java-cursor-rules/examples/requirements-examples/problem1/implementation/src/test/resources/__files`](/Users/jabrena/IdeaProjects/java-cursor-rules/examples/requirements-examples/problem1/implementation/src/test/resources/__files) + +## Execution Order +1. Recreate domain + controller + validation + error handling. +2. Recreate outbound client/config and parallel aggregation service. +3. Recreate unit + controller tests. +4. Add WireMock integration and acceptance tests. +5. Run tests and adjust contract mismatches against OpenAPI/feature. + +## Notes +- No retries will be introduced (explicit ADR scope). +- Response uses stringified numeric sum to avoid precision issues. +- Plan replaces the deleted agile plan artifact style (`US-001-plan-analysis.plan.md`). diff --git a/examples/requirements-examples/problem1/requirements/agile/US-001-plan-analysis.plan.md b/examples/requirements-examples/problem1/requirements/agile/US-001-plan-analysis.plan.md index fc770382..490d15df 100644 --- a/examples/requirements-examples/problem1/requirements/agile/US-001-plan-analysis.plan.md +++ b/examples/requirements-examples/problem1/requirements/agile/US-001-plan-analysis.plan.md @@ -1,218 +1,152 @@ --- -name: God Analysis API Implementation -overview: "Spring Boot REST API with parallel HTTP fetches: RestClient connect/read timeouts (defaults in application.yml), partial results when a source times out—no outbound retries; Unicode aggregation via London Style TDD." -todos: [] +name: US-001 God Analysis API Plan +overview: "Implement GET /api/v1/gods/stats/sum using London Style outside-in TDD, OpenAPI-first contract, servlet-only Spring MVC, and deterministic verification gates." +todos: + - id: 1 + content: Setup OpenAPI generation and baseline test scaffolding + status: TODO + - id: 2 + content: RED acceptance tests for HTTP contract and validations + status: TODO + - id: 3 + content: GREEN API boundary implementation + status: TODO + - id: 4 + content: Refactor API layer (logging then config/error handling) + status: TODO + - id: 5 + content: Verify Milestone A1 + status: TODO + - id: 6 + content: RED service and algorithm unit tests + status: TODO + - id: 7 + content: GREEN service implementation (parallel aggregation) + status: TODO + - id: 8 + content: Refactor service layer (logging then optimization) + status: TODO + - id: 9 + content: Verify Milestone A2 + status: TODO + - id: 10 + content: RED outbound integration tests with WireMock delays + status: TODO + - id: 11 + content: GREEN outbound clients and timeout-tolerant behavior + status: TODO + - id: 12 + content: Refactor integration layer (logging then hardening) + status: TODO + - id: 13 + content: Verify Milestone A3 and full build + status: TODO isProject: false --- # Problem 1: God Analysis API Implementation Plan ## Requirements Summary - -**User Story:** As an API consumer, I want to retrieve aggregated statistical data about god names from multiple mythological sources with resilient HTTP fetching and Unicode-based calculations. +**User Story:** Implement `GET /api/v1/gods/stats/sum` in `examples/requirements-examples/problem1/implementation` aligned with feature scenarios, OpenAPI contract, and ADR constraints. **Key Business Rules:** -- **Unicode Aggregation:** Convert each character to Unicode code point decimal, concatenate per name, sum as BigInteger -- **Case-Sensitive Filtering:** Filter names by first Unicode code point matching the filter parameter -- **Resilient HTTP:** **RestClient** connect/read timeouts (defaults in `application.yml`, overridable via environment variables). **Single attempt per source** per request; partial results when a source times out or errors. **No** automatic retries (aligned with ADR-002 / ADR-003 scope reduction). -- **Parallel Processing:** Fetch from multiple sources concurrently (Greek, Roman, Nordic) -- **Expected Result:** JSON response `{"sum": ""}` for successful aggregation +- **Contract source of truth:** OpenAPI (`US-001-god-analysis-api.openapi.yaml`) defines endpoint, params, response, and errors. +- **Input validation:** `filter` must be a single character; `sources` must contain only supported pantheons. +- **Aggregation behavior:** Sum is computed from Unicode-concatenated values of filtered god names and returned as decimal string. +- **Partial results on failures:** Timeout/failure on one source omits that source but still returns `200` with partial aggregation. +- **Architecture constraint:** Servlet-only Spring MVC stack; no reactive dependencies or patterns. ## Approach - -**Strategy:** London Style Outside-In TDD - Start with acceptance tests, work inward through REST controller, service layer, and HTTP client components. +Use **London Style (outside-in) TDD**: start with acceptance/API behavior, then service logic, then outbound integrations, with Verify gates after each GREEN slice. ```mermaid flowchart LR - subgraph Sources [External HTTP Sources] - G[Greek API] - R[Roman API] - N[Nordic API] + subgraph A1[Milestone A1 - API Boundary] + A[Acceptance RED] --> B[Controller GREEN] + B --> C[Refactor logging] + C --> D[Refactor config/errors] + D --> E[Verify A1] end - subgraph Application [God Analysis API] - Controller[REST Controller
GET /api/v1/gods/stats/sum] - Service[God Analysis Service
Orchestration & Aggregation] - Client[HTTP Client
RestClient timeouts] - Algorithm[Unicode Algorithm
Filter & Sum] + subgraph A2[Milestone A2 - Domain Service] + F[Service tests RED] --> G[Service impl GREEN] + G --> H[Refactor logging] + H --> I[Refactor optimize] + I --> J[Verify A2] end - Sources --> Client - Client --> Service - Service --> Algorithm - Algorithm --> Service - Service --> Controller - Controller --> Response[JSON Response
sum: string] + subgraph A3[Milestone A3 - Outbound Integration] + K[WireMock IT RED] --> L[Client impl GREEN] + L --> M[Refactor logging] + M --> N[Refactor hardening] + N --> O[Verify A3] + end + + E --> F + J --> K ``` ## Task List - | # | Task | Phase | TDD | Milestone | Parallel | Status | |---|------|-------|-----|-----------|----------|--------| -| 1 | Create Maven project structure with dependencies | Setup | | | A1 | ✔ | -| 2 | Write acceptance test for happy path sum calculation | RED | Test | | A1 | ✔ | -| 3 | Create REST controller stub to pass acceptance test | GREEN | Impl | | A1 | ✔ | -| 4 | Add structured logging for request/response | Refactor | | | A1 | ✔ | -| 5 | Optimize controller validation and error handling | Refactor | | | A1 | ✔ | -| 6 | Verify acceptance test passes with controller | Verify | | milestone | A1 | ✔ | -| 7 | Write service layer unit test for aggregation logic | RED | Test | | A2 | — | -| 8 | Implement service with Unicode algorithm | GREEN | Impl | | A2 | — | -| 9 | Add service-level logging | Refactor | | | A2 | — | -| 10 | Optimize service configuration and error handling | Refactor | | | A2 | — | -| 11 | Write integration test for filter=N zero result | RED | Test | | A2 | — | -| 12 | Implement filter logic validation | GREEN | Impl | | A2 | — | -| 13 | Verify service unit tests pass | Verify | | milestone | A2 | — | -| 14 | Write HTTP client tests for timeout-bound fetching (single attempt per source) | RED | Test | | A3 | — | -| 15 | Implement HTTP client with configured RestClient timeouts (no retries) | GREEN | Impl | | A3 | — | -| 16 | Add client-level logging for HTTP outcomes | Refactor | | | A3 | — | -| 17 | Optimize client configuration (timeouts, per-source isolation) | Refactor | | | A3 | — | -| 18 | Write integration test for Nordic timeout scenario | RED | Test | | A3 | — | -| 19 | Implement partial result handling in service | GREEN | Impl | | A3 | — | -| 20 | Verify HTTP client tests pass (timeout phase) | Verify | | milestone | A3 | — | -| 21 | Verify all integration tests pass | Verify | | milestone | A4 | ✔ | +| 1 | Configure `openapi-generator-maven-plugin` and generate API interfaces/DTOs | Setup | | | A1 | TODO | +| 2 | Write failing acceptance tests for happy path and validation errors using Spring `RestClient` | RED | Test | | A1 | TODO | +| 3 | Implement controller, request mapping, enum parsing, and error envelope to satisfy acceptance tests | GREEN | Impl | | A1 | TODO | +| 4 | Add API-layer observability/logging for request validation and failure mapping | Refactor | | | A1 | TODO | +| 5 | Optimize API configuration and exception handling consistency with OpenAPI responses | Refactor | | | A1 | TODO | +| 6 | Verify Milestone A1 (`./mvnw -q test`) and ensure acceptance tests are green | Verify | | milestone | A1 | TODO | +| 7 | Write failing unit tests for Unicode conversion, source parsing, and filter case-sensitivity | RED | Test | | A2 | TODO | +| 8 | Implement aggregation service with immutable flow, `CompletableFuture` parallelization, and `BigInteger` sum | GREEN | Impl | | A2 | TODO | +| 9 | Add service-level observability for source execution and fallback decisions | Refactor | | | A2 | TODO | +| 10 | Optimize service concurrency boundaries and deterministic merge behavior | Refactor | | | A2 | TODO | +| 11 | Verify Milestone A2 (`./mvnw -q test`) with unit + prior API tests green | Verify | | milestone | A2 | TODO | +| 12 | Write failing WireMock integration tests for full-source success and partial-timeout scenarios | RED | Test | | A3 | TODO | +| 13 | Implement outbound client config (`god.outbound.*`) and timeout-tolerant per-source calls | GREEN | Impl | | A3 | TODO | +| 14 | Add outbound-call logging/metrics hooks and interaction verification helpers | Refactor | | | A3 | TODO | +| 15 | Optimize timeout values, stub isolation/reset, and error-translation hardening | Refactor | | | A3 | TODO | +| 16 | Verify Milestone A3 with `./mvnw clean test` and confirm no reactive dependencies introduced | Verify | | milestone | A3 | TODO | ## Execution Instructions - When executing this plan: 1. Complete the current task. -2. **Update the Task List**: set the Status column for that task (e.g., ✔ or Done). -3. **For GREEN tasks**: MUST complete the associated Verify task before proceeding. -4. **For Verify tasks**: MUST ensure all tests pass and build succeeds before proceeding. -5. **Milestone rows** (Milestone column): a milestone is evolving complete software for that slice — complete the pair of Refactor tasks (logging, then optimize config/error handling/log levels) immediately before each milestone Verify. -6. Only then proceed to the next task. -7. Repeat for all tasks. Never advance without updating the plan. +2. **Update the Task List**: set the `Status` for that task (`TODO`, `IN_PROGRESS`, `DONE`, `BLOCKED`). +3. **Update frontmatter `todos`** for the corresponding task id to keep status synchronized. +4. **For GREEN tasks**: MUST complete the associated Verify task before proceeding. +5. **For Verify tasks**: MUST ensure all tests pass and build succeeds before proceeding. +6. **Milestone rows**: complete the pair of Refactor tasks (logging, then optimization/hardening) immediately before each Verify row. +7. Only then proceed to the next task. Never advance without status updates. **Critical Stability Rules:** -- After every GREEN implementation task, run the verification step -- All tests must pass before proceeding to the next implementation -- If any test fails during verification, fix the issue before advancing -- Never skip verification steps - they ensure software stability +- After every GREEN implementation task, run the verification step for the current milestone. +- All tests must pass before proceeding to the next implementation task. +- If any test fails during verification, fix the issue before advancing. +- Never skip verification steps. +- Keep behavior aligned with OpenAPI + `.feature` scenarios; document conflicts before any requirement changes. -**Parallel column:** Use grouping identifiers (A1, A2, A3, A4, etc.) to group tasks into the same delivery slice. Use when assigning agents or branches to a milestone scope. +**Parallel column:** `A1`, `A2`, `A3` define delivery slices. Parallel work is allowed only within an active slice if task dependencies permit it. ## File Checklist - | Order | File | |-------|------| -| 1 | `god-analysis-api/pom.xml` | -| 2 | `god-analysis-api/src/main/resources/application.yml` | -| 3 | `god-analysis-api/src/test/java/info/jab/ms/controller/GodAnalysisApiAT.java` | -| 4 | `god-analysis-api/src/main/java/info/jab/ms/controller/GodStatsController.java` | -| 5 | `god-analysis-api/src/main/java/info/jab/ms/dto/GodStatsResponse.java` | -| 6 | `god-analysis-api/src/test/java/info/jab/ms/service/GodAnalysisServiceTest.java` | -| 7 | `god-analysis-api/src/main/java/info/jab/ms/service/GodAnalysisService.java` | -| 8 | `god-analysis-api/src/main/java/info/jab/ms/algorithm/UnicodeAggregator.java` | -| 9 | `god-analysis-api/src/test/java/info/jab/ms/client/GodDataClientTest.java` | -| 10 | `god-analysis-api/src/main/java/info/jab/ms/client/GodDataClient.java` | -| 11 | `god-analysis-api/src/main/java/info/jab/ms/config/HttpClientConfig.java` | -| 12 | `god-analysis-api/src/test/java/info/jab/ms/controller/GodAnalysisApiIT.java` | -| 13 | `god-analysis-api/src/test/resources/wiremock/greek-gods.json` | -| 14 | `god-analysis-api/src/test/resources/wiremock/roman-gods.json` | -| 15 | `god-analysis-api/src/test/resources/wiremock/nordic-gods.json` | -| 16 | `god-analysis-api/src/test/java/info/jab/ms/support/WireMockExtension.java` (or equivalent) — WireMock lifecycle, base URLs, **reset stubs between tests** | -| 17 | `god-analysis-api/README.md` | - -## Authoritative Sources - -**Primary Contracts (Implementation Must Match):** -- [US-001_god_analysis_api.feature](US-001_god_analysis_api.feature) — Three test scenarios defining exact behavior -- [US-001-god-analysis-api.openapi.yaml](US-001-god-analysis-api.openapi.yaml) — API contract with response schema and parameters -- [ADR-001-God-Analysis-API-Functional-Requirements.md](../adr/ADR-001-God-Analysis-API-Functional-Requirements.md) — Architecture decisions (monolith, no auth, direct HTTP) -- [ADR-002-God-Analysis-API-Non-Functional-Requirements.md](../adr/ADR-002-God-Analysis-API-Non-Functional-Requirements.md) — Timeouts via `RestClient`, parallel calls, partial results; **no automatic retries**; **circuit breaker explicitly out of scope** for initial implementation -- [ADR-003-God-Analysis-API-Technology-Stack.md](../adr/ADR-003-God-Analysis-API-Technology-Stack.md) — Runtime stack, `RestClient` (outbound **and** HTTP acceptance tests), **WireMock** (in-process or Testcontainers) for isolated **timeout** tests - -**Test Scenarios:** -1. **Happy Path:** All sources respond → exact sum calculation -2. **Partial Failure:** Nordic times out → sum from Greek + Roman only -3. **Filter Edge Case:** `filter=N` → sum equals `"0"` - -**ADR-002 vs API contract:** Earlier drafts mentioned logging which sources contributed; the OpenAPI and Gherkin only require `sum`. **Approach:** satisfy the public contract first; optional **structured logging** for successful vs failed/timed-out sources per request. If product later wants this in JSON, extend OpenAPI in a follow-up. - -## Runtime stack (reconcile ADR-003 with repo) - -[ADR-003-God-Analysis-API-Technology-Stack.md](../adr/ADR-003-God-Analysis-API-Technology-Stack.md) selects **Spring Boot 4.0.4** and **Java 26**. The repo root uses **Java 25** ([pom.xml](../../../../../pom.xml)). **Recommendation:** use **Spring Boot 4.0.4** with **Java 25** unless you explicitly standardize this example on Java 26—Boot 4 supports Java 17+. - -Examples are **not** Maven modules of the root reactor; mirror [examples/spring-boot-demo/implementation/pom.xml](../../../../spring-boot-demo/implementation/pom.xml) as a **standalone** Maven project (new directory under `examples/requirements-examples/problem1/`, e.g. `god-analysis-api/`). - -**Package Structure:** All Java classes must use the base package `info.jab.ms` with appropriate sub-packages (controller, service, dto, client, config, algorithm) as specified in [ADR-003-God-Analysis-API-Technology-Stack.md](../adr/ADR-003-God-Analysis-API-Technology-Stack.md). - -## Domain algorithm (must match acceptance math) - -External APIs return **JSON arrays of strings** (array of god names). - -1. **Per source:** GET URL, deserialize to `List` (or stream) of names. -2. **Filter:** include names where the **first Unicode code point** equals the single `filter` code point (**case-sensitive**). -3. **Per-name value:** for each code point in the name, append `Integer.toString(codePoint)` as decimal digits, forming one big decimal string; parse to `BigInteger` (not `long`). -4. **Total:** sum all per-name `BigInteger` values; serialize result with `toString()` for JSON `sum`. - -Use `String.codePoints()` (or equivalent) so supplementary characters are handled correctly. - -```mermaid -flowchart LR - subgraph parallel [Parallel per selected source] - G[Greek HTTP] - R[Roman HTTP] - N[Nordic HTTP] - end - parallel --> merge[Merge name lists] - merge --> filter[Prefix filter case-sensitive] - filter --> convert[Concat codepoint decimals per name] - convert --> sum[BigInteger sum] - sum --> json["JSON sum string"] -``` - -## Configuration - -- **Single Configuration:** All settings in `application.yml` with production-ready defaults (e.g. 5s connect and read timeouts for outbound `RestClient`) -- **Base URLs** for `greek`, `roman`, `nordic` with defaults matching ADR-001 URLs -- **Environment Variables:** Runtime customization without profile complexity -- **Outbound HTTP:** One GET per selected source with configured timeouts; no retry properties—keep configuration minimal. -- **Parallelism:** Fetch selected sources concurrently; **wait** until each source returns or times out (single attempt), then merge and aggregate. - -Wire Spring configuration via `@ConfigurationProperties` for testability. - -## HTTP client and resilience - -- **Implementation:** **Timeouts + partial results** with **one attempt** per source per request. Matches ADR-001/ADR-002 without a retry phase. -- **Circuit breaker:** Not required for v1 (ADR-002 considered and rejected it until persistent failure patterns justify it). -- Use **RestClient** (Spring 6 / Boot 4 style) with a **shared** builder or factory applying **connect/read** timeouts from `application.yml`. -- Implement a small **GodListClient** (or per-source callable) that: - - Performs a **single** GET per source within the configured timeouts; on timeout or error, treat that source as empty for aggregation (partial result path). - - Does **not** wrap calls in Resilience4j Retry or Spring Retry for US-001. -- **Do not** fail the whole request if one source fails or times out. - -## REST layer - -- `@RestController` with class-level `@RequestMapping("/api/v1")` and `@GetMapping("/gods/stats/sum")`. -- Bind `filter` (`String` length 1) and `sources` (`String`); parse `sources` to enum or set (`greek`, `roman`, `nordic`). -- Response DTO: `{ "sum": string }` — Jackson serializes `sum` as string; use `BigInteger` internally, expose string in DTO. -- Optional: `springdoc-openapi` + static copy or generation from existing [US-001-god-analysis-api.openapi.yaml](US-001-god-analysis-api.openapi.yaml). -- Optional: `@ControllerAdvice` for **400** on missing/invalid params (OpenAPI reserves 400; feature does not require it—keep validation minimal if you want zero behavior change vs stubs). - -## Testing strategy - -**Stack (see [ADR-003-God-Analysis-API-Technology-Stack.md](../adr/ADR-003-God-Analysis-API-Technology-Stack.md)):** Use **WireMock** (in-process or **Testcontainers**-hosted) with **fixed delays** on stubbed upstream routes so the SUT’s **RestClient** hits configured read timeouts deterministically. Point the app at WireMock URLs in tests. **Reset stubs** in `@BeforeEach` / `@AfterEach` (or equivalent) so **every** timeout test starts **isolated**. - -| Goal | Approach | -|------|----------| -| Happy path / exact `sum` | `@SpringBootTest` + **Spring `RestClient`** against **RANDOM_PORT**; upstream **WireMock** JSON fixtures so `sum` equals **`78179288397447443426`**. No live network in CI. | -| Nordic (or Roman) timeout / partial sum | Stub one or more pantheon routes with **delay greater than read timeout**; assert partial `sum` matches the feature file expectation for Greek + Roman only. | -| `filter=N` → `"0"` | Same stack; assert `sum` is `"0"`. | -| Unit tests | Pure tests for conversion + filter + aggregation with small strings (**no** WireMock). | - -Tag tests to mirror Gherkin: e.g. JUnit `@Tag("acceptance-test")` and `@Tag("integration-test")` for Maven `groups`/Surefire filters if desired. - -**Gherkin execution:** Optional Cucumber step definitions are **not** required if JUnit tests implement the same assertions; only add Cucumber if you need literal `.feature` execution in CI. - -## Deliverables checklist - -- New Maven project with `spring-boot-starter-web`, `spring-boot-starter-actuator` (ADR-003), `spring-boot-starter-test` (includes AssertJ; use **`RestClient`** from `spring-web` for acceptance tests—**no** Rest Assured per ADR-003), optional **Testcontainers** + **`org.wiremock:wiremock`** if you run WireMock via containers; in-process WireMock needs no Docker. -- **Single configuration file** `application.yml` with production-ready **RestClient** timeout settings and environment variable support -- `README` or `DEVELOPER.md` in the new module: how to run, Docker only if Testcontainers is used, configure URLs/timeouts via environment variables, run tests. -- `./mvnw clean verify` from the new module passes. +| 1 | `examples/requirements-examples/problem1/implementation/pom.xml` | +| 2 | `examples/requirements-examples/problem1/implementation/src/main/resources/application.yml` | +| 3 | `examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/controller/GodStatsController.java` | +| 4 | `examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/controller/GlobalExceptionHandler.java` | +| 5 | `examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/service/GodAnalysisService.java` | +| 6 | `examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/algorithm/UnicodeAggregator.java` | +| 7 | `examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/client/GodDataClient.java` | +| 8 | `examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/config/GodOutboundProperties.java` | +| 9 | `examples/requirements-examples/problem1/implementation/src/main/java/info/jab/ms/config/HttpClientConfig.java` | +| 10 | `examples/requirements-examples/problem1/implementation/src/test/java/info/jab/ms/controller/GodAnalysisApiAT.java` | +| 11 | `examples/requirements-examples/problem1/implementation/src/test/java/info/jab/ms/service/GodAnalysisServiceTest.java` | +| 12 | `examples/requirements-examples/problem1/implementation/src/test/java/info/jab/ms/controller/GodAnalysisApiIT.java` | +| 13 | `examples/requirements-examples/problem1/implementation/src/test/java/info/jab/ms/controller/GodAnalysisPartialTimeoutIT.java` | +| 14 | `examples/requirements-examples/problem1/implementation/src/test/resources/mappings/*.json` | +| 15 | `examples/requirements-examples/problem1/implementation/src/test/resources/__files/*` | ## Notes - -- **Exact happy-path sum:** Depends on **current** Typicode data; **pin** test data in WireMock JSON so builds are deterministic. -- **Timeout bounds:** Worst-case latency is roughly bounded by the configured **read timeout** per slow source (parallel fetches), with **no** retry multiplier. -- **Java version:** Pick **25 vs 26** explicitly for this example module’s `pom.xml` `java.version`. +- Package layout target remains `info.jab.ms` with `controller`, `service`, `client`, `config`, `algorithm`, `dto`, and `exception` packages. +- Canonical source enum values: `greek`, `roman`, `nordic`. +- `GodStatsResponse.sum` remains string-based decimal to avoid numeric overflow concerns. +- First-character filter is exact and case-sensitive by design. +- Out of scope: retries/backoff, new endpoints, and modifications to requirement artifacts unless inconsistency is documented. diff --git a/examples/requirements-examples/problem1/requirements/diagrams/c4-level1-context.png b/examples/requirements-examples/problem1/requirements/diagrams/c4-level1-context.png deleted file mode 100644 index bd21bc0236e5bcbf96c2f217b9228269c4d63651..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33716 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

Qc7!;}Y!ResH?g$#MSZqb=NK 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 diff --git a/examples/requirements-examples/problem1/requirements/diagrams/c4-level1-context.puml b/examples/requirements-examples/problem1/requirements/diagrams/c4-level1-context.puml deleted file mode 100644 index 03018035..00000000 --- a/examples/requirements-examples/problem1/requirements/diagrams/c4-level1-context.puml +++ /dev/null @@ -1,20 +0,0 @@ -@startuml c4-level1-context -!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Context.puml - -title System Context Diagram - God Analysis API (Level 1) - -Person(apiClient, "API Client", "Developer or application consumer calling the God Analysis REST API to retrieve statistics about god names") - -System(godAnalysisApi, "God Analysis API", "Spring Boot microservice that fetches god names from external mythology data sources, filters them by a Unicode character, and computes the sum of Unicode code points") - -System_Ext(greekGodsApi, "Greek Gods API", "External JSON REST API providing a list of Greek god names\nhttps://my-json-server.typicode.com/.../greek") -System_Ext(romanGodsApi, "Roman Gods API", "External JSON REST API providing a list of Roman god names\nhttps://my-json-server.typicode.com/.../roman") -System_Ext(nordicGodsApi, "Nordic Gods API", "External JSON REST API providing a list of Nordic god names\nhttps://my-json-server.typicode.com/.../nordic") - -Rel(apiClient, godAnalysisApi, "Calls GET /api/v1/gods/stats/sum", "HTTPS/JSON") -Rel(godAnalysisApi, greekGodsApi, "Fetches Greek god names", "HTTPS/JSON") -Rel(godAnalysisApi, romanGodsApi, "Fetches Roman god names", "HTTPS/JSON") -Rel(godAnalysisApi, nordicGodsApi, "Fetches Nordic god names", "HTTPS/JSON") - -SHOW_LEGEND() -@enduml diff --git a/examples/requirements-examples/problem1/requirements/diagrams/c4-level2-container.png b/examples/requirements-examples/problem1/requirements/diagrams/c4-level2-container.png deleted file mode 100644 index 09ff0143b5e9a8fe30f84209e734c23a80fae4f5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33123 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 Fe*toyQN;iN diff --git a/examples/requirements-examples/problem1/requirements/diagrams/c4-level2-container.puml b/examples/requirements-examples/problem1/requirements/diagrams/c4-level2-container.puml deleted file mode 100644 index 8c159081..00000000 --- a/examples/requirements-examples/problem1/requirements/diagrams/c4-level2-container.puml +++ /dev/null @@ -1,22 +0,0 @@ -@startuml c4-level2-container -!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml - -title Container Diagram - God Analysis API (Level 2) - -Person(apiClient, "API Client", "Developer or application consumer calling the God Analysis REST API") - -System_Boundary(godAnalysisSystem, "God Analysis API") { - Container(springBootApp, "God Analysis Application", "Java 25, Spring Boot", "Exposes a REST endpoint to compute Unicode-based statistics over mythology god names fetched from external APIs") -} - -System_Ext(greekGodsApi, "Greek Gods API", "External REST API returning a JSON list of Greek god names") -System_Ext(romanGodsApi, "Roman Gods API", "External REST API returning a JSON list of Roman god names") -System_Ext(nordicGodsApi, "Nordic Gods API", "External REST API returning a JSON list of Nordic god names") - -Rel(apiClient, springBootApp, "GET /api/v1/gods/stats/sum?filter=A&sources=greek,roman,nordic", "HTTPS/JSON") -Rel(springBootApp, greekGodsApi, "Fetches Greek god names", "HTTPS/JSON, RestClient") -Rel(springBootApp, romanGodsApi, "Fetches Roman god names", "HTTPS/JSON, RestClient") -Rel(springBootApp, nordicGodsApi, "Fetches Nordic god names", "HTTPS/JSON, RestClient") - -SHOW_LEGEND() -@enduml diff --git a/examples/requirements-examples/problem1/requirements/diagrams/c4-level3-component.png b/examples/requirements-examples/problem1/requirements/diagrams/c4-level3-component.png deleted file mode 100644 index 1b4229afbc9bb49ca7786b64d6d6af0f4c57f02c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 66292 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 R?Fa?=NJ}V)SBe^b|6fd!Y54#E diff --git a/examples/requirements-examples/problem1/requirements/diagrams/c4-level3-component.puml b/examples/requirements-examples/problem1/requirements/diagrams/c4-level3-component.puml deleted file mode 100644 index 401897d9..00000000 --- a/examples/requirements-examples/problem1/requirements/diagrams/c4-level3-component.puml +++ /dev/null @@ -1,39 +0,0 @@ -@startuml c4-level3-component -!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Component.puml - -title Component Diagram - God Analysis Application (Level 3) - -Person(apiClient, "API Client", "Developer or application consumer") - -Container_Boundary(springBootApp, "God Analysis Application") { - Component(godStatsController, "GodStatsController", "Spring @RestController", "Handles GET /api/v1/gods/stats/sum. Validates filter (single Unicode char) and sources (greek, roman, nordic) query parameters") - - Component(globalExceptionHandler, "GlobalExceptionHandler", "Spring @RestControllerAdvice", "Catches IllegalArgumentException and returns HTTP 400 ProblemDetail responses for invalid input") - - Component(godAnalysisService, "GodAnalysisService", "Spring @Service", "Orchestrates fetching god names from requested mythology sources and delegates computation to UnicodeAggregator") - - Component(godDataClient, "GodDataClient", "Spring @Component", "HTTP client using Spring RestClient to fetch god name lists from the external Greek, Roman, and Nordic REST APIs") - - Component(unicodeAggregator, "UnicodeAggregator", "Plain Java class", "Filters names starting with the given Unicode code point, converts names to code-point strings, and sums as BigInteger") - - Component(httpClientConfig, "HttpClientConfig", "Spring @Configuration", "Configures the RestClient bean with connect and read timeouts sourced from GodApiProperties") - - Component(godApiProperties, "GodApiProperties", "Spring @ConfigurationProperties", "Holds external API URLs and timeout values loaded from application.yml or environment variables") -} - -System_Ext(greekGodsApi, "Greek Gods API", "External REST API returning JSON list of Greek god names") -System_Ext(romanGodsApi, "Roman Gods API", "External REST API returning JSON list of Roman god names") -System_Ext(nordicGodsApi, "Nordic Gods API", "External REST API returning JSON list of Nordic god names") - -Rel(apiClient, godStatsController, "GET /api/v1/gods/stats/sum", "HTTPS/JSON") -Rel(godStatsController, godAnalysisService, "computeSum(filter, sourceList)") -Rel(godStatsController, globalExceptionHandler, "IllegalArgumentException propagated to") -Rel(godAnalysisService, godDataClient, "fetchGreekGods() / fetchRomanGods() / fetchNordicGods()") -Rel(godAnalysisService, unicodeAggregator, "aggregate(names, filter)") -Rel(godDataClient, greekGodsApi, "HTTP GET greek URL", "HTTPS/JSON") -Rel(godDataClient, romanGodsApi, "HTTP GET roman URL", "HTTPS/JSON") -Rel(godDataClient, nordicGodsApi, "HTTP GET nordic URL", "HTTPS/JSON") -Rel(httpClientConfig, godApiProperties, "reads timeout configuration") - -SHOW_LEGEND() -@enduml diff --git a/examples/requirements-examples/problem1/requirements/openspec/changes/archive/2026-04-06-add-spring-actuator/design.md b/examples/requirements-examples/problem1/requirements/openspec/changes/archive/2026-04-06-add-spring-actuator/design.md deleted file mode 100644 index b58c8f15..00000000 --- a/examples/requirements-examples/problem1/requirements/openspec/changes/archive/2026-04-06-add-spring-actuator/design.md +++ /dev/null @@ -1,24 +0,0 @@ -# Design: Add Spring Boot Actuator - -## Overview - -Introduce Actuator into the existing Spring Boot service to provide standard operational endpoints, primarily health checks. - -## Decisions - -- Use Spring Boot Actuator (`spring-boot-starter-actuator`) as the observability baseline. -- Configure management endpoint exposure through application properties. -- Keep health endpoint publicly reachable in local/dev scenarios used by tests and examples. -- Do not modify domain logic or the aggregation algorithm. - -## Endpoint expectations - -- `GET /actuator/health` returns service health status. -- Optional additional endpoints can be exposed through configuration, but health is mandatory for this change. - -## Risks and mitigations - -- Risk: exposing too many management endpoints. - - Mitigation: explicitly configure a minimal exposure list (at least `health`). -- Risk: accidental behavior changes in existing API. - - Mitigation: run existing verification tests and keep endpoint additions isolated to configuration/dependencies. diff --git a/examples/requirements-examples/problem1/requirements/openspec/changes/archive/2026-04-06-add-spring-actuator/proposal.md b/examples/requirements-examples/problem1/requirements/openspec/changes/archive/2026-04-06-add-spring-actuator/proposal.md deleted file mode 100644 index bdb91936..00000000 --- a/examples/requirements-examples/problem1/requirements/openspec/changes/archive/2026-04-06-add-spring-actuator/proposal.md +++ /dev/null @@ -1,18 +0,0 @@ -# Change: Add Spring Boot Actuator - -## Why - -The `god-analysis-api` service needs operational visibility for runtime health and readiness checks. Adding Spring Boot Actuator enables standardized health endpoints used by local operations and deployment environments. - -## What changes - -- Add Spring Boot Actuator dependency to the implementation module. -- Expose actuator endpoints with explicit management configuration. -- Guarantee availability of health information for operators. -- Keep existing functional API behavior unchanged. - -## Scope - -- Capability affected: `god-analysis-api` -- Type: additive observability enhancement -- Backward compatibility: preserved for `/api/v1/gods/stats/sum` diff --git a/examples/requirements-examples/problem1/requirements/openspec/changes/archive/2026-04-06-add-spring-actuator/specs/god-analysis-api/spec.md b/examples/requirements-examples/problem1/requirements/openspec/changes/archive/2026-04-06-add-spring-actuator/specs/god-analysis-api/spec.md deleted file mode 100644 index da619f0b..00000000 --- a/examples/requirements-examples/problem1/requirements/openspec/changes/archive/2026-04-06-add-spring-actuator/specs/god-analysis-api/spec.md +++ /dev/null @@ -1,29 +0,0 @@ -# Specification delta: `god-analysis-api` (add-spring-actuator) - -## ADDED Requirements - -### Requirement: Actuator health endpoint - -The service MUST expose a Spring Boot Actuator health endpoint at `/actuator/health` so operators can verify runtime status without invoking business endpoints. - -#### Scenario: Health endpoint is reachable - -- **WHEN** a client calls `GET /actuator/health` -- **THEN** the service responds with HTTP 200 and a health payload that includes status information - -### Requirement: Management endpoint configuration - -The service MUST define explicit management endpoint exposure configuration that includes at least the `health` endpoint. - -#### Scenario: Health is explicitly exposed - -- **WHEN** management endpoint exposure settings are applied -- **THEN** `health` is included in the exposed endpoints list - -## MODIFIED Requirements - -_None._ - -## REMOVED Requirements - -_None._ diff --git a/examples/requirements-examples/problem1/requirements/openspec/changes/archive/2026-04-06-add-spring-actuator/tasks.md b/examples/requirements-examples/problem1/requirements/openspec/changes/archive/2026-04-06-add-spring-actuator/tasks.md deleted file mode 100644 index 3a393ed4..00000000 --- a/examples/requirements-examples/problem1/requirements/openspec/changes/archive/2026-04-06-add-spring-actuator/tasks.md +++ /dev/null @@ -1,8 +0,0 @@ -# Tasks: Add Spring Boot Actuator - -## OpenSpec task list - -- [x] Add `spring-boot-starter-actuator` dependency to the implementation module -- [x] Configure management endpoint exposure for `health` -- [x] Verify `GET /actuator/health` responds successfully in local run/tests -- [x] Verify existing `god-analysis-api` behavior remains unchanged diff --git a/examples/requirements-examples/problem1/requirements/openspec/changes/archive/2026-04-06-remove-spring-actuator/.openspec.yaml b/examples/requirements-examples/problem1/requirements/openspec/changes/archive/2026-04-06-remove-spring-actuator/.openspec.yaml deleted file mode 100644 index 9b8557a6..00000000 --- a/examples/requirements-examples/problem1/requirements/openspec/changes/archive/2026-04-06-remove-spring-actuator/.openspec.yaml +++ /dev/null @@ -1,2 +0,0 @@ -schema: spec-driven -created: 2026-04-06 diff --git a/examples/requirements-examples/problem1/requirements/openspec/changes/archive/2026-04-06-remove-spring-actuator/design.md b/examples/requirements-examples/problem1/requirements/openspec/changes/archive/2026-04-06-remove-spring-actuator/design.md deleted file mode 100644 index eeab39f3..00000000 --- a/examples/requirements-examples/problem1/requirements/openspec/changes/archive/2026-04-06-remove-spring-actuator/design.md +++ /dev/null @@ -1,27 +0,0 @@ -# Design: Remove Spring Boot Actuator - -## Overview - -Remove Spring Boot Actuator from the existing Spring Boot service to eliminate unused operational endpoints and simplify the application dependencies. - -## Decisions - -- Remove Spring Boot Actuator dependency (`spring-boot-starter-actuator`) from Maven pom.xml. -- Remove actuator-related configuration from application properties. -- Clean up any management endpoint configuration that was added for actuator. -- Do not modify domain logic or the aggregation algorithm. - -## Endpoint changes - -- Remove `GET /actuator/health` and any other actuator endpoints. -- Ensure no references to actuator endpoints remain in configuration or tests. -- Verify that application startup and functionality remain unchanged after removal. - -## Risks and mitigations - -- Risk: breaking tests that depend on actuator endpoints. - - Mitigation: review and update any tests that reference `/actuator/*` endpoints. -- Risk: accidental behavior changes in existing API. - - Mitigation: run existing verification tests to ensure `/api/v1/gods/stats/sum` continues to work. -- Risk: removing configuration that affects other parts of the application. - - Mitigation: only remove actuator-specific configuration, leave other Spring Boot settings intact. \ No newline at end of file diff --git a/examples/requirements-examples/problem1/requirements/openspec/changes/archive/2026-04-06-remove-spring-actuator/proposal.md b/examples/requirements-examples/problem1/requirements/openspec/changes/archive/2026-04-06-remove-spring-actuator/proposal.md deleted file mode 100644 index 822a5d12..00000000 --- a/examples/requirements-examples/problem1/requirements/openspec/changes/archive/2026-04-06-remove-spring-actuator/proposal.md +++ /dev/null @@ -1,18 +0,0 @@ -# Change: Remove Spring Boot Actuator - -## Why - -The `god-analysis-api` service no longer requires operational visibility through Spring Boot Actuator endpoints. Removing this dependency simplifies the application, reduces the attack surface, and eliminates unused monitoring capabilities that are not needed for the current deployment model. - -## What changes - -- Remove Spring Boot Actuator dependency from the implementation module. -- Remove actuator endpoint configuration from application properties. -- Clean up any actuator-related management configuration. -- Keep existing functional API behavior unchanged. - -## Scope - -- Capability affected: `god-analysis-api` -- Type: dependency removal and simplification -- Backward compatibility: preserved for `/api/v1/gods/stats/sum` \ No newline at end of file diff --git a/examples/requirements-examples/problem1/requirements/openspec/changes/archive/2026-04-06-remove-spring-actuator/specs/god-analysis-api/spec.md b/examples/requirements-examples/problem1/requirements/openspec/changes/archive/2026-04-06-remove-spring-actuator/specs/god-analysis-api/spec.md deleted file mode 100644 index 35d567c9..00000000 --- a/examples/requirements-examples/problem1/requirements/openspec/changes/archive/2026-04-06-remove-spring-actuator/specs/god-analysis-api/spec.md +++ /dev/null @@ -1,29 +0,0 @@ -# Specification delta: `god-analysis-api` (remove-spring-actuator) - -## ADDED Requirements - -_None._ - -## MODIFIED Requirements - -_None._ - -## REMOVED Requirements - -### Requirement: Actuator health endpoint - -The service no longer exposes a Spring Boot Actuator health endpoint at `/actuator/health`. Operators should use alternative monitoring approaches or the main API endpoints for service verification. - -#### Scenario: Health endpoint is no longer available - -- **WHEN** a client calls `GET /actuator/health` -- **THEN** the service responds with HTTP 404 (Not Found) as the endpoint does not exist - -### Requirement: Management endpoint configuration - -The service no longer defines management endpoint exposure configuration for actuator endpoints, as the actuator dependency has been removed. - -#### Scenario: Management endpoints are not configured - -- **WHEN** the application starts -- **THEN** no actuator management endpoints are available or configured \ No newline at end of file diff --git a/examples/requirements-examples/problem1/requirements/openspec/changes/archive/2026-04-06-remove-spring-actuator/tasks.md b/examples/requirements-examples/problem1/requirements/openspec/changes/archive/2026-04-06-remove-spring-actuator/tasks.md deleted file mode 100644 index 9d1447fa..00000000 --- a/examples/requirements-examples/problem1/requirements/openspec/changes/archive/2026-04-06-remove-spring-actuator/tasks.md +++ /dev/null @@ -1,9 +0,0 @@ -# Tasks: Remove Spring Boot Actuator - -## OpenSpec task list - -- [x] Remove `spring-boot-starter-actuator` dependency from the implementation module -- [x] Remove actuator-related configuration from application properties -- [x] Verify `GET /actuator/health` returns HTTP 404 (Not Found) after removal -- [x] Verify existing `god-analysis-api` behavior remains unchanged -- [x] Update any tests that reference actuator endpoints \ No newline at end of file diff --git a/examples/requirements-examples/problem1/requirements/openspec/changes/archive/2026-04-06-us-001-god-analysis-api/design.md b/examples/requirements-examples/problem1/requirements/openspec/changes/archive/2026-04-06-us-001-god-analysis-api/design.md deleted file mode 100644 index bc06b406..00000000 --- a/examples/requirements-examples/problem1/requirements/openspec/changes/archive/2026-04-06-us-001-god-analysis-api/design.md +++ /dev/null @@ -1,56 +0,0 @@ -# Design: US-001 God Analysis API - -This design consolidates [ADR-001](../../../adr/ADR-001-God-Analysis-API-Functional-Requirements.md), [ADR-002](../../../adr/ADR-002-God-Analysis-API-Non-Functional-Requirements.md), [ADR-003](../../../adr/ADR-003-God-Analysis-API-Technology-Stack.md), and the [implementation plan](../../../agile/US-001-plan-analysis.plan.md). - -## Architecture - -- **Style:** Monolithic Spring MVC **servlet** application (no WebFlux). -- **Base path:** `/api/v1` (class-level `@RequestMapping`). -- **Endpoint:** `GET /gods/stats/sum` → full path `/api/v1/gods/stats/sum`. -- **Package root:** `info.jab.ms` with subpackages `controller`, `service`, `dto`, `client`, `config`, `algorithm`. - -## Processing pipeline - -1. Parse and validate `filter` (exactly one Unicode code point) and `sources` (non-empty, known pantheon keys only) → **400** on violation (see Gherkin `@error-handling` and OpenAPI). -2. For each selected source, run **one** GET to the configured base URL + route, using **RestClient** with **connect + read** timeouts. -3. Execute source fetches **in parallel** (e.g. `CompletableFuture` on a **virtual thread** executor per ADR-003). -4. On timeout or transport/application error for a source, treat that source’s name list as **empty** for aggregation (partial result). **Do not retry** outbound calls. -5. Merge name lists from successful responses; deserialize JSON arrays of strings. -6. **Filter** names where the **first Unicode code point** equals the `filter` character (**case-sensitive**). -7. For each included name, compute per-name decimal string by concatenating each code point’s decimal digits (`String.codePoints()`); parse to `BigInteger` and sum. -8. Respond **200** with `{ "sum": "" }`. If no names match, `sum` is `"0"`. - -## HTTP client and configuration - -- **RestClient** only for outbound calls; timeouts from `application.yml` (e.g. 5000 ms connect/read defaults), overridable via environment variables. -- **@ConfigurationProperties** for base URLs (`greek`, `roman`, `nordic`) matching ADR-001 defaults. -- **No** Resilience4j retry, **no** Spring Retry for US-001. - -## REST layer - -- `@RestController`, Jackson for JSON. -- Response DTO exposes `sum` as **string** (large integers). -- Optional: `@ControllerAdvice` for 400 responses aligned with OpenAPI. -- Optional: `springdoc-openapi`; static contract remains [US-001-god-analysis-api.openapi.yaml](../../../agile/US-001-god-analysis-api.openapi.yaml). - -## Testing (summary) - -| Layer | Approach | -|-------|----------| -| Acceptance / integration | `@SpringBootTest(webEnvironment = RANDOM_PORT)` + Spring **`RestClient`** to `localhost` (not Rest Assured). | -| Upstream simulation | **WireMock** with JSON fixtures; **reset stubs** between tests; **fixedDelayMilliseconds** beyond read timeout for timeout scenarios. | -| Unit | Pure algorithm/filter tests without WireMock. | - -**Tags:** Mirror Gherkin tags with JUnit `@Tag` (e.g. `acceptance-test`, `integration-test`) if using Surefire groups. - -**Pinned data:** Use WireMock fixtures for deterministic sums (e.g. happy path `78179288397447443426`; partial scenario per [feature file](../../../agile/US-001_god_analysis_api.feature)). - -## Runtime versions - -- **Spring Boot:** 4.0.x (per ADR-003 / plan). -- **Java:** Align with repo standard (**25** recommended in plan) unless explicitly standardizing on 26. - -## References - -- [US-001-plan-analysis.plan.md](../../../agile/US-001-plan-analysis.plan.md) — task order, file checklist, execution rules. -- OpenAPI: [US-001-god-analysis-api.openapi.yaml](../../../agile/US-001-god-analysis-api.openapi.yaml). diff --git a/examples/requirements-examples/problem1/requirements/openspec/changes/archive/2026-04-06-us-001-god-analysis-api/proposal.md b/examples/requirements-examples/problem1/requirements/openspec/changes/archive/2026-04-06-us-001-god-analysis-api/proposal.md deleted file mode 100644 index 1cb9205a..00000000 --- a/examples/requirements-examples/problem1/requirements/openspec/changes/archive/2026-04-06-us-001-god-analysis-api/proposal.md +++ /dev/null @@ -1,51 +0,0 @@ -# Change: US-001 God Analysis API - -## Metadata - -| Field | Value | -|-------|--------| -| **ID** | `us-001-god-analysis-api` | -| **User story** | [US-001_God_Analysis_API.md](../../../agile/US-001_God_Analysis_API.md) | -| **Plan** | [US-001-plan-analysis.plan.md](../../../agile/US-001-plan-analysis.plan.md) | -| **Status** | Proposed | - -## Why - -API consumers need aggregated statistical data about god names from multiple mythological sources (Greek, Roman, Nordic) with resilient HTTP fetching, Unicode-based aggregation, and a stable JSON contract. - -## What changes - -Introduce a **Spring Boot** REST service that: - -- Exposes `GET /api/v1/gods/stats/sum` with query parameters `filter` (single character) and `sources` (comma-separated pantheon keys). -- Fetches configured upstream JSON god lists **in parallel** with **RestClient** connect/read timeouts from `application.yml` (overridable via environment variables). -- Applies **case-sensitive** first-code-point filtering and the **Unicode decimal concatenation → BigInteger sum** algorithm. -- Returns **partial results** when one or more sources time out or error on the **single attempt** per source (**no** automatic retries; **no** circuit breaker in v1 per ADR-002/ADR-003). - -## Primary contracts - -| Contract | Path | -|----------|------| -| Gherkin | [US-001_god_analysis_api.feature](../../../agile/US-001_god_analysis_api.feature) | -| OpenAPI 3 | [US-001-god-analysis-api.openapi.yaml](../../../agile/US-001-god-analysis-api.openapi.yaml) | - -## Architecture decision records (baseline) - -| ADR | Topic | -|-----|--------| -| [ADR-001](../../../adr/ADR-001-God-Analysis-API-Functional-Requirements.md) | Functional requirements, endpoint, resilience at API level | -| [ADR-002](../../../adr/ADR-002-God-Analysis-API-Non-Functional-Requirements.md) | Reliability: timeouts, parallel execution, partial results; retries/circuit breaker scope | -| [ADR-003](../../../adr/ADR-003-God-Analysis-API-Technology-Stack.md) | Spring Boot MVC, RestClient, WireMock, Spring RestClient for AT, package `info.jab.ms` | - -## Out of scope (v1) - -- Outbound automatic retries (Resilience4j Retry, Spring Retry, ad-hoc retry loops). -- Circuit breaker pattern. -- Rest Assured for HTTP-level acceptance tests (use Spring `RestClient` per ADR-003). -- Reactive stack (WebFlux / WebClient). - -## Success criteria - -- `./mvnw clean verify` passes for the new `god-analysis-api` module. -- Behavior matches the three **acceptance** scenarios in the feature file (happy path sum, partial result with timeouts, validation/error scenarios as tagged). -- OpenAPI-aligned JSON: `200` + `{ "sum": "" }` for successful aggregation; `400` for malformed requests per OpenAPI and Gherkin `@error-handling` scenarios. diff --git a/examples/requirements-examples/problem1/requirements/openspec/changes/archive/2026-04-06-us-001-god-analysis-api/specs/god-analysis-api/spec.md b/examples/requirements-examples/problem1/requirements/openspec/changes/archive/2026-04-06-us-001-god-analysis-api/specs/god-analysis-api/spec.md deleted file mode 100644 index d945bd89..00000000 --- a/examples/requirements-examples/problem1/requirements/openspec/changes/archive/2026-04-06-us-001-god-analysis-api/specs/god-analysis-api/spec.md +++ /dev/null @@ -1,103 +0,0 @@ -# Specification delta: `god-analysis-api` (US-001) - -**Related:** [ADR-001](../../../../../adr/ADR-001-God-Analysis-API-Functional-Requirements.md), [ADR-002](../../../../../adr/ADR-002-God-Analysis-API-Non-Functional-Requirements.md), [ADR-003](../../../../../adr/ADR-003-God-Analysis-API-Technology-Stack.md), [US-001-god-analysis-api.openapi.yaml](../../../../../agile/US-001-god-analysis-api.openapi.yaml), [US-001_god_analysis_api.feature](../../../../../agile/US-001_god_analysis_api.feature) - ---- - -## ADDED Requirements - -### Requirement: HTTP GET gods stats sum - -The system MUST expose `GET /api/v1/gods/stats/sum` with required query parameters `filter` (exactly one Unicode code point) and `sources` (comma-separated pantheon keys among `greek`, `roman`, `nordic`). The response MUST be `200` with `{ "sum": "" }` where `sum` matches `^[0-9]+$`. - -#### Scenario: Happy path matches pinned acceptance sum - -- **WHEN** `filter=n`, `sources=greek,roman,nordic`, and all upstream WireMock fixtures return successfully -- **THEN** the response is 200 and `sum` is `78179288397447443426` - -#### Scenario: Invalid sources rejected - -- **WHEN** `sources` contains unknown names or is empty -- **THEN** the response status is 400 - -### Requirement: Unicode decimal aggregation - -The system MUST compute each included name’s value by iterating Unicode code points, concatenating decimal digit strings per code point, parsing to `BigInteger`, and summing; the JSON `sum` MUST be `BigInteger.toString()` of the total. - -#### Scenario: Zero sum when filter matches no names - -- **WHEN** no name’s first code point equals `filter` (e.g. `filter=N` for lowercase-only data) -- **THEN** `sum` is `0` - -### Requirement: External source integration - -The system MUST consume JSON arrays of strings from configurable Greek, Roman, and Nordic HTTP endpoints (default URLs per ADR-001) using Spring `RestClient`. Caching of upstream responses MUST NOT be used in v1. - -#### Scenario: Deserialize array of god names - -- **WHEN** a source returns HTTP 200 with a JSON array of strings -- **THEN** those strings participate in filtering and aggregation - -### Requirement: Parallel fetch with single attempt - -For each request, the system MUST issue exactly one HTTP GET per selected source, MUST use `RestClient` connect and read timeouts from `application.yml` (overridable via environment variables), and MUST run fetches in parallel without Resilience4j or Spring Retry. - -#### Scenario: No second attempt after timeout - -- **WHEN** a source does not complete within the read timeout -- **THEN** that source contributes no names and no retry is performed for that source in the same request - -### Requirement: Partial results and graceful degradation - -If a source times out or errors, the system MUST treat that source as empty for aggregation, MUST NOT fail the whole request solely for that reason, and MUST return 200 with `{ "sum": "…" }` consistent with remaining data. - -#### Scenario: Nordic and Roman delayed beyond timeout - -- **WHEN** Nordic and Roman are configured to respond after the read timeout and Greek succeeds -- **THEN** the response is 200 and `sum` is `78101109179220212216` per the feature file - -### Requirement: Validation and error responses - -The system MUST return 400 when `filter` is missing, empty, or longer than one character, or when `sources` is missing, empty, or invalid, matching the `@error-handling` scenarios in the feature file. - -#### Scenario: Missing filter - -- **WHEN** only `sources` is provided -- **THEN** the response status is 400 - -### Requirement: Acceptance scenarios coverage - -Automated tests MUST implement assertions equivalent to the Gherkin scenarios for happy path, partial timeout, and validation errors, using Spring `RestClient` against `@SpringBootTest(RANDOM_PORT)` and WireMock with per-test stub reset as in ADR-003. - -#### Scenario: Integration test isolation - -- **WHEN** integration tests run in any order -- **THEN** WireMock stubs are reset between tests so timeout behavior is deterministic - -### Requirement: Technology constraints - -The implementation MUST use Spring Boot MVC (servlet) without WebFlux, MUST use `RestClient` for outbound calls, MUST use base package `info.jab.ms`, and MUST use Spring `RestClient` (not Rest Assured) for HTTP-level acceptance tests. - -#### Scenario: Servlet stack only - -- **WHEN** the application starts -- **THEN** no `spring-webflux` or `WebClient` beans are required for US-001 - -### Requirement: Observability - -The system MUST expose Spring Boot Actuator endpoints and MUST log structured messages suitable for diagnosing per-source success, timeout, or failure per request. - -#### Scenario: Health endpoint available - -- **WHEN** a client requests an actuator health endpoint -- **THEN** the application reports health status for operations use - ---- - -## MODIFIED Requirements - -_None — greenfield capability._ - -## REMOVED Requirements - -_None._ diff --git a/examples/requirements-examples/problem1/requirements/openspec/changes/archive/2026-04-06-us-001-god-analysis-api/tasks.md b/examples/requirements-examples/problem1/requirements/openspec/changes/archive/2026-04-06-us-001-god-analysis-api/tasks.md deleted file mode 100644 index 88011cce..00000000 --- a/examples/requirements-examples/problem1/requirements/openspec/changes/archive/2026-04-06-us-001-god-analysis-api/tasks.md +++ /dev/null @@ -1,95 +0,0 @@ -# Tasks: US-001 God Analysis API - -Derived from [US-001-plan-analysis.plan.md](../../../agile/US-001-plan-analysis.plan.md). Execute in order; after each **GREEN** implementation step, complete the associated **Verify** before proceeding. - -## Execution rules (from plan) - -1. Complete one task at a time; update **Status** in the source plan when tracking there. -2. **GREEN** tasks require a **Verify** milestone before the next implementation slice. -3. **Milestone** rows: complete refactor pair (logging → optimize) before each milestone Verify. - -## Task list - - -| # | Task | Phase | TDD | Milestone | Group | Status | -| --- | ------------------------------------------------------------------------------ | -------- | ---- | --------- | ----- | ------ | -| 1 | Create Maven project structure with dependencies | Setup | | | A1 | ✔ | -| 2 | Write acceptance test for happy path sum calculation | RED | Test | | A1 | ✔ | -| 3 | Create REST controller stub to pass acceptance test | GREEN | Impl | | A1 | ✔ | -| 4 | Add structured logging for request/response | Refactor | | | A1 | ✔ | -| 5 | Optimize controller validation and error handling | Refactor | | | A1 | ✔ | -| 6 | Verify acceptance test passes with controller | Verify | | milestone | A1 | ✔ | -| 7 | Write service layer unit test for aggregation logic | RED | Test | | A2 | ✔ | -| 8 | Implement service with Unicode algorithm | GREEN | Impl | | A2 | ✔ | -| 9 | Add service-level logging | Refactor | | | A2 | ✔ | -| 10 | Optimize service configuration and error handling | Refactor | | | A2 | ✔ | -| 11 | Write integration test for filter=N zero result | RED | Test | | A2 | ✔ | -| 12 | Implement filter logic validation | GREEN | Impl | | A2 | ✔ | -| 13 | Verify service unit tests pass | Verify | | milestone | A2 | ✔ | -| 14 | Write HTTP client tests for timeout-bound fetching (single attempt per source) | RED | Test | | A3 | ✔ | -| 15 | Implement HTTP client with configured RestClient timeouts (no retries) | GREEN | Impl | | A3 | ✔ | -| 16 | Add client-level logging for HTTP outcomes | Refactor | | | A3 | ✔ | -| 17 | Optimize client configuration (timeouts, per-source isolation) | Refactor | | | A3 | ✔ | -| 18 | Write integration test for Nordic/Roman timeout scenario (per feature file) | RED | Test | | A3 | ✔ | -| 19 | Implement partial result handling in service | GREEN | Impl | | A3 | ✔ | -| 20 | Verify HTTP client tests pass (timeout phase) | Verify | | milestone | A3 | ✔ | -| 21 | Verify all integration tests pass | Verify | | milestone | A4 | ✔ | - - -## OpenSpec checklist (CLI progress) - -Mirror of the **Task list** table above. [OpenSpec](https://github.com/Fission-AI/OpenSpec) counts only lines like `- [ ]` / `- [x]` for `openspec list` and `openspec view`. Keep this section in sync with the **Status** column. - -- [x] Create Maven project structure with dependencies -- [x] Write acceptance test for happy path sum calculation -- [x] Create REST controller stub to pass acceptance test -- [x] Add structured logging for request/response -- [x] Optimize controller validation and error handling -- [x] Verify acceptance test passes with controller -- [x] Write service layer unit test for aggregation logic -- [x] Implement service with Unicode algorithm -- [x] Add service-level logging -- [x] Optimize service configuration and error handling -- [x] Write integration test for filter=N zero result -- [x] Implement filter logic validation -- [x] Verify service unit tests pass -- [x] Write HTTP client tests for timeout-bound fetching (single attempt per source) -- [x] Implement HTTP client with configured RestClient timeouts (no retries) -- [x] Add client-level logging for HTTP outcomes -- [x] Optimize client configuration (timeouts, per-source isolation) -- [x] Write integration test for Nordic/Roman timeout scenario (per feature file) -- [x] Implement partial result handling in service -- [x] Verify HTTP client tests pass (timeout phase) -- [x] Verify all integration tests pass - - -## File checklist (from plan) - - -| Order | File | -| ----- | ------------------------------------------------------------------------------------------- | -| 1 | `god-analysis-api/pom.xml` | -| 2 | `god-analysis-api/src/main/resources/application.yml` | -| 3 | `god-analysis-api/src/test/java/info/jab/ms/controller/GodAnalysisApiAT.java` | -| 4 | `god-analysis-api/src/main/java/info/jab/ms/controller/GodStatsController.java` | -| 5 | `god-analysis-api/src/main/java/info/jab/ms/dto/GodStatsResponse.java` | -| 6 | `god-analysis-api/src/test/java/info/jab/ms/service/GodAnalysisServiceTest.java` | -| 7 | `god-analysis-api/src/main/java/info/jab/ms/service/GodAnalysisService.java` | -| 8 | `god-analysis-api/src/main/java/info/jab/ms/algorithm/UnicodeAggregator.java` | -| 9 | `god-analysis-api/src/test/java/info/jab/ms/client/GodDataClientTest.java` | -| 10 | `god-analysis-api/src/main/java/info/jab/ms/client/GodDataClient.java` | -| 11 | `god-analysis-api/src/main/java/info/jab/ms/config/HttpClientConfig.java` | -| 12 | `god-analysis-api/src/test/java/info/jab/ms/controller/GodAnalysisApiIT.java` | -| 13 | `god-analysis-api/src/test/resources/wiremock/greek-gods.json` | -| 14 | `god-analysis-api/src/test/resources/wiremock/roman-gods.json` | -| 15 | `god-analysis-api/src/test/resources/wiremock/nordic-gods.json` | -| 16 | `god-analysis-api/src/test/java/info/jab/ms/support/WireMockExtension.java` (or equivalent) | -| 17 | `god-analysis-api/README.md` | - - -## Deliverables - -- Module builds with `./mvnw clean verify` -- README documents run, env vars, WireMock/Testcontainers notes -- Acceptance scenarios aligned with [US-001_god_analysis_api.feature](../../../agile/US-001_god_analysis_api.feature) - diff --git a/examples/requirements-examples/problem1/requirements/openspec/changes/us-001-a1-api-boundary/proposal.md b/examples/requirements-examples/problem1/requirements/openspec/changes/us-001-a1-api-boundary/proposal.md new file mode 100644 index 00000000..c2cac054 --- /dev/null +++ b/examples/requirements-examples/problem1/requirements/openspec/changes/us-001-a1-api-boundary/proposal.md @@ -0,0 +1,21 @@ +# US-001 A1 API Boundary Slice + +## Problem Statement + +The implementation plan defines `A1` as the first delivery slice (setup, acceptance RED/GREEN, API-layer refactor, and verify), but this slice is not yet represented as an explicit OpenSpec change artifact. + +Without an isolated change for `A1`, API-boundary contract work can be mixed with service and outbound concerns, reducing traceability and making incremental verification harder. + +## Proposed Solution + +Create a dedicated OpenSpec change for `A1` that captures: +- OpenAPI generation and boundary scaffolding. +- Acceptance-test-first API behavior for happy path and validation errors. +- Controller/error-handling implementation aligned with the contract. +- API-layer logging/configuration consistency and milestone verification. + +## Success Criteria + +- `A1` tasks are represented as a single OpenSpec checklist. +- Spec deltas describe the API boundary obligations and validation behavior. +- Verification gate for `A1` (`./mvnw -q test`) is explicitly tracked. diff --git a/examples/requirements-examples/problem1/requirements/openspec/changes/us-001-a1-api-boundary/specs/god-analysis-api/spec.md b/examples/requirements-examples/problem1/requirements/openspec/changes/us-001-a1-api-boundary/specs/god-analysis-api/spec.md new file mode 100644 index 00000000..ebc9527a --- /dev/null +++ b/examples/requirements-examples/problem1/requirements/openspec/changes/us-001-a1-api-boundary/specs/god-analysis-api/spec.md @@ -0,0 +1,27 @@ +# god-analysis-api + +## ADDED Requirements + +### Requirement: A1 API boundary setup +The `A1` delivery slice SHALL establish API boundary scaffolding from the OpenAPI contract, including generated interfaces/DTOs needed by the REST boundary implementation. + +#### Scenario: OpenAPI artifacts are available for API boundary implementation +- **Given** the US-001 OpenAPI contract is the source of truth +- **When** the build generates API boundary artifacts +- **Then** generated interfaces/DTOs are available to implement `GET /api/v1/gods/stats/sum` + +### Requirement: A1 acceptance-first API behavior +The `A1` delivery slice SHALL implement acceptance tests first for happy path and validation errors, and SHALL make the controller pass those tests without introducing service/outbound implementation details from later slices. + +#### Scenario: Acceptance tests drive API boundary behavior +- **Given** failing acceptance tests for valid and invalid query parameters +- **When** the controller and API error mapping are implemented +- **Then** the tests pass with responses aligned to the OpenAPI contract + +### Requirement: A1 verification gate +The `A1` delivery slice SHALL conclude with a milestone verification gate using `./mvnw -q test`. + +#### Scenario: Milestone A1 verification succeeds +- **Given** all A1 tasks are complete +- **When** milestone verification is executed +- **Then** acceptance tests are green and the API boundary slice is considered complete diff --git a/examples/requirements-examples/problem1/requirements/openspec/changes/us-001-a1-api-boundary/tasks.md b/examples/requirements-examples/problem1/requirements/openspec/changes/us-001-a1-api-boundary/tasks.md new file mode 100644 index 00000000..6bf42bdc --- /dev/null +++ b/examples/requirements-examples/problem1/requirements/openspec/changes/us-001-a1-api-boundary/tasks.md @@ -0,0 +1,8 @@ +# Tasks: US-001 A1 API Boundary Slice + +- [x] Configure `openapi-generator-maven-plugin` and generate API interfaces/DTOs. +- [x] Write failing acceptance tests for happy path and validation errors using Spring `RestClient`. +- [x] Implement controller mapping, enum/source parsing, and error envelope to satisfy acceptance tests. +- [x] Add API-layer observability/logging for request validation and failure mapping. +- [x] Optimize API configuration and exception handling consistency with OpenAPI responses. +- [x] Verify Milestone A1 with `./mvnw -q test` and confirm acceptance tests are green. diff --git a/examples/requirements-examples/problem1/requirements/openspec/changes/us-001-a2-domain-service/proposal.md b/examples/requirements-examples/problem1/requirements/openspec/changes/us-001-a2-domain-service/proposal.md new file mode 100644 index 00000000..c698fdef --- /dev/null +++ b/examples/requirements-examples/problem1/requirements/openspec/changes/us-001-a2-domain-service/proposal.md @@ -0,0 +1,21 @@ +# US-001 A2 Domain Service Slice + +## Problem Statement + +The implementation plan defines `A2` for service-level RED/GREEN/refactor/verify work, but no independent OpenSpec change currently captures that scope. + +Without a dedicated `A2` change, service behavior (Unicode aggregation, parallel execution boundaries, and deterministic merge behavior) can become underspecified relative to the execution plan. + +## Proposed Solution + +Create a dedicated OpenSpec change for `A2` that captures: +- Unit-test-first service and algorithm behavior. +- Aggregation implementation with immutable flow, `CompletableFuture`, and `BigInteger`. +- Service-layer observability and deterministic merge/concurrency refactoring. +- Milestone `A2` verification with prior API tests still green. + +## Success Criteria + +- `A2` tasks are represented as a single OpenSpec checklist. +- Spec deltas define service-level correctness and concurrency boundaries. +- Verification gate for `A2` (`./mvnw -q test`) is explicitly tracked. diff --git a/examples/requirements-examples/problem1/requirements/openspec/changes/us-001-a2-domain-service/specs/god-analysis-api/spec.md b/examples/requirements-examples/problem1/requirements/openspec/changes/us-001-a2-domain-service/specs/god-analysis-api/spec.md new file mode 100644 index 00000000..eaaed0e0 --- /dev/null +++ b/examples/requirements-examples/problem1/requirements/openspec/changes/us-001-a2-domain-service/specs/god-analysis-api/spec.md @@ -0,0 +1,27 @@ +# god-analysis-api + +## ADDED Requirements + +### Requirement: A2 service and algorithm test-first delivery +The `A2` delivery slice SHALL introduce failing unit tests first for Unicode conversion, source parsing, and case-sensitive first-character filtering before service implementation is finalized. + +#### Scenario: Unit tests define service correctness boundaries +- **Given** failing tests for conversion, parsing, and filter behavior +- **When** domain service logic is implemented +- **Then** tests pass with behavior matching the plan and existing contract rules + +### Requirement: A2 deterministic parallel aggregation +The `A2` delivery slice SHALL implement aggregation using immutable flow, `CompletableFuture` parallel execution, and `BigInteger` summation with deterministic merge behavior. + +#### Scenario: Parallel execution yields deterministic sum +- **Given** selected sources return deterministic datasets +- **When** aggregation executes in parallel +- **Then** the resulting `sum` is stable across repeated executions + +### Requirement: A2 verification gate +The `A2` delivery slice SHALL conclude with milestone verification using `./mvnw -q test`, preserving previously green API-boundary tests from `A1`. + +#### Scenario: Milestone A2 verification succeeds +- **Given** all A2 tasks are complete +- **When** milestone verification is executed +- **Then** unit tests and prior API tests are green and the service slice is considered complete diff --git a/examples/requirements-examples/problem1/requirements/openspec/changes/us-001-a2-domain-service/tasks.md b/examples/requirements-examples/problem1/requirements/openspec/changes/us-001-a2-domain-service/tasks.md new file mode 100644 index 00000000..b209c1ae --- /dev/null +++ b/examples/requirements-examples/problem1/requirements/openspec/changes/us-001-a2-domain-service/tasks.md @@ -0,0 +1,7 @@ +# Tasks: US-001 A2 Domain Service Slice + +- [x] Write failing unit tests for Unicode conversion, source parsing, and filter case-sensitivity. +- [x] Implement aggregation service with immutable flow, `CompletableFuture` parallelization, and `BigInteger` sum. +- [x] Add service-level observability for source execution and fallback decisions. +- [x] Optimize service concurrency boundaries and deterministic merge behavior. +- [x] Verify Milestone A2 with `./mvnw -q test` and confirm unit + prior API tests are green. diff --git a/examples/requirements-examples/problem1/requirements/openspec/changes/us-001-a3-outbound-integration/proposal.md b/examples/requirements-examples/problem1/requirements/openspec/changes/us-001-a3-outbound-integration/proposal.md new file mode 100644 index 00000000..9dab02d8 --- /dev/null +++ b/examples/requirements-examples/problem1/requirements/openspec/changes/us-001-a3-outbound-integration/proposal.md @@ -0,0 +1,21 @@ +# US-001 A3 Outbound Integration Slice + +## Problem Statement + +The implementation plan defines `A3` as the outbound integration slice (WireMock RED/GREEN, timeout tolerance, hardening, and final verify), but this scope is not currently captured as a standalone OpenSpec change. + +Without an explicit `A3` change, partial-timeout behavior and timeout-tolerant source handling may be implemented without clear traceability to the planned verification gate. + +## Proposed Solution + +Create a dedicated OpenSpec change for `A3` that captures: +- WireMock integration-test-first scenarios for full success and partial timeout. +- Outbound configuration (`god.outbound.*`) and per-source timeout-tolerant calls. +- Outbound observability, stub isolation/reset, and error-translation hardening. +- Final milestone verification (`./mvnw clean test`) including servlet-only constraint checks. + +## Success Criteria + +- `A3` tasks are represented as a single OpenSpec checklist. +- Spec deltas define timeout-tolerant integration behavior and test isolation requirements. +- Verification gate for `A3` (`./mvnw clean test`) is explicitly tracked. diff --git a/examples/requirements-examples/problem1/requirements/openspec/changes/us-001-a3-outbound-integration/specs/god-analysis-api/spec.md b/examples/requirements-examples/problem1/requirements/openspec/changes/us-001-a3-outbound-integration/specs/god-analysis-api/spec.md new file mode 100644 index 00000000..7e576f38 --- /dev/null +++ b/examples/requirements-examples/problem1/requirements/openspec/changes/us-001-a3-outbound-integration/specs/god-analysis-api/spec.md @@ -0,0 +1,27 @@ +# god-analysis-api + +## ADDED Requirements + +### Requirement: A3 outbound integration test-first delivery +The `A3` delivery slice SHALL define failing WireMock integration tests first for full-source success and partial-timeout scenarios before outbound integration is finalized. + +#### Scenario: Integration tests define outbound expectations +- **Given** WireMock stubs for success and delayed responses +- **When** outbound integration is implemented +- **Then** integration tests pass for both full and partial-source outcomes + +### Requirement: A3 timeout-tolerant outbound behavior +The `A3` delivery slice SHALL implement configurable outbound calls (`god.outbound.*`) with per-source timeout tolerance so failed or timed-out sources are omitted while valid sources continue to contribute. + +#### Scenario: Timed-out source is omitted from aggregation +- **Given** one selected source exceeds configured timeout +- **When** the endpoint is called with multiple sources +- **Then** the response remains `200` and `sum` reflects only successful sources + +### Requirement: A3 verification gate +The `A3` delivery slice SHALL conclude with final verification using `./mvnw clean test` and SHALL preserve the servlet-only technology constraint. + +#### Scenario: Milestone A3 verification succeeds +- **Given** all A3 tasks are complete +- **When** final verification is executed +- **Then** the full test suite is green and no reactive dependencies are required diff --git a/examples/requirements-examples/problem1/requirements/openspec/changes/us-001-a3-outbound-integration/tasks.md b/examples/requirements-examples/problem1/requirements/openspec/changes/us-001-a3-outbound-integration/tasks.md new file mode 100644 index 00000000..588cbafb --- /dev/null +++ b/examples/requirements-examples/problem1/requirements/openspec/changes/us-001-a3-outbound-integration/tasks.md @@ -0,0 +1,7 @@ +# Tasks: US-001 A3 Outbound Integration Slice + +- [x] Write failing WireMock integration tests for full-source success and partial-timeout scenarios. +- [x] Implement outbound client configuration (`god.outbound.*`) and timeout-tolerant per-source calls. +- [x] Add outbound-call logging/metrics hooks and interaction verification helpers. +- [x] Optimize timeout values, stub isolation/reset, and error-translation hardening. +- [x] Verify Milestone A3 with `./mvnw clean test` and confirm no reactive dependencies are introduced. diff --git a/skills-generator/src/main/resources/skills/701-skill.xml b/skills-generator/src/main/resources/skills/701-skill.xml index cf7f8ce2..cb489366 100644 --- a/skills-generator/src/main/resources/skills/701-skill.xml +++ b/skills-generator/src/main/resources/skills/701-skill.xml @@ -38,11 +38,11 @@ Help teams produce maintainable **OpenAPI 3.x** contracts that stay aligned with - Review or author an OpenAPI 3.x spec (YAML or JSON) - Improve API contract documentation, examples, or reusable components - Define security schemes and global security requirements in OpenAPI - Add spec linting or CI validation (e.g. Spectral) for OpenAPI - Assess breaking vs compatible changes when evolving a published API contract + Review an OpenAPI + Improve an OpenAPI + Improve API contract + Improve API schema design + Validate an OpenAPI spec diff --git a/skills/701-technologies-openapi/SKILL.md b/skills/701-technologies-openapi/SKILL.md index 81d18d13..d9519fb5 100644 --- a/skills/701-technologies-openapi/SKILL.md +++ b/skills/701-technologies-openapi/SKILL.md @@ -33,11 +33,11 @@ Keep recommendations at the OpenAPI layer unless the user explicitly asks for Ja ## When to use this skill -- Review or author an OpenAPI 3.x spec (YAML or JSON) -- Improve API contract documentation, examples, or reusable components -- Define security schemes and global security requirements in OpenAPI -- Add spec linting or CI validation (e.g. Spectral) for OpenAPI -- Assess breaking vs compatible changes when evolving a published API contract +- Review an OpenAPI +- Improve an OpenAPI +- Improve API contract +- Improve API schema design +- Validate an OpenAPI spec ## Reference