This commit is contained in:
Erik Winter 2024-12-22 09:16:43 +01:00
parent 22b8852a0a
commit 5df7d3ff3d
5 changed files with 498 additions and 489 deletions

View File

@ -9,7 +9,7 @@ import (
)
const (
DateFormat = "2006-01-02 (Monday)"
DateFormat = "2006-01-02"
)
func Today() Date {
@ -174,20 +174,20 @@ func (d Date) String() string {
return strings.ToLower(d.t.Format(DateFormat))
}
func (d Date) Human() string {
switch {
case d.IsZero():
return "-"
case d.Equal(Today()):
return "today"
case d.Equal(Today().Add(1)):
return "tomorrow"
case d.After(Today()) && Today().Add(8).After(d):
return strings.ToLower(d.t.Format("Monday"))
default:
return strings.ToLower(d.t.Format(DateFormat))
}
}
// func (d Date) Human() string {
// switch {
// case d.IsZero():
// return "-"
// case d.Equal(Today()):
// return "today"
// case d.Equal(Today().Add(1)):
// return "tomorrow"
// case d.After(Today()) && Today().Add(8).After(d):
// return strings.ToLower(d.t.Format("Monday"))
// default:
// return strings.ToLower(d.t.Format(DateFormat))
// }
// }
func (d Date) IsZero() bool {
return d.t.IsZero()

View File

@ -1,248 +1,255 @@
package item_test
// func TestWeekdaysSort(t *testing.T) {
// for _, tc := range []struct {
// name string
// input task.Weekdays
// exp task.Weekdays
// }{
// {
// name: "empty",
// },
// {
// name: "one",
// input: task.Weekdays{time.Tuesday},
// exp: task.Weekdays{time.Tuesday},
// },
// {
// name: "multiple",
// input: task.Weekdays{time.Wednesday, time.Tuesday, time.Monday},
// exp: task.Weekdays{time.Monday, time.Tuesday, time.Wednesday},
// },
// {
// name: "sunday is last",
// input: task.Weekdays{time.Saturday, time.Sunday, time.Monday},
// exp: task.Weekdays{time.Monday, time.Saturday, time.Sunday},
// },
// } {
// t.Run(tc.name, func(t *testing.T) {
// sort.Sort(tc.input)
// test.Equals(t, tc.exp, tc.input)
// })
// }
// }
import (
"sort"
"testing"
"time"
// func TestWeekdaysUnique(t *testing.T) {
// for _, tc := range []struct {
// name string
// input task.Weekdays
// exp task.Weekdays
// }{
// {
// name: "empty",
// input: task.Weekdays{},
// exp: task.Weekdays{},
// },
// {
// name: "single",
// input: task.Weekdays{time.Monday},
// exp: task.Weekdays{time.Monday},
// },
// {
// name: "no doubles",
// input: task.Weekdays{time.Monday, time.Tuesday, time.Wednesday},
// exp: task.Weekdays{time.Monday, time.Tuesday, time.Wednesday},
// },
// {
// name: "doubles",
// input: task.Weekdays{time.Monday, time.Monday, time.Wednesday, time.Monday},
// exp: task.Weekdays{time.Monday, time.Wednesday},
// },
// } {
// t.Run(tc.name, func(t *testing.T) {
// test.Equals(t, tc.exp, tc.input.Unique())
// })
// }
// }
"github.com/google/go-cmp/cmp"
"go-mod.ewintr.nl/planner/item"
)
// func TestNewDateFromString(t *testing.T) {
// t.Run("no date", func(t *testing.T) {
// for _, tc := range []struct {
// name string
// input string
// exp task.Date
// }{
// {
// name: "empty",
// exp: task.Date{},
// },
// {
// name: "no date",
// input: "no date",
// exp: task.Date{},
// },
// } {
// t.Run(tc.name, func(t *testing.T) {
// test.Equals(t, tc.exp, task.NewDateFromString(tc.input))
// })
// }
// })
func TestWeekdaysSort(t *testing.T) {
for _, tc := range []struct {
name string
input item.Weekdays
exp item.Weekdays
}{
{
name: "empty",
},
{
name: "one",
input: item.Weekdays{time.Tuesday},
exp: item.Weekdays{time.Tuesday},
},
{
name: "multiple",
input: item.Weekdays{time.Wednesday, time.Tuesday, time.Monday},
exp: item.Weekdays{time.Monday, time.Tuesday, time.Wednesday},
},
{
name: "sunday is last",
input: item.Weekdays{time.Saturday, time.Sunday, time.Monday},
exp: item.Weekdays{time.Monday, time.Saturday, time.Sunday},
},
} {
t.Run(tc.name, func(t *testing.T) {
sort.Sort(tc.input)
if diff := cmp.Diff(tc.exp, tc.input); diff != "" {
t.Errorf("(-exp, +got)%s\n", diff)
}
})
}
}
// t.Run("digits", func(t *testing.T) {
// for _, tc := range []struct {
// name string
// input string
// exp task.Date
// }{
// {
// name: "normal",
// input: "2021-01-30 (saturday)",
// exp: task.NewDate(2021, 1, 30),
// },
// {
// name: "short",
// input: "2021-01-30",
// exp: task.NewDate(2021, 1, 30),
// },
// } {
// t.Run(tc.name, func(t *testing.T) {
// test.Equals(t, tc.exp, task.NewDateFromString(tc.input))
func TestWeekdaysUnique(t *testing.T) {
for _, tc := range []struct {
name string
input item.Weekdays
exp item.Weekdays
}{
{
name: "empty",
input: item.Weekdays{},
exp: item.Weekdays{},
},
{
name: "single",
input: item.Weekdays{time.Monday},
exp: item.Weekdays{time.Monday},
},
{
name: "no doubles",
input: item.Weekdays{time.Monday, time.Tuesday, time.Wednesday},
exp: item.Weekdays{time.Monday, time.Tuesday, time.Wednesday},
},
{
name: "doubles",
input: item.Weekdays{time.Monday, time.Monday, time.Wednesday, time.Monday},
exp: item.Weekdays{time.Monday, time.Wednesday},
},
} {
t.Run(tc.name, func(t *testing.T) {
if diff := cmp.Diff(tc.exp, tc.input.Unique()); diff != "" {
t.Errorf("(-exp, +got)%s\n", diff)
}
})
}
}
// })
// }
// })
func TestNewDateFromString(t *testing.T) {
t.Parallel()
// t.Run("day name", func(t *testing.T) {
// monday := task.Today().Add(1)
// for {
// if monday.Weekday() == time.Monday {
// break
// }
// monday = monday.Add(1)
// }
// for _, tc := range []struct {
// name string
// input string
// exp task.Date
// }{
// {
// name: "dayname lowercase",
// input: "monday",
// },
// {
// name: "dayname capitalized",
// input: "Monday",
// },
// {
// name: "dayname short",
// input: "mon",
// },
// } {
// t.Run(tc.name, func(t *testing.T) {
// test.Equals(t, monday, task.NewDateFromString(tc.input))
// })
// }
// })
t.Run("simple", func(t *testing.T) {
for _, tc := range []struct {
name string
input string
exp item.Date
}{
{
name: "empty",
exp: item.Date{},
},
{
name: "no date",
input: "no date",
exp: item.Date{},
},
{
name: "short",
input: "2021-01-30",
exp: item.NewDate(2021, 1, 30),
},
} {
t.Run(tc.name, func(t *testing.T) {
if diff := cmp.Diff(tc.exp, item.NewDateFromString(tc.input)); diff != "" {
t.Errorf("(-exp, +got)%s\n", diff)
}
})
}
})
// t.Run("relative days", func(t *testing.T) {
// for _, tc := range []struct {
// name string
// exp task.Date
// }{
// {
// name: "today",
// exp: task.Today(),
// },
// {
// name: "tod",
// exp: task.Today(),
// },
// {
// name: "tomorrow",
// exp: task.Today().Add(1),
// },
// {
// name: "tom",
// exp: task.Today().Add(1),
// },
// } {
// t.Run(tc.name, func(t *testing.T) {
// test.Equals(t, tc.exp, task.NewDateFromString(tc.name))
// })
// }
// })
// }
t.Run("day name", func(t *testing.T) {
monday := item.Today().Add(1)
for {
if monday.Weekday() == time.Monday {
break
}
monday = monday.Add(1)
}
for _, tc := range []struct {
name string
input string
}{
{
name: "dayname lowercase",
input: "monday",
},
{
name: "dayname capitalized",
input: "Monday",
},
{
name: "dayname short",
input: "mon",
},
} {
t.Run(tc.name, func(t *testing.T) {
if diff := cmp.Diff(monday, item.NewDateFromString(tc.input)); diff != "" {
t.Errorf("(-exp, +got)%s\n", diff)
}
})
}
})
// func TestDateDaysBetween(t *testing.T) {
// for _, tc := range []struct {
// name string
// d1 task.Date
// d2 task.Date
// exp int
// }{
// {
// name: "same",
// d1: task.NewDate(2021, 6, 23),
// d2: task.NewDate(2021, 6, 23),
// },
// {
// name: "one",
// d1: task.NewDate(2021, 6, 23),
// d2: task.NewDate(2021, 6, 24),
// exp: 1,
// },
// {
// name: "many",
// d1: task.NewDate(2021, 6, 23),
// d2: task.NewDate(2024, 3, 7),
// exp: 988,
// },
// {
// name: "edge",
// d1: task.NewDate(2020, 12, 30),
// d2: task.NewDate(2021, 1, 3),
// exp: 4,
// },
// {
// name: "reverse",
// d1: task.NewDate(2021, 6, 23),
// d2: task.NewDate(2021, 5, 23),
// exp: 31,
// },
// } {
// t.Run(tc.name, func(t *testing.T) {
// test.Equals(t, tc.exp, tc.d1.DaysBetween(tc.d2))
// })
// }
// }
t.Run("relative days", func(t *testing.T) {
for _, tc := range []struct {
name string
exp item.Date
}{
{
name: "today",
exp: item.Today(),
},
{
name: "tod",
exp: item.Today(),
},
{
name: "tomorrow",
exp: item.Today().Add(1),
},
{
name: "tom",
exp: item.Today().Add(1),
},
} {
t.Run(tc.name, func(t *testing.T) {
if diff := cmp.Diff(tc.exp, item.NewDateFromString(tc.name)); diff != "" {
t.Errorf("(-exp, +got)%s\n", diff)
}
})
}
})
}
// func TestDateString(t *testing.T) {
// for _, tc := range []struct {
// name string
// date task.Date
// exp string
// }{
// {
// name: "zero",
// date: task.NewDate(0, 0, 0),
// exp: "no date",
// },
// {
// name: "normal",
// date: task.NewDate(2021, 5, 30),
// exp: "2021-05-30 (sunday)",
// },
// {
// name: "normalize",
// date: task.NewDate(2021, 5, 32),
// exp: "2021-06-01 (tuesday)",
// },
// } {
// t.Run(tc.name, func(t *testing.T) {
// test.Equals(t, tc.exp, tc.date.String())
// })
// }
// }
func TestDateDaysBetween(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
name string
d1 item.Date
d2 item.Date
exp int
}{
{
name: "same",
d1: item.NewDate(2021, 6, 23),
d2: item.NewDate(2021, 6, 23),
},
{
name: "one",
d1: item.NewDate(2021, 6, 23),
d2: item.NewDate(2021, 6, 24),
exp: 1,
},
{
name: "many",
d1: item.NewDate(2021, 6, 23),
d2: item.NewDate(2024, 3, 7),
exp: 988,
},
{
name: "edge",
d1: item.NewDate(2020, 12, 30),
d2: item.NewDate(2021, 1, 3),
exp: 4,
},
{
name: "reverse",
d1: item.NewDate(2021, 6, 23),
d2: item.NewDate(2021, 5, 23),
exp: 31,
},
} {
t.Run(tc.name, func(t *testing.T) {
if tc.exp != tc.d1.DaysBetween(tc.d2) {
t.Errorf("exp %v, got %v", tc.exp, tc.d1.DaysBetween(tc.d2))
}
})
}
}
func TestDateString(t *testing.T) {
for _, tc := range []struct {
name string
date item.Date
exp string
}{
{
name: "zero",
date: item.NewDate(0, 0, 0),
exp: "no date",
},
{
name: "normal",
date: item.NewDate(2021, 5, 30),
exp: "2021-05-30",
},
{
name: "normalize",
date: item.NewDate(2021, 5, 32),
exp: "2021-06-01",
},
} {
t.Run(tc.name, func(t *testing.T) {
if tc.exp != tc.date.String() {
t.Errorf("exp %v, got %v", tc.exp, tc.date.String())
}
})
}
}
// func TestDateHuman(t *testing.T) {
// for _, tc := range []struct {
@ -277,34 +284,44 @@ package item_test
// }
// }
// func TestDateIsZero(t *testing.T) {
// test.Equals(t, true, task.Date{}.IsZero())
// test.Equals(t, false, task.NewDate(2021, 6, 24).IsZero())
// }
func TestDateIsZero(t *testing.T) {
t.Parallel()
// func TestDateAfter(t *testing.T) {
// day := task.NewDate(2021, 1, 31)
// for _, tc := range []struct {
// name string
// tDay task.Date
// exp bool
// }{
// {
// name: "after",
// tDay: task.NewDate(2021, 1, 30),
// exp: true,
// },
// {
// name: "on",
// tDay: day,
// },
// {
// name: "before",
// tDay: task.NewDate(2021, 2, 1),
// },
// } {
// t.Run(tc.name, func(t *testing.T) {
// test.Equals(t, tc.exp, day.After(tc.tDay))
// })
// }
// }
if !(item.Date{}.IsZero()) {
t.Errorf("exp true, got false")
}
if item.NewDate(2021, 6, 24).IsZero() {
t.Errorf("exp false, got true")
}
}
func TestDateAfter(t *testing.T) {
t.Parallel()
day := item.NewDate(2021, 1, 31)
for _, tc := range []struct {
name string
tDay item.Date
exp bool
}{
{
name: "after",
tDay: item.NewDate(2021, 1, 30),
exp: true,
},
{
name: "on",
tDay: day,
},
{
name: "before",
tDay: item.NewDate(2021, 2, 1),
},
} {
t.Run(tc.name, func(t *testing.T) {
if diff := cmp.Diff(tc.exp, day.After(tc.tDay)); diff != "" {
t.Errorf("(-exp, +got)%s\n", diff)
}
})
}
}

View File

@ -52,7 +52,7 @@ func (e *EventBody) UnmarshalJSON(data []byte) error {
type Event struct {
ID string `json:"id"`
Recurrer *Recur `json:"recurrer"`
Recurrer Recurrer `json:"recurrer"`
RecurNext time.Time `json:"recurNext"`
EventBody
}
@ -103,9 +103,9 @@ func (e Event) Valid() bool {
if e.Duration.Seconds() < 1 {
return false
}
if e.Recurrer != nil && !e.Recurrer.Valid() {
return false
}
// if e.Recurrer != nil && !e.Recurrer.Valid() {
// return false
// }
return true
}

View File

@ -1,213 +1,205 @@
package item_test
import (
"testing"
"time"
// func TestNewEvent(t *testing.T) {
// t.Parallel()
"github.com/google/go-cmp/cmp"
"go-mod.ewintr.nl/planner/item"
)
// oneHour, err := time.ParseDuration("1h")
// if err != nil {
// t.Errorf("exp nil, got %v", err)
// }
// for _, tc := range []struct {
// name string
// it item.Item
// expEvent item.Event
// expErr bool
// }{
// {
// name: "wrong kind",
// it: item.Item{
// ID: "a",
// Kind: item.KindTask,
// Body: `{
// "title":"title",
// "start":"2024-09-20T08:00:00Z",
// "duration":"1h"
// }`,
// },
// expErr: true,
// },
// {
// name: "invalid json",
// it: item.Item{
// ID: "a",
// Kind: item.KindEvent,
// Body: `{"id":"a"`,
// },
// expErr: true,
// },
// {
// name: "valid",
// it: item.Item{
// ID: "a",
// Kind: item.KindEvent,
// Recurrer: &item.Recur{
// Start: time.Date(2024, 12, 8, 9, 0, 0, 0, time.UTC),
// Period: item.PeriodDay,
// Count: 1,
// },
// Body: `{
// "title":"title",
// "start":"2024-09-20T08:00:00Z",
// "duration":"1h"
// }`,
// },
// expEvent: item.Event{
// ID: "a",
// Recurrer: &item.Recur{
// Start: time.Date(2024, 12, 8, 9, 0, 0, 0, time.UTC),
// Period: item.PeriodDay,
// Count: 1,
// },
// EventBody: item.EventBody{
// Title: "title",
// Start: time.Date(2024, 9, 20, 8, 0, 0, 0, time.UTC),
// Duration: oneHour,
// },
// },
// },
// } {
// t.Run(tc.name, func(t *testing.T) {
// actEvent, actErr := item.NewEvent(tc.it)
// if tc.expErr != (actErr != nil) {
// t.Errorf("exp nil, got %v", actErr)
// }
// if tc.expErr {
// return
// }
// if diff := cmp.Diff(tc.expEvent, actEvent); diff != "" {
// t.Errorf("(exp +, got -)\n%s", diff)
// }
// })
// }
// }
func TestNewEvent(t *testing.T) {
t.Parallel()
// func TestEventItem(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
it item.Item
expEvent item.Event
expErr bool
}{
{
name: "wrong kind",
it: item.Item{
ID: "a",
Kind: item.KindTask,
Body: `{
"title":"title",
"start":"2024-09-20T08:00:00Z",
"duration":"1h"
}`,
},
expErr: true,
},
{
name: "invalid json",
it: item.Item{
ID: "a",
Kind: item.KindEvent,
Body: `{"id":"a"`,
},
expErr: true,
},
{
name: "valid",
it: item.Item{
ID: "a",
Kind: item.KindEvent,
Recurrer: &item.Recur{
Start: time.Date(2024, 12, 8, 9, 0, 0, 0, time.UTC),
Period: item.PeriodDay,
Count: 1,
},
Body: `{
"title":"title",
"start":"2024-09-20T08:00:00Z",
"duration":"1h"
}`,
},
expEvent: item.Event{
ID: "a",
Recurrer: &item.Recur{
Start: time.Date(2024, 12, 8, 9, 0, 0, 0, time.UTC),
Period: item.PeriodDay,
Count: 1,
},
EventBody: item.EventBody{
Title: "title",
Start: time.Date(2024, 9, 20, 8, 0, 0, 0, time.UTC),
Duration: oneHour,
},
},
},
} {
t.Run(tc.name, func(t *testing.T) {
actEvent, actErr := item.NewEvent(tc.it)
if tc.expErr != (actErr != nil) {
t.Errorf("exp nil, got %v", actErr)
}
if tc.expErr {
return
}
if diff := cmp.Diff(tc.expEvent, actEvent); diff != "" {
t.Errorf("(exp +, got -)\n%s", diff)
}
})
}
}
// oneHour, err := time.ParseDuration("1h")
// if err != nil {
// t.Errorf("exp nil, got %v", err)
// }
// for _, tc := range []struct {
// name string
// event item.Event
// expItem item.Item
// expErr bool
// }{
// {
// name: "empty",
// expItem: item.Item{
// Kind: item.KindEvent,
// Updated: time.Time{},
// Body: `{"start":"0001-01-01T00:00:00Z","duration":"0s","title":""}`,
// },
// },
// {
// name: "normal",
// event: item.Event{
// ID: "a",
// EventBody: item.EventBody{
// Title: "title",
// Start: time.Date(2024, 9, 23, 8, 0, 0, 0, time.UTC),
// Duration: oneHour,
// },
// },
// expItem: item.Item{
// ID: "a",
// Kind: item.KindEvent,
// Updated: time.Time{},
// Body: `{"start":"2024-09-23T08:00:00Z","duration":"1h0m0s","title":"title"}`,
// },
// },
// } {
// t.Run(tc.name, func(t *testing.T) {
// actItem, actErr := tc.event.Item()
// if tc.expErr != (actErr != nil) {
// t.Errorf("exp nil, got %v", actErr)
// }
// if tc.expErr {
// return
// }
// if diff := cmp.Diff(tc.expItem, actItem); diff != "" {
// t.Errorf("(exp+, got -)\n%s", diff)
// }
// })
// }
// }
func TestEventItem(t *testing.T) {
t.Parallel()
// 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
expItem item.Item
expErr bool
}{
{
name: "empty",
expItem: item.Item{
Kind: item.KindEvent,
Updated: time.Time{},
Body: `{"start":"0001-01-01T00:00:00Z","duration":"0s","title":""}`,
},
},
{
name: "normal",
event: item.Event{
ID: "a",
EventBody: item.EventBody{
Title: "title",
Start: time.Date(2024, 9, 23, 8, 0, 0, 0, time.UTC),
Duration: oneHour,
},
},
expItem: item.Item{
ID: "a",
Kind: item.KindEvent,
Updated: time.Time{},
Body: `{"start":"2024-09-23T08:00:00Z","duration":"1h0m0s","title":"title"}`,
},
},
} {
t.Run(tc.name, func(t *testing.T) {
actItem, actErr := tc.event.Item()
if tc.expErr != (actErr != nil) {
t.Errorf("exp nil, got %v", actErr)
}
if tc.expErr {
return
}
if diff := cmp.Diff(tc.expItem, actItem); diff != "" {
t.Errorf("(exp+, got -)\n%s", diff)
}
})
}
}
// oneHour, err := time.ParseDuration("1h")
// if err != nil {
// t.Errorf("exp nil, got %v", err)
// }
func TestEventValidate(t *testing.T) {
t.Parallel()
// 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)
// }
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)
}
})
}
}
// })
// }
// }

View File

@ -22,7 +22,7 @@ type Item struct {
Kind Kind `json:"kind"`
Updated time.Time `json:"updated"`
Deleted bool `json:"deleted"`
Recurrer *Recur `json:"recurrer"`
Recurrer Recurrer `json:"recurrer"`
RecurNext time.Time `json:"recurNext"`
Body string `json:"body"`
}