This repository has been archived by the owner on Dec 18, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
internal packages for placeholders, path utility functions, globbing (#…
…90) - rewrote globbing - rewrote path splitting - rewrote pattern resolving
- Loading branch information
1 parent
62c380c
commit e05c793
Showing
16 changed files
with
500 additions
and
349 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
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,82 @@ | ||
package paths | ||
|
||
import ( | ||
"fmt" | ||
"os" | ||
"path/filepath" | ||
"strings" | ||
) | ||
|
||
var dirGlobOperator = string(filepath.Separator) + "**" + string(filepath.Separator) | ||
|
||
// Glob supports * and ** globbing according to https://phraseapp.com/docs/developers/cli/configuration/#globbing | ||
func Glob(pattern string) (matches []string, err error) { | ||
pattern = filepath.Clean(pattern) | ||
|
||
if strings.Count(pattern, dirGlobOperator) > 1 { | ||
return nil, fmt.Errorf("invalid pattern '%s': the ** globbing operator may only be used once in a pattern", pattern) | ||
} | ||
|
||
if strings.Contains(pattern, dirGlobOperator) { | ||
parts := strings.Split(pattern, dirGlobOperator) | ||
basePattern, endPattern := parts[0], parts[1] | ||
|
||
baseCandidates, err := filepath.Glob(basePattern) | ||
if err != nil { | ||
return nil, fmt.Errorf("invalid pattern '%s': %s", pattern, err) | ||
} | ||
|
||
for _, base := range directoriesOnly(baseCandidates) { | ||
err = filepath.Walk(filepath.Clean(base), func(path string, info os.FileInfo, err error) error { | ||
if !info.IsDir() { | ||
return nil | ||
} | ||
|
||
matchesInBase, err := Glob(filepath.Join(path, endPattern)) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
matches = append(matches, matchesInBase...) | ||
return nil | ||
}) | ||
} | ||
|
||
} else { | ||
candidates, err := filepath.Glob(pattern) | ||
if err != nil { | ||
return nil, fmt.Errorf("invalid pattern '%s': %s", pattern, err) | ||
} | ||
matches = filesOnly(candidates) | ||
} | ||
|
||
return matches, nil | ||
} | ||
|
||
func filter(candidates []string, f func(os.FileInfo) bool) []string { | ||
matches := []string{} | ||
for _, candidate := range candidates { | ||
fi, err := os.Stat(candidate) | ||
if err != nil { | ||
continue | ||
} | ||
|
||
if f(fi) { | ||
matches = append(matches, candidate) | ||
} | ||
} | ||
|
||
return matches | ||
} | ||
|
||
func filesOnly(candidates []string) []string { | ||
return filter(candidates, func(fi os.FileInfo) bool { | ||
return !fi.IsDir() | ||
}) | ||
} | ||
|
||
func directoriesOnly(candidates []string) []string { | ||
return filter(candidates, func(fi os.FileInfo) bool { | ||
return fi.IsDir() | ||
}) | ||
} |
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,96 @@ | ||
package paths | ||
|
||
import ( | ||
"io/ioutil" | ||
"os" | ||
"path/filepath" | ||
"sort" | ||
"testing" | ||
) | ||
|
||
func TestGlob(t *testing.T) { | ||
base, err := ioutil.TempDir("", "test-glob_") | ||
defer os.RemoveAll(base) | ||
if err != nil { | ||
t.Error(err) | ||
} | ||
|
||
directories := []string{ | ||
"foo/bar/baz/asd", | ||
"foo/bar/xyz/asd", | ||
"foo/bar/baz/xyz/asd", | ||
} | ||
|
||
files := []string{ | ||
"en.yml", | ||
"en.json", | ||
"de.docx", | ||
"nanana", | ||
} | ||
|
||
for _, dir := range directories { | ||
err := os.MkdirAll(filepath.Join(base, dir), 0755) | ||
defer os.RemoveAll(filepath.Join(base, dir)) | ||
if err != nil { | ||
t.Error(err) | ||
} | ||
|
||
for _, file := range files { | ||
_, err := os.Create(filepath.Join(base, dir, file)) | ||
if err != nil { | ||
t.Error(err) | ||
} | ||
} | ||
} | ||
|
||
tests := map[string][]string{ | ||
"**/*.mp3": {}, | ||
"foo/*/baz/**/asd/*.yml": {"foo/bar/baz/asd/en.yml", "foo/bar/baz/xyz/asd/en.yml"}, | ||
"foo/**/*.yml": {"foo/bar/baz/asd/en.yml", "foo/bar/xyz/asd/en.yml", "foo/bar/baz/xyz/asd/en.yml"}, | ||
"foo/bar/xyz/asd/*": {"foo/bar/xyz/asd/en.yml", "foo/bar/xyz/asd/en.json", "foo/bar/xyz/asd/de.docx", "foo/bar/xyz/asd/nanana"}, | ||
"**/asd/*": { | ||
"foo/bar/baz/asd/en.yml", | ||
"foo/bar/baz/asd/en.json", | ||
"foo/bar/baz/asd/de.docx", | ||
"foo/bar/baz/asd/nanana", | ||
|
||
"foo/bar/xyz/asd/en.yml", | ||
"foo/bar/xyz/asd/en.json", | ||
"foo/bar/xyz/asd/de.docx", | ||
"foo/bar/xyz/asd/nanana", | ||
|
||
"foo/bar/baz/xyz/asd/en.yml", | ||
"foo/bar/baz/xyz/asd/en.json", | ||
"foo/bar/baz/xyz/asd/de.docx", | ||
"foo/bar/baz/xyz/asd/nanana", | ||
}, | ||
} | ||
|
||
for pattern, expected := range tests { | ||
matches, err := Glob(filepath.Join(base, pattern)) | ||
if err != nil { | ||
t.Error(err) | ||
} | ||
|
||
for idx, match := range matches { | ||
matches[idx], _ = filepath.Rel(base, match) | ||
} | ||
|
||
if !areEqual(matches, expected) { | ||
t.Errorf("expected %v, got %v", expected, matches) | ||
} | ||
} | ||
} | ||
|
||
func areEqual(s, t []string) bool { | ||
sort.Strings(s) | ||
sort.Strings(t) | ||
|
||
for idx := range s { | ||
if s[idx] != t[idx] { | ||
return false | ||
} | ||
} | ||
|
||
return true | ||
} |
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,54 @@ | ||
package paths | ||
|
||
import ( | ||
"fmt" | ||
"os" | ||
"path/filepath" | ||
"strings" | ||
|
||
"github.com/phrase/phraseapp-client/internal/shared" | ||
) | ||
|
||
func Validate(file, formatName, formatExtension string) error { | ||
if strings.TrimSpace(file) == "" { | ||
return fmt.Errorf("File patterns may not be empty!\nFor more information see %s", shared.DocsConfigUrl) | ||
} | ||
|
||
fileExtension := strings.Trim(filepath.Ext(file), ".") | ||
|
||
if fileExtension == "" { | ||
return fmt.Errorf("%q has no file extension", file) | ||
} | ||
|
||
if fileExtension == "<locale_code>" { | ||
return nil | ||
} | ||
|
||
if formatExtension != "" && formatExtension != fileExtension { | ||
return fmt.Errorf( | ||
"File extension %q does not equal %q (format: %q) for file %q.\nFor more information see %s", | ||
fileExtension, formatExtension, formatName, file, shared.DocsFormatsUrl(formatName), | ||
) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func Exists(absPath string) error { | ||
if _, err := os.Stat(absPath); os.IsNotExist(err) { | ||
return fmt.Errorf("no such file or directory: %s", absPath) | ||
} | ||
return nil | ||
} | ||
|
||
func IsDir(path string) bool { | ||
stat, err := os.Lstat(path) | ||
if err != nil { | ||
return false | ||
} | ||
return stat.IsDir() | ||
} | ||
|
||
func Segments(s string) []string { | ||
return strings.FieldsFunc(filepath.Clean(s), func(c rune) bool { return c == filepath.Separator }) | ||
} |
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,85 @@ | ||
package placeholders | ||
|
||
import ( | ||
"fmt" | ||
"path/filepath" | ||
"regexp" | ||
"strings" | ||
|
||
"github.com/phrase/phraseapp-client/internal/stringz" | ||
) | ||
|
||
var ( | ||
anyPlaceholderRegexp = regexp.MustCompile("<(locale_name|tag|locale_code)>") | ||
localePlaceholder = regexp.MustCompile("<(locale_name|locale_code)>") | ||
tagPlaceholder = regexp.MustCompile("<(tag)>") | ||
) | ||
|
||
func ContainsAnyPlaceholders(s string) bool { | ||
return anyPlaceholderRegexp.MatchString(s) | ||
} | ||
|
||
func ContainsLocalePlaceholder(s string) bool { | ||
return localePlaceholder.MatchString(s) | ||
} | ||
|
||
func ContainsTagPlaceholder(s string) bool { | ||
return tagPlaceholder.MatchString(s) | ||
} | ||
|
||
func ToGlobbingPattern(s string) string { | ||
return anyPlaceholderRegexp.ReplaceAllString(s, "*") | ||
} | ||
|
||
// Resolve matches s against pattern and maps placeholders in pattern to | ||
// substrings of s. | ||
// Resolve handles '*' wildcards in the pattern, but will return an error | ||
// if the pattern contains '**'. | ||
func Resolve(s, pattern string) (map[string]string, error) { | ||
if strings.Contains(pattern, "**") { | ||
return map[string]string{}, fmt.Errorf("'**' wildcard not allowed in pattern") | ||
} | ||
|
||
placeholders := anyPlaceholderRegexp.FindAllString(pattern, -1) | ||
if len(placeholders) <= 0 { | ||
return map[string]string{}, nil | ||
} | ||
|
||
patternRE := regexp.QuoteMeta(pattern) | ||
patternRE = strings.Replace(patternRE, "\\*", ".*", -1) | ||
|
||
for _, placeholder := range stringz.RemoveDuplicates(placeholders) { | ||
placeholder = regexp.QuoteMeta(placeholder) | ||
placeholderRE := fmt.Sprintf("(?P%s[^%s]+)", placeholder, string(filepath.Separator)) // build named subexpression (capturing group) from placeholder | ||
patternRE = strings.Replace(patternRE, placeholder, placeholderRE, -1) | ||
} | ||
|
||
patternRegex, err := regexp.Compile(patternRE) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
matchNames := patternRegex.SubexpNames() | ||
matches := patternRegex.FindStringSubmatch(s) | ||
|
||
if len(matches) < len(placeholders)+1 || matches[0] != s { | ||
return nil, fmt.Errorf("string %q does not match pattern %q", s, patternRE) | ||
} | ||
|
||
// drop first element, which is the entire string s wich match name "" | ||
matches, matchNames = matches[1:], matchNames[1:] | ||
|
||
values := map[string]string{} | ||
for i, match := range matches { | ||
placeholder := matchNames[i] | ||
if value, ok := values[placeholder]; ok { | ||
if match != value { | ||
return nil, fmt.Errorf("string %q does not match pattern %q: placeholder %q is used twice with different values", s, patternRE, placeholder) | ||
} | ||
} | ||
|
||
values[placeholder] = match | ||
} | ||
|
||
return values, nil | ||
} |
Oops, something went wrong.