Still having the stupid ass nil dereferences, I think I might need to migrate to using success returns instead of pointers. Because they're fucked. And even more so now.
214 lines
7.5 KiB
Go
214 lines
7.5 KiB
Go
package service
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
domainEngagement "github.com/haydenhargreaves/Potion/internal/domain/engagement"
|
|
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
|
|
engagementRepository domainEngagement.EngagementRepository
|
|
}
|
|
|
|
// 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, engagementRepository domainEngagement.EngagementRepository) domain.RecipeService {
|
|
return &RecipeService{
|
|
recipeRepository: recipeRepository,
|
|
engagementRepository: engagementRepository,
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// GetUserViewedRecipes returns a list of the most recent x (limit) recipes viewed by a user, from
|
|
// the provided userId. This will return a list of size 'limit'. Any errors will be bubbled up to
|
|
// the caller.
|
|
func (s *RecipeService) GetUserViewedRecipes(userId, limit int) ([]domain.Recipe, error) {
|
|
engagement, err := s.engagementRepository.GetUserEngagementFiltered(userId, limit, domainEngagement.EngagementViewed)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ids := make([]int, len(engagement))
|
|
for i, eng := range engagement {
|
|
ids[i] = eng.Entity
|
|
}
|
|
|
|
return s.recipeRepository.GetRecipes(ids, &userId)
|
|
}
|
|
|
|
// GetUserMadeRecipes returns a list of the most recent x (limit) recipes made by a user, from the
|
|
// provided userId. This will return a list of size 'limit'. Any errors will be bubbled up to the
|
|
// caller.
|
|
func (s *RecipeService) GetUserMadeRecipes(userId, limit int) ([]domain.Recipe, error) {
|
|
engagement, err := s.engagementRepository.GetUserEngagementFiltered(userId, limit, domainEngagement.EngagementMade)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ids := make([]int, len(engagement))
|
|
for i, eng := range engagement {
|
|
ids[i] = eng.Entity
|
|
}
|
|
|
|
return s.recipeRepository.GetRecipes(ids, &userId)
|
|
}
|
|
|
|
func (s *RecipeService) GetRecipeOfTheWeek(userId *int, date time.Time) (*domain.Recipe, error) {
|
|
return s.recipeRepository.GetRecipeOfTheWeek(userId, date)
|
|
}
|