Skip to content

Commit cc72e2e

Browse files
committed
Merge branch 'release-v1.2.x'
2 parents cdf4a8b + 26f71bc commit cc72e2e

13 files changed

Lines changed: 241 additions & 14 deletions

File tree

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ Spring Boot integrations for RFC 7807 "Problem Details" - consistent error respo
88
| Module | Purpose | Entry point |
99
|----------------------------|------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
1010
| `problem4j-spring-bom` | BOM for dependency alignment | - |
11-
| `problem4j-spring-web` | Core Spring integration | [`ProblemAutoConfiguration`](../problem4j-spring-web/src/main/java/io/github/problem4j/spring/web/autoconfigure/ProblemAutoConfiguration.java) |
12-
| `problem4j-spring-webmvc` | WebMVC overrides | [`ProblemWebMvcAutoConfiguration`](../problem4j-spring-webmvc/src/main/java/io/github/problem4j/spring/webmvc/autoconfigure/ProblemWebMvcAutoConfiguration.java) |
13-
| `problem4j-spring-webflux` | WebFlux overrides | [`ProblemWebFluxAutoConfiguration`](../problem4j-spring-webflux/src/main/java/io/github/problem4j/spring/webflux/autoconfigure/ProblemWebFluxAutoConfiguration.java) |
11+
| `problem4j-spring-web` | Core Spring integration | [`ProblemAutoConfiguration`](problem4j-spring-web/src/main/java/io/github/problem4j/spring/web/autoconfigure/ProblemAutoConfiguration.java) |
12+
| `problem4j-spring-webmvc` | WebMVC overrides | [`ProblemWebMvcAutoConfiguration`](problem4j-spring-webmvc/src/main/java/io/github/problem4j/spring/webmvc/autoconfigure/ProblemWebMvcAutoConfiguration.java) |
13+
| `problem4j-spring-webflux` | WebFlux overrides | [`ProblemWebFluxAutoConfiguration`](problem4j-spring-webflux/src/main/java/io/github/problem4j/spring/webflux/autoconfigure/ProblemWebFluxAutoConfiguration.java) |
1414

1515
Prioritize changes in the module matching the Spring integration in context.
1616

@@ -30,6 +30,9 @@ Always validate changes with a full build and test run before considering the ta
3030
## Agent Rules
3131

3232
- Do not use terminal commands (e.g., `cat`, `find`, `ls`) to read or list project files - use IDE/agent tools instead.
33+
- Run tests once, save output to `build/test-run.log` inside the repo (`> build/test-run.log 2>&1`), then read from that
34+
file to extract errors. Never run the same test command multiple times, without changes in sources. Store test output
35+
in multiple files if you want to compare before/after changes (ex. `build/test-run-{i}.log`).
3336

3437
## Coding Rules
3538

@@ -46,6 +49,3 @@ Always validate changes with a full build and test run before considering the ta
4649
- Use AssertJ for assertions.
4750
- Integration tests in `problem4j-spring-webflux` and `problem4j-spring-webmvc` (`...integration` package) should be
4851
similar - both modules must resolve the same exceptions to the same response bodies.
49-
- If executing, run tests once, save output to `build/test-run.log` inside the repo (`> build/test-run.log 2>&1`), then
50-
read from that file to extract errors. Never run the same test command multiple times, without changes in sources. You
51-
can store test output in multiple files if you want to compare before/after changes (ex. `build/test-run-{i}.log`).

build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import internal.getBooleanProperty
33

44
plugins {
55
id("internal.common-convention")
6+
id("internal.idea-convention")
67
id("jacoco-report-aggregation")
78
alias(libs.plugins.nmcp).apply(false)
89
alias(libs.plugins.nmcp.aggregation)

buildSrc/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ repositories {
1414

1515
dependencies {
1616
implementation(plugin(libs.plugins.errorprone))
17+
implementation(plugin(libs.plugins.idea.ext))
1718
implementation(plugin(libs.plugins.kotlin.jvm))
1819
}
1920

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import org.jetbrains.gradle.ext.Application
2+
import org.jetbrains.gradle.ext.Gradle
3+
import org.jetbrains.gradle.ext.JUnit
4+
import org.jetbrains.gradle.ext.runConfigurations
5+
import org.jetbrains.gradle.ext.settings
6+
7+
plugins {
8+
id("org.jetbrains.gradle.plugin.idea-ext")
9+
}
10+
11+
idea {
12+
project {
13+
settings {
14+
runConfigurations {
15+
create<Gradle>("Clean [problem4j-spring]") {
16+
taskNames = listOf("clean")
17+
projectPath = rootProject.rootDir.absolutePath
18+
}
19+
create<Gradle>("Build [problem4j-spring]") {
20+
taskNames = listOf("spotlessApply build")
21+
projectPath = rootProject.rootDir.absolutePath
22+
}
23+
create<Gradle>("Format Code [problem4j-spring]") {
24+
taskNames = listOf("spotlessApply")
25+
projectPath = rootProject.rootDir.absolutePath
26+
}
27+
create<JUnit>("JUnit [problem4j-spring-web]") {
28+
moduleName = "problem4j-spring.problem4j-spring-web.test"
29+
workingDirectory = rootProject.rootDir.absolutePath
30+
packageName = "io.github.problem4j.spring.web"
31+
}
32+
create<JUnit>("JUnit [problem4j-spring-webflux]") {
33+
moduleName = "problem4j-spring.problem4j-spring-webflux.test"
34+
workingDirectory = rootProject.rootDir.absolutePath
35+
packageName = "io.github.problem4j.spring.webflux"
36+
}
37+
create<JUnit>("JUnit [problem4j-spring-webmvc]") {
38+
moduleName = "problem4j-spring.problem4j-spring-webmvc.test"
39+
workingDirectory = rootProject.rootDir.absolutePath
40+
packageName = "io.github.problem4j.spring.webmvc"
41+
}
42+
}
43+
}
44+
}
45+
}

gradle/libs.versions.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
[versions]
2-
jspecify = "1.0.0"
32
errorprone = "2.48.0"
43
errorprone-plugin = "5.1.0"
4+
idea-ext = "1.4.1"
5+
jspecify = "1.0.0"
56
kotlin = "2.3.20"
67
nmcp = "1.4.4"
78
nullaway = "0.13.1"
@@ -13,6 +14,7 @@ spring-boot = "4.0.4"
1314

1415
[plugins]
1516
errorprone = { id = "net.ltgt.errorprone", version.ref = "errorprone-plugin" }
17+
idea-ext = { id = "org.jetbrains.gradle.plugin.idea-ext", version.ref = "idea-ext" }
1618
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
1719
nmcp = { id = "com.gradleup.nmcp", version.ref = "nmcp" }
1820
nmcp-aggregation = { id = "com.gradleup.nmcp.aggregation", version.ref = "nmcp" }

problem4j-spring-web/src/main/java/io/github/problem4j/spring/web/SimpleTypeNameMapper.java

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,12 @@ public class SimpleTypeNameMapper implements TypeNameMapper {
4444
/** Common type name for all boolean types. */
4545
protected static final String BOOLEAN_TYPE = "boolean";
4646

47-
/** Common type name for string types, including {@code String} and {@code Enum} types. */
47+
/** Common type name for string types. */
4848
protected static final String STRING_TYPE = "string";
4949

50+
/** Common type name for enum types. */
51+
protected static final String ENUM_TYPE = "enum";
52+
5053
/** Common type name for array and collection types. */
5154
protected static final String ARRAY_TYPE = "array";
5255

@@ -60,7 +63,8 @@ public class SimpleTypeNameMapper implements TypeNameMapper {
6063
* <li>Decimal types ({@code double}, {@code float}, their wrapper classes and {@code
6164
* BigDecimal} ) to {@code "number"}
6265
* <li>Boolean types ({@code boolean} and {@code Boolean}) to {@code "boolean"}
63-
* <li>String type ({@code String}) and {@code Enum} types to {@code "string"}
66+
* <li>String type ({@code String}) to {@code "string"}
67+
* <li>Enum types to {@code "enum"}
6468
* <li>{@code Array} and {@code Collection} types to {@code "array"}
6569
* </ul>
6670
*
@@ -79,6 +83,8 @@ public Optional<String> map(@Nullable Class<?> type) {
7983
return Optional.of(NUMBER_TYPE);
8084
} else if (isBoolean(type)) {
8185
return Optional.of(BOOLEAN_TYPE);
86+
} else if (isEnum(type)) {
87+
return Optional.of(ENUM_TYPE);
8288
} else if (isString(type)) {
8389
return Optional.of(STRING_TYPE);
8490
} else if (isArray(type)) {
@@ -132,14 +138,23 @@ protected boolean isBoolean(Class<?> clazz) {
132138
}
133139

134140
/**
135-
* Determines if the given class represents a string type, which includes {@code String} and
136-
* {@code Enum} types.
141+
* Determines if the given class represents an enum type.
142+
*
143+
* @param type the class to check
144+
* @return {@code true} if the class is an enum type, {@code false} otherwise
145+
*/
146+
protected boolean isEnum(Class<?> type) {
147+
return type.isEnum();
148+
}
149+
150+
/**
151+
* Determines if the given class represents a string type.
137152
*
138153
* @param type the class to check
139-
* @return {@code true} if the class is a string
154+
* @return {@code true} if the class is a string type, {@code false} otherwise
140155
*/
141156
protected boolean isString(Class<?> type) {
142-
return type == String.class || type.isEnum();
157+
return type == String.class;
143158
}
144159

145160
/**

problem4j-spring-web/src/test/java/io/github/problem4j/spring/web/SimpleTypeNameMapperTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ static Stream<Arguments> supportedTypeMappings() {
8686
Arguments.of(boolean.class, "boolean"),
8787
Arguments.of(Boolean.class, "boolean"),
8888
Arguments.of(String.class, "string"),
89-
Arguments.of(TestEnum.class, "string"),
89+
Arguments.of(TestEnum.class, "enum"),
9090
Arguments.of(String[].class, "array"),
9191
Arguments.of(Collection.class, "array"),
9292
Arguments.of(List.class, "array"),
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright (c) 2025-2026 The Problem4J Authors
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a copy
5+
* of this software and associated documentation files (the "Software"), to deal
6+
* in the Software without restriction, including without limitation the rights
7+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
* copies of the Software, subject to the following conditions:
9+
*
10+
* The above copyright notice and this permission notice shall be included in all
11+
* copies or substantial portions of the Software.
12+
*
13+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19+
* SOFTWARE.
20+
*/
21+
22+
package io.github.problem4j.spring.webflux.app.model;
23+
24+
public record EnumRequest(String name, Status status) {
25+
26+
public enum Status {
27+
ACTIVE,
28+
INACTIVE,
29+
PENDING
30+
}
31+
}

problem4j-spring-webflux/src/test/java/io/github/problem4j/spring/webflux/app/rest/TypeMismatchController.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,19 @@
2121

2222
package io.github.problem4j.spring.webflux.app.rest;
2323

24+
import io.github.problem4j.spring.webflux.app.model.EnumRequest;
25+
import org.springframework.http.HttpStatus;
2426
import org.springframework.web.bind.annotation.CookieValue;
2527
import org.springframework.web.bind.annotation.GetMapping;
2628
import org.springframework.web.bind.annotation.PathVariable;
29+
import org.springframework.web.bind.annotation.PostMapping;
30+
import org.springframework.web.bind.annotation.RequestBody;
2731
import org.springframework.web.bind.annotation.RequestHeader;
2832
import org.springframework.web.bind.annotation.RequestMapping;
2933
import org.springframework.web.bind.annotation.RequestParam;
34+
import org.springframework.web.bind.annotation.ResponseStatus;
3035
import org.springframework.web.bind.annotation.RestController;
36+
import reactor.core.publisher.Mono;
3137

3238
@RestController
3339
@RequestMapping(path = "/type-mismatch")
@@ -52,4 +58,10 @@ public String requestHeader(@RequestHeader("X-Id") Integer id) {
5258
public String cookieValue(@CookieValue("id") Integer id) {
5359
return "OK";
5460
}
61+
62+
@PostMapping(path = "/request-body")
63+
@ResponseStatus(HttpStatus.OK)
64+
public Mono<Void> requestBody(@RequestBody Mono<EnumRequest> request) {
65+
return request.then();
66+
}
5567
}

problem4j-spring-webflux/src/test/java/io/github/problem4j/spring/webflux/integration/TypeMismatchWebFluxTest.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,4 +186,43 @@ void givenRequestWithValidCookieValue_shouldReturnOk() {
186186
.value(v -> assertThat(v).isNotNull())
187187
.isEqualTo("OK");
188188
}
189+
190+
@Test
191+
void givenRequestWithInvalidEnumInRequestBody_shouldReturnProblem() {
192+
String json = "{\"name\": \"Test\", \"status\": \"INVALID\"}";
193+
194+
webTestClient
195+
.post()
196+
.uri(uriBuilder -> uriBuilder.path("/type-mismatch/request-body").build())
197+
.contentType(org.springframework.http.MediaType.APPLICATION_JSON)
198+
.bodyValue(json)
199+
.exchange()
200+
.expectStatus()
201+
.isEqualTo(HttpStatus.BAD_REQUEST)
202+
.expectHeader()
203+
.contentType(Problem.CONTENT_TYPE)
204+
.expectBody(Problem.class)
205+
.value(v -> assertThat(v).isNotNull())
206+
.isEqualTo(
207+
Problem.builder()
208+
.status(HttpStatus.BAD_REQUEST.value())
209+
.detail(TYPE_MISMATCH_DETAIL)
210+
.extension(PROPERTY_EXTENSION, "status")
211+
.extension(KIND_EXTENSION, "enum")
212+
.build());
213+
}
214+
215+
@Test
216+
void givenRequestWithValidEnumInRequestBody_shouldReturnOk() {
217+
String json = "{\"name\": \"Test\", \"status\": \"ACTIVE\"}";
218+
219+
webTestClient
220+
.post()
221+
.uri(uriBuilder -> uriBuilder.path("/type-mismatch/request-body").build())
222+
.contentType(org.springframework.http.MediaType.APPLICATION_JSON)
223+
.bodyValue(json)
224+
.exchange()
225+
.expectStatus()
226+
.isEqualTo(HttpStatus.OK);
227+
}
189228
}

0 commit comments

Comments
 (0)