├── testdata ├── .phraseapp.yml ├── empty │ └── .gitkeep ├── phraseapp.yml ├── empty2 │ └── .gitkeep └── config_files │ ├── .phrase.yml │ └── .phraseapp.yml ├── CODEOWNERS ├── .gitignore ├── phraseapp ├── Makefile ├── version.go ├── config_unix.go ├── config_windows.go ├── http_cache_test.go ├── response.go ├── error.go ├── http_cache.go ├── config.go ├── client.go └── config_test.go ├── Makefile ├── go.mod ├── .github └── workflows │ └── main.yml ├── go.sum ├── LICENSE └── README.md /testdata/.phraseapp.yml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testdata/empty/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testdata/phraseapp.yml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @theSoenke 2 | -------------------------------------------------------------------------------- /testdata/empty2/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .idea/ 3 | *.iml 4 | .DS_STORE 5 | -------------------------------------------------------------------------------- /phraseapp/Makefile: -------------------------------------------------------------------------------- 1 | default: build 2 | 3 | build: 4 | go get ./... -------------------------------------------------------------------------------- /phraseapp/version.go: -------------------------------------------------------------------------------- 1 | package phraseapp 2 | 3 | var ClientVersion string = "DEV" 4 | -------------------------------------------------------------------------------- /testdata/config_files/.phrase.yml: -------------------------------------------------------------------------------- 1 | phrase: 2 | access_token: "123" 3 | push: 4 | sources: 5 | - file: "./config/locales/.yml" 6 | 7 | -------------------------------------------------------------------------------- /phraseapp/config_unix.go: -------------------------------------------------------------------------------- 1 | package phraseapp 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | func defaultConfigDir() string { 8 | return os.Getenv("HOME") 9 | } 10 | -------------------------------------------------------------------------------- /testdata/config_files/.phraseapp.yml: -------------------------------------------------------------------------------- 1 | phraseapp: 2 | access_token: "123" 3 | push: 4 | sources: 5 | - file: "./config/locales/.yml" 6 | 7 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | default: build 2 | 3 | all: build test vet 4 | 5 | build: 6 | go get ./... 7 | 8 | test: 9 | go test ./... 10 | 11 | vet: 12 | go vet ./... 13 | -------------------------------------------------------------------------------- /phraseapp/config_windows.go: -------------------------------------------------------------------------------- 1 | // +build linux darwin 2 | 3 | package phraseapp 4 | 5 | import ( 6 | "os" 7 | "path/filepath" 8 | ) 9 | 10 | func defaultConfigDir() string { 11 | return os.Getenv("HomePath") 12 | } 13 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/phrase/phraseapp-go 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/bgentry/speakeasy v0.1.0 7 | github.com/google/btree v1.0.0 // indirect 8 | github.com/peterbourgon/diskv v2.0.1+incompatible 9 | gopkg.in/yaml.v2 v2.2.8 10 | ) 11 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: [push] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-18.04 6 | steps: 7 | - name: Setup env 8 | shell: bash 9 | run: | 10 | echo "::set-env name=GOPATH::${{ github.workspace }}/go" 11 | echo "::add-path::${{ github.workspace }}/go/bin" 12 | - name: Install Go 13 | if: success() 14 | uses: actions/setup-go@v1 15 | with: 16 | go-version: 1.11 17 | - name: Checkout 18 | uses: actions/checkout@v1 19 | with: 20 | fetch-depth: 1 21 | path: phraseapp-go/go/src/github.com/${{ github.repository }} 22 | - name: Test 23 | run: make all 24 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= 2 | github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= 3 | github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= 4 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 5 | github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= 6 | github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= 7 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 9 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 10 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Dynport GmbH 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /phraseapp/http_cache_test.go: -------------------------------------------------------------------------------- 1 | package phraseapp 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | ) 10 | 11 | func TestLocaleDownloadCaching(t *testing.T) { 12 | var cached = false 13 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 14 | url := r.URL.String() 15 | if url == "/v2/projects/1/locales/1/download" { 16 | w.Header().Set("Etag", "123") 17 | if cached { 18 | etag := r.Header.Get("If-None-Match") 19 | if etag != "123" { 20 | t.Errorf("etag should be '123' but is: '%s'", etag) 21 | } 22 | 23 | w.WriteHeader(http.StatusNotModified) 24 | } else { 25 | w.WriteHeader(http.StatusOK) 26 | io.WriteString(w, "hello world") 27 | } 28 | cached = true 29 | } 30 | return 31 | })) 32 | defer server.Close() 33 | 34 | client, _ := NewClient(Credentials{Host: server.URL}, false) 35 | cacheDir, _ := ioutil.TempDir("", "") 36 | client.EnableCaching(CacheConfig{ 37 | CacheDir: cacheDir, 38 | }) 39 | 40 | originalContent, _ := client.LocaleDownload("1", "1", &LocaleDownloadParams{}) 41 | cachedContent, _ := client.LocaleDownload("1", "1", &LocaleDownloadParams{}) 42 | if string(originalContent) != string(cachedContent) { 43 | t.Error("Cached content does not match original content") 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /phraseapp/response.go: -------------------------------------------------------------------------------- 1 | package phraseapp 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | ) 9 | 10 | const docsURL = `https://developers.phrase.com/api/` 11 | 12 | func further() string { 13 | return fmt.Sprintf("\nFor further information see:\n%s", docsURL) 14 | } 15 | 16 | func handleResponseStatus(resp *http.Response, expectedStatus int) error { 17 | switch status := resp.StatusCode; status { 18 | case expectedStatus: 19 | return nil 20 | case http.StatusBadRequest: 21 | e := new(ErrorResponse) 22 | err := json.NewDecoder(resp.Body).Decode(&e) 23 | if err != nil { 24 | return err 25 | } 26 | return e 27 | case http.StatusUnauthorized: 28 | return fmt.Errorf("%d - %s\nThe credentials you provided are invalid.%s", status, http.StatusText(status), further()) 29 | case http.StatusForbidden: 30 | return fmt.Errorf("%d - %s\nYou are not authorized to perform the requested action on the requested resource. Check if your provided access_token has the correct scope.%s", status, http.StatusText(status), further()) 31 | case http.StatusNotFound: 32 | var rsp struct { 33 | Message string `json:"message"` 34 | } 35 | b, _ := ioutil.ReadAll(resp.Body) 36 | decodeErr := json.Unmarshal(b, &rsp) 37 | if decodeErr != nil { 38 | return ErrNotFound{Message: fmt.Sprintf("%d - Resource Not Found\nThe resource you requested or referenced resources you required do either not exist or you do not have the authorization to request this resource.", status)} 39 | } 40 | return ErrNotFound{Message: string(b)} 41 | case http.StatusUnsupportedMediaType, http.StatusUnprocessableEntity: 42 | e := new(ValidationErrorResponse) 43 | err := json.NewDecoder(resp.Body).Decode(&e) 44 | if err != nil { 45 | return err 46 | } 47 | return e 48 | case http.StatusTooManyRequests: 49 | e, err := NewRateLimitError(resp) 50 | if err != nil { 51 | return err 52 | } 53 | return e 54 | default: 55 | return fmt.Errorf("Unexpected HTTP Status Code (%d %s) received; expected %d %s.%s", status, http.StatusText(status), expectedStatus, http.StatusText(expectedStatus), further()) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /phraseapp/error.go: -------------------------------------------------------------------------------- 1 | package phraseapp 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "net/http" 7 | "strconv" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | func IsErrNotFound(err error) bool { 13 | if err == nil { 14 | return false 15 | } 16 | _, ok := err.(ErrNotFound) 17 | return ok 18 | } 19 | 20 | // ErrNotFound represents an error for requests of non existing resources 21 | type ErrNotFound struct { 22 | Message string 23 | } 24 | 25 | func (e ErrNotFound) Error() string { 26 | return e.Message 27 | } 28 | 29 | type ErrorResponse struct { 30 | Message string 31 | } 32 | 33 | func (err *ErrorResponse) Error() string { 34 | return err.Message 35 | } 36 | 37 | // ValidationErrorResponse represents the response for a failed validation of content 38 | type ValidationErrorResponse struct { 39 | ErrorResponse 40 | 41 | Errors []ValidationErrorMessage 42 | } 43 | 44 | func (err *ValidationErrorResponse) Error() string { 45 | msgs := make([]string, len(err.Errors)) 46 | for i := range err.Errors { 47 | msgs[i] = err.Errors[i].String() 48 | } 49 | return fmt.Sprintf("%s\n%s", err.Message, strings.Join(msgs, "\n")) 50 | } 51 | 52 | // ValidationErrorMessage represents an error for a failed validation of content 53 | type ValidationErrorMessage struct { 54 | Resource string 55 | Field string 56 | Message string 57 | } 58 | 59 | func (msg *ValidationErrorMessage) String() string { 60 | return fmt.Sprintf("\t[%s:%s] %s", msg.Resource, msg.Field, msg.Message) 61 | } 62 | 63 | // RateLimitingError is returned when hitting the API rate limit 64 | type RateLimitingError struct { 65 | Limit int 66 | Remaining int 67 | Reset time.Time 68 | TooManyRequests bool 69 | } 70 | 71 | const errorConcurrencyLimit = "Concurrency limit exceeded" 72 | 73 | func NewRateLimitError(resp *http.Response) (*RateLimitingError, error) { 74 | var err error 75 | re := new(RateLimitingError) 76 | b, err := ioutil.ReadAll(resp.Body) 77 | if err == nil && strings.TrimSpace(string(b)) == errorConcurrencyLimit { 78 | re.TooManyRequests = true 79 | } 80 | 81 | limit := resp.Header.Get("X-Rate-Limit-Limit") 82 | re.Limit, err = strconv.Atoi(limit) 83 | if err != nil { 84 | return nil, err 85 | } 86 | 87 | remaining := resp.Header.Get("X-Rate-Limit-Remaining") 88 | re.Remaining, err = strconv.Atoi(remaining) 89 | if err != nil { 90 | return nil, err 91 | } 92 | 93 | reset := resp.Header.Get("X-Rate-Limit-Reset") 94 | sinceEpoch, err := strconv.ParseInt(reset, 10, 64) 95 | if err != nil { 96 | return nil, err 97 | } 98 | re.Reset = time.Unix(sinceEpoch, 0) 99 | 100 | return re, nil 101 | } 102 | 103 | func (rle *RateLimitingError) Error() string { 104 | if rle.TooManyRequests { 105 | return fmt.Sprintf("Rate limit exceeded: too many parallel requests") 106 | } 107 | return fmt.Sprintf("Rate limit exceeded: from %d requests %d are remaining (reset in %d seconds)", rle.Limit, rle.Remaining, int64(rle.Reset.Sub(time.Now()).Seconds())) 108 | } 109 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Deprecated] 2 | This library is deprecated and has been replaced by [phrase/phrase-go](https://github.com/phrase/phrase-go) 3 | 4 | # phraseapp-go 5 | 6 | Go library for Phrase API v2. 7 | 8 | # Start using it 9 | 10 | 1. Download and install \ 11 | `go get github.com/phrase/phraseapp-go/phraseapp` 12 | 13 | 2. Import it in your code \ 14 | `import "github.com/phrase/phraseapp-go/phraseapp"` 15 | 16 | # API examples 17 | ### Init client 18 | ```go 19 | credentials := phraseapp.Credentials{ 20 | Host: "https://api.phrase.com", 21 | Token: "access_token", 22 | } 23 | client := phraseapp.Client{ 24 | Credentials: credentials, 25 | } 26 | ``` 27 | 28 | ### Create project 29 | ```go 30 | projectName := "project_name" 31 | sharesTranslationMemory := true 32 | projectParams := phraseapp.ProjectParams{ 33 | Name: &projectName, 34 | SharesTranslationMemory: &sharesTranslationMemory, 35 | } 36 | project, err := client.ProjectCreate(&projectParams) 37 | ``` 38 | 39 | ### Create locale 40 | ```go 41 | localeCode := "en-GB" 42 | localeDetails := phraseapp.LocaleParams{ 43 | Name: &localeCode, 44 | Code: &localeCode, 45 | } 46 | locale, err := client.LocaleCreate("project_id", &localeDetails) 47 | ``` 48 | 49 | ### Create key 50 | ```go 51 | keyName := "key_name" 52 | tags := "tag1, tag2" 53 | keyParams := phraseapp.TranslationKeyParams{ 54 | Name: &keyName, 55 | Tags: &tags, 56 | } 57 | key, err := client.KeyCreate("project_id", &keyParams) 58 | ``` 59 | 60 | ### Create translation 61 | ```go 62 | localeID := "locale_id" 63 | content := "my_content" 64 | keyID := "key_id" 65 | translationParams := phraseapp.TranslationParams{ 66 | KeyID: &keyID, 67 | LocaleID: &localeID, 68 | Content: &content, 69 | } 70 | translation, err := client.TranslationCreate("project_id", &translationParams) 71 | ``` 72 | 73 | ### Upload translation file 74 | ```go 75 | fileName := "file.json" 76 | fileFormat := "simple_json" 77 | updateTranslations := true 78 | uploadParams := phraseapp.UploadParams{ 79 | File: &fileName, 80 | LocaleID: &localeID, 81 | FileFormat: &fileFormat, 82 | UpdateTranslations: &updateTranslations, 83 | } 84 | upload, err := client.UploadCreate("project_id", &uploadParams) 85 | ``` 86 | 87 | ### Download locale as a file 88 | ```go 89 | fileFormat := "simple_json" 90 | localeDownloadParams := phraseapp.LocaleDownloadParams{ 91 | FileFormat: &fileFormat, 92 | } 93 | var localeData []byte 94 | localeData, err := client.LocaleDownload("project_id", "locale_id", &localeDownloadParams) 95 | ioutil.WriteFile("en.json", localeData, 0644) 96 | ``` 97 | 98 | ### Query translations 99 | ```go 100 | translationsQuery := "tags:tag1,tag2" 101 | translationSearchParams := phraseapp.TranslationsSearchParams{ 102 | Q: &translationsQuery, 103 | } 104 | translations, err := client.TranslationsSearch("project_id", 1, 1000, &translationSearchParams) 105 | ``` 106 | More [query options](https://developers.phrase.com/api/#translations) 107 | 108 | 109 | For a more complete example the wiki contains an example how to [upload files as translations](https://github.com/phrase/phraseapp-go/wiki/Sync-local-files-to-PhraseApp) to Phrase. 110 | 111 | ## Contributing 112 | 113 | This library is auto-generated from templates that run against a API specification file. Therefore we can not accept any pull requests in this repository. Please use the GitHub Issue Tracker to report bugs. 114 | -------------------------------------------------------------------------------- /phraseapp/http_cache.go: -------------------------------------------------------------------------------- 1 | package phraseapp 2 | 3 | import ( 4 | "bytes" 5 | "crypto/md5" 6 | "encoding/gob" 7 | "encoding/hex" 8 | "fmt" 9 | "io/ioutil" 10 | "log" 11 | "net/http" 12 | "os" 13 | "path/filepath" 14 | 15 | "github.com/peterbourgon/diskv" 16 | ) 17 | 18 | type httpCacheClient struct { 19 | cache *diskv.Diskv 20 | debug bool 21 | cacheSizeMax int64 22 | } 23 | 24 | type cacheRecord struct { 25 | URL string 26 | ETag string 27 | Response *httpResponse 28 | Payload []byte 29 | } 30 | 31 | // httpResponse is a serializable copy of a http.Response 32 | type httpResponse struct { 33 | Status string 34 | StatusCode int 35 | Proto string 36 | ProtoMajor int 37 | ProtoMinor int 38 | Header http.Header 39 | ContentLength int64 40 | TransferEncoding []string 41 | Uncompressed bool 42 | Trailer http.Header 43 | } 44 | 45 | // CacheConfig contains the configuration for caching api requests on disk 46 | type CacheConfig struct { 47 | CacheDir string 48 | CacheSizeMax int64 // size in bytes 49 | } 50 | 51 | func newHTTPCacheClient(debug bool, config CacheConfig) (*httpCacheClient, error) { 52 | if config.CacheDir == "" { 53 | cacheDir, err := os.UserCacheDir() 54 | if err != nil { 55 | return nil, err 56 | } 57 | config.CacheDir = cacheDir 58 | } 59 | 60 | if config.CacheSizeMax <= 0 { 61 | config.CacheSizeMax = 1024 * 1024 * 100 // 100MB 62 | } 63 | 64 | cachePath := filepath.Join(config.CacheDir, "phrase") 65 | err := os.MkdirAll(cachePath, 0755) 66 | if err != nil { 67 | return nil, err 68 | } 69 | 70 | cache := &httpCacheClient{ 71 | cache: diskv.New(diskv.Options{ 72 | BasePath: cachePath, 73 | }), 74 | cacheSizeMax: config.CacheSizeMax, 75 | debug: debug, 76 | } 77 | return cache, nil 78 | } 79 | 80 | func (client *httpCacheClient) RoundTrip(req *http.Request) (*http.Response, error) { 81 | if req.Method != "" && req.Method != "GET" { 82 | return http.DefaultTransport.RoundTrip(req) 83 | } 84 | 85 | cacheKey := cacheKey(req) 86 | cachedResponse, err := client.readCache(cacheKey) 87 | if err != nil { 88 | if err.Error() != "no cache entry" { 89 | return nil, err 90 | } 91 | } else { 92 | req.Header.Set("If-None-Match", cachedResponse.ETag) 93 | } 94 | 95 | rsp, err := http.DefaultTransport.RoundTrip(req) 96 | if err != nil { 97 | return nil, err 98 | } 99 | defer rsp.Body.Close() 100 | 101 | if rsp.StatusCode == http.StatusNotModified { 102 | if client.debug { 103 | log.Println("found cache and returning cached body") 104 | } 105 | cachedResponse.setCachedResponse(rsp) 106 | return rsp, nil 107 | } 108 | 109 | err = handleResponseStatus(rsp, 200) 110 | if err != nil { 111 | return rsp, err 112 | } 113 | 114 | cacheSize, err := dirSize(client.cache.BasePath) 115 | if err != nil { 116 | return nil, err 117 | } 118 | if cacheSize > client.cacheSizeMax { 119 | client.cache.EraseAll() 120 | } 121 | 122 | err = client.writeCache(cacheKey, req.URL.String(), rsp) 123 | return rsp, err 124 | } 125 | 126 | func cacheKey(req *http.Request) string { 127 | url := req.URL.String() 128 | requestParams := requestParams(req) 129 | return md5sum(url + requestParams) 130 | } 131 | 132 | func requestParams(req *http.Request) string { 133 | if req.Body != nil { 134 | body, err := req.GetBody() 135 | if err != nil { 136 | return "" 137 | } 138 | requestBody, err := ioutil.ReadAll(body) 139 | if err != nil { 140 | return "" 141 | } 142 | 143 | return string(requestBody) 144 | } 145 | 146 | return "" 147 | } 148 | 149 | func (client *httpCacheClient) readCache(cacheKey string) (*cacheRecord, error) { 150 | cache, err := client.cache.Read(cacheKey) 151 | if err != nil { 152 | if client.debug { 153 | log.Println("doing request without etag") 154 | } 155 | return nil, fmt.Errorf("no cache entry") 156 | } 157 | 158 | var cachedResponse *cacheRecord 159 | var buf bytes.Buffer 160 | buf.Write(cache) 161 | decoder := gob.NewDecoder(&buf) 162 | err = decoder.Decode(&cachedResponse) 163 | if err != nil { 164 | return nil, err 165 | } 166 | if client.debug { 167 | log.Printf("found etag %s for request\n", cachedResponse.ETag) 168 | } 169 | 170 | return cachedResponse, nil 171 | } 172 | 173 | func (client *httpCacheClient) writeCache(cacheKey string, url string, rsp *http.Response) error { 174 | body, err := ioutil.ReadAll(rsp.Body) 175 | if err != nil { 176 | return err 177 | } 178 | 179 | rsp.Body = ioutil.NopCloser(bytes.NewReader(body)) 180 | etag := rsp.Header.Get("Etag") 181 | var buf bytes.Buffer 182 | encoder := gob.NewEncoder(&buf) 183 | encoder.Encode(cacheRecord{ 184 | URL: url, 185 | ETag: etag, 186 | Payload: body, 187 | Response: &httpResponse{ 188 | Status: rsp.Status, 189 | StatusCode: rsp.StatusCode, 190 | Proto: rsp.Proto, 191 | ProtoMajor: rsp.ProtoMajor, 192 | ProtoMinor: rsp.ProtoMinor, 193 | Header: rsp.Header, 194 | ContentLength: rsp.ContentLength, 195 | TransferEncoding: rsp.TransferEncoding, 196 | Trailer: rsp.Header, 197 | }}) 198 | err = client.cache.Write(cacheKey, buf.Bytes()) 199 | return err 200 | } 201 | 202 | func (record *cacheRecord) setCachedResponse(rsp *http.Response) { 203 | rsp.Status = record.Response.Status 204 | rsp.StatusCode = record.Response.StatusCode 205 | rsp.Proto = record.Response.Proto 206 | rsp.ProtoMajor = record.Response.ProtoMajor 207 | rsp.ProtoMinor = record.Response.ProtoMinor 208 | rsp.Header = record.Response.Header 209 | rsp.ContentLength = record.Response.ContentLength 210 | rsp.TransferEncoding = record.Response.TransferEncoding 211 | rsp.Trailer = record.Response.Header 212 | rsp.Body = ioutil.NopCloser(bytes.NewReader(record.Payload)) 213 | } 214 | 215 | func dirSize(path string) (int64, error) { 216 | var size int64 217 | err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error { 218 | if !info.IsDir() { 219 | size += info.Size() 220 | } 221 | return err 222 | }) 223 | return size, err 224 | } 225 | 226 | func md5sum(text string) string { 227 | hasher := md5.New() 228 | hasher.Write([]byte(text)) 229 | return hex.EncodeToString(hasher.Sum(nil)) 230 | } 231 | -------------------------------------------------------------------------------- /phraseapp/config.go: -------------------------------------------------------------------------------- 1 | package phraseapp 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | 10 | "gopkg.in/yaml.v2" 11 | ) 12 | 13 | // Config contains all information from a .phraseapp.yml config file 14 | type Config struct { 15 | Credentials 16 | Debug bool `cli:"opt --verbose -v desc='Verbose output'"` 17 | 18 | Page *int 19 | PerPage *int 20 | 21 | DefaultProjectID string 22 | DefaultFileFormat string 23 | 24 | Defaults map[string]map[string]interface{} 25 | 26 | Targets []byte 27 | Sources []byte 28 | } 29 | 30 | var configNames = []string{".phrase.yml", ".phraseapp.yml"} 31 | 32 | // ReadConfig reads a .phrase.yml config file 33 | func ReadConfig() (*Config, error) { 34 | rawCfg := map[string]*Config{} 35 | content, err := configContent() 36 | switch { 37 | case err != nil: 38 | return nil, err 39 | case content == nil: 40 | return &Config{}, nil 41 | default: 42 | err := yaml.Unmarshal(content, rawCfg) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | if cfg, found := rawCfg["phrase"]; found { 48 | return cfg, nil 49 | } 50 | if cfg, found := rawCfg["phraseapp"]; found { 51 | return cfg, nil 52 | } 53 | 54 | return nil, errors.New("'phrase' key is missing in config") 55 | } 56 | } 57 | 58 | func configContent() ([]byte, error) { 59 | path, err := configPath() 60 | switch { 61 | case err != nil: 62 | return nil, err 63 | case path == "": 64 | return nil, nil 65 | default: 66 | return ioutil.ReadFile(path) 67 | } 68 | } 69 | 70 | func configPath() (string, error) { 71 | if possiblePath := os.Getenv("PHRASEAPP_CONFIG"); possiblePath != "" { 72 | _, err := os.Stat(possiblePath) 73 | if err == nil { 74 | return possiblePath, nil 75 | } 76 | 77 | if os.IsNotExist(err) { 78 | err = fmt.Errorf("file %q (from PHRASEAPP_CONFIG environment variable) doesn't exist", possiblePath) 79 | } 80 | 81 | return "", err 82 | } 83 | 84 | workingDir, err := os.Getwd() 85 | if err != nil { 86 | return "", nil 87 | } 88 | 89 | for _, configName := range configNames { 90 | possiblePath := filepath.Join(workingDir, configName) 91 | if _, err := os.Stat(possiblePath); err == nil { 92 | return possiblePath, nil 93 | } 94 | } 95 | 96 | for _, configName := range configNames { 97 | possiblePath := filepath.Join(defaultConfigDir(), configName) 98 | if _, err := os.Stat(possiblePath); err == nil { 99 | return possiblePath, nil 100 | } 101 | } 102 | 103 | return "", nil 104 | } 105 | 106 | func (cfg *Config) UnmarshalYAML(unmarshal func(i interface{}) error) error { 107 | m := map[string]interface{}{} 108 | err := ParseYAMLToMap(unmarshal, map[string]interface{}{ 109 | "access_token": &cfg.Credentials.Token, 110 | "host": &cfg.Credentials.Host, 111 | "debug": &cfg.Debug, 112 | "page": &cfg.Page, 113 | "per_page": &cfg.PerPage, 114 | "project_id": &cfg.DefaultProjectID, 115 | "file_format": &cfg.DefaultFileFormat, 116 | "push": &cfg.Sources, 117 | "pull": &cfg.Targets, 118 | "defaults": &m, 119 | }) 120 | if err != nil { 121 | return err 122 | } 123 | 124 | cfg.Defaults = map[string]map[string]interface{}{} 125 | for path, rawConfig := range m { 126 | cfg.Defaults[path], err = ValidateIsRawMap("defaults."+path, rawConfig) 127 | if err != nil { 128 | return err 129 | } 130 | } 131 | 132 | return nil 133 | } 134 | 135 | const cfgValueErrStr = "configuration key %q has invalid value: %T\nsee https://help.phrase.com/articles/2185247-configuration" 136 | const cfgKeyErrStr = "configuration key %q has invalid type: %T\nsee https://help.phrase.com/articles/2185247-configuration" 137 | const cfgInvalidKeyErrStr = "configuration key %q unknown\nsee https://help.phrase.com/articles/2185247-configuration" 138 | 139 | func ValidateIsString(k string, v interface{}) (string, error) { 140 | s, ok := v.(string) 141 | if !ok { 142 | return "", fmt.Errorf(cfgValueErrStr, k, v) 143 | } 144 | return s, nil 145 | } 146 | 147 | func ValidateIsBool(k string, v interface{}) (bool, error) { 148 | b, ok := v.(bool) 149 | if !ok { 150 | return false, fmt.Errorf(cfgValueErrStr, k, v) 151 | } 152 | return b, nil 153 | } 154 | 155 | func ValidateIsInt(k string, v interface{}) (int, error) { 156 | i, ok := v.(int) 157 | if !ok { 158 | return 0, fmt.Errorf(cfgValueErrStr, k, v) 159 | } 160 | return i, nil 161 | } 162 | 163 | func ValidateIsRawMap(k string, v interface{}) (map[string]interface{}, error) { 164 | raw, ok := v.(map[interface{}]interface{}) 165 | if !ok { 166 | return nil, fmt.Errorf(cfgValueErrStr, k, v) 167 | } 168 | 169 | ps := map[string]interface{}{} 170 | for mk, mv := range raw { 171 | s, ok := mk.(string) 172 | if !ok { 173 | return nil, fmt.Errorf(cfgKeyErrStr, fmt.Sprintf("%s.%v", k, mk), mk) 174 | } 175 | ps[s] = mv 176 | } 177 | return ps, nil 178 | } 179 | 180 | func ConvertToStringMap(raw map[string]interface{}) (map[string]string, error) { 181 | ps := map[string]string{} 182 | for mk, mv := range raw { 183 | switch v := mv.(type) { 184 | case string: 185 | ps[mk] = v 186 | case bool: 187 | ps[mk] = fmt.Sprintf("%t", v) 188 | case int: 189 | ps[mk] = fmt.Sprintf("%d", v) 190 | default: 191 | return nil, fmt.Errorf("invalid type of key %q: %T", mk, mv) 192 | } 193 | } 194 | return ps, nil 195 | } 196 | 197 | // Calls the YAML parser function (see yaml.v2/Unmarshaler interface) with a map 198 | // of string to interface. This map is then iterated to match against the given 199 | // map of keys to fields, validates the type and sets the fields accordingly. 200 | func ParseYAMLToMap(unmarshal func(interface{}) error, keysToField map[string]interface{}) error { 201 | m := map[string]interface{}{} 202 | if err := unmarshal(m); err != nil { 203 | return err 204 | } 205 | 206 | var err error 207 | for k, v := range m { 208 | value, found := keysToField[k] 209 | if !found { 210 | return fmt.Errorf(cfgInvalidKeyErrStr, k) 211 | } 212 | 213 | switch val := value.(type) { 214 | case *string: 215 | *val, err = ValidateIsString(k, v) 216 | case *int: 217 | *val, err = ValidateIsInt(k, v) 218 | case **int: 219 | *val = new(int) 220 | **val, err = ValidateIsInt(k, v) 221 | case *bool: 222 | *val, err = ValidateIsBool(k, v) 223 | case *map[string]interface{}: 224 | *val, err = ValidateIsRawMap(k, v) 225 | case *[]byte: 226 | *val, err = yaml.Marshal(v) 227 | default: 228 | err = fmt.Errorf(cfgValueErrStr, k, value) 229 | } 230 | if err != nil { 231 | return err 232 | } 233 | } 234 | 235 | return nil 236 | } 237 | -------------------------------------------------------------------------------- /phraseapp/client.go: -------------------------------------------------------------------------------- 1 | // Package phraseapp is a library for easier usage of the Phrase API 2 | package phraseapp 3 | 4 | import ( 5 | "bytes" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "net/http" 10 | "net/url" 11 | "os" 12 | "strconv" 13 | 14 | "github.com/bgentry/speakeasy" 15 | ) 16 | 17 | // Client is a generic PhraseApp client. It manages a connection to the PhraseApp API 18 | type Client struct { 19 | http.Client 20 | Credentials Credentials 21 | debug bool 22 | } 23 | 24 | // Credentials contains all information to authenticate against phrase.com or a custom host. 25 | type Credentials struct { 26 | Username string `cli:"opt --username -u desc='username used for authentication'"` 27 | Token string `cli:"opt --access-token -t desc='access token used for authentication'"` 28 | TFA bool `cli:"opt --tfa desc='use Two-Factor Authentication'"` 29 | Host string `cli:"opt --host desc='Host to send Request to'"` 30 | } 31 | 32 | // NewClient initializes a new client. 33 | // Uses PHRASEAPP_HOST and PHRASEAPP_ACCESS_TOKEN environment variables for host and access token with specified in environment. 34 | func NewClient(credentials Credentials, debug bool) (*Client, error) { 35 | credentials.init() 36 | client := &Client{ 37 | Credentials: credentials, 38 | debug: debug, 39 | } 40 | 41 | return client, nil 42 | } 43 | 44 | // EnableCaching for API requests on disk via etags 45 | func (client *Client) EnableCaching(config CacheConfig) error { 46 | cache, err := newHTTPCacheClient(client.debug, config) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | client.Transport = cache 52 | return nil 53 | } 54 | 55 | // DisableCaching for API requests 56 | func (client *Client) DisableCaching() { 57 | client.Transport = nil 58 | } 59 | 60 | func (c *Credentials) init() { 61 | envToken := os.Getenv("PHRASEAPP_ACCESS_TOKEN") 62 | if envToken != "" && c.Token == "" && c.Username == "" { 63 | c.Token = envToken 64 | } 65 | 66 | if c.Host == "" { 67 | envHost := os.Getenv("PHRASEAPP_HOST") 68 | if envHost != "" { 69 | c.Host = envHost 70 | } else { 71 | c.Host = "https://api.phrase.com" 72 | } 73 | } 74 | } 75 | 76 | func (client *Client) authenticate(req *http.Request) error { 77 | if client.Credentials.Token != "" { 78 | req.Header.Set("Authorization", "token "+client.Credentials.Token) 79 | } else if client.Credentials.Username != "" { 80 | pwd, err := speakeasy.Ask("Password: ") 81 | if err != nil { 82 | return err 83 | } 84 | req.SetBasicAuth(client.Credentials.Username, pwd) 85 | 86 | if client.Credentials.TFA { // TFA only required for username+password based login. 87 | token, err := speakeasy.Ask("TFA-Token: ") 88 | if err != nil { 89 | return err 90 | } 91 | req.Header.Set("X-PhraseApp-OTP", token) 92 | } 93 | } else { 94 | return fmt.Errorf("either username or token must be given") 95 | } 96 | 97 | req.Header.Set("User-Agent", GetUserAgent()) 98 | 99 | return nil 100 | } 101 | 102 | func (client *Client) sendRequestPaginated(method, urlPath, contentType string, body io.Reader, expectedStatus, page, perPage int) (io.ReadCloser, error) { 103 | endpointURL, err := url.Parse(client.Credentials.Host + urlPath) 104 | if err != nil { 105 | return nil, err 106 | } 107 | 108 | addPagination(endpointURL, page, perPage) 109 | 110 | req, err := client.buildRequest(method, endpointURL, body, contentType) 111 | if err != nil { 112 | return nil, err 113 | } 114 | 115 | resp, err := client.send(req, expectedStatus) 116 | if err != nil { 117 | return nil, err 118 | } 119 | 120 | return resp.Body, nil 121 | } 122 | 123 | func (client *Client) sendGetRequestPaginated(urlPath string, params map[string]string, expectedStatus, page, perPage int) (io.ReadCloser, error) { 124 | endpointURL, err := url.Parse(client.Credentials.Host + urlPath) 125 | if err != nil { 126 | return nil, err 127 | } 128 | 129 | addPagination(endpointURL, page, perPage) 130 | 131 | req, err := client.buildRequest("GET", endpointURL, nil, "") 132 | if err != nil { 133 | return nil, err 134 | } 135 | 136 | values := req.URL.Query() 137 | for key, value := range params { 138 | values.Add(key, value) 139 | } 140 | 141 | req.URL.RawQuery = values.Encode() 142 | resp, err := client.send(req, expectedStatus) 143 | if err != nil { 144 | return nil, err 145 | } 146 | 147 | return resp.Body, nil 148 | } 149 | 150 | 151 | func (client *Client) sendRequest(method, urlPath, contentType string, body io.Reader, expectedStatus int) (io.ReadCloser, error) { 152 | endpointURL, err := url.Parse(client.Credentials.Host + urlPath) 153 | if err != nil { 154 | return nil, err 155 | } 156 | 157 | req, err := client.buildRequest(method, endpointURL, body, contentType) 158 | if err != nil { 159 | return nil, err 160 | } 161 | 162 | resp, err := client.send(req, expectedStatus) 163 | if err != nil { 164 | return nil, err 165 | } 166 | return resp.Body, nil 167 | } 168 | 169 | func (client *Client) sendGetRequest(urlPath string, params map[string]string, expectedStatus int) (io.ReadCloser, error) { 170 | endpointURL, err := url.Parse(client.Credentials.Host + urlPath) 171 | if err != nil { 172 | return nil, err 173 | } 174 | 175 | req, err := client.buildRequest("GET", endpointURL, nil, "") 176 | if err != nil { 177 | return nil, err 178 | } 179 | 180 | values := req.URL.Query() 181 | for key, value := range params { 182 | values.Add(key, value) 183 | } 184 | 185 | req.URL.RawQuery = values.Encode() 186 | resp, err := client.send(req, expectedStatus) 187 | if err != nil { 188 | return nil, err 189 | } 190 | return resp.Body, nil 191 | } 192 | 193 | 194 | func (client *Client) send(req *http.Request, expectedStatus int) (*http.Response, error) { 195 | err := client.authenticate(req) 196 | if err != nil { 197 | return nil, err 198 | } 199 | 200 | if client.debug { 201 | b := new(bytes.Buffer) 202 | err = req.Header.Write(b) 203 | if err != nil { 204 | return nil, err 205 | } 206 | 207 | fmt.Fprintln(os.Stderr, "Header:", b.String()) 208 | } 209 | 210 | resp, err := client.Client.Do(req) 211 | if err != nil { 212 | return nil, err 213 | } 214 | 215 | if client.debug { 216 | fmt.Fprintf(os.Stderr, "\nResponse HTTP Status Code: %s\n", resp.Status) 217 | } 218 | 219 | err = handleResponseStatus(resp, expectedStatus) 220 | if err != nil { 221 | resp.Body.Close() 222 | } 223 | return resp, err 224 | } 225 | 226 | func addPagination(u *url.URL, page, perPage int) { 227 | query := u.Query() 228 | query.Add("page", strconv.Itoa(page)) 229 | query.Add("per_page", strconv.Itoa(perPage)) 230 | 231 | u.RawQuery = query.Encode() 232 | } 233 | 234 | func (client *Client) buildRequest(method string, u *url.URL, body io.Reader, contentType string) (*http.Request, error) { 235 | if client.debug { 236 | fmt.Fprintln(os.Stderr, "Method:", method) 237 | fmt.Fprintln(os.Stderr, "URL:", u) 238 | 239 | if body != nil { 240 | bodyBytes, err := ioutil.ReadAll(body) 241 | if err != nil { 242 | fmt.Fprintln(os.Stderr, "Error reading body:", err.Error()) 243 | } 244 | 245 | fmt.Fprintln(os.Stderr, "Body:", string(bodyBytes)) 246 | body = bytes.NewReader(bodyBytes) 247 | } 248 | } 249 | 250 | req, err := http.NewRequest(method, u.String(), body) 251 | if err != nil { 252 | return nil, err 253 | } 254 | 255 | if contentType != "" { 256 | req.Header.Add("Content-Type", contentType) 257 | } 258 | 259 | return req, nil 260 | } 261 | -------------------------------------------------------------------------------- /phraseapp/config_test.go: -------------------------------------------------------------------------------- 1 | package phraseapp 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "testing" 7 | ) 8 | 9 | func TestValidateIsType(t *testing.T) { 10 | var t1 string = "foobar" 11 | var t2 int = 1 12 | var t3 bool = true 13 | expErrT1 := fmt.Sprintf(cfgValueErrStr, "a", t1) 14 | expErrT2 := fmt.Sprintf(cfgValueErrStr, "a", t2) 15 | expErrT3 := fmt.Sprintf(cfgValueErrStr, "a", t3) 16 | 17 | switch res, err := ValidateIsString("a", t1); { 18 | case err != nil: 19 | t.Errorf("didn't expect an error, got %q", err) 20 | case res != t1: 21 | t.Errorf("expected value to be %q, got %q", t1, res) 22 | } 23 | 24 | switch _, err := ValidateIsString("a", t2); { 25 | case err == nil: 26 | t.Errorf("expect an error, got none") 27 | case err.Error() != expErrT2: 28 | t.Errorf("expected error to be %q, got %q", expErrT2, err) 29 | } 30 | 31 | switch _, err := ValidateIsString("a", t3); { 32 | case err == nil: 33 | t.Errorf("expect an error, got none") 34 | case err.Error() != expErrT3: 35 | t.Errorf("expected error to be %q, got %q", expErrT3, err) 36 | } 37 | 38 | switch _, err := ValidateIsInt("a", t1); { 39 | case err == nil: 40 | t.Errorf("expect an error, got none") 41 | case err.Error() != expErrT1: 42 | t.Errorf("expected error to be %q, got %q", expErrT1, err) 43 | } 44 | 45 | switch res, err := ValidateIsInt("a", t2); { 46 | case err != nil: 47 | t.Errorf("didn't expect an error, got %q", err) 48 | case res != t2: 49 | t.Errorf("expected value to be %q, got %q", t2, res) 50 | } 51 | 52 | switch _, err := ValidateIsInt("a", t3); { 53 | case err == nil: 54 | t.Errorf("expect an error, got none") 55 | case err.Error() != expErrT3: 56 | t.Errorf("expected error to be %q, got %q", expErrT3, err) 57 | } 58 | 59 | switch _, err := ValidateIsBool("a", t1); { 60 | case err == nil: 61 | t.Errorf("expect an error, got none") 62 | case err.Error() != expErrT1: 63 | t.Errorf("expected error to be %q, got %q", expErrT1, err) 64 | } 65 | 66 | switch _, err := ValidateIsBool("a", t2); { 67 | case err == nil: 68 | t.Errorf("expect an error, got none") 69 | case err.Error() != expErrT2: 70 | t.Errorf("expected error to be %q, got %q", expErrT2, err) 71 | } 72 | 73 | switch res, err := ValidateIsBool("a", t3); { 74 | case err != nil: 75 | t.Errorf("didn't expect an error, got %q", err) 76 | case res != t3: 77 | t.Errorf("expected value to be %t, got %t", t3, res) 78 | } 79 | } 80 | 81 | func TestValidateIsRawMapHappyPath(t *testing.T) { 82 | m := map[interface{}]interface{}{ 83 | "foo": "bar", 84 | "fuu": 1, 85 | "few": true, 86 | } 87 | 88 | res, err := ValidateIsRawMap("a", m) 89 | if err != nil { 90 | t.Errorf("didn't expect an error, got %q", err) 91 | } 92 | 93 | if len(m) != len(res) { 94 | t.Errorf("expected %d elements, got %d", len(m), len(res)) 95 | } 96 | 97 | for k, v := range res { 98 | if value, found := m[k]; !found { 99 | t.Errorf("expected key %q to be in source set, it wasn't", k) 100 | } else if value != v { 101 | t.Errorf("expected value of %q to be %q, got %q", k, value, v) 102 | } 103 | } 104 | } 105 | 106 | func TestValidateIsRawMapWithErrors(t *testing.T) { 107 | m := map[interface{}]interface{}{ 108 | 4: "should be error", 109 | } 110 | 111 | expErr := fmt.Sprintf(cfgKeyErrStr, "a.4", 4) 112 | _, err := ValidateIsRawMap("a", m) 113 | if err == nil { 114 | t.Errorf("expect an error, got none") 115 | } else if err.Error() != expErr { 116 | t.Errorf("expected error %q, got %q", expErr, err) 117 | } 118 | } 119 | 120 | func TestParseYAMLToMap(t *testing.T) { 121 | var a string 122 | var b int 123 | var c bool 124 | var d []byte 125 | e := map[string]interface{}{} 126 | 127 | err := ParseYAMLToMap(func(raw interface{}) error { 128 | m, ok := raw.(map[string]interface{}) 129 | if !ok { 130 | return fmt.Errorf("invalid type received") 131 | } 132 | m["a"] = "foo" 133 | m["b"] = 1 134 | m["c"] = true 135 | m["d"] = &struct { 136 | A string 137 | B int 138 | }{A: "bar", B: 2} 139 | m["e"] = map[interface{}]interface{}{"c": "baz", "d": 4} 140 | return nil 141 | }, map[string]interface{}{ 142 | "a": &a, 143 | "b": &b, 144 | "c": &c, 145 | "d": &d, 146 | "e": &e, 147 | }) 148 | if err != nil { 149 | t.Fatalf("didn't expect an error, got %q", err) 150 | } 151 | 152 | if a != "foo" { 153 | t.Errorf("expected %q, got %q", "foo", a) 154 | } 155 | 156 | if b != 1 { 157 | t.Errorf("expected %d, got %d", 1, b) 158 | } 159 | 160 | if c != true { 161 | t.Errorf("expected %t, got %t", true, c) 162 | } 163 | 164 | if string(d) != "a: bar\nb: 2\n" { 165 | t.Errorf("expected %s, got %s", "a: bar\nb: 2\n", string(d)) 166 | } 167 | 168 | if val, found := e["c"]; !found { 169 | t.Errorf("expected e to contain key %q, it didn't", "c") 170 | } else if val != "baz" { 171 | t.Errorf("expected e['c'] to have value %q, got %q", "baz", val) 172 | } 173 | 174 | if val, found := e["d"]; !found { 175 | t.Errorf("expected e to contain key %q, it didn't", "d") 176 | } else if val != 4 { 177 | t.Errorf("expected e['d'] to have value %d, got %d", 4, val) 178 | } 179 | } 180 | 181 | func TestConfigPath_ConfigFromEnv(t *testing.T) { 182 | // The phraseapp.yml file without the leading '.' so not hidden. Any file can be used from the environment! 183 | p := os.ExpandEnv("$GOPATH/src/github.com/phrase/phraseapp-go/testdata/phraseapp.yml") 184 | 185 | os.Setenv("PHRASEAPP_CONFIG", p) 186 | defer os.Unsetenv("PHRASEAPP_CONFIG") 187 | 188 | path, err := configPath() 189 | if err != nil { 190 | t.Fatalf("didn't expect an error, got: %s", err) 191 | } else if path != p { 192 | t.Errorf("expected path to be %q, got %q", p, path) 193 | } 194 | } 195 | 196 | func TestConfigPath_ConfigFromEnvButNotExisting(t *testing.T) { 197 | os.Setenv("PHRASEAPP_CONFIG", "phraseapp_does_not_exist.yml") 198 | defer os.Unsetenv("PHRASEAPP_CONFIG") 199 | 200 | _, err := configPath() 201 | if err == nil { 202 | t.Fatalf("expect an error, got none") 203 | } 204 | 205 | expErr := `file "phraseapp_does_not_exist.yml" (from PHRASEAPP_CONFIG environment variable) doesn't exist` 206 | if err.Error() != expErr { 207 | t.Errorf("expected error to be %q, got %q", expErr, err) 208 | } 209 | } 210 | 211 | func TestConfigPath_ConfigInCWD(t *testing.T) { 212 | cwd := os.ExpandEnv("$GOPATH/src/github.com/phrase/phraseapp-go/testdata") 213 | 214 | oldDir, _ := os.Getwd() 215 | err := os.Chdir(cwd) 216 | if err != nil { 217 | t.Fatalf("didn't expect an error changing the working directory, got: %s", err) 218 | } 219 | defer os.Chdir(oldDir) 220 | 221 | path, err := configPath() 222 | if err != nil { 223 | t.Fatalf("didn't expect an error, got: %s", err) 224 | } 225 | expPath := cwd + "/.phraseapp.yml" 226 | if path != expPath { 227 | t.Errorf("expected path to be %q, got %q", expPath, path) 228 | } 229 | } 230 | 231 | func TestConfigPath_ConfigPreference(t *testing.T) { 232 | cwd := os.ExpandEnv("$GOPATH/src/github.com/phrase/phraseapp-go/testdata/config_files") 233 | 234 | oldDir, _ := os.Getwd() 235 | err := os.Chdir(cwd) 236 | if err != nil { 237 | t.Fatalf("didn't expect an error changing the working directory, got: %s", err) 238 | } 239 | defer os.Chdir(oldDir) 240 | 241 | path, err := configPath() 242 | if err != nil { 243 | t.Fatalf("didn't expect an error, got: %s", err) 244 | } 245 | expPath := cwd + "/.phrase.yml" 246 | if path != expPath { 247 | t.Errorf("expected path to be %q, got %q", expPath, path) 248 | } 249 | } 250 | 251 | func TestConfigPath_ConfigInHomeDir(t *testing.T) { 252 | cwd := os.ExpandEnv("$GOPATH/src/github.com/phrase/phraseapp-go/testdata/empty") 253 | oldDir, _ := os.Getwd() 254 | err := os.Chdir(cwd) 255 | if err != nil { 256 | t.Fatalf("didn't expect an error changing the working directory, got: %s", err) 257 | } 258 | defer os.Chdir(oldDir) 259 | 260 | newHome := os.ExpandEnv("$GOPATH/src/github.com/phrase/phraseapp-go/testdata") 261 | oldHome := os.Getenv("HOME") 262 | os.Setenv("HOME", newHome) 263 | defer os.Setenv("HOME", oldHome) 264 | 265 | path, err := configPath() 266 | if err != nil { 267 | t.Fatalf("didn't expect an error, got: %s", err) 268 | } 269 | expPath := newHome + "/.phraseapp.yml" 270 | if path != expPath { 271 | t.Errorf("expected path to be %q, got %q", expPath, path) 272 | } 273 | } 274 | 275 | func TestConfigPath_NoConfigAvailable(t *testing.T) { 276 | // For this to work the configuration of the user running the test 277 | // must be obfuscated (changing the CWD and HOME env variable), so 278 | // user's files do not inflict the test environment. 279 | 280 | cwd := os.ExpandEnv("$GOPATH/src/github.com/phrase/phraseapp-go/testdata/empty") 281 | oldDir, _ := os.Getwd() 282 | err := os.Chdir(cwd) 283 | if err != nil { 284 | t.Fatalf("didn't expect an error changing the working directory, got: %s", err) 285 | } 286 | defer os.Chdir(oldDir) 287 | 288 | oldHome := os.Getenv("HOME") 289 | os.Setenv("HOME", os.ExpandEnv("$GOPATH/src/github.com/phrase/phraseapp-go/testdata/empty2")) 290 | defer os.Setenv("HOME", oldHome) 291 | 292 | path, err := configPath() 293 | if err != nil { 294 | t.Fatalf("didn't expect an error, got: %s", err) 295 | } 296 | expPath := "" 297 | if path != expPath { 298 | t.Errorf("expected path to be %q, got %q", expPath, path) 299 | } 300 | } 301 | 302 | func TestParseConfig(t *testing.T) { 303 | os.Setenv("PHRASEAPP_CONFIG", os.ExpandEnv("$GOPATH/src/github.com/phrase/phraseapp-go/testdata/config_files/.phrase.yml")) 304 | defer os.Unsetenv("PHRASEAPP_CONFIG") 305 | config, err := ReadConfig() 306 | if err != nil { 307 | t.Error(err) 308 | } 309 | 310 | if config.Token != "123" { 311 | t.Errorf("Got %s, expected %s", config.Token, "123") 312 | } 313 | } 314 | 315 | func TestParseConfig_LegacyPhraseApp(t *testing.T) { 316 | os.Setenv("PHRASEAPP_CONFIG", os.ExpandEnv("$GOPATH/src/github.com/phrase/phraseapp-go/testdata/config_files/.phraseapp.yml")) 317 | defer os.Unsetenv("PHRASEAPP_CONFIG") 318 | config, err := ReadConfig() 319 | if err != nil { 320 | t.Error(err) 321 | } 322 | 323 | if config.Token != "123" { 324 | t.Errorf("Got %s, expected %s", config.Token, "123") 325 | } 326 | } 327 | 328 | --------------------------------------------------------------------------------