~ewintr/gte

529b24aa13715163a475b095cb522663bf3c38b0 — Erik Winter 2 months ago 22cad82
async new with field parse
D cmd/cli/command/add.go => cmd/cli/command/add.go +0 -34
@@ 1,34 0,0 @@
package command

import (
	"strings"

	"git.ewintr.nl/gte/cmd/cli/format"
	"git.ewintr.nl/gte/internal/configuration"
	"git.ewintr.nl/gte/internal/storage"
	"git.ewintr.nl/gte/internal/task"
	"git.ewintr.nl/gte/pkg/msend"
)

// Add sends an action to the NEW folder so it can be updated to a real task later
type Add struct {
	disp   *storage.Dispatcher
	action string
}

func NewAdd(conf *configuration.Configuration, cmdArgs []string) (*Add, error) {
	disp := storage.NewDispatcher(msend.NewSSLSMTP(conf.SMTP()))

	return &Add{
		disp:   disp,
		action: strings.Join(cmdArgs, " "),
	}, nil
}

func (n *Add) Do() string {
	if err := n.disp.Dispatch(&task.Task{Action: n.action}); err != nil {
		return format.FormatError(err)
	}

	return "message sent\n"
}

M cmd/cli/command/command.go => cmd/cli/command/command.go +2 -3
@@ 11,7 11,6 @@ var (
	ErrInvalidAmountOfArgs = errors.New("invalid amount of args")
	ErrInvalidArg          = errors.New("invalid argument")
	ErrCouldNotFindTask    = errors.New("could not find task")
	ErrFieldAlreadyUsed    = errors.New("field was already used")
	ErrUnknownFolder       = errors.New("unknown folder")
)



@@ 47,8 46,8 @@ func Parse(args []string, conf *configuration.Configuration) (Command, error) {
		return NewProjects(conf)
	case "folder":
		return NewFolder(conf, cmdArgs)
	case "add":
		return NewAdd(conf, cmdArgs)
	case "new":
		return NewNew(conf, cmdArgs)
	case "remote":
		return parseRemote(conf, cmdArgs)
	default:

A cmd/cli/command/new.go => cmd/cli/command/new.go +37 -0
@@ 0,0 1,37 @@
package command

import (
	"git.ewintr.nl/gte/cmd/cli/format"
	"git.ewintr.nl/gte/internal/configuration"
	"git.ewintr.nl/gte/internal/process"
	"git.ewintr.nl/gte/internal/storage"
)

// New sends an action to the NEW folder so it can be updated to a real task later
type New struct {
	newer *process.New
}

func NewNew(conf *configuration.Configuration, cmdArgs []string) (*New, error) {
	local, err := storage.NewSqlite(conf.Sqlite())
	if err != nil {
		return &New{}, err
	}

	update, err := format.ParseTaskFieldArgs(cmdArgs)
	if err != nil {
		return &New{}, err
	}

	return &New{
		newer: process.NewNew(local, update),
	}, nil
}

func (n *New) Do() string {
	if err := n.newer.Process(); err != nil {
		return format.FormatError(err)
	}

	return ""
}

M cmd/cli/command/update.go => cmd/cli/command/update.go +2 -44
@@ 1,14 1,10 @@
package command

import (
	"fmt"
	"strings"

	"git.ewintr.nl/gte/cmd/cli/format"
	"git.ewintr.nl/gte/internal/configuration"
	"git.ewintr.nl/gte/internal/process"
	"git.ewintr.nl/gte/internal/storage"
	"git.ewintr.nl/gte/internal/task"
)

type Update struct {


@@ 21,7 17,7 @@ func NewUpdate(localId int, conf *configuration.Configuration, cmdArgs []string)
		return &Update{}, err
	}

	update, err := ParseTaskFieldArgs(cmdArgs)
	update, err := format.ParseTaskFieldArgs(cmdArgs)
	if err != nil {
		return &Update{}, err
	}


@@ 43,43 39,5 @@ func (u *Update) Do() string {
		return format.FormatError(err)
	}

	return "local task updated\n"
}

func ParseTaskFieldArgs(args []string) (*task.LocalUpdate, error) {
	lu := &task.LocalUpdate{}

	action, fields := []string{}, []string{}
	for _, f := range args {
		split := strings.SplitN(f, ":", 2)
		if len(split) == 2 {
			switch split[0] {
			case "project":
				if lu.Project != "" {
					return &task.LocalUpdate{}, fmt.Errorf("%w: %s", ErrFieldAlreadyUsed, task.FIELD_PROJECT)
				}
				lu.Project = split[1]
				fields = append(fields, task.FIELD_PROJECT)
			case "due":
				if !lu.Due.IsZero() {
					return &task.LocalUpdate{}, fmt.Errorf("%w: %s", ErrFieldAlreadyUsed, task.FIELD_DUE)
				}
				lu.Due = task.NewDateFromString(split[1])
				fields = append(fields, task.FIELD_DUE)
			}
		} else {
			if len(f) > 0 {
				action = append(action, f)
			}
		}
	}

	if len(action) > 0 {
		lu.Action = strings.Join(action, " ")
		fields = append(fields, task.FIELD_ACTION)
	}

	lu.Fields = fields

	return lu, nil
	return ""
}

M cmd/cli/format/format.go => cmd/cli/format/format.go +44 -0
@@ 1,12 1,18 @@
package format

import (
	"errors"
	"fmt"
	"sort"
	"strings"

	"git.ewintr.nl/gte/internal/task"
)

var (
	ErrFieldAlreadyUsed = errors.New("field was already used")
)

func FormatError(err error) string {
	return fmt.Sprintf("could not perform command.\n\nerror: %s\n", err.Error())
}


@@ 42,3 48,41 @@ due:     %s

	return output
}

func ParseTaskFieldArgs(args []string) (*task.LocalUpdate, error) {
	lu := &task.LocalUpdate{}

	action, fields := []string{}, []string{}
	for _, f := range args {
		split := strings.SplitN(f, ":", 2)
		if len(split) == 2 {
			switch split[0] {
			case "project":
				if lu.Project != "" {
					return &task.LocalUpdate{}, fmt.Errorf("%w: %s", ErrFieldAlreadyUsed, task.FIELD_PROJECT)
				}
				lu.Project = split[1]
				fields = append(fields, task.FIELD_PROJECT)
			case "due":
				if !lu.Due.IsZero() {
					return &task.LocalUpdate{}, fmt.Errorf("%w: %s", ErrFieldAlreadyUsed, task.FIELD_DUE)
				}
				lu.Due = task.NewDateFromString(split[1])
				fields = append(fields, task.FIELD_DUE)
			}
		} else {
			if len(f) > 0 {
				action = append(action, f)
			}
		}
	}

	if len(action) > 0 {
		lu.Action = strings.Join(action, " ")
		fields = append(fields, task.FIELD_ACTION)
	}

	lu.Fields = fields

	return lu, nil
}

R cmd/cli/command/update_test.go => cmd/cli/format/format_test.go +4 -4
@@ 1,4 1,4 @@
package command_test
package format_test

import (
	"errors"


@@ 6,7 6,7 @@ import (
	"testing"

	"git.ewintr.nl/go-kit/test"
	"git.ewintr.nl/gte/cmd/cli/command"
	"git.ewintr.nl/gte/cmd/cli/format"
	"git.ewintr.nl/gte/internal/task"
)



@@ 53,12 53,12 @@ func TestParseTaskFieldArgs(t *testing.T) {
			name:      "two projects",
			input:     "project:project1 project:project2",
			expUpdate: &task.LocalUpdate{},
			expErr:    command.ErrFieldAlreadyUsed,
			expErr:    format.ErrFieldAlreadyUsed,
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			args := strings.Split(tc.input, " ")
			act, err := command.ParseTaskFieldArgs(args)
			act, err := format.ParseTaskFieldArgs(args)
			test.Equals(t, tc.expUpdate, act)
			test.Assert(t, errors.Is(err, tc.expErr), "wrong err")
		})

A internal/process/new.go => internal/process/new.go +33 -0
@@ 0,0 1,33 @@
package process

import (
	"errors"
	"fmt"

	"git.ewintr.nl/gte/internal/storage"
	"git.ewintr.nl/gte/internal/task"
)

var (
	ErrNewTask = errors.New("could not add new task")
)

type New struct {
	local  storage.LocalRepository
	update *task.LocalUpdate
}

func NewNew(local storage.LocalRepository, update *task.LocalUpdate) *New {
	return &New{
		local:  local,
		update: update,
	}
}

func (n *New) Process() error {
	if _, err := n.local.Add(n.update); err != nil {
		return fmt.Errorf("%w: %v", ErrNewTask, err)
	}

	return nil
}

A internal/process/new_test.go => internal/process/new_test.go +28 -0
@@ 0,0 1,28 @@
package process_test

import (
	"testing"

	"git.ewintr.nl/go-kit/test"
	"git.ewintr.nl/gte/internal/process"
	"git.ewintr.nl/gte/internal/storage"
	"git.ewintr.nl/gte/internal/task"
)

func TestNew(t *testing.T) {
	local := storage.NewMemory()
	update := &task.LocalUpdate{
		Fields:  []string{task.FIELD_ACTION, task.FIELD_PROJECT, task.FIELD_DUE},
		Project: "project",
		Action:  "action",
		Due:     task.NewDate(2021, 9, 4),
	}
	n := process.NewNew(local, update)
	test.OK(t, n.Process())
	tasks, err := local.FindAll()
	test.OK(t, err)
	test.Assert(t, len(tasks) == 1, "amount of tasks was not 1")
	tsk := tasks[0]
	test.Assert(t, tsk.Id != "", "id was empty")
	test.Equals(t, update, tsk.LocalUpdate)
}

M internal/process/send_test.go => internal/process/send_test.go +1 -1
@@ 50,7 50,7 @@ func TestSend(t *testing.T) {
		lt, err := local.FindById(task2.Id)
		test.OK(t, err)
		lt.AddUpdate(lu)
		test.OK(t, local.SetLocalUpdate(lt))
		test.OK(t, local.SetLocalUpdate(lt.Id, lt.LocalUpdate))

		out := msend.NewMemory()
		disp := storage.NewDispatcher(out)

M internal/process/update.go => internal/process/update.go +1 -1
@@ 33,7 33,7 @@ func (u *Update) Process() error {
		return fmt.Errorf("%w: %v", ErrUpdateTask, err)
	}
	tsk.AddUpdate(u.update)
	if err := u.local.SetLocalUpdate(tsk); err != nil {
	if err := u.local.SetLocalUpdate(tsk.Id, tsk.LocalUpdate); err != nil {
		return fmt.Errorf("%w: %v", ErrUpdateTask, err)
	}


M internal/storage/local.go => internal/storage/local.go +2 -1
@@ 18,8 18,9 @@ type LocalRepository interface {
	FindAll() ([]*task.LocalTask, error)
	FindById(id string) (*task.LocalTask, error)
	FindByLocalId(id int) (*task.LocalTask, error)
	SetLocalUpdate(tsk *task.LocalTask) error
	SetLocalUpdate(id string, update *task.LocalUpdate) error
	MarkDispatched(id int) error
	Add(update *task.LocalUpdate) (*task.LocalTask, error)
}

// NextLocalId finds a new local id by incrememting to a variable limit.

M internal/storage/memory.go => internal/storage/memory.go +26 -3
@@ 4,6 4,7 @@ import (
	"time"

	"git.ewintr.nl/gte/internal/task"
	"github.com/google/uuid"
)

// Memory is an in memory implementation of LocalRepository


@@ 68,9 69,9 @@ func (m *Memory) FindByLocalId(localId int) (*task.LocalTask, error) {
	return &task.LocalTask{}, ErrTaskNotFound
}

func (m *Memory) SetLocalUpdate(tsk *task.LocalTask) error {
	tsk.LocalStatus = task.STATUS_UPDATED
	m.tasks[tsk.Id] = tsk
func (m *Memory) SetLocalUpdate(id string, update *task.LocalUpdate) error {
	m.tasks[id].LocalStatus = task.STATUS_UPDATED
	m.tasks[id].LocalUpdate = update

	return nil
}


@@ 81,3 82,25 @@ func (m *Memory) MarkDispatched(localId int) error {

	return nil
}

func (m *Memory) Add(update *task.LocalUpdate) (*task.LocalTask, error) {
	var used []int
	for _, t := range m.tasks {
		used = append(used, t.LocalId)
	}

	tsk := &task.LocalTask{
		Task: task.Task{
			Id:      uuid.New().String(),
			Version: 0,
			Folder:  task.FOLDER_NEW,
		},
		LocalId:     NextLocalId(used),
		LocalStatus: task.STATUS_UPDATED,
		LocalUpdate: update,
	}

	m.tasks[tsk.Id] = tsk

	return tsk, nil
}

M internal/storage/memory_test.go => internal/storage/memory_test.go +34 -7
@@ 105,13 105,8 @@ func TestMemory(t *testing.T) {
			Recur:      task.NewRecurrer("today, weekly, monday"),
			Done:       true,
		}
		lt := &task.LocalTask{
			Task:        *task2,
			LocalId:     2,
			LocalUpdate: expUpdate,
		}
		test.OK(t, mem.SetLocalUpdate(lt))
		actTask, err := mem.FindByLocalId(2)
		test.OK(t, mem.SetLocalUpdate(task2.Id, expUpdate))
		actTask, err := mem.FindById(task2.Id)
		test.OK(t, err)
		test.Equals(t, expUpdate, actTask.LocalUpdate)
		test.Equals(t, task.STATUS_UPDATED, actTask.LocalStatus)


@@ 127,4 122,36 @@ func TestMemory(t *testing.T) {
		test.OK(t, err)
		test.Equals(t, task.STATUS_DISPATCHED, act.LocalStatus)
	})

	t.Run("add", func(t *testing.T) {

		action := "action"
		project := "project"
		due := task.NewDate(2021, 9, 4)
		recur := task.Daily{Start: task.NewDate(2021, 9, 5)}
		mem := storage.NewMemory()
		expUpdate := &task.LocalUpdate{
			Fields:  []string{task.FIELD_ACTION, task.FIELD_PROJECT, task.FIELD_DUE, task.FIELD_RECUR},
			Action:  action,
			Project: project,
			Due:     due,
			Recur:   recur,
		}
		act1, err := mem.Add(expUpdate)
		test.OK(t, err)
		test.Assert(t, act1.Id != "", "id was empty")
		test.Equals(t, task.FOLDER_NEW, act1.Folder)
		test.Equals(t, "", act1.Action)
		test.Equals(t, "", act1.Project)
		test.Assert(t, act1.Due.IsZero(), "date was not zero")
		test.Equals(t, nil, act1.Recur)
		test.Equals(t, 0, act1.Version)
		test.Equals(t, 1, act1.LocalId)
		test.Equals(t, task.STATUS_UPDATED, act1.LocalStatus)
		test.Equals(t, expUpdate, act1.LocalUpdate)

		act2, err := mem.FindById(act1.Id)
		test.OK(t, err)
		test.Equals(t, act1, act2)
	})
}

M internal/storage/sqlite.go => internal/storage/sqlite.go +46 -2
@@ 7,6 7,7 @@ import (
	"time"

	"git.ewintr.nl/gte/internal/task"
	"github.com/google/uuid"
	_ "modernc.org/sqlite"
)



@@ 188,11 189,11 @@ func (s *Sqlite) FindByLocalId(localId int) (*task.LocalTask, error) {
	return t, nil
}

func (s *Sqlite) SetLocalUpdate(tsk *task.LocalTask) error {
func (s *Sqlite) SetLocalUpdate(id string, update *task.LocalUpdate) error {
	if _, err := s.db.Exec(`
UPDATE task
SET local_update = ?, local_status = ?
WHERE local_id = ?`, tsk.LocalUpdate, task.STATUS_UPDATED, tsk.LocalId); err != nil {
WHERE id = ?`, update, task.STATUS_UPDATED, id); err != nil {
		return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
	}



@@ 209,6 210,49 @@ WHERE local_id = ?`, task.STATUS_DISPATCHED, localId); err != nil {
	return nil
}

func (s *Sqlite) Add(update *task.LocalUpdate) (*task.LocalTask, error) {
	rows, err := s.db.Query(`SELECT local_id FROM task`)
	if err != nil {
		return &task.LocalTask{}, fmt.Errorf("%w: %v", ErrSqliteFailure, err)
	}
	var used []int
	for rows.Next() {
		var localId int
		if err := rows.Scan(&localId); err != nil {
			return &task.LocalTask{}, fmt.Errorf("%w: %v", ErrSqliteFailure, err)
		}
		used = append(used, localId)
	}
	rows.Close()

	tsk := &task.LocalTask{
		Task: task.Task{
			Id:      uuid.New().String(),
			Version: 0,
			Folder:  task.FOLDER_NEW,
		},
		LocalId:     NextLocalId(used),
		LocalStatus: task.STATUS_UPDATED,
		LocalUpdate: update,
	}

	var recurStr string
	if tsk.LocalUpdate.Recur != nil {
		recurStr = tsk.LocalUpdate.Recur.String()
	}
	if _, err := s.db.Exec(`
INSERT INTO task
(id, local_id, version, folder, action, project, due, recur, local_status, local_update)
VALUES
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
		tsk.Id, tsk.LocalId, tsk.Version, tsk.Folder, tsk.Action, tsk.Project,
		tsk.Due.String(), recurStr, tsk.LocalStatus, tsk.LocalUpdate); err != nil {
		return &task.LocalTask{}, fmt.Errorf("%w: %v", ErrSqliteFailure, err)
	}

	return tsk, nil
}

func (s *Sqlite) migrate(wanted []sqliteMigration) error {
	// admin table
	if _, err := s.db.Exec(`

M internal/task/localtask.go => internal/task/localtask.go +1 -1
@@ 37,7 37,7 @@ func (lt *LocalTask) ApplyUpdate() {
		return
	}
	u := lt.LocalUpdate
	if u.ForVersion == 0 || u.ForVersion != lt.Version {
	if u.ForVersion != lt.Version {
		lt.LocalUpdate = &LocalUpdate{}
		return
	}