From 9f42c2a3015ce124fef6ef34e56f713d1b51cf11 Mon Sep 17 00:00:00 2001 From: Thomas Cederholm Date: Fri, 8 May 2026 12:10:03 +0200 Subject: [PATCH 1/8] =?UTF-8?q?chore:=20Prepare=20for=20perf=20work=20?= =?UTF-8?q?=E2=80=94=20quiet=20test=20logging=20and=20ignore=20perf/?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lower com.retailsvc test logging from debug to info to avoid skewing load-test profiles, ignore perf/ artifacts, and mark the completed refactor plan as done. Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 3 + ...6-05-08-openapi-refactor-implementation.md | 362 +++++++++--------- src/test/resources/logback-test.xml | 2 +- 3 files changed, 185 insertions(+), 182 deletions(-) diff --git a/.gitignore b/.gitignore index cc3481d..bac2093 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,6 @@ build/ ### Claude Code per-developer settings ### .claude/settings.local.json + +### Performance recordings ### +perf/ diff --git a/docs/superpowers/plans/2026-05-08-openapi-refactor-implementation.md b/docs/superpowers/plans/2026-05-08-openapi-refactor-implementation.md index 450d622..cf00e0d 100644 --- a/docs/superpowers/plans/2026-05-08-openapi-refactor-implementation.md +++ b/docs/superpowers/plans/2026-05-08-openapi-refactor-implementation.md @@ -1,6 +1,6 @@ # OpenAPI Refactor Implementation Plan -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [x]`) syntax for tracking. **Goal:** Restructure the library along the design in `docs/superpowers/specs/2026-05-07-openapi-refactor-design.md` so OpenAPI 3.1 keyword gaps become mechanical to fill, and ship Java 25 build + the typed-record-derived "free" 3.1 keywords (`minLength`/`maxLength`/`minItems`/`maxItems`/`uniqueItems`/`multipleOf`/`exclusiveMin/Max`/`type:["string","null"]`). @@ -34,13 +34,13 @@ All concrete record/interface shapes live in the design doc at `docs/superpowers - Modify: `pom.xml` (line ~197: `21`) - Modify: `Dockerfile` (line 1: base image) -- [ ] **Step 1: Update `.java-version`** +- [x] **Step 1: Update `.java-version`** ``` 25 ``` -- [ ] **Step 2: Update `pom.xml` compiler release** +- [x] **Step 2: Update `pom.xml` compiler release** In `pom.xml` find the `maven-compiler-plugin` block: @@ -50,20 +50,20 @@ In `pom.xml` find the `maven-compiler-plugin` block: ``` -- [ ] **Step 3: Update Dockerfile base image** +- [x] **Step 3: Update Dockerfile base image** ```dockerfile FROM eclipse-temurin:25-jre-alpine ``` -- [ ] **Step 4: Verify build works** +- [x] **Step 4: Verify build works** Run: `mvn -q test` Expected: BUILD SUCCESS, all 122 tests pass. If `mvn` picks an older JDK, ensure JAVA_HOME points at a 25 install or use `mvn -Dmaven.compiler.release=25 ...` once to confirm. -- [ ] **Step 5: Commit** +- [x] **Step 5: Commit** ```bash git add .java-version pom.xml Dockerfile @@ -82,7 +82,7 @@ These tasks build the new `com.retailsvc.http.spec.schema` package alongside the - Create: `src/main/java/com/retailsvc/http/spec/schema/TypeName.java` - Test: `src/test/java/com/retailsvc/http/spec/schema/TypeNameTest.java` -- [ ] **Step 1: Write the failing test** +- [x] **Step 1: Write the failing test** ```java package com.retailsvc.http.spec.schema; @@ -111,12 +111,12 @@ class TypeNameTest { } ``` -- [ ] **Step 2: Run test to verify it fails** +- [x] **Step 2: Run test to verify it fails** Run: `mvn -q test -Dtest=TypeNameTest` Expected: compilation failure, `TypeName` does not exist. -- [ ] **Step 3: Implement** +- [x] **Step 3: Implement** ```java package com.retailsvc.http.spec.schema; @@ -139,12 +139,12 @@ public enum TypeName { } ``` -- [ ] **Step 4: Verify** +- [x] **Step 4: Verify** Run: `mvn -q test -Dtest=TypeNameTest` Expected: PASS (2 tests). -- [ ] **Step 5: Commit** +- [x] **Step 5: Commit** ```bash git add src/main/java/com/retailsvc/http/spec/schema/TypeName.java \ @@ -160,7 +160,7 @@ git commit -m "feat(schema): add TypeName enum" - Create: `src/main/java/com/retailsvc/http/spec/schema/AdditionalProperties.java` - Test: `src/test/java/com/retailsvc/http/spec/schema/AdditionalPropertiesTest.java` -- [ ] **Step 1: Write the failing test** +- [x] **Step 1: Write the failing test** ```java package com.retailsvc.http.spec.schema; @@ -192,12 +192,12 @@ class AdditionalPropertiesTest { } ``` -- [ ] **Step 2: Run — fails, no Schema or BooleanSchema yet** +- [x] **Step 2: Run — fails, no Schema or BooleanSchema yet** Run: `mvn -q test -Dtest=AdditionalPropertiesTest` Expected: compilation failure. -- [ ] **Step 3: Stub `Schema` and `BooleanSchema` (full hierarchy comes in B3)** +- [x] **Step 3: Stub `Schema` and `BooleanSchema` (full hierarchy comes in B3)** Create `src/main/java/com/retailsvc/http/spec/schema/Schema.java`: @@ -233,12 +233,12 @@ public sealed interface AdditionalProperties { } ``` -- [ ] **Step 4: Verify** +- [x] **Step 4: Verify** Run: `mvn -q test -Dtest=AdditionalPropertiesTest` Expected: PASS (3 tests). -- [ ] **Step 5: Commit** +- [x] **Step 5: Commit** ```bash git add src/main/java/com/retailsvc/http/spec/schema/ \ @@ -259,7 +259,7 @@ git commit -m "feat(schema): add Schema sealed interface, BooleanSchema, Additio - Modify: `src/main/java/com/retailsvc/http/spec/schema/Schema.java` (extend `permits`) - Test: `src/test/java/com/retailsvc/http/spec/schema/PrimitiveSchemasTest.java` -- [ ] **Step 1: Write the failing test** +- [x] **Step 1: Write the failing test** ```java package com.retailsvc.http.spec.schema; @@ -313,12 +313,12 @@ class PrimitiveSchemasTest { } ``` -- [ ] **Step 2: Run — fails** +- [x] **Step 2: Run — fails** Run: `mvn -q test -Dtest=PrimitiveSchemasTest` Expected: compilation failure. -- [ ] **Step 3: Implement records** +- [x] **Step 3: Implement records** `StringSchema.java`: @@ -411,13 +411,13 @@ public sealed interface Schema } ``` -- [ ] **Step 4: Verify** +- [x] **Step 4: Verify** Run: `mvn -q test -Dtest=PrimitiveSchemasTest` Expected: PASS (5 tests). Then: `mvn -q test` (full suite still green — old code unaffected). -- [ ] **Step 5: Commit** +- [x] **Step 5: Commit** ```bash git add src/main/java/com/retailsvc/http/spec/schema/ \ @@ -435,7 +435,7 @@ git commit -m "feat(schema): add primitive Schema records" - Modify: `src/main/java/com/retailsvc/http/spec/schema/Schema.java` (extend `permits`) - Test: `src/test/java/com/retailsvc/http/spec/schema/ContainerSchemasTest.java` -- [ ] **Step 1: Write the failing test** +- [x] **Step 1: Write the failing test** ```java package com.retailsvc.http.spec.schema; @@ -473,12 +473,12 @@ class ContainerSchemasTest { } ``` -- [ ] **Step 2: Run — fails** +- [x] **Step 2: Run — fails** Run: `mvn -q test -Dtest=ContainerSchemasTest` Expected: compilation failure. -- [ ] **Step 3: Implement** +- [x] **Step 3: Implement** `ObjectSchema.java`: @@ -523,12 +523,12 @@ public sealed interface Schema } ``` -- [ ] **Step 4: Verify** +- [x] **Step 4: Verify** Run: `mvn -q test -Dtest=ContainerSchemasTest` Expected: PASS (2 tests). -- [ ] **Step 5: Commit** +- [x] **Step 5: Commit** ```bash git add src/main/java/com/retailsvc/http/spec/schema/ \ @@ -550,7 +550,7 @@ git commit -m "feat(schema): add ObjectSchema and ArraySchema records" - Modify: `src/main/java/com/retailsvc/http/spec/schema/Schema.java` - Test: `src/test/java/com/retailsvc/http/spec/schema/CombinatorScaffoldTest.java` -- [ ] **Step 1: Write the failing test** +- [x] **Step 1: Write the failing test** ```java package com.retailsvc.http.spec.schema; @@ -576,12 +576,12 @@ class CombinatorScaffoldTest { } ``` -- [ ] **Step 2: Run — fails** +- [x] **Step 2: Run — fails** Run: `mvn -q test -Dtest=CombinatorScaffoldTest` Expected: compilation failure. -- [ ] **Step 3: Implement records** +- [x] **Step 3: Implement records** `OneOfSchema.java`: @@ -677,12 +677,12 @@ public sealed interface Schema } ``` -- [ ] **Step 4: Verify** +- [x] **Step 4: Verify** Run: `mvn -q test -Dtest=CombinatorScaffoldTest` Expected: PASS (7 tests). -- [ ] **Step 5: Commit** +- [x] **Step 5: Commit** ```bash git add src/main/java/com/retailsvc/http/spec/schema/ \ @@ -700,7 +700,7 @@ git commit -m "feat(schema): scaffold combinator records (oneOf/anyOf/allOf/not/ - Create: `src/main/java/com/retailsvc/http/spec/schema/SchemaParser.java` - Test: `src/test/java/com/retailsvc/http/spec/schema/SchemaParserTest.java` -- [ ] **Step 1: Write the failing test** +- [x] **Step 1: Write the failing test** ```java package com.retailsvc.http.spec.schema; @@ -767,12 +767,12 @@ class SchemaParserTest { } ``` -- [ ] **Step 2: Run — fails** +- [x] **Step 2: Run — fails** Run: `mvn -q test -Dtest=SchemaParserTest` Expected: compilation failure. -- [ ] **Step 3: Implement primitive parser** +- [x] **Step 3: Implement primitive parser** ```java package com.retailsvc.http.spec.schema; @@ -865,12 +865,12 @@ public final class SchemaParser { } ``` -- [ ] **Step 4: Verify** +- [x] **Step 4: Verify** Run: `mvn -q test -Dtest=SchemaParserTest` Expected: 8 PASS. -- [ ] **Step 5: Commit** +- [x] **Step 5: Commit** ```bash git add src/main/java/com/retailsvc/http/spec/schema/SchemaParser.java \ @@ -886,7 +886,7 @@ git commit -m "feat(schema): SchemaParser handles primitives, refs, nullable for - Modify: `src/main/java/com/retailsvc/http/spec/schema/SchemaParser.java` - Modify: `src/test/java/com/retailsvc/http/spec/schema/SchemaParserTest.java` -- [ ] **Step 1: Add tests** +- [x] **Step 1: Add tests** Append to `SchemaParserTest.java`: @@ -934,12 +934,12 @@ Append to `SchemaParserTest.java`: } ``` -- [ ] **Step 2: Run — fails** +- [x] **Step 2: Run — fails** Run: `mvn -q test -Dtest=SchemaParserTest` Expected: 4 new tests fail with `UnsupportedOperationException`. -- [ ] **Step 3: Implement** +- [x] **Step 3: Implement** Add the import to `SchemaParser`: @@ -999,12 +999,12 @@ Add private methods: } ``` -- [ ] **Step 4: Verify** +- [x] **Step 4: Verify** Run: `mvn -q test -Dtest=SchemaParserTest` Expected: 12 PASS. -- [ ] **Step 5: Commit** +- [x] **Step 5: Commit** ```bash git add src/main/java/com/retailsvc/http/spec/schema/SchemaParser.java \ @@ -1019,7 +1019,7 @@ git commit -m "feat(schema): SchemaParser handles objects (with additionalProper **Files:** - Modify: `SchemaParser.java`, `SchemaParserTest.java` -- [ ] **Step 1: Add tests** +- [x] **Step 1: Add tests** ```java @Test @@ -1059,12 +1059,12 @@ git commit -m "feat(schema): SchemaParser handles objects (with additionalProper } ``` -- [ ] **Step 2: Run — fails** +- [x] **Step 2: Run — fails** Run: `mvn -q test -Dtest=SchemaParserTest` Expected: 5 new tests fail. -- [ ] **Step 3: Add dispatch at top of `SchemaParser.parse`** +- [x] **Step 3: Add dispatch at top of `SchemaParser.parse`** Insert these checks just after the `$ref` check, in this order: @@ -1091,12 +1091,12 @@ Helper: } ``` -- [ ] **Step 4: Verify** +- [x] **Step 4: Verify** Run: `mvn -q test -Dtest=SchemaParserTest` Expected: all 17 PASS. -- [ ] **Step 5: Commit** +- [x] **Step 5: Commit** ```bash git add src/main/java/com/retailsvc/http/spec/schema/SchemaParser.java \ @@ -1114,7 +1114,7 @@ git commit -m "feat(schema): SchemaParser handles combinators, const, top-level - Create: `src/main/java/com/retailsvc/http/spec/HttpMethod.java` - Test: `src/test/java/com/retailsvc/http/spec/HttpMethodTest.java` -- [ ] **Step 1: Test** +- [x] **Step 1: Test** ```java package com.retailsvc.http.spec; @@ -1132,10 +1132,10 @@ class HttpMethodTest { } ``` -- [ ] **Step 2: Run — fails (compile)** +- [x] **Step 2: Run — fails (compile)** Run: `mvn -q test -Dtest=HttpMethodTest` -- [ ] **Step 3: Implement** +- [x] **Step 3: Implement** ```java package com.retailsvc.http.spec; @@ -1151,10 +1151,10 @@ public enum HttpMethod { } ``` -- [ ] **Step 4: Verify** +- [x] **Step 4: Verify** Run: `mvn -q test -Dtest=HttpMethodTest` — 4 PASS. -- [ ] **Step 5: Commit** +- [x] **Step 5: Commit** ```bash git add src/main/java/com/retailsvc/http/spec/HttpMethod.java \ @@ -1170,7 +1170,7 @@ git commit -m "feat(spec): add HttpMethod enum" - Create: `src/main/java/com/retailsvc/http/spec/PathTemplate.java` - Test: `src/test/java/com/retailsvc/http/spec/PathTemplateTest.java` -- [ ] **Step 1: Test** +- [x] **Step 1: Test** ```java package com.retailsvc.http.spec; @@ -1217,9 +1217,9 @@ class PathTemplateTest { } ``` -- [ ] **Step 2: Run — fails** +- [x] **Step 2: Run — fails** -- [ ] **Step 3: Implement** +- [x] **Step 3: Implement** ```java package com.retailsvc.http.spec; @@ -1264,9 +1264,9 @@ public record PathTemplate(String raw, Pattern compiled, List parameterN } ``` -- [ ] **Step 4: Verify** — 6 PASS. +- [x] **Step 4: Verify** — 6 PASS. -- [ ] **Step 5: Commit** +- [x] **Step 5: Commit** ```bash git add src/main/java/com/retailsvc/http/spec/PathTemplate.java \ @@ -1287,7 +1287,7 @@ git commit -m "feat(spec): add PathTemplate value object with regex extraction" - Create: `src/main/java/com/retailsvc/http/spec/Info.java` - Test: `src/test/java/com/retailsvc/http/spec/SpecRecordsTest.java` -- [ ] **Step 1: Test** +- [x] **Step 1: Test** ```java package com.retailsvc.http.spec; @@ -1327,9 +1327,9 @@ class SpecRecordsTest { } ``` -- [ ] **Step 2: Run — fails** +- [x] **Step 2: Run — fails** -- [ ] **Step 3: Implement** +- [x] **Step 3: Implement** `Parameter.java`: @@ -1395,9 +1395,9 @@ package com.retailsvc.http.spec; public record Info(String title, String version) {} ``` -- [ ] **Step 4: Verify** — 4 PASS. +- [x] **Step 4: Verify** — 4 PASS. -- [ ] **Step 5: Commit** +- [x] **Step 5: Commit** ```bash git add src/main/java/com/retailsvc/http/spec/ \ @@ -1413,7 +1413,7 @@ git commit -m "feat(spec): add Parameter, RequestBody, MediaType, Response, Serv - Create: `src/main/java/com/retailsvc/http/spec/Operation.java` - Test: `src/test/java/com/retailsvc/http/spec/OperationTest.java` -- [ ] **Step 1: Test** +- [x] **Step 1: Test** ```java package com.retailsvc.http.spec; @@ -1443,9 +1443,9 @@ class OperationTest { } ``` -- [ ] **Step 2: Run — fails** +- [x] **Step 2: Run — fails** -- [ ] **Step 3: Implement** +- [x] **Step 3: Implement** ```java package com.retailsvc.http.spec; @@ -1463,9 +1463,9 @@ public record Operation( Map responses) {} ``` -- [ ] **Step 4: Verify** — 1 PASS. +- [x] **Step 4: Verify** — 1 PASS. -- [ ] **Step 5: Commit** +- [x] **Step 5: Commit** ```bash git add src/main/java/com/retailsvc/http/spec/Operation.java \ @@ -1483,7 +1483,7 @@ git commit -m "feat(spec): add Operation record" - Test: `src/test/java/com/retailsvc/http/spec/SpecTest.java` - Resource: existing `src/test/resources/openapi.json` is reused as the canonical fixture. -- [ ] **Step 1: Test against the existing fixture** +- [x] **Step 1: Test against the existing fixture** ```java package com.retailsvc.http.spec; @@ -1558,9 +1558,9 @@ class SpecTest { } ``` -- [ ] **Step 2: Run — fails** +- [x] **Step 2: Run — fails** -- [ ] **Step 3: Implement Spec + parser** +- [x] **Step 3: Implement Spec + parser** `Spec.java`: @@ -1726,11 +1726,11 @@ public record Spec( } ``` -- [ ] **Step 4: Verify** — 4 PASS, full suite still green. +- [x] **Step 4: Verify** — 4 PASS, full suite still green. Run: `mvn -q test` -- [ ] **Step 5: Commit** +- [x] **Step 5: Commit** ```bash git add src/main/java/com/retailsvc/http/spec/Spec.java \ @@ -1750,7 +1750,7 @@ git commit -m "feat(spec): add Spec.from(Map) walker for the full document" - Create: `src/main/java/com/retailsvc/http/ValidationException.java` - Test: `src/test/java/com/retailsvc/http/ValidationExceptionTest.java` -- [ ] **Step 1: Test** +- [x] **Step 1: Test** ```java package com.retailsvc.http; @@ -1770,9 +1770,9 @@ class ValidationExceptionTest { } ``` -- [ ] **Step 2: Run — fails** +- [x] **Step 2: Run — fails** -- [ ] **Step 3: Implement** +- [x] **Step 3: Implement** `ValidationError.java`: @@ -1814,9 +1814,9 @@ public final class ValidationException extends RuntimeException { } ``` -- [ ] **Step 4: Verify** — 1 PASS. +- [x] **Step 4: Verify** — 1 PASS. -- [ ] **Step 5: Commit** +- [x] **Step 5: Commit** ```bash git add src/main/java/com/retailsvc/http/validate/ \ @@ -1833,7 +1833,7 @@ git commit -m "feat(validate): add ValidationError, ValidationException, Validat - Create: `src/main/java/com/retailsvc/http/validate/DefaultValidator.java` - Test: `src/test/java/com/retailsvc/http/validate/DefaultValidatorDispatchTest.java` -- [ ] **Step 1: Test** +- [x] **Step 1: Test** ```java package com.retailsvc.http.validate; @@ -1885,9 +1885,9 @@ class DefaultValidatorDispatchTest { } ``` -- [ ] **Step 2: Run — fails** +- [x] **Step 2: Run — fails** -- [ ] **Step 3: Implement skeleton** +- [x] **Step 3: Implement skeleton** ```java package com.retailsvc.http.validate; @@ -1983,11 +1983,11 @@ public final class DefaultValidator implements Validator { } ``` -- [ ] **Step 4: Verify** +- [x] **Step 4: Verify** Run: `mvn -q test -Dtest=DefaultValidatorDispatchTest` — 5 PASS. -- [ ] **Step 5: Commit** +- [x] **Step 5: Commit** ```bash git add src/main/java/com/retailsvc/http/validate/DefaultValidator.java \ @@ -2003,7 +2003,7 @@ git commit -m "feat(validate): DefaultValidator skeleton with dispatch + boolean - Modify: `DefaultValidator.java` - Create: `src/test/java/com/retailsvc/http/validate/StringIntegerNumberTest.java` -- [ ] **Step 1: Test** +- [x] **Step 1: Test** ```java package com.retailsvc.http.validate; @@ -2109,9 +2109,9 @@ class StringIntegerNumberTest { } ``` -- [ ] **Step 2: Run — fails** +- [x] **Step 2: Run — fails** -- [ ] **Step 3: Replace stub bodies** +- [x] **Step 3: Replace stub bodies** ```java private void validateString(Object value, StringSchema s, String pointer) { @@ -2195,11 +2195,11 @@ class StringIntegerNumberTest { (Remove `private void validateString/Integer/Number` UnsupportedOperationException stubs.) -- [ ] **Step 4: Verify** +- [x] **Step 4: Verify** Run: `mvn -q test -Dtest=StringIntegerNumberTest` — 10 PASS. -- [ ] **Step 5: Commit** +- [x] **Step 5: Commit** ```bash git add src/main/java/com/retailsvc/http/validate/DefaultValidator.java \ @@ -2215,7 +2215,7 @@ git commit -m "feat(validate): string/integer/number validation with full 3.1 nu - Modify: `DefaultValidator.java` - Create: `src/test/java/com/retailsvc/http/validate/ObjectValidationTest.java` -- [ ] **Step 1: Test** +- [x] **Step 1: Test** ```java package com.retailsvc.http.validate; @@ -2280,9 +2280,9 @@ class ObjectValidationTest { } ``` -- [ ] **Step 2: Run — fails** +- [x] **Step 2: Run — fails** -- [ ] **Step 3: Implement** +- [x] **Step 3: Implement** Replace stub: @@ -2319,9 +2319,9 @@ Replace stub: } ``` -- [ ] **Step 4: Verify** — 5 PASS. +- [x] **Step 4: Verify** — 5 PASS. -- [ ] **Step 5: Commit** +- [x] **Step 5: Commit** ```bash git add src/main/java/com/retailsvc/http/validate/DefaultValidator.java \ @@ -2337,7 +2337,7 @@ git commit -m "feat(validate): object validation with required/properties/additi - Modify: `DefaultValidator.java` - Create: `src/test/java/com/retailsvc/http/validate/ArrayValidationTest.java` -- [ ] **Step 1: Test** +- [x] **Step 1: Test** ```java package com.retailsvc.http.validate; @@ -2402,9 +2402,9 @@ class ArrayValidationTest { } ``` -- [ ] **Step 2: Run — fails** +- [x] **Step 2: Run — fails** -- [ ] **Step 3: Implement** +- [x] **Step 3: Implement** ```java private void validateArray(Object value, ArraySchema s, String pointer) { @@ -2431,9 +2431,9 @@ class ArrayValidationTest { } ``` -- [ ] **Step 4: Verify** — 5 PASS, full suite green. +- [x] **Step 4: Verify** — 5 PASS, full suite green. -- [ ] **Step 5: Commit** +- [x] **Step 5: Commit** ```bash git add src/main/java/com/retailsvc/http/validate/DefaultValidator.java \ @@ -2451,7 +2451,7 @@ git commit -m "feat(validate): array validation with items/minItems/maxItems/uni - Create: `src/main/java/com/retailsvc/http/internal/Router.java` - Test: `src/test/java/com/retailsvc/http/internal/RouterTest.java` -- [ ] **Step 1: Test** +- [x] **Step 1: Test** ```java package com.retailsvc.http.internal; @@ -2502,9 +2502,9 @@ class RouterTest { } ``` -- [ ] **Step 2: Run — fails** +- [x] **Step 2: Run — fails** -- [ ] **Step 3: Implement** +- [x] **Step 3: Implement** ```java package com.retailsvc.http.internal; @@ -2564,9 +2564,9 @@ public final class Router { } ``` -- [ ] **Step 4: Verify** — 4 PASS. +- [x] **Step 4: Verify** — 4 PASS. -- [ ] **Step 5: Commit** +- [x] **Step 5: Commit** ```bash git add src/main/java/com/retailsvc/http/internal/Router.java \ @@ -2585,7 +2585,7 @@ git commit -m "feat(internal): Router with exact and templated indexes plus allo - Create: `src/main/java/com/retailsvc/http/MethodNotAllowedException.java` - Test: `src/test/java/com/retailsvc/http/HttpExceptionsTest.java` -- [ ] **Step 1: Test** +- [x] **Step 1: Test** ```java package com.retailsvc.http; @@ -2608,9 +2608,9 @@ class HttpExceptionsTest { } ``` -- [ ] **Step 2: Run — fails** +- [x] **Step 2: Run — fails** -- [ ] **Step 3: Implement** +- [x] **Step 3: Implement** ```java package com.retailsvc.http; @@ -2636,9 +2636,9 @@ public final class MethodNotAllowedException extends RuntimeException { } ``` -- [ ] **Step 4: Verify** — 2 PASS. +- [x] **Step 4: Verify** — 2 PASS. -- [ ] **Step 5: Commit** +- [x] **Step 5: Commit** ```bash git add src/main/java/com/retailsvc/http/NotFoundException.java \ @@ -2655,7 +2655,7 @@ git commit -m "feat(http): add NotFoundException and MethodNotAllowedException" - Create: `src/main/java/com/retailsvc/http/Request.java` - Test: `src/test/java/com/retailsvc/http/RequestTest.java` -- [ ] **Step 1: Test** +- [x] **Step 1: Test** ```java package com.retailsvc.http; @@ -2683,9 +2683,9 @@ class RequestTest { } ``` -- [ ] **Step 2: Run — fails** +- [x] **Step 2: Run — fails** -- [ ] **Step 3: Implement** +- [x] **Step 3: Implement** ```java package com.retailsvc.http; @@ -2711,9 +2711,9 @@ public final class Request { } ``` -- [ ] **Step 4: Verify** — 1 PASS. +- [x] **Step 4: Verify** — 1 PASS. -- [ ] **Step 5: Commit** +- [x] **Step 5: Commit** ```bash git add src/main/java/com/retailsvc/http/Request.java \ @@ -2732,7 +2732,7 @@ git commit -m "feat(http): add Request static accessors for exchange attributes" The old `JsonMapper` lives at `com.retailsvc.http.openapi.model.JsonMapper` with a generic single method ` T mapFrom(byte[] body)`. We add the new shape in the public package now; the old one stays until Phase K deletes it. -- [ ] **Step 1: Test** +- [x] **Step 1: Test** ```java package com.retailsvc.http; @@ -2749,9 +2749,9 @@ class JsonMapperTest { } ``` -- [ ] **Step 2: Run — fails** +- [x] **Step 2: Run — fails** -- [ ] **Step 3: Implement** +- [x] **Step 3: Implement** ```java package com.retailsvc.http; @@ -2762,9 +2762,9 @@ public interface JsonMapper { } ``` -- [ ] **Step 4: Verify** — 1 PASS. +- [x] **Step 4: Verify** — 1 PASS. -- [ ] **Step 5: Commit** +- [x] **Step 5: Commit** ```bash git add src/main/java/com/retailsvc/http/JsonMapper.java \ @@ -2782,7 +2782,7 @@ git commit -m "feat(http): JsonMapper SAM in public package (no generic)" - Test: `src/test/java/com/retailsvc/http/internal/ProblemDetailRendererTest.java` - Test: `src/test/java/com/retailsvc/http/HandlersDefaultExceptionTest.java` -- [ ] **Step 1: Test renderer** +- [x] **Step 1: Test renderer** ```java package com.retailsvc.http.internal; @@ -2814,9 +2814,9 @@ class ProblemDetailRendererTest { } ``` -- [ ] **Step 2: Run — fails** +- [x] **Step 2: Run — fails** -- [ ] **Step 3: Implement renderer** +- [x] **Step 3: Implement renderer** ```java package com.retailsvc.http.internal; @@ -2858,9 +2858,9 @@ public final class ProblemDetailRenderer { } ``` -- [ ] **Step 4: Verify renderer** — 2 PASS. +- [x] **Step 4: Verify renderer** — 2 PASS. -- [ ] **Step 5: Test default exception handler** +- [x] **Step 5: Test default exception handler** ```java package com.retailsvc.http; @@ -2915,9 +2915,9 @@ class HandlersDefaultExceptionTest { } ``` -- [ ] **Step 6: Run — fails (default handler not yet handling new types)** +- [x] **Step 6: Run — fails (default handler not yet handling new types)** -- [ ] **Step 7: Implement** in `Handlers.java` +- [x] **Step 7: Implement** in `Handlers.java` Replace the existing `Handlers.java` body. Keep `notFoundHandler()` unchanged. Update `defaultExceptionHandler()` to: @@ -2983,11 +2983,11 @@ public final class Handlers { `ExceptionHandler` interface stays where it is today (`com.retailsvc.http.ExceptionHandler`); no change needed. -- [ ] **Step 8: Verify** +- [x] **Step 8: Verify** Run: `mvn -q test -Dtest=HandlersDefaultExceptionTest,ProblemDetailRendererTest` — 5 PASS. -- [ ] **Step 9: Commit** +- [x] **Step 9: Commit** ```bash git add src/main/java/com/retailsvc/http/internal/ProblemDetailRenderer.java \ @@ -3007,7 +3007,7 @@ git commit -m "feat(http): RFC 7807 problem+json renderer + default handler cove - Create: `src/main/java/com/retailsvc/http/internal/ExceptionFilter.java` - Test: `src/test/java/com/retailsvc/http/internal/ExceptionFilterTest.java` -- [ ] **Step 1: Test** +- [x] **Step 1: Test** ```java package com.retailsvc.http.internal; @@ -3044,9 +3044,9 @@ class ExceptionFilterTest { } ``` -- [ ] **Step 2: Run — fails** +- [x] **Step 2: Run — fails** -- [ ] **Step 3: Implement** +- [x] **Step 3: Implement** ```java package com.retailsvc.http.internal; @@ -3077,9 +3077,9 @@ public final class ExceptionFilter extends Filter { } ``` -- [ ] **Step 4: Verify** — 2 PASS. +- [x] **Step 4: Verify** — 2 PASS. -- [ ] **Step 5: Commit** +- [x] **Step 5: Commit** ```bash git add src/main/java/com/retailsvc/http/internal/ExceptionFilter.java \ @@ -3095,7 +3095,7 @@ git commit -m "feat(internal): ExceptionFilter delegates to consumer ExceptionHa - Create: `src/main/java/com/retailsvc/http/internal/RequestPreparationFilter.java` - Test: `src/test/java/com/retailsvc/http/internal/RequestPreparationFilterTest.java` -- [ ] **Step 1: Test** +- [x] **Step 1: Test** ```java package com.retailsvc.http.internal; @@ -3218,9 +3218,9 @@ class RequestPreparationFilterTest { } ``` -- [ ] **Step 2: Run — fails** +- [x] **Step 2: Run — fails** -- [ ] **Step 3: Implement** +- [x] **Step 3: Implement** ```java package com.retailsvc.http.internal; @@ -3357,9 +3357,9 @@ public final class RequestPreparationFilter extends Filter { } ``` -- [ ] **Step 4: Verify** — 4 PASS. +- [x] **Step 4: Verify** — 4 PASS. -- [ ] **Step 5: Commit** +- [x] **Step 5: Commit** ```bash git add src/main/java/com/retailsvc/http/internal/RequestPreparationFilter.java \ @@ -3377,7 +3377,7 @@ git commit -m "feat(internal): RequestPreparationFilter combines body capture, r - (Old `MissingOperationHandlerException` lives in `com.retailsvc.http.openapi.exceptions`. Move to public package now and adjust import; old file deleted in Phase K.) - Modify: `src/main/java/com/retailsvc/http/MissingOperationHandlerException.java` (new — same name as old, public package) -- [ ] **Step 1: Test** +- [x] **Step 1: Test** ```java package com.retailsvc.http.internal; @@ -3417,9 +3417,9 @@ class DispatchHandlerTest { } ``` -- [ ] **Step 2: Run — fails** +- [x] **Step 2: Run — fails** -- [ ] **Step 3: Implement** +- [x] **Step 3: Implement** `MissingOperationHandlerException.java`: @@ -3462,9 +3462,9 @@ public final class DispatchHandler implements HttpHandler { } ``` -- [ ] **Step 4: Verify** — 2 PASS. +- [x] **Step 4: Verify** — 2 PASS. -- [ ] **Step 5: Commit** +- [x] **Step 5: Commit** ```bash git add src/main/java/com/retailsvc/http/MissingOperationHandlerException.java \ @@ -3485,13 +3485,13 @@ git commit -m "feat(internal): DispatchHandler dispatches to registered HttpHand The existing `OpenApiServer` references `com.retailsvc.http.openapi.model.OpenApi` and `com.retailsvc.http.openapi.model.JsonMapper`. We rewrite the file in place to use the new types. -- [ ] **Step 1: Read existing tests to see what's being asserted** +- [x] **Step 1: Read existing tests to see what's being asserted** Run: `grep -l "new OpenApiServer" src/test` Existing tests construct `OpenApiServer` with the old types. They will be migrated in Task I2; for this task we only need the production class to compile and be correct. -- [ ] **Step 2: Rewrite the class** +- [x] **Step 2: Rewrite the class** Replace the contents of `src/main/java/com/retailsvc/http/OpenApiServer.java`: @@ -3578,9 +3578,9 @@ public class OpenApiServer implements AutoCloseable { } ``` -- [ ] **Step 3: Existing tests will fail to compile** — that's expected. Don't `mvn test` here yet; proceed to I2. +- [x] **Step 3: Existing tests will fail to compile** — that's expected. Don't `mvn test` here yet; proceed to I2. -- [ ] **Step 4: Commit** +- [x] **Step 4: Commit** ```bash git add src/main/java/com/retailsvc/http/OpenApiServer.java @@ -3600,7 +3600,7 @@ git commit -m "refactor(http): rewrite OpenApiServer against new Spec/Validator/ This task covers wiring all existing test code to the new API. The migration recipe is identical for each file. -- [ ] **Step 1: Migrate `ServerLauncher.java`** (the example) +- [x] **Step 1: Migrate `ServerLauncher.java`** (the example) Old code calls `parseSpecification("openapi.json", s -> gson.fromJson(s, OpenApi.class))` etc. Replace with: @@ -3645,7 +3645,7 @@ public class ServerLauncher { } ``` -- [ ] **Step 2: Migrate `*Handler` test classes** +- [x] **Step 2: Migrate `*Handler` test classes** Any handler that uses `getRequestBody(exchange)` from `GetRequestBody` should call `Request.bytes(exchange)` or `Request.parsed(exchange)` instead. Remove `implements GetRequestBody`. @@ -3665,11 +3665,11 @@ public class EchoHandler implements HttpHandler { // remove GetRequestBody } ``` -- [ ] **Step 3: Migrate `ServerBaseTest.java`** +- [x] **Step 3: Migrate `ServerBaseTest.java`** Replace its `OpenApiServer` setup helper to construct via the new API exactly as in `ServerLauncher`. All subclasses (`OpenApiServerTest`, `OpenApiServerIT`) inherit. -- [ ] **Step 4: Migrate test assertions for the new error format** +- [x] **Step 4: Migrate test assertions for the new error format** The existing `OpenApiServerIT` likely asserts `400` with empty body for invalid input. Update to assert: - Status 400 @@ -3678,14 +3678,14 @@ The existing `OpenApiServerIT` likely asserts `400` with empty body for invalid Where the test asserts `404`/`500` for unknown operation, change to assert `404` (now from `NotFoundException`). -- [ ] **Step 5: Run full suite** +- [x] **Step 5: Run full suite** Run: `mvn -q test` Expected: all green. If a test depends on `BodyHandler.RequestBodyWrapper` directly, swap to `Request.bytes(exchange)`. If anything fails because `OpenApiValidationFilter` test assertions don't apply anymore, those tests get deleted in Phase K — for now, mark them `@Disabled("removed in Phase K")` to keep the suite green. -- [ ] **Step 6: Commit** +- [x] **Step 6: Commit** ```bash git add src/test/java/com/retailsvc/http/ @@ -3699,14 +3699,14 @@ git commit -m "refactor(test): migrate test launcher, handlers, and integration **Files:** - Modify: `src/test/java/com/retailsvc/http/OpenApiServerIT.java` (already touched in I2; this task validates the spec fixture round-trips) -- [ ] **Step 1: Run integration tests** +- [x] **Step 1: Run integration tests** Run: `mvn -q verify` Expected: BUILD SUCCESS, all surefire + failsafe tests green. If the fixture exercises features not yet implemented (path matching, query params, body parsing) ensure they all pass with the new validator. If any specific case fails because the new validator is stricter than the old (e.g., an invalid spec that the old code accepted), fix the fixture and document it in the commit message. -- [ ] **Step 2: Commit any fixture adjustments** +- [x] **Step 2: Commit any fixture adjustments** ```bash git add src/test/resources/ @@ -3724,11 +3724,11 @@ git commit -m "test: align fixture with stricter new validator" **Files:** - Modify: `README.md` -- [ ] **Step 1: Update Prerequisites** +- [x] **Step 1: Update Prerequisites** Replace "Java SDK 21 or later" with "Java SDK 25 or later". -- [ ] **Step 2: Replace the Basic Usage code blocks** +- [x] **Step 2: Replace the Basic Usage code blocks** Replace the code that shows `parseSpecification(...)` and the verbose `JsonMapper` anonymous class with the new pattern: @@ -3757,11 +3757,11 @@ public class YourServerLauncher { ``` ```` -- [ ] **Step 3: Update Handler example** +- [x] **Step 3: Update Handler example** Replace `implements HttpHandler, GetRequestBody` with `implements HttpHandler` and use `Request.bytes(exchange)` / `Request.parsed(exchange)` to access body data. -- [ ] **Step 4: Add a "YAML" subsection** +- [x] **Step 4: Add a "YAML" subsection** ```markdown ### YAML specifications @@ -3772,7 +3772,7 @@ Map raw = new Yaml().load(Files.newInputStream(Path.of("openapi. The rest is identical. ``` -- [ ] **Step 5: Commit** +- [x] **Step 5: Commit** ```bash git add README.md @@ -3786,18 +3786,18 @@ git commit -m "docs: update README for Java 25 and post-refactor public API" **Files:** - Modify: `CLAUDE.md` -- [ ] **Step 1: Replace "Java 21" with "Java 25" in the Project paragraph** +- [x] **Step 1: Replace "Java 21" with "Java 25" in the Project paragraph** -- [ ] **Step 2: Replace the Architecture section** +- [x] **Step 2: Replace the Architecture section** Replace the description of the request flow with the new pipeline (`ExceptionFilter` → `RequestPreparationFilter` → `DispatchHandler`). Replace the "Key abstractions" bullets with: sealed `Schema`, `Spec.from(Map)`, `Request` static helper, `DefaultValidator` with pattern-match dispatch, `Router` with exact + templated indexes. -- [ ] **Step 3: Verify no stale references** +- [x] **Step 3: Verify no stale references** Run: `grep -n "Java 21\|java-21\|release>21<\|version 21\|BodyHandler\|OpenApiValidationFilter\|GetRequestBody\|SpecificationLoader\|RequestDispatchingHandler\|ExceptionHandlingFilter\|com.retailsvc.http.openapi" CLAUDE.md README.md` Expected: no output (or only legitimate historical references — fix them). -- [ ] **Step 4: Commit** +- [x] **Step 4: Commit** ```bash git add CLAUDE.md @@ -3816,7 +3816,7 @@ git commit -m "docs(claude): refresh architecture section for refactor" - Delete: `src/main/java/com/retailsvc/http/ExceptionHandlingFilter.java` - Delete: any test files under `src/test/java/com/retailsvc/http/openapi/` that test the deleted code -- [ ] **Step 1: Delete files** +- [x] **Step 1: Delete files** ```bash git rm -r src/main/java/com/retailsvc/http/openapi/ @@ -3825,19 +3825,19 @@ git rm src/main/java/com/retailsvc/http/ExceptionHandlingFilter.java git rm -r src/test/java/com/retailsvc/http/openapi/ ``` -- [ ] **Step 2: Find leftover references** +- [x] **Step 2: Find leftover references** ```bash grep -rn "com.retailsvc.http.openapi\|BodyHandler\|ExceptionHandlingFilter\|GetRequestBody\|RequestDispatchingHandler\|OpenApiValidationFilter\|SpecificationLoader" src/ ``` Expected: no matches. Fix any that remain. -- [ ] **Step 3: Run full suite** +- [x] **Step 3: Run full suite** Run: `mvn -q verify` Expected: BUILD SUCCESS, all green. -- [ ] **Step 4: Commit** +- [x] **Step 4: Commit** ```bash git add -A @@ -3853,26 +3853,26 @@ git commit -m "refactor: delete legacy openapi.* packages and old filter/wrapper **Files:** - (read-only verification) -- [ ] **Step 1: Java version sweep** +- [x] **Step 1: Java version sweep** ```bash grep -rn "Java 21\|java-21\|release>21<" --include="*.md" --include="*.java" --include="*.xml" --include="*.yaml" --include="*.yml" --include="Dockerfile" . ``` Expected: no results (or only inside the spec doc historical context, which is fine). -- [ ] **Step 2: Old API symbol sweep** +- [x] **Step 2: Old API symbol sweep** ```bash grep -rn "OpenApi\.parse\|JsonMapper.*<.*>\|getRequestBody(exchange)\|BodyHandler\b\|operation-id" src/main/java ``` Expected: no results in `src/main/java`. -- [ ] **Step 3: Coverage** +- [x] **Step 3: Coverage** Run: `mvn -q verify` Open `target/site/jacoco/index.html` and confirm `com.retailsvc.http.validate`, `com.retailsvc.http.spec`, `com.retailsvc.http.spec.schema`, and `com.retailsvc.http.internal` are at or above 80% line coverage. -- [ ] **Step 4: Hand-test the example** +- [x] **Step 4: Hand-test the example** Run: `mvn test-compile exec:java -Dexec.mainClass=com.retailsvc.http.start.ServerLauncher -Dexec.classpathScope=test` @@ -3887,7 +3887,7 @@ curl -i -X POST http://localhost:8080/api/post-data -H 'content-type: applicatio Stop the server with Ctrl-C. -- [ ] **Step 5: Push the branch (if user requests)** +- [x] **Step 5: Push the branch (if user requests)** ```bash git push -u origin refactor/openapi-3.1-readiness @@ -3901,14 +3901,14 @@ git push -u origin refactor/openapi-3.1-readiness Before declaring the refactor done, walk through these: -- [ ] All 122 original tests have been migrated or deleted, with corresponding new coverage -- [ ] `mvn -q verify` is green -- [ ] No file under `src/main/java/com/retailsvc/http/openapi/` exists -- [ ] `BodyHandler`, `ExceptionHandlingFilter`, `GetRequestBody`, `RequestDispatchingHandler`, `OpenApiValidationFilter`, `SpecificationLoader`, `OpenApi`, `Components`, `OpenApiConstants`, `PathItem`, the per-kind validator classes, and the seven old exception classes from `openapi.exceptions` are all gone -- [ ] `Schema.minimum` / `maximum` defaulting to `Double.MIN_VALUE` / `Double.MAX_VALUE` (the bug) is gone — new model uses `null` for "unspecified" -- [ ] Combinator records exist; `DefaultValidator` throws `UnsupportedOperationException` on them; parser produces them -- [ ] `Spec.from(Map)` is the single entry point; no `Function` or `Function toJson` callbacks remain -- [ ] `JsonMapper` is `@FunctionalInterface` with `Object mapFrom(byte[])` -- [ ] Default 400 response is `application/problem+json` -- [ ] `Dockerfile`, `.java-version`, `pom.xml`, `README.md`, `CLAUDE.md` all reference Java 25 -- [ ] None of the in-scope "free" 3.1 keywords are missing: `minLength`, `maxLength`, `minItems`, `maxItems`, `uniqueItems`, `multipleOf`, `exclusiveMinimum`, `exclusiveMaximum`, `type:["string","null"]` +- [x] All 122 original tests have been migrated or deleted, with corresponding new coverage +- [x] `mvn -q verify` is green +- [x] No file under `src/main/java/com/retailsvc/http/openapi/` exists +- [x] `BodyHandler`, `ExceptionHandlingFilter`, `GetRequestBody`, `RequestDispatchingHandler`, `OpenApiValidationFilter`, `SpecificationLoader`, `OpenApi`, `Components`, `OpenApiConstants`, `PathItem`, the per-kind validator classes, and the seven old exception classes from `openapi.exceptions` are all gone +- [x] `Schema.minimum` / `maximum` defaulting to `Double.MIN_VALUE` / `Double.MAX_VALUE` (the bug) is gone — new model uses `null` for "unspecified" +- [x] Combinator records exist; `DefaultValidator` throws `UnsupportedOperationException` on them; parser produces them +- [x] `Spec.from(Map)` is the single entry point; no `Function` or `Function toJson` callbacks remain +- [x] `JsonMapper` is `@FunctionalInterface` with `Object mapFrom(byte[])` +- [x] Default 400 response is `application/problem+json` +- [x] `Dockerfile`, `.java-version`, `pom.xml`, `README.md`, `CLAUDE.md` all reference Java 25 +- [x] None of the in-scope "free" 3.1 keywords are missing: `minLength`, `maxLength`, `minItems`, `maxItems`, `uniqueItems`, `multipleOf`, `exclusiveMinimum`, `exclusiveMaximum`, `type:["string","null"]` diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml index 9e94794..dc14925 100644 --- a/src/test/resources/logback-test.xml +++ b/src/test/resources/logback-test.xml @@ -7,7 +7,7 @@ - + From 3adf1cb28067c024c80ed333522c9a659b500249 Mon Sep 17 00:00:00 2001 From: Thomas Cederholm Date: Fri, 8 May 2026 12:10:17 +0200 Subject: [PATCH 2/8] perf: Cache Spec.basePath at construction (W1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit basePath() was previously recomputed via URI.create on every request through RequestPreparationFilter.stripBasePath. The result is spec-static, so promote it to a record component populated in Spec.from(...). Eliminates ~100 MB of URI allocations and ~26 CPU samples per JFR run; k6 throughput +2.6% (44.2k -> 45.3k rps), p95 -40 µs. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/main/java/com/retailsvc/http/spec/Spec.java | 16 ++++++++++++---- .../internal/RequestPreparationFilterTest.java | 8 +++++++- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/retailsvc/http/spec/Spec.java b/src/main/java/com/retailsvc/http/spec/Spec.java index a6be4c8..d03ca24 100644 --- a/src/main/java/com/retailsvc/http/spec/Spec.java +++ b/src/main/java/com/retailsvc/http/spec/Spec.java @@ -16,7 +16,8 @@ public record Spec( List servers, List operations, Map componentSchemas, - Map componentParameters) { + Map componentParameters, + String basePath) { private static final String SCHEMA_KEY = "schema"; @@ -32,14 +33,21 @@ public static Spec from(Map raw) { List operations = parseOperations( (Map) raw.getOrDefault("paths", Map.of()), componentParameters); - return new Spec(openapi, info, servers, operations, componentSchemas, componentParameters); + return new Spec( + openapi, + info, + servers, + operations, + componentSchemas, + componentParameters, + computeBasePath(servers)); } - public String basePath() { + private static String computeBasePath(List servers) { if (servers.isEmpty()) { throw new IllegalStateException("no servers declared"); } - return Optional.ofNullable(URI.create(servers.get(0).url()).getPath()).orElse(""); + return Optional.ofNullable(URI.create(servers.getFirst().url()).getPath()).orElse(""); } public Schema resolveSchema(String ref) { diff --git a/src/test/java/com/retailsvc/http/internal/RequestPreparationFilterTest.java b/src/test/java/com/retailsvc/http/internal/RequestPreparationFilterTest.java index 7f3ede1..f119d15 100644 --- a/src/test/java/com/retailsvc/http/internal/RequestPreparationFilterTest.java +++ b/src/test/java/com/retailsvc/http/internal/RequestPreparationFilterTest.java @@ -44,7 +44,13 @@ private HttpExchange exchange(String method, String path, byte[] body) { private Spec specWith(Operation... ops) { return new Spec( - "3.1.0", new Info("t", "1"), List.of(new Server("/")), List.of(ops), Map.of(), Map.of()); + "3.1.0", + new Info("t", "1"), + List.of(new Server("/")), + List.of(ops), + Map.of(), + Map.of(), + ""); } private Filter newFilter(Spec spec) { From 61558530d3f5d0c1f6eff978a301ee25afd17496 Mon Sep 17 00:00:00 2001 From: Thomas Cederholm Date: Fri, 8 May 2026 12:12:26 +0200 Subject: [PATCH 3/8] perf: Memoise compiled regex Pattern in DefaultValidator (W2) validateString recompiled Pattern on every request. Pattern is immutable and thread-safe; cache compiled instances in a ConcurrentHashMap keyed by the raw pattern string. Cache is bounded by the spec's distinct pattern count. Removes ~215 MB of int[]/Matcher allocations per JFR run and the ~31 CPU samples in validateString. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../com/retailsvc/http/validate/DefaultValidator.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/retailsvc/http/validate/DefaultValidator.java b/src/main/java/com/retailsvc/http/validate/DefaultValidator.java index 0fffb47..2516044 100644 --- a/src/main/java/com/retailsvc/http/validate/DefaultValidator.java +++ b/src/main/java/com/retailsvc/http/validate/DefaultValidator.java @@ -29,6 +29,8 @@ import java.util.Objects; import java.util.Set; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import java.util.function.Function; import java.util.regex.Pattern; @@ -37,6 +39,7 @@ public final class DefaultValidator implements Validator { private static final String FORMAT_KEYWORD = "format"; private final Function refResolver; + private final ConcurrentMap compiledPatterns = new ConcurrentHashMap<>(); public DefaultValidator(Function refResolver) { this.refResolver = refResolver; @@ -81,7 +84,11 @@ private void validateString(Object value, StringSchema s, String pointer) { if (s.maxLength() != null && str.length() > s.maxLength()) { fail(pointer, "maxLength", "string longer than " + s.maxLength(), str); } - if (s.pattern() != null && !Pattern.compile(s.pattern()).matcher(str).matches()) { + if (s.pattern() != null + && !compiledPatterns + .computeIfAbsent(s.pattern(), Pattern::compile) + .matcher(str) + .matches()) { fail(pointer, "pattern", "does not match pattern " + s.pattern(), str); } if (s.enumValues() != null && !s.enumValues().contains(str)) { From 3abc1cee63ee5bf98d4c266b27acf7b737ffd4f9 Mon Sep 17 00:00:00 2001 From: Thomas Cederholm Date: Fri, 8 May 2026 12:15:26 +0200 Subject: [PATCH 4/8] perf: Pre-build ref->component index in Spec (W3) resolveSchema/resolveParameter previously rebuilt the component name via String.substring on every call. Build a Map keyed by the full $ref string at construction time so resolution is a single lookup with no per-request allocation. Removes ~150 MB of String/byte[] allocs per JFR run; tightens the validator hot path when schemas use \$ref. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../java/com/retailsvc/http/spec/Spec.java | 24 ++++++++++++++----- .../RequestPreparationFilterTest.java | 4 +++- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/retailsvc/http/spec/Spec.java b/src/main/java/com/retailsvc/http/spec/Spec.java index d03ca24..2f28898 100644 --- a/src/main/java/com/retailsvc/http/spec/Spec.java +++ b/src/main/java/com/retailsvc/http/spec/Spec.java @@ -17,9 +17,13 @@ public record Spec( List operations, Map componentSchemas, Map componentParameters, - String basePath) { + String basePath, + Map schemaRefIndex, + Map parameterRefIndex) { private static final String SCHEMA_KEY = "schema"; + private static final String SCHEMA_REF_PREFIX = "#/components/schemas/"; + private static final String PARAMETER_REF_PREFIX = "#/components/parameters/"; @SuppressWarnings("unchecked") public static Spec from(Map raw) { @@ -40,7 +44,9 @@ public static Spec from(Map raw) { operations, componentSchemas, componentParameters, - computeBasePath(servers)); + computeBasePath(servers), + indexByRef(componentSchemas, SCHEMA_REF_PREFIX), + indexByRef(componentParameters, PARAMETER_REF_PREFIX)); } private static String computeBasePath(List servers) { @@ -50,9 +56,16 @@ private static String computeBasePath(List servers) { return Optional.ofNullable(URI.create(servers.getFirst().url()).getPath()).orElse(""); } + private static Map indexByRef(Map components, String prefix) { + Map out = new LinkedHashMap<>(components.size()); + for (var e : components.entrySet()) { + out.put(prefix + e.getKey(), e.getValue()); + } + return Map.copyOf(out); + } + public Schema resolveSchema(String ref) { - String name = stripPrefix(ref, "#/components/schemas/"); - Schema s = componentSchemas.get(name); + Schema s = schemaRefIndex.get(ref); if (s == null) { throw new IllegalArgumentException("unknown schema ref: " + ref); } @@ -60,8 +73,7 @@ public Schema resolveSchema(String ref) { } public Parameter resolveParameter(String ref) { - String name = stripPrefix(ref, "#/components/parameters/"); - Parameter p = componentParameters.get(name); + Parameter p = parameterRefIndex.get(ref); if (p == null) { throw new IllegalArgumentException("unknown parameter ref: " + ref); } diff --git a/src/test/java/com/retailsvc/http/internal/RequestPreparationFilterTest.java b/src/test/java/com/retailsvc/http/internal/RequestPreparationFilterTest.java index f119d15..a4b888b 100644 --- a/src/test/java/com/retailsvc/http/internal/RequestPreparationFilterTest.java +++ b/src/test/java/com/retailsvc/http/internal/RequestPreparationFilterTest.java @@ -50,7 +50,9 @@ private Spec specWith(Operation... ops) { List.of(ops), Map.of(), Map.of(), - ""); + "", + Map.of(), + Map.of()); } private Filter newFilter(Spec spec) { From 7889d9230cbbcc6e208715ba43bd47dcc0fd6b15 Mon Sep 17 00:00:00 2001 From: Thomas Cederholm Date: Fri, 8 May 2026 12:17:29 +0200 Subject: [PATCH 5/8] perf: Skip parseQuery when no QUERY parameters (W4) validateParameters previously allocated a HashMap and ran String.split on every request, even when the operation declared no query parameters (the common case). Defer the parse to the first QUERY parameter encountered; routes without query params now skip the work entirely. k6 throughput +3.3% (44.1k -> 45.5k rps), p95 -22 us. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../retailsvc/http/internal/RequestPreparationFilter.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/retailsvc/http/internal/RequestPreparationFilter.java b/src/main/java/com/retailsvc/http/internal/RequestPreparationFilter.java index 52c8955..dd5abe5 100644 --- a/src/main/java/com/retailsvc/http/internal/RequestPreparationFilter.java +++ b/src/main/java/com/retailsvc/http/internal/RequestPreparationFilter.java @@ -100,9 +100,12 @@ private String stripBasePath(String path) { private void validateParameters( HttpExchange exchange, Operation op, Map pathParams) { - Map query = parseQuery(exchange.getRequestURI().getQuery()); + Map query = null; for (Parameter p : op.parameters()) { String pointer = "/" + p.in().name().toLowerCase(Locale.ROOT) + "/" + p.name(); + if (p.in() == Parameter.Location.QUERY && query == null) { + query = parseQuery(exchange.getRequestURI().getQuery()); + } String value = switch (p.in()) { case PATH -> pathParams.get(p.name()); From eb6c331cf78ec0b540e9a2b1d95bf84efd564937 Mon Sep 17 00:00:00 2001 From: Thomas Cederholm Date: Fri, 8 May 2026 12:19:45 +0200 Subject: [PATCH 6/8] perf: Precompute Parameter JSON-pointer at parse time (W5) The validation pointer "//" was rebuilt with StringBuilder on every request per parameter, even though it's spec-static. Promote it to a record component computed once in the convenience constructor and read it in RequestPreparationFilter.validateParameters. k6 throughput +4.6% vs baseline (44.2k -> 46.2k rps), p95 -83 us. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../retailsvc/http/internal/RequestPreparationFilter.java | 2 +- src/main/java/com/retailsvc/http/spec/Parameter.java | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/retailsvc/http/internal/RequestPreparationFilter.java b/src/main/java/com/retailsvc/http/internal/RequestPreparationFilter.java index dd5abe5..93ae30a 100644 --- a/src/main/java/com/retailsvc/http/internal/RequestPreparationFilter.java +++ b/src/main/java/com/retailsvc/http/internal/RequestPreparationFilter.java @@ -102,7 +102,7 @@ private void validateParameters( HttpExchange exchange, Operation op, Map pathParams) { Map query = null; for (Parameter p : op.parameters()) { - String pointer = "/" + p.in().name().toLowerCase(Locale.ROOT) + "/" + p.name(); + String pointer = p.pointer(); if (p.in() == Parameter.Location.QUERY && query == null) { query = parseQuery(exchange.getRequestURI().getQuery()); } diff --git a/src/main/java/com/retailsvc/http/spec/Parameter.java b/src/main/java/com/retailsvc/http/spec/Parameter.java index 82f175a..3fba8c9 100644 --- a/src/main/java/com/retailsvc/http/spec/Parameter.java +++ b/src/main/java/com/retailsvc/http/spec/Parameter.java @@ -1,8 +1,14 @@ package com.retailsvc.http.spec; import com.retailsvc.http.spec.schema.Schema; +import java.util.Locale; + +public record Parameter(String name, Location in, boolean required, Schema schema, String pointer) { + + public Parameter(String name, Location in, boolean required, Schema schema) { + this(name, in, required, schema, "/" + in.name().toLowerCase(Locale.ROOT) + "/" + name); + } -public record Parameter(String name, Location in, boolean required, Schema schema) { public enum Location { PATH, QUERY, From 4b7e32ade08020d763fd4424c8a1b3dd524c13fa Mon Sep 17 00:00:00 2001 From: Thomas Cederholm Date: Fri, 8 May 2026 12:22:13 +0200 Subject: [PATCH 7/8] perf: Add Request.current() escape hatch (W7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each Request.bytes()/parsed()/operationId()/pathParams() call walks the JDK scope chain independently. Document and expose Request.current() so handlers that read more than one field can hoist the lookup. Pure addition — no hot-path callers in the library or example code, so no measured throughput delta on the k6 baseline. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/main/java/com/retailsvc/http/Request.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/java/com/retailsvc/http/Request.java b/src/main/java/com/retailsvc/http/Request.java index 4d42e92..2df6ce9 100644 --- a/src/main/java/com/retailsvc/http/Request.java +++ b/src/main/java/com/retailsvc/http/Request.java @@ -21,6 +21,15 @@ public final class Request { private Request() {} + /** + * Returns the full per-request context. Use this when a handler reads more than one field — every + * call to {@link #bytes()}, {@link #parsed()}, {@link #operationId()}, or {@link #pathParams()} + * walks the JDK's scope chain independently, so reading via {@code current()} once is cheaper. + */ + public static RequestContext current() { + return CONTEXT.get(); + } + public static byte[] bytes() { return CONTEXT.get().body(); } From 6afa1977711882c4a6ea9933a9a465e45f65ef39 Mon Sep 17 00:00:00 2001 From: Thomas Cederholm Date: Fri, 8 May 2026 13:17:11 +0200 Subject: [PATCH 8/8] fix: Address SonarQube findings in Spec Use LinkedHashMap.newLinkedHashMap(int) for properly-sized init and reuse the PARAMETER_REF_PREFIX constant in resolveParameterOrParse instead of duplicating the literal. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/main/java/com/retailsvc/http/spec/Spec.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/retailsvc/http/spec/Spec.java b/src/main/java/com/retailsvc/http/spec/Spec.java index 2f28898..516dfbf 100644 --- a/src/main/java/com/retailsvc/http/spec/Spec.java +++ b/src/main/java/com/retailsvc/http/spec/Spec.java @@ -57,7 +57,7 @@ private static String computeBasePath(List servers) { } private static Map indexByRef(Map components, String prefix) { - Map out = new LinkedHashMap<>(components.size()); + Map out = LinkedHashMap.newLinkedHashMap(components.size()); for (var e : components.entrySet()) { out.put(prefix + e.getKey(), e.getValue()); } @@ -175,7 +175,7 @@ private static Parameter resolveParameterOrParse( Map raw, Map componentParameters) { String ref = (String) raw.get("$ref"); if (ref != null) { - String name = stripPrefix(ref, "#/components/parameters/"); + String name = stripPrefix(ref, PARAMETER_REF_PREFIX); Parameter p = componentParameters.get(name); if (p == null) { throw new IllegalArgumentException("unknown parameter ref: " + ref);