From 2e0ef67ad5f09eb59ef36eee2b7fad4e1f71487a Mon Sep 17 00:00:00 2001 From: plastikfan Date: Thu, 16 May 2024 10:18:00 +0100 Subject: [PATCH] feat: formalise bootstrap phases (#29) --- builders.go | 56 +++++ core/core-defs.go | 52 ++++ core/errors.go | 14 ++ core/filtering.go | 4 + cycle/events.go | 4 +- director-error_test.go | 97 +++++++ director.go | 209 ++++++++++------ director_test.go | 305 +++++++++++++++++++++++ driver.go | 13 +- enums/subscription-en.go | 2 +- hiber/hibernate-plugin.go | 48 ++++ hiber/hibernation-defs.go | 24 +- internal-traverse-defs.go | 40 +-- internal/kernel/kernel-defs.go | 8 + internal/kernel/navigation-controller.go | 25 ++ internal/kernel/navigator-factory.go | 74 ++++++ internal/services/broker.go | 29 ++- internal/types/definitions.go | 25 +- pref/binder.go | 1 + pref/options-context.go | 4 + pref/options-core.go | 8 + pref/options-filter.go | 17 ++ pref/options-hibernate.go | 9 + pref/options-navigation-behaviours.go | 9 + pref/options.go | 47 ++-- pref/options_test.go | 18 +- refine/filter-plugin.go | 32 +++ sampling/sampling-plugin.go | 32 +++ session.go | 2 + traverse-api.go | 56 +++++ traverse_test.go | 122 --------- 31 files changed, 1104 insertions(+), 282 deletions(-) create mode 100644 builders.go create mode 100644 core/errors.go create mode 100644 core/filtering.go create mode 100644 director-error_test.go create mode 100644 director_test.go create mode 100644 hiber/hibernate-plugin.go create mode 100644 internal/kernel/navigator-factory.go create mode 100644 pref/options-filter.go create mode 100644 pref/options-hibernate.go create mode 100644 refine/filter-plugin.go create mode 100644 sampling/sampling-plugin.go delete mode 100644 traverse_test.go diff --git a/builders.go b/builders.go new file mode 100644 index 0000000..cfb7407 --- /dev/null +++ b/builders.go @@ -0,0 +1,56 @@ +package traverse + +import ( + "errors" + + "github.com/snivilised/traverse/core" + "github.com/snivilised/traverse/internal/types" + "github.com/snivilised/traverse/pref" +) + +type buildArtefacts struct { + o *pref.Options + nav core.Navigator + plugins []types.Plugin +} + +type Builders struct { + ob optionsBuilder + nb navigatorBuilder + pb pluginsBuilder +} + +func (bs *Builders) buildAll() (*buildArtefacts, error) { + o, optionsErr := bs.ob.build() + if optionsErr != nil { + return nil, optionsErr + } + + nav, navErr := bs.nb.build(o) + if navErr != nil { + return nil, navErr + } + + plugins, pluginsErr := bs.pb.build(o) + if pluginsErr != nil { + return nil, pluginsErr + } + + if host, ok := nav.(types.UsePlugin); ok { + es := []error{} + for _, p := range plugins { + registrationErr := host.Register(p) + es = append(es, registrationErr) + } + + if pluginErr := errors.Join(es...); pluginErr != nil { + return nil, pluginErr + } + } + + return &buildArtefacts{ + o: o, + nav: nav, + plugins: plugins, + }, nil +} diff --git a/core/core-defs.go b/core/core-defs.go index 2369de7..ecf830e 100644 --- a/core/core-defs.go +++ b/core/core-defs.go @@ -1,5 +1,7 @@ package core +import "github.com/snivilised/traverse/enums" + // core contains universal definitions and handles cross cutting concerns // try to keep to a minimum to reduce rippling changes @@ -33,3 +35,53 @@ type ( // the traversal node, such as directory ascend or descend. NodeHandler func(node *Node) ) + +type Using struct { + Root string + Subscription enums.Subscription + Handler Client +} + +func (u Using) Validate() error { + if u.Root == "" { + return UsingError{ + message: "missing root path", + } + } + + if u.Subscription == enums.SubscribeUndefined { + return UsingError{ + message: "missing subscription", + } + } + + if u.Handler == nil { + return UsingError{ + message: "missing handler", + } + } + + return nil +} + +type As struct { + Using + From string + Strategy enums.ResumeStrategy +} + +func (a As) Validate() error { + if a.From == "" { + return UsingError{ + message: "missing restore from path", + } + } + + if a.Strategy == enums.ResumeStrategyUndefined { + return UsingError{ + message: "missing subscription", + } + } + + return a.Using.Validate() +} diff --git a/core/errors.go b/core/errors.go new file mode 100644 index 0000000..027383e --- /dev/null +++ b/core/errors.go @@ -0,0 +1,14 @@ +package core + +import ( + "fmt" +) + +type UsingError struct { + message string +} + +func (e UsingError) Error() string { + // TODO: i18n + return fmt.Sprintf("using error: %v", e.message) +} diff --git a/core/filtering.go b/core/filtering.go new file mode 100644 index 0000000..8e5649e --- /dev/null +++ b/core/filtering.go @@ -0,0 +1,4 @@ +package core + +type FilterDef struct { +} diff --git a/cycle/events.go b/cycle/events.go index 556cb6e..7a65417 100644 --- a/cycle/events.go +++ b/cycle/events.go @@ -5,11 +5,11 @@ import ( ) type ( - broadcasterFunc[F any] func(listeners []F) F + announce[F any] func(listeners []F) F Dispatch[F any] struct { Invoke F - broadcaster broadcasterFunc[F] + broadcaster announce[F] } // NotificationCtrl contains the handler function to be invoked. The control diff --git a/director-error_test.go b/director-error_test.go new file mode 100644 index 0000000..1ee4646 --- /dev/null +++ b/director-error_test.go @@ -0,0 +1,97 @@ +package traverse_test + +import ( + "fmt" + + . "github.com/onsi/ginkgo/v2" //nolint:revive // ok + . "github.com/onsi/gomega" //nolint:revive // ok + + "github.com/snivilised/traverse" + "github.com/snivilised/traverse/internal/services" +) + +type traverseErrorTE struct { + given string + using *traverse.Using + as *traverse.As +} + +var _ = Describe("director error", Ordered, func() { + var handler traverse.Client + + BeforeAll(func() { + handler = func(_ *traverse.Node) error { + return nil + } + }) + + BeforeEach(func() { + services.Reset() + }) + + DescribeTable("Validate", + func(entry *traverseErrorTE) { + if entry.using != nil { + Expect(entry.using.Validate()).NotTo(Succeed()) + + return + } + + if entry.as != nil { + Expect(entry.as.Validate()).NotTo(Succeed()) + + return + } + }, + func(entry *traverseErrorTE) string { + return fmt.Sprintf("given: %v, 🧪 should fail", entry.given) + }, + Entry(nil, &traverseErrorTE{ + given: "using missing root path", + using: &traverse.Using{ + Subscription: traverse.SubscribeFiles, + Handler: handler, + }, + }), + + Entry(nil, &traverseErrorTE{ + given: "using missing subscription", + using: &traverse.Using{ + Root: "/root-traverse-path", + Handler: handler, + }, + }), + + Entry(nil, &traverseErrorTE{ + given: "using missing handler", + using: &traverse.Using{ + Root: "/root-traverse-path", + Subscription: traverse.SubscribeFiles, + }, + }), + + Entry(nil, &traverseErrorTE{ + given: "as missing restore from path", + as: &traverse.As{ + Using: traverse.Using{ + Root: "/root-traverse-path", + Subscription: traverse.SubscribeFiles, + Handler: handler, + }, + Strategy: traverse.ResumeStrategySpawn, + }, + }), + + Entry(nil, &traverseErrorTE{ + given: "as missing resume strategy", + as: &traverse.As{ + Using: traverse.Using{ + Root: "/root-traverse-path", + Subscription: traverse.SubscribeFiles, + Handler: handler, + }, + From: "/restore-from-path", + }, + }), + ) +}) diff --git a/director.go b/director.go index cc10015..62fee15 100644 --- a/director.go +++ b/director.go @@ -3,10 +3,17 @@ package traverse import ( "github.com/snivilised/traverse/core" "github.com/snivilised/traverse/enums" + "github.com/snivilised/traverse/hiber" + "github.com/snivilised/traverse/internal/kernel" + "github.com/snivilised/traverse/internal/services" + "github.com/snivilised/traverse/internal/types" "github.com/snivilised/traverse/pref" + "github.com/snivilised/traverse/refine" + "github.com/snivilised/traverse/sampling" ) -type duffNavigatorController struct { +type duffPrimeController struct { + err error root string client core.Client from string @@ -19,42 +26,117 @@ func (r *duffResult) Error() error { return nil } -func (n *duffNavigatorController) Navigate() (core.TraverseResult, error) { +func (c *duffPrimeController) Navigate() (core.TraverseResult, error) { return &duffResult{}, nil } -// Prime extent -func Prime(opts ...pref.Option) OptionsBuilder { - return optionals(func() *pref.Options { - binder := pref.NewBinder() +type duffResumeController struct { + err error + from string + strategy enums.ResumeStrategy +} - // we probably need to mark something somehow to indicate - // Prime - // - return pref.Request(binder, opts...) - }) +type ifActive func(o *pref.Options) types.Plugin + +// activated interrogates options and invokes requests on behalf of the user +// to activate features according to option selections +func activated(o *pref.Options) ([]types.Plugin, error) { + var ( + all = []ifActive{ + hiber.IfActive, refine.IfActive, sampling.IfActive, + } + plugins = []types.Plugin{} + err error + ) + + for _, active := range all { + if plugin := active(o); plugin != nil { + plugins = append(plugins, plugin) + err = plugin.Init() + } + } + + return plugins, err } -// Resume extent -func Resume(from string, _ enums.ResumeStrategy, opts ...pref.Option) OptionsBuilder { - // we need state; record the hibernation wake point, so - // using a func here is probably not optimal. - // - return optionals(func() *pref.Options { - binder := pref.NewBinder() +func (c *duffResumeController) Navigate() (core.TraverseResult, error) { + return &duffResult{}, nil +} - // we probably need to mark something somehow to indicate - // Resume so we can query the hibernation condition and - // apply. - // - load, _ := pref.Load(binder, from, opts...) +// Prime extent +func Prime(using core.Using, settings ...pref.Option) *Builders { + return &Builders{ + ob: optionals(func() (*pref.Options, error) { + if err := using.Validate(); err != nil { + return nil, err + } + + // we probably need to mark something somehow to indicate + // Prime + // + return pref.Get(settings...) + }), + nb: factory(func(o *pref.Options) (core.Navigator, error) { + controller, err := kernel.Prime(using, o) + + if err != nil { + controller = &duffPrimeController{ + err: err, + } + } + + return controller, err + }), + pb: features(activated), + } +} - // get the resume point from the resume persistence file - // then set up hibernation with this defined as a hibernation - // filter. +// Resume extent +func Resume(as As, settings ...pref.Option) *Builders { + return &Builders{ + // we need state; record the hibernation wake point, so + // using a func here is probably not optimal. // - return load.O - }) + ob: optionals(func() (*pref.Options, error) { + if err := as.Validate(); err != nil { + return nil, err + } + + // we probably need to mark something somehow to indicate + // Resume so we can query the hibernation condition and + // apply. + // + o, err := pref.Load(as.From, settings...) + + // get the resume point from the resume persistence file + // then set up hibernation with this defined as a hibernation + // filter. + // + return o, err + }), + nb: factory(func(o *pref.Options) (core.Navigator, error) { + controller, err := kernel.Resume(as, o, + kernel.DecorateController(func(n core.Navigator) core.Navigator { + // TODO: create the resume controller + // + return n + }), + ) + + if err != nil { + controller = &duffResumeController{ + err: err, + } + } + + // at this point, the resume controller does not know + // the wake point as would be loaded by the options + // builder. + // + return controller, err + }), + pb: features(activated), + } } // Director @@ -63,7 +145,7 @@ type Director interface { // perform a full Prime run, or Resume from a previously // cancelled run. // - Extent(ob OptionsBuilder) core.Navigator + Extent(bs *Builders) core.Navigator } // NavigatorFactory @@ -77,81 +159,58 @@ type NavigatorFactory interface { Configure() Director } -type baseFactory struct { - factory syncBuilder // the sync factory function -} - -type linear struct { // NavigatorFactory - baseFactory +type walker struct { // NavigatorFactory } -func (f *linear) Configure() Director { +func (f *walker) Configure() Director { // Walk // - return direct(func(ob OptionsBuilder) core.Navigator { + return director(func(bs *Builders) core.Navigator { // resume or prime? If resume, we need to access the hibernation // wake condition on the retrieved options. But how do we know what // the extent is, so we know if we need to make this query? // // + artefacts, _ := bs.buildAll() // TODO: check error + + // Announce the availability of the navigator via UsePlugin interface + ctx, _ := artefacts.o.Acceleration.Cancellation() + _ = services.Broker.Emit(ctx, services.TopicInterceptNavigator, artefacts.nav) + return &driver{ session{ - o: ob.get(), - nav: &duffNavigatorController{}, + // do we store a context/cancel on the session? (and pass in via Configure) + // + o: artefacts.o, + nav: artefacts.nav, + plugins: artefacts.plugins, }, } }) } -type accelerator struct { // NavigatorFactory - baseFactory +type runner struct { // NavigatorFactory } -func (f *accelerator) Configure() Director { +func (f *runner) Configure() Director { // Run: create the observable/worker-pool // - return direct(func(ob OptionsBuilder) core.Navigator { + return director(func(bs *Builders) core.Navigator { + artefacts, _ := bs.buildAll() // TODO: check error + return &driver{ session{ - o: ob.get(), - nav: &duffNavigatorController{}, + o: artefacts.o, + nav: artefacts.nav, }, } }) } func Walk() NavigatorFactory { - // this could just be a function, because linear doesn't carry any - // state and implements a single method interface - // - return &linear{ - baseFactory{ - // TODO: where to invoke this from??? (pass in extent?) - factory: sync(func(at string) error { - _ = at - - // TODO: set up hibernation filter on navigator/options - // - return nil - }), - }, - // extent builder (primary or resume) - // sync builder (sequential) ---> depends on extent (resume: query hibernate condition) - } + return &walker{} } func Run() NavigatorFactory { - return &accelerator{ - baseFactory{ - factory: sync(func(at string) error { - _ = at - - // TODO: set up hibernation filter on observable - // - return nil - }), - }, - // extent builder (primary or resume) - // sync builder (reactive) ---> depends on extent (resume: query hibernate condition) - } + return &runner{} } diff --git a/director_test.go b/director_test.go new file mode 100644 index 0000000..6096f0a --- /dev/null +++ b/director_test.go @@ -0,0 +1,305 @@ +package traverse_test + +import ( + "context" + + "github.com/fortytw2/leaktest" + . "github.com/onsi/ginkgo/v2" //nolint:revive // ok + . "github.com/onsi/gomega" //nolint:revive // ok + + "github.com/snivilised/traverse" + "github.com/snivilised/traverse/core" + "github.com/snivilised/traverse/internal/services" + "github.com/snivilised/traverse/pref" +) + +const ( + RootPath = "/traversal-root-path" + RestorePath = "/from-restore-path" + files = 3 + folders = 2 +) + +var _ = Describe("Traverse", Ordered, func() { + var restore pref.Option + + BeforeAll(func() { + restore = func(o *traverse.Options) error { + o.Events.Begin.On(func(_ string) {}) + + return nil + } + }) + + BeforeEach(func() { + services.Reset() + }) + + Context("simple", func() { + Context("Walk", func() { + // We don't need to provide a context. For walk + // cancellations, we use an internal context instead. + // + When("Prime", func() { + It("🧪 should: walk primary navigation successfully", func() { + defer leaktest.Check(GinkgoT())() + + _, err := traverse.Walk().Configure().Extent(traverse.Prime( + traverse.Using{ + Root: RootPath, + Subscription: traverse.SubscribeFiles, + Handler: func(_ *traverse.Node) error { + return nil + }, + }, + traverse.WithSubscription(traverse.SubscribeFiles), + )).Navigate() + + Expect(err).To(Succeed()) + }) + }) + + When("Resume", func() { + It("🧪 should: walk resume navigation successfully", func() { + defer leaktest.Check(GinkgoT())() + + _, err := traverse.Walk().Configure().Extent(traverse.Resume( + traverse.As{ + Using: traverse.Using{ + Root: RootPath, + Subscription: traverse.SubscribeFiles, + Handler: func(_ *traverse.Node) error { + return nil + }, + }, + From: RestorePath, + Strategy: traverse.ResumeStrategyFastward, + }, + restore, + )).Navigate() + + Expect(err).To(Succeed()) + }) + }) + }) + + Context("Run", func() { + When("Prime without cancel", func() { + It("🧪 should: perform run navigation successfully", func() { + defer leaktest.Check(GinkgoT())() + + // need to make sure that when a ctrl-c occurs, who is + // responsible for handling the cancellation; ie if a + // ctrl-c occurs should the client handle it or do we? + // + // Internally, we could create our own child context + // from this parent content which contains a cancelFunc. + // This way, when ctrl-c occurs, we can trap that, + // and perform a save. If we don't do this, then how + // can we tap into cancellation? + // + // The context has a lifetime. The kernel will know when + // it has become invalidated, at which point a message + // is sent on the message bus, on a topic called + // something like "context.expired" + // + ctx := context.Background() + _, err := traverse.Run().Configure().Extent(traverse.Prime( + traverse.Using{ + Root: RootPath, + Subscription: traverse.SubscribeFiles, + Handler: func(_ *traverse.Node) error { + return nil + }, + }, + traverse.WithSubscription(traverse.SubscribeFiles), + traverse.WithContext(ctx), + )).Navigate() + + Expect(err).To(Succeed()) + }) + }) + + When("Prime with cancel", func() { + It("🧪 should: perform run navigation successfully", func() { + defer leaktest.Check(GinkgoT())() + + ctx, cancel := context.WithCancel(context.Background()) + + _, err := traverse.Run().Configure().Extent(traverse.Prime( + traverse.Using{ + Root: RootPath, + Subscription: traverse.SubscribeFiles, + Handler: func(_ *traverse.Node) error { + return nil + }, + }, + traverse.WithSubscription(traverse.SubscribeFiles), + traverse.WithContext(ctx), + traverse.WithCancel(cancel), + )).Navigate() + + Expect(err).To(Succeed()) + }) + }) + + When("Resume", func() { + It("🧪 should: perform run navigation successfully", func() { + defer leaktest.Check(GinkgoT())() + + _, err := traverse.Run().Configure().Extent(traverse.Resume( + traverse.As{ + Using: traverse.Using{ + Root: RootPath, + Subscription: traverse.SubscribeFiles, + Handler: func(_ *traverse.Node) error { + return nil + }, + }, + From: RestorePath, + Strategy: traverse.ResumeStrategySpawn, + }, + restore, + )).Navigate() + + Expect(err).To(Succeed()) + }) + }) + }) + }) + + Context("features", func() { + Context("Prime", func() { + When("hibernate", func() { + It("🧪 should: register ok", func() { + defer leaktest.Check(GinkgoT())() + + _, err := traverse.Walk().Configure().Extent(traverse.Prime( + traverse.Using{ + Root: RootPath, + Subscription: traverse.SubscribeFiles, + Handler: func(_ *traverse.Node) error { + return nil + }, + }, + traverse.WithSubscription(traverse.SubscribeFiles), + traverse.WithHibernation(&core.FilterDef{}), + )).Navigate() + + Expect(err).To(Succeed()) + }) + }) + + When("filter", func() { + It("🧪 should: register ok", func() { + defer leaktest.Check(GinkgoT())() + + _, err := traverse.Walk().Configure().Extent(traverse.Prime( + traverse.Using{ + Root: RootPath, + Subscription: traverse.SubscribeFiles, + Handler: func(_ *traverse.Node) error { + return nil + }, + }, + traverse.WithSubscription(traverse.SubscribeFiles), + traverse.WithFilter(&core.FilterDef{}), + )).Navigate() + + Expect(err).To(Succeed()) + }) + }) + + When("sample", func() { + It("🧪 should: register ok", func() { + defer leaktest.Check(GinkgoT())() + + _, err := traverse.Walk().Configure().Extent(traverse.Prime( + traverse.Using{ + Root: RootPath, + Subscription: traverse.SubscribeFiles, + Handler: func(_ *traverse.Node) error { + return nil + }, + }, + traverse.WithSubscription(traverse.SubscribeFiles), + traverse.WithSampling(files, folders), + )).Navigate() + + Expect(err).To(Succeed()) + }) + }) + }) + + Context("Resume", func() { + When("hibernate", func() { + It("🧪 should: register ok", func() { + defer leaktest.Check(GinkgoT())() + + _, err := traverse.Run().Configure().Extent(traverse.Resume( + traverse.As{ + Using: traverse.Using{ + Root: RootPath, + Subscription: traverse.SubscribeFiles, + Handler: func(_ *traverse.Node) error { + return nil + }, + }, + From: RestorePath, + Strategy: traverse.ResumeStrategySpawn, + }, + traverse.WithHibernation(&core.FilterDef{}), + )).Navigate() + + Expect(err).To(Succeed()) + }) + }) + + When("filter", func() { + It("🧪 should: register ok", func() { + defer leaktest.Check(GinkgoT())() + + _, err := traverse.Run().Configure().Extent(traverse.Resume( + traverse.As{ + Using: traverse.Using{ + Root: RootPath, + Subscription: traverse.SubscribeFiles, + Handler: func(_ *traverse.Node) error { + return nil + }, + }, + From: RestorePath, + Strategy: traverse.ResumeStrategySpawn, + }, + traverse.WithFilter(&core.FilterDef{}), + )).Navigate() + + Expect(err).To(Succeed()) + }) + }) + + When("sample", func() { + It("🧪 should: register ok", func() { + defer leaktest.Check(GinkgoT())() + + _, err := traverse.Run().Configure().Extent(traverse.Resume( + traverse.As{ + Using: traverse.Using{ + Root: RootPath, + Subscription: traverse.SubscribeFiles, + Handler: func(_ *traverse.Node) error { + return nil + }, + }, + From: RestorePath, + Strategy: traverse.ResumeStrategySpawn, + }, + traverse.WithSampling(files, folders), + )).Navigate() + + Expect(err).To(Succeed()) + }) + }) + }) + }) +}) diff --git a/driver.go b/driver.go index 660d952..74330e3 100644 --- a/driver.go +++ b/driver.go @@ -10,10 +10,14 @@ import ( ) const ( - badge = "navigation-driver" + badge = "badge: navigation-driver" ) -func init() { +type driver struct { + s session +} + +func (d *driver) init() { services.Broker.RegisterHandler(badge, bus.Handler{ Handle: func(_ context.Context, m bus.Message) { m.Data.(types.ContextExpiry).Expired() @@ -30,11 +34,8 @@ func init() { }) } -type driver struct { - s session -} - func (d *driver) Navigate() (core.TraverseResult, error) { + d.init() d.s.start() result, err := d.s.exec() diff --git a/enums/subscription-en.go b/enums/subscription-en.go index c51dddd..8e29e86 100644 --- a/enums/subscription-en.go +++ b/enums/subscription-en.go @@ -7,7 +7,7 @@ package enums type Subscription uint const ( - _ Subscription = iota + SubscribeUndefined Subscription = iota SubscribeFiles // subscribe-files SubscribeFolders // subscribe-folders SubscribeFoldersWithFiles // subscribe-folders-with-files diff --git a/hiber/hibernate-plugin.go b/hiber/hibernate-plugin.go new file mode 100644 index 0000000..443731c --- /dev/null +++ b/hiber/hibernate-plugin.go @@ -0,0 +1,48 @@ +package hiber + +import ( + "context" + + "github.com/snivilised/extendio/bus" + "github.com/snivilised/traverse/internal/lo" + "github.com/snivilised/traverse/internal/services" + "github.com/snivilised/traverse/internal/types" + "github.com/snivilised/traverse/pref" +) + +func IfActive(o *pref.Options) types.Plugin { + active := o.Core.Hibernate.Wake != nil + plugin := lo.TernaryF(active, + func() types.Plugin { + return &Plugin{} + }, + func() types.Plugin { + return nil + }, + ) + + return plugin +} + +type Plugin struct { +} + +func (p *Plugin) Name() string { + return "hibernation" +} + +func (p *Plugin) Init() error { + h := bus.Handler{ + Handle: func(_ context.Context, m bus.Message) { + if in, ok := m.Data.(types.UsePlugin); ok { + _ = in.Interceptor() // TODO: call Intercept + _ = in.Facilitate() + } + }, + Matcher: services.TopicInterceptNavigator, + } + + services.Broker.RegisterHandler(badge, h) + + return nil +} diff --git a/hiber/hibernation-defs.go b/hiber/hibernation-defs.go index 6dbdbe2..92165d9 100644 --- a/hiber/hibernation-defs.go +++ b/hiber/hibernation-defs.go @@ -1,14 +1,7 @@ package hiber -import ( - "context" - - "github.com/snivilised/extendio/bus" - "github.com/snivilised/traverse/internal/services" -) - const ( - badge = "hibernator" + badge = "badge: hibernator" ) // hiber represents the facility to be able to start navigation in hibernated state, @@ -20,21 +13,6 @@ const ( // Hibernation depends on filtering. // -func init() { - h := bus.Handler{ - Handle: func(_ context.Context, m bus.Message) { - // The data field will contain the appropriate - // object (represented behind an interface of some kind) that is related - // to the topic. - // - _ = m.Data - }, - Matcher: services.TopicOptionsAnnounce, - } - - services.Broker.RegisterHandler(badge, h) -} - // subscribe to options.before func RestoreOptions() { // called by resume to load options from json file and diff --git a/internal-traverse-defs.go b/internal-traverse-defs.go index eff1f89..be5d228 100644 --- a/internal-traverse-defs.go +++ b/internal-traverse-defs.go @@ -2,37 +2,45 @@ package traverse import ( "github.com/snivilised/traverse/core" + "github.com/snivilised/traverse/internal/types" "github.com/snivilised/traverse/pref" ) -// extentFunc -type extentFunc func(ob OptionsBuilder) *pref.Options +type navigatorBuilder interface { + build(o *pref.Options) (core.Navigator, error) +} + +type factory func(o *pref.Options) (core.Navigator, error) + +func (fn factory) build(o *pref.Options) (core.Navigator, error) { + return fn(o) +} -type OptionsBuilder interface { - get() *pref.Options +type optionsBuilder interface { + build() (*pref.Options, error) } -type optionals func() *pref.Options +type optionals func() (*pref.Options, error) -func (fn optionals) get() *pref.Options { +func (fn optionals) build() (*pref.Options, error) { return fn() } -// TODO: do we pass in another func to the directory that represents the sync? -type direct func(ob OptionsBuilder) core.Navigator - -func (fn direct) Extent(ob OptionsBuilder) core.Navigator { - return fn(ob) +type pluginsBuilder interface { + build(*pref.Options) ([]types.Plugin, error) } -type syncBuilder interface { - wake(at string) error // we might need to pass in options +type features func(*pref.Options) ([]types.Plugin, error) + +func (fn features) build(o *pref.Options) ([]types.Plugin, error) { + return fn(o) } -type sync func(at string) error +// TODO: do we pass in another func to the directory that represents the sync? +type director func(bs *Builders) core.Navigator -func (fn sync) wake(at string) error { - return fn(at) +func (fn director) Extent(bs *Builders) core.Navigator { + return fn(bs) } // ✨ ========================================================================= diff --git a/internal/kernel/kernel-defs.go b/internal/kernel/kernel-defs.go index f0d7ee1..3403489 100644 --- a/internal/kernel/kernel-defs.go +++ b/internal/kernel/kernel-defs.go @@ -9,3 +9,11 @@ package kernel // func (fn NavigatorFunc) Navigate() (core.TraverseResult, error) { // return fn() // } + +type navigationResult struct { + err error +} + +func (r *navigationResult) Error() error { + return r.err +} diff --git a/internal/kernel/navigation-controller.go b/internal/kernel/navigation-controller.go index 55108bd..367f2e9 100644 --- a/internal/kernel/navigation-controller.go +++ b/internal/kernel/navigation-controller.go @@ -1,5 +1,30 @@ package kernel +import ( + "github.com/snivilised/traverse/core" + "github.com/snivilised/traverse/internal/types" + "github.com/snivilised/traverse/pref" +) + type navigationController struct { impl navigatorImpl + o *pref.Options +} + +func (nc *navigationController) Navigate() (core.TraverseResult, error) { + return &navigationResult{}, nil +} + +func (nc *navigationController) Register(plugin types.Plugin) error { + _ = plugin + + return nil +} + +func (nc *navigationController) Interceptor() types.Interception { + return nil +} + +func (nc *navigationController) Facilitate() types.Facilities { + return nil } diff --git a/internal/kernel/navigator-factory.go b/internal/kernel/navigator-factory.go new file mode 100644 index 0000000..5ffa199 --- /dev/null +++ b/internal/kernel/navigator-factory.go @@ -0,0 +1,74 @@ +package kernel + +import ( + "errors" + + "github.com/snivilised/traverse/core" + "github.com/snivilised/traverse/enums" + "github.com/snivilised/traverse/pref" +) + +func Prime(using core.Using, o *pref.Options) (core.Navigator, error) { + return newController(&using, o) +} + +func Resume(with core.As, o *pref.Options, resumption Resumption) (core.Navigator, error) { + controller, err := newController(&with.Using, o) + + return resumption.Decorate(controller), err +} + +type Resumption interface { + Decorate(core.Navigator) core.Navigator +} + +type DecorateController func(core.Navigator) core.Navigator + +func (f DecorateController) Decorate(source core.Navigator) core.Navigator { + return f(source) +} + +func newController(using *core.Using, o *pref.Options) (core.Navigator, error) { + if err := using.Validate(); err != nil { + return nil, err + } + + var ( + impl core.Navigator + err error + ) + + impl, err = newImpl(using, o) + + navigator := &navigationController{ + impl: impl, + o: o, + } + + return navigator, err +} + +func newImpl(using *core.Using, o *pref.Options) (core.Navigator, error) { + var ( + navigator core.Navigator + err error + subscription = using.Subscription + ) + + switch subscription { + case enums.SubscribeFiles: + navigator = &navigationController{ + o: o, + } // just temporary (create the impl's) + case enums.SubscribeFolders: + navigator = &navigationController{} + case enums.SubscribeFoldersWithFiles: + navigator = &navigationController{} + case enums.SubscribeUniversal: + navigator = &navigationController{} + case enums.SubscribeUndefined: + err = errors.New("invalid subscription") + } + + return navigator, err +} diff --git a/internal/services/broker.go b/internal/services/broker.go index b316690..ebd8fb8 100644 --- a/internal/services/broker.go +++ b/internal/services/broker.go @@ -3,29 +3,36 @@ package services import "github.com/snivilised/extendio/bus" const ( - format = "%03d" - TopicContextExpired = "context.expired" - TopicOptionsAnnounce = "options.announce" - TopicOptionsBefore = "options.before" - TopicOptionsComplete = "options.complete" - TopicTraverseResult = "traverse.result" + format = "%03d" + TopicContextExpired = "topic:context.expired" + TopicInitNavigator = "topic:init.navigator" + TopicInterceptNavigator = "topic:intercept.navigator" + TopicOptionsAnnounce = "topic:options.announce" + TopicOptionsBefore = "topic:options.before" + TopicOptionsComplete = "topic:options.complete" + TopicTraverseResult = "topic:traverse.result" ) var ( Broker *bus.Broker topics = []string{ TopicContextExpired, + TopicInitNavigator, + TopicInterceptNavigator, TopicOptionsAnnounce, TopicOptionsBefore, TopicOptionsComplete, + TopicTraverseResult, } ) -func init() { - Reset() -} +type ( + InitBroker interface { + Available(b *bus.Broker) + } +) -func Reset() { +func Reset() *bus.Broker { b, err := bus.New(&bus.Sequential{ Format: format, }) @@ -42,4 +49,6 @@ func Reset() { // function/object around it that implements synchronisation using locks. // Broker = b + + return b } diff --git a/internal/types/definitions.go b/internal/types/definitions.go index 811ae56..6f7b95f 100644 --- a/internal/types/definitions.go +++ b/internal/types/definitions.go @@ -1,6 +1,6 @@ package types -// package types +// package types internal types type ( ContextExpiry interface { @@ -11,3 +11,26 @@ type ( NavigateResult struct { } ) + +type Plugin interface { + Name() string + Init() error +} + +// UsePlugin invoked by the plugin to the navigator +type UsePlugin interface { + // this interface needs to be exposed internally but not externally + Register(plugin Plugin) error + Interceptor() Interception + Facilitate() Facilities +} + +// Facilities is the interface provided to plugins to enable them +// to initialise successfully. +type Facilities interface { + Foo() +} + +type Interception interface { + Intercept() +} diff --git a/pref/binder.go b/pref/binder.go index b38fc4f..4105b70 100644 --- a/pref/binder.go +++ b/pref/binder.go @@ -10,6 +10,7 @@ type ( // Binder contains items derived from Options Binder struct { Notification cycle.Controls + Loaded *LoadInfo } ) diff --git a/pref/options-context.go b/pref/options-context.go index ac90ee4..c5a317f 100644 --- a/pref/options-context.go +++ b/pref/options-context.go @@ -11,6 +11,10 @@ type AccelerationOptions struct { now int } +func (ao *AccelerationOptions) Cancellation() (context.Context, context.CancelFunc) { + return ao.ctx, ao.cancel +} + func WithContext(ctx context.Context) Option { return func(o *Options) error { o.Acceleration.ctx = ctx diff --git a/pref/options-core.go b/pref/options-core.go index a79c205..38633be 100644 --- a/pref/options-core.go +++ b/pref/options-core.go @@ -17,6 +17,14 @@ type CoreOptions struct { // Sampling options // Sampling SamplingOptions + + // Filter + // + Filter FilterOptions + + // Hibernation + // + Hibernate HibernateOptions } func WithSubscription(subscription enums.Subscription) Option { diff --git a/pref/options-filter.go b/pref/options-filter.go new file mode 100644 index 0000000..0272d33 --- /dev/null +++ b/pref/options-filter.go @@ -0,0 +1,17 @@ +package pref + +import ( + "github.com/snivilised/traverse/core" +) + +type FilterOptions struct { + Node *core.FilterDef +} + +func WithFilter(filter *core.FilterDef) Option { + return func(o *Options) error { + o.Core.Filter.Node = filter + + return nil + } +} diff --git a/pref/options-hibernate.go b/pref/options-hibernate.go new file mode 100644 index 0000000..432b03f --- /dev/null +++ b/pref/options-hibernate.go @@ -0,0 +1,9 @@ +package pref + +import ( + "github.com/snivilised/traverse/core" +) + +type HibernateOptions struct { + Wake *core.FilterDef +} diff --git a/pref/options-navigation-behaviours.go b/pref/options-navigation-behaviours.go index 16b5df2..54d23f6 100644 --- a/pref/options-navigation-behaviours.go +++ b/pref/options-navigation-behaviours.go @@ -1,6 +1,7 @@ package pref import ( + "github.com/snivilised/traverse/core" "github.com/snivilised/traverse/enums" ) @@ -86,6 +87,14 @@ func WithSortBehaviour(sb *SortBehaviour) Option { } } +func WithHibernation(wake *core.FilterDef) Option { + return func(o *Options) error { + o.Core.Hibernate.Wake = wake + + return nil + } +} + func WithHibernationBehaviour(hb *HibernationBehaviour) Option { return func(o *Options) error { o.Core.Behaviours.Hibernation = *hb diff --git a/pref/options.go b/pref/options.go index 1830eda..24e7ff0 100644 --- a/pref/options.go +++ b/pref/options.go @@ -14,10 +14,10 @@ import ( // package: pref contains user option definitions; do not use anything in kernel (cyclic) const ( - badge = "option-requester" + badge = "badge: option-requester" ) -func init() { +func initTbd() { h := bus.Handler{ Handle: func(_ context.Context, m bus.Message) { _ = m.Data @@ -55,45 +55,60 @@ type ( // Acceleration AccelerationOptions - binder *Binder + Binder *Binder } // Option functional traverse options Option func(o *Options) error ) -func Request(binder *Binder, opts ...Option) *Options { +func Get(settings ...Option) (*Options, error) { o := DefaultOptions() + binder := NewBinder() o.Events.Bind(&binder.Notification) - apply(o, opts...) + apply(o, settings...) - o.binder = binder + if o.Acceleration.ctx == nil { + o.Acceleration.ctx = context.Background() + } - return o + o.Binder = binder + + return o, nil } type LoadInfo struct { - O *Options + // O *Options WakeAt string } -func Load(binder *Binder, from string, opts ...Option) (*LoadInfo, error) { +func Load(from string, settings ...Option) (*Options, error) { o := DefaultOptions() // do load _ = from - o.binder = binder + binder := NewBinder() + o.Events.Bind(&binder.Notification) + o.Binder = binder + + // TODO: save any active state on the binder, eg the wake point - apply(o, opts...) + apply(o, settings...) - return &LoadInfo{ - O: o, + if o.Acceleration.ctx == nil { + o.Acceleration.ctx = context.Background() + } + + o.Binder.Loaded = &LoadInfo{ + // O: o, WakeAt: "tbd", - }, nil + } + + return o, nil } -func apply(o *Options, opts ...Option) { - for _, option := range opts { +func apply(o *Options, settings ...Option) { + for _, option := range settings { // TODO: check error _ = option(o) } diff --git a/pref/options_test.go b/pref/options_test.go index 1e5f9d7..a29fa10 100644 --- a/pref/options_test.go +++ b/pref/options_test.go @@ -14,13 +14,13 @@ var _ = Describe("Options", func() { When("client listens", func() { It("🧪 should: invoke client's handler", func() { begun := false - binder := pref.NewBinder() - o := pref.Request(binder) + + o, _ := pref.Get() o.Events.Begin.On(func(_ string) { begun = true }) - binder.Notification.Begin.Dispatch.Invoke("/traversal-root") + o.Binder.Notification.Begin.Dispatch.Invoke("/traversal-root") Expect(begun).To(BeTrue()) }) @@ -29,8 +29,7 @@ var _ = Describe("Options", func() { When("multiple listeners", func() { It("🧪 should: broadcast", func() { count := 0 - binder := pref.NewBinder() - o := pref.Request(binder) + o, _ := pref.Get() o.Events.Begin.On(func(_ string) { count++ @@ -38,7 +37,7 @@ var _ = Describe("Options", func() { o.Events.Begin.On(func(_ string) { count++ }) - binder.Notification.Begin.Dispatch.Invoke("/traversal-root") + o.Binder.Notification.Begin.Dispatch.Invoke("/traversal-root") Expect(count).To(Equal(2), "not all listeners were invoked for first notification") count = 0 @@ -46,17 +45,16 @@ var _ = Describe("Options", func() { count++ }) - binder.Notification.Begin.Dispatch.Invoke("/another-root") + o.Binder.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() { - binder := pref.NewBinder() - _ = pref.Request(binder) + o, _ := pref.Get() - binder.Notification.Begin.Dispatch.Invoke("/traversal-root") + o.Binder.Notification.Begin.Dispatch.Invoke("/traversal-root") }) }) }) diff --git a/refine/filter-plugin.go b/refine/filter-plugin.go new file mode 100644 index 0000000..8aca65d --- /dev/null +++ b/refine/filter-plugin.go @@ -0,0 +1,32 @@ +package refine + +import ( + "github.com/snivilised/traverse/internal/lo" + "github.com/snivilised/traverse/internal/types" + "github.com/snivilised/traverse/pref" +) + +func IfActive(o *pref.Options) types.Plugin { + active := o.Core.Filter.Node != nil + plugin := lo.TernaryF(active, + func() types.Plugin { + return &Plugin{} + }, + func() types.Plugin { + return nil + }, + ) + + return plugin +} + +type Plugin struct { +} + +func (p *Plugin) Name() string { + return "filtering" +} + +func (p *Plugin) Init() error { + return nil +} diff --git a/sampling/sampling-plugin.go b/sampling/sampling-plugin.go new file mode 100644 index 0000000..e2108fc --- /dev/null +++ b/sampling/sampling-plugin.go @@ -0,0 +1,32 @@ +package sampling + +import ( + "github.com/snivilised/traverse/internal/lo" + "github.com/snivilised/traverse/internal/types" + "github.com/snivilised/traverse/pref" +) + +func IfActive(o *pref.Options) types.Plugin { + active := (o.Core.Sampling.NoOf.Files > 0) || (o.Core.Sampling.NoOf.Folders > 0) + plugin := lo.TernaryF(active, + func() types.Plugin { + return &Plugin{} + }, + func() types.Plugin { + return nil + }, + ) + + return plugin +} + +type Plugin struct { +} + +func (p *Plugin) Name() string { + return "sampling" +} + +func (p *Plugin) Init() error { + return nil +} diff --git a/session.go b/session.go index 2cae03d..49eda36 100644 --- a/session.go +++ b/session.go @@ -5,6 +5,7 @@ import ( "time" "github.com/snivilised/traverse/core" + "github.com/snivilised/traverse/internal/types" "github.com/snivilised/traverse/pref" ) @@ -22,6 +23,7 @@ type session struct { cancel context.CancelFunc nav core.Navigator o *pref.Options + plugins []types.Plugin } func (s *session) start() { diff --git a/traverse-api.go b/traverse-api.go index 8ee8f6c..895845e 100644 --- a/traverse-api.go +++ b/traverse-api.go @@ -1,9 +1,65 @@ package traverse +import ( + "github.com/snivilised/traverse/core" + "github.com/snivilised/traverse/enums" + "github.com/snivilised/traverse/pref" +) + // traverse is the front line user facing interface to this module. It sits // on the top of the code stack and is allowed to use anything, but nothing // else can depend on definitions here, except unit tests. +type Client = core.Client +type Node = core.Node + +type Option = pref.Option +type Options = pref.Options + +type Subscription = enums.Subscription + +const ( + SubscribeFiles = enums.SubscribeFiles + SubscribeFolders = enums.SubscribeFolders + SubscribeFoldersWithFiles = enums.SubscribeFoldersWithFiles + SubscribeUniversal = enums.SubscribeUniversal +) + +type ResumeStrategy = enums.ResumeStrategy + +const ( + ResumeStrategySpawn = enums.ResumeStrategySpawn + ResumeStrategyFastward = enums.ResumeStrategyFastward +) + +type ( + As = core.As +) + +var ( + WithCancel = pref.WithCancel + WithCPU = pref.WithCPU + WithContext = pref.WithContext + WithDepth = pref.WithDepth + WithFilter = pref.WithFilter + WithHibernation = pref.WithHibernation + WithHibernationBehaviour = pref.WithHibernationBehaviour + WithNavigationBehaviours = pref.WithNavigationBehaviours + WithNoRecurse = pref.WithNoRecurse + WithNoW = pref.WithNoW + WithSamplerOptions = pref.WithSamplerOptions + WithSampling = pref.WithSampling + WithSamplingInReverse = pref.WithSamplingInReverse + WithSamplingNoOf = pref.WithSamplingNoOf + WithSamplingOptions = pref.WithSamplingOptions + WithSamplingType = pref.WithSamplingType + WithSortBehaviour = pref.WithSortBehaviour + WithSubPathBehaviour = pref.WithSubPathBehaviour + WithSubscription = pref.WithSubscription +) + +type Using = core.Using + // sub package description: // diff --git a/traverse_test.go b/traverse_test.go deleted file mode 100644 index f1a72d8..0000000 --- a/traverse_test.go +++ /dev/null @@ -1,122 +0,0 @@ -package traverse_test - -import ( - "context" - - "github.com/fortytw2/leaktest" - . "github.com/onsi/ginkgo/v2" //nolint:revive // ok - . "github.com/onsi/gomega" //nolint:revive // ok - - "github.com/snivilised/traverse" - "github.com/snivilised/traverse/enums" - "github.com/snivilised/traverse/internal/services" - "github.com/snivilised/traverse/pref" -) - -var _ = Describe("Traverse", Ordered, func() { - BeforeEach(func() { - services.Reset() - }) - - Context("speculation", func() { - Context("Walk", func() { - // We don't need to provide a context. For walk - // cancellations, we use an internal context instead. - // - When("Prime", func() { - It("🧪 should: walk primary navigation successfully", func() { - defer leaktest.Check(GinkgoT())() - - _, err := traverse.Walk().Configure().Extent(traverse.Prime( - pref.WithSubscription(enums.SubscribeFiles), - )).Navigate() - - Expect(err).To(Succeed()) - }) - }) - - When("Resume", func() { - It("🧪 should: walk resume navigation successfully", func() { - defer leaktest.Check(GinkgoT())() - - restore := func(_ *pref.Options) error { - return nil - } - - _, err := traverse.Walk().Configure().Extent(traverse.Resume( - "/from-restore-path", - enums.ResumeStrategyFastward, - restore, - )).Navigate() - - Expect(err).To(Succeed()) - }) - }) - }) - - Context("Run", func() { - When("Prime without cancel", func() { - It("🧪 should: perform run navigation successfully", func() { - defer leaktest.Check(GinkgoT())() - - // need to make sure that when a ctrl-c occurs, who is - // responsible for handling the cancellation; ie if a - // ctrl-c occurs should the client handle it or do we? - // - // Internally, we could create our own child context - // from this parent content which contains a cancelFunc. - // This way, when ctrl-c occurs, we can trap that, - // and perform a save. If we don't do this, then how - // can we tap into cancellation? - // - // The context has a lifetime. The kernel will know when - // it has become invalidated, at which point a message - // is sent on the message bus, on a topic called - // something like "context.expired" - // - ctx := context.Background() - _, err := traverse.Run().Configure().Extent(traverse.Prime( - pref.WithSubscription(enums.SubscribeFiles), - pref.WithContext(ctx), - )).Navigate() - - Expect(err).To(Succeed()) - }) - }) - - When("Prime with cancel", func() { - It("🧪 should: perform run navigation successfully", func() { - defer leaktest.Check(GinkgoT())() - - ctx, cancel := context.WithCancel(context.Background()) - - _, err := traverse.Run().Configure().Extent(traverse.Prime( - pref.WithSubscription(enums.SubscribeFiles), - pref.WithContext(ctx), - pref.WithCancel(cancel), - )).Navigate() - - Expect(err).To(Succeed()) - }) - }) - - When("Resume", func() { - It("🧪 should: perform run navigation successfully", func() { - defer leaktest.Check(GinkgoT())() - - restore := func(_ *pref.Options) error { - return nil - } - - _, err := traverse.Run().Configure().Extent(traverse.Resume( - "/from-restore-path", - enums.ResumeStrategySpawn, - restore, - )).Navigate() - - Expect(err).To(Succeed()) - }) - }) - }) - }) -})