Furthermore, not sure how we are going to handle the searching. Maybe a full-text search index? For now, it has been ignored, but the filters seem to be working properly.
176 lines
5.3 KiB
Go
176 lines
5.3 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
|
|
}
|
|
|
|
// toBits converts an array of stringified numbers into a single summed value
|
|
func toBits(arr []string) (bits int) {
|
|
for _, x := range arr {
|
|
num, _ := strconv.Atoi(x)
|
|
bits += num
|
|
}
|
|
return
|
|
}
|
|
|
|
// isBitActive returns true when the bit at pos (0 indexed) is true.
|
|
func isBitActive(bits, pos int) bool {
|
|
return (bits>>pos)&1 == 1
|
|
}
|
|
|
|
func (s *RecipeService) SearchRecipes(ctx *gin.Context) ([]domain.Recipe, error) {
|
|
// NOTE: How are the filters handled?
|
|
// 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 above (isBitActive) provides an example of testing of testing
|
|
// the filter parsing.
|
|
|
|
search := ctx.PostForm("search") // string, search query for titles
|
|
meal := toBits(ctx.PostFormArray("meal"))
|
|
time := toBits(ctx.PostFormArray("time"))
|
|
difficulty := toBits(ctx.PostFormArray("difficulty"))
|
|
serving := toBits(ctx.PostFormArray("serving"))
|
|
|
|
filters := domain.SearchFilters{
|
|
Search: search,
|
|
MealType: meal,
|
|
Time: time,
|
|
Difficulty: difficulty,
|
|
ServingSize: serving,
|
|
}
|
|
|
|
return s.recipeRepository.SearchRecipes(filters)
|
|
}
|