Compare commits
3 Commits
Author | SHA1 | Date |
---|---|---|
|
0abe8ad0ac | |
|
6677d156d8 | |
|
4818d7e71a |
|
@ -2,12 +2,17 @@ package item
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidKind = errors.New("invalid kind")
|
||||||
|
)
|
||||||
|
|
||||||
type Kind string
|
type Kind string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
package item
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ScheduleBody struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Schedule struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Date Date `json:"date"`
|
||||||
|
Recurrer Recurrer `json:"recurrer"`
|
||||||
|
RecurNext Date `json:"recurNext"`
|
||||||
|
ScheduleBody
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSchedule(i Item) (Schedule, error) {
|
||||||
|
if i.Kind != KindSchedule {
|
||||||
|
return Schedule{}, ErrInvalidKind
|
||||||
|
}
|
||||||
|
|
||||||
|
var s Schedule
|
||||||
|
if err := json.Unmarshal([]byte(i.Body), &s); err != nil {
|
||||||
|
return Schedule{}, fmt.Errorf("could not unmarshal item body: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.ID = i.ID
|
||||||
|
s.Date = i.Date
|
||||||
|
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Schedule) Item() (Item, error) {
|
||||||
|
body, err := json.Marshal(s.ScheduleBody)
|
||||||
|
if err != nil {
|
||||||
|
return Item{}, fmt.Errorf("could not marshal schedule body: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Item{
|
||||||
|
ID: s.ID,
|
||||||
|
Kind: KindSchedule,
|
||||||
|
Date: s.Date,
|
||||||
|
Body: string(body),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ScheduleDiff(a, b Schedule) string {
|
||||||
|
aJSON, _ := json.Marshal(a)
|
||||||
|
bJSON, _ := json.Marshal(b)
|
||||||
|
|
||||||
|
return cmp.Diff(string(aJSON), string(bJSON))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ScheduleDiffs(a, b []Schedule) string {
|
||||||
|
aJSON, _ := json.Marshal(a)
|
||||||
|
bJSON, _ := json.Marshal(b)
|
||||||
|
|
||||||
|
return cmp.Diff(string(aJSON), string(bJSON))
|
||||||
|
}
|
|
@ -56,7 +56,7 @@ type Task struct {
|
||||||
|
|
||||||
func NewTask(i Item) (Task, error) {
|
func NewTask(i Item) (Task, error) {
|
||||||
if i.Kind != KindTask {
|
if i.Kind != KindTask {
|
||||||
return Task{}, fmt.Errorf("item is not an task")
|
return Task{}, ErrInvalidKind
|
||||||
}
|
}
|
||||||
|
|
||||||
var t Task
|
var t Task
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
package memory
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"go-mod.ewintr.nl/planner/item"
|
||||||
|
"go-mod.ewintr.nl/planner/plan/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Schedule struct {
|
||||||
|
scheds map[string]item.Schedule
|
||||||
|
mutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSchedule() *Schedule {
|
||||||
|
return &Schedule{
|
||||||
|
scheds: make(map[string]item.Schedule),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Schedule) Store(sched item.Schedule) error {
|
||||||
|
s.mutex.Lock()
|
||||||
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
|
s.scheds[sched.ID] = sched
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Schedule) Find(start, end item.Date) ([]item.Schedule, error) {
|
||||||
|
s.mutex.RLock()
|
||||||
|
defer s.mutex.RUnlock()
|
||||||
|
|
||||||
|
res := make([]item.Schedule, 0)
|
||||||
|
for _, sched := range s.scheds {
|
||||||
|
if start.After(sched.Date) || sched.Date.After(end) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
res = append(res, sched)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Schedule) Delete(id string) error {
|
||||||
|
s.mutex.Lock()
|
||||||
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
|
if _, exists := s.scheds[id]; !exists {
|
||||||
|
return storage.ErrNotFound
|
||||||
|
}
|
||||||
|
delete(s.scheds, id)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
package memory_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"go-mod.ewintr.nl/planner/item"
|
||||||
|
"go-mod.ewintr.nl/planner/plan/storage/memory"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSchedule(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
mem := memory.NewSchedule()
|
||||||
|
|
||||||
|
actScheds, actErr := mem.Find(item.NewDateFromString("1900-01-01"), item.NewDateFromString("9999-12-31"))
|
||||||
|
if actErr != nil {
|
||||||
|
t.Errorf("exp nil, got %v", actErr)
|
||||||
|
}
|
||||||
|
if len(actScheds) != 0 {
|
||||||
|
t.Errorf("exp 0, got %d", len(actScheds))
|
||||||
|
}
|
||||||
|
|
||||||
|
s1 := item.Schedule{
|
||||||
|
ID: "id-1",
|
||||||
|
Date: item.NewDateFromString("2025-01-20"),
|
||||||
|
}
|
||||||
|
if err := mem.Store(s1); err != nil {
|
||||||
|
t.Errorf("exp nil, got %v", err)
|
||||||
|
}
|
||||||
|
s2 := item.Schedule{
|
||||||
|
ID: "id-2",
|
||||||
|
Date: item.NewDateFromString("2025-01-21"),
|
||||||
|
}
|
||||||
|
if err := mem.Store(s2); err != nil {
|
||||||
|
t.Errorf("exp nil, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
start string
|
||||||
|
end string
|
||||||
|
exp []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "all",
|
||||||
|
start: "1900-01-01",
|
||||||
|
end: "9999-12-31",
|
||||||
|
exp: []string{s1.ID, s2.ID},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "last",
|
||||||
|
start: s2.Date.String(),
|
||||||
|
end: "9999-12-31",
|
||||||
|
exp: []string{s2.ID},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "first",
|
||||||
|
start: "1900-01-01",
|
||||||
|
end: s1.Date.String(),
|
||||||
|
exp: []string{s1.ID},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "none",
|
||||||
|
start: "1900-01-01",
|
||||||
|
end: "2025-01-01",
|
||||||
|
exp: make([]string, 0),
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
actScheds, actErr = mem.Find(item.NewDateFromString(tc.start), item.NewDateFromString(tc.end))
|
||||||
|
if actErr != nil {
|
||||||
|
t.Errorf("exp nil, got %v", actErr)
|
||||||
|
}
|
||||||
|
actIDs := make([]string, 0, len(actScheds))
|
||||||
|
for _, s := range actScheds {
|
||||||
|
actIDs = append(actIDs, s.ID)
|
||||||
|
}
|
||||||
|
sort.Strings(actIDs)
|
||||||
|
if diff := cmp.Diff(tc.exp, actIDs); diff != "" {
|
||||||
|
t.Errorf("(+exp, -got)%s\n", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -36,7 +36,7 @@ func (t *Task) FindMany(params storage.TaskListParams) ([]item.Task, error) {
|
||||||
|
|
||||||
tasks := make([]item.Task, 0, len(t.tasks))
|
tasks := make([]item.Task, 0, len(t.tasks))
|
||||||
for _, tsk := range t.tasks {
|
for _, tsk := range t.tasks {
|
||||||
if storage.Match(tsk, params) {
|
if storage.MatchTask(tsk, params) {
|
||||||
tasks = append(tasks, tsk)
|
tasks = append(tasks, tsk)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,4 +44,11 @@ var migrations = []string{
|
||||||
`ALTER TABLE tasks ADD COLUMN project TEXT NOT NULL DEFAULT ''`,
|
`ALTER TABLE tasks ADD COLUMN project TEXT NOT NULL DEFAULT ''`,
|
||||||
`CREATE TABLE syncupdate ("timestamp" TIMESTAMP NOT NULL)`,
|
`CREATE TABLE syncupdate ("timestamp" TIMESTAMP NOT NULL)`,
|
||||||
`INSERT INTO syncupdate (timestamp) VALUES ("0001-01-01T00:00:00Z")`,
|
`INSERT INTO syncupdate (timestamp) VALUES ("0001-01-01T00:00:00Z")`,
|
||||||
|
|
||||||
|
`CREATE TABLE schedules (
|
||||||
|
"id" TEXT UNIQUE NOT NULL DEFAULT '',
|
||||||
|
"title" TEXT NOT NULL DEFAULT '',
|
||||||
|
"date" TEXT NOT NULL DEFAULT '',
|
||||||
|
"recur" TEXT NOT NULL DEFAULT '',
|
||||||
|
"recur_next" TEXT NOT NULL DEFAULT '')`,
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,81 @@
|
||||||
|
package sqlite
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"go-mod.ewintr.nl/planner/item"
|
||||||
|
"go-mod.ewintr.nl/planner/plan/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SqliteSchedule struct {
|
||||||
|
tx *storage.Tx
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss *SqliteSchedule) Store(sched item.Schedule) error {
|
||||||
|
var recurStr string
|
||||||
|
if sched.Recurrer != nil {
|
||||||
|
recurStr = sched.Recurrer.String()
|
||||||
|
}
|
||||||
|
if _, err := ss.tx.Exec(`
|
||||||
|
INSERT INTO schedules
|
||||||
|
(id, title, date, recurrer)
|
||||||
|
VALUES
|
||||||
|
(?, ?, ?, ?)
|
||||||
|
ON CONFLICT(id) DO UPDATE
|
||||||
|
SET
|
||||||
|
title=?,
|
||||||
|
date=?,
|
||||||
|
recurrer=?
|
||||||
|
`,
|
||||||
|
sched.ID, sched.Title, sched.Date.String(), recurStr,
|
||||||
|
sched.Title, sched.Date.String(), recurStr); err != nil {
|
||||||
|
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss *SqliteSchedule) Find(start, end item.Date) ([]item.Schedule, error) {
|
||||||
|
rows, err := ss.tx.Query(`SELECT
|
||||||
|
id, title, date, recurrer
|
||||||
|
FROM schedules
|
||||||
|
WHERE date >= ? AND date <= ?`, start.String(), end.String())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
scheds := make([]item.Schedule, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
var sched item.Schedule
|
||||||
|
var dateStr, recurStr string
|
||||||
|
if err := rows.Scan(&sched.ID, &sched.Title, &dateStr, &recurStr); err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||||
|
}
|
||||||
|
sched.Date = item.NewDateFromString(dateStr)
|
||||||
|
sched.Recurrer = item.NewRecurrer(recurStr)
|
||||||
|
|
||||||
|
scheds = append(scheds, sched)
|
||||||
|
}
|
||||||
|
|
||||||
|
return scheds, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss *SqliteSchedule) Delete(id string) error {
|
||||||
|
|
||||||
|
result, err := ss.tx.Exec(`
|
||||||
|
DELETE FROM schedules
|
||||||
|
WHERE id = ?`, id)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rowsAffected, err := result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if rowsAffected == 0 {
|
||||||
|
return storage.ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -49,7 +49,13 @@ type Task interface {
|
||||||
Projects() (map[string]int, error)
|
Projects() (map[string]int, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Match(tsk item.Task, params TaskListParams) bool {
|
type Schedule interface {
|
||||||
|
Store(sched item.Schedule) error
|
||||||
|
Find(start, end item.Date) ([]item.Schedule, error)
|
||||||
|
Delete(id string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func MatchTask(tsk item.Task, params TaskListParams) bool {
|
||||||
if params.HasRecurrer && tsk.Recurrer == nil {
|
if params.HasRecurrer && tsk.Recurrer == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,10 +59,10 @@ func TestMatch(t *testing.T) {
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
if !storage.Match(tskMatch, tc.params) {
|
if !storage.MatchTask(tskMatch, tc.params) {
|
||||||
t.Errorf("exp tsk to match")
|
t.Errorf("exp tsk to match")
|
||||||
}
|
}
|
||||||
if storage.Match(tskNotMatch, tc.params) {
|
if storage.MatchTask(tskNotMatch, tc.params) {
|
||||||
t.Errorf("exp tsk to not match")
|
t.Errorf("exp tsk to not match")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in New Issue