Skip to content

Commit 1637adf

Browse files
authored
refactor(engine): extract RowSlots helper for weights/children guard (#85)
Polish pass on PR-7.3's defence-in-depth IllegalArgumentException added in v1.6.5 (commit f868499). The guard lived inline as duplicated code at two engine call sites — LayoutCompiler#distributeRowSlotWidths and NodeDefinitionSupport#measureRow — with no direct test. The path is unreachable through public API (RowNode canonical constructor blocks the mismatched state), but unreachable code that lacks coverage rots: the next refactor can silently delete either guard with no test going red. Changes: - New com.demcha.compose.document.layout.RowSlots (package-private) with a single helper, validateWeightsMatchChildren(weights, n). Error message unchanged from PR-7.3. - Both call sites replaced with the helper call. - New RowSlotsTest with 4 tests: happy path, more-weights-than-children, fewer-weights-than-children, and a message-quality check that the error tells the caller how to fix it. - GraphCompose.DocumentBuilder#pageBackgrounds(...) Javadoc now spells out the empty-list-clears semantics in prose, not just in @param. Inner comment in DocumentBuilder.create() shortened — the rationale moved up to the public Javadoc where it belongs. No behaviour change. Suite: 1023 tests, 0 failures, 0 errors (~47s). Surfaced by retrospective senior-review of PR-7.3 via the new graphcompose-senior-review skill.
1 parent 109e810 commit 1637adf

6 files changed

Lines changed: 128 additions & 13 deletions

File tree

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,20 @@ JitPack continue to resolve through the existing coordinates.
2222
land in `target/japicmp/`. JitPack repository is scoped to the
2323
`japicmp` profile, so downstream consumers do not inherit it.
2424

25+
### Engine internals (no behaviour change)
26+
27+
- **`RowSlots` helper extracted** from `LayoutCompiler` and
28+
`NodeDefinitionSupport`. The defence-in-depth `IllegalArgumentException`
29+
guard added in v1.6.5 (PR-7.3) for the row weights / children size
30+
mismatch lived as duplicated inline code at both engine call sites
31+
with no direct test — a future refactor could have silently deleted
32+
either copy. The validation now lives in
33+
`com.demcha.compose.document.layout.RowSlots#validateWeightsMatchChildren`
34+
(package-private), with `RowSlotsTest` driving it directly. Error
35+
message is unchanged. `GraphCompose.DocumentBuilder#pageBackgrounds(...)`
36+
Javadoc now spells out the empty-list-clears semantics in prose, not
37+
only in the `@param` line.
38+
2539
## v1.6.5 — 2026-05-30
2640

2741
### Templates v2

src/main/java/com/demcha/compose/GraphCompose.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,11 @@ public DocumentBuilder pageBackground(java.awt.Color color) {
251251
* {@link com.demcha.compose.document.api.DocumentSession#pageBackgrounds}
252252
* for the full semantics.
253253
*
254+
* <p>Calling this method with an empty list is an <b>explicit clear</b>:
255+
* it overrides any earlier {@link #pageBackground(com.demcha.compose.document.style.DocumentColor)}
256+
* call on the same builder and emits no page-background fragments.
257+
* Passing {@code null} has the same effect.</p>
258+
*
254259
* @param fills ordered fills, or {@code null}/empty to clear
255260
* @return this builder
256261
*/
@@ -381,9 +386,8 @@ public DocumentSession create() {
381386
markdown,
382387
guideLines);
383388
if (pageBackgrounds != null) {
384-
// Explicit pageBackgrounds() call wins — even an empty
385-
// list is an intentional clear that should override any
386-
// earlier pageBackground(color) on the same builder.
389+
// Explicit pageBackgrounds() call wins over a prior
390+
// pageBackground(color). Empty list = clear; see builder Javadoc.
387391
session.pageBackgrounds(pageBackgrounds);
388392
} else if (pageBackground != null) {
389393
session.pageBackground(pageBackground);

src/main/java/com/demcha/compose/document/layout/LayoutCompiler.java

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -746,11 +746,7 @@ private static double[] distributeRowSlotWidths(List<DocumentNode> children,
746746
}
747747
return slots;
748748
}
749-
if (weights.size() != n) {
750-
throw new IllegalArgumentException(
751-
"Row weights size (" + weights.size() + ") must match children size (" + n
752-
+ "). Pass exactly " + n + " weight(s) or leave weights empty for an even split.");
753-
}
749+
RowSlots.validateWeightsMatchChildren(weights, n);
754750
double total = 0.0;
755751
for (double w : weights) {
756752
total += w;

src/main/java/com/demcha/compose/document/layout/NodeDefinitionSupport.java

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -324,11 +324,7 @@ public static MeasureResult measureRow(RowNode node,
324324
slotWidths[i] = share;
325325
}
326326
} else {
327-
if (node.weights().size() != n) {
328-
throw new IllegalArgumentException(
329-
"Row weights size (" + node.weights().size() + ") must match children size (" + n
330-
+ "). Pass exactly " + n + " weight(s) or leave weights empty for an even split.");
331-
}
327+
RowSlots.validateWeightsMatchChildren(node.weights(), n);
332328
double total = 0.0;
333329
for (Double w : node.weights()) {
334330
total += w;
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package com.demcha.compose.document.layout;
2+
3+
import java.util.List;
4+
5+
/**
6+
* Shared validation helpers for row weight / children distribution code.
7+
*
8+
* <p>Centralises the {@link IllegalArgumentException} contract used by both
9+
* {@link LayoutCompiler#distributeRowSlotWidths(List, List, double, double) compile-phase}
10+
* and {@link NodeDefinitionSupport#measureRow measure-phase} row distribution.
11+
* The {@code RowNode} canonical constructor already rejects a mismatched
12+
* weights list at construction time; these helpers are defence-in-depth
13+
* for any path that bypasses the constructor (reflection-based
14+
* deserialization, framework proxies, etc.) and arrives at the engine
15+
* with an inconsistent {@code (weights, children)} pair.</p>
16+
*
17+
* <p>Package-private intentionally — engine surface, not public API.</p>
18+
*
19+
* @author Artem Demchyshyn
20+
*/
21+
final class RowSlots {
22+
23+
private RowSlots() {
24+
// Utility class, no instantiation.
25+
}
26+
27+
/**
28+
* Asserts that an explicit {@code weights} list matches the row's
29+
* children count. Callers must skip this check when {@code weights}
30+
* is null or empty — the even-split fallback applies there instead.
31+
*
32+
* @param weights non-null, non-empty weights list
33+
* @param childCount number of row children
34+
* @throws IllegalArgumentException if {@code weights.size() != childCount}
35+
*/
36+
static void validateWeightsMatchChildren(List<Double> weights, int childCount) {
37+
if (weights.size() != childCount) {
38+
throw new IllegalArgumentException(
39+
"Row weights size (" + weights.size() + ") must match children size ("
40+
+ childCount + "). Pass exactly " + childCount
41+
+ " weight(s) or leave weights empty for an even split.");
42+
}
43+
}
44+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package com.demcha.compose.document.layout;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import java.util.List;
6+
7+
import static org.assertj.core.api.Assertions.assertThat;
8+
import static org.assertj.core.api.Assertions.assertThatCode;
9+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
10+
11+
/**
12+
* Engine-level contract for the row weights / children-count guard
13+
* shared by {@link LayoutCompiler} and {@link NodeDefinitionSupport}.
14+
*
15+
* <p>The {@code RowNode} canonical constructor already rejects the
16+
* mismatched state via {@link RowBuilder}, so under normal authoring the
17+
* helper is unreachable. The helper exists for defence-in-depth paths
18+
* that bypass the constructor (reflection-based deserialization, etc.);
19+
* these tests pin the contract so a future refactor cannot silently
20+
* delete the guard at either call site.</p>
21+
*/
22+
class RowSlotsTest {
23+
24+
@Test
25+
void matchingSizesPassWithoutThrowing() {
26+
assertThatCode(() -> RowSlots.validateWeightsMatchChildren(List.of(1.0, 2.0, 3.0), 3))
27+
.doesNotThrowAnyException();
28+
}
29+
30+
@Test
31+
void moreWeightsThanChildrenIsRejectedWithBothSizesNamed() {
32+
assertThatThrownBy(() -> RowSlots.validateWeightsMatchChildren(List.of(1.0, 2.0, 3.0), 2))
33+
.isInstanceOf(IllegalArgumentException.class)
34+
.hasMessageContaining("weights")
35+
.hasMessageContaining("children")
36+
.hasMessageContaining("(3)")
37+
.hasMessageContaining("(2)");
38+
}
39+
40+
@Test
41+
void fewerWeightsThanChildrenIsRejectedWithBothSizesNamed() {
42+
assertThatThrownBy(() -> RowSlots.validateWeightsMatchChildren(List.of(1.0, 2.0), 5))
43+
.isInstanceOf(IllegalArgumentException.class)
44+
.hasMessageContaining("(2)")
45+
.hasMessageContaining("(5)");
46+
}
47+
48+
@Test
49+
void errorMessageHintsAtTheFix() {
50+
// The senior-review bar for engine exception messages: name the
51+
// values AND tell the caller how to fix it. Asserting the verb
52+
// is enough — the exact wording is implementation detail.
53+
assertThatThrownBy(() -> RowSlots.validateWeightsMatchChildren(List.of(1.0), 4))
54+
.isInstanceOf(IllegalArgumentException.class)
55+
.extracting(Throwable::getMessage)
56+
.satisfies(msg -> {
57+
String s = (String) msg;
58+
assertThat(s).containsAnyOf("Pass", "Provide", "Use");
59+
});
60+
}
61+
}

0 commit comments

Comments
 (0)