Skip to content

Commit c2623be

Browse files
committed
refactor(api): simplify handler structure
1 parent 9711547 commit c2623be

File tree

5 files changed

+296
-265
lines changed

5 files changed

+296
-265
lines changed

internal/api/config.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package api
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
7+
"github.com/pixel365/pulse/internal/config"
8+
)
9+
10+
func (h *Handler) ensureService(serviceID string) error {
11+
if _, ok := h.cfgProvider.Current().Services[serviceID]; ok {
12+
return nil
13+
}
14+
15+
return apiError{
16+
status: http.StatusNotFound,
17+
err: fmt.Errorf("service %s not found", serviceID),
18+
}
19+
}
20+
21+
func (h *Handler) ensureCheck(serviceID, checkID string) error {
22+
_, err := h.checkFields(serviceID, checkID)
23+
return err
24+
}
25+
26+
func (h *Handler) checkFields(serviceID, checkID string) (config.CheckFields, error) {
27+
cfg := h.cfgProvider.Current()
28+
29+
if fields, ok := checkFields(cfg.HttpChecks, serviceID, checkID); ok {
30+
return fields, nil
31+
}
32+
33+
if fields, ok := checkFields(cfg.TCPChecks, serviceID, checkID); ok {
34+
return fields, nil
35+
}
36+
37+
if fields, ok := checkFields(cfg.GRPCChecks, serviceID, checkID); ok {
38+
return fields, nil
39+
}
40+
41+
if fields, ok := checkFields(cfg.DNSChecks, serviceID, checkID); ok {
42+
return fields, nil
43+
}
44+
45+
if fields, ok := checkFields(cfg.TLSChecks, serviceID, checkID); ok {
46+
return fields, nil
47+
}
48+
49+
return config.CheckFields{}, apiError{
50+
status: http.StatusNotFound,
51+
err: fmt.Errorf("check %s/%s not found", serviceID, checkID),
52+
}
53+
}
54+
55+
func checkFields[T any](
56+
checks map[string]config.TypedCheck[T],
57+
serviceID string,
58+
checkID string,
59+
) (config.CheckFields, bool) {
60+
for i := range checks {
61+
if checks[i].Service == serviceID && checks[i].ID == checkID {
62+
return checks[i].CheckFields, true
63+
}
64+
}
65+
66+
return config.CheckFields{}, false
67+
}

internal/api/errors.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package api
2+
3+
import (
4+
"errors"
5+
"net/http"
6+
7+
"github.com/go-chi/render"
8+
)
9+
10+
type apiError struct {
11+
err error
12+
status int
13+
}
14+
15+
func (e apiError) Error() string {
16+
return e.err.Error()
17+
}
18+
19+
func statusCode(err error, fallback int) int {
20+
var apiErr apiError
21+
if ok := errors.As(err, &apiErr); ok {
22+
return apiErr.status
23+
}
24+
25+
return fallback
26+
}
27+
28+
func errorResponse(w http.ResponseWriter, r *http.Request, status int, err error) {
29+
render.Status(r, status)
30+
render.JSON(w, r, map[string]string{
31+
"error": err.Error(),
32+
})
33+
}

internal/api/filters.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package api
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"strconv"
7+
"time"
8+
9+
"github.com/go-chi/chi/v5"
10+
11+
"github.com/pixel365/pulse/internal/model"
12+
)
13+
14+
func executionFilterFromRequest(r *http.Request) (model.CheckExecutionFilter, error) {
15+
filter := model.CheckExecutionFilter{
16+
ServiceID: chi.URLParam(r, "serviceId"),
17+
CheckID: chi.URLParam(r, "checkId"),
18+
}
19+
20+
query := r.URL.Query()
21+
22+
if raw := query.Get("from"); raw != "" {
23+
from, err := time.Parse(time.RFC3339, raw)
24+
if err != nil {
25+
return filter, err
26+
}
27+
filter.From = &from
28+
}
29+
30+
if raw := query.Get("to"); raw != "" {
31+
to, err := time.Parse(time.RFC3339, raw)
32+
if err != nil {
33+
return filter, err
34+
}
35+
filter.To = &to
36+
}
37+
38+
if raw := query.Get("limit"); raw != "" {
39+
limit, err := strconv.Atoi(raw)
40+
if err != nil {
41+
return filter, err
42+
}
43+
filter.Limit = limit
44+
}
45+
46+
return filter, nil
47+
}
48+
49+
func (h *Handler) timelineFilterFromRequest(
50+
r *http.Request,
51+
) (model.CheckExecutionTimelineFilter, error) {
52+
filter := model.CheckExecutionTimelineFilter{
53+
ServiceID: chi.URLParam(r, "serviceId"),
54+
CheckID: chi.URLParam(r, "checkId"),
55+
}
56+
57+
query := r.URL.Query()
58+
59+
if raw := query.Get("from"); raw != "" {
60+
from, err := time.Parse(time.RFC3339, raw)
61+
if err != nil {
62+
return filter, err
63+
}
64+
filter.From = from
65+
}
66+
67+
if raw := query.Get("to"); raw != "" {
68+
to, err := time.Parse(time.RFC3339, raw)
69+
if err != nil {
70+
return filter, err
71+
}
72+
filter.To = to
73+
}
74+
75+
filter.Bucket = model.CheckExecutionBucket(query.Get("bucket"))
76+
77+
fields, err := h.checkFields(filter.ServiceID, filter.CheckID)
78+
if err != nil {
79+
return filter, err
80+
}
81+
filter.Interval = fields.Interval
82+
83+
if !isAllowedBucket(filter.Bucket, fields.AllowedBuckets) {
84+
return filter, fmt.Errorf(
85+
"bucket %q is not allowed for check %s/%s; allowed buckets: %v",
86+
filter.Bucket,
87+
filter.ServiceID,
88+
filter.CheckID,
89+
allowedBuckets(fields.AllowedBuckets),
90+
)
91+
}
92+
93+
return filter, filter.Validate()
94+
}
95+
96+
func allowedBuckets(allowed []string) []string {
97+
if len(allowed) > 0 {
98+
return allowed
99+
}
100+
101+
return []string{
102+
string(model.CheckExecutionBucketMinute),
103+
string(model.CheckExecutionBucketHour),
104+
}
105+
}
106+
107+
func isAllowedBucket(bucket model.CheckExecutionBucket, allowed []string) bool {
108+
if bucket == "" {
109+
bucket = model.CheckExecutionBucketMinute
110+
}
111+
112+
if len(allowed) == 0 {
113+
return bucket == model.CheckExecutionBucketMinute ||
114+
bucket == model.CheckExecutionBucketHour
115+
}
116+
117+
for i := range allowed {
118+
if model.CheckExecutionBucket(allowed[i]) == bucket {
119+
return true
120+
}
121+
}
122+
123+
return false
124+
}

0 commit comments

Comments
 (0)