package utils import ( "context" "fmt" "net/http" "time" "github.com/gin-gonic/gin" "github.com/timetracker/backend/internal/api/responses" "github.com/timetracker/backend/internal/types" ) // ParseID parses an ID from the URL parameter and converts it to a types.ULID func ParseID(c *gin.Context, paramName string) (types.ULID, error) { idStr := c.Param(paramName) return types.ULIDFromString(idStr) } // BindJSON binds the request body to the provided struct func BindJSON(c *gin.Context, obj interface{}) error { if err := c.ShouldBindJSON(obj); err != nil { return fmt.Errorf("invalid request body: %w", err) } return nil } // ConvertToDTO converts a slice of models to a slice of DTOs using the provided conversion function func ConvertToDTO[M any, D any](models []M, convertFn func(*M) D) []D { dtos := make([]D, len(models)) for i, model := range models { // Create a copy of the model to avoid issues with loop variable capture modelCopy := model dtos[i] = convertFn(&modelCopy) } return dtos } // HandleGetAll is a generic function to handle GET all entities endpoints func HandleGetAll[M any, D any]( c *gin.Context, getAllFn func(ctx context.Context) ([]M, error), convertFn func(*M) D, entityName string, ) { // Get entities from the database entities, err := getAllFn(c.Request.Context()) if err != nil { responses.InternalErrorResponse(c, fmt.Sprintf("Error retrieving %s: %s", entityName, err.Error())) return } // Convert to DTOs dtos := ConvertToDTO(entities, convertFn) responses.SuccessResponse(c, 200, dtos) } // HandleGetByID is a generic function to handle GET entity by ID endpoints func HandleGetByID[M any, D any]( c *gin.Context, getByIDFn func(ctx context.Context, id types.ULID) (*M, error), convertFn func(*M) D, entityName string, ) { // Parse ID from URL id, err := ParseID(c, "id") if err != nil { responses.BadRequestResponse(c, fmt.Sprintf("Invalid %s ID format", entityName)) return } // Get entity from the database entity, err := getByIDFn(c.Request.Context(), id) if err != nil { responses.InternalErrorResponse(c, fmt.Sprintf("Error retrieving %s: %s", entityName, err.Error())) return } if entity == nil { responses.NotFoundResponse(c, fmt.Sprintf("%s not found", entityName)) return } // Convert to DTO dto := convertFn(entity) responses.SuccessResponse(c, 200, dto) } // HandleCreate is a generic function to handle POST entity endpoints func HandleCreate[C any, M any, D any]( c *gin.Context, createFn func(ctx context.Context, create C) (*M, error), convertFn func(*M) D, entityName string, ) { // Parse request body var createDTO C if err := BindJSON(c, &createDTO); err != nil { responses.BadRequestResponse(c, err.Error()) return } // Create entity in the database entity, err := createFn(c.Request.Context(), createDTO) if err != nil { responses.InternalErrorResponse(c, fmt.Sprintf("Error creating %s: %s", entityName, err.Error())) return } // Convert to DTO dto := convertFn(entity) responses.SuccessResponse(c, 201, dto) } // HandleDelete is a generic function to handle DELETE entity endpoints func HandleDelete( c *gin.Context, deleteFn func(ctx context.Context, id types.ULID) error, entityName string, ) { // Parse ID from URL id, err := ParseID(c, "id") if err != nil { responses.BadRequestResponse(c, fmt.Sprintf("Invalid %s ID format", entityName)) return } // Delete entity from the database err = deleteFn(c.Request.Context(), id) if err != nil { responses.InternalErrorResponse(c, fmt.Sprintf("Error deleting %s: %s", entityName, err.Error())) return } responses.SuccessResponse(c, 204, nil) } // HandleUpdate is a generic function to handle PUT entity endpoints // It takes a prepareUpdateFn that handles parsing the ID, binding the JSON, and converting the DTO to a model update object func HandleUpdate[U any, M any, D any]( c *gin.Context, updateFn func(ctx context.Context, update U) (*M, error), convertFn func(*M) D, prepareUpdateFn func(*gin.Context) (U, error), entityName string, ) { // Prepare the update object (parse ID, bind JSON, convert DTO to model) update, err := prepareUpdateFn(c) if err != nil { // The prepareUpdateFn should handle setting the appropriate error response return } // Update entity in the database entity, err := updateFn(c.Request.Context(), update) if err != nil { responses.InternalErrorResponse(c, fmt.Sprintf("Error updating %s: %s", entityName, err.Error())) return } if entity == nil { responses.NotFoundResponse(c, fmt.Sprintf("%s not found", entityName)) return } // Convert to DTO dto := convertFn(entity) responses.SuccessResponse(c, http.StatusOK, dto) } // HandleGetByFilter is a generic function to handle GET entities by a filter parameter func HandleGetByFilter[M any, D any]( c *gin.Context, getByFilterFn func(ctx context.Context, filterID types.ULID) ([]M, error), convertFn func(*M) D, entityName string, paramName string, ) { // Parse filter ID from URL filterID, err := ParseID(c, paramName) if err != nil { responses.BadRequestResponse(c, fmt.Sprintf("Invalid %s ID format", paramName)) return } // Get entities from the database entities, err := getByFilterFn(c.Request.Context(), filterID) if err != nil { responses.InternalErrorResponse(c, fmt.Sprintf("Error retrieving %s: %s", entityName, err.Error())) return } // Convert to DTOs dtos := ConvertToDTO(entities, convertFn) responses.SuccessResponse(c, http.StatusOK, dtos) } // HandleGetByUserID is a specialized function to handle GET entities by user ID func HandleGetByUserID[M any, D any]( c *gin.Context, getByUserIDFn func(ctx context.Context, userID types.ULID) ([]M, error), convertFn func(*M) D, entityName string, ) { // Get user ID from context (set by AuthMiddleware) userID, exists := c.Get("userID") if !exists { responses.UnauthorizedResponse(c, "User not authenticated") return } userIDStr, ok := userID.(string) if !ok { responses.InternalErrorResponse(c, "Invalid user ID type in context") return } parsedUserID, err := types.ULIDFromString(userIDStr) if err != nil { responses.InternalErrorResponse(c, fmt.Sprintf("Error parsing user ID: %s", err.Error())) return } // Get entities from the database entities, err := getByUserIDFn(c.Request.Context(), parsedUserID) if err != nil { responses.InternalErrorResponse(c, fmt.Sprintf("Error retrieving %s: %s", entityName, err.Error())) return } // Convert to DTOs dtos := ConvertToDTO(entities, convertFn) responses.SuccessResponse(c, http.StatusOK, dtos) } // HandleGetByDateRange is a specialized function to handle GET entities by date range func HandleGetByDateRange[M any, D any]( c *gin.Context, getByDateRangeFn func(ctx context.Context, start, end time.Time) ([]M, error), convertFn func(*M) D, entityName string, ) { // Parse date range from query parameters startStr := c.Query("start") endStr := c.Query("end") if startStr == "" || endStr == "" { responses.BadRequestResponse(c, "Start and end dates are required") return } start, err := time.Parse(time.RFC3339, startStr) if err != nil { responses.BadRequestResponse(c, "Invalid start date format. Use ISO 8601 format (e.g., 2023-01-01T00:00:00Z)") return } end, err := time.Parse(time.RFC3339, endStr) if err != nil { responses.BadRequestResponse(c, "Invalid end date format. Use ISO 8601 format (e.g., 2023-01-01T00:00:00Z)") return } if end.Before(start) { responses.BadRequestResponse(c, "End date cannot be before start date") return } // Get entities from the database entities, err := getByDateRangeFn(c.Request.Context(), start, end) if err != nil { responses.InternalErrorResponse(c, fmt.Sprintf("Error retrieving %s: %s", entityName, err.Error())) return } // Convert to DTOs dtos := ConvertToDTO(entities, convertFn) responses.SuccessResponse(c, http.StatusOK, dtos) }