Skip to content

Commit a027d62

Browse files
Add toString to builder
It probably won't be used anyway, but it will be usefull in debug mode.
1 parent 0c67868 commit a027d62

5 files changed

Lines changed: 177 additions & 26 deletions

File tree

problem4j-core/src/main/java/io/github/problem4j/core/AbstractProblem.java

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
*/
2121
package io.github.problem4j.core;
2222

23+
import static io.github.problem4j.core.JsonEscape.escape;
24+
2325
import java.io.Serializable;
2426
import java.net.URI;
2527
import java.util.ArrayList;
@@ -209,17 +211,17 @@ public int hashCode() {
209211
public String toString() {
210212
List<String> lines = new ArrayList<>();
211213
if (getType() != null) {
212-
lines.add("\"type\" : \"" + quote(getType().toString()) + "\"");
214+
lines.add("\"type\" : \"" + escape(getType().toString()) + "\"");
213215
}
214216
if (getTitle() != null) {
215-
lines.add("\"title\" : \"" + quote(getTitle()) + "\"");
217+
lines.add("\"title\" : \"" + escape(getTitle()) + "\"");
216218
}
217219
lines.add("\"status\" : " + getStatus());
218220
if (getDetail() != null) {
219-
lines.add("\"detail\" : \"" + quote(getDetail()) + "\"");
221+
lines.add("\"detail\" : \"" + escape(getDetail()) + "\"");
220222
}
221223
if (getInstance() != null) {
222-
lines.add("\"instance\" : \"" + quote(getInstance().toString()) + "\"");
224+
lines.add("\"instance\" : \"" + escape(getInstance().toString()) + "\"");
223225
}
224226

225227
getExtensionMembers()
@@ -230,7 +232,7 @@ public String toString() {
230232
}
231233

232234
if (value instanceof String) {
233-
lines.add("\"" + field + "\" : \"" + quote((String) value) + "\"");
235+
lines.add("\"" + field + "\" : \"" + escape((String) value) + "\"");
234236
} else if (value instanceof Number || value instanceof Boolean) {
235237
lines.add("\"" + field + "\" : " + value);
236238
} else {
@@ -241,13 +243,9 @@ public String toString() {
241243
return lines.stream().collect(Collectors.joining(", ", "{ ", " }"));
242244
}
243245

244-
private static String quote(String string) {
245-
return JsonEscape.escape(string);
246-
}
247-
248246
private String getObjectLine(String field, Object value) {
249247
String className = value.getClass().getSimpleName();
250-
return "\"" + field + "\" : \"" + className + ":" + quote(value.toString()) + "\"";
248+
return "\"" + field + "\" : \"" + className + ":" + escape(value.toString()) + "\"";
251249
}
252250

253251
/** Represents a single key-value extension in a {@link Problem}. */
@@ -321,9 +319,9 @@ public String toString() {
321319
if (getValue() instanceof Number || getValue() instanceof Boolean) {
322320
valueLine = "\"value\" : " + getValue();
323321
} else {
324-
valueLine = "\"value\" : " + "\"" + quote(getValue().toString()) + "\"";
322+
valueLine = "\"value\" : " + "\"" + escape(getValue().toString()) + "\"";
325323
}
326-
return "{ \"key\" : \"" + quote(getKey()) + "\", " + valueLine + " }";
324+
return "{ \"key\" : \"" + escape(getKey()) + "\", " + valueLine + " }";
327325
}
328326
}
329327
}

problem4j-core/src/main/java/io/github/problem4j/core/AbstractProblemBuilder.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,17 @@
2020
*/
2121
package io.github.problem4j.core;
2222

23+
import static io.github.problem4j.core.JsonEscape.escape;
24+
2325
import java.io.Serializable;
2426
import java.net.URI;
27+
import java.util.ArrayList;
2528
import java.util.Collection;
2629
import java.util.HashMap;
30+
import java.util.List;
2731
import java.util.Map;
2832
import java.util.Optional;
33+
import java.util.stream.Collectors;
2934
import java.util.stream.Stream;
3035

3136
/**
@@ -267,7 +272,47 @@ public Problem build() {
267272
return new ProblemImpl(type, title, status, detail, instance, extensions);
268273
}
269274

275+
@Override
276+
public String toString() {
277+
List<String> lines = new ArrayList<>();
278+
if (type != null) {
279+
lines.add("\"type\" : \"" + escape(type.toString()) + "\"");
280+
}
281+
if (title != null) {
282+
lines.add("\"title\" : \"" + escape(title) + "\"");
283+
}
284+
lines.add("\"status\" : " + status);
285+
if (detail != null) {
286+
lines.add("\"detail\" : \"" + escape(detail) + "\"");
287+
}
288+
if (instance != null) {
289+
lines.add("\"instance\" : \"" + escape(instance.toString()) + "\"");
290+
}
291+
292+
extensions.forEach(
293+
(field, value) -> {
294+
if (value == null) {
295+
return;
296+
}
297+
298+
if (value instanceof String) {
299+
lines.add("\"" + field + "\" : \"" + escape((String) value) + "\"");
300+
} else if (value instanceof Number || value instanceof Boolean) {
301+
lines.add("\"" + field + "\" : " + value);
302+
} else {
303+
lines.add(getObjectLine(field, value));
304+
}
305+
});
306+
307+
return lines.stream().collect(Collectors.joining(", ", "{ ", " }"));
308+
}
309+
270310
private static boolean isExtensionValid(Problem.Extension extension) {
271311
return extension != null && extension.getKey() != null && extension.getValue() != null;
272312
}
313+
314+
private String getObjectLine(String field, Object value) {
315+
String className = value.getClass().getSimpleName();
316+
return "\"" + field + "\" : \"" + className + ":" + escape(value.toString()) + "\"";
317+
}
273318
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright (c) 2025 Damian Malczewski
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+
package io.github.problem4j.core;
22+
23+
/**
24+
* Simple object used for testing situations where extension is not a primitive nor simple object.
25+
*/
26+
class DummyObject {
27+
28+
private final String value;
29+
30+
DummyObject(String value) {
31+
this.value = value;
32+
}
33+
34+
@Override
35+
public String toString() {
36+
return value;
37+
}
38+
}

problem4j-core/src/test/java/io/github/problem4j/core/ProblemBuilderTest.java

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,4 +312,88 @@ void givenAssigningTheSameExtensionLater_shouldOverwriteEarlierValues() {
312312
assertThat(problem.getExtensions()).containsExactly("k");
313313
assertThat(problem.getExtensionMembers()).isEqualTo(mapOf("k", "v2"));
314314
}
315+
316+
@Test
317+
void givenAllFieldsPopulated_whenToString_thenContainsAllFieldsProperly() {
318+
URI type = URI.create("https://example.com/problem");
319+
String title = "Test Problem";
320+
int status = 400;
321+
String detail = "Something went wrong";
322+
URI instance = URI.create("https://example.com/instance");
323+
Map<String, Object> extensions = new HashMap<>();
324+
extensions.put("stringExt", "value");
325+
extensions.put("numberExt", 42);
326+
extensions.put("booleanExt", true);
327+
extensions.put("objectExt", new DummyObject("foo"));
328+
329+
ProblemBuilder builder =
330+
Problem.builder().type(type).title(title).status(status).detail(detail).instance(instance);
331+
extensions.forEach(builder::extension);
332+
333+
String result = builder.toString();
334+
335+
assertThat(result).contains("\"type\" : \"" + JsonEscape.escape(type.toString()) + "\"");
336+
assertThat(result).contains("\"title\" : \"" + JsonEscape.escape(title) + "\"");
337+
assertThat(result).contains("\"status\" : " + status);
338+
assertThat(result).contains("\"detail\" : \"" + JsonEscape.escape(detail) + "\"");
339+
assertThat(result)
340+
.contains("\"instance\" : \"" + JsonEscape.escape(instance.toString()) + "\"");
341+
assertThat(result).contains("\"stringExt\" : \"" + JsonEscape.escape("value") + "\"");
342+
assertThat(result).contains("\"numberExt\" : 42");
343+
assertThat(result).contains("\"booleanExt\" : true");
344+
assertThat(result).contains("\"objectExt\" : \"DummyObject:" + JsonEscape.escape("foo") + "\"");
345+
}
346+
347+
@Test
348+
void givenNullExtensionsAndNullableFields_whenToString_thenOmitsNulls() {
349+
ProblemBuilder builder = Problem.builder().status(200);
350+
String result = builder.toString();
351+
assertThat(result).isEqualTo("{ \"status\" : 200 }");
352+
}
353+
354+
@Test
355+
void givenOnlyStringExtensions_whenToString_thenContainsQuotedStrings() {
356+
ProblemBuilder builder = Problem.builder();
357+
builder.extension("ext1", "value1");
358+
builder.extension("ext2", "value2");
359+
String result = builder.toString();
360+
assertThat(result).contains("\"ext1\" : \"" + JsonEscape.escape("value1") + "\"");
361+
assertThat(result).contains("\"ext2\" : \"" + JsonEscape.escape("value2") + "\"");
362+
}
363+
364+
@Test
365+
void givenOnlyNumberExtensions_whenToString_thenContainsNumbers() {
366+
ProblemBuilder builder = Problem.builder();
367+
builder.extension("ext1", 123);
368+
builder.extension("ext2", 456.78);
369+
String result = builder.toString();
370+
assertThat(result).contains("\"ext1\" : 123");
371+
assertThat(result).contains("\"ext2\" : 456.78");
372+
}
373+
374+
@Test
375+
void givenOnlyBooleanExtensions_whenToString_thenContainsBooleans() {
376+
ProblemBuilder builder = Problem.builder();
377+
builder.extension("flag1", true);
378+
builder.extension("flag2", false);
379+
String result = builder.toString();
380+
assertThat(result).contains("\"flag1\" : true");
381+
assertThat(result).contains("\"flag2\" : false");
382+
}
383+
384+
@Test
385+
void givenNonPrimitiveExtension_whenToString_thenUsesClassNamePrefix() {
386+
ProblemBuilder builder = Problem.builder();
387+
builder.extension("obj", new DummyObject("bar"));
388+
String result = builder.toString();
389+
assertThat(result).contains("\"obj\" : \"DummyObject:" + JsonEscape.escape("bar") + "\"");
390+
}
391+
392+
@Test
393+
void givenStringWithSpecialCharacters_whenToString_thenProperlyEscapes() {
394+
ProblemBuilder builder = Problem.builder();
395+
builder.extension("ext", "a\"b\\c\nd");
396+
String result = builder.toString();
397+
assertThat(result).contains("\"ext\" : \"" + JsonEscape.escape("a\"b\\c\nd") + "\"");
398+
}
315399
}

problem4j-core/src/test/java/io/github/problem4j/core/ProblemImplTest.java

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -244,18 +244,4 @@ void givenTwoDifferentExtensions_shouldHaveDifferentHashCodes() {
244244

245245
assertThat(ext1.hashCode()).isNotEqualTo(ext2.hashCode());
246246
}
247-
248-
private static class DummyObject {
249-
250-
private final String value;
251-
252-
DummyObject(String value) {
253-
this.value = value;
254-
}
255-
256-
@Override
257-
public String toString() {
258-
return value;
259-
}
260-
}
261247
}

0 commit comments

Comments
 (0)