diff --git a/xfs/nav/helpers_test.go b/xfs/nav/helpers_test.go index c98fbe4..446ffb1 100644 --- a/xfs/nav/helpers_test.go +++ b/xfs/nav/helpers_test.go @@ -117,6 +117,12 @@ type resumeTE struct { log bool } +type cascadeTE struct { + naviTE + skim bool + depth uint +} + type resumeTestProfile struct { filtered bool prohibited map[string]string diff --git a/xfs/nav/navigation-periscope.go b/xfs/nav/navigation-periscope.go index 3856c7f..edb3acf 100644 --- a/xfs/nav/navigation-periscope.go +++ b/xfs/nav/navigation-periscope.go @@ -50,8 +50,14 @@ func (p *navigationPeriscope) difference(root, current string) { p._offset = currentSize - rootSize } -func (p *navigationPeriscope) descend() { +func (p *navigationPeriscope) descend(max uint) bool { + if max > 0 && p._depth > int(max) { + return false + } + p._depth++ + + return true } func (p *navigationPeriscope) ascend() { diff --git a/xfs/nav/navigator-abstract.go b/xfs/nav/navigator-abstract.go index a8da808..e9e28ff 100644 --- a/xfs/nav/navigator-abstract.go +++ b/xfs/nav/navigator-abstract.go @@ -102,14 +102,21 @@ func (n *navigator) logger() log.Logger { return n.log.Get() } -func (n *navigator) descend(navi *NavigationInfo) { - navi.frame.periscope.descend() +func (n *navigator) descend(navi *NavigationInfo) bool { + if !navi.frame.periscope.descend(n.o.Store.Behaviours.Cascade.Depth) { + return false + } + navi.frame.notifiers.descend.invoke(navi.Item) + + return true } -func (n *navigator) ascend(navi *NavigationInfo) { - navi.frame.periscope.ascend() - navi.frame.notifiers.ascend.invoke(navi.Item) +func (n *navigator) ascend(navi *NavigationInfo, permit bool) { + if permit { + navi.frame.periscope.ascend() + navi.frame.notifiers.ascend.invoke(navi.Item) + } } func (n *navigator) finish() error { diff --git a/xfs/nav/navigator-files.go b/xfs/nav/navigator-files.go index 2882af4..9fc1dd3 100644 --- a/xfs/nav/navigator-files.go +++ b/xfs/nav/navigator-files.go @@ -45,21 +45,25 @@ func (n *filesNavigator) traverse(params *traverseParams) (*TraverseItem, error) Item: params.current, frame: params.frame, } + params.navi = navi + descended := n.descend(navi) + // // For files, the registered callback will only be invoked for file entries. This means // that the client will have no way to skip the descending of a particular directory. In // this case, the client should use the OnDescend callback (yet to be implemented) and // return SkipDir from there. - defer func() { + defer func(permit bool) { if n.samplingFilterActive { delete(n.agent.cache, params.current.key()) } - n.ascend(navi) - }() + n.ascend(navi, permit) + }(descended) - params.navi = navi - n.descend(navi) + if !descended { + return nil, nil + } stash := n.inspect(params) diff --git a/xfs/nav/navigator-folders.go b/xfs/nav/navigator-folders.go index bc75664..6786314 100644 --- a/xfs/nav/navigator-folders.go +++ b/xfs/nav/navigator-folders.go @@ -76,16 +76,21 @@ func (n *foldersNavigator) traverse(params *traverseParams) (*TraverseItem, erro Item: params.current, frame: params.frame, } - defer func() { + + params.navi = navi + descended := n.descend(navi) + + defer func(permit bool) { if n.samplingFilterActive { delete(n.agent.cache, params.current.key()) } - n.ascend(navi) - }() + n.ascend(navi, permit) + }(descended) - params.navi = navi - n.descend(navi) + if !descended { + return nil, nil + } stash := n.inspect(params) entries := stash.entries diff --git a/xfs/nav/navigator-universal.go b/xfs/nav/navigator-universal.go index b3fea44..e8a728b 100644 --- a/xfs/nav/navigator-universal.go +++ b/xfs/nav/navigator-universal.go @@ -49,16 +49,20 @@ func (n *universalNavigator) traverse(params *traverseParams) (*TraverseItem, er frame: params.frame, } - defer func() { + params.navi = navi + descended := n.descend(navi) + + defer func(permit bool) { if n.samplingFilterActive { delete(n.agent.cache, params.current.key()) } - n.ascend(navi) - }() + n.ascend(navi, permit) + }(descended) - params.navi = navi - n.descend(navi) + if !descended { + return nil, nil + } stash := n.inspect(params) entries := stash.entries diff --git a/xfs/nav/sampling-while-iterator.go b/xfs/nav/sampling-while-iterator.go index 4a9ab99..c2f9150 100644 --- a/xfs/nav/sampling-while-iterator.go +++ b/xfs/nav/sampling-while-iterator.go @@ -138,7 +138,7 @@ func (i *directoryEntryWhileIt) start(entries []fs.DirEntry) { func (i *directoryEntryWhileIt) sample(entries []fs.DirEntry, processingFiles bool) []fs.DirEntry { i.start(entries) - result := i.loop() + result := i.enumerate() return lo.Ternary[[]fs.DirEntry](processingFiles, result.Files, result.Folders) } @@ -148,7 +148,7 @@ func (i *directoryEntryWhileIt) samples( ) (files, folders []fs.DirEntry) { i.start(sourceEntries.All()) - result := i.loop() + result := i.enumerate() files = result.Files folders = result.Folders @@ -156,7 +156,7 @@ func (i *directoryEntryWhileIt) samples( return } -func (i *directoryEntryWhileIt) loop() *DirectoryContents { +func (i *directoryEntryWhileIt) enumerate() *DirectoryContents { result := newEmptyDirectoryEntries(i.o, &i.o.Store.Sampling.NoOf) parent := i.tp.current diff --git a/xfs/nav/traverse-navigator-cascade_test.go b/xfs/nav/traverse-navigator-cascade_test.go new file mode 100644 index 0000000..d0e3797 --- /dev/null +++ b/xfs/nav/traverse-navigator-cascade_test.go @@ -0,0 +1,242 @@ +package nav_test + +import ( + "fmt" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + . "github.com/snivilised/extendio/i18n" + "github.com/snivilised/extendio/internal/helpers" + "github.com/snivilised/extendio/xfs/nav" +) + +var _ = Describe("TraverseNavigatorCascade", Ordered, func() { + var root string + + BeforeAll(func() { + root = musico() + }) + + BeforeEach(func() { + if err := Use(func(o *UseOptions) { + o.Tag = DefaultLanguage.Get() + }); err != nil { + Fail(err.Error()) + } + }) + + DescribeTable("cascade", + func(entry *cascadeTE) { + path := helpers.Path(root, entry.relative) + optionFn := func(o *nav.TraverseOptions) { + o.Notify.OnBegin = begin("🛡️") + o.Store.Subscription = entry.subscription + o.Callback = entry.callback + o.Store.Behaviours.Cascade.Skim = entry.skim + o.Store.Behaviours.Cascade.Depth = entry.depth + } + + result, err := nav.New().Primary(&nav.Prime{ + Path: path, + OptionsFn: optionFn, + }).Run() + _ = result + + Expect(err).Error().To(BeNil()) + + Expect(result.Metrics.Count(nav.MetricNoFilesInvokedEn)).To(Equal(entry.expectedNoOf.files), + "Incorrect no of files") + Expect(result.Metrics.Count(nav.MetricNoFoldersInvokedEn)).To(Equal(entry.expectedNoOf.folders), + "Incorrect no of folders") + }, + func(entry *cascadeTE) string { + return fmt.Sprintf("🧪 ===> given: '%v', should: '%v'", entry.message, entry.should) + }, + + // === universal ===================================================== + + Entry(nil, &cascadeTE{ + naviTE: naviTE{ + message: "universal: Path contains folders only, skim", + should: "traverse single level", + relative: "RETRO-WAVE", + subscription: nav.SubscribeAny, + callback: universalScopeCallback("CONTAINS-FOLDERS"), + expectedNoOf: directoryQuantities{ + files: 0, + folders: 4, + }, + }, + skim: true, + }), + + Entry(nil, &cascadeTE{ + naviTE: naviTE{ + message: "universal: Path contains files only, skim", + should: "traverse single level (containing files)", + relative: "RETRO-WAVE/Chromatics/Night Drive", + subscription: nav.SubscribeAny, + callback: universalScopeCallback("CONTAINS-FILES"), + expectedNoOf: directoryQuantities{ + files: 4, + folders: 1, + }, + }, + skim: true, + }), + + Entry(nil, &cascadeTE{ + naviTE: naviTE{ + message: "universal: Path contains folders only, depth=1", + should: "traverse single level", + relative: "RETRO-WAVE", + subscription: nav.SubscribeAny, + callback: universalScopeCallback("CONTAINS-FOLDERS"), + expectedNoOf: directoryQuantities{ + files: 0, + folders: 4, + }, + }, + depth: 1, + }), + + Entry(nil, &cascadeTE{ + naviTE: naviTE{ + message: "universal: Path contains folders only, depth=2", + should: "traverse 2 levels", + relative: "RETRO-WAVE", + subscription: nav.SubscribeAny, + callback: universalScopeCallback("CONTAINS-FOLDERS"), + expectedNoOf: directoryQuantities{ + files: 0, + folders: 8, + }, + }, + depth: 2, + }), + + Entry(nil, &cascadeTE{ + naviTE: naviTE{ + message: "universal: Path contains folders only, depth=3", + should: "traverse 3 levels (containing files)", + relative: "RETRO-WAVE", + subscription: nav.SubscribeAny, + callback: universalScopeCallback("CONTAINS-FOLDERS"), + expectedNoOf: directoryQuantities{ + files: 14, + folders: 8, + }, + }, + depth: 3, + }), + + // === folders ======================================================= + + Entry(nil, &cascadeTE{ + naviTE: naviTE{ + message: "universal: Path contains folders only, skim", + should: "traverse single level", + relative: "RETRO-WAVE", + subscription: nav.SubscribeFolders, + callback: foldersCallback("CONTAINS-FILES"), + expectedNoOf: directoryQuantities{ + files: 0, + folders: 4, + }, + }, + skim: true, + }), + + Entry(nil, &cascadeTE{ + naviTE: naviTE{ + message: "universal: Path contains files only, skim", + should: "traverse single level (containing files)", + relative: "RETRO-WAVE/Chromatics/Night Drive", + subscription: nav.SubscribeFolders, + callback: universalScopeCallback("LEAF-PATH"), + expectedNoOf: directoryQuantities{ + files: 0, + folders: 1, + }, + }, + skim: true, + }), + + Entry(nil, &cascadeTE{ + naviTE: naviTE{ + message: "universal: Path contains folders only, depth=1", + should: "traverse single level", + relative: "RETRO-WAVE", + subscription: nav.SubscribeFolders, + callback: universalScopeCallback("CONTAINS-FOLDERS"), + expectedNoOf: directoryQuantities{ + files: 0, + folders: 4, + }, + }, + depth: 1, + }), + + Entry(nil, &cascadeTE{ + naviTE: naviTE{ + message: "universal: Path contains folders only, depth=2", + should: "traverse 2 levels", + relative: "RETRO-WAVE", + subscription: nav.SubscribeFolders, + callback: universalScopeCallback("CONTAINS-FOLDERS"), + expectedNoOf: directoryQuantities{ + files: 0, + folders: 8, + }, + }, + depth: 2, + }), + + Entry(nil, &cascadeTE{ + naviTE: naviTE{ + message: "universal: Path contains folders only, depth=3", + should: "traverse 3 levels (containing files)", + relative: "RETRO-WAVE", + subscription: nav.SubscribeFolders, + callback: universalScopeCallback("CONTAINS-FOLDERS"), + expectedNoOf: directoryQuantities{ + files: 0, + folders: 8, + }, + }, + depth: 3, + }), + + // === files ========================================================= + + Entry(nil, &cascadeTE{ + naviTE: naviTE{ + message: "file: Path contains folders only, skim", + should: "traverse single level", + relative: "RETRO-WAVE/Chromatics/Night Drive", + subscription: nav.SubscribeFiles, + callback: filesCallback("FILE"), + expectedNoOf: directoryQuantities{ + files: 4, + folders: 0, + }, + }, + skim: true, + }), + + Entry(nil, &cascadeTE{ + naviTE: naviTE{ + message: "file: Path contains folders only, depth=1", + should: "traverse single level", + relative: "RETRO-WAVE/Chromatics/Night Drive", + subscription: nav.SubscribeFiles, + callback: filesCallback("FILE"), + expectedNoOf: directoryQuantities{ + files: 4, + folders: 0, + }, + }, + depth: 1, + }), + ) +}) diff --git a/xfs/nav/traverse-options.go b/xfs/nav/traverse-options.go index bdf34b7..3cabf3c 100644 --- a/xfs/nav/traverse-options.go +++ b/xfs/nav/traverse-options.go @@ -26,6 +26,21 @@ type SortBehaviour struct { DirectoryEntryOrder DirectoryContentsOrderEnum } +type CascadeBehaviour struct { + // Depth sets a maximum traversal depth + // + Depth uint + + // Skim is an alternative to using Depth, but limits the traversal + // to just the path specified by the user. Since the raison d'etre + // of the navigator is to recursively process a directory tree, using + // Skim would appear to be contrary to its natural behaviour. However + // there are clear usage scenarios where a client needs to process + // only the files in a specified directory. + // + Skim bool +} + // NavigationBehaviours type NavigationBehaviours struct { // SubPath, behaviours relating to handling of sub-path calculation @@ -39,6 +54,10 @@ type NavigationBehaviours struct { // Listen, behaviours relating to listen functionality. // Listen ListenBehaviour + + // Cascade controls how deep to navigate + // + Cascade CascadeBehaviour } // Notifications @@ -301,6 +320,10 @@ func (o *TraverseOptions) afterUserOptions() { noEach := o.Sampler.Custom.Each == nil && o.Sampler.Custom.While != nil noWhile := o.Sampler.Custom.Each != nil && o.Sampler.Custom.While == nil + if o.Store.Behaviours.Cascade.Skim { + o.Store.Behaviours.Cascade.Depth = 1 + } + if noEach || noWhile { panic("invalid SamplingIteratorOptions (set both or neither: Each, While)") } diff --git a/xfs/nav/traverse-samplers.go b/xfs/nav/traverse-samplers.go index 436f086..47ee008 100644 --- a/xfs/nav/traverse-samplers.go +++ b/xfs/nav/traverse-samplers.go @@ -6,8 +6,7 @@ import ( "github.com/samber/lo" ) -func sampleWithSliceController(params *samplerControllerFuncParams, -) { +func sampleWithSliceController(params *samplerControllerFuncParams) { params.adapters[params.subscription].slice( params.contents, params.noOf,