Compare commits
No commits in common. "sync" and "main" have entirely different histories.
|
@ -1,86 +0,0 @@
|
||||||
package command
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/urfave/cli/v2"
|
|
||||||
"go-mod.ewintr.nl/planner/item"
|
|
||||||
"go-mod.ewintr.nl/planner/plan/storage"
|
|
||||||
"go-mod.ewintr.nl/planner/sync/client"
|
|
||||||
)
|
|
||||||
|
|
||||||
var SyncCmd = &cli.Command{
|
|
||||||
Name: "sync",
|
|
||||||
Usage: "Synchronize with server",
|
|
||||||
Flags: []cli.Flag{
|
|
||||||
&cli.BoolFlag{
|
|
||||||
Name: "full",
|
|
||||||
Aliases: []string{"f"},
|
|
||||||
Usage: "Force full sync",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewSyncCmd(client client.Client, syncRepo storage.Sync, localIDRepo storage.LocalID, eventRepo storage.Event) *cli.Command {
|
|
||||||
SyncCmd.Action = func(cCtx *cli.Context) error {
|
|
||||||
return Sync(client, syncRepo, localIDRepo, eventRepo, cCtx.Bool("full"))
|
|
||||||
}
|
|
||||||
return SyncCmd
|
|
||||||
}
|
|
||||||
|
|
||||||
func Sync(client client.Client, syncRepo storage.Sync, localIDRepo storage.LocalID, eventRepo storage.Event, full bool) error {
|
|
||||||
// local new and updated
|
|
||||||
sendItems, err := syncRepo.FindAll()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("could not get updated items: %v", err)
|
|
||||||
}
|
|
||||||
if err := client.Update(sendItems); err != nil {
|
|
||||||
return fmt.Errorf("could not send updated items: %v", err)
|
|
||||||
}
|
|
||||||
if err := syncRepo.DeleteAll(); err != nil {
|
|
||||||
return fmt.Errorf("could not clear updated items: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// get new/updated items
|
|
||||||
ts, err := syncRepo.LastUpdate()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("could not find timestamp of last update: %v", err)
|
|
||||||
}
|
|
||||||
recItems, err := client.Updated([]item.Kind{item.KindEvent}, ts)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("could not receive updates: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// import to local
|
|
||||||
lidMap, err := localIDRepo.FindAll()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("could not get local ids: %v", err)
|
|
||||||
}
|
|
||||||
for _, ri := range recItems {
|
|
||||||
var eBody item.EventBody
|
|
||||||
if err := json.Unmarshal([]byte(ri.Body), &eBody); err != nil {
|
|
||||||
return fmt.Errorf("could not unmarshal event body: %v", err)
|
|
||||||
}
|
|
||||||
e := item.Event{
|
|
||||||
ID: ri.ID,
|
|
||||||
EventBody: eBody,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := eventRepo.Store(e); err != nil {
|
|
||||||
return fmt.Errorf("could not store event: %v", err)
|
|
||||||
}
|
|
||||||
lid, ok := lidMap[ri.ID]
|
|
||||||
if !ok {
|
|
||||||
lid, err = localIDRepo.Next()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("could not get next local id: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := localIDRepo.Store(ri.ID, lid); err != nil {
|
|
||||||
return fmt.Errorf("could not store local id: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,187 +0,0 @@
|
||||||
package command_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
|
||||||
"go-mod.ewintr.nl/planner/item"
|
|
||||||
"go-mod.ewintr.nl/planner/plan/command"
|
|
||||||
"go-mod.ewintr.nl/planner/plan/storage/memory"
|
|
||||||
"go-mod.ewintr.nl/planner/sync/client"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSyncSend(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
syncClient := client.NewMemory()
|
|
||||||
syncRepo := memory.NewSync()
|
|
||||||
localIDRepo := memory.NewLocalID()
|
|
||||||
eventRepo := memory.NewEvent()
|
|
||||||
|
|
||||||
it := item.Item{
|
|
||||||
ID: "a",
|
|
||||||
Kind: item.KindEvent,
|
|
||||||
Body: `{
|
|
||||||
"title":"title",
|
|
||||||
"start":"2024-10-18T08:00:00Z",
|
|
||||||
"duration":"1h"
|
|
||||||
}`,
|
|
||||||
}
|
|
||||||
if err := syncRepo.Store(it); err != nil {
|
|
||||||
t.Errorf("exp nil, got %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range []struct {
|
|
||||||
name string
|
|
||||||
ks []item.Kind
|
|
||||||
ts time.Time
|
|
||||||
expItems []item.Item
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "single",
|
|
||||||
ks: []item.Kind{item.KindEvent},
|
|
||||||
expItems: []item.Item{it},
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
|
|
||||||
if err := command.Sync(syncClient, syncRepo, localIDRepo, eventRepo, false); err != nil {
|
|
||||||
t.Errorf("exp nil, got %v", err)
|
|
||||||
}
|
|
||||||
actItems, actErr := syncClient.Updated(tc.ks, tc.ts)
|
|
||||||
if actErr != nil {
|
|
||||||
t.Errorf("exp nil, got %v", actErr)
|
|
||||||
}
|
|
||||||
if diff := cmp.Diff(tc.expItems, actItems); diff != "" {
|
|
||||||
t.Errorf("(exp +, got -)\n%s", diff)
|
|
||||||
}
|
|
||||||
|
|
||||||
actLeft, actErr := syncRepo.FindAll()
|
|
||||||
if actErr != nil {
|
|
||||||
t.Errorf("exp nil, got %v", actErr)
|
|
||||||
}
|
|
||||||
if len(actLeft) != 0 {
|
|
||||||
t.Errorf("exp 0, got %v", actLeft)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSyncReceive(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
oneHour, err := time.ParseDuration("1h")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("exp nil, got %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range []struct {
|
|
||||||
name string
|
|
||||||
present []item.Event
|
|
||||||
updated []item.Item
|
|
||||||
expEvent []item.Event
|
|
||||||
expLocalID map[string]int
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "no new",
|
|
||||||
expEvent: []item.Event{},
|
|
||||||
expLocalID: map[string]int{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "new",
|
|
||||||
updated: []item.Item{{
|
|
||||||
ID: "a",
|
|
||||||
Kind: item.KindEvent,
|
|
||||||
Body: `{
|
|
||||||
"title":"title",
|
|
||||||
"start":"2024-10-23T08:00:00Z",
|
|
||||||
"duration":"1h"
|
|
||||||
}`,
|
|
||||||
}},
|
|
||||||
expEvent: []item.Event{{
|
|
||||||
ID: "a",
|
|
||||||
EventBody: item.EventBody{
|
|
||||||
Title: "title",
|
|
||||||
Start: time.Date(2024, 10, 23, 8, 0, 0, 0, time.UTC),
|
|
||||||
Duration: oneHour,
|
|
||||||
},
|
|
||||||
}},
|
|
||||||
expLocalID: map[string]int{
|
|
||||||
"a": 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "update existing",
|
|
||||||
present: []item.Event{{
|
|
||||||
ID: "a",
|
|
||||||
EventBody: item.EventBody{
|
|
||||||
Title: "title",
|
|
||||||
Start: time.Date(2024, 10, 23, 8, 0, 0, 0, time.UTC),
|
|
||||||
Duration: oneHour,
|
|
||||||
},
|
|
||||||
}},
|
|
||||||
updated: []item.Item{{
|
|
||||||
ID: "a",
|
|
||||||
Kind: item.KindEvent,
|
|
||||||
Body: `{
|
|
||||||
"title":"new title",
|
|
||||||
"start":"2024-10-23T08:00:00Z",
|
|
||||||
"duration":"1h"
|
|
||||||
}`,
|
|
||||||
}},
|
|
||||||
expEvent: []item.Event{{
|
|
||||||
ID: "a",
|
|
||||||
EventBody: item.EventBody{
|
|
||||||
Title: "new title",
|
|
||||||
Start: time.Date(2024, 10, 23, 8, 0, 0, 0, time.UTC),
|
|
||||||
Duration: oneHour,
|
|
||||||
},
|
|
||||||
}},
|
|
||||||
expLocalID: map[string]int{
|
|
||||||
"a": 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
// setup
|
|
||||||
syncClient := client.NewMemory()
|
|
||||||
syncRepo := memory.NewSync()
|
|
||||||
localIDRepo := memory.NewLocalID()
|
|
||||||
eventRepo := memory.NewEvent()
|
|
||||||
|
|
||||||
for i, p := range tc.present {
|
|
||||||
if err := eventRepo.Store(p); err != nil {
|
|
||||||
t.Errorf("exp nil, got %v", err)
|
|
||||||
}
|
|
||||||
if err := localIDRepo.Store(p.ID, i+1); err != nil {
|
|
||||||
t.Errorf("exp nil, got %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := syncClient.Update(tc.updated); err != nil {
|
|
||||||
t.Errorf("exp nil, got %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// sync
|
|
||||||
if err := command.Sync(syncClient, syncRepo, localIDRepo, eventRepo, false); err != nil {
|
|
||||||
t.Errorf("exp nil, got %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// check result
|
|
||||||
actEvents, err := eventRepo.FindAll()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("exp nil, got %v", err)
|
|
||||||
}
|
|
||||||
if diff := cmp.Diff(tc.expEvent, actEvents); diff != "" {
|
|
||||||
t.Errorf("(exp +, got -)\n%s", diff)
|
|
||||||
}
|
|
||||||
actLocalIDs, err := localIDRepo.FindAll()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("exp nil, got %v", err)
|
|
||||||
}
|
|
||||||
if diff := cmp.Diff(tc.expLocalID, actLocalIDs); diff != "" {
|
|
||||||
t.Errorf("(exp +, got -)\n%s", diff)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
46
plan/main.go
46
plan/main.go
|
@ -8,7 +8,6 @@ import (
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
"go-mod.ewintr.nl/planner/plan/command"
|
"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"
|
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -24,14 +23,12 @@ func main() {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
localIDRepo, eventRepo, syncRepo, err := sqlite.NewSqlites(conf.DBPath)
|
localIDRepo, eventRepo, err := sqlite.NewSqlites(conf.DBPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("could not open db file: %s\n", err)
|
fmt.Printf("could not open db file: %s\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
syncClient := client.New(conf.SyncURL, conf.ApiKey)
|
|
||||||
|
|
||||||
app := &cli.App{
|
app := &cli.App{
|
||||||
Name: "plan",
|
Name: "plan",
|
||||||
Usage: "Plan your day with events",
|
Usage: "Plan your day with events",
|
||||||
|
@ -40,7 +37,6 @@ func main() {
|
||||||
command.NewListCmd(localIDRepo, eventRepo),
|
command.NewListCmd(localIDRepo, eventRepo),
|
||||||
command.NewUpdateCmd(localIDRepo, eventRepo),
|
command.NewUpdateCmd(localIDRepo, eventRepo),
|
||||||
command.NewDeleteCmd(localIDRepo, eventRepo),
|
command.NewDeleteCmd(localIDRepo, eventRepo),
|
||||||
command.NewSyncCmd(syncClient, syncRepo, localIDRepo, eventRepo),
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,12 +44,46 @@ func main() {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// all, err := repo.FindAll()
|
||||||
|
// if err != nil {
|
||||||
|
// fmt.Println(err)
|
||||||
|
// os.Exit(1)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fmt.Printf("all: %+v\n", all)
|
||||||
|
|
||||||
|
// c := client.NewClient("http://localhost:8092", "testKey")
|
||||||
|
// items, err := c.Updated([]item.Kind{item.KindEvent}, time.Time{})
|
||||||
|
// if err != nil {
|
||||||
|
// fmt.Println(err)
|
||||||
|
// os.Exit(1)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fmt.Printf("%+v\n", items)
|
||||||
|
|
||||||
|
// i := item.Item{
|
||||||
|
// ID: "id-1",
|
||||||
|
// Kind: item.KindEvent,
|
||||||
|
// Updated: time.Now(),
|
||||||
|
// Body: "body",
|
||||||
|
// }
|
||||||
|
// if err := c.Update([]item.Item{i}); err != nil {
|
||||||
|
// fmt.Println(err)
|
||||||
|
// os.Exit(1)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// items, err = c.Updated([]item.Kind{item.KindEvent}, time.Time{})
|
||||||
|
// if err != nil {
|
||||||
|
// fmt.Println(err)
|
||||||
|
// os.Exit(1)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fmt.Printf("%+v\n", items)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Configuration struct {
|
type Configuration struct {
|
||||||
DBPath string `yaml:"dbpath"`
|
DBPath string `yaml:"dbpath"`
|
||||||
SyncURL string `yaml:"sync_url"`
|
|
||||||
ApiKey string `yaml:"api_key"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadConfig(path string) (Configuration, error) {
|
func LoadConfig(path string) (Configuration, error) {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package memory
|
package memory
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"go-mod.ewintr.nl/planner/plan/storage"
|
"go-mod.ewintr.nl/planner/plan/storage"
|
||||||
|
@ -25,30 +24,6 @@ func (ml *LocalID) FindAll() (map[string]int, error) {
|
||||||
return ml.ids, nil
|
return ml.ids, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ml *LocalID) Find(id string) (int, error) {
|
|
||||||
ml.mutex.RLock()
|
|
||||||
defer ml.mutex.RUnlock()
|
|
||||||
|
|
||||||
lid, ok := ml.ids[id]
|
|
||||||
if !ok {
|
|
||||||
return 0, storage.ErrNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
return lid, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ml *LocalID) FindOrNext(id string) (int, error) {
|
|
||||||
lid, err := ml.Find(id)
|
|
||||||
switch {
|
|
||||||
case errors.Is(err, storage.ErrNotFound):
|
|
||||||
return ml.Next()
|
|
||||||
case err != nil:
|
|
||||||
return 0, err
|
|
||||||
default:
|
|
||||||
return lid, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ml *LocalID) Next() (int, error) {
|
func (ml *LocalID) Next() (int, error) {
|
||||||
ml.mutex.RLock()
|
ml.mutex.RLock()
|
||||||
defer ml.mutex.RUnlock()
|
defer ml.mutex.RUnlock()
|
||||||
|
|
|
@ -37,23 +37,6 @@ func TestLocalID(t *testing.T) {
|
||||||
t.Errorf("exp nil, got %v", actErr)
|
t.Errorf("exp nil, got %v", actErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Log("retrieve known")
|
|
||||||
actLid, actErr := repo.FindOrNext("test")
|
|
||||||
if actErr != nil {
|
|
||||||
t.Errorf("exp nil, got %v", actErr)
|
|
||||||
}
|
|
||||||
if actLid != 1 {
|
|
||||||
t.Errorf("exp 1, git %v", actLid)
|
|
||||||
}
|
|
||||||
t.Log("retrieve unknown")
|
|
||||||
actLid, actErr = repo.FindOrNext("new")
|
|
||||||
if actErr != nil {
|
|
||||||
t.Errorf("exp nil, got %v", actErr)
|
|
||||||
}
|
|
||||||
if actLid != 2 {
|
|
||||||
t.Errorf("exp 2, got %v", actLid)
|
|
||||||
}
|
|
||||||
|
|
||||||
actIDs, actErr = repo.FindAll()
|
actIDs, actErr = repo.FindAll()
|
||||||
if actErr != nil {
|
if actErr != nil {
|
||||||
t.Errorf("exp nil, got %v", actErr)
|
t.Errorf("exp nil, got %v", actErr)
|
||||||
|
|
|
@ -1,67 +0,0 @@
|
||||||
package memory
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sort"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"go-mod.ewintr.nl/planner/item"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Sync struct {
|
|
||||||
items map[string]item.Item
|
|
||||||
mutex sync.RWMutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewSync() *Sync {
|
|
||||||
return &Sync{
|
|
||||||
items: make(map[string]item.Item),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Sync) FindAll() ([]item.Item, error) {
|
|
||||||
r.mutex.RLock()
|
|
||||||
defer r.mutex.RUnlock()
|
|
||||||
|
|
||||||
items := make([]item.Item, 0, len(r.items))
|
|
||||||
for _, item := range r.items {
|
|
||||||
items = append(items, item)
|
|
||||||
}
|
|
||||||
sort.Slice(items, func(i, j int) bool {
|
|
||||||
return items[i].ID < items[j].ID
|
|
||||||
})
|
|
||||||
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Sync) Store(e item.Item) error {
|
|
||||||
r.mutex.Lock()
|
|
||||||
defer r.mutex.Unlock()
|
|
||||||
|
|
||||||
r.items[e.ID] = e
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Sync) DeleteAll() error {
|
|
||||||
r.mutex.Lock()
|
|
||||||
defer r.mutex.Unlock()
|
|
||||||
|
|
||||||
r.items = make(map[string]item.Item)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Sync) LastUpdate() (time.Time, error) {
|
|
||||||
r.mutex.RLock()
|
|
||||||
defer r.mutex.RUnlock()
|
|
||||||
|
|
||||||
var last time.Time
|
|
||||||
for _, i := range r.items {
|
|
||||||
if i.Updated.After(last) {
|
|
||||||
last = i.Updated
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return last, nil
|
|
||||||
}
|
|
|
@ -1,59 +0,0 @@
|
||||||
package memory_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"go-mod.ewintr.nl/planner/item"
|
|
||||||
"go-mod.ewintr.nl/planner/plan/storage/memory"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSync(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
mem := memory.NewSync()
|
|
||||||
|
|
||||||
t.Log("store")
|
|
||||||
now := time.Now()
|
|
||||||
ts := now
|
|
||||||
count := 3
|
|
||||||
for i := 0; i < count; i++ {
|
|
||||||
mem.Store(item.Item{
|
|
||||||
ID: fmt.Sprintf("id-%d", i),
|
|
||||||
Updated: ts,
|
|
||||||
})
|
|
||||||
ts = ts.Add(-1 * time.Minute)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Log("find all")
|
|
||||||
actItems, actErr := mem.FindAll()
|
|
||||||
if actErr != nil {
|
|
||||||
t.Errorf("exp nil, got %v", actErr)
|
|
||||||
}
|
|
||||||
if len(actItems) != count {
|
|
||||||
t.Errorf("exp %v, got %v", count, len(actItems))
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Log("last update")
|
|
||||||
actLU, actErr := mem.LastUpdate()
|
|
||||||
if actErr != nil {
|
|
||||||
t.Errorf("exp nil, got %v", actErr)
|
|
||||||
}
|
|
||||||
if !actLU.Equal(now) {
|
|
||||||
t.Errorf("exp %v, got %v", now, actLU)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Log("delete all")
|
|
||||||
if err := mem.DeleteAll(); err != nil {
|
|
||||||
t.Errorf("exp nil, got %v", err)
|
|
||||||
}
|
|
||||||
actItems, actErr = mem.FindAll()
|
|
||||||
if actErr != nil {
|
|
||||||
t.Errorf("exp nil, got %v", actErr)
|
|
||||||
}
|
|
||||||
if len(actItems) != 0 {
|
|
||||||
t.Errorf("exp 0, got %v", len(actItems))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -33,10 +33,6 @@ FROM localids
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *LocalID) FindOrNext(id string) (int, error) {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *LocalID) Next() (int, error) {
|
func (l *LocalID) Next() (int, error) {
|
||||||
idMap, err := l.FindAll()
|
idMap, err := l.FindAll()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -27,10 +27,10 @@ var (
|
||||||
ErrSqliteFailure = errors.New("sqlite returned an error")
|
ErrSqliteFailure = errors.New("sqlite returned an error")
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewSqlites(dbPath string) (*LocalID, *SqliteEvent, *SqliteSync, error) {
|
func NewSqlites(dbPath string) (*LocalID, *SqliteEvent, error) {
|
||||||
db, err := sql.Open("sqlite", dbPath)
|
db, err := sql.Open("sqlite", dbPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, nil, fmt.Errorf("%w: %v", ErrInvalidConfiguration, err)
|
return nil, nil, fmt.Errorf("%w: %v", ErrInvalidConfiguration, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
sl := &LocalID{
|
sl := &LocalID{
|
||||||
|
@ -39,13 +39,12 @@ func NewSqlites(dbPath string) (*LocalID, *SqliteEvent, *SqliteSync, error) {
|
||||||
se := &SqliteEvent{
|
se := &SqliteEvent{
|
||||||
db: db,
|
db: db,
|
||||||
}
|
}
|
||||||
ss := &SqliteSync{}
|
|
||||||
|
|
||||||
if err := migrate(db, migrations); err != nil {
|
if err := migrate(db, migrations); err != nil {
|
||||||
return nil, nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return sl, se, ss, nil
|
return sl, se, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func migrate(db *sql.DB, wanted []string) error {
|
func migrate(db *sql.DB, wanted []string) error {
|
||||||
|
|
|
@ -1,29 +0,0 @@
|
||||||
package sqlite
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"go-mod.ewintr.nl/planner/item"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SqliteSync struct{}
|
|
||||||
|
|
||||||
func NewSqliteSync() *SqliteSync {
|
|
||||||
return &SqliteSync{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SqliteSync) FindAll() ([]item.Item, error) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SqliteSync) Store(i item.Item) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SqliteSync) DeleteAll() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SqliteSync) LastUpdate() (time.Time, error) {
|
|
||||||
return time.Time{}, nil
|
|
||||||
}
|
|
|
@ -3,7 +3,6 @@ package storage
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"sort"
|
"sort"
|
||||||
"time"
|
|
||||||
|
|
||||||
"go-mod.ewintr.nl/planner/item"
|
"go-mod.ewintr.nl/planner/item"
|
||||||
)
|
)
|
||||||
|
@ -14,19 +13,11 @@ var (
|
||||||
|
|
||||||
type LocalID interface {
|
type LocalID interface {
|
||||||
FindAll() (map[string]int, error)
|
FindAll() (map[string]int, error)
|
||||||
FindOrNext(id string) (int, error)
|
|
||||||
Next() (int, error)
|
Next() (int, error)
|
||||||
Store(id string, localID int) error
|
Store(id string, localID int) error
|
||||||
Delete(id string) error
|
Delete(id string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type Sync interface {
|
|
||||||
FindAll() ([]item.Item, error)
|
|
||||||
Store(i item.Item) error
|
|
||||||
DeleteAll() error
|
|
||||||
LastUpdate() (time.Time, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Event interface {
|
type Event interface {
|
||||||
Store(event item.Event) error
|
Store(event item.Event) error
|
||||||
Find(id string) (item.Event, error)
|
Find(id string) (item.Event, error)
|
||||||
|
|
|
@ -1,12 +1,89 @@
|
||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go-mod.ewintr.nl/planner/item"
|
"go-mod.ewintr.nl/planner/item"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Client interface {
|
type Client struct {
|
||||||
Update(items []item.Item) error
|
baseURL string
|
||||||
Updated(ks []item.Kind, ts time.Time) ([]item.Item, error)
|
apiKey string
|
||||||
|
c *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClient(url, apiKey string) *Client {
|
||||||
|
return &Client{
|
||||||
|
baseURL: url,
|
||||||
|
apiKey: apiKey,
|
||||||
|
c: &http.Client{
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Update(items []item.Item) error {
|
||||||
|
body, err := json.Marshal(items)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not marhal body: %v", err)
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/sync", c.baseURL), bytes.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not create request: %v", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.apiKey))
|
||||||
|
|
||||||
|
res, err := c.c.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not make request: %v", err)
|
||||||
|
}
|
||||||
|
if res.StatusCode != http.StatusNoContent {
|
||||||
|
return fmt.Errorf("server returned status %d", res.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Updated(ks []item.Kind, ts time.Time) ([]item.Item, error) {
|
||||||
|
ksStr := make([]string, 0, len(ks))
|
||||||
|
for _, k := range ks {
|
||||||
|
ksStr = append(ksStr, string(k))
|
||||||
|
}
|
||||||
|
u := fmt.Sprintf("%s/sync?ks=%s", c.baseURL, strings.Join(ksStr, ","))
|
||||||
|
if !ts.IsZero() {
|
||||||
|
u = fmt.Sprintf("%s&ts=", url.QueryEscape(ts.Format(time.RFC3339)))
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest(http.MethodGet, u, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not create request: %v", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.apiKey))
|
||||||
|
|
||||||
|
res, err := c.c.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not get response: %v", err)
|
||||||
|
}
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("server returned status %d", res.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer res.Body.Close()
|
||||||
|
body, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not read response body: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var items []item.Item
|
||||||
|
if err := json.Unmarshal(body, &items); err != nil {
|
||||||
|
return nil, fmt.Errorf("could not unmarshal response body: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,89 +0,0 @@
|
||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"go-mod.ewintr.nl/planner/item"
|
|
||||||
)
|
|
||||||
|
|
||||||
type HTTP struct {
|
|
||||||
baseURL string
|
|
||||||
apiKey string
|
|
||||||
c *http.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(url, apiKey string) *HTTP {
|
|
||||||
return &HTTP{
|
|
||||||
baseURL: url,
|
|
||||||
apiKey: apiKey,
|
|
||||||
c: &http.Client{
|
|
||||||
Timeout: 10 * time.Second,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *HTTP) Update(items []item.Item) error {
|
|
||||||
body, err := json.Marshal(items)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("could not marhal body: %v", err)
|
|
||||||
}
|
|
||||||
req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/sync", c.baseURL), bytes.NewReader(body))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("could not create request: %v", err)
|
|
||||||
}
|
|
||||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.apiKey))
|
|
||||||
|
|
||||||
res, err := c.c.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("could not make request: %v", err)
|
|
||||||
}
|
|
||||||
if res.StatusCode != http.StatusNoContent {
|
|
||||||
return fmt.Errorf("server returned status %d", res.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *HTTP) Updated(ks []item.Kind, ts time.Time) ([]item.Item, error) {
|
|
||||||
ksStr := make([]string, 0, len(ks))
|
|
||||||
for _, k := range ks {
|
|
||||||
ksStr = append(ksStr, string(k))
|
|
||||||
}
|
|
||||||
u := fmt.Sprintf("%s/sync?ks=%s", c.baseURL, strings.Join(ksStr, ","))
|
|
||||||
if !ts.IsZero() {
|
|
||||||
u = fmt.Sprintf("%s&ts=", url.QueryEscape(ts.Format(time.RFC3339)))
|
|
||||||
}
|
|
||||||
req, err := http.NewRequest(http.MethodGet, u, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("could not create request: %v", err)
|
|
||||||
}
|
|
||||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.apiKey))
|
|
||||||
|
|
||||||
res, err := c.c.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("could not get response: %v", err)
|
|
||||||
}
|
|
||||||
if res.StatusCode != http.StatusOK {
|
|
||||||
return nil, fmt.Errorf("server returned status %d", res.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer res.Body.Close()
|
|
||||||
body, err := io.ReadAll(res.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("could not read response body: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var items []item.Item
|
|
||||||
if err := json.Unmarshal(body, &items); err != nil {
|
|
||||||
return nil, fmt.Errorf("could not unmarshal response body: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return items, nil
|
|
||||||
}
|
|
|
@ -1,45 +0,0 @@
|
||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"slices"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"go-mod.ewintr.nl/planner/item"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Memory struct {
|
|
||||||
items map[string]item.Item
|
|
||||||
sync.RWMutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewMemory() *Memory {
|
|
||||||
return &Memory{
|
|
||||||
items: make(map[string]item.Item, 0),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Memory) Update(items []item.Item) error {
|
|
||||||
m.Lock()
|
|
||||||
defer m.Unlock()
|
|
||||||
|
|
||||||
for _, i := range items {
|
|
||||||
m.items[i.ID] = i
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Memory) Updated(kw []item.Kind, ts time.Time) ([]item.Item, error) {
|
|
||||||
m.RLock()
|
|
||||||
defer m.RUnlock()
|
|
||||||
|
|
||||||
res := make([]item.Item, 0)
|
|
||||||
for _, i := range m.items {
|
|
||||||
if slices.Contains(kw, i.Kind) && (i.Updated.After(ts) || i.Updated.Equal(ts)) {
|
|
||||||
res = append(res, i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return res, nil
|
|
||||||
}
|
|
|
@ -1,60 +0,0 @@
|
||||||
package client_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
|
||||||
"go-mod.ewintr.nl/planner/item"
|
|
||||||
"go-mod.ewintr.nl/planner/sync/client"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMemory(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
mem := client.NewMemory()
|
|
||||||
|
|
||||||
now := time.Now()
|
|
||||||
items := []item.Item{
|
|
||||||
{ID: "a", Kind: item.KindTask, Updated: now.Add(-15 * time.Minute)},
|
|
||||||
{ID: "b", Kind: item.KindEvent, Updated: now.Add(-10 * time.Minute)},
|
|
||||||
{ID: "c", Kind: item.KindTask, Updated: now.Add(-5 * time.Minute)},
|
|
||||||
}
|
|
||||||
if err := mem.Update(items); err != nil {
|
|
||||||
t.Errorf("exp nil, got %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range []struct {
|
|
||||||
name string
|
|
||||||
ks []item.Kind
|
|
||||||
ts time.Time
|
|
||||||
expItems []item.Item
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "empty",
|
|
||||||
ks: make([]item.Kind, 0),
|
|
||||||
expItems: make([]item.Item, 0),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "kind",
|
|
||||||
ks: []item.Kind{item.KindEvent},
|
|
||||||
expItems: []item.Item{items[1]},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "timestamp",
|
|
||||||
ks: []item.Kind{item.KindTask, item.KindEvent},
|
|
||||||
ts: now.Add(-10 * time.Minute),
|
|
||||||
expItems: items[1:],
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
actItems, actErr := mem.Updated(tc.ks, tc.ts)
|
|
||||||
if actErr != nil {
|
|
||||||
t.Errorf("exp nil, got %v", actErr)
|
|
||||||
}
|
|
||||||
if diff := cmp.Diff(tc.expItems, actItems); diff != "" {
|
|
||||||
t.Errorf("(exp +, got -)\n%s", diff)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue