diff --git a/.vscode/settings.json b/.vscode/settings.json index 2005365..0d119e3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,6 +4,7 @@ "--fast" ], "cSpell.words": [ + "alloc", "astrolib", "bodyclose", "cmds", diff --git a/Taskfile.yml b/Taskfile.yml index 4e8afe9..8dcb94e 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -20,6 +20,10 @@ tasks: # === test ================================================= + dry: + cmds: + - ginkgo -v --dry-run ./... + t: cmds: - go test ./... @@ -28,9 +32,9 @@ tasks: cmds: - go test ./i18n - dry: + tp: cmds: - - ginkgo -v --dry-run ./... + - go test ./pref # === ginkgo ================================================ diff --git a/core/core-defs.go b/core/core-defs.go index f8b43ae..9b065aa 100644 --- a/core/core-defs.go +++ b/core/core-defs.go @@ -2,3 +2,6 @@ package core // core contains universal definitions and handles cross cutting concerns // try to keep to a minimum to reduce rippling changes + +type TraverseResult interface { +} diff --git a/cycle/cycle-defs.go b/cycle/cycle-defs.go index e0e1bcc..ba66f9b 100644 --- a/cycle/cycle-defs.go +++ b/cycle/cycle-defs.go @@ -1,9 +1,39 @@ package cycle -// cycle represents life cycle events +import ( + "github.com/snivilised/traverse/core" + "github.com/snivilised/traverse/event" +) + +// cycle represents life cycle events; can't use prefs // beforeX // afterX // eg beforeOptions // afterOptions + +type ( + Event[F any] interface { + // On subscribes to a life cycle event + On(handler F) + } + + // SimpleHandler is a function that takes no parameters and can + // be used by any notification with this signature. + SimpleHandler func() + + // BeginHandler invoked before traversal begins + BeginHandler func(root string) + + // EndHandler invoked at the end of traversal + EndHandler func(result core.TraverseResult) + + // HibernateHandler is a generic handler that is used by hibernation + // to indicate wake or sleep. + HibernateHandler func(description string) + + // NodeHandler is a generic handler that is for any notification that contains + // the traversal node, such as directory ascend or descend. + NodeHandler func(node *event.Node) +) diff --git a/cycle/cycle_suite_test.go b/cycle/cycle_suite_test.go new file mode 100644 index 0000000..2a52e24 --- /dev/null +++ b/cycle/cycle_suite_test.go @@ -0,0 +1,13 @@ +package cycle_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" //nolint:revive // ok + . "github.com/onsi/gomega" //nolint:revive // ok +) + +func TestCycle(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Cycle Suite") +} diff --git a/cycle/notify.go b/cycle/notify.go new file mode 100644 index 0000000..d3a8ff6 --- /dev/null +++ b/cycle/notify.go @@ -0,0 +1,156 @@ +package cycle + +import ( + "github.com/snivilised/traverse/core" + "github.com/snivilised/traverse/event" +) + +type ( + broadcasterFunc[F any] func(listeners []F) F + + Dispatch[F any] struct { + Invoke F + broadcaster broadcasterFunc[F] + } + + // NotificationCtrl contains the handler function to be invoked. The control + // is agnostic to the handler's signature and therefore can not invoke it. + NotificationCtrl[F any] struct { + Dispatch Dispatch[F] + subscribed bool + listeners []F + } + + Events struct { // --> options + Ascend Event[NodeHandler] + Begin Event[BeginHandler] + Descend Event[NodeHandler] + End Event[EndHandler] + Start Event[HibernateHandler] + Stop Event[HibernateHandler] + } + + Controls struct { // --> registry + Ascend NotificationCtrl[NodeHandler] + Begin NotificationCtrl[BeginHandler] + Descend NotificationCtrl[NodeHandler] + End NotificationCtrl[EndHandler] + Start NotificationCtrl[HibernateHandler] + Stop NotificationCtrl[HibernateHandler] + } +) + +var ( + AscendDispatcher Dispatch[NodeHandler] + BeginDispatcher Dispatch[BeginHandler] + DescendDispatcher Dispatch[NodeHandler] + EndDispatcher Dispatch[EndHandler] + StartDispatcher Dispatch[HibernateHandler] + StopDispatcher Dispatch[HibernateHandler] +) + +func init() { + AscendDispatcher = Dispatch[NodeHandler]{ + Invoke: func(_ *event.Node) {}, + broadcaster: BroadcastNode, + } + + BeginDispatcher = Dispatch[BeginHandler]{ + Invoke: func(_ string) {}, + broadcaster: BroadcastBegin, + } + + DescendDispatcher = Dispatch[NodeHandler]{ + Invoke: func(_ *event.Node) {}, + broadcaster: BroadcastNode, + } + + EndDispatcher = Dispatch[EndHandler]{ + Invoke: func(_ core.TraverseResult) {}, + broadcaster: BroadcastEnd, + } + + StartDispatcher = Dispatch[HibernateHandler]{ + Invoke: func(_ string) {}, + broadcaster: BroadcastHibernate, + } + + StopDispatcher = Dispatch[HibernateHandler]{ + Invoke: func(_ string) {}, + broadcaster: BroadcastHibernate, + } +} + +// Bind attaches the underlying notification controllers to the +// Events. +func (e *Events) Bind(cs *Controls) { + e.Ascend = &cs.Ascend + e.Begin = &cs.Begin + e.Descend = &cs.Descend + e.End = &cs.End + e.Start = &cs.Start + e.Stop = &cs.Stop +} + +// On subscribes to a life cycle event +func (c *NotificationCtrl[F]) On(handler F) { + if !c.subscribed { + c.Dispatch.Invoke = handler + c.subscribed = true + + return + } + + if c.listeners == nil { + const alloc = 2 + c.listeners = make([]F, 0, alloc) + c.listeners = append(c.listeners, c.Dispatch.Invoke) + } + + c.listeners = append(c.listeners, handler) + c.Dispatch.Invoke = c.broadcaster(c.listeners) +} + +func (c *NotificationCtrl[F]) broadcaster(listeners []F) F { + return c.Dispatch.broadcaster(listeners) +} + +func BroadcastBegin(listeners []BeginHandler) BeginHandler { + return func(root string) { + for _, listener := range listeners { + listener(root) + } + } +} + +func BroadcastEnd(listeners []EndHandler) EndHandler { + return func(result core.TraverseResult) { + for _, listener := range listeners { + listener(result) + } + } +} + +func BroadcastNode(listeners []NodeHandler) NodeHandler { + return func(node *event.Node) { + for _, listener := range listeners { + listener(node) + } + } +} + +func BroadcastHibernate(listeners []HibernateHandler) HibernateHandler { + return func(description string) { + for _, listener := range listeners { + listener(description) + } + } +} + +func BroadcastSimple(listeners []SimpleHandler) SimpleHandler { + return func() { + for _, listener := range listeners { + listener() + } + } +} diff --git a/cycle/notify_test.go b/cycle/notify_test.go new file mode 100644 index 0000000..f12c8ca --- /dev/null +++ b/cycle/notify_test.go @@ -0,0 +1,47 @@ +package cycle_test + +import ( + . "github.com/onsi/ginkgo/v2" //nolint:revive // ok + . "github.com/onsi/gomega" //nolint:revive // ok + + "github.com/snivilised/traverse/core" + "github.com/snivilised/traverse/cycle" +) + +var _ = Describe("Notify", func() { + Context("foo", func() { + It("should:", func() { + const path = "/traversal-root" + + var ( + notifications cycle.Controls + taps cycle.Events + begun bool + ended bool + ) + + // init(registry->options): + // + taps.Bind(¬ifications) + + // client: + // + taps.Begin.On(func(root string) { + begun = true + Expect(root).To(Equal(path)) + }) + + taps.End.On(func(_ core.TraverseResult) { + ended = true + }) + + // component side: + // + notifications.Begin.Dispatch.Invoke(path) + notifications.End.Dispatch.Invoke(nil) + + Expect(begun).To(BeTrue()) + Expect(ended).To(BeTrue()) + }) + }) +}) diff --git a/enums/hibernation-en.go b/enums/hibernation-en.go index 7eeb99b..7ff12f0 100644 --- a/enums/hibernation-en.go +++ b/enums/hibernation-en.go @@ -20,7 +20,7 @@ const ( // HibernationPending // pending-hibernation - // HibernationActive conditional listening is active (callback is invoked) + // HibernationAwake conditional listening is active (callback is invoked) // HibernationAwake // awake-hibernation diff --git a/pref/options.go b/pref/options.go index ab33f10..9c94c97 100644 --- a/pref/options.go +++ b/pref/options.go @@ -3,10 +3,11 @@ package pref import ( "log/slog" + "github.com/snivilised/traverse/cycle" "github.com/snivilised/traverse/enums" ) -// package: pref contains user option definitions; do not use anything in nav (cyclic) +// package: pref contains user option definitions; do not use anything in kernel (cyclic) type ( Options struct { @@ -23,6 +24,10 @@ type ( // Sampler SamplerOptions + // Events provides the ability to tap into life cycle events + // + Events cycle.Events + // Monitor contains externally provided logger // Monitor MonitorOptions @@ -32,25 +37,26 @@ type ( OptionFn func(o *Options, reg *Registry) error ) -func requestOptions(with ...OptionFn) *Options { - o := getDetDefaultOptions() - reg := &Registry{} +func RequestOptions(reg *Registry, with ...OptionFn) *Options { + o := defaultOptions() + o.Events.Bind(®.Notification) - for _, functionalOption := range with { + for _, option := range with { // TODO: check error - _ = functionalOption(o, reg) + _ = option(o, reg) } + reg.O = o + return o } -func getDetDefaultOptions() *Options { +func defaultOptions() *Options { nopLogger := &slog.Logger{} - return &Options{ + o := &Options{ Core: CoreOptions{ Subscription: enums.SubscribeUniversal, - Behaviours: NavigationBehaviours{ SubPath: SubPathBehaviour{ KeepTrailingSep: true, @@ -72,4 +78,6 @@ func getDetDefaultOptions() *Options { Log: nopLogger, }, } + + return o } diff --git a/pref/options_test.go b/pref/options_test.go new file mode 100644 index 0000000..f90ba8e --- /dev/null +++ b/pref/options_test.go @@ -0,0 +1,65 @@ +package pref_test + +import ( + . "github.com/onsi/ginkgo/v2" //nolint:revive // ok + . "github.com/onsi/gomega" //nolint:revive // ok + + "github.com/snivilised/traverse/pref" +) + +var _ = Describe("Options", func() { + Context("Init", func() { + Context("RequestOptions", func() { + Context("Notification", func() { + When("client listens", func() { + It("should: invoke client's handler", func() { + begun := false + reg := pref.NewRegistry() + o := pref.RequestOptions(reg) + + o.Events.Begin.On(func(_ string) { + begun = true + }) + reg.Notification.Begin.Dispatch.Invoke("/traversal-root") + + Expect(begun).To(BeTrue()) + }) + }) + + When("multiple listeners", func() { + It("should: broadcast", func() { + count := 0 + reg := pref.NewRegistry() + o := pref.RequestOptions(reg) + + o.Events.Begin.On(func(_ string) { + count++ + }) + o.Events.Begin.On(func(_ string) { + count++ + }) + reg.Notification.Begin.Dispatch.Invoke("/traversal-root") + Expect(count).To(Equal(2), "not all listeners were invoked for first notification") + + count = 0 + o.Events.Begin.On(func(_ string) { + count++ + }) + + reg.Notification.Begin.Dispatch.Invoke("/another-root") + Expect(count).To(Equal(3), "not all listeners were invoked for second notification") + }) + }) + + When("no subscription", func() { + It("should: ...", func() { + reg := pref.NewRegistry() + _ = pref.RequestOptions(reg) + + reg.Notification.Begin.Dispatch.Invoke("/traversal-root") + }) + }) + }) + }) + }) +}) diff --git a/pref/pref_suite_test.go b/pref/pref_suite_test.go new file mode 100644 index 0000000..785c715 --- /dev/null +++ b/pref/pref_suite_test.go @@ -0,0 +1,13 @@ +package pref_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" //nolint:revive // ok + . "github.com/onsi/gomega" //nolint:revive // ok +) + +func TestPref(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Pref Suite") +} diff --git a/pref/registry.go b/pref/registry.go index 112bd33..72faf6a 100644 --- a/pref/registry.go +++ b/pref/registry.go @@ -1,9 +1,38 @@ package pref -type ( +import ( + "github.com/snivilised/traverse/cycle" +) +type ( // Registry contains items derived from Options Registry struct { - o *Options + O *Options + Notification cycle.Controls } ) + +func NewRegistry() *Registry { + return &Registry{ + Notification: cycle.Controls{ + Ascend: cycle.NotificationCtrl[cycle.NodeHandler]{ + Dispatch: cycle.DescendDispatcher, + }, + Begin: cycle.NotificationCtrl[cycle.BeginHandler]{ + Dispatch: cycle.BeginDispatcher, + }, + Descend: cycle.NotificationCtrl[cycle.NodeHandler]{ + Dispatch: cycle.DescendDispatcher, + }, + End: cycle.NotificationCtrl[cycle.EndHandler]{ + Dispatch: cycle.EndDispatcher, + }, + Start: cycle.NotificationCtrl[cycle.HibernateHandler]{ + Dispatch: cycle.StartDispatcher, + }, + Stop: cycle.NotificationCtrl[cycle.HibernateHandler]{ + Dispatch: cycle.StopDispatcher, + }, + }, + } +} diff --git a/tapable/tapable_test.go b/tapable/tapable_test.go index 92c9954..769751b 100644 --- a/tapable/tapable_test.go +++ b/tapable/tapable_test.go @@ -105,7 +105,8 @@ var _ = Describe("Tapable", decorators.Label("use-case"), func() { It("should: ", func() { // This could be exposed to the client as a WithXXX option, // or the client could perform the Tap manually themselves. - // + // Container is an internal affair, it should be created and + // used internally only. container := tapable.NewContainer[enums.Role]() // perhaps we make the tapping mechanism internal only and if