Compare commits

..

3 Commits

Author SHA1 Message Date
Erik Winter 4357695eec ignore urls when looking for flags 2025-02-04 14:53:47 +01:00
Erik Winter 594f956794 increase sync timeout 2025-02-03 14:58:23 +01:00
Erik Winter ad78080e62 schedule add 2025-01-27 10:01:16 +01:00
23 changed files with 455 additions and 171 deletions

BIN
dist/plan vendored

Binary file not shown.

56
plan/cli/arg/arg.go Normal file
View File

@ -0,0 +1,56 @@
package arg
import (
"fmt"
"slices"
"strings"
"go-mod.ewintr.nl/planner/plan/command"
)
func FindFields(args []string) ([]string, map[string]string) {
fields := make(map[string]string)
main := make([]string, 0)
for i := 0; i < len(args); i++ {
if strings.HasPrefix(args[i], "http://") || strings.HasPrefix(args[i], "https://") {
main = append(main, args[i])
continue
}
// normal key:value
if k, v, ok := strings.Cut(args[i], ":"); ok && !strings.Contains(k, " ") {
fields[k] = v
continue
}
// empty key:
if !strings.Contains(args[i], " ") && strings.HasSuffix(args[i], ":") {
k := strings.TrimSuffix(args[i], ":")
fields[k] = ""
}
main = append(main, args[i])
}
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", command.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", command.ErrInvalidArg, strings.Join(ks, ","))
}
return res, nil
}

View File

@ -1,12 +1,14 @@
package command_test package arg_test
import ( import (
"testing" "testing"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"go-mod.ewintr.nl/planner/plan/command" "go-mod.ewintr.nl/planner/plan/cli/arg"
) )
// Please add a testcase to the following function that checks whether fields
// that start with "http:" and "https:" are ignored
func TestFindFields(t *testing.T) { func TestFindFields(t *testing.T) {
t.Parallel() t.Parallel()
@ -53,9 +55,17 @@ func TestFindFields(t *testing.T) {
"flag1": "value1", "flag1": "value1",
}, },
}, },
{
name: "ignore http and https fields",
args: []string{"one", "http://example.com", "two", "https://example.com", "flag1:value1"},
expMain: []string{"one", "http://example.com", "two", "https://example.com"},
expFields: map[string]string{
"flag1": "value1",
},
},
} { } {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
actMain, actFields := command.FindFields(tc.args) actMain, actFields := arg.FindFields(tc.args)
if diff := cmp.Diff(tc.expMain, actMain); diff != "" { if diff := cmp.Diff(tc.expMain, actMain); diff != "" {
t.Errorf("(exp +, got -)\n%s", diff) t.Errorf("(exp +, got -)\n%s", diff)
} }
@ -111,7 +121,7 @@ func TestResolveFields(t *testing.T) {
}, },
} { } {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
actRes, actErr := command.ResolveFields(tc.fields, tmpl) actRes, actErr := arg.ResolveFields(tc.fields, tmpl)
if tc.expErr != (actErr != nil) { if tc.expErr != (actErr != nil) {
t.Errorf("exp %v, got %v", tc.expErr, actErr != nil) t.Errorf("exp %v, got %v", tc.expErr, actErr != nil)
} }

57
plan/cli/cli.go Normal file
View File

@ -0,0 +1,57 @@
package cli
import (
"errors"
"fmt"
"go-mod.ewintr.nl/planner/plan/cli/arg"
"go-mod.ewintr.nl/planner/plan/command"
"go-mod.ewintr.nl/planner/plan/command/schedule"
"go-mod.ewintr.nl/planner/plan/command/task"
"go-mod.ewintr.nl/planner/sync/client"
)
type CLI struct {
repos command.Repositories
client client.Client
cmdArgs []command.CommandArgs
}
func NewCLI(repos command.Repositories, client client.Client) *CLI {
return &CLI{
repos: repos,
client: client,
cmdArgs: []command.CommandArgs{
command.NewSyncArgs(),
// task
task.NewShowArgs(), task.NewProjectsArgs(),
task.NewAddArgs(), task.NewDeleteArgs(), task.NewListArgs(),
task.NewUpdateArgs(),
// schedule
schedule.NewAddArgs(),
},
}
}
func (cli *CLI) Run(args []string) error {
main, fields := arg.FindFields(args)
for _, ca := range cli.cmdArgs {
cmd, err := ca.Parse(main, fields)
switch {
case errors.Is(err, command.ErrWrongCommand):
continue
case err != nil:
return err
}
result, err := cmd.Do(cli.repos, cli.client)
if err != nil {
return err
}
fmt.Println(result.Render())
return nil
}
return fmt.Errorf("could not find matching command")
}

View File

@ -2,9 +2,6 @@ package command
import ( import (
"errors" "errors"
"fmt"
"slices"
"strings"
"go-mod.ewintr.nl/planner/plan/storage" "go-mod.ewintr.nl/planner/plan/storage"
"go-mod.ewintr.nl/planner/sync/client" "go-mod.ewintr.nl/planner/sync/client"
@ -25,6 +22,7 @@ type Repositories interface {
LocalID(tx *storage.Tx) storage.LocalID LocalID(tx *storage.Tx) storage.LocalID
Sync(tx *storage.Tx) storage.Sync Sync(tx *storage.Tx) storage.Sync
Task(tx *storage.Tx) storage.Task Task(tx *storage.Tx) storage.Task
Schedule(tx *storage.Tx) storage.Schedule
} }
type CommandArgs interface { type CommandArgs interface {
@ -38,88 +36,3 @@ type Command interface {
type CommandResult interface { type CommandResult interface {
Render() string Render() string
} }
type CLI struct {
repos Repositories
client client.Client
cmdArgs []CommandArgs
}
func NewCLI(repos Repositories, client client.Client) *CLI {
return &CLI{
repos: repos,
client: client,
cmdArgs: []CommandArgs{
NewShowArgs(), NewProjectsArgs(),
NewAddArgs(), NewDeleteArgs(), NewListArgs(),
NewSyncArgs(), NewUpdateArgs(),
},
}
}
func (cli *CLI) Run(args []string) error {
main, fields := FindFields(args)
for _, ca := range cli.cmdArgs {
cmd, err := ca.Parse(main, fields)
switch {
case errors.Is(err, ErrWrongCommand):
continue
case err != nil:
return err
}
result, err := cmd.Do(cli.repos, cli.client)
if err != nil {
return err
}
fmt.Println(result.Render())
return nil
}
return fmt.Errorf("could not find matching command")
}
func FindFields(args []string) ([]string, map[string]string) {
fields := make(map[string]string)
main := make([]string, 0)
for i := 0; i < len(args); i++ {
// normal key:value
if k, v, ok := strings.Cut(args[i], ":"); ok && !strings.Contains(k, " ") {
fields[k] = v
continue
}
// empty key:
if !strings.Contains(args[i], " ") && strings.HasSuffix(args[i], ":") {
k := strings.TrimSuffix(args[i], ":")
fields[k] = ""
}
main = append(main, args[i])
}
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

@ -0,0 +1,124 @@
package schedule
import (
"fmt"
"slices"
"strings"
"github.com/google/uuid"
"go-mod.ewintr.nl/planner/item"
"go-mod.ewintr.nl/planner/plan/cli/arg"
"go-mod.ewintr.nl/planner/plan/command"
"go-mod.ewintr.nl/planner/plan/format"
"go-mod.ewintr.nl/planner/sync/client"
)
type AddArgs struct {
fieldTPL map[string][]string
Schedule item.Schedule
}
func NewAddArgs() AddArgs {
return AddArgs{
fieldTPL: map[string][]string{
"date": {"d", "date", "on"},
"recurrer": {"rec", "recurrer"},
},
}
}
func (aa AddArgs) Parse(main []string, fields map[string]string) (command.Command, error) {
if len(main) == 0 || !slices.Contains([]string{"s", "sched", "schedule"}, main[0]) {
return nil, command.ErrWrongCommand
}
main = main[1:]
if len(main) == 0 || !slices.Contains([]string{"add", "a", "new", "n"}, main[0]) {
return nil, command.ErrWrongCommand
}
main = main[1:]
if len(main) == 0 {
return nil, fmt.Errorf("%w: title is required for add", command.ErrInvalidArg)
}
fields, err := arg.ResolveFields(fields, aa.fieldTPL)
if err != nil {
return nil, err
}
sched := item.Schedule{
ID: uuid.New().String(),
ScheduleBody: item.ScheduleBody{
Title: strings.Join(main, " "),
},
}
if val, ok := fields["date"]; ok {
d := item.NewDateFromString(val)
if d.IsZero() {
return nil, fmt.Errorf("%w: could not parse date", command.ErrInvalidArg)
}
sched.Date = d
}
if val, ok := fields["recurrer"]; ok {
rec := item.NewRecurrer(val)
if rec == nil {
return nil, fmt.Errorf("%w: could not parse recurrer", command.ErrInvalidArg)
}
sched.Recurrer = rec
sched.RecurNext = sched.Recurrer.First()
}
return &Add{
Args: AddArgs{
Schedule: sched,
},
}, nil
}
type Add struct {
Args AddArgs
}
func (a Add) Do(repos command.Repositories, _ client.Client) (command.CommandResult, error) {
tx, err := repos.Begin()
if err != nil {
return nil, fmt.Errorf("could not start transaction: %v", err)
}
defer tx.Rollback()
if err := repos.Schedule(tx).Store(a.Args.Schedule); err != nil {
return nil, fmt.Errorf("could not store schedule: %v", err)
}
localID, err := repos.LocalID(tx).Next()
if err != nil {
return nil, fmt.Errorf("could not create next local id: %v", err)
}
if err := repos.LocalID(tx).Store(a.Args.Schedule.ID, localID); err != nil {
return nil, fmt.Errorf("could not store local id: %v", err)
}
it, err := a.Args.Schedule.Item()
if err != nil {
return nil, fmt.Errorf("could not convert schedule to sync item: %v", err)
}
if err := repos.Sync(tx).Store(it); err != nil {
return nil, fmt.Errorf("could not store sync item: %v", err)
}
if err := tx.Commit(); err != nil {
return nil, fmt.Errorf("could not add schedule: %v", err)
}
return AddResult{
LocalID: localID,
}, nil
}
type AddResult struct {
LocalID int
}
func (ar AddResult) Render() string {
return fmt.Sprintf("stored schedule %s", format.Bold(fmt.Sprintf("%d", ar.LocalID)))
}

View File

@ -0,0 +1,105 @@
package schedule_test
import (
"testing"
"go-mod.ewintr.nl/planner/item"
"go-mod.ewintr.nl/planner/plan/command/schedule"
"go-mod.ewintr.nl/planner/plan/storage/memory"
)
func TestAdd(t *testing.T) {
t.Parallel()
aDate := item.NewDate(2024, 11, 2)
for _, tc := range []struct {
name string
main []string
fields map[string]string
expErr bool
expSchedule item.Schedule
}{
{
name: "empty",
expErr: true,
},
{
name: "title missing",
main: []string{"sched", "add"},
fields: map[string]string{
"date": aDate.String(),
},
expErr: true,
},
{
name: "all",
main: []string{"sched", "add", "title"},
fields: map[string]string{
"date": aDate.String(),
},
expSchedule: item.Schedule{
ID: "title",
Date: aDate,
ScheduleBody: item.ScheduleBody{
Title: "title",
},
},
},
} {
t.Run(tc.name, func(t *testing.T) {
// setup
mems := memory.New()
// parse
cmd, actParseErr := schedule.NewAddArgs().Parse(tc.main, tc.fields)
if tc.expErr != (actParseErr != nil) {
t.Errorf("exp %v, got %v", tc.expErr, actParseErr)
}
if tc.expErr {
return
}
// do
if _, err := cmd.Do(mems, nil); err != nil {
t.Errorf("exp nil, got %v", err)
}
// check
actSchedules, err := mems.Schedule(nil).Find(aDate.Add(-1), aDate.Add(1))
if err != nil {
t.Errorf("exp nil, got %v", err)
}
if len(actSchedules) != 1 {
t.Errorf("exp 1, got %d", len(actSchedules))
}
actLocalIDs, err := mems.LocalID(nil).FindAll()
if err != nil {
t.Errorf("exp nil, got %v", err)
}
if len(actLocalIDs) != 1 {
t.Errorf("exp 1, got %v", len(actLocalIDs))
}
if _, ok := actLocalIDs[actSchedules[0].ID]; !ok {
t.Errorf("exp true, got %v", ok)
}
if actSchedules[0].ID == "" {
t.Errorf("exp string not te be empty")
}
tc.expSchedule.ID = actSchedules[0].ID
if diff := item.ScheduleDiff(tc.expSchedule, actSchedules[0]); diff != "" {
t.Errorf("(exp -, got +)\n%s", diff)
}
updated, err := mems.Sync(nil).FindAll()
if err != nil {
t.Errorf("exp nil, got %v", err)
}
if len(updated) != 1 {
t.Errorf("exp 1, got %v", len(updated))
}
})
}
}

View File

@ -1,4 +1,4 @@
package command package task
import ( import (
"fmt" "fmt"
@ -8,6 +8,8 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"go-mod.ewintr.nl/planner/item" "go-mod.ewintr.nl/planner/item"
"go-mod.ewintr.nl/planner/plan/cli/arg"
"go-mod.ewintr.nl/planner/plan/command"
"go-mod.ewintr.nl/planner/plan/format" "go-mod.ewintr.nl/planner/plan/format"
"go-mod.ewintr.nl/planner/sync/client" "go-mod.ewintr.nl/planner/sync/client"
) )
@ -29,16 +31,16 @@ func NewAddArgs() AddArgs {
} }
} }
func (aa AddArgs) Parse(main []string, fields map[string]string) (Command, error) { func (aa AddArgs) Parse(main []string, fields map[string]string) (command.Command, error) {
if len(main) == 0 || !slices.Contains([]string{"add", "a", "new", "n"}, main[0]) { if len(main) == 0 || !slices.Contains([]string{"add", "a", "new", "n"}, main[0]) {
return nil, ErrWrongCommand return nil, command.ErrWrongCommand
} }
main = main[1:] main = main[1:]
if len(main) == 0 { if len(main) == 0 {
return nil, fmt.Errorf("%w: title is required for add", ErrInvalidArg) return nil, fmt.Errorf("%w: title is required for add", command.ErrInvalidArg)
} }
fields, err := ResolveFields(fields, aa.fieldTPL) fields, err := arg.ResolveFields(fields, aa.fieldTPL)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -56,28 +58,28 @@ func (aa AddArgs) Parse(main []string, fields map[string]string) (Command, error
if val, ok := fields["date"]; ok { if val, ok := fields["date"]; ok {
d := item.NewDateFromString(val) d := item.NewDateFromString(val)
if d.IsZero() { if d.IsZero() {
return nil, fmt.Errorf("%w: could not parse date", ErrInvalidArg) return nil, fmt.Errorf("%w: could not parse date", command.ErrInvalidArg)
} }
tsk.Date = d tsk.Date = d
} }
if val, ok := fields["time"]; ok { if val, ok := fields["time"]; ok {
t := item.NewTimeFromString(val) t := item.NewTimeFromString(val)
if t.IsZero() { if t.IsZero() {
return nil, fmt.Errorf("%w: could not parse time", ErrInvalidArg) return nil, fmt.Errorf("%w: could not parse time", command.ErrInvalidArg)
} }
tsk.Time = t tsk.Time = t
} }
if val, ok := fields["duration"]; ok { if val, ok := fields["duration"]; ok {
d, err := time.ParseDuration(val) d, err := time.ParseDuration(val)
if err != nil { if err != nil {
return nil, fmt.Errorf("%w: could not parse duration", ErrInvalidArg) return nil, fmt.Errorf("%w: could not parse duration", command.ErrInvalidArg)
} }
tsk.Duration = d tsk.Duration = d
} }
if val, ok := fields["recurrer"]; ok { if val, ok := fields["recurrer"]; ok {
rec := item.NewRecurrer(val) rec := item.NewRecurrer(val)
if rec == nil { if rec == nil {
return nil, fmt.Errorf("%w: could not parse recurrer", ErrInvalidArg) return nil, fmt.Errorf("%w: could not parse recurrer", command.ErrInvalidArg)
} }
tsk.Recurrer = rec tsk.Recurrer = rec
tsk.RecurNext = tsk.Recurrer.First() tsk.RecurNext = tsk.Recurrer.First()
@ -94,7 +96,7 @@ type Add struct {
Args AddArgs Args AddArgs
} }
func (a Add) Do(repos Repositories, _ client.Client) (CommandResult, error) { func (a Add) Do(repos command.Repositories, _ client.Client) (command.CommandResult, error) {
tx, err := repos.Begin() tx, err := repos.Begin()
if err != nil { if err != nil {
return nil, fmt.Errorf("could not start transaction: %v", err) return nil, fmt.Errorf("could not start transaction: %v", err)
@ -102,7 +104,7 @@ func (a Add) Do(repos Repositories, _ client.Client) (CommandResult, error) {
defer tx.Rollback() defer tx.Rollback()
if err := repos.Task(tx).Store(a.Args.Task); err != nil { if err := repos.Task(tx).Store(a.Args.Task); err != nil {
return nil, fmt.Errorf("could not store event: %v", err) return nil, fmt.Errorf("could not store task: %v", err)
} }
localID, err := repos.LocalID(tx).Next() localID, err := repos.LocalID(tx).Next()
@ -115,7 +117,7 @@ func (a Add) Do(repos Repositories, _ client.Client) (CommandResult, error) {
it, err := a.Args.Task.Item() it, err := a.Args.Task.Item()
if err != nil { if err != nil {
return nil, fmt.Errorf("could not convert event to sync item: %v", err) return nil, fmt.Errorf("could not convert task to sync item: %v", err)
} }
if err := repos.Sync(tx).Store(it); err != nil { if err := repos.Sync(tx).Store(it); err != nil {
return nil, fmt.Errorf("could not store sync item: %v", err) return nil, fmt.Errorf("could not store sync item: %v", err)

View File

@ -1,11 +1,11 @@
package command_test package task_test
import ( import (
"testing" "testing"
"time" "time"
"go-mod.ewintr.nl/planner/item" "go-mod.ewintr.nl/planner/item"
"go-mod.ewintr.nl/planner/plan/command" "go-mod.ewintr.nl/planner/plan/command/task"
"go-mod.ewintr.nl/planner/plan/storage" "go-mod.ewintr.nl/planner/plan/storage"
"go-mod.ewintr.nl/planner/plan/storage/memory" "go-mod.ewintr.nl/planner/plan/storage/memory"
) )
@ -63,7 +63,7 @@ func TestAdd(t *testing.T) {
mems := memory.New() mems := memory.New()
// parse // parse
cmd, actParseErr := command.NewAddArgs().Parse(tc.main, tc.fields) cmd, actParseErr := task.NewAddArgs().Parse(tc.main, tc.fields)
if tc.expErr != (actParseErr != nil) { if tc.expErr != (actParseErr != nil) {
t.Errorf("exp %v, got %v", tc.expErr, actParseErr) t.Errorf("exp %v, got %v", tc.expErr, actParseErr)
} }

View File

@ -1,10 +1,11 @@
package command package task
import ( import (
"fmt" "fmt"
"slices" "slices"
"strconv" "strconv"
"go-mod.ewintr.nl/planner/plan/command"
"go-mod.ewintr.nl/planner/plan/format" "go-mod.ewintr.nl/planner/plan/format"
"go-mod.ewintr.nl/planner/sync/client" "go-mod.ewintr.nl/planner/sync/client"
) )
@ -17,9 +18,9 @@ func NewDeleteArgs() DeleteArgs {
return DeleteArgs{} return DeleteArgs{}
} }
func (da DeleteArgs) Parse(main []string, flags map[string]string) (Command, error) { func (da DeleteArgs) Parse(main []string, flags map[string]string) (command.Command, error) {
if len(main) != 2 { if len(main) != 2 {
return nil, ErrWrongCommand return nil, command.ErrWrongCommand
} }
aliases := []string{"d", "delete", "done"} aliases := []string{"d", "delete", "done"}
var localIDStr string var localIDStr string
@ -29,7 +30,7 @@ func (da DeleteArgs) Parse(main []string, flags map[string]string) (Command, err
case slices.Contains(aliases, main[1]): case slices.Contains(aliases, main[1]):
localIDStr = main[0] localIDStr = main[0]
default: default:
return nil, ErrWrongCommand return nil, command.ErrWrongCommand
} }
localID, err := strconv.Atoi(localIDStr) localID, err := strconv.Atoi(localIDStr)
@ -48,7 +49,7 @@ type Delete struct {
Args DeleteArgs Args DeleteArgs
} }
func (del Delete) Do(repos Repositories, _ client.Client) (CommandResult, error) { func (del Delete) Do(repos command.Repositories, _ client.Client) (command.CommandResult, error) {
tx, err := repos.Begin() tx, err := repos.Begin()
if err != nil { if err != nil {
return nil, fmt.Errorf("could not start transaction: %v", err) return nil, fmt.Errorf("could not start transaction: %v", err)

View File

@ -1,11 +1,11 @@
package command_test package task_test
import ( import (
"errors" "errors"
"testing" "testing"
"go-mod.ewintr.nl/planner/item" "go-mod.ewintr.nl/planner/item"
"go-mod.ewintr.nl/planner/plan/command" "go-mod.ewintr.nl/planner/plan/command/task"
"go-mod.ewintr.nl/planner/plan/storage" "go-mod.ewintr.nl/planner/plan/storage"
"go-mod.ewintr.nl/planner/plan/storage/memory" "go-mod.ewintr.nl/planner/plan/storage/memory"
) )
@ -59,7 +59,7 @@ func TestDelete(t *testing.T) {
} }
// parse // parse
cmd, actParseErr := command.NewDeleteArgs().Parse(tc.main, tc.flags) cmd, actParseErr := task.NewDeleteArgs().Parse(tc.main, tc.flags)
if tc.expParseErr != (actParseErr != nil) { if tc.expParseErr != (actParseErr != nil) {
t.Errorf("exp %v, got %v", tc.expParseErr, actParseErr) t.Errorf("exp %v, got %v", tc.expParseErr, actParseErr)
} }

View File

@ -1,4 +1,4 @@
package command package task
import ( import (
"fmt" "fmt"
@ -7,6 +7,8 @@ import (
"time" "time"
"go-mod.ewintr.nl/planner/item" "go-mod.ewintr.nl/planner/item"
"go-mod.ewintr.nl/planner/plan/cli/arg"
"go-mod.ewintr.nl/planner/plan/command"
"go-mod.ewintr.nl/planner/plan/format" "go-mod.ewintr.nl/planner/plan/format"
"go-mod.ewintr.nl/planner/plan/storage" "go-mod.ewintr.nl/planner/plan/storage"
"go-mod.ewintr.nl/planner/sync/client" "go-mod.ewintr.nl/planner/sync/client"
@ -31,12 +33,12 @@ func NewListArgs() ListArgs {
} }
} }
func (la ListArgs) Parse(main []string, fields map[string]string) (Command, error) { func (la ListArgs) Parse(main []string, fields map[string]string) (command.Command, error) {
if len(main) > 1 { if len(main) > 1 {
return nil, ErrWrongCommand return nil, command.ErrWrongCommand
} }
fields, err := ResolveFields(fields, la.fieldTPL) fields, err := arg.ResolveFields(fields, la.fieldTPL)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -63,7 +65,7 @@ func (la ListArgs) Parse(main []string, fields map[string]string) (Command, erro
// fields["from"] = today.String() // fields["from"] = today.String()
// fields["to"] = today.String() // fields["to"] = today.String()
default: default:
return nil, ErrWrongCommand return nil, command.ErrWrongCommand
} }
} }
@ -97,7 +99,7 @@ type List struct {
Args ListArgs Args ListArgs
} }
func (list List) Do(repos Repositories, _ client.Client) (CommandResult, error) { func (list List) Do(repos command.Repositories, _ client.Client) (command.CommandResult, error) {
tx, err := repos.Begin() tx, err := repos.Begin()
if err != nil { if err != nil {
return nil, fmt.Errorf("could not start transaction: %v", err) return nil, fmt.Errorf("could not start transaction: %v", err)

View File

@ -1,4 +1,4 @@
package command_test package task_test
import ( import (
"testing" "testing"
@ -7,7 +7,7 @@ import (
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts" "github.com/google/go-cmp/cmp/cmpopts"
"go-mod.ewintr.nl/planner/item" "go-mod.ewintr.nl/planner/item"
"go-mod.ewintr.nl/planner/plan/command" "go-mod.ewintr.nl/planner/plan/command/task"
"go-mod.ewintr.nl/planner/plan/storage/memory" "go-mod.ewintr.nl/planner/plan/storage/memory"
) )
@ -20,20 +20,20 @@ func TestListParse(t *testing.T) {
name string name string
main []string main []string
fields map[string]string fields map[string]string
expArgs command.ListArgs expArgs task.ListArgs
expErr bool expErr bool
}{ }{
{ {
name: "empty", name: "empty",
main: []string{}, main: []string{},
fields: map[string]string{}, fields: map[string]string{},
expArgs: command.ListArgs{}, expArgs: task.ListArgs{},
}, },
{ {
name: "today", name: "today",
main: []string{"tod"}, main: []string{"tod"},
fields: map[string]string{}, fields: map[string]string{},
expArgs: command.ListArgs{ expArgs: task.ListArgs{
To: today, To: today,
}, },
}, },
@ -41,7 +41,7 @@ func TestListParse(t *testing.T) {
name: "tomorrow", name: "tomorrow",
main: []string{"tom"}, main: []string{"tom"},
fields: map[string]string{}, fields: map[string]string{},
expArgs: command.ListArgs{ expArgs: task.ListArgs{
From: today.Add(1), From: today.Add(1),
To: today.Add(1), To: today.Add(1),
}, },
@ -50,14 +50,14 @@ func TestListParse(t *testing.T) {
name: "week", name: "week",
main: []string{"week"}, main: []string{"week"},
fields: map[string]string{}, fields: map[string]string{},
expArgs: command.ListArgs{ expArgs: task.ListArgs{
From: today, From: today,
To: today.Add(7), To: today.Add(7),
}, },
}, },
} { } {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
nla := command.NewListArgs() nla := task.NewListArgs()
cmd, actErr := nla.Parse(tc.main, tc.fields) cmd, actErr := nla.Parse(tc.main, tc.fields)
if tc.expErr != (actErr != nil) { if tc.expErr != (actErr != nil) {
t.Errorf("exp %v, got %v", tc.expErr, actErr != nil) t.Errorf("exp %v, got %v", tc.expErr, actErr != nil)
@ -65,7 +65,7 @@ func TestListParse(t *testing.T) {
if tc.expErr { if tc.expErr {
return return
} }
listCmd, ok := cmd.(command.List) listCmd, ok := cmd.(task.List)
if !ok { if !ok {
t.Errorf("exp true, got false") t.Errorf("exp true, got false")
} }
@ -97,7 +97,7 @@ func TestList(t *testing.T) {
for _, tc := range []struct { for _, tc := range []struct {
name string name string
cmd command.List cmd task.List
expRes bool expRes bool
expErr bool expErr bool
}{ }{
@ -107,8 +107,8 @@ func TestList(t *testing.T) {
}, },
{ {
name: "empty list", name: "empty list",
cmd: command.List{ cmd: task.List{
Args: command.ListArgs{ Args: task.ListArgs{
HasRecurrer: true, HasRecurrer: true,
}, },
}, },
@ -120,7 +120,7 @@ func TestList(t *testing.T) {
t.Errorf("exp nil, got %v", err) t.Errorf("exp nil, got %v", err)
} }
listRes := res.(command.ListResult) listRes := res.(task.ListResult)
actRes := len(listRes.Tasks) > 0 actRes := len(listRes.Tasks) > 0
if tc.expRes != actRes { if tc.expRes != actRes {
t.Errorf("exp %v, got %v", tc.expRes, actRes) t.Errorf("exp %v, got %v", tc.expRes, actRes)

View File

@ -1,9 +1,10 @@
package command package task
import ( import (
"fmt" "fmt"
"sort" "sort"
"go-mod.ewintr.nl/planner/plan/command"
"go-mod.ewintr.nl/planner/plan/format" "go-mod.ewintr.nl/planner/plan/format"
"go-mod.ewintr.nl/planner/sync/client" "go-mod.ewintr.nl/planner/sync/client"
) )
@ -14,9 +15,9 @@ func NewProjectsArgs() ProjectsArgs {
return ProjectsArgs{} return ProjectsArgs{}
} }
func (pa ProjectsArgs) Parse(main []string, fields map[string]string) (Command, error) { func (pa ProjectsArgs) Parse(main []string, fields map[string]string) (command.Command, error) {
if len(main) != 1 || main[0] != "projects" { if len(main) != 1 || main[0] != "projects" {
return nil, ErrWrongCommand return nil, command.ErrWrongCommand
} }
return Projects{}, nil return Projects{}, nil
@ -24,7 +25,7 @@ func (pa ProjectsArgs) Parse(main []string, fields map[string]string) (Command,
type Projects struct{} type Projects struct{}
func (ps Projects) Do(repos Repositories, _ client.Client) (CommandResult, error) { func (ps Projects) Do(repos command.Repositories, _ client.Client) (command.CommandResult, error) {
tx, err := repos.Begin() tx, err := repos.Begin()
if err != nil { if err != nil {
return nil, fmt.Errorf("could not start transaction: %v", err) return nil, fmt.Errorf("could not start transaction: %v", err)

View File

@ -1,4 +1,4 @@
package command package task
import ( import (
"errors" "errors"
@ -6,6 +6,7 @@ import (
"strconv" "strconv"
"go-mod.ewintr.nl/planner/item" "go-mod.ewintr.nl/planner/item"
"go-mod.ewintr.nl/planner/plan/command"
"go-mod.ewintr.nl/planner/plan/format" "go-mod.ewintr.nl/planner/plan/format"
"go-mod.ewintr.nl/planner/plan/storage" "go-mod.ewintr.nl/planner/plan/storage"
"go-mod.ewintr.nl/planner/sync/client" "go-mod.ewintr.nl/planner/sync/client"
@ -19,13 +20,13 @@ func NewShowArgs() ShowArgs {
return ShowArgs{} return ShowArgs{}
} }
func (sa ShowArgs) Parse(main []string, fields map[string]string) (Command, error) { func (sa ShowArgs) Parse(main []string, fields map[string]string) (command.Command, error) {
if len(main) != 1 { if len(main) != 1 {
return nil, ErrWrongCommand return nil, command.ErrWrongCommand
} }
lid, err := strconv.Atoi(main[0]) lid, err := strconv.Atoi(main[0])
if err != nil { if err != nil {
return nil, ErrWrongCommand return nil, command.ErrWrongCommand
} }
return &Show{ return &Show{
@ -39,7 +40,7 @@ type Show struct {
args ShowArgs args ShowArgs
} }
func (s Show) Do(repos Repositories, _ client.Client) (CommandResult, error) { func (s Show) Do(repos command.Repositories, _ client.Client) (command.CommandResult, error) {
tx, err := repos.Begin() tx, err := repos.Begin()
if err != nil { if err != nil {
return nil, fmt.Errorf("could not start transaction: %v", err) return nil, fmt.Errorf("could not start transaction: %v", err)

View File

@ -1,11 +1,11 @@
package command_test package task_test
import ( import (
"fmt" "fmt"
"testing" "testing"
"go-mod.ewintr.nl/planner/item" "go-mod.ewintr.nl/planner/item"
"go-mod.ewintr.nl/planner/plan/command" "go-mod.ewintr.nl/planner/plan/command/task"
"go-mod.ewintr.nl/planner/plan/storage/memory" "go-mod.ewintr.nl/planner/plan/storage/memory"
) )
@ -60,7 +60,7 @@ func TestShow(t *testing.T) {
} { } {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
// parse // parse
cmd, actParseErr := command.NewShowArgs().Parse(tc.main, nil) cmd, actParseErr := task.NewShowArgs().Parse(tc.main, nil)
if tc.expParseErr != (actParseErr != nil) { if tc.expParseErr != (actParseErr != nil) {
t.Errorf("exp %v, got %v", tc.expParseErr, actParseErr != nil) t.Errorf("exp %v, got %v", tc.expParseErr, actParseErr != nil)
} }

View File

@ -1,4 +1,4 @@
package command package task
import ( import (
"errors" "errors"
@ -9,6 +9,8 @@ import (
"time" "time"
"go-mod.ewintr.nl/planner/item" "go-mod.ewintr.nl/planner/item"
"go-mod.ewintr.nl/planner/plan/cli/arg"
"go-mod.ewintr.nl/planner/plan/command"
"go-mod.ewintr.nl/planner/plan/format" "go-mod.ewintr.nl/planner/plan/format"
"go-mod.ewintr.nl/planner/plan/storage" "go-mod.ewintr.nl/planner/plan/storage"
"go-mod.ewintr.nl/planner/sync/client" "go-mod.ewintr.nl/planner/sync/client"
@ -38,9 +40,9 @@ func NewUpdateArgs() UpdateArgs {
} }
} }
func (ua UpdateArgs) Parse(main []string, fields map[string]string) (Command, error) { func (ua UpdateArgs) Parse(main []string, fields map[string]string) (command.Command, error) {
if len(main) < 2 { if len(main) < 2 {
return nil, ErrWrongCommand return nil, command.ErrWrongCommand
} }
aliases := []string{"u", "update", "m", "mod"} aliases := []string{"u", "update", "m", "mod"}
var localIDStr string var localIDStr string
@ -50,13 +52,13 @@ func (ua UpdateArgs) Parse(main []string, fields map[string]string) (Command, er
case slices.Contains(aliases, main[1]): case slices.Contains(aliases, main[1]):
localIDStr = main[0] localIDStr = main[0]
default: default:
return nil, ErrWrongCommand return nil, command.ErrWrongCommand
} }
localID, err := strconv.Atoi(localIDStr) localID, err := strconv.Atoi(localIDStr)
if err != nil { if err != nil {
return nil, fmt.Errorf("not a local id: %v", main[1]) return nil, fmt.Errorf("not a local id: %v", main[1])
} }
fields, err = ResolveFields(fields, ua.fieldTPL) fields, err = arg.ResolveFields(fields, ua.fieldTPL)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -75,7 +77,7 @@ func (ua UpdateArgs) Parse(main []string, fields map[string]string) (Command, er
if val != "" { if val != "" {
d := item.NewDateFromString(val) d := item.NewDateFromString(val)
if d.IsZero() { if d.IsZero() {
return nil, fmt.Errorf("%w: could not parse date", ErrInvalidArg) return nil, fmt.Errorf("%w: could not parse date", command.ErrInvalidArg)
} }
args.Date = d args.Date = d
} }
@ -85,7 +87,7 @@ func (ua UpdateArgs) Parse(main []string, fields map[string]string) (Command, er
if val != "" { if val != "" {
t := item.NewTimeFromString(val) t := item.NewTimeFromString(val)
if t.IsZero() { if t.IsZero() {
return nil, fmt.Errorf("%w: could not parse time", ErrInvalidArg) return nil, fmt.Errorf("%w: could not parse time", command.ErrInvalidArg)
} }
args.Time = t args.Time = t
} }
@ -95,7 +97,7 @@ func (ua UpdateArgs) Parse(main []string, fields map[string]string) (Command, er
if val != "" { if val != "" {
d, err := time.ParseDuration(val) d, err := time.ParseDuration(val)
if err != nil { if err != nil {
return nil, fmt.Errorf("%w: could not parse duration", ErrInvalidArg) return nil, fmt.Errorf("%w: could not parse duration", command.ErrInvalidArg)
} }
args.Duration = d args.Duration = d
} }
@ -105,7 +107,7 @@ func (ua UpdateArgs) Parse(main []string, fields map[string]string) (Command, er
if val != "" { if val != "" {
rec := item.NewRecurrer(val) rec := item.NewRecurrer(val)
if rec == nil { if rec == nil {
return nil, fmt.Errorf("%w: could not parse recurrer", ErrInvalidArg) return nil, fmt.Errorf("%w: could not parse recurrer", command.ErrInvalidArg)
} }
args.Recurrer = rec args.Recurrer = rec
} }
@ -118,7 +120,7 @@ type Update struct {
args UpdateArgs args UpdateArgs
} }
func (u Update) Do(repos Repositories, _ client.Client) (CommandResult, error) { func (u Update) Do(repos command.Repositories, _ client.Client) (command.CommandResult, error) {
tx, err := repos.Begin() tx, err := repos.Begin()
if err != nil { if err != nil {
return nil, fmt.Errorf("could not start transaction: %v", err) return nil, fmt.Errorf("could not start transaction: %v", err)

View File

@ -1,4 +1,4 @@
package command_test package task_test
import ( import (
"fmt" "fmt"
@ -6,7 +6,7 @@ import (
"time" "time"
"go-mod.ewintr.nl/planner/item" "go-mod.ewintr.nl/planner/item"
"go-mod.ewintr.nl/planner/plan/command" "go-mod.ewintr.nl/planner/plan/command/task"
"go-mod.ewintr.nl/planner/plan/storage/memory" "go-mod.ewintr.nl/planner/plan/storage/memory"
) )
@ -207,7 +207,7 @@ func TestUpdateExecute(t *testing.T) {
} }
// parse // parse
cmd, actErr := command.NewUpdateArgs().Parse(tc.main, tc.fields) cmd, actErr := task.NewUpdateArgs().Parse(tc.main, tc.fields)
if tc.expParseErr != (actErr != nil) { if tc.expParseErr != (actErr != nil) {
t.Errorf("exp %v, got %v", tc.expParseErr, actErr) t.Errorf("exp %v, got %v", tc.expParseErr, actErr)
} }

View File

@ -5,7 +5,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"go-mod.ewintr.nl/planner/plan/command" "go-mod.ewintr.nl/planner/plan/cli"
"go-mod.ewintr.nl/planner/plan/storage/sqlite" "go-mod.ewintr.nl/planner/plan/storage/sqlite"
"go-mod.ewintr.nl/planner/sync/client" "go-mod.ewintr.nl/planner/sync/client"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
@ -35,7 +35,7 @@ func main() {
syncClient := client.New(conf.SyncURL, conf.ApiKey) syncClient := client.New(conf.SyncURL, conf.ApiKey)
cli := command.NewCLI(repos, syncClient) cli := cli.NewCLI(repos, syncClient)
if err := cli.Run(os.Args[1:]); err != nil { if err := cli.Run(os.Args[1:]); err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)

View File

@ -8,6 +8,7 @@ type Memories struct {
localID *LocalID localID *LocalID
sync *Sync sync *Sync
task *Task task *Task
schedule *Schedule
} }
func New() *Memories { func New() *Memories {
@ -15,6 +16,7 @@ func New() *Memories {
localID: NewLocalID(), localID: NewLocalID(),
sync: NewSync(), sync: NewSync(),
task: NewTask(), task: NewTask(),
schedule: NewSchedule(),
} }
} }
@ -33,3 +35,7 @@ func (mems *Memories) Sync(_ *storage.Tx) storage.Sync {
func (mems *Memories) Task(_ *storage.Tx) storage.Task { func (mems *Memories) Task(_ *storage.Tx) storage.Task {
return mems.task return mems.task
} }
func (mems *Memories) Schedule(_ *storage.Tx) storage.Schedule {
return mems.schedule
}

View File

@ -18,14 +18,14 @@ func (ss *SqliteSchedule) Store(sched item.Schedule) error {
} }
if _, err := ss.tx.Exec(` if _, err := ss.tx.Exec(`
INSERT INTO schedules INSERT INTO schedules
(id, title, date, recurrer) (id, title, date, recur)
VALUES VALUES
(?, ?, ?, ?) (?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE ON CONFLICT(id) DO UPDATE
SET SET
title=?, title=?,
date=?, date=?,
recurrer=? recur=?
`, `,
sched.ID, sched.Title, sched.Date.String(), recurStr, sched.ID, sched.Title, sched.Date.String(), recurStr,
sched.Title, sched.Date.String(), recurStr); err != nil { sched.Title, sched.Date.String(), recurStr); err != nil {
@ -36,7 +36,7 @@ recurrer=?
func (ss *SqliteSchedule) Find(start, end item.Date) ([]item.Schedule, error) { func (ss *SqliteSchedule) Find(start, end item.Date) ([]item.Schedule, error) {
rows, err := ss.tx.Query(`SELECT rows, err := ss.tx.Query(`SELECT
id, title, date, recurrer id, title, date, recur
FROM schedules FROM schedules
WHERE date >= ? AND date <= ?`, start.String(), end.String()) WHERE date >= ? AND date <= ?`, start.String(), end.String())
if err != nil { if err != nil {

View File

@ -44,6 +44,10 @@ func (sqs *Sqlites) Task(tx *storage.Tx) storage.Task {
return &SqliteTask{tx: tx} return &SqliteTask{tx: tx}
} }
func (sqs *Sqlites) Schedule(tx *storage.Tx) storage.Schedule {
return &SqliteSchedule{tx: tx}
}
func NewSqlites(dbPath string) (*Sqlites, error) { func NewSqlites(dbPath string) (*Sqlites, error) {
db, err := sql.Open("sqlite", dbPath) db, err := sql.Open("sqlite", dbPath)
if err != nil { if err != nil {

View File

@ -24,7 +24,7 @@ func New(url, apiKey string) *HTTP {
baseURL: url, baseURL: url,
apiKey: apiKey, apiKey: apiKey,
c: &http.Client{ c: &http.Client{
Timeout: 10 * time.Second, Timeout: 300 * time.Second,
}, },
} }
} }