From aec96efa2857240c282bc142380af81133363da5 Mon Sep 17 00:00:00 2001 From: Sergey Basov Date: Thu, 5 Feb 2026 18:30:22 +0300 Subject: [PATCH 1/3] feat: add optional schedule parser into the NewManager --- cron.go | 46 ++++++++++++++++++++++++++++++++++++++++++---- cron_test.go | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 4 deletions(-) diff --git a/cron.go b/cron.go index 4c9f0c6..26f6b5f 100644 --- a/cron.go +++ b/cron.go @@ -48,6 +48,7 @@ func (ss Schedule) IsActive() bool { return ss != Schedule(stateDisabled) && ss // Manager is a Cron manager with context and middleware support. type Manager struct { cron *cron.Cron + parser *cron.Parser middleware []MiddlewareFunc jobs []job muState sync.Mutex @@ -72,10 +73,35 @@ type jobState struct { duration time.Duration } -func NewManager() *Manager { - return &Manager{ - cron: cron.New(), +type Option func(*Manager) + +// WithParser defines the custom Schedule parser for the Manager. +func WithParser(parser cron.Parser) Option { + return func(m *Manager) { + m.parser = &parser + } +} + +// WithStandardParser makes Manager to use the standard Schedule parser. +func WithStandardParser() Option { + return func(m *Manager) { + m.parser = nil + } +} + +func NewManager(opts ...Option) *Manager { + m := &Manager{} + for _, opt := range opts { + opt(m) } + + cronOpt := make([]cron.Option, 0, 1) + if m.parser != nil { + cronOpt = append(cronOpt, cron.WithParser(*m.parser)) + } + m.cron = cron.New(cronOpt...) + + return m } // AddFunc adds func to cron. @@ -106,7 +132,7 @@ func (cm *Manager) validateJobs() (string, error) { // parse schedule if job.schedule.IsActive() { - _, err := cron.ParseStandard(job.schedule.String()) + _, err := cm.parseSchedule(job.schedule) if err != nil { return job.name, err } @@ -189,6 +215,18 @@ func (cm *Manager) Stop() context.Context { return cm.cron.Stop() } +// parseSchedule parses the given schedule using the registered Parser or with Standard Parser if none. +func (cm *Manager) parseSchedule(s Schedule) (cron.Schedule, error) { + var fn func(s string) (cron.Schedule, error) + if cm.parser != nil { + fn = cm.parser.Parse + } else { + fn = cron.ParseStandard + } + + return fn(s.String()) +} + // updateState set. func (cm *Manager) updateState(idx int, state cronState, err error) { cm.muState.Lock() diff --git a/cron_test.go b/cron_test.go index f4bf0f9..97bf413 100644 --- a/cron_test.go +++ b/cron_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + "github.com/robfig/cron/v3" . "github.com/smartystreets/goconvey/convey" ) @@ -47,6 +48,47 @@ func TestManager_Validate(t *testing.T) { }) } +func TestManager_Options(t *testing.T) { + Convey("Test NewManager with custom parser", t, func() { + p := cron.NewParser(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor) + m := NewManager(WithParser(p)) + + Convey("When standard schedule format", func() { + m.AddFunc("f1", "0 0 * * *", newCronFunc("f1")) + + name, err := m.validateJobs() + So(err, ShouldNotBeNil) + So(name, ShouldEqual, "f1") + }) + + Convey("When custom schedule format", func() { + m.AddFunc("f2", "10 * * * * *", newCronFunc("f1")) + + _, err := m.validateJobs() + So(err, ShouldBeNil) + }) + }) + + Convey("Test NewManager with standard parser", t, func() { + m := NewManager(WithStandardParser()) + + Convey("When standard schedule format", func() { + m.AddFunc("f1", "0 0 * * *", newCronFunc("f1")) + + _, err := m.validateJobs() + So(err, ShouldBeNil) + }) + + Convey("When custom schedule format", func() { + m.AddFunc("f2", "10 * * * * *", newCronFunc("f1")) + + name, err := m.validateJobs() + So(err, ShouldNotBeNil) + So(name, ShouldEqual, "f2") + }) + }) +} + func TestManager_Run(t *testing.T) { Convey("Test validate function", t, func() { ctx := t.Context() From 4b9ef8b764086a83e1f4bd00d71bc970701a6872 Mon Sep 17 00:00:00 2001 From: Sergey Basov Date: Thu, 5 Feb 2026 20:03:32 +0300 Subject: [PATCH 2/3] fix: avoid import the robfig/cron in the outside usage --- README.md | 20 ++++++++++++++++++++ cron.go | 26 +++++++++++++++++--------- cron_test.go | 6 ++---- 3 files changed, 39 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index f90e716..6815c3e 100644 --- a/README.md +++ b/README.md @@ -80,3 +80,23 @@ Please see `examples/main.go` for basic usage. http.HandleFunc("/debug/cron", m.Handler) ``` + +## Schedule parser customization + +To use non-default schedule parser, invoke the `NewManager()` with the `WithParseOptions()` option: + +```go +// Init some custom parser, +// see https://pkg.go.dev/github.com/robfig/cron/v3#hdr-Alternative_Formats for more info. +m := cron.NewManager(cron.WithParseOptions(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor)) + +// Use a schedule format acceptable by the custom parser. +m.AddFunc("f1", "* * * * * *", func(_ context.Context) error { + // Do some important stuff... + return nil +}) + +if err := m.Run(context.Background()); err != nil { + log.Fatal(err) +} +``` \ No newline at end of file diff --git a/cron.go b/cron.go index 26f6b5f..db15c9f 100644 --- a/cron.go +++ b/cron.go @@ -21,6 +21,20 @@ const ( stateSkipped cronState = "skipped" ) +type ParseOption int + +const ( + Second ParseOption = 1 << iota // Seconds field, default 0 + SecondOptional // Optional seconds field, default 0 + Minute // Minutes field, default 0 + Hour // Hours field, default 0 + Dom // Day of month field, default * + Month // Month field, default * + Dow // Day of week field, default * + DowOptional // Optional day of week field, default * + Descriptor // Allow descriptors such as @monthly, @weekly, etc. +) + var ( ErrSkipped = errors.New("skipped") ErrNotFound = errors.New("job not found") @@ -75,20 +89,14 @@ type jobState struct { type Option func(*Manager) -// WithParser defines the custom Schedule parser for the Manager. -func WithParser(parser cron.Parser) Option { +// WithParseOptions defines the custom Schedule parser options for the Manager. +func WithParseOptions(parseOptions ParseOption) Option { + parser := cron.NewParser(cron.ParseOption(parseOptions)) return func(m *Manager) { m.parser = &parser } } -// WithStandardParser makes Manager to use the standard Schedule parser. -func WithStandardParser() Option { - return func(m *Manager) { - m.parser = nil - } -} - func NewManager(opts ...Option) *Manager { m := &Manager{} for _, opt := range opts { diff --git a/cron_test.go b/cron_test.go index 97bf413..7ee14fa 100644 --- a/cron_test.go +++ b/cron_test.go @@ -7,7 +7,6 @@ import ( "testing" "time" - "github.com/robfig/cron/v3" . "github.com/smartystreets/goconvey/convey" ) @@ -50,8 +49,7 @@ func TestManager_Validate(t *testing.T) { func TestManager_Options(t *testing.T) { Convey("Test NewManager with custom parser", t, func() { - p := cron.NewParser(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor) - m := NewManager(WithParser(p)) + m := NewManager(WithParseOptions(Second | Minute | Hour | Dom | Month | Dow | Descriptor)) Convey("When standard schedule format", func() { m.AddFunc("f1", "0 0 * * *", newCronFunc("f1")) @@ -70,7 +68,7 @@ func TestManager_Options(t *testing.T) { }) Convey("Test NewManager with standard parser", t, func() { - m := NewManager(WithStandardParser()) + m := NewManager() Convey("When standard schedule format", func() { m.AddFunc("f1", "0 0 * * *", newCronFunc("f1")) From 8ca622fcfc93ff917af4f1dbbcae8bf217f6f8de Mon Sep 17 00:00:00 2001 From: Sergey Basov Date: Fri, 6 Feb 2026 10:51:32 +0300 Subject: [PATCH 3/3] fix: use NewManagerWithParser instead of options in constructor --- README.md | 7 +++++-- cron.go | 46 +++++++++++++--------------------------------- cron_test.go | 2 +- 3 files changed, 19 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 6815c3e..e800aa1 100644 --- a/README.md +++ b/README.md @@ -83,12 +83,15 @@ Please see `examples/main.go` for basic usage. ## Schedule parser customization -To use non-default schedule parser, invoke the `NewManager()` with the `WithParseOptions()` option: +To use non-default schedule parser, invoke the `NewManagerWithParser()` function: ```go // Init some custom parser, // see https://pkg.go.dev/github.com/robfig/cron/v3#hdr-Alternative_Formats for more info. -m := cron.NewManager(cron.WithParseOptions(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor)) +m := cron.NewManagerWithParser(cron.WithParseOptions(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor)) + +// Or use the predefined one: +m = cron.NewManagerWithParser(cron.ParserWithSeconds) // Use a schedule format acceptable by the custom parser. m.AddFunc("f1", "* * * * * *", func(_ context.Context) error { diff --git a/cron.go b/cron.go index db15c9f..6680244 100644 --- a/cron.go +++ b/cron.go @@ -21,26 +21,17 @@ const ( stateSkipped cronState = "skipped" ) -type ParseOption int - -const ( - Second ParseOption = 1 << iota // Seconds field, default 0 - SecondOptional // Optional seconds field, default 0 - Minute // Minutes field, default 0 - Hour // Hours field, default 0 - Dom // Day of month field, default * - Month // Month field, default * - Dow // Day of week field, default * - DowOptional // Optional day of week field, default * - Descriptor // Allow descriptors such as @monthly, @weekly, etc. -) - var ( ErrSkipped = errors.New("skipped") ErrNotFound = errors.New("job not found") ErrDuplicate = errors.New("duplicate cron name") ) +var ( + // ParserWithSeconds is a schedule parser with the leading seconds support. + ParserWithSeconds = cron.NewParser(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor) +) + type ( contextKey string cronState string @@ -87,29 +78,18 @@ type jobState struct { duration time.Duration } -type Option func(*Manager) - -// WithParseOptions defines the custom Schedule parser options for the Manager. -func WithParseOptions(parseOptions ParseOption) Option { - parser := cron.NewParser(cron.ParseOption(parseOptions)) - return func(m *Manager) { - m.parser = &parser +func NewManager() *Manager { + return &Manager{ + cron: cron.New(), } } -func NewManager(opts ...Option) *Manager { - m := &Manager{} - for _, opt := range opts { - opt(m) +// NewManagerWithParser creates new Manager instance with the custom schedule parser. +func NewManagerWithParser(parser cron.Parser) *Manager { + return &Manager{ + cron: cron.New(cron.WithParser(parser)), + parser: &parser, } - - cronOpt := make([]cron.Option, 0, 1) - if m.parser != nil { - cronOpt = append(cronOpt, cron.WithParser(*m.parser)) - } - m.cron = cron.New(cronOpt...) - - return m } // AddFunc adds func to cron. diff --git a/cron_test.go b/cron_test.go index 7ee14fa..d697a76 100644 --- a/cron_test.go +++ b/cron_test.go @@ -49,7 +49,7 @@ func TestManager_Validate(t *testing.T) { func TestManager_Options(t *testing.T) { Convey("Test NewManager with custom parser", t, func() { - m := NewManager(WithParseOptions(Second | Minute | Hour | Dom | Month | Dow | Descriptor)) + m := NewManagerWithParser(ParserWithSeconds) Convey("When standard schedule format", func() { m.AddFunc("f1", "0 0 * * *", newCronFunc("f1"))