Potion/internal/app/service/recipe_service.go
Hayden Hargreaves ced9da5bf2 (DB/FEAT): Recipe tags have been implemented!
This is part of the step towards finishing the creation wizard, all that
is left is the image. However, that is a bigger problem since it
requires a file store and file server. But for now, tags are implemented
and working!
2025-07-12 12:50:26 -07:00

153 lines
5.0 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 {
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)
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)
}