Skip to content

Commit

Permalink
strip number punctuation prefix tag func
Browse files Browse the repository at this point in the history
  • Loading branch information
RangelReale committed Oct 18, 2023
1 parent 469f466 commit 4faf86e
Show file tree
Hide file tree
Showing 3 changed files with 236 additions and 0 deletions.
159 changes: 159 additions & 0 deletions fileprovider_fs.go
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
}
24 changes: 24 additions & 0 deletions util.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package debefix

import (
"slices"
"strings"

"github.com/goccy/go-yaml/ast"
)
Expand All @@ -25,3 +26,26 @@ func getStringNode(node ast.Node) (string, error) {
return "", NewParseError("node is not string", node.GetPath(), node.GetToken().Position)
}
}

// sliceMap applies a function to each slice item and return the resulting slice.
func sliceMap[T any, U []T](ts U, f func(T) T) U {
us := make(U, len(ts))
for i := range ts {
us[i] = f(ts[i])
}
return us
}

// stripNumberPunctuationPrefix removes [numberPunctuation] from the string prefix.
func stripNumberPunctuationPrefix(s string) string {
isPrefix := true
return strings.Map(func(r rune) rune {
if !isPrefix || strings.IndexRune(numberPunctuation, r) < 0 {
isPrefix = false
return r
}
return -1
}, s)
}

var numberPunctuation = "0123456789!\"#$%&'()*+,-./:;?@[\\]^_`{|}~"
53 changes: 53 additions & 0 deletions util_test.go
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)
})
}
}

0 comments on commit 4faf86e

Please sign in to comment.