Files
mila/httpserver/handler.go

158 lines
3.8 KiB
Go

package httpserver
import (
"context"
"encoding/json"
"errors"
"log/slog"
"net/http"
"strings"
"time"
"gitea.unprism.ru/KRBL/mila/protocol"
)
type Handler struct {
cfg protocol.Config
store protocol.Store
logger *slog.Logger
onSet func(protocol.ContentState)
}
func NewHandler(cfg protocol.Config, store protocol.Store, opts ...Option) *Handler {
if store == nil {
store = nilStore{}
}
h := &Handler{
cfg: protocol.NormalizeConfig(cfg),
store: store,
logger: slog.Default(),
}
for _, opt := range opts {
opt(h)
}
return h
}
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
start := time.Now()
status := http.StatusOK
defer func() {
h.logger.Info("mila request",
slog.String("method", r.Method),
slog.String("path", r.URL.Path),
slog.String("remote_addr", r.RemoteAddr),
slog.Int("status", status),
slog.Duration("duration", time.Since(start)),
)
}()
if r.Method != http.MethodGet {
status = http.StatusMethodNotAllowed
w.Header().Set("Allow", http.MethodGet)
writeError(w, status, "method not allowed")
return
}
switch cleanPath(r.URL.Path) {
case "/set-content":
status = h.handleSetContent(w, r)
case "/get-content":
status = h.handleGetContent(w, r)
default:
status = http.StatusNotFound
writeError(w, status, "path not found")
}
}
func (h *Handler) handleSetContent(w http.ResponseWriter, r *http.Request) int {
q := r.URL.Query()
state := protocol.ContentState{
Route: strings.TrimSpace(q.Get("route")),
Text: q.Get("str"),
Template: strings.TrimSpace(q.Get("tpl_name")),
UpdatedAt: time.Now().UTC(),
}
if err := protocol.Validate(h.cfg, state); err != nil {
if errors.Is(err, protocol.ErrInvalidData) {
writeError(w, http.StatusBadRequest, err.Error())
return http.StatusBadRequest
}
writeError(w, http.StatusInternalServerError, err.Error())
return http.StatusInternalServerError
}
if err := h.store.Set(r.Context(), state); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return http.StatusInternalServerError
}
if h.onSet != nil {
h.onSet(state)
}
writeJSON(w, http.StatusOK, map[string]string{"status": "OK"})
return http.StatusOK
}
func (h *Handler) handleGetContent(w http.ResponseWriter, r *http.Request) int {
state, err := h.store.Get(r.Context())
if err != nil {
if errors.Is(err, protocol.ErrNotReady) {
writeError(w, http.StatusServiceUnavailable, "content state is not ready")
return http.StatusServiceUnavailable
}
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
writeError(w, http.StatusServiceUnavailable, err.Error())
return http.StatusServiceUnavailable
}
writeError(w, http.StatusInternalServerError, err.Error())
return http.StatusInternalServerError
}
writeJSON(w, http.StatusOK, protocolContentResponse{
Route: state.Route,
Text: state.Text,
Template: state.Template,
})
return http.StatusOK
}
type protocolContentResponse struct {
Route string `json:"route"`
Text string `json:"str"`
Template string `json:"tpl_name"`
}
type nilStore struct{}
func (nilStore) Get(context.Context) (protocol.ContentState, error) {
return protocol.ContentState{}, protocol.ErrNotReady
}
func (nilStore) Set(context.Context, protocol.ContentState) error {
return errors.New("mila: store is not configured")
}
func cleanPath(path string) string {
if path == "" {
return "/"
}
if len(path) > 1 {
path = strings.TrimRight(path, "/")
}
return path
}
func writeJSON(w http.ResponseWriter, status int, payload any) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(payload)
}
func writeError(w http.ResponseWriter, status int, msg string) {
writeJSON(w, status, map[string]string{"error": msg})
}