diff --git a/.vscode/settings.json b/.vscode/settings.json index 74db50d..4c7c8a5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,6 +5,7 @@ ], "cSpell.words": [ "alloc", + "argh", "astrolib", "Berthe", "bodyclose", @@ -20,6 +21,7 @@ "dogsled", "dotenv", "dupl", + "ents", "errcheck", "Errorf", "exportloopref", @@ -54,6 +56,7 @@ "mattn", "musico", "nakedret", + "navi", "nolint", "nolintlint", "onsi", @@ -63,6 +66,7 @@ "pixa", "prealloc", "repotoken", + "rsys", "samber", "shogo", "sidewalk", diff --git a/builders.go b/builders.go index 2a89115..a787dac 100644 --- a/builders.go +++ b/builders.go @@ -15,53 +15,62 @@ type buildArtefacts struct { } type Builders struct { - fs pref.FsBuilder - options optionsBuilder - navigator kernel.NavigatorBuilder - plugins pluginsBuilder - extent extentBuilder + filesystem pref.FileSystemBuilder + options optionsBuilder + navigator kernel.NavigatorBuilder + plugins pluginsBuilder + extent extentBuilder } func (bs *Builders) buildAll() (*buildArtefacts, error) { - ext := bs.extent.build(bs.fs.Build()) + // BUILD FILE SYSTEM & EXTENT + // + ext := bs.extent.build(bs.filesystem.Build()) + // BUILD OPTIONS + // o, optionsErr := bs.options.build(ext) if optionsErr != nil { - had := kernel.HadesNav(optionsErr) - return &buildArtefacts{ o: o, - nav: had, + nav: kernel.HadesNav(optionsErr), ext: ext, }, optionsErr } - artefacts, navErr := bs.navigator.Build(o) + // BUILD NAVIGATOR + // + artefacts, navErr := bs.navigator.Build(o, &types.Resources{ + FS: types.FileSystems{ + N: ext.navFS(), + R: ext.resFS(), + }, + }) if navErr != nil { - had := kernel.HadesNav(navErr) - return &buildArtefacts{ o: o, - nav: had, + nav: kernel.HadesNav(navErr), ext: ext, }, navErr } + // BUILD PLUGINS + // plugins, pluginsErr := bs.plugins.build(o, artefacts.Mediator, ext.plugin(artefacts.Mediator), ) if pluginsErr != nil { - had := kernel.HadesNav(pluginsErr) - return &buildArtefacts{ o: o, - nav: had, + nav: kernel.HadesNav(pluginsErr), ext: ext, }, pluginsErr } + // INIT PLUGINS + // for _, p := range plugins { if bindErr := p.Init(); bindErr != nil { return &buildArtefacts{ diff --git a/core/core-defs.go b/core/core-defs.go index 2369de7..fe885c7 100644 --- a/core/core-defs.go +++ b/core/core-defs.go @@ -1,7 +1,7 @@ package core -// core contains universal definitions and handles cross cutting concerns -// try to keep to a minimum to reduce rippling changes +// core contains universal definitions and handles user facing cross +// cutting concerns try to keep to a minimum to reduce rippling changes. type ( // TraverseResult diff --git a/core/directory-contents.go b/core/directory-contents.go index 3275524..af9b915 100644 --- a/core/directory-contents.go +++ b/core/directory-contents.go @@ -8,7 +8,7 @@ import ( // handles sorting order which by default is different between various // operating systems. This abstraction removes the differences in sorting // behaviour on different platforms. -type DirectoryContents struct { - Folders []fs.DirEntry - Files []fs.DirEntry +type DirectoryContents interface { + Folders() []fs.DirEntry + Files() []fs.DirEntry } diff --git a/core/hooks.go b/core/hooks.go index bb16c96..9514d34 100644 --- a/core/hooks.go +++ b/core/hooks.go @@ -17,7 +17,7 @@ type ( // SortHook hook function to define how directory entries are sorted. Does not // have to be set explicitly. This will be set according to the IsCaseSensitive on // the TraverseOptions, but can be overridden if needed. - SortHook func(entries []fs.DirEntry, custom ...any) error + SortHook func(entries []fs.DirEntry, custom ...any) SubPathInfo struct { Root string diff --git a/core/node.go b/core/node.go index d8c3945..3174c78 100644 --- a/core/node.go +++ b/core/node.go @@ -36,7 +36,7 @@ type Extension struct { Custom any // to be set and used by the client } -// New create a new node Event +// New create a new Node func New( path string, entry fs.DirEntry, info fs.FileInfo, parent *Node, err error, ) *Node { @@ -53,8 +53,8 @@ func New( return node } -// Root creates a new node Event which represents the root of directory -// tree to traverse. +// Root creates a new Node which represents the root of the +// directory tree to traverse. func Root(root string, info fs.FileInfo) *Node { node := &Node{ Path: root, @@ -66,7 +66,7 @@ func Root(root string, info fs.FileInfo) *Node { return node } -// Clone makes shallow copy of Event (excluding the error). +// Clone makes shallow copy of Node (excluding the error). func (n *Node) Clone() *Node { c := *n c.Error = nil @@ -74,14 +74,13 @@ func (n *Node) Clone() *Node { return &c } -// IsFolder indicates wether this event is a folder. +// IsFolder indicates wether this node is a folder. func (n *Node) IsFolder() bool { return n.dir } func (n *Node) key() string { - // ti.Extension.SubPath - return "ti.Extension.SubPath" + return n.Extension.SubPath } func isDir(n *Node) bool { diff --git a/director.go b/director.go index 1861cb4..20ac01b 100644 --- a/director.go +++ b/director.go @@ -61,7 +61,7 @@ func Prime(using *pref.Using, settings ...pref.Option) *Builders { // by a panic. // return &Builders{ - fs: pref.FileSystem(func() fs.FS { + filesystem: pref.FileSystem(func() fs.FS { if using.GetFS != nil { return using.GetFS() } @@ -88,8 +88,8 @@ func Prime(using *pref.Using, settings ...pref.Option) *Builders { return ext.options(settings...) }), - navigator: kernel.Builder(func(o *pref.Options) (*kernel.Artefacts, error) { - return kernel.New(using, o, &kernel.Benign{}), nil + navigator: kernel.Builder(func(o *pref.Options, res *types.Resources) (*kernel.Artefacts, error) { + return kernel.New(using, o, &kernel.Benign{}, res), nil }), plugins: features(activated), // swap over features & activated } @@ -106,7 +106,7 @@ func Resume(was *Was, settings ...pref.Option) *Builders { // path was. // return &Builders{ - fs: pref.FileSystem(func() fs.FS { + filesystem: pref.FileSystem(func() fs.FS { if was.Using.GetFS != nil { return was.Using.GetFS() } @@ -132,8 +132,8 @@ func Resume(was *Was, settings ...pref.Option) *Builders { return ext.options(settings...) }), - navigator: kernel.Builder(func(o *pref.Options) (*kernel.Artefacts, error) { - artefacts := kernel.New(&was.Using, o, resume.GetSealer(was)) + navigator: kernel.Builder(func(o *pref.Options, res *types.Resources) (*kernel.Artefacts, error) { + artefacts := kernel.New(&was.Using, o, resume.GetSealer(was), res) return resume.NewController(was, artefacts), nil }), diff --git a/enums/entry-type-en.go b/enums/entry-type-en.go new file mode 100644 index 0000000..83813c7 --- /dev/null +++ b/enums/entry-type-en.go @@ -0,0 +1,14 @@ +package enums + +// EntryType used to enable selecting directory entry type. +type EntryType uint + +const ( + // EntryTypeFolder + // + EntryTypeFolder EntryType = iota // folder-entry + + // EntryTypeFile + // + EntryTypeFile // file-entry +) diff --git a/extent.go b/extent.go index 0741d6b..7d52b7b 100644 --- a/extent.go +++ b/extent.go @@ -14,6 +14,8 @@ type extent interface { was() *pref.Was plugin(types.Mediator) types.Plugin options(...pref.Option) (*pref.Options, error) + navFS() fs.FS + resFS() fs.FS } type fileSystems struct { @@ -25,6 +27,14 @@ type baseExtent struct { fsys fileSystems } +func (ex *baseExtent) navFS() fs.FS { + return ex.fsys.nas +} + +func (ex *baseExtent) resFS() fs.FS { + return ex.fsys.nas +} + type primeExtent struct { baseExtent u *pref.Using diff --git a/factories.go b/factories.go index 600e540..caf0a1c 100644 --- a/factories.go +++ b/factories.go @@ -42,10 +42,10 @@ func (f *walkerFac) Configure() Director { session{ sync: &sequential{ trunk: trunk{ - nav: artefacts.nav, - o: artefacts.o, - extent: artefacts.ext, - err: err, + nav: artefacts.nav, + o: artefacts.o, + ext: artefacts.ext, + err: err, }, }, plugins: artefacts.plugins, @@ -70,10 +70,10 @@ func (f *runnerFac) Configure() Director { session{ sync: &concurrent{ trunk: trunk{ - nav: artefacts.nav, - o: artefacts.o, - extent: artefacts.ext, - err: err, + nav: artefacts.nav, + o: artefacts.o, + ext: artefacts.ext, + err: err, }, wg: f.wg, }, diff --git a/internal/hiber/hibernate-plugin.go b/internal/hiber/hibernate-plugin.go index e1d9afe..31b7ee4 100644 --- a/internal/hiber/hibernate-plugin.go +++ b/internal/hiber/hibernate-plugin.go @@ -9,9 +9,7 @@ import ( ) func IfActive(o *pref.Options, mediator types.Mediator) types.Plugin { - active := o.Core.Hibernate.Wake != nil - - if active { + if o.Core.Hibernate.Wake != nil { return &Plugin{ BasePlugin: kernel.BasePlugin{ Mediator: mediator, diff --git a/internal/kernel/builder.go b/internal/kernel/builder.go index 79029f5..0efa47d 100644 --- a/internal/kernel/builder.go +++ b/internal/kernel/builder.go @@ -11,15 +11,16 @@ type ( Navigator core.Navigator Mediator types.Mediator Facilities types.Facilities + Resources *types.Resources } NavigatorBuilder interface { - Build(o *pref.Options) (*Artefacts, error) + Build(o *pref.Options, res *types.Resources) (*Artefacts, error) } - Builder func(o *pref.Options) (*Artefacts, error) + Builder func(o *pref.Options, res *types.Resources) (*Artefacts, error) ) -func (fn Builder) Build(o *pref.Options) (*Artefacts, error) { - return fn(o) +func (fn Builder) Build(o *pref.Options, res *types.Resources) (*Artefacts, error) { + return fn(o, res) } diff --git a/internal/kernel/directory-contents.go b/internal/kernel/directory-contents.go new file mode 100644 index 0000000..6b323de --- /dev/null +++ b/internal/kernel/directory-contents.go @@ -0,0 +1,128 @@ +package kernel + +import ( + "io/fs" + + "github.com/snivilised/traverse/enums" + "github.com/snivilised/traverse/internal/lo" + "github.com/snivilised/traverse/pref" +) + +func newDirectoryContents(o *pref.Options, + entries []fs.DirEntry, +) *DirectoryContents { + contents := DirectoryContents{ + o: o, + } + + contents.Arrange(entries) + + return &contents +} + +// DirectoryContents represents the contents of a directory's contents and +// handles sorting order which by default is different between various +// operating systems. This abstraction removes the differences in sorting +// behaviour on different platforms. +type DirectoryContents struct { + folders []fs.DirEntry + files []fs.DirEntry + o *pref.Options +} + +func (c *DirectoryContents) Folders() []fs.DirEntry { + return c.folders +} + +func (c *DirectoryContents) Files() []fs.DirEntry { + return c.files +} + +// All returns the contents of a directory respecting the directory sorting +// order defined in the traversal options. +func (c *DirectoryContents) All() []fs.DirEntry { + result := make([]fs.DirEntry, 0, len(c.files)+len(c.folders)) + + switch c.o.Core.Behaviours.Sort.DirectoryEntryOrder { + case enums.DirectoryContentsOrderFoldersFirst: + result = c.folders + result = append(result, c.files...) + + case enums.DirectoryContentsOrderFilesFirst: + result = c.files + result = append(result, c.folders...) + } + + return result +} + +// Sort will sort either the folders or files or if no +// entry type is specified, sort both. +func (c *DirectoryContents) Sort(ents ...enums.EntryType) { + // This looks complicated, but it really isn't. The reason is + // we only want to sort only what's really required and the sorting + // of entries must be separated by type, ie we dont want all the + // files and folders sorted into a single collection and the + // requested entry types need to be mapped into the corresponding + // internal directory entries. + // + for _, entries := range lo.TernaryF(len(ents) == 0, + func() [][]fs.DirEntry { + return [][]fs.DirEntry{ + c.folders, c.files, + } + }, + func() [][]fs.DirEntry { + if ents[0] == enums.EntryTypeFolder { + return [][]fs.DirEntry{c.folders} + } + + return [][]fs.DirEntry{c.files} + }, + ) { + c.o.Hooks.Sort.Invoke()(entries) + } +} + +func (c *DirectoryContents) Clear() { + c.files = []fs.DirEntry{} + c.folders = []fs.DirEntry{} +} + +func (c *DirectoryContents) Arrange(entries []fs.DirEntry) { + grouped := lo.GroupBy(entries, func(entry fs.DirEntry) bool { + return entry.IsDir() + }) + + c.folders = grouped[true] + c.files = grouped[false] + + if c.folders == nil { + c.folders = []fs.DirEntry{} + } + + if c.files == nil { + c.files = []fs.DirEntry{} + } +} + +func newEmptyDirectoryEntries(o *pref.Options, + prealloc ...*pref.EntryQuantities, +) *DirectoryContents { + return lo.TernaryF(len(prealloc) == 0, + func() *DirectoryContents { + return &DirectoryContents{ + o: o, + files: []fs.DirEntry{}, + folders: []fs.DirEntry{}, + } + }, + func() *DirectoryContents { + return &DirectoryContents{ + o: o, + files: make([]fs.DirEntry, 0, prealloc[0].Files), + folders: make([]fs.DirEntry, 0, prealloc[0].Folders), + } + }, + ) +} diff --git a/internal/kernel/directory-entries.go b/internal/kernel/directory-entries.go deleted file mode 100644 index 70ff742..0000000 --- a/internal/kernel/directory-entries.go +++ /dev/null @@ -1,64 +0,0 @@ -package kernel - -import ( - "io/fs" - - "github.com/snivilised/traverse/enums" - "github.com/snivilised/traverse/internal/lo" - "github.com/snivilised/traverse/pref" -) - -func newDirectoryContents(o *pref.Options, entries []fs.DirEntry) *DirectoryContents { - contents := DirectoryContents{ - o: o, - } - - contents.arrange(entries) - - return &contents -} - -// DirectoryContents represents the contents of a directory's contents and -// handles sorting order which by default is different between various -// operating systems. This abstraction removes the differences in sorting -// behaviour on different platforms. -type DirectoryContents struct { - Folders []fs.DirEntry - Files []fs.DirEntry - o *pref.Options -} - -// All returns the contents of a directory respecting the directory sorting -// order defined in the traversal options. -func (e *DirectoryContents) All() []fs.DirEntry { - result := make([]fs.DirEntry, 0, len(e.Files)+len(e.Folders)) - - switch e.o.Core.Behaviours.Sort.DirectoryEntryOrder { - case enums.DirectoryContentsOrderFoldersFirst: - result = e.Folders - result = append(result, e.Files...) - - case enums.DirectoryContentsOrderFilesFirst: - result = e.Files - result = append(result, e.Folders...) - } - - return result -} - -func (e *DirectoryContents) arrange(entries []fs.DirEntry) { - grouped := lo.GroupBy(entries, func(entry fs.DirEntry) bool { - return entry.IsDir() - }) - - e.Folders = grouped[true] - e.Files = grouped[false] - - if e.Folders == nil { - e.Folders = []fs.DirEntry{} - } - - if e.Files == nil { - e.Files = []fs.DirEntry{} - } -} diff --git a/internal/kernel/extend.go b/internal/kernel/extend.go new file mode 100644 index 0000000..ab8a8cf --- /dev/null +++ b/internal/kernel/extend.go @@ -0,0 +1,66 @@ +package kernel + +import ( + "path/filepath" + "strings" + + "github.com/snivilised/traverse/core" + "github.com/snivilised/traverse/enums" + "github.com/snivilised/traverse/internal/lo" +) + +func extend(ns *navigationStatic, vapour inspection) { + var ( + scope enums.FilterScope + isLeaf bool + current = vapour.current() + contents = vapour.contents() + ) + + if current.IsFolder() { + isLeaf = len(contents.Folders()) == 0 + scope = ns.mediator.frame.periscope.Scope(isLeaf) + scope |= enums.ScopeFolder + } else { + scope = enums.ScopeLeaf + scope |= enums.ScopeFile + } + + parent, name := filepath.Split(current.Path) + current.Extension = core.Extension{ + Depth: ns.mediator.frame.periscope.Depth(), + IsLeaf: isLeaf, + Name: name, + Parent: parent, + Scope: scope, + } + + keepTrailingSep := ns.mediator.o.Core.Behaviours.SubPath.KeepTrailingSep + + spInfo := &core.SubPathInfo{ + Root: ns.root, + Node: current, + KeepTrailingSep: keepTrailingSep, + } + + subpath := lo.TernaryF(current.IsFolder(), + func() string { return ns.mediator.o.Hooks.FolderSubPath.Invoke()(spInfo) }, + func() string { return ns.mediator.o.Hooks.FileSubPath.Invoke()(spInfo) }, + ) + + subpath = lo.TernaryF(keepTrailingSep, + func() string { return subpath }, + func() string { + result := subpath + sep := string(filepath.Separator) + + if strings.HasSuffix(subpath, sep) { + result = subpath[:strings.LastIndex(subpath, sep)] + } + + return result + }, + ) + + current.Extension.SubPath = subpath +} diff --git a/internal/kernel/guardian.go b/internal/kernel/guardian.go index adc3f19..8860b9b 100644 --- a/internal/kernel/guardian.go +++ b/internal/kernel/guardian.go @@ -41,9 +41,9 @@ type iterationContainer struct { // guardian controls access to the client callback type guardian struct { - client core.Client - chain iterationContainer - master types.GuardianSealer + callback core.Client + chain iterationContainer + master types.GuardianSealer } func newGuardian(callback core.Client, master types.GuardianSealer) *guardian { @@ -57,7 +57,7 @@ func newGuardian(callback core.Client, master types.GuardianSealer) *guardian { }) return &guardian{ - client: callback, + callback: callback, chain: iterationContainer{ invocationChain: *stack, it: collections.ReverseIt(stack.Content(), nil), @@ -104,6 +104,9 @@ func (g *guardian) Unwind(enums.Role) error { // the invocation of the client's callback, depending on the contents // of the chain. func (g *guardian) Invoke(node *core.Node) error { + // TODO: Actually, using an iterator is not the best way forward as it + // adds unnecessary overhead. Each link should have access to the next, + // without depending on an iterator. for link := g.chain.it.Start(); g.chain.it.Valid(); g.chain.it.Next() { if next, err := link.Next(node); !next || err != nil { return err diff --git a/internal/kernel/kernel-defs.go b/internal/kernel/kernel-defs.go index 6f2eb16..f2c9021 100644 --- a/internal/kernel/kernel-defs.go +++ b/internal/kernel/kernel-defs.go @@ -14,14 +14,16 @@ type ( NavigatorImpl interface { // Top Top(ctx context.Context, - static *navigationStatic, - ) (*types.NavigateResult, error) - - // Traverse - Traverse(ctx context.Context, - static *navigationStatic, + ns *navigationStatic, + ) (*types.KernelResult, error) + + // Travel is the internal version of Traverse. It is useful to distinguish + // between the external Traverse and the internal Travel, because they + // have different return types and semantics. + Travel(ctx context.Context, + ns *navigationStatic, current *core.Node, - ) (*core.Node, error) + ) (bool, error) } // NavigatorDriver @@ -92,47 +94,50 @@ type ( // navigationVapour represents short-lived navigation data whose state relates // only to the current Node. (equivalent to inspection in extendio) - navigationVapour struct { + navigationVapour struct { // after content has been read + ns *navigationStatic currentNode *core.Node - parentNode *core.Node directoryContents *DirectoryContents + ents []fs.DirEntry + } + + navigationInfo struct { // pre content read } - assets interface { + inspection interface { // after content has been read static() *navigationStatic current() *core.Node - parent() *core.Node - contents() *DirectoryContents + contents() core.DirectoryContents + entries() []fs.DirEntry + clear() } navigationAssets struct { ns navigationStatic vapour *navigationVapour } - - navigationFault struct { - err error - path string - info fs.FileInfo - ns *navigationStatic - } ) -func (v *navigationVapour) reset(current, parent *core.Node) { - v.currentNode = current - v.parentNode = parent +func (v *navigationVapour) static() *navigationStatic { + return v.ns } -/* -type fileSystemErrorParams struct { - err error - path string - info fs.FileInfo - agent *navigationAgent - frame *navigationFrame +func (v *navigationVapour) current() *core.Node { + return v.currentNode } -type fileSystemErrorHandler interface { - accept(params *fileSystemErrorParams) error +func (v *navigationVapour) contents() core.DirectoryContents { + return v.directoryContents +} + +func (v *navigationVapour) entries() []fs.DirEntry { + return v.ents +} + +func (v *navigationVapour) clear() { + if v.directoryContents != nil { + v.directoryContents.Clear() + } else { + newEmptyDirectoryEntries(v.ns.mediator.o) + } } -*/ diff --git a/internal/kernel/mediator.go b/internal/kernel/mediator.go index ee6db3d..8db0ad5 100644 --- a/internal/kernel/mediator.go +++ b/internal/kernel/mediator.go @@ -12,12 +12,13 @@ import ( // mediator controls traversal events, sends notifications and emits life-cycle events type mediator struct { - root string - impl NavigatorImpl - client Invokable - frame *navigationFrame - pad *scratchPad // gets created just before nav begins - o *pref.Options + root string + impl NavigatorImpl + client Invokable + frame *navigationFrame + pad *scratchPad // gets created just before nav begins + o *pref.Options + resources *types.Resources // there should be a registration phase; but doing so mean that // these entities should already exist, which is counter productive. // possibly use dependency inject where entities declare their diff --git a/internal/kernel/navigator-agent.go b/internal/kernel/navigator-agent.go index a846210..ecbb39a 100644 --- a/internal/kernel/navigator-agent.go +++ b/internal/kernel/navigator-agent.go @@ -2,14 +2,22 @@ package kernel import ( "context" + "errors" + "io/fs" + "path/filepath" "github.com/snivilised/traverse/core" + "github.com/snivilised/traverse/internal/lo" "github.com/snivilised/traverse/internal/types" + "github.com/snivilised/traverse/pref" ) // navigatorAgent does work on behalf of the navigator. It is distinct // from navigatorBase and should only be used when the limited polymorphism -// on base is inadequate. +// on base is inadequate. The agent functions performs generic tasks that +// apply to all navigators. agent is really an abstract concept that isn't +// represented by state, just functions that take state, +// typically navigationStatic. type navigatorAgent struct { } @@ -18,23 +26,101 @@ func newAgent() *navigatorAgent { } func top(ctx context.Context, - static *navigationStatic, -) (*types.NavigateResult, error) { - info, err := static.mediator.o.Hooks.QueryStatus.Invoke()(static.root) - - if err != nil { - return &types.NavigateResult{ - Err: err, - }, err - } - - node := core.Root(static.root, info) + ns *navigationStatic, +) (*types.KernelResult, error) { + info, ie := ns.mediator.o.Hooks.QueryStatus.Invoke()(ns.root) + err := lo.TernaryF(ie != nil, + func() error { + return ns.mediator.o.Defects.Fault.Accept(&pref.NavigationFault{ + Err: ie, + Path: ns.root, + Info: info, + }) + }, + func() error { + _, te := ns.mediator.impl.Travel(ctx, ns, + core.Root(ns.root, info), + ) - _, _ = static.mediator.impl.Traverse(ctx, static, node) + return te + }, + ) - return &types.NavigateResult{}, nil + return &types.KernelResult{ + Err: err, + }, nil } -func traverse(_ navigationStatic) { +const ( + continueTraversal = true + skipTraversal = false +) + +// travel is the general recursive navigation function which returns a bool +// indicating whether we continue travelling or not in response to an +// error. +// true: success path; continue/progress +// false: skip (all, dir) +// +// When an error occurs for this node, we return false (skipTraversal) indicating +// a skip. A skip can mean skip the entire navigation process (fs.SkipAll), +// or just skip all remaining sibling nodes in this directory (fs.SkipDir). +func travel(ctx context.Context, + ns *navigationStatic, + vapour inspection, +) (bool, error) { + var ( + parent = vapour.current() + ) + + for _, entry := range vapour.entries() { + path := filepath.Join(parent.Path, entry.Name()) + info, e := entry.Info() + + // TODO: check sampling; should happen transparently, by plugin + + current := core.New( + path, + entry, + info, + parent, + e, + ) + + // TODO: ok for Travel to by-pass mediator? + // + if progress, err := ns.mediator.impl.Travel( + ctx, ns, current, + ); !progress { + if err != nil { + if errors.Is(err, fs.SkipDir) { + // The returning of skipTraversal by the child, denotes + // a skip. So when a child node returns a SkipDir error and + // skipTraversal, what we're saying is that we want to skip + // processing all successive siblings but continue traversal. + // The !progress indicates we're skipping the remaining + // processing of all of the parent item's remaining children. + // (see the ✨ below ...) + // + return skipTraversal, err + } + + return continueTraversal, err + } + } else if err != nil { + // ✨ ... we skip processing all the remaining children for + // this node, but still continue the overall traversal. + // + switch { + case errors.Is(err, fs.SkipDir): + continue + case errors.Is(err, fs.SkipAll): + break + default: + return continueTraversal, err + } + } + } + return continueTraversal, nil } diff --git a/internal/kernel/navigator-base.go b/internal/kernel/navigator-base.go index c513304..613c738 100644 --- a/internal/kernel/navigator-base.go +++ b/internal/kernel/navigator-base.go @@ -11,22 +11,50 @@ import ( type navigatorBase struct { o *pref.Options using *pref.Using + res *types.Resources } -func (n *navigatorBase) Top(ctx context.Context, - static *navigationStatic, -) (*types.NavigateResult, error) { - _, _ = ctx, static +/* +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 +} - return &types.NavigateResult{}, nil +func (n *navigator) ascend(navi *NavigationInfo, permit bool) { + if permit { + navi.frame.periscope.ascend() + navi.frame.notifiers.ascend.invoke(navi.Item) + } } -func (n *navigatorBase) Traverse(ctx context.Context, - static *navigationStatic, - current *core.Node, -) (*core.Node, error) { - _, _ = ctx, static - _ = current +*/ + +func (n *navigatorBase) descend(navi *navigationInfo) bool { + _ = navi + + return true +} + +func (n *navigatorBase) ascend(navi *navigationInfo, permit bool) { + _, _ = navi, permit +} + +func (n *navigatorBase) Top(ctx context.Context, + ns *navigationStatic, +) (*types.KernelResult, error) { + _, _ = ctx, ns + + return &types.KernelResult{}, nil +} - return nil, nil +func (n *navigatorBase) Travel(context.Context, + *navigationStatic, + *core.Node, +) (bool, error) { + return continueTraversal, nil } diff --git a/internal/kernel/navigator-factory.go b/internal/kernel/navigator-factory.go index 951a940..7ce2446 100644 --- a/internal/kernel/navigator-factory.go +++ b/internal/kernel/navigator-factory.go @@ -14,13 +14,15 @@ func (f *facilities) Inject(pref.ActiveState) {} func New(using *pref.Using, o *pref.Options, sealer types.GuardianSealer, + res *types.Resources, ) *Artefacts { - impl := newImpl(using, o) - controller := newController(using, o, impl, sealer) + impl := newImpl(using, o, res) + controller := newController(using, o, impl, sealer, res) return &Artefacts{ Navigator: controller, Mediator: controller.mediator, + Resources: res, } } @@ -28,6 +30,7 @@ func newController(using *pref.Using, o *pref.Options, impl NavigatorImpl, sealer types.GuardianSealer, + res *types.Resources, ) *NavigationController { return &NavigationController{ mediator: &mediator{ @@ -37,18 +40,21 @@ func newController(using *pref.Using, frame: &navigationFrame{ periscope: level.New(), }, - pad: newScratch(o), - o: o, + pad: newScratch(o), + o: o, + resources: res, }, } } func newImpl(using *pref.Using, o *pref.Options, + res *types.Resources, ) (navigator NavigatorImpl) { base := navigatorBase{ using: using, o: o, + res: res, } switch using.Subscription { diff --git a/internal/kernel/navigator-files.go b/internal/kernel/navigator-files.go index 19d8553..5b02c1b 100644 --- a/internal/kernel/navigator-files.go +++ b/internal/kernel/navigator-files.go @@ -11,7 +11,7 @@ type navigatorFiles struct { } func (n *navigatorFiles) Top(ctx context.Context, - static *navigationStatic, -) (*types.NavigateResult, error) { - return n.navigatorBase.Top(ctx, static) + ns *navigationStatic, +) (*types.KernelResult, error) { + return n.navigatorBase.Top(ctx, ns) } diff --git a/internal/kernel/navigator-folders.go b/internal/kernel/navigator-folders.go index 873fc5d..335d76e 100644 --- a/internal/kernel/navigator-folders.go +++ b/internal/kernel/navigator-folders.go @@ -11,7 +11,7 @@ type navigatorFolders struct { } func (n *navigatorFolders) Top(ctx context.Context, - static *navigationStatic, -) (*types.NavigateResult, error) { - return n.navigatorBase.Top(ctx, static) + ns *navigationStatic, +) (*types.KernelResult, error) { + return n.navigatorBase.Top(ctx, ns) } diff --git a/internal/kernel/navigator-universal.go b/internal/kernel/navigator-universal.go index ccc4563..4543c70 100644 --- a/internal/kernel/navigator-universal.go +++ b/internal/kernel/navigator-universal.go @@ -3,6 +3,8 @@ package kernel import ( "context" + "github.com/snivilised/traverse/core" + "github.com/snivilised/traverse/enums" "github.com/snivilised/traverse/internal/types" ) @@ -11,7 +13,55 @@ type navigatorUniversal struct { } func (n *navigatorUniversal) Top(ctx context.Context, - static *navigationStatic, -) (*types.NavigateResult, error) { - return top(ctx, static) + ns *navigationStatic, +) (*types.KernelResult, error) { + return top(ctx, ns) +} + +func (n *navigatorUniversal) Travel(ctx context.Context, + ns *navigationStatic, + current *core.Node, +) (bool, error) { + if err := ns.mediator.Invoke(current); err != nil { + return continueTraversal, err + } + + vapour, err := n.inspect(ns, current) + + if skip, e := ns.mediator.o.Defects.Skip.Ask( + current, vapour.contents(), err, + ); skip == enums.SkipAllTraversal { + return continueTraversal, e + } else if skip == enums.SkipDirTraversal { + return skipTraversal, e + } + + return travel(ctx, ns, vapour) +} + +func (n *navigatorUniversal) inspect(ns *navigationStatic, current *core.Node) (inspection, error) { + var ( + vapour = &navigationVapour{ + ns: ns, + currentNode: current, + } + err error + ) + + if current.IsFolder() { + vapour.directoryContents, err = read(ns.mediator.resources.FS.N, + ns.mediator.o, + current.Path, + ) + + vapour.directoryContents.Sort(enums.EntryTypeFolder, enums.EntryTypeFile) + + vapour.ents = vapour.directoryContents.All() + } else { + vapour.clear() + } + + extend(ns, vapour) + + return vapour, err } diff --git a/internal/kernel/navigator-universal_test.go b/internal/kernel/navigator-universal_test.go index 9ca4e2e..8a422ff 100644 --- a/internal/kernel/navigator-universal_test.go +++ b/internal/kernel/navigator-universal_test.go @@ -2,6 +2,7 @@ package kernel_test import ( "context" + "fmt" "io/fs" "path/filepath" "testing/fstest" @@ -17,8 +18,8 @@ import ( var _ = Describe("NavigatorUniversal", Ordered, func() { var ( - memFS fstest.MapFS - root string + vfs fstest.MapFS + root string ) BeforeAll(func() { @@ -26,7 +27,7 @@ var _ = Describe("NavigatorUniversal", Ordered, func() { verbose = true ) var portion = filepath.Join("MUSICO", "bass") - memFS, root = helpers.Musico(portion, verbose) + vfs, root = helpers.Musico(portion, verbose) Expect(root).NotTo(BeEmpty()) }) @@ -46,19 +47,21 @@ var _ = Describe("NavigatorUniversal", Ordered, func() { &tv.Using{ Root: root, Subscription: tv.SubscribeUniversal, - Handler: func(_ *tv.Node) error { + Handler: func(node *tv.Node) error { + fmt.Printf("🌀 node: '%v'\n", node.Path) + return nil }, GetFS: func() fs.FS { - return memFS + return vfs }, }, tv.WithHookQueryStatus(func(path string) (fs.FileInfo, error) { - return memFS.Stat(helpers.TrimRoot(path)) + return vfs.Stat(helpers.TrimRoot(path)) }), tv.WithHookReadDirectory(func(_ fs.FS, dirname string) ([]fs.DirEntry, error) { - return memFS.ReadDir(helpers.TrimRoot(dirname)) + return vfs.ReadDir(helpers.TrimRoot(dirname)) }), ), ).Navigate(ctx) diff --git a/internal/refine/filter-plugin.go b/internal/refine/filter-plugin.go index 30e2e86..846c402 100644 --- a/internal/refine/filter-plugin.go +++ b/internal/refine/filter-plugin.go @@ -9,9 +9,7 @@ import ( ) func IfActive(o *pref.Options, mediator types.Mediator) types.Plugin { - active := o.Core.Filter.Node != nil - - if active { + if o.Core.Filter.Node != nil { return &Plugin{ BasePlugin: kernel.BasePlugin{ Mediator: mediator, diff --git a/internal/resume/controller.go b/internal/resume/controller.go index 8dec460..984dbdd 100644 --- a/internal/resume/controller.go +++ b/internal/resume/controller.go @@ -73,7 +73,7 @@ func newStrategy(was *pref.Was, nav core.Navigator) (strategy resumeStrategy, er } func (c *Controller) Navigate(_ context.Context) (core.TraverseResult, error) { - return &types.NavigateResult{ + return &types.KernelResult{ Err: errors.Wrap(core.ErrNotImpl, "resume.Controller.Navigate"), }, nil } diff --git a/internal/resume/resume-defs.go b/internal/resume/resume-defs.go index f301010..b588b25 100644 --- a/internal/resume/resume-defs.go +++ b/internal/resume/resume-defs.go @@ -19,7 +19,7 @@ type resumeStrategy interface { init() attach() detach() - resume() (*types.NavigateResult, error) + resume() (*types.KernelResult, error) finish() error } diff --git a/internal/resume/strategy-fastward.go b/internal/resume/strategy-fastward.go index a1ced1f..0b663dd 100644 --- a/internal/resume/strategy-fastward.go +++ b/internal/resume/strategy-fastward.go @@ -38,8 +38,8 @@ func (s *fastwardStrategy) detach() { } -func (s *fastwardStrategy) resume() (*types.NavigateResult, error) { - return &types.NavigateResult{}, nil +func (s *fastwardStrategy) resume() (*types.KernelResult, error) { + return &types.KernelResult{}, nil } func (s *fastwardStrategy) finish() error { diff --git a/internal/resume/strategy-spawn.go b/internal/resume/strategy-spawn.go index 9b01348..5f988a8 100644 --- a/internal/resume/strategy-spawn.go +++ b/internal/resume/strategy-spawn.go @@ -20,8 +20,8 @@ func (s *spawnStrategy) detach() { } -func (s *spawnStrategy) resume() (*types.NavigateResult, error) { - return &types.NavigateResult{}, nil +func (s *spawnStrategy) resume() (*types.KernelResult, error) { + return &types.KernelResult{}, nil } func (s *spawnStrategy) finish() error { diff --git a/internal/sampling/sampling-plugin.go b/internal/sampling/sampling-plugin.go index 206ab6b..f43f2a7 100644 --- a/internal/sampling/sampling-plugin.go +++ b/internal/sampling/sampling-plugin.go @@ -9,9 +9,7 @@ import ( ) func IfActive(o *pref.Options, mediator types.Mediator) types.Plugin { - active := (o.Core.Sampling.NoOf.Files > 0) || (o.Core.Sampling.NoOf.Folders > 0) - - if active { + if (o.Core.Sampling.NoOf.Files > 0) || (o.Core.Sampling.NoOf.Folders > 0) { return &Plugin{ BasePlugin: kernel.BasePlugin{ Mediator: mediator, diff --git a/internal/types/definitions.go b/internal/types/definitions.go index d4d3b55..9194bf8 100644 --- a/internal/types/definitions.go +++ b/internal/types/definitions.go @@ -2,13 +2,14 @@ package types import ( "context" + "io/fs" "github.com/snivilised/traverse/core" "github.com/snivilised/traverse/enums" "github.com/snivilised/traverse/pref" ) -// package types internal types +// package types defines internal types type ( // Link represents a single decorator in the chain @@ -22,11 +23,11 @@ type ( Role() enums.Role } - // GuardianSealer protects again invalid decorations. There can only + // GuardianSealer protects against invalid decorations. There can only // be 1 sealer (the master) and currently that only comes into play // for fastward resume. An ordinary filter is decorate-able, so it // can't be the sealer. It is not mandatory for a master to be registered. - // When no master is registered, minnow will be used. + // When no master is registered, Benign will be used. GuardianSealer interface { Seal(link Link) error IsSealed(top Link) bool @@ -43,32 +44,44 @@ type ( Navigate(ctx context.Context) (core.TraverseResult, error) Spawn(ctx context.Context, root string) (core.TraverseResult, error) } -) -type Plugin interface { - Name() string - Register() error - Init() error -} + FileSystems struct { + N fs.FS + R fs.FS + } + // Resources are dependencies required for navigation + Resources struct { + FS FileSystems + } -type Restoration interface { - Inject(state pref.ActiveState) -} + // Plugin used to define interaction with supplementary features + Plugin interface { + Name() string + Register() error + Init() error + } -// Facilities is the interface provided to plugins to enable them -// to initialise successfully. -type Facilities interface { - Restoration -} + // Restoration; tbd... + Restoration interface { + Inject(state pref.ActiveState) + } -type NavigateResult struct { + // Facilities is the interface provided to plugins to enable them + // to initialise successfully. + Facilities interface { + Restoration + } + + // TraverseController + TraverseController interface { + core.Navigator + } +) + +type KernelResult struct { Err error } -func (r NavigateResult) Error() error { +func (r KernelResult) Error() error { return r.Err } - -type TraverseController interface { - core.Navigator -} diff --git a/pref/defaults.go b/pref/defaults.go index d056b41..b0cc8b1 100644 --- a/pref/defaults.go +++ b/pref/defaults.go @@ -28,22 +28,18 @@ func DefaultReadEntriesHook(sys fs.FS, dirname string) ([]fs.DirEntry, error) { // CaseSensitiveSortHook hook function for case sensitive directory traversal. A // directory of "a" will be visited after a sibling directory "B". -func CaseSensitiveSortHook(entries []fs.DirEntry, _ ...any) error { +func CaseSensitiveSortHook(entries []fs.DirEntry, _ ...any) { sort.Slice(entries, func(i, j int) bool { return entries[i].Name() < entries[j].Name() }) - - return nil } // CaseInSensitiveSortHook hook function for case insensitive directory traversal. A // directory of "a" will be visited before a sibling directory "B". -func CaseInSensitiveSortHook(entries []fs.DirEntry, _ ...any) error { +func CaseInSensitiveSortHook(entries []fs.DirEntry, _ ...any) { sort.Slice(entries, func(i, j int) bool { return strings.ToLower(entries[i].Name()) < strings.ToLower(entries[j].Name()) }) - - return nil } // tail extracts the end part of a string, starting from the offset @@ -79,3 +75,15 @@ func RootParentSubPathHook(info *core.SubPathInfo) string { return difference(info.Root, info.Node.Extension.Parent) } + +func DefaultFaultHandler(*NavigationFault) error { + return nil +} + +func DefaultPanicHandler() { + // may this should invoke save +} + +func DefaultSkipHandler(*core.Node, core.DirectoryContents, error) (enums.SkipTraversal, error) { + return enums.SkipNoneTraversal, nil +} diff --git a/pref/defects.go b/pref/defects.go new file mode 100644 index 0000000..e505034 --- /dev/null +++ b/pref/defects.go @@ -0,0 +1,82 @@ +package pref + +import ( + "io/fs" + + "github.com/snivilised/traverse/core" + "github.com/snivilised/traverse/enums" +) + +type ( + NavigationFault struct { // the contents of this is probably not right + Err error + Path string + Info fs.FileInfo + } + + PanicHandler interface { + Rescue() + } + + Rescuer func() + + FaultHandler interface { + Accept(fault *NavigationFault) error + } + + Accepter func(fault *NavigationFault) error + + SkipHandler interface { + Ask(current *core.Node, + contents core.DirectoryContents, + err error, + ) (enums.SkipTraversal, error) + } + + Asker func(current *core.Node, + contents core.DirectoryContents, + err error, + ) (enums.SkipTraversal, error) + + DefectOptions struct { + Fault FaultHandler + Panic PanicHandler + Skip SkipHandler + } +) + +func (fn Accepter) Accept(fault *NavigationFault) error { + return fn(fault) +} + +func (fn Rescuer) Rescue() { + fn() +} + +func (fn Asker) Ask(current *core.Node, contents core.DirectoryContents, err error) (enums.SkipTraversal, error) { + return fn(current, contents, err) +} + +func WithFaultHandler(handler FaultHandler) Option { + return func(o *Options) error { + o.Defects.Fault = handler + + return nil + } +} + +func WithPanicHandler(handler PanicHandler) Option { + return func(o *Options) error { + o.Defects.Panic = handler + + return nil + } +} + +func WithSkipHandler(handler SkipHandler) Option { + return func(o *Options) error { + o.Defects.Skip = handler + + return nil + } +} diff --git a/pref/options.go b/pref/options.go index 24e67ff..3bbdf5e 100644 --- a/pref/options.go +++ b/pref/options.go @@ -41,6 +41,10 @@ type ( // Monitor MonitorOptions + // Defects contains error handling options + // + Defects DefectOptions + Binder *Binder } @@ -139,6 +143,12 @@ func DefaultOptions() *Options { Monitor: MonitorOptions{ Log: nopLogger, }, + + Defects: DefectOptions{ + Fault: Accepter(DefaultFaultHandler), + Panic: Rescuer(DefaultPanicHandler), + Skip: Asker(DefaultSkipHandler), + }, } return o diff --git a/pref/using.go b/pref/using.go index fea5833..0ede1c3 100644 --- a/pref/using.go +++ b/pref/using.go @@ -94,7 +94,7 @@ func validate(using *Using) error { return nil } -type FsBuilder interface { +type FileSystemBuilder interface { Build() fs.FS } diff --git a/synchronise.go b/synchronise.go index 190fb60..57153c1 100644 --- a/synchronise.go +++ b/synchronise.go @@ -16,12 +16,17 @@ type synchroniser interface { } type trunk struct { - nav core.Navigator - o *pref.Options - extent extent - err error - u *pref.Using - w *pref.Was + nav core.Navigator + o *pref.Options + ext extent + err error + u *pref.Using + // TODO: w => !!! code-smell: argh, this does not look right (only required for resume) + w *pref.Was +} + +func (t trunk) extent() extent { + return t.ext } type concurrent struct { @@ -36,7 +41,7 @@ func (c *concurrent) Navigate(ctx context.Context) (core.TraverseResult, error) defer c.close() if c.err != nil { - return types.NavigateResult{ + return types.KernelResult{ Err: c.err, }, c.err } @@ -53,7 +58,7 @@ func (c *concurrent) Navigate(ctx context.Context) (core.TraverseResult, error) // input := &TraverseInput{ Node: node, - Handler: c.extent.using().Handler, + Handler: c.ext.using().Handler, } c.inputCh <- input // support for timeout (TimeoutOnSendInput) ??? @@ -77,7 +82,7 @@ func (c *concurrent) Navigate(ctx context.Context) (core.TraverseResult, error) if c.err != nil { err := errors.Wrapf(c.err, i18n.ErrWorkerPoolCreationFailed.Error()) - return types.NavigateResult{ + return types.KernelResult{ Err: err, }, err } @@ -102,7 +107,7 @@ type sequential struct { func (s *sequential) Navigate(ctx context.Context) (core.TraverseResult, error) { if s.err != nil { - return types.NavigateResult{ + return types.KernelResult{ Err: s.err, }, s.err } diff --git a/tapable/tapable_test.go b/tapable/tapable_test.go index dad4a8d..df705db 100644 --- a/tapable/tapable_test.go +++ b/tapable/tapable_test.go @@ -91,12 +91,11 @@ var _ = Describe("Tapable", Ordered, func() { When("Sort hooked", func() { It("🧪 should: invoke hook", func() { - o.Hooks.Sort.Tap(func(_ []fs.DirEntry, _ ...any) error { + o.Hooks.Sort.Tap(func(_ []fs.DirEntry, _ ...any) { invoked = true - return nil }) - _ = o.Hooks.Sort.Default()([]fs.DirEntry{}) - _ = o.Hooks.Sort.Invoke()([]fs.DirEntry{}) + o.Hooks.Sort.Default()([]fs.DirEntry{}) + o.Hooks.Sort.Invoke()([]fs.DirEntry{}) Expect(invoked).To(BeTrue(), "Sort hook not invoked") }) diff --git a/traverse-api.go b/traverse-api.go index 007191d..56235ee 100644 --- a/traverse-api.go +++ b/traverse-api.go @@ -73,6 +73,7 @@ type Was = pref.Was var ( WithCPU = pref.WithCPU WithDepth = pref.WithDepth + WithFaultHandler = pref.WithFaultHandler WithFilter = pref.WithFilter WithHibernationWake = pref.WithHibernationWake WithHibernationSleep = pref.WithHibernationSleep @@ -89,6 +90,7 @@ var ( WithOnEnd = pref.WithOnEnd WithOnStart = pref.WithOnStart WithOnStop = pref.WithOnStop + WithPanicHandler = pref.WithPanicHandler WithNoRecurse = pref.WithNoRecurse WithNoW = pref.WithNoW WithSampler = pref.WithSampler @@ -97,6 +99,7 @@ var ( WithSamplingNoOf = pref.WithSamplingNoOf WithSamplingOptions = pref.WithSamplingOptions WithSamplingType = pref.WithSamplingType + WithSkipHandler = pref.WithSkipHandler WithSortBehaviour = pref.WithSortBehaviour WithSubPathBehaviour = pref.WithSubPathBehaviour )