Skip to content

Commit a9ea6ce

Browse files
committed
feat: add /stats.json endpoint for JSON stats API
Returns user statistics (user, link_count, quote_count) as JSON with pagination support via limit and offset query parameters. Limit defaults to 50 and is capped at 1000 to prevent abuse.
1 parent d7c2b9f commit a9ea6ce

3 files changed

Lines changed: 106 additions & 0 deletions

File tree

cmd/tumble/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ func main() {
223223
mux.HandleFunc("/", h.Index)
224224
mux.HandleFunc("/index.cgi", h.Index)
225225
mux.HandleFunc("/stats", h.Stats)
226+
mux.HandleFunc("/stats.json", h.StatsJSON)
226227
mux.HandleFunc("/search", h.Search)
227228
mux.HandleFunc("/search.cgi", h.Search)
228229
mux.HandleFunc("/link/", h.IRCLinkHandler) // Primary endpoint for links

internal/assets/openapi.json

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -670,6 +670,79 @@
670670
}
671671
}
672672
}
673+
},
674+
"/stats.json": {
675+
"get": {
676+
"summary": "Get User Statistics as JSON",
677+
"description": "Returns user statistics including link and quote counts for each user, sorted by link count descending. Supports pagination.",
678+
"parameters": [
679+
{
680+
"name": "limit",
681+
"in": "query",
682+
"description": "Maximum number of results to return (default: 50, max: 1000)",
683+
"schema": {
684+
"type": "integer",
685+
"default": 50,
686+
"minimum": 1,
687+
"maximum": 1000
688+
}
689+
},
690+
{
691+
"name": "offset",
692+
"in": "query",
693+
"description": "Number of results to skip (default: 0)",
694+
"schema": {
695+
"type": "integer",
696+
"default": 0,
697+
"minimum": 0
698+
}
699+
}
700+
],
701+
"responses": {
702+
"200": {
703+
"description": "Array of user statistics",
704+
"content": {
705+
"application/json": {
706+
"schema": {
707+
"type": "array",
708+
"items": {
709+
"type": "object",
710+
"properties": {
711+
"user": {
712+
"type": "string",
713+
"description": "The username"
714+
},
715+
"link_count": {
716+
"type": "integer",
717+
"description": "Number of links submitted by this user"
718+
},
719+
"quote_count": {
720+
"type": "integer",
721+
"description": "Number of quotes submitted by this user"
722+
}
723+
}
724+
},
725+
"example": [
726+
{
727+
"user": "alice",
728+
"link_count": 150,
729+
"quote_count": 42
730+
},
731+
{
732+
"user": "bob",
733+
"link_count": 120,
734+
"quote_count": 35
735+
}
736+
]
737+
}
738+
}
739+
}
740+
},
741+
"500": {
742+
"description": "Server Error"
743+
}
744+
}
745+
}
673746
}
674747
}
675748
}

internal/handler/handlers.go

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

33
import (
44
"context"
5+
"encoding/json"
56
"fmt"
67
"html/template"
78
"log/slog"
@@ -716,3 +717,34 @@ func (h *Handler) Stats(w http.ResponseWriter, r *http.Request) {
716717
slog.Error("Error rendering stats", "error", err)
717718
}
718719
}
720+
721+
// StatsJSON returns user statistics as JSON with pagination support
722+
func (h *Handler) StatsJSON(w http.ResponseWriter, r *http.Request) {
723+
ctx := r.Context()
724+
725+
// Parse pagination parameters
726+
limit := 50
727+
if limitParam := r.URL.Query().Get("limit"); limitParam != "" {
728+
if val, err := strconv.Atoi(limitParam); err == nil && val > 0 && val <= 1000 {
729+
limit = val
730+
}
731+
}
732+
733+
offset := 0
734+
if offsetParam := r.URL.Query().Get("offset"); offsetParam != "" {
735+
if val, err := strconv.Atoi(offsetParam); err == nil && val >= 0 {
736+
offset = val
737+
}
738+
}
739+
740+
stats, err := h.Store.GetUserStats(ctx, "links", limit, offset)
741+
if err != nil {
742+
h.ServerError(w, r, err)
743+
return
744+
}
745+
746+
w.Header().Set("Content-Type", "application/json")
747+
if err := json.NewEncoder(w).Encode(stats); err != nil {
748+
slog.Error("Error encoding stats JSON", "error", err)
749+
}
750+
}

0 commit comments

Comments
 (0)