Compare commits

..

3 Commits

Author SHA1 Message Date
Erik Winter 0abe8ad0ac mem 2025-01-20 10:52:03 +01:00
Erik Winter 6677d156d8 sqlite 2025-01-19 11:16:34 +01:00
Erik Winter 4818d7e71a type 2025-01-19 10:56:03 +01:00
23 changed files with 171 additions and 455 deletions

BIN
dist/plan vendored

Binary file not shown.

View File

@ -1,56 +0,0 @@
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,57 +0,0 @@
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

@ -1,4 +1,4 @@
package task package command
import ( import (
"fmt" "fmt"
@ -8,8 +8,6 @@ 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"
) )
@ -31,16 +29,16 @@ func NewAddArgs() AddArgs {
} }
} }
func (aa AddArgs) Parse(main []string, fields map[string]string) (command.Command, error) { func (aa AddArgs) Parse(main []string, fields map[string]string) (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, command.ErrWrongCommand return nil, 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", command.ErrInvalidArg) return nil, fmt.Errorf("%w: title is required for add", ErrInvalidArg)
} }
fields, err := arg.ResolveFields(fields, aa.fieldTPL) fields, err := ResolveFields(fields, aa.fieldTPL)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -58,28 +56,28 @@ func (aa AddArgs) Parse(main []string, fields map[string]string) (command.Comman
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", command.ErrInvalidArg) return nil, fmt.Errorf("%w: could not parse date", 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", command.ErrInvalidArg) return nil, fmt.Errorf("%w: could not parse time", 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", command.ErrInvalidArg) return nil, fmt.Errorf("%w: could not parse duration", 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", command.ErrInvalidArg) return nil, fmt.Errorf("%w: could not parse recurrer", ErrInvalidArg)
} }
tsk.Recurrer = rec tsk.Recurrer = rec
tsk.RecurNext = tsk.Recurrer.First() tsk.RecurNext = tsk.Recurrer.First()
@ -96,7 +94,7 @@ type Add struct {
Args AddArgs Args AddArgs
} }
func (a Add) Do(repos command.Repositories, _ client.Client) (command.CommandResult, error) { func (a Add) Do(repos Repositories, _ client.Client) (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)
@ -104,7 +102,7 @@ func (a Add) Do(repos command.Repositories, _ client.Client) (command.CommandRes
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 task: %v", err) return nil, fmt.Errorf("could not store event: %v", err)
} }
localID, err := repos.LocalID(tx).Next() localID, err := repos.LocalID(tx).Next()
@ -117,7 +115,7 @@ func (a Add) Do(repos command.Repositories, _ client.Client) (command.CommandRes
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 task to sync item: %v", err) return nil, fmt.Errorf("could not convert event 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 task_test package command_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/task" "go-mod.ewintr.nl/planner/plan/command"
"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 := task.NewAddArgs().Parse(tc.main, tc.fields) cmd, actParseErr := command.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

@ -2,6 +2,9 @@ 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"
@ -22,7 +25,6 @@ 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 {
@ -36,3 +38,88 @@ 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

@ -1,14 +1,12 @@
package arg_test package command_test
import ( import (
"testing" "testing"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"go-mod.ewintr.nl/planner/plan/cli/arg" "go-mod.ewintr.nl/planner/plan/command"
) )
// 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()
@ -55,17 +53,9 @@ 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 := arg.FindFields(tc.args) actMain, actFields := command.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)
} }
@ -121,7 +111,7 @@ func TestResolveFields(t *testing.T) {
}, },
} { } {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
actRes, actErr := arg.ResolveFields(tc.fields, tmpl) actRes, actErr := command.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)
} }

View File

@ -1,11 +1,10 @@
package task package command
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"
) )
@ -18,9 +17,9 @@ func NewDeleteArgs() DeleteArgs {
return DeleteArgs{} return DeleteArgs{}
} }
func (da DeleteArgs) Parse(main []string, flags map[string]string) (command.Command, error) { func (da DeleteArgs) Parse(main []string, flags map[string]string) (Command, error) {
if len(main) != 2 { if len(main) != 2 {
return nil, command.ErrWrongCommand return nil, ErrWrongCommand
} }
aliases := []string{"d", "delete", "done"} aliases := []string{"d", "delete", "done"}
var localIDStr string var localIDStr string
@ -30,7 +29,7 @@ func (da DeleteArgs) Parse(main []string, flags map[string]string) (command.Comm
case slices.Contains(aliases, main[1]): case slices.Contains(aliases, main[1]):
localIDStr = main[0] localIDStr = main[0]
default: default:
return nil, command.ErrWrongCommand return nil, ErrWrongCommand
} }
localID, err := strconv.Atoi(localIDStr) localID, err := strconv.Atoi(localIDStr)
@ -49,7 +48,7 @@ type Delete struct {
Args DeleteArgs Args DeleteArgs
} }
func (del Delete) Do(repos command.Repositories, _ client.Client) (command.CommandResult, error) { func (del Delete) Do(repos Repositories, _ client.Client) (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 task_test package command_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/task" "go-mod.ewintr.nl/planner/plan/command"
"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 := task.NewDeleteArgs().Parse(tc.main, tc.flags) cmd, actParseErr := command.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 task package command
import ( import (
"fmt" "fmt"
@ -7,8 +7,6 @@ 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"
@ -33,12 +31,12 @@ func NewListArgs() ListArgs {
} }
} }
func (la ListArgs) Parse(main []string, fields map[string]string) (command.Command, error) { func (la ListArgs) Parse(main []string, fields map[string]string) (Command, error) {
if len(main) > 1 { if len(main) > 1 {
return nil, command.ErrWrongCommand return nil, ErrWrongCommand
} }
fields, err := arg.ResolveFields(fields, la.fieldTPL) fields, err := ResolveFields(fields, la.fieldTPL)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -65,7 +63,7 @@ func (la ListArgs) Parse(main []string, fields map[string]string) (command.Comma
// fields["from"] = today.String() // fields["from"] = today.String()
// fields["to"] = today.String() // fields["to"] = today.String()
default: default:
return nil, command.ErrWrongCommand return nil, ErrWrongCommand
} }
} }
@ -99,7 +97,7 @@ type List struct {
Args ListArgs Args ListArgs
} }
func (list List) Do(repos command.Repositories, _ client.Client) (command.CommandResult, error) { func (list List) Do(repos Repositories, _ client.Client) (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 task_test package command_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/task" "go-mod.ewintr.nl/planner/plan/command"
"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 task.ListArgs expArgs command.ListArgs
expErr bool expErr bool
}{ }{
{ {
name: "empty", name: "empty",
main: []string{}, main: []string{},
fields: map[string]string{}, fields: map[string]string{},
expArgs: task.ListArgs{}, expArgs: command.ListArgs{},
}, },
{ {
name: "today", name: "today",
main: []string{"tod"}, main: []string{"tod"},
fields: map[string]string{}, fields: map[string]string{},
expArgs: task.ListArgs{ expArgs: command.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: task.ListArgs{ expArgs: command.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: task.ListArgs{ expArgs: command.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 := task.NewListArgs() nla := command.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.(task.List) listCmd, ok := cmd.(command.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 task.List cmd command.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: task.List{ cmd: command.List{
Args: task.ListArgs{ Args: command.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.(task.ListResult) listRes := res.(command.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,10 +1,9 @@
package task package command
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"
) )
@ -15,9 +14,9 @@ func NewProjectsArgs() ProjectsArgs {
return ProjectsArgs{} return ProjectsArgs{}
} }
func (pa ProjectsArgs) Parse(main []string, fields map[string]string) (command.Command, error) { func (pa ProjectsArgs) Parse(main []string, fields map[string]string) (Command, error) {
if len(main) != 1 || main[0] != "projects" { if len(main) != 1 || main[0] != "projects" {
return nil, command.ErrWrongCommand return nil, ErrWrongCommand
} }
return Projects{}, nil return Projects{}, nil
@ -25,7 +24,7 @@ func (pa ProjectsArgs) Parse(main []string, fields map[string]string) (command.C
type Projects struct{} type Projects struct{}
func (ps Projects) Do(repos command.Repositories, _ client.Client) (command.CommandResult, error) { func (ps Projects) Do(repos Repositories, _ client.Client) (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,124 +0,0 @@
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

@ -1,105 +0,0 @@
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 task package command
import ( import (
"errors" "errors"
@ -6,7 +6,6 @@ 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"
@ -20,13 +19,13 @@ func NewShowArgs() ShowArgs {
return ShowArgs{} return ShowArgs{}
} }
func (sa ShowArgs) Parse(main []string, fields map[string]string) (command.Command, error) { func (sa ShowArgs) Parse(main []string, fields map[string]string) (Command, error) {
if len(main) != 1 { if len(main) != 1 {
return nil, command.ErrWrongCommand return nil, ErrWrongCommand
} }
lid, err := strconv.Atoi(main[0]) lid, err := strconv.Atoi(main[0])
if err != nil { if err != nil {
return nil, command.ErrWrongCommand return nil, ErrWrongCommand
} }
return &Show{ return &Show{
@ -40,7 +39,7 @@ type Show struct {
args ShowArgs args ShowArgs
} }
func (s Show) Do(repos command.Repositories, _ client.Client) (command.CommandResult, error) { func (s Show) Do(repos Repositories, _ client.Client) (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 task_test package command_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/task" "go-mod.ewintr.nl/planner/plan/command"
"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 := task.NewShowArgs().Parse(tc.main, nil) cmd, actParseErr := command.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 task package command
import ( import (
"errors" "errors"
@ -9,8 +9,6 @@ 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"
@ -40,9 +38,9 @@ func NewUpdateArgs() UpdateArgs {
} }
} }
func (ua UpdateArgs) Parse(main []string, fields map[string]string) (command.Command, error) { func (ua UpdateArgs) Parse(main []string, fields map[string]string) (Command, error) {
if len(main) < 2 { if len(main) < 2 {
return nil, command.ErrWrongCommand return nil, ErrWrongCommand
} }
aliases := []string{"u", "update", "m", "mod"} aliases := []string{"u", "update", "m", "mod"}
var localIDStr string var localIDStr string
@ -52,13 +50,13 @@ func (ua UpdateArgs) Parse(main []string, fields map[string]string) (command.Com
case slices.Contains(aliases, main[1]): case slices.Contains(aliases, main[1]):
localIDStr = main[0] localIDStr = main[0]
default: default:
return nil, command.ErrWrongCommand return nil, 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 = arg.ResolveFields(fields, ua.fieldTPL) fields, err = ResolveFields(fields, ua.fieldTPL)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -77,7 +75,7 @@ func (ua UpdateArgs) Parse(main []string, fields map[string]string) (command.Com
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", command.ErrInvalidArg) return nil, fmt.Errorf("%w: could not parse date", ErrInvalidArg)
} }
args.Date = d args.Date = d
} }
@ -87,7 +85,7 @@ func (ua UpdateArgs) Parse(main []string, fields map[string]string) (command.Com
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", command.ErrInvalidArg) return nil, fmt.Errorf("%w: could not parse time", ErrInvalidArg)
} }
args.Time = t args.Time = t
} }
@ -97,7 +95,7 @@ func (ua UpdateArgs) Parse(main []string, fields map[string]string) (command.Com
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", command.ErrInvalidArg) return nil, fmt.Errorf("%w: could not parse duration", ErrInvalidArg)
} }
args.Duration = d args.Duration = d
} }
@ -107,7 +105,7 @@ func (ua UpdateArgs) Parse(main []string, fields map[string]string) (command.Com
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", command.ErrInvalidArg) return nil, fmt.Errorf("%w: could not parse recurrer", ErrInvalidArg)
} }
args.Recurrer = rec args.Recurrer = rec
} }
@ -120,7 +118,7 @@ type Update struct {
args UpdateArgs args UpdateArgs
} }
func (u Update) Do(repos command.Repositories, _ client.Client) (command.CommandResult, error) { func (u Update) Do(repos Repositories, _ client.Client) (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 task_test package command_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/task" "go-mod.ewintr.nl/planner/plan/command"
"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 := task.NewUpdateArgs().Parse(tc.main, tc.fields) cmd, actErr := command.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/cli" "go-mod.ewintr.nl/planner/plan/command"
"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 := cli.NewCLI(repos, syncClient) cli := command.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

@ -5,18 +5,16 @@ import (
) )
type Memories struct { type Memories struct {
localID *LocalID localID *LocalID
sync *Sync sync *Sync
task *Task task *Task
schedule *Schedule
} }
func New() *Memories { func New() *Memories {
return &Memories{ return &Memories{
localID: NewLocalID(), localID: NewLocalID(),
sync: NewSync(), sync: NewSync(),
task: NewTask(), task: NewTask(),
schedule: NewSchedule(),
} }
} }
@ -35,7 +33,3 @@ 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, recur) (id, title, date, recurrer)
VALUES VALUES
(?, ?, ?, ?) (?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE ON CONFLICT(id) DO UPDATE
SET SET
title=?, title=?,
date=?, date=?,
recur=? recurrer=?
`, `,
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 @@ recur=?
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, recur id, title, date, recurrer
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,10 +44,6 @@ 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: 300 * time.Second, Timeout: 10 * time.Second,
}, },
} }
} }