package gui
import (
"go-shopping-list/pkg/common"
"go-shopping-list/pkg/recipe"
"log"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/widget"
)
const (
screenWidth = 600
screenHeight = 500
progressBarHeight = 50
submitBarHeight = 50
recipeListHeight = screenHeight - progressBarHeight - submitBarHeight
progressBarEmpty = 0.0
progressBarFull = 1.0
listContainerCols = 1
listCols = 2
listBoxIndex = 0
checkIndex = 0
labelIndex = 1
)
type screen struct {
p *widget.ProgressBar
l *widget.Label
}
func (s *screen) UpdateProgessBar(percent float64) {
s.p.SetValue(percent)
s.p.Refresh()
}
func (s *screen) UpdateLabel(msg string) {
s.l.SetText(msg)
s.l.Refresh()
}
func createSubmitButton(s common.ScreenInterface, wf common.WorkflowInterface, fr recipe.FileReader, recipes map[string]bool, recipeMap map[string]recipe.Recipe) *widget.Button {
b := widget.NewButtonWithIcon("Add To Shopping List", fyne.NewMenuItemSeparator().Icon, func() {
selectedRecipes := []string{}
for k, v := range recipes {
if v {
selectedRecipes = append(selectedRecipes, k)
}
}
err := wf.SubmitShoppingList(s, wf, fr, selectedRecipes, recipeMap)
if err != nil {
log.Println(err)
}
})
b.Importance = widget.HighImportance
return b
}
// NewApp returns a fyne.Window
func NewApp(recipes []recipe.Recipe, recipeMap map[string]recipe.Recipe, wf common.WorkflowInterface) fyne.Window {
myApp := app.New()
myWindow := myApp.NewWindow("List Widget")
label := widget.NewLabel("Click a recipe to add the ingredients...")
// Progress bar for adding ings
p := widget.NewProgressBar()
s := &screen{
l: label,
p: p,
}
fr := &recipe.FileInteractionImpl{}
// Recipe list with all recipes
var recipesAsStrings []string
for _, v := range recipes {
recipesAsStrings = append(recipesAsStrings, v.Name)
}
selectedRecipe := map[string]bool{}
recipeList, selectedRecipe := createNewListOfRecipes(selectedRecipe, recipesAsStrings)
submit := createSubmitButton(s, wf, fr, selectedRecipe, recipeMap)
gridTop := container.New(layout.NewGridWrapLayout(fyne.NewSize(screenWidth, progressBarHeight)), label, p)
grid := container.New(layout.NewGridWrapLayout(fyne.NewSize(screenWidth, recipeListHeight)), recipeList)
gridBottum := container.New(layout.NewGridWrapLayout(fyne.NewSize(screenWidth, submitBarHeight)), submit)
masterGrid := container.New(layout.NewVBoxLayout(), gridTop, grid, gridBottum)
myWindow.Resize(fyne.Size{Width: screenWidth, Height: screenHeight})
myWindow.SetContent(masterGrid)
return myWindow
}
func createNewListOfRecipes(selectedRecipe map[string]bool, recipesStr []string) (*widget.List, map[string]bool) {
for _, r := range recipesStr {
selectedRecipe[r] = false
}
l := widget.NewList(
func() int {
return len(recipesStr)
},
func() fyne.CanvasObject {
hbox := container.NewGridWithColumns(listContainerCols)
hbox1 := container.NewGridWithColumns(listCols)
hbox1.Add(widget.NewCheck("", func(bool) {}))
hbox1.Add(widget.NewLabel("table"))
hbox.Add(hbox1)
return hbox
},
func(li widget.ListItemID, o fyne.CanvasObject) {
// Update Checkbox
listContainer := o.(*fyne.Container).Objects[listBoxIndex].(*fyne.Container)
recipeCheckBox := listContainer.Objects[checkIndex].(*widget.Check)
recipeCheckBox.Checked = selectedRecipe[recipesStr[li]]
recipeCheckBox.OnChanged = func(b bool) {
selectedRecipe[recipesStr[li]] = b
}
recipeCheckBox.Refresh()
// Update label
listContainer.Objects[labelIndex].(*widget.Label).SetText(recipesStr[li])
})
return l, selectedRecipe
}
package recipe
import (
fs "io/fs"
"time"
)
const (
mockIntReturn = 1
mockDateYear = 1998
mockDateMonth = time.April
mockDateDay = 26
mockDateHour = 0
mockDateMin = 0
mockDateSec = 0
mockDateNsec = 0
)
type mockFileInfo struct{}
func (*mockFileInfo) Name() string {
return "DURIAN"
}
func (*mockFileInfo) Size() int64 {
return mockIntReturn
} // length in bytes for regular files; system-dependent for others
func (*mockFileInfo) Mode() fs.FileMode {
return mockIntReturn
}
func (*mockFileInfo) ModTime() time.Time {
return time.Date(mockDateYear, mockDateMonth, mockDateDay, mockDateHour, mockDateMin, mockDateSec, mockDateNsec, time.UTC)
}
func (*mockFileInfo) IsDir() bool {
return false
}
func (*mockFileInfo) Sys() interface{} {
return "APPLE"
}
// Code generated by mockery v1.0.0. DO NOT EDIT.
package recipe
import (
fs "io/fs"
mock "github.com/stretchr/testify/mock"
)
// MockFileReader is an autogenerated mock type for the FileReader type
type MockFileReader struct {
mock.Mock
}
// IncrementPopularity provides a mock function with given fields: f, recipeName
func (_m *MockFileReader) IncrementPopularity(f FileReader, recipeName string) error {
ret := _m.Called(f, recipeName)
var r0 error
if rf, ok := ret.Get(0).(func(FileReader, string) error); ok {
r0 = rf(f, recipeName)
} else {
r0 = ret.Error(0)
}
return r0
}
// getPopularity provides a mock function with given fields: f, recipeName
func (_m *MockFileReader) getPopularity(f FileReader, recipeName string) (int, error) {
ret := _m.Called(f, recipeName)
var r0 int
if rf, ok := ret.Get(0).(func(FileReader, string) int); ok {
r0 = rf(f, recipeName)
} else {
r0 = ret.Get(0).(int)
}
var r1 error
if rf, ok := ret.Get(1).(func(FileReader, string) error); ok {
r1 = rf(f, recipeName)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// loadPopularityFile provides a mock function with given fields: f
func (_m *MockFileReader) loadPopularityFile(f FileReader) (PopularityFile, error) {
ret := _m.Called(f)
var r0 PopularityFile
if rf, ok := ret.Get(0).(func(FileReader) PopularityFile); ok {
r0 = rf(f)
} else {
r0 = ret.Get(0).(PopularityFile)
}
var r1 error
if rf, ok := ret.Get(1).(func(FileReader) error); ok {
r1 = rf(f)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// loadRecipeFile provides a mock function with given fields: f, fileName
func (_m *MockFileReader) loadRecipeFile(f FileReader, fileName fs.FileInfo) (Recipe, error) {
ret := _m.Called(f, fileName)
var r0 Recipe
if rf, ok := ret.Get(0).(func(FileReader, fs.FileInfo) Recipe); ok {
r0 = rf(f, fileName)
} else {
r0 = ret.Get(0).(Recipe)
}
var r1 error
if rf, ok := ret.Get(1).(func(FileReader, fs.FileInfo) error); ok {
r1 = rf(f, fileName)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// makeDir provides a mock function with given fields:
func (_m *MockFileReader) makeDir() error {
ret := _m.Called()
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// marshallJSON provides a mock function with given fields: pop
func (_m *MockFileReader) marshallJSON(pop PopularityFile) ([]byte, error) {
ret := _m.Called(pop)
var r0 []byte
if rf, ok := ret.Get(0).(func(PopularityFile) []byte); ok {
r0 = rf(pop)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]byte)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(PopularityFile) error); ok {
r1 = rf(pop)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// readFile provides a mock function with given fields: filePath
func (_m *MockFileReader) readFile(filePath string) ([]byte, error) {
ret := _m.Called(filePath)
var r0 []byte
if rf, ok := ret.Get(0).(func(string) []byte); ok {
r0 = rf(filePath)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]byte)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(filePath)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// readRecipeDirectory provides a mock function with given fields:
func (_m *MockFileReader) readRecipeDirectory() ([]fs.FileInfo, error) {
ret := _m.Called()
var r0 []fs.FileInfo
if rf, ok := ret.Get(0).(func() []fs.FileInfo); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]fs.FileInfo)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// unmarshallJSONToPopularity provides a mock function with given fields: file
func (_m *MockFileReader) unmarshallJSONToPopularity(file []byte) (PopularityFile, error) {
ret := _m.Called(file)
var r0 PopularityFile
if rf, ok := ret.Get(0).(func([]byte) PopularityFile); ok {
r0 = rf(file)
} else {
r0 = ret.Get(0).(PopularityFile)
}
var r1 error
if rf, ok := ret.Get(1).(func([]byte) error); ok {
r1 = rf(file)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// unmarshallJSONToRecipe provides a mock function with given fields: file
func (_m *MockFileReader) unmarshallJSONToRecipe(file []byte) (Recipe, error) {
ret := _m.Called(file)
var r0 Recipe
if rf, ok := ret.Get(0).(func([]byte) Recipe); ok {
r0 = rf(file)
} else {
r0 = ret.Get(0).(Recipe)
}
var r1 error
if rf, ok := ret.Get(1).(func([]byte) error); ok {
r1 = rf(file)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// writeFile provides a mock function with given fields: newFile
func (_m *MockFileReader) writeFile(newFile []byte) error {
ret := _m.Called(newFile)
var r0 error
if rf, ok := ret.Get(0).(func([]byte) error); ok {
r0 = rf(newFile)
} else {
r0 = ret.Error(0)
}
return r0
}
// writePopularityFile provides a mock function with given fields: f, pop
func (_m *MockFileReader) writePopularityFile(f FileReader, pop PopularityFile) error {
ret := _m.Called(f, pop)
var r0 error
if rf, ok := ret.Get(0).(func(FileReader, PopularityFile) error); ok {
r0 = rf(f, pop)
} else {
r0 = ret.Error(0)
}
return r0
}
package recipe
import (
"fmt"
"log"
"strconv"
"github.com/bradfitz/slice" //nolint:all
)
const (
floatBitSize = 32
)
func convertMapToSlice(im map[string]Ingredient) []Ingredient {
ingsToReturn := []Ingredient{}
for _, ing := range im {
ingsToReturn = append(ingsToReturn, ing)
}
slice.Sort(ingsToReturn[:], func(i, j int) bool { //nolint:all
return ingsToReturn[i].UnitSize > ingsToReturn[j].UnitSize
})
return ingsToReturn
}
func calculateNewSize(uniqueIngredients map[string]Ingredient, combineTypeName, currentIngSize string) (string, error) {
currentSize, err := strconv.ParseFloat(uniqueIngredients[combineTypeName].UnitSize, floatBitSize)
if err != nil {
return "", err
}
sizeToAdd, err := strconv.ParseFloat(currentIngSize, floatBitSize)
if err != nil {
return "nil", err
}
return fmt.Sprintf("%.2f", currentSize+sizeToAdd), nil
}
// CombineRecipesToIngredients combines the ingredients within mutiple recipes
func CombineRecipesToIngredients(recipes []Recipe) ([]Ingredient, error) {
log.Printf("Combining %d recipes", len(recipes))
defer func() {
log.Printf("Finished combining %d recipes", len(recipes))
}()
uniqueIngredients := map[string]Ingredient{}
for _, r := range recipes {
for _, i := range r.Ings {
// unique identifier is type and name, as could have grams, ounces, kg's of same ing
combineTypeName := fmt.Sprintf("%s-%s", i.UnitType, i.IngredientName)
if _, ok := uniqueIngredients[combineTypeName]; !ok {
uniqueIngredients[combineTypeName] = i
} else {
oldIng := uniqueIngredients[combineTypeName]
newSize, err := calculateNewSize(uniqueIngredients, combineTypeName, i.UnitSize)
if err != nil {
return nil, err
}
oldIng.UnitSize = newSize
uniqueIngredients[combineTypeName] = oldIng
}
}
}
return convertMapToSlice(uniqueIngredients), nil
}
package recipe
import (
"encoding/json"
"fmt"
"io/fs" //nolint:all
"io/ioutil" //nolint:all
"log"
"os"
"github.com/bradfitz/slice" //nolint:all
)
const (
popularityFileName = "popularity.json"
recipeFolder = "recipes/"
errorIntReturn = -1
defaultCount = 0
minimumSliceLength = 1
emptyString = ""
writePermissionCode = 0644
)
// FileReader deals with interactions with files
//
//go:generate go run github.com/vektra/mockery/cmd/mockery -name FileReader -inpkg --filename file_reader_mock.go
type FileReader interface {
getPopularity(f FileReader, recipeName string) (int, error)
IncrementPopularity(f FileReader, recipeName string) error
loadPopularityFile(f FileReader) (PopularityFile, error)
marshallJSON(pop PopularityFile) ([]byte, error)
readRecipeDirectory() ([]fs.FileInfo, error)
loadRecipeFile(f FileReader, fileName fs.FileInfo) (Recipe, error)
readFile(filePath string) ([]byte, error)
unmarshallJSONToPopularity(file []byte) (PopularityFile, error)
unmarshallJSONToRecipe(file []byte) (Recipe, error)
writePopularityFile(f FileReader, pop PopularityFile) error
writeFile(newFile []byte) error
makeDir() error
}
// FileInteractionImpl is a struct to implement FileReader
type FileInteractionImpl struct{}
func (*FileInteractionImpl) getPopularity(f FileReader, recipeName string) (int, error) {
return getPopularityImpl(f, recipeName)
}
// IncrementPopularity incrementes the popularity count of a recipe by 1
func (*FileInteractionImpl) IncrementPopularity(f FileReader, recipeName string) error {
return incrementPopularityImpl(f, recipeName)
}
func (*FileInteractionImpl) loadPopularityFile(f FileReader) (PopularityFile, error) {
return loadPopularityFileImpl(f)
}
func (*FileInteractionImpl) makeDir() error {
return os.MkdirAll(recipeFolder, os.ModePerm)
}
func (*FileInteractionImpl) readRecipeDirectory() ([]fs.FileInfo, error) {
return ioutil.ReadDir(recipeFolder)
}
func (*FileInteractionImpl) loadRecipeFile(f FileReader, fileName fs.FileInfo) (Recipe, error) {
return loadRecipeFileImpl(f, fileName)
}
func (*FileInteractionImpl) readFile(filePath string) ([]byte, error) {
return ioutil.ReadFile(filePath)
}
func (*FileInteractionImpl) unmarshallJSONToPopularity(file []byte) (PopularityFile, error) {
popularity := PopularityFile{}
return popularity, json.Unmarshal([]byte(file), &popularity)
}
func (*FileInteractionImpl) unmarshallJSONToRecipe(file []byte) (Recipe, error) {
recipe := Recipe{}
return recipe, json.Unmarshal([]byte(file), &recipe)
}
func (*FileInteractionImpl) marshallJSON(pop PopularityFile) ([]byte, error) {
return json.MarshalIndent(pop, emptyString, " ")
}
func (*FileInteractionImpl) writeFile(newFile []byte) error {
return ioutil.WriteFile(popularityFileName, newFile, writePermissionCode)
}
func (*FileInteractionImpl) writePopularityFile(f FileReader, pop PopularityFile) error {
return writePopularityFileImpl(f, pop)
}
// Implementations of functions
func incrementPopularityImpl(f FileReader, recipeName string) error {
pop, err := f.loadPopularityFile(f)
if err != nil {
return err
}
updateIndex := errorIntReturn
for i, p := range pop.Pop {
if recipeName == p.Name {
updateIndex = i
}
}
pop.Pop[updateIndex].Count++
return f.writePopularityFile(f, pop)
}
func getPopularityImpl(f FileReader, recipeName string) (int, error) {
pop, err := f.loadPopularityFile(f)
if err != nil {
log.Printf("error unmarshalling file=%s", popularityFileName)
return errorIntReturn, err
}
mapOfPops := map[string]int{}
for _, p := range pop.Pop {
mapOfPops[p.Name] = p.Count
}
if val, ok := mapOfPops[recipeName]; ok {
return val, nil
}
pop.Pop = append(pop.Pop, Popularity{Name: recipeName, Count: defaultCount})
return defaultCount, f.writePopularityFile(f, pop)
}
func loadPopularityFileImpl(f FileReader) (PopularityFile, error) {
file, err := f.readFile(popularityFileName)
if err != nil {
return PopularityFile{}, err
}
return f.unmarshallJSONToPopularity(file)
}
func loadRecipeFileImpl(f FileReader, fileName fs.FileInfo) (Recipe, error) {
file, err := f.readFile(fmt.Sprintf("recipes/%s", fileName.Name()))
if err != nil {
return Recipe{}, err
}
return f.unmarshallJSONToRecipe(file)
}
func writePopularityFileImpl(f FileReader, pop PopularityFile) error {
newFile, err := f.marshallJSON(pop)
if err != nil {
return err
}
return f.writeFile(newFile)
}
func validateRecipe(f FileReader, uniqueRecipeNames map[string]Recipe, fileName fs.FileInfo) (Recipe, error) {
recipe, err := f.loadRecipeFile(f, fileName)
if err != nil {
return Recipe{}, err
}
if recipe.Name == emptyString {
return Recipe{}, fmt.Errorf("file-name=%s is missing a recipe name", fileName.Name())
}
// Check if duplicate recipe name
if _, ok := uniqueRecipeNames[recipe.Name]; ok {
return Recipe{}, fmt.Errorf("duplicate recipe name detected. file-name=%s", fileName.Name())
}
if len(recipe.Ings) < minimumSliceLength {
return Recipe{}, fmt.Errorf("file-name=%s has 0 ingredients", fileName.Name())
}
for _, ing := range recipe.Ings {
if ing.IngredientName == emptyString {
return Recipe{}, fmt.Errorf("file-name=%s ing=%s with nil name", fileName.Name(), ing)
}
}
if len(recipe.Ings) < minimumSliceLength {
return Recipe{}, fmt.Errorf("recipe=%s has 0 ingredients", recipe.Name)
}
for _, ing := range recipe.Ings {
if ing.IngredientName == emptyString {
return Recipe{}, fmt.Errorf("recipe=%s ing=%s with nil name", recipe.Name, ing)
}
}
recipe.Count, err = f.getPopularity(f, recipe.Name)
if err != nil {
return Recipe{}, err
}
return recipe, nil
}
// ProcessRecipes processes recipe JSON files from the recipe folder
func ProcessRecipes(f FileReader) ([]Recipe, map[string]Recipe, error) {
if err := f.makeDir(); err != nil {
return nil, nil, err
}
files, err := f.readRecipeDirectory()
if err != nil {
return nil, nil, err
}
uniqueRecipeNames := map[string]Recipe{}
// Process every file and put into Recipe strucr
allRecipes := []Recipe{}
for _, fileName := range files {
if !fileName.IsDir() {
recipe, err := validateRecipe(f, uniqueRecipeNames, fileName)
if err != nil {
return nil, nil, err
}
allRecipes = append(allRecipes, recipe)
uniqueRecipeNames[recipe.Name] = recipe
}
}
slice.Sort(allRecipes[:], func(i, j int) bool { //nolint:all
return allRecipes[i].Count > allRecipes[j].Count
})
log.Printf("amount of recipes=%d", len(allRecipes))
return allRecipes, uniqueRecipeNames, nil
}
package recipe
import "fmt"
// Recipe represents a recipe JSON file
type Recipe struct {
Name string `json:"recipe_name"`
Ings []Ingredient `json:"ingredients"`
Meth []string `json:"method"`
Count int
}
// PopularityFile represents the popularity.json file
type PopularityFile struct {
Pop []Popularity `json:"popularity"`
}
// Popularity contains a recipe name and popularity count
type Popularity struct {
Name string `json:"name"`
Count int `json:"count"`
}
// Ingredient represents an ingredient from a recipe
type Ingredient struct {
UnitSize string `json:"unit_size"`
UnitType string `json:"unit_type"`
IngredientName string `json:"ingredient_name"`
}
func (i *Ingredient) String() string {
return fmt.Sprintf("%s %s %s", i.UnitSize, i.UnitType, i.IngredientName)
}
// Code generated by mockery v1.0.0. DO NOT EDIT.
package workflow
import (
mock "github.com/stretchr/testify/mock"
excelize "github.com/xuri/excelize/v2"
)
// mockExcel is an autogenerated mock type for the excel type
type mockExcel struct {
mock.Mock
}
// newFile provides a mock function with given fields:
func (_m *mockExcel) newFile() *excelize.File {
ret := _m.Called()
var r0 *excelize.File
if rf, ok := ret.Get(0).(func() *excelize.File); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*excelize.File)
}
}
return r0
}
// saveAs provides a mock function with given fields: f, name, opt
func (_m *mockExcel) saveAs(f *excelize.File, name string, opt ...excelize.Options) error {
_va := make([]interface{}, len(opt))
for _i := range opt {
_va[_i] = opt[_i]
}
var _ca []interface{}
_ca = append(_ca, f, name)
_ca = append(_ca, _va...)
ret := _m.Called(_ca...)
var r0 error
if rf, ok := ret.Get(0).(func(*excelize.File, string, ...excelize.Options) error); ok {
r0 = rf(f, name, opt...)
} else {
r0 = ret.Error(0)
}
return r0
}
// setCellValue provides a mock function with given fields: f, sheet, axis, value
func (_m *mockExcel) setCellValue(f *excelize.File, sheet string, axis string, value interface{}) error {
ret := _m.Called(f, sheet, axis, value)
var r0 error
if rf, ok := ret.Get(0).(func(*excelize.File, string, string, interface{}) error); ok {
r0 = rf(f, sheet, axis, value)
} else {
r0 = ret.Error(0)
}
return r0
}
// Package workflow contains file excel workflow contains code which generates an excel spreadsheet with list of ingredients
package workflow
import (
"fmt"
"go-shopping-list/pkg/common"
"go-shopping-list/pkg/recipe"
"log"
"os"
"time"
excelize "github.com/xuri/excelize/v2"
)
const (
titleColRow = "B2"
titleVal = "INGREDIENTS TO BUY"
dateColRow = "B3"
ingsRowStart = 5
ingsColStart = "B"
listFolder = "excel-lists"
sheetName = "Sheet1"
ingAddedStartIndex = 0
)
// excelWorkflow will create an excel sheet with ingredients
type excelWorkflow struct{}
// SubmitShoppingList combines recipes together and submits a shopping list
func (*excelWorkflow) SubmitShoppingList(s common.ScreenInterface, wf common.WorkflowInterface, fr recipe.FileReader, recipes []string, recipeMap map[string]recipe.Recipe) error {
return SubmitShoppingList(s, wf, fr, recipes, recipeMap)
}
// AddIngredientsToReminders adds ingredients to the list
func (*excelWorkflow) AddIngredientsToReminders(ings []recipe.Ingredient, s common.ScreenInterface, _ common.WorkflowInterface) error {
year, month, day := time.Now().Date()
dateString := fmt.Sprintf("%d-%d-%d", year, month, day)
return createExcelSheet(s, &excelImpl{}, ings, dateString)
}
// RunReminder not used
func (*excelWorkflow) RunReminder(_ common.ScreenInterface, _ recipe.Ingredient) error {
return nil
}
//go:generate go run github.com/vektra/mockery/cmd/mockery -name excel -inpkg --filename excel_mock.go
type excel interface {
newFile() *excelize.File
setCellValue(f *excelize.File, sheet string, axis string, value interface{}) error
saveAs(f *excelize.File, name string, opt ...excelize.Options) error
}
type excelImpl struct{}
func (*excelImpl) newFile() *excelize.File {
return excelize.NewFile()
}
func (*excelImpl) setCellValue(f *excelize.File, sheet string, axis string, value interface{}) error {
return f.SetCellValue(sheet, axis, value)
}
func (*excelImpl) saveAs(f *excelize.File, name string, opt ...excelize.Options) error {
err := os.MkdirAll(listFolder, os.ModePerm)
if err != nil {
return err
}
return f.SaveAs(name, opt...)
}
func createExcelSheet(s common.ScreenInterface, e excel, ings []recipe.Ingredient, dateString string) error {
f := e.newFile()
if err := e.setCellValue(f, sheetName, titleColRow, titleVal); err != nil {
return err
}
if err := e.setCellValue(f, sheetName, dateColRow, dateString); err != nil {
return err
}
ingAdded := ingAddedStartIndex
row := ingsRowStart
for _, ing := range ings {
log.Printf("ingredient=%s status=IN PROGRESS", ing.String())
cellLocation := fmt.Sprintf("%s%d", ingsColStart, row)
cellValue := ing.String()
log.Printf("loc=%s val=%s", cellLocation, cellValue)
if err := e.setCellValue(f, sheetName, cellLocation, cellValue); err != nil {
return err
}
row++
ingAdded++
progress := float64(ingAdded) / float64(len(ings))
s.UpdateProgessBar(progress)
log.Printf("ingredient=%s status=DONE progress=%.2f", ing.String(), progress)
}
listName := fmt.Sprintf("%s/%s.xlsx", listFolder, dateString)
return e.saveAs(f, listName)
}
// Code generated by mockery v1.0.0. DO NOT EDIT.
package workflow
import (
fs "io/fs"
mock "github.com/stretchr/testify/mock"
)
// mockFileChecker is an autogenerated mock type for the fileChecker type
type mockFileChecker struct {
mock.Mock
}
// checkWorkflowExists provides a mock function with given fields: f
func (_m *mockFileChecker) checkWorkflowExists(f fileChecker) (bool, error) {
ret := _m.Called(f)
var r0 bool
if rf, ok := ret.Get(0).(func(fileChecker) bool); ok {
r0 = rf(f)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(fileChecker) error); ok {
r1 = rf(f)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// isNotExist provides a mock function with given fields: err
func (_m *mockFileChecker) isNotExist(err error) bool {
ret := _m.Called(err)
var r0 bool
if rf, ok := ret.Get(0).(func(error) bool); ok {
r0 = rf(err)
} else {
r0 = ret.Get(0).(bool)
}
return r0
}
// stat provides a mock function with given fields: name
func (_m *mockFileChecker) stat(name string) (fs.FileInfo, error) {
ret := _m.Called(name)
var r0 fs.FileInfo
if rf, ok := ret.Get(0).(func(string) fs.FileInfo); ok {
r0 = rf(name)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(fs.FileInfo)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(name)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
package workflow
import (
"fmt"
"go-shopping-list/pkg/common"
"go-shopping-list/pkg/recipe"
"os/exec"
)
var execCommand = exec.Command
type macWorkflow struct{}
// SubmitShoppingList combines recipes together and submits a shopping list
func (*macWorkflow) SubmitShoppingList(s common.ScreenInterface, wf common.WorkflowInterface, fr recipe.FileReader, recipes []string, recipeMap map[string]recipe.Recipe) error {
return SubmitShoppingList(s, wf, fr, recipes, recipeMap)
}
// AddIngredientsToReminders adds ingredients to the list
func (*macWorkflow) AddIngredientsToReminders(ings []recipe.Ingredient, s common.ScreenInterface, w common.WorkflowInterface) error {
return AddIngredientsToReminders(ings, s, w)
}
// RunReminder simulates adding a ing to reminders
func (*macWorkflow) RunReminder(s common.ScreenInterface, currentIng recipe.Ingredient) error {
cmd := execCommand("automator", "-i", fmt.Sprintf(`"%s"`, currentIng.String()), "shopping.workflow")
_, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("error adding the following ingredient=%s err=%w", currentIng.String(), err)
}
s.UpdateLabel(fmt.Sprintf("Added Ingredient: %s", currentIng.String()))
return nil
}
// Package workflow contains file excel workflow contains code which generates an excel spreadsheet with list of ingredients
package workflow
import (
"go-shopping-list/pkg/common"
"go-shopping-list/pkg/recipe"
"log"
"math/rand"
"time"
)
// TerminalFakeWorkflow can be used to just print to termnial
type TerminalFakeWorkflow struct{}
// SubmitShoppingList combines recipes together and submits a shopping list
func (*TerminalFakeWorkflow) SubmitShoppingList(s common.ScreenInterface, wf common.WorkflowInterface, fr recipe.FileReader, recipes []string, recipeMap map[string]recipe.Recipe) error {
return SubmitShoppingList(s, wf, fr, recipes, recipeMap)
}
// AddIngredientsToReminders adds ingredients to the list
func (*TerminalFakeWorkflow) AddIngredientsToReminders(ings []recipe.Ingredient, s common.ScreenInterface, w common.WorkflowInterface) error {
return AddIngredientsToReminders(ings, s, w)
}
// RunReminder simulates adding a ing to reminders
func (*TerminalFakeWorkflow) RunReminder(_ common.ScreenInterface, currentIng recipe.Ingredient) error {
log.Printf("PRETENDING TO ADD INGREDIENT=%s", currentIng.String())
millisecondsToWait := rand.Intn(maxMilliseconds-minMilliseconds) + minMilliseconds
time.Sleep(time.Millisecond * time.Duration(millisecondsToWait))
// The below line creates a bug. I think because race conditions. Maybe I should implement mutex?
// s.UpdateLabel(fmt.Sprintf("Added Ingredient: %s", currentIng.String()))
return nil
}
// Package workflow is used by all workflow files
package workflow
import (
"go-shopping-list/pkg/common"
"go-shopping-list/pkg/recipe"
"io/fs"
"log"
"os"
"golang.org/x/sync/errgroup"
)
const (
workflowName = "shopping.workflow"
macOSName = "darwin"
minMilliseconds = 100
maxMilliseconds = 500
numOfGoRoutines = 10
ingredientsCountStart = 0
progressBarEmpty = 0.0
progressBarFull = 1.0
recipeFinishLabel = "Finished. Select another recipe to add more."
)
// NewWorkflow will return a mac workflow if workflow file present and running on mac
// Else will return terminal workflow which prints to terminal
func NewWorkflow(f fileChecker, osString string) (common.WorkflowInterface, error) {
workflowPresent, err := f.checkWorkflowExists(f)
if err != nil {
return nil, err
}
if workflowPresent {
if osString == macOSName {
log.Println("Using mac workflow to create reminders")
return &macWorkflow{}, nil
}
log.Println("Not running on mac!")
} else {
log.Println("No workflow found!")
}
log.Println("Using excel workflow")
return &excelWorkflow{}, nil
}
// CheckWorkflow is struct used to check if the shopping.workflow file exists
type CheckWorkflow struct{}
//go:generate go run github.com/vektra/mockery/cmd/mockery -name fileChecker -inpkg --filename file_checker_mock.go
type fileChecker interface {
checkWorkflowExists(f fileChecker) (bool, error)
stat(name string) (fs.FileInfo, error)
isNotExist(err error) bool
}
// checkWorkflowExists tests to see if the mac workflow exists
func (*CheckWorkflow) checkWorkflowExists(f fileChecker) (bool, error) {
return checkWorkflowExistsImpl(f)
}
func (*CheckWorkflow) stat(name string) (fs.FileInfo, error) {
return os.Stat(name)
}
func (*CheckWorkflow) isNotExist(err error) bool {
return os.IsNotExist(err)
}
func checkWorkflowExistsImpl(f fileChecker) (bool, error) {
_, err := f.stat(workflowName)
if err == nil {
return true, nil
}
if f.isNotExist(err) {
return false, nil
}
return false, err
}
// SubmitShoppingList combines recipes and then creates shopping list
func SubmitShoppingList(s common.ScreenInterface, wf common.WorkflowInterface, fr recipe.FileReader, recipes []string, recipeMap map[string]recipe.Recipe) error {
log.Println("Currently selected Recipes:")
recipesSelected := []recipe.Recipe{}
for _, v := range recipes {
log.Println(v)
if r, ok := recipeMap[v]; ok {
recipesSelected = append(recipesSelected, r)
if err := fr.IncrementPopularity(fr, r.Name); err != nil {
return err
}
}
}
ings, err := recipe.CombineRecipesToIngredients(recipesSelected)
if err != nil {
return err
}
return wf.AddIngredientsToReminders(ings, s, wf)
}
func ingQueue(ings []recipe.Ingredient, c chan<- recipe.Ingredient) {
defer close(c)
for _, ing := range ings {
c <- ing
}
}
func ingSend(s common.ScreenInterface, w common.WorkflowInterface, c <-chan recipe.Ingredient, ingAdded *int, totalIngs int) error {
for ing := range c {
log.Printf("ingredient=%s status=IN PROGRESS", ing.String())
if err := w.RunReminder(s, ing); err != nil {
return err
}
*ingAdded++
progress := float64(*ingAdded) / float64(totalIngs)
s.UpdateProgessBar(progress)
log.Printf("ingredient=%s status=DONE progress=%.2f", ing.String(), progress)
}
return nil
}
// AddIngredientsToReminders adds ingredients to reminders app
func AddIngredientsToReminders(ings []recipe.Ingredient, s common.ScreenInterface, w common.WorkflowInterface) error {
progress := float64(progressBarEmpty)
s.UpdateProgessBar(progress)
ingAdded := ingredientsCountStart
ingWaitingList := make(chan recipe.Ingredient, numOfGoRoutines)
g := new(errgroup.Group)
for i := ingredientsCountStart; i < numOfGoRoutines; i++ {
g.Go(func() error {
return ingSend(s, w, ingWaitingList, &ingAdded, len(ings))
})
}
ingQueue(ings, ingWaitingList)
if err := g.Wait(); err != nil {
return err
}
progress = progressBarFull
log.Printf("progress=%.2f", progress)
s.UpdateProgessBar(progress)
s.UpdateLabel(recipeFinishLabel)
return nil
}