package service import ( "errors" "fmt" "net/http" "strconv" "strings" "time" "github.com/gin-gonic/gin" domain "github.com/haydenhargreaves/Potion/internal/domain/recipe" domainServer "github.com/haydenhargreaves/Potion/internal/domain/server" ) // RecipeService implements the domain.RecipeService defined in the domain module. type RecipeService struct { recipeRepository domain.RecipeRepository } // Compile-time check to ensure the RecipeService implements domain.RecipeService var _ domain.RecipeService = (*RecipeService)(nil) // NewRecipeService creates a user service object which can be passed into the context. The service // requires a recipe repository which it will use to hit the database when needed. func NewRecipeService(recipeRepository domain.RecipeRepository) domain.RecipeService { return &RecipeService{recipeRepository: recipeRepository} } // CreateRecipe creates a recipe in the database using the recipe repository. This function requires // all the data to be present, though validation does not occur in this function. However, the UI // will enforce validation, as will the database. Errors will be returned to the called when they // occur. // // TODO: Implement validation in the API. // TODO: Implement image creation. func (s *RecipeService) CreateRecipe(ctx *gin.Context) (*domain.Recipe, error) { // Ensure user is logged in if !domainServer.IsLoggedIn(ctx) { return nil, fmt.Errorf("User is not logged in.") } title := ctx.PostForm("title") description := ctx.PostForm("description") preparation := ctx.PostForm("preparation-time") cook := ctx.PostForm("cook-time") serving := ctx.PostForm("serving-size") category := ctx.PostForm("category") difficulty := ctx.PostForm("difficulty") ingredients := ctx.PostFormArray("ingredients") quantity := ctx.PostFormArray("quantity") instructions := ctx.PostFormArray("instructions") tags := strings.Split(ctx.PostForm("tags"), ",") userId := ctx.MustGet("userId").(int) // Have to get the image differently image, err := ctx.FormFile("image") if err != nil && !errors.Is(err, http.ErrMissingFile) { // Error getting image } // Convert to proper values servingInt, _ := strconv.Atoi(serving) difficultyInt, _ := strconv.Atoi(difficulty) prepInt, _ := strconv.Atoi(preparation) cookInt, _ := strconv.Atoi(cook) var ingredientSlice []domain.RecipeIngredient for i := range len(ingredients) { if strings.TrimSpace(ingredients[i]) != "" { ins := domain.RecipeIngredient{ Name: ingredients[i], Quantity: quantity[i], } ingredientSlice = append(ingredientSlice, ins) } } var instructionSlice []string for _, ins := range instructions { if ins != "" { instructionSlice = append(instructionSlice, ins) } } // Create the recipe recipe := domain.Recipe{ Title: title, Description: description, Instructions: instructionSlice, Serves: servingInt, Difficulty: difficultyInt, Duration: domain.RecipeDuration{ Total: prepInt + cookInt, Prep: prepInt, Cook: cookInt, }, Category: domain.RecipeMeal(category), Ingredients: ingredientSlice, UserId: userId, Created: time.Now(), } if err := s.recipeRepository.CreateRecipe(&recipe); err != nil { return &recipe, err } // TODO: Upload the image if image != nil { } // Create the tags if len(tags) > 0 { if err := s.recipeRepository.CreateRecipeTags(recipe, tags); err != nil { return &recipe, fmt.Errorf("Failed to attach/create tags. %s\n", err.Error()) } } return &recipe, nil } // GetRecipe will get a recipe via its ID. Any errors will be bubbled to the caller. Furthermore, // if the recipe is nil, an error will be returned, so the caller does not need to check for a nil // recipe (e.g., if the error is nil the recipe exists) // // A userId should be provided to allow the favorite status to be updated. Without a userId (nil), // the favorite status will return false, not because its not a favorite, but because it cannot find // out! func (s *RecipeService) GetRecipe(id int, userId *int) (*domain.Recipe, error) { recipe, err := s.recipeRepository.GetRecipe(id, userId) if recipe == nil { return nil, fmt.Errorf("Failed to get recipe from database. Nil result.") } return recipe, err } // SearchRecipes will search the database using the filters provided. The recipes can be passed into // a template and displayed in the UI as the search result. A more detailed definition of the // filters is provided below. // // Each input is given a bit value (e.g., 00001 for 1) and will be passed // back to this handler as an array. The values are then added together // and will result in a integer which represents bit values. These bits // can then be passed to the repository and are then parsed to determine // which filters should be applied. // Parsing these is simple, for each filter option, use the bitwise and (&) // operator with the value we expect for the filter. When 1, we can ensure // the filter is provided. // A function `isBitActive` in the recipe repository provides an example of // testing of testing the filter parsing. // // The favorites parameter is used to only return filters favorited by the userId provided. func (s *RecipeService) SearchRecipes(filters domain.SearchFilters, userId *int, favorites bool) ([]domain.Recipe, error) { return s.recipeRepository.SearchRecipes(filters, userId, favorites) } // GetUserRecipes returns a list of the recipes that the user has created. The user's // ID should be provided. Any errors will be bubbled to the caller. func (s *RecipeService) GetUserRecipes(id int) ([]domain.Recipe, error) { return s.recipeRepository.GetUserRecipes(id) } // GetUserFavoriteRecipes returns a list of the recipes that the user has marked as a // favorite. The user's ID should be provided. Any errors will be bubbled to the caller. func (s *RecipeService) GetUserFavoriteRecipes(id int) ([]domain.Recipe, error) { return s.recipeRepository.GetUserFavoriteRecipes(id) }