Skip to content

Commit a0feb16

Browse files
authored
refactor(http): consolidate UI list handler filter parsing into BaseFilter (#173)
* refactor(http): consolidate UI list handler filter parsing into BaseFilter Introduce filters.BaseFilter with shared Q, Enabled, Sort, Dir fields and a ParseBaseFilter() helper that centralizes common query param parsing logic. Three filter types now embed BaseFilter instead of duplicating the same trim/parse patterns: - iocsFilter: delegates Q and Enabled parsing to BaseFilter - sitesFilter: delegates Q, Enabled, Sort, Dir parsing to BaseFilter - sourcesFilters: delegates Q parsing to BaseFilter (keeps IncludeTests) Removes ~40 lines of duplicated filter parsing code across three files. All existing tests pass; golangci-lint reports 0 issues. * chore: remove code-reduction-findings.md from PR * chore: remove AGENT.md from PR * refactor(http): use WrapListFetcher for secrets and alert sinks handlers Add WrapListFetcher generic adapter in list_handler.go that adapts a service List(ctx, limit, offset) call into a ListFetcher with consistent pagination bounds and error logging. Replace inline Fetcher closures in ui_secrets.go and ui_alert_sinks.go with WrapListFetcher calls, removing ~20 lines of duplicated boilerplate.
1 parent 45fcde2 commit a0feb16

7 files changed

Lines changed: 80 additions & 64 deletions

File tree

services/merrymaker-go/internal/http/filters.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,37 @@ import (
55
"strings"
66
)
77

8+
// BaseFilter holds common filter fields shared across list pages.
9+
// Embed this struct in page-specific filters to avoid duplicating parsing logic.
10+
type BaseFilter struct {
11+
Q string
12+
Enabled *bool
13+
Sort string
14+
Dir string
15+
}
16+
17+
// ParseBaseFilter parses common fields from URL query parameters.
18+
// Callers should embed the result and then override/extend with page-specific fields.
19+
func ParseBaseFilter(q url.Values) BaseFilter {
20+
var enabledPtr *bool
21+
switch strings.TrimSpace(q.Get("enabled")) {
22+
case StrTrue, "1":
23+
b := true
24+
enabledPtr = &b
25+
case StrFalse, "0":
26+
b := false
27+
enabledPtr = &b
28+
}
29+
sort, dir := ParseSortParam(q, "sort", "dir")
30+
31+
return BaseFilter{
32+
Q: strings.TrimSpace(q.Get("q")),
33+
Enabled: enabledPtr,
34+
Sort: sort,
35+
Dir: dir,
36+
}
37+
}
38+
839
const (
940
// StrTrue represents the string "true" for boolean query parameters.
1041
StrTrue = "true"

services/merrymaker-go/internal/http/list_handler.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package httpx
22

33
import (
44
"context"
5+
"log/slog"
56
"net/http"
67
"net/url"
78
)
@@ -11,6 +12,31 @@ import (
1112
// Maintains ≤3 parameters per project constraints.
1213
type ListFetcher[T any] func(ctx context.Context, pg pageOpts) ([]T, error)
1314

15+
// WrapListFetcher adapts a service List method with signature (ctx, limit, offset) -> ([]T, error)
16+
// into a ListFetcher that handles pagination bounds and error logging.
17+
//
18+
// Usage:
19+
//
20+
// Fetcher: WrapListFetcher(h.SecretSvc.List, h.logger(), "failed to load secrets for UI"),
21+
func WrapListFetcher[T any](
22+
listFunc func(ctx context.Context, limit, offset int) ([]T, error),
23+
logger *slog.Logger,
24+
msg string,
25+
) ListFetcher[T] {
26+
return func(ctx context.Context, pg pageOpts) ([]T, error) {
27+
limit, offset := pg.LimitAndOffset()
28+
items, err := listFunc(ctx, limit, offset)
29+
if err != nil {
30+
logger.Error(msg,
31+
"error", err,
32+
"page", pg.Page,
33+
"page_size", pg.PageSize,
34+
)
35+
}
36+
return items, err
37+
}
38+
}
39+
1440
// FilterParser is a function type for parsing URL query parameters into filter data.
1541
// It takes url.Values and returns the parsed filter of type F, or an error if parsing fails.
1642
// The error allows the handler to show meaningful validation errors for invalid filter params.

services/merrymaker-go/internal/http/ui_alert_sinks.go

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -56,18 +56,9 @@ func (h *UIHandlers) AlertSinks(w http.ResponseWriter, r *http.Request) {
5656
Handler: h,
5757
W: w,
5858
R: r,
59-
Fetcher: func(ctx context.Context, pg pageOpts) ([]*model.HTTPAlertSink, error) {
60-
limit, offset := pg.LimitAndOffset()
61-
items, err := h.Sinks.List(ctx, limit, offset)
62-
if err != nil {
63-
h.logger().Error("failed to load alert sinks for UI",
64-
"error", err,
65-
"page", pg.Page,
66-
"page_size", pg.PageSize,
67-
)
68-
}
69-
return items, err
70-
},
59+
Fetcher: WrapListFetcher(func(ctx context.Context, limit, offset int) ([]*model.HTTPAlertSink, error) {
60+
return h.Sinks.List(ctx, limit, offset)
61+
}, h.logger(), "failed to load alert sinks for UI"),
7162
BasePath: alertSinksBasePath,
7263
PageMeta: alertSinkListMeta(),
7364
ItemsKey: "AlertSinks",

services/merrymaker-go/internal/http/ui_iocs.go

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ const errMsgUnableLoadIOCs = "Unable to load IOCs."
1212

1313
// iocsFilter holds filter parameters for the IOCs list view.
1414
type iocsFilter struct {
15-
Type *model.IOCType
16-
Enabled *bool
17-
Search *string
15+
BaseFilter
16+
Type *model.IOCType
17+
Search *string
1818
}
1919

2020
// IOCs serves the IOCs list page, HTMX-aware.
@@ -85,23 +85,17 @@ func (h *UIHandlers) fetchIOCsWithFilters(ctx context.Context, f iocsFilter, pg
8585

8686
// parseIOCsFilter parses filter parameters from query string.
8787
func parseIOCsFilter(q url.Values) (iocsFilter, error) {
88-
var f iocsFilter
88+
f := iocsFilter{BaseFilter: ParseBaseFilter(q)}
8989

9090
// Type filter
9191
if typeStr := q.Get("type"); typeStr != "" {
9292
iocType := model.IOCType(typeStr)
9393
f.Type = &iocType
9494
}
9595

96-
// Enabled filter
97-
if enabledStr := q.Get("enabled"); enabledStr != "" {
98-
enabled := enabledStr == StrTrue || enabledStr == "1"
99-
f.Enabled = &enabled
100-
}
101-
102-
// Search filter
103-
if search := q.Get("q"); search != "" {
104-
f.Search = &search
96+
// Search filter (use BaseFilter.Q, but keep pointer form for backward compat)
97+
if f.Q != "" {
98+
f.Search = &f.Q
10599
}
106100

107101
return f, nil
@@ -127,14 +121,10 @@ func (h *UIHandlers) enrichIOCsData() DataEnricher[*model.IOC, iocsFilter] {
127121
}
128122
builder.With("EnabledFilter", enabledFilter)
129123

130-
searchQuery := ""
131-
if f.Search != nil {
132-
searchQuery = *f.Search
133-
}
134-
builder.With("SearchQuery", searchQuery)
124+
builder.With("SearchQuery", f.Q)
135125

136126
// Add flag to indicate if any filters are active
137-
hasActiveFilters := f.Type != nil || f.Enabled != nil || (f.Search != nil && *f.Search != "")
127+
hasActiveFilters := f.Type != nil || f.Enabled != nil || f.Q != ""
138128
builder.With("HasActiveFilters", hasActiveFilters)
139129
}
140130
}

services/merrymaker-go/internal/http/ui_secrets.go

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,19 +25,9 @@ func (h *UIHandlers) Secrets(w http.ResponseWriter, r *http.Request) {
2525
Handler: h,
2626
W: w,
2727
R: r,
28-
Fetcher: func(ctx context.Context, pg pageOpts) ([]*model.Secret, error) {
29-
// Fetch pageSize+1 to detect hasNext
30-
limit, offset := pg.LimitAndOffset()
31-
secrets, err := h.SecretSvc.List(ctx, limit, offset)
32-
if err != nil {
33-
h.logger().Error("failed to load secrets for UI",
34-
"error", err,
35-
"page", pg.Page,
36-
"page_size", pg.PageSize,
37-
)
38-
}
39-
return secrets, err
40-
},
28+
Fetcher: WrapListFetcher(func(ctx context.Context, limit, offset int) ([]*model.Secret, error) {
29+
return h.SecretSvc.List(ctx, limit, offset)
30+
}, h.logger(), "failed to load secrets for UI"),
4131
BasePath: "/secrets",
4232
PageMeta: PageMeta{
4333
Title: "Merrymaker - Secrets",

services/merrymaker-go/internal/http/ui_sites.go

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,8 @@ type siteRow struct {
1919
}
2020

2121
type sitesFilter struct {
22-
Q string
23-
Enabled *bool
24-
Scope string
25-
Sort string
26-
Dir string
22+
BaseFilter
23+
Scope string
2724
}
2825

2926
type pageBounds struct {
@@ -32,18 +29,10 @@ type pageBounds struct {
3229
}
3330

3431
func parseSitesFilter(q url.Values) (sitesFilter, error) {
35-
qv := strings.TrimSpace(q.Get("q"))
36-
enabledStr := strings.TrimSpace(q.Get("enabled"))
37-
var enabledPtr *bool
38-
switch enabledStr {
39-
case StrTrue, StrFalse:
40-
b := enabledStr == StrTrue
41-
enabledPtr = &b
42-
}
43-
scope := strings.TrimSpace(q.Get("scope"))
44-
sort, dir := ParseSortParam(q, "sort", "dir")
45-
46-
return sitesFilter{Q: qv, Enabled: enabledPtr, Scope: scope, Sort: sort, Dir: dir}, nil
32+
return sitesFilter{
33+
BaseFilter: ParseBaseFilter(q),
34+
Scope: strings.TrimSpace(q.Get("scope")),
35+
}, nil
4736
}
4837

4938
func toSiteRows(sites []*model.Site) []siteRow {

services/merrymaker-go/internal/http/ui_sources.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,17 @@ const errMsgUnableLoadSources = "Unable to load sources."
1414
// --- helpers to keep handlers small and readable ---
1515

1616
type sourcesFilters struct {
17-
Q string
17+
BaseFilter
1818
IncludeTests bool
1919
}
2020

2121
func readSourcesFilters(v url.Values) sourcesFilters {
22-
q := strings.TrimSpace(v.Get("q"))
2322
inc := false
2423
switch strings.ToLower(strings.TrimSpace(v.Get("include_tests"))) {
2524
case "1", StrTrue, "on", "yes":
2625
inc = true
2726
}
28-
return sourcesFilters{Q: q, IncludeTests: inc}
27+
return sourcesFilters{BaseFilter: ParseBaseFilter(v), IncludeTests: inc}
2928
}
3029

3130
func (h *UIHandlers) buildSourceCounts(

0 commit comments

Comments
 (0)