split message->task->message chain

This commit is contained in:
Erik Winter 2021-06-04 10:45:56 +02:00
parent 05ad9720e1
commit ad5a864a17
6 changed files with 225 additions and 239 deletions

View File

@ -43,12 +43,10 @@ func (inbox *Inbox) Process() (*InboxResult, error) {
var cleanupNeeded bool var cleanupNeeded bool
for _, t := range tasks { for _, t := range tasks {
if t.Dirty { if err := inbox.taskRepo.Update(t); err != nil {
if err := inbox.taskRepo.Update(t); err != nil { return &InboxResult{}, fmt.Errorf("%w: %v", ErrInboxProcess, err)
return &InboxResult{}, fmt.Errorf("%w: %v", ErrInboxProcess, err)
}
cleanupNeeded = true
} }
cleanupNeeded = true
} }
if cleanupNeeded { if cleanupNeeded {
if err := inbox.taskRepo.CleanUp(); err != nil { if err := inbox.taskRepo.CleanUp(); err != nil {

View File

@ -75,7 +75,6 @@ func TestWeekdaysUnique(t *testing.T) {
} }
func TestNewDateFromString(t *testing.T) { func TestNewDateFromString(t *testing.T) {
t.Run("no date", func(t *testing.T) { t.Run("no date", func(t *testing.T) {
task.Today = task.NewDate(2021, 1, 30) task.Today = task.NewDate(2021, 1, 30)
for _, tc := range []struct { 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) { func TestDateString(t *testing.T) {
for _, tc := range []struct { for _, tc := range []struct {
name string name string

View File

@ -33,7 +33,7 @@ func (tr *TaskRepo) FindAll(folder string) ([]*Task, error) {
tasks := []*Task{} tasks := []*Task{}
for _, msg := range msgs { for _, msg := range msgs {
if msg.Valid() { 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 { if t == nil {
return ErrInvalidTask return ErrInvalidTask
} }
if !t.Current {
return ErrOutdatedTask
}
if !t.Dirty {
return nil
}
// add new // add new
if err := tr.Add(t); err != nil { 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) return fmt.Errorf("%w: %s", ErrMStoreError, err)
} }
t.Current = false
return nil return nil
} }
@ -71,7 +63,8 @@ func (tr *TaskRepo) Add(t *Task) error {
return ErrInvalidTask 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) return fmt.Errorf("%w: %v", ErrMStoreError, err)
} }

View File

@ -67,8 +67,8 @@ func TestRepoFindAll(t *testing.T) {
func TestRepoUpdate(t *testing.T) { func TestRepoUpdate(t *testing.T) {
id := "id" id := "id"
oldFolder := "old folder" oldFolder := task.FOLDER_INBOX
folder := "folder" folder := task.FOLDER_NEW
action := "action" action := "action"
oldMsg := &mstore.Message{ oldMsg := &mstore.Message{
@ -90,45 +90,19 @@ func TestRepoUpdate(t *testing.T) {
}, },
{ {
name: "task without message", 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{ task: &task.Task{
Id: id, Id: id,
Folder: folder, Folder: folder,
Action: action, 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", name: "changed task",
task: &task.Task{ task: &task.Task{
Id: id, Id: id,
Folder: folder, Folder: folder,
Action: action, Action: action,
Current: true,
Dirty: true,
Message: oldMsg, Message: oldMsg,
}, },
expMsgs: []*mstore.Message{ expMsgs: []*mstore.Message{

View File

@ -44,141 +44,93 @@ var (
FOLDER_PLANNED, FOLDER_PLANNED,
FOLDER_UNPLANNED, 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 { type Task struct {
Message *mstore.Message
// Id is a UUID that gets carried over when a new message is constructed Id string
Id string
// Version is a method to determine the latest version for cleanup
Version int Version int
Folder string
// Folder is the same name as the mstore folder
Folder string
// Ordinary task attributes
Action string Action string
Project string Project string
Due Date Due Date
Recur Recurrer 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. func NewFromMessage(msg *mstore.Message) *Task {
// t := &Task{
// The data in the message is stored as key: value pairs, one per line. The line can start with quoting marks. Folder: msg.Folder,
// The subject line also contains values in the format "date - project - action". Message: msg,
// 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 { // parse fields from message
// Id subjectFields := map[string]string{}
dirty := false for _, f := range subjectFieldNames {
newId := false subjectFields[f] = FieldFromSubject(f, msg.Subject)
id, d := FieldFromBody(FIELD_ID, msg.Body) }
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 == "" { if id == "" {
id = uuid.New().String() id = uuid.New().String()
dirty = true version = 0
newId = true
} }
if d { t.Id = id
dirty = true t.Version = version
t.Action = bodyFields[FIELD_ACTION]
if t.Action == "" {
t.Action = subjectFields[FIELD_ACTION]
} }
// Version, cannot manually be incremented from body t.Project = bodyFields[FIELD_PROJECT]
versionStr, _ := FieldFromBody(FIELD_VERSION, msg.Body) t.Due = NewDateFromString(bodyFields[FIELD_DUE])
version, _ := strconv.Atoi(versionStr) t.Recur = NewRecurrer(bodyFields[FIELD_RECUR])
if version == 0 {
dirty = true
}
// Action return t
action, d := FieldFromBody(FIELD_ACTION, msg.Body) }
if action == "" {
action = FieldFromSubject(FIELD_ACTION, msg.Subject)
if action != "" {
dirty = true
}
}
if d {
dirty = true
}
// Due func (t *Task) TargetFolder() string {
dueStr, d := FieldFromBody(FIELD_DUE, msg.Body) switch {
if dueStr == "" { case t.Version == 0:
dueStr = FieldFromSubject(FIELD_DUE, msg.Subject) return FOLDER_NEW
if dueStr != "" { case t.IsRecurrer():
dirty = true return FOLDER_RECURRING
} case !t.Due.IsZero():
return FOLDER_PLANNED
default:
return FOLDER_UNPLANNED
} }
if d { }
dirty = true
}
due := NewDateFromString(dueStr)
// Recurrer func (t *Task) NextMessage() *mstore.Message {
recurStr, d := FieldFromBody(FIELD_RECUR, msg.Body) tNew := t
if d { tNew.Folder = t.TargetFolder()
dirty = true tNew.Version++
}
recur := NewRecurrer(recurStr)
// Folder return &mstore.Message{
folderOld := msg.Folder Folder: tNew.Folder,
folderNew := folderOld Subject: tNew.FormatSubject(),
if folderOld == FOLDER_INBOX { Body: tNew.FormatBody(),
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,
} }
} }

View File

@ -18,18 +18,15 @@ func TestNewFromMessage(t *testing.T) {
recurs := "2021-06-04, daily" recurs := "2021-06-04, daily"
for _, tc := range []struct { for _, tc := range []struct {
name string name string
message *mstore.Message message *mstore.Message
hasId bool hasId bool
hasVersion bool exp *task.Task
exp *task.Task
}{ }{
{ {
name: "empty", name: "empty",
message: &mstore.Message{}, message: &mstore.Message{},
exp: &task.Task{ exp: &task.Task{},
Dirty: true,
},
}, },
{ {
name: "id, action, project and folder", name: "id, action, project and folder",
@ -43,8 +40,7 @@ action: %s
project: %s project: %s
`, id, version, action, project), `, id, version, action, project),
}, },
hasId: true, hasId: true,
hasVersion: true,
exp: &task.Task{ exp: &task.Task{
Id: id, Id: id,
Version: version, Version: version,
@ -64,8 +60,7 @@ version: %d
action: %s action: %s
`, id, date.String(), version, action), `, id, date.String(), version, action),
}, },
hasId: true, hasId: true,
hasVersion: true,
exp: &task.Task{ exp: &task.Task{
Id: id, Id: id,
Folder: task.FOLDER_PLANNED, Folder: task.FOLDER_PLANNED,
@ -74,58 +69,6 @@ action: %s
Due: date, 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", name: "action in body takes precedence",
message: &mstore.Message{ message: &mstore.Message{
@ -138,8 +81,7 @@ version: %d
action: %s action: %s
`, id, version, action), `, id, version, action),
}, },
hasId: true, hasId: true,
hasVersion: true,
exp: &task.Task{ exp: &task.Task{
Id: id, Id: id,
Version: version, Version: version,
@ -159,7 +101,6 @@ action: %s
Id: id, Id: id,
Folder: task.FOLDER_PLANNED, Folder: task.FOLDER_PLANNED,
Action: action, Action: action,
Dirty: true,
}, },
}, },
{ {
@ -175,8 +116,7 @@ action: %s
project: %s project: %s
`, id, version, action, project), `, id, version, action, project),
}, },
hasId: true, hasId: true,
hasVersion: true,
exp: &task.Task{ exp: &task.Task{
Id: id, Id: id,
Version: version, Version: version,
@ -202,11 +142,10 @@ Forwarded message:
Id: id, Id: id,
Folder: task.FOLDER_PLANNED, Folder: task.FOLDER_PLANNED,
Action: action, Action: action,
Dirty: true,
}, },
}, },
{ {
name: "recur takes precedence over date", name: "recur",
message: &mstore.Message{ message: &mstore.Message{
Folder: task.FOLDER_INBOX, Folder: task.FOLDER_INBOX,
Body: fmt.Sprintf(` Body: fmt.Sprintf(`
@ -218,35 +157,123 @@ id :%s
version: %d version: %d
`, action, recurs, project, id, version), `, action, recurs, project, id, version),
}, },
hasId: true, hasId: true,
hasVersion: true,
exp: &task.Task{ exp: &task.Task{
Id: id, Id: id,
Version: version + 1, Version: version,
Folder: task.FOLDER_RECURRING, Folder: task.FOLDER_INBOX,
Action: action, Action: action,
Project: project, Project: project,
Recur: task.Daily{Start: task.NewDate(2021, 6, 4)}, Recur: task.Daily{Start: task.NewDate(2021, 6, 4)},
Dirty: true,
}, },
}, },
} { } {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
act := task.New(tc.message) act := task.NewFromMessage(tc.message)
if !tc.hasId { if !tc.hasId {
test.Equals(t, false, "" == act.Id) test.Equals(t, false, "" == act.Id)
tc.exp.Id = act.Id tc.exp.Id = act.Id
} }
if !tc.hasVersion {
tc.exp.Version = 1
}
tc.exp.Message = tc.message tc.exp.Message = tc.message
tc.exp.Current = true
test.Equals(t, tc.exp, act) 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) { func TestFormatSubject(t *testing.T) {
action := "an action" action := "an action"
project := " a project" project := " a project"