Skip to content

Commit ad78080

Browse files
committedJan 27, 2025
schedule add
1 parent f28a364 commit ad78080

31 files changed

+747
-172
lines changed
 

‎dist/plan

28.4 KB
Binary file not shown.

‎item/item.go

+5
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,17 @@ package item
22

33
import (
44
"encoding/json"
5+
"errors"
56
"time"
67

78
"github.com/google/go-cmp/cmp"
89
"github.com/google/uuid"
910
)
1011

12+
var (
13+
ErrInvalidKind = errors.New("invalid kind")
14+
)
15+
1116
type Kind string
1217

1318
const (

‎item/schedule.go

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package item
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
7+
"github.com/google/go-cmp/cmp"
8+
)
9+
10+
type ScheduleBody struct {
11+
Title string `json:"title"`
12+
}
13+
14+
type Schedule struct {
15+
ID string `json:"id"`
16+
Date Date `json:"date"`
17+
Recurrer Recurrer `json:"recurrer"`
18+
RecurNext Date `json:"recurNext"`
19+
ScheduleBody
20+
}
21+
22+
func NewSchedule(i Item) (Schedule, error) {
23+
if i.Kind != KindSchedule {
24+
return Schedule{}, ErrInvalidKind
25+
}
26+
27+
var s Schedule
28+
if err := json.Unmarshal([]byte(i.Body), &s); err != nil {
29+
return Schedule{}, fmt.Errorf("could not unmarshal item body: %v", err)
30+
}
31+
32+
s.ID = i.ID
33+
s.Date = i.Date
34+
35+
return s, nil
36+
}
37+
38+
func (s Schedule) Item() (Item, error) {
39+
body, err := json.Marshal(s.ScheduleBody)
40+
if err != nil {
41+
return Item{}, fmt.Errorf("could not marshal schedule body: %v", err)
42+
}
43+
44+
return Item{
45+
ID: s.ID,
46+
Kind: KindSchedule,
47+
Date: s.Date,
48+
Body: string(body),
49+
}, nil
50+
}
51+
52+
func ScheduleDiff(a, b Schedule) string {
53+
aJSON, _ := json.Marshal(a)
54+
bJSON, _ := json.Marshal(b)
55+
56+
return cmp.Diff(string(aJSON), string(bJSON))
57+
}
58+
59+
func ScheduleDiffs(a, b []Schedule) string {
60+
aJSON, _ := json.Marshal(a)
61+
bJSON, _ := json.Marshal(b)
62+
63+
return cmp.Diff(string(aJSON), string(bJSON))
64+
}

‎item/task.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ type Task struct {
5656

5757
func NewTask(i Item) (Task, error) {
5858
if i.Kind != KindTask {
59-
return Task{}, fmt.Errorf("item is not an task")
59+
return Task{}, ErrInvalidKind
6060
}
6161

6262
var t Task

‎plan/cli/arg/arg.go

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package arg
2+
3+
import (
4+
"fmt"
5+
"slices"
6+
"strings"
7+
8+
"go-mod.ewintr.nl/planner/plan/command"
9+
)
10+
11+
func FindFields(args []string) ([]string, map[string]string) {
12+
fields := make(map[string]string)
13+
main := make([]string, 0)
14+
for i := 0; i < len(args); i++ {
15+
// normal key:value
16+
if k, v, ok := strings.Cut(args[i], ":"); ok && !strings.Contains(k, " ") {
17+
fields[k] = v
18+
continue
19+
}
20+
// empty key:
21+
if !strings.Contains(args[i], " ") && strings.HasSuffix(args[i], ":") {
22+
k := strings.TrimSuffix(args[i], ":")
23+
fields[k] = ""
24+
}
25+
main = append(main, args[i])
26+
}
27+
28+
return main, fields
29+
}
30+
31+
func ResolveFields(fields map[string]string, tmpl map[string][]string) (map[string]string, error) {
32+
res := make(map[string]string)
33+
for k, v := range fields {
34+
for tk, tv := range tmpl {
35+
if slices.Contains(tv, k) {
36+
if _, ok := res[tk]; ok {
37+
return nil, fmt.Errorf("%w: duplicate field: %v", command.ErrInvalidArg, tk)
38+
}
39+
res[tk] = v
40+
delete(fields, k)
41+
}
42+
}
43+
}
44+
if len(fields) > 0 {
45+
ks := make([]string, 0, len(fields))
46+
for k := range fields {
47+
ks = append(ks, k)
48+
}
49+
return nil, fmt.Errorf("%w: unknown field(s): %v", command.ErrInvalidArg, strings.Join(ks, ","))
50+
}
51+
52+
return res, nil
53+
}

‎plan/command/command_test.go ‎plan/cli/arg/arg_test.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
package command_test
1+
package arg_test
22

33
import (
44
"testing"
55

66
"github.com/google/go-cmp/cmp"
7-
"go-mod.ewintr.nl/planner/plan/command"
7+
"go-mod.ewintr.nl/planner/plan/cli/arg"
88
)
99

1010
func TestFindFields(t *testing.T) {
@@ -55,7 +55,7 @@ func TestFindFields(t *testing.T) {
5555
},
5656
} {
5757
t.Run(tc.name, func(t *testing.T) {
58-
actMain, actFields := command.FindFields(tc.args)
58+
actMain, actFields := arg.FindFields(tc.args)
5959
if diff := cmp.Diff(tc.expMain, actMain); diff != "" {
6060
t.Errorf("(exp +, got -)\n%s", diff)
6161
}
@@ -111,7 +111,7 @@ func TestResolveFields(t *testing.T) {
111111
},
112112
} {
113113
t.Run(tc.name, func(t *testing.T) {
114-
actRes, actErr := command.ResolveFields(tc.fields, tmpl)
114+
actRes, actErr := arg.ResolveFields(tc.fields, tmpl)
115115
if tc.expErr != (actErr != nil) {
116116
t.Errorf("exp %v, got %v", tc.expErr, actErr != nil)
117117
}

‎plan/cli/cli.go

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package cli
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
7+
"go-mod.ewintr.nl/planner/plan/cli/arg"
8+
"go-mod.ewintr.nl/planner/plan/command"
9+
"go-mod.ewintr.nl/planner/plan/command/schedule"
10+
"go-mod.ewintr.nl/planner/plan/command/task"
11+
"go-mod.ewintr.nl/planner/sync/client"
12+
)
13+
14+
type CLI struct {
15+
repos command.Repositories
16+
client client.Client
17+
cmdArgs []command.CommandArgs
18+
}
19+
20+
func NewCLI(repos command.Repositories, client client.Client) *CLI {
21+
return &CLI{
22+
repos: repos,
23+
client: client,
24+
cmdArgs: []command.CommandArgs{
25+
command.NewSyncArgs(),
26+
// task
27+
task.NewShowArgs(), task.NewProjectsArgs(),
28+
task.NewAddArgs(), task.NewDeleteArgs(), task.NewListArgs(),
29+
task.NewUpdateArgs(),
30+
// schedule
31+
schedule.NewAddArgs(),
32+
},
33+
}
34+
}
35+
36+
func (cli *CLI) Run(args []string) error {
37+
main, fields := arg.FindFields(args)
38+
for _, ca := range cli.cmdArgs {
39+
cmd, err := ca.Parse(main, fields)
40+
switch {
41+
case errors.Is(err, command.ErrWrongCommand):
42+
continue
43+
case err != nil:
44+
return err
45+
}
46+
47+
result, err := cmd.Do(cli.repos, cli.client)
48+
if err != nil {
49+
return err
50+
}
51+
fmt.Println(result.Render())
52+
53+
return nil
54+
}
55+
56+
return fmt.Errorf("could not find matching command")
57+
}

‎plan/command/command.go

+1-88
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@ package command
22

33
import (
44
"errors"
5-
"fmt"
6-
"slices"
7-
"strings"
85

96
"go-mod.ewintr.nl/planner/plan/storage"
107
"go-mod.ewintr.nl/planner/sync/client"
@@ -25,6 +22,7 @@ type Repositories interface {
2522
LocalID(tx *storage.Tx) storage.LocalID
2623
Sync(tx *storage.Tx) storage.Sync
2724
Task(tx *storage.Tx) storage.Task
25+
Schedule(tx *storage.Tx) storage.Schedule
2826
}
2927

3028
type CommandArgs interface {
@@ -38,88 +36,3 @@ type Command interface {
3836
type CommandResult interface {
3937
Render() string
4038
}
41-
42-
type CLI struct {
43-
repos Repositories
44-
client client.Client
45-
cmdArgs []CommandArgs
46-
}
47-
48-
func NewCLI(repos Repositories, client client.Client) *CLI {
49-
return &CLI{
50-
repos: repos,
51-
client: client,
52-
cmdArgs: []CommandArgs{
53-
NewShowArgs(), NewProjectsArgs(),
54-
NewAddArgs(), NewDeleteArgs(), NewListArgs(),
55-
NewSyncArgs(), NewUpdateArgs(),
56-
},
57-
}
58-
}
59-
60-
func (cli *CLI) Run(args []string) error {
61-
main, fields := FindFields(args)
62-
for _, ca := range cli.cmdArgs {
63-
cmd, err := ca.Parse(main, fields)
64-
switch {
65-
case errors.Is(err, ErrWrongCommand):
66-
continue
67-
case err != nil:
68-
return err
69-
}
70-
71-
result, err := cmd.Do(cli.repos, cli.client)
72-
if err != nil {
73-
return err
74-
}
75-
fmt.Println(result.Render())
76-
77-
return nil
78-
}
79-
80-
return fmt.Errorf("could not find matching command")
81-
}
82-
83-
func FindFields(args []string) ([]string, map[string]string) {
84-
fields := make(map[string]string)
85-
main := make([]string, 0)
86-
for i := 0; i < len(args); i++ {
87-
// normal key:value
88-
if k, v, ok := strings.Cut(args[i], ":"); ok && !strings.Contains(k, " ") {
89-
fields[k] = v
90-
continue
91-
}
92-
// empty key:
93-
if !strings.Contains(args[i], " ") && strings.HasSuffix(args[i], ":") {
94-
k := strings.TrimSuffix(args[i], ":")
95-
fields[k] = ""
96-
}
97-
main = append(main, args[i])
98-
}
99-
100-
return main, fields
101-
}
102-
103-
func ResolveFields(fields map[string]string, tmpl map[string][]string) (map[string]string, error) {
104-
res := make(map[string]string)
105-
for k, v := range fields {
106-
for tk, tv := range tmpl {
107-
if slices.Contains(tv, k) {
108-
if _, ok := res[tk]; ok {
109-
return nil, fmt.Errorf("%w: duplicate field: %v", ErrInvalidArg, tk)
110-
}
111-
res[tk] = v
112-
delete(fields, k)
113-
}
114-
}
115-
}
116-
if len(fields) > 0 {
117-
ks := make([]string, 0, len(fields))
118-
for k := range fields {
119-
ks = append(ks, k)
120-
}
121-
return nil, fmt.Errorf("%w: unknown field(s): %v", ErrInvalidArg, strings.Join(ks, ","))
122-
}
123-
124-
return res, nil
125-
}

‎plan/command/schedule/add.go

+124
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package schedule
2+
3+
import (
4+
"fmt"
5+
"slices"
6+
"strings"
7+
8+
"github.com/google/uuid"
9+
"go-mod.ewintr.nl/planner/item"
10+
"go-mod.ewintr.nl/planner/plan/cli/arg"
11+
"go-mod.ewintr.nl/planner/plan/command"
12+
"go-mod.ewintr.nl/planner/plan/format"
13+
"go-mod.ewintr.nl/planner/sync/client"
14+
)
15+
16+
type AddArgs struct {
17+
fieldTPL map[string][]string
18+
Schedule item.Schedule
19+
}
20+
21+
func NewAddArgs() AddArgs {
22+
return AddArgs{
23+
fieldTPL: map[string][]string{
24+
"date": {"d", "date", "on"},
25+
"recurrer": {"rec", "recurrer"},
26+
},
27+
}
28+
}
29+
30+
func (aa AddArgs) Parse(main []string, fields map[string]string) (command.Command, error) {
31+
if len(main) == 0 || !slices.Contains([]string{"s", "sched", "schedule"}, main[0]) {
32+
return nil, command.ErrWrongCommand
33+
}
34+
main = main[1:]
35+
if len(main) == 0 || !slices.Contains([]string{"add", "a", "new", "n"}, main[0]) {
36+
return nil, command.ErrWrongCommand
37+
}
38+
39+
main = main[1:]
40+
if len(main) == 0 {
41+
return nil, fmt.Errorf("%w: title is required for add", command.ErrInvalidArg)
42+
}
43+
fields, err := arg.ResolveFields(fields, aa.fieldTPL)
44+
if err != nil {
45+
return nil, err
46+
}
47+
48+
sched := item.Schedule{
49+
ID: uuid.New().String(),
50+
ScheduleBody: item.ScheduleBody{
51+
Title: strings.Join(main, " "),
52+
},
53+
}
54+
55+
if val, ok := fields["date"]; ok {
56+
d := item.NewDateFromString(val)
57+
if d.IsZero() {
58+
return nil, fmt.Errorf("%w: could not parse date", command.ErrInvalidArg)
59+
}
60+
sched.Date = d
61+
}
62+
if val, ok := fields["recurrer"]; ok {
63+
rec := item.NewRecurrer(val)
64+
if rec == nil {
65+
return nil, fmt.Errorf("%w: could not parse recurrer", command.ErrInvalidArg)
66+
}
67+
sched.Recurrer = rec
68+
sched.RecurNext = sched.Recurrer.First()
69+
}
70+
71+
return &Add{
72+
Args: AddArgs{
73+
Schedule: sched,
74+
},
75+
}, nil
76+
}
77+
78+
type Add struct {
79+
Args AddArgs
80+
}
81+
82+
func (a Add) Do(repos command.Repositories, _ client.Client) (command.CommandResult, error) {
83+
tx, err := repos.Begin()
84+
if err != nil {
85+
return nil, fmt.Errorf("could not start transaction: %v", err)
86+
}
87+
defer tx.Rollback()
88+
89+
if err := repos.Schedule(tx).Store(a.Args.Schedule); err != nil {
90+
return nil, fmt.Errorf("could not store schedule: %v", err)
91+
}
92+
93+
localID, err := repos.LocalID(tx).Next()
94+
if err != nil {
95+
return nil, fmt.Errorf("could not create next local id: %v", err)
96+
}
97+
if err := repos.LocalID(tx).Store(a.Args.Schedule.ID, localID); err != nil {
98+
return nil, fmt.Errorf("could not store local id: %v", err)
99+
}
100+
101+
it, err := a.Args.Schedule.Item()
102+
if err != nil {
103+
return nil, fmt.Errorf("could not convert schedule to sync item: %v", err)
104+
}
105+
if err := repos.Sync(tx).Store(it); err != nil {
106+
return nil, fmt.Errorf("could not store sync item: %v", err)
107+
}
108+
109+
if err := tx.Commit(); err != nil {
110+
return nil, fmt.Errorf("could not add schedule: %v", err)
111+
}
112+
113+
return AddResult{
114+
LocalID: localID,
115+
}, nil
116+
}
117+
118+
type AddResult struct {
119+
LocalID int
120+
}
121+
122+
func (ar AddResult) Render() string {
123+
return fmt.Sprintf("stored schedule %s", format.Bold(fmt.Sprintf("%d", ar.LocalID)))
124+
}

‎plan/command/schedule/add_test.go

+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package schedule_test
2+
3+
import (
4+
"testing"
5+
6+
"go-mod.ewintr.nl/planner/item"
7+
"go-mod.ewintr.nl/planner/plan/command/schedule"
8+
"go-mod.ewintr.nl/planner/plan/storage/memory"
9+
)
10+
11+
func TestAdd(t *testing.T) {
12+
t.Parallel()
13+
14+
aDate := item.NewDate(2024, 11, 2)
15+
16+
for _, tc := range []struct {
17+
name string
18+
main []string
19+
fields map[string]string
20+
expErr bool
21+
expSchedule item.Schedule
22+
}{
23+
{
24+
name: "empty",
25+
expErr: true,
26+
},
27+
{
28+
name: "title missing",
29+
main: []string{"sched", "add"},
30+
fields: map[string]string{
31+
"date": aDate.String(),
32+
},
33+
expErr: true,
34+
},
35+
{
36+
name: "all",
37+
main: []string{"sched", "add", "title"},
38+
fields: map[string]string{
39+
"date": aDate.String(),
40+
},
41+
expSchedule: item.Schedule{
42+
ID: "title",
43+
Date: aDate,
44+
ScheduleBody: item.ScheduleBody{
45+
Title: "title",
46+
},
47+
},
48+
},
49+
} {
50+
t.Run(tc.name, func(t *testing.T) {
51+
// setup
52+
mems := memory.New()
53+
54+
// parse
55+
cmd, actParseErr := schedule.NewAddArgs().Parse(tc.main, tc.fields)
56+
if tc.expErr != (actParseErr != nil) {
57+
t.Errorf("exp %v, got %v", tc.expErr, actParseErr)
58+
}
59+
if tc.expErr {
60+
return
61+
}
62+
63+
// do
64+
if _, err := cmd.Do(mems, nil); err != nil {
65+
t.Errorf("exp nil, got %v", err)
66+
}
67+
68+
// check
69+
actSchedules, err := mems.Schedule(nil).Find(aDate.Add(-1), aDate.Add(1))
70+
if err != nil {
71+
t.Errorf("exp nil, got %v", err)
72+
}
73+
if len(actSchedules) != 1 {
74+
t.Errorf("exp 1, got %d", len(actSchedules))
75+
}
76+
77+
actLocalIDs, err := mems.LocalID(nil).FindAll()
78+
if err != nil {
79+
t.Errorf("exp nil, got %v", err)
80+
}
81+
if len(actLocalIDs) != 1 {
82+
t.Errorf("exp 1, got %v", len(actLocalIDs))
83+
}
84+
if _, ok := actLocalIDs[actSchedules[0].ID]; !ok {
85+
t.Errorf("exp true, got %v", ok)
86+
}
87+
88+
if actSchedules[0].ID == "" {
89+
t.Errorf("exp string not te be empty")
90+
}
91+
tc.expSchedule.ID = actSchedules[0].ID
92+
if diff := item.ScheduleDiff(tc.expSchedule, actSchedules[0]); diff != "" {
93+
t.Errorf("(exp -, got +)\n%s", diff)
94+
}
95+
96+
updated, err := mems.Sync(nil).FindAll()
97+
if err != nil {
98+
t.Errorf("exp nil, got %v", err)
99+
}
100+
if len(updated) != 1 {
101+
t.Errorf("exp 1, got %v", len(updated))
102+
}
103+
})
104+
}
105+
}

‎plan/command/add.go ‎plan/command/task/add.go

+14-12
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package command
1+
package task
22

33
import (
44
"fmt"
@@ -8,6 +8,8 @@ import (
88

99
"github.com/google/uuid"
1010
"go-mod.ewintr.nl/planner/item"
11+
"go-mod.ewintr.nl/planner/plan/cli/arg"
12+
"go-mod.ewintr.nl/planner/plan/command"
1113
"go-mod.ewintr.nl/planner/plan/format"
1214
"go-mod.ewintr.nl/planner/sync/client"
1315
)
@@ -29,16 +31,16 @@ func NewAddArgs() AddArgs {
2931
}
3032
}
3133

32-
func (aa AddArgs) Parse(main []string, fields map[string]string) (Command, error) {
34+
func (aa AddArgs) Parse(main []string, fields map[string]string) (command.Command, error) {
3335
if len(main) == 0 || !slices.Contains([]string{"add", "a", "new", "n"}, main[0]) {
34-
return nil, ErrWrongCommand
36+
return nil, command.ErrWrongCommand
3537
}
3638

3739
main = main[1:]
3840
if len(main) == 0 {
39-
return nil, fmt.Errorf("%w: title is required for add", ErrInvalidArg)
41+
return nil, fmt.Errorf("%w: title is required for add", command.ErrInvalidArg)
4042
}
41-
fields, err := ResolveFields(fields, aa.fieldTPL)
43+
fields, err := arg.ResolveFields(fields, aa.fieldTPL)
4244
if err != nil {
4345
return nil, err
4446
}
@@ -56,28 +58,28 @@ func (aa AddArgs) Parse(main []string, fields map[string]string) (Command, error
5658
if val, ok := fields["date"]; ok {
5759
d := item.NewDateFromString(val)
5860
if d.IsZero() {
59-
return nil, fmt.Errorf("%w: could not parse date", ErrInvalidArg)
61+
return nil, fmt.Errorf("%w: could not parse date", command.ErrInvalidArg)
6062
}
6163
tsk.Date = d
6264
}
6365
if val, ok := fields["time"]; ok {
6466
t := item.NewTimeFromString(val)
6567
if t.IsZero() {
66-
return nil, fmt.Errorf("%w: could not parse time", ErrInvalidArg)
68+
return nil, fmt.Errorf("%w: could not parse time", command.ErrInvalidArg)
6769
}
6870
tsk.Time = t
6971
}
7072
if val, ok := fields["duration"]; ok {
7173
d, err := time.ParseDuration(val)
7274
if err != nil {
73-
return nil, fmt.Errorf("%w: could not parse duration", ErrInvalidArg)
75+
return nil, fmt.Errorf("%w: could not parse duration", command.ErrInvalidArg)
7476
}
7577
tsk.Duration = d
7678
}
7779
if val, ok := fields["recurrer"]; ok {
7880
rec := item.NewRecurrer(val)
7981
if rec == nil {
80-
return nil, fmt.Errorf("%w: could not parse recurrer", ErrInvalidArg)
82+
return nil, fmt.Errorf("%w: could not parse recurrer", command.ErrInvalidArg)
8183
}
8284
tsk.Recurrer = rec
8385
tsk.RecurNext = tsk.Recurrer.First()
@@ -94,15 +96,15 @@ type Add struct {
9496
Args AddArgs
9597
}
9698

97-
func (a Add) Do(repos Repositories, _ client.Client) (CommandResult, error) {
99+
func (a Add) Do(repos command.Repositories, _ client.Client) (command.CommandResult, error) {
98100
tx, err := repos.Begin()
99101
if err != nil {
100102
return nil, fmt.Errorf("could not start transaction: %v", err)
101103
}
102104
defer tx.Rollback()
103105

104106
if err := repos.Task(tx).Store(a.Args.Task); err != nil {
105-
return nil, fmt.Errorf("could not store event: %v", err)
107+
return nil, fmt.Errorf("could not store task: %v", err)
106108
}
107109

108110
localID, err := repos.LocalID(tx).Next()
@@ -115,7 +117,7 @@ func (a Add) Do(repos Repositories, _ client.Client) (CommandResult, error) {
115117

116118
it, err := a.Args.Task.Item()
117119
if err != nil {
118-
return nil, fmt.Errorf("could not convert event to sync item: %v", err)
120+
return nil, fmt.Errorf("could not convert task to sync item: %v", err)
119121
}
120122
if err := repos.Sync(tx).Store(it); err != nil {
121123
return nil, fmt.Errorf("could not store sync item: %v", err)

‎plan/command/add_test.go ‎plan/command/task/add_test.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
package command_test
1+
package task_test
22

33
import (
44
"testing"
55
"time"
66

77
"go-mod.ewintr.nl/planner/item"
8-
"go-mod.ewintr.nl/planner/plan/command"
8+
"go-mod.ewintr.nl/planner/plan/command/task"
99
"go-mod.ewintr.nl/planner/plan/storage"
1010
"go-mod.ewintr.nl/planner/plan/storage/memory"
1111
)
@@ -63,7 +63,7 @@ func TestAdd(t *testing.T) {
6363
mems := memory.New()
6464

6565
// parse
66-
cmd, actParseErr := command.NewAddArgs().Parse(tc.main, tc.fields)
66+
cmd, actParseErr := task.NewAddArgs().Parse(tc.main, tc.fields)
6767
if tc.expErr != (actParseErr != nil) {
6868
t.Errorf("exp %v, got %v", tc.expErr, actParseErr)
6969
}

‎plan/command/delete.go ‎plan/command/task/delete.go

+6-5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
package command
1+
package task
22

33
import (
44
"fmt"
55
"slices"
66
"strconv"
77

8+
"go-mod.ewintr.nl/planner/plan/command"
89
"go-mod.ewintr.nl/planner/plan/format"
910
"go-mod.ewintr.nl/planner/sync/client"
1011
)
@@ -17,9 +18,9 @@ func NewDeleteArgs() DeleteArgs {
1718
return DeleteArgs{}
1819
}
1920

20-
func (da DeleteArgs) Parse(main []string, flags map[string]string) (Command, error) {
21+
func (da DeleteArgs) Parse(main []string, flags map[string]string) (command.Command, error) {
2122
if len(main) != 2 {
22-
return nil, ErrWrongCommand
23+
return nil, command.ErrWrongCommand
2324
}
2425
aliases := []string{"d", "delete", "done"}
2526
var localIDStr string
@@ -29,7 +30,7 @@ func (da DeleteArgs) Parse(main []string, flags map[string]string) (Command, err
2930
case slices.Contains(aliases, main[1]):
3031
localIDStr = main[0]
3132
default:
32-
return nil, ErrWrongCommand
33+
return nil, command.ErrWrongCommand
3334
}
3435

3536
localID, err := strconv.Atoi(localIDStr)
@@ -48,7 +49,7 @@ type Delete struct {
4849
Args DeleteArgs
4950
}
5051

51-
func (del Delete) Do(repos Repositories, _ client.Client) (CommandResult, error) {
52+
func (del Delete) Do(repos command.Repositories, _ client.Client) (command.CommandResult, error) {
5253
tx, err := repos.Begin()
5354
if err != nil {
5455
return nil, fmt.Errorf("could not start transaction: %v", err)

‎plan/command/delete_test.go ‎plan/command/task/delete_test.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
package command_test
1+
package task_test
22

33
import (
44
"errors"
55
"testing"
66

77
"go-mod.ewintr.nl/planner/item"
8-
"go-mod.ewintr.nl/planner/plan/command"
8+
"go-mod.ewintr.nl/planner/plan/command/task"
99
"go-mod.ewintr.nl/planner/plan/storage"
1010
"go-mod.ewintr.nl/planner/plan/storage/memory"
1111
)
@@ -59,7 +59,7 @@ func TestDelete(t *testing.T) {
5959
}
6060

6161
// parse
62-
cmd, actParseErr := command.NewDeleteArgs().Parse(tc.main, tc.flags)
62+
cmd, actParseErr := task.NewDeleteArgs().Parse(tc.main, tc.flags)
6363
if tc.expParseErr != (actParseErr != nil) {
6464
t.Errorf("exp %v, got %v", tc.expParseErr, actParseErr)
6565
}

‎plan/command/list.go ‎plan/command/task/list.go

+8-6
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package command
1+
package task
22

33
import (
44
"fmt"
@@ -7,6 +7,8 @@ import (
77
"time"
88

99
"go-mod.ewintr.nl/planner/item"
10+
"go-mod.ewintr.nl/planner/plan/cli/arg"
11+
"go-mod.ewintr.nl/planner/plan/command"
1012
"go-mod.ewintr.nl/planner/plan/format"
1113
"go-mod.ewintr.nl/planner/plan/storage"
1214
"go-mod.ewintr.nl/planner/sync/client"
@@ -31,12 +33,12 @@ func NewListArgs() ListArgs {
3133
}
3234
}
3335

34-
func (la ListArgs) Parse(main []string, fields map[string]string) (Command, error) {
36+
func (la ListArgs) Parse(main []string, fields map[string]string) (command.Command, error) {
3537
if len(main) > 1 {
36-
return nil, ErrWrongCommand
38+
return nil, command.ErrWrongCommand
3739
}
3840

39-
fields, err := ResolveFields(fields, la.fieldTPL)
41+
fields, err := arg.ResolveFields(fields, la.fieldTPL)
4042
if err != nil {
4143
return nil, err
4244
}
@@ -63,7 +65,7 @@ func (la ListArgs) Parse(main []string, fields map[string]string) (Command, erro
6365
// fields["from"] = today.String()
6466
// fields["to"] = today.String()
6567
default:
66-
return nil, ErrWrongCommand
68+
return nil, command.ErrWrongCommand
6769
}
6870
}
6971

@@ -97,7 +99,7 @@ type List struct {
9799
Args ListArgs
98100
}
99101

100-
func (list List) Do(repos Repositories, _ client.Client) (CommandResult, error) {
102+
func (list List) Do(repos command.Repositories, _ client.Client) (command.CommandResult, error) {
101103
tx, err := repos.Begin()
102104
if err != nil {
103105
return nil, fmt.Errorf("could not start transaction: %v", err)

‎plan/command/list_test.go ‎plan/command/task/list_test.go

+13-13
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package command_test
1+
package task_test
22

33
import (
44
"testing"
@@ -7,7 +7,7 @@ import (
77
"github.com/google/go-cmp/cmp"
88
"github.com/google/go-cmp/cmp/cmpopts"
99
"go-mod.ewintr.nl/planner/item"
10-
"go-mod.ewintr.nl/planner/plan/command"
10+
"go-mod.ewintr.nl/planner/plan/command/task"
1111
"go-mod.ewintr.nl/planner/plan/storage/memory"
1212
)
1313

@@ -20,28 +20,28 @@ func TestListParse(t *testing.T) {
2020
name string
2121
main []string
2222
fields map[string]string
23-
expArgs command.ListArgs
23+
expArgs task.ListArgs
2424
expErr bool
2525
}{
2626
{
2727
name: "empty",
2828
main: []string{},
2929
fields: map[string]string{},
30-
expArgs: command.ListArgs{},
30+
expArgs: task.ListArgs{},
3131
},
3232
{
3333
name: "today",
3434
main: []string{"tod"},
3535
fields: map[string]string{},
36-
expArgs: command.ListArgs{
36+
expArgs: task.ListArgs{
3737
To: today,
3838
},
3939
},
4040
{
4141
name: "tomorrow",
4242
main: []string{"tom"},
4343
fields: map[string]string{},
44-
expArgs: command.ListArgs{
44+
expArgs: task.ListArgs{
4545
From: today.Add(1),
4646
To: today.Add(1),
4747
},
@@ -50,22 +50,22 @@ func TestListParse(t *testing.T) {
5050
name: "week",
5151
main: []string{"week"},
5252
fields: map[string]string{},
53-
expArgs: command.ListArgs{
53+
expArgs: task.ListArgs{
5454
From: today,
5555
To: today.Add(7),
5656
},
5757
},
5858
} {
5959
t.Run(tc.name, func(t *testing.T) {
60-
nla := command.NewListArgs()
60+
nla := task.NewListArgs()
6161
cmd, actErr := nla.Parse(tc.main, tc.fields)
6262
if tc.expErr != (actErr != nil) {
6363
t.Errorf("exp %v, got %v", tc.expErr, actErr != nil)
6464
}
6565
if tc.expErr {
6666
return
6767
}
68-
listCmd, ok := cmd.(command.List)
68+
listCmd, ok := cmd.(task.List)
6969
if !ok {
7070
t.Errorf("exp true, got false")
7171
}
@@ -97,7 +97,7 @@ func TestList(t *testing.T) {
9797

9898
for _, tc := range []struct {
9999
name string
100-
cmd command.List
100+
cmd task.List
101101
expRes bool
102102
expErr bool
103103
}{
@@ -107,8 +107,8 @@ func TestList(t *testing.T) {
107107
},
108108
{
109109
name: "empty list",
110-
cmd: command.List{
111-
Args: command.ListArgs{
110+
cmd: task.List{
111+
Args: task.ListArgs{
112112
HasRecurrer: true,
113113
},
114114
},
@@ -120,7 +120,7 @@ func TestList(t *testing.T) {
120120
t.Errorf("exp nil, got %v", err)
121121
}
122122

123-
listRes := res.(command.ListResult)
123+
listRes := res.(task.ListResult)
124124
actRes := len(listRes.Tasks) > 0
125125
if tc.expRes != actRes {
126126
t.Errorf("exp %v, got %v", tc.expRes, actRes)

‎plan/command/projects.go ‎plan/command/task/projects.go

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
package command
1+
package task
22

33
import (
44
"fmt"
55
"sort"
66

7+
"go-mod.ewintr.nl/planner/plan/command"
78
"go-mod.ewintr.nl/planner/plan/format"
89
"go-mod.ewintr.nl/planner/sync/client"
910
)
@@ -14,17 +15,17 @@ func NewProjectsArgs() ProjectsArgs {
1415
return ProjectsArgs{}
1516
}
1617

17-
func (pa ProjectsArgs) Parse(main []string, fields map[string]string) (Command, error) {
18+
func (pa ProjectsArgs) Parse(main []string, fields map[string]string) (command.Command, error) {
1819
if len(main) != 1 || main[0] != "projects" {
19-
return nil, ErrWrongCommand
20+
return nil, command.ErrWrongCommand
2021
}
2122

2223
return Projects{}, nil
2324
}
2425

2526
type Projects struct{}
2627

27-
func (ps Projects) Do(repos Repositories, _ client.Client) (CommandResult, error) {
28+
func (ps Projects) Do(repos command.Repositories, _ client.Client) (command.CommandResult, error) {
2829
tx, err := repos.Begin()
2930
if err != nil {
3031
return nil, fmt.Errorf("could not start transaction: %v", err)

‎plan/command/show.go ‎plan/command/task/show.go

+6-5
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
package command
1+
package task
22

33
import (
44
"errors"
55
"fmt"
66
"strconv"
77

88
"go-mod.ewintr.nl/planner/item"
9+
"go-mod.ewintr.nl/planner/plan/command"
910
"go-mod.ewintr.nl/planner/plan/format"
1011
"go-mod.ewintr.nl/planner/plan/storage"
1112
"go-mod.ewintr.nl/planner/sync/client"
@@ -19,13 +20,13 @@ func NewShowArgs() ShowArgs {
1920
return ShowArgs{}
2021
}
2122

22-
func (sa ShowArgs) Parse(main []string, fields map[string]string) (Command, error) {
23+
func (sa ShowArgs) Parse(main []string, fields map[string]string) (command.Command, error) {
2324
if len(main) != 1 {
24-
return nil, ErrWrongCommand
25+
return nil, command.ErrWrongCommand
2526
}
2627
lid, err := strconv.Atoi(main[0])
2728
if err != nil {
28-
return nil, ErrWrongCommand
29+
return nil, command.ErrWrongCommand
2930
}
3031

3132
return &Show{
@@ -39,7 +40,7 @@ type Show struct {
3940
args ShowArgs
4041
}
4142

42-
func (s Show) Do(repos Repositories, _ client.Client) (CommandResult, error) {
43+
func (s Show) Do(repos command.Repositories, _ client.Client) (command.CommandResult, error) {
4344
tx, err := repos.Begin()
4445
if err != nil {
4546
return nil, fmt.Errorf("could not start transaction: %v", err)

‎plan/command/show_test.go ‎plan/command/task/show_test.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
package command_test
1+
package task_test
22

33
import (
44
"fmt"
55
"testing"
66

77
"go-mod.ewintr.nl/planner/item"
8-
"go-mod.ewintr.nl/planner/plan/command"
8+
"go-mod.ewintr.nl/planner/plan/command/task"
99
"go-mod.ewintr.nl/planner/plan/storage/memory"
1010
)
1111

@@ -60,7 +60,7 @@ func TestShow(t *testing.T) {
6060
} {
6161
t.Run(tc.name, func(t *testing.T) {
6262
// parse
63-
cmd, actParseErr := command.NewShowArgs().Parse(tc.main, nil)
63+
cmd, actParseErr := task.NewShowArgs().Parse(tc.main, nil)
6464
if tc.expParseErr != (actParseErr != nil) {
6565
t.Errorf("exp %v, got %v", tc.expParseErr, actParseErr != nil)
6666
}

‎plan/command/update.go ‎plan/command/task/update.go

+12-10
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package command
1+
package task
22

33
import (
44
"errors"
@@ -9,6 +9,8 @@ import (
99
"time"
1010

1111
"go-mod.ewintr.nl/planner/item"
12+
"go-mod.ewintr.nl/planner/plan/cli/arg"
13+
"go-mod.ewintr.nl/planner/plan/command"
1214
"go-mod.ewintr.nl/planner/plan/format"
1315
"go-mod.ewintr.nl/planner/plan/storage"
1416
"go-mod.ewintr.nl/planner/sync/client"
@@ -38,9 +40,9 @@ func NewUpdateArgs() UpdateArgs {
3840
}
3941
}
4042

41-
func (ua UpdateArgs) Parse(main []string, fields map[string]string) (Command, error) {
43+
func (ua UpdateArgs) Parse(main []string, fields map[string]string) (command.Command, error) {
4244
if len(main) < 2 {
43-
return nil, ErrWrongCommand
45+
return nil, command.ErrWrongCommand
4446
}
4547
aliases := []string{"u", "update", "m", "mod"}
4648
var localIDStr string
@@ -50,13 +52,13 @@ func (ua UpdateArgs) Parse(main []string, fields map[string]string) (Command, er
5052
case slices.Contains(aliases, main[1]):
5153
localIDStr = main[0]
5254
default:
53-
return nil, ErrWrongCommand
55+
return nil, command.ErrWrongCommand
5456
}
5557
localID, err := strconv.Atoi(localIDStr)
5658
if err != nil {
5759
return nil, fmt.Errorf("not a local id: %v", main[1])
5860
}
59-
fields, err = ResolveFields(fields, ua.fieldTPL)
61+
fields, err = arg.ResolveFields(fields, ua.fieldTPL)
6062
if err != nil {
6163
return nil, err
6264
}
@@ -75,7 +77,7 @@ func (ua UpdateArgs) Parse(main []string, fields map[string]string) (Command, er
7577
if val != "" {
7678
d := item.NewDateFromString(val)
7779
if d.IsZero() {
78-
return nil, fmt.Errorf("%w: could not parse date", ErrInvalidArg)
80+
return nil, fmt.Errorf("%w: could not parse date", command.ErrInvalidArg)
7981
}
8082
args.Date = d
8183
}
@@ -85,7 +87,7 @@ func (ua UpdateArgs) Parse(main []string, fields map[string]string) (Command, er
8587
if val != "" {
8688
t := item.NewTimeFromString(val)
8789
if t.IsZero() {
88-
return nil, fmt.Errorf("%w: could not parse time", ErrInvalidArg)
90+
return nil, fmt.Errorf("%w: could not parse time", command.ErrInvalidArg)
8991
}
9092
args.Time = t
9193
}
@@ -95,7 +97,7 @@ func (ua UpdateArgs) Parse(main []string, fields map[string]string) (Command, er
9597
if val != "" {
9698
d, err := time.ParseDuration(val)
9799
if err != nil {
98-
return nil, fmt.Errorf("%w: could not parse duration", ErrInvalidArg)
100+
return nil, fmt.Errorf("%w: could not parse duration", command.ErrInvalidArg)
99101
}
100102
args.Duration = d
101103
}
@@ -105,7 +107,7 @@ func (ua UpdateArgs) Parse(main []string, fields map[string]string) (Command, er
105107
if val != "" {
106108
rec := item.NewRecurrer(val)
107109
if rec == nil {
108-
return nil, fmt.Errorf("%w: could not parse recurrer", ErrInvalidArg)
110+
return nil, fmt.Errorf("%w: could not parse recurrer", command.ErrInvalidArg)
109111
}
110112
args.Recurrer = rec
111113
}
@@ -118,7 +120,7 @@ type Update struct {
118120
args UpdateArgs
119121
}
120122

121-
func (u Update) Do(repos Repositories, _ client.Client) (CommandResult, error) {
123+
func (u Update) Do(repos command.Repositories, _ client.Client) (command.CommandResult, error) {
122124
tx, err := repos.Begin()
123125
if err != nil {
124126
return nil, fmt.Errorf("could not start transaction: %v", err)

‎plan/command/update_test.go ‎plan/command/task/update_test.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
package command_test
1+
package task_test
22

33
import (
44
"fmt"
55
"testing"
66
"time"
77

88
"go-mod.ewintr.nl/planner/item"
9-
"go-mod.ewintr.nl/planner/plan/command"
9+
"go-mod.ewintr.nl/planner/plan/command/task"
1010
"go-mod.ewintr.nl/planner/plan/storage/memory"
1111
)
1212

@@ -207,7 +207,7 @@ func TestUpdateExecute(t *testing.T) {
207207
}
208208

209209
// parse
210-
cmd, actErr := command.NewUpdateArgs().Parse(tc.main, tc.fields)
210+
cmd, actErr := task.NewUpdateArgs().Parse(tc.main, tc.fields)
211211
if tc.expParseErr != (actErr != nil) {
212212
t.Errorf("exp %v, got %v", tc.expParseErr, actErr)
213213
}

‎plan/main.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import (
55
"os"
66
"path/filepath"
77

8-
"go-mod.ewintr.nl/planner/plan/command"
8+
"go-mod.ewintr.nl/planner/plan/cli"
99
"go-mod.ewintr.nl/planner/plan/storage/sqlite"
1010
"go-mod.ewintr.nl/planner/sync/client"
1111
"gopkg.in/yaml.v3"
@@ -35,7 +35,7 @@ func main() {
3535

3636
syncClient := client.New(conf.SyncURL, conf.ApiKey)
3737

38-
cli := command.NewCLI(repos, syncClient)
38+
cli := cli.NewCLI(repos, syncClient)
3939
if err := cli.Run(os.Args[1:]); err != nil {
4040
fmt.Println(err)
4141
os.Exit(1)

‎plan/storage/memory/memory.go

+12-6
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,18 @@ import (
55
)
66

77
type Memories struct {
8-
localID *LocalID
9-
sync *Sync
10-
task *Task
8+
localID *LocalID
9+
sync *Sync
10+
task *Task
11+
schedule *Schedule
1112
}
1213

1314
func New() *Memories {
1415
return &Memories{
15-
localID: NewLocalID(),
16-
sync: NewSync(),
17-
task: NewTask(),
16+
localID: NewLocalID(),
17+
sync: NewSync(),
18+
task: NewTask(),
19+
schedule: NewSchedule(),
1820
}
1921
}
2022

@@ -33,3 +35,7 @@ func (mems *Memories) Sync(_ *storage.Tx) storage.Sync {
3335
func (mems *Memories) Task(_ *storage.Tx) storage.Task {
3436
return mems.task
3537
}
38+
39+
func (mems *Memories) Schedule(_ *storage.Tx) storage.Schedule {
40+
return mems.schedule
41+
}

‎plan/storage/memory/schedule.go

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package memory
2+
3+
import (
4+
"sync"
5+
6+
"go-mod.ewintr.nl/planner/item"
7+
"go-mod.ewintr.nl/planner/plan/storage"
8+
)
9+
10+
type Schedule struct {
11+
scheds map[string]item.Schedule
12+
mutex sync.RWMutex
13+
}
14+
15+
func NewSchedule() *Schedule {
16+
return &Schedule{
17+
scheds: make(map[string]item.Schedule),
18+
}
19+
}
20+
21+
func (s *Schedule) Store(sched item.Schedule) error {
22+
s.mutex.Lock()
23+
defer s.mutex.Unlock()
24+
25+
s.scheds[sched.ID] = sched
26+
return nil
27+
}
28+
29+
func (s *Schedule) Find(start, end item.Date) ([]item.Schedule, error) {
30+
s.mutex.RLock()
31+
defer s.mutex.RUnlock()
32+
33+
res := make([]item.Schedule, 0)
34+
for _, sched := range s.scheds {
35+
if start.After(sched.Date) || sched.Date.After(end) {
36+
continue
37+
}
38+
res = append(res, sched)
39+
}
40+
41+
return res, nil
42+
}
43+
44+
func (s *Schedule) Delete(id string) error {
45+
s.mutex.Lock()
46+
defer s.mutex.Unlock()
47+
48+
if _, exists := s.scheds[id]; !exists {
49+
return storage.ErrNotFound
50+
}
51+
delete(s.scheds, id)
52+
53+
return nil
54+
}

‎plan/storage/memory/schedule_test.go

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package memory_test
2+
3+
import (
4+
"sort"
5+
"testing"
6+
7+
"github.com/google/go-cmp/cmp"
8+
"go-mod.ewintr.nl/planner/item"
9+
"go-mod.ewintr.nl/planner/plan/storage/memory"
10+
)
11+
12+
func TestSchedule(t *testing.T) {
13+
t.Parallel()
14+
15+
mem := memory.NewSchedule()
16+
17+
actScheds, actErr := mem.Find(item.NewDateFromString("1900-01-01"), item.NewDateFromString("9999-12-31"))
18+
if actErr != nil {
19+
t.Errorf("exp nil, got %v", actErr)
20+
}
21+
if len(actScheds) != 0 {
22+
t.Errorf("exp 0, got %d", len(actScheds))
23+
}
24+
25+
s1 := item.Schedule{
26+
ID: "id-1",
27+
Date: item.NewDateFromString("2025-01-20"),
28+
}
29+
if err := mem.Store(s1); err != nil {
30+
t.Errorf("exp nil, got %v", err)
31+
}
32+
s2 := item.Schedule{
33+
ID: "id-2",
34+
Date: item.NewDateFromString("2025-01-21"),
35+
}
36+
if err := mem.Store(s2); err != nil {
37+
t.Errorf("exp nil, got %v", err)
38+
}
39+
40+
for _, tc := range []struct {
41+
name string
42+
start string
43+
end string
44+
exp []string
45+
}{
46+
{
47+
name: "all",
48+
start: "1900-01-01",
49+
end: "9999-12-31",
50+
exp: []string{s1.ID, s2.ID},
51+
},
52+
{
53+
name: "last",
54+
start: s2.Date.String(),
55+
end: "9999-12-31",
56+
exp: []string{s2.ID},
57+
},
58+
{
59+
name: "first",
60+
start: "1900-01-01",
61+
end: s1.Date.String(),
62+
exp: []string{s1.ID},
63+
},
64+
{
65+
name: "none",
66+
start: "1900-01-01",
67+
end: "2025-01-01",
68+
exp: make([]string, 0),
69+
},
70+
} {
71+
t.Run(tc.name, func(t *testing.T) {
72+
actScheds, actErr = mem.Find(item.NewDateFromString(tc.start), item.NewDateFromString(tc.end))
73+
if actErr != nil {
74+
t.Errorf("exp nil, got %v", actErr)
75+
}
76+
actIDs := make([]string, 0, len(actScheds))
77+
for _, s := range actScheds {
78+
actIDs = append(actIDs, s.ID)
79+
}
80+
sort.Strings(actIDs)
81+
if diff := cmp.Diff(tc.exp, actIDs); diff != "" {
82+
t.Errorf("(+exp, -got)%s\n", diff)
83+
}
84+
})
85+
}
86+
87+
}

‎plan/storage/memory/task.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ func (t *Task) FindMany(params storage.TaskListParams) ([]item.Task, error) {
3636

3737
tasks := make([]item.Task, 0, len(t.tasks))
3838
for _, tsk := range t.tasks {
39-
if storage.Match(tsk, params) {
39+
if storage.MatchTask(tsk, params) {
4040
tasks = append(tasks, tsk)
4141
}
4242
}

‎plan/storage/sqlite/migrations.go

+7
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,11 @@ var migrations = []string{
4444
`ALTER TABLE tasks ADD COLUMN project TEXT NOT NULL DEFAULT ''`,
4545
`CREATE TABLE syncupdate ("timestamp" TIMESTAMP NOT NULL)`,
4646
`INSERT INTO syncupdate (timestamp) VALUES ("0001-01-01T00:00:00Z")`,
47+
48+
`CREATE TABLE schedules (
49+
"id" TEXT UNIQUE NOT NULL DEFAULT '',
50+
"title" TEXT NOT NULL DEFAULT '',
51+
"date" TEXT NOT NULL DEFAULT '',
52+
"recur" TEXT NOT NULL DEFAULT '',
53+
"recur_next" TEXT NOT NULL DEFAULT '')`,
4754
}

‎plan/storage/sqlite/schedule.go

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package sqlite
2+
3+
import (
4+
"fmt"
5+
6+
"go-mod.ewintr.nl/planner/item"
7+
"go-mod.ewintr.nl/planner/plan/storage"
8+
)
9+
10+
type SqliteSchedule struct {
11+
tx *storage.Tx
12+
}
13+
14+
func (ss *SqliteSchedule) Store(sched item.Schedule) error {
15+
var recurStr string
16+
if sched.Recurrer != nil {
17+
recurStr = sched.Recurrer.String()
18+
}
19+
if _, err := ss.tx.Exec(`
20+
INSERT INTO schedules
21+
(id, title, date, recur)
22+
VALUES
23+
(?, ?, ?, ?)
24+
ON CONFLICT(id) DO UPDATE
25+
SET
26+
title=?,
27+
date=?,
28+
recur=?
29+
`,
30+
sched.ID, sched.Title, sched.Date.String(), recurStr,
31+
sched.Title, sched.Date.String(), recurStr); err != nil {
32+
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
33+
}
34+
return nil
35+
}
36+
37+
func (ss *SqliteSchedule) Find(start, end item.Date) ([]item.Schedule, error) {
38+
rows, err := ss.tx.Query(`SELECT
39+
id, title, date, recur
40+
FROM schedules
41+
WHERE date >= ? AND date <= ?`, start.String(), end.String())
42+
if err != nil {
43+
return nil, fmt.Errorf("%w: %v", ErrSqliteFailure, err)
44+
}
45+
defer rows.Close()
46+
scheds := make([]item.Schedule, 0)
47+
for rows.Next() {
48+
var sched item.Schedule
49+
var dateStr, recurStr string
50+
if err := rows.Scan(&sched.ID, &sched.Title, &dateStr, &recurStr); err != nil {
51+
return nil, fmt.Errorf("%w: %v", ErrSqliteFailure, err)
52+
}
53+
sched.Date = item.NewDateFromString(dateStr)
54+
sched.Recurrer = item.NewRecurrer(recurStr)
55+
56+
scheds = append(scheds, sched)
57+
}
58+
59+
return scheds, nil
60+
}
61+
62+
func (ss *SqliteSchedule) Delete(id string) error {
63+
64+
result, err := ss.tx.Exec(`
65+
DELETE FROM schedules
66+
WHERE id = ?`, id)
67+
if err != nil {
68+
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
69+
}
70+
71+
rowsAffected, err := result.RowsAffected()
72+
if err != nil {
73+
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
74+
}
75+
76+
if rowsAffected == 0 {
77+
return storage.ErrNotFound
78+
}
79+
80+
return nil
81+
}

‎plan/storage/sqlite/sqlite.go

+4
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ func (sqs *Sqlites) Task(tx *storage.Tx) storage.Task {
4444
return &SqliteTask{tx: tx}
4545
}
4646

47+
func (sqs *Sqlites) Schedule(tx *storage.Tx) storage.Schedule {
48+
return &SqliteSchedule{tx: tx}
49+
}
50+
4751
func NewSqlites(dbPath string) (*Sqlites, error) {
4852
db, err := sql.Open("sqlite", dbPath)
4953
if err != nil {

‎plan/storage/storage.go

+7-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,13 @@ type Task interface {
4949
Projects() (map[string]int, error)
5050
}
5151

52-
func Match(tsk item.Task, params TaskListParams) bool {
52+
type Schedule interface {
53+
Store(sched item.Schedule) error
54+
Find(start, end item.Date) ([]item.Schedule, error)
55+
Delete(id string) error
56+
}
57+
58+
func MatchTask(tsk item.Task, params TaskListParams) bool {
5359
if params.HasRecurrer && tsk.Recurrer == nil {
5460
return false
5561
}

‎plan/storage/storage_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,10 @@ func TestMatch(t *testing.T) {
5959
},
6060
} {
6161
t.Run(tc.name, func(t *testing.T) {
62-
if !storage.Match(tskMatch, tc.params) {
62+
if !storage.MatchTask(tskMatch, tc.params) {
6363
t.Errorf("exp tsk to match")
6464
}
65-
if storage.Match(tskNotMatch, tc.params) {
65+
if storage.MatchTask(tskNotMatch, tc.params) {
6666
t.Errorf("exp tsk to not match")
6767
}
6868
})

0 commit comments

Comments
 (0)
Please sign in to comment.