summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-03 20:06:52 +0200
committerPaul Buetow <paul@buetow.org>2026-03-03 20:06:52 +0200
commitaa7495388de82446a78b4f767a62728de9f777ed (patch)
treebc14fb754fd0100a9bfbf085905e5c1c70b20a89 /internal
parent9c7b4517c3d7bf73a0f0b32df1bc061c9e8d8120 (diff)
refactor(report): add format-specific reporter implementations (task 333)
Diffstat (limited to 'internal')
-rw-r--r--internal/goprecords/report.go84
-rw-r--r--internal/goprecords/report_test.go64
2 files changed, 93 insertions, 55 deletions
diff --git a/internal/goprecords/report.go b/internal/goprecords/report.go
index 0b4454d..148563c 100644
--- a/internal/goprecords/report.go
+++ b/internal/goprecords/report.go
@@ -104,35 +104,65 @@ func WriteReports(w io.Writer, aggregates *Aggregates, cfg ReportConfig) error {
return nil
}
-// Reporter builds a single report (category + metric + format).
-type Reporter struct {
+type Reporter interface {
+ Report() string
+}
+
+type reportBuilder struct {
aggregates *Aggregates
limit uint
category Category
metric Metric
- outputFormat OutputFormat
headerIndent uint
}
-// NewReporter returns a Reporter for the given category and metric.
-func NewReporter(aggregates *Aggregates, category Category, limit uint, metric Metric, outputFormat OutputFormat, headerIndent uint) *Reporter {
- return &Reporter{
+type PlaintextReporter struct {
+ builder reportBuilder
+}
+
+type MarkdownReporter struct {
+ builder reportBuilder
+}
+
+type GemtextReporter struct {
+ builder reportBuilder
+}
+
+func NewReporter(aggregates *Aggregates, category Category, limit uint, metric Metric, outputFormat OutputFormat, headerIndent uint) Reporter {
+ builder := reportBuilder{
aggregates: aggregates,
limit: limit,
category: category,
metric: metric,
- outputFormat: outputFormat,
headerIndent: headerIndent,
}
+ switch outputFormat {
+ case FormatMarkdown:
+ return &MarkdownReporter{builder: builder}
+ case FormatGemtext:
+ return &GemtextReporter{builder: builder}
+ default:
+ return &PlaintextReporter{builder: builder}
+ }
}
-// NewHostReporter returns a Reporter for Host category.
-func NewHostReporter(aggregates *Aggregates, limit uint, metric Metric, outputFormat OutputFormat, headerIndent uint) *Reporter {
+func NewHostReporter(aggregates *Aggregates, limit uint, metric Metric, outputFormat OutputFormat, headerIndent uint) Reporter {
return NewReporter(aggregates, CategoryHost, limit, metric, outputFormat, headerIndent)
}
-// Report returns the formatted report string.
-func (r Reporter) Report() string {
+func (r *PlaintextReporter) Report() string {
+ return r.builder.Report(FormatPlaintext)
+}
+
+func (r *MarkdownReporter) Report() string {
+ return r.builder.Report(FormatMarkdown)
+}
+
+func (r *GemtextReporter) Report() string {
+ return r.builder.Report(FormatGemtext)
+}
+
+func (r reportBuilder) Report(outputFormat OutputFormat) string {
var rows []tableRow
var hasLastKernel bool
if r.category == CategoryHost {
@@ -143,10 +173,10 @@ func (r Reporter) Report() string {
if len(rows) == 0 {
return ""
}
- return r.formatReport(rows, hasLastKernel)
+ return r.formatReport(rows, hasLastKernel, outputFormat)
}
-func (r Reporter) buildHostTable() ([]tableRow, bool) {
+func (r reportBuilder) buildHostTable() ([]tableRow, bool) {
type keyVal struct {
agg *HostAggregate
key uint64
@@ -191,7 +221,7 @@ func (r Reporter) buildHostTable() ([]tableRow, bool) {
return rows, true
}
-func (r Reporter) buildCategoryTable() ([]tableRow, bool) {
+func (r reportBuilder) buildCategoryTable() ([]tableRow, bool) {
m := r.aggregates.Kernel
switch r.category {
case CategoryKernelMajor:
@@ -238,7 +268,7 @@ func (r Reporter) buildCategoryTable() ([]tableRow, bool) {
return rows, false
}
-func (r Reporter) humanStrHost(h *HostAggregate) string {
+func (r reportBuilder) humanStrHost(h *HostAggregate) string {
switch r.metric {
case MetricUptime:
return formatDuration(h.Uptime)
@@ -255,7 +285,7 @@ func (r Reporter) humanStrHost(h *HostAggregate) string {
}
}
-func (r Reporter) humanStrAgg(a *Aggregate) string {
+func (r reportBuilder) humanStrAgg(a *Aggregate) string {
switch r.metric {
case MetricUptime:
return formatDuration(a.Uptime)
@@ -268,20 +298,20 @@ func (r Reporter) humanStrAgg(a *Aggregate) string {
}
}
-func (r Reporter) formatReport(rows []tableRow, hasLastKernel bool) string {
+func (r reportBuilder) formatReport(rows []tableRow, hasLastKernel bool, outputFormat OutputFormat) string {
cW, nW, vW, lkW := r.reportWidths(rows, hasLastKernel)
border := r.buildBorder(cW, nW, vW, lkW, hasLastKernel)
- header := r.buildReportHeader(cW, nW, vW, lkW, hasLastKernel, border)
+ header := r.buildReportHeader(cW, nW, vW, lkW, hasLastKernel, border, outputFormat)
fmtStr := r.buildFormatStr(cW, nW, vW, lkW, hasLastKernel)
body := r.buildReportBody(rows, fmtStr, hasLastKernel)
out := header + body + border
- if r.outputFormat == FormatMarkdown || r.outputFormat == FormatGemtext {
+ if outputFormat == FormatMarkdown || outputFormat == FormatGemtext {
out += "```\n"
}
return out
}
-func (r Reporter) reportWidths(rows []tableRow, hasLastKernel bool) (countW, nameW, valueW, lastKernelW int) {
+func (r reportBuilder) reportWidths(rows []tableRow, hasLastKernel bool) (countW, nameW, valueW, lastKernelW int) {
countW = 3
nameW = len(r.category.String())
valueW = len(r.metric.String())
@@ -305,7 +335,7 @@ func (r Reporter) reportWidths(rows []tableRow, hasLastKernel bool) (countW, nam
return countW, nameW, valueW, lastKernelW
}
-func (r Reporter) buildBorder(countW, nameW, valueW, lastKernelW int, hasLastKernel bool) string {
+func (r reportBuilder) buildBorder(countW, nameW, valueW, lastKernelW int, hasLastKernel bool) string {
parts := []string{
"+" + strings.Repeat("-", 2+countW),
"+" + strings.Repeat("-", 2+nameW),
@@ -317,19 +347,19 @@ func (r Reporter) buildBorder(countW, nameW, valueW, lastKernelW int, hasLastKer
return strings.Join(parts, "") + "+\n"
}
-func (r Reporter) buildReportHeader(countW, nameW, valueW, lastKernelW int, hasLastKernel bool, border string) string {
+func (r reportBuilder) buildReportHeader(countW, nameW, valueW, lastKernelW int, hasLastKernel bool, border string, outputFormat OutputFormat) string {
var h string
- if r.outputFormat == FormatMarkdown || r.outputFormat == FormatGemtext {
+ if outputFormat == FormatMarkdown || outputFormat == FormatGemtext {
h = strings.Repeat("#", int(r.headerIndent)) + " "
}
h += fmt.Sprintf("Top %d %s's by %s\n\n", r.limit, r.metric, r.category)
desc := MetricDescription(r.metric)
lineLimit := len(border)
- if r.outputFormat == FormatPlaintext && lineLimit > 0 && len(desc) > lineLimit-1 {
+ if outputFormat == FormatPlaintext && lineLimit > 0 && len(desc) > lineLimit-1 {
desc = " " + wordWrap(desc, lineLimit-1)
}
h += desc + "\n\n"
- if r.outputFormat == FormatMarkdown || r.outputFormat == FormatGemtext {
+ if outputFormat == FormatMarkdown || outputFormat == FormatGemtext {
h += "```\n"
}
h += border
@@ -343,14 +373,14 @@ func (r Reporter) buildReportHeader(countW, nameW, valueW, lastKernelW int, hasL
return h
}
-func (r Reporter) buildFormatStr(countW, nameW, valueW, lastKernelW int, hasLastKernel bool) string {
+func (r reportBuilder) buildFormatStr(countW, nameW, valueW, lastKernelW int, hasLastKernel bool) string {
if hasLastKernel {
return fmt.Sprintf("| %%%ds | %%%ds | %%%ds | %%%ds |", countW, nameW, valueW, lastKernelW)
}
return fmt.Sprintf("| %%%ds | %%%ds | %%%ds |", countW, nameW, valueW)
}
-func (r Reporter) buildReportBody(rows []tableRow, fmtStr string, hasLastKernel bool) string {
+func (r reportBuilder) buildReportBody(rows []tableRow, fmtStr string, hasLastKernel bool) string {
var b strings.Builder
for _, row := range rows {
if hasLastKernel {
diff --git a/internal/goprecords/report_test.go b/internal/goprecords/report_test.go
index f6bdab9..2daedec 100644
--- a/internal/goprecords/report_test.go
+++ b/internal/goprecords/report_test.go
@@ -13,13 +13,17 @@ func TestNewReporter(t *testing.T) {
KernelMajor: make(map[string]*Aggregate),
KernelName: make(map[string]*Aggregate),
}
-
+
reporter := NewReporter(aggs, CategoryHost, 20, MetricUptime, FormatPlaintext, 1)
- if reporter.limit != 20 {
- t.Errorf("expected limit 20, got %d", reporter.limit)
+ plain, ok := reporter.(*PlaintextReporter)
+ if !ok {
+ t.Fatalf("expected PlaintextReporter, got %T", reporter)
+ }
+ if plain.builder.limit != 20 {
+ t.Errorf("expected limit 20, got %d", plain.builder.limit)
}
- if reporter.category != CategoryHost {
- t.Errorf("expected CategoryHost, got %v", reporter.category)
+ if plain.builder.category != CategoryHost {
+ t.Errorf("expected CategoryHost, got %v", plain.builder.category)
}
}
@@ -30,10 +34,14 @@ func TestNewHostReporter(t *testing.T) {
KernelMajor: make(map[string]*Aggregate),
KernelName: make(map[string]*Aggregate),
}
-
+
reporter := NewHostReporter(aggs, 20, MetricUptime, FormatPlaintext, 1)
- if reporter.category != CategoryHost {
- t.Errorf("expected CategoryHost, got %v", reporter.category)
+ plain, ok := reporter.(*PlaintextReporter)
+ if !ok {
+ t.Fatalf("expected PlaintextReporter, got %T", reporter)
+ }
+ if plain.builder.category != CategoryHost {
+ t.Errorf("expected CategoryHost, got %v", plain.builder.category)
}
}
@@ -44,7 +52,7 @@ func TestReportEmpty(t *testing.T) {
KernelMajor: make(map[string]*Aggregate),
KernelName: make(map[string]*Aggregate),
}
-
+
reporter := NewReporter(aggs, CategoryHost, 20, MetricUptime, FormatPlaintext, 1)
report := reporter.Report()
if report != "" {
@@ -59,7 +67,7 @@ func TestReportWithData(t *testing.T) {
KernelMajor: make(map[string]*Aggregate),
KernelName: make(map[string]*Aggregate),
}
-
+
// Add a host
hagg := NewHostAggregate("host1", "Linux 5.10")
hagg.Uptime = 86400000
@@ -67,10 +75,10 @@ func TestReportWithData(t *testing.T) {
hagg.FirstBoot = 1000
hagg.LastSeen = 86401000
aggs.Host["host1"] = hagg
-
+
reporter := NewReporter(aggs, CategoryHost, 20, MetricUptime, FormatPlaintext, 1)
report := reporter.Report()
-
+
if report == "" {
t.Error("expected non-empty report")
}
@@ -89,17 +97,17 @@ func TestReportMarkdown(t *testing.T) {
KernelMajor: make(map[string]*Aggregate),
KernelName: make(map[string]*Aggregate),
}
-
+
hagg := NewHostAggregate("host1", "Linux 5.10")
hagg.Uptime = 86400000
hagg.Boots = 10
hagg.FirstBoot = 1000
hagg.LastSeen = 86401000
aggs.Host["host1"] = hagg
-
+
reporter := NewReporter(aggs, CategoryHost, 20, MetricUptime, FormatMarkdown, 2)
report := reporter.Report()
-
+
if !strings.Contains(report, "##") {
t.Error("expected markdown header ##")
}
@@ -115,17 +123,17 @@ func TestReportGemtext(t *testing.T) {
KernelMajor: make(map[string]*Aggregate),
KernelName: make(map[string]*Aggregate),
}
-
+
hagg := NewHostAggregate("host1", "Linux 5.10")
hagg.Uptime = 86400000
hagg.Boots = 10
hagg.FirstBoot = 1000
hagg.LastSeen = 86401000
aggs.Host["host1"] = hagg
-
+
reporter := NewReporter(aggs, CategoryHost, 20, MetricUptime, FormatGemtext, 2)
report := reporter.Report()
-
+
if !strings.Contains(report, "##") {
t.Error("expected gemtext header ##")
}
@@ -141,19 +149,19 @@ func TestReportMetrics(t *testing.T) {
KernelMajor: make(map[string]*Aggregate),
KernelName: make(map[string]*Aggregate),
}
-
+
hagg := NewHostAggregate("host1", "Linux 5.10")
hagg.Uptime = 86400000
hagg.Boots = 10
hagg.FirstBoot = 1000
hagg.LastSeen = 86401000
aggs.Host["host1"] = hagg
-
+
metrics := []Metric{MetricBoots, MetricUptime, MetricScore, MetricDowntime, MetricLifespan}
for _, metric := range metrics {
reporter := NewReporter(aggs, CategoryHost, 20, metric, FormatPlaintext, 1)
report := reporter.Report()
-
+
if report == "" {
t.Errorf("expected non-empty report for metric %v", metric)
}
@@ -167,16 +175,16 @@ func TestReportKernelCategory(t *testing.T) {
KernelMajor: make(map[string]*Aggregate),
KernelName: make(map[string]*Aggregate),
}
-
+
// Add kernel data
kernel := NewAggregate("Linux 5.10.0")
kernel.Uptime = 86400000
kernel.Boots = 5
aggs.Kernel["Linux 5.10.0"] = kernel
-
+
reporter := NewReporter(aggs, CategoryKernel, 20, MetricUptime, FormatPlaintext, 1)
report := reporter.Report()
-
+
if report == "" {
t.Error("expected non-empty report for Kernel category")
}
@@ -192,7 +200,7 @@ func TestReportLimit(t *testing.T) {
KernelMajor: make(map[string]*Aggregate),
KernelName: make(map[string]*Aggregate),
}
-
+
// Add multiple hosts
for i := 0; i < 10; i++ {
host := hostName(i)
@@ -200,10 +208,10 @@ func TestReportLimit(t *testing.T) {
hagg.Uptime = uint64(86400000 * (10 - i))
aggs.Host[host] = hagg
}
-
+
reporter := NewReporter(aggs, CategoryHost, 5, MetricUptime, FormatPlaintext, 1)
report := reporter.Report()
-
+
// Count entries (each entry line starts with |)
lines := strings.Split(report, "\n")
entryCount := 0
@@ -212,7 +220,7 @@ func TestReportLimit(t *testing.T) {
entryCount++
}
}
-
+
if entryCount > 5 {
t.Errorf("expected at most 5 entries, got %d", entryCount)
}