sync
This commit is contained in:
parent
bdd794a1e7
commit
bb522038f9
|
@ -44,14 +44,14 @@ var AddCmd = &cli.Command{
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAddCmd(localRepo storage.LocalID, eventRepo storage.Event) *cli.Command {
|
func NewAddCmd(localRepo storage.LocalID, eventRepo storage.Event, syncRepo storage.Sync) *cli.Command {
|
||||||
AddCmd.Action = func(cCtx *cli.Context) error {
|
AddCmd.Action = func(cCtx *cli.Context) error {
|
||||||
return Add(localRepo, eventRepo, cCtx.String("name"), cCtx.String("on"), cCtx.String("at"), cCtx.String("for"))
|
return Add(localRepo, eventRepo, syncRepo, cCtx.String("name"), cCtx.String("on"), cCtx.String("at"), cCtx.String("for"))
|
||||||
}
|
}
|
||||||
return AddCmd
|
return AddCmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func Add(localIDRepo storage.LocalID, eventRepo storage.Event, nameStr, onStr, atStr, frStr string) error {
|
func Add(localIDRepo storage.LocalID, eventRepo storage.Event, syncRepo storage.Sync, nameStr, onStr, atStr, frStr string) error {
|
||||||
if nameStr == "" {
|
if nameStr == "" {
|
||||||
return fmt.Errorf("%w: name is required", ErrInvalidArg)
|
return fmt.Errorf("%w: name is required", ErrInvalidArg)
|
||||||
}
|
}
|
||||||
|
@ -103,5 +103,13 @@ func Add(localIDRepo storage.LocalID, eventRepo storage.Event, nameStr, onStr, a
|
||||||
return fmt.Errorf("could not store local id: %v", err)
|
return fmt.Errorf("could not store local id: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
it, err := e.Item()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not convert event to sync item: %v", err)
|
||||||
|
}
|
||||||
|
if err := syncRepo.Store(it); err != nil {
|
||||||
|
return fmt.Errorf("could not store sync item: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -106,7 +106,8 @@ func TestAdd(t *testing.T) {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
eventRepo := memory.NewEvent()
|
eventRepo := memory.NewEvent()
|
||||||
localRepo := memory.NewLocalID()
|
localRepo := memory.NewLocalID()
|
||||||
actErr := command.Add(localRepo, eventRepo, tc.args["name"], tc.args["on"], tc.args["at"], tc.args["for"]) != nil
|
syncRepo := memory.NewSync()
|
||||||
|
actErr := command.Add(localRepo, eventRepo, syncRepo, tc.args["name"], tc.args["on"], tc.args["at"], tc.args["for"]) != nil
|
||||||
if tc.expErr != actErr {
|
if tc.expErr != actErr {
|
||||||
t.Errorf("exp %v, got %v", tc.expErr, actErr)
|
t.Errorf("exp %v, got %v", tc.expErr, actErr)
|
||||||
}
|
}
|
||||||
|
@ -139,6 +140,14 @@ func TestAdd(t *testing.T) {
|
||||||
if diff := cmp.Diff(tc.expEvent, actEvents[0]); diff != "" {
|
if diff := cmp.Diff(tc.expEvent, actEvents[0]); diff != "" {
|
||||||
t.Errorf("(exp +, got -)\n%s", diff)
|
t.Errorf("(exp +, got -)\n%s", diff)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updated, err := syncRepo.FindAll()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("exp nil, got %v", err)
|
||||||
|
}
|
||||||
|
if len(updated) != 1 {
|
||||||
|
t.Errorf("exp 1, got %v", len(updated))
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,14 +20,14 @@ var DeleteCmd = &cli.Command{
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDeleteCmd(localRepo storage.LocalID, eventRepo storage.Event) *cli.Command {
|
func NewDeleteCmd(localRepo storage.LocalID, eventRepo storage.Event, syncRepo storage.Sync) *cli.Command {
|
||||||
DeleteCmd.Action = func(cCtx *cli.Context) error {
|
DeleteCmd.Action = func(cCtx *cli.Context) error {
|
||||||
return Delete(localRepo, eventRepo, cCtx.Int("localID"))
|
return Delete(localRepo, eventRepo, syncRepo, cCtx.Int("localID"))
|
||||||
}
|
}
|
||||||
return DeleteCmd
|
return DeleteCmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func Delete(localRepo storage.LocalID, eventRepo storage.Event, localID int) error {
|
func Delete(localRepo storage.LocalID, eventRepo storage.Event, syncRepo storage.Sync, localID int) error {
|
||||||
var id string
|
var id string
|
||||||
idMap, err := localRepo.FindAll()
|
idMap, err := localRepo.FindAll()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -46,5 +46,17 @@ func Delete(localRepo storage.LocalID, eventRepo storage.Event, localID int) err
|
||||||
return fmt.Errorf("could not delete event: %v", err)
|
return fmt.Errorf("could not delete event: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
e, err := eventRepo.Find(id)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not get event: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
it, err := e.Item()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not convert event to sync item: %v", err)
|
||||||
|
}
|
||||||
|
if err := syncRepo.Store(it); err != nil {
|
||||||
|
return fmt.Errorf("could not store sync item: %v", err)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,6 +35,7 @@ func TestDelete(t *testing.T) {
|
||||||
} {
|
} {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
eventRepo := memory.NewEvent()
|
eventRepo := memory.NewEvent()
|
||||||
|
syncRepo := memory.NewSync()
|
||||||
if err := eventRepo.Store(e); err != nil {
|
if err := eventRepo.Store(e); err != nil {
|
||||||
t.Errorf("exp nil, got %v", err)
|
t.Errorf("exp nil, got %v", err)
|
||||||
}
|
}
|
||||||
|
@ -43,7 +44,7 @@ func TestDelete(t *testing.T) {
|
||||||
t.Errorf("exp nil, got %v", err)
|
t.Errorf("exp nil, got %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
actErr := command.Delete(localRepo, eventRepo, tc.localID) != nil
|
actErr := command.Delete(localRepo, eventRepo, syncRepo, tc.localID) != nil
|
||||||
if tc.expErr != actErr {
|
if tc.expErr != actErr {
|
||||||
t.Errorf("exp %v, got %v", tc.expErr, actErr)
|
t.Errorf("exp %v, got %v", tc.expErr, actErr)
|
||||||
}
|
}
|
||||||
|
@ -62,6 +63,13 @@ func TestDelete(t *testing.T) {
|
||||||
if len(idMap) != 0 {
|
if len(idMap) != 0 {
|
||||||
t.Errorf("exp 0, got %v", len(idMap))
|
t.Errorf("exp 0, got %v", len(idMap))
|
||||||
}
|
}
|
||||||
|
updated, err := syncRepo.FindAll()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("exp nil, got %v", err)
|
||||||
|
}
|
||||||
|
if len(updated) != 1 {
|
||||||
|
t.Errorf("exp 1, got %v", len(updated))
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,100 @@
|
||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"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)
|
||||||
|
}
|
||||||
|
|
||||||
|
updated := make([]item.Item, 0)
|
||||||
|
for _, ri := range recItems {
|
||||||
|
if ri.Deleted {
|
||||||
|
if err := localIDRepo.Delete(ri.ID); err != nil {
|
||||||
|
return fmt.Errorf("could not delete local id: %v", err)
|
||||||
|
}
|
||||||
|
if err := eventRepo.Delete(ri.ID); err != nil && !errors.Is(err, storage.ErrNotFound) {
|
||||||
|
return fmt.Errorf("could not delete event: %v", err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
updated = append(updated, ri)
|
||||||
|
}
|
||||||
|
|
||||||
|
lidMap, err := localIDRepo.FindAll()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not get local ids: %v", err)
|
||||||
|
}
|
||||||
|
for _, u := range updated {
|
||||||
|
var eBody item.EventBody
|
||||||
|
if err := json.Unmarshal([]byte(u.Body), &eBody); err != nil {
|
||||||
|
return fmt.Errorf("could not unmarshal event body: %v", err)
|
||||||
|
}
|
||||||
|
e := item.Event{
|
||||||
|
ID: u.ID,
|
||||||
|
EventBody: eBody,
|
||||||
|
}
|
||||||
|
if err := eventRepo.Store(e); err != nil {
|
||||||
|
return fmt.Errorf("could not store event: %v", err)
|
||||||
|
}
|
||||||
|
lid, ok := lidMap[u.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(u.ID, lid); err != nil {
|
||||||
|
return fmt.Errorf("could not store local id: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,187 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -41,14 +41,14 @@ var UpdateCmd = &cli.Command{
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUpdateCmd(localRepo storage.LocalID, eventRepo storage.Event) *cli.Command {
|
func NewUpdateCmd(localRepo storage.LocalID, eventRepo storage.Event, syncRepo storage.Sync) *cli.Command {
|
||||||
UpdateCmd.Action = func(cCtx *cli.Context) error {
|
UpdateCmd.Action = func(cCtx *cli.Context) error {
|
||||||
return Update(localRepo, eventRepo, cCtx.Int("localID"), cCtx.String("name"), cCtx.String("on"), cCtx.String("at"), cCtx.String("for"))
|
return Update(localRepo, eventRepo, syncRepo, cCtx.Int("localID"), cCtx.String("name"), cCtx.String("on"), cCtx.String("at"), cCtx.String("for"))
|
||||||
}
|
}
|
||||||
return UpdateCmd
|
return UpdateCmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func Update(localRepo storage.LocalID, eventRepo storage.Event, localID int, nameStr, onStr, atStr, frStr string) error {
|
func Update(localRepo storage.LocalID, eventRepo storage.Event, syncRepo storage.Sync, localID int, nameStr, onStr, atStr, frStr string) error {
|
||||||
var id string
|
var id string
|
||||||
idMap, err := localRepo.FindAll()
|
idMap, err := localRepo.FindAll()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -99,5 +99,13 @@ func Update(localRepo storage.LocalID, eventRepo storage.Event, localID int, nam
|
||||||
return fmt.Errorf("could not store event: %v", err)
|
return fmt.Errorf("could not store event: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
it, err := e.Item()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not convert event to sync item: %v", err)
|
||||||
|
}
|
||||||
|
if err := syncRepo.Store(it); err != nil {
|
||||||
|
return fmt.Errorf("could not store sync item: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -154,6 +154,7 @@ func TestUpdate(t *testing.T) {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
eventRepo := memory.NewEvent()
|
eventRepo := memory.NewEvent()
|
||||||
localIDRepo := memory.NewLocalID()
|
localIDRepo := memory.NewLocalID()
|
||||||
|
syncRepo := memory.NewSync()
|
||||||
if err := eventRepo.Store(item.Event{
|
if err := eventRepo.Store(item.Event{
|
||||||
ID: eid,
|
ID: eid,
|
||||||
EventBody: item.EventBody{
|
EventBody: item.EventBody{
|
||||||
|
@ -168,7 +169,7 @@ func TestUpdate(t *testing.T) {
|
||||||
t.Errorf("exp nil, ,got %v", err)
|
t.Errorf("exp nil, ,got %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
actErr := command.Update(localIDRepo, eventRepo, tc.localID, tc.args["name"], tc.args["on"], tc.args["at"], tc.args["for"]) != nil
|
actErr := command.Update(localIDRepo, eventRepo, syncRepo, tc.localID, tc.args["name"], tc.args["on"], tc.args["at"], tc.args["for"]) != nil
|
||||||
if tc.expErr != actErr {
|
if tc.expErr != actErr {
|
||||||
t.Errorf("exp %v, got %v", tc.expErr, actErr)
|
t.Errorf("exp %v, got %v", tc.expErr, actErr)
|
||||||
}
|
}
|
||||||
|
@ -183,7 +184,13 @@ func TestUpdate(t *testing.T) {
|
||||||
if diff := cmp.Diff(tc.expEvent, actEvent); diff != "" {
|
if diff := cmp.Diff(tc.expEvent, actEvent); diff != "" {
|
||||||
t.Errorf("(exp +, got -)\n%s", diff)
|
t.Errorf("(exp +, got -)\n%s", diff)
|
||||||
}
|
}
|
||||||
|
updated, err := syncRepo.FindAll()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("exp nil, got %v", err)
|
||||||
|
}
|
||||||
|
if len(updated) != 1 {
|
||||||
|
t.Errorf("exp 1, got %v", len(updated))
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
52
plan/main.go
52
plan/main.go
|
@ -8,6 +8,7 @@ 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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -23,20 +24,23 @@ func main() {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
localIDRepo, eventRepo, err := sqlite.NewSqlites(conf.DBPath)
|
localIDRepo, eventRepo, syncRepo, 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",
|
||||||
Commands: []*cli.Command{
|
Commands: []*cli.Command{
|
||||||
command.NewAddCmd(localIDRepo, eventRepo),
|
command.NewAddCmd(localIDRepo, eventRepo, syncRepo),
|
||||||
command.NewListCmd(localIDRepo, eventRepo),
|
command.NewListCmd(localIDRepo, eventRepo),
|
||||||
command.NewUpdateCmd(localIDRepo, eventRepo),
|
command.NewUpdateCmd(localIDRepo, eventRepo, syncRepo),
|
||||||
command.NewDeleteCmd(localIDRepo, eventRepo),
|
command.NewDeleteCmd(localIDRepo, eventRepo, syncRepo),
|
||||||
|
command.NewSyncCmd(syncClient, syncRepo, localIDRepo, eventRepo),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,46 +48,12 @@ 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,6 +1,7 @@
|
||||||
package memory
|
package memory
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"go-mod.ewintr.nl/planner/plan/storage"
|
"go-mod.ewintr.nl/planner/plan/storage"
|
||||||
|
@ -24,6 +25,30 @@ 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,6 +37,23 @@ 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)
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
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
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
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,6 +33,10 @@ 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 {
|
||||||
|
|
|
@ -18,6 +18,13 @@ var migrations = []string{
|
||||||
`PRAGMA synchronous=NORMAL`,
|
`PRAGMA synchronous=NORMAL`,
|
||||||
`PRAGMA cache_size=2000`,
|
`PRAGMA cache_size=2000`,
|
||||||
`CREATE TABLE localids ("id" TEXT UNIQUE, "local_id" INTEGER)`,
|
`CREATE TABLE localids ("id" TEXT UNIQUE, "local_id" INTEGER)`,
|
||||||
|
`CREATE TABLE items (
|
||||||
|
id TEXT PRIMARY KEY NOT NULL,
|
||||||
|
kind TEXT NOT NULL,
|
||||||
|
updated TIMESTAMP NOT NULL,
|
||||||
|
deleted BOOLEAN NOT NULL,
|
||||||
|
body TEXT NOT NULL
|
||||||
|
)`,
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -27,10 +34,10 @@ var (
|
||||||
ErrSqliteFailure = errors.New("sqlite returned an error")
|
ErrSqliteFailure = errors.New("sqlite returned an error")
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewSqlites(dbPath string) (*LocalID, *SqliteEvent, error) {
|
func NewSqlites(dbPath string) (*LocalID, *SqliteEvent, *SqliteSync, error) {
|
||||||
db, err := sql.Open("sqlite", dbPath)
|
db, err := sql.Open("sqlite", dbPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("%w: %v", ErrInvalidConfiguration, err)
|
return nil, nil, nil, fmt.Errorf("%w: %v", ErrInvalidConfiguration, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
sl := &LocalID{
|
sl := &LocalID{
|
||||||
|
@ -39,12 +46,15 @@ func NewSqlites(dbPath string) (*LocalID, *SqliteEvent, error) {
|
||||||
se := &SqliteEvent{
|
se := &SqliteEvent{
|
||||||
db: db,
|
db: db,
|
||||||
}
|
}
|
||||||
|
ss := &SqliteSync{
|
||||||
if err := migrate(db, migrations); err != nil {
|
db: db,
|
||||||
return nil, nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return sl, se, nil
|
if err := migrate(db, migrations); err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return sl, se, ss, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func migrate(db *sql.DB, wanted []string) error {
|
func migrate(db *sql.DB, wanted []string) error {
|
||||||
|
|
|
@ -0,0 +1,92 @@
|
||||||
|
package sqlite
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go-mod.ewintr.nl/planner/item"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SqliteSync struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSqliteSync(db *sql.DB) *SqliteSync {
|
||||||
|
return &SqliteSync{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SqliteSync) FindAll() ([]item.Item, error) {
|
||||||
|
rows, err := s.db.Query("SELECT id, kind, updated, deleted, body FROM items")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: failed to query items: %v", ErrSqliteFailure, err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var items []item.Item
|
||||||
|
for rows.Next() {
|
||||||
|
var i item.Item
|
||||||
|
var updatedStr string
|
||||||
|
err := rows.Scan(&i.ID, &i.Kind, &updatedStr, &i.Deleted, &i.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: failed to scan item: %v", ErrSqliteFailure, err)
|
||||||
|
}
|
||||||
|
i.Updated, err = time.Parse(time.RFC3339, updatedStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: failed to parse updated time: %v", ErrSqliteFailure, err)
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = rows.Err(); err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: error iterating over rows: %v", ErrSqliteFailure, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SqliteSync) Store(i item.Item) error {
|
||||||
|
// Ensure we have a valid time
|
||||||
|
if i.Updated.IsZero() {
|
||||||
|
i.Updated = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := s.db.Exec(
|
||||||
|
"INSERT OR REPLACE INTO items (id, kind, updated, deleted, body) VALUES (?, ?, ?, ?, ?)",
|
||||||
|
i.ID,
|
||||||
|
i.Kind,
|
||||||
|
i.Updated.UTC().Format(time.RFC3339),
|
||||||
|
i.Deleted,
|
||||||
|
sql.NullString{String: i.Body, Valid: i.Body != ""}, // This allows empty string but not NULL
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%w: failed to store item: %v", ErrSqliteFailure, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SqliteSync) DeleteAll() error {
|
||||||
|
_, err := s.db.Exec("DELETE FROM items")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%w: failed to delete all items: %v", ErrSqliteFailure, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SqliteSync) LastUpdate() (time.Time, error) {
|
||||||
|
var updatedStr sql.NullString
|
||||||
|
err := s.db.QueryRow("SELECT MAX(updated) FROM items").Scan(&updatedStr)
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, fmt.Errorf("%w: failed to get last update: %v", ErrSqliteFailure, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !updatedStr.Valid {
|
||||||
|
return time.Time{}, nil // Return zero time if NULL or no rows
|
||||||
|
}
|
||||||
|
|
||||||
|
lastUpdate, err := time.Parse(time.RFC3339, updatedStr.String)
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, fmt.Errorf("%w: failed to parse last update time: %v", ErrSqliteFailure, err)
|
||||||
|
}
|
||||||
|
return lastUpdate, nil
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ package storage
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"sort"
|
"sort"
|
||||||
|
"time"
|
||||||
|
|
||||||
"go-mod.ewintr.nl/planner/item"
|
"go-mod.ewintr.nl/planner/item"
|
||||||
)
|
)
|
||||||
|
@ -13,11 +14,19 @@ 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,89 +1,12 @@
|
||||||
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 struct {
|
type Client interface {
|
||||||
baseURL string
|
Update(items []item.Item) error
|
||||||
apiKey string
|
Updated(ks []item.Kind, ts time.Time) ([]item.Item, error)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,89 @@
|
||||||
|
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
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
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
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
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