package api import ( "encoding/json" "fmt" "net/http" "strings" "github.com/evanreichard/markdown-editor/internal/logging" "github.com/evanreichard/markdown-editor/internal/storage" ) // Handlers contains the HTTP handlers type Handlers struct { storage *storage.Storage } // New creates a new Handlers instance func New(s *storage.Storage) *Handlers { logging.Logger.Info("API handlers initialized") return &Handlers{storage: s} } // ErrorResponse represents an error response type ErrorResponse struct { Error string `json:"error"` } // sendError sends a JSON error response func (h *Handlers) sendError(w http.ResponseWriter, r *http.Request, statusCode int, err error) { logging.Logger.WithFields(map[string]interface{}{ "path": r.URL.Path, "status": statusCode, "error": err.Error(), }).Warn("API error") w.Header().Set("Content-Type", "application/json") w.WriteHeader(statusCode) json.NewEncoder(w).Encode(ErrorResponse{Error: err.Error()}) } // ListFiles handles GET /api/files func (h *Handlers) ListFiles(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { h.sendError(w, r, http.StatusMethodNotAllowed, fmt.Errorf("method not allowed")) return } files, err := h.storage.List() if err != nil { h.sendError(w, r, http.StatusInternalServerError, err) return } // Ensure we always encode an array, never null if files == nil { files = []*storage.File{} } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(files) } // GetFile handles GET /api/files/:name func (h *Handlers) GetFile(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { h.sendError(w, r, http.StatusMethodNotAllowed, fmt.Errorf("method not allowed")) return } name := strings.TrimPrefix(r.URL.Path, "/api/files/") if name == "" { h.sendError(w, r, http.StatusBadRequest, fmt.Errorf("file name required")) return } file, err := h.storage.Get(name) if err != nil { if err == storage.ErrFileNotFound { h.sendError(w, r, http.StatusNotFound, err) return } h.sendError(w, r, http.StatusBadRequest, err) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(file) } // CreateFile handles POST /api/files func (h *Handlers) CreateFile(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { h.sendError(w, r, http.StatusMethodNotAllowed, fmt.Errorf("method not allowed")) return } var file storage.File if err := json.NewDecoder(r.Body).Decode(&file); err != nil { h.sendError(w, r, http.StatusBadRequest, fmt.Errorf("invalid request body: %w", err)) return } result, err := h.storage.Create(file.Name, file.Content) if err != nil { if err == storage.ErrInvalidName { h.sendError(w, r, http.StatusBadRequest, err) return } h.sendError(w, r, http.StatusConflict, err) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(result) } // UpdateFile handles PUT /api/files/:name func (h *Handlers) UpdateFile(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPut { h.sendError(w, r, http.StatusMethodNotAllowed, fmt.Errorf("method not allowed")) return } name := strings.TrimPrefix(r.URL.Path, "/api/files/") if name == "" { h.sendError(w, r, http.StatusBadRequest, fmt.Errorf("file name required")) return } var file storage.File if err := json.NewDecoder(r.Body).Decode(&file); err != nil { h.sendError(w, r, http.StatusBadRequest, fmt.Errorf("invalid request body: %w", err)) return } result, err := h.storage.Update(name, file.Content) if err != nil { if err == storage.ErrFileNotFound { h.sendError(w, r, http.StatusNotFound, err) return } h.sendError(w, r, http.StatusBadRequest, err) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(result) } // DeleteFile handles DELETE /api/files/:name func (h *Handlers) DeleteFile(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodDelete { h.sendError(w, r, http.StatusMethodNotAllowed, fmt.Errorf("method not allowed")) return } name := strings.TrimPrefix(r.URL.Path, "/api/files/") if name == "" { h.sendError(w, r, http.StatusBadRequest, fmt.Errorf("file name required")) return } err := h.storage.Delete(name) if err != nil { if err == storage.ErrFileNotFound { h.sendError(w, r, http.StatusNotFound, err) return } h.sendError(w, r, http.StatusBadRequest, err) return } w.WriteHeader(http.StatusNoContent) }