T2.1: Add domain.Schedule value object

Introduce src/domain/schedule.go with a Schedule value object that
centralizes schedule parsing and validation: Parse, Validate, and
Next(time.Time). It owns the cron parser and @every handling, moved out
of the scheduler. The scheduler's nextRunTime is kept as a thin wrapper
delegating to domain.Parse for now (T2.2 will parse once on load/edit).

Add unit tests covering invalid specs, @every intervals, five-field cron,
cron descriptors, whitespace trimming, the zero-value Next, and String.

Mark T2.1 complete in REFACTORING.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
mixeme
2026-06-18 22:55:20 +03:00
parent 9938164c7a
commit 0f17782174
4 changed files with 195 additions and 21 deletions
+4 -20
View File
@@ -10,11 +10,8 @@ import (
"gitea.mixdep.ru/mix/gosentry/src/domain"
"gitea.mixdep.ru/mix/gosentry/src/storage"
"gitea.mixdep.ru/mix/gosentry/src/runner"
"github.com/robfig/cron/v3"
)
var cronParser = cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor)
// Scheduler owns the timing loop for jobs that are currently loaded in the GUI.
// It receives a pointer to the jobs slice because the GUI edits the same slice;
// this keeps the early architecture simple while storage and scheduling are
@@ -230,24 +227,11 @@ func (s *Scheduler) prepareNextRun(job *domain.Job, from time.Time) {
job.NextRun = job.NextDue.Format("2006-01-02 15:04:05")
}
// nextRunTime is a thin wrapper over domain.Schedule kept for the scheduler's
// existing call sites. It parses the schedule on every call for now; T2.2
// replaces this with a Schedule parsed once on load/edit.
func nextRunTime(schedule string, from time.Time) (time.Time, bool) {
schedule = strings.TrimSpace(schedule)
if schedule == "" {
return time.Time{}, false
}
if strings.HasPrefix(schedule, "@every ") {
// @every is kept alongside cron because it is convenient for quick tests
// and for simple intervals that are awkward to express as five fields.
interval, err := time.ParseDuration(strings.TrimSpace(strings.TrimPrefix(schedule, "@every ")))
if err != nil || interval <= 0 {
return time.Time{}, false
}
return from.Add(interval), true
}
// Standard five-field cron keeps GoSentry compatible with the mental model
// users already know from Unix cron, while robfig/cron handles edge cases
// such as ranges, steps, and day-of-week names.
parsed, err := cronParser.Parse(schedule)
parsed, err := domain.Parse(schedule)
if err != nil {
return time.Time{}, false
}