own argument parser and flags
This commit is contained in:
parent
6fe8561a6b
commit
5b5ae5727d
|
@ -86,3 +86,17 @@ func (e Event) Item() (Item, error) {
|
|||
Body: string(body),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (e Event) Valid() bool {
|
||||
if e.Title == "" {
|
||||
return false
|
||||
}
|
||||
if e.Start.IsZero() || e.Start.Year() < 2024 {
|
||||
return false
|
||||
}
|
||||
if e.Duration.Seconds() < 1 {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -132,3 +132,72 @@ func TestEventItem(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEventValidate(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
|
||||
event item.Event
|
||||
exp bool
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
},
|
||||
{
|
||||
name: "missing title",
|
||||
event: item.Event{
|
||||
ID: "a",
|
||||
EventBody: item.EventBody{
|
||||
Start: time.Date(2024, 9, 20, 8, 0, 0, 0, time.UTC),
|
||||
Duration: oneHour,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no date",
|
||||
event: item.Event{
|
||||
ID: "a",
|
||||
EventBody: item.EventBody{
|
||||
Title: "title",
|
||||
Start: time.Date(0, 0, 0, 8, 0, 0, 0, time.UTC),
|
||||
Duration: oneHour,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no duration",
|
||||
event: item.Event{
|
||||
ID: "a",
|
||||
EventBody: item.EventBody{
|
||||
Title: "title",
|
||||
Start: time.Date(2024, 9, 20, 8, 0, 0, 0, time.UTC),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid",
|
||||
event: item.Event{
|
||||
ID: "a",
|
||||
EventBody: item.EventBody{
|
||||
Title: "title",
|
||||
Start: time.Date(2024, 9, 20, 8, 0, 0, 0, time.UTC),
|
||||
Duration: oneHour,
|
||||
},
|
||||
},
|
||||
exp: true,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if act := tc.event.Valid(); tc.exp != act {
|
||||
t.Errorf("exp %v, got %v", tc.exp, act)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,105 +1,107 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/urfave/cli/v2"
|
||||
"go-mod.ewintr.nl/planner/item"
|
||||
"go-mod.ewintr.nl/planner/plan/storage"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidArg = errors.New("invalid argument")
|
||||
)
|
||||
|
||||
var AddCmd = &cli.Command{
|
||||
Name: "add",
|
||||
Usage: "Add a new event",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "name",
|
||||
Aliases: []string{"n"},
|
||||
Usage: "The event that will happen",
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "on",
|
||||
Aliases: []string{"o"},
|
||||
Usage: "The date, in YYYY-MM-DD format",
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "at",
|
||||
Aliases: []string{"a"},
|
||||
Usage: "The time, in HH:MM format. If omitted, the event will last the whole day",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "for",
|
||||
Aliases: []string{"f"},
|
||||
Usage: "The duration, in show format (e.g. 1h30m)",
|
||||
},
|
||||
},
|
||||
type Add struct {
|
||||
localIDRepo storage.LocalID
|
||||
eventRepo storage.Event
|
||||
syncRepo storage.Sync
|
||||
argSet *ArgSet
|
||||
}
|
||||
|
||||
func NewAddCmd(localRepo storage.LocalID, eventRepo storage.Event, syncRepo storage.Sync) *cli.Command {
|
||||
AddCmd.Action = func(cCtx *cli.Context) error {
|
||||
return Add(localRepo, eventRepo, syncRepo, cCtx.String("name"), cCtx.String("on"), cCtx.String("at"), cCtx.String("for"))
|
||||
func NewAdd(localRepo storage.LocalID, eventRepo storage.Event, syncRepo storage.Sync) Command {
|
||||
return &Add{
|
||||
localIDRepo: localRepo,
|
||||
eventRepo: eventRepo,
|
||||
syncRepo: syncRepo,
|
||||
argSet: &ArgSet{
|
||||
Flags: map[string]Flag{
|
||||
FlagOn: &FlagDate{},
|
||||
FlagAt: &FlagTime{},
|
||||
FlagFor: &FlagDuration{},
|
||||
},
|
||||
},
|
||||
}
|
||||
return AddCmd
|
||||
}
|
||||
|
||||
func Add(localIDRepo storage.LocalID, eventRepo storage.Event, syncRepo storage.Sync, nameStr, onStr, atStr, frStr string) error {
|
||||
if nameStr == "" {
|
||||
return fmt.Errorf("%w: name is required", ErrInvalidArg)
|
||||
func (add *Add) Execute(main []string, flags map[string]string) error {
|
||||
if len(main) == 0 || main[0] != "add" {
|
||||
return ErrWrongCommand
|
||||
}
|
||||
if onStr == "" {
|
||||
as := add.argSet
|
||||
if len(main) > 1 {
|
||||
as.Main = strings.Join(main[1:], " ")
|
||||
}
|
||||
for k := range as.Flags {
|
||||
v, ok := flags[k]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if err := as.Set(k, v); err != nil {
|
||||
return fmt.Errorf("could not set %s: %v", k, err)
|
||||
}
|
||||
}
|
||||
if as.Main == "" {
|
||||
return fmt.Errorf("%w: title is required", ErrInvalidArg)
|
||||
}
|
||||
if !as.IsSet(FlagOn) {
|
||||
return fmt.Errorf("%w: date is required", ErrInvalidArg)
|
||||
}
|
||||
if atStr == "" && frStr != "" {
|
||||
if !as.IsSet(FlagAt) && as.IsSet(FlagFor) {
|
||||
return fmt.Errorf("%w: can not have duration without start time", ErrInvalidArg)
|
||||
}
|
||||
if atStr == "" && frStr == "" {
|
||||
frStr = "24h"
|
||||
if as.IsSet(FlagAt) && !as.IsSet(FlagFor) {
|
||||
if err := as.Flags[FlagFor].Set("1h"); err != nil {
|
||||
return fmt.Errorf("could not set duration to one hour")
|
||||
}
|
||||
}
|
||||
if !as.IsSet(FlagAt) && !as.IsSet(FlagFor) {
|
||||
if err := as.Flags[FlagFor].Set("24h"); err != nil {
|
||||
return fmt.Errorf("could not set duration to 24 hours")
|
||||
}
|
||||
}
|
||||
|
||||
startFormat := "2006-01-02"
|
||||
startStr := onStr
|
||||
if atStr != "" {
|
||||
startFormat = fmt.Sprintf("%s 15:04", startFormat)
|
||||
startStr = fmt.Sprintf("%s %s", startStr, atStr)
|
||||
}
|
||||
start, err := time.Parse(startFormat, startStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: could not parse start time and date: %v", ErrInvalidArg, err)
|
||||
return add.do()
|
||||
}
|
||||
|
||||
func (add *Add) do() error {
|
||||
as := add.argSet
|
||||
start := as.GetTime(FlagOn)
|
||||
if as.IsSet(FlagAt) {
|
||||
at := as.GetTime(FlagAt)
|
||||
h := time.Duration(at.Hour()) * time.Hour
|
||||
m := time.Duration(at.Minute()) * time.Minute
|
||||
start = start.Add(h).Add(m)
|
||||
}
|
||||
|
||||
e := item.Event{
|
||||
ID: uuid.New().String(),
|
||||
EventBody: item.EventBody{
|
||||
Title: nameStr,
|
||||
Title: as.Main,
|
||||
Start: start,
|
||||
},
|
||||
}
|
||||
|
||||
if frStr != "" {
|
||||
fr, err := time.ParseDuration(frStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: could not parse duration: %s", ErrInvalidArg, err)
|
||||
if as.IsSet(FlagFor) {
|
||||
e.Duration = as.GetDuration(FlagFor)
|
||||
}
|
||||
e.Duration = fr
|
||||
}
|
||||
if err := eventRepo.Store(e); err != nil {
|
||||
if err := add.eventRepo.Store(e); err != nil {
|
||||
return fmt.Errorf("could not store event: %v", err)
|
||||
}
|
||||
|
||||
localID, err := localIDRepo.Next()
|
||||
localID, err := add.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 {
|
||||
if err := add.localIDRepo.Store(e.ID, localID); err != nil {
|
||||
return fmt.Errorf("could not store local id: %v", err)
|
||||
}
|
||||
|
||||
|
@ -107,7 +109,7 @@ func Add(localIDRepo storage.LocalID, eventRepo storage.Event, syncRepo storage.
|
|||
if err != nil {
|
||||
return fmt.Errorf("could not convert event to sync item: %v", err)
|
||||
}
|
||||
if err := syncRepo.Store(it); err != nil {
|
||||
if err := add.syncRepo.Store(it); err != nil {
|
||||
return fmt.Errorf("could not store sync item: %v", err)
|
||||
}
|
||||
|
||||
|
|
|
@ -13,107 +13,109 @@ import (
|
|||
func TestAdd(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
oneHour, err := time.ParseDuration("1h")
|
||||
if err != nil {
|
||||
t.Errorf("exp nil, got %v", err)
|
||||
}
|
||||
oneDay, err := time.ParseDuration("24h")
|
||||
if err != nil {
|
||||
t.Errorf("exp nil, got %v", err)
|
||||
}
|
||||
aDateStr := "2024-11-02"
|
||||
aDate := time.Date(2024, 11, 2, 0, 0, 0, 0, time.UTC)
|
||||
aTimeStr := "12:00"
|
||||
aDay := time.Duration(24) * time.Hour
|
||||
anHourStr := "1h"
|
||||
anHour := time.Hour
|
||||
aDateAndTime := time.Date(2024, 11, 2, 12, 0, 0, 0, time.UTC)
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
args map[string]string
|
||||
expEvent item.Event
|
||||
main []string
|
||||
flags map[string]string
|
||||
expErr bool
|
||||
expEvent item.Event
|
||||
}{
|
||||
{
|
||||
name: "no name",
|
||||
args: map[string]string{
|
||||
"on": "2024-10-01",
|
||||
"at": "9:00",
|
||||
"for": "1h",
|
||||
name: "empty",
|
||||
expErr: true,
|
||||
},
|
||||
{
|
||||
name: "title missing",
|
||||
main: []string{"add"},
|
||||
flags: map[string]string{
|
||||
command.FlagOn: aDateStr,
|
||||
},
|
||||
expErr: true,
|
||||
},
|
||||
{
|
||||
name: "no date",
|
||||
args: map[string]string{
|
||||
"name": "event",
|
||||
"at": "9:00",
|
||||
"for": "1h",
|
||||
},
|
||||
name: "date missing",
|
||||
main: []string{"add", "some", "title"},
|
||||
expErr: true,
|
||||
},
|
||||
{
|
||||
name: "duration, but no time",
|
||||
args: map[string]string{
|
||||
"name": "event",
|
||||
"on": "2024-10-01",
|
||||
"for": "1h",
|
||||
name: "only date",
|
||||
main: []string{"add", "title"},
|
||||
flags: map[string]string{
|
||||
command.FlagOn: aDateStr,
|
||||
},
|
||||
expEvent: item.Event{
|
||||
ID: "title",
|
||||
EventBody: item.EventBody{
|
||||
Title: "title",
|
||||
Start: aDate,
|
||||
Duration: aDay,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "date and time",
|
||||
main: []string{"add", "title"},
|
||||
flags: map[string]string{
|
||||
command.FlagOn: aDateStr,
|
||||
command.FlagAt: aTimeStr,
|
||||
},
|
||||
expEvent: item.Event{
|
||||
ID: "title",
|
||||
EventBody: item.EventBody{
|
||||
Title: "title",
|
||||
Start: aDateAndTime,
|
||||
Duration: anHour,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "date, time and duration",
|
||||
main: []string{"add", "title"},
|
||||
flags: map[string]string{
|
||||
command.FlagOn: aDateStr,
|
||||
command.FlagAt: aTimeStr,
|
||||
command.FlagFor: anHourStr,
|
||||
},
|
||||
expEvent: item.Event{
|
||||
ID: "title",
|
||||
EventBody: item.EventBody{
|
||||
Title: "title",
|
||||
Start: aDateAndTime,
|
||||
Duration: anHour,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "date and duration",
|
||||
main: []string{"add", "title"},
|
||||
flags: map[string]string{
|
||||
command.FlagOn: aDateStr,
|
||||
command.FlagFor: anHourStr,
|
||||
},
|
||||
expErr: true,
|
||||
},
|
||||
{
|
||||
name: "time, but no duration",
|
||||
args: map[string]string{
|
||||
"name": "event",
|
||||
"on": "2024-10-01",
|
||||
"at": "9:00",
|
||||
},
|
||||
expEvent: item.Event{
|
||||
ID: "a",
|
||||
EventBody: item.EventBody{
|
||||
Title: "event",
|
||||
Start: time.Date(2024, 10, 1, 9, 0, 0, 0, time.UTC),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no time, no duration",
|
||||
args: map[string]string{
|
||||
"name": "event",
|
||||
"on": "2024-10-01",
|
||||
},
|
||||
expEvent: item.Event{
|
||||
ID: "a",
|
||||
EventBody: item.EventBody{
|
||||
Title: "event",
|
||||
Start: time.Date(2024, 10, 1, 0, 0, 0, 0, time.UTC),
|
||||
Duration: oneDay,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "full",
|
||||
args: map[string]string{
|
||||
"name": "event",
|
||||
"on": "2024-10-01",
|
||||
"at": "9:00",
|
||||
"for": "1h",
|
||||
},
|
||||
expEvent: item.Event{
|
||||
ID: "a",
|
||||
EventBody: item.EventBody{
|
||||
Title: "event",
|
||||
Start: time.Date(2024, 10, 1, 9, 0, 0, 0, time.UTC),
|
||||
Duration: oneHour,
|
||||
},
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
eventRepo := memory.NewEvent()
|
||||
localRepo := memory.NewLocalID()
|
||||
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 {
|
||||
t.Errorf("exp %v, got %v", tc.expErr, actErr)
|
||||
cmd := command.NewAdd(localRepo, eventRepo, syncRepo)
|
||||
actParseErr := cmd.Execute(tc.main, tc.flags) != nil
|
||||
if tc.expErr != actParseErr {
|
||||
t.Errorf("exp %v, got %v", tc.expErr, actParseErr)
|
||||
}
|
||||
if tc.expErr {
|
||||
return
|
||||
}
|
||||
|
||||
actEvents, err := eventRepo.FindAll()
|
||||
if err != nil {
|
||||
t.Errorf("exp nil, got %v", err)
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ArgSet struct {
|
||||
Main string
|
||||
Flags map[string]Flag
|
||||
}
|
||||
|
||||
func (as *ArgSet) Set(name, val string) error {
|
||||
f, ok := as.Flags[name]
|
||||
if !ok {
|
||||
return fmt.Errorf("unknown flag %s", name)
|
||||
}
|
||||
return f.Set(val)
|
||||
}
|
||||
|
||||
func (as *ArgSet) IsSet(name string) bool {
|
||||
f, ok := as.Flags[name]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return f.IsSet()
|
||||
}
|
||||
|
||||
func (as *ArgSet) GetString(name string) string {
|
||||
flag, ok := as.Flags[name]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
val, ok := flag.Get().(string)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
func (as *ArgSet) GetTime(name string) time.Time {
|
||||
flag, ok := as.Flags[name]
|
||||
if !ok {
|
||||
return time.Time{}
|
||||
}
|
||||
val, ok := flag.Get().(time.Time)
|
||||
if !ok {
|
||||
return time.Time{}
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
func (as *ArgSet) GetDuration(name string) time.Duration {
|
||||
flag, ok := as.Flags[name]
|
||||
if !ok {
|
||||
return time.Duration(0)
|
||||
}
|
||||
val, ok := flag.Get().(time.Duration)
|
||||
if !ok {
|
||||
return time.Duration(0)
|
||||
}
|
||||
return val
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
package command_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"go-mod.ewintr.nl/planner/plan/command"
|
||||
)
|
||||
|
||||
func TestArgSet(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
flags map[string]command.Flag
|
||||
flagName string
|
||||
setValue string
|
||||
exp interface{}
|
||||
expErr bool
|
||||
}{
|
||||
{
|
||||
name: "string flag success",
|
||||
flags: map[string]command.Flag{
|
||||
"title": &command.FlagString{Name: "title"},
|
||||
},
|
||||
flagName: "title",
|
||||
setValue: "test title",
|
||||
exp: "test title",
|
||||
},
|
||||
{
|
||||
name: "date flag success",
|
||||
flags: map[string]command.Flag{
|
||||
"date": &command.FlagDate{Name: "date"},
|
||||
},
|
||||
flagName: "date",
|
||||
setValue: "2024-01-02",
|
||||
exp: time.Date(2024, 1, 2, 0, 0, 0, 0, time.UTC),
|
||||
},
|
||||
{
|
||||
name: "time flag success",
|
||||
flags: map[string]command.Flag{
|
||||
"time": &command.FlagTime{Name: "time"},
|
||||
},
|
||||
flagName: "time",
|
||||
setValue: "15:04",
|
||||
exp: time.Date(0, 1, 1, 15, 4, 0, 0, time.UTC),
|
||||
},
|
||||
{
|
||||
name: "duration flag success",
|
||||
flags: map[string]command.Flag{
|
||||
"duration": &command.FlagDuration{Name: "duration"},
|
||||
},
|
||||
flagName: "duration",
|
||||
setValue: "2h30m",
|
||||
exp: 2*time.Hour + 30*time.Minute,
|
||||
},
|
||||
{
|
||||
name: "unknown flag error",
|
||||
flags: map[string]command.Flag{},
|
||||
flagName: "unknown",
|
||||
setValue: "value",
|
||||
expErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid date format error",
|
||||
flags: map[string]command.Flag{
|
||||
"date": &command.FlagDate{Name: "date"},
|
||||
},
|
||||
flagName: "date",
|
||||
setValue: "invalid",
|
||||
expErr: true,
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
as := &command.ArgSet{
|
||||
Main: "test",
|
||||
Flags: tt.flags,
|
||||
}
|
||||
|
||||
err := as.Set(tt.flagName, tt.setValue)
|
||||
if (err != nil) != tt.expErr {
|
||||
t.Errorf("ArgSet.Set() error = %v, expErr %v", err, tt.expErr)
|
||||
return
|
||||
}
|
||||
|
||||
if tt.expErr {
|
||||
return
|
||||
}
|
||||
|
||||
// Verify IsSet() returns true after setting
|
||||
if !as.IsSet(tt.flagName) {
|
||||
t.Errorf("ArgSet.IsSet() = false, want true for flag %s", tt.flagName)
|
||||
}
|
||||
|
||||
// Verify the value was set correctly based on flag type
|
||||
switch v := tt.exp.(type) {
|
||||
case string:
|
||||
if got := as.GetString(tt.flagName); got != v {
|
||||
t.Errorf("ArgSet.GetString() = %v, want %v", got, v)
|
||||
}
|
||||
case time.Time:
|
||||
if got := as.GetTime(tt.flagName); !got.Equal(v) {
|
||||
t.Errorf("ArgSet.GetTime() = %v, want %v", got, v)
|
||||
}
|
||||
case time.Duration:
|
||||
if got := as.GetDuration(tt.flagName); got != v {
|
||||
t.Errorf("ArgSet.GetDuration() = %v, want %v", got, v)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
FlagTitle = "title"
|
||||
FlagOn = "on"
|
||||
FlagAt = "at"
|
||||
FlagFor = "for"
|
||||
)
|
||||
|
||||
type Command interface {
|
||||
Execute([]string, map[string]string) error
|
||||
}
|
||||
|
||||
type CLI struct {
|
||||
Commands []Command
|
||||
}
|
||||
|
||||
func (cli *CLI) Run(args []string) error {
|
||||
main, flags, err := ParseFlags(args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, c := range cli.Commands {
|
||||
err := c.Execute(main, flags)
|
||||
switch {
|
||||
case errors.Is(err, ErrWrongCommand):
|
||||
continue
|
||||
case err != nil:
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("could not find matching command")
|
||||
}
|
||||
|
||||
func ParseFlags(args []string) ([]string, map[string]string, error) {
|
||||
flags := make(map[string]string)
|
||||
main := make([]string, 0)
|
||||
var inMain bool
|
||||
for i := 0; i < len(args); i++ {
|
||||
if strings.HasPrefix(args[i], "-") {
|
||||
inMain = false
|
||||
if i+1 >= len(args) {
|
||||
return nil, nil, fmt.Errorf("flag wihout value")
|
||||
}
|
||||
flags[strings.TrimPrefix(args[i], "-")] = args[i+1]
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
if !inMain && len(main) > 0 {
|
||||
return nil, nil, fmt.Errorf("two mains")
|
||||
}
|
||||
inMain = true
|
||||
main = append(main, args[i])
|
||||
}
|
||||
|
||||
return main, flags, nil
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
package command_test
|
||||
|
||||
// func TestArgSet(t *testing.T) {
|
||||
// t.Parallel()
|
||||
|
||||
// as := command.ArgSet{
|
||||
// Main: "main",
|
||||
// Flags: map[string]string{
|
||||
// "name 1": "value 1",
|
||||
// "name 2": "value 2",
|
||||
// "name 3": "value 3",
|
||||
// },
|
||||
// }
|
||||
|
||||
// t.Run("hasflag", func(t *testing.T) {
|
||||
// t.Run("true", func(t *testing.T) {
|
||||
// if has := as.HasFlag("name 1"); !has {
|
||||
// t.Errorf("exp true, got %v", has)
|
||||
// }
|
||||
// })
|
||||
// t.Run("false", func(t *testing.T) {
|
||||
// if has := as.HasFlag("unknown"); has {
|
||||
// t.Errorf("exp false, got %v", has)
|
||||
// }
|
||||
// })
|
||||
// })
|
||||
|
||||
// t.Run("flag", func(t *testing.T) {
|
||||
// t.Run("known", func(t *testing.T) {
|
||||
// if val := as.Flag("name 1"); val != "value 1" {
|
||||
// t.Errorf("exp value 1, got %v", val)
|
||||
// }
|
||||
// })
|
||||
// t.Run("unknown", func(t *testing.T) {
|
||||
// if val := as.Flag("unknown"); val != "" {
|
||||
// t.Errorf(`exp "", got %v`, val)
|
||||
// }
|
||||
// })
|
||||
// })
|
||||
|
||||
// t.Run("setflag", func(t *testing.T) {
|
||||
// exp := "new value"
|
||||
// as.SetFlag("new name", exp)
|
||||
// if act := as.Flag("new name"); exp != act {
|
||||
// t.Errorf("exp %v, got %v", exp, act)
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
|
||||
// func TestParseArgs(t *testing.T) {
|
||||
// t.Parallel()
|
||||
|
||||
// for _, tc := range []struct {
|
||||
// name string
|
||||
// args []string
|
||||
// expAS *command.ArgSet
|
||||
// expErr bool
|
||||
// }{
|
||||
// {
|
||||
// name: "empty",
|
||||
// expAS: &command.ArgSet{
|
||||
// Flags: map[string]string{},
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// name: "just main",
|
||||
// args: []string{"one", "two three", "four"},
|
||||
// expAS: &command.ArgSet{
|
||||
// Main: "one two three four",
|
||||
// Flags: map[string]string{},
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// name: "with flags",
|
||||
// args: []string{"-flag1", "value1", "one", "two", "-flag2", "value2", "-flag3", "value3"},
|
||||
// expAS: &command.ArgSet{
|
||||
// Main: "one two",
|
||||
// Flags: map[string]string{
|
||||
// "flag1": "value1",
|
||||
// "flag2": "value2",
|
||||
// "flag3": "value3",
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// name: "flag without value",
|
||||
// args: []string{"one", "two", "-flag1"},
|
||||
// expErr: true,
|
||||
// },
|
||||
// {
|
||||
// name: "split main",
|
||||
// args: []string{"one", "-flag1", "value1", "two"},
|
||||
// expErr: true,
|
||||
// },
|
||||
// } {
|
||||
// t.Run(tc.name, func(t *testing.T) {
|
||||
// actAS, actErr := command.ParseArgs(tc.args)
|
||||
// if tc.expErr != (actErr != nil) {
|
||||
// t.Errorf("exp %v, got %v", tc.expErr, actErr)
|
||||
// }
|
||||
// if tc.expErr {
|
||||
// return
|
||||
// }
|
||||
// if diff := cmp.Diff(tc.expAS, actAS); diff != "" {
|
||||
// t.Errorf("(exp +, got -)\n%s", diff)
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
// }
|
|
@ -2,39 +2,47 @@ package command
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
"go-mod.ewintr.nl/planner/plan/storage"
|
||||
)
|
||||
|
||||
var DeleteCmd = &cli.Command{
|
||||
Name: "delete",
|
||||
Usage: "Delete an event",
|
||||
Flags: []cli.Flag{
|
||||
&cli.IntFlag{
|
||||
Name: "localID",
|
||||
Aliases: []string{"l"},
|
||||
Usage: "The local id of the event",
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
type Delete struct {
|
||||
localIDRepo storage.LocalID
|
||||
eventRepo storage.Event
|
||||
syncRepo storage.Sync
|
||||
localID int
|
||||
}
|
||||
|
||||
func NewDeleteCmd(localRepo storage.LocalID, eventRepo storage.Event, syncRepo storage.Sync) *cli.Command {
|
||||
DeleteCmd.Action = func(cCtx *cli.Context) error {
|
||||
return Delete(localRepo, eventRepo, syncRepo, cCtx.Int("localID"))
|
||||
func NewDelete(localIDRepo storage.LocalID, eventRepo storage.Event, syncRepo storage.Sync) Command {
|
||||
return &Delete{
|
||||
localIDRepo: localIDRepo,
|
||||
eventRepo: eventRepo,
|
||||
syncRepo: syncRepo,
|
||||
}
|
||||
return DeleteCmd
|
||||
}
|
||||
|
||||
func Delete(localRepo storage.LocalID, eventRepo storage.Event, syncRepo storage.Sync, localID int) error {
|
||||
func (del *Delete) Execute(main []string, flags map[string]string) error {
|
||||
if len(main) < 2 || main[0] != "delete" {
|
||||
return ErrWrongCommand
|
||||
}
|
||||
localID, err := strconv.Atoi(main[1])
|
||||
if err != nil {
|
||||
return fmt.Errorf("not a local id: %v", main[1])
|
||||
}
|
||||
del.localID = localID
|
||||
|
||||
return del.do()
|
||||
}
|
||||
|
||||
func (del *Delete) do() error {
|
||||
var id string
|
||||
idMap, err := localRepo.FindAll()
|
||||
idMap, err := del.localIDRepo.FindAll()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not get local ids: %v", err)
|
||||
}
|
||||
for eid, lid := range idMap {
|
||||
if localID == lid {
|
||||
if del.localID == lid {
|
||||
id = eid
|
||||
}
|
||||
}
|
||||
|
@ -42,11 +50,7 @@ func Delete(localRepo storage.LocalID, eventRepo storage.Event, syncRepo storage
|
|||
return fmt.Errorf("could not find local id")
|
||||
}
|
||||
|
||||
if err := eventRepo.Delete(id); err != nil {
|
||||
return fmt.Errorf("could not delete event: %v", err)
|
||||
}
|
||||
|
||||
e, err := eventRepo.Find(id)
|
||||
e, err := del.eventRepo.Find(id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not get event: %v", err)
|
||||
}
|
||||
|
@ -55,8 +59,18 @@ func Delete(localRepo storage.LocalID, eventRepo storage.Event, syncRepo storage
|
|||
if err != nil {
|
||||
return fmt.Errorf("could not convert event to sync item: %v", err)
|
||||
}
|
||||
if err := syncRepo.Store(it); err != nil {
|
||||
it.Deleted = true
|
||||
if err := del.syncRepo.Store(it); err != nil {
|
||||
return fmt.Errorf("could not store sync item: %v", err)
|
||||
}
|
||||
|
||||
if err := del.localIDRepo.Delete(id); err != nil {
|
||||
return fmt.Errorf("could not delete local id: %v", err)
|
||||
}
|
||||
|
||||
if err := del.eventRepo.Delete(id); err != nil {
|
||||
return fmt.Errorf("could not delete event: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -24,14 +24,24 @@ func TestDelete(t *testing.T) {
|
|||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
localID int
|
||||
main []string
|
||||
flags map[string]string
|
||||
expErr bool
|
||||
}{
|
||||
{
|
||||
name: "not found",
|
||||
localID: 5,
|
||||
name: "invalid",
|
||||
main: []string{"update"},
|
||||
expErr: true,
|
||||
},
|
||||
{
|
||||
name: "not found",
|
||||
main: []string{"delete", "5"},
|
||||
expErr: true,
|
||||
},
|
||||
{
|
||||
name: "valid",
|
||||
main: []string{"delete", "1"},
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
eventRepo := memory.NewEvent()
|
||||
|
@ -44,7 +54,9 @@ func TestDelete(t *testing.T) {
|
|||
t.Errorf("exp nil, got %v", err)
|
||||
}
|
||||
|
||||
actErr := command.Delete(localRepo, eventRepo, syncRepo, tc.localID) != nil
|
||||
cmd := command.NewDelete(localRepo, eventRepo, syncRepo)
|
||||
|
||||
actErr := cmd.Execute(tc.main, tc.flags) != nil
|
||||
if tc.expErr != actErr {
|
||||
t.Errorf("exp %v, got %v", tc.expErr, actErr)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
DateFormat = "2006-01-02"
|
||||
TimeFormat = "15:04"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrWrongCommand = errors.New("wrong command")
|
||||
ErrInvalidArg = errors.New("invalid argument")
|
||||
)
|
||||
|
||||
type Flag interface {
|
||||
Set(val string) error
|
||||
IsSet() bool
|
||||
Get() any
|
||||
}
|
||||
|
||||
type FlagString struct {
|
||||
Name string
|
||||
Value string
|
||||
}
|
||||
|
||||
func (fs *FlagString) Set(val string) error {
|
||||
fs.Value = val
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fs *FlagString) IsSet() bool {
|
||||
return fs.Value != ""
|
||||
}
|
||||
|
||||
func (fs *FlagString) Get() any {
|
||||
return fs.Value
|
||||
}
|
||||
|
||||
type FlagDate struct {
|
||||
Name string
|
||||
Value time.Time
|
||||
}
|
||||
|
||||
func (ft *FlagDate) Set(val string) error {
|
||||
d, err := time.Parse(DateFormat, val)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse date: %v", d)
|
||||
}
|
||||
ft.Value = d
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ft *FlagDate) IsSet() bool {
|
||||
return !ft.Value.IsZero()
|
||||
}
|
||||
|
||||
func (fs *FlagDate) Get() any {
|
||||
return fs.Value
|
||||
}
|
||||
|
||||
type FlagTime struct {
|
||||
Name string
|
||||
Value time.Time
|
||||
}
|
||||
|
||||
func (ft *FlagTime) Set(val string) error {
|
||||
d, err := time.Parse(TimeFormat, val)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse date: %v", d)
|
||||
}
|
||||
ft.Value = d
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fd *FlagTime) IsSet() bool {
|
||||
return !fd.Value.IsZero()
|
||||
}
|
||||
|
||||
func (fs *FlagTime) Get() any {
|
||||
return fs.Value
|
||||
}
|
||||
|
||||
type FlagDuration struct {
|
||||
Name string
|
||||
Value time.Duration
|
||||
}
|
||||
|
||||
func (fd *FlagDuration) Set(val string) error {
|
||||
dur, err := time.ParseDuration(val)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse duration: %v", err)
|
||||
}
|
||||
fd.Value = dur
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fd *FlagDuration) IsSet() bool {
|
||||
return fd.Value.String() != "0s"
|
||||
}
|
||||
|
||||
func (fs *FlagDuration) Get() any {
|
||||
return fs.Value
|
||||
}
|
|
@ -0,0 +1,115 @@
|
|||
package command_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"go-mod.ewintr.nl/planner/plan/command"
|
||||
)
|
||||
|
||||
func TestFlagString(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
valid := "test"
|
||||
f := command.FlagString{}
|
||||
if f.IsSet() {
|
||||
t.Errorf("exp false, got true")
|
||||
}
|
||||
|
||||
if err := f.Set(valid); err != nil {
|
||||
t.Errorf("exp nil, got %v", err)
|
||||
}
|
||||
|
||||
if !f.IsSet() {
|
||||
t.Errorf("exp true, got false")
|
||||
}
|
||||
|
||||
act, ok := f.Get().(string)
|
||||
if !ok {
|
||||
t.Errorf("exp true, got false")
|
||||
}
|
||||
if act != valid {
|
||||
t.Errorf("exp %v, got %v", valid, act)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlagDate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
valid := time.Date(2024, 11, 20, 0, 0, 0, 0, time.UTC)
|
||||
validStr := "2024-11-20"
|
||||
f := command.FlagDate{}
|
||||
if f.IsSet() {
|
||||
t.Errorf("exp false, got true")
|
||||
}
|
||||
|
||||
if err := f.Set(validStr); err != nil {
|
||||
t.Errorf("exp nil, got %v", err)
|
||||
}
|
||||
|
||||
if !f.IsSet() {
|
||||
t.Errorf("exp true, got false")
|
||||
}
|
||||
|
||||
act, ok := f.Get().(time.Time)
|
||||
if !ok {
|
||||
t.Errorf("exp true, got false")
|
||||
}
|
||||
if act != valid {
|
||||
t.Errorf("exp %v, got %v", valid, act)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlagTime(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
valid := time.Date(0, 1, 1, 12, 30, 0, 0, time.UTC)
|
||||
validStr := "12:30"
|
||||
f := command.FlagTime{}
|
||||
if f.IsSet() {
|
||||
t.Errorf("exp false, got true")
|
||||
}
|
||||
|
||||
if err := f.Set(validStr); err != nil {
|
||||
t.Errorf("exp nil, got %v", err)
|
||||
}
|
||||
|
||||
if !f.IsSet() {
|
||||
t.Errorf("exp true, got false")
|
||||
}
|
||||
|
||||
act, ok := f.Get().(time.Time)
|
||||
if !ok {
|
||||
t.Errorf("exp true, got false")
|
||||
}
|
||||
if act != valid {
|
||||
t.Errorf("exp %v, got %v", valid, act)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlagDurationTime(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
valid := time.Hour
|
||||
validStr := "1h"
|
||||
f := command.FlagDuration{}
|
||||
if f.IsSet() {
|
||||
t.Errorf("exp false, got true")
|
||||
}
|
||||
|
||||
if err := f.Set(validStr); err != nil {
|
||||
t.Errorf("exp nil, got %v", err)
|
||||
}
|
||||
|
||||
if !f.IsSet() {
|
||||
t.Errorf("exp true, got false")
|
||||
}
|
||||
|
||||
act, ok := f.Get().(time.Duration)
|
||||
if !ok {
|
||||
t.Errorf("exp true, got false")
|
||||
}
|
||||
if act != valid {
|
||||
t.Errorf("exp %v, got %v", valid, act)
|
||||
}
|
||||
}
|
|
@ -4,28 +4,35 @@ import (
|
|||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
"go-mod.ewintr.nl/planner/plan/storage"
|
||||
)
|
||||
|
||||
var ListCmd = &cli.Command{
|
||||
Name: "list",
|
||||
Usage: "List everything",
|
||||
type List struct {
|
||||
localIDRepo storage.LocalID
|
||||
eventRepo storage.Event
|
||||
}
|
||||
|
||||
func NewListCmd(localRepo storage.LocalID, eventRepo storage.Event) *cli.Command {
|
||||
ListCmd.Action = func(cCtx *cli.Context) error {
|
||||
return List(localRepo, eventRepo)
|
||||
func NewList(localIDRepo storage.LocalID, eventRepo storage.Event) Command {
|
||||
return &List{
|
||||
localIDRepo: localIDRepo,
|
||||
eventRepo: eventRepo,
|
||||
}
|
||||
return ListCmd
|
||||
}
|
||||
|
||||
func List(localRepo storage.LocalID, eventRepo storage.Event) error {
|
||||
localIDs, err := localRepo.FindAll()
|
||||
func (list *List) Execute(main []string, flags map[string]string) error {
|
||||
if len(main) > 0 && main[0] != "list" {
|
||||
return ErrWrongCommand
|
||||
}
|
||||
|
||||
return list.do()
|
||||
}
|
||||
|
||||
func (list *List) do() error {
|
||||
localIDs, err := list.localIDRepo.FindAll()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not get local ids: %v", err)
|
||||
}
|
||||
all, err := eventRepo.FindAll()
|
||||
all, err := list.eventRepo.FindAll()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
package command_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"go-mod.ewintr.nl/planner/item"
|
||||
"go-mod.ewintr.nl/planner/plan/command"
|
||||
"go-mod.ewintr.nl/planner/plan/storage/memory"
|
||||
)
|
||||
|
||||
func TestList(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
eventRepo := memory.NewEvent()
|
||||
localRepo := memory.NewLocalID()
|
||||
e := item.Event{
|
||||
ID: "id",
|
||||
EventBody: item.EventBody{
|
||||
Title: "name",
|
||||
Start: time.Date(2024, 10, 7, 9, 30, 0, 0, time.UTC),
|
||||
},
|
||||
}
|
||||
if err := eventRepo.Store(e); err != nil {
|
||||
t.Errorf("exp nil, got %v", err)
|
||||
}
|
||||
if err := localRepo.Store(e.ID, 1); err != nil {
|
||||
t.Errorf("exp nil, got %v", err)
|
||||
}
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
main []string
|
||||
expErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
main: []string{},
|
||||
},
|
||||
{
|
||||
name: "list",
|
||||
main: []string{"list"},
|
||||
},
|
||||
{
|
||||
name: "wrong",
|
||||
main: []string{"delete"},
|
||||
expErr: true,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cmd := command.NewList(localRepo, eventRepo)
|
||||
actErr := cmd.Execute(tc.main, nil) != nil
|
||||
if tc.expErr != actErr {
|
||||
t.Errorf("exp %v, got %v", tc.expErr, actErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
package command
|
|
@ -5,50 +5,54 @@ import (
|
|||
"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",
|
||||
},
|
||||
},
|
||||
type Sync struct {
|
||||
client client.Client
|
||||
syncRepo storage.Sync
|
||||
localIDRepo storage.LocalID
|
||||
eventRepo storage.Event
|
||||
}
|
||||
|
||||
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"))
|
||||
func NewSync(client client.Client, syncRepo storage.Sync, localIDRepo storage.LocalID, eventRepo storage.Event) Command {
|
||||
return &Sync{
|
||||
client: client,
|
||||
syncRepo: syncRepo,
|
||||
localIDRepo: localIDRepo,
|
||||
eventRepo: eventRepo,
|
||||
}
|
||||
return SyncCmd
|
||||
}
|
||||
|
||||
func Sync(client client.Client, syncRepo storage.Sync, localIDRepo storage.LocalID, eventRepo storage.Event, full bool) error {
|
||||
func (sync *Sync) Execute(main []string, flags map[string]string) error {
|
||||
if len(main) == 0 || main[0] != "sync" {
|
||||
return ErrWrongCommand
|
||||
}
|
||||
|
||||
return sync.do()
|
||||
}
|
||||
|
||||
func (sync *Sync) do() error {
|
||||
// local new and updated
|
||||
sendItems, err := syncRepo.FindAll()
|
||||
sendItems, err := sync.syncRepo.FindAll()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not get updated items: %v", err)
|
||||
}
|
||||
if err := client.Update(sendItems); err != nil {
|
||||
if err := sync.client.Update(sendItems); err != nil {
|
||||
return fmt.Errorf("could not send updated items: %v", err)
|
||||
}
|
||||
if err := syncRepo.DeleteAll(); err != nil {
|
||||
if err := sync.syncRepo.DeleteAll(); err != nil {
|
||||
return fmt.Errorf("could not clear updated items: %v", err)
|
||||
}
|
||||
|
||||
// get new/updated items
|
||||
ts, err := syncRepo.LastUpdate()
|
||||
ts, err := sync.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)
|
||||
recItems, err := sync.client.Updated([]item.Kind{item.KindEvent}, ts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not receive updates: %v", err)
|
||||
}
|
||||
|
@ -56,10 +60,10 @@ func Sync(client client.Client, syncRepo storage.Sync, localIDRepo storage.Local
|
|||
updated := make([]item.Item, 0)
|
||||
for _, ri := range recItems {
|
||||
if ri.Deleted {
|
||||
if err := localIDRepo.Delete(ri.ID); err != nil && !errors.Is(err, storage.ErrNotFound) {
|
||||
if err := sync.localIDRepo.Delete(ri.ID); err != nil && !errors.Is(err, storage.ErrNotFound) {
|
||||
return fmt.Errorf("could not delete local id: %v", err)
|
||||
}
|
||||
if err := eventRepo.Delete(ri.ID); err != nil && !errors.Is(err, storage.ErrNotFound) {
|
||||
if err := sync.eventRepo.Delete(ri.ID); err != nil && !errors.Is(err, storage.ErrNotFound) {
|
||||
return fmt.Errorf("could not delete event: %v", err)
|
||||
}
|
||||
continue
|
||||
|
@ -67,7 +71,7 @@ func Sync(client client.Client, syncRepo storage.Sync, localIDRepo storage.Local
|
|||
updated = append(updated, ri)
|
||||
}
|
||||
|
||||
lidMap, err := localIDRepo.FindAll()
|
||||
lidMap, err := sync.localIDRepo.FindAll()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not get local ids: %v", err)
|
||||
}
|
||||
|
@ -80,17 +84,17 @@ func Sync(client client.Client, syncRepo storage.Sync, localIDRepo storage.Local
|
|||
ID: u.ID,
|
||||
EventBody: eBody,
|
||||
}
|
||||
if err := eventRepo.Store(e); err != nil {
|
||||
if err := sync.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()
|
||||
lid, err = sync.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 {
|
||||
if err := sync.localIDRepo.Store(u.ID, lid); err != nil {
|
||||
return fmt.Errorf("could not store local id: %v", err)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,43 @@ import (
|
|||
"go-mod.ewintr.nl/planner/sync/client"
|
||||
)
|
||||
|
||||
func TestSyncParse(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
syncClient := client.NewMemory()
|
||||
syncRepo := memory.NewSync()
|
||||
localIDRepo := memory.NewLocalID()
|
||||
eventRepo := memory.NewEvent()
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
main []string
|
||||
expErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
expErr: true,
|
||||
},
|
||||
{
|
||||
name: "wrong",
|
||||
main: []string{"wrong"},
|
||||
expErr: true,
|
||||
},
|
||||
{
|
||||
name: "valid",
|
||||
main: []string{"sync"},
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cmd := command.NewSync(syncClient, syncRepo, localIDRepo, eventRepo)
|
||||
actErr := cmd.Execute(tc.main, nil) != nil
|
||||
if tc.expErr != actErr {
|
||||
t.Errorf("exp %v, got %v", tc.expErr, actErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncSend(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
@ -45,8 +82,8 @@ func TestSyncSend(t *testing.T) {
|
|||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
|
||||
if err := command.Sync(syncClient, syncRepo, localIDRepo, eventRepo, false); err != nil {
|
||||
cmd := command.NewSync(syncClient, syncRepo, localIDRepo, eventRepo)
|
||||
if err := cmd.Execute([]string{"sync"}, nil); err != nil {
|
||||
t.Errorf("exp nil, got %v", err)
|
||||
}
|
||||
actItems, actErr := syncClient.Updated(tc.ks, tc.ts)
|
||||
|
@ -163,7 +200,8 @@ func TestSyncReceive(t *testing.T) {
|
|||
}
|
||||
|
||||
// sync
|
||||
if err := command.Sync(syncClient, syncRepo, localIDRepo, eventRepo, false); err != nil {
|
||||
cmd := command.NewSync(syncClient, syncRepo, localIDRepo, eventRepo)
|
||||
if err := cmd.Execute([]string{"sync"}, nil); err != nil {
|
||||
t.Errorf("exp nil, got %v", err)
|
||||
}
|
||||
|
||||
|
|
|
@ -2,60 +2,73 @@ package command
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
"go-mod.ewintr.nl/planner/plan/storage"
|
||||
)
|
||||
|
||||
var UpdateCmd = &cli.Command{
|
||||
Name: "update",
|
||||
Usage: "Update an event",
|
||||
Flags: []cli.Flag{
|
||||
&cli.IntFlag{
|
||||
Name: "localID",
|
||||
Aliases: []string{"l"},
|
||||
Usage: "The local id of the event",
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "name",
|
||||
Aliases: []string{"n"},
|
||||
Usage: "The event that will happen",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "on",
|
||||
Aliases: []string{"o"},
|
||||
Usage: "The date, in YYYY-MM-DD format",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "at",
|
||||
Aliases: []string{"a"},
|
||||
Usage: "The time, in HH:MM format. If omitted, the event will last the whole day",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "for",
|
||||
Aliases: []string{"f"},
|
||||
Usage: "The duration, in show format (e.g. 1h30m)",
|
||||
},
|
||||
},
|
||||
type Update struct {
|
||||
localIDRepo storage.LocalID
|
||||
eventRepo storage.Event
|
||||
syncRepo storage.Sync
|
||||
argSet *ArgSet
|
||||
localID int
|
||||
}
|
||||
|
||||
func NewUpdateCmd(localRepo storage.LocalID, eventRepo storage.Event, syncRepo storage.Sync) *cli.Command {
|
||||
UpdateCmd.Action = func(cCtx *cli.Context) error {
|
||||
return Update(localRepo, eventRepo, syncRepo, cCtx.Int("localID"), cCtx.String("name"), cCtx.String("on"), cCtx.String("at"), cCtx.String("for"))
|
||||
func NewUpdate(localIDRepo storage.LocalID, eventRepo storage.Event, syncRepo storage.Sync) Command {
|
||||
return &Update{
|
||||
localIDRepo: localIDRepo,
|
||||
eventRepo: eventRepo,
|
||||
syncRepo: syncRepo,
|
||||
argSet: &ArgSet{
|
||||
Flags: map[string]Flag{
|
||||
FlagTitle: &FlagString{},
|
||||
FlagOn: &FlagDate{},
|
||||
FlagAt: &FlagTime{},
|
||||
FlagFor: &FlagDuration{},
|
||||
},
|
||||
},
|
||||
}
|
||||
return UpdateCmd
|
||||
}
|
||||
|
||||
func Update(localRepo storage.LocalID, eventRepo storage.Event, syncRepo storage.Sync, localID int, nameStr, onStr, atStr, frStr string) error {
|
||||
func (update *Update) Execute(main []string, flags map[string]string) error {
|
||||
if len(main) < 2 || main[0] != "update" {
|
||||
return ErrWrongCommand
|
||||
}
|
||||
localID, err := strconv.Atoi(main[1])
|
||||
if err != nil {
|
||||
return fmt.Errorf("not a local id: %v", main[1])
|
||||
}
|
||||
update.localID = localID
|
||||
main = main[2:]
|
||||
|
||||
as := update.argSet
|
||||
as.Main = strings.Join(main, " ")
|
||||
for k := range as.Flags {
|
||||
v, ok := flags[k]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if err := as.Set(k, v); err != nil {
|
||||
return fmt.Errorf("could not set %s: %v", k, err)
|
||||
}
|
||||
}
|
||||
update.argSet = as
|
||||
|
||||
return update.do()
|
||||
}
|
||||
|
||||
func (update *Update) do() error {
|
||||
as := update.argSet
|
||||
var id string
|
||||
idMap, err := localRepo.FindAll()
|
||||
idMap, err := update.localIDRepo.FindAll()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not get local ids: %v", err)
|
||||
}
|
||||
for eid, lid := range idMap {
|
||||
if localID == lid {
|
||||
if update.localID == lid {
|
||||
id = eid
|
||||
}
|
||||
}
|
||||
|
@ -63,39 +76,39 @@ func Update(localRepo storage.LocalID, eventRepo storage.Event, syncRepo storage
|
|||
return fmt.Errorf("could not find local id")
|
||||
}
|
||||
|
||||
e, err := eventRepo.Find(id)
|
||||
e, err := update.eventRepo.Find(id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not find event")
|
||||
}
|
||||
|
||||
if nameStr != "" {
|
||||
e.Title = nameStr
|
||||
if as.Main != "" {
|
||||
e.Title = as.Main
|
||||
}
|
||||
if onStr != "" || atStr != "" {
|
||||
oldStart := e.Start
|
||||
dateStr := oldStart.Format("2006-01-02")
|
||||
if onStr != "" {
|
||||
dateStr = onStr
|
||||
if as.IsSet(FlagOn) || as.IsSet(FlagAt) {
|
||||
on := time.Date(e.Start.Year(), e.Start.Month(), e.Start.Day(), 0, 0, 0, 0, time.UTC)
|
||||
atH := time.Duration(e.Start.Hour()) * time.Hour
|
||||
atM := time.Duration(e.Start.Minute()) * time.Minute
|
||||
|
||||
if as.IsSet(FlagOn) {
|
||||
on = as.GetTime(FlagOn)
|
||||
}
|
||||
timeStr := oldStart.Format("15:04")
|
||||
if atStr != "" {
|
||||
timeStr = atStr
|
||||
if as.IsSet(FlagAt) {
|
||||
at := as.GetTime(FlagAt)
|
||||
atH = time.Duration(at.Hour()) * time.Hour
|
||||
atM = time.Duration(at.Minute()) * time.Minute
|
||||
}
|
||||
newStart, err := time.Parse("2006-01-02 15:04", fmt.Sprintf("%s %s", dateStr, timeStr))
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse new start: %v", err)
|
||||
}
|
||||
e.Start = newStart
|
||||
e.Start = on.Add(atH).Add(atM)
|
||||
}
|
||||
|
||||
if frStr != "" { // no check on at, can set a duration with at 00:00, making it not a whole day
|
||||
fr, err := time.ParseDuration(frStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: could not parse duration: %s", ErrInvalidArg, err)
|
||||
if as.IsSet(FlagFor) {
|
||||
e.Duration = as.GetDuration(FlagFor)
|
||||
}
|
||||
e.Duration = fr
|
||||
|
||||
if !e.Valid() {
|
||||
return fmt.Errorf("event is unvalid")
|
||||
}
|
||||
if err := eventRepo.Store(e); err != nil {
|
||||
|
||||
if err := update.eventRepo.Store(e); err != nil {
|
||||
return fmt.Errorf("could not store event: %v", err)
|
||||
}
|
||||
|
||||
|
@ -103,7 +116,7 @@ func Update(localRepo storage.LocalID, eventRepo storage.Event, syncRepo storage
|
|||
if err != nil {
|
||||
return fmt.Errorf("could not convert event to sync item: %v", err)
|
||||
}
|
||||
if err := syncRepo.Store(it); err != nil {
|
||||
if err := update.syncRepo.Store(it); err != nil {
|
||||
return fmt.Errorf("could not store sync item: %v", err)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package command_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
@ -10,7 +11,7 @@ import (
|
|||
"go-mod.ewintr.nl/planner/plan/storage/memory"
|
||||
)
|
||||
|
||||
func TestUpdate(t *testing.T) {
|
||||
func TestUpdateExecute(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
eid := "c"
|
||||
|
@ -29,21 +30,14 @@ func TestUpdate(t *testing.T) {
|
|||
for _, tc := range []struct {
|
||||
name string
|
||||
localID int
|
||||
args map[string]string
|
||||
main []string
|
||||
flags map[string]string
|
||||
expEvent item.Event
|
||||
expErr bool
|
||||
}{
|
||||
{
|
||||
name: "no args",
|
||||
localID: lid,
|
||||
expEvent: item.Event{
|
||||
ID: eid,
|
||||
EventBody: item.EventBody{
|
||||
Title: title,
|
||||
Start: start,
|
||||
Duration: oneHour,
|
||||
},
|
||||
},
|
||||
expErr: true,
|
||||
},
|
||||
{
|
||||
name: "not found",
|
||||
|
@ -53,9 +47,7 @@ func TestUpdate(t *testing.T) {
|
|||
{
|
||||
name: "name",
|
||||
localID: lid,
|
||||
args: map[string]string{
|
||||
"name": "updated",
|
||||
},
|
||||
main: []string{"update", fmt.Sprintf("%d", lid), "updated"},
|
||||
expEvent: item.Event{
|
||||
ID: eid,
|
||||
EventBody: item.EventBody{
|
||||
|
@ -68,7 +60,8 @@ func TestUpdate(t *testing.T) {
|
|||
{
|
||||
name: "invalid on",
|
||||
localID: lid,
|
||||
args: map[string]string{
|
||||
main: []string{"update", fmt.Sprintf("%d", lid)},
|
||||
flags: map[string]string{
|
||||
"on": "invalid",
|
||||
},
|
||||
expErr: true,
|
||||
|
@ -76,7 +69,8 @@ func TestUpdate(t *testing.T) {
|
|||
{
|
||||
name: "on",
|
||||
localID: lid,
|
||||
args: map[string]string{
|
||||
main: []string{"update", fmt.Sprintf("%d", lid)},
|
||||
flags: map[string]string{
|
||||
"on": "2024-10-02",
|
||||
},
|
||||
expEvent: item.Event{
|
||||
|
@ -91,7 +85,8 @@ func TestUpdate(t *testing.T) {
|
|||
{
|
||||
name: "invalid at",
|
||||
localID: lid,
|
||||
args: map[string]string{
|
||||
main: []string{"update", fmt.Sprintf("%d", lid)},
|
||||
flags: map[string]string{
|
||||
"at": "invalid",
|
||||
},
|
||||
expErr: true,
|
||||
|
@ -99,7 +94,8 @@ func TestUpdate(t *testing.T) {
|
|||
{
|
||||
name: "at",
|
||||
localID: lid,
|
||||
args: map[string]string{
|
||||
main: []string{"update", fmt.Sprintf("%d", lid)},
|
||||
flags: map[string]string{
|
||||
"at": "11:00",
|
||||
},
|
||||
expEvent: item.Event{
|
||||
|
@ -114,7 +110,8 @@ func TestUpdate(t *testing.T) {
|
|||
{
|
||||
name: "on and at",
|
||||
localID: lid,
|
||||
args: map[string]string{
|
||||
main: []string{"update", fmt.Sprintf("%d", lid)},
|
||||
flags: map[string]string{
|
||||
"on": "2024-10-02",
|
||||
"at": "11:00",
|
||||
},
|
||||
|
@ -130,7 +127,8 @@ func TestUpdate(t *testing.T) {
|
|||
{
|
||||
name: "invalid for",
|
||||
localID: lid,
|
||||
args: map[string]string{
|
||||
main: []string{"update", fmt.Sprintf("%d", lid)},
|
||||
flags: map[string]string{
|
||||
"for": "invalid",
|
||||
},
|
||||
expErr: true,
|
||||
|
@ -138,7 +136,8 @@ func TestUpdate(t *testing.T) {
|
|||
{
|
||||
name: "for",
|
||||
localID: lid,
|
||||
args: map[string]string{
|
||||
main: []string{"update", fmt.Sprintf("%d", lid)},
|
||||
flags: map[string]string{
|
||||
"for": "2h",
|
||||
},
|
||||
expEvent: item.Event{
|
||||
|
@ -169,9 +168,10 @@ func TestUpdate(t *testing.T) {
|
|||
t.Errorf("exp nil, ,got %v", err)
|
||||
}
|
||||
|
||||
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 {
|
||||
t.Errorf("exp %v, got %v", tc.expErr, actErr)
|
||||
cmd := command.NewUpdate(localIDRepo, eventRepo, syncRepo)
|
||||
actParseErr := cmd.Execute(tc.main, tc.flags) != nil
|
||||
if tc.expErr != actParseErr {
|
||||
t.Errorf("exp %v, got %v", tc.expErr, actParseErr)
|
||||
}
|
||||
if tc.expErr {
|
||||
return
|
||||
|
|
19
plan/main.go
19
plan/main.go
|
@ -5,7 +5,6 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
"go-mod.ewintr.nl/planner/plan/command"
|
||||
"go-mod.ewintr.nl/planner/plan/storage/sqlite"
|
||||
"go-mod.ewintr.nl/planner/sync/client"
|
||||
|
@ -32,19 +31,17 @@ func main() {
|
|||
|
||||
syncClient := client.New(conf.SyncURL, conf.ApiKey)
|
||||
|
||||
app := &cli.App{
|
||||
Name: "plan",
|
||||
Usage: "Plan your day with events",
|
||||
Commands: []*cli.Command{
|
||||
command.NewAddCmd(localIDRepo, eventRepo, syncRepo),
|
||||
command.NewListCmd(localIDRepo, eventRepo),
|
||||
command.NewUpdateCmd(localIDRepo, eventRepo, syncRepo),
|
||||
command.NewDeleteCmd(localIDRepo, eventRepo, syncRepo),
|
||||
command.NewSyncCmd(syncClient, syncRepo, localIDRepo, eventRepo),
|
||||
cli := command.CLI{
|
||||
Commands: []command.Command{
|
||||
command.NewAdd(localIDRepo, eventRepo, syncRepo),
|
||||
command.NewList(localIDRepo, eventRepo),
|
||||
command.NewUpdate(localIDRepo, eventRepo, syncRepo),
|
||||
command.NewDelete(localIDRepo, eventRepo, syncRepo),
|
||||
command.NewSync(syncClient, syncRepo, localIDRepo, eventRepo),
|
||||
},
|
||||
}
|
||||
|
||||
if err := app.Run(os.Args); err != nil {
|
||||
if err := cli.Run(os.Args); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
package memory
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"go-mod.ewintr.nl/planner/item"
|
||||
"go-mod.ewintr.nl/planner/plan/storage"
|
||||
)
|
||||
|
||||
type Event struct {
|
||||
|
@ -25,7 +25,7 @@ func (r *Event) Find(id string) (item.Event, error) {
|
|||
|
||||
event, exists := r.events[id]
|
||||
if !exists {
|
||||
return item.Event{}, errors.New("event not found")
|
||||
return item.Event{}, storage.ErrNotFound
|
||||
}
|
||||
return event, nil
|
||||
}
|
||||
|
@ -59,7 +59,7 @@ func (r *Event) Delete(id string) error {
|
|||
defer r.mutex.Unlock()
|
||||
|
||||
if _, exists := r.events[id]; !exists {
|
||||
return errors.New("event not found")
|
||||
return storage.ErrNotFound
|
||||
}
|
||||
delete(r.events, id)
|
||||
|
||||
|
|
Loading…
Reference in New Issue