diff --git a/sdk/models/replay.go b/sdk/models/replay.go index fead7f18..72478bbe 100644 --- a/sdk/models/replay.go +++ b/sdk/models/replay.go @@ -16,9 +16,10 @@ const maximumAllowedReplayDuration = time.Hour * 24 * 30 // Replay Struct used for posting replay entity. type Replay struct { - Project string `json:"project"` - Slo string `json:"slo"` - Duration ReplayDuration `json:"duration"` + Project string `json:"project"` + Slo string `json:"slo"` + Duration ReplayDuration `json:"duration,omitempty"` + TimeRange ReplayTimeRange `json:"timeRange,omitempty"` } type ReplayDuration struct { @@ -26,6 +27,11 @@ type ReplayDuration struct { Value int `json:"value"` } +type ReplayTimeRange struct { + StartDate time.Time `json:"startDate,omitempty"` + EndDate time.Time `json:"endDate,omitempty"` // not supported yet +} + // ReplayWithStatus used for returning Replay data with status. type ReplayWithStatus struct { Project string `json:"project"` @@ -72,10 +78,30 @@ var replayValidation = govy.New[Replay]( Required(), govy.For(func(r Replay) ReplayDuration { return r.Duration }). WithName("duration"). - Required(). + When( + func(r Replay) bool { + return !isEmpty(r.Duration) || (r.TimeRange.StartDate.IsZero() && isEmpty(r.Duration)) + }, + ). Cascade(govy.CascadeModeStop). Include(replayDurationValidation). Rules(replayDurationValidationRule()), + govy.For(func(r Replay) time.Time { return r.TimeRange.StartDate }). + WithName("startDate"). + When( + func(r Replay) bool { return !r.TimeRange.StartDate.IsZero() }, + ). + Rules( + replayStartTimeValidationRule(), + replayStartTimeNotInFutureValidationRule(), + ), + govy.For(func(r Replay) Replay { return r }). + Rules(govy.NewRule(func(r Replay) error { + if !isEmpty(r.Duration) && !r.TimeRange.StartDate.IsZero() { + return errors.New("only one of duration or startDate can be set") + } + return nil + }).WithErrorCode(replayDurationAndStartDateValidationError)), ) var replayDurationValidation = govy.New[ReplayDuration]( @@ -98,8 +124,10 @@ func (r Replay) Validate() error { } const ( - replayDurationValidationErrorCode = "replay_duration" - replayDurationUnitValidationErrorCode = "replay_duration_unit" + replayDurationValidationErrorCode = "replay_duration" + replayDurationUnitValidationErrorCode = "replay_duration_unit" + replayDurationAndStartDateValidationError = "replay_duration_or_start_date" + replayStartDateInTheFutureValidationError = "replay_duration_or_start_date_future" ) func replayDurationValidationRule() govy.Rule[ReplayDuration] { @@ -116,6 +144,27 @@ func replayDurationValidationRule() govy.Rule[ReplayDuration] { }).WithErrorCode(replayDurationValidationErrorCode) } +func replayStartTimeValidationRule() govy.Rule[time.Time] { + return govy.NewRule(func(v time.Time) error { + duration := time.Since(v) + if duration > maximumAllowedReplayDuration { + return errors.Errorf("%s duration must not be greater than %s", + duration, maximumAllowedReplayDuration) + } + return nil + }).WithErrorCode(replayDurationValidationErrorCode) +} + +func replayStartTimeNotInFutureValidationRule() govy.Rule[time.Time] { + return govy.NewRule(func(v time.Time) error { + now := time.Now() + if v.After(now) { + return errors.Errorf("startDate %s must not be in the future", v) + } + return nil + }).WithErrorCode(replayStartDateInTheFutureValidationError) +} + // ParseJSONToReplayStruct parse raw json into v1alpha.Replay struct with govy. func ParseJSONToReplayStruct(data io.Reader) (Replay, error) { replay := Replay{} @@ -168,3 +217,7 @@ func ValidateReplayDurationUnit(unit string) error { } return ErrInvalidReplayDurationUnit } + +func isEmpty(duration ReplayDuration) bool { + return duration.Unit == "" || duration.Value == 0 +} diff --git a/sdk/models/replay_test.go b/sdk/models/replay_test.go index 4b5d8976..4bc08e14 100644 --- a/sdk/models/replay_test.go +++ b/sdk/models/replay_test.go @@ -131,6 +131,75 @@ func TestReplayStructDatesValidation(t *testing.T) { isValid: false, ErrorCode: rules.ErrorCodeRequired, }, + { + name: "correct struct start date", + replay: Replay{ + Project: "project", + Slo: "slo", + TimeRange: ReplayTimeRange{ + StartDate: time.Now().Add(-time.Hour * 24), + }, + }, + isValid: true, + }, + { + name: "only one of duration or start date can be set", + replay: Replay{ + Project: "project", + Slo: "slo", + Duration: ReplayDuration{ + Unit: "Day", + Value: 30, + }, + TimeRange: ReplayTimeRange{ + StartDate: time.Now().Add(-time.Hour * 24), + }, + }, + isValid: false, + ErrorCode: replayDurationAndStartDateValidationError, + }, + { + name: "start date cannot be in the future", + replay: Replay{ + Project: "project", + Slo: "slo", + TimeRange: ReplayTimeRange{ + StartDate: time.Now().Add(time.Minute * 1), + }, + }, + isValid: false, + ErrorCode: replayStartDateInTheFutureValidationError, + }, + { + name: "use start date without duration", + replay: Replay{ + Project: "project", + Slo: "slo", + Duration: ReplayDuration{ + Unit: "", + Value: 0, + }, + TimeRange: ReplayTimeRange{ + StartDate: time.Now().Add(-time.Hour * 24), + }, + }, + isValid: true, + }, + { + name: "only one of duration", + replay: Replay{ + Project: "project", + Slo: "slo", + Duration: ReplayDuration{ + Unit: "Day", + Value: 30, + }, + TimeRange: ReplayTimeRange{ + StartDate: time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC), + }, + }, + isValid: true, + }, } for _, tt := range tests {