typetui 0.2.1

A terminal-based typing test.
Documentation
package main

import (
    "encoding/json"
    "fmt"
    "io"
    "log"
    "net/http"
    "time"
)

type Handler func(http.ResponseWriter, *http.Request) error

type ErrorResponse struct {
    Error   string `json:"error"`
    Code    int    `json:"code"`
    Message string `json:"message"`
}

func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if err := h(w, r); err != nil {
        respondWithError(w, err)
    }
}

func respondWithError(w http.ResponseWriter, err error) {
    code := http.StatusInternalServerError
    if httpErr, ok := err.(HTTPError); ok {
        code = httpErr.Code
    }

    w.WriteHeader(code)
    json.NewEncoder(w).Encode(ErrorResponse{
        Error:   err.Error(),
        Code:    code,
        Message: "Request failed",
    })
}

type HTTPError struct {
    Code int
    Msg  string
}

func (e HTTPError) Error() string {
    return e.Msg
}

type Middleware func(http.Handler) http.Handler

func LoggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        next.ServeHTTP(w, r)
        log.Printf("%s %s %s", r.Method, r.URL.Path, time.Since(start))
    })
}

func RecoveryMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                log.Printf("Panic recovered: %v", err)
                http.Error(w, "Internal Server Error", http.StatusInternalServerError)
            }
        }()
        next.ServeHTTP(w, r)
    })
}

func AuthMiddleware(token string) Middleware {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            authHeader := r.Header.Get("Authorization")
            if authHeader != "Bearer "+token {
                http.Error(w, "Unauthorized", http.StatusUnauthorized)
                return
            }
            next.ServeHTTP(w, r)
        })
    }
}

func Chain(handler http.Handler, middlewares ...Middleware) http.Handler {
    for i := len(middlewares) - 1; i >= 0; i-- {
        handler = middlewares[i](handler)
    }
    return handler
}

type User struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

func handleUsers(w http.ResponseWriter, r *http.Request) error {
    switch r.Method {
    case http.MethodGet:
        users := []User{
            {ID: 1, Name: "Alice", Email: "alice@example.com"},
            {ID: 2, Name: "Bob", Email: "bob@example.com"},
        }
        return respondWithJSON(w, users, http.StatusOK)

    case http.MethodPost:
        body, err := io.ReadAll(r.Body)
        if err != nil {
            return HTTPError{Code: http.StatusBadRequest, Msg: "Invalid body"}
        }

        var user User
        if err := json.Unmarshal(body, &user); err != nil {
            return HTTPError{Code: http.StatusBadRequest, Msg: "Invalid JSON"}
        }

        user.ID = 3
        return respondWithJSON(w, user, http.StatusCreated)

    default:
        return HTTPError{Code: http.StatusMethodNotAllowed, Msg: "Method not allowed"}
    }
}

func respondWithJSON(w http.ResponseWriter, data interface{}, code int) error {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(code)
    return json.NewEncoder(w).Encode(data)
}

type HTTPClient struct {
    BaseURL    string
    HTTPClient *http.Client
    Headers    map[string]string
}

func NewHTTPClient(baseURL string) *HTTPClient {
    return &HTTPClient{
        BaseURL: baseURL,
        HTTPClient: &http.Client{
            Timeout: 30 * time.Second,
        },
        Headers: make(map[string]string),
    }
}

func (c *HTTPClient) Get(endpoint string, result interface{}) error {
    req, err := http.NewRequest(http.MethodGet, c.BaseURL+endpoint, nil)
    if err != nil {
        return err
    }

    for k, v := range c.Headers {
        req.Header.Set(k, v)
    }

    resp, err := c.HTTPClient.Do(req)
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    if resp.StatusCode >= 400 {
        return fmt.Errorf("HTTP %d", resp.StatusCode)
    }

    return json.NewDecoder(resp.Body).Decode(result)
}

func (c *HTTPClient) Post(endpoint string, body, result interface{}) error {
    jsonBody, err := json.Marshal(body)
    if err != nil {
        return err
    }

    req, err := http.NewRequest(http.MethodPost, c.BaseURL+endpoint, nil)
    if err != nil {
        return err
    }

    req.Body = nil
    req.GetBody = nil
    req.ContentLength = int64(len(jsonBody))
    req.Body = &bodyReader{data: jsonBody}
    req.Header.Set("Content-Type", "application/json")

    for k, v := range c.Headers {
        req.Header.Set(k, v)
    }

    resp, err := c.HTTPClient.Do(req)
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    if resp.StatusCode >= 400 {
        return fmt.Errorf("HTTP %d", resp.StatusCode)
    }

    return json.NewDecoder(resp.Body).Decode(result)
}

type bodyReader struct {
    data []byte
    pos  int
}

func (b *bodyReader) Read(p []byte) (int, error) {
    if b.pos >= len(b.data) {
        return 0, nil
    }
    n := copy(p, b.data[b.pos:])
    b.pos += n
    return n, nil
}

func (b *bodyReader) Close() error {
    return nil
}

func ServerExample() {
    mux := http.NewServeMux()

    mux.Handle("/users", Handler(handleUsers))

    handler := Chain(mux,
        RecoveryMiddleware,
        LoggingMiddleware,
    )

    server := &http.Server{
        Addr:         ":8080",
        Handler:      handler,
        ReadTimeout:  15 * time.Second,
        WriteTimeout: 15 * time.Second,
    }

    log.Fatal(server.ListenAndServe())
}

func main() {
    ServerExample()
}