refactor: разнести пакеты и обновить валидацию
This commit is contained in:
157
httpserver/handler.go
Normal file
157
httpserver/handler.go
Normal file
@@ -0,0 +1,157 @@
|
||||
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})
|
||||
}
|
||||
57
httpserver/handler_test.go
Normal file
57
httpserver/handler_test.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"gitea.unprism.ru/KRBL/mila/protocol"
|
||||
"gitea.unprism.ru/KRBL/mila/storage"
|
||||
)
|
||||
|
||||
func TestHandlerSetAndGetContent(t *testing.T) {
|
||||
handler := NewHandler(protocol.DefaultConfig(), storage.NewMemoryStore())
|
||||
|
||||
params := url.Values{}
|
||||
params.Set("route", "3")
|
||||
params.Set("str", "Гостиный двор - Gostiny dvor")
|
||||
params.Set("tpl_name", "Гостиный двор")
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/set-content?"+params.Encode(), nil)
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("set-content status=%d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
req = httptest.NewRequest(http.MethodGet, "/get-content", nil)
|
||||
rec = httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("get-content status=%d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
var state protocol.ContentState
|
||||
if err := json.NewDecoder(rec.Body).Decode(&state); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
if state.Route != "3" || state.Text != "Гостиный двор - Gostiny dvor" || state.Template != "Гостиный двор" {
|
||||
t.Fatalf("unexpected state: %+v", state)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandlerGetBeforeSetReturns503(t *testing.T) {
|
||||
handler := NewHandler(protocol.DefaultConfig(), storage.NewMemoryStore())
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/get-content", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusServiceUnavailable {
|
||||
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
}
|
||||
23
httpserver/options.go
Normal file
23
httpserver/options.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
|
||||
"gitea.unprism.ru/KRBL/mila/protocol"
|
||||
)
|
||||
|
||||
type Option func(*Handler)
|
||||
|
||||
func WithLogger(logger *slog.Logger) Option {
|
||||
return func(h *Handler) {
|
||||
if logger != nil {
|
||||
h.logger = logger
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func WithOnSet(fn func(protocol.ContentState)) Option {
|
||||
return func(h *Handler) {
|
||||
h.onSet = fn
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user