diff --git a/dist/plan b/dist/plan index fe46657..84b7512 100755 Binary files a/dist/plan and b/dist/plan differ diff --git a/item/task.go b/item/task.go index 31fbfa0..556b167 100644 --- a/item/task.go +++ b/item/task.go @@ -91,12 +91,6 @@ func (t Task) Valid() bool { if t.Title == "" { return false } - if t.Date.IsZero() { - return false - } - if t.Duration.Seconds() < 1 { - return false - } return true } diff --git a/plan/command/add.go b/plan/command/add.go index 7aaaab4..ac291b2 100644 --- a/plan/command/add.go +++ b/plan/command/add.go @@ -11,16 +11,16 @@ import ( type AddArgs struct { fieldTPL map[string][]string - task item.Task + Task item.Task } func NewAddArgs() AddArgs { return AddArgs{ fieldTPL: map[string][]string{ - "date": []string{"d", "date", "on"}, - "time": []string{"t", "time", "at"}, - "duration": []string{"dur", "duration", "for"}, - "recurrer": []string{"rec", "recurrer"}, + "date": {"d", "date", "on"}, + "time": {"t", "time", "at"}, + "duration": {"dur", "duration", "for"}, + "recurrer": {"rec", "recurrer"}, }, } } @@ -41,7 +41,7 @@ func (aa AddArgs) Parse(main []string, fields map[string]string) (Command, error tsk := item.Task{ ID: uuid.New().String(), TaskBody: item.TaskBody{ - Title: strings.Join(main, ","), + Title: strings.Join(main, " "), }, } @@ -76,36 +76,43 @@ func (aa AddArgs) Parse(main []string, fields map[string]string) (Command, error } return &Add{ - args: AddArgs{ - task: tsk, + Args: AddArgs{ + Task: tsk, }, }, nil } type Add struct { - args AddArgs + Args AddArgs } -func (a *Add) Do(deps Dependencies) error { - if err := deps.TaskRepo.Store(a.args.task); err != nil { - return fmt.Errorf("could not store event: %v", err) +func (a *Add) Do(deps Dependencies) (CommandResult, error) { + if err := deps.TaskRepo.Store(a.Args.Task); err != nil { + return nil, fmt.Errorf("could not store event: %v", err) } localID, err := deps.LocalIDRepo.Next() if err != nil { - return fmt.Errorf("could not create next local id: %v", err) + return nil, fmt.Errorf("could not create next local id: %v", err) } - if err := deps.LocalIDRepo.Store(a.args.task.ID, localID); err != nil { - return fmt.Errorf("could not store local id: %v", err) + if err := deps.LocalIDRepo.Store(a.Args.Task.ID, localID); err != nil { + return nil, fmt.Errorf("could not store local id: %v", err) } - it, err := a.args.task.Item() + it, err := a.Args.Task.Item() if err != nil { - return fmt.Errorf("could not convert event to sync item: %v", err) + return nil, fmt.Errorf("could not convert event to sync item: %v", err) } if err := deps.SyncRepo.Store(it); err != nil { - return fmt.Errorf("could not store sync item: %v", err) + return nil, fmt.Errorf("could not store sync item: %v", err) } - return nil + return nil, nil +} + +type AddRender struct { +} + +func (ar AddRender) Render() string { + return "stored task" } diff --git a/plan/command/add_test.go b/plan/command/add_test.go index ad14feb..0937cfb 100644 --- a/plan/command/add_test.go +++ b/plan/command/add_test.go @@ -6,6 +6,7 @@ import ( "go-mod.ewintr.nl/planner/item" "go-mod.ewintr.nl/planner/plan/command" + "go-mod.ewintr.nl/planner/plan/storage" "go-mod.ewintr.nl/planner/plan/storage/memory" ) @@ -56,9 +57,12 @@ func TestAdd(t *testing.T) { }, } { t.Run(tc.name, func(t *testing.T) { + // setup taskRepo := memory.NewTask() localIDRepo := memory.NewLocalID() syncRepo := memory.NewSync() + + // parse cmd, actParseErr := command.NewAddArgs().Parse(tc.main, tc.fields) if tc.expErr != (actParseErr != nil) { t.Errorf("exp %v, got %v", tc.expErr, actParseErr) @@ -66,7 +70,9 @@ func TestAdd(t *testing.T) { if tc.expErr { return } - if err := cmd.Do(command.Dependencies{ + + // do + if _, err := cmd.Do(command.Dependencies{ TaskRepo: taskRepo, LocalIDRepo: localIDRepo, SyncRepo: syncRepo, @@ -74,7 +80,8 @@ func TestAdd(t *testing.T) { t.Errorf("exp nil, got %v", err) } - actTasks, err := taskRepo.FindAll() + // check + actTasks, err := taskRepo.FindMany(storage.TaskListParams{}) if err != nil { t.Errorf("exp nil, got %v", err) } diff --git a/plan/command/command.go b/plan/command/command.go index 2a8467d..da371a9 100644 --- a/plan/command/command.go +++ b/plan/command/command.go @@ -32,7 +32,11 @@ type CommandArgs interface { } type Command interface { - Do(deps Dependencies) error + Do(deps Dependencies) (CommandResult, error) +} + +type CommandResult interface { + Render() string } type CLI struct { @@ -44,6 +48,7 @@ func NewCLI(deps Dependencies) *CLI { return &CLI{ deps: deps, cmdArgs: []CommandArgs{ + NewShowArgs(), NewAddArgs(), NewDeleteArgs(), NewListArgs(), NewSyncArgs(), NewUpdateArgs(), }, @@ -51,17 +56,23 @@ func NewCLI(deps Dependencies) *CLI { } func (cli *CLI) Run(args []string) error { - main, flags := FindFields(args) + main, fields := FindFields(args) for _, ca := range cli.cmdArgs { - cmd, err := ca.Parse(main, flags) + cmd, err := ca.Parse(main, fields) switch { case errors.Is(err, ErrWrongCommand): continue case err != nil: return err - default: - return cmd.Do(cli.deps) } + + result, err := cmd.Do(cli.deps) + if err != nil { + return err + } + fmt.Println(result.Render()) + + return nil } return fmt.Errorf("could not find matching command") diff --git a/plan/command/delete.go b/plan/command/delete.go index cc2c777..9d23bb3 100644 --- a/plan/command/delete.go +++ b/plan/command/delete.go @@ -24,52 +24,58 @@ func (da DeleteArgs) Parse(main []string, flags map[string]string) (Command, err } return &Delete{ - args: DeleteArgs{ + Args: DeleteArgs{ LocalID: localID, }, }, nil } type Delete struct { - args DeleteArgs + Args DeleteArgs } -func (del *Delete) Do(deps Dependencies) error { +func (del *Delete) Do(deps Dependencies) (CommandResult, error) { var id string idMap, err := deps.LocalIDRepo.FindAll() if err != nil { - return fmt.Errorf("could not get local ids: %v", err) + return nil, fmt.Errorf("could not get local ids: %v", err) } for tskID, lid := range idMap { - if del.args.LocalID == lid { + if del.Args.LocalID == lid { id = tskID } } if id == "" { - return fmt.Errorf("could not find local id") + return nil, fmt.Errorf("could not find local id") } - tsk, err := deps.TaskRepo.Find(id) + tsk, err := deps.TaskRepo.FindOne(id) if err != nil { - return fmt.Errorf("could not get task: %v", err) + return nil, fmt.Errorf("could not get task: %v", err) } it, err := tsk.Item() if err != nil { - return fmt.Errorf("could not convert task to sync item: %v", err) + return nil, fmt.Errorf("could not convert task to sync item: %v", err) } it.Deleted = true if err := deps.SyncRepo.Store(it); err != nil { - return fmt.Errorf("could not store sync item: %v", err) + return nil, fmt.Errorf("could not store sync item: %v", err) } if err := deps.LocalIDRepo.Delete(id); err != nil { - return fmt.Errorf("could not delete local id: %v", err) + return nil, fmt.Errorf("could not delete local id: %v", err) } if err := deps.TaskRepo.Delete(id); err != nil { - return fmt.Errorf("could not delete task: %v", err) + return nil, fmt.Errorf("could not delete task: %v", err) } - return nil + return nil, nil +} + +type DeleteResult struct{} + +func (dr DeleteResult) Render() string { + return "task deleted" } diff --git a/plan/command/delete_test.go b/plan/command/delete_test.go index bfd39e3..1a1145a 100644 --- a/plan/command/delete_test.go +++ b/plan/command/delete_test.go @@ -44,6 +44,7 @@ func TestDelete(t *testing.T) { }, } { t.Run(tc.name, func(t *testing.T) { + // setup taskRepo := memory.NewTask() syncRepo := memory.NewSync() if err := taskRepo.Store(e); err != nil { @@ -54,6 +55,7 @@ func TestDelete(t *testing.T) { t.Errorf("exp nil, got %v", err) } + // parse cmd, actParseErr := command.NewDeleteArgs().Parse(tc.main, tc.flags) if tc.expParseErr != (actParseErr != nil) { t.Errorf("exp %v, got %v", tc.expParseErr, actParseErr) @@ -61,19 +63,22 @@ func TestDelete(t *testing.T) { if tc.expParseErr { return } - actDoErr := cmd.Do(command.Dependencies{ + + // do + _, actDoErr := cmd.Do(command.Dependencies{ TaskRepo: taskRepo, LocalIDRepo: localIDRepo, SyncRepo: syncRepo, - }) != nil - if tc.expDoErr != actDoErr { + }) + if tc.expDoErr != (actDoErr != nil) { t.Errorf("exp false, got %v", actDoErr) } if tc.expDoErr { return } - _, repoErr := taskRepo.Find(e.ID) + // check + _, repoErr := taskRepo.FindOne(e.ID) if !errors.Is(repoErr, storage.ErrNotFound) { t.Errorf("exp %v, got %v", storage.ErrNotFound, repoErr) } diff --git a/plan/command/list.go b/plan/command/list.go index b31bca8..5696dfa 100644 --- a/plan/command/list.go +++ b/plan/command/list.go @@ -2,9 +2,16 @@ package command import ( "fmt" + "slices" + "time" + + "go-mod.ewintr.nl/planner/item" + "go-mod.ewintr.nl/planner/plan/format" + "go-mod.ewintr.nl/planner/plan/storage" ) type ListArgs struct { + params storage.TaskListParams } func NewListArgs() ListArgs { @@ -12,32 +19,96 @@ func NewListArgs() ListArgs { } func (la ListArgs) Parse(main []string, flags map[string]string) (Command, error) { - if len(main) > 0 && main[0] != "list" { + if len(main) > 2 { return nil, ErrWrongCommand } - return &List{}, nil + now := time.Now() + today := item.NewDate(now.Year(), int(now.Month()), now.Day()) + tomorrow := item.NewDate(now.Year(), int(now.Month()), now.Day()+1) + var date item.Date + var includeBefore, recurrer bool + + switch len(main) { + case 0: + date = today + includeBefore = true + case 1: + switch { + case slices.Contains([]string{"today", "tod"}, main[0]): + date = today + includeBefore = true + case slices.Contains([]string{"tomorrow", "tom"}, main[0]): + date = tomorrow + case main[0] == "list": + default: + return nil, ErrWrongCommand + } + case 2: + if main[0] == "list" && main[1] == "recur" { + recurrer = true + } else { + return nil, ErrWrongCommand + } + default: + return nil, ErrWrongCommand + } + + return &List{ + args: ListArgs{ + params: storage.TaskListParams{ + Date: date, + IncludeBefore: includeBefore, + Recurrer: recurrer, + }, + }, + }, nil } type List struct { + args ListArgs } -func (list *List) Do(deps Dependencies) error { +func (list *List) Do(deps Dependencies) (CommandResult, error) { localIDs, err := deps.LocalIDRepo.FindAll() if err != nil { - return fmt.Errorf("could not get local ids: %v", err) + return nil, fmt.Errorf("could not get local ids: %v", err) } - all, err := deps.TaskRepo.FindAll() + all, err := deps.TaskRepo.FindMany(list.args.params) if err != nil { - return err - } - for _, e := range all { - lid, ok := localIDs[e.ID] - if !ok { - return fmt.Errorf("could not find local id for %s", e.ID) - } - fmt.Printf("%s\t%d\t%s\t%s\t%s\n", e.ID, lid, e.Title, e.Date.String(), e.Duration.String()) + return nil, err } - return nil + res := make([]TaskWithLID, 0, len(all)) + for _, tsk := range all { + lid, ok := localIDs[tsk.ID] + if !ok { + return nil, fmt.Errorf("could not find local id for %s", tsk.ID) + } + res = append(res, TaskWithLID{ + LocalID: lid, + Task: tsk, + }) + } + return ListResult{ + Tasks: res, + }, nil +} + +type TaskWithLID struct { + LocalID int + Task item.Task +} + +type ListResult struct { + Tasks []TaskWithLID +} + +func (lr ListResult) Render() string { + data := [][]string{{"id", "date", "dur", "title"}} + for _, tl := range lr.Tasks { + data = append(data, []string{fmt.Sprintf("%d", tl.LocalID), tl.Task.Date.String(), tl.Task.Duration.String(), tl.Task.Title}) + } + + return fmt.Sprintf("\n%s\n", format.Table(data)) } diff --git a/plan/command/list_test.go b/plan/command/list_test.go index 953ba39..20d0234 100644 --- a/plan/command/list_test.go +++ b/plan/command/list_test.go @@ -30,15 +30,22 @@ func TestList(t *testing.T) { for _, tc := range []struct { name string main []string + expRes bool expErr bool }{ { - name: "empty", - main: []string{}, + name: "empty", + main: []string{}, + expRes: true, }, { - name: "list", - main: []string{"list"}, + name: "list", + main: []string{"list"}, + expRes: true, + }, + { + name: "empty list", + main: []string{"list", "recur"}, }, { name: "wrong", @@ -47,6 +54,7 @@ func TestList(t *testing.T) { }, } { t.Run(tc.name, func(t *testing.T) { + // parse cmd, actErr := command.NewListArgs().Parse(tc.main, nil) if tc.expErr != (actErr != nil) { t.Errorf("exp %v, got %v", tc.expErr, actErr) @@ -54,12 +62,22 @@ func TestList(t *testing.T) { if tc.expErr { return } - if err := cmd.Do(command.Dependencies{ + + // do + res, err := cmd.Do(command.Dependencies{ TaskRepo: taskRepo, LocalIDRepo: localRepo, - }); err != nil { + }) + if err != nil { t.Errorf("exp nil, got %v", err) } + + // check + listRes := res.(command.ListResult) + actRes := len(listRes.Tasks) > 0 + if tc.expRes != actRes { + t.Errorf("exp %v, got %v", tc.expRes, actRes) + } }) } } diff --git a/plan/command/show.go b/plan/command/show.go index d47dcf0..0fce2b5 100644 --- a/plan/command/show.go +++ b/plan/command/show.go @@ -1 +1,83 @@ package command + +import ( + "errors" + "fmt" + "strconv" + + "go-mod.ewintr.nl/planner/item" + "go-mod.ewintr.nl/planner/plan/format" + "go-mod.ewintr.nl/planner/plan/storage" +) + +type ShowArgs struct { + localID int +} + +func NewShowArgs() ShowArgs { + return ShowArgs{} +} + +func (sa ShowArgs) Parse(main []string, fields map[string]string) (Command, error) { + if len(main) != 1 { + return nil, ErrWrongCommand + } + lid, err := strconv.Atoi(main[0]) + if err != nil { + return nil, ErrWrongCommand + } + + return &Show{ + args: ShowArgs{ + localID: lid, + }, + }, nil +} + +type Show struct { + args ShowArgs +} + +func (s *Show) Do(deps Dependencies) (CommandResult, error) { + id, err := deps.LocalIDRepo.FindOne(s.args.localID) + switch { + case errors.Is(err, storage.ErrNotFound): + return nil, fmt.Errorf("could not find local id") + case err != nil: + return nil, err + } + + tsk, err := deps.TaskRepo.FindOne(id) + if err != nil { + return nil, fmt.Errorf("could not find task") + } + + return ShowResult{ + LocalID: s.args.localID, + Task: tsk, + }, nil +} + +type ShowResult struct { + LocalID int + Task item.Task +} + +func (sr ShowResult) Render() string { + + var recurStr string + if sr.Task.Recurrer != nil { + recurStr = sr.Task.Recurrer.String() + } + data := [][]string{ + {"title", sr.Task.Title}, + {"local id", fmt.Sprintf("%d", sr.LocalID)}, + {"date", sr.Task.Date.String()}, + {"time", sr.Task.Time.String()}, + {"duration", sr.Task.Duration.String()}, + {"recur", recurStr}, + // {"id", s.Task.ID}, + } + + return fmt.Sprintf("\n%s\n", format.Table(data)) +} diff --git a/plan/command/show_test.go b/plan/command/show_test.go new file mode 100644 index 0000000..1414ab8 --- /dev/null +++ b/plan/command/show_test.go @@ -0,0 +1,88 @@ +package command_test + +import ( + "fmt" + "testing" + + "go-mod.ewintr.nl/planner/item" + "go-mod.ewintr.nl/planner/plan/command" + "go-mod.ewintr.nl/planner/plan/storage/memory" +) + +func TestShow(t *testing.T) { + t.Parallel() + + taskRepo := memory.NewTask() + localRepo := memory.NewLocalID() + tsk := item.Task{ + ID: "id", + Date: item.NewDate(2024, 10, 7), + TaskBody: item.TaskBody{ + Title: "name", + }, + } + if err := taskRepo.Store(tsk); err != nil { + t.Errorf("exp nil, got %v", err) + } + if err := localRepo.Store(tsk.ID, 1); err != nil { + t.Errorf("exp nil, got %v", err) + } + + for _, tc := range []struct { + name string + main []string + expData [][]string + expParseErr bool + expDoErr bool + }{ + { + name: "empty", + main: []string{}, + expParseErr: true, + }, + { + name: "wrong", + main: []string{"delete"}, + expParseErr: true, + }, + { + name: "local id", + main: []string{"1"}, + expData: [][]string{ + {"title", tsk.Title}, + {"local id", fmt.Sprintf("%d", 1)}, + {"date", tsk.Date.String()}, + {"time", tsk.Time.String()}, + {"duration", tsk.Duration.String()}, + {"recur", ""}, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + // parse + cmd, actParseErr := command.NewShowArgs().Parse(tc.main, nil) + if tc.expParseErr != (actParseErr != nil) { + t.Errorf("exp %v, got %v", tc.expParseErr, actParseErr != nil) + } + if tc.expParseErr { + return + } + + // do + _, actDoErr := cmd.Do(command.Dependencies{ + TaskRepo: taskRepo, + LocalIDRepo: localRepo, + }) + if tc.expDoErr != (actDoErr != nil) { + t.Errorf("exp %v, got %v", tc.expDoErr, actDoErr != nil) + } + if tc.expDoErr { + return + } + // if diff := cmp.Diff(tc.expData, actData); diff != "" { + // t.Errorf("(+exp, -got)%s\n", diff) + // } + }) + } + +} diff --git a/plan/command/sync.go b/plan/command/sync.go index 7da90df..26d040c 100644 --- a/plan/command/sync.go +++ b/plan/command/sync.go @@ -25,37 +25,37 @@ func (sa SyncArgs) Parse(main []string, flags map[string]string) (Command, error type Sync struct{} -func (s *Sync) Do(deps Dependencies) error { +func (s *Sync) Do(deps Dependencies) (CommandResult, error) { // local new and updated sendItems, err := deps.SyncRepo.FindAll() if err != nil { - return fmt.Errorf("could not get updated items: %v", err) + return nil, fmt.Errorf("could not get updated items: %v", err) } if err := deps.SyncClient.Update(sendItems); err != nil { - return fmt.Errorf("could not send updated items: %v", err) + return nil, fmt.Errorf("could not send updated items: %v", err) } if err := deps.SyncRepo.DeleteAll(); err != nil { - return fmt.Errorf("could not clear updated items: %v", err) + return nil, fmt.Errorf("could not clear updated items: %v", err) } // get new/updated items ts, err := deps.SyncRepo.LastUpdate() if err != nil { - return fmt.Errorf("could not find timestamp of last update: %v", err) + return nil, fmt.Errorf("could not find timestamp of last update: %v", err) } recItems, err := deps.SyncClient.Updated([]item.Kind{item.KindTask}, ts) if err != nil { - return fmt.Errorf("could not receive updates: %v", err) + return nil, fmt.Errorf("could not receive updates: %v", err) } updated := make([]item.Item, 0) for _, ri := range recItems { if ri.Deleted { if err := deps.LocalIDRepo.Delete(ri.ID); err != nil && !errors.Is(err, storage.ErrNotFound) { - return fmt.Errorf("could not delete local id: %v", err) + return nil, fmt.Errorf("could not delete local id: %v", err) } if err := deps.TaskRepo.Delete(ri.ID); err != nil && !errors.Is(err, storage.ErrNotFound) { - return fmt.Errorf("could not delete task: %v", err) + return nil, fmt.Errorf("could not delete task: %v", err) } continue } @@ -64,12 +64,12 @@ func (s *Sync) Do(deps Dependencies) error { lidMap, err := deps.LocalIDRepo.FindAll() if err != nil { - return fmt.Errorf("could not get local ids: %v", err) + return nil, fmt.Errorf("could not get local ids: %v", err) } for _, u := range updated { var tskBody item.TaskBody if err := json.Unmarshal([]byte(u.Body), &tskBody); err != nil { - return fmt.Errorf("could not unmarshal task body: %v", err) + return nil, fmt.Errorf("could not unmarshal task body: %v", err) } tsk := item.Task{ ID: u.ID, @@ -79,20 +79,24 @@ func (s *Sync) Do(deps Dependencies) error { TaskBody: tskBody, } if err := deps.TaskRepo.Store(tsk); err != nil { - return fmt.Errorf("could not store task: %v", err) + return nil, fmt.Errorf("could not store task: %v", err) } lid, ok := lidMap[u.ID] if !ok { lid, err = deps.LocalIDRepo.Next() if err != nil { - return fmt.Errorf("could not get next local id: %v", err) + return nil, fmt.Errorf("could not get next local id: %v", err) } if err := deps.LocalIDRepo.Store(u.ID, lid); err != nil { - return fmt.Errorf("could not store local id: %v", err) + return nil, fmt.Errorf("could not store local id: %v", err) } } } - return nil + return SyncResult{}, nil } + +type SyncResult struct{} + +func (sr SyncResult) Render() string { return "" } diff --git a/plan/command/sync_test.go b/plan/command/sync_test.go index 151e174..9e3b55a 100644 --- a/plan/command/sync_test.go +++ b/plan/command/sync_test.go @@ -7,6 +7,7 @@ import ( "github.com/google/go-cmp/cmp" "go-mod.ewintr.nl/planner/item" "go-mod.ewintr.nl/planner/plan/command" + "go-mod.ewintr.nl/planner/plan/storage" "go-mod.ewintr.nl/planner/plan/storage/memory" "go-mod.ewintr.nl/planner/sync/client" ) @@ -80,7 +81,7 @@ func TestSyncSend(t *testing.T) { if err != nil { t.Errorf("exp nil, got %v", err) } - if err := cmd.Do(command.Dependencies{ + if _, err := cmd.Do(command.Dependencies{ TaskRepo: taskRepo, LocalIDRepo: localIDRepo, SyncRepo: syncRepo, @@ -206,7 +207,7 @@ func TestSyncReceive(t *testing.T) { if err != nil { t.Errorf("exp nil, got %v", err) } - if err := cmd.Do(command.Dependencies{ + if _, err := cmd.Do(command.Dependencies{ TaskRepo: taskRepo, LocalIDRepo: localIDRepo, SyncRepo: syncRepo, @@ -216,7 +217,7 @@ func TestSyncReceive(t *testing.T) { } // check result - actTasks, err := taskRepo.FindAll() + actTasks, err := taskRepo.FindMany(storage.TaskListParams{}) if err != nil { t.Errorf("exp nil, got %v", err) } diff --git a/plan/command/update.go b/plan/command/update.go index 1dd70c1..de9ea75 100644 --- a/plan/command/update.go +++ b/plan/command/update.go @@ -1,12 +1,14 @@ package command import ( + "errors" "fmt" "strconv" "strings" "time" "go-mod.ewintr.nl/planner/item" + "go-mod.ewintr.nl/planner/plan/storage" ) type UpdateArgs struct { @@ -22,10 +24,10 @@ type UpdateArgs struct { func NewUpdateArgs() UpdateArgs { return UpdateArgs{ fieldTPL: map[string][]string{ - "date": []string{"d", "date", "on"}, - "time": []string{"t", "time", "at"}, - "duration": []string{"dur", "duration", "for"}, - "recurrer": []string{"rec", "recurrer"}, + "date": {"d", "date", "on"}, + "time": {"t", "time", "at"}, + "duration": {"dur", "duration", "for"}, + "recurrer": {"rec", "recurrer"}, }, } } @@ -83,24 +85,18 @@ type Update struct { args UpdateArgs } -func (u *Update) Do(deps Dependencies) error { - var id string - idMap, err := deps.LocalIDRepo.FindAll() - if err != nil { - return fmt.Errorf("could not get local ids: %v", err) - } - for tid, lid := range idMap { - if u.args.LocalID == lid { - id = tid - } - } - if id == "" { - return fmt.Errorf("could not find local id") +func (u *Update) Do(deps Dependencies) (CommandResult, error) { + id, err := deps.LocalIDRepo.FindOne(u.args.LocalID) + switch { + case errors.Is(err, storage.ErrNotFound): + return nil, fmt.Errorf("could not find local id") + case err != nil: + return nil, err } - tsk, err := deps.TaskRepo.Find(id) + tsk, err := deps.TaskRepo.FindOne(id) if err != nil { - return fmt.Errorf("could not find task") + return nil, fmt.Errorf("could not find task") } if u.args.Title != "" { @@ -121,20 +117,26 @@ func (u *Update) Do(deps Dependencies) error { } if !tsk.Valid() { - return fmt.Errorf("task is unvalid") + return nil, fmt.Errorf("task is unvalid") } if err := deps.TaskRepo.Store(tsk); err != nil { - return fmt.Errorf("could not store task: %v", err) + return nil, fmt.Errorf("could not store task: %v", err) } it, err := tsk.Item() if err != nil { - return fmt.Errorf("could not convert task to sync item: %v", err) + return nil, fmt.Errorf("could not convert task to sync item: %v", err) } if err := deps.SyncRepo.Store(it); err != nil { - return fmt.Errorf("could not store sync item: %v", err) + return nil, fmt.Errorf("could not store sync item: %v", err) } - return nil + return UpdateResult{}, nil +} + +type UpdateResult struct{} + +func (ur UpdateResult) Render() string { + return "task updated" } diff --git a/plan/command/update_test.go b/plan/command/update_test.go index fa1ab82..aae062b 100644 --- a/plan/command/update_test.go +++ b/plan/command/update_test.go @@ -164,6 +164,7 @@ func TestUpdateExecute(t *testing.T) { }, } { t.Run(tc.name, func(t *testing.T) { + // setup taskRepo := memory.NewTask() localIDRepo := memory.NewLocalID() syncRepo := memory.NewSync() @@ -182,6 +183,7 @@ func TestUpdateExecute(t *testing.T) { t.Errorf("exp nil, ,got %v", err) } + // parse cmd, actErr := command.NewUpdateArgs().Parse(tc.main, tc.fields) if tc.expParseErr != (actErr != nil) { t.Errorf("exp %v, got %v", tc.expParseErr, actErr) @@ -189,19 +191,22 @@ func TestUpdateExecute(t *testing.T) { if tc.expParseErr { return } - actDoErr := cmd.Do(command.Dependencies{ + + // do + _, actDoErr := cmd.Do(command.Dependencies{ TaskRepo: taskRepo, LocalIDRepo: localIDRepo, SyncRepo: syncRepo, - }) != nil - if tc.expDoErr != actDoErr { + }) + if tc.expDoErr != (actDoErr != nil) { t.Errorf("exp %v, got %v", tc.expDoErr, actDoErr) } if tc.expDoErr { return } - actTask, err := taskRepo.Find(tskID) + // check + actTask, err := taskRepo.FindOne(tskID) if err != nil { t.Errorf("exp nil, got %v", err) } diff --git a/plan/format/format.go b/plan/format/format.go new file mode 100644 index 0000000..5b5e955 --- /dev/null +++ b/plan/format/format.go @@ -0,0 +1,97 @@ +package format + +import ( + "fmt" + "os" + "os/exec" + "strconv" + "strings" +) + +func Table(data [][]string) string { + if len(data) == 0 { + return "" + } + + // make all cells in a column the same width + max := make([]int, len(data[0])) + for _, row := range data { + for c, cell := range row { + if len(cell) > max[c] { + max[c] = len(cell) + } + } + } + for r, row := range data { + for c, cell := range row { + for s := len(cell); s < max[c]; s++ { + data[r][c] += " " + } + } + } + + // make it smaller if the result is too wide + // only by making the widest column smaller for now + maxWidth := findTermWidth() + if maxWidth != 0 { + width := len(max) - 1 + for _, m := range max { + width += m + } + shortenWith := width - maxWidth + widestColNo, widestColLen := 0, 0 + for i, m := range max { + if m > widestColLen { + widestColNo, widestColLen = i, m + } + } + newTaskColWidth := max[widestColNo] - shortenWith + if newTaskColWidth < 0 { + return "table is too wide to display\n" + } + if newTaskColWidth < max[widestColNo] { + for r, row := range data { + data[r][widestColNo] = row[widestColNo][:newTaskColWidth] + } + } + } + + // print the rows + var output string + for r, row := range data { + if r%3 == 0 { + output += fmt.Sprintf("%s", "\x1b[48;5;237m") + } + for c, col := range row { + output += col + if c != len(row)-1 { + output += " " + } + } + if r%3 == 0 { + output += fmt.Sprintf("%s", "\x1b[49m") + } + output += "\n" + } + + return output +} + +func findTermWidth() int { + cmd := exec.Command("stty", "size") + cmd.Stdin = os.Stdin + out, err := cmd.Output() + if err != nil { + return 0 + } + + s := string(out) + s = strings.TrimSpace(s) + sArr := strings.Split(s, " ") + + width, err := strconv.Atoi(sArr[1]) + if err != nil { + return 0 + } + return width +} diff --git a/plan/storage/memory/localid.go b/plan/storage/memory/localid.go index ece7ba1..59923e8 100644 --- a/plan/storage/memory/localid.go +++ b/plan/storage/memory/localid.go @@ -18,6 +18,19 @@ func NewLocalID() *LocalID { } } +func (ml *LocalID) FindOne(lid int) (string, error) { + ml.mutex.RLock() + defer ml.mutex.RUnlock() + + for id, l := range ml.ids { + if lid == l { + return id, nil + } + } + + return "", storage.ErrNotFound +} + func (ml *LocalID) FindAll() (map[string]int, error) { ml.mutex.RLock() defer ml.mutex.RUnlock() diff --git a/plan/storage/memory/localid_test.go b/plan/storage/memory/localid_test.go index eddaf6f..bd56be1 100644 --- a/plan/storage/memory/localid_test.go +++ b/plan/storage/memory/localid_test.go @@ -54,6 +54,21 @@ func TestLocalID(t *testing.T) { t.Errorf("exp 2, got %v", actLid) } + t.Log("find by local id") + actID, actErr := repo.FindOne(1) + if actErr != nil { + t.Errorf("exp nil, got %v", actErr) + } + if actID != "test" { + t.Errorf("exp test, got %v", actID) + } + + t.Log("unknown local id") + actID, actErr = repo.FindOne(2) + if !errors.Is(actErr, storage.ErrNotFound) { + t.Errorf("exp ErrNotFound, got %v", actErr) + } + actIDs, actErr = repo.FindAll() if actErr != nil { t.Errorf("exp nil, got %v", actErr) diff --git a/plan/storage/memory/task.go b/plan/storage/memory/task.go index bfba997..10290a1 100644 --- a/plan/storage/memory/task.go +++ b/plan/storage/memory/task.go @@ -19,7 +19,7 @@ func NewTask() *Task { } } -func (t *Task) Find(id string) (item.Task, error) { +func (t *Task) FindOne(id string) (item.Task, error) { t.mutex.RLock() defer t.mutex.RUnlock() @@ -30,13 +30,15 @@ func (t *Task) Find(id string) (item.Task, error) { return task, nil } -func (t *Task) FindAll() ([]item.Task, error) { +func (t *Task) FindMany(params storage.TaskListParams) ([]item.Task, error) { t.mutex.RLock() defer t.mutex.RUnlock() tasks := make([]item.Task, 0, len(t.tasks)) for _, tsk := range t.tasks { - tasks = append(tasks, tsk) + if storage.Match(tsk, params) { + tasks = append(tasks, tsk) + } } sort.Slice(tasks, func(i, j int) bool { return tasks[i].ID < tasks[j].ID diff --git a/plan/storage/memory/task_test.go b/plan/storage/memory/task_test.go index 265ab67..7fac228 100644 --- a/plan/storage/memory/task_test.go +++ b/plan/storage/memory/task_test.go @@ -4,6 +4,7 @@ import ( "testing" "go-mod.ewintr.nl/planner/item" + "go-mod.ewintr.nl/planner/plan/storage" ) func TestTask(t *testing.T) { @@ -12,7 +13,7 @@ func TestTask(t *testing.T) { mem := NewTask() t.Log("empty") - actTasks, actErr := mem.FindAll() + actTasks, actErr := mem.FindMany(storage.TaskListParams{}) if actErr != nil { t.Errorf("exp nil, got %v", actErr) } @@ -22,7 +23,8 @@ func TestTask(t *testing.T) { t.Log("store") tsk1 := item.Task{ - ID: "id-1", + ID: "id-1", + Date: item.NewDate(2024, 12, 29), } if err := mem.Store(tsk1); err != nil { t.Errorf("exp nil, got %v", err) @@ -36,7 +38,7 @@ func TestTask(t *testing.T) { } t.Log("find one") - actTask, actErr := mem.Find(tsk1.ID) + actTask, actErr := mem.FindOne(tsk1.ID) if actErr != nil { t.Errorf("exp nil, got %v", actErr) } @@ -45,11 +47,23 @@ func TestTask(t *testing.T) { } t.Log("find all") - actTasks, actErr = mem.FindAll() + actTasks, actErr = mem.FindMany(storage.TaskListParams{}) if actErr != nil { t.Errorf("exp nil, got %v", actErr) } if diff := item.TaskDiffs([]item.Task{tsk1, tsk2}, actTasks); diff != "" { t.Errorf("(exp -, got +)\n%s", diff) } + + t.Log("fond some") + actTasks, actErr = mem.FindMany(storage.TaskListParams{ + Date: item.NewDate(2024, 12, 29), + }) + if actErr != nil { + t.Errorf("exp nil, got %v", actErr) + } + if diff := item.TaskDiffs([]item.Task{tsk1}, actTasks); diff != "" { + t.Errorf("(exp -, got +)\n%s", diff) + } + } diff --git a/plan/storage/sqlite/localid.go b/plan/storage/sqlite/localid.go index f97f1d0..08f8629 100644 --- a/plan/storage/sqlite/localid.go +++ b/plan/storage/sqlite/localid.go @@ -2,6 +2,7 @@ package sqlite import ( "database/sql" + "errors" "fmt" "go-mod.ewintr.nl/planner/plan/storage" @@ -11,6 +12,23 @@ type LocalID struct { db *sql.DB } +func (l *LocalID) FindOne(lid int) (string, error) { + var id string + err := l.db.QueryRow(` +SELECT id +FROM localids +WHERE local_id = ? +`, lid).Scan(&id) + switch { + case errors.Is(err, sql.ErrNoRows): + return "", storage.ErrNotFound + case err != nil: + return "", fmt.Errorf("%w: %v", ErrSqliteFailure, err) + } + + return id, nil +} + func (l *LocalID) FindAll() (map[string]int, error) { rows, err := l.db.Query(` SELECT id, local_id diff --git a/plan/storage/sqlite/task.go b/plan/storage/sqlite/task.go index 9b23b50..ecf9a79 100644 --- a/plan/storage/sqlite/task.go +++ b/plan/storage/sqlite/task.go @@ -38,7 +38,7 @@ recurrer=? return nil } -func (t *SqliteTask) Find(id string) (item.Task, error) { +func (t *SqliteTask) FindOne(id string) (item.Task, error) { var tsk item.Task var dateStr, timeStr, recurStr, durStr string err := t.db.QueryRow(` @@ -63,14 +63,35 @@ WHERE id = ?`, id).Scan(&tsk.ID, &tsk.Title, &dateStr, &timeStr, &durStr, &recur return tsk, nil } -func (t *SqliteTask) FindAll() ([]item.Task, error) { - rows, err := t.db.Query(` -SELECT id, title, date, time, duration, recurrer -FROM tasks`) +func (t *SqliteTask) FindMany(params storage.TaskListParams) ([]item.Task, error) { + query := `SELECT id, title, date, time, duration, recurrer FROM tasks` + args := []interface{}{} + where := []string{} + + if params.Recurrer { + where = append(where, `recurrer IS NOT NULL AND recurrer != ''`) + } + if !params.Date.IsZero() && !params.IncludeBefore { + where = append(where, `date = ?`) + args = append(args, params.Date.String()) + } + if !params.Date.IsZero() && params.IncludeBefore { + where = append(where, `date <= ?`) + args = append(args, params.Date.String()) + } + + if len(where) > 0 { + query += ` WHERE ` + where[0] + for _, w := range where[1:] { + query += ` AND ` + w + } + } + + rows, err := t.db.Query(query, args...) if err != nil { return nil, fmt.Errorf("%w: %v", ErrSqliteFailure, err) } - result := make([]item.Task, 0) + tasks := make([]item.Task, 0) defer rows.Close() for rows.Next() { var tsk item.Task @@ -87,10 +108,10 @@ FROM tasks`) tsk.Duration = dur tsk.Recurrer = item.NewRecurrer(recurStr) - result = append(result, tsk) + tasks = append(tasks, tsk) } - return result, nil + return tasks, nil } func (s *SqliteTask) Delete(id string) error { diff --git a/plan/storage/storage.go b/plan/storage/storage.go index ed7fbff..8634ffd 100644 --- a/plan/storage/storage.go +++ b/plan/storage/storage.go @@ -13,6 +13,7 @@ var ( ) type LocalID interface { + FindOne(lid int) (string, error) FindAll() (map[string]int, error) FindOrNext(id string) (int, error) Next() (int, error) @@ -27,13 +28,35 @@ type Sync interface { LastUpdate() (time.Time, error) } +type TaskListParams struct { + Recurrer bool + Date item.Date + IncludeBefore bool +} + type Task interface { Store(task item.Task) error - Find(id string) (item.Task, error) - FindAll() ([]item.Task, error) + FindOne(id string) (item.Task, error) + FindMany(params TaskListParams) ([]item.Task, error) Delete(id string) error } +func Match(tsk item.Task, params TaskListParams) bool { + if params.Recurrer && tsk.Recurrer == nil { + return false + } + if !params.Date.IsZero() { + if !params.IncludeBefore && !params.Date.Equal(tsk.Date) { + return false + } + if params.IncludeBefore && tsk.Date.After(params.Date) { + return false + } + } + + return true +} + func NextLocalID(used []int) int { if len(used) == 0 { return 1 diff --git a/plan/storage/storage_test.go b/plan/storage/storage_test.go index 528ec1a..937e7b9 100644 --- a/plan/storage/storage_test.go +++ b/plan/storage/storage_test.go @@ -3,10 +3,60 @@ package storage_test import ( "testing" + "go-mod.ewintr.nl/planner/item" "go-mod.ewintr.nl/planner/plan/storage" ) +func TestMatch(t *testing.T) { + t.Parallel() + + tskMatch := item.Task{ + ID: "id", + Date: item.NewDate(2024, 12, 29), + Recurrer: item.NewRecurrer("2024-12-29, daily"), + TaskBody: item.TaskBody{ + Title: "name", + }, + } + tskNotMatch := item.Task{ + ID: "id", + Date: item.NewDate(2024, 12, 28), + TaskBody: item.TaskBody{ + Title: "name", + }, + } + + for _, tc := range []struct { + name string + params storage.TaskListParams + }{ + { + name: "date", + params: storage.TaskListParams{ + Date: item.NewDate(2024, 12, 29), + }, + }, + { + name: "recurrer", + params: storage.TaskListParams{ + Recurrer: true, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + if !storage.Match(tskMatch, tc.params) { + t.Errorf("exp tsk to match") + } + if storage.Match(tskNotMatch, tc.params) { + t.Errorf("exp tsk to not match") + } + }) + } +} + func TestNextLocalId(t *testing.T) { + t.Parallel() + for _, tc := range []struct { name string used []int