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

View File

@ -1,248 +1,255 @@
package item_test package item_test
// func TestWeekdaysSort(t *testing.T) { import (
// for _, tc := range []struct { "sort"
// name string "testing"
// input task.Weekdays "time"
// 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)
// })
// }
// }
// func TestWeekdaysUnique(t *testing.T) { "github.com/google/go-cmp/cmp"
// for _, tc := range []struct { "go-mod.ewintr.nl/planner/item"
// 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())
// })
// }
// }
// func TestNewDateFromString(t *testing.T) { func TestWeekdaysSort(t *testing.T) {
// t.Run("no date", func(t *testing.T) { for _, tc := range []struct {
// for _, tc := range []struct { name string
// name string input item.Weekdays
// input string exp item.Weekdays
// exp task.Date }{
// }{ {
// { name: "empty",
// name: "empty", },
// exp: task.Date{}, {
// }, name: "one",
// { input: item.Weekdays{time.Tuesday},
// name: "no date", exp: item.Weekdays{time.Tuesday},
// input: "no date", },
// exp: task.Date{}, {
// }, name: "multiple",
// } { input: item.Weekdays{time.Wednesday, time.Tuesday, time.Monday},
// t.Run(tc.name, func(t *testing.T) { exp: item.Weekdays{time.Monday, time.Tuesday, time.Wednesday},
// test.Equals(t, tc.exp, task.NewDateFromString(tc.input)) },
// }) {
// } 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) { func TestWeekdaysUnique(t *testing.T) {
// for _, tc := range []struct { for _, tc := range []struct {
// name string name string
// input string input item.Weekdays
// exp task.Date exp item.Weekdays
// }{ }{
// { {
// name: "normal", name: "empty",
// input: "2021-01-30 (saturday)", input: item.Weekdays{},
// exp: task.NewDate(2021, 1, 30), exp: item.Weekdays{},
// }, },
// { {
// name: "short", name: "single",
// input: "2021-01-30", input: item.Weekdays{time.Monday},
// exp: task.NewDate(2021, 1, 30), exp: item.Weekdays{time.Monday},
// }, },
// } { {
// t.Run(tc.name, func(t *testing.T) { name: "no doubles",
// test.Equals(t, tc.exp, task.NewDateFromString(tc.input)) 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) { t.Run("simple", func(t *testing.T) {
// monday := task.Today().Add(1) for _, tc := range []struct {
// for { name string
// if monday.Weekday() == time.Monday { input string
// break exp item.Date
// } }{
// monday = monday.Add(1) {
// } name: "empty",
// for _, tc := range []struct { exp: item.Date{},
// name string },
// input string {
// exp task.Date name: "no date",
// }{ input: "no date",
// { exp: item.Date{},
// name: "dayname lowercase", },
// input: "monday", {
// }, name: "short",
// { input: "2021-01-30",
// name: "dayname capitalized", exp: item.NewDate(2021, 1, 30),
// input: "Monday", },
// }, } {
// { t.Run(tc.name, func(t *testing.T) {
// name: "dayname short", if diff := cmp.Diff(tc.exp, item.NewDateFromString(tc.input)); diff != "" {
// input: "mon", t.Errorf("(-exp, +got)%s\n", diff)
// }, }
// } { })
// t.Run(tc.name, func(t *testing.T) { }
// test.Equals(t, monday, task.NewDateFromString(tc.input)) })
// })
// }
// })
// t.Run("relative days", func(t *testing.T) { t.Run("day name", func(t *testing.T) {
// for _, tc := range []struct { monday := item.Today().Add(1)
// name string for {
// exp task.Date if monday.Weekday() == time.Monday {
// }{ break
// { }
// name: "today", monday = monday.Add(1)
// exp: task.Today(), }
// }, for _, tc := range []struct {
// { name string
// name: "tod", input string
// exp: task.Today(), }{
// }, {
// { name: "dayname lowercase",
// name: "tomorrow", input: "monday",
// exp: task.Today().Add(1), },
// }, {
// { name: "dayname capitalized",
// name: "tom", input: "Monday",
// exp: task.Today().Add(1), },
// }, {
// } { name: "dayname short",
// t.Run(tc.name, func(t *testing.T) { input: "mon",
// test.Equals(t, tc.exp, task.NewDateFromString(tc.name)) },
// }) } {
// } 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) { t.Run("relative days", func(t *testing.T) {
// for _, tc := range []struct { for _, tc := range []struct {
// name string name string
// d1 task.Date exp item.Date
// d2 task.Date }{
// exp int {
// }{ name: "today",
// { exp: item.Today(),
// name: "same", },
// d1: task.NewDate(2021, 6, 23), {
// d2: task.NewDate(2021, 6, 23), name: "tod",
// }, exp: item.Today(),
// { },
// name: "one", {
// d1: task.NewDate(2021, 6, 23), name: "tomorrow",
// d2: task.NewDate(2021, 6, 24), exp: item.Today().Add(1),
// exp: 1, },
// }, {
// { name: "tom",
// name: "many", exp: item.Today().Add(1),
// d1: task.NewDate(2021, 6, 23), },
// d2: task.NewDate(2024, 3, 7), } {
// exp: 988, 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)
// 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))
// })
// }
// }
// func TestDateString(t *testing.T) { func TestDateDaysBetween(t *testing.T) {
// for _, tc := range []struct { t.Parallel()
// name string
// date task.Date for _, tc := range []struct {
// exp string name string
// }{ d1 item.Date
// { d2 item.Date
// name: "zero", exp int
// date: task.NewDate(0, 0, 0), }{
// exp: "no date", {
// }, name: "same",
// { d1: item.NewDate(2021, 6, 23),
// name: "normal", d2: item.NewDate(2021, 6, 23),
// date: task.NewDate(2021, 5, 30), },
// exp: "2021-05-30 (sunday)", {
// }, name: "one",
// { d1: item.NewDate(2021, 6, 23),
// name: "normalize", d2: item.NewDate(2021, 6, 24),
// date: task.NewDate(2021, 5, 32), exp: 1,
// exp: "2021-06-01 (tuesday)", },
// }, {
// } { name: "many",
// t.Run(tc.name, func(t *testing.T) { d1: item.NewDate(2021, 6, 23),
// test.Equals(t, tc.exp, tc.date.String()) 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) { // func TestDateHuman(t *testing.T) {
// for _, tc := range []struct { // for _, tc := range []struct {
@ -277,34 +284,44 @@ package item_test
// } // }
// } // }
// func TestDateIsZero(t *testing.T) { func TestDateIsZero(t *testing.T) {
// test.Equals(t, true, task.Date{}.IsZero()) t.Parallel()
// test.Equals(t, false, task.NewDate(2021, 6, 24).IsZero())
// }
// func TestDateAfter(t *testing.T) { if !(item.Date{}.IsZero()) {
// day := task.NewDate(2021, 1, 31) t.Errorf("exp true, got false")
// for _, tc := range []struct { }
// name string if item.NewDate(2021, 6, 24).IsZero() {
// tDay task.Date t.Errorf("exp false, got true")
// exp bool }
// }{ }
// {
// name: "after", func TestDateAfter(t *testing.T) {
// tDay: task.NewDate(2021, 1, 30), t.Parallel()
// exp: true,
// }, day := item.NewDate(2021, 1, 31)
// { for _, tc := range []struct {
// name: "on", name string
// tDay: day, tDay item.Date
// }, exp bool
// { }{
// name: "before", {
// tDay: task.NewDate(2021, 2, 1), name: "after",
// }, tDay: item.NewDate(2021, 1, 30),
// } { exp: true,
// t.Run(tc.name, func(t *testing.T) { },
// test.Equals(t, tc.exp, day.After(tc.tDay)) {
// }) 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 { type Event struct {
ID string `json:"id"` ID string `json:"id"`
Recurrer *Recur `json:"recurrer"` Recurrer Recurrer `json:"recurrer"`
RecurNext time.Time `json:"recurNext"` RecurNext time.Time `json:"recurNext"`
EventBody EventBody
} }
@ -103,9 +103,9 @@ func (e Event) Valid() bool {
if e.Duration.Seconds() < 1 { if e.Duration.Seconds() < 1 {
return false return false
} }
if e.Recurrer != nil && !e.Recurrer.Valid() { // if e.Recurrer != nil && !e.Recurrer.Valid() {
return false // return false
} // }
return true return true
} }

View File

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