From ad5a864a176b892d31ee6f126a1996cb6228f820 Mon Sep 17 00:00:00 2001 From: Erik Winter Date: Fri, 4 Jun 2021 10:45:56 +0200 Subject: [PATCH] split message->task->message chain --- internal/process/inbox.go | 8 +- internal/task/date_test.go | 44 ++++++++- internal/task/repo.go | 13 +-- internal/task/repo_test.go | 32 +------ internal/task/task.go | 178 +++++++++++++--------------------- internal/task/task_test.go | 189 +++++++++++++++++++++---------------- 6 files changed, 225 insertions(+), 239 deletions(-) diff --git a/internal/process/inbox.go b/internal/process/inbox.go index ff3418e..9ea0e6b 100644 --- a/internal/process/inbox.go +++ b/internal/process/inbox.go @@ -43,12 +43,10 @@ func (inbox *Inbox) Process() (*InboxResult, error) { var cleanupNeeded bool for _, t := range tasks { - if t.Dirty { - if err := inbox.taskRepo.Update(t); err != nil { - return &InboxResult{}, fmt.Errorf("%w: %v", ErrInboxProcess, err) - } - cleanupNeeded = true + if err := inbox.taskRepo.Update(t); err != nil { + return &InboxResult{}, fmt.Errorf("%w: %v", ErrInboxProcess, err) } + cleanupNeeded = true } if cleanupNeeded { if err := inbox.taskRepo.CleanUp(); err != nil { diff --git a/internal/task/date_test.go b/internal/task/date_test.go index cf18c82..c930ae8 100644 --- a/internal/task/date_test.go +++ b/internal/task/date_test.go @@ -75,7 +75,6 @@ func TestWeekdaysUnique(t *testing.T) { } func TestNewDateFromString(t *testing.T) { - t.Run("no date", func(t *testing.T) { task.Today = task.NewDate(2021, 1, 30) for _, tc := range []struct { @@ -251,6 +250,49 @@ func TestNewDateFromString(t *testing.T) { }) } +func TestDateDaysBetween(t *testing.T) { + for _, tc := range []struct { + name string + d1 task.Date + d2 task.Date + exp int + }{ + { + name: "same", + d1: task.NewDate(2021, 6, 23), + d2: task.NewDate(2021, 6, 23), + }, + { + name: "one", + d1: task.NewDate(2021, 6, 23), + d2: task.NewDate(2021, 6, 24), + exp: 1, + }, + { + name: "many", + d1: task.NewDate(2021, 6, 23), + d2: task.NewDate(2024, 3, 7), + exp: 988, + }, + { + name: "edge", + d1: task.NewDate(2020, 12, 30), + d2: task.NewDate(2021, 1, 3), + exp: 4, + }, + { + name: "reverse", + d1: task.NewDate(2021, 6, 23), + d2: task.NewDate(2021, 5, 23), + exp: 31, + }, + } { + t.Run(tc.name, func(t *testing.T) { + test.Equals(t, tc.exp, tc.d1.DaysBetween(tc.d2)) + }) + } +} + func TestDateString(t *testing.T) { for _, tc := range []struct { name string diff --git a/internal/task/repo.go b/internal/task/repo.go index 5275921..5504e27 100644 --- a/internal/task/repo.go +++ b/internal/task/repo.go @@ -33,7 +33,7 @@ func (tr *TaskRepo) FindAll(folder string) ([]*Task, error) { tasks := []*Task{} for _, msg := range msgs { if msg.Valid() { - tasks = append(tasks, New(msg)) + tasks = append(tasks, NewFromMessage(msg)) } } @@ -44,12 +44,6 @@ func (tr *TaskRepo) Update(t *Task) error { if t == nil { return ErrInvalidTask } - if !t.Current { - return ErrOutdatedTask - } - if !t.Dirty { - return nil - } // add new if err := tr.Add(t); err != nil { @@ -61,8 +55,6 @@ func (tr *TaskRepo) Update(t *Task) error { return fmt.Errorf("%w: %s", ErrMStoreError, err) } - t.Current = false - return nil } @@ -71,7 +63,8 @@ func (tr *TaskRepo) Add(t *Task) error { return ErrInvalidTask } - if err := tr.mstore.Add(t.Folder, t.FormatSubject(), t.FormatBody()); err != nil { + msg := t.NextMessage() + if err := tr.mstore.Add(msg.Folder, msg.Subject, msg.Body); err != nil { return fmt.Errorf("%w: %v", ErrMStoreError, err) } diff --git a/internal/task/repo_test.go b/internal/task/repo_test.go index e720e20..4c97a6d 100644 --- a/internal/task/repo_test.go +++ b/internal/task/repo_test.go @@ -67,8 +67,8 @@ func TestRepoFindAll(t *testing.T) { func TestRepoUpdate(t *testing.T) { id := "id" - oldFolder := "old folder" - folder := "folder" + oldFolder := task.FOLDER_INBOX + folder := task.FOLDER_NEW action := "action" oldMsg := &mstore.Message{ @@ -90,45 +90,19 @@ func TestRepoUpdate(t *testing.T) { }, { name: "task without message", - task: &task.Task{ - Id: id, - Folder: folder, - Action: action, - Current: true, - Dirty: true, - }, - expErr: task.ErrMStoreError, - }, - { - name: "outdated task", task: &task.Task{ Id: id, Folder: folder, Action: action, - Dirty: true, }, - expErr: task.ErrOutdatedTask, + expErr: task.ErrMStoreError, }, - /* - { - name: "unchanged task", - task: &task.Task{ - Id: id, - Folder: folder, - Action: action, - Current: true, - }, - expMsgs: []*mstore.Message{}, - }, - */ { name: "changed task", task: &task.Task{ Id: id, Folder: folder, Action: action, - Current: true, - Dirty: true, Message: oldMsg, }, expMsgs: []*mstore.Message{ diff --git a/internal/task/task.go b/internal/task/task.go index cac809c..e801f3f 100644 --- a/internal/task/task.go +++ b/internal/task/task.go @@ -44,141 +44,93 @@ var ( FOLDER_PLANNED, FOLDER_UNPLANNED, } + + subjectFieldNames = []string{FIELD_ACTION} + bodyFieldNames = []string{ + FIELD_ID, + FIELD_VERSION, + FIELD_ACTION, + FIELD_PROJECT, + FIELD_DUE, + FIELD_RECUR, + } ) -// Task reperesents a task based on the data stored in a message type Task struct { + Message *mstore.Message - // Id is a UUID that gets carried over when a new message is constructed - Id string - // Version is a method to determine the latest version for cleanup + Id string Version int + Folder string - // Folder is the same name as the mstore folder - Folder string - - // Ordinary task attributes Action string Project string Due Date Recur Recurrer - - //Message is the underlying message - Message *mstore.Message - - // Current indicates whether the task represents an existing message in the mstore - Current bool - - // Dirty indicates whether the task contains updates not present in the message - Dirty bool } -// New constructs a Task based on an mstore.Message. -// -// The data in the message is stored as key: value pairs, one per line. The line can start with quoting marks. -// The subject line also contains values in the format "date - project - action". -// Keys that exist more than once are merged. The one that appears first in the body takes precedence. A value present in the Body takes precedence over one in the subject. -// This enables updating a task by forwarding a topposted message whith new values for fields that the user wants to update. -func New(msg *mstore.Message) *Task { - // Id - dirty := false - newId := false - id, d := FieldFromBody(FIELD_ID, msg.Body) +func NewFromMessage(msg *mstore.Message) *Task { + t := &Task{ + Folder: msg.Folder, + Message: msg, + } + + // parse fields from message + subjectFields := map[string]string{} + for _, f := range subjectFieldNames { + subjectFields[f] = FieldFromSubject(f, msg.Subject) + } + + bodyFields := map[string]string{} + for _, f := range bodyFieldNames { + value, _ := FieldFromBody(f, msg.Body) + bodyFields[f] = value + } + + // apply precedence rules + version, _ := strconv.Atoi(bodyFields[FIELD_VERSION]) + id := bodyFields[FIELD_ID] if id == "" { id = uuid.New().String() - dirty = true - newId = true + version = 0 } - if d { - dirty = true + t.Id = id + t.Version = version + + t.Action = bodyFields[FIELD_ACTION] + if t.Action == "" { + t.Action = subjectFields[FIELD_ACTION] } - // Version, cannot manually be incremented from body - versionStr, _ := FieldFromBody(FIELD_VERSION, msg.Body) - version, _ := strconv.Atoi(versionStr) - if version == 0 { - dirty = true - } + t.Project = bodyFields[FIELD_PROJECT] + t.Due = NewDateFromString(bodyFields[FIELD_DUE]) + t.Recur = NewRecurrer(bodyFields[FIELD_RECUR]) - // Action - action, d := FieldFromBody(FIELD_ACTION, msg.Body) - if action == "" { - action = FieldFromSubject(FIELD_ACTION, msg.Subject) - if action != "" { - dirty = true - } - } - if d { - dirty = true - } + return t +} - // Due - dueStr, d := FieldFromBody(FIELD_DUE, msg.Body) - if dueStr == "" { - dueStr = FieldFromSubject(FIELD_DUE, msg.Subject) - if dueStr != "" { - dirty = true - } +func (t *Task) TargetFolder() string { + switch { + case t.Version == 0: + return FOLDER_NEW + case t.IsRecurrer(): + return FOLDER_RECURRING + case !t.Due.IsZero(): + return FOLDER_PLANNED + default: + return FOLDER_UNPLANNED } - if d { - dirty = true - } - due := NewDateFromString(dueStr) +} - // Recurrer - recurStr, d := FieldFromBody(FIELD_RECUR, msg.Body) - if d { - dirty = true - } - recur := NewRecurrer(recurStr) +func (t *Task) NextMessage() *mstore.Message { + tNew := t + tNew.Folder = t.TargetFolder() + tNew.Version++ - // Folder - folderOld := msg.Folder - folderNew := folderOld - if folderOld == FOLDER_INBOX { - switch { - case newId: - folderNew = FOLDER_NEW - case !newId && recur != nil: - folderNew = FOLDER_RECURRING - case !newId && recur == nil && due.IsZero(): - folderNew = FOLDER_UNPLANNED - case !newId && recur == nil && !due.IsZero(): - folderNew = FOLDER_PLANNED - } - - } - if folderOld != folderNew { - dirty = true - } - - // Project - project, d := FieldFromBody(FIELD_PROJECT, msg.Body) - if project == "" { - project = FieldFromSubject(FIELD_PROJECT, msg.Subject) - if project != "" { - dirty = true - } - } - if d { - dirty = true - } - - if dirty { - version++ - } - - return &Task{ - Id: id, - Version: version, - Folder: folderNew, - Action: action, - Due: due, - Recur: recur, - Project: project, - Message: msg, - Current: true, - Dirty: dirty, + return &mstore.Message{ + Folder: tNew.Folder, + Subject: tNew.FormatSubject(), + Body: tNew.FormatBody(), } } diff --git a/internal/task/task_test.go b/internal/task/task_test.go index c5cf571..c8c251a 100644 --- a/internal/task/task_test.go +++ b/internal/task/task_test.go @@ -18,18 +18,15 @@ func TestNewFromMessage(t *testing.T) { recurs := "2021-06-04, daily" for _, tc := range []struct { - name string - message *mstore.Message - hasId bool - hasVersion bool - exp *task.Task + name string + message *mstore.Message + hasId bool + exp *task.Task }{ { name: "empty", message: &mstore.Message{}, - exp: &task.Task{ - Dirty: true, - }, + exp: &task.Task{}, }, { name: "id, action, project and folder", @@ -43,8 +40,7 @@ action: %s project: %s `, id, version, action, project), }, - hasId: true, - hasVersion: true, + hasId: true, exp: &task.Task{ Id: id, Version: version, @@ -64,8 +60,7 @@ version: %d action: %s `, id, date.String(), version, action), }, - hasId: true, - hasVersion: true, + hasId: true, exp: &task.Task{ Id: id, Folder: task.FOLDER_PLANNED, @@ -74,58 +69,6 @@ action: %s Due: date, }, }, - { - name: "folder inbox get updated to new", - message: &mstore.Message{ - Folder: task.FOLDER_INBOX, - Body: fmt.Sprintf(` -action: %s -`, action), - }, - exp: &task.Task{ - Id: id, - Folder: task.FOLDER_NEW, - Action: action, - Dirty: true, - }, - }, - { - name: "folder inbox gets updated to planned", - message: &mstore.Message{ - Folder: task.FOLDER_INBOX, - Body: fmt.Sprintf(` -id: %s -due: %s -action: %s -`, id, date.String(), action), - }, - hasId: true, - exp: &task.Task{ - Id: id, - Folder: task.FOLDER_PLANNED, - Action: action, - Due: date, - Dirty: true, - }, - }, - { - name: "folder new gets updated to unplanned", - message: &mstore.Message{ - Folder: task.FOLDER_INBOX, - Body: fmt.Sprintf(` -id: %s -due: no date -action: %s -`, id, action), - }, - hasId: true, - exp: &task.Task{ - Id: id, - Folder: task.FOLDER_UNPLANNED, - Action: action, - Dirty: true, - }, - }, { name: "action in body takes precedence", message: &mstore.Message{ @@ -138,8 +81,7 @@ version: %d action: %s `, id, version, action), }, - hasId: true, - hasVersion: true, + hasId: true, exp: &task.Task{ Id: id, Version: version, @@ -159,7 +101,6 @@ action: %s Id: id, Folder: task.FOLDER_PLANNED, Action: action, - Dirty: true, }, }, { @@ -175,8 +116,7 @@ action: %s project: %s `, id, version, action, project), }, - hasId: true, - hasVersion: true, + hasId: true, exp: &task.Task{ Id: id, Version: version, @@ -202,11 +142,10 @@ Forwarded message: Id: id, Folder: task.FOLDER_PLANNED, Action: action, - Dirty: true, }, }, { - name: "recur takes precedence over date", + name: "recur", message: &mstore.Message{ Folder: task.FOLDER_INBOX, Body: fmt.Sprintf(` @@ -218,35 +157,123 @@ id :%s version: %d `, action, recurs, project, id, version), }, - hasId: true, - hasVersion: true, + hasId: true, exp: &task.Task{ Id: id, - Version: version + 1, - Folder: task.FOLDER_RECURRING, + Version: version, + Folder: task.FOLDER_INBOX, Action: action, Project: project, Recur: task.Daily{Start: task.NewDate(2021, 6, 4)}, - Dirty: true, }, }, } { t.Run(tc.name, func(t *testing.T) { - act := task.New(tc.message) + act := task.NewFromMessage(tc.message) if !tc.hasId { test.Equals(t, false, "" == act.Id) tc.exp.Id = act.Id } - if !tc.hasVersion { - tc.exp.Version = 1 - } tc.exp.Message = tc.message - tc.exp.Current = true test.Equals(t, tc.exp, act) }) } } +func TestTaskTargetFolder(t *testing.T) { + for _, tc := range []struct { + name string + tsk *task.Task + expFolder string + }{ + { + name: "new", + tsk: &task.Task{}, + expFolder: task.FOLDER_NEW, + }, + { + name: "recurring", + tsk: &task.Task{ + Id: "id", + Version: 2, + Recur: task.Daily{Start: task.NewDate(2021, 06, 21)}, + }, + expFolder: task.FOLDER_RECURRING, + }, + { + name: "planned", + tsk: &task.Task{ + Id: "id", + Version: 2, + Due: task.NewDate(2021, 06, 21), + }, + expFolder: task.FOLDER_PLANNED, + }, + { + name: "unplanned", + tsk: &task.Task{ + Id: "id", + Version: 2, + }, + expFolder: task.FOLDER_UNPLANNED, + }, + } { + t.Run(tc.name, func(t *testing.T) { + test.Equals(t, tc.tsk.TargetFolder(), tc.expFolder) + }) + } +} + +func TestTaskNextMessage(t *testing.T) { + for _, tc := range []struct { + name string + tsk *task.Task + expMessage *mstore.Message + }{ + { + name: "empty", + tsk: &task.Task{}, + expMessage: &mstore.Message{ + Folder: task.FOLDER_NEW, + Subject: "", + Body: ` +action: +due: no date +project: +version: 1 +id: +`, + }, + }, + { + name: "normal", + tsk: &task.Task{ + Id: "id", + Version: 3, + Folder: task.FOLDER_INBOX, + Action: "action", + Project: "project", + Due: task.NewDate(2021, 06, 22), + }, + expMessage: &mstore.Message{ + Folder: task.FOLDER_PLANNED, + Subject: "2021-06-22 (tuesday) - project - action", + Body: ` +action: action +due: 2021-06-22 (tuesday) +project: project +version: 4 +id: id +`, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + test.Equals(t, tc.expMessage, tc.tsk.NextMessage()) + }) + } +} + func TestFormatSubject(t *testing.T) { action := "an action" project := " a project"