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 bd21bc02..00000000 Binary files a/examples/requirements-examples/problem1/requirements/diagrams/c4-level1-context.png and /dev/null differ 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 09ff0143..00000000 Binary files a/examples/requirements-examples/problem1/requirements/diagrams/c4-level2-container.png and /dev/null differ 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 1b4229af..00000000 Binary files a/examples/requirements-examples/problem1/requirements/diagrams/c4-level3-component.png and /dev/null differ 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