typetui 0.2.0

A terminal-based typing test.
Documentation
package main

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

type Middleware func(http.Handler) http.Handler

func LoggerMiddleware(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 %v", r.Method, r.URL.Path, time.Since(start))
    })
}

func RecoverMiddleware(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: %v", err)
                http.Error(w, "Internal Server Error", http.StatusInternalServerError)
            }
        }()
        next.ServeHTTP(w, r)
    })
}

type responseWriter struct {
    http.ResponseWriter
    status int
}

func (rw *responseWriter) WriteHeader(code int) {
    rw.status = code
    rw.ResponseWriter.WriteHeader(code)
}

type APIHandler struct {
    version string
}

func (h *APIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    switch r.Method {
    case http.MethodGet:
        h.handleGet(w, r)
    case http.MethodPost:
        h.handlePost(w, r)
    default:
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
    }
}

func (h *APIHandler) handleGet(w http.ResponseWriter, r *http.Request) {
    data := map[string]interface{}{
        "version": h.version,
        "status":  "ok",
    }
    respondJSON(w, data)
}

func (h *APIHandler) handlePost(w http.ResponseWriter, r *http.Request) {
    var payload map[string]interface{}
    if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    respondJSON(w, payload)
}

func respondJSON(w http.ResponseWriter, data interface{}) {
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(data)
}

type AppError struct {
    Code    int
    Message string
    Err     error
}

func (e *AppError) Error() string {
    if e.Err != nil {
        return fmt.Sprintf("%s: %v", e.Message, e.Err)
    }
    return e.Message
}

func NewAppError(code int, message string) *AppError {
    return &AppError{Code: code, Message: message}
}

func WrapError(err error, message string) *AppError {
    return &AppError{Code: 500, Message: message, Err: err}
}

func ReadConfig(path string) (map[string]string, error) {
    file, err := os.Open(path)
    if err != nil {
        return nil, fmt.Errorf("opening config: %w", err)
    }
    defer file.Close()
    data := make(map[string]string)
    content, err := io.ReadAll(file)
    if err != nil {
        return nil, fmt.Errorf("reading config: %w", err)
    }
    
    lines := strings.Split(string(content), "\n")
    for _, line := range lines {
        line = strings.TrimSpace(line)
        if line == "" || strings.HasPrefix(line, "#") {
            continue
        }
        parts := strings.SplitN(line, "=", 2)
        if len(parts) == 2 {
            data[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
        }
    }
    return data, nil
}

func WriteJSONFile(path string, data interface{}) error {
    file, err := os.Create(path)
    if err != nil {
        return err
    }
    defer file.Close()
    encoder := json.NewEncoder(file)
    encoder.SetIndent("", "  ")
    return encoder.Encode(data)
}

func BuildQuery(params map[string]string) string {
    var sb strings.Builder
    first := true
    
    for key, value := range params {
        if !first {
            sb.WriteByte('&')
        }
        first = false
        sb.WriteString(key)
        sb.WriteByte('=')
        sb.WriteString(value)
    }
    
    return sb.String()
}

func FormatDuration(d time.Duration) string {
    if d < time.Millisecond {
        return fmt.Sprintf("%dµs", d.Microseconds())
    }
    if d < time.Second {
        return fmt.Sprintf("%dms", d.Milliseconds())
    }
    return d.Round(time.Second).String()
}

type Timestamp time.Time

func (t Timestamp) MarshalJSON() ([]byte, error) {
    return json.Marshal(time.Time(t).Format(time.RFC3339))
}

func (t *Timestamp) UnmarshalJSON(data []byte) error {
    var s string
    if err := json.Unmarshal(data, &s); err != nil {
        return err
    }
    parsed, err := time.Parse(time.RFC3339, s)
    if err != nil {
        return err
    }
    *t = Timestamp(parsed)
    return nil
}

func main() {
    mux := http.NewServeMux()
    api := &APIHandler{version: "1.0"}
    mux.Handle("/api/", http.StripPrefix("/api", api))
    handler := LoggerMiddleware(RecoverMiddleware(mux))
    
    server := &http.Server{
        Addr:         ":8080",
        Handler:      handler,
        ReadTimeout:  15 * time.Second,
        WriteTimeout: 15 * time.Second,
    }
    log.Println("Server starting on :8080")
    log.Fatal(server.ListenAndServe())
}