Skip to content

Commit 97c339d

Browse files
committed
refactor(internal): Extract ProblemDetailRenderer helpers and constants
Splits render() into per-field appenders, names magic strings/status as constants (PROBLEM_TYPE/TITLE/STATUS), pulls control-char threshold to FIRST_PRINTABLE_ASCII, adds Javadoc on the unicode-escape path.
1 parent c3cee1b commit 97c339d

1 file changed

Lines changed: 61 additions & 28 deletions

File tree

src/main/java/com/retailsvc/http/internal/ProblemDetailRenderer.java

Lines changed: 61 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,42 +2,75 @@
22

33
import com.retailsvc.http.validate.ValidationError;
44

5+
/**
6+
* Renders a {@link ValidationError} as an RFC 7807 {@code application/problem+json} document.
7+
*
8+
* <p>Hand-rolled to avoid pulling in a JSON library; only six fixed fields are emitted, all with
9+
* known shapes, so a writer-from-scratch is safer than a generic encoder.
10+
*/
511
public final class ProblemDetailRenderer {
12+
13+
private static final String PROBLEM_TYPE = "about:blank";
14+
private static final String PROBLEM_TITLE = "Bad Request";
15+
private static final int PROBLEM_STATUS = 400;
16+
17+
/** Codepoints below this value are control characters and must be unicode-escaped in JSON. */
18+
private static final int FIRST_PRINTABLE_ASCII = 0x20;
19+
620
private ProblemDetailRenderer() {}
721

822
public static String render(ValidationError error) {
9-
return "{"
10-
+ "\"type\":\"about:blank\","
11-
+ "\"title\":\"Bad Request\","
12-
+ "\"status\":400,"
13-
+ "\"detail\":\""
14-
+ escape(error.message())
15-
+ "\","
16-
+ "\"pointer\":\""
17-
+ escape(error.pointer())
18-
+ "\","
19-
+ "\"keyword\":\""
20-
+ escape(error.keyword())
21-
+ "\""
22-
+ "}";
23+
StringBuilder out = new StringBuilder(128);
24+
out.append('{');
25+
appendStringField(out, "type", PROBLEM_TYPE);
26+
out.append(',');
27+
appendStringField(out, "title", PROBLEM_TITLE);
28+
out.append(',');
29+
appendIntField(out, "status", PROBLEM_STATUS);
30+
out.append(',');
31+
appendStringField(out, "detail", error.message());
32+
out.append(',');
33+
appendStringField(out, "pointer", error.pointer());
34+
out.append(',');
35+
appendStringField(out, "keyword", error.keyword());
36+
out.append('}');
37+
return out.toString();
38+
}
39+
40+
private static void appendStringField(StringBuilder out, String name, String value) {
41+
out.append('"').append(name).append("\":\"");
42+
appendEscaped(out, value);
43+
out.append('"');
2344
}
2445

25-
private static String escape(String s) {
26-
StringBuilder b = new StringBuilder(s.length() + 8);
27-
for (int i = 0; i < s.length(); i++) {
28-
char c = s.charAt(i);
46+
private static void appendIntField(StringBuilder out, String name, int value) {
47+
out.append('"').append(name).append("\":").append(value);
48+
}
49+
50+
/**
51+
* Appends {@code value} to {@code out} with JSON-string escaping applied. Handles the six
52+
* mandatory escape sequences and emits {@code &#92;uXXXX} for control characters below {@link
53+
* #FIRST_PRINTABLE_ASCII}.
54+
*/
55+
private static void appendEscaped(StringBuilder out, String value) {
56+
for (int i = 0; i < value.length(); i++) {
57+
char c = value.charAt(i);
2958
switch (c) {
30-
case '\\' -> b.append("\\\\");
31-
case '"' -> b.append("\\\"");
32-
case '\n' -> b.append("\\n");
33-
case '\r' -> b.append("\\r");
34-
case '\t' -> b.append("\\t");
35-
default -> {
36-
if (c < 0x20) b.append(String.format("\\u%04x", (int) c));
37-
else b.append(c);
38-
}
59+
case '\\' -> out.append("\\\\");
60+
case '"' -> out.append("\\\"");
61+
case '\n' -> out.append("\\n");
62+
case '\r' -> out.append("\\r");
63+
case '\t' -> out.append("\\t");
64+
default -> appendUnicodeOrLiteral(out, c);
3965
}
4066
}
41-
return b.toString();
67+
}
68+
69+
private static void appendUnicodeOrLiteral(StringBuilder out, char c) {
70+
if (c < FIRST_PRINTABLE_ASCII) {
71+
out.append(String.format("\\u%04x", (int) c));
72+
} else {
73+
out.append(c);
74+
}
4275
}
4376
}

0 commit comments

Comments
 (0)