feat(ext-openapi): a pattern says in words what its regex says in symbols
A schema's pattern is precise and unreadable. When the constraint declares a message, that message now goes in the property's description too, after whatever the property already said, so the document carries both the rule a machine checks and the sentence a person reads. Only @Pattern does this. Every other constraint has a keyword that already reads: required, maxLength, format: email. Repeating those as prose would be noise. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
5ece97ca7b
commit
5164f8c41f
+23
-1
@@ -64,7 +64,10 @@ final class ConstraintHints {
|
||||
if (field.isAnnotationPresent(Email.class)) property.putIfAbsent("format", "email");
|
||||
|
||||
Pattern pattern = field.getAnnotation(Pattern.class);
|
||||
if (pattern != null) property.putIfAbsent("pattern", pattern.regexp());
|
||||
if (pattern != null) {
|
||||
property.putIfAbsent("pattern", pattern.regexp());
|
||||
describe(property, pattern.message());
|
||||
}
|
||||
|
||||
if (field.isAnnotationPresent(NotBlank.class) && isString) property.putIfAbsent("minLength", 1);
|
||||
if (field.isAnnotationPresent(NotEmpty.class)) {
|
||||
@@ -78,6 +81,25 @@ final class ConstraintHints {
|
||||
}
|
||||
|
||||
/** Constraint annotations this bridge understands, for documentation and tests. */
|
||||
/**
|
||||
* Says in words what a regex says in symbols.
|
||||
*
|
||||
* <p>Only {@code @Pattern} needs this: every other constraint has a keyword a reader
|
||||
* understands — {@code required}, {@code maxLength}, {@code format: email} — and repeating it
|
||||
* as prose would be noise. A regex has none, so the rule's own message goes in the
|
||||
* description beside it, after whatever the property already said.
|
||||
*/
|
||||
private static void describe(Map<String, Object> property, String message) {
|
||||
if (message == null || message.isBlank() || message.startsWith("{")) return; // jakarta's default is a bundle key
|
||||
|
||||
String sentence = Character.toUpperCase(message.charAt(0)) + message.substring(1);
|
||||
if (!sentence.endsWith(".")) sentence += ".";
|
||||
|
||||
Object said = property.get("description");
|
||||
if (said == null) property.put("description", sentence);
|
||||
else if (!said.toString().contains(sentence)) property.put("description", said + " " + sentence);
|
||||
}
|
||||
|
||||
static List<String> supported() {
|
||||
return List.of("@NotNull", "@NotBlank", "@NotEmpty", "@Size", "@Min", "@Max", "@Email", "@Pattern");
|
||||
}
|
||||
|
||||
+34
@@ -226,6 +226,40 @@ class OpenApiBuilderTest {
|
||||
@Override public Object handle(Request request, Response response) { return null; }
|
||||
}
|
||||
|
||||
@Schema(name = "Keyed")
|
||||
static class KeyedDto {
|
||||
@SchemaProperty(description = "Unique in the project.")
|
||||
@jakarta.validation.constraints.Pattern(regexp = "[a-z.]+", message = "uses lowercase letters and dots")
|
||||
public String key;
|
||||
|
||||
@jakarta.validation.constraints.Pattern(regexp = "[0-9]+")
|
||||
public String code;
|
||||
}
|
||||
|
||||
@GET("/keyed")
|
||||
@ApiOperation(summary = "Keyed")
|
||||
@APIResponse(responseCode = "200", content = @Content(schema = KeyedDto.class))
|
||||
static class KeyedHandler extends RequestHandler {
|
||||
@Override public Object handle(Request request, Response response) { return null; }
|
||||
}
|
||||
|
||||
@Test
|
||||
void a_pattern_says_in_words_what_its_regex_says_in_symbols() {
|
||||
OpenApiBuilder b = new OpenApiBuilder();
|
||||
b.addOperation(OpenApiBuilder.routeOf(KeyedHandler.class), KeyedHandler.class.getAnnotation(ApiOperation.class), KeyedHandler.class);
|
||||
|
||||
Map<String, Object> components = cast(b.build().get("components"));
|
||||
Map<String, Object> schemas = cast(components.get("schemas"));
|
||||
Map<String, Object> keyed = cast(schemas.get("Keyed"));
|
||||
Map<String, Object> properties = cast(keyed.get("properties"));
|
||||
Map<String, Object> key = cast(properties.get("key"));
|
||||
Map<String, Object> code = cast(properties.get("code"));
|
||||
|
||||
assertEquals("Unique in the project. Uses lowercase letters and dots.", key.get("description"));
|
||||
assertEquals("[a-z.]+", key.get("pattern"));
|
||||
assertFalse(code.containsKey("description"), "a pattern with no message of its own says nothing extra");
|
||||
}
|
||||
|
||||
@Test
|
||||
void a_route_that_says_it_is_not_part_of_the_api_is_left_out() {
|
||||
OpenApiBuilder b = new OpenApiBuilder();
|
||||
|
||||
Reference in New Issue
Block a user