Skip to content

Commit

Permalink
Improve UI for StartTime and fix StartTime struct
Browse files Browse the repository at this point in the history
  • Loading branch information
calvinmclean committed Jun 17, 2024
1 parent 62a0044 commit 2b1a633
Show file tree
Hide file tree
Showing 18 changed files with 239 additions and 133 deletions.
16 changes: 8 additions & 8 deletions garden-app/integration_tests/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ func GardenTests(t *testing.T) {
// Create new Garden with LightOnTime in the near future, so LightDelay will assume the light is currently off,
// meaning adhoc action is going to be predictably delayed
maxZones := uint(1)
startTime := &pkg.StartTime{time.Now().In(time.Local).Add(1 * time.Second).Truncate(time.Second)}
startTime := pkg.NewStartTime(time.Now().In(time.Local).Add(1 * time.Second).Truncate(time.Second))
newGarden := &pkg.Garden{
Name: "TestGarden",
TopicPrefix: "test",
Expand Down Expand Up @@ -197,7 +197,7 @@ func GardenTests(t *testing.T) {
status, err = makeRequest(http.MethodGet, fmt.Sprintf("/gardens/%s", g.ID.String()), http.NoBody, &getG)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, status)
assert.Equal(t, startTime.Add(1*time.Second), getG.NextLightAction.Time.Local())
assert.Equal(t, startTime.Time.Add(1*time.Second), getG.NextLightAction.Time.Local())

time.Sleep(3 * time.Second)

Expand All @@ -209,7 +209,7 @@ func GardenTests(t *testing.T) {
})
t.Run("ChangeLightScheduleStartTimeResetsLightSchedule", func(t *testing.T) {
// Reschedule Light to turn in in 1 second, for 1 second
newStartTime := &pkg.StartTime{time.Now().Add(1 * time.Second).Truncate(time.Second)}
newStartTime := pkg.NewStartTime(time.Now().Add(1 * time.Second).Truncate(time.Second))
var g server.GardenResponse
status, err := makeRequest(http.MethodPatch, "/gardens/"+gardenID, pkg.Garden{
LightSchedule: &pkg.LightSchedule{
Expand All @@ -228,7 +228,7 @@ func GardenTests(t *testing.T) {
status, err = makeRequest(http.MethodGet, "/gardens/"+gardenID, nil, &g2)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, status)
assert.Equal(t, newStartTime.String(), (&pkg.StartTime{g2.NextLightAction.Time.Local()}).String())
assert.Equal(t, newStartTime.String(), pkg.NewStartTime(g2.NextLightAction.Time.Local()).String())
assert.Equal(t, pkg.LightStateOn, g2.NextLightAction.State)

time.Sleep(2 * time.Second)
Expand Down Expand Up @@ -371,12 +371,12 @@ func ZoneTests(t *testing.T) {
newStartTime := time.Now().Add(2 * time.Second).Truncate(time.Second)
var ws server.WaterScheduleResponse
status, err := makeRequest(http.MethodPatch, "/water_schedules/"+waterScheduleID, pkg.WaterSchedule{
StartTime: &pkg.StartTime{newStartTime},
StartTime: pkg.NewStartTime(newStartTime),
Duration: &pkg.Duration{Duration: time.Second},
}, &ws)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, status)
assert.Equal(t, (&pkg.StartTime{newStartTime}).String(), ws.WaterSchedule.StartTime.String())
assert.Equal(t, pkg.NewStartTime(newStartTime).String(), ws.WaterSchedule.StartTime.String())

time.Sleep(100 * time.Millisecond)

Expand Down Expand Up @@ -422,12 +422,12 @@ func WaterScheduleTests(t *testing.T) {
newStartTime := time.Now().Add(2 * time.Second).Truncate(time.Second)
var ws server.WaterScheduleResponse
status, err := makeRequest(http.MethodPatch, "/water_schedules/"+waterScheduleID, pkg.WaterSchedule{
StartTime: &pkg.StartTime{newStartTime},
StartTime: pkg.NewStartTime(newStartTime),
Duration: &pkg.Duration{Duration: time.Second},
}, &ws)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, status)
assert.Equal(t, (&pkg.StartTime{newStartTime}).String(), ws.WaterSchedule.StartTime.String())
assert.Equal(t, pkg.NewStartTime(newStartTime).String(), ws.WaterSchedule.StartTime.String())

time.Sleep(100 * time.Millisecond)

Expand Down
36 changes: 36 additions & 0 deletions garden-app/pkg/duration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package pkg

import (
"encoding/json"
"net/url"
"testing"
"time"

"github.com/ajg/form"
"github.com/stretchr/testify/assert"
"gopkg.in/yaml.v3"
)
Expand Down Expand Up @@ -158,3 +160,37 @@ func TestDurationYAMLMarshal(t *testing.T) {
assert.Equal(t, "cron:*/5 * * * 1\n", string(result))
})
}

func TestDurationUnmarshalText(t *testing.T) {
tests := []struct {
name string
input url.Values
expected Duration
}{
{
"DurationString",
url.Values{
"Duration": []string{"1m0s"},
},
Duration{Duration: 1 * time.Minute},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var result struct {
Duration Duration
}
err := form.DecodeString(&result, tt.input.Encode())
assert.NoError(t, err)
assert.Equal(t, tt.expected, result.Duration)

var formResult struct {
Duration Duration
}
err = form.DecodeValues(&formResult, tt.input)
assert.NoError(t, err)
assert.Equal(t, tt.expected, formResult.Duration)
})
}
}
15 changes: 12 additions & 3 deletions garden-app/pkg/garden.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,9 +158,12 @@ func (g *Garden) Bind(r *http.Request) error {
} else if *g.MaxZones == 0 {
return errors.New("max_zones must not be 0")
}
// consider empty LightSchedule as nil
if g.LightSchedule != nil && (g.LightSchedule.Duration == nil || g.LightSchedule.Duration.Duration == 0) && g.LightSchedule.StartTime == nil {
g.LightSchedule = nil
// consider empty LightSchedule as nil for removing from HTML form
if g.LightSchedule != nil && (g.LightSchedule.Duration == nil || g.LightSchedule.Duration.Duration == 0) {
startTimeEmpty := g.LightSchedule.StartTime == nil || g.LightSchedule.StartTime.Time.IsZero()
if startTimeEmpty {
g.LightSchedule = nil

Check warning on line 165 in garden-app/pkg/garden.go

View check run for this annotation

Codecov / codecov/patch

garden-app/pkg/garden.go#L163-L165

Added lines #L163 - L165 were not covered by tests
}
}
if g.LightSchedule != nil {
if g.LightSchedule.Duration == nil {
Expand All @@ -185,6 +188,12 @@ func (g *Garden) Bind(r *http.Request) error {
}

if g.LightSchedule != nil {
if g.LightSchedule.StartTime != nil {
err = g.LightSchedule.StartTime.Validate()
if err != nil {
return err

Check warning on line 194 in garden-app/pkg/garden.go

View check run for this annotation

Codecov / codecov/patch

garden-app/pkg/garden.go#L194

Added line #L194 was not covered by tests
}
}
// Check that Duration is valid Duration
if g.LightSchedule.Duration != nil {
if g.LightSchedule.Duration.Duration >= 24*time.Hour {
Expand Down
4 changes: 2 additions & 2 deletions garden-app/pkg/garden_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ func TestGardenPatch(t *testing.T) {
{
"PatchLightSchedule.StartTime",
&Garden{LightSchedule: &LightSchedule{
StartTime: &StartTime{Time: time.Date(0, 1, 1, 15, 4, 0, 0, time.FixedZone("", 0))},
StartTime: NewStartTime(time.Date(0, 1, 1, 15, 4, 0, 0, time.FixedZone("", 0))),
}},
},
{
Expand Down Expand Up @@ -159,7 +159,7 @@ func TestGardenPatch(t *testing.T) {
t.Run("RemoveLightSchedule", func(t *testing.T) {
g := &Garden{
LightSchedule: &LightSchedule{
StartTime: &StartTime{Time: time.Date(0, 1, 1, 15, 4, 0, 0, time.FixedZone("", 0))},
StartTime: NewStartTime(time.Date(0, 1, 1, 15, 4, 0, 0, time.FixedZone("", 0))),
Duration: &Duration{2 * time.Hour, ""},
},
}
Expand Down
62 changes: 28 additions & 34 deletions garden-app/pkg/start_time.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
package pkg

import (
"bytes"
"encoding/json"
"fmt"
"strconv"
"time"

"github.com/ajg/form"
)

const (
Expand All @@ -18,30 +15,43 @@ const (
// StartTime allows for special handling of Time without the date and also allows several
// formats for decoding so it is more easily compatible with HTML forms
type StartTime struct {
time.Time
}
Time time.Time `form:"-"`

type startTimeSplit struct {
Hour int
Minute int
TZ string
}

func (st *startTimeSplit) String() string {
return fmt.Sprintf("%02d:%02d:00%s", st.Hour, st.Minute, st.TZ)
}

func StartTimeFromString(startTime string) (*StartTime, error) {
result, err := time.Parse(startTimeFormat, startTime)
if err != nil {
return nil, fmt.Errorf("error parsing start time: %w", err)

Check warning on line 28 in garden-app/pkg/start_time.go

View check run for this annotation

Codecov / codecov/patch

garden-app/pkg/start_time.go#L28

Added line #L28 was not covered by tests
}

return &StartTime{result}, nil
return &StartTime{Time: result}, nil
}

func NewStartTime(t time.Time) *StartTime {
return &StartTime{Time: t}
}

func (st *StartTime) String() string {
return st.Format(startTimeFormat)
return st.Time.Format(startTimeFormat)
}

// Validate is used after parsing from HTML form so the time can be parsed
func (st *StartTime) Validate() error {
if !st.Time.IsZero() {
return nil
}

result, err := time.Parse(startTimeFormat, fmt.Sprintf("%02d:%02d:00%s", st.Hour, st.Minute, st.TZ))
if err != nil {
return err

Check warning on line 50 in garden-app/pkg/start_time.go

View check run for this annotation

Codecov / codecov/patch

garden-app/pkg/start_time.go#L48-L50

Added lines #L48 - L50 were not covered by tests
}
st.Time = result

Check warning on line 52 in garden-app/pkg/start_time.go

View check run for this annotation

Codecov / codecov/patch

garden-app/pkg/start_time.go#L52

Added line #L52 was not covered by tests

return nil

Check warning on line 54 in garden-app/pkg/start_time.go

View check run for this annotation

Codecov / codecov/patch

garden-app/pkg/start_time.go#L54

Added line #L54 was not covered by tests
}

func (st *StartTime) MarshalJSON() ([]byte, error) {
Expand All @@ -60,13 +70,17 @@ func (st *StartTime) UnmarshalJSON(data []byte) error {
case string:
timeString = v
case map[string]any:
var splitTime startTimeSplit
var splitTime struct {
Hour int
Minute int
TZ string

Check warning on line 76 in garden-app/pkg/start_time.go

View check run for this annotation

Codecov / codecov/patch

garden-app/pkg/start_time.go#L72-L76

Added lines #L72 - L76 were not covered by tests
}
err := json.Unmarshal(data, &splitTime)
if err != nil {
return err

Check warning on line 80 in garden-app/pkg/start_time.go

View check run for this annotation

Codecov / codecov/patch

garden-app/pkg/start_time.go#L78-L80

Added lines #L78 - L80 were not covered by tests
}

timeString = splitTime.String()
timeString = fmt.Sprintf("%02d:%02d:00%s", splitTime.Hour, splitTime.Minute, splitTime.TZ)
default:
return fmt.Errorf("unexpected type %T, must be string or object", v)

Check warning on line 85 in garden-app/pkg/start_time.go

View check run for this annotation

Codecov / codecov/patch

garden-app/pkg/start_time.go#L83-L85

Added lines #L83 - L85 were not covered by tests
}
Expand All @@ -80,26 +94,6 @@ func (st *StartTime) UnmarshalJSON(data []byte) error {
return nil
}

func (st *StartTime) UnmarshalText(data []byte) error {
var timeString string

var splitTime startTimeSplit
err := form.NewDecoder(bytes.NewBuffer(data)).Decode(&splitTime)
if err != nil {
timeString = string(data)
} else {
timeString = splitTime.String()
}

startTime, err := StartTimeFromString(timeString)
if err != nil {
return err
}
st.Time = startTime.Time

return nil
}

// TimeLocationFromOffset uses an offset minutes from JS `new Date().getTimezoneOffset()` and parses it into
// Go's time.Location. JS offsets are positive if they are behind UTC
func TimeLocationFromOffset(offsetMinutes string) (*time.Location, error) {
Expand Down
Loading

0 comments on commit 2b1a633

Please sign in to comment.