diff --git a/go.mod b/go.mod index 15e2f22..8391d9f 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.21 require ( github.com/onsi/ginkgo/v2 v2.13.0 github.com/onsi/gomega v1.28.1 + github.com/pkg/errors v0.9.1 github.com/samber/lo v1.38.1 github.com/snivilised/extendio v0.3.0 github.com/snivilised/lorax v0.4.1 @@ -22,7 +23,6 @@ require ( github.com/google/uuid v1.3.1 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/onsi/ginkgo v1.16.5 // indirect - github.com/pkg/errors v0.9.1 // indirect github.com/sagikazarmark/locafero v0.3.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect diff --git a/src/app/command/bootstrap.go b/src/app/command/bootstrap.go index 2094a74..971176b 100644 --- a/src/app/command/bootstrap.go +++ b/src/app/command/bootstrap.go @@ -58,17 +58,17 @@ type ConfigInfo struct { // without resorting to the use of Go's init() mechanism and minimal // use of package global variables. type Bootstrap struct { - Container *assistant.CobraContainer - options ConfigureOptions + Container *assistant.CobraContainer + optionsInfo ConfigureOptionsInfo } -type ConfigureOptions struct { +type ConfigureOptionsInfo struct { Detector LocaleDetector Executor proxy.Executor Config ConfigInfo } -type ConfigureOptionFn func(*ConfigureOptions) +type ConfigureOptionFn func(*ConfigureOptionsInfo) // Root builds the command tree and returns the root command, ready // to be executed. @@ -76,7 +76,7 @@ func (b *Bootstrap) Root(options ...ConfigureOptionFn) *cobra.Command { home, err := os.UserHomeDir() cobra.CheckErr(err) - b.options = ConfigureOptions{ + b.optionsInfo = ConfigureOptionsInfo{ Detector: &Jabber{}, Executor: &ProgramExecutor{ Name: "magick", @@ -89,21 +89,21 @@ func (b *Bootstrap) Root(options ...ConfigureOptionFn) *cobra.Command { }, } - if _, err := b.options.Executor.Look(); err != nil { - b.options.Executor = &DummyExecutor{ - Name: b.options.Executor.ProgName(), + if _, err := b.optionsInfo.Executor.Look(); err != nil { + b.optionsInfo.Executor = &DummyExecutor{ + Name: b.optionsInfo.Executor.ProgName(), } } for _, fo := range options { - fo(&b.options) + fo(&b.optionsInfo) } b.configure() // JUST TEMPORARY: make the executor the dummy for safety // - b.options.Executor = &DummyExecutor{ + b.optionsInfo.Executor = &DummyExecutor{ Name: "magick", } @@ -126,7 +126,7 @@ func (b *Bootstrap) Root(options ...ConfigureOptionFn) *cobra.Command { // ---> execute root core // - return proxy.EnterRoot(inputs, b.options.Executor, b.options.Config.Viper) + return proxy.EnterRoot(inputs, b.optionsInfo.Executor, b.optionsInfo.Config.Viper) }, }, ) @@ -139,8 +139,8 @@ func (b *Bootstrap) Root(options ...ConfigureOptionFn) *cobra.Command { } func (b *Bootstrap) configure() { - vc := b.options.Config.Viper - ci := b.options.Config + vc := b.optionsInfo.Config.Viper + ci := b.optionsInfo.Config vc.SetConfigName(ci.Name) vc.SetConfigType(ci.ConfigType) diff --git a/src/app/command/bootstrap_test.go b/src/app/command/bootstrap_test.go index ebbdbb2..8f87a98 100644 --- a/src/app/command/bootstrap_test.go +++ b/src/app/command/bootstrap_test.go @@ -49,7 +49,7 @@ var _ = Describe("Bootstrap", Ordered, func() { Context("given: root defined with magick sub-command", func() { It("๐Ÿงช should: setup command without error", func() { bootstrap := command.Bootstrap{} - rootCmd := bootstrap.Root(func(co *command.ConfigureOptions) { + rootCmd := bootstrap.Root(func(co *command.ConfigureOptionsInfo) { co.Detector = &DetectorStub{} co.Executor = &ExecutorStub{ Name: "magick", diff --git a/src/app/command/magick-cmd_test.go b/src/app/command/magick-cmd_test.go index 703b4ac..978fff8 100644 --- a/src/app/command/magick-cmd_test.go +++ b/src/app/command/magick-cmd_test.go @@ -55,7 +55,7 @@ var _ = Describe("MagickCmd", Ordered, func() { bootstrap := command.Bootstrap{} tester := helpers.CommandTester{ Args: []string{"mag"}, - Root: bootstrap.Root(func(co *command.ConfigureOptions) { + Root: bootstrap.Root(func(co *command.ConfigureOptionsInfo) { co.Detector = &DetectorStub{} }), } diff --git a/src/app/command/root-cmd_test.go b/src/app/command/root-cmd_test.go index afa1679..fea9cc8 100644 --- a/src/app/command/root-cmd_test.go +++ b/src/app/command/root-cmd_test.go @@ -35,7 +35,7 @@ var _ = Describe("RootCmd", Ordered, func() { bootstrap := command.Bootstrap{} tester = helpers.CommandTester{ Args: []string{"./"}, - Root: bootstrap.Root(func(co *command.ConfigureOptions) { + Root: bootstrap.Root(func(co *command.ConfigureOptionsInfo) { co.Detector = &DetectorStub{} co.Executor = &ExecutorStub{ Name: "magick", @@ -49,7 +49,7 @@ var _ = Describe("RootCmd", Ordered, func() { bootstrap := command.Bootstrap{} tester = helpers.CommandTester{ Args: entry.commandLine, - Root: bootstrap.Root(func(co *command.ConfigureOptions) { + Root: bootstrap.Root(func(co *command.ConfigureOptionsInfo) { co.Detector = &DetectorStub{} co.Executor = &ExecutorStub{ Name: "magick", diff --git a/src/app/command/shrink-cmd.go b/src/app/command/shrink-cmd.go index 248c6bf..d5d716c 100644 --- a/src/app/command/shrink-cmd.go +++ b/src/app/command/shrink-cmd.go @@ -124,8 +124,8 @@ func (b *Bootstrap) buildShrinkCommand(container *assistant.CobraContainer) *cob appErr = proxy.EnterShrink( inputs, - b.options.Executor, - b.options.Config.Viper, + b.optionsInfo.Executor, + b.optionsInfo.Config.Viper, ) } else { return xvErr diff --git a/src/app/command/shrink-cmd_test.go b/src/app/command/shrink-cmd_test.go index 90434e3..545e5ef 100644 --- a/src/app/command/shrink-cmd_test.go +++ b/src/app/command/shrink-cmd_test.go @@ -40,7 +40,7 @@ func expectValidShrinkCmdInvocation(entry *shrinkTE) { tester := helpers.CommandTester{ Args: append(options, entry.args...), - Root: bootstrap.Root(func(co *command.ConfigureOptions) { + Root: bootstrap.Root(func(co *command.ConfigureOptionsInfo) { co.Detector = &DetectorStub{} co.Executor = &ExecutorStub{ Name: "magick", diff --git a/src/app/proxy/config_test.go b/src/app/proxy/config_test.go index da0317d..8ce741f 100644 --- a/src/app/proxy/config_test.go +++ b/src/app/proxy/config_test.go @@ -33,7 +33,7 @@ func expectValidShrinkCmdInvocation(entry *configTE) { tester := helpers.CommandTester{ Args: append(options, entry.args...), - Root: bootstrap.Root(func(co *command.ConfigureOptions) { + Root: bootstrap.Root(func(co *command.ConfigureOptionsInfo) { co.Detector = &helpers.DetectorStub{} co.Executor = &helpers.ExecutorStub{ Name: prog, diff --git a/src/app/proxy/enter-root.go b/src/app/proxy/enter-root.go index af13c1d..a2cfc8e 100644 --- a/src/app/proxy/enter-root.go +++ b/src/app/proxy/enter-root.go @@ -35,6 +35,25 @@ type RootEntry struct { files []string } +func (e *RootEntry) principalFn(item *nav.TraverseItem) error { + depth := item.Extension.Depth + indicator := lo.Ternary(len(item.Children) > 0, "โ˜€๏ธ", "๐ŸŒŠ") + + for _, entry := range item.Children { + fullPath := filepath.Join(item.Path, entry.Name()) + e.files = append(e.files, fullPath) + } + + fmt.Printf( + "---> %v ROOT-CALLBACK: (depth:%v, files:%v) '%v'\n", + indicator, + depth, len(item.Children), + item.Path, + ) + + return e.Program.Execute("--version") +} + func (e *RootEntry) ConfigureOptions(o *nav.TraverseOptions) { o.Notify.OnBegin = func(_ *nav.NavigationState) { fmt.Printf("===> ๐Ÿ›ก๏ธ beginning traversal ...\n") @@ -46,24 +65,7 @@ func (e *RootEntry) ConfigureOptions(o *nav.TraverseOptions) { } o.Callback = &nav.LabelledTraverseCallback{ Label: "Root Entry Callback", - Fn: func(item *nav.TraverseItem) error { - depth := item.Extension.Depth - indicator := lo.Ternary(len(item.Children) > 0, "โ˜€๏ธ", "๐ŸŒŠ") - - for _, entry := range item.Children { - fullPath := filepath.Join(item.Path, entry.Name()) - e.files = append(e.files, fullPath) - } - - fmt.Printf( - "---> %v ROOT-CALLBACK: (depth:%v, files:%v) '%v'\n", - indicator, - depth, len(item.Children), - item.Path, - ) - - return e.Program.Execute("--version") - }, + Fn: e.principalFn, } o.Store.Subscription = nav.SubscribeFoldersWithFiles o.Store.DoExtend = true @@ -71,35 +73,9 @@ func (e *RootEntry) ConfigureOptions(o *nav.TraverseOptions) { } func (e *RootEntry) run() error { - files := []string{} runnerWith := composeWith(e.Inputs.ParamSet) - resumption := &nav.Resumption{ - RestorePath: "/json-path-to-come-from-a-flag-option/restore.json", - Restorer: func(o *nav.TraverseOptions, active *nav.ActiveState) { - o.Callback = &nav.LabelledTraverseCallback{ - Label: "Root Entry Callback", - Fn: func(item *nav.TraverseItem) error { - depth := item.Extension.Depth - indicator := lo.Ternary(len(item.Children) > 0, "โ˜€๏ธ", "๐ŸŒŠ") - - for _, entry := range item.Children { - fullPath := filepath.Join(item.Path, entry.Name()) - files = append(files, fullPath) - } - - fmt.Printf( - "---> %v ROOT-CALLBACK: (depth:%v, files:%v) '%v'\n", - indicator, - depth, len(item.Children), - item.Path, - ) - - return nil - }, - } - }, - Strategy: nav.ResumeStrategySpawnEn, // to come from an arg - } + + var nilResumption *nav.Resumption // root does not need to support resume after := func(result *nav.TraverseResult, err error) { for _, file := range e.files { @@ -107,7 +83,21 @@ func (e *RootEntry) run() error { } } - return e.navigate(GetTraverseOptionsFunc(e), runnerWith, resumption, after) + principal := func(o *nav.TraverseOptions) { + e.ConfigureOptions(o) + o.Callback = &nav.LabelledTraverseCallback{ + Label: "Root Entry Callback", + Fn: e.principalFn, + } + } + + return e.navigate( + principal, + runnerWith, + nilResumption, + after, + summariseAfter, + ) } func composeWith(rps *assistant.ParamSet[RootParameterSet]) nav.CreateNewRunnerWith { diff --git a/src/app/proxy/enter-shrink.go b/src/app/proxy/enter-shrink.go index 90f4833..e5931b0 100644 --- a/src/app/proxy/enter-shrink.go +++ b/src/app/proxy/enter-shrink.go @@ -2,7 +2,12 @@ package proxy import ( "fmt" + "os" + "path" + "path/filepath" + "strings" + "github.com/pkg/errors" "github.com/samber/lo" "github.com/snivilised/cobrass" "github.com/snivilised/cobrass/src/assistant/configuration" @@ -16,17 +21,57 @@ type ShrinkEntry struct { jobs []string } -func (e *ShrinkEntry) ConfigureOptions(o *nav.TraverseOptions) { - o.Notify.OnBegin = func(_ *nav.NavigationState) { - fmt.Printf("===> ๐Ÿ›ก๏ธ beginning traversal ...\n") +func FilenameWithoutExtension(name string) string { + return strings.TrimSuffix(name, path.Ext(name)) +} + +func (e *ShrinkEntry) principalFn(item *nav.TraverseItem) error { + depth := item.Extension.Depth + + fmt.Printf( + "---> ๐Ÿ“œ SHRINK-CALLBACK-FILE: (depth:%v) '%v'\n", + depth, + item.Path, + ) + + positional := []string{ + fmt.Sprintf("'%v'", item.Path), } - o.Notify.OnEnd = func(result *nav.TraverseResult) { - fmt.Printf("===> ๐Ÿšฉ finished traversal - folders '%v'\n", - result.Metrics.Count(nav.MetricNoFoldersInvokedEn), - ) + + return e.Program.Execute(clif.Expand(positional, e.ThirdPartyCL)...) +} + +func (e *ShrinkEntry) LookAheadOptionsFn(o *nav.TraverseOptions) { + e.ConfigureOptions(o) + o.Callback = &nav.LabelledTraverseCallback{ + Label: "LookAhead: Shrink Entry Callback", + Fn: func(item *nav.TraverseItem) error { + withoutExt := FilenameWithoutExtension(item.Extension.Name) + ".journal.txt" + pathWithoutExt := filepath.Join(item.Extension.Parent, withoutExt) + + // TODO: only create file if not exits + // + file, err := os.Create(pathWithoutExt) // TODO: use vfs + + if err == nil { + defer file.Close() + } + + fmt.Printf( + "---> ๐Ÿ“œ SHRINK-JOURNAL-FILE: (create journal:%v) '%v'\n", + pathWithoutExt, + item.Path, + ) + + return err + }, } +} + +func (e *ShrinkEntry) PrincipalOptionsFn(o *nav.TraverseOptions) { + e.ConfigureOptions(o) o.Callback = &nav.LabelledTraverseCallback{ - Label: "Shrink Entry Callback", + Label: "Principal: Shrink Entry Callback", Fn: func(item *nav.TraverseItem) error { depth := item.Extension.Depth @@ -39,15 +84,74 @@ func (e *ShrinkEntry) ConfigureOptions(o *nav.TraverseOptions) { positional := []string{ fmt.Sprintf("'%v'", item.Path), } + return e.Program.Execute(clif.Expand(positional, e.ThirdPartyCL)...) }, } +} + +func (e *ShrinkEntry) ConfigureOptions(o *nav.TraverseOptions) { + o.Notify.OnBegin = func(_ *nav.NavigationState) { + fmt.Printf("===> ๐Ÿ›ก๏ธ beginning traversal ...\n") + } + o.Notify.OnEnd = func(result *nav.TraverseResult) { + fmt.Printf("===> ๐Ÿšฉ finished traversal - folders '%v'\n", + result.Metrics.Count(nav.MetricNoFoldersInvokedEn), + ) + } o.Store.Subscription = nav.SubscribeFiles o.Store.DoExtend = true e.EntryBase.ConfigureOptions(o) } +func clearResumeFromWith(with nav.CreateNewRunnerWith) nav.CreateNewRunnerWith { + // ref: https://go.dev/ref/spec#Arithmetic_operators + // + return (with &^ nav.RunnerWithResume) +} + +func (e *ShrinkEntry) navigateWithLookAhead( + with nav.CreateNewRunnerWith, + resumption *nav.Resumption, + after ...afterFunc, +) error { + var nilResumption *nav.Resumption + + if err := e.navigate( + e.LookAheadOptionsFn, + clearResumeFromWith(with), + nilResumption, + ); err != nil { + return errors.Wrap(err, "shrink look-ahead phase failed") + } + + return e.navigate( + e.PrincipalOptionsFn, + with, + resumption, + after..., + ) +} + +func (e *ShrinkEntry) resumeFn(item *nav.TraverseItem) error { + depth := item.Extension.Depth + indicator := lo.Ternary(len(item.Children) > 0, "โ˜€๏ธ", "๐ŸŒŠ") + + fmt.Printf( + "---> %v SHRINK-RESTORE-CALLBACK: (depth:%v) '%v'\n", + indicator, + depth, + item.Path, + ) + + positional := []string{ + fmt.Sprintf("'%v'", item.Path), + } + + return e.Program.Execute(clif.Expand(positional, e.ThirdPartyCL)...) +} + func (e *ShrinkEntry) run(config configuration.ViperConfig) error { _ = config @@ -59,33 +163,29 @@ func (e *ShrinkEntry) run(config configuration.ViperConfig) error { runnerWith := composeWith(e.Inputs.RootInputs.ParamSet) resumption := &nav.Resumption{ + // actually, we need to come up with a convenient way for the restore + // file to be found. Let's assume we declare a specific location for + // resume files, eg ~/.pixa/resume/resumeAt..json + // this way, we can make it easy for the user and potentially + // an auto-select feature, assuming only a single file was found. + // If there are multiple found, then display a menu from which + // the user can select. + // RestorePath: "/json-path-to-come-from-a-flag-option/restore.json", Restorer: func(o *nav.TraverseOptions, active *nav.ActiveState) { o.Callback = &nav.LabelledTraverseCallback{ - Label: "Shrink Entry Callback", - Fn: func(item *nav.TraverseItem) error { - depth := item.Extension.Depth - indicator := lo.Ternary(len(item.Children) > 0, "โ˜€๏ธ", "๐ŸŒŠ") - - fmt.Printf( - "---> %v SHRINK-RESTORE-CALLBACK: (depth:%v) '%v'\n", - indicator, - depth, - item.Path, - ) - - positional := []string{ - fmt.Sprintf("'%v'", item.Path), - } - - return e.Program.Execute(clif.Expand(positional, e.ThirdPartyCL)...) - }, + Label: "Resume Shrink Entry Callback", + Fn: e.resumeFn, } }, Strategy: nav.ResumeStrategySpawnEn, // to come from an arg } - return e.navigate(GetTraverseOptionsFunc(e), runnerWith, resumption) + return e.navigateWithLookAhead( + runnerWith, + resumption, + summariseAfter, + ) } func EnterShrink( diff --git a/src/app/proxy/entry-base.go b/src/app/proxy/entry-base.go index d4c2a26..e453754 100644 --- a/src/app/proxy/entry-base.go +++ b/src/app/proxy/entry-base.go @@ -16,14 +16,31 @@ import ( type afterFunc func(*nav.TraverseResult, error) +func summariseAfter(result *nav.TraverseResult, err error) { + measure := fmt.Sprintf("started: '%v', elapsed: '%v'", + result.Session.StartedAt().Format(time.RFC1123), result.Session.Elapsed(), + ) + files := result.Metrics.Count(nav.MetricNoFilesInvokedEn) + folders := result.Metrics.Count(nav.MetricNoFoldersInvokedEn) + summary := fmt.Sprintf("files: %v, folders: %v", files, folders) + message := lo.Ternary(err == nil, + fmt.Sprintf("navigation completed (%v) โœ”๏ธ [%v]", summary, measure), + fmt.Sprintf("error occurred during navigation (%v)โŒ [%v]", err, measure), + ) + fmt.Println(message) +} + type EntryBase struct { Inputs *RootCommandInputs Program Executor Config configuration.ViperConfig ThirdPartyCL cobrass.ThirdPartyCommandLine + Options *nav.TraverseOptions } func (e *EntryBase) ConfigureOptions(o *nav.TraverseOptions) { + e.Options = o + // TODO: to apply the folder filters in combination with these // file filters, we need to define a custom compound // filter. @@ -129,21 +146,9 @@ func (e *EntryBase) navigate( nav.IfWithPoolUseContext(with, ctx, cancel)..., ) - if len(after) > 0 { - after[0](result, err) + for _, fn := range after { + fn(result, err) } - measure := fmt.Sprintf("started: '%v', elapsed: '%v'", - result.Session.StartedAt().Format(time.RFC1123), result.Session.Elapsed(), - ) - files := result.Metrics.Count(nav.MetricNoFilesInvokedEn) - folders := result.Metrics.Count(nav.MetricNoFoldersInvokedEn) - summary := fmt.Sprintf("files: %v, folders: %v", files, folders) - message := lo.Ternary(err == nil, - fmt.Sprintf("navigation completed (%v) โœ”๏ธ [%v]", summary, measure), - fmt.Sprintf("error occurred during navigation (%v)โŒ [%v]", err, measure), - ) - fmt.Println(message) - return err } diff --git a/src/app/proxy/generic.go b/src/app/proxy/generic.go deleted file mode 100644 index b735101..0000000 --- a/src/app/proxy/generic.go +++ /dev/null @@ -1,15 +0,0 @@ -package proxy - -import ( - "github.com/snivilised/extendio/xfs/nav" -) - -type GenericEntry interface { - ConfigureOptions(o *nav.TraverseOptions) -} - -func GetTraverseOptionsFunc[E GenericEntry](entry E) func(o *nav.TraverseOptions) { - return func(o *nav.TraverseOptions) { - entry.ConfigureOptions(o) - } -}