refactor args and cmds

This commit is contained in:
Erik Winter 2024-12-27 11:20:32 +01:00
parent 2a62e6c335
commit c209ba6197
18 changed files with 494 additions and 899 deletions

BIN
dist/plan vendored

Binary file not shown.

View File

@ -3,109 +3,107 @@ package command
import (
"fmt"
"strings"
"time"
"github.com/google/uuid"
"go-mod.ewintr.nl/planner/item"
"go-mod.ewintr.nl/planner/plan/storage"
)
type Add struct {
localIDRepo storage.LocalID
taskRepo storage.Task
syncRepo storage.Sync
argSet *ArgSet
type AddArgs struct {
fieldTPL map[string][]string
task item.Task
}
func NewAdd(localRepo storage.LocalID, taskRepo storage.Task, syncRepo storage.Sync) Command {
return &Add{
localIDRepo: localRepo,
taskRepo: taskRepo,
syncRepo: syncRepo,
argSet: &ArgSet{
Flags: map[string]Flag{
FlagOn: &FlagDate{},
FlagAt: &FlagTime{},
FlagFor: &FlagDuration{},
FlagRec: &FlagRecurrer{},
},
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"},
},
}
}
func (add *Add) Execute(main []string, flags map[string]string) error {
func (aa AddArgs) Parse(main []string, fields map[string]string) (Command, error) {
if len(main) == 0 || main[0] != "add" {
return ErrWrongCommand
return nil, ErrWrongCommand
}
as := add.argSet
if len(main) > 1 {
as.Main = strings.Join(main[1:], " ")
}
for k := range as.Flags {
v, ok := flags[k]
if !ok {
continue
}
if err := as.Set(k, v); err != nil {
return fmt.Errorf("could not set %s: %v", k, err)
}
}
if as.Main == "" {
return fmt.Errorf("%w: title is required", ErrInvalidArg)
}
if !as.IsSet(FlagOn) {
return fmt.Errorf("%w: date is required", ErrInvalidArg)
}
if !as.IsSet(FlagAt) && as.IsSet(FlagFor) {
return fmt.Errorf("%w: can not have duration without start time", ErrInvalidArg)
}
if as.IsSet(FlagAt) && !as.IsSet(FlagFor) {
if err := as.Flags[FlagFor].Set("1h"); err != nil {
return fmt.Errorf("could not set duration to one hour")
}
}
if !as.IsSet(FlagAt) && !as.IsSet(FlagFor) {
if err := as.Flags[FlagFor].Set("24h"); err != nil {
return fmt.Errorf("could not set duration to 24 hours")
main = main[1:]
if len(main) == 0 {
return nil, fmt.Errorf("%w: title is required for add", ErrInvalidArg)
}
fields, err := ResolveFields(fields, aa.fieldTPL)
if err != nil {
return nil, err
}
return add.do()
}
func (add *Add) do() error {
as := add.argSet
rec := as.GetRecurrer(FlagRec)
tsk := item.Task{
ID: uuid.New().String(),
Date: as.GetDate(FlagOn),
Recurrer: rec,
TaskBody: item.TaskBody{
Title: as.Main,
Time: as.GetTime(FlagAt),
Duration: as.GetDuration(FlagFor),
Title: strings.Join(main, ","),
},
}
if rec != nil {
tsk.RecurNext = rec.First()
if val, ok := fields["date"]; ok {
d := item.NewDateFromString(val)
if d.IsZero() {
return nil, fmt.Errorf("%w: could not parse date", ErrInvalidArg)
}
tsk.Date = d
}
if val, ok := fields["time"]; ok {
t := item.NewTimeFromString(val)
if t.IsZero() {
return nil, fmt.Errorf("%w: could not parse time", ErrInvalidArg)
}
tsk.Time = t
}
if val, ok := fields["duration"]; ok {
d, err := time.ParseDuration(val)
if err != nil {
return nil, fmt.Errorf("%w: could not parse duration", ErrInvalidArg)
}
tsk.Duration = d
}
if val, ok := fields["recurrer"]; ok {
rec := item.NewRecurrer(val)
if rec == nil {
return nil, fmt.Errorf("%w: could not parse recurrer", ErrInvalidArg)
}
tsk.Recurrer = rec
tsk.RecurNext = tsk.Recurrer.First()
}
if err := add.taskRepo.Store(tsk); err != nil {
return &Add{
args: AddArgs{
task: tsk,
},
}, nil
}
type Add struct {
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)
}
localID, err := add.localIDRepo.Next()
localID, err := deps.LocalIDRepo.Next()
if err != nil {
return fmt.Errorf("could not create next local id: %v", err)
}
if err := add.localIDRepo.Store(tsk.ID, localID); err != nil {
if err := deps.LocalIDRepo.Store(a.args.task.ID, localID); err != nil {
return fmt.Errorf("could not store local id: %v", err)
}
it, err := tsk.Item()
it, err := a.args.task.Item()
if err != nil {
return fmt.Errorf("could not convert event to sync item: %v", err)
}
if err := add.syncRepo.Store(it); err != nil {
if err := deps.SyncRepo.Store(it); err != nil {
return fmt.Errorf("could not store sync item: %v", err)
}

View File

@ -14,14 +14,13 @@ func TestAdd(t *testing.T) {
aDate := item.NewDate(2024, 11, 2)
aTime := item.NewTime(12, 0)
aDay := time.Duration(24) * time.Hour
anHourStr := "1h"
anHour := time.Hour
for _, tc := range []struct {
name string
main []string
flags map[string]string
fields map[string]string
expErr bool
expTask item.Task
}{
@ -32,38 +31,18 @@ func TestAdd(t *testing.T) {
{
name: "title missing",
main: []string{"add"},
flags: map[string]string{
command.FlagOn: aDate.String(),
fields: map[string]string{
"date": aDate.String(),
},
expErr: true,
},
{
name: "date missing",
main: []string{"add", "some", "title"},
expErr: true,
},
{
name: "only date",
name: "date time duration",
main: []string{"add", "title"},
flags: map[string]string{
command.FlagOn: aDate.String(),
},
expTask: item.Task{
ID: "title",
Date: aDate,
TaskBody: item.TaskBody{
Title: "title",
Duration: aDay,
},
},
},
{
name: "date, time and duration",
main: []string{"add", "title"},
flags: map[string]string{
command.FlagOn: aDate.String(),
command.FlagAt: aTime.String(),
command.FlagFor: anHourStr,
fields: map[string]string{
"date": aDate.String(),
"time": aTime.String(),
"duration": anHourStr,
},
expTask: item.Task{
ID: "title",
@ -75,28 +54,25 @@ func TestAdd(t *testing.T) {
},
},
},
{
name: "date and duration",
main: []string{"add", "title"},
flags: map[string]string{
command.FlagOn: aDate.String(),
command.FlagFor: anHourStr,
},
expErr: true,
},
} {
t.Run(tc.name, func(t *testing.T) {
taskRepo := memory.NewTask()
localRepo := memory.NewLocalID()
localIDRepo := memory.NewLocalID()
syncRepo := memory.NewSync()
cmd := command.NewAdd(localRepo, taskRepo, syncRepo)
actParseErr := cmd.Execute(tc.main, tc.flags) != nil
if tc.expErr != actParseErr {
cmd, actParseErr := command.NewAddArgs().Parse(tc.main, tc.fields)
if tc.expErr != (actParseErr != nil) {
t.Errorf("exp %v, got %v", tc.expErr, actParseErr)
}
if tc.expErr {
return
}
if err := cmd.Do(command.Dependencies{
TaskRepo: taskRepo,
LocalIDRepo: localIDRepo,
SyncRepo: syncRepo,
}); err != nil {
t.Errorf("exp nil, got %v", err)
}
actTasks, err := taskRepo.FindAll()
if err != nil {
@ -106,7 +82,7 @@ func TestAdd(t *testing.T) {
t.Errorf("exp 1, got %d", len(actTasks))
}
actLocalIDs, err := localRepo.FindAll()
actLocalIDs, err := localIDRepo.FindAll()
if err != nil {
t.Errorf("exp nil, got %v", err)
}

View File

@ -1,101 +0,0 @@
package command
import (
"fmt"
"time"
"go-mod.ewintr.nl/planner/item"
)
type ArgSet struct {
Main string
Flags map[string]Flag
}
func (as *ArgSet) Set(name, val string) error {
f, ok := as.Flags[name]
if !ok {
return fmt.Errorf("unknown flag %s", name)
}
return f.Set(val)
}
func (as *ArgSet) IsSet(name string) bool {
f, ok := as.Flags[name]
if !ok {
return false
}
return f.IsSet()
}
func (as *ArgSet) GetString(name string) string {
flag, ok := as.Flags[name]
if !ok {
return ""
}
val, ok := flag.Get().(string)
if !ok {
return ""
}
return val
}
func (as *ArgSet) GetDate(name string) item.Date {
flag, ok := as.Flags[name]
if !ok {
return item.Date{}
}
val, ok := flag.Get().(item.Date)
if !ok {
return item.Date{}
}
return val
}
func (as *ArgSet) GetTime(name string) item.Time {
flag, ok := as.Flags[name]
if !ok {
return item.Time{}
}
val, ok := flag.Get().(item.Time)
if !ok {
return item.Time{}
}
return val
}
func (as *ArgSet) GetDuration(name string) time.Duration {
flag, ok := as.Flags[name]
if !ok {
return time.Duration(0)
}
val, ok := flag.Get().(time.Duration)
if !ok {
return time.Duration(0)
}
return val
}
func (as *ArgSet) GetRecurrer(name string) item.Recurrer {
flag, ok := as.Flags[name]
if !ok {
return nil
}
val, ok := flag.Get().(item.Recurrer)
if !ok {
return nil
}
return val
}
func (as *ArgSet) GetInt(name string) int {
flag, ok := as.Flags[name]
if !ok {
return 0
}
val, ok := flag.Get().(int)
if !ok {
return 0
}
return val
}

View File

@ -1,103 +0,0 @@
package command_test
import (
"testing"
"time"
"go-mod.ewintr.nl/planner/item"
"go-mod.ewintr.nl/planner/plan/command"
)
func TestArgSet(t *testing.T) {
for _, tt := range []struct {
name string
flags map[string]command.Flag
flagName string
setValue string
exp interface{}
expErr bool
}{
{
name: "string flag success",
flags: map[string]command.Flag{
"title": &command.FlagString{Name: "title"},
},
flagName: "title",
setValue: "test title",
exp: "test title",
},
{
name: "date flag success",
flags: map[string]command.Flag{
"date": &command.FlagDate{Name: "date"},
},
flagName: "date",
setValue: "2024-01-02",
exp: time.Date(2024, 1, 2, 0, 0, 0, 0, time.UTC),
},
{
name: "time flag success",
flags: map[string]command.Flag{
"time": &command.FlagTime{Name: "time"},
},
flagName: "time",
setValue: "15:04",
exp: time.Date(0, 1, 1, 15, 4, 0, 0, time.UTC),
},
{
name: "duration flag success",
flags: map[string]command.Flag{
"duration": &command.FlagDuration{Name: "duration"},
},
flagName: "duration",
setValue: "2h30m",
exp: 2*time.Hour + 30*time.Minute,
},
{
name: "recur period flag success",
flags: map[string]command.Flag{
"recur": &command.FlagRecurrer{Name: "recur"},
},
flagName: "recur",
setValue: "2024-12-23, daily",
exp: item.NewRecurrer("2024-12-23, daily"),
},
{
name: "unknown flag error",
flags: map[string]command.Flag{},
flagName: "unknown",
setValue: "value",
expErr: true,
},
{
name: "invalid date format error",
flags: map[string]command.Flag{
"date": &command.FlagDate{Name: "date"},
},
flagName: "date",
setValue: "invalid",
expErr: true,
},
} {
t.Run(tt.name, func(t *testing.T) {
as := &command.ArgSet{
Main: "test",
Flags: tt.flags,
}
err := as.Set(tt.flagName, tt.setValue)
if (err != nil) != tt.expErr {
t.Errorf("ArgSet.Set() error = %v, expErr %v", err, tt.expErr)
return
}
if tt.expErr {
return
}
if !as.IsSet(tt.flagName) {
t.Errorf("ArgSet.IsSet() = false, want true for flag %s", tt.flagName)
}
})
}
}

View File

@ -3,66 +3,104 @@ package command
import (
"errors"
"fmt"
"slices"
"strings"
"go-mod.ewintr.nl/planner/plan/storage"
"go-mod.ewintr.nl/planner/sync/client"
)
const (
FlagTitle = "title"
FlagOn = "on"
FlagAt = "at"
FlagFor = "for"
FlagRec = "rec"
DateFormat = "2006-01-02"
TimeFormat = "15:04"
)
var (
ErrWrongCommand = errors.New("wrong command")
ErrInvalidArg = errors.New("invalid argument")
)
type Dependencies struct {
LocalIDRepo storage.LocalID
TaskRepo storage.Task
SyncRepo storage.Sync
SyncClient client.Client
}
type CommandArgs interface {
Parse(main []string, fields map[string]string) (Command, error)
}
type Command interface {
Execute([]string, map[string]string) error
Do(deps Dependencies) error
}
type CLI struct {
Commands []Command
deps Dependencies
cmdArgs []CommandArgs
}
func NewCLI(deps Dependencies) *CLI {
return &CLI{
deps: deps,
cmdArgs: []CommandArgs{
NewAddArgs(), NewDeleteArgs(), NewListArgs(),
NewSyncArgs(), NewUpdateArgs(),
},
}
}
func (cli *CLI) Run(args []string) error {
main, flags, err := ParseFlags(args)
if err != nil {
return err
}
for _, c := range cli.Commands {
err := c.Execute(main, flags)
main, flags := FindFields(args)
for _, ca := range cli.cmdArgs {
cmd, err := ca.Parse(main, flags)
switch {
case errors.Is(err, ErrWrongCommand):
continue
case err != nil:
return err
default:
return nil
return cmd.Do(cli.deps)
}
}
return fmt.Errorf("could not find matching command")
}
func ParseFlags(args []string) ([]string, map[string]string, error) {
flags := make(map[string]string)
func FindFields(args []string) ([]string, map[string]string) {
fields := make(map[string]string)
main := make([]string, 0)
var inMain bool
for i := 0; i < len(args); i++ {
if strings.HasPrefix(args[i], "-") {
inMain = false
if i+1 >= len(args) {
return nil, nil, fmt.Errorf("flag wihout value")
}
flags[strings.TrimPrefix(args[i], "-")] = args[i+1]
i++
if k, v, ok := strings.Cut(args[i], ":"); ok && !strings.Contains(k, " ") {
fields[k] = v
continue
}
if !inMain && len(main) > 0 {
return nil, nil, fmt.Errorf("two mains")
}
inMain = true
main = append(main, args[i])
}
return main, flags, nil
return main, fields
}
func ResolveFields(fields map[string]string, tmpl map[string][]string) (map[string]string, error) {
res := make(map[string]string)
for k, v := range fields {
for tk, tv := range tmpl {
if slices.Contains(tv, k) {
if _, ok := res[tk]; ok {
return nil, fmt.Errorf("%w: duplicate field: %v", ErrInvalidArg, tk)
}
res[tk] = v
delete(fields, k)
}
}
}
if len(fields) > 0 {
ks := make([]string, 0, len(fields))
for k := range fields {
ks = append(ks, k)
}
return nil, fmt.Errorf("%w: unknown field(s): %v", ErrInvalidArg, strings.Join(ks, ","))
}
return res, nil
}

View File

@ -7,32 +7,31 @@ import (
"go-mod.ewintr.nl/planner/plan/command"
)
func TestParseArgs(t *testing.T) {
func TestFindFields(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
name string
args []string
expMain []string
expFlags map[string]string
expErr bool
expFields map[string]string
}{
{
name: "empty",
expMain: []string{},
expFlags: map[string]string{},
expFields: map[string]string{},
},
{
name: "just main",
args: []string{"one", "two three", "four"},
expMain: []string{"one", "two three", "four"},
expFlags: map[string]string{},
expFields: map[string]string{},
},
{
name: "with flags",
args: []string{"-flag1", "value1", "one", "two", "-flag2", "value2", "-flag3", "value3"},
args: []string{"flag1:value1", "one", "two", "flag2:value2", "flag3:value3"},
expMain: []string{"one", "two"},
expFlags: map[string]string{
expFields: map[string]string{
"flag1": "value1",
"flag2": "value2",
"flag3": "value3",
@ -40,29 +39,88 @@ func TestParseArgs(t *testing.T) {
},
{
name: "flag without value",
args: []string{"one", "two", "-flag1"},
expErr: true,
args: []string{"one", "two", "flag1:"},
expMain: []string{"one", "two"},
expFields: map[string]string{
"flag1": "",
},
},
{
name: "split main",
args: []string{"one", "-flag1", "value1", "two"},
expErr: true,
args: []string{"one", "flag1:value1", "two"},
expMain: []string{"one", "two"},
expFields: map[string]string{
"flag1": "value1",
},
},
} {
t.Run(tc.name, func(t *testing.T) {
actMain, actFlags, actErr := command.ParseFlags(tc.args)
if tc.expErr != (actErr != nil) {
t.Errorf("exp %v, got %v", tc.expErr, actErr)
}
if tc.expErr {
return
}
actMain, actFields := command.FindFields(tc.args)
if diff := cmp.Diff(tc.expMain, actMain); diff != "" {
t.Errorf("(exp +, got -)\n%s", diff)
}
if diff := cmp.Diff(tc.expFlags, actFlags); diff != "" {
if diff := cmp.Diff(tc.expFields, actFields); diff != "" {
t.Errorf("(exp +, got -)\n%s", diff)
}
})
}
}
func TestResolveFields(t *testing.T) {
t.Parallel()
tmpl := map[string][]string{
"one": []string{"a", "b"},
"two": []string{"c", "d"},
}
for _, tc := range []struct {
name string
fields map[string]string
expRes map[string]string
expErr bool
}{
{
name: "empty",
expRes: map[string]string{},
},
{
name: "unknown",
fields: map[string]string{
"unknown": "value",
},
expErr: true,
},
{
name: "duplicate",
fields: map[string]string{
"a": "val1",
"b": "val2",
},
expErr: true,
},
{
name: "valid",
fields: map[string]string{
"a": "val1",
"d": "val2",
},
expRes: map[string]string{
"one": "val1",
"two": "val2",
},
},
} {
t.Run(tc.name, func(t *testing.T) {
actRes, actErr := command.ResolveFields(tc.fields, tmpl)
if tc.expErr != (actErr != nil) {
t.Errorf("exp %v, got %v", tc.expErr, actErr != nil)
}
if tc.expErr {
return
}
if diff := cmp.Diff(tc.expRes, actRes); diff != "" {
t.Errorf("(+exp, -got)%s\n", diff)
}
})
}
}

View File

@ -3,46 +3,45 @@ package command
import (
"fmt"
"strconv"
"go-mod.ewintr.nl/planner/plan/storage"
)
type Delete struct {
localIDRepo storage.LocalID
taskRepo storage.Task
syncRepo storage.Sync
localID int
type DeleteArgs struct {
LocalID int
}
func NewDelete(localIDRepo storage.LocalID, taskRepo storage.Task, syncRepo storage.Sync) Command {
return &Delete{
localIDRepo: localIDRepo,
taskRepo: taskRepo,
syncRepo: syncRepo,
}
func NewDeleteArgs() DeleteArgs {
return DeleteArgs{}
}
func (del *Delete) Execute(main []string, flags map[string]string) error {
func (da DeleteArgs) Parse(main []string, flags map[string]string) (Command, error) {
if len(main) < 2 || main[0] != "delete" {
return ErrWrongCommand
return nil, ErrWrongCommand
}
localID, err := strconv.Atoi(main[1])
if err != nil {
return fmt.Errorf("not a local id: %v", main[1])
}
del.localID = localID
return del.do()
return nil, fmt.Errorf("not a local id: %v", main[1])
}
func (del *Delete) do() error {
return &Delete{
args: DeleteArgs{
LocalID: localID,
},
}, nil
}
type Delete struct {
args DeleteArgs
}
func (del *Delete) Do(deps Dependencies) error {
var id string
idMap, err := del.localIDRepo.FindAll()
idMap, err := deps.LocalIDRepo.FindAll()
if err != nil {
return fmt.Errorf("could not get local ids: %v", err)
}
for tskID, lid := range idMap {
if del.localID == lid {
if del.args.LocalID == lid {
id = tskID
}
}
@ -50,7 +49,7 @@ func (del *Delete) do() error {
return fmt.Errorf("could not find local id")
}
tsk, err := del.taskRepo.Find(id)
tsk, err := deps.TaskRepo.Find(id)
if err != nil {
return fmt.Errorf("could not get task: %v", err)
}
@ -60,15 +59,15 @@ func (del *Delete) do() error {
return fmt.Errorf("could not convert task to sync item: %v", err)
}
it.Deleted = true
if err := del.syncRepo.Store(it); err != nil {
if err := deps.SyncRepo.Store(it); err != nil {
return fmt.Errorf("could not store sync item: %v", err)
}
if err := del.localIDRepo.Delete(id); err != nil {
if err := deps.LocalIDRepo.Delete(id); err != nil {
return fmt.Errorf("could not delete local id: %v", err)
}
if err := del.taskRepo.Delete(id); err != nil {
if err := deps.TaskRepo.Delete(id); err != nil {
return fmt.Errorf("could not delete task: %v", err)
}

View File

@ -25,17 +25,18 @@ func TestDelete(t *testing.T) {
name string
main []string
flags map[string]string
expErr bool
expParseErr bool
expDoErr bool
}{
{
name: "invalid",
main: []string{"update"},
expErr: true,
expParseErr: true,
},
{
name: "not found",
main: []string{"delete", "5"},
expErr: true,
expDoErr: true,
},
{
name: "valid",
@ -48,26 +49,35 @@ func TestDelete(t *testing.T) {
if err := taskRepo.Store(e); err != nil {
t.Errorf("exp nil, got %v", err)
}
localRepo := memory.NewLocalID()
if err := localRepo.Store(e.ID, 1); err != nil {
localIDRepo := memory.NewLocalID()
if err := localIDRepo.Store(e.ID, 1); err != nil {
t.Errorf("exp nil, got %v", err)
}
cmd := command.NewDelete(localRepo, taskRepo, syncRepo)
actErr := cmd.Execute(tc.main, tc.flags) != nil
if tc.expErr != actErr {
t.Errorf("exp %v, got %v", tc.expErr, actErr)
cmd, actParseErr := command.NewDeleteArgs().Parse(tc.main, tc.flags)
if tc.expParseErr != (actParseErr != nil) {
t.Errorf("exp %v, got %v", tc.expParseErr, actParseErr)
}
if tc.expErr {
if tc.expParseErr {
return
}
actDoErr := cmd.Do(command.Dependencies{
TaskRepo: taskRepo,
LocalIDRepo: localIDRepo,
SyncRepo: syncRepo,
}) != nil
if tc.expDoErr != actDoErr {
t.Errorf("exp false, got %v", actDoErr)
}
if tc.expDoErr {
return
}
_, repoErr := taskRepo.Find(e.ID)
if !errors.Is(repoErr, storage.ErrNotFound) {
t.Errorf("exp %v, got %v", storage.ErrNotFound, actErr)
t.Errorf("exp %v, got %v", storage.ErrNotFound, repoErr)
}
idMap, idErr := localRepo.FindAll()
idMap, idErr := localIDRepo.FindAll()
if idErr != nil {
t.Errorf("exp nil, got %v", idErr)
}

View File

@ -1,156 +0,0 @@
package command
import (
"errors"
"fmt"
"strconv"
"time"
"go-mod.ewintr.nl/planner/item"
)
const (
DateFormat = "2006-01-02"
TimeFormat = "15:04"
)
var (
ErrWrongCommand = errors.New("wrong command")
ErrInvalidArg = errors.New("invalid argument")
)
type Flag interface {
Set(val string) error
IsSet() bool
Get() any
}
type FlagString struct {
Name string
Value string
}
func (fs *FlagString) Set(val string) error {
fs.Value = val
return nil
}
func (fs *FlagString) IsSet() bool {
return fs.Value != ""
}
func (fs *FlagString) Get() any {
return fs.Value
}
type FlagDate struct {
Name string
Value item.Date
}
func (fd *FlagDate) Set(val string) error {
d := item.NewDateFromString(val)
if d.IsZero() {
return fmt.Errorf("could not parse date: %v", d)
}
fd.Value = d
return nil
}
func (fd *FlagDate) IsSet() bool {
return !fd.Value.IsZero()
}
func (fd *FlagDate) Get() any {
return fd.Value
}
type FlagTime struct {
Name string
Value item.Time
}
func (ft *FlagTime) Set(val string) error {
d := item.NewTimeFromString(val)
if d.IsZero() {
return fmt.Errorf("could not parse date: %v", d)
}
ft.Value = d
return nil
}
func (fd *FlagTime) IsSet() bool {
return !fd.Value.IsZero()
}
func (fs *FlagTime) Get() any {
return fs.Value
}
type FlagDuration struct {
Name string
Value time.Duration
}
func (fd *FlagDuration) Set(val string) error {
dur, err := time.ParseDuration(val)
if err != nil {
return fmt.Errorf("could not parse duration: %v", err)
}
fd.Value = dur
return nil
}
func (fd *FlagDuration) IsSet() bool {
return fd.Value.String() != "0s"
}
func (fs *FlagDuration) Get() any {
return fs.Value
}
type FlagRecurrer struct {
Name string
Value item.Recurrer
}
func (fr *FlagRecurrer) Set(val string) error {
fr.Value = item.NewRecurrer(val)
if fr.Value == nil {
return fmt.Errorf("not a valid recurrer: %v", val)
}
return nil
}
func (fr *FlagRecurrer) IsSet() bool {
return fr.Value != nil
}
func (fr *FlagRecurrer) Get() any {
return fr.Value
}
type FlagInt struct {
Name string
Value int
}
func (fi *FlagInt) Set(val string) error {
i, err := strconv.Atoi(val)
if err != nil {
return fmt.Errorf("not a valid integer: %v", val)
}
fi.Value = i
return nil
}
func (fi *FlagInt) IsSet() bool {
return fi.Value != 0
}
func (fi *FlagInt) Get() any {
return fi.Value
}

View File

@ -1,141 +0,0 @@
package command_test
import (
"testing"
"time"
"go-mod.ewintr.nl/planner/item"
"go-mod.ewintr.nl/planner/plan/command"
)
func TestFlagString(t *testing.T) {
t.Parallel()
valid := "test"
f := command.FlagString{}
if f.IsSet() {
t.Errorf("exp false, got true")
}
if err := f.Set(valid); err != nil {
t.Errorf("exp nil, got %v", err)
}
if !f.IsSet() {
t.Errorf("exp true, got false")
}
act, ok := f.Get().(string)
if !ok {
t.Errorf("exp true, got false")
}
if act != valid {
t.Errorf("exp %v, got %v", valid, act)
}
}
func TestFlagDate(t *testing.T) {
t.Parallel()
valid := item.NewDate(2024, 11, 20)
f := command.FlagDate{}
if f.IsSet() {
t.Errorf("exp false, got true")
}
if err := f.Set(valid.String()); err != nil {
t.Errorf("exp nil, got %v", err)
}
if !f.IsSet() {
t.Errorf("exp true, got false")
}
act, ok := f.Get().(item.Date)
if !ok {
t.Errorf("exp true, got false")
}
if act.String() != valid.String() {
t.Errorf("exp %v, got %v", valid.String(), act.String())
}
}
func TestFlagTime(t *testing.T) {
t.Parallel()
valid := item.NewTime(12, 30)
f := command.FlagTime{}
if f.IsSet() {
t.Errorf("exp false, got true")
}
if err := f.Set(valid.String()); err != nil {
t.Errorf("exp nil, got %v", err)
}
if !f.IsSet() {
t.Errorf("exp true, got false")
}
act, ok := f.Get().(item.Time)
if !ok {
t.Errorf("exp true, got false")
}
if act.String() != valid.String() {
t.Errorf("exp %v, got %v", valid.String(), act.String())
}
}
func TestFlagDurationTime(t *testing.T) {
t.Parallel()
valid := time.Hour
validStr := "1h"
f := command.FlagDuration{}
if f.IsSet() {
t.Errorf("exp false, got true")
}
if err := f.Set(validStr); err != nil {
t.Errorf("exp nil, got %v", err)
}
if !f.IsSet() {
t.Errorf("exp true, got false")
}
act, ok := f.Get().(time.Duration)
if !ok {
t.Errorf("exp true, got false")
}
if act != valid {
t.Errorf("exp %v, got %v", valid, act)
}
}
func TestFlagRecurrer(t *testing.T) {
t.Parallel()
validStr := "2024-12-23, daily"
valid := item.NewRecurrer(validStr)
f := command.FlagRecurrer{}
if f.IsSet() {
t.Errorf("exp false, got true")
}
if err := f.Set(validStr); err != nil {
t.Errorf("exp nil, got %v", err)
}
if !f.IsSet() {
t.Errorf("exp true, got false")
}
act, ok := f.Get().(item.Recurrer)
if !ok {
t.Errorf("exp true, got false")
}
if act != valid {
t.Errorf("exp %v, got %v", valid, act)
}
}

View File

@ -2,36 +2,32 @@ package command
import (
"fmt"
"go-mod.ewintr.nl/planner/plan/storage"
)
type List struct {
localIDRepo storage.LocalID
taskRepo storage.Task
type ListArgs struct {
}
func NewList(localIDRepo storage.LocalID, taskRepo storage.Task) Command {
return &List{
localIDRepo: localIDRepo,
taskRepo: taskRepo,
}
func NewListArgs() ListArgs {
return ListArgs{}
}
func (list *List) Execute(main []string, flags map[string]string) error {
func (la ListArgs) Parse(main []string, flags map[string]string) (Command, error) {
if len(main) > 0 && main[0] != "list" {
return ErrWrongCommand
return nil, ErrWrongCommand
}
return list.do()
return &List{}, nil
}
func (list *List) do() error {
localIDs, err := list.localIDRepo.FindAll()
type List struct {
}
func (list *List) Do(deps Dependencies) error {
localIDs, err := deps.LocalIDRepo.FindAll()
if err != nil {
return fmt.Errorf("could not get local ids: %v", err)
}
all, err := list.taskRepo.FindAll()
all, err := deps.TaskRepo.FindAll()
if err != nil {
return err
}

View File

@ -47,11 +47,19 @@ func TestList(t *testing.T) {
},
} {
t.Run(tc.name, func(t *testing.T) {
cmd := command.NewList(localRepo, taskRepo)
actErr := cmd.Execute(tc.main, nil) != nil
if tc.expErr != actErr {
cmd, actErr := command.NewListArgs().Parse(tc.main, nil)
if tc.expErr != (actErr != nil) {
t.Errorf("exp %v, got %v", tc.expErr, actErr)
}
if tc.expErr {
return
}
if err := cmd.Do(command.Dependencies{
TaskRepo: taskRepo,
LocalIDRepo: localRepo,
}); err != nil {
t.Errorf("exp nil, got %v", err)
}
})
}
}

View File

@ -7,52 +7,43 @@ import (
"go-mod.ewintr.nl/planner/item"
"go-mod.ewintr.nl/planner/plan/storage"
"go-mod.ewintr.nl/planner/sync/client"
)
type Sync struct {
client client.Client
syncRepo storage.Sync
localIDRepo storage.LocalID
taskRepo storage.Task
type SyncArgs struct{}
func NewSyncArgs() SyncArgs {
return SyncArgs{}
}
func NewSync(client client.Client, syncRepo storage.Sync, localIDRepo storage.LocalID, taskRepo storage.Task) Command {
return &Sync{
client: client,
syncRepo: syncRepo,
localIDRepo: localIDRepo,
taskRepo: taskRepo,
}
}
func (sync *Sync) Execute(main []string, flags map[string]string) error {
func (sa SyncArgs) Parse(main []string, flags map[string]string) (Command, error) {
if len(main) == 0 || main[0] != "sync" {
return ErrWrongCommand
return nil, ErrWrongCommand
}
return sync.do()
return &Sync{}, nil
}
func (sync *Sync) do() error {
type Sync struct{}
func (s *Sync) Do(deps Dependencies) error {
// local new and updated
sendItems, err := sync.syncRepo.FindAll()
sendItems, err := deps.SyncRepo.FindAll()
if err != nil {
return fmt.Errorf("could not get updated items: %v", err)
}
if err := sync.client.Update(sendItems); err != nil {
if err := deps.SyncClient.Update(sendItems); err != nil {
return fmt.Errorf("could not send updated items: %v", err)
}
if err := sync.syncRepo.DeleteAll(); err != nil {
if err := deps.SyncRepo.DeleteAll(); err != nil {
return fmt.Errorf("could not clear updated items: %v", err)
}
// get new/updated items
ts, err := sync.syncRepo.LastUpdate()
ts, err := deps.SyncRepo.LastUpdate()
if err != nil {
return fmt.Errorf("could not find timestamp of last update: %v", err)
}
recItems, err := sync.client.Updated([]item.Kind{item.KindTask}, ts)
recItems, err := deps.SyncClient.Updated([]item.Kind{item.KindTask}, ts)
if err != nil {
return fmt.Errorf("could not receive updates: %v", err)
}
@ -60,10 +51,10 @@ func (sync *Sync) do() error {
updated := make([]item.Item, 0)
for _, ri := range recItems {
if ri.Deleted {
if err := sync.localIDRepo.Delete(ri.ID); err != nil && !errors.Is(err, storage.ErrNotFound) {
if err := deps.LocalIDRepo.Delete(ri.ID); err != nil && !errors.Is(err, storage.ErrNotFound) {
return fmt.Errorf("could not delete local id: %v", err)
}
if err := sync.taskRepo.Delete(ri.ID); err != nil && !errors.Is(err, storage.ErrNotFound) {
if err := deps.TaskRepo.Delete(ri.ID); err != nil && !errors.Is(err, storage.ErrNotFound) {
return fmt.Errorf("could not delete task: %v", err)
}
continue
@ -71,7 +62,7 @@ func (sync *Sync) do() error {
updated = append(updated, ri)
}
lidMap, err := sync.localIDRepo.FindAll()
lidMap, err := deps.LocalIDRepo.FindAll()
if err != nil {
return fmt.Errorf("could not get local ids: %v", err)
}
@ -87,17 +78,17 @@ func (sync *Sync) do() error {
RecurNext: u.RecurNext,
TaskBody: tskBody,
}
if err := sync.taskRepo.Store(tsk); err != nil {
if err := deps.TaskRepo.Store(tsk); err != nil {
return fmt.Errorf("could not store task: %v", err)
}
lid, ok := lidMap[u.ID]
if !ok {
lid, err = sync.localIDRepo.Next()
lid, err = deps.LocalIDRepo.Next()
if err != nil {
return fmt.Errorf("could not get next local id: %v", err)
}
if err := sync.localIDRepo.Store(u.ID, lid); err != nil {
if err := deps.LocalIDRepo.Store(u.ID, lid); err != nil {
return fmt.Errorf("could not store local id: %v", err)
}
}

View File

@ -14,11 +14,6 @@ import (
func TestSyncParse(t *testing.T) {
t.Parallel()
syncClient := client.NewMemory()
syncRepo := memory.NewSync()
localIDRepo := memory.NewLocalID()
taskRepo := memory.NewTask()
for _, tc := range []struct {
name string
main []string
@ -39,9 +34,8 @@ func TestSyncParse(t *testing.T) {
},
} {
t.Run(tc.name, func(t *testing.T) {
cmd := command.NewSync(syncClient, syncRepo, localIDRepo, taskRepo)
actErr := cmd.Execute(tc.main, nil) != nil
if tc.expErr != actErr {
_, actErr := command.SyncArgs{}.Parse(tc.main, nil)
if tc.expErr != (actErr != nil) {
t.Errorf("exp %v, got %v", tc.expErr, actErr)
}
})
@ -82,8 +76,16 @@ func TestSyncSend(t *testing.T) {
},
} {
t.Run(tc.name, func(t *testing.T) {
cmd := command.NewSync(syncClient, syncRepo, localIDRepo, taskRepo)
if err := cmd.Execute([]string{"sync"}, nil); err != nil {
cmd, err := command.SyncArgs{}.Parse([]string{"sync"}, nil)
if err != nil {
t.Errorf("exp nil, got %v", err)
}
if err := cmd.Do(command.Dependencies{
TaskRepo: taskRepo,
LocalIDRepo: localIDRepo,
SyncRepo: syncRepo,
SyncClient: syncClient,
}); err != nil {
t.Errorf("exp nil, got %v", err)
}
actItems, actErr := syncClient.Updated(tc.ks, tc.ts)
@ -181,12 +183,12 @@ func TestSyncReceive(t *testing.T) {
},
} {
t.Run(tc.name, func(t *testing.T) {
// setup
syncClient := client.NewMemory()
syncRepo := memory.NewSync()
localIDRepo := memory.NewLocalID()
taskRepo := memory.NewTask()
// setup
for i, p := range tc.present {
if err := taskRepo.Store(p); err != nil {
t.Errorf("exp nil, got %v", err)
@ -200,8 +202,16 @@ func TestSyncReceive(t *testing.T) {
}
// sync
cmd := command.NewSync(syncClient, syncRepo, localIDRepo, taskRepo)
if err := cmd.Execute([]string{"sync"}, nil); err != nil {
cmd, err := command.NewSyncArgs().Parse([]string{"sync"}, nil)
if err != nil {
t.Errorf("exp nil, got %v", err)
}
if err := cmd.Do(command.Dependencies{
TaskRepo: taskRepo,
LocalIDRepo: localIDRepo,
SyncRepo: syncRepo,
SyncClient: syncClient,
}); err != nil {
t.Errorf("exp nil, got %v", err)
}

View File

@ -4,71 +4,93 @@ import (
"fmt"
"strconv"
"strings"
"time"
"go-mod.ewintr.nl/planner/plan/storage"
"go-mod.ewintr.nl/planner/item"
)
type Update struct {
localIDRepo storage.LocalID
taskRepo storage.Task
syncRepo storage.Sync
argSet *ArgSet
localID int
type UpdateArgs struct {
fieldTPL map[string][]string
LocalID int
Title string
Date item.Date
Time item.Time
Duration time.Duration
Recurrer item.Recurrer
}
func NewUpdate(localIDRepo storage.LocalID, taskRepo storage.Task, syncRepo storage.Sync) Command {
return &Update{
localIDRepo: localIDRepo,
taskRepo: taskRepo,
syncRepo: syncRepo,
argSet: &ArgSet{
Flags: map[string]Flag{
FlagTitle: &FlagString{},
FlagOn: &FlagDate{},
FlagAt: &FlagTime{},
FlagFor: &FlagDuration{},
FlagRec: &FlagRecurrer{},
},
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"},
},
}
}
func (update *Update) Execute(main []string, flags map[string]string) error {
func (ua UpdateArgs) Parse(main []string, fields map[string]string) (Command, error) {
if len(main) < 2 || main[0] != "update" {
return ErrWrongCommand
return nil, ErrWrongCommand
}
localID, err := strconv.Atoi(main[1])
if err != nil {
return fmt.Errorf("not a local id: %v", main[1])
return nil, fmt.Errorf("not a local id: %v", main[1])
}
update.localID = localID
main = main[2:]
as := update.argSet
as.Main = strings.Join(main, " ")
for k := range as.Flags {
v, ok := flags[k]
if !ok {
continue
fields, err = ResolveFields(fields, ua.fieldTPL)
if err != nil {
return nil, err
}
if err := as.Set(k, v); err != nil {
return fmt.Errorf("could not set %s: %v", k, err)
}
}
update.argSet = as
return update.do()
args := UpdateArgs{
LocalID: localID,
Title: strings.Join(main[2:], " "),
}
func (update *Update) do() error {
as := update.argSet
if val, ok := fields["date"]; ok {
d := item.NewDateFromString(val)
if d.IsZero() {
return nil, fmt.Errorf("%w: could not parse date", ErrInvalidArg)
}
args.Date = d
}
if val, ok := fields["time"]; ok {
t := item.NewTimeFromString(val)
if t.IsZero() {
return nil, fmt.Errorf("%w: could not parse time", ErrInvalidArg)
}
args.Time = t
}
if val, ok := fields["duration"]; ok {
d, err := time.ParseDuration(val)
if err != nil {
return nil, fmt.Errorf("%w: could not parse duration", ErrInvalidArg)
}
args.Duration = d
}
if val, ok := fields["recurrer"]; ok {
rec := item.NewRecurrer(val)
if rec == nil {
return nil, fmt.Errorf("%w: could not parse recurrer", ErrInvalidArg)
}
args.Recurrer = rec
}
return &Update{args}, nil
}
type Update struct {
args UpdateArgs
}
func (u *Update) Do(deps Dependencies) error {
var id string
idMap, err := update.localIDRepo.FindAll()
idMap, err := deps.LocalIDRepo.FindAll()
if err != nil {
return fmt.Errorf("could not get local ids: %v", err)
}
for tid, lid := range idMap {
if update.localID == lid {
if u.args.LocalID == lid {
id = tid
}
}
@ -76,32 +98,33 @@ func (update *Update) do() error {
return fmt.Errorf("could not find local id")
}
tsk, err := update.taskRepo.Find(id)
tsk, err := deps.TaskRepo.Find(id)
if err != nil {
return fmt.Errorf("could not find task")
}
if as.Main != "" {
tsk.Title = as.Main
if u.args.Title != "" {
tsk.Title = u.args.Title
}
if as.IsSet(FlagOn) {
tsk.Date = as.GetDate(FlagOn)
if !u.args.Date.IsZero() {
tsk.Date = u.args.Date
}
if as.IsSet(FlagAt) {
tsk.Time = as.GetTime(FlagAt)
if !u.args.Time.IsZero() {
tsk.Time = u.args.Time
}
if as.IsSet(FlagFor) {
tsk.Duration = as.GetDuration(FlagFor)
if u.args.Duration != 0 {
tsk.Duration = u.args.Duration
}
if as.IsSet(FlagRec) {
tsk.Recurrer = as.GetRecurrer(FlagRec)
if u.args.Recurrer != nil {
tsk.Recurrer = u.args.Recurrer
tsk.RecurNext = tsk.Recurrer.First()
}
if !tsk.Valid() {
return fmt.Errorf("task is unvalid")
}
if err := update.taskRepo.Store(tsk); err != nil {
if err := deps.TaskRepo.Store(tsk); err != nil {
return fmt.Errorf("could not store task: %v", err)
}
@ -109,7 +132,7 @@ func (update *Update) do() error {
if err != nil {
return fmt.Errorf("could not convert task to sync item: %v", err)
}
if err := update.syncRepo.Store(it); err != nil {
if err := deps.SyncRepo.Store(it); err != nil {
return fmt.Errorf("could not store sync item: %v", err)
}

View File

@ -31,18 +31,19 @@ func TestUpdateExecute(t *testing.T) {
name string
localID int
main []string
flags map[string]string
fields map[string]string
expTask item.Task
expErr bool
expParseErr bool
expDoErr bool
}{
{
name: "no args",
expErr: true,
expParseErr: true,
},
{
name: "not found",
localID: 1,
expErr: true,
main: []string{"update", "1"},
expDoErr: true,
},
{
name: "name",
@ -59,19 +60,19 @@ func TestUpdateExecute(t *testing.T) {
},
},
{
name: "invalid on",
name: "invalid date",
localID: lid,
main: []string{"update", fmt.Sprintf("%d", lid)},
flags: map[string]string{
fields: map[string]string{
"on": "invalid",
},
expErr: true,
expParseErr: true,
},
{
name: "on",
name: "date",
localID: lid,
main: []string{"update", fmt.Sprintf("%d", lid)},
flags: map[string]string{
fields: map[string]string{
"on": "2024-10-02",
},
expTask: item.Task{
@ -85,20 +86,20 @@ func TestUpdateExecute(t *testing.T) {
},
},
{
name: "invalid at",
name: "invalid time",
localID: lid,
main: []string{"update", fmt.Sprintf("%d", lid)},
flags: map[string]string{
fields: map[string]string{
"at": "invalid",
},
expErr: true,
expParseErr: true,
},
{
name: "at",
name: "time",
localID: lid,
main: []string{"update", fmt.Sprintf("%d", lid)},
flags: map[string]string{
"at": "11:00",
fields: map[string]string{
"time": "11:00",
},
expTask: item.Task{
ID: tskID,
@ -111,37 +112,19 @@ func TestUpdateExecute(t *testing.T) {
},
},
{
name: "on and at",
name: "invalid duration",
localID: lid,
main: []string{"update", fmt.Sprintf("%d", lid)},
flags: map[string]string{
"on": "2024-10-02",
"at": "11:00",
},
expTask: item.Task{
ID: tskID,
Date: item.NewDate(2024, 10, 2),
TaskBody: item.TaskBody{
Title: title,
Time: item.NewTime(11, 0),
Duration: oneHour,
},
},
},
{
name: "invalid for",
localID: lid,
main: []string{"update", fmt.Sprintf("%d", lid)},
flags: map[string]string{
fields: map[string]string{
"for": "invalid",
},
expErr: true,
expParseErr: true,
},
{
name: "for",
name: "duration",
localID: lid,
main: []string{"update", fmt.Sprintf("%d", lid)},
flags: map[string]string{
fields: map[string]string{
"for": "2h",
},
expTask: item.Task{
@ -155,17 +138,17 @@ func TestUpdateExecute(t *testing.T) {
},
},
{
name: "invalid rec",
name: "invalid recurrer",
main: []string{"update", fmt.Sprintf("%d", lid)},
flags: map[string]string{
fields: map[string]string{
"rec": "invalud",
},
expErr: true,
expParseErr: true,
},
{
name: "valid rec",
name: "valid recurrer",
main: []string{"update", fmt.Sprintf("%d", lid)},
flags: map[string]string{
fields: map[string]string{
"rec": "2024-12-08, daily",
},
expTask: item.Task{
@ -199,12 +182,22 @@ func TestUpdateExecute(t *testing.T) {
t.Errorf("exp nil, ,got %v", err)
}
cmd := command.NewUpdate(localIDRepo, taskRepo, syncRepo)
actParseErr := cmd.Execute(tc.main, tc.flags) != nil
if tc.expErr != actParseErr {
t.Errorf("exp %v, got %v", tc.expErr, actParseErr)
cmd, actErr := command.NewUpdateArgs().Parse(tc.main, tc.fields)
if tc.expParseErr != (actErr != nil) {
t.Errorf("exp %v, got %v", tc.expParseErr, actErr)
}
if tc.expErr {
if tc.expParseErr {
return
}
actDoErr := cmd.Do(command.Dependencies{
TaskRepo: taskRepo,
LocalIDRepo: localIDRepo,
SyncRepo: syncRepo,
}) != nil
if tc.expDoErr != actDoErr {
t.Errorf("exp %v, got %v", tc.expDoErr, actDoErr)
}
if tc.expDoErr {
return
}

View File

@ -35,16 +35,12 @@ func main() {
syncClient := client.New(conf.SyncURL, conf.ApiKey)
cli := command.CLI{
Commands: []command.Command{
command.NewAdd(localIDRepo, taskRepo, syncRepo),
command.NewList(localIDRepo, taskRepo),
command.NewUpdate(localIDRepo, taskRepo, syncRepo),
command.NewDelete(localIDRepo, taskRepo, syncRepo),
command.NewSync(syncClient, syncRepo, localIDRepo, taskRepo),
},
}
cli := command.NewCLI(command.Dependencies{
LocalIDRepo: localIDRepo,
TaskRepo: taskRepo,
SyncRepo: syncRepo,
SyncClient: syncClient,
})
if err := cli.Run(os.Args[1:]); err != nil {
fmt.Println(err)
os.Exit(1)