Adding Download options (#2)

* Adding crowdin branch support

Signed-off-by: Jonas Franz <info@jonasfranz.de>

* Adding documentation for ignore_branche

Signed-off-by: Jonas Franz <info@jonasfranz.de>

* Adding Download functionality
Adding Unzip utility
Adding Download options to DOCS.md

Signed-off-by: Jonas Franz <info@jonasfranz.de>

* Fixing tests

Signed-off-by: Jonas Franz <info@jonasfranz.de>

* Fixing tests

Signed-off-by: Jonas Franz <info@jonasfranz.de>

* Fixing URLs

Signed-off-by: Jonas Franz <info@jonasfranz.de>

* Adding unit tests

Signed-off-by: Jonas Franz <info@jonasfranz.de>

* Adding unit tests

Signed-off-by: Jonas Franz <info@jonasfranz.de>

* Adding unit tests

Signed-off-by: Jonas Franz <info@jonasfranz.de>

* Removing fmt.Println debug

Signed-off-by: Jonas Franz <info@jonasfranz.de>
pull/3/head
Jonas Franz 6 years ago committed by GitHub
parent 39fb5cfbe0
commit 77f8b6e9d9
  1. 2
      .drone.yml
  2. 10
      DOCS.md
  3. 19
      main.go
  4. 161
      plugin.go
  5. 43
      plugin_test.go
  6. 24
      responses/responses.go
  7. 61
      responses/responses_test.go
  8. 67
      utils/unzip.go
  9. 15
      vendor/github.com/davecgh/go-spew/LICENSE
  10. 152
      vendor/github.com/davecgh/go-spew/spew/bypass.go
  11. 38
      vendor/github.com/davecgh/go-spew/spew/bypasssafe.go
  12. 341
      vendor/github.com/davecgh/go-spew/spew/common.go
  13. 306
      vendor/github.com/davecgh/go-spew/spew/config.go
  14. 211
      vendor/github.com/davecgh/go-spew/spew/doc.go
  15. 509
      vendor/github.com/davecgh/go-spew/spew/dump.go
  16. 419
      vendor/github.com/davecgh/go-spew/spew/format.go
  17. 148
      vendor/github.com/davecgh/go-spew/spew/spew.go
  18. 27
      vendor/github.com/pmezard/go-difflib/LICENSE
  19. 772
      vendor/github.com/pmezard/go-difflib/difflib/difflib.go
  20. 22
      vendor/github.com/stretchr/testify/LICENCE.txt
  21. 22
      vendor/github.com/stretchr/testify/LICENSE
  22. 332
      vendor/github.com/stretchr/testify/README.md
  23. 379
      vendor/github.com/stretchr/testify/assert/assertion_format.go
  24. 4
      vendor/github.com/stretchr/testify/assert/assertion_format.go.tmpl
  25. 746
      vendor/github.com/stretchr/testify/assert/assertion_forward.go
  26. 4
      vendor/github.com/stretchr/testify/assert/assertion_forward.go.tmpl
  27. 1208
      vendor/github.com/stretchr/testify/assert/assertions.go
  28. 45
      vendor/github.com/stretchr/testify/assert/doc.go
  29. 10
      vendor/github.com/stretchr/testify/assert/errors.go
  30. 16
      vendor/github.com/stretchr/testify/assert/forward_assertions.go
  31. 127
      vendor/github.com/stretchr/testify/assert/http_assertions.go
  32. 22
      vendor/github.com/stretchr/testify/doc.go
  33. 24
      vendor/vendor.json

@ -8,7 +8,7 @@ pipeline:
pull: true
commands:
- go vet
- go test -cover -coverprofile=coverage.out
- go test -cover $(go list ./... | grep -v /vendor/)
build_linux_amd64:
image: golang:1.9

@ -8,6 +8,10 @@ You must provide in your configuration:
* key: the Crowdin file name
* value: the real path the to file
* `ignore_branch` It will send the Drone branch to Crowdin if it is `false`. (Default: `false`)
* `download` Downloads translated files from Crowdin if it is `true`. (Default: `false`)
* `export_dir` Export directory of the translated strings
* `languages` Languages which should be downloaded/exported from Crowdin. (Default: `all`)
Information about API keys: https://support.crowdin.com/api/api-integration-setup/
## Example
@ -23,4 +27,10 @@ pipeline:
files:
example: options/example.ini
example2: options/example2.ini
ignore_branch: true
download: true
export_dir: langs/
languages:
- de
- fr
```

@ -44,6 +44,22 @@ func main() {
Usage: "if true it will not pass the branch to crowdin",
EnvVar: "PLUGIN_IGNORE_BRANCH",
},
cli.StringFlag{
Name: "export-dir",
Usage: "the directory where the translated files should be extracted in",
EnvVar: "PLUGIN_EXPORT_DIR",
},
cli.BoolFlag{
Name: "download",
Usage: "downloads translated files if true",
EnvVar: "PLUGIN_DOWNLOAD",
},
cli.StringSliceFlag{
Name: "languages",
Usage: "the languages that should be exported",
EnvVar: "PLUGIN_LANGUAGES",
Value: &cli.StringSlice{"all"},
},
}
if err := app.Run(os.Args); err != nil {
log.Fatal(err)
@ -57,6 +73,9 @@ func run(c *cli.Context) error {
Identifier: c.String("project-identifier"),
Key: c.String("project-key"),
},
DoDownload: c.Bool("download"),
Languages: c.StringSlice("languages"),
ExportDirectory: c.String("export-dir"),
}
if !c.Bool("ignore-branch") {
plugin.Branch = c.String("commit.branch")

@ -2,10 +2,9 @@ package main
import (
"bytes"
"encoding/xml"
"fmt"
"github.com/JonasFranzDEV/drone-crowdin/responses"
"golang.org/x/net/html/charset"
"github.com/JonasFranzDEV/drone-crowdin/utils"
"io"
"mime/multipart"
"net/http"
@ -24,39 +23,106 @@ type (
// Plugin represents the drone-crowdin plugin including config and file-mapping.
Plugin struct {
Config Config
Files Files
Branch string
Config Config
Files Files
Branch string
Languages []string
ExportDirectory string
DoDownload bool
}
)
// ToURL returns the API-endpoint including identifier and API-KEY
func (c Config) ToURL() string {
return fmt.Sprintf("https://api.crowdin.com/api/project/%s/update-file?key=%s", c.Identifier, c.Key)
// ToProjectURL returns the base URL of the api endpoint
func (c Config) ToProjectURL() string {
return fmt.Sprintf("https://api.crowdin.com/api/project/%s", c.Identifier)
}
// ToUploadURL returns the API-endpoint including identifier and API-KEY
func (c Config) ToUploadURL() string {
return fmt.Sprintf("%s/update-file?key=%s", c.ToProjectURL(), c.Key)
}
// ToLanguageDownloadURL returns the download endpoint for the given language
func (p Plugin) ToLanguageDownloadURL(language string) string {
if p.Branch != "" {
return fmt.Sprintf("%s/download/%s.zip?key=%s&branch=%s", p.Config.ToProjectURL(), language, p.Config.Key, p.Branch)
}
return fmt.Sprintf("%s/download/%s.zip?key=%s", p.Config.ToProjectURL(), language, p.Config.Key)
}
// Exec starts the plugin and updates the crowdin translation by uploading files from the files map
func (p Plugin) Exec() error {
client := &http.Client{}
//SECTION: Upload
if len(p.Files) > 20 {
return fmt.Errorf("20 files max are allowed to upload. %d files given", len(p.Files))
} else if len(p.Files) > 0 {
req, err := p.buildUploadRequest()
if err != nil {
return fmt.Errorf("error while building upload request: %v", err)
}
resp, err := client.Do(req)
if err != nil {
return err
}
body := &bytes.Buffer{}
if _, err := body.ReadFrom(resp.Body); err != nil {
return err
}
if err := resp.Body.Close(); err != nil {
return err
}
if resp.StatusCode != 200 {
if e, err := responses.ParseAsError(body); err != nil {
return err
} else {
return fmt.Errorf("error while uploading: %v", e)
}
}
var success = new(responses.Success)
if success, err = responses.ParseAsSuccess(body); err != nil {
return err
}
for _, file := range success.Stats {
fmt.Printf("%s: %s\n", file.Name, file.Status)
}
}
//SECTION: Download
if p.DoDownload {
if err := p.buildTranslations(client); err != nil {
return fmt.Errorf("error while building languages: %v", err)
}
for _, language := range p.Languages {
if err := p.downloadLanguage(client, language); err != nil {
return fmt.Errorf("error while downloading %s: %v", language, err)
}
fmt.Printf("Downloaded package: %s\n", language)
}
}
return nil
}
func (p Plugin) buildUploadRequest() (*http.Request, error) {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
for crowdinPath, path := range p.Files {
var err error
var file *os.File
if file, err = os.Open(path); err != nil {
return err
return nil, err
}
defer file.Close()
part, err := writer.CreateFormFile(fmt.Sprintf("files[%s]", crowdinPath), crowdinPath)
if err != nil {
return err
return nil, err
}
if _, err = io.Copy(part, file); err != nil {
return err
return nil, err
}
}
// Adding branch if it is not ignored
@ -64,45 +130,68 @@ func (p Plugin) Exec() error {
writer.WriteField("branch", p.Branch)
}
if err := writer.Close(); err != nil {
return err
return nil, err
}
var req *http.Request
var err error
if req, err = http.NewRequest("POST", p.Config.ToURL(), body); err != nil {
return err
if req, err = http.NewRequest("POST", p.Config.ToUploadURL(), body); err != nil {
return nil, err
}
req.Header.Set("Content-Type", writer.FormDataContentType())
return req, nil
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
func (p Plugin) buildTranslations(client *http.Client) error {
// Step 1: Export translations (aka generate server side)
exportURL := fmt.Sprintf("%s/export?key=%s", p.Config.ToProjectURL(), p.Config.Key)
if p.Branch != "" {
exportURL = fmt.Sprintf("%s&branch=%s", exportURL, p.Branch)
}
if resp, err := client.Get(exportURL); err != nil {
return err
} else if resp.StatusCode != 200 {
defer resp.Body.Close()
if e, err := responses.ParseAsError(resp.Body); err != nil {
return err
} else {
return e
}
} else {
defer resp.Body.Close()
}
return nil
}
body = &bytes.Buffer{}
if _, err := body.ReadFrom(resp.Body); err != nil {
func (p Plugin) downloadLanguage(client *http.Client, language string) error {
file, err := downloadFromUrl(p.ToLanguageDownloadURL(language))
if err != nil {
return err
}
if err := resp.Body.Close(); err != nil {
err = utils.Unzip(file.Name(), p.ExportDirectory)
if err != nil {
return err
}
if resp.StatusCode != 200 {
var errResponse = new(responses.Error)
decoder := xml.NewDecoder(body)
decoder.CharsetReader = charset.NewReaderLabel
if err := decoder.Decode(&errResponse); err != nil {
return err
}
return errResponse
}
var success = new(responses.Success)
decoder := xml.NewDecoder(body)
decoder.CharsetReader = charset.NewReaderLabel
if err := decoder.Decode(&success); err != nil {
err = os.Remove(file.Name())
if err != nil {
return err
}
for _, file := range success.Stats {
fmt.Printf("%s: %s\n", file.Name, file.Status)
}
return nil
}
func downloadFromUrl(url string) (*os.File, error) {
output, err := os.Create("lang.zip")
if err != nil {
return nil, err
}
defer output.Close()
response, err := http.Get(url)
if err != nil {
os.Remove(output.Name())
return nil, err
}
defer response.Body.Close()
_, err = io.Copy(output, response.Body)
return output, err
}

@ -1,11 +1,42 @@
package main
import "testing"
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestConfig_ToURL(t *testing.T) {
exampleConfig := &Config{Identifier: "test", Key: "MYKEY"}
result := exampleConfig.ToURL()
if result != "https://api.crowdin.com/api/project/test/update-file?key=MYKEY" {
t.Fatalf("ToURL returns \"%s\" instead of the expected \"%s\"", result, "https://api.crowdin.com/api/project/test/update-file?key=MYKEY")
func setupExamplePlugin() *Plugin {
return &Plugin{
ExportDirectory: "demo/",
Languages: []string{"all"},
DoDownload: true,
Files: map[string]string{"locale_en-US.ini": "LICENSE"},
Config: Config{
Key: "MYKEY",
Identifier: "test",
},
Branch: "master",
}
}
func TestConfig_ToUploadURL(t *testing.T) {
exampleConfig := setupExamplePlugin().Config
result := exampleConfig.ToUploadURL()
assert.Equal(t, "https://api.crowdin.com/api/project/test/update-file?key=MYKEY", result, "ToUploadURL")
}
func TestPlugin_ToLanguageDownloadURL(t *testing.T) {
examplePlugin := setupExamplePlugin()
result := examplePlugin.ToLanguageDownloadURL(examplePlugin.Languages[0])
assert.Equal(t, "https://api.crowdin.com/api/project/test/download/all.zip?key=MYKEY&branch=master", result, "ToLanguageDownloadURL")
examplePlugin.Branch = ""
result = examplePlugin.ToLanguageDownloadURL(examplePlugin.Languages[0])
assert.Equal(t, "https://api.crowdin.com/api/project/test/download/all.zip?key=MYKEY", result, "ToLanguageDownloadURL")
}
func TestConfig_ToProjectURL(t *testing.T) {
exampleConfig := setupExamplePlugin().Config
result := exampleConfig.ToProjectURL()
assert.Equal(t, "https://api.crowdin.com/api/project/test", result, "ToProjectURL")
}

@ -3,6 +3,8 @@ package responses
import (
"encoding/xml"
"fmt"
"golang.org/x/net/html/charset"
"io"
)
// Error is a crowdin error message
@ -17,12 +19,34 @@ func (e *Error) Error() string {
return fmt.Sprintf("Error from crowdin: %s (error code %d)", e.Message, e.Code)
}
// ParseAsError parses XML to Error
func ParseAsError(body io.Reader) (*Error, error) {
var errResponse = new(Error)
decoder := xml.NewDecoder(body)
decoder.CharsetReader = charset.NewReaderLabel
if err := decoder.Decode(&errResponse); err != nil {
return nil, err
}
return errResponse, nil
}
// Success is a crowdin success message
type Success struct {
XMLName xml.Name `xml:"success"`
Stats []File `xml:"stats>file"`
}
// ParseAsSuccess parses XML to Success
func ParseAsSuccess(body io.Reader) (*Success, error) {
var success = new(Success)
decoder := xml.NewDecoder(body)
decoder.CharsetReader = charset.NewReaderLabel
if err := decoder.Decode(&success); err != nil {
return nil, err
}
return success, nil
}
// File represents the status of an uploaded file
type File struct {
XMLName xml.Name `xml:"file"`

@ -0,0 +1,61 @@
package responses
import (
"bytes"
"github.com/stretchr/testify/assert"
"testing"
)
var errorData = `<?xml version="1.0" encoding="ISO-8859-1"?>
<error>
<code>3</code>
<message>API key is not valid</message>
</error>
`
var invalidErrorData = `<?xml version="1.0" encoding="ISO-fdsfsdf-1"?>
<error>
<code>3</code>
<message>API key is not valid</message>
</error>
`
var successData = `<?xml version="1.0" encoding="ISO-8859-1"?>
<success>
<stats>
<file status="skipped" name="demo.ini"></file>
</stats>
</success>`
var invalidSuccessData = `<?xml version="1.0" encoding="ISO-sefsgdfb-1"?>
<success>
<stats>
<file status="skipped" name="demo.ini"></file>
</stats>
</success>`
func TestParseAsError(t *testing.T) {
result, err := ParseAsError(bytes.NewBufferString(errorData))
assert.NoError(t, err)
assert.Equal(t, 3, result.Code, "error code")
assert.Equal(t, "API key is not valid", result.Message, "error message")
_, err = ParseAsSuccess(bytes.NewBufferString(invalidErrorData))
assert.Error(t, err)
}
func TestError_Error(t *testing.T) {
result, err := ParseAsError(bytes.NewBufferString(errorData))
assert.NoError(t, err)
assert.Error(t, result)
assert.Equal(t, result.Error(), "Error from crowdin: API key is not valid (error code 3)", "error message")
}
func TestParseAsSuccess(t *testing.T) {
result, err := ParseAsSuccess(bytes.NewBufferString(successData))
assert.NoError(t, err)
assert.Len(t, result.Stats, 1, "files")
assert.Equal(t, result.Stats[0].Status, "skipped", "status of first file")
assert.Equal(t, result.Stats[0].Name, "demo.ini", "name of first file")
_, err = ParseAsError(bytes.NewBufferString(invalidSuccessData))
assert.Error(t, err)
}

@ -0,0 +1,67 @@
package utils
import (
"archive/zip"
"io"
"os"
"path/filepath"
)
func Unzip(src, dest string) error {
r, err := zip.OpenReader(src)
if err != nil {
return err
}
defer func() {
if err := r.Close(); err != nil {
panic(err)
}
}()
os.MkdirAll(dest, 0755)
// Closure to address file descriptors issue with all the deferred .Close() methods
extractAndWriteFile := func(f *zip.File) error {
rc, err := f.Open()
if err != nil {
return err
}
defer func() {
if err := rc.Close(); err != nil {
panic(err)
}
}()
path := filepath.Join(dest, f.Name)
if f.FileInfo().IsDir() {
os.MkdirAll(path, f.Mode())
} else {
os.MkdirAll(filepath.Dir(path), f.Mode())
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
if err != nil {
return err
}
defer func() {
if err := f.Close(); err != nil {
panic(err)
}
}()
_, err = io.Copy(f, rc)
if err != nil {
return err
}
}
return nil
}
for _, f := range r.File {
err := extractAndWriteFile(f)
if err != nil {
return err
}
}
return nil
}

@ -0,0 +1,15 @@
ISC License
Copyright (c) 2012-2016 Dave Collins <dave@davec.name>
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

@ -0,0 +1,152 @@
// Copyright (c) 2015-2016 Dave Collins <dave@davec.name>
//
// Permission to use, copy, modify, and distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
// NOTE: Due to the following build constraints, this file will only be compiled
// when the code is not running on Google App Engine, compiled by GopherJS, and
// "-tags safe" is not added to the go build command line. The "disableunsafe"
// tag is deprecated and thus should not be used.
// +build !js,!appengine,!safe,!disableunsafe
package spew
import (
"reflect"
"unsafe"
)
const (
// UnsafeDisabled is a build-time constant which specifies whether or
// not access to the unsafe package is available.
UnsafeDisabled = false
// ptrSize is the size of a pointer on the current arch.
ptrSize = unsafe.Sizeof((*byte)(nil))
)
var (
// offsetPtr, offsetScalar, and offsetFlag are the offsets for the
// internal reflect.Value fields. These values are valid before golang
// commit ecccf07e7f9d which changed the format. The are also valid
// after commit 82f48826c6c7 which changed the format again to mirror
// the original format. Code in the init function updates these offsets
// as necessary.
offsetPtr = ptrSize
offsetScalar = uintptr(0)
offsetFlag = ptrSize * 2
// flagKindWidth and flagKindShift indicate various bits that the
// reflect package uses internally to track kind information.
//
// flagRO indicates whether or not the value field of a reflect.Value is
// read-only.
//
// flagIndir indicates whether the value field of a reflect.Value is
// the actual data or a pointer to the data.
//
// These values are valid before golang commit 90a7c3c86944 which
// changed their positions. Code in the init function updates these
// flags as necessary.
flagKindWidth = uintptr(5)
flagKindShift = flagKindWidth - 1
flagRO = uintptr(1 << 0)
flagIndir = uintptr(1 << 1)
)
func init() {
// Older versions of reflect.Value stored small integers directly in the
// ptr field (which is named val in the older versions). Versions
// between commits ecccf07e7f9d and 82f48826c6c7 added a new field named
// scalar for this purpose which unfortunately came before the flag
// field, so the offset of the flag field is different for those
// versions.
//
// This code constructs a new reflect.Value from a known small integer
// and checks if the size of the reflect.Value struct indicates it has
// the scalar field. When it does, the offsets are updated accordingly.
vv := reflect.ValueOf(0xf00)
if unsafe.Sizeof(vv) == (ptrSize * 4) {
offsetScalar = ptrSize * 2
offsetFlag = ptrSize * 3
}
// Commit 90a7c3c86944 changed the flag positions such that the low
// order bits are the kind. This code extracts the kind from the flags
// field and ensures it's the correct type. When it's not, the flag
// order has been changed to the newer format, so the flags are updated
// accordingly.
upf := unsafe.Pointer(uintptr(unsafe.Pointer(&vv)) + offsetFlag)
upfv := *(*uintptr)(upf)
flagKindMask := uintptr((1<<flagKindWidth - 1) << flagKindShift)
if (upfv&flagKindMask)>>flagKindShift != uintptr(reflect.Int) {
flagKindShift = 0
flagRO = 1 << 5
flagIndir = 1 << 6
// Commit adf9b30e5594 modified the flags to separate the
// flagRO flag into two bits which specifies whether or not the
// field is embedded. This causes flagIndir to move over a bit
// and means that flagRO is the combination of either of the
// original flagRO bit and the new bit.
//
// This code detects the change by extracting what used to be
// the indirect bit to ensure it's set. When it's not, the flag
// order has been changed to the newer format, so the flags are
// updated accordingly.
if upfv&flagIndir == 0 {
flagRO = 3 << 5
flagIndir = 1 << 7
}
}
}
// unsafeReflectValue converts the passed reflect.Value into a one that bypasses
// the typical safety restrictions preventing access to unaddressable and
// unexported data. It works by digging the raw pointer to the underlying
// value out of the protected value and generating a new unprotected (unsafe)
// reflect.Value to it.
//
// This allows us to check for implementations of the Stringer and error
// interfaces to be used for pretty printing ordinarily unaddressable and
// inaccessible values such as unexported struct fields.
func unsafeReflectValue(v reflect.Value) (rv reflect.Value) {
indirects := 1
vt := v.Type()
upv := unsafe.Pointer(uintptr(unsafe.Pointer(&v)) + offsetPtr)
rvf := *(*uintptr)(unsafe.Pointer(uintptr(unsafe.Pointer(&v)) + offsetFlag))
if rvf&flagIndir != 0 {
vt = reflect.PtrTo(v.Type())
indirects++
} else if offsetScalar != 0 {
// The value is in the scalar field when it's not one of the
// reference types.
switch vt.Kind() {
case reflect.Uintptr:
case reflect.Chan:
case reflect.Func:
case reflect.Map:
case reflect.Ptr:
case reflect.UnsafePointer:
default:
upv = unsafe.Pointer(uintptr(unsafe.Pointer(&v)) +
offsetScalar)
}
}
pv := reflect.NewAt(vt, upv)
rv = pv
for i := 0; i < indirects; i++ {
rv = rv.Elem()
}
return rv
}

@ -0,0 +1,38 @@
// Copyright (c) 2015-2016 Dave Collins <dave@davec.name>
//
// Permission to use, copy, modify, and distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
// NOTE: Due to the following build constraints, this file will only be compiled
// when the code is running on Google App Engine, compiled by GopherJS, or
// "-tags safe" is added to the go build command line. The "disableunsafe"
// tag is deprecated and thus should not be used.
// +build js appengine safe disableunsafe
package spew
import "reflect"
const (
// UnsafeDisabled is a build-time constant which specifies whether or
// not access to the unsafe package is available.
UnsafeDisabled = true
)
// unsafeReflectValue typically converts the passed reflect.Value into a one
// that bypasses the typical safety restrictions preventing access to
// unaddressable and unexported data. However, doing this relies on access to
// the unsafe package. This is a stub version which simply returns the passed
// reflect.Value when the unsafe package is not available.
func unsafeReflectValue(v reflect.Value) reflect.Value {
return v
}

@ -0,0 +1,341 @@
/*
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
package spew
import (
"bytes"
"fmt"
"io"
"reflect"
"sort"
"strconv"
)
// Some constants in the form of bytes to avoid string overhead. This mirrors
// the technique used in the fmt package.
var (
panicBytes = []byte("(PANIC=")
plusBytes = []byte("+")
iBytes = []byte("i")
trueBytes = []byte("true")
falseBytes = []byte("false")
interfaceBytes = []byte("(interface {})")
commaNewlineBytes = []byte(",\n")
newlineBytes = []byte("\n")
openBraceBytes = []byte("{")
openBraceNewlineBytes = []byte("{\n")
closeBraceBytes = []byte("}")
asteriskBytes = []byte("*")
colonBytes = []byte(":")
colonSpaceBytes = []byte(": ")
openParenBytes = []byte("(")
closeParenBytes = []byte(")")
spaceBytes = []byte(" ")
pointerChainBytes = []byte("->")
nilAngleBytes = []byte("<nil>")
maxNewlineBytes = []byte("<max depth reached>\n")
maxShortBytes = []byte("<max>")
circularBytes = []byte("<already shown>")
circularShortBytes = []byte("<shown>")
invalidAngleBytes = []byte("<invalid>")
openBracketBytes = []byte("[")
closeBracketBytes = []byte("]")
percentBytes = []byte("%")
precisionBytes = []byte(".")
openAngleBytes = []byte("<")
closeAngleBytes = []byte(">")
openMapBytes = []byte("map[")
closeMapBytes = []byte("]")
lenEqualsBytes = []byte("len=")
capEqualsBytes = []byte("cap=")
)
// hexDigits is used to map a decimal value to a hex digit.
var hexDigits = "0123456789abcdef"
// catchPanic handles any panics that might occur during the handleMethods
// calls.
func catchPanic(w io.Writer, v reflect.Value) {
if err := recover(); err != nil {
w.Write(panicBytes)
fmt.Fprintf(w, "%v", err)
w.Write(closeParenBytes)
}
}
// handleMethods attempts to call the Error and String methods on the underlying
// type the passed reflect.Value represents and outputes the result to Writer w.
//
// It handles panics in any called methods by catching and displaying the error
// as the formatted value.
func handleMethods(cs *ConfigState, w io.Writer, v reflect.Value) (handled bool) {
// We need an interface to check if the type implements the error or
// Stringer interface. However, the reflect package won't give us an
// interface on certain things like unexported struct fields in order
// to enforce visibility rules. We use unsafe, when it's available,
// to bypass these restrictions since this package does not mutate the
// values.
if !v.CanInterface() {
if UnsafeDisabled {
return false
}
v = unsafeReflectValue(v)
}
// Choose whether or not to do error and Stringer interface lookups against
// the base type or a pointer to the base type depending on settings.
// Technically calling one of these methods with a pointer receiver can
// mutate the value, however, types which choose to satisify an error or
// Stringer interface with a pointer receiver should not be mutating their
// state inside these interface methods.
if !cs.DisablePointerMethods && !UnsafeDisabled && !v.CanAddr() {
v = unsafeReflectValue(v)
}
if v.CanAddr() {
v = v.Addr()
}
// Is it an error or Stringer?
switch iface := v.Interface().(type) {
case error:
defer catchPanic(w, v)
if cs.ContinueOnMethod {
w.Write(openParenBytes)
w.Write([]byte(iface.Error()))
w.Write(closeParenBytes)
w.Write(spaceBytes)
return false
}
w.Write([]byte(iface.Error()))
return true
case fmt.Stringer:
defer catchPanic(w, v)
if cs.ContinueOnMethod {
w.Write(openParenBytes)
w.Write([]byte(iface.String()))
w.Write(closeParenBytes)
w.Write(spaceBytes)
return false
}
w.Write([]byte(iface.String()))
return true
}
return false
}
// printBool outputs a boolean value as true or false to Writer w.
func printBool(w io.Writer, val bool) {
if val {
w.Write(trueBytes)
} else {
w.Write(falseBytes)
}
}
// printInt outputs a signed integer value to Writer w.
func printInt(w io.Writer, val int64, base int) {
w.Write([]byte(strconv.FormatInt(val, base)))
}
// printUint outputs an unsigned integer value to Writer w.
func printUint(w io.Writer, val uint64, base int) {
w.Write([]byte(strconv.FormatUint(val, base)))
}
// printFloat outputs a floating point value using the specified precision,
// which is expected to be 32 or 64bit, to Writer w.
func printFloat(w io.Writer, val float64, precision int) {
w.Write([]byte(strconv.FormatFloat(val, 'g', -1, precision)))
}
// printComplex outputs a complex value using the specified float precision
// for the real and imaginary parts to Writer w.
func printComplex(w io.Writer, c complex128, floatPrecision int) {
r := real(c)
w.Write(openParenBytes)
w.Write([]byte(strconv.FormatFloat(r, 'g', -1, floatPrecision)))
i := imag(c)
if i >= 0 {
w.Write(plusBytes)
}
w.Write([]byte(strconv.FormatFloat(i, 'g', -1, floatPrecision)))
w.Write(iBytes)
w.Write(closeParenBytes)
}
// printHexPtr outputs a uintptr formatted as hexadecimal with a leading '0x'
// prefix to Writer w.
func printHexPtr(w io.Writer, p uintptr) {
// Null pointer.
num := uint64(p)
if num == 0 {
w.Write(nilAngleBytes)
return
}
// Max uint64 is 16 bytes in hex + 2 bytes for '0x' prefix
buf := make([]byte, 18)
// It's simpler to construct the hex string right to left.
base := uint64(16)
i := len(buf) - 1
for num >= base {
buf[i] = hexDigits[num%base]
num /= base
i--
}
buf[i] = hexDigits[num]
// Add '0x' prefix.
i--
buf[i] = 'x'
i--
buf[i] = '0'
// Strip unused leading bytes.
buf = buf[i:]
w.Write(buf)
}
// valuesSorter implements sort.Interface to allow a slice of reflect.Value
// elements to be sorted.
type valuesSorter struct {
values []reflect.Value
strings []string // either nil or same len and values
cs *ConfigState
}
// newValuesSorter initializes a valuesSorter instance, which holds a set of
// surrogate keys on which the data should be sorted. It uses flags in
// ConfigState to decide if and how to populate those surrogate keys.
func newValuesSorter(values []reflect.Value, cs *ConfigState) sort.Interface {
vs := &valuesSorter{values: values, cs: cs}
if canSortSimply(vs.values[0].Kind()) {
return vs
}
if !cs.DisableMethods {
vs.strings = make([]string, len(values))
for i := range vs.values {
b := bytes.Buffer{}
if !handleMethods(cs, &b, vs.values[i]) {
vs.strings = nil
break
}
vs.strings[i] = b.String()
}
}
if vs.strings == nil && cs.SpewKeys {
vs.strings = make([]string, len(values))
for i := range vs.values {
vs.strings[i] = Sprintf("%#v", vs.values[i].Interface())
}
}
return vs
}
// canSortSimply tests whether a reflect.Kind is a primitive that can be sorted
// directly, or whether it should be considered for sorting by surrogate keys
// (if the ConfigState allows it).
func canSortSimply(kind reflect.Kind) bool {
// This switch parallels valueSortLess, except for the default case.
switch kind {
case reflect.Bool:
return true
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
return true
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
return true
case reflect.Float32, reflect.Float64:
return true
case reflect.String:
return true
case reflect.Uintptr:
return true
case reflect.Array:
return true
}
return false
}
// Len returns the number of values in the slice. It is part of the
// sort.Interface implementation.
func (s *valuesSorter) Len() int {
return len(s.values)
}
// Swap swaps the values at the passed indices. It is part of the
// sort.Interface implementation.
func (s *valuesSorter) Swap(i, j int) {
s.values[i], s.values[j] = s.values[j], s.values[i]
if s.strings != nil {
s.strings[i], s.strings[j] = s.strings[j], s.strings[i]
}
}
// valueSortLess returns whether the first value should sort before the second
// value. It is used by valueSorter.Less as part of the sort.Interface
// implementation.
func valueSortLess(a, b reflect.Value) bool {
switch a.Kind() {
case reflect.Bool:
return !a.Bool() && b.Bool()
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
return a.Int() < b.Int()
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
return a.Uint() < b.Uint()
case reflect.Float32, reflect.Float64:
return a.Float() < b.Float()
case reflect.String:
return a.String() < b.String()
case reflect.Uintptr:
return a.Uint() < b.Uint()
case reflect.Array:
// Compare the contents of both arrays.
l := a.Len()
for i := 0; i < l; i++ {
av := a.Index(i)
bv := b.Index(i)
if av.Interface() == bv.Interface() {
continue
}
return valueSortLess(av, bv)
}
}
return a.String() < b.String()
}
// Less returns whether the value at index i should sort before the
// value at index j. It is part of the sort.Interface implementation.
func (s *valuesSorter) Less(i, j int) bool {
if s.strings == nil {
return valueSortLess(s.values[i], s.values[j])
}
return s.strings[i] < s.strings[j]
}
// sortValues is a sort function that handles both native types and any type that
// can be converted to error or Stringer. Other inputs are sorted according to
// their Value.String() value to ensure display stability.
func sortValues(values []reflect.Value, cs *ConfigState) {
if len(values) == 0 {
return
}
sort.Sort(newValuesSorter(values, cs))
}

@ -0,0 +1,306 @@
/*
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
package spew
import (
"bytes"
"fmt"
"io"
"os"
)
// ConfigState houses the configuration options used by spew to format and
// display values. There is a global instance, Config, that is used to control
// all top-level Formatter and Dump functionality. Each ConfigState instance
// provides methods equivalent to the top-level functions.
//
// The zero value for ConfigState provides no indentation. You would typically
// want to set it to a space or a tab.
//
// Alternatively, you can use NewDefaultConfig to get a ConfigState instance
// with default settings. See the documentation of NewDefaultConfig for default
// values.
type ConfigState struct {
// Indent specifies the string to use for each indentation level. The
// global config instance that all top-level functions use set this to a
// single space by default. If you would like more indentation, you might
// set this to a tab with "\t" or perhaps two spaces with " ".
Indent string
// MaxDepth controls the maximum number of levels to descend into nested
// data structures. The default, 0, means there is no limit.
//
// NOTE: Circular data structures are properly detected, so it is not
// necessary to set this value unless you specifically want to limit deeply
// nested data structures.
MaxDepth int
// DisableMethods specifies whether or not error and Stringer interfaces are
// invoked for types that implement them.
DisableMethods bool
// DisablePointerMethods specifies whether or not to check for and invoke
// error and Stringer interfaces on types which only accept a pointer
// receiver when the current type is not a pointer.
//
// NOTE: This might be an unsafe action since calling one of these methods
// with a pointer receiver could technically mutate the value, however,
// in practice, types which choose to satisify an error or Stringer
// interface with a pointer receiver should not be mutating their state
// inside these interface methods. As a result, this option relies on
// access to the unsafe package, so it will not have any effect when
// running in environments without access to the unsafe package such as
// Google App Engine or with the "safe" build tag specified.
DisablePointerMethods bool
// DisablePointerAddresses specifies whether to disable the printing of
// pointer addresses. This is useful when diffing data structures in tests.
DisablePointerAddresses bool
// DisableCapacities specifies whether to disable the printing of capacities
// for arrays, slices, maps and channels. This is useful when diffing
// data structures in tests.
DisableCapacities bool
// ContinueOnMethod specifies whether or not recursion should continue once
// a custom error or Stringer interface is invoked. The default, false,
// means it will print the results of invoking the custom error or Stringer
// interface and return immediately instead of continuing to recurse into
// the internals of the data type.
//
// NOTE: This flag does not have any effect if method invocation is disabled
// via the DisableMethods or DisablePointerMethods options.
ContinueOnMethod bool
// SortKeys specifies map keys should be sorted before being printed. Use
// this to have a more deterministic, diffable output. Note that only
// native types (bool, int, uint, floats, uintptr and string) and types
// that support the error or Stringer interfaces (if methods are
// enabled) are supported, with other types sorted according to the
// reflect.Value.String() output which guarantees display stability.
SortKeys bool
// SpewKeys specifies that, as a last resort attempt, map keys should
// be spewed to strings and sorted by those strings. This is only
// considered if SortKeys is true.
SpewKeys bool
}
// Config is the active configuration of the top-level functions.
// The configuration can be changed by modifying the contents of spew.Config.
var Config = ConfigState{Indent: " "}
// Errorf is a wrapper for fmt.Errorf that treats each argument as if it were
// passed with a Formatter interface returned by c.NewFormatter. It returns
// the formatted string as a value that satisfies error. See NewFormatter
// for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Errorf(format, c.NewFormatter(a), c.NewFormatter(b))
func (c *ConfigState) Errorf(format string, a ...interface{}) (err error) {
return fmt.Errorf(format, c.convertArgs(a)...)
}
// Fprint is a wrapper for fmt.Fprint that treats each argument as if it were
// passed with a Formatter interface returned by c.NewFormatter. It returns
// the number of bytes written and any write error encountered. See
// NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Fprint(w, c.NewFormatter(a), c.NewFormatter(b))
func (c *ConfigState) Fprint(w io.Writer, a ...interface{}) (n int, err error) {
return fmt.Fprint(w, c.convertArgs(a)...)
}
// Fprintf is a wrapper for fmt.Fprintf that treats each argument as if it were
// passed with a Formatter interface returned by c.NewFormatter. It returns
// the number of bytes written and any write error encountered. See
// NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Fprintf(w, format, c.NewFormatter(a), c.NewFormatter(b))
func (c *ConfigState) Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
return fmt.Fprintf(w, format, c.convertArgs(a)...)
}
// Fprintln is a wrapper for fmt.Fprintln that treats each argument as if it
// passed with a Formatter interface returned by c.NewFormatter. See
// NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Fprintln(w, c.NewFormatter(a), c.NewFormatter(b))
func (c *ConfigState) Fprintln(w io.Writer, a ...interface{}) (n int, err error) {
return fmt.Fprintln(w, c.convertArgs(a)...)
}
// Print is a wrapper for fmt.Print that treats each argument as if it were
// passed with a Formatter interface returned by c.NewFormatter. It returns
// the number of bytes written and any write error encountered. See
// NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Print(c.NewFormatter(a), c.NewFormatter(b))
func (c *ConfigState) Print(a ...interface{}) (n int, err error) {
return fmt.Print(c.convertArgs(a)...)
}
// Printf is a wrapper for fmt.Printf that treats each argument as if it were
// passed with a Formatter interface returned by c.NewFormatter. It returns
// the number of bytes written and any write error encountered. See
// NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Printf(format, c.NewFormatter(a), c.NewFormatter(b))
func (c *ConfigState) Printf(format string, a ...interface{}) (n int, err error) {
return fmt.Printf(format, c.convertArgs(a)...)
}
// Println is a wrapper for fmt.Println that treats each argument as if it were
// passed with a Formatter interface returned by c.NewFormatter. It returns
// the number of bytes written and any write error encountered. See
// NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Println(c.NewFormatter(a), c.NewFormatter(b))
func (c *ConfigState) Println(a ...interface{}) (n int, err error) {
return fmt.Println(c.convertArgs(a)...)
}
// Sprint is a wrapper for fmt.Sprint that treats each argument as if it were
// passed with a Formatter interface returned by c.NewFormatter. It returns
// the resulting string. See NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Sprint(c.NewFormatter(a), c.NewFormatter(b))
func (c *ConfigState) Sprint(a ...interface{}) string {
return fmt.Sprint(c.convertArgs(a)...)
}
// Sprintf is a wrapper for fmt.Sprintf that treats each argument as if it were
// passed with a Formatter interface returned by c.NewFormatter. It returns
// the resulting string. See NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Sprintf(format, c.NewFormatter(a), c.NewFormatter(b))
func (c *ConfigState) Sprintf(format string, a ...interface{}) string {
return fmt.Sprintf(format, c.convertArgs(a)...)
}
// Sprintln is a wrapper for fmt.Sprintln that treats each argument as if it
// were passed with a Formatter interface returned by c.NewFormatter. It
// returns the resulting string. See NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Sprintln(c.NewFormatter(a), c.NewFormatter(b))
func (c *ConfigState) Sprintln(a ...interface{}) string {
return fmt.Sprintln(c.convertArgs(a)...)
}
/*
NewFormatter returns a custom formatter that satisfies the fmt.Formatter
interface. As a result, it integrates cleanly with standard fmt package
printing functions. The formatter is useful for inline printing of smaller data
types similar to the standard %v format specifier.
The custom formatter only responds to the %v (most compact), %+v (adds pointer
addresses), %#v (adds types), and %#+v (adds types and pointer addresses) verb
combinations. Any other verbs such as %x and %q will be sent to the the
standard fmt package for formatting. In addition, the custom formatter ignores
the width and precision arguments (however they will still work on the format
specifiers not handled by the custom formatter).
Typically this function shouldn't be called directly. It is much easier to make
use of the custom formatter by calling one of the convenience functions such as
c.Printf, c.Println, or c.Printf.
*/
func (c *ConfigState) NewFormatter(v interface{}) fmt.Formatter {
return newFormatter(c, v)
}
// Fdump formats and displays the passed arguments to io.Writer w. It formats
// exactly the same as Dump.
func (c *ConfigState) Fdump(w io.Writer, a ...interface{}) {
fdump(c, w, a...)
}
/*
Dump displays the passed parameters to standard out with newlines, customizable
indentation, and additional debug information such as complete types and all
pointer addresses used to indirect to the final value. It provides the
following features over the built-in printing facilities provided by the fmt
package:
* Pointers are dereferenced and followed
* Circular data structures are detected and handled properly
* Custom Stringer/error interfaces are optionally invoked, including
on unexported types
* Custom types which only implement the Stringer/error interfaces via
a pointer receiver are optionally invoked when passing non-pointer
variables
* Byte arrays and slices are dumped like the hexdump -C command which
includes offsets, byte values in hex, and ASCII output
The configuration options are controlled by modifying the public members
of c. See ConfigState for options documentation.
See Fdump if you would prefer dumping to an arbitrary io.Writer or Sdump to
get the formatted result as a string.
*/
func (c *ConfigState) Dump(a ...interface{}) {
fdump(c, os.Stdout, a...)
}
// Sdump returns a string with the passed arguments formatted exactly the same
// as Dump.
func (c *ConfigState) Sdump(a ...interface{}) string {
var buf bytes.Buffer
fdump(c, &buf, a...)
return buf.String()
}
// convertArgs accepts a slice of arguments and returns a slice of the same
// length with each argument converted to a spew Formatter interface using
// the ConfigState associated with s.
func (c *ConfigState) convertArgs(args []interface{}) (formatters []interface{}) {
formatters = make([]interface{}, len(args))
for index, arg := range args {
formatters[index] = newFormatter(c, arg)
}
return formatters
}
// NewDefaultConfig returns a ConfigState with the following default settings.
//
// Indent: " "
// MaxDepth: 0
// DisableMethods: false
// DisablePointerMethods: false
// ContinueOnMethod: false
// SortKeys: false
func NewDefaultConfig() *ConfigState {
return &ConfigState{Indent: " "}
}