package imagevariant import ( "RenewCMS/api/pkg/apperr" validator "RenewCMS/api/pkg/validator" "RenewCMS/internal/domain/image" "RenewCMS/internal/infrastructure/useCases" "encoding/json" "errors" "fmt" "net/http" "strings" "time" validatorlib "github.com/go-playground/validator/v10" "github.com/go-chi/chi/v5" ) type Handler struct { list *useCases.ListVariantsUseCase create *useCases.CreateVariantUseCase update *useCases.UpdateVariantUseCase setEnabled *useCases.SetVariantEnabledUseCase remove *useCases.DeleteVariantUseCase purge *useCases.PurgeVariantUseCase } func NewHandler(list *useCases.ListVariantsUseCase, create *useCases.CreateVariantUseCase, update *useCases.UpdateVariantUseCase, setEnabled *useCases.SetVariantEnabledUseCase, remove *useCases.DeleteVariantUseCase, purge *useCases.PurgeVariantUseCase) *Handler { return &Handler{list, create, update, setEnabled, remove, purge} } type definitionRequest struct { Width *int `json:"width" validate:"required_without=Height,omitempty,min=1,max=8000"` Height *int `json:"height" validate:"required_without=Width,omitempty,min=1,max=8000"` Fit string `json:"fit" validate:"omitempty,oneof=cover contain inside"` Format string `json:"format" validate:"omitempty,oneof=original jpeg png webp"` Quality *int `json:"quality" validate:"omitempty,min=1,max=100"` } type switchesRequest struct { GenerateOnUpload *bool `json:"generate_on_upload"` Enabled *bool `json:"enabled"` } type createRequest struct { Name string `json:"name" validate:"required,variantname"` definitionRequest switchesRequest } type updateRequest struct { definitionRequest switchesRequest } type enabledRequest struct { Enabled *bool `json:"enabled" validate:"required"` } type variantResponse struct { Name string `json:"name"` Width *int `json:"width"` Height *int `json:"height"` Fit image.VariantFit `json:"fit"` Format image.VariantFormat `json:"format"` Quality *int `json:"quality"` GenerateOnUpload bool `json:"generate_on_upload"` Enabled bool `json:"enabled"` GeneratedFiles int `json:"generated_files"` CacheSize int64 `json:"cache_size"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } type updatedVariantResponse struct { variantResponse PurgedFiles int `json:"purged_files"` } type listResponse struct { Items []variantResponse `json:"items"` } type purgeResponse struct { PurgedFiles int `json:"purged_files"` PurgedBytes int64 `json:"purged_bytes"` } func responseOf(summary useCases.VariantSummary) variantResponse { variant := summary.Variant return variantResponse{ Name: variant.Name, Width: variant.Width, Height: variant.Height, Fit: variant.Fit, Format: variant.Format, Quality: variant.Quality, GenerateOnUpload: variant.GenerateOnUpload, Enabled: variant.Enabled, GeneratedFiles: summary.GeneratedFiles, CacheSize: summary.CacheSize, CreatedAt: variant.CreatedAt, UpdatedAt: variant.UpdatedAt, } } func (h *Handler) List(w http.ResponseWriter, _ *http.Request) { summaries, err := h.list.List() if err != nil { h.writeError(w, "", err) return } items := make([]variantResponse, 0, len(summaries)) for _, summary := range summaries { items = append(items, responseOf(summary)) } writeJSON(w, http.StatusOK, listResponse{items}) } func (h *Handler) Create(w http.ResponseWriter, r *http.Request) { var request createRequest if !h.decode(w, r, &request) { return } summary, err := h.create.Create(r.Context(), useCases.CreateVariantCommand{ Name: request.Name, VariantDefinitionCommand: request.definitionRequest.command(), GenerateOnUpload: request.GenerateOnUpload, Enabled: request.Enabled, }) if err != nil { h.writeError(w, request.Name, err) return } writeJSON(w, http.StatusCreated, responseOf(summary)) } func (h *Handler) Update(w http.ResponseWriter, r *http.Request) { var request updateRequest if !h.decode(w, r, &request) { return } name := chi.URLParam(r, "name") result, err := h.update.Update(r.Context(), useCases.UpdateVariantCommand{ Name: name, VariantDefinitionCommand: request.definitionRequest.command(), GenerateOnUpload: request.GenerateOnUpload, Enabled: request.Enabled, }) if err != nil { h.writeError(w, name, err) return } writeJSON(w, http.StatusOK, updatedVariantResponse{responseOf(result.VariantSummary), result.PurgedFiles}) } func (h *Handler) SetEnabled(w http.ResponseWriter, r *http.Request) { var request enabledRequest if !h.decode(w, r, &request) { return } name := chi.URLParam(r, "name") summary, err := h.setEnabled.SetEnabled(r.Context(), name, *request.Enabled) if err != nil { h.writeError(w, name, err) return } writeJSON(w, http.StatusOK, responseOf(summary)) } func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) { name := chi.URLParam(r, "name") if err := h.remove.Delete(r.Context(), name); err != nil { h.writeError(w, name, err) return } w.WriteHeader(http.StatusNoContent) } func (h *Handler) Purge(w http.ResponseWriter, r *http.Request) { name := chi.URLParam(r, "name") purged, err := h.purge.Purge(r.Context(), name) if err != nil { h.writeError(w, name, err) return } writeJSON(w, http.StatusOK, purgeResponse{purged.Files, purged.Bytes}) } func (d definitionRequest) command() useCases.VariantDefinitionCommand { return useCases.VariantDefinitionCommand{Width: d.Width, Height: d.Height, Fit: d.Fit, Format: d.Format, Quality: d.Quality} } func (h *Handler) decode(w http.ResponseWriter, r *http.Request, request any) bool { if err := json.NewDecoder(r.Body).Decode(request); err != nil { http.Error(w, apperr.BodyErrorMessage, http.StatusBadRequest) return false } if err := validator.Validate.Struct(request); err != nil { http.Error(w, h.validationMessage(err), http.StatusUnprocessableEntity) return false } return true } func (h *Handler) validationMessage(err error) string { var failures validatorlib.ValidationErrors if !errors.As(err, &failures) || len(failures) == 0 { return apperr.VariantInvalidErrorMessage } switch failures[0].StructField() { case "Name": return apperr.VariantNameErrorMessage case "Width", "Height": return apperr.VariantDimensionsErrorMessage case "Fit": return apperr.VariantFitErrorMessage case "Format": return h.formatMessage() case "Quality": return apperr.VariantQualityErrorMessage default: return apperr.VariantInvalidErrorMessage } } func (h *Handler) formatMessage() string { encodable := h.create.EncodableFormats() names := make([]string, 0, len(encodable)) for _, format := range encodable { names = append(names, string(format)) } last := len(names) - 1 return fmt.Sprintf(apperr.VariantFormatErrorMessageFormat, strings.Join(names[:last], ", ")+" or "+names[last]) } func (h *Handler) writeError(w http.ResponseWriter, name string, err error) { switch { case errors.Is(err, image.ErrVariantNotFound): http.Error(w, apperr.VariantNotFoundErrorMessage, http.StatusNotFound) case errors.Is(err, image.ErrVariantExists): http.Error(w, fmt.Sprintf(apperr.VariantExistsErrorMessageFormat, name), http.StatusConflict) case errors.Is(err, image.ErrVariantFormatUnavailable): http.Error(w, h.formatMessage(), http.StatusUnprocessableEntity) case errors.Is(err, image.ErrVariantInvalid): http.Error(w, apperr.VariantInvalidErrorMessage, http.StatusUnprocessableEntity) default: http.Error(w, "Internal server error", http.StatusInternalServerError) } } func writeJSON(w http.ResponseWriter, status int, value any) { body, err := json.Marshal(value) if err != nil { http.Error(w, "Internal server error", http.StatusInternalServerError) return } w.WriteHeader(status) _, _ = w.Write(body) }