Skip to content
This repository has been archived by the owner on Dec 18, 2020. It is now read-only.

Commit

Permalink
internal packages for placeholders, path utility functions, globbing (#…
Browse files Browse the repository at this point in the history
…90)

- rewrote globbing
- rewrote path splitting
- rewrote pattern resolving
  • Loading branch information
alexanderwilling authored Mar 6, 2017
1 parent 62c380c commit e05c793
Show file tree
Hide file tree
Showing 16 changed files with 500 additions and 349 deletions.
12 changes: 7 additions & 5 deletions init.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import (
"regexp"
"strings"

"github.com/phrase/phraseapp-client/internal/paths"
"github.com/phrase/phraseapp-client/internal/print"
"github.com/phrase/phraseapp-client/internal/prompt"
"github.com/phrase/phraseapp-client/internal/shared"
"github.com/phrase/phraseapp-client/internal/spinner"
"github.com/phrase/phraseapp-go/phraseapp"
"gopkg.in/yaml.v2"
Expand Down Expand Up @@ -285,7 +287,7 @@ func (cmd *InitCommand) selectFormat() error {

func (cmd *InitCommand) configureSources() error {
fmt.Println("Enter the path to the language file you want to upload to PhraseApp.")
fmt.Printf("For documentation, see %s#push\n", docsConfigUrl)
fmt.Printf("For documentation, see %s#push\n", shared.DocsConfigUrl)

pushPath := ""
for {
Expand All @@ -294,7 +296,7 @@ func (cmd *InitCommand) configureSources() error {
return err
}

err = ValidPath(pushPath, cmd.FileFormat.ApiName, cmd.FileFormat.Extension)
err = paths.Validate(pushPath, cmd.FileFormat.ApiName, cmd.FileFormat.Extension)
if err != nil {
print.Failure(err.Error())
} else {
Expand All @@ -316,7 +318,7 @@ func (cmd *InitCommand) configureSources() error {

func (cmd *InitCommand) configureTargets() error {
fmt.Println("Enter the path to which to download language files from PhraseApp.")
fmt.Printf("For documentation, see %s#pull\n", docsConfigUrl)
fmt.Printf("For documentation, see %s#pull\n", shared.DocsConfigUrl)

pullPath := ""
for {
Expand All @@ -325,7 +327,7 @@ func (cmd *InitCommand) configureTargets() error {
return err
}

err = ValidPath(pullPath, cmd.FileFormat.ApiName, cmd.FileFormat.Extension)
err = paths.Validate(pullPath, cmd.FileFormat.ApiName, cmd.FileFormat.Extension)
if err != nil {
print.Failure(err.Error())
} else {
Expand Down Expand Up @@ -368,7 +370,7 @@ func (cmd *InitCommand) writeConfig() error {
fmt.Println()
fmt.Println(string(yamlBytes))

print.Success("For advanced configuration options, take a look at the documentation: " + docsConfigUrl)
print.Success("For advanced configuration options, take a look at the documentation: " + shared.DocsConfigUrl)
print.Success("You can now use the push & pull commands in your workflow:")
fmt.Println()
fmt.Println("$ phraseapp push")
Expand Down
82 changes: 82 additions & 0 deletions internal/paths/glob.go
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()
})
}
96 changes: 96 additions & 0 deletions internal/paths/glob_test.go
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
}
54 changes: 54 additions & 0 deletions internal/paths/paths.go
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 })
}
85 changes: 85 additions & 0 deletions internal/placeholders/placeholders.go
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
}
Loading

0 comments on commit e05c793

Please sign in to comment.