package handler import ( "context" "errors" "io" "net/http" "strconv" "strings" "github.com/labstack/echo/v4" "osdb/internal/domain" ) var ( ErrDuplicateOSDBPubID = errors.New("duplicate osdb_pub_id") ErrDuplicatePubDescription = errors.New("duplicate publication description for this fruit") ) // PublicationRepository is the consumer-defined interface the handler depends on. type PublicationRepository interface { List(ctx context.Context) ([]domain.Publication, error) Get(ctx context.Context, id int) (domain.Publication, error) Create(ctx context.Context, dto domain.PublicationWriteDTO) (domain.Publication, error) Update(ctx context.Context, id int, dto domain.PublicationWriteDTO) (domain.Publication, error) Delete(ctx context.Context, id int) error SetImage(ctx context.Context, pubID int, data []byte) error GetImageData(ctx context.Context, pubID int) ([]byte, error) DeleteImage(ctx context.Context, pubID int) error ListFruits(ctx context.Context, pubID int) ([]domain.PublicationFruit, error) LinkFruit(ctx context.Context, pubID, fruitID int) error UnlinkFruit(ctx context.Context, pubID, fruitID int) error ListDescriptions(ctx context.Context, pubID int) ([]domain.PublicationDescription, error) AddDescription(ctx context.Context, pubID, fruitID int, data []byte) (domain.PublicationDescription, error) GetDescriptionData(ctx context.Context, pubID, descID int) ([]byte, error) DeleteDescription(ctx context.Context, pubID, descID int) error ListFruitImages(ctx context.Context, pubID int) ([]domain.PublicationFruitImage, error) AddFruitImage(ctx context.Context, pubID, fruitID int, filename *string, data []byte) (domain.PublicationFruitImage, error) GetFruitImageData(ctx context.Context, pubID, imgID int) ([]byte, error) GetFruitImageThumbnailData(ctx context.Context, pubID, imgID int) ([]byte, error) DeleteFruitImage(ctx context.Context, pubID, imgID int) error GetFruitDescriptions(ctx context.Context, fruitID int) ([]domain.PublicationDescription, error) GetFruitPublicationImages(ctx context.Context, fruitID int) ([]domain.PublicationFruitImage, error) } type PublicationHandler struct { repo PublicationRepository } func NewPublicationHandler(repo PublicationRepository) *PublicationHandler { return &PublicationHandler{repo: repo} } func validatePublication(dto domain.PublicationWriteDTO) []string { var errs []string if strings.TrimSpace(dto.Title) == "" { errs = append(errs, "title is required") } if strings.TrimSpace(dto.OSDBPubID) == "" { errs = append(errs, "osdb_pub_id is required") } return errs } func mapPubRepoError(c echo.Context, err error) error { switch { case errors.Is(err, ErrNotFound): return c.JSON(http.StatusNotFound, map[string]string{"error": "not found"}) case errors.Is(err, ErrDuplicateOSDBPubID): return c.JSON(http.StatusConflict, map[string]string{"error": "osdb_pub_id already exists"}) case errors.Is(err, ErrDuplicatePubDescription): return c.JSON(http.StatusConflict, map[string]string{"error": "description already exists for this fruit in this publication"}) default: return c.JSON(http.StatusInternalServerError, map[string]string{"error": "internal server error"}) } } func (h *PublicationHandler) List(c echo.Context) error { pubs, err := h.repo.List(c.Request().Context()) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": "internal server error"}) } if pubs == nil { pubs = []domain.Publication{} } return c.JSON(http.StatusOK, pubs) } func (h *PublicationHandler) Get(c echo.Context) error { id, err := parseID(c, "id") if err != nil { return err } pub, err := h.repo.Get(c.Request().Context(), id) if err != nil { return mapPubRepoError(c, err) } return c.JSON(http.StatusOK, pub) } func (h *PublicationHandler) Create(c echo.Context) error { var dto domain.PublicationWriteDTO if err := c.Bind(&dto); err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request body"}) } if errs := validatePublication(dto); len(errs) > 0 { return c.JSON(http.StatusUnprocessableEntity, map[string]interface{}{"errors": errs}) } pub, err := h.repo.Create(c.Request().Context(), dto) if err != nil { return mapPubRepoError(c, err) } return c.JSON(http.StatusCreated, pub) } func (h *PublicationHandler) Update(c echo.Context) error { id, err := parseID(c, "id") if err != nil { return err } var dto domain.PublicationWriteDTO if err := c.Bind(&dto); err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request body"}) } if errs := validatePublication(dto); len(errs) > 0 { return c.JSON(http.StatusUnprocessableEntity, map[string]interface{}{"errors": errs}) } pub, err := h.repo.Update(c.Request().Context(), id, dto) if err != nil { return mapPubRepoError(c, err) } return c.JSON(http.StatusOK, pub) } func (h *PublicationHandler) Delete(c echo.Context) error { id, err := parseID(c, "id") if err != nil { return err } if err := h.repo.Delete(c.Request().Context(), id); err != nil { return mapPubRepoError(c, err) } return c.NoContent(http.StatusNoContent) } func (h *PublicationHandler) UploadCoverImage(c echo.Context) error { pubID, err := parseID(c, "id") if err != nil { return err } file, err := c.FormFile("image") if err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "image file is required"}) } src, err := file.Open() if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": "internal server error"}) } defer src.Close() data, err := io.ReadAll(src) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": "internal server error"}) } if err := h.repo.SetImage(c.Request().Context(), pubID, data); err != nil { return mapPubRepoError(c, err) } return c.JSON(http.StatusCreated, map[string]string{"message": "image uploaded"}) } func (h *PublicationHandler) ServeCoverImage(c echo.Context) error { pubID, err := parseID(c, "id") if err != nil { return err } data, err := h.repo.GetImageData(c.Request().Context(), pubID) if err != nil { return mapPubRepoError(c, err) } contentType := http.DetectContentType(data) return c.Blob(http.StatusOK, contentType, data) } func (h *PublicationHandler) DeleteCoverImage(c echo.Context) error { pubID, err := parseID(c, "id") if err != nil { return err } if err := h.repo.DeleteImage(c.Request().Context(), pubID); err != nil { return mapPubRepoError(c, err) } return c.NoContent(http.StatusNoContent) } func (h *PublicationHandler) ListFruits(c echo.Context) error { pubID, err := parseID(c, "id") if err != nil { return err } fruits, err := h.repo.ListFruits(c.Request().Context(), pubID) if err != nil { return mapPubRepoError(c, err) } if fruits == nil { fruits = []domain.PublicationFruit{} } return c.JSON(http.StatusOK, fruits) } func (h *PublicationHandler) LinkFruit(c echo.Context) error { pubID, err := parseID(c, "id") if err != nil { return err } var body struct { FruitID int `json:"fruit_id"` } if err := c.Bind(&body); err != nil || body.FruitID == 0 { return c.JSON(http.StatusBadRequest, map[string]string{"error": "fruit_id is required"}) } if err := h.repo.LinkFruit(c.Request().Context(), pubID, body.FruitID); err != nil { return mapPubRepoError(c, err) } return c.JSON(http.StatusCreated, map[string]int{"fruit_id": body.FruitID}) } func (h *PublicationHandler) UnlinkFruit(c echo.Context) error { pubID, err := parseID(c, "id") if err != nil { return err } fruitID, err := parseID(c, "fruitId") if err != nil { return err } if err := h.repo.UnlinkFruit(c.Request().Context(), pubID, fruitID); err != nil { return mapPubRepoError(c, err) } return c.NoContent(http.StatusNoContent) } func (h *PublicationHandler) ListDescriptions(c echo.Context) error { pubID, err := parseID(c, "id") if err != nil { return err } descs, err := h.repo.ListDescriptions(c.Request().Context(), pubID) if err != nil { return mapPubRepoError(c, err) } if descs == nil { descs = []domain.PublicationDescription{} } return c.JSON(http.StatusOK, descs) } func (h *PublicationHandler) UploadDescription(c echo.Context) error { pubID, err := parseID(c, "id") if err != nil { return err } fruitIDStr := c.FormValue("fruit_id") fruitID, convErr := strconv.Atoi(fruitIDStr) if convErr != nil || fruitID == 0 { return c.JSON(http.StatusBadRequest, map[string]string{"error": "fruit_id is required"}) } file, err := c.FormFile("pdf") if err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "pdf file is required"}) } src, err := file.Open() if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": "internal server error"}) } defer src.Close() data, err := io.ReadAll(src) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": "internal server error"}) } desc, err := h.repo.AddDescription(c.Request().Context(), pubID, fruitID, data) if err != nil { return mapPubRepoError(c, err) } return c.JSON(http.StatusCreated, desc) } func (h *PublicationHandler) ServeDescription(c echo.Context) error { pubID, err := parseID(c, "id") if err != nil { return err } descID, err := parseID(c, "descId") if err != nil { return err } data, err := h.repo.GetDescriptionData(c.Request().Context(), pubID, descID) if err != nil { return mapPubRepoError(c, err) } return c.Blob(http.StatusOK, "application/pdf", data) } func (h *PublicationHandler) DeleteDescription(c echo.Context) error { pubID, err := parseID(c, "id") if err != nil { return err } descID, err := parseID(c, "descId") if err != nil { return err } if err := h.repo.DeleteDescription(c.Request().Context(), pubID, descID); err != nil { return mapPubRepoError(c, err) } return c.NoContent(http.StatusNoContent) } func (h *PublicationHandler) ListFruitImages(c echo.Context) error { pubID, err := parseID(c, "id") if err != nil { return err } imgs, err := h.repo.ListFruitImages(c.Request().Context(), pubID) if err != nil { return mapPubRepoError(c, err) } if imgs == nil { imgs = []domain.PublicationFruitImage{} } return c.JSON(http.StatusOK, imgs) } func (h *PublicationHandler) UploadFruitImage(c echo.Context) error { pubID, err := parseID(c, "id") if err != nil { return err } fruitIDStr := c.FormValue("fruit_id") fruitID, convErr := strconv.Atoi(fruitIDStr) if convErr != nil || fruitID == 0 { return c.JSON(http.StatusBadRequest, map[string]string{"error": "fruit_id is required"}) } file, err := c.FormFile("image") if err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "image file is required"}) } src, err := file.Open() if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": "internal server error"}) } defer src.Close() data, err := io.ReadAll(src) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": "internal server error"}) } var filename *string if file.Filename != "" { s := file.Filename filename = &s } img, err := h.repo.AddFruitImage(c.Request().Context(), pubID, fruitID, filename, data) if err != nil { return mapPubRepoError(c, err) } return c.JSON(http.StatusCreated, img) } func (h *PublicationHandler) ServeFruitImage(c echo.Context) error { pubID, err := parseID(c, "id") if err != nil { return err } imgID, err := parseID(c, "imgId") if err != nil { return err } data, err := h.repo.GetFruitImageData(c.Request().Context(), pubID, imgID) if err != nil { return mapPubRepoError(c, err) } contentType := http.DetectContentType(data) return c.Blob(http.StatusOK, contentType, data) } func (h *PublicationHandler) ServeFruitImageThumbnail(c echo.Context) error { pubID, err := parseID(c, "id") if err != nil { return err } imgID, err := parseID(c, "imgId") if err != nil { return err } data, err := h.repo.GetFruitImageThumbnailData(c.Request().Context(), pubID, imgID) if err != nil { return mapPubRepoError(c, err) } contentType := http.DetectContentType(data) return c.Blob(http.StatusOK, contentType, data) } func (h *PublicationHandler) DeleteFruitImage(c echo.Context) error { pubID, err := parseID(c, "id") if err != nil { return err } imgID, err := parseID(c, "imgId") if err != nil { return err } if err := h.repo.DeleteFruitImage(c.Request().Context(), pubID, imgID); err != nil { return mapPubRepoError(c, err) } return c.NoContent(http.StatusNoContent) } func (h *PublicationHandler) GetFruitDescriptions(c echo.Context) error { fruitID, err := parseID(c, "id") if err != nil { return err } descs, err := h.repo.GetFruitDescriptions(c.Request().Context(), fruitID) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": "internal server error"}) } if descs == nil { descs = []domain.PublicationDescription{} } return c.JSON(http.StatusOK, descs) } func (h *PublicationHandler) GetFruitPublicationImages(c echo.Context) error { fruitID, err := parseID(c, "id") if err != nil { return err } imgs, err := h.repo.GetFruitPublicationImages(c.Request().Context(), fruitID) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": "internal server error"}) } if imgs == nil { imgs = []domain.PublicationFruitImage{} } return c.JSON(http.StatusOK, imgs) }