GoalReportBuilder.java
package network.ike.plugin.support;
import java.util.List;
/**
* Fluent builder for the body of an IKE goal report.
*
* <p>{@link GoalReport} owns the report <em>frame</em> — the
* {@code # <prefix>:<goal>} title and the timestamp line. This builder
* owns the <em>body</em>: the sections, paragraphs, bullet lists,
* tables, and fenced code blocks beneath that frame. Routing every
* report through one builder keeps section depth, table syntax, and
* spacing identical across goals instead of each mojo hand-rolling its
* own {@link StringBuilder} (IKE-Network/ike-issues#408).
*
* <p>Methods return {@code this} so a report reads as a single
* chained expression:
*
* <pre>{@code
* String body = new GoalReportBuilder()
* .section("Status")
* .paragraph("3 cloned, 1 not cloned.")
* .table(List.of("Subproject", "Branch"),
* List.of(new String[]{"ike-docs", "main"}))
* .build();
* }</pre>
*
* <p>The builder emits GitHub-flavoured Markdown. Section headings use
* {@code ##} — one level below the {@code #} title {@code GoalReport}
* prepends. Pass the {@link #build()} result to
* {@code AbstractGoalMojo.writeReport} (ike plugin) or
* {@code AbstractWorkspaceMojo.writeReport} (ws plugin).
*/
public final class GoalReportBuilder {
private final StringBuilder body = new StringBuilder(512);
/** Creates an empty report-body builder. */
public GoalReportBuilder() {
}
/**
* Append a section heading ({@code ## title}).
*
* @param title the section title
* @return this builder
*/
public GoalReportBuilder section(String title) {
ensureBlankLine();
body.append("## ").append(title).append("\n\n");
return this;
}
/**
* Append a paragraph followed by a blank line.
*
* @param text the paragraph text
* @return this builder
*/
public GoalReportBuilder paragraph(String text) {
body.append(text.stripTrailing()).append("\n\n");
return this;
}
/**
* Append a single bullet-list item ({@code - text}). Consecutive
* calls build a contiguous list.
*
* @param text the bullet text
* @return this builder
*/
public GoalReportBuilder bullet(String text) {
body.append("- ").append(text.stripTrailing()).append("\n");
return this;
}
/**
* Append a GitHub-flavoured Markdown table. A no-op when
* {@code rows} is empty — an empty table is never emitted.
*
* @param headers column headers (defines the column count)
* @param rows row cells; each array should match the header
* count (shorter rows are padded, longer rows
* truncated, so a ragged caller cannot break the
* table grid)
* @return this builder
*/
public GoalReportBuilder table(List<String> headers,
List<String[]> rows) {
if (rows == null || rows.isEmpty()) {
return this;
}
int cols = headers.size();
ensureBlankLine();
row(headers.toArray(new String[0]), cols);
StringBuilder sep = new StringBuilder("|");
for (int i = 0; i < cols; i++) {
sep.append("---|");
}
body.append(sep).append("\n");
for (String[] r : rows) {
row(r, cols);
}
body.append("\n");
return this;
}
/**
* Append a fenced code block.
*
* @param language the fence language hint (e.g. {@code "dot"});
* may be empty for a plain fence
* @param content the block content (a trailing newline is added
* if absent)
* @return this builder
*/
public GoalReportBuilder codeBlock(String language, String content) {
ensureBlankLine();
body.append("```").append(language == null ? "" : language)
.append("\n");
body.append(content);
if (!content.endsWith("\n")) {
body.append("\n");
}
body.append("```\n\n");
return this;
}
/**
* Append a pre-rendered Markdown fragment verbatim — the escape
* hatch for content a primitive does not cover (e.g.
* {@code DriftReport.toMarkdown()}).
*
* @param markdown the Markdown fragment
* @return this builder
*/
public GoalReportBuilder raw(String markdown) {
body.append(markdown);
return this;
}
/**
* Render the accumulated body as a Markdown string.
*
* @return the report body (no title or timestamp — those are
* added by {@link GoalReport})
*/
public String build() {
return body.toString().stripTrailing() + "\n";
}
private void row(String[] cells, int cols) {
body.append("|");
for (int i = 0; i < cols; i++) {
String cell = i < cells.length && cells[i] != null
? cells[i] : "";
body.append(' ').append(cell).append(" |");
}
body.append("\n");
}
private void ensureBlankLine() {
if (body.length() > 0 && !body.toString().endsWith("\n\n")) {
if (body.charAt(body.length() - 1) != '\n') {
body.append('\n');
}
body.append('\n');
}
}
}