diff --git a/plan/command/add.go b/plan/command/add.go index 67a22cf..a91460e 100644 --- a/plan/command/add.go +++ b/plan/command/add.go @@ -44,14 +44,14 @@ var AddCmd = &cli.Command{ }, } -func NewAddCmd(_ storage.LocalIDRepo, eventRepo storage.EventRepo) *cli.Command { +func NewAddCmd(localRepo storage.LocalID, eventRepo storage.Event) *cli.Command { AddCmd.Action = func(cCtx *cli.Context) error { - return Add(cCtx.String("name"), cCtx.String("on"), cCtx.String("at"), cCtx.String("for"), eventRepo) + return Add(localRepo, eventRepo, cCtx.String("name"), cCtx.String("on"), cCtx.String("at"), cCtx.String("for")) } return AddCmd } -func Add(nameStr, onStr, atStr, frStr string, repo storage.EventRepo) error { +func Add(localIDRepo storage.LocalID, eventRepo storage.Event, nameStr, onStr, atStr, frStr string) error { if nameStr == "" { return fmt.Errorf("%w: name is required", ErrInvalidArg) } @@ -91,9 +91,17 @@ func Add(nameStr, onStr, atStr, frStr string, repo storage.EventRepo) error { } e.Duration = fr } - if err := repo.Store(e); err != nil { + if err := eventRepo.Store(e); err != nil { return fmt.Errorf("could not store event: %v", err) } + localID, err := localIDRepo.Next() + if err != nil { + return fmt.Errorf("could not create next local id: %v", err) + } + if err := localIDRepo.Store(e.ID, localID); err != nil { + return fmt.Errorf("could not store local id: %v", err) + } + return nil } diff --git a/plan/command/add_test.go b/plan/command/add_test.go index f1b16a5..8c9a426 100644 --- a/plan/command/add_test.go +++ b/plan/command/add_test.go @@ -7,7 +7,7 @@ import ( "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" + "go-mod.ewintr.nl/planner/plan/storage/memory" ) func TestAdd(t *testing.T) { @@ -104,21 +104,34 @@ func TestAdd(t *testing.T) { }, } { t.Run(tc.name, func(t *testing.T) { - mem := storage.NewMemory() - actErr := command.Add(tc.args["name"], tc.args["on"], tc.args["at"], tc.args["for"], mem) != nil + eventRepo := memory.NewEvent() + localRepo := memory.NewLocalID() + actErr := command.Add(localRepo, eventRepo, tc.args["name"], tc.args["on"], tc.args["at"], tc.args["for"]) != nil if tc.expErr != actErr { t.Errorf("exp %v, got %v", tc.expErr, actErr) } if tc.expErr { return } - actEvents, err := mem.FindAll() + actEvents, err := eventRepo.FindAll() if err != nil { t.Errorf("exp nil, got %v", err) } if len(actEvents) != 1 { t.Errorf("exp 1, got %d", len(actEvents)) } + + actLocalIDs, err := localRepo.FindAll() + if err != nil { + t.Errorf("exp nil, got %v", err) + } + if len(actLocalIDs) != 1 { + t.Errorf("exp 1, got %v", len(actLocalIDs)) + } + if _, ok := actLocalIDs[actEvents[0].ID]; !ok { + t.Errorf("exp true, got %v", ok) + } + if actEvents[0].ID == "" { t.Errorf("exp string not te be empty") } diff --git a/plan/command/list.go b/plan/command/list.go index 07b8d09..271780e 100644 --- a/plan/command/list.go +++ b/plan/command/list.go @@ -13,22 +13,21 @@ var ListCmd = &cli.Command{ Usage: "List everything", } -func NewListCmd(_ storage.LocalIDRepo, repo storage.EventRepo) *cli.Command { - ListCmd.Action = NewListAction(repo) +func NewListCmd(localRepo storage.LocalID, eventRepo storage.Event) *cli.Command { + ListCmd.Action = func(cCtx *cli.Context) error { + return List(localRepo, eventRepo) + } return ListCmd } -func NewListAction(repo storage.EventRepo) func(*cli.Context) error { - return func(cCtx *cli.Context) error { - all, err := repo.FindAll() - if err != nil { - return err - } - for _, e := range all { - fmt.Printf("%s\t%s\t%s\t%s\n", e.ID, e.Title, e.Start.Format(time.DateTime), e.Duration.String()) - } - - return nil +func List(localRepo storage.LocalID, eventRepo storage.Event) error { + all, err := eventRepo.FindAll() + if err != nil { + return err + } + for _, e := range all { + fmt.Printf("%s\t%s\t%s\t%s\n", e.ID, e.Title, e.Start.Format(time.DateTime), e.Duration.String()) } + return nil } diff --git a/plan/main.go b/plan/main.go index d93fa6c..4df6f3b 100644 --- a/plan/main.go +++ b/plan/main.go @@ -7,7 +7,7 @@ import ( "github.com/urfave/cli/v2" "go-mod.ewintr.nl/planner/plan/command" - "go-mod.ewintr.nl/planner/plan/storage" + "go-mod.ewintr.nl/planner/plan/storage/sqlite" "gopkg.in/yaml.v3" ) @@ -23,7 +23,7 @@ func main() { os.Exit(1) } - localIDRepo, eventRepo, err := storage.NewSqlites(conf.DBPath) + localIDRepo, eventRepo, err := sqlite.NewSqlites(conf.DBPath) if err != nil { fmt.Printf("could not open db file: %s\n", err) os.Exit(1) diff --git a/plan/storage/memory/event.go b/plan/storage/memory/event.go index 486b7f7..32214ed 100644 --- a/plan/storage/memory/event.go +++ b/plan/storage/memory/event.go @@ -8,18 +8,18 @@ import ( "go-mod.ewintr.nl/planner/item" ) -type MemoryEvent struct { +type Event struct { events map[string]item.Event mutex sync.RWMutex } -func NewMemoryEvent() *MemoryEvent { - return &MemoryEvent{ +func NewEvent() *Event { + return &Event{ events: make(map[string]item.Event), } } -func (r *MemoryEvent) Find(id string) (item.Event, error) { +func (r *Event) Find(id string) (item.Event, error) { r.mutex.RLock() defer r.mutex.RUnlock() @@ -30,7 +30,7 @@ func (r *MemoryEvent) Find(id string) (item.Event, error) { return event, nil } -func (r *MemoryEvent) FindAll() ([]item.Event, error) { +func (r *Event) FindAll() ([]item.Event, error) { r.mutex.RLock() defer r.mutex.RUnlock() @@ -45,7 +45,7 @@ func (r *MemoryEvent) FindAll() ([]item.Event, error) { return events, nil } -func (r *MemoryEvent) Store(e item.Event) error { +func (r *Event) Store(e item.Event) error { r.mutex.Lock() defer r.mutex.Unlock() @@ -54,7 +54,7 @@ func (r *MemoryEvent) Store(e item.Event) error { return nil } -func (r *MemoryEvent) Delete(id string) error { +func (r *Event) Delete(id string) error { r.mutex.Lock() defer r.mutex.Unlock() diff --git a/plan/storage/memory/event_test.go b/plan/storage/memory/event_test.go index b22dbfb..54645c3 100644 --- a/plan/storage/memory/event_test.go +++ b/plan/storage/memory/event_test.go @@ -7,10 +7,10 @@ import ( "go-mod.ewintr.nl/planner/item" ) -func TestMemory(t *testing.T) { +func TestEvent(t *testing.T) { t.Parallel() - mem := NewMemory() + mem := NewEvent() t.Log("empty") actEvents, actErr := mem.FindAll() diff --git a/plan/storage/memory/memory.go b/plan/storage/memory/localid.go similarity index 58% rename from plan/storage/memory/memory.go rename to plan/storage/memory/localid.go index 237284f..40024b1 100644 --- a/plan/storage/memory/memory.go +++ b/plan/storage/memory/localid.go @@ -3,34 +3,31 @@ package memory import ( "sync" - "github.com/google/uuid" "go-mod.ewintr.nl/planner/plan/storage" ) -type MemoryLocalID struct { +type LocalID struct { ids map[string]int mutex sync.RWMutex } -func NewMemoryLocalID() *MemoryLocalID { - return &MemoryLocalID{ +func NewLocalID() *LocalID { + return &LocalID{ ids: make(map[string]int), } } -func (ml *MemoryLocalID) FindAll() (map[string]int, error) { +func (ml *LocalID) FindAll() (map[string]int, error) { ml.mutex.RLock() defer ml.mutex.RUnlock() return ml.ids, nil } -func (ml *MemoryLocalID) Next() (string, int, error) { +func (ml *LocalID) Next() (int, error) { ml.mutex.RLock() defer ml.mutex.RUnlock() - id := uuid.New().String() - cur := make([]int, 0, len(ml.ids)) for _, i := range ml.ids { cur = append(cur, i) @@ -38,10 +35,10 @@ func (ml *MemoryLocalID) Next() (string, int, error) { localID := storage.NextLocalID(cur) - return id, localID, nil + return localID, nil } -func (ml *MemoryLocalID) Store(id string, localID int) error { +func (ml *LocalID) Store(id string, localID int) error { ml.mutex.Lock() defer ml.mutex.Unlock() @@ -50,12 +47,12 @@ func (ml *MemoryLocalID) Store(id string, localID int) error { return nil } -func (ml *MemoryLocalID) Delete(id string) error { +func (ml *LocalID) Delete(id string) error { ml.mutex.Lock() defer ml.mutex.Unlock() if _, ok := ml.ids[id]; !ok { - return ErrNotFound + return storage.ErrNotFound } delete(ml.ids, id) diff --git a/plan/storage/memory/localid_test.go b/plan/storage/memory/localid_test.go new file mode 100644 index 0000000..02d060e --- /dev/null +++ b/plan/storage/memory/localid_test.go @@ -0,0 +1,68 @@ +package memory_test + +import ( + "errors" + "testing" + + "github.com/google/go-cmp/cmp" + "go-mod.ewintr.nl/planner/plan/storage" + "go-mod.ewintr.nl/planner/plan/storage/memory" +) + +func TestLocalID(t *testing.T) { + t.Parallel() + + repo := memory.NewLocalID() + + t.Log("start empty") + actIDs, actErr := repo.FindAll() + if actErr != nil { + t.Errorf("exp nil, got %v", actErr) + } + if len(actIDs) != 0 { + t.Errorf("exp nil, got %v", actErr) + } + + t.Log("next id") + actNext, actErr := repo.Next() + if actErr != nil { + t.Errorf("exp nil, got %v", actErr) + } + if actNext != 1 { + t.Errorf("exp 1, got %v", actNext) + } + + t.Log("store") + if actErr = repo.Store("test", 1); actErr != nil { + t.Errorf("exp nil, got %v", actErr) + } + + actIDs, actErr = repo.FindAll() + if actErr != nil { + t.Errorf("exp nil, got %v", actErr) + } + expIDs := map[string]int{ + "test": 1, + } + if diff := cmp.Diff(expIDs, actIDs); diff != "" { + t.Errorf("(exp +, got -)\n%s", diff) + } + + t.Log("delete") + if actErr = repo.Delete("test"); actErr != nil { + t.Errorf("exp nil, got %v", actErr) + } + actIDs, actErr = repo.FindAll() + if actErr != nil { + t.Errorf("exp nil, got %v", actErr) + } + if len(actIDs) != 0 { + t.Errorf("exp 0, got %v", actErr) + } + + t.Log("delete non-existing") + actErr = repo.Delete("non-existing") + if !errors.Is(actErr, storage.ErrNotFound) { + t.Errorf("exp %v, got %v", storage.ErrNotFound, actErr) + } +} diff --git a/plan/storage/sqlite/event.go b/plan/storage/sqlite/event.go index afbca9b..6f553b2 100644 --- a/plan/storage/sqlite/event.go +++ b/plan/storage/sqlite/event.go @@ -6,6 +6,7 @@ import ( "time" "go-mod.ewintr.nl/planner/item" + "go-mod.ewintr.nl/planner/plan/storage" ) type SqliteEvent struct { @@ -59,7 +60,6 @@ FROM events`) if err != nil { return nil, fmt.Errorf("%w: %v", ErrSqliteFailure, err) } - result := make([]item.Event, 0) defer rows.Close() for rows.Next() { @@ -93,7 +93,7 @@ WHERE id = ?`, id) } if rowsAffected == 0 { - return fmt.Errorf("event not found: %s", id) + return storage.ErrNotFound } return nil diff --git a/plan/storage/sqlite/localid.go b/plan/storage/sqlite/localid.go new file mode 100644 index 0000000..1e213c3 --- /dev/null +++ b/plan/storage/sqlite/localid.go @@ -0,0 +1,77 @@ +package sqlite + +import ( + "database/sql" + "fmt" + + "go-mod.ewintr.nl/planner/plan/storage" +) + +type LocalID struct { + db *sql.DB +} + +func (l *LocalID) FindAll() (map[string]int, error) { + rows, err := l.db.Query(` +SELECT id, local_id +FROM localids +`) + if err != nil { + return nil, fmt.Errorf("%w: %v", ErrSqliteFailure, err) + } + result := make(map[string]int) + defer rows.Close() + for rows.Next() { + var id string + var localID int + if err := rows.Scan(&id, &localID); err != nil { + return nil, fmt.Errorf("%w: %v", ErrSqliteFailure, err) + } + result[id] = localID + } + + return result, nil +} + +func (l *LocalID) Next() (int, error) { + idMap, err := l.FindAll() + if err != nil { + return 0, err + } + cur := make([]int, 0, len(idMap)) + for _, localID := range idMap { + cur = append(cur, localID) + } + + return storage.NextLocalID(cur), nil +} + +func (l *LocalID) Store(id string, localID int) error { + if _, err := l.db.Exec(` +INSERT INTO localids +(id, local_id) +VALUES +(? ,?)`, id, localID); err != nil { + return fmt.Errorf("%w: %v", ErrSqliteFailure, err) + } + + return nil +} + +func (l *LocalID) Delete(id string) error { + result, err := l.db.Exec(` +DELETE FROM localids +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 +} diff --git a/plan/storage/sqlite/sqlite.go b/plan/storage/sqlite/sqlite.go index c240f26..90f84f3 100644 --- a/plan/storage/sqlite/sqlite.go +++ b/plan/storage/sqlite/sqlite.go @@ -27,33 +27,29 @@ var ( ErrSqliteFailure = errors.New("sqlite returned an error") ) -type SqliteLocal struct { - db *sql.DB -} - -func NewSqlites(dbPath string) (*SqliteLocal, *SqliteEvent, error) { +func NewSqlites(dbPath string) (*LocalID, *SqliteEvent, error) { db, err := sql.Open("sqlite", dbPath) if err != nil { return nil, nil, fmt.Errorf("%w: %v", ErrInvalidConfiguration, err) } - sl := &SqliteLocal{ + sl := &LocalID{ db: db, } se := &SqliteEvent{ db: db, } - if err := sl.migrate(migrations); err != nil { + if err := migrate(db, migrations); err != nil { return nil, nil, err } return sl, se, nil } -func (s *SqliteLocal) migrate(wanted []string) error { +func migrate(db *sql.DB, wanted []string) error { // admin table - if _, err := s.db.Exec(` + if _, err := db.Exec(` CREATE TABLE IF NOT EXISTS migration ("id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "query" TEXT) `); err != nil { @@ -61,7 +57,7 @@ CREATE TABLE IF NOT EXISTS migration } // find existing - rows, err := s.db.Query(`SELECT query FROM migration ORDER BY id`) + rows, err := db.Query(`SELECT query FROM migration ORDER BY id`) if err != nil { return fmt.Errorf("%w: %v", ErrSqliteFailure, err) } @@ -84,12 +80,12 @@ CREATE TABLE IF NOT EXISTS migration // execute missing for _, query := range missing { - if _, err := s.db.Exec(string(query)); err != nil { + if _, err := db.Exec(string(query)); err != nil { return fmt.Errorf("%w: %v", ErrSqliteFailure, err) } // register - if _, err := s.db.Exec(` + if _, err := db.Exec(` INSERT INTO migration (query) VALUES (?) `, query); err != nil { diff --git a/plan/storage/storage.go b/plan/storage/storage.go index b7a9476..e20c24b 100644 --- a/plan/storage/storage.go +++ b/plan/storage/storage.go @@ -11,14 +11,14 @@ var ( ErrNotFound = errors.New("not found") ) -type LocalIDRepo interface { +type LocalID interface { FindAll() (map[string]int, error) - Next() (string, int, error) + Next() (int, error) Store(id string, localID int) error Delete(id string) error } -type EventRepo interface { +type Event interface { Store(event item.Event) error Find(id string) (item.Event, error) FindAll() ([]item.Event, error)