-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
strip number punctuation prefix tag func
- Loading branch information
1 parent
469f466
commit 4faf86e
Showing
3 changed files
with
236 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
package debefix | ||
|
||
import ( | ||
"cmp" | ||
"errors" | ||
"fmt" | ||
"io/fs" | ||
"os" | ||
"path" | ||
"slices" | ||
"strings" | ||
) | ||
|
||
type fsFileProvider struct { | ||
fs fs.FS | ||
include func(path string, entry os.DirEntry) bool | ||
tagFunc func(dirs []string) []string | ||
} | ||
|
||
// NewDirectoryFileProvider creates a [FileProvider] that list files from a directory, sorted by name. | ||
// Only files with the ".dbf.yaml" extension are returned. | ||
// Returned file names are relative to the rootDir. | ||
func NewDirectoryFileProvider(rootDir string, options ...FSFileProviderOption) FileProvider { | ||
return NewFSFileProvider(os.DirFS(rootDir), options...) | ||
} | ||
|
||
// NewFSFileProvider creates a [FileProvider] that list files from a [fs.FS], sorted by name. | ||
// Only files with the ".dbf.yaml" extension are returned. | ||
func NewFSFileProvider(fs fs.FS, options ...FSFileProviderOption) FileProvider { | ||
ret := &fsFileProvider{ | ||
fs: fs, | ||
} | ||
for _, opt := range options { | ||
opt.apply(ret) | ||
} | ||
if ret.include == nil { | ||
ret.include = func(string, os.DirEntry) bool { | ||
return true | ||
} | ||
} | ||
if ret.tagFunc == nil { | ||
ret.tagFunc = noDirectoryTagFunc | ||
} | ||
return ret | ||
} | ||
|
||
// WithDirectoryIncludeFunc sets a callback to allow choosing files that will be read. | ||
// Check entry [os.DirEntry.IsDir] to detect files or directories. | ||
func WithDirectoryIncludeFunc(include func(path string, entry os.DirEntry) bool) FSFileProviderOption { | ||
return fnFSFileProviderOption(func(provider *fsFileProvider) { | ||
provider.include = include | ||
}) | ||
} | ||
|
||
// WithDirectoryAsTag creates tags for each directory. Inner directories will be concatenated by a dot (.). | ||
func WithDirectoryAsTag() FSFileProviderOption { | ||
return fnFSFileProviderOption(func(provider *fsFileProvider) { | ||
provider.tagFunc = DefaultDirectoryTagFunc | ||
}) | ||
} | ||
|
||
// WithDirectoryTagFunc allows returning custom tags for each directory entry. | ||
func WithDirectoryTagFunc(tagFunc func(dirs []string) []string) FSFileProviderOption { | ||
return fnFSFileProviderOption(func(provider *fsFileProvider) { | ||
provider.tagFunc = tagFunc | ||
}) | ||
} | ||
|
||
// DefaultDirectoryTagFunc joins directories using a dot (.). | ||
func DefaultDirectoryTagFunc(dirs []string) []string { | ||
return []string{strings.Join(dirs, ".")} | ||
} | ||
|
||
// StripNumberPunctuationPrefixDirectoryTagFunc strips number and punctuation prefixes from each | ||
// dir (like "01-") and joins directories using a dot (.). | ||
func StripNumberPunctuationPrefixDirectoryTagFunc(dirs []string) []string { | ||
stripDirs := sliceMap[string](dirs, func(s string) string { | ||
return stripNumberPunctuationPrefix(s) | ||
}) | ||
return []string{strings.Join(stripDirs, ".")} | ||
} | ||
|
||
// noDirectoryTagFunc don't add tags to directories. | ||
func noDirectoryTagFunc(dirs []string) []string { | ||
return nil | ||
} | ||
|
||
func (d fsFileProvider) Load(f FileProviderCallback) error { | ||
return d.loadFiles(".", nil, f) | ||
} | ||
|
||
func (d fsFileProvider) loadFiles(currentPath string, tags []string, f FileProviderCallback) error { | ||
files, err := d.readDirSorted(currentPath) | ||
if err != nil { | ||
return fmt.Errorf("error reading directory '%s': %w", currentPath, err) | ||
} | ||
|
||
var dirs []string | ||
|
||
for _, file := range files { | ||
if !d.include(currentPath, file) { | ||
continue | ||
} | ||
|
||
fullPath := path.Join(currentPath, file.Name()) | ||
|
||
if file.IsDir() { | ||
dirs = append(dirs, file.Name()) | ||
continue | ||
} | ||
|
||
if strings.HasSuffix(file.Name(), ".dbf.yaml") { | ||
localFile, err := d.fs.Open(fullPath) | ||
if err != nil { | ||
return fmt.Errorf("error opening file '%s': %w", fullPath, err) | ||
} | ||
|
||
err = f(FileInfo{ | ||
Name: fullPath, | ||
File: localFile, | ||
Tags: d.tagFunc(tags), | ||
}) | ||
|
||
fileErr := localFile.Close() | ||
if fileErr != nil { | ||
return errors.Join(fmt.Errorf("error closing file '%s': %w", fullPath, fileErr), err) | ||
} | ||
|
||
if err != nil { | ||
return fmt.Errorf("error processing file '%s': %w", fullPath, err) | ||
} | ||
} | ||
} | ||
|
||
for _, dir := range dirs { | ||
fullPath := path.Join(currentPath, dir) | ||
|
||
// each directory may become a tag | ||
err := d.loadFiles(fullPath, append(slices.Clone(tags), dir), f) | ||
if err != nil { | ||
return err | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (d fsFileProvider) readDirSorted(currentPath string) ([]os.DirEntry, error) { | ||
files, err := fs.ReadDir(d.fs, currentPath) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
slices.SortFunc(files, func(a, b os.DirEntry) int { | ||
return cmp.Compare(a.Name(), b.Name()) | ||
}) | ||
|
||
return files, err | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
package debefix | ||
|
||
import ( | ||
"testing" | ||
|
||
"gotest.tools/v3/assert" | ||
) | ||
|
||
func TestStripNumberPunctuationPrefix(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
str string | ||
expected string | ||
}{ | ||
{ | ||
name: "numbers", | ||
str: "01test", | ||
expected: "test", | ||
}, | ||
{ | ||
name: "punctuation", | ||
str: ":test", | ||
expected: "test", | ||
}, | ||
{ | ||
name: "numbers and punctuation", | ||
str: "01-test", | ||
expected: "test", | ||
}, | ||
{ | ||
name: "numbers and punctuation mix", | ||
str: ":1x1test", | ||
expected: "x1test", | ||
}, | ||
{ | ||
name: "numbers after alpha", | ||
str: "01-test5", | ||
expected: "test5", | ||
}, | ||
{ | ||
name: "japanese chars", | ||
str: "01-JP-日本", | ||
expected: "JP-日本", | ||
}, | ||
} | ||
|
||
for _, test := range tests { | ||
t.Run(test.name, func(t *testing.T) { | ||
ret := stripNumberPunctuationPrefix(test.str) | ||
assert.Equal(t, test.expected, ret) | ||
}) | ||
} | ||
} |