Skip to content

Commit 3e09075

Browse files
committed
feat: Add HealthRenderer for health-response JSON
1 parent 5fe3b7d commit 3e09075

2 files changed

Lines changed: 129 additions & 0 deletions

File tree

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package com.retailsvc.http.internal;
2+
3+
import com.retailsvc.http.Dependency;
4+
import com.retailsvc.http.HealthOutcome;
5+
import java.util.List;
6+
7+
/**
8+
* Hand-rolled JSON renderer for {@link HealthOutcome} responses.
9+
*
10+
* <p>Mirrors {@link ProblemDetailRenderer} — the library avoids pulling in a JSON writer for a
11+
* handful of fixed fields with known shapes.
12+
*/
13+
public final class HealthRenderer {
14+
15+
/** Initial capacity sized for a typical health document with a handful of dependencies. */
16+
private static final int INITIAL_BUFFER_CAPACITY = 128;
17+
18+
/** Codepoints below this value are control characters and must be unicode-escaped in JSON. */
19+
private static final int FIRST_PRINTABLE_ASCII = 0x20;
20+
21+
private HealthRenderer() {}
22+
23+
public static String toJson(HealthOutcome outcome) {
24+
StringBuilder out = new StringBuilder(INITIAL_BUFFER_CAPACITY);
25+
out.append('{');
26+
appendStringField(out, "outcome", outcome.outcome());
27+
out.append(",\"dependencies\":[");
28+
appendDependencies(out, outcome.dependencies());
29+
out.append("]}");
30+
return out.toString();
31+
}
32+
33+
private static void appendDependencies(StringBuilder out, List<Dependency> deps) {
34+
for (int i = 0; i < deps.size(); i++) {
35+
if (i > 0) {
36+
out.append(',');
37+
}
38+
Dependency d = deps.get(i);
39+
out.append('{');
40+
appendStringField(out, "id", d.id());
41+
out.append(',');
42+
appendStringField(out, "status", d.status());
43+
out.append('}');
44+
}
45+
}
46+
47+
private static void appendStringField(StringBuilder out, String name, String value) {
48+
out.append('"').append(name).append("\":\"");
49+
appendEscaped(out, value);
50+
out.append('"');
51+
}
52+
53+
private static void appendEscaped(StringBuilder out, String value) {
54+
for (int i = 0; i < value.length(); i++) {
55+
char c = value.charAt(i);
56+
switch (c) {
57+
case '\\' -> out.append("\\\\");
58+
case '"' -> out.append("\\\"");
59+
case '\n' -> out.append("\\n");
60+
case '\r' -> out.append("\\r");
61+
case '\t' -> out.append("\\t");
62+
case '\b' -> out.append("\\b");
63+
case '\f' -> out.append("\\f");
64+
default -> appendUnicodeOrLiteral(out, c);
65+
}
66+
}
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+
}
75+
}
76+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package com.retailsvc.http.internal;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import com.fasterxml.jackson.databind.JsonNode;
6+
import com.fasterxml.jackson.databind.ObjectMapper;
7+
import com.retailsvc.http.Dependency;
8+
import com.retailsvc.http.HealthOutcome;
9+
import java.util.List;
10+
import org.junit.jupiter.api.Test;
11+
12+
class HealthRendererTest {
13+
14+
private static final ObjectMapper MAPPER = new ObjectMapper();
15+
16+
@Test
17+
void rendersOutcomeAndEmptyDependencies() {
18+
String json = HealthRenderer.toJson(new HealthOutcome("Up", List.of()));
19+
assertThat(json).isEqualTo("{\"outcome\":\"Up\",\"dependencies\":[]}");
20+
}
21+
22+
@Test
23+
void rendersOutcomeAndDependencies() throws Exception {
24+
String json =
25+
HealthRenderer.toJson(
26+
new HealthOutcome(
27+
"Down", List.of(new Dependency("jdbc", "Down"), new Dependency("cache", "Up"))));
28+
29+
JsonNode root = MAPPER.readTree(json);
30+
assertThat(root.get("outcome").asText()).isEqualTo("Down");
31+
assertThat(root.get("dependencies")).hasSize(2);
32+
assertThat(root.get("dependencies").get(0).get("id").asText()).isEqualTo("jdbc");
33+
assertThat(root.get("dependencies").get(0).get("status").asText()).isEqualTo("Down");
34+
assertThat(root.get("dependencies").get(1).get("id").asText()).isEqualTo("cache");
35+
assertThat(root.get("dependencies").get(1).get("status").asText()).isEqualTo("Up");
36+
}
37+
38+
@Test
39+
void escapesQuotesAndBackslashes() throws Exception {
40+
String json =
41+
HealthRenderer.toJson(new HealthOutcome("Up", List.of(new Dependency("a\"b\\c", "Up"))));
42+
JsonNode root = MAPPER.readTree(json);
43+
assertThat(root.get("dependencies").get(0).get("id").asText()).isEqualTo("a\"b\\c");
44+
}
45+
46+
@Test
47+
void escapesControlCharacters() throws Exception {
48+
String id = "tab\there\nnextend";
49+
String json = HealthRenderer.toJson(new HealthOutcome("Up", List.of(new Dependency(id, "Up"))));
50+
JsonNode root = MAPPER.readTree(json);
51+
assertThat(root.get("dependencies").get(0).get("id").asText()).isEqualTo(id);
52+
}
53+
}

0 commit comments

Comments
 (0)