The search is nearly complete for the initial implementation. Just need to figure out what to do with the text search provided, make any required UI changes, and eventual implement pagination via a "load more" button.
150 lines
4.8 KiB
Go
150 lines
4.8 KiB
Go
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 and tag 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 {
|
|
}
|
|
|
|
// TODO: Create the tags in the database
|
|
if len(tags) > 0 {
|
|
}
|
|
|
|
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)
|
|
func (s *RecipeService) GetRecipe(id int) (*domain.Recipe, error) {
|
|
recipe, err := s.recipeRepository.GetRecipe(id)
|
|
|
|
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.
|
|
func (s *RecipeService) SearchRecipes(filters domain.SearchFilters) ([]domain.Recipe, error) {
|
|
return s.recipeRepository.SearchRecipes(filters)
|
|
}
|