diff --git a/Taskfile.yml b/Taskfile.yml index b327b76..bafff43 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -64,7 +64,7 @@ tasks: cmds: - go test ./collections - tt: + tn: cmds: - go test diff --git a/file-systems.go b/file-systems.go index 5ed0c55..e7e7947 100644 --- a/file-systems.go +++ b/file-systems.go @@ -5,6 +5,7 @@ import ( "io/fs" "os" "path/filepath" + "strings" ) // 🔥 An important note about using standard golang file systems (io.fs/fs.FS) @@ -254,6 +255,48 @@ func (f *makeDirAllFS) MakeDirAll(name string, perm os.FileMode) error { return os.MkdirAll(path, perm) } +// EnsurePathAt ensures that the specified path exists (including any non +// existing intermediate directories). Given a path and a default filename, +// the specified path is created in the following manner: +// - If the path denotes a file (path does not end is a directory separator), then +// the parent folder is created if it doesn't exist on the file-system provided. +// - If the path denotes a directory, then that directory is created. +// +// The returned string represents the file, so if the path specified was a +// directory path, then the defaultFilename provided is joined to the path +// and returned, otherwise the original path is returned un-modified. +// Note: filepath.Join does not preserve a trailing separator, therefore to make sure +// a path is interpreted as a directory and not a file, then the separator has +// to be appended manually onto the end of the path. +func (f *makeDirAllFS) Ensure(as PathAs, +) (at string, err error) { + if !fs.ValidPath(as.Name) { + return "", NewInvalidPathError(as.Name) + } + + var ( + file string + ) + + if f.FileExists(as.Name) { + _, file = filepath.Split(as.Name) + + return file, nil + } + + if f.DirectoryExists(as.Name) { + return as.Default, nil + } + + if as.AsFile { + directory, file := SplitParent(as.Name) + + return file, f.MakeDirAll(directory, as.Perm) + } + + return as.Default, f.MakeDirAll(as.Name, as.Perm) +} + // 🎯 removeFS type removeFS struct { @@ -501,3 +544,7 @@ func compose(root string) *entities { reader: reader, } } + +func join(segments ...string) string { + return strings.Join(segments, "/") +} diff --git a/fs-ensure-at_test.go b/fs-ensure-at_test.go new file mode 100644 index 0000000..a1fe04e --- /dev/null +++ b/fs-ensure-at_test.go @@ -0,0 +1,207 @@ +package nef_test + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "testing/fstest" + + . "github.com/onsi/ginkgo/v2" //nolint:revive // ok + . "github.com/onsi/gomega" //nolint:revive // ok + + nef "github.com/snivilised/nefilim" + lab "github.com/snivilised/nefilim/internal/laboratory" +) + +var _ = Describe("Ensure", Ordered, func() { + var ( + mocks *nef.ResolveMocks + mapFS *makeDirMapFS + root string + fS nef.MakeDirFS + ) + + BeforeAll(func() { + root = Repo("test") + }) + + BeforeEach(func() { + mocks = &nef.ResolveMocks{ + HomeFunc: func() (string, error) { + return filepath.Join(string(filepath.Separator), "home", "prodigy"), nil + }, + AbsFunc: func(_ string) (string, error) { // no-op + return "", errors.New("not required for these tests") + }, + } + + mapFS = &makeDirMapFS{ + mapFS: fstest.MapFS{ + filepath.Join("home", "prodigy"): &fstest.MapFile{ + Mode: os.ModeDir, + }, + }, + } + scratch(root) + + fS = nef.NewMakeDirFS(nef.At{ + Root: root, + }) + }) + + DescribeTable("local-fs", + func(entry fsTE[nef.MakeDirFS]) { + if entry.arrange != nil { + entry.arrange(entry, fS) + } + entry.action(entry, fS) + }, + func(entry fsTE[nef.MakeDirFS]) string { + return fmt.Sprintf("🧪 ===> given: '%v', %v should: '%v'", + entry.given, entry.op, entry.should, + ) + }, + + Entry(nil, fsTE[nef.MakeDirFS]{ + given: "path exists as file", + should: "return filename of path", + op: "Ensure", + require: lab.Static.FS.Scratch, + target: lab.Static.FS.Ensure.Log.File, + arrange: func(entry fsTE[nef.MakeDirFS], _ nef.MakeDirFS) { + directory := lab.Static.FS.Ensure.Default.Directory + Expect(require(root, directory, entry.target)).To(Succeed()) + }, + action: func(entry fsTE[nef.MakeDirFS], fS nef.MakeDirFS) { + result, err := fS.Ensure( + nef.PathAs{ + Name: entry.target, + Default: lab.Static.FS.Ensure.Default.File, + Perm: lab.Perms.Dir, + }, + ) + Expect(err).To(Succeed()) + _, file := filepath.Split(lab.Static.FS.Ensure.Log.File) + Expect(result).To(Equal(file)) + }, + }), + + Entry(nil, fsTE[nef.MakeDirFS]{ + given: "path exists as directory", + should: "return default", + op: "Ensure", + require: lab.Static.FS.Scratch, + target: lab.Static.FS.Ensure.Log.Directory, + arrange: func(entry fsTE[nef.MakeDirFS], _ nef.MakeDirFS) { + Expect(require(root, entry.target)).To(Succeed()) + }, + action: func(entry fsTE[nef.MakeDirFS], fS nef.MakeDirFS) { + _, file := filepath.Split(lab.Static.FS.Ensure.Default.File) + result, err := fS.Ensure( + nef.PathAs{ + Name: entry.target, + Default: file, + Perm: lab.Perms.Dir, + }, + ) + Expect(err).To(Succeed()) + Expect(result).To(Equal(file)) + }, + }), + + Entry(nil, fsTE[nef.MakeDirFS]{ + given: "file does not exist", + should: "create parent directory and return file", + op: "Ensure", + require: lab.Static.FS.Scratch, + target: lab.Static.FS.Ensure.Log.File, + from: lab.Static.FS.Ensure.Home, + arrange: func(entry fsTE[nef.MakeDirFS], _ nef.MakeDirFS) { + parent := Join(entry.require, entry.from) + Expect(require(root, parent)).To(Succeed()) + }, + action: func(entry fsTE[nef.MakeDirFS], fS nef.MakeDirFS) { + _, file := filepath.Split(lab.Static.FS.Ensure.Default.File) + result, err := fS.Ensure( + nef.PathAs{ + Name: entry.target, + Default: file, + Perm: lab.Perms.Dir, + AsFile: true, + }, + ) + Expect(err).To(Succeed()) + ensureAt := lab.Static.FS.Ensure.Default.Directory + Expect(AsDirectory(ensureAt)).To(ExistInFS(fS)) + _, file = filepath.Split(entry.target) + Expect(result).To(Equal(file)) + }, + }), + + Entry(nil, fsTE[nef.MakeDirFS]{ + given: "directory does not exist", + should: "create directory and return default", + op: "Ensure", + require: lab.Static.FS.Scratch, + target: lab.Static.FS.Ensure.Log.Directory, + from: lab.Static.FS.Ensure.Home, + arrange: func(entry fsTE[nef.MakeDirFS], _ nef.MakeDirFS) { + parent := Join(entry.require, entry.from) + Expect(require(root, parent)).To(Succeed()) + }, + action: func(entry fsTE[nef.MakeDirFS], fS nef.MakeDirFS) { + _, file := filepath.Split(lab.Static.FS.Ensure.Default.File) + result, err := fS.Ensure( + nef.PathAs{ + Name: entry.target, + Default: file, + Perm: lab.Perms.Dir, + }, + ) + Expect(err).To(Succeed()) + ensureAt := lab.Static.FS.Ensure.Default.Directory + Expect(AsDirectory(ensureAt)).To(ExistInFS(fS)) + Expect(result).To(Equal(file)) + }, + }), + ) + + DescribeTable("with mapFS", + func(entry *ensureTE) { + home, _ := mocks.HomeFunc() + location := TrimRoot(filepath.Join(home, entry.relative)) + + if entry.directory { + location += string(filepath.Separator) + } + + actual, err := nef.EnsurePathAt(location, lab.Static.FS.Ensure.Default.File, lab.Perms.File, mapFS) + directory, _ := filepath.Split(actual) + directory = filepath.Clean(directory) + expected := TrimRoot(Path(home, entry.expected)) + + Expect(err).Error().To(BeNil()) + Expect(actual).To(Equal(expected)) + Expect(AsDirectory(TrimRoot(directory))).To(ExistInFS(mapFS)) + }, + func(entry *ensureTE) string { + return fmt.Sprintf("🧪 ===> given: '%v', should: '%v'", entry.given, entry.should) + }, + + XEntry(nil, &ensureTE{ + given: "path is file", + should: "create parent directory and return specified file path", + relative: filepath.Join("logs", "test.log"), // home/logs/test.log + expected: "logs/test.log", + }), + + XEntry(nil, &ensureTE{ + given: "path is directory", + should: "create parent directory and return default file path", + relative: "logs/", + directory: true, + expected: "logs/default-test.log", + }), + ) +}) diff --git a/internal/laboratory/static.go b/internal/laboratory/static.go index 83f1c5b..db6befc 100644 --- a/internal/laboratory/static.go +++ b/internal/laboratory/static.go @@ -12,6 +12,12 @@ type ( Destination string } + Ensure struct { + Home string + Default Pair + Log Pair + } + Pair struct { File string Directory string @@ -45,6 +51,7 @@ type ( StaticFs struct { Copy Copy Create Create + Ensure Ensure Existing Pair MakeDir MakeDir Move Move @@ -78,6 +85,17 @@ var ( Create: Create{ Destination: "scratch/pictures-of-you.CREATE.txt", }, + Ensure: Ensure{ + Home: "home/marina", + Default: Pair{ + File: "scratch/home/marina/logs/default-test.log", + Directory: "scratch/home/marina/logs", + }, + Log: Pair{ + File: "scratch/home/marina/logs/test.log", + Directory: "scratch/home/marina/logs", + }, + }, Existing: Pair{ File: "data/fS/paradise-lost.txt", Directory: "data/fS", diff --git a/make-dir-map-fs_test.go b/make-dir-map-fs_test.go index 76edb3e..07db3c8 100644 --- a/make-dir-map-fs_test.go +++ b/make-dir-map-fs_test.go @@ -6,6 +6,8 @@ import ( "path/filepath" "strings" "testing/fstest" + + nef "github.com/snivilised/nefilim" ) type ( @@ -74,3 +76,9 @@ func (f *makeDirMapFS) MakeDirAll(path string, perm os.FileMode) error { return nil } + +func (f *makeDirMapFS) Ensure(as nef.PathAs, +) (at string, err error) { + _ = as + panic("NOT-IMPL: makeDirMapFS.Ensure") +} diff --git a/nefilim-defs.go b/nefilim-defs.go index 2c92ad3..97a42ab 100644 --- a/nefilim-defs.go +++ b/nefilim-defs.go @@ -49,11 +49,31 @@ type ( ReadFileFS } + // PathAs used with Ensure to define how to ensure that a path exists + // at the location specified + PathAs struct { + Name string + Default string + Perm os.FileMode + AsFile bool + } + // MakeDirFS is a file system with a MkDirAll method. MakeDirFS interface { ExistsInFS MakeDir(name string, perm os.FileMode) error MakeDirAll(name string, perm os.FileMode) error + // Ensure makes sure that a path exists (PathAs.Name). If the path exists + // as a file then no directories need to be created and this file name + // (PathAs.Name) is returned. If the path exists as a directory, then again + // no directories are created, but the default (PathAs.Default) is returned. + // + // If the path does not exist, then 1 of 2 things can happen. If PathAs.AsFile + // is set to true, then the parent of the path is created, and file portion + // of the path is returned. When PathAs.AsFile is not set, ie the path + // provided is to be interpreted as a directory, then this directory is + // created and the default is returned. + Ensure(as PathAs) (string, error) } // MoveFS diff --git a/nefilim-suite_test.go b/nefilim-suite_test.go index 66fd1b3..4069518 100644 --- a/nefilim-suite_test.go +++ b/nefilim-suite_test.go @@ -143,6 +143,10 @@ func Repo(relative string) string { return Path(repo, relative) } +func Join(segments ...string) string { + return strings.Join(segments, "/") +} + func Normalise(p string) string { return strings.ReplaceAll(p, "/", string(filepath.Separator)) }