diff --git a/commands/move.go b/commands/move.go new file mode 100644 index 0000000..f3b2119 --- /dev/null +++ b/commands/move.go @@ -0,0 +1,34 @@ +package commands + +import ( + "fmt" +) + +type taskMovement struct { + id string + from string + to string +} + +func NewTaskMovement(taskId string, from string, to string) Command { + return taskMovement{id: taskId, from: from, to: to} +} + +func (m taskMovement) Execute() error { + + fromDate, err := ResolveDate(m.from) + + if err != nil { + return fmt.Errorf("invalid due date format provided: %s", fromDate) + } + + toDate, err := ResolveDueDate(m.to) + + if err != nil { + return fmt.Errorf("invalid target date format provided: %s", toDate) + } + + tasksDao.Move(m.id, fromDate, toDate) + + return nil +} diff --git a/dao/dao.go b/dao/dao.go index 649787b..c4cf4c1 100644 --- a/dao/dao.go +++ b/dao/dao.go @@ -11,4 +11,5 @@ type TaskDao interface { RemoveByDate(date time.Time) RetrieveByDate(date time.Time) []model.Task RemovePast() + Move(id string, source time.Time, target time.Time) } diff --git a/dao/filesystem_dao.go b/dao/filesystem_dao.go index 0627335..fb6fd3a 100644 --- a/dao/filesystem_dao.go +++ b/dao/filesystem_dao.go @@ -125,6 +125,10 @@ func (f filesystem) RetrieveByDate(date time.Time) []model.Task { return []model.Task{} } +func (f filesystem) Move(id string, source time.Time, target time.Time) { + log.Fatal("Filesystem Dao for Task Movements is Not Supported") +} + func readLines(path string) (lines []string, err error) { var ( file *os.File diff --git a/dao/json_dao.go b/dao/json_dao.go index b9ccda3..c5fe1ca 100644 --- a/dao/json_dao.go +++ b/dao/json_dao.go @@ -23,7 +23,9 @@ type taskJSON struct { } func NewJSONDao() TaskDao { + configDir := path.Join(os.Getenv("HOME"), ".doo") + return newJSONDao(configDir) } @@ -45,6 +47,18 @@ func (db json) Save(task *model.Task) { db.update(db.dateToString(task.Date), db.adaptFromTask(*task)) } +func (db json) Move(taskID string, from time.Time, to time.Time) { + + if db.exists() { + + fromDate := db.dateToString(from) + + toDate := db.dateToString(to) + + db.moveByID(fromDate, toDate, taskID) + } +} + func (db json) RemoveByDate(date time.Time) { if db.exists() { @@ -52,7 +66,6 @@ func (db json) RemoveByDate(date time.Time) { targetDate := db.dateToString(date) db.remove(targetDate) - } } @@ -134,6 +147,28 @@ func (db json) remove(date string) { db.persistAll(taskList) } +func (db json) moveByID(fromDate string, toDate string, id string) { + + taskList := db.findAll() + + var index int + var taskJSON taskJSON + + for i, task := range taskList[fromDate] { + if task.ID == id { + index = i + taskJSON = task + break + } + } + + taskList[fromDate] = remove(taskList[fromDate], index) + + taskList[toDate] = append(taskList[toDate], taskJSON) + + db.persistAll(taskList) +} + func (db json) persistAll(tasks map[string][]taskJSON) { updatedJSON, err := js.Marshal(tasks) @@ -145,6 +180,10 @@ func (db json) persistAll(tasks map[string][]taskJSON) { ioutil.WriteFile(db.path(), updatedJSON, 0644) } +func remove(slice []taskJSON, s int) []taskJSON { + return append(slice[:s], slice[s+1:]...) +} + func (db json) adaptFromTask(task model.Task) taskJSON { return taskJSON{ ID: task.Id, diff --git a/dao/json_dao_test.go b/dao/json_dao_test.go index adcd64d..eaeb2a0 100644 --- a/dao/json_dao_test.go +++ b/dao/json_dao_test.go @@ -63,6 +63,54 @@ func Test_json_Save(t *testing.T) { } } +func Test_json_Move(t *testing.T) { + + from := time.Now() + + to := time.Now().AddDate(0, 0, 1) + + task := model.NewTask("description", from) + + underTest := json{ + configDir: tempConfigDir(), + filename: dbFile(), + } + + underTest.Save(&task) + + type fields struct { + configDir string + filename string + } + type args struct { + taskID string + from time.Time + to time.Time + } + tests := []struct { + name string + args args + }{ + { + "Should move tasks between task lists", args{taskID: task.Id, from: from, to: to}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + underTest.Move(tt.args.taskID, tt.args.from, tt.args.to) + + sourceTaskList := underTest.RetrieveByDate(tt.args.from) + + assert.Empty(t, sourceTaskList) + + targetTaskList := underTest.RetrieveByDate(tt.args.to) + + assert.Len(t, targetTaskList, 1) + }) + } +} + func Test_json_RemoveByDate(t *testing.T) { task := model.NewTask("description", time.Now()) diff --git a/main.go b/main.go index 24eebdb..ed0901e 100644 --- a/main.go +++ b/main.go @@ -22,6 +22,11 @@ const ( ListSubCommand = "ls" ListDateOption = "d" + + MoveSubCommand = "mv" + MoveTaskIDOption = "id" + MoveFromDateOption = "f" + MoveToDateOption = "t" ) var ( @@ -41,6 +46,16 @@ func (o AddCommandOptions) valid() bool { return *o.desc != "" && *o.date != "" } +type MoveCommandOptions struct { + id *string + from *string + to *string +} + +func (m MoveCommandOptions) valid() bool { + return *m.id != "" && *m.from != "" && *m.to != "" +} + type RemoveCommandOptions struct { date *string allPast *bool @@ -64,6 +79,11 @@ func main() { todoDescriptionPointer := addCommand.String(AddDescriptionOption, "", "task description") todoDatePointer := addCommand.String(AddDueDateOption, "today", "task due date") + moveCommand := flag.NewFlagSet(MoveSubCommand, flag.ExitOnError) + moveIDPointer := moveCommand.String(MoveTaskIDOption, "", "task id to move (Required)") + moveFromDatePointer := moveCommand.String(MoveFromDateOption, "", "date of source task list (Required)") + moveToDatePointer := moveCommand.String(MoveToDateOption, "", "date of target task list (Required)") + removeCommand := flag.NewFlagSet(RemoveSubCommand, flag.ExitOnError) removeDatePointer := removeCommand.String(RemoveDateOption, "", "Date of the task that you'd like to delete") removePastPointer := removeCommand.Bool(RemovePastOption, false, "Remove all past task lists") @@ -72,7 +92,7 @@ func main() { listDatePointer := listCommand.String(ListDateOption, "today", "Date of the task list you'd like to see information for") if len(os.Args) < 2 { - fmt.Println(fmt.Sprintf("You need to Provide a command: %s, %s, %s", AddSubCommand, ListSubCommand, RemoveSubCommand)) + fmt.Println(fmt.Sprintf("You need to Provide a command: %s, %s, %s, %s", AddSubCommand, MoveSubCommand, ListSubCommand, RemoveSubCommand)) os.Exit(1) } @@ -80,6 +100,9 @@ func main() { case AddSubCommand: addCommand.Parse(os.Args[2:]) break + case MoveSubCommand: + moveCommand.Parse(os.Args[2:]) + break case RemoveSubCommand: removeCommand.Parse(os.Args[2:]) break @@ -106,6 +129,13 @@ func main() { checkArgumentsAndExecute(*addCommand, options, commands.NewTaskCreation(*options.date, *options.desc)) } + if moveCommand.Parsed() { + + options := MoveCommandOptions{id: moveIDPointer, from: moveFromDatePointer, to: moveToDatePointer} + + checkArgumentsAndExecute(*moveCommand, options, commands.NewTaskMovement(*options.id, *options.from, *options.to)) + } + if removeCommand.Parsed() { options := RemoveCommandOptions{date: removeDatePointer, allPast: removePastPointer} @@ -115,6 +145,12 @@ func main() { if listCommand.Parsed() { + args := listCommand.Args() + + if len(args) == 1 && flag.Lookup(ListDateOption) == nil { + listDatePointer = &args[0] + } + options := ListCommandOptions{date: listDatePointer} checkArgumentsAndExecute(*listCommand, options, commands.NewTaskListRetrieval(*options.date)) diff --git a/mocks/mock_tasks_dao.go b/mocks/mock_tasks_dao.go index 3b617b6..24a3184 100644 --- a/mocks/mock_tasks_dao.go +++ b/mocks/mock_tasks_dao.go @@ -1,15 +1,14 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: dao/tasks_dao.go +// Source: dao/dao.go // Package mock_dao is a generated GoMock package. package mock_dao import ( - reflect "reflect" - time "time" - model "github.com/gekalogiros/Doo/model" gomock "github.com/golang/mock/gomock" + reflect "reflect" + time "time" ) // MockTaskDao is a mock of TaskDao interface @@ -76,3 +75,13 @@ func (m *MockTaskDao) RemovePast() { func (mr *MockTaskDaoMockRecorder) RemovePast() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemovePast", reflect.TypeOf((*MockTaskDao)(nil).RemovePast)) } + +// Move mocks base method +func (m *MockTaskDao) Move(id string, source, target time.Time) { + m.ctrl.Call(m, "Move", id, source, target) +} + +// Move indicates an expected call of Move +func (mr *MockTaskDaoMockRecorder) Move(id, source, target interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Move", reflect.TypeOf((*MockTaskDao)(nil).Move), id, source, target) +} diff --git a/model/hash_generator.go b/model/hash_generator.go index b11b450..cb43066 100644 --- a/model/hash_generator.go +++ b/model/hash_generator.go @@ -6,7 +6,7 @@ import ( ) func generateHash() string { - b := make([]byte, 3) + b := make([]byte, 2) rand.Read(b) return fmt.Sprintf("%x", b) -} \ No newline at end of file +} diff --git a/model/hash_generator_test.go b/model/hash_generator_test.go index 9cc41d4..e2cdca9 100644 --- a/model/hash_generator_test.go +++ b/model/hash_generator_test.go @@ -1,11 +1,12 @@ package model import ( - "github.com/stretchr/testify/assert" "testing" + + "github.com/stretchr/testify/assert" ) func TestGenerateHash(t *testing.T) { hash := generateHash() - assert.Equal(t, 6, len(hash), "") + assert.Equal(t, 4, len(hash), "") }