Commit 1fcb56f4 authored by Nick Thomas's avatar Nick Thomas

Merge branch 'bvl-feature-flag-commands' into 'master'

Parse commands to enable feature flags

See merge request gitlab-org/gitlab-shell!270
parents 6c5b1953 d762f4ec
...@@ -4,8 +4,9 @@ import ( ...@@ -4,8 +4,9 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"syscall"
"gitlab.com/gitlab-org/gitlab-shell/go/internal/command"
"gitlab.com/gitlab-org/gitlab-shell/go/internal/command/fallback"
"gitlab.com/gitlab-org/gitlab-shell/go/internal/config" "gitlab.com/gitlab-org/gitlab-shell/go/internal/config"
) )
...@@ -19,20 +20,12 @@ func init() { ...@@ -19,20 +20,12 @@ func init() {
rootDir = filepath.Dir(binDir) rootDir = filepath.Dir(binDir)
} }
func migrate(*config.Config) (int, bool) {
// TODO: Dispatch appropriate requests to Go handlers and return
// <exitstatus, true> depending on how they fare
return 0, false
}
// rubyExec will never return. It either replaces the current process with a // rubyExec will never return. It either replaces the current process with a
// Ruby interpreter, or outputs an error and kills the process. // Ruby interpreter, or outputs an error and kills the process.
func execRuby() { func execRuby() {
rubyCmd := filepath.Join(binDir, "gitlab-shell-ruby") cmd := &fallback.Command{}
if err := cmd.Execute(); err != nil {
execErr := syscall.Exec(rubyCmd, os.Args, os.Environ()) fmt.Fprintf(os.Stderr, "Failed to exec: %v\n", err)
if execErr != nil {
fmt.Fprintf(os.Stderr, "Failed to exec(%q): %v\n", rubyCmd, execErr)
os.Exit(1) os.Exit(1)
} }
} }
...@@ -46,11 +39,18 @@ func main() { ...@@ -46,11 +39,18 @@ func main() {
execRuby() execRuby()
} }
// Try to handle the command with the Go implementation cmd, err := command.New(os.Args, config)
if exitCode, done := migrate(config); done { if err != nil {
os.Exit(exitCode) // For now this could happen if `SSH_CONNECTION` is not set on
// the environment
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
} }
// Since a migration has not handled the command, fall back to Ruby to do so // The command will write to STDOUT on execution or replace the current
execRuby() // process in case of the `fallback.Command`
if err = cmd.Execute(); err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
} }
package command
import (
"gitlab.com/gitlab-org/gitlab-shell/go/internal/command/commandargs"
"gitlab.com/gitlab-org/gitlab-shell/go/internal/command/discover"
"gitlab.com/gitlab-org/gitlab-shell/go/internal/command/fallback"
"gitlab.com/gitlab-org/gitlab-shell/go/internal/config"
)
type Command interface {
Execute() error
}
func New(arguments []string, config *config.Config) (Command, error) {
args, err := commandargs.Parse(arguments)
if err != nil {
return nil, err
}
if config.FeatureEnabled(string(args.CommandType)) {
return buildCommand(args, config), nil
}
return &fallback.Command{}, nil
}
func buildCommand(args *commandargs.CommandArgs, config *config.Config) Command {
switch args.CommandType {
case commandargs.Discover:
return &discover.Command{Config: config, Args: args}
}
return nil
}
package command
import (
"testing"
"github.com/stretchr/testify/assert"
"gitlab.com/gitlab-org/gitlab-shell/go/internal/command/discover"
"gitlab.com/gitlab-org/gitlab-shell/go/internal/command/fallback"
"gitlab.com/gitlab-org/gitlab-shell/go/internal/config"
"gitlab.com/gitlab-org/gitlab-shell/go/internal/testhelper"
)
func TestNew(t *testing.T) {
testCases := []struct {
desc string
arguments []string
config *config.Config
environment map[string]string
expectedType interface{}
}{
{
desc: "it returns a Discover command if the feature is enabled",
arguments: []string{},
config: &config.Config{
GitlabUrl: "http+unix://gitlab.socket",
Migration: config.MigrationConfig{Enabled: true, Features: []string{"discover"}},
},
environment: map[string]string{
"SSH_CONNECTION": "1",
"SSH_ORIGINAL_COMMAND": "",
},
expectedType: &discover.Command{},
},
{
desc: "it returns a Fallback command no feature is enabled",
arguments: []string{},
config: &config.Config{
GitlabUrl: "http+unix://gitlab.socket",
Migration: config.MigrationConfig{Enabled: false},
},
environment: map[string]string{
"SSH_CONNECTION": "1",
"SSH_ORIGINAL_COMMAND": "",
},
expectedType: &fallback.Command{},
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
restoreEnv := testhelper.TempEnv(tc.environment)
defer restoreEnv()
command, err := New(tc.arguments, tc.config)
assert.NoError(t, err)
assert.IsType(t, tc.expectedType, command)
})
}
}
func TestFailingNew(t *testing.T) {
t.Run("It returns an error when SSH_CONNECTION is not set", func(t *testing.T) {
restoreEnv := testhelper.TempEnv(map[string]string{})
defer restoreEnv()
_, err := New([]string{}, &config.Config{})
assert.Error(t, err, "Only ssh allowed")
})
}
package commandargs
import (
"errors"
"os"
"regexp"
)
type CommandType string
const (
Discover CommandType = "discover"
)
var (
whoKeyRegex = regexp.MustCompile(`\bkey-(?P<keyid>\d+)\b`)
whoUsernameRegex = regexp.MustCompile(`\busername-(?P<username>\S+)\b`)
)
type CommandArgs struct {
GitlabUsername string
GitlabKeyId string
SshCommand string
CommandType CommandType
}
func Parse(arguments []string) (*CommandArgs, error) {
if sshConnection := os.Getenv("SSH_CONNECTION"); sshConnection == "" {
return nil, errors.New("Only ssh allowed")
}
info := &CommandArgs{}
info.parseWho(arguments)
info.parseCommand(os.Getenv("SSH_ORIGINAL_COMMAND"))
return info, nil
}
func (c *CommandArgs) parseWho(arguments []string) {
for _, argument := range arguments {
if keyId := tryParseKeyId(argument); keyId != "" {
c.GitlabKeyId = keyId
break
}
if username := tryParseUsername(argument); username != "" {
c.GitlabUsername = username
break
}
}
}
func tryParseKeyId(argument string) string {
matchInfo := whoKeyRegex.FindStringSubmatch(argument)
if len(matchInfo) == 2 {
// The first element is the full matched string
// The second element is the named `keyid`
return matchInfo[1]
}
return ""
}
func tryParseUsername(argument string) string {
matchInfo := whoUsernameRegex.FindStringSubmatch(argument)
if len(matchInfo) == 2 {
// The first element is the full matched string
// The second element is the named `username`
return matchInfo[1]
}
return ""
}
func (c *CommandArgs) parseCommand(commandString string) {
c.SshCommand = commandString
if commandString == "" {
c.CommandType = Discover
}
}
package commandargs
import (
"testing"
"github.com/stretchr/testify/assert"
"gitlab.com/gitlab-org/gitlab-shell/go/internal/testhelper"
)
func TestParseSuccess(t *testing.T) {
testCases := []struct {
desc string
arguments []string
environment map[string]string
expectedArgs *CommandArgs
}{
// Setting the used env variables for every case to ensure we're
// not using anything set in the original env.
{
desc: "It sets discover as the command when the command string was empty",
environment: map[string]string{
"SSH_CONNECTION": "1",
"SSH_ORIGINAL_COMMAND": "",
},
expectedArgs: &CommandArgs{CommandType: Discover},
},
{
desc: "It passes on the original ssh command from the environment",
environment: map[string]string{
"SSH_CONNECTION": "1",
"SSH_ORIGINAL_COMMAND": "hello world",
},
expectedArgs: &CommandArgs{SshCommand: "hello world"},
}, {
desc: "It finds the key id in any passed arguments",
environment: map[string]string{
"SSH_CONNECTION": "1",
"SSH_ORIGINAL_COMMAND": "",
},
arguments: []string{"hello", "key-123"},
expectedArgs: &CommandArgs{CommandType: Discover, GitlabKeyId: "123"},
}, {
desc: "It finds the username in any passed arguments",
environment: map[string]string{
"SSH_CONNECTION": "1",
"SSH_ORIGINAL_COMMAND": "",
},
arguments: []string{"hello", "username-jane-doe"},
expectedArgs: &CommandArgs{CommandType: Discover, GitlabUsername: "jane-doe"},
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
restoreEnv := testhelper.TempEnv(tc.environment)
defer restoreEnv()
result, err := Parse(tc.arguments)
assert.NoError(t, err)
assert.Equal(t, tc.expectedArgs, result)
})
}
}
func TestParseFailure(t *testing.T) {
t.Run("It fails if SSH connection is not set", func(t *testing.T) {
_, err := Parse([]string{})
assert.Error(t, err, "Only ssh allowed")
})
}
package discover
import (
"fmt"
"gitlab.com/gitlab-org/gitlab-shell/go/internal/command/commandargs"
"gitlab.com/gitlab-org/gitlab-shell/go/internal/config"
)
type Command struct {
Config *config.Config
Args *commandargs.CommandArgs
}
func (c *Command) Execute() error {
return fmt.Errorf("No feature is implemented yet")
}
package fallback
import (
"os"
"path/filepath"
"syscall"
)
type Command struct{}
var (
binDir = filepath.Dir(os.Args[0])
)
func (c *Command) Execute() error {
rubyCmd := filepath.Join(binDir, "gitlab-shell-ruby")
execErr := syscall.Exec(rubyCmd, os.Args, os.Environ())
return execErr
}
...@@ -2,8 +2,10 @@ package config ...@@ -2,8 +2,10 @@ package config
import ( import (
"io/ioutil" "io/ioutil"
"net/url"
"os" "os"
"path" "path"
"strings"
yaml "gopkg.in/yaml.v2" yaml "gopkg.in/yaml.v2"
) )
...@@ -23,6 +25,7 @@ type Config struct { ...@@ -23,6 +25,7 @@ type Config struct {
LogFile string `yaml:"log_file"` LogFile string `yaml:"log_file"`
LogFormat string `yaml:"log_format"` LogFormat string `yaml:"log_format"`
Migration MigrationConfig `yaml:"migration"` Migration MigrationConfig `yaml:"migration"`
GitlabUrl string `yaml:"gitlab_url"`
} }
func New() (*Config, error) { func New() (*Config, error) {
...@@ -38,6 +41,24 @@ func NewFromDir(dir string) (*Config, error) { ...@@ -38,6 +41,24 @@ func NewFromDir(dir string) (*Config, error) {
return newFromFile(path.Join(dir, configFile)) return newFromFile(path.Join(dir, configFile))
} }
func (c *Config) FeatureEnabled(featureName string) bool {
if !c.Migration.Enabled {
return false
}
if !strings.HasPrefix(c.GitlabUrl, "http+unix://") {
return false
}
for _, enabledFeature := range c.Migration.Features {
if enabledFeature == featureName {
return true
}
}
return false
}
func newFromFile(filename string) (*Config, error) { func newFromFile(filename string) (*Config, error) {
cfg := &Config{RootDir: path.Dir(filename)} cfg := &Config{RootDir: path.Dir(filename)}
...@@ -71,5 +92,14 @@ func parseConfig(configBytes []byte, cfg *Config) error { ...@@ -71,5 +92,14 @@ func parseConfig(configBytes []byte, cfg *Config) error {
cfg.LogFormat = "text" cfg.LogFormat = "text"
} }
if cfg.GitlabUrl != "" {
unescapedUrl, err := url.PathUnescape(cfg.GitlabUrl)
if err != nil {
return err
}
cfg.GitlabUrl = unescapedUrl
}
return nil return nil
} }
...@@ -4,6 +4,8 @@ import ( ...@@ -4,6 +4,8 @@ import (
"fmt" "fmt"
"strings" "strings"
"testing" "testing"
"github.com/stretchr/testify/assert"
) )
func TestParseConfig(t *testing.T) { func TestParseConfig(t *testing.T) {
...@@ -12,6 +14,7 @@ func TestParseConfig(t *testing.T) { ...@@ -12,6 +14,7 @@ func TestParseConfig(t *testing.T) {
yaml string yaml string
path string path string
format string format string
gitlabUrl string
migration MigrationConfig migration MigrationConfig
}{ }{
{path: "/foo/bar/gitlab-shell.log", format: "text"}, {path: "/foo/bar/gitlab-shell.log", format: "text"},
...@@ -24,6 +27,12 @@ func TestParseConfig(t *testing.T) { ...@@ -24,6 +27,12 @@ func TestParseConfig(t *testing.T) {
format: "text", format: "text",
migration: MigrationConfig{Enabled: true, Features: []string{"foo", "bar"}}, migration: MigrationConfig{Enabled: true, Features: []string{"foo", "bar"}},
}, },
{
yaml: "gitlab_url: http+unix://%2Fpath%2Fto%2Fgitlab%2Fgitlab.socket",
path: "/foo/bar/gitlab-shell.log",
format: "text",
gitlabUrl: "http+unix:///path/to/gitlab/gitlab.socket",
},
} }
for _, tc := range testCases { for _, tc := range testCases {
...@@ -48,6 +57,60 @@ func TestParseConfig(t *testing.T) { ...@@ -48,6 +57,60 @@ func TestParseConfig(t *testing.T) {
if cfg.LogFormat != tc.format { if cfg.LogFormat != tc.format {
t.Fatalf("expected %q, got %q", tc.format, cfg.LogFormat) t.Fatalf("expected %q, got %q", tc.format, cfg.LogFormat)
} }
assert.Equal(t, tc.gitlabUrl, cfg.GitlabUrl)
})
}
}
func TestFeatureEnabled(t *testing.T) {
testCases := []struct {
desc string
config *Config
feature string
expectEnabled bool
}{
{
desc: "When the protocol is supported and the feature enabled",
config: &Config{
GitlabUrl: "http+unix://gitlab.socket",
Migration: MigrationConfig{Enabled: true, Features: []string{"discover"}},
},
feature: "discover",
expectEnabled: true,
},
{
desc: "When the protocol is supported and the feature is not enabled",
config: &Config{
GitlabUrl: "http+unix://gitlab.socket",
Migration: MigrationConfig{Enabled: true, Features: []string{}},
},
feature: "discover",
expectEnabled: false,
},
{
desc: "When the protocol is supported and all features are disabled",
config: &Config{
GitlabUrl: "http+unix://gitlab.socket",
Migration: MigrationConfig{Enabled: false, Features: []string{"discover"}},
},
feature: "discover",
expectEnabled: false,
},
{
desc: "When the protocol is not supported",
config: &Config{
GitlabUrl: "https://localhost:3000",
Migration: MigrationConfig{Enabled: true, Features: []string{"discover"}},
},
feature: "discover",
expectEnabled: false,
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
assert.Equal(t, tc.expectEnabled, tc.config.FeatureEnabled(string(tc.feature)))
}) })
} }
} }
package testhelper
import "os"
func TempEnv(env map[string]string) func() {
var original = make(map[string]string)
for key, value := range env {
original[key] = os.Getenv(key)
os.Setenv(key, value)
}
return func() {
for key, originalValue := range original {
os.Setenv(key, originalValue)
}
}
}
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.
// 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.
// Go versions prior to 1.4 are disabled because they use a different layout
// for interfaces which make the implementation of unsafeReflectValue more complex.
// +build !js,!appengine,!safe,!disableunsafe,go1.4
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))
)
type flag uintptr
var (
// flagRO indicates whether the value field of a reflect.Value
// is read-only.
flagRO flag
// flagAddr indicates whether the address of the reflect.Value's
// value may be taken.
flagAddr flag
)
// flagKindMask holds the bits that make up the kind
// part of the flags field. In all the supported versions,
// it is in the lower 5 bits.
const flagKindMask = flag(0x1f)
// Different versions of Go have used different
// bit layouts for the flags type. This table
// records the known combinations.
var okFlags = []struct {
ro, addr flag
}{{
// From Go 1.4 to 1.5
ro: 1 << 5,
addr: 1 << 7,
}, {
// Up to Go tip.
ro: 1<<5 | 1<<6,
addr: 1 << 8,
}}
var flagValOffset = func() uintptr {
field, ok := reflect.TypeOf(reflect.Value{}).FieldByName("flag")
if !ok {
panic("reflect.Value has no flag field")
}
return field.Offset
}()
// flagField returns a pointer to the flag field of a reflect.Value.
func flagField(v *reflect.Value) *flag {
return (*flag)(unsafe.Pointer(uintptr(unsafe.Pointer(v)) + flagValOffset))
}
// 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) reflect.Value {
if !v.IsValid() || (v.CanInterface() && v.CanAddr()) {
return v
}
flagFieldPtr := flagField(&v)
*flagFieldPtr &^= flagRO
*flagFieldPtr |= flagAddr
return v
}
// Sanity checks against future reflect package changes
// to the type or semantics of the Value.flag field.
func init() {
field, ok := reflect.TypeOf(reflect.Value{}).FieldByName("flag")
if !ok {
panic("reflect.Value has no flag field")
}
if field.Type.Kind() != reflect.TypeOf(flag(0)).Kind() {
panic("reflect.Value flag field has changed kind")
}
type t0 int
var t struct {
A t0
// t0 will have flagEmbedRO set.
t0
// a will have flagStickyRO set
a t0
}
vA := reflect.ValueOf(t).FieldByName("A")
va := reflect.ValueOf(t).FieldByName("a")
vt0 := reflect.ValueOf(t).FieldByName("t0")
// Infer flagRO from the difference between the flags
// for the (otherwise identical) fields in t.
flagPublic := *flagField(&vA)
flagWithRO := *flagField(&va) | *flagField(&vt0)
flagRO = flagPublic ^ flagWithRO
// Infer flagAddr from the difference between a value
// taken from a pointer and not.
vPtrA := reflect.ValueOf(&t).Elem().FieldByName("A")
flagNoPtr := *flagField(&vA)
flagPtr := *flagField(&vPtrA)
flagAddr = flagNoPtr ^ flagPtr
// Check that the inferred flags tally with one of the known versions.
for _, f := range okFlags {
if flagRO == f.ro && flagAddr == f.addr {
return
}
}
panic("reflect.Value read-only flag has changed semantics")
}
// 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 !go1.4
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
}
This diff is collapsed.
This diff is collapsed.
/*
* 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 implements a deep pretty printer for Go data structures to aid in
debugging.
A quick overview of the additional features spew provides over the built-in
printing facilities for Go data types are as follows:
* 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 (only when using
Dump style)
There are two different approaches spew allows for dumping Go data structures:
* Dump style which prints with newlines, customizable indentation,
and additional debug information such as types and all pointer addresses
used to indirect to the final value
* A custom Formatter interface that integrates cleanly with the standard fmt
package and replaces %v, %+v, %#v, and %#+v to provide inline printing
similar to the default %v while providing the additional functionality
outlined above and passing unsupported format verbs such as %x and %q
along to fmt
Quick Start
This section demonstrates how to quickly get started with spew. See the
sections below for further details on formatting and configuration options.
To dump a variable with full newlines, indentation, type, and pointer
information use Dump, Fdump, or Sdump:
spew.Dump(myVar1, myVar2, ...)
spew.Fdump(someWriter, myVar1, myVar2, ...)
str := spew.Sdump(myVar1, myVar2, ...)
Alternatively, if you would prefer to use format strings with a compacted inline
printing style, use the convenience wrappers Printf, Fprintf, etc with
%v (most compact), %+v (adds pointer addresses), %#v (adds types), or
%#+v (adds types and pointer addresses):
spew.Printf("myVar1: %v -- myVar2: %+v", myVar1, myVar2)
spew.Printf("myVar3: %#v -- myVar4: %#+v", myVar3, myVar4)
spew.Fprintf(someWriter, "myVar1: %v -- myVar2: %+v", myVar1, myVar2)
spew.Fprintf(someWriter, "myVar3: %#v -- myVar4: %#+v", myVar3, myVar4)
Configuration Options
Configuration of spew is handled by fields in the ConfigState type. For
convenience, all of the top-level functions use a global state available
via the spew.Config global.
It is also possible to create a ConfigState instance that provides methods
equivalent to the top-level functions. This allows concurrent configuration
options. See the ConfigState documentation for more details.
The following configuration options are available:
* Indent
String to use for each indentation level for Dump functions.
It is a single space by default. A popular alternative is "\t".
* MaxDepth
Maximum number of levels to descend into nested data structures.
There is no limit by default.
* DisableMethods
Disables invocation of error and Stringer interface methods.
Method invocation is enabled by default.
* DisablePointerMethods
Disables invocation of error and Stringer interface methods on types
which only accept pointer receivers from non-pointer variables.
Pointer method invocation is enabled by default.
* DisablePointerAddresses
DisablePointerAddresses specifies whether to disable the printing of
pointer addresses. This is useful when diffing data structures in tests.
* DisableCapacities
DisableCapacities specifies whether to disable the printing of
capacities for arrays, slices, maps and channels. This is useful when
diffing data structures in tests.
* ContinueOnMethod
Enables recursion into types after invoking error and Stringer interface
methods. Recursion after method invocation is disabled by default.
* 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 which implement error or Stringer interfaces are
supported with other types sorted according to the
reflect.Value.String() output which guarantees display
stability. Natural map order is used by default.
* 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.
Dump Usage
Simply call spew.Dump with a list of variables you want to dump:
spew.Dump(myVar1, myVar2, ...)
You may also call spew.Fdump if you would prefer to output to an arbitrary
io.Writer. For example, to dump to standard error:
spew.Fdump(os.Stderr, myVar1, myVar2, ...)
A third option is to call spew.Sdump to get the formatted output as a string:
str := spew.Sdump(myVar1, myVar2, ...)
Sample Dump Output
See the Dump example for details on the setup of the types and variables being
shown here.
(main.Foo) {
unexportedField: (*main.Bar)(0xf84002e210)({
flag: (main.Flag) flagTwo,
data: (uintptr) <nil>
}),
ExportedField: (map[interface {}]interface {}) (len=1) {
(string) (len=3) "one": (bool) true
}
}
Byte (and uint8) arrays and slices are displayed uniquely like the hexdump -C
command as shown.
([]uint8) (len=32 cap=32) {
00000000 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 |............... |
00000010 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 |!"#$%&'()*+,-./0|
00000020 31 32 |12|
}
Custom Formatter
Spew provides a custom formatter that implements the fmt.Formatter interface
so that 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), or %#+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).
Custom Formatter Usage
The simplest way to make use of the spew custom formatter is to call one of the
convenience functions such as spew.Printf, spew.Println, or spew.Printf. The
functions have syntax you are most likely already familiar with:
spew.Printf("myVar1: %v -- myVar2: %+v", myVar1, myVar2)
spew.Printf("myVar3: %#v -- myVar4: %#+v", myVar3, myVar4)
spew.Println(myVar, myVar2)
spew.Fprintf(os.Stderr, "myVar1: %v -- myVar2: %+v", myVar1, myVar2)
spew.Fprintf(os.Stderr, "myVar3: %#v -- myVar4: %#+v", myVar3, myVar4)
See the Index for the full list convenience functions.
Sample Formatter Output
Double pointer to a uint8:
%v: <**>5
%+v: <**>(0xf8400420d0->0xf8400420c8)5
%#v: (**uint8)5
%#+v: (**uint8)(0xf8400420d0->0xf8400420c8)5
Pointer to circular struct with a uint8 field and a pointer to itself:
%v: <*>{1 <*><shown>}
%+v: <*>(0xf84003e260){ui8:1 c:<*>(0xf84003e260)<shown>}
%#v: (*main.circular){ui8:(uint8)1 c:(*main.circular)<shown>}
%#+v: (*main.circular)(0xf84003e260){ui8:(uint8)1 c:(*main.circular)(0xf84003e260)<shown>}
See the Printf example for details on the setup of variables being shown
here.
Errors
Since it is possible for custom Stringer/error interfaces to panic, spew
detects them and handles them internally by printing the panic information
inline with the output. Since spew is intended to provide deep pretty printing
capabilities on structures, it intentionally does not return any errors.
*/
package spew
This diff is collapsed.
This diff is collapsed.
/*
* 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 (
"fmt"
"io"
)
// Errorf is a wrapper for fmt.Errorf that treats each argument as if it were
// passed with a default Formatter interface returned by 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, spew.NewFormatter(a), spew.NewFormatter(b))
func Errorf(format string, a ...interface{}) (err error) {
return fmt.Errorf(format, convertArgs(a)...)
}
// Fprint is a wrapper for fmt.Fprint that treats each argument as if it were
// passed with a default Formatter interface returned by 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, spew.NewFormatter(a), spew.NewFormatter(b))
func Fprint(w io.Writer, a ...interface{}) (n int, err error) {
return fmt.Fprint(w, convertArgs(a)...)
}
// Fprintf is a wrapper for fmt.Fprintf that treats each argument as if it were
// passed with a default Formatter interface returned by 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, spew.NewFormatter(a), spew.NewFormatter(b))
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
return fmt.Fprintf(w, format, convertArgs(a)...)
}
// Fprintln is a wrapper for fmt.Fprintln that treats each argument as if it
// passed with a default Formatter interface returned by NewFormatter. See
// NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Fprintln(w, spew.NewFormatter(a), spew.NewFormatter(b))
func Fprintln(w io.Writer, a ...interface{}) (n int, err error) {
return fmt.Fprintln(w, convertArgs(a)...)
}
// Print is a wrapper for fmt.Print that treats each argument as if it were
// passed with a default Formatter interface returned by 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(spew.NewFormatter(a), spew.NewFormatter(b))
func Print(a ...interface{}) (n int, err error) {
return fmt.Print(convertArgs(a)...)
}
// Printf is a wrapper for fmt.Printf that treats each argument as if it were
// passed with a default Formatter interface returned by 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, spew.NewFormatter(a), spew.NewFormatter(b))
func Printf(format string, a ...interface{}) (n int, err error) {
return fmt.Printf(format, convertArgs(a)...)
}
// Println is a wrapper for fmt.Println that treats each argument as if it were
// passed with a default Formatter interface returned by 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(spew.NewFormatter(a), spew.NewFormatter(b))
func Println(a ...interface{}) (n int, err error) {
return fmt.Println(convertArgs(a)...)
}
// Sprint is a wrapper for fmt.Sprint that treats each argument as if it were
// passed with a default Formatter interface returned by NewFormatter. It
// returns the resulting string. See NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Sprint(spew.NewFormatter(a), spew.NewFormatter(b))
func Sprint(a ...interface{}) string {
return fmt.Sprint(convertArgs(a)...)
}
// Sprintf is a wrapper for fmt.Sprintf that treats each argument as if it were
// passed with a default Formatter interface returned by NewFormatter. It
// returns the resulting string. See NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Sprintf(format, spew.NewFormatter(a), spew.NewFormatter(b))
func Sprintf(format string, a ...interface{}) string {
return fmt.Sprintf(format, convertArgs(a)...)
}
// Sprintln is a wrapper for fmt.Sprintln that treats each argument as if it
// were passed with a default Formatter interface returned by NewFormatter. It
// returns the resulting string. See NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Sprintln(spew.NewFormatter(a), spew.NewFormatter(b))
func Sprintln(a ...interface{}) string {
return fmt.Sprintln(convertArgs(a)...)
}
// convertArgs accepts a slice of arguments and returns a slice of the same
// length with each argument converted to a default spew Formatter interface.
func convertArgs(args []interface{}) (formatters []interface{}) {
formatters = make([]interface{}, len(args))
for index, arg := range args {
formatters[index] = NewFormatter(arg)
}
return formatters
}
Copyright (c) 2013, Patrick Mezard
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
The names of its contributors may not be used to endorse or promote
products derived from this software without specific prior written
permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
This diff is collapsed.
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
digest = "1:ffe9824d294da03b391f44e1ae8281281b4afc1bdaa9588c9097785e3af10cec"
name = "github.com/davecgh/go-spew"
packages = ["spew"]
pruneopts = "NUT"
revision = "8991bc29aa16c548c550c7ff78260e27b9ab7c73"
version = "v1.1.1"
[[projects]]
digest = "1:0028cb19b2e4c3112225cd871870f2d9cf49b9b4276531f03438a88e94be86fe"
name = "github.com/pmezard/go-difflib"
packages = ["difflib"]
pruneopts = "NUT"
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
version = "v1.0.0"
[[projects]]
digest = "1:0331452965d8695c0a5633e0f509012987681a654f388f2ad0c3b2d7f7004b1c"
name = "github.com/stretchr/testify"
packages = [
"assert",
"require",
]
pruneopts = "NUT"
revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686"
version = "v1.2.2"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
input-imports = [
"github.com/stretchr/testify/assert",
"github.com/stretchr/testify/require",
]
solver-name = "gps-cdcl"
solver-version = 1
[prune]
unused-packages = true
non-go = true
go-tests = true
[[constraint]]
name = "github.com/stretchr/testify"
version = "~1.2.0"
The MIT License
Copyright (c) 2014 Stretchr, Inc.
Copyright (c) 2017-2018 objx contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
# Objx
[![Build Status](https://travis-ci.org/stretchr/objx.svg?branch=master)](https://travis-ci.org/stretchr/objx)
[![Go Report Card](https://goreportcard.com/badge/github.com/stretchr/objx)](https://goreportcard.com/report/github.com/stretchr/objx)
[![Maintainability](https://api.codeclimate.com/v1/badges/1d64bc6c8474c2074f2b/maintainability)](https://codeclimate.com/github/stretchr/objx/maintainability)
[![Test Coverage](https://api.codeclimate.com/v1/badges/1d64bc6c8474c2074f2b/test_coverage)](https://codeclimate.com/github/stretchr/objx/test_coverage)
[![Sourcegraph](https://sourcegraph.com/github.com/stretchr/objx/-/badge.svg)](https://sourcegraph.com/github.com/stretchr/objx)
[![GoDoc](https://godoc.org/github.com/stretchr/objx?status.svg)](https://godoc.org/github.com/stretchr/objx)
Objx - Go package for dealing with maps, slices, JSON and other data.
Get started:
- Install Objx with [one line of code](#installation), or [update it with another](#staying-up-to-date)
- Check out the API Documentation http://godoc.org/github.com/stretchr/objx
## Overview
Objx provides the `objx.Map` type, which is a `map[string]interface{}` that exposes a powerful `Get` method (among others) that allows you to easily and quickly get access to data within the map, without having to worry too much about type assertions, missing data, default values etc.
### Pattern
Objx uses a preditable pattern to make access data from within `map[string]interface{}` easy. Call one of the `objx.` functions to create your `objx.Map` to get going:
m, err := objx.FromJSON(json)
NOTE: Any methods or functions with the `Must` prefix will panic if something goes wrong, the rest will be optimistic and try to figure things out without panicking.
Use `Get` to access the value you're interested in. You can use dot and array
notation too:
m.Get("places[0].latlng")
Once you have sought the `Value` you're interested in, you can use the `Is*` methods to determine its type.
if m.Get("code").IsStr() { // Your code... }
Or you can just assume the type, and use one of the strong type methods to extract the real value:
m.Get("code").Int()
If there's no value there (or if it's the wrong type) then a default value will be returned, or you can be explicit about the default value.
Get("code").Int(-1)
If you're dealing with a slice of data as a value, Objx provides many useful methods for iterating, manipulating and selecting that data. You can find out more by exploring the index below.
### Reading data
A simple example of how to use Objx:
// Use MustFromJSON to make an objx.Map from some JSON
m := objx.MustFromJSON(`{"name": "Mat", "age": 30}`)
// Get the details
name := m.Get("name").Str()
age := m.Get("age").Int()
// Get their nickname (or use their name if they don't have one)
nickname := m.Get("nickname").Str(name)
### Ranging
Since `objx.Map` is a `map[string]interface{}` you can treat it as such. For example, to `range` the data, do what you would expect:
m := objx.MustFromJSON(json)
for key, value := range m {
// Your code...
}
## Installation
To install Objx, use go get:
go get github.com/stretchr/objx
### Staying up to date
To update Objx to the latest version, run:
go get -u github.com/stretchr/objx
### Supported go versions
We support the lastest three major Go versions, which are 1.9, 1.10 and 1.11 at the moment.
## Contributing
Please feel free to submit issues, fork the repository and send pull requests!
default:
deps: [test]
update-deps:
desc: Updates dependencies
cmds:
- dep ensure
- dep ensure -update
lint:
desc: Checks code style
cmds:
- gofmt -d -s *.go
- go vet .
silent: true
lint-fix:
desc: Fixes code style
cmds:
- gofmt -w -s *.go
test:
desc: Runs go tests
cmds:
- go test -race .
test-coverage:
desc: Runs go tests and calucates test coverage
cmds:
- go test -coverprofile=c.out .
package objx
import (
"regexp"
"strconv"
"strings"
)
const (
// PathSeparator is the character used to separate the elements
// of the keypath.
//
// For example, `location.address.city`
PathSeparator string = "."
// arrayAccesRegexString is the regex used to extract the array number
// from the access path
arrayAccesRegexString = `^(.+)\[([0-9]+)\]$`
)
// arrayAccesRegex is the compiled arrayAccesRegexString
var arrayAccesRegex = regexp.MustCompile(arrayAccesRegexString)
// Get gets the value using the specified selector and
// returns it inside a new Obj object.
//
// If it cannot find the value, Get will return a nil
// value inside an instance of Obj.
//
// Get can only operate directly on map[string]interface{} and []interface.
//
// Example
//
// To access the title of the third chapter of the second book, do:
//
// o.Get("books[1].chapters[2].title")
func (m Map) Get(selector string) *Value {
rawObj := access(m, selector, nil, false)
return &Value{data: rawObj}
}
// Set sets the value using the specified selector and
// returns the object on which Set was called.
//
// Set can only operate directly on map[string]interface{} and []interface
//
// Example
//
// To set the title of the third chapter of the second book, do:
//
// o.Set("books[1].chapters[2].title","Time to Go")
func (m Map) Set(selector string, value interface{}) Map {
access(m, selector, value, true)
return m
}
// getIndex returns the index, which is hold in s by two braches.
// It also returns s withour the index part, e.g. name[1] will return (1, name).
// If no index is found, -1 is returned
func getIndex(s string) (int, string) {
arrayMatches := arrayAccesRegex.FindStringSubmatch(s)
if len(arrayMatches) > 0 {
// Get the key into the map
selector := arrayMatches[1]
// Get the index into the array at the key
// We know this cannt fail because arrayMatches[2] is an int for sure
index, _ := strconv.Atoi(arrayMatches[2])
return index, selector
}
return -1, s
}
// access accesses the object using the selector and performs the
// appropriate action.
func access(current interface{}, selector string, value interface{}, isSet bool) interface{} {
selSegs := strings.SplitN(selector, PathSeparator, 2)
thisSel := selSegs[0]
index := -1
if strings.Contains(thisSel, "[") {
index, thisSel = getIndex(thisSel)
}
if curMap, ok := current.(Map); ok {
current = map[string]interface{}(curMap)
}
// get the object in question
switch current.(type) {
case map[string]interface{}:
curMSI := current.(map[string]interface{})
if len(selSegs) <= 1 && isSet {
curMSI[thisSel] = value
return nil
}
_, ok := curMSI[thisSel].(map[string]interface{})
if (curMSI[thisSel] == nil || !ok) && index == -1 && isSet {
curMSI[thisSel] = map[string]interface{}{}
}
current = curMSI[thisSel]
default:
current = nil
}
// do we need to access the item of an array?
if index > -1 {
if array, ok := current.([]interface{}); ok {
if index < len(array) {
current = array[index]
} else {
current = nil
}
}
}
if len(selSegs) > 1 {
current = access(current, selSegs[1], value, isSet)
}
return current
}
package objx
import (
"bytes"
"encoding/base64"
"encoding/json"
"errors"
"net/url"
"strconv"
)
// SignatureSeparator is the character that is used to
// separate the Base64 string from the security signature.
const SignatureSeparator = "_"
// URLValuesSliceKeySuffix is the character that is used to
// specify a suffic for slices parsed by URLValues.
// If the suffix is set to "[i]", then the index of the slice
// is used in place of i
// Ex: Suffix "[]" would have the form a[]=b&a[]=c
// OR Suffix "[i]" would have the form a[0]=b&a[1]=c
// OR Suffix "" would have the form a=b&a=c
var urlValuesSliceKeySuffix = "[]"
const (
URLValuesSliceKeySuffixEmpty = ""
URLValuesSliceKeySuffixArray = "[]"
URLValuesSliceKeySuffixIndex = "[i]"
)
// SetURLValuesSliceKeySuffix sets the character that is used to
// specify a suffic for slices parsed by URLValues.
// If the suffix is set to "[i]", then the index of the slice
// is used in place of i
// Ex: Suffix "[]" would have the form a[]=b&a[]=c
// OR Suffix "[i]" would have the form a[0]=b&a[1]=c
// OR Suffix "" would have the form a=b&a=c
func SetURLValuesSliceKeySuffix(s string) error {
if s == URLValuesSliceKeySuffixEmpty || s == URLValuesSliceKeySuffixArray || s == URLValuesSliceKeySuffixIndex {
urlValuesSliceKeySuffix = s
return nil
}
return errors.New("objx: Invalid URLValuesSliceKeySuffix provided.")
}
// JSON converts the contained object to a JSON string
// representation
func (m Map) JSON() (string, error) {
result, err := json.Marshal(m)
if err != nil {
err = errors.New("objx: JSON encode failed with: " + err.Error())
}
return string(result), err
}
// MustJSON converts the contained object to a JSON string
// representation and panics if there is an error
func (m Map) MustJSON() string {
result, err := m.JSON()
if err != nil {
panic(err.Error())
}
return result
}
// Base64 converts the contained object to a Base64 string
// representation of the JSON string representation
func (m Map) Base64() (string, error) {
var buf bytes.Buffer
jsonData, err := m.JSON()
if err != nil {
return "", err
}
encoder := base64.NewEncoder(base64.StdEncoding, &buf)
_, _ = encoder.Write([]byte(jsonData))
_ = encoder.Close()
return buf.String(), nil
}
// MustBase64 converts the contained object to a Base64 string
// representation of the JSON string representation and panics
// if there is an error
func (m Map) MustBase64() string {
result, err := m.Base64()
if err != nil {
panic(err.Error())
}
return result
}
// SignedBase64 converts the contained object to a Base64 string
// representation of the JSON string representation and signs it
// using the provided key.
func (m Map) SignedBase64(key string) (string, error) {
base64, err := m.Base64()
if err != nil {
return "", err
}
sig := HashWithKey(base64, key)
return base64 + SignatureSeparator + sig, nil
}
// MustSignedBase64 converts the contained object to a Base64 string
// representation of the JSON string representation and signs it
// using the provided key and panics if there is an error
func (m Map) MustSignedBase64(key string) string {
result, err := m.SignedBase64(key)
if err != nil {
panic(err.Error())
}
return result
}
/*
URL Query
------------------------------------------------
*/
// URLValues creates a url.Values object from an Obj. This
// function requires that the wrapped object be a map[string]interface{}
func (m Map) URLValues() url.Values {
vals := make(url.Values)
m.parseURLValues(m, vals, "")
return vals
}
func (m Map) parseURLValues(queryMap Map, vals url.Values, key string) {
useSliceIndex := false
if urlValuesSliceKeySuffix == "[i]" {
useSliceIndex = true
}
for k, v := range queryMap {
val := &Value{data: v}
switch {
case val.IsObjxMap():
if key == "" {
m.parseURLValues(val.ObjxMap(), vals, k)
} else {
m.parseURLValues(val.ObjxMap(), vals, key+"["+k+"]")
}
case val.IsObjxMapSlice():
sliceKey := k
if key != "" {
sliceKey = key + "[" + k + "]"
}
if useSliceIndex {
for i, sv := range val.MustObjxMapSlice() {
sk := sliceKey + "[" + strconv.FormatInt(int64(i), 10) + "]"
m.parseURLValues(sv, vals, sk)
}
} else {
sliceKey = sliceKey + urlValuesSliceKeySuffix
for _, sv := range val.MustObjxMapSlice() {
m.parseURLValues(sv, vals, sliceKey)
}
}
case val.IsMSISlice():
sliceKey := k
if key != "" {
sliceKey = key + "[" + k + "]"
}
if useSliceIndex {
for i, sv := range val.MustMSISlice() {
sk := sliceKey + "[" + strconv.FormatInt(int64(i), 10) + "]"
m.parseURLValues(New(sv), vals, sk)
}
} else {
sliceKey = sliceKey + urlValuesSliceKeySuffix
for _, sv := range val.MustMSISlice() {
m.parseURLValues(New(sv), vals, sliceKey)
}
}
case val.IsStrSlice(), val.IsBoolSlice(),
val.IsFloat32Slice(), val.IsFloat64Slice(),
val.IsIntSlice(), val.IsInt8Slice(), val.IsInt16Slice(), val.IsInt32Slice(), val.IsInt64Slice(),
val.IsUintSlice(), val.IsUint8Slice(), val.IsUint16Slice(), val.IsUint32Slice(), val.IsUint64Slice():
sliceKey := k
if key != "" {
sliceKey = key + "[" + k + "]"
}
if useSliceIndex {
for i, sv := range val.StringSlice() {
sk := sliceKey + "[" + strconv.FormatInt(int64(i), 10) + "]"
vals.Set(sk, sv)
}
} else {
sliceKey = sliceKey + urlValuesSliceKeySuffix
vals[sliceKey] = val.StringSlice()
}
default:
if key == "" {
vals.Set(k, val.String())
} else {
vals.Set(key+"["+k+"]", val.String())
}
}
}
}
// URLQuery gets an encoded URL query representing the given
// Obj. This function requires that the wrapped object be a
// map[string]interface{}
func (m Map) URLQuery() (string, error) {
return m.URLValues().Encode(), nil
}
/*
Objx - Go package for dealing with maps, slices, JSON and other data.
Overview
Objx provides the `objx.Map` type, which is a `map[string]interface{}` that exposes
a powerful `Get` method (among others) that allows you to easily and quickly get
access to data within the map, without having to worry too much about type assertions,
missing data, default values etc.
Pattern
Objx uses a preditable pattern to make access data from within `map[string]interface{}` easy.
Call one of the `objx.` functions to create your `objx.Map` to get going:
m, err := objx.FromJSON(json)
NOTE: Any methods or functions with the `Must` prefix will panic if something goes wrong,
the rest will be optimistic and try to figure things out without panicking.
Use `Get` to access the value you're interested in. You can use dot and array
notation too:
m.Get("places[0].latlng")
Once you have sought the `Value` you're interested in, you can use the `Is*` methods to determine its type.
if m.Get("code").IsStr() { // Your code... }
Or you can just assume the type, and use one of the strong type methods to extract the real value:
m.Get("code").Int()
If there's no value there (or if it's the wrong type) then a default value will be returned,
or you can be explicit about the default value.
Get("code").Int(-1)
If you're dealing with a slice of data as a value, Objx provides many useful methods for iterating,
manipulating and selecting that data. You can find out more by exploring the index below.
Reading data
A simple example of how to use Objx:
// Use MustFromJSON to make an objx.Map from some JSON
m := objx.MustFromJSON(`{"name": "Mat", "age": 30}`)
// Get the details
name := m.Get("name").Str()
age := m.Get("age").Int()
// Get their nickname (or use their name if they don't have one)
nickname := m.Get("nickname").Str(name)
Ranging
Since `objx.Map` is a `map[string]interface{}` you can treat it as such.
For example, to `range` the data, do what you would expect:
m := objx.MustFromJSON(json)
for key, value := range m {
// Your code...
}
*/
package objx
package objx
import (
"encoding/base64"
"encoding/json"
"errors"
"io/ioutil"
"net/url"
"strings"
)
// MSIConvertable is an interface that defines methods for converting your
// custom types to a map[string]interface{} representation.
type MSIConvertable interface {
// MSI gets a map[string]interface{} (msi) representing the
// object.
MSI() map[string]interface{}
}
// Map provides extended functionality for working with
// untyped data, in particular map[string]interface (msi).
type Map map[string]interface{}
// Value returns the internal value instance
func (m Map) Value() *Value {
return &Value{data: m}
}
// Nil represents a nil Map.
var Nil = New(nil)
// New creates a new Map containing the map[string]interface{} in the data argument.
// If the data argument is not a map[string]interface, New attempts to call the
// MSI() method on the MSIConvertable interface to create one.
func New(data interface{}) Map {
if _, ok := data.(map[string]interface{}); !ok {
if converter, ok := data.(MSIConvertable); ok {
data = converter.MSI()
} else {
return nil
}
}
return Map(data.(map[string]interface{}))
}
// MSI creates a map[string]interface{} and puts it inside a new Map.
//
// The arguments follow a key, value pattern.
//
//
// Returns nil if any key argument is non-string or if there are an odd number of arguments.
//
// Example
//
// To easily create Maps:
//
// m := objx.MSI("name", "Mat", "age", 29, "subobj", objx.MSI("active", true))
//
// // creates an Map equivalent to
// m := objx.Map{"name": "Mat", "age": 29, "subobj": objx.Map{"active": true}}
func MSI(keyAndValuePairs ...interface{}) Map {
newMap := Map{}
keyAndValuePairsLen := len(keyAndValuePairs)
if keyAndValuePairsLen%2 != 0 {
return nil
}
for i := 0; i < keyAndValuePairsLen; i = i + 2 {
key := keyAndValuePairs[i]
value := keyAndValuePairs[i+1]
// make sure the key is a string
keyString, keyStringOK := key.(string)
if !keyStringOK {
return nil
}
newMap[keyString] = value
}
return newMap
}
// ****** Conversion Constructors
// MustFromJSON creates a new Map containing the data specified in the
// jsonString.
//
// Panics if the JSON is invalid.
func MustFromJSON(jsonString string) Map {
o, err := FromJSON(jsonString)
if err != nil {
panic("objx: MustFromJSON failed with error: " + err.Error())
}
return o
}
// FromJSON creates a new Map containing the data specified in the
// jsonString.
//
// Returns an error if the JSON is invalid.
func FromJSON(jsonString string) (Map, error) {
var m Map
err := json.Unmarshal([]byte(jsonString), &m)
if err != nil {
return Nil, err
}
m.tryConvertFloat64()
return m, nil
}
func (m Map) tryConvertFloat64() {
for k, v := range m {
switch v.(type) {
case float64:
f := v.(float64)
if float64(int(f)) == f {
m[k] = int(f)
}
case map[string]interface{}:
t := New(v)
t.tryConvertFloat64()
m[k] = t
case []interface{}:
m[k] = tryConvertFloat64InSlice(v.([]interface{}))
}
}
}
func tryConvertFloat64InSlice(s []interface{}) []interface{} {
for k, v := range s {
switch v.(type) {
case float64:
f := v.(float64)
if float64(int(f)) == f {
s[k] = int(f)
}
case map[string]interface{}:
t := New(v)
t.tryConvertFloat64()
s[k] = t
case []interface{}:
s[k] = tryConvertFloat64InSlice(v.([]interface{}))
}
}
return s
}
// FromBase64 creates a new Obj containing the data specified
// in the Base64 string.
//
// The string is an encoded JSON string returned by Base64
func FromBase64(base64String string) (Map, error) {
decoder := base64.NewDecoder(base64.StdEncoding, strings.NewReader(base64String))
decoded, err := ioutil.ReadAll(decoder)
if err != nil {
return nil, err
}
return FromJSON(string(decoded))
}
// MustFromBase64 creates a new Obj containing the data specified
// in the Base64 string and panics if there is an error.
//
// The string is an encoded JSON string returned by Base64
func MustFromBase64(base64String string) Map {
result, err := FromBase64(base64String)
if err != nil {
panic("objx: MustFromBase64 failed with error: " + err.Error())
}
return result
}
// FromSignedBase64 creates a new Obj containing the data specified
// in the Base64 string.
//
// The string is an encoded JSON string returned by SignedBase64
func FromSignedBase64(base64String, key string) (Map, error) {
parts := strings.Split(base64String, SignatureSeparator)
if len(parts) != 2 {
return nil, errors.New("objx: Signed base64 string is malformed")
}
sig := HashWithKey(parts[0], key)
if parts[1] != sig {
return nil, errors.New("objx: Signature for base64 data does not match")
}
return FromBase64(parts[0])
}
// MustFromSignedBase64 creates a new Obj containing the data specified
// in the Base64 string and panics if there is an error.
//
// The string is an encoded JSON string returned by Base64
func MustFromSignedBase64(base64String, key string) Map {
result, err := FromSignedBase64(base64String, key)
if err != nil {
panic("objx: MustFromSignedBase64 failed with error: " + err.Error())
}
return result
}
// FromURLQuery generates a new Obj by parsing the specified
// query.
//
// For queries with multiple values, the first value is selected.
func FromURLQuery(query string) (Map, error) {
vals, err := url.ParseQuery(query)
if err != nil {
return nil, err
}
m := Map{}
for k, vals := range vals {
m[k] = vals[0]
}
return m, nil
}
// MustFromURLQuery generates a new Obj by parsing the specified
// query.
//
// For queries with multiple values, the first value is selected.
//
// Panics if it encounters an error
func MustFromURLQuery(query string) Map {
o, err := FromURLQuery(query)
if err != nil {
panic("objx: MustFromURLQuery failed with error: " + err.Error())
}
return o
}
package objx
// Exclude returns a new Map with the keys in the specified []string
// excluded.
func (m Map) Exclude(exclude []string) Map {
excluded := make(Map)
for k, v := range m {
if !contains(exclude, k) {
excluded[k] = v
}
}
return excluded
}
// Copy creates a shallow copy of the Obj.
func (m Map) Copy() Map {
copied := Map{}
for k, v := range m {
copied[k] = v
}
return copied
}
// Merge blends the specified map with a copy of this map and returns the result.
//
// Keys that appear in both will be selected from the specified map.
// This method requires that the wrapped object be a map[string]interface{}
func (m Map) Merge(merge Map) Map {
return m.Copy().MergeHere(merge)
}
// MergeHere blends the specified map with this map and returns the current map.
//
// Keys that appear in both will be selected from the specified map. The original map
// will be modified. This method requires that
// the wrapped object be a map[string]interface{}
func (m Map) MergeHere(merge Map) Map {
for k, v := range merge {
m[k] = v
}
return m
}
// Transform builds a new Obj giving the transformer a chance
// to change the keys and values as it goes. This method requires that
// the wrapped object be a map[string]interface{}
func (m Map) Transform(transformer func(key string, value interface{}) (string, interface{})) Map {
newMap := Map{}
for k, v := range m {
modifiedKey, modifiedVal := transformer(k, v)
newMap[modifiedKey] = modifiedVal
}
return newMap
}
// TransformKeys builds a new map using the specified key mapping.
//
// Unspecified keys will be unaltered.
// This method requires that the wrapped object be a map[string]interface{}
func (m Map) TransformKeys(mapping map[string]string) Map {
return m.Transform(func(key string, value interface{}) (string, interface{}) {
if newKey, ok := mapping[key]; ok {
return newKey, value
}
return key, value
})
}
// Checks if a string slice contains a string
func contains(s []string, e string) bool {
for _, a := range s {
if a == e {
return true
}
}
return false
}
package objx
import (
"crypto/sha1"
"encoding/hex"
)
// HashWithKey hashes the specified string using the security key
func HashWithKey(data, key string) string {
d := sha1.Sum([]byte(data + ":" + key))
return hex.EncodeToString(d[:])
}
package objx
// Has gets whether there is something at the specified selector
// or not.
//
// If m is nil, Has will always return false.
func (m Map) Has(selector string) bool {
if m == nil {
return false
}
return !m.Get(selector).IsNil()
}
// IsNil gets whether the data is nil or not.
func (v *Value) IsNil() bool {
return v == nil || v.data == nil
}
package objx
/*
MSI (map[string]interface{} and []map[string]interface{})
*/
// MSI gets the value as a map[string]interface{}, returns the optionalDefault
// value or a system default object if the value is the wrong type.
func (v *Value) MSI(optionalDefault ...map[string]interface{}) map[string]interface{} {
if s, ok := v.data.(map[string]interface{}); ok {
return s
}
if s, ok := v.data.(Map); ok {
return map[string]interface{}(s)
}
if len(optionalDefault) == 1 {
return optionalDefault[0]
}
return nil
}
// MustMSI gets the value as a map[string]interface{}.
//
// Panics if the object is not a map[string]interface{}.
func (v *Value) MustMSI() map[string]interface{} {
if s, ok := v.data.(Map); ok {
return map[string]interface{}(s)
}
return v.data.(map[string]interface{})
}
// MSISlice gets the value as a []map[string]interface{}, returns the optionalDefault
// value or nil if the value is not a []map[string]interface{}.
func (v *Value) MSISlice(optionalDefault ...[]map[string]interface{}) []map[string]interface{} {
if s, ok := v.data.([]map[string]interface{}); ok {
return s
}
s := v.ObjxMapSlice()
if s == nil {
if len(optionalDefault) == 1 {
return optionalDefault[0]
}
return nil
}
result := make([]map[string]interface{}, len(s))
for i := range s {
result[i] = s[i].Value().MSI()
}
return result
}
// MustMSISlice gets the value as a []map[string]interface{}.
//
// Panics if the object is not a []map[string]interface{}.
func (v *Value) MustMSISlice() []map[string]interface{} {
if s := v.MSISlice(); s != nil {
return s
}
return v.data.([]map[string]interface{})
}
// IsMSI gets whether the object contained is a map[string]interface{} or not.
func (v *Value) IsMSI() bool {
_, ok := v.data.(map[string]interface{})
if !ok {
_, ok = v.data.(Map)
}
return ok
}
// IsMSISlice gets whether the object contained is a []map[string]interface{} or not.
func (v *Value) IsMSISlice() bool {
_, ok := v.data.([]map[string]interface{})
if !ok {
_, ok = v.data.([]Map)
}
return ok
}
// EachMSI calls the specified callback for each object
// in the []map[string]interface{}.
//
// Panics if the object is the wrong type.
func (v *Value) EachMSI(callback func(int, map[string]interface{}) bool) *Value {
for index, val := range v.MustMSISlice() {
carryon := callback(index, val)
if !carryon {
break
}
}
return v
}
// WhereMSI uses the specified decider function to select items
// from the []map[string]interface{}. The object contained in the result will contain
// only the selected items.
func (v *Value) WhereMSI(decider func(int, map[string]interface{}) bool) *Value {
var selected []map[string]interface{}
v.EachMSI(func(index int, val map[string]interface{}) bool {
shouldSelect := decider(index, val)
if !shouldSelect {
selected = append(selected, val)
}
return true
})
return &Value{data: selected}
}
// GroupMSI uses the specified grouper function to group the items
// keyed by the return of the grouper. The object contained in the
// result will contain a map[string][]map[string]interface{}.
func (v *Value) GroupMSI(grouper func(int, map[string]interface{}) string) *Value {
groups := make(map[string][]map[string]interface{})
v.EachMSI(func(index int, val map[string]interface{}) bool {
group := grouper(index, val)
if _, ok := groups[group]; !ok {
groups[group] = make([]map[string]interface{}, 0)
}
groups[group] = append(groups[group], val)
return true
})
return &Value{data: groups}
}
// ReplaceMSI uses the specified function to replace each map[string]interface{}s
// by iterating each item. The data in the returned result will be a
// []map[string]interface{} containing the replaced items.
func (v *Value) ReplaceMSI(replacer func(int, map[string]interface{}) map[string]interface{}) *Value {
arr := v.MustMSISlice()
replaced := make([]map[string]interface{}, len(arr))
v.EachMSI(func(index int, val map[string]interface{}) bool {
replaced[index] = replacer(index, val)
return true
})
return &Value{data: replaced}
}
// CollectMSI uses the specified collector function to collect a value
// for each of the map[string]interface{}s in the slice. The data returned will be a
// []interface{}.
func (v *Value) CollectMSI(collector func(int, map[string]interface{}) interface{}) *Value {
arr := v.MustMSISlice()
collected := make([]interface{}, len(arr))
v.EachMSI(func(index int, val map[string]interface{}) bool {
collected[index] = collector(index, val)
return true
})
return &Value{data: collected}
}
/*
ObjxMap ((Map) and [](Map))
*/
// ObjxMap gets the value as a (Map), returns the optionalDefault
// value or a system default object if the value is the wrong type.
func (v *Value) ObjxMap(optionalDefault ...(Map)) Map {
if s, ok := v.data.((Map)); ok {
return s
}
if s, ok := v.data.(map[string]interface{}); ok {
return s
}
if len(optionalDefault) == 1 {
return optionalDefault[0]
}
return New(nil)
}
// MustObjxMap gets the value as a (Map).
//
// Panics if the object is not a (Map).
func (v *Value) MustObjxMap() Map {
if s, ok := v.data.(map[string]interface{}); ok {
return s
}
return v.data.((Map))
}
// ObjxMapSlice gets the value as a [](Map), returns the optionalDefault
// value or nil if the value is not a [](Map).
func (v *Value) ObjxMapSlice(optionalDefault ...[](Map)) [](Map) {
if s, ok := v.data.([]Map); ok {
return s
}
if s, ok := v.data.([]map[string]interface{}); ok {
result := make([]Map, len(s))
for i := range s {
result[i] = s[i]
}
return result
}
s, ok := v.data.([]interface{})
if !ok {
if len(optionalDefault) == 1 {
return optionalDefault[0]
}
return nil
}
result := make([]Map, len(s))
for i := range s {
switch s[i].(type) {
case Map:
result[i] = s[i].(Map)
case map[string]interface{}:
result[i] = New(s[i])
default:
return nil
}
}
return result
}
// MustObjxMapSlice gets the value as a [](Map).
//
// Panics if the object is not a [](Map).
func (v *Value) MustObjxMapSlice() [](Map) {
if s := v.ObjxMapSlice(); s != nil {
return s
}
return v.data.([](Map))
}
// IsObjxMap gets whether the object contained is a (Map) or not.
func (v *Value) IsObjxMap() bool {
_, ok := v.data.((Map))
if !ok {
_, ok = v.data.(map[string]interface{})
}
return ok
}
// IsObjxMapSlice gets whether the object contained is a [](Map) or not.
func (v *Value) IsObjxMapSlice() bool {
_, ok := v.data.([](Map))
if !ok {
_, ok = v.data.([]map[string]interface{})
}
return ok
}
// EachObjxMap calls the specified callback for each object
// in the [](Map).
//
// Panics if the object is the wrong type.
func (v *Value) EachObjxMap(callback func(int, Map) bool) *Value {
for index, val := range v.MustObjxMapSlice() {
carryon := callback(index, val)
if !carryon {
break
}
}
return v
}
// WhereObjxMap uses the specified decider function to select items
// from the [](Map). The object contained in the result will contain
// only the selected items.
func (v *Value) WhereObjxMap(decider func(int, Map) bool) *Value {
var selected [](Map)
v.EachObjxMap(func(index int, val Map) bool {
shouldSelect := decider(index, val)
if !shouldSelect {
selected = append(selected, val)
}
return true
})
return &Value{data: selected}
}
// GroupObjxMap uses the specified grouper function to group the items
// keyed by the return of the grouper. The object contained in the
// result will contain a map[string][](Map).
func (v *Value) GroupObjxMap(grouper func(int, Map) string) *Value {
groups := make(map[string][](Map))
v.EachObjxMap(func(index int, val Map) bool {
group := grouper(index, val)
if _, ok := groups[group]; !ok {
groups[group] = make([](Map), 0)
}
groups[group] = append(groups[group], val)
return true
})
return &Value{data: groups}
}
// ReplaceObjxMap uses the specified function to replace each (Map)s
// by iterating each item. The data in the returned result will be a
// [](Map) containing the replaced items.
func (v *Value) ReplaceObjxMap(replacer func(int, Map) Map) *Value {
arr := v.MustObjxMapSlice()
replaced := make([](Map), len(arr))
v.EachObjxMap(func(index int, val Map) bool {
replaced[index] = replacer(index, val)
return true
})
return &Value{data: replaced}
}
// CollectObjxMap uses the specified collector function to collect a value
// for each of the (Map)s in the slice. The data returned will be a
// []interface{}.
func (v *Value) CollectObjxMap(collector func(int, Map) interface{}) *Value {
arr := v.MustObjxMapSlice()
collected := make([]interface{}, len(arr))
v.EachObjxMap(func(index int, val Map) bool {
collected[index] = collector(index, val)
return true
})
return &Value{data: collected}
}
This diff is collapsed.
package objx
import (
"fmt"
"strconv"
)
// Value provides methods for extracting interface{} data in various
// types.
type Value struct {
// data contains the raw data being managed by this Value
data interface{}
}
// Data returns the raw data contained by this Value
func (v *Value) Data() interface{} {
return v.data
}
// String returns the value always as a string
func (v *Value) String() string {
switch {
case v.IsNil():
return ""
case v.IsStr():
return v.Str()
case v.IsBool():
return strconv.FormatBool(v.Bool())
case v.IsFloat32():
return strconv.FormatFloat(float64(v.Float32()), 'f', -1, 32)
case v.IsFloat64():
return strconv.FormatFloat(v.Float64(), 'f', -1, 64)
case v.IsInt():
return strconv.FormatInt(int64(v.Int()), 10)
case v.IsInt8():
return strconv.FormatInt(int64(v.Int8()), 10)
case v.IsInt16():
return strconv.FormatInt(int64(v.Int16()), 10)
case v.IsInt32():
return strconv.FormatInt(int64(v.Int32()), 10)
case v.IsInt64():
return strconv.FormatInt(v.Int64(), 10)
case v.IsUint():
return strconv.FormatUint(uint64(v.Uint()), 10)
case v.IsUint8():
return strconv.FormatUint(uint64(v.Uint8()), 10)
case v.IsUint16():
return strconv.FormatUint(uint64(v.Uint16()), 10)
case v.IsUint32():
return strconv.FormatUint(uint64(v.Uint32()), 10)
case v.IsUint64():
return strconv.FormatUint(v.Uint64(), 10)
}
return fmt.Sprintf("%#v", v.Data())
}
// StringSlice returns the value always as a []string
func (v *Value) StringSlice(optionalDefault ...[]string) []string {
switch {
case v.IsStrSlice():
return v.MustStrSlice()
case v.IsBoolSlice():
slice := v.MustBoolSlice()
vals := make([]string, len(slice))
for i, iv := range slice {
vals[i] = strconv.FormatBool(iv)
}
return vals
case v.IsFloat32Slice():
slice := v.MustFloat32Slice()
vals := make([]string, len(slice))
for i, iv := range slice {
vals[i] = strconv.FormatFloat(float64(iv), 'f', -1, 32)
}
return vals
case v.IsFloat64Slice():
slice := v.MustFloat64Slice()
vals := make([]string, len(slice))
for i, iv := range slice {
vals[i] = strconv.FormatFloat(iv, 'f', -1, 64)
}
return vals
case v.IsIntSlice():
slice := v.MustIntSlice()
vals := make([]string, len(slice))
for i, iv := range slice {
vals[i] = strconv.FormatInt(int64(iv), 10)
}
return vals
case v.IsInt8Slice():
slice := v.MustInt8Slice()
vals := make([]string, len(slice))
for i, iv := range slice {
vals[i] = strconv.FormatInt(int64(iv), 10)
}
return vals
case v.IsInt16Slice():
slice := v.MustInt16Slice()
vals := make([]string, len(slice))
for i, iv := range slice {
vals[i] = strconv.FormatInt(int64(iv), 10)
}
return vals
case v.IsInt32Slice():
slice := v.MustInt32Slice()
vals := make([]string, len(slice))
for i, iv := range slice {
vals[i] = strconv.FormatInt(int64(iv), 10)
}
return vals
case v.IsInt64Slice():
slice := v.MustInt64Slice()
vals := make([]string, len(slice))
for i, iv := range slice {
vals[i] = strconv.FormatInt(iv, 10)
}
return vals
case v.IsUintSlice():
slice := v.MustUintSlice()
vals := make([]string, len(slice))
for i, iv := range slice {
vals[i] = strconv.FormatUint(uint64(iv), 10)
}
return vals
case v.IsUint8Slice():
slice := v.MustUint8Slice()
vals := make([]string, len(slice))
for i, iv := range slice {
vals[i] = strconv.FormatUint(uint64(iv), 10)
}
return vals
case v.IsUint16Slice():
slice := v.MustUint16Slice()
vals := make([]string, len(slice))
for i, iv := range slice {
vals[i] = strconv.FormatUint(uint64(iv), 10)
}
return vals
case v.IsUint32Slice():
slice := v.MustUint32Slice()
vals := make([]string, len(slice))
for i, iv := range slice {
vals[i] = strconv.FormatUint(uint64(iv), 10)
}
return vals
case v.IsUint64Slice():
slice := v.MustUint64Slice()
vals := make([]string, len(slice))
for i, iv := range slice {
vals[i] = strconv.FormatUint(iv, 10)
}
return vals
}
if len(optionalDefault) == 1 {
return optionalDefault[0]
}
return []string{}
}
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
name = "github.com/davecgh/go-spew"
packages = ["spew"]
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
version = "v1.1.0"
[[projects]]
name = "github.com/pmezard/go-difflib"
packages = ["difflib"]
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
version = "v1.0.0"
[[projects]]
name = "github.com/stretchr/objx"
packages = ["."]
revision = "facf9a85c22f48d2f52f2380e4efce1768749a89"
version = "v0.1"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "448ddae4702c6aded2555faafd390c537789bb1c483f70b0431e6634f73f2090"
solver-name = "gps-cdcl"
solver-version = 1
[prune]
unused-packages = true
non-go = true
go-tests = true
[[constraint]]
name = "github.com/davecgh/go-spew"
version = "~1.1.0"
[[constraint]]
name = "github.com/pmezard/go-difflib"
version = "~1.0.0"
[[constraint]]
name = "github.com/stretchr/objx"
version = "~0.1.0"
MIT License
Copyright (c) 2012-2018 Mat Ryer and Tyler Bunnell
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
This diff is collapsed.
{{.CommentFormat}}
func {{.DocInfo.Name}}f(t TestingT, {{.ParamsFormat}}) bool {
if h, ok := t.(tHelper); ok { h.Helper() }
return {{.DocInfo.Name}}(t, {{.ForwardedParamsFormat}})
}
{{.CommentWithoutT "a"}}
func (a *Assertions) {{.DocInfo.Name}}({{.Params}}) bool {
if h, ok := a.t.(tHelper); ok { h.Helper() }
return {{.DocInfo.Name}}(a.t, {{.ForwardedParams}})
}
This diff is collapsed.
// Package assert provides a set of comprehensive testing tools for use with the normal Go testing system.
//
// Example Usage
//
// The following is a complete example using assert in a standard test function:
// import (
// "testing"
// "github.com/stretchr/testify/assert"
// )
//
// func TestSomething(t *testing.T) {
//
// var a string = "Hello"
// var b string = "Hello"
//
// assert.Equal(t, a, b, "The two words should be the same.")
//
// }
//
// if you assert many times, use the format below:
//
// import (
// "testing"
// "github.com/stretchr/testify/assert"
// )
//
// func TestSomething(t *testing.T) {
// assert := assert.New(t)
//
// var a string = "Hello"
// var b string = "Hello"
//
// assert.Equal(a, b, "The two words should be the same.")
// }
//
// Assertions
//
// Assertions allow you to easily write test code, and are global funcs in the `assert` package.
// All assertion functions take, as the first argument, the `*testing.T` object provided by the
// testing framework. This allows the assertion funcs to write the failings and other details to
// the correct place.
//
// Every assertion function also takes an optional string message as the final argument,
// allowing custom error messages to be appended to the message the assertion method outputs.
package assert
package assert
import (
"errors"
)
// AnError is an error instance useful for testing. If the code does not care
// about error specifics, and only needs to return the error for example, this
// error should be used to make the test code more readable.
var AnError = errors.New("assert.AnError general error for testing")
package assert
// Assertions provides assertion methods around the
// TestingT interface.
type Assertions struct {
t TestingT
}
// New makes a new Assertions object for the specified TestingT.
func New(t TestingT) *Assertions {
return &Assertions{
t: t,
}
}
//go:generate go run ../_codegen/main.go -output-package=assert -template=assertion_forward.go.tmpl -include-format-funcs
This diff is collapsed.
module github.com/stretchr/testify
require (
github.com/davecgh/go-spew v1.1.0
github.com/pmezard/go-difflib v1.0.0
github.com/stretchr/objx v0.1.0
)
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
// Package http DEPRECATED USE net/http/httptest
package http
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment