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
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 {

View File

@ -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

View File

@ -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)
}

View File

@ -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{

View File

@ -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(),
}
}

View File

@ -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"