diff --git a/problem/src/main/java/org/zalando/problem/Problem.java b/problem/src/main/java/org/zalando/problem/Problem.java index 7e8fbe43..36dc1667 100644 --- a/problem/src/main/java/org/zalando/problem/Problem.java +++ b/problem/src/main/java/org/zalando/problem/Problem.java @@ -92,6 +92,28 @@ static ProblemBuilder builder() { return new ProblemBuilder(); } + /** + * Creates a new {@link ProblemBuilder} pre-populated with all fields from this problem instance. + * This allows you to clone a problem and add or override fields while retaining the original + * standard fields and any custom parameters. + * + *

Note: type-specific fields from subclasses (e.g. {@code violations} on + * {@code ConstraintViolationProblem}) that are not exposed via {@link #getParameters()} + * will not be carried over.

+ * + * @return a pre-populated builder based on this problem + */ + default ProblemBuilder toBuilder() { + final ProblemBuilder builder = Problem.builder() + .withType(getType()) + .withTitle(getTitle()) + .withStatus(getStatus()) + .withDetail(getDetail()) + .withInstance(getInstance()); + getParameters().forEach(builder::with); + return builder; + } + static ThrowableProblem valueOf(final StatusType status) { return GenericProblems.create(status).build(); } diff --git a/problem/src/test/java/org/zalando/problem/ProblemBuilderTest.java b/problem/src/test/java/org/zalando/problem/ProblemBuilderTest.java index 12489538..0bf08268 100644 --- a/problem/src/test/java/org/zalando/problem/ProblemBuilderTest.java +++ b/problem/src/test/java/org/zalando/problem/ProblemBuilderTest.java @@ -144,4 +144,84 @@ void shouldThrowOnCustomCause() { assertThrows(IllegalArgumentException.class, () -> Problem.builder().with("cause", "foo")); } + @Test + void shouldCreateBuilderFromExistingProblem() { + final Problem original = Problem.builder() + .withType(type) + .withTitle("Out of Stock") + .withStatus(BAD_REQUEST) + .withDetail("Item B00027Y5QG is no longer available") + .withInstance(URI.create("https://example.com/")) + .build(); + + final Problem copy = original.toBuilder().build(); + + assertThat(copy, hasFeature("type", Problem::getType, is(type))); + assertThat(copy, hasFeature("title", Problem::getTitle, is("Out of Stock"))); + assertThat(copy, hasFeature("status", Problem::getStatus, is(BAD_REQUEST))); + assertThat(copy, hasFeature("detail", Problem::getDetail, is("Item B00027Y5QG is no longer available"))); + assertThat(copy, hasFeature("instance", Problem::getInstance, is(URI.create("https://example.com/")))); + } + + @Test + void shouldCopyParametersFromExistingProblem() { + final Problem original = Problem.builder() + .withType(type) + .withStatus(BAD_REQUEST) + .with("traceId", "abc-123") + .with("region", "eu-west-1") + .build(); + + final Problem copy = original.toBuilder().build(); + + assertThat(copy.getParameters(), hasEntry("traceId", "abc-123")); + assertThat(copy.getParameters(), hasEntry("region", "eu-west-1")); + } + + @Test + void shouldAllowAddingNewParametersViaToBuilder() { + final Problem original = Problem.builder() + .withType(type) + .withStatus(BAD_REQUEST) + .with("existingKey", "existingValue") + .build(); + + // primary use case — add a trace ID to any problem response + final Problem enriched = original.toBuilder() + .with("traceId", "xyz-789") + .build(); + + assertThat(enriched.getParameters(), hasEntry("existingKey", "existingValue")); + assertThat(enriched.getParameters(), hasEntry("traceId", "xyz-789")); + } + + @Test + void shouldAllowOverridingFieldsViaToBuilder() { + final Problem original = Problem.builder() + .withType(type) + .withStatus(BAD_REQUEST) + .withDetail("original detail") + .build(); + + final Problem modified = original.toBuilder() + .withDetail("updated detail") + .build(); + + assertThat(modified, hasFeature("detail", Problem::getDetail, is("updated detail"))); + assertThat(modified, hasFeature("type", Problem::getType, is(type))); + assertThat(modified, hasFeature("status", Problem::getStatus, is(BAD_REQUEST))); + } + + @Test + void toBuilderOnEmptyProblemShouldProduceEquivalentProblem() { + final Problem original = Problem.builder().build(); + final Problem copy = original.toBuilder().build(); + + assertThat(copy, hasFeature("type", Problem::getType, hasToString("about:blank"))); + assertThat(copy, hasFeature("title", Problem::getTitle, is(nullValue()))); + assertThat(copy, hasFeature("status", Problem::getStatus, is(nullValue()))); + assertThat(copy, hasFeature("detail", Problem::getDetail, is(nullValue()))); + assertThat(copy, hasFeature("instance", Problem::getInstance, is(nullValue()))); + } + }