Fnlb/k8s grouper (#563)

* WIP: add k8s grouper

- This shares a great deal of behaviour with allGrouper. Once it's
tested, refactor that to share as much as possible

- Glide hell. Checked in the yaml and lock files but a glide i -v
will be required to bring vendor/ up-to-date. Will address once this
is ready.

* Update README. Make the watch tracking work.

(To follow: add the junk that was pulled in via the glide update.)

* Vendor updates.

* go fmt

* Use the allGrouper with a k8s-backed DBStore instead.

This is much tidier :-)

* Fix up go vet
This commit is contained in:
jan grant
2017-12-06 18:45:27 +00:00
committed by Reed Allman
parent 53a2b2702a
commit 8fc4bdcf97
2626 changed files with 626646 additions and 89487 deletions

View File

@@ -66,3 +66,65 @@ To list functions nodes and their state:
```sh ```sh
curl -sSL -X GET <fnlb_address>/1/lb/nodes curl -sSL -X GET <fnlb_address>/1/lb/nodes
``` ```
## Running under Kubernetes
The fnlb supports a mode of operation which relies on Kubernetes to inform it as Fn pods come in and out of service. In order to run in this mode, some additional command-line flags are required. `-db=k8s` will select Kubernetes operation; in this mode, the `nodes` flag is ignored. `-label-selector=...` is a standard Kubernetes selector expression.
A sample k8s configuration follows; this expects Fn pods to be labelled `app=fn,role=fn-service`. By default, the lb will look in its own namespace for Fn pods. This can be changed by explicitly passing the `-namespace=...` option.
```yaml
apiVersion: v1
kind: Service
metadata:
name: fn-service
namespace: fn
labels:
app: fn
role: fn-lb
spec:
type: NodePort
ports:
- name: fn-service
port: 8080
targetPort: 8080
nodePort: 32180
selector:
app: fn
role: fn-lb
---
apiVersion: extensions/v1beta1
kind: DaemonSet
metadata:
name: fn-lb
namespace: fn
spec:
updateStrategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
minReadySeconds: 30
template:
metadata:
labels:
app: fn
role: fn-lb
spec:
containers:
- name: fn-lb
image: fnproject/fnlb
imagePullPolicy: Always
args:
- "-db=k8s"
- "-label-selector=app=fn,role=fn-service"
- "-listen=:8080"
- "-mgmt-listen=:8080"
ports:
- containerPort: 8080
env:
- name: LOG_LEVEL
value: debug
```
In this mode, the shared database is not required; each lb will listen to the Kubernetes master independently to derive the same information. The lb nodes continue to health-check Fn pods *in addition* to the health checks running directly as part of a Pod definition.

View File

@@ -65,6 +65,9 @@ type nodeState struct {
// current health state // current health state
healthy healthState healthy healthState
// IP address of remote
address string
} }
// allGrouper will return all healthy nodes it is tracking from List. // allGrouper will return all healthy nodes it is tracking from List.

View File

@@ -47,6 +47,9 @@ func (mock *mockDB) List() ([]string, error) {
} }
return list, nil return list, nil
} }
func (mock *mockDB) Close() error {
return nil
}
func initializeRunner() (Grouper, error) { func initializeRunner() (Grouper, error) {
db := &mockDB{ db := &mockDB{

View File

@@ -3,6 +3,7 @@ package lb
import ( import (
"database/sql" "database/sql"
"errors" "errors"
"io"
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
@@ -16,6 +17,9 @@ import (
) )
func NewDB(conf Config) (DBStore, error) { func NewDB(conf Config) (DBStore, error) {
if conf.DBurl == "k8s" {
return NewK8sStore(conf)
}
db, err := db(conf.DBurl) db, err := db(conf.DBurl)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -26,6 +30,7 @@ func NewDB(conf Config) (DBStore, error) {
// TODO put this somewhere better // TODO put this somewhere better
type DBStore interface { type DBStore interface {
io.Closer
Add(string) error Add(string) error
Delete(string) error Delete(string) error
List() ([]string, error) List() ([]string, error)
@@ -151,3 +156,7 @@ func (s *sqlStore) List() ([]string, error) {
return nodes, err return nodes, err
} }
func (s *sqlStore) Close() error {
return s.db.Close()
}

181
fnlb/lb/k8s_store.go Normal file
View File

@@ -0,0 +1,181 @@
package lb
import (
"errors"
"fmt"
"io/ioutil"
"strings"
"sync"
"github.com/sirupsen/logrus"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/pkg/api/v1"
"k8s.io/client-go/rest"
)
var _ DBStore = &k8sStore{}
func NewK8sClient(conf Config) (*kubernetes.Clientset, error) {
k8sConfig, err := rest.InClusterConfig()
if err != nil {
return nil, err
}
return kubernetes.NewForConfig(k8sConfig)
}
func NewK8sStore(conf Config) (*k8sStore, error) {
client, err := NewK8sClient(conf)
if err != nil {
return nil, err
}
ns := conf.Namespace
if ns == "" {
ns, err = namespace()
if err != nil {
return nil, err
}
}
_, err = labels.Parse(conf.LabelSelector)
if err != nil {
return nil, err
}
k := &k8sStore{
client: client,
cancel: make(chan struct{}),
nodes: map[string]string{},
}
go k.watch(ns, conf.LabelSelector, conf.TargetPort)
return k, nil
}
type k8sStore struct {
client *kubernetes.Clientset
cancel chan struct{}
nodeLock sync.RWMutex
nodes map[string]string
}
func (*k8sStore) Add(string) error {
return errors.New("kubernetes driver does not support manual addition of targets")
}
func (*k8sStore) Delete(string) error {
return errors.New("kubernetes driver does not support manual deletion of targets")
}
func (k *k8sStore) List() ([]string, error) {
nodes := []string{}
k.nodeLock.RLock()
defer k.nodeLock.RUnlock()
for _, addr := range k.nodes {
if addr != "" {
nodes = append(nodes, addr)
}
}
return nodes, nil
}
func (k *k8sStore) Close() error {
close(k.cancel)
return nil
}
// Manage node update and deletion under the hood
func (k *k8sStore) registerNode(node string, addr string) {
k.nodeLock.Lock()
defer k.nodeLock.Unlock()
if address, ok := k.nodes[node]; ok {
if address != addr {
logrus.WithField("node", node).WithField("address", addr).Info("Updating address of registered node")
} else {
logrus.WithField("node", node).WithField("address", addr).Debug("Attempt to add extant node at known address")
}
}
k.nodes[node] = addr
}
func (k *k8sStore) unregisterNode(node string) {
k.nodeLock.Lock()
defer k.nodeLock.Unlock()
if addr, ok := k.nodes[node]; ok {
logrus.WithField("node", node).WithField("address", addr).Info("Removing registered node")
} else {
logrus.WithField("node", node).Warn("Attempt to remove unknown node")
}
delete(k.nodes, node)
}
// Kubernetes-specific stuff
func namespace() (string, error) {
// Use our current namespace
ns, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/" + v1.ServiceAccountNamespaceKey)
if err != nil {
// If we're not running inside k8s, bail out
return "", err
}
return strings.TrimSpace(string(ns)), nil
}
// Watch for pods under a given namespace matching a particular labelselector.
// As they become ready, register an address derived from their PodIP and the specified port
func (k *k8sStore) watch(ns string, ls string, port int) {
pods, err := k.client.CoreV1().Pods(ns).Watch(metav1.ListOptions{LabelSelector: ls})
if err != nil {
panic(err.Error())
}
logrus.WithField("Namespace", ns).WithField("LabelSelector", ls).Info("Watching for pod changes")
// This runs forever.
for {
select {
case <-k.cancel:
logrus.Info("Stopped watching for pod changes")
pods.Stop()
return
case event := <-pods.ResultChan():
if pod, ok := event.Object.(*v1.Pod); ok {
logrus.
WithField("Event", event.Type).
WithField("Pod", pod.SelfLink).
WithField("PodPhase", pod.Status.Phase).
WithField("Address", pod.Status.PodIP).
Debug("Change detected")
switch event.Type {
case watch.Added:
fallthrough
case watch.Modified:
logrus.WithField("Pod", pod.SelfLink).WithField("PodIp", pod.Status.PodIP).Debug("New pod detected")
address := ""
if pod.Status.PodIP != "" && pod.Status.Phase == v1.PodRunning {
address = fmt.Sprintf("%s:%d", pod.Status.PodIP, port)
}
k.registerNode(pod.SelfLink, address)
case watch.Deleted:
logrus.WithField("Pod", pod.SelfLink).WithField("PodIp", pod.Status.PodIP).Debug("Pod removed")
k.unregisterNode(pod.SelfLink)
}
}
}
}
}

View File

@@ -43,6 +43,11 @@ type Config struct {
HealthcheckTimeout int `json:"healthcheck_timeout"` HealthcheckTimeout int `json:"healthcheck_timeout"`
MinAPIVersion *semver.Version `json:"min_api_version"` MinAPIVersion *semver.Version `json:"min_api_version"`
// Kubernetes support
Namespace string `json:"k8s_namespace"`
LabelSelector string `json:"k8s_label_selector"`
TargetPort int `json:"target_port"`
Transport *http.Transport Transport *http.Transport
} }

View File

@@ -21,11 +21,17 @@ const VERSION = "0.0.184"
func main() { func main() {
// XXX (reed): normalize // XXX (reed): normalize
level, err := logrus.ParseLevel(os.Getenv("LOG_LEVEL"))
if err != nil {
level = logrus.InfoLevel
}
logrus.SetLevel(level)
fnodes := flag.String("nodes", "", "comma separated list of functions nodes") fnodes := flag.String("nodes", "", "comma separated list of functions nodes")
minAPIVersion := flag.String("min-api-version", "0.0.151", "minimal node API to accept") minAPIVersion := flag.String("min-api-version", "0.0.151", "minimal node API to accept")
var conf lb.Config var conf lb.Config
flag.StringVar(&conf.DBurl, "db", "sqlite3://:memory:", "backend to store nodes, default to in memory") flag.StringVar(&conf.DBurl, "db", "sqlite3://:memory:", "backend to store nodes, default to in memory; use k8s for kuberneted")
flag.StringVar(&conf.Listen, "listen", ":8081", "port to run on") flag.StringVar(&conf.Listen, "listen", ":8081", "port to run on")
flag.StringVar(&conf.MgmtListen, "mgmt-listen", ":8081", "management port to run on") flag.StringVar(&conf.MgmtListen, "mgmt-listen", ":8081", "management port to run on")
flag.IntVar(&conf.ShutdownTimeout, "shutdown-timeout", 0, "graceful shutdown timeout") flag.IntVar(&conf.ShutdownTimeout, "shutdown-timeout", 0, "graceful shutdown timeout")
@@ -35,6 +41,10 @@ func main() {
flag.IntVar(&conf.HealthcheckHealthy, "hc-healthy", 1, "threshold of success checks to declare node healthy") flag.IntVar(&conf.HealthcheckHealthy, "hc-healthy", 1, "threshold of success checks to declare node healthy")
flag.IntVar(&conf.HealthcheckTimeout, "hc-timeout", 5, "timeout of healthcheck endpoint, in seconds") flag.IntVar(&conf.HealthcheckTimeout, "hc-timeout", 5, "timeout of healthcheck endpoint, in seconds")
flag.StringVar(&conf.ZipkinURL, "zipkin", "", "zipkin endpoint to send traces") flag.StringVar(&conf.ZipkinURL, "zipkin", "", "zipkin endpoint to send traces")
flag.StringVar(&conf.Namespace, "namespace", "", "kubernetes namespace to monitor")
flag.StringVar(&conf.LabelSelector, "label-selector", "", "kubernetes label selector to monitor")
flag.IntVar(&conf.TargetPort, "target-port", 8080, "kubernetes port to target on selected pods")
flag.Parse() flag.Parse()
conf.MinAPIVersion = semver.New(*minAPIVersion) conf.MinAPIVersion = semver.New(*minAPIVersion)
@@ -57,10 +67,11 @@ func main() {
}, },
} }
db, err := lb.NewDB(conf) db, err := lb.NewDB(conf) // Handles case where DBurl == "k8s"
if err != nil { if err != nil {
logrus.WithError(err).Fatal("error setting up database") logrus.WithError(err).Fatal("error setting up database")
} }
defer db.Close()
g, err := lb.NewAllGrouper(conf, db) g, err := lb.NewAllGrouper(conf, db)
if err != nil { if err != nil {

203
glide.lock generated
View File

@@ -1,5 +1,5 @@
hash: f5176ffa46ec8ac2a1900aee611589c59b5404e9fc668ec3e833dc67c0ad842d hash: 3fa5b86f68121ba1422761f2937e6a9b7f5c330adc6f84e2f3b7b78b87a747d7
updated: 2017-11-15T14:57:38.939238313-08:00 updated: 2017-12-04T17:26:34.506649Z
imports: imports:
- name: github.com/amir/raidman - name: github.com/amir/raidman
version: 1ccc43bfb9c93cb401a4025e49c64ba71e5e668b version: 1ccc43bfb9c93cb401a4025e49c64ba71e5e668b
@@ -23,8 +23,6 @@ imports:
version: ce77ca9ecdee1c3ffd097e32f9bb832825ccb203 version: ce77ca9ecdee1c3ffd097e32f9bb832825ccb203
subpackages: subpackages:
- statsd - statsd
- name: github.com/cenkalti/backoff
version: 80e08cb804a3eb3e576876c777e957e874609a9a
- name: github.com/cloudflare/cfssl - name: github.com/cloudflare/cfssl
version: 7d88da830aad9d533c2fb8532da23f6a75331b52 version: 7d88da830aad9d533c2fb8532da23f6a75331b52
subpackages: subpackages:
@@ -52,7 +50,7 @@ imports:
subpackages: subpackages:
- semver - semver
- name: github.com/davecgh/go-spew - name: github.com/davecgh/go-spew
version: a476722483882dd40b8111f0eb64e1d7f43f56e4 version: 5215b55f46b2b919f50a1df0eaa5886afe4e3b3d
subpackages: subpackages:
- spew - spew
- name: github.com/dchest/siphash - name: github.com/dchest/siphash
@@ -63,8 +61,6 @@ imports:
- twitter - twitter
- name: github.com/dghubble/oauth1 - name: github.com/dghubble/oauth1
version: 7d51c10e15ca32917b32ce43f6e25840d6951db4 version: 7d51c10e15ca32917b32ce43f6e25840d6951db4
- name: github.com/dghubble/sling
version: 80ec33c6152a53edb5545864ca37567b506c4ca5
- name: github.com/dgrijalva/jwt-go - name: github.com/dgrijalva/jwt-go
version: a539ee1a749a2b895533f979515ac7e6e0f5b650 version: a539ee1a749a2b895533f979515ac7e6e0f5b650
- name: github.com/docker/cli - name: github.com/docker/cli
@@ -167,6 +163,12 @@ imports:
version: bb955e01b9346ac19dc29eb16586c90ded99a98c version: bb955e01b9346ac19dc29eb16586c90ded99a98c
- name: github.com/eapache/queue - name: github.com/eapache/queue
version: 44cc805cf13205b55f69e14bcb69867d1ae92f98 version: 44cc805cf13205b55f69e14bcb69867d1ae92f98
- name: github.com/emicklei/go-restful
version: ff4f55a206334ef123e4f79bbf348980da81ca46
subpackages:
- log
- name: github.com/emicklei/go-restful-swagger12
version: dcef7f55730566d41eae5db10e7d6981829720f6
- name: github.com/fnproject/fn_go - name: github.com/fnproject/fn_go
version: 7ce3bb2e624df60cdfbfc1ee5483f6df80bb2b1b version: 7ce3bb2e624df60cdfbfc1ee5483f6df80bb2b1b
subpackages: subpackages:
@@ -176,8 +178,6 @@ imports:
- client/operations - client/operations
- client/routes - client/routes
- models - models
- name: github.com/fsnotify/fsnotify
version: 4da3e2cfbabc9f751898f250b49f2439785783a1
- name: github.com/fsouza/go-dockerclient - name: github.com/fsouza/go-dockerclient
version: 98edf3edfae6a6500fecc69d2bcccf1302544004 version: 98edf3edfae6a6500fecc69d2bcccf1302544004
- name: github.com/garyburd/redigo - name: github.com/garyburd/redigo
@@ -185,6 +185,8 @@ imports:
subpackages: subpackages:
- internal - internal
- redis - redis
- name: github.com/ghodss/yaml
version: 73d445a93680fa1a78ae23a5839bad48f32ba1ee
- name: github.com/gin-contrib/cors - name: github.com/gin-contrib/cors
version: cf4846e6a636a76237a28d9286f163c132e841bc version: cf4846e6a636a76237a28d9286f163c132e841bc
- name: github.com/gin-contrib/sse - name: github.com/gin-contrib/sse
@@ -206,7 +208,7 @@ imports:
- name: github.com/go-openapi/jsonpointer - name: github.com/go-openapi/jsonpointer
version: 779f45308c19820f1a69e9a4cd965f496e0da10f version: 779f45308c19820f1a69e9a4cd965f496e0da10f
- name: github.com/go-openapi/jsonreference - name: github.com/go-openapi/jsonreference
version: 36d33bfe519efae5632669801b180bf1a245da3b version: 13c6e3589ad90f49bd3e3bbe2c2cb3d7a4142272
- name: github.com/go-openapi/loads - name: github.com/go-openapi/loads
version: a80dea3052f00e5f032e860dd7355cd0cc67e24d version: a80dea3052f00e5f032e860dd7355cd0cc67e24d
subpackages: subpackages:
@@ -226,15 +228,17 @@ imports:
- name: github.com/go-sql-driver/mysql - name: github.com/go-sql-driver/mysql
version: 21d7e97c9f760ca685a01ecea202e1c84276daa1 version: 21d7e97c9f760ca685a01ecea202e1c84276daa1
- name: github.com/gogo/protobuf - name: github.com/gogo/protobuf
version: 100ba4e885062801d56799d78530b73b178a78f3 version: c0656edd0d9eab7c66d1eb0c568f9039345796f7
subpackages: subpackages:
- gogoproto - gogoproto
- proto - proto
- protoc-gen-gogo/descriptor - protoc-gen-gogo/descriptor
- sortkeys - sortkeys
- types - types
- name: github.com/golang/glog
version: 44145f04b68cf362d9c4df2182967c2275eaefed
- name: github.com/golang/protobuf - name: github.com/golang/protobuf
version: 5a0f697c9ed9d68fef0116532c6e05cfeae00e55 version: 4bd1920723d7b7c925de087aa32e2187708897f7
subpackages: subpackages:
- proto - proto
- ptypes/any - ptypes/any
@@ -252,10 +256,8 @@ imports:
- tls - tls
- x509 - x509
- x509/pkix - x509/pkix
- name: github.com/google/go-querystring - name: github.com/google/gofuzz
version: 53e6ce116135b80d037921a7fdd5138cf32d7a8a version: 44d81051d367757e1c7c6a5a86423ece9afcf63c
subpackages:
- query
- name: github.com/gorilla/context - name: github.com/gorilla/context
version: 08b5f424b9271eedf6f9f0ce86cb9396ed337a42 version: 08b5f424b9271eedf6f9f0ce86cb9396ed337a42
- name: github.com/gorilla/mux - name: github.com/gorilla/mux
@@ -267,20 +269,9 @@ imports:
- name: github.com/hashicorp/go-memdb - name: github.com/hashicorp/go-memdb
version: ec43fcf8f202880feb35d2abb40a570c1f4172e9 version: ec43fcf8f202880feb35d2abb40a570c1f4172e9
- name: github.com/hashicorp/golang-lru - name: github.com/hashicorp/golang-lru
version: 0a025b7e63adc15a622f29b0b2c4c3848243bbf6 version: a0d98a5f288019575c6d1f4bb1573fef2d1fcdc4
subpackages: subpackages:
- simplelru - simplelru
- name: github.com/hashicorp/hcl
version: 68e816d1c783414e79bc65b3994d9ab6b0a722ab
subpackages:
- hcl/ast
- hcl/parser
- hcl/scanner
- hcl/strconv
- hcl/token
- json/parser
- json/scanner
- json/token
- name: github.com/jmoiron/jsonq - name: github.com/jmoiron/jsonq
version: e874b168d07ecc7808bc950a17998a8aa3141d82 version: e874b168d07ecc7808bc950a17998a8aa3141d82
- name: github.com/jmoiron/sqlx - name: github.com/jmoiron/sqlx
@@ -289,16 +280,16 @@ imports:
- reflectx - reflectx
- name: github.com/json-iterator/go - name: github.com/json-iterator/go
version: fdfe0b9a69118ff692d6e1005e9de7e0cffb7d6b version: fdfe0b9a69118ff692d6e1005e9de7e0cffb7d6b
- name: github.com/juju/ratelimit
version: 5b9ff866471762aa2ab2dced63c9fb6f53921342
- name: github.com/kr/logfmt - name: github.com/kr/logfmt
version: b84e30acd515aadc4b783ad4ff83aff3299bdfe0 version: b84e30acd515aadc4b783ad4ff83aff3299bdfe0
- name: github.com/lib/pq - name: github.com/lib/pq
version: 23da1db4f16d9658a86ae9b717c245fc078f10f1 version: 23da1db4f16d9658a86ae9b717c245fc078f10f1
subpackages: subpackages:
- oid - oid
- name: github.com/magiconair/properties
version: 8d7837e64d3c1ee4e54a880c5a920ab4316fc90a
- name: github.com/mailru/easyjson - name: github.com/mailru/easyjson
version: 2f5df55504ebc322e4d52d34df6a1f5b503bf26d version: 32fa128f234d041f196a9f3e0fea5ac9772c08e1
subpackages: subpackages:
- buffer - buffer
- jlexer - jlexer
@@ -362,8 +353,6 @@ imports:
- wire - wire
- name: github.com/patrickmn/go-cache - name: github.com/patrickmn/go-cache
version: a3647f8e31d79543b2d0f0ae2fe5c379d72cedc0 version: a3647f8e31d79543b2d0f0ae2fe5c379d72cedc0
- name: github.com/pelletier/go-toml
version: 1d6b12b7cb290426e27e6b4e38b89fcda3aeef03
- name: github.com/pierrec/lz4 - name: github.com/pierrec/lz4
version: 08c27939df1bd95e881e2c2367a749964ad1fceb version: 08c27939df1bd95e881e2c2367a749964ad1fceb
- name: github.com/pierrec/xxHash - name: github.com/pierrec/xxHash
@@ -392,9 +381,9 @@ imports:
subpackages: subpackages:
- xfs - xfs
- name: github.com/PuerkitoBio/purell - name: github.com/PuerkitoBio/purell
version: 7cf257f0a33260797b0febf39f95fccd86aab2a3 version: 8a290539e2e8629dbc4e6bad948158f790ec31f4
- name: github.com/PuerkitoBio/urlesc - name: github.com/PuerkitoBio/urlesc
version: de5bf2ad457846296e2031421a34e2568e304e35 version: 5bd2802263f21d8788851d5305584c82a5c75d7e
- name: github.com/rcrowley/go-metrics - name: github.com/rcrowley/go-metrics
version: 1f30fe9094a513ce4c700b9a54458bbb0c96996c version: 1f30fe9094a513ce4c700b9a54458bbb0c96996c
- name: github.com/rdallman/migrate - name: github.com/rdallman/migrate
@@ -411,21 +400,21 @@ imports:
version: 89742aefa4b206dcf400792f3bd35b542998eb3b version: 89742aefa4b206dcf400792f3bd35b542998eb3b
subpackages: subpackages:
- hooks/syslog - hooks/syslog
- name: github.com/spf13/pflag
version: 9ff6c6923cfffbcd502984b8e0c80539a94968b7
- name: github.com/ugorji/go - name: github.com/ugorji/go
version: 54210f4e076c57f351166f0ed60e67d3fca57a36 version: ded73eae5db7e7a0ef6f55aace87a2873c5d2b74
subpackages: subpackages:
- codec - codec
- name: golang.org/x/crypto - name: golang.org/x/crypto
version: 9f005a07e0d31d45e6656d241bb5c0f2efd4bc94 version: 94eea52f7b742c7cbe0b03b22f0c4c8631ece122
subpackages: subpackages:
- bcrypt
- blowfish
- ocsp - ocsp
- pkcs12 - pkcs12
- pkcs12/internal/rc2 - pkcs12/internal/rc2
- ssh/terminal - ssh/terminal
- name: golang.org/x/net - name: golang.org/x/net
version: 66aacef3dd8a676686c7ae3716979581e8b03c47 version: a8b9294777976932365dabb6640cf1468d95c70f
subpackages: subpackages:
- context - context
- context/ctxhttp - context/ctxhttp
@@ -436,14 +425,20 @@ imports:
- lex/httplex - lex/httplex
- trace - trace
- name: golang.org/x/sys - name: golang.org/x/sys
version: 07c182904dbd53199946ba614a412c61d3c548f5 version: 8b4580aae2a0dd0c231a45d3ccb8434ff533b840
subpackages: subpackages:
- unix - unix
- windows - windows
- name: golang.org/x/text - name: golang.org/x/text
version: b19bf474d317b857955b12035d2c5acb57ce8b01 version: 57961680700a5336d15015c8c50686ca5ba362a4
subpackages: subpackages:
- cases
- internal
- internal/tag
- language
- runes
- secure/bidirule - secure/bidirule
- secure/precis
- transform - transform
- unicode/bidi - unicode/bidi
- unicode/norm - unicode/norm
@@ -470,13 +465,131 @@ imports:
- transport - transport
- name: gopkg.in/go-playground/validator.v8 - name: gopkg.in/go-playground/validator.v8
version: 5f1438d3fca68893a817e4a66806cea46a9e4ebf version: 5f1438d3fca68893a817e4a66806cea46a9e4ebf
- name: gopkg.in/inf.v0
version: 3887ee99ecf07df5b447e9b00d9c0b2adaa9f3e4
- name: gopkg.in/mgo.v2 - name: gopkg.in/mgo.v2
version: 3f83fa5005286a7fe593b055f0d7771a7dce4655 version: 3f83fa5005286a7fe593b055f0d7771a7dce4655
subpackages: subpackages:
- bson - bson
- internal/json - internal/json
- internal/sasl
- internal/scram
- name: gopkg.in/yaml.v2 - name: gopkg.in/yaml.v2
version: eb3733d160e74a9c7e442f435eb3bea458e1d19f version: 53feefa2559fb8dfa8d81baad31be332c97d6c77
- name: k8s.io/apimachinery
version: 1fd2e63a9a370677308a42f24fd40c86438afddf
subpackages:
- pkg/api/equality
- pkg/api/errors
- pkg/api/meta
- pkg/api/resource
- pkg/apimachinery
- pkg/apimachinery/announced
- pkg/apimachinery/registered
- pkg/apis/meta/v1
- pkg/apis/meta/v1/unstructured
- pkg/apis/meta/v1alpha1
- pkg/conversion
- pkg/conversion/queryparams
- pkg/conversion/unstructured
- pkg/fields
- pkg/labels
- pkg/openapi
- pkg/runtime
- pkg/runtime/schema
- pkg/runtime/serializer
- pkg/runtime/serializer/json
- pkg/runtime/serializer/protobuf
- pkg/runtime/serializer/recognizer
- pkg/runtime/serializer/streaming
- pkg/runtime/serializer/versioning
- pkg/selection
- pkg/types
- pkg/util/clock
- pkg/util/diff
- pkg/util/errors
- pkg/util/framer
- pkg/util/intstr
- pkg/util/json
- pkg/util/net
- pkg/util/rand
- pkg/util/runtime
- pkg/util/sets
- pkg/util/validation
- pkg/util/validation/field
- pkg/util/wait
- pkg/util/yaml
- pkg/version
- pkg/watch
- third_party/forked/golang/reflect
- name: k8s.io/client-go
version: d92e8497f71b7b4e0494e5bd204b48d34bd6f254
subpackages:
- discovery
- kubernetes
- kubernetes/scheme
- kubernetes/typed/admissionregistration/v1alpha1
- kubernetes/typed/apps/v1beta1
- kubernetes/typed/authentication/v1
- kubernetes/typed/authentication/v1beta1
- kubernetes/typed/authorization/v1
- kubernetes/typed/authorization/v1beta1
- kubernetes/typed/autoscaling/v1
- kubernetes/typed/autoscaling/v2alpha1
- kubernetes/typed/batch/v1
- kubernetes/typed/batch/v2alpha1
- kubernetes/typed/certificates/v1beta1
- kubernetes/typed/core/v1
- kubernetes/typed/extensions/v1beta1
- kubernetes/typed/networking/v1
- kubernetes/typed/policy/v1beta1
- kubernetes/typed/rbac/v1alpha1
- kubernetes/typed/rbac/v1beta1
- kubernetes/typed/settings/v1alpha1
- kubernetes/typed/storage/v1
- kubernetes/typed/storage/v1beta1
- pkg/api
- pkg/api/v1
- pkg/api/v1/ref
- pkg/apis/admissionregistration
- pkg/apis/admissionregistration/v1alpha1
- pkg/apis/apps
- pkg/apis/apps/v1beta1
- pkg/apis/authentication
- pkg/apis/authentication/v1
- pkg/apis/authentication/v1beta1
- pkg/apis/authorization
- pkg/apis/authorization/v1
- pkg/apis/authorization/v1beta1
- pkg/apis/autoscaling
- pkg/apis/autoscaling/v1
- pkg/apis/autoscaling/v2alpha1
- pkg/apis/batch
- pkg/apis/batch/v1
- pkg/apis/batch/v2alpha1
- pkg/apis/certificates
- pkg/apis/certificates/v1beta1
- pkg/apis/extensions
- pkg/apis/extensions/v1beta1
- pkg/apis/networking
- pkg/apis/networking/v1
- pkg/apis/policy
- pkg/apis/policy/v1beta1
- pkg/apis/rbac
- pkg/apis/rbac/v1alpha1
- pkg/apis/rbac/v1beta1
- pkg/apis/settings
- pkg/apis/settings/v1alpha1
- pkg/apis/storage
- pkg/apis/storage/v1
- pkg/apis/storage/v1beta1
- pkg/util
- pkg/util/parsers
- pkg/version
- rest
- rest/watch
- tools/clientcmd/api
- tools/metrics
- transport
- util/cert
- util/flowcontrol
- util/integer
testImports: [] testImports: []

View File

@@ -35,7 +35,6 @@ import:
- redis - redis
- package: github.com/gin-gonic/gin - package: github.com/gin-gonic/gin
- package: github.com/rdallman/migrate - package: github.com/rdallman/migrate
# TODO change to mattes/migrate w/ https://github.com/mattes/migrate/pull/299
version: bc72eeb997c7334cb5f05f5aefd2d70bc34d71ef version: bc72eeb997c7334cb5f05f5aefd2d70bc34d71ef
- package: github.com/go-openapi/errors - package: github.com/go-openapi/errors
- package: github.com/go-openapi/loads - package: github.com/go-openapi/loads
@@ -75,5 +74,25 @@ import:
- package: github.com/prometheus/client_golang - package: github.com/prometheus/client_golang
- package: github.com/gin-contrib/cors - package: github.com/gin-contrib/cors
version: ~1.2.0 version: ~1.2.0
- package: k8s.io/client-go
version: ^v4.0.0
subpackages:
- kubernetes
- package: github.com/emicklei/go-restful-swagger12
- package: github.com/juju/ratelimit
- package: golang.org/x/sys
version: master
subpackages:
- unix
- package: golang.org/x/net
version: master
subpackages:
- http2
- package: golang.org/x/text
version: master
- package: github.com/mailru/easyjson
version: master
subpackages:
- jwriter
testImport: testImport:
- package: github.com/patrickmn/go-cache - package: github.com/patrickmn/go-cache

View File

@@ -1,9 +1,7 @@
language: go language: go
go: go:
- 1.4.x - 1.4
- 1.5.x - 1.5
- 1.6.x - 1.6
- 1.7.x
- 1.8.x
- tip - tip

View File

@@ -4,7 +4,7 @@ Purell is a tiny Go library to normalize URLs. It returns a pure URL. Pure-ell.
Based on the [wikipedia paper][wiki] and the [RFC 3986 document][rfc]. Based on the [wikipedia paper][wiki] and the [RFC 3986 document][rfc].
[![build status](https://travis-ci.org/PuerkitoBio/purell.svg?branch=master)](http://travis-ci.org/PuerkitoBio/purell) [![build status](https://secure.travis-ci.org/PuerkitoBio/purell.png)](http://travis-ci.org/PuerkitoBio/purell)
## Install ## Install
@@ -12,7 +12,6 @@ Based on the [wikipedia paper][wiki] and the [RFC 3986 document][rfc].
## Changelog ## Changelog
* **2016-11-14 (v1.1.0)** : IDN: Conform to RFC 5895: Fold character width (thanks to @beeker1121).
* **2016-07-27 (v1.0.0)** : Normalize IDN to ASCII (thanks to @zenovich). * **2016-07-27 (v1.0.0)** : Normalize IDN to ASCII (thanks to @zenovich).
* **2015-02-08** : Add fix for relative paths issue ([PR #5][pr5]) and add fix for unnecessary encoding of reserved characters ([see issue #7][iss7]). * **2015-02-08** : Add fix for relative paths issue ([PR #5][pr5]) and add fix for unnecessary encoding of reserved characters ([see issue #7][iss7]).
* **v0.2.0** : Add benchmarks, Attempt IDN support. * **v0.2.0** : Add benchmarks, Attempt IDN support.
@@ -173,7 +172,6 @@ And with `FlagsUnsafeGreedy`:
@opennota @opennota
@pchristopher1275 @pchristopher1275
@zenovich @zenovich
@beeker1121
## License ## License

View File

@@ -15,8 +15,8 @@ import (
"github.com/PuerkitoBio/urlesc" "github.com/PuerkitoBio/urlesc"
"golang.org/x/net/idna" "golang.org/x/net/idna"
"golang.org/x/text/secure/precis"
"golang.org/x/text/unicode/norm" "golang.org/x/text/unicode/norm"
"golang.org/x/text/width"
) )
// A set of normalization flags determines how a URL will // A set of normalization flags determines how a URL will
@@ -150,26 +150,22 @@ func MustNormalizeURLString(u string, f NormalizationFlags) string {
// NormalizeURLString returns the normalized string, or an error if it can't be parsed into an URL object. // NormalizeURLString returns the normalized string, or an error if it can't be parsed into an URL object.
// It takes an URL string as input, as well as the normalization flags. // It takes an URL string as input, as well as the normalization flags.
func NormalizeURLString(u string, f NormalizationFlags) (string, error) { func NormalizeURLString(u string, f NormalizationFlags) (string, error) {
parsed, err := url.Parse(u) if parsed, e := url.Parse(u); e != nil {
if err != nil { return "", e
return "", err } else {
options := make([]precis.Option, 1, 3)
options[0] = precis.IgnoreCase
if f&FlagLowercaseHost == FlagLowercaseHost {
options = append(options, precis.FoldCase())
}
options = append(options, precis.Norm(norm.NFC))
profile := precis.NewFreeform(options...)
if parsed.Host, e = idna.ToASCII(profile.NewTransformer().String(parsed.Host)); e != nil {
return "", e
}
return NormalizeURL(parsed, f), nil
} }
panic("Unreachable code.")
if f&FlagLowercaseHost == FlagLowercaseHost {
parsed.Host = strings.ToLower(parsed.Host)
}
// The idna package doesn't fully conform to RFC 5895
// (https://tools.ietf.org/html/rfc5895), so we do it here.
// Taken from Go 1.8 cycle source, courtesy of bradfitz.
// TODO: Remove when (if?) idna package conforms to RFC 5895.
parsed.Host = width.Fold.String(parsed.Host)
parsed.Host = norm.NFC.String(parsed.Host)
if parsed.Host, err = idna.ToASCII(parsed.Host); err != nil {
return "", err
}
return NormalizeURL(parsed, f), nil
} }
// NormalizeURL returns the normalized string. // NormalizeURL returns the normalized string.

View File

@@ -35,7 +35,6 @@ func TestUrlnorm(t *testing.T) {
"http://XBLA\u306eXbox.com": "http://xn--xblaxbox-jf4g.com", //test utf8 and unicode "http://XBLA\u306eXbox.com": "http://xn--xblaxbox-jf4g.com", //test utf8 and unicode
"http://президент.рф": "http://xn--d1abbgf6aiiy.xn--p1ai", "http://президент.рф": "http://xn--d1abbgf6aiiy.xn--p1ai",
"http://ПРЕЗИДЕНТ.РФ": "http://xn--d1abbgf6aiiy.xn--p1ai", "http://ПРЕЗИДЕНТ.РФ": "http://xn--d1abbgf6aiiy.xn--p1ai",
"http://ab¥ヲ₩○.com": "http://xn--ab-ida8983azmfnvs.com", //test width folding
"http://\u00e9.com": "http://xn--9ca.com", "http://\u00e9.com": "http://xn--9ca.com",
"http://e\u0301.com": "http://xn--9ca.com", "http://e\u0301.com": "http://xn--9ca.com",
"http://ja.wikipedia.org/wiki/%E3%82%AD%E3%83%A3%E3%82%BF%E3%83%94%E3%83%A9%E3%83%BC%E3%82%B8%E3%83%A3%E3%83%91%E3%83%B3": "http://ja.wikipedia.org/wiki/%E3%82%AD%E3%83%A3%E3%82%BF%E3%83%94%E3%83%A9%E3%83%BC%E3%82%B8%E3%83%A3%E3%83%91%E3%83%B3", "http://ja.wikipedia.org/wiki/%E3%82%AD%E3%83%A3%E3%82%BF%E3%83%94%E3%83%A9%E3%83%BC%E3%82%B8%E3%83%A3%E3%83%91%E3%83%B3": "http://ja.wikipedia.org/wiki/%E3%82%AD%E3%83%A3%E3%82%BF%E3%83%94%E3%83%A9%E3%83%BC%E3%82%B8%E3%83%A3%E3%83%91%E3%83%B3",

View File

@@ -1,11 +1,7 @@
language: go language: go
go: go:
- 1.4.x - 1.4
- 1.5.x
- 1.6.x
- 1.7.x
- 1.8.x
- tip - tip
install: install:

View File

@@ -1,4 +1,4 @@
urlesc [![Build Status](https://travis-ci.org/PuerkitoBio/urlesc.svg?branch=master)](https://travis-ci.org/PuerkitoBio/urlesc) [![GoDoc](http://godoc.org/github.com/PuerkitoBio/urlesc?status.svg)](http://godoc.org/github.com/PuerkitoBio/urlesc) urlesc [![Build Status](https://travis-ci.org/PuerkitoBio/urlesc.png?branch=master)](https://travis-ci.org/PuerkitoBio/urlesc) [![GoDoc](http://godoc.org/github.com/PuerkitoBio/urlesc?status.svg)](http://godoc.org/github.com/PuerkitoBio/urlesc)
====== ======
Package urlesc implements query escaping as per RFC 3986. Package urlesc implements query escaping as per RFC 3986.

View File

@@ -1,22 +0,0 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe

View File

@@ -1,10 +0,0 @@
language: go
go:
- 1.3.3
- 1.x
- tip
before_install:
- go get github.com/mattn/goveralls
- go get golang.org/x/tools/cmd/cover
script:
- $HOME/gopath/bin/goveralls -service=travis-ci

View File

@@ -1,20 +0,0 @@
The MIT License (MIT)
Copyright (c) 2014 Cenk Altı
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.

View File

@@ -1,30 +0,0 @@
# Exponential Backoff [![GoDoc][godoc image]][godoc] [![Build Status][travis image]][travis] [![Coverage Status][coveralls image]][coveralls]
This is a Go port of the exponential backoff algorithm from [Google's HTTP Client Library for Java][google-http-java-client].
[Exponential backoff][exponential backoff wiki]
is an algorithm that uses feedback to multiplicatively decrease the rate of some process,
in order to gradually find an acceptable rate.
The retries exponentially increase and stop increasing when a certain threshold is met.
## Usage
See https://godoc.org/github.com/cenkalti/backoff#pkg-examples
## Contributing
* I would like to keep this library as small as possible.
* Please don't send a PR without opening an issue and discussing it first.
* If proposed change is not a common use case, I will probably not accept it.
[godoc]: https://godoc.org/github.com/cenkalti/backoff
[godoc image]: https://godoc.org/github.com/cenkalti/backoff?status.png
[travis]: https://travis-ci.org/cenkalti/backoff
[travis image]: https://travis-ci.org/cenkalti/backoff.png?branch=master
[coveralls]: https://coveralls.io/github/cenkalti/backoff?branch=master
[coveralls image]: https://coveralls.io/repos/github/cenkalti/backoff/badge.svg?branch=master
[google-http-java-client]: https://github.com/google/google-http-java-client
[exponential backoff wiki]: http://en.wikipedia.org/wiki/Exponential_backoff
[advanced example]: https://godoc.org/github.com/cenkalti/backoff#example_

View File

@@ -1,66 +0,0 @@
// Package backoff implements backoff algorithms for retrying operations.
//
// Use Retry function for retrying operations that may fail.
// If Retry does not meet your needs,
// copy/paste the function into your project and modify as you wish.
//
// There is also Ticker type similar to time.Ticker.
// You can use it if you need to work with channels.
//
// See Examples section below for usage examples.
package backoff
import "time"
// BackOff is a backoff policy for retrying an operation.
type BackOff interface {
// NextBackOff returns the duration to wait before retrying the operation,
// or backoff.Stop to indicate that no more retries should be made.
//
// Example usage:
//
// duration := backoff.NextBackOff();
// if (duration == backoff.Stop) {
// // Do not retry operation.
// } else {
// // Sleep for duration and retry operation.
// }
//
NextBackOff() time.Duration
// Reset to initial state.
Reset()
}
// Stop indicates that no more retries should be made for use in NextBackOff().
const Stop time.Duration = -1
// ZeroBackOff is a fixed backoff policy whose backoff time is always zero,
// meaning that the operation is retried immediately without waiting, indefinitely.
type ZeroBackOff struct{}
func (b *ZeroBackOff) Reset() {}
func (b *ZeroBackOff) NextBackOff() time.Duration { return 0 }
// StopBackOff is a fixed backoff policy that always returns backoff.Stop for
// NextBackOff(), meaning that the operation should never be retried.
type StopBackOff struct{}
func (b *StopBackOff) Reset() {}
func (b *StopBackOff) NextBackOff() time.Duration { return Stop }
// ConstantBackOff is a backoff policy that always returns the same backoff delay.
// This is in contrast to an exponential backoff policy,
// which returns a delay that grows longer as you call NextBackOff() over and over again.
type ConstantBackOff struct {
Interval time.Duration
}
func (b *ConstantBackOff) Reset() {}
func (b *ConstantBackOff) NextBackOff() time.Duration { return b.Interval }
func NewConstantBackOff(d time.Duration) *ConstantBackOff {
return &ConstantBackOff{Interval: d}
}

View File

@@ -1,27 +0,0 @@
package backoff
import (
"testing"
"time"
)
func TestNextBackOffMillis(t *testing.T) {
subtestNextBackOff(t, 0, new(ZeroBackOff))
subtestNextBackOff(t, Stop, new(StopBackOff))
}
func subtestNextBackOff(t *testing.T, expectedValue time.Duration, backOffPolicy BackOff) {
for i := 0; i < 10; i++ {
next := backOffPolicy.NextBackOff()
if next != expectedValue {
t.Errorf("got: %d expected: %d", next, expectedValue)
}
}
}
func TestConstantBackOff(t *testing.T) {
backoff := NewConstantBackOff(time.Second)
if backoff.NextBackOff() != time.Second {
t.Error("invalid interval")
}
}

View File

@@ -1,60 +0,0 @@
package backoff
import (
"time"
"golang.org/x/net/context"
)
// BackOffContext is a backoff policy that stops retrying after the context
// is canceled.
type BackOffContext interface {
BackOff
Context() context.Context
}
type backOffContext struct {
BackOff
ctx context.Context
}
// WithContext returns a BackOffContext with context ctx
//
// ctx must not be nil
func WithContext(b BackOff, ctx context.Context) BackOffContext {
if ctx == nil {
panic("nil context")
}
if b, ok := b.(*backOffContext); ok {
return &backOffContext{
BackOff: b.BackOff,
ctx: ctx,
}
}
return &backOffContext{
BackOff: b,
ctx: ctx,
}
}
func ensureContext(b BackOff) BackOffContext {
if cb, ok := b.(BackOffContext); ok {
return cb
}
return WithContext(b, context.Background())
}
func (b *backOffContext) Context() context.Context {
return b.ctx
}
func (b *backOffContext) NextBackOff() time.Duration {
select {
case <-b.Context().Done():
return Stop
default:
return b.BackOff.NextBackOff()
}
}

View File

@@ -1,26 +0,0 @@
package backoff
import (
"testing"
"time"
"golang.org/x/net/context"
)
func TestContext(t *testing.T) {
b := NewConstantBackOff(time.Millisecond)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
cb := WithContext(b, ctx)
if cb.Context() != ctx {
t.Error("invalid context")
}
cancel()
if cb.NextBackOff() != Stop {
t.Error("invalid next back off")
}
}

View File

@@ -1,73 +0,0 @@
package backoff
import (
"log"
"golang.org/x/net/context"
)
func ExampleRetry() {
// An operation that may fail.
operation := func() error {
return nil // or an error
}
err := Retry(operation, NewExponentialBackOff())
if err != nil {
// Handle error.
return
}
// Operation is successful.
}
func ExampleRetryContext() {
// A context
ctx := context.Background()
// An operation that may fail.
operation := func() error {
return nil // or an error
}
b := WithContext(NewExponentialBackOff(), ctx)
err := Retry(operation, b)
if err != nil {
// Handle error.
return
}
// Operation is successful.
}
func ExampleTicker() {
// An operation that may fail.
operation := func() error {
return nil // or an error
}
ticker := NewTicker(NewExponentialBackOff())
var err error
// Ticks will continue to arrive when the previous operation is still running,
// so operations that take a while to fail could run in quick succession.
for _ = range ticker.C {
if err = operation(); err != nil {
log.Println(err, "will retry...")
continue
}
ticker.Stop()
break
}
if err != nil {
// Operation has failed.
return
}
// Operation is successful.
return
}

View File

@@ -1,156 +0,0 @@
package backoff
import (
"math/rand"
"time"
)
/*
ExponentialBackOff is a backoff implementation that increases the backoff
period for each retry attempt using a randomization function that grows exponentially.
NextBackOff() is calculated using the following formula:
randomized interval =
RetryInterval * (random value in range [1 - RandomizationFactor, 1 + RandomizationFactor])
In other words NextBackOff() will range between the randomization factor
percentage below and above the retry interval.
For example, given the following parameters:
RetryInterval = 2
RandomizationFactor = 0.5
Multiplier = 2
the actual backoff period used in the next retry attempt will range between 1 and 3 seconds,
multiplied by the exponential, that is, between 2 and 6 seconds.
Note: MaxInterval caps the RetryInterval and not the randomized interval.
If the time elapsed since an ExponentialBackOff instance is created goes past the
MaxElapsedTime, then the method NextBackOff() starts returning backoff.Stop.
The elapsed time can be reset by calling Reset().
Example: Given the following default arguments, for 10 tries the sequence will be,
and assuming we go over the MaxElapsedTime on the 10th try:
Request # RetryInterval (seconds) Randomized Interval (seconds)
1 0.5 [0.25, 0.75]
2 0.75 [0.375, 1.125]
3 1.125 [0.562, 1.687]
4 1.687 [0.8435, 2.53]
5 2.53 [1.265, 3.795]
6 3.795 [1.897, 5.692]
7 5.692 [2.846, 8.538]
8 8.538 [4.269, 12.807]
9 12.807 [6.403, 19.210]
10 19.210 backoff.Stop
Note: Implementation is not thread-safe.
*/
type ExponentialBackOff struct {
InitialInterval time.Duration
RandomizationFactor float64
Multiplier float64
MaxInterval time.Duration
// After MaxElapsedTime the ExponentialBackOff stops.
// It never stops if MaxElapsedTime == 0.
MaxElapsedTime time.Duration
Clock Clock
currentInterval time.Duration
startTime time.Time
random *rand.Rand
}
// Clock is an interface that returns current time for BackOff.
type Clock interface {
Now() time.Time
}
// Default values for ExponentialBackOff.
const (
DefaultInitialInterval = 500 * time.Millisecond
DefaultRandomizationFactor = 0.5
DefaultMultiplier = 1.5
DefaultMaxInterval = 60 * time.Second
DefaultMaxElapsedTime = 15 * time.Minute
)
// NewExponentialBackOff creates an instance of ExponentialBackOff using default values.
func NewExponentialBackOff() *ExponentialBackOff {
b := &ExponentialBackOff{
InitialInterval: DefaultInitialInterval,
RandomizationFactor: DefaultRandomizationFactor,
Multiplier: DefaultMultiplier,
MaxInterval: DefaultMaxInterval,
MaxElapsedTime: DefaultMaxElapsedTime,
Clock: SystemClock,
random: rand.New(rand.NewSource(time.Now().UnixNano())),
}
b.Reset()
return b
}
type systemClock struct{}
func (t systemClock) Now() time.Time {
return time.Now()
}
// SystemClock implements Clock interface that uses time.Now().
var SystemClock = systemClock{}
// Reset the interval back to the initial retry interval and restarts the timer.
func (b *ExponentialBackOff) Reset() {
b.currentInterval = b.InitialInterval
b.startTime = b.Clock.Now()
}
// NextBackOff calculates the next backoff interval using the formula:
// Randomized interval = RetryInterval +/- (RandomizationFactor * RetryInterval)
func (b *ExponentialBackOff) NextBackOff() time.Duration {
// Make sure we have not gone over the maximum elapsed time.
if b.MaxElapsedTime != 0 && b.GetElapsedTime() > b.MaxElapsedTime {
return Stop
}
defer b.incrementCurrentInterval()
if b.random == nil {
b.random = rand.New(rand.NewSource(time.Now().UnixNano()))
}
return getRandomValueFromInterval(b.RandomizationFactor, b.random.Float64(), b.currentInterval)
}
// GetElapsedTime returns the elapsed time since an ExponentialBackOff instance
// is created and is reset when Reset() is called.
//
// The elapsed time is computed using time.Now().UnixNano().
func (b *ExponentialBackOff) GetElapsedTime() time.Duration {
return b.Clock.Now().Sub(b.startTime)
}
// Increments the current interval by multiplying it with the multiplier.
func (b *ExponentialBackOff) incrementCurrentInterval() {
// Check for overflow, if overflow is detected set the current interval to the max interval.
if float64(b.currentInterval) >= float64(b.MaxInterval)/b.Multiplier {
b.currentInterval = b.MaxInterval
} else {
b.currentInterval = time.Duration(float64(b.currentInterval) * b.Multiplier)
}
}
// Returns a random value from the following interval:
// [randomizationFactor * currentInterval, randomizationFactor * currentInterval].
func getRandomValueFromInterval(randomizationFactor, random float64, currentInterval time.Duration) time.Duration {
var delta = randomizationFactor * float64(currentInterval)
var minInterval = float64(currentInterval) - delta
var maxInterval = float64(currentInterval) + delta
// Get a random value from the range [minInterval, maxInterval].
// The formula used below has a +1 because if the minInterval is 1 and the maxInterval is 3 then
// we want a 33% chance for selecting either 1, 2 or 3.
return time.Duration(minInterval + (random * (maxInterval - minInterval + 1)))
}

View File

@@ -1,108 +0,0 @@
package backoff
import (
"math"
"testing"
"time"
)
func TestBackOff(t *testing.T) {
var (
testInitialInterval = 500 * time.Millisecond
testRandomizationFactor = 0.1
testMultiplier = 2.0
testMaxInterval = 5 * time.Second
testMaxElapsedTime = 15 * time.Minute
)
exp := NewExponentialBackOff()
exp.InitialInterval = testInitialInterval
exp.RandomizationFactor = testRandomizationFactor
exp.Multiplier = testMultiplier
exp.MaxInterval = testMaxInterval
exp.MaxElapsedTime = testMaxElapsedTime
exp.Reset()
var expectedResults = []time.Duration{500, 1000, 2000, 4000, 5000, 5000, 5000, 5000, 5000, 5000}
for i, d := range expectedResults {
expectedResults[i] = d * time.Millisecond
}
for _, expected := range expectedResults {
assertEquals(t, expected, exp.currentInterval)
// Assert that the next backoff falls in the expected range.
var minInterval = expected - time.Duration(testRandomizationFactor*float64(expected))
var maxInterval = expected + time.Duration(testRandomizationFactor*float64(expected))
var actualInterval = exp.NextBackOff()
if !(minInterval <= actualInterval && actualInterval <= maxInterval) {
t.Error("error")
}
}
}
func TestGetRandomizedInterval(t *testing.T) {
// 33% chance of being 1.
assertEquals(t, 1, getRandomValueFromInterval(0.5, 0, 2))
assertEquals(t, 1, getRandomValueFromInterval(0.5, 0.33, 2))
// 33% chance of being 2.
assertEquals(t, 2, getRandomValueFromInterval(0.5, 0.34, 2))
assertEquals(t, 2, getRandomValueFromInterval(0.5, 0.66, 2))
// 33% chance of being 3.
assertEquals(t, 3, getRandomValueFromInterval(0.5, 0.67, 2))
assertEquals(t, 3, getRandomValueFromInterval(0.5, 0.99, 2))
}
type TestClock struct {
i time.Duration
start time.Time
}
func (c *TestClock) Now() time.Time {
t := c.start.Add(c.i)
c.i += time.Second
return t
}
func TestGetElapsedTime(t *testing.T) {
var exp = NewExponentialBackOff()
exp.Clock = &TestClock{}
exp.Reset()
var elapsedTime = exp.GetElapsedTime()
if elapsedTime != time.Second {
t.Errorf("elapsedTime=%d", elapsedTime)
}
}
func TestMaxElapsedTime(t *testing.T) {
var exp = NewExponentialBackOff()
exp.Clock = &TestClock{start: time.Time{}.Add(10000 * time.Second)}
// Change the currentElapsedTime to be 0 ensuring that the elapsed time will be greater
// than the max elapsed time.
exp.startTime = time.Time{}
assertEquals(t, Stop, exp.NextBackOff())
}
func TestBackOffOverflow(t *testing.T) {
var (
testInitialInterval time.Duration = math.MaxInt64 / 2
testMaxInterval time.Duration = math.MaxInt64
testMultiplier = 2.1
)
exp := NewExponentialBackOff()
exp.InitialInterval = testInitialInterval
exp.Multiplier = testMultiplier
exp.MaxInterval = testMaxInterval
exp.Reset()
exp.NextBackOff()
// Assert that when an overflow is possible the current varerval time.Duration is set to the max varerval time.Duration .
assertEquals(t, testMaxInterval, exp.currentInterval)
}
func assertEquals(t *testing.T, expected, value time.Duration) {
if expected != value {
t.Errorf("got: %d, expected: %d", value, expected)
}
}

View File

@@ -1,78 +0,0 @@
package backoff
import "time"
// An Operation is executing by Retry() or RetryNotify().
// The operation will be retried using a backoff policy if it returns an error.
type Operation func() error
// Notify is a notify-on-error function. It receives an operation error and
// backoff delay if the operation failed (with an error).
//
// NOTE that if the backoff policy stated to stop retrying,
// the notify function isn't called.
type Notify func(error, time.Duration)
// Retry the operation o until it does not return error or BackOff stops.
// o is guaranteed to be run at least once.
// It is the caller's responsibility to reset b after Retry returns.
//
// If o returns a *PermanentError, the operation is not retried, and the
// wrapped error is returned.
//
// Retry sleeps the goroutine for the duration returned by BackOff after a
// failed operation returns.
func Retry(o Operation, b BackOff) error { return RetryNotify(o, b, nil) }
// RetryNotify calls notify function with the error and wait duration
// for each failed attempt before sleep.
func RetryNotify(operation Operation, b BackOff, notify Notify) error {
var err error
var next time.Duration
cb := ensureContext(b)
b.Reset()
for {
if err = operation(); err == nil {
return nil
}
if permanent, ok := err.(*PermanentError); ok {
return permanent.Err
}
if next = b.NextBackOff(); next == Stop {
return err
}
if notify != nil {
notify(err, next)
}
t := time.NewTimer(next)
select {
case <-cb.Context().Done():
t.Stop()
return err
case <-t.C:
}
}
}
// PermanentError signals that the operation should not be retried.
type PermanentError struct {
Err error
}
func (e *PermanentError) Error() string {
return e.Err.Error()
}
// Permanent wraps the given err in a *PermanentError.
func Permanent(err error) *PermanentError {
return &PermanentError{
Err: err,
}
}

View File

@@ -1,99 +0,0 @@
package backoff
import (
"errors"
"fmt"
"log"
"testing"
"time"
"golang.org/x/net/context"
)
func TestRetry(t *testing.T) {
const successOn = 3
var i = 0
// This function is successful on "successOn" calls.
f := func() error {
i++
log.Printf("function is called %d. time\n", i)
if i == successOn {
log.Println("OK")
return nil
}
log.Println("error")
return errors.New("error")
}
err := Retry(f, NewExponentialBackOff())
if err != nil {
t.Errorf("unexpected error: %s", err.Error())
}
if i != successOn {
t.Errorf("invalid number of retries: %d", i)
}
}
func TestRetryContext(t *testing.T) {
var cancelOn = 3
var i = 0
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// This function cancels context on "cancelOn" calls.
f := func() error {
i++
log.Printf("function is called %d. time\n", i)
// cancelling the context in the operation function is not a typical
// use-case, however it allows to get predictable test results.
if i == cancelOn {
cancel()
}
log.Println("error")
return fmt.Errorf("error (%d)", i)
}
err := Retry(f, WithContext(NewConstantBackOff(time.Millisecond), ctx))
if err == nil {
t.Errorf("error is unexpectedly nil")
}
if err.Error() != "error (3)" {
t.Errorf("unexpected error: %s", err.Error())
}
if i != cancelOn {
t.Errorf("invalid number of retries: %d", i)
}
}
func TestRetryPermenent(t *testing.T) {
const permanentOn = 3
var i = 0
// This function fails permanently after permanentOn tries
f := func() error {
i++
log.Printf("function is called %d. time\n", i)
if i == permanentOn {
log.Println("permanent error")
return Permanent(errors.New("permanent error"))
}
log.Println("error")
return errors.New("error")
}
err := Retry(f, NewExponentialBackOff())
if err == nil || err.Error() != "permanent error" {
t.Errorf("unexpected error: %s", err)
}
if i != permanentOn {
t.Errorf("invalid number of retries: %d", i)
}
}

View File

@@ -1,81 +0,0 @@
package backoff
import (
"runtime"
"sync"
"time"
)
// Ticker holds a channel that delivers `ticks' of a clock at times reported by a BackOff.
//
// Ticks will continue to arrive when the previous operation is still running,
// so operations that take a while to fail could run in quick succession.
type Ticker struct {
C <-chan time.Time
c chan time.Time
b BackOffContext
stop chan struct{}
stopOnce sync.Once
}
// NewTicker returns a new Ticker containing a channel that will send the time at times
// specified by the BackOff argument. Ticker is guaranteed to tick at least once.
// The channel is closed when Stop method is called or BackOff stops.
func NewTicker(b BackOff) *Ticker {
c := make(chan time.Time)
t := &Ticker{
C: c,
c: c,
b: ensureContext(b),
stop: make(chan struct{}),
}
go t.run()
runtime.SetFinalizer(t, (*Ticker).Stop)
return t
}
// Stop turns off a ticker. After Stop, no more ticks will be sent.
func (t *Ticker) Stop() {
t.stopOnce.Do(func() { close(t.stop) })
}
func (t *Ticker) run() {
c := t.c
defer close(c)
t.b.Reset()
// Ticker is guaranteed to tick at least once.
afterC := t.send(time.Now())
for {
if afterC == nil {
return
}
select {
case tick := <-afterC:
afterC = t.send(tick)
case <-t.stop:
t.c = nil // Prevent future ticks from being sent to the channel.
return
case <-t.b.Context().Done():
return
}
}
}
func (t *Ticker) send(tick time.Time) <-chan time.Time {
select {
case t.c <- tick:
case <-t.stop:
return nil
}
next := t.b.NextBackOff()
if next == Stop {
t.Stop()
return nil
}
return time.After(next)
}

View File

@@ -1,94 +0,0 @@
package backoff
import (
"errors"
"fmt"
"log"
"testing"
"time"
"golang.org/x/net/context"
)
func TestTicker(t *testing.T) {
const successOn = 3
var i = 0
// This function is successful on "successOn" calls.
f := func() error {
i++
log.Printf("function is called %d. time\n", i)
if i == successOn {
log.Println("OK")
return nil
}
log.Println("error")
return errors.New("error")
}
b := NewExponentialBackOff()
ticker := NewTicker(b)
var err error
for _ = range ticker.C {
if err = f(); err != nil {
t.Log(err)
continue
}
break
}
if err != nil {
t.Errorf("unexpected error: %s", err.Error())
}
if i != successOn {
t.Errorf("invalid number of retries: %d", i)
}
}
func TestTickerContext(t *testing.T) {
const cancelOn = 3
var i = 0
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// This function cancels context on "cancelOn" calls.
f := func() error {
i++
log.Printf("function is called %d. time\n", i)
// cancelling the context in the operation function is not a typical
// use-case, however it allows to get predictable test results.
if i == cancelOn {
cancel()
}
log.Println("error")
return fmt.Errorf("error (%d)", i)
}
b := WithContext(NewConstantBackOff(time.Millisecond), ctx)
ticker := NewTicker(b)
var err error
for _ = range ticker.C {
if err = f(); err != nil {
t.Log(err)
continue
}
break
}
if err == nil {
t.Errorf("error is unexpectedly nil")
}
if err.Error() != "error (3)" {
t.Errorf("unexpected error: %s", err.Error())
}
if i != cancelOn {
t.Errorf("invalid number of retries: %d", i)
}
}

View File

@@ -1,35 +0,0 @@
package backoff
import "time"
/*
WithMaxTries creates a wrapper around another BackOff, which will
return Stop if NextBackOff() has been called too many times since
the last time Reset() was called
Note: Implementation is not thread-safe.
*/
func WithMaxTries(b BackOff, max uint64) BackOff {
return &backOffTries{delegate: b, maxTries: max}
}
type backOffTries struct {
delegate BackOff
maxTries uint64
numTries uint64
}
func (b *backOffTries) NextBackOff() time.Duration {
if b.maxTries > 0 {
if b.maxTries <= b.numTries {
return Stop
}
b.numTries++
}
return b.delegate.NextBackOff()
}
func (b *backOffTries) Reset() {
b.numTries = 0
b.delegate.Reset()
}

View File

@@ -1,55 +0,0 @@
package backoff
import (
"math/rand"
"testing"
"time"
)
func TestMaxTriesHappy(t *testing.T) {
r := rand.New(rand.NewSource(time.Now().UnixNano()))
max := 17 + r.Intn(13)
bo := WithMaxTries(&ZeroBackOff{}, uint64(max))
// Load up the tries count, but reset should clear the record
for ix := 0; ix < max/2; ix++ {
bo.NextBackOff()
}
bo.Reset()
// Now fill the tries count all the way up
for ix := 0; ix < max; ix++ {
d := bo.NextBackOff()
if d == Stop {
t.Errorf("returned Stop on try %d", ix)
}
}
// We have now called the BackOff max number of times, we expect
// the next result to be Stop, even if we try it multiple times
for ix := 0; ix < 7; ix++ {
d := bo.NextBackOff()
if d != Stop {
t.Error("invalid next back off")
}
}
// Reset makes it all work again
bo.Reset()
d := bo.NextBackOff()
if d == Stop {
t.Error("returned Stop after reset")
}
}
func TestMaxTriesZero(t *testing.T) {
// It might not make sense, but its okay to send a zero
bo := WithMaxTries(&ZeroBackOff{}, uint64(0))
for ix := 0; ix < 11; ix++ {
d := bo.NextBackOff()
if d == Stop {
t.Errorf("returned Stop on try %d", ix)
}
}
}

View File

@@ -1,25 +1,11 @@
language: go language: go
go: go: 1.2
- 1.6.x
- 1.7.x
- 1.8.x
- 1.9.x
sudo: false
install: install:
- go get -v github.com/alecthomas/gometalinter - go get -v code.google.com/p/go.tools/cmd/cover
- gometalinter --install
script: script:
- export PATH=$PATH:$HOME/gopath/bin - go test -v -tags=disableunsafe ./spew
- export GORACE="halt_on_error=1" - go test -v -tags=testcgo ./spew -covermode=count -coverprofile=profile.cov
- test -z "$(gometalinter --disable-all
--enable=gofmt
--enable=golint
--enable=vet
--enable=gosimple
--enable=unconvert
--deadline=4m ./spew | tee /dev/stderr)"
- go test -v -race -tags safe ./spew
- go test -v -race -tags testcgo ./spew -covermode=atomic -coverprofile=profile.cov
after_success: after_success:
- go get -v github.com/mattn/goveralls - go get -v github.com/mattn/goveralls
- export PATH=$PATH:$HOME/gopath/bin
- goveralls -coverprofile=profile.cov -service=travis-ci - goveralls -coverprofile=profile.cov -service=travis-ci

View File

@@ -1,8 +1,6 @@
ISC License Copyright (c) 2012-2013 Dave Collins <dave@davec.name>
Copyright (c) 2012-2016 Dave Collins <dave@davec.name> Permission to use, copy, modify, and distribute this software for any
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies. copyright notice and this permission notice appear in all copies.

View File

@@ -1,9 +1,10 @@
go-spew go-spew
======= =======
[![Build Status](https://img.shields.io/travis/davecgh/go-spew.svg)](https://travis-ci.org/davecgh/go-spew) [![Build Status](https://travis-ci.org/davecgh/go-spew.png?branch=master)]
[![ISC License](http://img.shields.io/badge/license-ISC-blue.svg)](http://copyfree.org) (https://travis-ci.org/davecgh/go-spew) [![Coverage Status]
[![Coverage Status](https://img.shields.io/coveralls/davecgh/go-spew.svg)](https://coveralls.io/r/davecgh/go-spew?branch=master) (https://coveralls.io/repos/davecgh/go-spew/badge.png?branch=master)]
(https://coveralls.io/r/davecgh/go-spew?branch=master)
Go-spew implements a deep pretty printer for Go data structures to aid in Go-spew implements a deep pretty printer for Go data structures to aid in
debugging. A comprehensive suite of tests with 100% test coverage is provided debugging. A comprehensive suite of tests with 100% test coverage is provided
@@ -14,11 +15,12 @@ open source or commercial projects.
If you're interested in reading about how this package came to life and some If you're interested in reading about how this package came to life and some
of the challenges involved in providing a deep pretty printer, there is a blog of the challenges involved in providing a deep pretty printer, there is a blog
post about it post about it
[here](https://web.archive.org/web/20160304013555/https://blog.cyphertite.com/go-spew-a-journey-into-dumping-go-data-structures/). [here](https://blog.cyphertite.com/go-spew-a-journey-into-dumping-go-data-structures/).
## Documentation ## Documentation
[![GoDoc](https://img.shields.io/badge/godoc-reference-blue.svg)](http://godoc.org/github.com/davecgh/go-spew/spew) [![GoDoc](https://godoc.org/github.com/davecgh/go-spew/spew?status.png)]
(http://godoc.org/github.com/davecgh/go-spew/spew)
Full `go doc` style documentation for the project can be viewed online without Full `go doc` style documentation for the project can be viewed online without
installing this package by using the excellent GoDoc site here: installing this package by using the excellent GoDoc site here:
@@ -155,18 +157,9 @@ options. See the ConfigState documentation for more details.
which only accept pointer receivers from non-pointer variables. This option which only accept pointer receivers from non-pointer variables. This option
relies on access to the unsafe package, so it will not have any effect when 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 running in environments without access to the unsafe package such as Google
App Engine or with the "safe" build tag specified. App Engine or with the "disableunsafe" build tag specified.
Pointer method invocation is enabled by default. 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 * ContinueOnMethod
Enables recursion into types after invoking error and Stringer interface Enables recursion into types after invoking error and Stringer interface
methods. Recursion after method invocation is disabled by default. methods. Recursion after method invocation is disabled by default.
@@ -192,10 +185,10 @@ options. See the ConfigState documentation for more details.
This package relies on the unsafe package to perform some of the more advanced This package relies on the unsafe package to perform some of the more advanced
features, however it also supports a "limited" mode which allows it to work in features, however it also supports a "limited" mode which allows it to work in
environments where the unsafe package is not available. By default, it will environments where the unsafe package is not available. By default, it will
operate in this mode on Google App Engine and when compiled with GopherJS. The operate in this mode on Google App Engine. The "disableunsafe" build tag may
"safe" build tag may also be specified to force the package to build without also be specified to force the package to build without using the unsafe
using the unsafe package. package.
## License ## License
Go-spew is licensed under the [copyfree](http://copyfree.org) ISC License. Go-spew is licensed under the liberal ISC License.

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2015-2016 Dave Collins <dave@davec.name> // Copyright (c) 2015 Dave Collins <dave@davec.name>
// //
// Permission to use, copy, modify, and distribute this software for any // Permission to use, copy, modify, and distribute this software for any
// purpose with or without fee is hereby granted, provided that the above // purpose with or without fee is hereby granted, provided that the above
@@ -13,10 +13,9 @@
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
// NOTE: Due to the following build constraints, this file will only be compiled // 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 // when the code is not running on Google App Engine and "-tags disableunsafe"
// "-tags safe" is not added to the go build command line. The "disableunsafe" // is not added to the go build command line.
// tag is deprecated and thus should not be used. // +build !appengine,!disableunsafe
// +build !js,!appengine,!safe,!disableunsafe
package spew package spew
@@ -41,9 +40,9 @@ var (
// after commit 82f48826c6c7 which changed the format again to mirror // after commit 82f48826c6c7 which changed the format again to mirror
// the original format. Code in the init function updates these offsets // the original format. Code in the init function updates these offsets
// as necessary. // as necessary.
offsetPtr = ptrSize offsetPtr = uintptr(ptrSize)
offsetScalar = uintptr(0) offsetScalar = uintptr(0)
offsetFlag = ptrSize * 2 offsetFlag = uintptr(ptrSize * 2)
// flagKindWidth and flagKindShift indicate various bits that the // flagKindWidth and flagKindShift indicate various bits that the
// reflect package uses internally to track kind information. // reflect package uses internally to track kind information.
@@ -58,7 +57,7 @@ var (
// changed their positions. Code in the init function updates these // changed their positions. Code in the init function updates these
// flags as necessary. // flags as necessary.
flagKindWidth = uintptr(5) flagKindWidth = uintptr(5)
flagKindShift = flagKindWidth - 1 flagKindShift = uintptr(flagKindWidth - 1)
flagRO = uintptr(1 << 0) flagRO = uintptr(1 << 0)
flagIndir = uintptr(1 << 1) flagIndir = uintptr(1 << 1)
) )

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2015-2016 Dave Collins <dave@davec.name> // Copyright (c) 2015 Dave Collins <dave@davec.name>
// //
// Permission to use, copy, modify, and distribute this software for any // Permission to use, copy, modify, and distribute this software for any
// purpose with or without fee is hereby granted, provided that the above // purpose with or without fee is hereby granted, provided that the above
@@ -13,10 +13,9 @@
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
// NOTE: Due to the following build constraints, this file will only be compiled // 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 // when either the code is running on Google App Engine or "-tags disableunsafe"
// "-tags safe" is added to the go build command line. The "disableunsafe" // is added to the go build command line.
// tag is deprecated and thus should not be used. // +build appengine disableunsafe
// +build js appengine safe disableunsafe
package spew package spew

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name> * Copyright (c) 2013 Dave Collins <dave@davec.name>
* *
* Permission to use, copy, modify, and distribute this software for any * Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above * purpose with or without fee is hereby granted, provided that the above
@@ -180,7 +180,7 @@ func printComplex(w io.Writer, c complex128, floatPrecision int) {
w.Write(closeParenBytes) w.Write(closeParenBytes)
} }
// printHexPtr outputs a uintptr formatted as hexadecimal with a leading '0x' // printHexPtr outputs a uintptr formatted as hexidecimal with a leading '0x'
// prefix to Writer w. // prefix to Writer w.
func printHexPtr(w io.Writer, p uintptr) { func printHexPtr(w io.Writer, p uintptr) {
// Null pointer. // Null pointer.

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name> * Copyright (c) 2013 Dave Collins <dave@davec.name>
* *
* Permission to use, copy, modify, and distribute this software for any * Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above * purpose with or without fee is hereby granted, provided that the above

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name> * Copyright (c) 2013 Dave Collins <dave@davec.name>
* *
* Permission to use, copy, modify, and distribute this software for any * Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above * purpose with or without fee is hereby granted, provided that the above
@@ -64,18 +64,9 @@ type ConfigState struct {
// inside these interface methods. As a result, this option relies on // inside these interface methods. As a result, this option relies on
// access to the unsafe package, so it will not have any effect when // access to the unsafe package, so it will not have any effect when
// running in environments without access to the unsafe package such as // running in environments without access to the unsafe package such as
// Google App Engine or with the "safe" build tag specified. // Google App Engine or with the "disableunsafe" build tag specified.
DisablePointerMethods bool 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 // ContinueOnMethod specifies whether or not recursion should continue once
// a custom error or Stringer interface is invoked. The default, false, // a custom error or Stringer interface is invoked. The default, false,
// means it will print the results of invoking the custom error or Stringer // means it will print the results of invoking the custom error or Stringer

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name> * Copyright (c) 2013 Dave Collins <dave@davec.name>
* *
* Permission to use, copy, modify, and distribute this software for any * Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above * purpose with or without fee is hereby granted, provided that the above
@@ -91,15 +91,6 @@ The following configuration options are available:
which only accept pointer receivers from non-pointer variables. which only accept pointer receivers from non-pointer variables.
Pointer method invocation is enabled by default. 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 * ContinueOnMethod
Enables recursion into types after invoking error and Stringer interface Enables recursion into types after invoking error and Stringer interface
methods. Recursion after method invocation is disabled by default. methods. Recursion after method invocation is disabled by default.

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name> * Copyright (c) 2013 Dave Collins <dave@davec.name>
* *
* Permission to use, copy, modify, and distribute this software for any * Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above * purpose with or without fee is hereby granted, provided that the above
@@ -35,16 +35,16 @@ var (
// cCharRE is a regular expression that matches a cgo char. // cCharRE is a regular expression that matches a cgo char.
// It is used to detect character arrays to hexdump them. // It is used to detect character arrays to hexdump them.
cCharRE = regexp.MustCompile(`^.*\._Ctype_char$`) cCharRE = regexp.MustCompile("^.*\\._Ctype_char$")
// cUnsignedCharRE is a regular expression that matches a cgo unsigned // cUnsignedCharRE is a regular expression that matches a cgo unsigned
// char. It is used to detect unsigned character arrays to hexdump // char. It is used to detect unsigned character arrays to hexdump
// them. // them.
cUnsignedCharRE = regexp.MustCompile(`^.*\._Ctype_unsignedchar$`) cUnsignedCharRE = regexp.MustCompile("^.*\\._Ctype_unsignedchar$")
// cUint8tCharRE is a regular expression that matches a cgo uint8_t. // cUint8tCharRE is a regular expression that matches a cgo uint8_t.
// It is used to detect uint8_t arrays to hexdump them. // It is used to detect uint8_t arrays to hexdump them.
cUint8tCharRE = regexp.MustCompile(`^.*\._Ctype_uint8_t$`) cUint8tCharRE = regexp.MustCompile("^.*\\._Ctype_uint8_t$")
) )
// dumpState contains information about the state of a dump operation. // dumpState contains information about the state of a dump operation.
@@ -129,7 +129,7 @@ func (d *dumpState) dumpPtr(v reflect.Value) {
d.w.Write(closeParenBytes) d.w.Write(closeParenBytes)
// Display pointer information. // Display pointer information.
if !d.cs.DisablePointerAddresses && len(pointerChain) > 0 { if len(pointerChain) > 0 {
d.w.Write(openParenBytes) d.w.Write(openParenBytes)
for i, addr := range pointerChain { for i, addr := range pointerChain {
if i > 0 { if i > 0 {
@@ -143,10 +143,10 @@ func (d *dumpState) dumpPtr(v reflect.Value) {
// Display dereferenced value. // Display dereferenced value.
d.w.Write(openParenBytes) d.w.Write(openParenBytes)
switch { switch {
case nilFound: case nilFound == true:
d.w.Write(nilAngleBytes) d.w.Write(nilAngleBytes)
case cycleFound: case cycleFound == true:
d.w.Write(circularBytes) d.w.Write(circularBytes)
default: default:
@@ -282,13 +282,13 @@ func (d *dumpState) dump(v reflect.Value) {
case reflect.Map, reflect.String: case reflect.Map, reflect.String:
valueLen = v.Len() valueLen = v.Len()
} }
if valueLen != 0 || !d.cs.DisableCapacities && valueCap != 0 { if valueLen != 0 || valueCap != 0 {
d.w.Write(openParenBytes) d.w.Write(openParenBytes)
if valueLen != 0 { if valueLen != 0 {
d.w.Write(lenEqualsBytes) d.w.Write(lenEqualsBytes)
printInt(d.w, int64(valueLen), 10) printInt(d.w, int64(valueLen), 10)
} }
if !d.cs.DisableCapacities && valueCap != 0 { if valueCap != 0 {
if valueLen != 0 { if valueLen != 0 {
d.w.Write(spaceBytes) d.w.Write(spaceBytes)
} }

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name> * Copyright (c) 2013 Dave Collins <dave@davec.name>
* *
* Permission to use, copy, modify, and distribute this software for any * Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above * purpose with or without fee is hereby granted, provided that the above
@@ -70,7 +70,7 @@ import (
"github.com/davecgh/go-spew/spew" "github.com/davecgh/go-spew/spew"
) )
// dumpTest is used to describe a test to be performed against the Dump method. // dumpTest is used to describe a test to be perfomed against the Dump method.
type dumpTest struct { type dumpTest struct {
in interface{} in interface{}
wants []string wants []string

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2013-2016 Dave Collins <dave@davec.name> // Copyright (c) 2013 Dave Collins <dave@davec.name>
// //
// Permission to use, copy, modify, and distribute this software for any // Permission to use, copy, modify, and distribute this software for any
// purpose with or without fee is hereby granted, provided that the above // purpose with or without fee is hereby granted, provided that the above
@@ -59,11 +59,10 @@ func addCgoDumpTests() {
v3Len := fmt.Sprintf("%d", v3l) v3Len := fmt.Sprintf("%d", v3l)
v3Cap := fmt.Sprintf("%d", v3c) v3Cap := fmt.Sprintf("%d", v3c)
v3t := "[6]testdata._Ctype_unsignedchar" v3t := "[6]testdata._Ctype_unsignedchar"
v3t2 := "[6]testdata._Ctype_uchar"
v3s := "(len=" + v3Len + " cap=" + v3Cap + ") " + v3s := "(len=" + v3Len + " cap=" + v3Cap + ") " +
"{\n 00000000 74 65 73 74 33 00 " + "{\n 00000000 74 65 73 74 33 00 " +
" |test3.|\n}" " |test3.|\n}"
addDumpTest(v3, "("+v3t+") "+v3s+"\n", "("+v3t2+") "+v3s+"\n") addDumpTest(v3, "("+v3t+") "+v3s+"\n")
// C signed char array. // C signed char array.
v4, v4l, v4c := testdata.GetCgoSignedCharArray() v4, v4l, v4c := testdata.GetCgoSignedCharArray()

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name> * Copyright (c) 2013 Dave Collins <dave@davec.name>
* *
* Permission to use, copy, modify, and distribute this software for any * Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above * purpose with or without fee is hereby granted, provided that the above

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name> * Copyright (c) 2013 Dave Collins <dave@davec.name>
* *
* Permission to use, copy, modify, and distribute this software for any * Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above * purpose with or without fee is hereby granted, provided that the above
@@ -182,10 +182,10 @@ func (f *formatState) formatPtr(v reflect.Value) {
// Display dereferenced value. // Display dereferenced value.
switch { switch {
case nilFound: case nilFound == true:
f.fs.Write(nilAngleBytes) f.fs.Write(nilAngleBytes)
case cycleFound: case cycleFound == true:
f.fs.Write(circularShortBytes) f.fs.Write(circularShortBytes)
default: default:

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name> * Copyright (c) 2013 Dave Collins <dave@davec.name>
* *
* Permission to use, copy, modify, and distribute this software for any * Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above * purpose with or without fee is hereby granted, provided that the above
@@ -75,7 +75,7 @@ import (
"github.com/davecgh/go-spew/spew" "github.com/davecgh/go-spew/spew"
) )
// formatterTest is used to describe a test to be performed against NewFormatter. // formatterTest is used to describe a test to be perfomed against NewFormatter.
type formatterTest struct { type formatterTest struct {
format string format string
in interface{} in interface{}
@@ -1536,14 +1536,14 @@ func TestPrintSortedKeys(t *testing.T) {
t.Errorf("Sorted keys mismatch 3:\n %v %v", s, expected) t.Errorf("Sorted keys mismatch 3:\n %v %v", s, expected)
} }
s = cfg.Sprint(map[testStruct]int{{1}: 1, {3}: 3, {2}: 2}) s = cfg.Sprint(map[testStruct]int{testStruct{1}: 1, testStruct{3}: 3, testStruct{2}: 2})
expected = "map[ts.1:1 ts.2:2 ts.3:3]" expected = "map[ts.1:1 ts.2:2 ts.3:3]"
if s != expected { if s != expected {
t.Errorf("Sorted keys mismatch 4:\n %v %v", s, expected) t.Errorf("Sorted keys mismatch 4:\n %v %v", s, expected)
} }
if !spew.UnsafeDisabled { if !spew.UnsafeDisabled {
s = cfg.Sprint(map[testStructP]int{{1}: 1, {3}: 3, {2}: 2}) s = cfg.Sprint(map[testStructP]int{testStructP{1}: 1, testStructP{3}: 3, testStructP{2}: 2})
expected = "map[ts.1:1 ts.2:2 ts.3:3]" expected = "map[ts.1:1 ts.2:2 ts.3:3]"
if s != expected { if s != expected {
t.Errorf("Sorted keys mismatch 5:\n %v %v", s, expected) t.Errorf("Sorted keys mismatch 5:\n %v %v", s, expected)

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name> * Copyright (c) 2013 Dave Collins <dave@davec.name>
* *
* Permission to use, copy, modify, and distribute this software for any * Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above * purpose with or without fee is hereby granted, provided that the above
@@ -36,7 +36,10 @@ type dummyFmtState struct {
} }
func (dfs *dummyFmtState) Flag(f int) bool { func (dfs *dummyFmtState) Flag(f int) bool {
return f == int('+') if f == int('+') {
return true
}
return false
} }
func (dfs *dummyFmtState) Precision() (int, bool) { func (dfs *dummyFmtState) Precision() (int, bool) {

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2013-2016 Dave Collins <dave@davec.name> // Copyright (c) 2013-2015 Dave Collins <dave@davec.name>
// Permission to use, copy, modify, and distribute this software for any // Permission to use, copy, modify, and distribute this software for any
// purpose with or without fee is hereby granted, provided that the above // purpose with or without fee is hereby granted, provided that the above
@@ -13,10 +13,9 @@
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
// NOTE: Due to the following build constraints, this file will only be compiled // 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 // when the code is not running on Google App Engine and "-tags disableunsafe"
// "-tags safe" is not added to the go build command line. The "disableunsafe" // is not added to the go build command line.
// tag is deprecated and thus should not be used. // +build !appengine,!disableunsafe
// +build !js,!appengine,!safe,!disableunsafe
/* /*
This test file is part of the spew package rather than than the spew_test This test file is part of the spew package rather than than the spew_test

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name> * Copyright (c) 2013 Dave Collins <dave@davec.name>
* *
* Permission to use, copy, modify, and distribute this software for any * Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above * purpose with or without fee is hereby granted, provided that the above

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name> * Copyright (c) 2013 Dave Collins <dave@davec.name>
* *
* Permission to use, copy, modify, and distribute this software for any * Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above * purpose with or without fee is hereby granted, provided that the above
@@ -130,19 +130,12 @@ func initSpewTests() {
scsNoPmethods := &spew.ConfigState{Indent: " ", DisablePointerMethods: true} scsNoPmethods := &spew.ConfigState{Indent: " ", DisablePointerMethods: true}
scsMaxDepth := &spew.ConfigState{Indent: " ", MaxDepth: 1} scsMaxDepth := &spew.ConfigState{Indent: " ", MaxDepth: 1}
scsContinue := &spew.ConfigState{Indent: " ", ContinueOnMethod: true} scsContinue := &spew.ConfigState{Indent: " ", ContinueOnMethod: true}
scsNoPtrAddr := &spew.ConfigState{DisablePointerAddresses: true}
scsNoCap := &spew.ConfigState{DisableCapacities: true}
// Variables for tests on types which implement Stringer interface with and // Variables for tests on types which implement Stringer interface with and
// without a pointer receiver. // without a pointer receiver.
ts := stringer("test") ts := stringer("test")
tps := pstringer("test") tps := pstringer("test")
type ptrTester struct {
s *struct{}
}
tptr := &ptrTester{s: &struct{}{}}
// depthTester is used to test max depth handling for structs, array, slices // depthTester is used to test max depth handling for structs, array, slices
// and maps. // and maps.
type depthTester struct { type depthTester struct {
@@ -199,10 +192,6 @@ func initSpewTests() {
{scsContinue, fCSFprint, "", te, "(error: 10) 10"}, {scsContinue, fCSFprint, "", te, "(error: 10) 10"},
{scsContinue, fCSFdump, "", te, "(spew_test.customError) " + {scsContinue, fCSFdump, "", te, "(spew_test.customError) " +
"(error: 10) 10\n"}, "(error: 10) 10\n"},
{scsNoPtrAddr, fCSFprint, "", tptr, "<*>{<*>{}}"},
{scsNoPtrAddr, fCSSdump, "", tptr, "(*spew_test.ptrTester)({\ns: (*struct {})({\n})\n})\n"},
{scsNoCap, fCSSdump, "", make([]string, 0, 10), "([]string) {\n}\n"},
{scsNoCap, fCSSdump, "", make([]string, 1, 10), "([]string) (len=1) {\n(string) \"\"\n}\n"},
} }
} }

View File

@@ -1,10 +0,0 @@
language: go
go:
- 1.7
- 1.8
- tip
install:
- go get github.com/golang/lint/golint
- go get -v -t .
script:
- ./test

View File

@@ -1,58 +0,0 @@
# Sling Changelog
Notable changes between releases.
## Latest
## v1.1.0 (2016-12-19)
* Allow JSON decoding, regardless of response Content-Type (#26)
* Add `BodyProvider` interface and setter so request Body encoding can be customized (#23)
* Add `Doer` interface and setter so request sending behavior can be customized (#21)
* Add `SetBasicAuth` setter for Authorization headers (#16)
* Add Sling `Body` setter to set an `io.Reader` on the Request (#9)
## v1.0.0 (2015-05-23)
* Added support for receiving and decoding error JSON structs
* Renamed Sling `JsonBody` setter to `BodyJSON` (breaking)
* Renamed Sling `BodyStruct` setter to `BodyForm` (breaking)
* Renamed Sling fields `httpClient`, `method`, `rawURL`, and `header` to be internal (breaking)
* Changed `Do` and `Receive` to skip response JSON decoding if "application/json" Content-Type is missing
* Changed `Sling.Receive(v interface{})` to `Sling.Receive(successV, failureV interface{})` (breaking)
* Previously `Receive` attempted to decode the response Body in all cases
* Updated `Receive` will decode the response Body into successV for 2XX responses or decode the Body into failureV for other status codes. Pass a nil `successV` or `failureV` to skip JSON decoding into that value.
* To upgrade, pass nil for the `failureV` argument or consider defining a JSON tagged struct appropriate for the API endpoint. (e.g. `s.Receive(&issue, nil)`, `s.Receive(&issue, &githubError)`)
* To retain the old behavior, duplicate the first argument (e.g. s.Receive(&tweet, &tweet))
* Changed `Sling.Do(http.Request, v interface{})` to `Sling.Do(http.Request, successV, failureV interface{})` (breaking)
* See the changelog entry about `Receive`, the upgrade path is the same.
* Removed HEAD, GET, POST, PUT, PATCH, DELETE constants, no reason to export them (breaking)
## v0.4.0 (2015-04-26)
* Improved golint compliance
* Fixed typos and test printouts
## v0.3.0 (2015-04-21)
* Added BodyStruct method for setting a url encoded form body on the Request
* Added Add and Set methods for adding or setting Request Headers
* Added JsonBody method for setting JSON Request Body
* Improved examples and documentation
## v0.2.0 (2015-04-05)
* Added http.Client setter
* Added Sling.New() method to return a copy of a Sling
* Added Base setter and Path extension support
* Added method setters (Get, Post, Put, Patch, Delete, Head)
* Added support for encoding URL Query parameters
* Added example tiny Github API
* Changed v0.1.0 method signatures and names (breaking)
* Removed Go 1.0 support
## v0.1.0 (2015-04-01)
* Support decoding JSON responses.

View File

@@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2015 Dalton Hubble
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.

View File

@@ -1,294 +0,0 @@
# Sling [![Build Status](https://travis-ci.org/dghubble/sling.png?branch=master)](https://travis-ci.org/dghubble/sling) [![GoDoc](https://godoc.org/github.com/dghubble/sling?status.png)](https://godoc.org/github.com/dghubble/sling)
<img align="right" src="https://s3.amazonaws.com/dghubble/small-gopher-with-sling.png">
Sling is a Go HTTP client library for creating and sending API requests.
Slings store HTTP Request properties to simplify sending requests and decoding responses. Check [usage](#usage) or the [examples](examples) to learn how to compose a Sling into your API client.
### Features
* Method Setters: Get/Post/Put/Patch/Delete/Head
* Add or Set Request Headers
* Base/Path: Extend a Sling for different endpoints
* Encode structs into URL query parameters
* Encode a form or JSON into the Request Body
* Receive JSON success or failure responses
## Install
go get github.com/dghubble/sling
## Documentation
Read [GoDoc](https://godoc.org/github.com/dghubble/sling)
## Usage
Use a Sling to set path, method, header, query, or body properties and create an `http.Request`.
```go
type Params struct {
Count int `url:"count,omitempty"`
}
params := &Params{Count: 5}
req, err := sling.New().Get("https://example.com").QueryStruct(params).Request()
client.Do(req)
```
### Path
Use `Path` to set or extend the URL for created Requests. Extension means the path will be resolved relative to the existing URL.
```go
// creates a GET request to https://example.com/foo/bar
req, err := sling.New().Base("https://example.com/").Path("foo/").Path("bar").Request()
```
Use `Get`, `Post`, `Put`, `Patch`, `Delete`, or `Head` which are exactly the same as `Path` except they set the HTTP method too.
```go
req, err := sling.New().Post("http://upload.com/gophers")
```
### Headers
`Add` or `Set` headers for requests created by a Sling.
```go
s := sling.New().Base(baseUrl).Set("User-Agent", "Gophergram API Client")
req, err := s.New().Get("gophergram/list").Request()
```
### Query
#### QueryStruct
Define [url tagged structs](https://godoc.org/github.com/google/go-querystring/query). Use `QueryStruct` to encode a struct as query parameters on requests.
```go
// Github Issue Parameters
type IssueParams struct {
Filter string `url:"filter,omitempty"`
State string `url:"state,omitempty"`
Labels string `url:"labels,omitempty"`
Sort string `url:"sort,omitempty"`
Direction string `url:"direction,omitempty"`
Since string `url:"since,omitempty"`
}
```
```go
githubBase := sling.New().Base("https://api.github.com/").Client(httpClient)
path := fmt.Sprintf("repos/%s/%s/issues", owner, repo)
params := &IssueParams{Sort: "updated", State: "open"}
req, err := githubBase.New().Get(path).QueryStruct(params).Request()
```
### Body
#### JSON Body
Define [JSON tagged structs](https://golang.org/pkg/encoding/json/). Use `BodyJSON` to JSON encode a struct as the Body on requests.
```go
type IssueRequest struct {
Title string `json:"title,omitempty"`
Body string `json:"body,omitempty"`
Assignee string `json:"assignee,omitempty"`
Milestone int `json:"milestone,omitempty"`
Labels []string `json:"labels,omitempty"`
}
```
```go
githubBase := sling.New().Base("https://api.github.com/").Client(httpClient)
path := fmt.Sprintf("repos/%s/%s/issues", owner, repo)
body := &IssueRequest{
Title: "Test title",
Body: "Some issue",
}
req, err := githubBase.New().Post(path).BodyJSON(body).Request()
```
Requests will include an `application/json` Content-Type header.
#### Form Body
Define [url tagged structs](https://godoc.org/github.com/google/go-querystring/query). Use `BodyForm` to form url encode a struct as the Body on requests.
```go
type StatusUpdateParams struct {
Status string `url:"status,omitempty"`
InReplyToStatusId int64 `url:"in_reply_to_status_id,omitempty"`
MediaIds []int64 `url:"media_ids,omitempty,comma"`
}
```
```go
tweetParams := &StatusUpdateParams{Status: "writing some Go"}
req, err := twitterBase.New().Post(path).BodyForm(tweetParams).Request()
```
Requests will include an `application/x-www-form-urlencoded` Content-Type header.
#### Plain Body
Use `Body` to set a plain `io.Reader` on requests created by a Sling.
```go
body := strings.NewReader("raw body")
req, err := sling.New().Base("https://example.com").Body(body).Request()
```
Set a content type header, if desired (e.g. `Set("Content-Type", "text/plain")`).
### Extend a Sling
Each Sling creates a standard `http.Request` (e.g. with some path and query
params) each time `Request()` is called. You may wish to extend an existing Sling to minimize duplication (e.g. a common client or base url).
Each Sling instance provides a `New()` method which creates an independent copy, so setting properties on the child won't mutate the parent Sling.
```go
const twitterApi = "https://api.twitter.com/1.1/"
base := sling.New().Base(twitterApi).Client(authClient)
// statuses/show.json Sling
tweetShowSling := base.New().Get("statuses/show.json").QueryStruct(params)
req, err := tweetShowSling.Request()
// statuses/update.json Sling
tweetPostSling := base.New().Post("statuses/update.json").BodyForm(params)
req, err := tweetPostSling.Request()
```
Without the calls to `base.New()`, `tweetShowSling` and `tweetPostSling` would reference the base Sling and POST to
"https://api.twitter.com/1.1/statuses/show.json/statuses/update.json", which
is undesired.
Recap: If you wish to *extend* a Sling, create a new child copy with `New()`.
### Sending
#### Receive
Define a JSON struct to decode a type from 2XX success responses. Use `ReceiveSuccess(successV interface{})` to send a new Request and decode the response body into `successV` if it succeeds.
```go
// Github Issue (abbreviated)
type Issue struct {
Title string `json:"title"`
Body string `json:"body"`
}
```
```go
issues := new([]Issue)
resp, err := githubBase.New().Get(path).QueryStruct(params).ReceiveSuccess(issues)
fmt.Println(issues, resp, err)
```
Most APIs return failure responses with JSON error details. To decode these, define success and failure JSON structs. Use `Receive(successV, failureV interface{})` to send a new Request that will automatically decode the response into the `successV` for 2XX responses or into `failureV` for non-2XX responses.
```go
type GithubError struct {
Message string `json:"message"`
Errors []struct {
Resource string `json:"resource"`
Field string `json:"field"`
Code string `json:"code"`
} `json:"errors"`
DocumentationURL string `json:"documentation_url"`
}
```
```go
issues := new([]Issue)
githubError := new(GithubError)
resp, err := githubBase.New().Get(path).QueryStruct(params).Receive(issues, githubError)
fmt.Println(issues, githubError, resp, err)
```
Pass a nil `successV` or `failureV` argument to skip JSON decoding into that value.
### Modify a Request
Sling provides the raw http.Request so modifications can be made using standard net/http features. For example, in Go 1.7+ , add HTTP tracing to a request with a context:
```go
req, err := sling.New().Get("https://example.com").QueryStruct(params).Request()
// handle error
trace := &httptrace.ClientTrace{
DNSDone: func(dnsInfo httptrace.DNSDoneInfo) {
fmt.Printf("DNS Info: %+v\n", dnsInfo)
},
GotConn: func(connInfo httptrace.GotConnInfo) {
fmt.Printf("Got Conn: %+v\n", connInfo)
},
}
req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))
client.Do(req)
```
### Build an API
APIs typically define an endpoint (also called a service) for each type of resource. For example, here is a tiny Github IssueService which [lists](https://developer.github.com/v3/issues/#list-issues-for-a-repository) repository issues.
```go
const baseURL = "https://api.github.com/"
type IssueService struct {
sling *sling.Sling
}
func NewIssueService(httpClient *http.Client) *IssueService {
return &IssueService{
sling: sling.New().Client(httpClient).Base(baseURL),
}
}
func (s *IssueService) ListByRepo(owner, repo string, params *IssueListParams) ([]Issue, *http.Response, error) {
issues := new([]Issue)
githubError := new(GithubError)
path := fmt.Sprintf("repos/%s/%s/issues", owner, repo)
resp, err := s.sling.New().Get(path).QueryStruct(params).Receive(issues, githubError)
if err == nil {
err = githubError
}
return *issues, resp, err
}
```
## Example APIs using Sling
* Digits [dghubble/go-digits](https://github.com/dghubble/go-digits)
* GoSquared [drinkin/go-gosquared](https://github.com/drinkin/go-gosquared)
* Kala [ajvb/kala](https://github.com/ajvb/kala)
* Parse [fergstar/go-parse](https://github.com/fergstar/go-parse)
* Rdio [apriendeau/shares](https://github.com/apriendeau/shares)
* Swagger Generator [swagger-api/swagger-codegen](https://github.com/swagger-api/swagger-codegen)
* Twitter [dghubble/go-twitter](https://github.com/dghubble/go-twitter)
* Hacker News [mirceamironenco/go-hackernews](https://github.com/mirceamironenco/go-hackernews)
* Stacksmith [jesustinoco/go-smith](https://github.com/jesustinoco/go-smith)
Create a Pull Request to add a link to your own API.
## Motivation
Many client libraries follow the lead of [google/go-github](https://github.com/google/go-github) (our inspiration!), but do so by reimplementing logic common to all clients.
This project borrows and abstracts those ideas into a Sling, an agnostic component any API client can use for creating and sending requests.
## Contributing
See the [Contributing Guide](https://gist.github.com/dghubble/be682c123727f70bcfe7).
## License
[MIT License](LICENSE)

View File

@@ -1,68 +0,0 @@
package sling
import (
"bytes"
"encoding/json"
"io"
"strings"
goquery "github.com/google/go-querystring/query"
)
// BodyProvider provides Body content for http.Request attachment.
type BodyProvider interface {
// ContentType returns the Content-Type of the body.
ContentType() string
// Body returns the io.Reader body.
Body() (io.Reader, error)
}
// bodyProvider provides the wrapped body value as a Body for reqests.
type bodyProvider struct {
body io.Reader
}
func (p bodyProvider) ContentType() string {
return ""
}
func (p bodyProvider) Body() (io.Reader, error) {
return p.body, nil
}
// jsonBodyProvider encodes a JSON tagged struct value as a Body for requests.
// See https://golang.org/pkg/encoding/json/#MarshalIndent for details.
type jsonBodyProvider struct {
payload interface{}
}
func (p jsonBodyProvider) ContentType() string {
return jsonContentType
}
func (p jsonBodyProvider) Body() (io.Reader, error) {
buf := &bytes.Buffer{}
err := json.NewEncoder(buf).Encode(p.payload)
if err != nil {
return nil, err
}
return buf, nil
}
// formBodyProvider encodes a url tagged struct value as Body for requests.
// See https://godoc.org/github.com/google/go-querystring/query for details.
type formBodyProvider struct {
payload interface{}
}
func (p formBodyProvider) ContentType() string {
return formContentType
}
func (p formBodyProvider) Body() (io.Reader, error) {
values, err := goquery.Values(p.payload)
if err != nil {
return nil, err
}
return strings.NewReader(values.Encode()), nil
}

View File

@@ -1,179 +0,0 @@
/*
Package sling is a Go HTTP client library for creating and sending API requests.
Slings store HTTP Request properties to simplify sending requests and decoding
responses. Check the examples to learn how to compose a Sling into your API
client.
Usage
Use a Sling to set path, method, header, query, or body properties and create an
http.Request.
type Params struct {
Count int `url:"count,omitempty"`
}
params := &Params{Count: 5}
req, err := sling.New().Get("https://example.com").QueryStruct(params).Request()
client.Do(req)
Path
Use Path to set or extend the URL for created Requests. Extension means the
path will be resolved relative to the existing URL.
// creates a GET request to https://example.com/foo/bar
req, err := sling.New().Base("https://example.com/").Path("foo/").Path("bar").Request()
Use Get, Post, Put, Patch, Delete, or Head which are exactly the same as Path
except they set the HTTP method too.
req, err := sling.New().Post("http://upload.com/gophers")
Headers
Add or Set headers for requests created by a Sling.
s := sling.New().Base(baseUrl).Set("User-Agent", "Gophergram API Client")
req, err := s.New().Get("gophergram/list").Request()
QueryStruct
Define url parameter structs (https://godoc.org/github.com/google/go-querystring/query).
Use QueryStruct to encode a struct as query parameters on requests.
// Github Issue Parameters
type IssueParams struct {
Filter string `url:"filter,omitempty"`
State string `url:"state,omitempty"`
Labels string `url:"labels,omitempty"`
Sort string `url:"sort,omitempty"`
Direction string `url:"direction,omitempty"`
Since string `url:"since,omitempty"`
}
githubBase := sling.New().Base("https://api.github.com/").Client(httpClient)
path := fmt.Sprintf("repos/%s/%s/issues", owner, repo)
params := &IssueParams{Sort: "updated", State: "open"}
req, err := githubBase.New().Get(path).QueryStruct(params).Request()
Json Body
Define JSON tagged structs (https://golang.org/pkg/encoding/json/).
Use BodyJSON to JSON encode a struct as the Body on requests.
type IssueRequest struct {
Title string `json:"title,omitempty"`
Body string `json:"body,omitempty"`
Assignee string `json:"assignee,omitempty"`
Milestone int `json:"milestone,omitempty"`
Labels []string `json:"labels,omitempty"`
}
githubBase := sling.New().Base("https://api.github.com/").Client(httpClient)
path := fmt.Sprintf("repos/%s/%s/issues", owner, repo)
body := &IssueRequest{
Title: "Test title",
Body: "Some issue",
}
req, err := githubBase.New().Post(path).BodyJSON(body).Request()
Requests will include an "application/json" Content-Type header.
Form Body
Define url tagged structs (https://godoc.org/github.com/google/go-querystring/query).
Use BodyForm to form url encode a struct as the Body on requests.
type StatusUpdateParams struct {
Status string `url:"status,omitempty"`
InReplyToStatusId int64 `url:"in_reply_to_status_id,omitempty"`
MediaIds []int64 `url:"media_ids,omitempty,comma"`
}
tweetParams := &StatusUpdateParams{Status: "writing some Go"}
req, err := twitterBase.New().Post(path).BodyForm(tweetParams).Request()
Requests will include an "application/x-www-form-urlencoded" Content-Type
header.
Plain Body
Use Body to set a plain io.Reader on requests created by a Sling.
body := strings.NewReader("raw body")
req, err := sling.New().Base("https://example.com").Body(body).Request()
Set a content type header, if desired (e.g. Set("Content-Type", "text/plain")).
Extend a Sling
Each Sling generates an http.Request (say with some path and query params)
each time Request() is called, based on its state. When creating
different slings, you may wish to extend an existing Sling to minimize
duplication (e.g. a common client).
Each Sling instance provides a New() method which creates an independent copy,
so setting properties on the child won't mutate the parent Sling.
const twitterApi = "https://api.twitter.com/1.1/"
base := sling.New().Base(twitterApi).Client(authClient)
// statuses/show.json Sling
tweetShowSling := base.New().Get("statuses/show.json").QueryStruct(params)
req, err := tweetShowSling.Request()
// statuses/update.json Sling
tweetPostSling := base.New().Post("statuses/update.json").BodyForm(params)
req, err := tweetPostSling.Request()
Without the calls to base.New(), tweetShowSling and tweetPostSling would
reference the base Sling and POST to
"https://api.twitter.com/1.1/statuses/show.json/statuses/update.json", which
is undesired.
Recap: If you wish to extend a Sling, create a new child copy with New().
Receive
Define a JSON struct to decode a type from 2XX success responses. Use
ReceiveSuccess(successV interface{}) to send a new Request and decode the
response body into successV if it succeeds.
// Github Issue (abbreviated)
type Issue struct {
Title string `json:"title"`
Body string `json:"body"`
}
issues := new([]Issue)
resp, err := githubBase.New().Get(path).QueryStruct(params).ReceiveSuccess(issues)
fmt.Println(issues, resp, err)
Most APIs return failure responses with JSON error details. To decode these,
define success and failure JSON structs. Use
Receive(successV, failureV interface{}) to send a new Request that will
automatically decode the response into the successV for 2XX responses or into
failureV for non-2XX responses.
type GithubError struct {
Message string `json:"message"`
Errors []struct {
Resource string `json:"resource"`
Field string `json:"field"`
Code string `json:"code"`
} `json:"errors"`
DocumentationURL string `json:"documentation_url"`
}
issues := new([]Issue)
githubError := new(GithubError)
resp, err := githubBase.New().Get(path).QueryStruct(params).Receive(issues, githubError)
fmt.Println(issues, githubError, resp, err)
Pass a nil successV or failureV argument to skip JSON decoding into that value.
*/
package sling

View File

@@ -1,19 +0,0 @@
## Example API Client with Sling
Try the example Github API Client.
cd examples
go get .
List the public issues on the [github.com/golang/go](https://github.com/golang/go) repository.
go run github.go
To list your public and private Github issues, pass your [Github Access Token](https://github.com/settings/tokens)
go run github.go -access-token=xxx
or set the `GITHUB_ACCESS_TOKEN` environment variable.
For a complete Github API, see the excellent [google/go-github](https://github.com/google/go-github) package.

View File

@@ -1,161 +0,0 @@
package main
import (
"flag"
"fmt"
"log"
"net/http"
"os"
"github.com/coreos/pkg/flagutil"
"github.com/dghubble/sling"
"golang.org/x/oauth2"
)
const baseURL = "https://api.github.com/"
// Issue is a simplified Github issue
// https://developer.github.com/v3/issues/#response
type Issue struct {
ID int `json:"id"`
URL string `json:"url"`
Number int `json:"number"`
State string `json:"state"`
Title string `json:"title"`
Body string `json:"body"`
}
// GithubError represents a Github API error response
// https://developer.github.com/v3/#client-errors
type GithubError struct {
Message string `json:"message"`
Errors []struct {
Resource string `json:"resource"`
Field string `json:"field"`
Code string `json:"code"`
} `json:"errors"`
DocumentationURL string `json:"documentation_url"`
}
func (e GithubError) Error() string {
return fmt.Sprintf("github: %v %+v %v", e.Message, e.Errors, e.DocumentationURL)
}
// IssueRequest is a simplified issue request
// https://developer.github.com/v3/issues/#create-an-issue
type IssueRequest struct {
Title string `json:"title,omitempty"`
Body string `json:"body,omitempty"`
Assignee string `json:"assignee,omitempty"`
Milestone int `json:"milestone,omitempty"`
Labels []string `json:"labels,omitempty"`
}
// IssueListParams are the params for IssueService.List
// https://developer.github.com/v3/issues/#parameters
type IssueListParams struct {
Filter string `url:"filter,omitempty"`
State string `url:"state,omitempty"`
Labels string `url:"labels,omitempty"`
Sort string `url:"sort,omitempty"`
Direction string `url:"direction,omitempty"`
Since string `url:"since,omitempty"`
}
// Services
// IssueService provides methods for creating and reading issues.
type IssueService struct {
sling *sling.Sling
}
// NewIssueService returns a new IssueService.
func NewIssueService(httpClient *http.Client) *IssueService {
return &IssueService{
sling: sling.New().Client(httpClient).Base(baseURL),
}
}
// List returns the authenticated user's issues across repos and orgs.
func (s *IssueService) List(params *IssueListParams) ([]Issue, *http.Response, error) {
issues := new([]Issue)
githubError := new(GithubError)
resp, err := s.sling.New().Path("issues").QueryStruct(params).Receive(issues, githubError)
if err == nil {
err = githubError
}
return *issues, resp, err
}
// ListByRepo returns a repository's issues.
func (s *IssueService) ListByRepo(owner, repo string, params *IssueListParams) ([]Issue, *http.Response, error) {
issues := new([]Issue)
githubError := new(GithubError)
path := fmt.Sprintf("repos/%s/%s/issues", owner, repo)
resp, err := s.sling.New().Get(path).QueryStruct(params).Receive(issues, githubError)
if err == nil {
err = githubError
}
return *issues, resp, err
}
// Create creates a new issue on the specified repository.
func (s *IssueService) Create(owner, repo string, issueBody *IssueRequest) (*Issue, *http.Response, error) {
issue := new(Issue)
githubError := new(GithubError)
path := fmt.Sprintf("repos/%s/%s/issues", owner, repo)
resp, err := s.sling.New().Post(path).BodyJSON(issueBody).Receive(issue, githubError)
if err == nil {
err = githubError
}
return issue, resp, err
}
// Client to wrap services
// Client is a tiny Github client
type Client struct {
IssueService *IssueService
// other service endpoints...
}
// NewClient returns a new Client
func NewClient(httpClient *http.Client) *Client {
return &Client{
IssueService: NewIssueService(httpClient),
}
}
func main() {
// Github Unauthenticated API
client := NewClient(nil)
params := &IssueListParams{Sort: "updated"}
issues, _, _ := client.IssueService.ListByRepo("golang", "go", params)
fmt.Printf("Public golang/go Issues:\n%v\n", issues)
// Github OAuth2 API
flags := flag.NewFlagSet("github-example", flag.ExitOnError)
// -access-token=xxx or GITHUB_ACCESS_TOKEN env var
accessToken := flags.String("access-token", "", "Github Access Token")
flags.Parse(os.Args[1:])
flagutil.SetFlagsFromEnv(flags, "GITHUB")
if *accessToken == "" {
log.Fatal("Github Access Token required to list private issues")
}
config := &oauth2.Config{}
token := &oauth2.Token{AccessToken: *accessToken}
httpClient := config.Client(oauth2.NoContext, token)
client = NewClient(httpClient)
issues, _, _ = client.IssueService.List(params)
fmt.Printf("Your Github Issues:\n%v\n", issues)
// body := &IssueRequest{
// Title: "Test title",
// Body: "Some test issue",
// }
// issue, _, _ := client.IssueService.Create("dghubble", "temp", body)
// fmt.Println(issue)
}

View File

@@ -1,390 +0,0 @@
package sling
import (
"encoding/base64"
"encoding/json"
"io"
"net/http"
"net/url"
goquery "github.com/google/go-querystring/query"
)
const (
contentType = "Content-Type"
jsonContentType = "application/json"
formContentType = "application/x-www-form-urlencoded"
)
// Doer executes http requests. It is implemented by *http.Client. You can
// wrap *http.Client with layers of Doers to form a stack of client-side
// middleware.
type Doer interface {
Do(req *http.Request) (*http.Response, error)
}
// Sling is an HTTP Request builder and sender.
type Sling struct {
// http Client for doing requests
httpClient Doer
// HTTP method (GET, POST, etc.)
method string
// raw url string for requests
rawURL string
// stores key-values pairs to add to request's Headers
header http.Header
// url tagged query structs
queryStructs []interface{}
// body provider
bodyProvider BodyProvider
}
// New returns a new Sling with an http DefaultClient.
func New() *Sling {
return &Sling{
httpClient: http.DefaultClient,
method: "GET",
header: make(http.Header),
queryStructs: make([]interface{}, 0),
}
}
// New returns a copy of a Sling for creating a new Sling with properties
// from a parent Sling. For example,
//
// parentSling := sling.New().Client(client).Base("https://api.io/")
// fooSling := parentSling.New().Get("foo/")
// barSling := parentSling.New().Get("bar/")
//
// fooSling and barSling will both use the same client, but send requests to
// https://api.io/foo/ and https://api.io/bar/ respectively.
//
// Note that query and body values are copied so if pointer values are used,
// mutating the original value will mutate the value within the child Sling.
func (s *Sling) New() *Sling {
// copy Headers pairs into new Header map
headerCopy := make(http.Header)
for k, v := range s.header {
headerCopy[k] = v
}
return &Sling{
httpClient: s.httpClient,
method: s.method,
rawURL: s.rawURL,
header: headerCopy,
queryStructs: append([]interface{}{}, s.queryStructs...),
bodyProvider: s.bodyProvider,
}
}
// Http Client
// Client sets the http Client used to do requests. If a nil client is given,
// the http.DefaultClient will be used.
func (s *Sling) Client(httpClient *http.Client) *Sling {
if httpClient == nil {
return s.Doer(http.DefaultClient)
}
return s.Doer(httpClient)
}
// Doer sets the custom Doer implementation used to do requests.
// If a nil client is given, the http.DefaultClient will be used.
func (s *Sling) Doer(doer Doer) *Sling {
if doer == nil {
s.httpClient = http.DefaultClient
} else {
s.httpClient = doer
}
return s
}
// Method
// Head sets the Sling method to HEAD and sets the given pathURL.
func (s *Sling) Head(pathURL string) *Sling {
s.method = "HEAD"
return s.Path(pathURL)
}
// Get sets the Sling method to GET and sets the given pathURL.
func (s *Sling) Get(pathURL string) *Sling {
s.method = "GET"
return s.Path(pathURL)
}
// Post sets the Sling method to POST and sets the given pathURL.
func (s *Sling) Post(pathURL string) *Sling {
s.method = "POST"
return s.Path(pathURL)
}
// Put sets the Sling method to PUT and sets the given pathURL.
func (s *Sling) Put(pathURL string) *Sling {
s.method = "PUT"
return s.Path(pathURL)
}
// Patch sets the Sling method to PATCH and sets the given pathURL.
func (s *Sling) Patch(pathURL string) *Sling {
s.method = "PATCH"
return s.Path(pathURL)
}
// Delete sets the Sling method to DELETE and sets the given pathURL.
func (s *Sling) Delete(pathURL string) *Sling {
s.method = "DELETE"
return s.Path(pathURL)
}
// Header
// Add adds the key, value pair in Headers, appending values for existing keys
// to the key's values. Header keys are canonicalized.
func (s *Sling) Add(key, value string) *Sling {
s.header.Add(key, value)
return s
}
// Set sets the key, value pair in Headers, replacing existing values
// associated with key. Header keys are canonicalized.
func (s *Sling) Set(key, value string) *Sling {
s.header.Set(key, value)
return s
}
// SetBasicAuth sets the Authorization header to use HTTP Basic Authentication
// with the provided username and password. With HTTP Basic Authentication
// the provided username and password are not encrypted.
func (s *Sling) SetBasicAuth(username, password string) *Sling {
return s.Set("Authorization", "Basic "+basicAuth(username, password))
}
// basicAuth returns the base64 encoded username:password for basic auth copied
// from net/http.
func basicAuth(username, password string) string {
auth := username + ":" + password
return base64.StdEncoding.EncodeToString([]byte(auth))
}
// Url
// Base sets the rawURL. If you intend to extend the url with Path,
// baseUrl should be specified with a trailing slash.
func (s *Sling) Base(rawURL string) *Sling {
s.rawURL = rawURL
return s
}
// Path extends the rawURL with the given path by resolving the reference to
// an absolute URL. If parsing errors occur, the rawURL is left unmodified.
func (s *Sling) Path(path string) *Sling {
baseURL, baseErr := url.Parse(s.rawURL)
pathURL, pathErr := url.Parse(path)
if baseErr == nil && pathErr == nil {
s.rawURL = baseURL.ResolveReference(pathURL).String()
return s
}
return s
}
// QueryStruct appends the queryStruct to the Sling's queryStructs. The value
// pointed to by each queryStruct will be encoded as url query parameters on
// new requests (see Request()).
// The queryStruct argument should be a pointer to a url tagged struct. See
// https://godoc.org/github.com/google/go-querystring/query for details.
func (s *Sling) QueryStruct(queryStruct interface{}) *Sling {
if queryStruct != nil {
s.queryStructs = append(s.queryStructs, queryStruct)
}
return s
}
// Body
// Body sets the Sling's body. The body value will be set as the Body on new
// requests (see Request()).
// If the provided body is also an io.Closer, the request Body will be closed
// by http.Client methods.
func (s *Sling) Body(body io.Reader) *Sling {
if body == nil {
return s
}
return s.BodyProvider(bodyProvider{body: body})
}
// BodyProvider sets the Sling's body provider.
func (s *Sling) BodyProvider(body BodyProvider) *Sling {
if body == nil {
return s
}
s.bodyProvider = body
ct := body.ContentType()
if ct != "" {
s.Set(contentType, ct)
}
return s
}
// BodyJSON sets the Sling's bodyJSON. The value pointed to by the bodyJSON
// will be JSON encoded as the Body on new requests (see Request()).
// The bodyJSON argument should be a pointer to a JSON tagged struct. See
// https://golang.org/pkg/encoding/json/#MarshalIndent for details.
func (s *Sling) BodyJSON(bodyJSON interface{}) *Sling {
if bodyJSON == nil {
return s
}
return s.BodyProvider(jsonBodyProvider{payload: bodyJSON})
}
// BodyForm sets the Sling's bodyForm. The value pointed to by the bodyForm
// will be url encoded as the Body on new requests (see Request()).
// The bodyForm argument should be a pointer to a url tagged struct. See
// https://godoc.org/github.com/google/go-querystring/query for details.
func (s *Sling) BodyForm(bodyForm interface{}) *Sling {
if bodyForm == nil {
return s
}
return s.BodyProvider(formBodyProvider{payload: bodyForm})
}
// Requests
// Request returns a new http.Request created with the Sling properties.
// Returns any errors parsing the rawURL, encoding query structs, encoding
// the body, or creating the http.Request.
func (s *Sling) Request() (*http.Request, error) {
reqURL, err := url.Parse(s.rawURL)
if err != nil {
return nil, err
}
err = addQueryStructs(reqURL, s.queryStructs)
if err != nil {
return nil, err
}
var body io.Reader
if s.bodyProvider != nil {
body, err = s.bodyProvider.Body()
if err != nil {
return nil, err
}
}
req, err := http.NewRequest(s.method, reqURL.String(), body)
if err != nil {
return nil, err
}
addHeaders(req, s.header)
return req, err
}
// addQueryStructs parses url tagged query structs using go-querystring to
// encode them to url.Values and format them onto the url.RawQuery. Any
// query parsing or encoding errors are returned.
func addQueryStructs(reqURL *url.URL, queryStructs []interface{}) error {
urlValues, err := url.ParseQuery(reqURL.RawQuery)
if err != nil {
return err
}
// encodes query structs into a url.Values map and merges maps
for _, queryStruct := range queryStructs {
queryValues, err := goquery.Values(queryStruct)
if err != nil {
return err
}
for key, values := range queryValues {
for _, value := range values {
urlValues.Add(key, value)
}
}
}
// url.Values format to a sorted "url encoded" string, e.g. "key=val&foo=bar"
reqURL.RawQuery = urlValues.Encode()
return nil
}
// addHeaders adds the key, value pairs from the given http.Header to the
// request. Values for existing keys are appended to the keys values.
func addHeaders(req *http.Request, header http.Header) {
for key, values := range header {
for _, value := range values {
req.Header.Add(key, value)
}
}
}
// Sending
// ReceiveSuccess creates a new HTTP request and returns the response. Success
// responses (2XX) are JSON decoded into the value pointed to by successV.
// Any error creating the request, sending it, or decoding a 2XX response
// is returned.
func (s *Sling) ReceiveSuccess(successV interface{}) (*http.Response, error) {
return s.Receive(successV, nil)
}
// Receive creates a new HTTP request and returns the response. Success
// responses (2XX) are JSON decoded into the value pointed to by successV and
// other responses are JSON decoded into the value pointed to by failureV.
// Any error creating the request, sending it, or decoding the response is
// returned.
// Receive is shorthand for calling Request and Do.
func (s *Sling) Receive(successV, failureV interface{}) (*http.Response, error) {
req, err := s.Request()
if err != nil {
return nil, err
}
return s.Do(req, successV, failureV)
}
// Do sends an HTTP request and returns the response. Success responses (2XX)
// are JSON decoded into the value pointed to by successV and other responses
// are JSON decoded into the value pointed to by failureV.
// Any error sending the request or decoding the response is returned.
func (s *Sling) Do(req *http.Request, successV, failureV interface{}) (*http.Response, error) {
resp, err := s.httpClient.Do(req)
if err != nil {
return resp, err
}
// when err is nil, resp contains a non-nil resp.Body which must be closed
defer resp.Body.Close()
// Don't try to decode on 204s
if resp.StatusCode == 204 {
return resp, nil
}
// Decode from json
if successV != nil || failureV != nil {
err = decodeResponseJSON(resp, successV, failureV)
}
return resp, err
}
// decodeResponse decodes response Body into the value pointed to by successV
// if the response is a success (2XX) or into the value pointed to by failureV
// otherwise. If the successV or failureV argument to decode into is nil,
// decoding is skipped.
// Caller is responsible for closing the resp.Body.
func decodeResponseJSON(resp *http.Response, successV, failureV interface{}) error {
if code := resp.StatusCode; 200 <= code && code <= 299 {
if successV != nil {
return decodeResponseBodyJSON(resp, successV)
}
} else {
if failureV != nil {
return decodeResponseBodyJSON(resp, failureV)
}
}
return nil
}
// decodeResponseBodyJSON JSON decodes a Response Body into the value pointed
// to by v.
// Caller must provide a non-nil v and close the resp.Body.
func decodeResponseBodyJSON(resp *http.Response, v interface{}) error {
return json.NewDecoder(resp.Body).Decode(v)
}

View File

@@ -1,843 +0,0 @@
package sling
import (
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
"math"
"net/http"
"net/http/httptest"
"net/url"
"reflect"
"strings"
"testing"
)
type FakeParams struct {
KindName string `url:"kind_name"`
Count int `url:"count"`
}
// Url-tagged query struct
var paramsA = struct {
Limit int `url:"limit"`
}{
30,
}
var paramsB = FakeParams{KindName: "recent", Count: 25}
// Json-tagged model struct
type FakeModel struct {
Text string `json:"text,omitempty"`
FavoriteCount int64 `json:"favorite_count,omitempty"`
Temperature float64 `json:"temperature,omitempty"`
}
var modelA = FakeModel{Text: "note", FavoriteCount: 12}
func TestNew(t *testing.T) {
sling := New()
if sling.httpClient != http.DefaultClient {
t.Errorf("expected %v, got %v", http.DefaultClient, sling.httpClient)
}
if sling.header == nil {
t.Errorf("Header map not initialized with make")
}
if sling.queryStructs == nil {
t.Errorf("queryStructs not initialized with make")
}
}
func TestSlingNew(t *testing.T) {
fakeBodyProvider := jsonBodyProvider{FakeModel{}}
cases := []*Sling{
&Sling{httpClient: &http.Client{}, method: "GET", rawURL: "http://example.com"},
&Sling{httpClient: nil, method: "", rawURL: "http://example.com"},
&Sling{queryStructs: make([]interface{}, 0)},
&Sling{queryStructs: []interface{}{paramsA}},
&Sling{queryStructs: []interface{}{paramsA, paramsB}},
&Sling{bodyProvider: fakeBodyProvider},
&Sling{bodyProvider: fakeBodyProvider},
&Sling{bodyProvider: nil},
New().Add("Content-Type", "application/json"),
New().Add("A", "B").Add("a", "c").New(),
New().Add("A", "B").New().Add("a", "c"),
New().BodyForm(paramsB),
New().BodyForm(paramsB).New(),
}
for _, sling := range cases {
child := sling.New()
if child.httpClient != sling.httpClient {
t.Errorf("expected %v, got %v", sling.httpClient, child.httpClient)
}
if child.method != sling.method {
t.Errorf("expected %s, got %s", sling.method, child.method)
}
if child.rawURL != sling.rawURL {
t.Errorf("expected %s, got %s", sling.rawURL, child.rawURL)
}
// Header should be a copy of parent Sling header. For example, calling
// baseSling.Add("k","v") should not mutate previously created child Slings
if sling.header != nil {
// struct literal cases don't init Header in usual way, skip header check
if !reflect.DeepEqual(sling.header, child.header) {
t.Errorf("not DeepEqual: expected %v, got %v", sling.header, child.header)
}
sling.header.Add("K", "V")
if child.header.Get("K") != "" {
t.Errorf("child.header was a reference to original map, should be copy")
}
}
// queryStruct slice should be a new slice with a copy of the contents
if len(sling.queryStructs) > 0 {
// mutating one slice should not mutate the other
child.queryStructs[0] = nil
if sling.queryStructs[0] == nil {
t.Errorf("child.queryStructs was a re-slice, expected slice with copied contents")
}
}
// body should be copied
if child.bodyProvider != sling.bodyProvider {
t.Errorf("expected %v, got %v", sling.bodyProvider, child.bodyProvider)
}
}
}
func TestClientSetter(t *testing.T) {
developerClient := &http.Client{}
cases := []struct {
input *http.Client
expected *http.Client
}{
{nil, http.DefaultClient},
{developerClient, developerClient},
}
for _, c := range cases {
sling := New()
sling.Client(c.input)
if sling.httpClient != c.expected {
t.Errorf("input %v, expected %v, got %v", c.input, c.expected, sling.httpClient)
}
}
}
func TestDoerSetter(t *testing.T) {
developerClient := &http.Client{}
cases := []struct {
input Doer
expected Doer
}{
{nil, http.DefaultClient},
{developerClient, developerClient},
}
for _, c := range cases {
sling := New()
sling.Doer(c.input)
if sling.httpClient != c.expected {
t.Errorf("input %v, expected %v, got %v", c.input, c.expected, sling.httpClient)
}
}
}
func TestBaseSetter(t *testing.T) {
cases := []string{"http://a.io/", "http://b.io", "/path", "path", ""}
for _, base := range cases {
sling := New().Base(base)
if sling.rawURL != base {
t.Errorf("expected %s, got %s", base, sling.rawURL)
}
}
}
func TestPathSetter(t *testing.T) {
cases := []struct {
rawURL string
path string
expectedRawURL string
}{
{"http://a.io/", "foo", "http://a.io/foo"},
{"http://a.io/", "/foo", "http://a.io/foo"},
{"http://a.io", "foo", "http://a.io/foo"},
{"http://a.io", "/foo", "http://a.io/foo"},
{"http://a.io/foo/", "bar", "http://a.io/foo/bar"},
// rawURL should end in trailing slash if it is to be Path extended
{"http://a.io/foo", "bar", "http://a.io/bar"},
{"http://a.io/foo", "/bar", "http://a.io/bar"},
// path extension is absolute
{"http://a.io", "http://b.io/", "http://b.io/"},
{"http://a.io/", "http://b.io/", "http://b.io/"},
{"http://a.io", "http://b.io", "http://b.io"},
{"http://a.io/", "http://b.io", "http://b.io"},
// empty base, empty path
{"", "http://b.io", "http://b.io"},
{"http://a.io", "", "http://a.io"},
{"", "", ""},
}
for _, c := range cases {
sling := New().Base(c.rawURL).Path(c.path)
if sling.rawURL != c.expectedRawURL {
t.Errorf("expected %s, got %s", c.expectedRawURL, sling.rawURL)
}
}
}
func TestMethodSetters(t *testing.T) {
cases := []struct {
sling *Sling
expectedMethod string
}{
{New().Path("http://a.io"), "GET"},
{New().Head("http://a.io"), "HEAD"},
{New().Get("http://a.io"), "GET"},
{New().Post("http://a.io"), "POST"},
{New().Put("http://a.io"), "PUT"},
{New().Patch("http://a.io"), "PATCH"},
{New().Delete("http://a.io"), "DELETE"},
}
for _, c := range cases {
if c.sling.method != c.expectedMethod {
t.Errorf("expected method %s, got %s", c.expectedMethod, c.sling.method)
}
}
}
func TestAddHeader(t *testing.T) {
cases := []struct {
sling *Sling
expectedHeader map[string][]string
}{
{New().Add("authorization", "OAuth key=\"value\""), map[string][]string{"Authorization": []string{"OAuth key=\"value\""}}},
// header keys should be canonicalized
{New().Add("content-tYPE", "application/json").Add("User-AGENT", "sling"), map[string][]string{"Content-Type": []string{"application/json"}, "User-Agent": []string{"sling"}}},
// values for existing keys should be appended
{New().Add("A", "B").Add("a", "c"), map[string][]string{"A": []string{"B", "c"}}},
// Add should add to values for keys added by parent Slings
{New().Add("A", "B").Add("a", "c").New(), map[string][]string{"A": []string{"B", "c"}}},
{New().Add("A", "B").New().Add("a", "c"), map[string][]string{"A": []string{"B", "c"}}},
}
for _, c := range cases {
// type conversion from header to alias'd map for deep equality comparison
headerMap := map[string][]string(c.sling.header)
if !reflect.DeepEqual(c.expectedHeader, headerMap) {
t.Errorf("not DeepEqual: expected %v, got %v", c.expectedHeader, headerMap)
}
}
}
func TestSetHeader(t *testing.T) {
cases := []struct {
sling *Sling
expectedHeader map[string][]string
}{
// should replace existing values associated with key
{New().Add("A", "B").Set("a", "c"), map[string][]string{"A": []string{"c"}}},
{New().Set("content-type", "A").Set("Content-Type", "B"), map[string][]string{"Content-Type": []string{"B"}}},
// Set should replace values received by copying parent Slings
{New().Set("A", "B").Add("a", "c").New(), map[string][]string{"A": []string{"B", "c"}}},
{New().Add("A", "B").New().Set("a", "c"), map[string][]string{"A": []string{"c"}}},
}
for _, c := range cases {
// type conversion from Header to alias'd map for deep equality comparison
headerMap := map[string][]string(c.sling.header)
if !reflect.DeepEqual(c.expectedHeader, headerMap) {
t.Errorf("not DeepEqual: expected %v, got %v", c.expectedHeader, headerMap)
}
}
}
func TestBasicAuth(t *testing.T) {
cases := []struct {
sling *Sling
expectedAuth []string
}{
// basic auth: username & password
{New().SetBasicAuth("Aladdin", "open sesame"), []string{"Aladdin", "open sesame"}},
// empty username
{New().SetBasicAuth("", "secret"), []string{"", "secret"}},
// empty password
{New().SetBasicAuth("admin", ""), []string{"admin", ""}},
}
for _, c := range cases {
req, err := c.sling.Request()
if err != nil {
t.Errorf("unexpected error when building Request with .SetBasicAuth()")
}
username, password, ok := req.BasicAuth()
if !ok {
t.Errorf("basic auth missing when expected")
}
auth := []string{username, password}
if !reflect.DeepEqual(c.expectedAuth, auth) {
t.Errorf("not DeepEqual: expected %v, got %v", c.expectedAuth, auth)
}
}
}
func TestQueryStructSetter(t *testing.T) {
cases := []struct {
sling *Sling
expectedStructs []interface{}
}{
{New(), []interface{}{}},
{New().QueryStruct(nil), []interface{}{}},
{New().QueryStruct(paramsA), []interface{}{paramsA}},
{New().QueryStruct(paramsA).QueryStruct(paramsA), []interface{}{paramsA, paramsA}},
{New().QueryStruct(paramsA).QueryStruct(paramsB), []interface{}{paramsA, paramsB}},
{New().QueryStruct(paramsA).New(), []interface{}{paramsA}},
{New().QueryStruct(paramsA).New().QueryStruct(paramsB), []interface{}{paramsA, paramsB}},
}
for _, c := range cases {
if count := len(c.sling.queryStructs); count != len(c.expectedStructs) {
t.Errorf("expected length %d, got %d", len(c.expectedStructs), count)
}
check:
for _, expected := range c.expectedStructs {
for _, param := range c.sling.queryStructs {
if param == expected {
continue check
}
}
t.Errorf("expected to find %v in %v", expected, c.sling.queryStructs)
}
}
}
func TestBodyJSONSetter(t *testing.T) {
fakeModel := &FakeModel{}
fakeBodyProvider := jsonBodyProvider{payload: fakeModel}
cases := []struct {
initial BodyProvider
input interface{}
expected BodyProvider
}{
// json tagged struct is set as bodyJSON
{nil, fakeModel, fakeBodyProvider},
// nil argument to bodyJSON does not replace existing bodyJSON
{fakeBodyProvider, nil, fakeBodyProvider},
// nil bodyJSON remains nil
{nil, nil, nil},
}
for _, c := range cases {
sling := New()
sling.bodyProvider = c.initial
sling.BodyJSON(c.input)
if sling.bodyProvider != c.expected {
t.Errorf("expected %v, got %v", c.expected, sling.bodyProvider)
}
// Header Content-Type should be application/json if bodyJSON arg was non-nil
if c.input != nil && sling.header.Get(contentType) != jsonContentType {
t.Errorf("Incorrect or missing header, expected %s, got %s", jsonContentType, sling.header.Get(contentType))
} else if c.input == nil && sling.header.Get(contentType) != "" {
t.Errorf("did not expect a Content-Type header, got %s", sling.header.Get(contentType))
}
}
}
func TestBodyFormSetter(t *testing.T) {
fakeParams := FakeParams{KindName: "recent", Count: 25}
fakeBodyProvider := formBodyProvider{payload: fakeParams}
cases := []struct {
initial BodyProvider
input interface{}
expected BodyProvider
}{
// url tagged struct is set as bodyStruct
{nil, paramsB, fakeBodyProvider},
// nil argument to bodyStruct does not replace existing bodyStruct
{fakeBodyProvider, nil, fakeBodyProvider},
// nil bodyStruct remains nil
{nil, nil, nil},
}
for _, c := range cases {
sling := New()
sling.bodyProvider = c.initial
sling.BodyForm(c.input)
if sling.bodyProvider != c.expected {
t.Errorf("expected %v, got %v", c.expected, sling.bodyProvider)
}
// Content-Type should be application/x-www-form-urlencoded if bodyStruct was non-nil
if c.input != nil && sling.header.Get(contentType) != formContentType {
t.Errorf("Incorrect or missing header, expected %s, got %s", formContentType, sling.header.Get(contentType))
} else if c.input == nil && sling.header.Get(contentType) != "" {
t.Errorf("did not expect a Content-Type header, got %s", sling.header.Get(contentType))
}
}
}
func TestBodySetter(t *testing.T) {
fakeInput := ioutil.NopCloser(strings.NewReader("test"))
fakeBodyProvider := bodyProvider{body: fakeInput}
cases := []struct {
initial BodyProvider
input io.Reader
expected BodyProvider
}{
// nil body is overriden by a set body
{nil, fakeInput, fakeBodyProvider},
// initial body is not overriden by nil body
{fakeBodyProvider, nil, fakeBodyProvider},
// nil body is returned unaltered
{nil, nil, nil},
}
for _, c := range cases {
sling := New()
sling.bodyProvider = c.initial
sling.Body(c.input)
if sling.bodyProvider != c.expected {
t.Errorf("expected %v, got %v", c.expected, sling.bodyProvider)
}
}
}
func TestRequest_urlAndMethod(t *testing.T) {
cases := []struct {
sling *Sling
expectedMethod string
expectedURL string
expectedErr error
}{
{New().Base("http://a.io"), "GET", "http://a.io", nil},
{New().Path("http://a.io"), "GET", "http://a.io", nil},
{New().Get("http://a.io"), "GET", "http://a.io", nil},
{New().Put("http://a.io"), "PUT", "http://a.io", nil},
{New().Base("http://a.io/").Path("foo"), "GET", "http://a.io/foo", nil},
{New().Base("http://a.io/").Post("foo"), "POST", "http://a.io/foo", nil},
// if relative path is an absolute url, base is ignored
{New().Base("http://a.io").Path("http://b.io"), "GET", "http://b.io", nil},
{New().Path("http://a.io").Path("http://b.io"), "GET", "http://b.io", nil},
// last method setter takes priority
{New().Get("http://b.io").Post("http://a.io"), "POST", "http://a.io", nil},
{New().Post("http://a.io/").Put("foo/").Delete("bar"), "DELETE", "http://a.io/foo/bar", nil},
// last Base setter takes priority
{New().Base("http://a.io").Base("http://b.io"), "GET", "http://b.io", nil},
// Path setters are additive
{New().Base("http://a.io/").Path("foo/").Path("bar"), "GET", "http://a.io/foo/bar", nil},
{New().Path("http://a.io/").Path("foo/").Path("bar"), "GET", "http://a.io/foo/bar", nil},
// removes extra '/' between base and ref url
{New().Base("http://a.io/").Get("/foo"), "GET", "http://a.io/foo", nil},
}
for _, c := range cases {
req, err := c.sling.Request()
if err != c.expectedErr {
t.Errorf("expected error %v, got %v for %+v", c.expectedErr, err, c.sling)
}
if req.URL.String() != c.expectedURL {
t.Errorf("expected url %s, got %s for %+v", c.expectedURL, req.URL.String(), c.sling)
}
if req.Method != c.expectedMethod {
t.Errorf("expected method %s, got %s for %+v", c.expectedMethod, req.Method, c.sling)
}
}
}
func TestRequest_queryStructs(t *testing.T) {
cases := []struct {
sling *Sling
expectedURL string
}{
{New().Base("http://a.io").QueryStruct(paramsA), "http://a.io?limit=30"},
{New().Base("http://a.io").QueryStruct(paramsA).QueryStruct(paramsB), "http://a.io?count=25&kind_name=recent&limit=30"},
{New().Base("http://a.io/").Path("foo?path=yes").QueryStruct(paramsA), "http://a.io/foo?limit=30&path=yes"},
{New().Base("http://a.io").QueryStruct(paramsA).New(), "http://a.io?limit=30"},
{New().Base("http://a.io").QueryStruct(paramsA).New().QueryStruct(paramsB), "http://a.io?count=25&kind_name=recent&limit=30"},
}
for _, c := range cases {
req, _ := c.sling.Request()
if req.URL.String() != c.expectedURL {
t.Errorf("expected url %s, got %s for %+v", c.expectedURL, req.URL.String(), c.sling)
}
}
}
func TestRequest_body(t *testing.T) {
cases := []struct {
sling *Sling
expectedBody string // expected Body io.Reader as a string
expectedContentType string
}{
// BodyJSON
{New().BodyJSON(modelA), "{\"text\":\"note\",\"favorite_count\":12}\n", jsonContentType},
{New().BodyJSON(&modelA), "{\"text\":\"note\",\"favorite_count\":12}\n", jsonContentType},
{New().BodyJSON(&FakeModel{}), "{}\n", jsonContentType},
{New().BodyJSON(FakeModel{}), "{}\n", jsonContentType},
// BodyJSON overrides existing values
{New().BodyJSON(&FakeModel{}).BodyJSON(&FakeModel{Text: "msg"}), "{\"text\":\"msg\"}\n", jsonContentType},
// BodyForm
{New().BodyForm(paramsA), "limit=30", formContentType},
{New().BodyForm(paramsB), "count=25&kind_name=recent", formContentType},
{New().BodyForm(&paramsB), "count=25&kind_name=recent", formContentType},
// BodyForm overrides existing values
{New().BodyForm(paramsA).New().BodyForm(paramsB), "count=25&kind_name=recent", formContentType},
// Mixture of BodyJSON and BodyForm prefers body setter called last with a non-nil argument
{New().BodyForm(paramsB).New().BodyJSON(modelA), "{\"text\":\"note\",\"favorite_count\":12}\n", jsonContentType},
{New().BodyJSON(modelA).New().BodyForm(paramsB), "count=25&kind_name=recent", formContentType},
{New().BodyForm(paramsB).New().BodyJSON(nil), "count=25&kind_name=recent", formContentType},
{New().BodyJSON(modelA).New().BodyForm(nil), "{\"text\":\"note\",\"favorite_count\":12}\n", jsonContentType},
// Body
{New().Body(strings.NewReader("this-is-a-test")), "this-is-a-test", ""},
{New().Body(strings.NewReader("a")).Body(strings.NewReader("b")), "b", ""},
}
for _, c := range cases {
req, _ := c.sling.Request()
buf := new(bytes.Buffer)
buf.ReadFrom(req.Body)
// req.Body should have contained the expectedBody string
if value := buf.String(); value != c.expectedBody {
t.Errorf("expected Request.Body %s, got %s", c.expectedBody, value)
}
// Header Content-Type should be expectedContentType ("" means no contentType expected)
if actualHeader := req.Header.Get(contentType); actualHeader != c.expectedContentType && c.expectedContentType != "" {
t.Errorf("Incorrect or missing header, expected %s, got %s", c.expectedContentType, actualHeader)
}
}
}
func TestRequest_bodyNoData(t *testing.T) {
// test that Body is left nil when no bodyJSON or bodyStruct set
slings := []*Sling{
New(),
New().BodyJSON(nil),
New().BodyForm(nil),
}
for _, sling := range slings {
req, _ := sling.Request()
if req.Body != nil {
t.Errorf("expected nil Request.Body, got %v", req.Body)
}
// Header Content-Type should not be set when bodyJSON argument was nil or never called
if actualHeader := req.Header.Get(contentType); actualHeader != "" {
t.Errorf("did not expect a Content-Type header, got %s", actualHeader)
}
}
}
func TestRequest_bodyEncodeErrors(t *testing.T) {
cases := []struct {
sling *Sling
expectedErr error
}{
// check that Encode errors are propagated, illegal JSON field
{New().BodyJSON(FakeModel{Temperature: math.Inf(1)}), errors.New("json: unsupported value: +Inf")},
}
for _, c := range cases {
req, err := c.sling.Request()
if err == nil || err.Error() != c.expectedErr.Error() {
t.Errorf("expected error %v, got %v", c.expectedErr, err)
}
if req != nil {
t.Errorf("expected nil Request, got %+v", req)
}
}
}
func TestRequest_headers(t *testing.T) {
cases := []struct {
sling *Sling
expectedHeader map[string][]string
}{
{New().Add("authorization", "OAuth key=\"value\""), map[string][]string{"Authorization": []string{"OAuth key=\"value\""}}},
// header keys should be canonicalized
{New().Add("content-tYPE", "application/json").Add("User-AGENT", "sling"), map[string][]string{"Content-Type": []string{"application/json"}, "User-Agent": []string{"sling"}}},
// values for existing keys should be appended
{New().Add("A", "B").Add("a", "c"), map[string][]string{"A": []string{"B", "c"}}},
// Add should add to values for keys added by parent Slings
{New().Add("A", "B").Add("a", "c").New(), map[string][]string{"A": []string{"B", "c"}}},
{New().Add("A", "B").New().Add("a", "c"), map[string][]string{"A": []string{"B", "c"}}},
// Add and Set
{New().Add("A", "B").Set("a", "c"), map[string][]string{"A": []string{"c"}}},
{New().Set("content-type", "A").Set("Content-Type", "B"), map[string][]string{"Content-Type": []string{"B"}}},
// Set should replace values received by copying parent Slings
{New().Set("A", "B").Add("a", "c").New(), map[string][]string{"A": []string{"B", "c"}}},
{New().Add("A", "B").New().Set("a", "c"), map[string][]string{"A": []string{"c"}}},
}
for _, c := range cases {
req, _ := c.sling.Request()
// type conversion from Header to alias'd map for deep equality comparison
headerMap := map[string][]string(req.Header)
if !reflect.DeepEqual(c.expectedHeader, headerMap) {
t.Errorf("not DeepEqual: expected %v, got %v", c.expectedHeader, headerMap)
}
}
}
func TestAddQueryStructs(t *testing.T) {
cases := []struct {
rawurl string
queryStructs []interface{}
expected string
}{
{"http://a.io", []interface{}{}, "http://a.io"},
{"http://a.io", []interface{}{paramsA}, "http://a.io?limit=30"},
{"http://a.io", []interface{}{paramsA, paramsA}, "http://a.io?limit=30&limit=30"},
{"http://a.io", []interface{}{paramsA, paramsB}, "http://a.io?count=25&kind_name=recent&limit=30"},
// don't blow away query values on the rawURL (parsed into RawQuery)
{"http://a.io?initial=7", []interface{}{paramsA}, "http://a.io?initial=7&limit=30"},
}
for _, c := range cases {
reqURL, _ := url.Parse(c.rawurl)
addQueryStructs(reqURL, c.queryStructs)
if reqURL.String() != c.expected {
t.Errorf("expected %s, got %s", c.expected, reqURL.String())
}
}
}
// Sending
type APIError struct {
Message string `json:"message"`
Code int `json:"code"`
}
func TestDo_onSuccess(t *testing.T) {
const expectedText = "Some text"
const expectedFavoriteCount int64 = 24
client, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/success", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"text": "Some text", "favorite_count": 24}`)
})
sling := New().Client(client)
req, _ := http.NewRequest("GET", "http://example.com/success", nil)
model := new(FakeModel)
apiError := new(APIError)
resp, err := sling.Do(req, model, apiError)
if err != nil {
t.Errorf("expected nil, got %v", err)
}
if resp.StatusCode != 200 {
t.Errorf("expected %d, got %d", 200, resp.StatusCode)
}
if model.Text != expectedText {
t.Errorf("expected %s, got %s", expectedText, model.Text)
}
if model.FavoriteCount != expectedFavoriteCount {
t.Errorf("expected %d, got %d", expectedFavoriteCount, model.FavoriteCount)
}
}
func TestDo_onSuccessWithNilValue(t *testing.T) {
client, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/success", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"text": "Some text", "favorite_count": 24}`)
})
sling := New().Client(client)
req, _ := http.NewRequest("GET", "http://example.com/success", nil)
apiError := new(APIError)
resp, err := sling.Do(req, nil, apiError)
if err != nil {
t.Errorf("expected nil, got %v", err)
}
if resp.StatusCode != 200 {
t.Errorf("expected %d, got %d", 200, resp.StatusCode)
}
expected := &APIError{}
if !reflect.DeepEqual(expected, apiError) {
t.Errorf("failureV should not be populated, exepcted %v, got %v", expected, apiError)
}
}
func TestDo_onFailure(t *testing.T) {
const expectedMessage = "Invalid argument"
const expectedCode int = 215
client, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/failure", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(400)
fmt.Fprintf(w, `{"message": "Invalid argument", "code": 215}`)
})
sling := New().Client(client)
req, _ := http.NewRequest("GET", "http://example.com/failure", nil)
model := new(FakeModel)
apiError := new(APIError)
resp, err := sling.Do(req, model, apiError)
if err != nil {
t.Errorf("expected nil, got %v", err)
}
if resp.StatusCode != 400 {
t.Errorf("expected %d, got %d", 400, resp.StatusCode)
}
if apiError.Message != expectedMessage {
t.Errorf("expected %s, got %s", expectedMessage, apiError.Message)
}
if apiError.Code != expectedCode {
t.Errorf("expected %d, got %d", expectedCode, apiError.Code)
}
}
func TestDo_onFailureWithNilValue(t *testing.T) {
client, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/failure", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(420)
fmt.Fprintf(w, `{"message": "Enhance your calm", "code": 88}`)
})
sling := New().Client(client)
req, _ := http.NewRequest("GET", "http://example.com/failure", nil)
model := new(FakeModel)
resp, err := sling.Do(req, model, nil)
if err != nil {
t.Errorf("expected nil, got %v", err)
}
if resp.StatusCode != 420 {
t.Errorf("expected %d, got %d", 420, resp.StatusCode)
}
expected := &FakeModel{}
if !reflect.DeepEqual(expected, model) {
t.Errorf("successV should not be populated, exepcted %v, got %v", expected, model)
}
}
func TestReceive_success(t *testing.T) {
client, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/foo/submit", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "POST", r)
assertQuery(t, map[string]string{"kind_name": "vanilla", "count": "11"}, r)
assertPostForm(t, map[string]string{"kind_name": "vanilla", "count": "11"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"text": "Some text", "favorite_count": 24}`)
})
endpoint := New().Client(client).Base("http://example.com/").Path("foo/").Post("submit")
// encode url-tagged struct in query params and as post body for testing purposes
params := FakeParams{KindName: "vanilla", Count: 11}
model := new(FakeModel)
apiError := new(APIError)
resp, err := endpoint.New().QueryStruct(params).BodyForm(params).Receive(model, apiError)
if err != nil {
t.Errorf("expected nil, got %v", err)
}
if resp.StatusCode != 200 {
t.Errorf("expected %d, got %d", 200, resp.StatusCode)
}
expectedModel := &FakeModel{Text: "Some text", FavoriteCount: 24}
if !reflect.DeepEqual(expectedModel, model) {
t.Errorf("expected %v, got %v", expectedModel, model)
}
expectedAPIError := &APIError{}
if !reflect.DeepEqual(expectedAPIError, apiError) {
t.Errorf("failureV should be zero valued, exepcted %v, got %v", expectedAPIError, apiError)
}
}
func TestReceive_failure(t *testing.T) {
client, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/foo/submit", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "POST", r)
assertQuery(t, map[string]string{"kind_name": "vanilla", "count": "11"}, r)
assertPostForm(t, map[string]string{"kind_name": "vanilla", "count": "11"}, r)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(429)
fmt.Fprintf(w, `{"message": "Rate limit exceeded", "code": 88}`)
})
endpoint := New().Client(client).Base("http://example.com/").Path("foo/").Post("submit")
// encode url-tagged struct in query params and as post body for testing purposes
params := FakeParams{KindName: "vanilla", Count: 11}
model := new(FakeModel)
apiError := new(APIError)
resp, err := endpoint.New().QueryStruct(params).BodyForm(params).Receive(model, apiError)
if err != nil {
t.Errorf("expected nil, got %v", err)
}
if resp.StatusCode != 429 {
t.Errorf("expected %d, got %d", 429, resp.StatusCode)
}
expectedAPIError := &APIError{Message: "Rate limit exceeded", Code: 88}
if !reflect.DeepEqual(expectedAPIError, apiError) {
t.Errorf("expected %v, got %v", expectedAPIError, apiError)
}
expectedModel := &FakeModel{}
if !reflect.DeepEqual(expectedModel, model) {
t.Errorf("successV should not be zero valued, expected %v, got %v", expectedModel, model)
}
}
func TestReceive_errorCreatingRequest(t *testing.T) {
expectedErr := errors.New("json: unsupported value: +Inf")
resp, err := New().BodyJSON(FakeModel{Temperature: math.Inf(1)}).Receive(nil, nil)
if err == nil || err.Error() != expectedErr.Error() {
t.Errorf("expected %v, got %v", expectedErr, err)
}
if resp != nil {
t.Errorf("expected nil resp, got %v", resp)
}
}
// Testing Utils
// testServer returns an http Client, ServeMux, and Server. The client proxies
// requests to the server and handlers can be registered on the mux to handle
// requests. The caller must close the test server.
func testServer() (*http.Client, *http.ServeMux, *httptest.Server) {
mux := http.NewServeMux()
server := httptest.NewServer(mux)
transport := &http.Transport{
Proxy: func(req *http.Request) (*url.URL, error) {
return url.Parse(server.URL)
},
}
client := &http.Client{Transport: transport}
return client, mux, server
}
func assertMethod(t *testing.T, expectedMethod string, req *http.Request) {
if actualMethod := req.Method; actualMethod != expectedMethod {
t.Errorf("expected method %s, got %s", expectedMethod, actualMethod)
}
}
// assertQuery tests that the Request has the expected url query key/val pairs
func assertQuery(t *testing.T, expected map[string]string, req *http.Request) {
queryValues := req.URL.Query() // net/url Values is a map[string][]string
expectedValues := url.Values{}
for key, value := range expected {
expectedValues.Add(key, value)
}
if !reflect.DeepEqual(expectedValues, queryValues) {
t.Errorf("expected parameters %v, got %v", expected, req.URL.RawQuery)
}
}
// assertPostForm tests that the Request has the expected key values pairs url
// encoded in its Body
func assertPostForm(t *testing.T, expected map[string]string, req *http.Request) {
req.ParseForm() // parses request Body to put url.Values in r.Form/r.PostForm
expectedValues := url.Values{}
for key, value := range expected {
expectedValues.Add(key, value)
}
if !reflect.DeepEqual(expectedValues, req.PostForm) {
t.Errorf("expected parameters %v, got %v", expected, req.PostForm)
}
}

View File

@@ -1,23 +0,0 @@
#!/usr/bin/env bash
set -e
PKGS=$(go list ./... | grep -v /examples)
FORMATTABLE="$(find . -maxdepth 1 -type d)"
LINTABLE=$(go list ./...)
go test $PKGS -cover
go vet $PKGS
echo "Checking gofmt..."
fmtRes=$(gofmt -l $FORMATTABLE)
if [ -n "${fmtRes}" ]; then
echo -e "gofmt checking failed:\n${fmtRes}"
exit 2
fi
echo "Checking golint..."
lintRes=$(echo $LINTABLE | xargs -n 1 golint)
if [ -n "${lintRes}" ]; then
echo -e "golint checking failed:\n${lintRes}"
exit 2
fi

View File

@@ -0,0 +1,4 @@
language: go
go:
- 1.x

View File

@@ -0,0 +1,46 @@
Change history of swagger
=
2017-01-30
- moved from go-restful/swagger to go-restful-swagger12
2015-10-16
- add type override mechanism for swagger models (MR 254, nathanejohnson)
- replace uses of wildcard in generated apidocs (issue 251)
2015-05-25
- (api break) changed the type of Properties in Model
- (api break) changed the type of Models in ApiDeclaration
- (api break) changed the parameter type of PostBuildDeclarationMapFunc
2015-04-09
- add ModelBuildable interface for customization of Model
2015-03-17
- preserve order of Routes per WebService in Swagger listing
- fix use of $ref and type in Swagger models
- add api version to listing
2014-11-14
- operation parameters are now sorted using ordering path,query,form,header,body
2014-11-12
- respect omitempty tag value for embedded structs
- expose ApiVersion of WebService to Swagger ApiDeclaration
2014-05-29
- (api add) Ability to define custom http.Handler to serve swagger-ui static files
2014-05-04
- (fix) include model for array element type of response
2014-01-03
- (fix) do not add primitive type to the Api models
2013-11-27
- (fix) make Swagger work for WebServices with root ("/" or "") paths
2013-10-29
- (api add) package variable LogInfo to customize logging function
2013-10-15
- upgraded to spec version 1.2 (https://github.com/wordnik/swagger-core/wiki/1.2-transition)

View File

@@ -0,0 +1,22 @@
Copyright (c) 2017 Ernest Micklei
MIT License
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.

View File

@@ -0,0 +1,83 @@
# go-restful-swagger12
[![Build Status](https://travis-ci.org/emicklei/go-restful-swagger12.png)](https://travis-ci.org/emicklei/go-restful-swagger12)
[![GoDoc](https://godoc.org/github.com/emicklei/go-restful-swagger12?status.svg)](https://godoc.org/github.com/emicklei/go-restful-swagger12)
How to use Swagger UI with go-restful
=
Get the Swagger UI sources (version 1.2 only)
git clone https://github.com/wordnik/swagger-ui.git
The project contains a "dist" folder.
Its contents has all the Swagger UI files you need.
The `index.html` has an `url` set to `http://petstore.swagger.wordnik.com/api/api-docs`.
You need to change that to match your WebService JSON endpoint e.g. `http://localhost:8080/apidocs.json`
Now, you can install the Swagger WebService for serving the Swagger specification in JSON.
config := swagger.Config{
WebServices: restful.RegisteredWebServices(),
ApiPath: "/apidocs.json",
SwaggerPath: "/apidocs/",
SwaggerFilePath: "/Users/emicklei/Projects/swagger-ui/dist"}
swagger.InstallSwaggerService(config)
Documenting Structs
--
Currently there are 2 ways to document your structs in the go-restful Swagger.
###### By using struct tags
- Use tag "description" to annotate a struct field with a description to show in the UI
- Use tag "modelDescription" to annotate the struct itself with a description to show in the UI. The tag can be added in an field of the struct and in case that there are multiple definition, they will be appended with an empty line.
###### By using the SwaggerDoc method
Here is an example with an `Address` struct and the documentation for each of the fields. The `""` is a special entry for **documenting the struct itself**.
type Address struct {
Country string `json:"country,omitempty"`
PostCode int `json:"postcode,omitempty"`
}
func (Address) SwaggerDoc() map[string]string {
return map[string]string{
"": "Address doc",
"country": "Country doc",
"postcode": "PostCode doc",
}
}
This example will generate a JSON like this
{
"Address": {
"id": "Address",
"description": "Address doc",
"properties": {
"country": {
"type": "string",
"description": "Country doc"
},
"postcode": {
"type": "integer",
"format": "int32",
"description": "PostCode doc"
}
}
}
}
**Very Important Notes:**
- `SwaggerDoc()` is using a **NON-Pointer** receiver (e.g. func (Address) and not func (*Address))
- The returned map should use as key the name of the field as defined in the JSON parameter (e.g. `"postcode"` and not `"PostCode"`)
Notes
--
- The Nickname of an Operation is automatically set by finding the name of the function. You can override it using RouteBuilder.Operation(..)
- The WebServices field of swagger.Config can be used to control which service you want to expose and document ; you can have multiple configs and therefore multiple endpoints.
© 2017, ernestmicklei.com. MIT License. Contributions welcome.

View File

@@ -0,0 +1,64 @@
package swagger
// Copyright 2015 Ernest Micklei. All rights reserved.
// Use of this source code is governed by a license
// that can be found in the LICENSE file.
import (
"bytes"
"encoding/json"
)
// ApiDeclarationList maintains an ordered list of ApiDeclaration.
type ApiDeclarationList struct {
List []ApiDeclaration
}
// At returns the ApiDeclaration by its path unless absent, then ok is false
func (l *ApiDeclarationList) At(path string) (a ApiDeclaration, ok bool) {
for _, each := range l.List {
if each.ResourcePath == path {
return each, true
}
}
return a, false
}
// Put adds or replaces a ApiDeclaration with this name
func (l *ApiDeclarationList) Put(path string, a ApiDeclaration) {
// maybe replace existing
for i, each := range l.List {
if each.ResourcePath == path {
// replace
l.List[i] = a
return
}
}
// add
l.List = append(l.List, a)
}
// Do enumerates all the properties, each with its assigned name
func (l *ApiDeclarationList) Do(block func(path string, decl ApiDeclaration)) {
for _, each := range l.List {
block(each.ResourcePath, each)
}
}
// MarshalJSON writes the ModelPropertyList as if it was a map[string]ModelProperty
func (l ApiDeclarationList) MarshalJSON() ([]byte, error) {
var buf bytes.Buffer
encoder := json.NewEncoder(&buf)
buf.WriteString("{\n")
for i, each := range l.List {
buf.WriteString("\"")
buf.WriteString(each.ResourcePath)
buf.WriteString("\": ")
encoder.Encode(each)
if i < len(l.List)-1 {
buf.WriteString(",\n")
}
}
buf.WriteString("}")
return buf.Bytes(), nil
}

View File

@@ -0,0 +1,46 @@
package swagger
import (
"net/http"
"reflect"
"github.com/emicklei/go-restful"
)
// PostBuildDeclarationMapFunc can be used to modify the api declaration map.
type PostBuildDeclarationMapFunc func(apiDeclarationMap *ApiDeclarationList)
// MapSchemaFormatFunc can be used to modify typeName at definition time.
type MapSchemaFormatFunc func(typeName string) string
// MapModelTypeNameFunc can be used to return the desired typeName for a given
// type. It will return false if the default name should be used.
type MapModelTypeNameFunc func(t reflect.Type) (string, bool)
type Config struct {
// url where the services are available, e.g. http://localhost:8080
// if left empty then the basePath of Swagger is taken from the actual request
WebServicesUrl string
// path where the JSON api is avaiable , e.g. /apidocs
ApiPath string
// [optional] path where the swagger UI will be served, e.g. /swagger
SwaggerPath string
// [optional] location of folder containing Swagger HTML5 application index.html
SwaggerFilePath string
// api listing is constructed from this list of restful WebServices.
WebServices []*restful.WebService
// will serve all static content (scripts,pages,images)
StaticHandler http.Handler
// [optional] on default CORS (Cross-Origin-Resource-Sharing) is enabled.
DisableCORS bool
// Top-level API version. Is reflected in the resource listing.
ApiVersion string
// If set then call this handler after building the complete ApiDeclaration Map
PostBuildHandler PostBuildDeclarationMapFunc
// Swagger global info struct
Info Info
// [optional] If set, model builder should call this handler to get addition typename-to-swagger-format-field conversion.
SchemaFormatHandler MapSchemaFormatFunc
// [optional] If set, model builder should call this handler to retrieve the name for a given type.
ModelTypeNameHandler MapModelTypeNameFunc
}

View File

@@ -0,0 +1,467 @@
package swagger
import (
"encoding/json"
"reflect"
"strings"
)
// ModelBuildable is used for extending Structs that need more control over
// how the Model appears in the Swagger api declaration.
type ModelBuildable interface {
PostBuildModel(m *Model) *Model
}
type modelBuilder struct {
Models *ModelList
Config *Config
}
type documentable interface {
SwaggerDoc() map[string]string
}
// Check if this structure has a method with signature func (<theModel>) SwaggerDoc() map[string]string
// If it exists, retrive the documentation and overwrite all struct tag descriptions
func getDocFromMethodSwaggerDoc2(model reflect.Type) map[string]string {
if docable, ok := reflect.New(model).Elem().Interface().(documentable); ok {
return docable.SwaggerDoc()
}
return make(map[string]string)
}
// addModelFrom creates and adds a Model to the builder and detects and calls
// the post build hook for customizations
func (b modelBuilder) addModelFrom(sample interface{}) {
if modelOrNil := b.addModel(reflect.TypeOf(sample), ""); modelOrNil != nil {
// allow customizations
if buildable, ok := sample.(ModelBuildable); ok {
modelOrNil = buildable.PostBuildModel(modelOrNil)
b.Models.Put(modelOrNil.Id, *modelOrNil)
}
}
}
func (b modelBuilder) addModel(st reflect.Type, nameOverride string) *Model {
// Turn pointers into simpler types so further checks are
// correct.
if st.Kind() == reflect.Ptr {
st = st.Elem()
}
modelName := b.keyFrom(st)
if nameOverride != "" {
modelName = nameOverride
}
// no models needed for primitive types
if b.isPrimitiveType(modelName) {
return nil
}
// golang encoding/json packages says array and slice values encode as
// JSON arrays, except that []byte encodes as a base64-encoded string.
// If we see a []byte here, treat it at as a primitive type (string)
// and deal with it in buildArrayTypeProperty.
if (st.Kind() == reflect.Slice || st.Kind() == reflect.Array) &&
st.Elem().Kind() == reflect.Uint8 {
return nil
}
// see if we already have visited this model
if _, ok := b.Models.At(modelName); ok {
return nil
}
sm := Model{
Id: modelName,
Required: []string{},
Properties: ModelPropertyList{}}
// reference the model before further initializing (enables recursive structs)
b.Models.Put(modelName, sm)
// check for slice or array
if st.Kind() == reflect.Slice || st.Kind() == reflect.Array {
b.addModel(st.Elem(), "")
return &sm
}
// check for structure or primitive type
if st.Kind() != reflect.Struct {
return &sm
}
fullDoc := getDocFromMethodSwaggerDoc2(st)
modelDescriptions := []string{}
for i := 0; i < st.NumField(); i++ {
field := st.Field(i)
jsonName, modelDescription, prop := b.buildProperty(field, &sm, modelName)
if len(modelDescription) > 0 {
modelDescriptions = append(modelDescriptions, modelDescription)
}
// add if not omitted
if len(jsonName) != 0 {
// update description
if fieldDoc, ok := fullDoc[jsonName]; ok {
prop.Description = fieldDoc
}
// update Required
if b.isPropertyRequired(field) {
sm.Required = append(sm.Required, jsonName)
}
sm.Properties.Put(jsonName, prop)
}
}
// We always overwrite documentation if SwaggerDoc method exists
// "" is special for documenting the struct itself
if modelDoc, ok := fullDoc[""]; ok {
sm.Description = modelDoc
} else if len(modelDescriptions) != 0 {
sm.Description = strings.Join(modelDescriptions, "\n")
}
// update model builder with completed model
b.Models.Put(modelName, sm)
return &sm
}
func (b modelBuilder) isPropertyRequired(field reflect.StructField) bool {
required := true
if jsonTag := field.Tag.Get("json"); jsonTag != "" {
s := strings.Split(jsonTag, ",")
if len(s) > 1 && s[1] == "omitempty" {
return false
}
}
return required
}
func (b modelBuilder) buildProperty(field reflect.StructField, model *Model, modelName string) (jsonName, modelDescription string, prop ModelProperty) {
jsonName = b.jsonNameOfField(field)
if len(jsonName) == 0 {
// empty name signals skip property
return "", "", prop
}
if field.Name == "XMLName" && field.Type.String() == "xml.Name" {
// property is metadata for the xml.Name attribute, can be skipped
return "", "", prop
}
if tag := field.Tag.Get("modelDescription"); tag != "" {
modelDescription = tag
}
prop.setPropertyMetadata(field)
if prop.Type != nil {
return jsonName, modelDescription, prop
}
fieldType := field.Type
// check if type is doing its own marshalling
marshalerType := reflect.TypeOf((*json.Marshaler)(nil)).Elem()
if fieldType.Implements(marshalerType) {
var pType = "string"
if prop.Type == nil {
prop.Type = &pType
}
if prop.Format == "" {
prop.Format = b.jsonSchemaFormat(b.keyFrom(fieldType))
}
return jsonName, modelDescription, prop
}
// check if annotation says it is a string
if jsonTag := field.Tag.Get("json"); jsonTag != "" {
s := strings.Split(jsonTag, ",")
if len(s) > 1 && s[1] == "string" {
stringt := "string"
prop.Type = &stringt
return jsonName, modelDescription, prop
}
}
fieldKind := fieldType.Kind()
switch {
case fieldKind == reflect.Struct:
jsonName, prop := b.buildStructTypeProperty(field, jsonName, model)
return jsonName, modelDescription, prop
case fieldKind == reflect.Slice || fieldKind == reflect.Array:
jsonName, prop := b.buildArrayTypeProperty(field, jsonName, modelName)
return jsonName, modelDescription, prop
case fieldKind == reflect.Ptr:
jsonName, prop := b.buildPointerTypeProperty(field, jsonName, modelName)
return jsonName, modelDescription, prop
case fieldKind == reflect.String:
stringt := "string"
prop.Type = &stringt
return jsonName, modelDescription, prop
case fieldKind == reflect.Map:
// if it's a map, it's unstructured, and swagger 1.2 can't handle it
objectType := "object"
prop.Type = &objectType
return jsonName, modelDescription, prop
}
fieldTypeName := b.keyFrom(fieldType)
if b.isPrimitiveType(fieldTypeName) {
mapped := b.jsonSchemaType(fieldTypeName)
prop.Type = &mapped
prop.Format = b.jsonSchemaFormat(fieldTypeName)
return jsonName, modelDescription, prop
}
modelType := b.keyFrom(fieldType)
prop.Ref = &modelType
if fieldType.Name() == "" { // override type of anonymous structs
nestedTypeName := modelName + "." + jsonName
prop.Ref = &nestedTypeName
b.addModel(fieldType, nestedTypeName)
}
return jsonName, modelDescription, prop
}
func hasNamedJSONTag(field reflect.StructField) bool {
parts := strings.Split(field.Tag.Get("json"), ",")
if len(parts) == 0 {
return false
}
for _, s := range parts[1:] {
if s == "inline" {
return false
}
}
return len(parts[0]) > 0
}
func (b modelBuilder) buildStructTypeProperty(field reflect.StructField, jsonName string, model *Model) (nameJson string, prop ModelProperty) {
prop.setPropertyMetadata(field)
// Check for type override in tag
if prop.Type != nil {
return jsonName, prop
}
fieldType := field.Type
// check for anonymous
if len(fieldType.Name()) == 0 {
// anonymous
anonType := model.Id + "." + jsonName
b.addModel(fieldType, anonType)
prop.Ref = &anonType
return jsonName, prop
}
if field.Name == fieldType.Name() && field.Anonymous && !hasNamedJSONTag(field) {
// embedded struct
sub := modelBuilder{new(ModelList), b.Config}
sub.addModel(fieldType, "")
subKey := sub.keyFrom(fieldType)
// merge properties from sub
subModel, _ := sub.Models.At(subKey)
subModel.Properties.Do(func(k string, v ModelProperty) {
model.Properties.Put(k, v)
// if subModel says this property is required then include it
required := false
for _, each := range subModel.Required {
if k == each {
required = true
break
}
}
if required {
model.Required = append(model.Required, k)
}
})
// add all new referenced models
sub.Models.Do(func(key string, sub Model) {
if key != subKey {
if _, ok := b.Models.At(key); !ok {
b.Models.Put(key, sub)
}
}
})
// empty name signals skip property
return "", prop
}
// simple struct
b.addModel(fieldType, "")
var pType = b.keyFrom(fieldType)
prop.Ref = &pType
return jsonName, prop
}
func (b modelBuilder) buildArrayTypeProperty(field reflect.StructField, jsonName, modelName string) (nameJson string, prop ModelProperty) {
// check for type override in tags
prop.setPropertyMetadata(field)
if prop.Type != nil {
return jsonName, prop
}
fieldType := field.Type
if fieldType.Elem().Kind() == reflect.Uint8 {
stringt := "string"
prop.Type = &stringt
return jsonName, prop
}
var pType = "array"
prop.Type = &pType
isPrimitive := b.isPrimitiveType(fieldType.Elem().Name())
elemTypeName := b.getElementTypeName(modelName, jsonName, fieldType.Elem())
prop.Items = new(Item)
if isPrimitive {
mapped := b.jsonSchemaType(elemTypeName)
prop.Items.Type = &mapped
} else {
prop.Items.Ref = &elemTypeName
}
// add|overwrite model for element type
if fieldType.Elem().Kind() == reflect.Ptr {
fieldType = fieldType.Elem()
}
if !isPrimitive {
b.addModel(fieldType.Elem(), elemTypeName)
}
return jsonName, prop
}
func (b modelBuilder) buildPointerTypeProperty(field reflect.StructField, jsonName, modelName string) (nameJson string, prop ModelProperty) {
prop.setPropertyMetadata(field)
// Check for type override in tags
if prop.Type != nil {
return jsonName, prop
}
fieldType := field.Type
// override type of pointer to list-likes
if fieldType.Elem().Kind() == reflect.Slice || fieldType.Elem().Kind() == reflect.Array {
var pType = "array"
prop.Type = &pType
isPrimitive := b.isPrimitiveType(fieldType.Elem().Elem().Name())
elemName := b.getElementTypeName(modelName, jsonName, fieldType.Elem().Elem())
if isPrimitive {
primName := b.jsonSchemaType(elemName)
prop.Items = &Item{Ref: &primName}
} else {
prop.Items = &Item{Ref: &elemName}
}
if !isPrimitive {
// add|overwrite model for element type
b.addModel(fieldType.Elem().Elem(), elemName)
}
} else {
// non-array, pointer type
fieldTypeName := b.keyFrom(fieldType.Elem())
var pType = b.jsonSchemaType(fieldTypeName) // no star, include pkg path
if b.isPrimitiveType(fieldTypeName) {
prop.Type = &pType
prop.Format = b.jsonSchemaFormat(fieldTypeName)
return jsonName, prop
}
prop.Ref = &pType
elemName := ""
if fieldType.Elem().Name() == "" {
elemName = modelName + "." + jsonName
prop.Ref = &elemName
}
b.addModel(fieldType.Elem(), elemName)
}
return jsonName, prop
}
func (b modelBuilder) getElementTypeName(modelName, jsonName string, t reflect.Type) string {
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
if t.Name() == "" {
return modelName + "." + jsonName
}
return b.keyFrom(t)
}
func (b modelBuilder) keyFrom(st reflect.Type) string {
key := st.String()
if b.Config != nil && b.Config.ModelTypeNameHandler != nil {
if name, ok := b.Config.ModelTypeNameHandler(st); ok {
key = name
}
}
if len(st.Name()) == 0 { // unnamed type
// Swagger UI has special meaning for [
key = strings.Replace(key, "[]", "||", -1)
}
return key
}
// see also https://golang.org/ref/spec#Numeric_types
func (b modelBuilder) isPrimitiveType(modelName string) bool {
if len(modelName) == 0 {
return false
}
return strings.Contains("uint uint8 uint16 uint32 uint64 int int8 int16 int32 int64 float32 float64 bool string byte rune time.Time", modelName)
}
// jsonNameOfField returns the name of the field as it should appear in JSON format
// An empty string indicates that this field is not part of the JSON representation
func (b modelBuilder) jsonNameOfField(field reflect.StructField) string {
if jsonTag := field.Tag.Get("json"); jsonTag != "" {
s := strings.Split(jsonTag, ",")
if s[0] == "-" {
// empty name signals skip property
return ""
} else if s[0] != "" {
return s[0]
}
}
return field.Name
}
// see also http://json-schema.org/latest/json-schema-core.html#anchor8
func (b modelBuilder) jsonSchemaType(modelName string) string {
schemaMap := map[string]string{
"uint": "integer",
"uint8": "integer",
"uint16": "integer",
"uint32": "integer",
"uint64": "integer",
"int": "integer",
"int8": "integer",
"int16": "integer",
"int32": "integer",
"int64": "integer",
"byte": "integer",
"float64": "number",
"float32": "number",
"bool": "boolean",
"time.Time": "string",
}
mapped, ok := schemaMap[modelName]
if !ok {
return modelName // use as is (custom or struct)
}
return mapped
}
func (b modelBuilder) jsonSchemaFormat(modelName string) string {
if b.Config != nil && b.Config.SchemaFormatHandler != nil {
if mapped := b.Config.SchemaFormatHandler(modelName); mapped != "" {
return mapped
}
}
schemaMap := map[string]string{
"int": "int32",
"int32": "int32",
"int64": "int64",
"byte": "byte",
"uint": "integer",
"uint8": "byte",
"float64": "double",
"float32": "float",
"time.Time": "date-time",
"*time.Time": "date-time",
}
mapped, ok := schemaMap[modelName]
if !ok {
return "" // no format
}
return mapped
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,86 @@
package swagger
// Copyright 2015 Ernest Micklei. All rights reserved.
// Use of this source code is governed by a license
// that can be found in the LICENSE file.
import (
"bytes"
"encoding/json"
)
// NamedModel associates a name with a Model (not using its Id)
type NamedModel struct {
Name string
Model Model
}
// ModelList encapsulates a list of NamedModel (association)
type ModelList struct {
List []NamedModel
}
// Put adds or replaces a Model by its name
func (l *ModelList) Put(name string, model Model) {
for i, each := range l.List {
if each.Name == name {
// replace
l.List[i] = NamedModel{name, model}
return
}
}
// add
l.List = append(l.List, NamedModel{name, model})
}
// At returns a Model by its name, ok is false if absent
func (l *ModelList) At(name string) (m Model, ok bool) {
for _, each := range l.List {
if each.Name == name {
return each.Model, true
}
}
return m, false
}
// Do enumerates all the models, each with its assigned name
func (l *ModelList) Do(block func(name string, value Model)) {
for _, each := range l.List {
block(each.Name, each.Model)
}
}
// MarshalJSON writes the ModelList as if it was a map[string]Model
func (l ModelList) MarshalJSON() ([]byte, error) {
var buf bytes.Buffer
encoder := json.NewEncoder(&buf)
buf.WriteString("{\n")
for i, each := range l.List {
buf.WriteString("\"")
buf.WriteString(each.Name)
buf.WriteString("\": ")
encoder.Encode(each.Model)
if i < len(l.List)-1 {
buf.WriteString(",\n")
}
}
buf.WriteString("}")
return buf.Bytes(), nil
}
// UnmarshalJSON reads back a ModelList. This is an expensive operation.
func (l *ModelList) UnmarshalJSON(data []byte) error {
raw := map[string]interface{}{}
json.NewDecoder(bytes.NewReader(data)).Decode(&raw)
for k, v := range raw {
// produces JSON bytes for each value
data, err := json.Marshal(v)
if err != nil {
return err
}
var m Model
json.NewDecoder(bytes.NewReader(data)).Decode(&m)
l.Put(k, m)
}
return nil
}

View File

@@ -0,0 +1,48 @@
package swagger
import (
"encoding/json"
"testing"
)
func TestModelList(t *testing.T) {
m := Model{}
m.Id = "m"
l := ModelList{}
l.Put("m", m)
k, ok := l.At("m")
if !ok {
t.Error("want model back")
}
if got, want := k.Id, "m"; got != want {
t.Errorf("got %v want %v", got, want)
}
}
func TestModelList_Marshal(t *testing.T) {
l := ModelList{}
m := Model{Id: "myid"}
l.Put("myid", m)
data, err := json.Marshal(l)
if err != nil {
t.Error(err)
}
if got, want := string(data), `{"myid":{"id":"myid","properties":{}}}`; got != want {
t.Errorf("got %v want %v", got, want)
}
}
func TestModelList_Unmarshal(t *testing.T) {
data := `{"myid":{"id":"myid","properties":{}}}`
l := ModelList{}
if err := json.Unmarshal([]byte(data), &l); err != nil {
t.Error(err)
}
m, ok := l.At("myid")
if !ok {
t.Error("expected myid")
}
if got, want := m.Id, "myid"; got != want {
t.Errorf("got %v want %v", got, want)
}
}

View File

@@ -0,0 +1,81 @@
package swagger
import (
"reflect"
"strings"
)
func (prop *ModelProperty) setDescription(field reflect.StructField) {
if tag := field.Tag.Get("description"); tag != "" {
prop.Description = tag
}
}
func (prop *ModelProperty) setDefaultValue(field reflect.StructField) {
if tag := field.Tag.Get("default"); tag != "" {
prop.DefaultValue = Special(tag)
}
}
func (prop *ModelProperty) setEnumValues(field reflect.StructField) {
// We use | to separate the enum values. This value is chosen
// since its unlikely to be useful in actual enumeration values.
if tag := field.Tag.Get("enum"); tag != "" {
prop.Enum = strings.Split(tag, "|")
}
}
func (prop *ModelProperty) setMaximum(field reflect.StructField) {
if tag := field.Tag.Get("maximum"); tag != "" {
prop.Maximum = tag
}
}
func (prop *ModelProperty) setType(field reflect.StructField) {
if tag := field.Tag.Get("type"); tag != "" {
// Check if the first two characters of the type tag are
// intended to emulate slice/array behaviour.
//
// If type is intended to be a slice/array then add the
// overriden type to the array item instead of the main property
if len(tag) > 2 && tag[0:2] == "[]" {
pType := "array"
prop.Type = &pType
prop.Items = new(Item)
iType := tag[2:]
prop.Items.Type = &iType
return
}
prop.Type = &tag
}
}
func (prop *ModelProperty) setMinimum(field reflect.StructField) {
if tag := field.Tag.Get("minimum"); tag != "" {
prop.Minimum = tag
}
}
func (prop *ModelProperty) setUniqueItems(field reflect.StructField) {
tag := field.Tag.Get("unique")
switch tag {
case "true":
v := true
prop.UniqueItems = &v
case "false":
v := false
prop.UniqueItems = &v
}
}
func (prop *ModelProperty) setPropertyMetadata(field reflect.StructField) {
prop.setDescription(field)
prop.setEnumValues(field)
prop.setMinimum(field)
prop.setMaximum(field)
prop.setUniqueItems(field)
prop.setDefaultValue(field)
prop.setType(field)
}

View File

@@ -0,0 +1,70 @@
package swagger
import (
"net"
"testing"
)
// clear && go test -v -test.run TestThatExtraTagsAreReadIntoModel ...swagger
func TestThatExtraTagsAreReadIntoModel(t *testing.T) {
type fakeint int
type fakearray string
type Anything struct {
Name string `description:"name" modelDescription:"a test"`
Size int `minimum:"0" maximum:"10"`
Stati string `enum:"off|on" default:"on" modelDescription:"more description"`
ID string `unique:"true"`
FakeInt fakeint `type:"integer"`
FakeArray fakearray `type:"[]string"`
IP net.IP `type:"string"`
Password string
}
m := modelsFromStruct(Anything{})
props, _ := m.At("swagger.Anything")
p1, _ := props.Properties.At("Name")
if got, want := p1.Description, "name"; got != want {
t.Errorf("got %v want %v", got, want)
}
p2, _ := props.Properties.At("Size")
if got, want := p2.Minimum, "0"; got != want {
t.Errorf("got %v want %v", got, want)
}
if got, want := p2.Maximum, "10"; got != want {
t.Errorf("got %v want %v", got, want)
}
p3, _ := props.Properties.At("Stati")
if got, want := p3.Enum[0], "off"; got != want {
t.Errorf("got %v want %v", got, want)
}
if got, want := p3.Enum[1], "on"; got != want {
t.Errorf("got %v want %v", got, want)
}
p4, _ := props.Properties.At("ID")
if got, want := *p4.UniqueItems, true; got != want {
t.Errorf("got %v want %v", got, want)
}
p5, _ := props.Properties.At("Password")
if got, want := *p5.Type, "string"; got != want {
t.Errorf("got %v want %v", got, want)
}
p6, _ := props.Properties.At("FakeInt")
if got, want := *p6.Type, "integer"; got != want {
t.Errorf("got %v want %v", got, want)
}
p7, _ := props.Properties.At("FakeArray")
if got, want := *p7.Type, "array"; got != want {
t.Errorf("got %v want %v", got, want)
}
p7p, _ := props.Properties.At("FakeArray")
if got, want := *p7p.Items.Type, "string"; got != want {
t.Errorf("got %v want %v", got, want)
}
p8, _ := props.Properties.At("IP")
if got, want := *p8.Type, "string"; got != want {
t.Errorf("got %v want %v", got, want)
}
if got, want := props.Description, "a test\nmore description"; got != want {
t.Errorf("got %v want %v", got, want)
}
}

View File

@@ -0,0 +1,87 @@
package swagger
// Copyright 2015 Ernest Micklei. All rights reserved.
// Use of this source code is governed by a license
// that can be found in the LICENSE file.
import (
"bytes"
"encoding/json"
)
// NamedModelProperty associates a name to a ModelProperty
type NamedModelProperty struct {
Name string
Property ModelProperty
}
// ModelPropertyList encapsulates a list of NamedModelProperty (association)
type ModelPropertyList struct {
List []NamedModelProperty
}
// At returns the ModelPropety by its name unless absent, then ok is false
func (l *ModelPropertyList) At(name string) (p ModelProperty, ok bool) {
for _, each := range l.List {
if each.Name == name {
return each.Property, true
}
}
return p, false
}
// Put adds or replaces a ModelProperty with this name
func (l *ModelPropertyList) Put(name string, prop ModelProperty) {
// maybe replace existing
for i, each := range l.List {
if each.Name == name {
// replace
l.List[i] = NamedModelProperty{Name: name, Property: prop}
return
}
}
// add
l.List = append(l.List, NamedModelProperty{Name: name, Property: prop})
}
// Do enumerates all the properties, each with its assigned name
func (l *ModelPropertyList) Do(block func(name string, value ModelProperty)) {
for _, each := range l.List {
block(each.Name, each.Property)
}
}
// MarshalJSON writes the ModelPropertyList as if it was a map[string]ModelProperty
func (l ModelPropertyList) MarshalJSON() ([]byte, error) {
var buf bytes.Buffer
encoder := json.NewEncoder(&buf)
buf.WriteString("{\n")
for i, each := range l.List {
buf.WriteString("\"")
buf.WriteString(each.Name)
buf.WriteString("\": ")
encoder.Encode(each.Property)
if i < len(l.List)-1 {
buf.WriteString(",\n")
}
}
buf.WriteString("}")
return buf.Bytes(), nil
}
// UnmarshalJSON reads back a ModelPropertyList. This is an expensive operation.
func (l *ModelPropertyList) UnmarshalJSON(data []byte) error {
raw := map[string]interface{}{}
json.NewDecoder(bytes.NewReader(data)).Decode(&raw)
for k, v := range raw {
// produces JSON bytes for each value
data, err := json.Marshal(v)
if err != nil {
return err
}
var m ModelProperty
json.NewDecoder(bytes.NewReader(data)).Decode(&m)
l.Put(k, m)
}
return nil
}

View File

@@ -0,0 +1,47 @@
package swagger
import (
"encoding/json"
"testing"
)
func TestModelPropertyList(t *testing.T) {
l := ModelPropertyList{}
p := ModelProperty{Description: "d"}
l.Put("p", p)
q, ok := l.At("p")
if !ok {
t.Error("expected p")
}
if got, want := q.Description, "d"; got != want {
t.Errorf("got %v want %v", got, want)
}
}
func TestModelPropertyList_Marshal(t *testing.T) {
l := ModelPropertyList{}
p := ModelProperty{Description: "d"}
l.Put("p", p)
data, err := json.Marshal(l)
if err != nil {
t.Error(err)
}
if got, want := string(data), `{"p":{"description":"d"}}`; got != want {
t.Errorf("got %v want %v", got, want)
}
}
func TestModelPropertyList_Unmarshal(t *testing.T) {
data := `{"p":{"description":"d"}}`
l := ModelPropertyList{}
if err := json.Unmarshal([]byte(data), &l); err != nil {
t.Error(err)
}
m, ok := l.At("p")
if !ok {
t.Error("expected p")
}
if got, want := m.Description, "d"; got != want {
t.Errorf("got %v want %v", got, want)
}
}

View File

@@ -0,0 +1,36 @@
package swagger
// Copyright 2015 Ernest Micklei. All rights reserved.
// Use of this source code is governed by a license
// that can be found in the LICENSE file.
import "github.com/emicklei/go-restful"
type orderedRouteMap struct {
elements map[string][]restful.Route
keys []string
}
func newOrderedRouteMap() *orderedRouteMap {
return &orderedRouteMap{
elements: map[string][]restful.Route{},
keys: []string{},
}
}
func (o *orderedRouteMap) Add(key string, route restful.Route) {
routes, ok := o.elements[key]
if ok {
routes = append(routes, route)
o.elements[key] = routes
return
}
o.elements[key] = []restful.Route{route}
o.keys = append(o.keys, key)
}
func (o *orderedRouteMap) Do(block func(key string, routes []restful.Route)) {
for _, k := range o.keys {
block(k, o.elements[k])
}
}

View File

@@ -0,0 +1,29 @@
package swagger
import (
"testing"
"github.com/emicklei/go-restful"
)
// go test -v -test.run TestOrderedRouteMap ...swagger
func TestOrderedRouteMap(t *testing.T) {
m := newOrderedRouteMap()
r1 := restful.Route{Path: "/r1"}
r2 := restful.Route{Path: "/r2"}
m.Add("a", r1)
m.Add("b", r2)
m.Add("b", r1)
m.Add("d", r2)
m.Add("c", r2)
order := ""
m.Do(func(k string, routes []restful.Route) {
order += k
if len(routes) == 0 {
t.Fail()
}
})
if order != "abdc" {
t.Fail()
}
}

View File

@@ -0,0 +1,42 @@
package swagger
import "testing"
type Boat struct {
Length int `json:"-"` // on default, this makes the fields not required
Weight int `json:"-"`
}
// PostBuildModel is from swagger.ModelBuildable
func (b Boat) PostBuildModel(m *Model) *Model {
// override required
m.Required = []string{"Length", "Weight"}
// add model property (just to test is can be added; is this a real usecase?)
extraType := "string"
m.Properties.Put("extra", ModelProperty{
Description: "extra description",
DataTypeFields: DataTypeFields{
Type: &extraType,
},
})
return m
}
func TestCustomPostModelBuilde(t *testing.T) {
testJsonFromStruct(t, Boat{}, `{
"swagger.Boat": {
"id": "swagger.Boat",
"required": [
"Length",
"Weight"
],
"properties": {
"extra": {
"type": "string",
"description": "extra description"
}
}
}
}`)
}

View File

@@ -0,0 +1,185 @@
// Package swagger implements the structures of the Swagger
// https://github.com/wordnik/swagger-spec/blob/master/versions/1.2.md
package swagger
const swaggerVersion = "1.2"
// 4.3.3 Data Type Fields
type DataTypeFields struct {
Type *string `json:"type,omitempty"` // if Ref not used
Ref *string `json:"$ref,omitempty"` // if Type not used
Format string `json:"format,omitempty"`
DefaultValue Special `json:"defaultValue,omitempty"`
Enum []string `json:"enum,omitempty"`
Minimum string `json:"minimum,omitempty"`
Maximum string `json:"maximum,omitempty"`
Items *Item `json:"items,omitempty"`
UniqueItems *bool `json:"uniqueItems,omitempty"`
}
type Special string
// 4.3.4 Items Object
type Item struct {
Type *string `json:"type,omitempty"`
Ref *string `json:"$ref,omitempty"`
Format string `json:"format,omitempty"`
}
// 5.1 Resource Listing
type ResourceListing struct {
SwaggerVersion string `json:"swaggerVersion"` // e.g 1.2
Apis []Resource `json:"apis"`
ApiVersion string `json:"apiVersion"`
Info Info `json:"info"`
Authorizations []Authorization `json:"authorizations,omitempty"`
}
// 5.1.2 Resource Object
type Resource struct {
Path string `json:"path"` // relative or absolute, must start with /
Description string `json:"description"`
}
// 5.1.3 Info Object
type Info struct {
Title string `json:"title"`
Description string `json:"description"`
TermsOfServiceUrl string `json:"termsOfServiceUrl,omitempty"`
Contact string `json:"contact,omitempty"`
License string `json:"license,omitempty"`
LicenseUrl string `json:"licenseUrl,omitempty"`
}
// 5.1.5
type Authorization struct {
Type string `json:"type"`
PassAs string `json:"passAs"`
Keyname string `json:"keyname"`
Scopes []Scope `json:"scopes"`
GrantTypes []GrantType `json:"grandTypes"`
}
// 5.1.6, 5.2.11
type Scope struct {
// Required. The name of the scope.
Scope string `json:"scope"`
// Recommended. A short description of the scope.
Description string `json:"description"`
}
// 5.1.7
type GrantType struct {
Implicit Implicit `json:"implicit"`
AuthorizationCode AuthorizationCode `json:"authorization_code"`
}
// 5.1.8 Implicit Object
type Implicit struct {
// Required. The login endpoint definition.
loginEndpoint LoginEndpoint `json:"loginEndpoint"`
// An optional alternative name to standard "access_token" OAuth2 parameter.
TokenName string `json:"tokenName"`
}
// 5.1.9 Authorization Code Object
type AuthorizationCode struct {
TokenRequestEndpoint TokenRequestEndpoint `json:"tokenRequestEndpoint"`
TokenEndpoint TokenEndpoint `json:"tokenEndpoint"`
}
// 5.1.10 Login Endpoint Object
type LoginEndpoint struct {
// Required. The URL of the authorization endpoint for the implicit grant flow. The value SHOULD be in a URL format.
Url string `json:"url"`
}
// 5.1.11 Token Request Endpoint Object
type TokenRequestEndpoint struct {
// Required. The URL of the authorization endpoint for the authentication code grant flow. The value SHOULD be in a URL format.
Url string `json:"url"`
// An optional alternative name to standard "client_id" OAuth2 parameter.
ClientIdName string `json:"clientIdName"`
// An optional alternative name to the standard "client_secret" OAuth2 parameter.
ClientSecretName string `json:"clientSecretName"`
}
// 5.1.12 Token Endpoint Object
type TokenEndpoint struct {
// Required. The URL of the token endpoint for the authentication code grant flow. The value SHOULD be in a URL format.
Url string `json:"url"`
// An optional alternative name to standard "access_token" OAuth2 parameter.
TokenName string `json:"tokenName"`
}
// 5.2 API Declaration
type ApiDeclaration struct {
SwaggerVersion string `json:"swaggerVersion"`
ApiVersion string `json:"apiVersion"`
BasePath string `json:"basePath"`
ResourcePath string `json:"resourcePath"` // must start with /
Info Info `json:"info"`
Apis []Api `json:"apis,omitempty"`
Models ModelList `json:"models,omitempty"`
Produces []string `json:"produces,omitempty"`
Consumes []string `json:"consumes,omitempty"`
Authorizations []Authorization `json:"authorizations,omitempty"`
}
// 5.2.2 API Object
type Api struct {
Path string `json:"path"` // relative or absolute, must start with /
Description string `json:"description"`
Operations []Operation `json:"operations,omitempty"`
}
// 5.2.3 Operation Object
type Operation struct {
DataTypeFields
Method string `json:"method"`
Summary string `json:"summary,omitempty"`
Notes string `json:"notes,omitempty"`
Nickname string `json:"nickname"`
Authorizations []Authorization `json:"authorizations,omitempty"`
Parameters []Parameter `json:"parameters"`
ResponseMessages []ResponseMessage `json:"responseMessages,omitempty"` // optional
Produces []string `json:"produces,omitempty"`
Consumes []string `json:"consumes,omitempty"`
Deprecated string `json:"deprecated,omitempty"`
}
// 5.2.4 Parameter Object
type Parameter struct {
DataTypeFields
ParamType string `json:"paramType"` // path,query,body,header,form
Name string `json:"name"`
Description string `json:"description"`
Required bool `json:"required"`
AllowMultiple bool `json:"allowMultiple"`
}
// 5.2.5 Response Message Object
type ResponseMessage struct {
Code int `json:"code"`
Message string `json:"message"`
ResponseModel string `json:"responseModel,omitempty"`
}
// 5.2.6, 5.2.7 Models Object
type Model struct {
Id string `json:"id"`
Description string `json:"description,omitempty"`
Required []string `json:"required,omitempty"`
Properties ModelPropertyList `json:"properties"`
SubTypes []string `json:"subTypes,omitempty"`
Discriminator string `json:"discriminator,omitempty"`
}
// 5.2.8 Properties Object
type ModelProperty struct {
DataTypeFields
Description string `json:"description,omitempty"`
}
// 5.2.10
type Authorizations map[string]Authorization

View File

@@ -0,0 +1,21 @@
package swagger
type SwaggerBuilder struct {
SwaggerService
}
func NewSwaggerBuilder(config Config) *SwaggerBuilder {
return &SwaggerBuilder{*newSwaggerService(config)}
}
func (sb SwaggerBuilder) ProduceListing() ResourceListing {
return sb.SwaggerService.produceListing()
}
func (sb SwaggerBuilder) ProduceAllDeclarations() map[string]ApiDeclaration {
return sb.SwaggerService.produceAllDeclarations()
}
func (sb SwaggerBuilder) ProduceDeclarations(route string) (*ApiDeclaration, bool) {
return sb.SwaggerService.produceDeclarations(route)
}

View File

@@ -0,0 +1,318 @@
package swagger
import (
"encoding/json"
"testing"
"github.com/emicklei/go-restful"
"github.com/emicklei/go-restful-swagger12/test_package"
)
func TestInfoStruct_Issue231(t *testing.T) {
config := Config{
Info: Info{
Title: "Title",
Description: "Description",
TermsOfServiceUrl: "http://example.com",
Contact: "example@example.com",
License: "License",
LicenseUrl: "http://example.com/license.txt",
},
}
sws := newSwaggerService(config)
str, err := json.MarshalIndent(sws.produceListing(), "", " ")
if err != nil {
t.Fatal(err)
}
compareJson(t, string(str), `
{
"apiVersion": "",
"swaggerVersion": "1.2",
"apis": null,
"info": {
"title": "Title",
"description": "Description",
"termsOfServiceUrl": "http://example.com",
"contact": "example@example.com",
"license": "License",
"licenseUrl": "http://example.com/license.txt"
}
}
`)
}
// go test -v -test.run TestThatMultiplePathsOnRootAreHandled ...swagger
func TestThatMultiplePathsOnRootAreHandled(t *testing.T) {
ws1 := new(restful.WebService)
ws1.Route(ws1.GET("/_ping").To(dummy))
ws1.Route(ws1.GET("/version").To(dummy))
cfg := Config{
WebServicesUrl: "http://here.com",
ApiPath: "/apipath",
WebServices: []*restful.WebService{ws1},
}
sws := newSwaggerService(cfg)
decl := sws.composeDeclaration(ws1, "/")
if got, want := len(decl.Apis), 2; got != want {
t.Errorf("got %v want %v", got, want)
}
}
func TestWriteSamples(t *testing.T) {
ws1 := new(restful.WebService)
ws1.Route(ws1.GET("/object").To(dummy).Writes(test_package.TestStruct{}))
ws1.Route(ws1.GET("/array").To(dummy).Writes([]test_package.TestStruct{}))
ws1.Route(ws1.GET("/object_and_array").To(dummy).Writes(struct{ Abc test_package.TestStruct }{}))
cfg := Config{
WebServicesUrl: "http://here.com",
ApiPath: "/apipath",
WebServices: []*restful.WebService{ws1},
}
sws := newSwaggerService(cfg)
decl := sws.composeDeclaration(ws1, "/")
str, err := json.MarshalIndent(decl.Apis, "", " ")
if err != nil {
t.Fatal(err)
}
compareJson(t, string(str), `
[
{
"path": "/object",
"description": "",
"operations": [
{
"type": "test_package.TestStruct",
"method": "GET",
"nickname": "dummy",
"parameters": []
}
]
},
{
"path": "/array",
"description": "",
"operations": [
{
"type": "array",
"items": {
"$ref": "test_package.TestStruct"
},
"method": "GET",
"nickname": "dummy",
"parameters": []
}
]
},
{
"path": "/object_and_array",
"description": "",
"operations": [
{
"type": "struct { Abc test_package.TestStruct }",
"method": "GET",
"nickname": "dummy",
"parameters": []
}
]
}
]`)
str, err = json.MarshalIndent(decl.Models, "", " ")
if err != nil {
t.Fatal(err)
}
compareJson(t, string(str), `
{
"test_package.TestStruct": {
"id": "test_package.TestStruct",
"required": [
"TestField"
],
"properties": {
"TestField": {
"type": "string"
}
}
},
"||test_package.TestStruct": {
"id": "||test_package.TestStruct",
"properties": {}
},
"struct { Abc test_package.TestStruct }": {
"id": "struct { Abc test_package.TestStruct }",
"required": [
"Abc"
],
"properties": {
"Abc": {
"$ref": "test_package.TestStruct"
}
}
}
}`)
}
func TestRoutesWithCommonPart(t *testing.T) {
ws1 := new(restful.WebService)
ws1.Path("/")
ws1.Route(ws1.GET("/foobar").To(dummy).Writes(test_package.TestStruct{}))
ws1.Route(ws1.HEAD("/foobar").To(dummy).Writes(test_package.TestStruct{}))
ws1.Route(ws1.GET("/foo").To(dummy).Writes([]test_package.TestStruct{}))
ws1.Route(ws1.HEAD("/foo").To(dummy).Writes(test_package.TestStruct{}))
cfg := Config{
WebServicesUrl: "http://here.com",
ApiPath: "/apipath",
WebServices: []*restful.WebService{ws1},
}
sws := newSwaggerService(cfg)
decl := sws.composeDeclaration(ws1, "/foo")
str, err := json.MarshalIndent(decl.Apis, "", " ")
if err != nil {
t.Fatal(err)
}
compareJson(t, string(str), `[
{
"path": "/foo",
"description": "",
"operations": [
{
"type": "array",
"items": {
"$ref": "test_package.TestStruct"
},
"method": "GET",
"nickname": "dummy",
"parameters": []
},
{
"type": "test_package.TestStruct",
"method": "HEAD",
"nickname": "dummy",
"parameters": []
}
]
}
]`)
}
// go test -v -test.run TestServiceToApi ...swagger
func TestServiceToApi(t *testing.T) {
ws := new(restful.WebService)
ws.Path("/tests")
ws.Consumes(restful.MIME_JSON)
ws.Produces(restful.MIME_XML)
ws.Route(ws.GET("/a").To(dummy).Writes(sample{}))
ws.Route(ws.PUT("/b").To(dummy).Writes(sample{}))
ws.Route(ws.POST("/c").To(dummy).Writes(sample{}))
ws.Route(ws.DELETE("/d").To(dummy).Writes(sample{}))
ws.Route(ws.GET("/d").To(dummy).Writes(sample{}))
ws.Route(ws.PUT("/c").To(dummy).Writes(sample{}))
ws.Route(ws.POST("/b").To(dummy).Writes(sample{}))
ws.Route(ws.DELETE("/a").To(dummy).Writes(sample{}))
ws.ApiVersion("1.2.3")
cfg := Config{
WebServicesUrl: "http://here.com",
ApiPath: "/apipath",
WebServices: []*restful.WebService{ws},
PostBuildHandler: func(in *ApiDeclarationList) {},
}
sws := newSwaggerService(cfg)
decl := sws.composeDeclaration(ws, "/tests")
// checks
if decl.ApiVersion != "1.2.3" {
t.Errorf("got %v want %v", decl.ApiVersion, "1.2.3")
}
if decl.BasePath != "http://here.com" {
t.Errorf("got %v want %v", decl.BasePath, "http://here.com")
}
if len(decl.Apis) != 4 {
t.Errorf("got %v want %v", len(decl.Apis), 4)
}
pathOrder := ""
for _, each := range decl.Apis {
pathOrder += each.Path
for _, other := range each.Operations {
pathOrder += other.Method
}
}
if pathOrder != "/tests/aGETDELETE/tests/bPUTPOST/tests/cPOSTPUT/tests/dDELETEGET" {
t.Errorf("got %v want %v", pathOrder, "see test source")
}
}
func dummy(i *restful.Request, o *restful.Response) {}
// go test -v -test.run TestIssue78 ...swagger
type Response struct {
Code int
Users *[]User
Items *[]TestItem
}
type User struct {
Id, Name string
}
type TestItem struct {
Id, Name string
}
// clear && go test -v -test.run TestComposeResponseMessages ...swagger
func TestComposeResponseMessages(t *testing.T) {
responseErrors := map[int]restful.ResponseError{}
responseErrors[400] = restful.ResponseError{Code: 400, Message: "Bad Request", Model: TestItem{}}
route := restful.Route{ResponseErrors: responseErrors}
decl := new(ApiDeclaration)
decl.Models = ModelList{}
msgs := composeResponseMessages(route, decl, &Config{})
if msgs[0].ResponseModel != "swagger.TestItem" {
t.Errorf("got %s want swagger.TestItem", msgs[0].ResponseModel)
}
}
func TestIssue78(t *testing.T) {
sws := newSwaggerService(Config{})
models := new(ModelList)
sws.addModelFromSampleTo(&Operation{}, true, Response{Items: &[]TestItem{}}, models)
model, ok := models.At("swagger.Response")
if !ok {
t.Fatal("missing response model")
}
if "swagger.Response" != model.Id {
t.Fatal("wrong model id:" + model.Id)
}
code, ok := model.Properties.At("Code")
if !ok {
t.Fatal("missing code")
}
if "integer" != *code.Type {
t.Fatal("wrong code type:" + *code.Type)
}
items, ok := model.Properties.At("Items")
if !ok {
t.Fatal("missing items")
}
if "array" != *items.Type {
t.Fatal("wrong items type:" + *items.Type)
}
items_items := items.Items
if items_items == nil {
t.Fatal("missing items->items")
}
ref := items_items.Ref
if ref == nil {
t.Fatal("missing $ref")
}
if *ref != "swagger.TestItem" {
t.Fatal("wrong $ref:" + *ref)
}
}

View File

@@ -0,0 +1,443 @@
package swagger
import (
"fmt"
"github.com/emicklei/go-restful"
// "github.com/emicklei/hopwatch"
"net/http"
"reflect"
"sort"
"strings"
"github.com/emicklei/go-restful/log"
)
type SwaggerService struct {
config Config
apiDeclarationMap *ApiDeclarationList
}
func newSwaggerService(config Config) *SwaggerService {
sws := &SwaggerService{
config: config,
apiDeclarationMap: new(ApiDeclarationList)}
// Build all ApiDeclarations
for _, each := range config.WebServices {
rootPath := each.RootPath()
// skip the api service itself
if rootPath != config.ApiPath {
if rootPath == "" || rootPath == "/" {
// use routes
for _, route := range each.Routes() {
entry := staticPathFromRoute(route)
_, exists := sws.apiDeclarationMap.At(entry)
if !exists {
sws.apiDeclarationMap.Put(entry, sws.composeDeclaration(each, entry))
}
}
} else { // use root path
sws.apiDeclarationMap.Put(each.RootPath(), sws.composeDeclaration(each, each.RootPath()))
}
}
}
// if specified then call the PostBuilderHandler
if config.PostBuildHandler != nil {
config.PostBuildHandler(sws.apiDeclarationMap)
}
return sws
}
// LogInfo is the function that is called when this package needs to log. It defaults to log.Printf
var LogInfo = func(format string, v ...interface{}) {
// use the restful package-wide logger
log.Printf(format, v...)
}
// InstallSwaggerService add the WebService that provides the API documentation of all services
// conform the Swagger documentation specifcation. (https://github.com/wordnik/swagger-core/wiki).
func InstallSwaggerService(aSwaggerConfig Config) {
RegisterSwaggerService(aSwaggerConfig, restful.DefaultContainer)
}
// RegisterSwaggerService add the WebService that provides the API documentation of all services
// conform the Swagger documentation specifcation. (https://github.com/wordnik/swagger-core/wiki).
func RegisterSwaggerService(config Config, wsContainer *restful.Container) {
sws := newSwaggerService(config)
ws := new(restful.WebService)
ws.Path(config.ApiPath)
ws.Produces(restful.MIME_JSON)
if config.DisableCORS {
ws.Filter(enableCORS)
}
ws.Route(ws.GET("/").To(sws.getListing))
ws.Route(ws.GET("/{a}").To(sws.getDeclarations))
ws.Route(ws.GET("/{a}/{b}").To(sws.getDeclarations))
ws.Route(ws.GET("/{a}/{b}/{c}").To(sws.getDeclarations))
ws.Route(ws.GET("/{a}/{b}/{c}/{d}").To(sws.getDeclarations))
ws.Route(ws.GET("/{a}/{b}/{c}/{d}/{e}").To(sws.getDeclarations))
ws.Route(ws.GET("/{a}/{b}/{c}/{d}/{e}/{f}").To(sws.getDeclarations))
ws.Route(ws.GET("/{a}/{b}/{c}/{d}/{e}/{f}/{g}").To(sws.getDeclarations))
LogInfo("[restful/swagger] listing is available at %v%v", config.WebServicesUrl, config.ApiPath)
wsContainer.Add(ws)
// Check paths for UI serving
if config.StaticHandler == nil && config.SwaggerFilePath != "" && config.SwaggerPath != "" {
swaggerPathSlash := config.SwaggerPath
// path must end with slash /
if "/" != config.SwaggerPath[len(config.SwaggerPath)-1:] {
LogInfo("[restful/swagger] use corrected SwaggerPath ; must end with slash (/)")
swaggerPathSlash += "/"
}
LogInfo("[restful/swagger] %v%v is mapped to folder %v", config.WebServicesUrl, swaggerPathSlash, config.SwaggerFilePath)
wsContainer.Handle(swaggerPathSlash, http.StripPrefix(swaggerPathSlash, http.FileServer(http.Dir(config.SwaggerFilePath))))
//if we define a custom static handler use it
} else if config.StaticHandler != nil && config.SwaggerPath != "" {
swaggerPathSlash := config.SwaggerPath
// path must end with slash /
if "/" != config.SwaggerPath[len(config.SwaggerPath)-1:] {
LogInfo("[restful/swagger] use corrected SwaggerFilePath ; must end with slash (/)")
swaggerPathSlash += "/"
}
LogInfo("[restful/swagger] %v%v is mapped to custom Handler %T", config.WebServicesUrl, swaggerPathSlash, config.StaticHandler)
wsContainer.Handle(swaggerPathSlash, config.StaticHandler)
} else {
LogInfo("[restful/swagger] Swagger(File)Path is empty ; no UI is served")
}
}
func staticPathFromRoute(r restful.Route) string {
static := r.Path
bracket := strings.Index(static, "{")
if bracket <= 1 { // result cannot be empty
return static
}
if bracket != -1 {
static = r.Path[:bracket]
}
if strings.HasSuffix(static, "/") {
return static[:len(static)-1]
} else {
return static
}
}
func enableCORS(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) {
if origin := req.HeaderParameter(restful.HEADER_Origin); origin != "" {
// prevent duplicate header
if len(resp.Header().Get(restful.HEADER_AccessControlAllowOrigin)) == 0 {
resp.AddHeader(restful.HEADER_AccessControlAllowOrigin, origin)
}
}
chain.ProcessFilter(req, resp)
}
func (sws SwaggerService) getListing(req *restful.Request, resp *restful.Response) {
listing := sws.produceListing()
resp.WriteAsJson(listing)
}
func (sws SwaggerService) produceListing() ResourceListing {
listing := ResourceListing{SwaggerVersion: swaggerVersion, ApiVersion: sws.config.ApiVersion, Info: sws.config.Info}
sws.apiDeclarationMap.Do(func(k string, v ApiDeclaration) {
ref := Resource{Path: k}
if len(v.Apis) > 0 { // use description of first (could still be empty)
ref.Description = v.Apis[0].Description
}
listing.Apis = append(listing.Apis, ref)
})
return listing
}
func (sws SwaggerService) getDeclarations(req *restful.Request, resp *restful.Response) {
decl, ok := sws.produceDeclarations(composeRootPath(req))
if !ok {
resp.WriteErrorString(http.StatusNotFound, "ApiDeclaration not found")
return
}
// unless WebServicesUrl is given
if len(sws.config.WebServicesUrl) == 0 {
// update base path from the actual request
// TODO how to detect https? assume http for now
var host string
// X-Forwarded-Host or Host or Request.Host
hostvalues, ok := req.Request.Header["X-Forwarded-Host"] // apache specific?
if !ok || len(hostvalues) == 0 {
forwarded, ok := req.Request.Header["Host"] // without reverse-proxy
if !ok || len(forwarded) == 0 {
// fallback to Host field
host = req.Request.Host
} else {
host = forwarded[0]
}
} else {
host = hostvalues[0]
}
// inspect Referer for the scheme (http vs https)
scheme := "http"
if referer := req.Request.Header["Referer"]; len(referer) > 0 {
if strings.HasPrefix(referer[0], "https") {
scheme = "https"
}
}
decl.BasePath = fmt.Sprintf("%s://%s", scheme, host)
}
resp.WriteAsJson(decl)
}
func (sws SwaggerService) produceAllDeclarations() map[string]ApiDeclaration {
decls := map[string]ApiDeclaration{}
sws.apiDeclarationMap.Do(func(k string, v ApiDeclaration) {
decls[k] = v
})
return decls
}
func (sws SwaggerService) produceDeclarations(route string) (*ApiDeclaration, bool) {
decl, ok := sws.apiDeclarationMap.At(route)
if !ok {
return nil, false
}
decl.BasePath = sws.config.WebServicesUrl
return &decl, true
}
// composeDeclaration uses all routes and parameters to create a ApiDeclaration
func (sws SwaggerService) composeDeclaration(ws *restful.WebService, pathPrefix string) ApiDeclaration {
decl := ApiDeclaration{
SwaggerVersion: swaggerVersion,
BasePath: sws.config.WebServicesUrl,
ResourcePath: pathPrefix,
Models: ModelList{},
ApiVersion: ws.Version()}
// collect any path parameters
rootParams := []Parameter{}
for _, param := range ws.PathParameters() {
rootParams = append(rootParams, asSwaggerParameter(param.Data()))
}
// aggregate by path
pathToRoutes := newOrderedRouteMap()
for _, other := range ws.Routes() {
if strings.HasPrefix(other.Path, pathPrefix) {
if len(pathPrefix) > 1 && len(other.Path) > len(pathPrefix) && other.Path[len(pathPrefix)] != '/' {
continue
}
pathToRoutes.Add(other.Path, other)
}
}
pathToRoutes.Do(func(path string, routes []restful.Route) {
api := Api{Path: strings.TrimSuffix(withoutWildcard(path), "/"), Description: ws.Documentation()}
voidString := "void"
for _, route := range routes {
operation := Operation{
Method: route.Method,
Summary: route.Doc,
Notes: route.Notes,
// Type gets overwritten if there is a write sample
DataTypeFields: DataTypeFields{Type: &voidString},
Parameters: []Parameter{},
Nickname: route.Operation,
ResponseMessages: composeResponseMessages(route, &decl, &sws.config)}
operation.Consumes = route.Consumes
operation.Produces = route.Produces
// share root params if any
for _, swparam := range rootParams {
operation.Parameters = append(operation.Parameters, swparam)
}
// route specific params
for _, param := range route.ParameterDocs {
operation.Parameters = append(operation.Parameters, asSwaggerParameter(param.Data()))
}
sws.addModelsFromRouteTo(&operation, route, &decl)
api.Operations = append(api.Operations, operation)
}
decl.Apis = append(decl.Apis, api)
})
return decl
}
func withoutWildcard(path string) string {
if strings.HasSuffix(path, ":*}") {
return path[0:len(path)-3] + "}"
}
return path
}
// composeResponseMessages takes the ResponseErrors (if any) and creates ResponseMessages from them.
func composeResponseMessages(route restful.Route, decl *ApiDeclaration, config *Config) (messages []ResponseMessage) {
if route.ResponseErrors == nil {
return messages
}
// sort by code
codes := sort.IntSlice{}
for code := range route.ResponseErrors {
codes = append(codes, code)
}
codes.Sort()
for _, code := range codes {
each := route.ResponseErrors[code]
message := ResponseMessage{
Code: code,
Message: each.Message,
}
if each.Model != nil {
st := reflect.TypeOf(each.Model)
isCollection, st := detectCollectionType(st)
// collection cannot be in responsemodel
if !isCollection {
modelName := modelBuilder{}.keyFrom(st)
modelBuilder{Models: &decl.Models, Config: config}.addModel(st, "")
message.ResponseModel = modelName
}
}
messages = append(messages, message)
}
return
}
// addModelsFromRoute takes any read or write sample from the Route and creates a Swagger model from it.
func (sws SwaggerService) addModelsFromRouteTo(operation *Operation, route restful.Route, decl *ApiDeclaration) {
if route.ReadSample != nil {
sws.addModelFromSampleTo(operation, false, route.ReadSample, &decl.Models)
}
if route.WriteSample != nil {
sws.addModelFromSampleTo(operation, true, route.WriteSample, &decl.Models)
}
}
func detectCollectionType(st reflect.Type) (bool, reflect.Type) {
isCollection := false
if st.Kind() == reflect.Slice || st.Kind() == reflect.Array {
st = st.Elem()
isCollection = true
} else {
if st.Kind() == reflect.Ptr {
if st.Elem().Kind() == reflect.Slice || st.Elem().Kind() == reflect.Array {
st = st.Elem().Elem()
isCollection = true
}
}
}
return isCollection, st
}
// addModelFromSample creates and adds (or overwrites) a Model from a sample resource
func (sws SwaggerService) addModelFromSampleTo(operation *Operation, isResponse bool, sample interface{}, models *ModelList) {
mb := modelBuilder{Models: models, Config: &sws.config}
if isResponse {
sampleType, items := asDataType(sample, &sws.config)
operation.Type = sampleType
operation.Items = items
}
mb.addModelFrom(sample)
}
func asSwaggerParameter(param restful.ParameterData) Parameter {
return Parameter{
DataTypeFields: DataTypeFields{
Type: &param.DataType,
Format: asFormat(param.DataType, param.DataFormat),
DefaultValue: Special(param.DefaultValue),
},
Name: param.Name,
Description: param.Description,
ParamType: asParamType(param.Kind),
Required: param.Required}
}
// Between 1..7 path parameters is supported
func composeRootPath(req *restful.Request) string {
path := "/" + req.PathParameter("a")
b := req.PathParameter("b")
if b == "" {
return path
}
path = path + "/" + b
c := req.PathParameter("c")
if c == "" {
return path
}
path = path + "/" + c
d := req.PathParameter("d")
if d == "" {
return path
}
path = path + "/" + d
e := req.PathParameter("e")
if e == "" {
return path
}
path = path + "/" + e
f := req.PathParameter("f")
if f == "" {
return path
}
path = path + "/" + f
g := req.PathParameter("g")
if g == "" {
return path
}
return path + "/" + g
}
func asFormat(dataType string, dataFormat string) string {
if dataFormat != "" {
return dataFormat
}
return "" // TODO
}
func asParamType(kind int) string {
switch {
case kind == restful.PathParameterKind:
return "path"
case kind == restful.QueryParameterKind:
return "query"
case kind == restful.BodyParameterKind:
return "body"
case kind == restful.HeaderParameterKind:
return "header"
case kind == restful.FormParameterKind:
return "form"
}
return ""
}
func asDataType(any interface{}, config *Config) (*string, *Item) {
// If it's not a collection, return the suggested model name
st := reflect.TypeOf(any)
isCollection, st := detectCollectionType(st)
modelName := modelBuilder{}.keyFrom(st)
// if it's not a collection we are done
if !isCollection {
return &modelName, nil
}
// XXX: This is not very elegant
// We create an Item object referring to the given model
models := ModelList{}
mb := modelBuilder{Models: &models, Config: config}
mb.addModelFrom(any)
elemTypeName := mb.getElementTypeName(modelName, "", st)
item := new(Item)
if mb.isPrimitiveType(elemTypeName) {
mapped := mb.jsonSchemaType(elemTypeName)
item.Type = &mapped
} else {
item.Ref = &elemTypeName
}
tmp := "array"
return &tmp, item
}

View File

@@ -0,0 +1,5 @@
package test_package
type TestStruct struct {
TestField string
}

View File

@@ -0,0 +1,86 @@
package swagger
import (
"bytes"
"encoding/json"
"fmt"
"reflect"
"strings"
"testing"
)
func testJsonFromStructWithConfig(t *testing.T, sample interface{}, expectedJson string, config *Config) bool {
m := modelsFromStructWithConfig(sample, config)
data, _ := json.MarshalIndent(m, " ", " ")
return compareJson(t, string(data), expectedJson)
}
func modelsFromStructWithConfig(sample interface{}, config *Config) *ModelList {
models := new(ModelList)
builder := modelBuilder{Models: models, Config: config}
builder.addModelFrom(sample)
return models
}
func testJsonFromStruct(t *testing.T, sample interface{}, expectedJson string) bool {
return testJsonFromStructWithConfig(t, sample, expectedJson, &Config{})
}
func modelsFromStruct(sample interface{}) *ModelList {
return modelsFromStructWithConfig(sample, &Config{})
}
func compareJson(t *testing.T, actualJsonAsString string, expectedJsonAsString string) bool {
success := false
var actualMap map[string]interface{}
json.Unmarshal([]byte(actualJsonAsString), &actualMap)
var expectedMap map[string]interface{}
err := json.Unmarshal([]byte(expectedJsonAsString), &expectedMap)
if err != nil {
var actualArray []interface{}
json.Unmarshal([]byte(actualJsonAsString), &actualArray)
var expectedArray []interface{}
err := json.Unmarshal([]byte(expectedJsonAsString), &expectedArray)
success = reflect.DeepEqual(actualArray, expectedArray)
if err != nil {
t.Fatalf("Unparsable expected JSON: %s, actual: %v, expected: %v", err, actualJsonAsString, expectedJsonAsString)
}
} else {
success = reflect.DeepEqual(actualMap, expectedMap)
}
if !success {
t.Log("---- expected -----")
t.Log(withLineNumbers(expectedJsonAsString))
t.Log("---- actual -----")
t.Log(withLineNumbers(actualJsonAsString))
t.Log("---- raw -----")
t.Log(actualJsonAsString)
t.Error("there are differences")
return false
}
return true
}
func indexOfNonMatchingLine(actual, expected string) int {
a := strings.Split(actual, "\n")
e := strings.Split(expected, "\n")
size := len(a)
if len(e) < len(a) {
size = len(e)
}
for i := 0; i < size; i++ {
if a[i] != e[i] {
return i
}
}
return -1
}
func withLineNumbers(content string) string {
var buffer bytes.Buffer
lines := strings.Split(content, "\n")
for i, each := range lines {
buffer.WriteString(fmt.Sprintf("%d:%s\n", i, each))
}
return buffer.String()
}

70
vendor/github.com/emicklei/go-restful/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,70 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
restful.html
*.out
tmp.prof
go-restful.test
examples/restful-basic-authentication
examples/restful-encoding-filter
examples/restful-filters
examples/restful-hello-world
examples/restful-resource-functions
examples/restful-serve-static
examples/restful-user-service
*.DS_Store
examples/restful-user-resource
examples/restful-multi-containers
examples/restful-form-handling
examples/restful-CORS-filter
examples/restful-options-filter
examples/restful-curly-router
examples/restful-cpuprofiler-service
examples/restful-pre-post-filters
curly.prof
examples/restful-NCSA-logging
examples/restful-html-template
s.html
restful-path-tail

6
vendor/github.com/emicklei/go-restful/.travis.yml generated vendored Normal file
View File

@@ -0,0 +1,6 @@
language: go
go:
- 1.x
script: go test -v

223
vendor/github.com/emicklei/go-restful/CHANGES.md generated vendored Normal file
View File

@@ -0,0 +1,223 @@
Change history of go-restful
=
2017-02-16
- solved issue #304, make operation names unique
2017-01-30
[IMPORTANT] For swagger users, change your import statement to:
swagger "github.com/emicklei/go-restful-swagger12"
- moved swagger 1.2 code to go-restful-swagger12
- created TAG 2.0.0
2017-01-27
- remove defer request body close
- expose Dispatch for testing filters and Routefunctions
- swagger response model cannot be array
- created TAG 1.0.0
2016-12-22
- (API change) Remove code related to caching request content. Removes SetCacheReadEntity(doCache bool)
2016-11-26
- Default change! now use CurlyRouter (was RouterJSR311)
- Default change! no more caching of request content
- Default change! do not recover from panics
2016-09-22
- fix the DefaultRequestContentType feature
2016-02-14
- take the qualify factor of the Accept header mediatype into account when deciding the contentype of the response
- add constructors for custom entity accessors for xml and json
2015-09-27
- rename new WriteStatusAnd... to WriteHeaderAnd... for consistency
2015-09-25
- fixed problem with changing Header after WriteHeader (issue 235)
2015-09-14
- changed behavior of WriteHeader (immediate write) and WriteEntity (no status write)
- added support for custom EntityReaderWriters.
2015-08-06
- add support for reading entities from compressed request content
- use sync.Pool for compressors of http response and request body
- add Description to Parameter for documentation in Swagger UI
2015-03-20
- add configurable logging
2015-03-18
- if not specified, the Operation is derived from the Route function
2015-03-17
- expose Parameter creation functions
- make trace logger an interface
- fix OPTIONSFilter
- customize rendering of ServiceError
- JSR311 router now handles wildcards
- add Notes to Route
2014-11-27
- (api add) PrettyPrint per response. (as proposed in #167)
2014-11-12
- (api add) ApiVersion(.) for documentation in Swagger UI
2014-11-10
- (api change) struct fields tagged with "description" show up in Swagger UI
2014-10-31
- (api change) ReturnsError -> Returns
- (api add) RouteBuilder.Do(aBuilder) for DRY use of RouteBuilder
- fix swagger nested structs
- sort Swagger response messages by code
2014-10-23
- (api add) ReturnsError allows you to document Http codes in swagger
- fixed problem with greedy CurlyRouter
- (api add) Access-Control-Max-Age in CORS
- add tracing functionality (injectable) for debugging purposes
- support JSON parse 64bit int
- fix empty parameters for swagger
- WebServicesUrl is now optional for swagger
- fixed duplicate AccessControlAllowOrigin in CORS
- (api change) expose ServeMux in container
- (api add) added AllowedDomains in CORS
- (api add) ParameterNamed for detailed documentation
2014-04-16
- (api add) expose constructor of Request for testing.
2014-06-27
- (api add) ParameterNamed gives access to a Parameter definition and its data (for further specification).
- (api add) SetCacheReadEntity allow scontrol over whether or not the request body is being cached (default true for compatibility reasons).
2014-07-03
- (api add) CORS can be configured with a list of allowed domains
2014-03-12
- (api add) Route path parameters can use wildcard or regular expressions. (requires CurlyRouter)
2014-02-26
- (api add) Request now provides information about the matched Route, see method SelectedRoutePath
2014-02-17
- (api change) renamed parameter constants (go-lint checks)
2014-01-10
- (api add) support for CloseNotify, see http://golang.org/pkg/net/http/#CloseNotifier
2014-01-07
- (api change) Write* methods in Response now return the error or nil.
- added example of serving HTML from a Go template.
- fixed comparing Allowed headers in CORS (is now case-insensitive)
2013-11-13
- (api add) Response knows how many bytes are written to the response body.
2013-10-29
- (api add) RecoverHandler(handler RecoverHandleFunction) to change how panic recovery is handled. Default behavior is to log and return a stacktrace. This may be a security issue as it exposes sourcecode information.
2013-10-04
- (api add) Response knows what HTTP status has been written
- (api add) Request can have attributes (map of string->interface, also called request-scoped variables
2013-09-12
- (api change) Router interface simplified
- Implemented CurlyRouter, a Router that does not use|allow regular expressions in paths
2013-08-05
- add OPTIONS support
- add CORS support
2013-08-27
- fixed some reported issues (see github)
- (api change) deprecated use of WriteError; use WriteErrorString instead
2014-04-15
- (fix) v1.0.1 tag: fix Issue 111: WriteErrorString
2013-08-08
- (api add) Added implementation Container: a WebServices collection with its own http.ServeMux allowing multiple endpoints per program. Existing uses of go-restful will register their services to the DefaultContainer.
- (api add) the swagger package has be extended to have a UI per container.
- if panic is detected then a small stack trace is printed (thanks to runner-mei)
- (api add) WriteErrorString to Response
Important API changes:
- (api remove) package variable DoNotRecover no longer works ; use restful.DefaultContainer.DoNotRecover(true) instead.
- (api remove) package variable EnableContentEncoding no longer works ; use restful.DefaultContainer.EnableContentEncoding(true) instead.
2013-07-06
- (api add) Added support for response encoding (gzip and deflate(zlib)). This feature is disabled on default (for backwards compatibility). Use restful.EnableContentEncoding = true in your initialization to enable this feature.
2013-06-19
- (improve) DoNotRecover option, moved request body closer, improved ReadEntity
2013-06-03
- (api change) removed Dispatcher interface, hide PathExpression
- changed receiver names of type functions to be more idiomatic Go
2013-06-02
- (optimize) Cache the RegExp compilation of Paths.
2013-05-22
- (api add) Added support for request/response filter functions
2013-05-18
- (api add) Added feature to change the default Http Request Dispatch function (travis cline)
- (api change) Moved Swagger Webservice to swagger package (see example restful-user)
[2012-11-14 .. 2013-05-18>
- See https://github.com/emicklei/go-restful/commits
2012-11-14
- Initial commit

22
vendor/github.com/emicklei/go-restful/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,22 @@
Copyright (c) 2012,2013 Ernest Micklei
MIT License
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.

7
vendor/github.com/emicklei/go-restful/Makefile generated vendored Normal file
View File

@@ -0,0 +1,7 @@
all: test
test:
go test -v .
ex:
cd examples && ls *.go | xargs go build -o /tmp/ignore

74
vendor/github.com/emicklei/go-restful/README.md generated vendored Normal file
View File

@@ -0,0 +1,74 @@
go-restful
==========
package for building REST-style Web Services using Google Go
[![Build Status](https://travis-ci.org/emicklei/go-restful.png)](https://travis-ci.org/emicklei/go-restful)
[![Go Report Card](https://goreportcard.com/badge/github.com/emicklei/go-restful)](https://goreportcard.com/report/github.com/emicklei/go-restful)
[![GoDoc](https://godoc.org/github.com/emicklei/go-restful?status.svg)](https://godoc.org/github.com/emicklei/go-restful)
- [Code examples](https://github.com/emicklei/go-restful/tree/master/examples)
REST asks developers to use HTTP methods explicitly and in a way that's consistent with the protocol definition. This basic REST design principle establishes a one-to-one mapping between create, read, update, and delete (CRUD) operations and HTTP methods. According to this mapping:
- GET = Retrieve a representation of a resource
- POST = Create if you are sending content to the server to create a subordinate of the specified resource collection, using some server-side algorithm.
- PUT = Create if you are sending the full content of the specified resource (URI).
- PUT = Update if you are updating the full content of the specified resource.
- DELETE = Delete if you are requesting the server to delete the resource
- PATCH = Update partial content of a resource
- OPTIONS = Get information about the communication options for the request URI
### Example
```Go
ws := new(restful.WebService)
ws.
Path("/users").
Consumes(restful.MIME_XML, restful.MIME_JSON).
Produces(restful.MIME_JSON, restful.MIME_XML)
ws.Route(ws.GET("/{user-id}").To(u.findUser).
Doc("get a user").
Param(ws.PathParameter("user-id", "identifier of the user").DataType("string")).
Writes(User{}))
...
func (u UserResource) findUser(request *restful.Request, response *restful.Response) {
id := request.PathParameter("user-id")
...
}
```
[Full API of a UserResource](https://github.com/emicklei/go-restful/tree/master/examples/restful-user-resource.go)
### Features
- Routes for request &#8594; function mapping with path parameter (e.g. {id}) support
- Configurable router:
- (default) Fast routing algorithm that allows static elements, regular expressions and dynamic parameters in the URL path (e.g. /meetings/{id} or /static/{subpath:*}
- Routing algorithm after [JSR311](http://jsr311.java.net/nonav/releases/1.1/spec/spec.html) that is implemented using (but does **not** accept) regular expressions
- Request API for reading structs from JSON/XML and accesing parameters (path,query,header)
- Response API for writing structs to JSON/XML and setting headers
- Customizable encoding using EntityReaderWriter registration
- Filters for intercepting the request &#8594; response flow on Service or Route level
- Request-scoped variables using attributes
- Containers for WebServices on different HTTP endpoints
- Content encoding (gzip,deflate) of request and response payloads
- Automatic responses on OPTIONS (using a filter)
- Automatic CORS request handling (using a filter)
- API declaration for Swagger UI (see [go-restful-swagger12](https://github.com/emicklei/go-restful-swagger12),[go-restful-openapi](https://github.com/emicklei/go-restful-openapi))
- Panic recovery to produce HTTP 500, customizable using RecoverHandler(...)
- Route errors produce HTTP 404/405/406/415 errors, customizable using ServiceErrorHandler(...)
- Configurable (trace) logging
- Customizable gzip/deflate readers and writers using CompressorProvider registration
### Resources
- [Example posted on blog](http://ernestmicklei.com/2012/11/go-restful-first-working-example/)
- [Design explained on blog](http://ernestmicklei.com/2012/11/go-restful-api-design/)
- [sourcegraph](https://sourcegraph.com/github.com/emicklei/go-restful)
- [showcase: Mora - MongoDB REST Api server](https://github.com/emicklei/mora)
Type ```git shortlog -s``` for a full list of contributors.
© 2012 - 2017, http://ernestmicklei.com. MIT License. Contributions are welcome.

1
vendor/github.com/emicklei/go-restful/Srcfile generated vendored Normal file
View File

@@ -0,0 +1 @@
{"SkipDirs": ["examples"]}

View File

@@ -0,0 +1,51 @@
package restful
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
)
func setupCurly(container *Container) []string {
wsCount := 26
rtCount := 26
urisCurly := []string{}
container.Router(CurlyRouter{})
for i := 0; i < wsCount; i++ {
root := fmt.Sprintf("/%s/{%s}/", string(i+97), string(i+97))
ws := new(WebService).Path(root)
for j := 0; j < rtCount; j++ {
sub := fmt.Sprintf("/%s2/{%s2}", string(j+97), string(j+97))
ws.Route(ws.GET(sub).Consumes("application/xml").Produces("application/xml").To(echoCurly))
}
container.Add(ws)
for _, each := range ws.Routes() {
urisCurly = append(urisCurly, "http://bench.com"+each.Path)
}
}
return urisCurly
}
func echoCurly(req *Request, resp *Response) {}
func BenchmarkManyCurly(b *testing.B) {
container := NewContainer()
urisCurly := setupCurly(container)
b.ResetTimer()
for t := 0; t < b.N; t++ {
for r := 0; r < 1000; r++ {
for _, each := range urisCurly {
sendNoReturnTo(each, container, t)
}
}
}
}
func sendNoReturnTo(address string, container *Container, t int) {
httpRequest, _ := http.NewRequest("GET", address, nil)
httpRequest.Header.Set("Accept", "application/xml")
httpWriter := httptest.NewRecorder()
container.dispatch(httpWriter, httpRequest)
}

43
vendor/github.com/emicklei/go-restful/bench_test.go generated vendored Normal file
View File

@@ -0,0 +1,43 @@
package restful
import (
"fmt"
"io"
"testing"
)
var uris = []string{}
func setup(container *Container) {
wsCount := 26
rtCount := 26
for i := 0; i < wsCount; i++ {
root := fmt.Sprintf("/%s/{%s}/", string(i+97), string(i+97))
ws := new(WebService).Path(root)
for j := 0; j < rtCount; j++ {
sub := fmt.Sprintf("/%s2/{%s2}", string(j+97), string(j+97))
ws.Route(ws.GET(sub).To(echo))
}
container.Add(ws)
for _, each := range ws.Routes() {
uris = append(uris, "http://bench.com"+each.Path)
}
}
}
func echo(req *Request, resp *Response) {
io.WriteString(resp.ResponseWriter, "echo")
}
func BenchmarkMany(b *testing.B) {
container := NewContainer()
setup(container)
b.ResetTimer()
for t := 0; t < b.N; t++ {
for _, each := range uris {
// println(each)
sendItTo(each, container)
}
}
}

10
vendor/github.com/emicklei/go-restful/bench_test.sh generated vendored Normal file
View File

@@ -0,0 +1,10 @@
#go test -run=none -file bench_test.go -test.bench . -cpuprofile=bench_test.out
go test -c
./go-restful.test -test.run=none -test.cpuprofile=tmp.prof -test.bench=BenchmarkMany
./go-restful.test -test.run=none -test.cpuprofile=curly.prof -test.bench=BenchmarkManyCurly
#go tool pprof go-restful.test tmp.prof
go tool pprof go-restful.test curly.prof

123
vendor/github.com/emicklei/go-restful/compress.go generated vendored Normal file
View File

@@ -0,0 +1,123 @@
package restful
// Copyright 2013 Ernest Micklei. All rights reserved.
// Use of this source code is governed by a license
// that can be found in the LICENSE file.
import (
"bufio"
"compress/gzip"
"compress/zlib"
"errors"
"io"
"net"
"net/http"
"strings"
)
// OBSOLETE : use restful.DefaultContainer.EnableContentEncoding(true) to change this setting.
var EnableContentEncoding = false
// CompressingResponseWriter is a http.ResponseWriter that can perform content encoding (gzip and zlib)
type CompressingResponseWriter struct {
writer http.ResponseWriter
compressor io.WriteCloser
encoding string
}
// Header is part of http.ResponseWriter interface
func (c *CompressingResponseWriter) Header() http.Header {
return c.writer.Header()
}
// WriteHeader is part of http.ResponseWriter interface
func (c *CompressingResponseWriter) WriteHeader(status int) {
c.writer.WriteHeader(status)
}
// Write is part of http.ResponseWriter interface
// It is passed through the compressor
func (c *CompressingResponseWriter) Write(bytes []byte) (int, error) {
if c.isCompressorClosed() {
return -1, errors.New("Compressing error: tried to write data using closed compressor")
}
return c.compressor.Write(bytes)
}
// CloseNotify is part of http.CloseNotifier interface
func (c *CompressingResponseWriter) CloseNotify() <-chan bool {
return c.writer.(http.CloseNotifier).CloseNotify()
}
// Close the underlying compressor
func (c *CompressingResponseWriter) Close() error {
if c.isCompressorClosed() {
return errors.New("Compressing error: tried to close already closed compressor")
}
c.compressor.Close()
if ENCODING_GZIP == c.encoding {
currentCompressorProvider.ReleaseGzipWriter(c.compressor.(*gzip.Writer))
}
if ENCODING_DEFLATE == c.encoding {
currentCompressorProvider.ReleaseZlibWriter(c.compressor.(*zlib.Writer))
}
// gc hint needed?
c.compressor = nil
return nil
}
func (c *CompressingResponseWriter) isCompressorClosed() bool {
return nil == c.compressor
}
// Hijack implements the Hijacker interface
// This is especially useful when combining Container.EnabledContentEncoding
// in combination with websockets (for instance gorilla/websocket)
func (c *CompressingResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
hijacker, ok := c.writer.(http.Hijacker)
if !ok {
return nil, nil, errors.New("ResponseWriter doesn't support Hijacker interface")
}
return hijacker.Hijack()
}
// WantsCompressedResponse reads the Accept-Encoding header to see if and which encoding is requested.
func wantsCompressedResponse(httpRequest *http.Request) (bool, string) {
header := httpRequest.Header.Get(HEADER_AcceptEncoding)
gi := strings.Index(header, ENCODING_GZIP)
zi := strings.Index(header, ENCODING_DEFLATE)
// use in order of appearance
if gi == -1 {
return zi != -1, ENCODING_DEFLATE
} else if zi == -1 {
return gi != -1, ENCODING_GZIP
} else {
if gi < zi {
return true, ENCODING_GZIP
}
return true, ENCODING_DEFLATE
}
}
// NewCompressingResponseWriter create a CompressingResponseWriter for a known encoding = {gzip,deflate}
func NewCompressingResponseWriter(httpWriter http.ResponseWriter, encoding string) (*CompressingResponseWriter, error) {
httpWriter.Header().Set(HEADER_ContentEncoding, encoding)
c := new(CompressingResponseWriter)
c.writer = httpWriter
var err error
if ENCODING_GZIP == encoding {
w := currentCompressorProvider.AcquireGzipWriter()
w.Reset(httpWriter)
c.compressor = w
c.encoding = ENCODING_GZIP
} else if ENCODING_DEFLATE == encoding {
w := currentCompressorProvider.AcquireZlibWriter()
w.Reset(httpWriter)
c.compressor = w
c.encoding = ENCODING_DEFLATE
} else {
return nil, errors.New("Unknown encoding:" + encoding)
}
return c, err
}

125
vendor/github.com/emicklei/go-restful/compress_test.go generated vendored Normal file
View File

@@ -0,0 +1,125 @@
package restful
import (
"bytes"
"compress/gzip"
"compress/zlib"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
)
// go test -v -test.run TestGzip ...restful
func TestGzip(t *testing.T) {
EnableContentEncoding = true
httpRequest, _ := http.NewRequest("GET", "/test", nil)
httpRequest.Header.Set("Accept-Encoding", "gzip,deflate")
httpWriter := httptest.NewRecorder()
wanted, encoding := wantsCompressedResponse(httpRequest)
if !wanted {
t.Fatal("should accept gzip")
}
if encoding != "gzip" {
t.Fatal("expected gzip")
}
c, err := NewCompressingResponseWriter(httpWriter, encoding)
if err != nil {
t.Fatal(err.Error())
}
c.Write([]byte("Hello World"))
c.Close()
if httpWriter.Header().Get("Content-Encoding") != "gzip" {
t.Fatal("Missing gzip header")
}
reader, err := gzip.NewReader(httpWriter.Body)
if err != nil {
t.Fatal(err.Error())
}
data, err := ioutil.ReadAll(reader)
if err != nil {
t.Fatal(err.Error())
}
if got, want := string(data), "Hello World"; got != want {
t.Errorf("got %v want %v", got, want)
}
}
func TestDeflate(t *testing.T) {
EnableContentEncoding = true
httpRequest, _ := http.NewRequest("GET", "/test", nil)
httpRequest.Header.Set("Accept-Encoding", "deflate,gzip")
httpWriter := httptest.NewRecorder()
wanted, encoding := wantsCompressedResponse(httpRequest)
if !wanted {
t.Fatal("should accept deflate")
}
if encoding != "deflate" {
t.Fatal("expected deflate")
}
c, err := NewCompressingResponseWriter(httpWriter, encoding)
if err != nil {
t.Fatal(err.Error())
}
c.Write([]byte("Hello World"))
c.Close()
if httpWriter.Header().Get("Content-Encoding") != "deflate" {
t.Fatal("Missing deflate header")
}
reader, err := zlib.NewReader(httpWriter.Body)
if err != nil {
t.Fatal(err.Error())
}
data, err := ioutil.ReadAll(reader)
if err != nil {
t.Fatal(err.Error())
}
if got, want := string(data), "Hello World"; got != want {
t.Errorf("got %v want %v", got, want)
}
}
func TestGzipDecompressRequestBody(t *testing.T) {
b := new(bytes.Buffer)
w := newGzipWriter()
w.Reset(b)
io.WriteString(w, `{"msg":"hi"}`)
w.Flush()
w.Close()
req := new(Request)
httpRequest, _ := http.NewRequest("GET", "/", bytes.NewReader(b.Bytes()))
httpRequest.Header.Set("Content-Type", "application/json")
httpRequest.Header.Set("Content-Encoding", "gzip")
req.Request = httpRequest
doc := make(map[string]interface{})
req.ReadEntity(&doc)
if got, want := doc["msg"], "hi"; got != want {
t.Errorf("got %v want %v", got, want)
}
}
func TestZlibDecompressRequestBody(t *testing.T) {
b := new(bytes.Buffer)
w := newZlibWriter()
w.Reset(b)
io.WriteString(w, `{"msg":"hi"}`)
w.Flush()
w.Close()
req := new(Request)
httpRequest, _ := http.NewRequest("GET", "/", bytes.NewReader(b.Bytes()))
httpRequest.Header.Set("Content-Type", "application/json")
httpRequest.Header.Set("Content-Encoding", "deflate")
req.Request = httpRequest
doc := make(map[string]interface{})
req.ReadEntity(&doc)
if got, want := doc["msg"], "hi"; got != want {
t.Errorf("got %v want %v", got, want)
}
}

View File

@@ -0,0 +1,103 @@
package restful
// Copyright 2015 Ernest Micklei. All rights reserved.
// Use of this source code is governed by a license
// that can be found in the LICENSE file.
import (
"compress/gzip"
"compress/zlib"
)
// BoundedCachedCompressors is a CompressorProvider that uses a cache with a fixed amount
// of writers and readers (resources).
// If a new resource is acquired and all are in use, it will return a new unmanaged resource.
type BoundedCachedCompressors struct {
gzipWriters chan *gzip.Writer
gzipReaders chan *gzip.Reader
zlibWriters chan *zlib.Writer
writersCapacity int
readersCapacity int
}
// NewBoundedCachedCompressors returns a new, with filled cache, BoundedCachedCompressors.
func NewBoundedCachedCompressors(writersCapacity, readersCapacity int) *BoundedCachedCompressors {
b := &BoundedCachedCompressors{
gzipWriters: make(chan *gzip.Writer, writersCapacity),
gzipReaders: make(chan *gzip.Reader, readersCapacity),
zlibWriters: make(chan *zlib.Writer, writersCapacity),
writersCapacity: writersCapacity,
readersCapacity: readersCapacity,
}
for ix := 0; ix < writersCapacity; ix++ {
b.gzipWriters <- newGzipWriter()
b.zlibWriters <- newZlibWriter()
}
for ix := 0; ix < readersCapacity; ix++ {
b.gzipReaders <- newGzipReader()
}
return b
}
// AcquireGzipWriter returns an resettable *gzip.Writer. Needs to be released.
func (b *BoundedCachedCompressors) AcquireGzipWriter() *gzip.Writer {
var writer *gzip.Writer
select {
case writer, _ = <-b.gzipWriters:
default:
// return a new unmanaged one
writer = newGzipWriter()
}
return writer
}
// ReleaseGzipWriter accepts a writer (does not have to be one that was cached)
// only when the cache has room for it. It will ignore it otherwise.
func (b *BoundedCachedCompressors) ReleaseGzipWriter(w *gzip.Writer) {
// forget the unmanaged ones
if len(b.gzipWriters) < b.writersCapacity {
b.gzipWriters <- w
}
}
// AcquireGzipReader returns a *gzip.Reader. Needs to be released.
func (b *BoundedCachedCompressors) AcquireGzipReader() *gzip.Reader {
var reader *gzip.Reader
select {
case reader, _ = <-b.gzipReaders:
default:
// return a new unmanaged one
reader = newGzipReader()
}
return reader
}
// ReleaseGzipReader accepts a reader (does not have to be one that was cached)
// only when the cache has room for it. It will ignore it otherwise.
func (b *BoundedCachedCompressors) ReleaseGzipReader(r *gzip.Reader) {
// forget the unmanaged ones
if len(b.gzipReaders) < b.readersCapacity {
b.gzipReaders <- r
}
}
// AcquireZlibWriter returns an resettable *zlib.Writer. Needs to be released.
func (b *BoundedCachedCompressors) AcquireZlibWriter() *zlib.Writer {
var writer *zlib.Writer
select {
case writer, _ = <-b.zlibWriters:
default:
// return a new unmanaged one
writer = newZlibWriter()
}
return writer
}
// ReleaseZlibWriter accepts a writer (does not have to be one that was cached)
// only when the cache has room for it. It will ignore it otherwise.
func (b *BoundedCachedCompressors) ReleaseZlibWriter(w *zlib.Writer) {
// forget the unmanaged ones
if len(b.zlibWriters) < b.writersCapacity {
b.zlibWriters <- w
}
}

View File

@@ -0,0 +1,91 @@
package restful
// Copyright 2015 Ernest Micklei. All rights reserved.
// Use of this source code is governed by a license
// that can be found in the LICENSE file.
import (
"bytes"
"compress/gzip"
"compress/zlib"
"sync"
)
// SyncPoolCompessors is a CompressorProvider that use the standard sync.Pool.
type SyncPoolCompessors struct {
GzipWriterPool *sync.Pool
GzipReaderPool *sync.Pool
ZlibWriterPool *sync.Pool
}
// NewSyncPoolCompessors returns a new ("empty") SyncPoolCompessors.
func NewSyncPoolCompessors() *SyncPoolCompessors {
return &SyncPoolCompessors{
GzipWriterPool: &sync.Pool{
New: func() interface{} { return newGzipWriter() },
},
GzipReaderPool: &sync.Pool{
New: func() interface{} { return newGzipReader() },
},
ZlibWriterPool: &sync.Pool{
New: func() interface{} { return newZlibWriter() },
},
}
}
func (s *SyncPoolCompessors) AcquireGzipWriter() *gzip.Writer {
return s.GzipWriterPool.Get().(*gzip.Writer)
}
func (s *SyncPoolCompessors) ReleaseGzipWriter(w *gzip.Writer) {
s.GzipWriterPool.Put(w)
}
func (s *SyncPoolCompessors) AcquireGzipReader() *gzip.Reader {
return s.GzipReaderPool.Get().(*gzip.Reader)
}
func (s *SyncPoolCompessors) ReleaseGzipReader(r *gzip.Reader) {
s.GzipReaderPool.Put(r)
}
func (s *SyncPoolCompessors) AcquireZlibWriter() *zlib.Writer {
return s.ZlibWriterPool.Get().(*zlib.Writer)
}
func (s *SyncPoolCompessors) ReleaseZlibWriter(w *zlib.Writer) {
s.ZlibWriterPool.Put(w)
}
func newGzipWriter() *gzip.Writer {
// create with an empty bytes writer; it will be replaced before using the gzipWriter
writer, err := gzip.NewWriterLevel(new(bytes.Buffer), gzip.BestSpeed)
if err != nil {
panic(err.Error())
}
return writer
}
func newGzipReader() *gzip.Reader {
// create with an empty reader (but with GZIP header); it will be replaced before using the gzipReader
// we can safely use currentCompressProvider because it is set on package initialization.
w := currentCompressorProvider.AcquireGzipWriter()
defer currentCompressorProvider.ReleaseGzipWriter(w)
b := new(bytes.Buffer)
w.Reset(b)
w.Flush()
w.Close()
reader, err := gzip.NewReader(bytes.NewReader(b.Bytes()))
if err != nil {
panic(err.Error())
}
return reader
}
func newZlibWriter() *zlib.Writer {
writer, err := zlib.NewWriterLevel(new(bytes.Buffer), gzip.BestSpeed)
if err != nil {
panic(err.Error())
}
return writer
}

54
vendor/github.com/emicklei/go-restful/compressors.go generated vendored Normal file
View File

@@ -0,0 +1,54 @@
package restful
// Copyright 2015 Ernest Micklei. All rights reserved.
// Use of this source code is governed by a license
// that can be found in the LICENSE file.
import (
"compress/gzip"
"compress/zlib"
)
// CompressorProvider describes a component that can provider compressors for the std methods.
type CompressorProvider interface {
// Returns a *gzip.Writer which needs to be released later.
// Before using it, call Reset().
AcquireGzipWriter() *gzip.Writer
// Releases an aqcuired *gzip.Writer.
ReleaseGzipWriter(w *gzip.Writer)
// Returns a *gzip.Reader which needs to be released later.
AcquireGzipReader() *gzip.Reader
// Releases an aqcuired *gzip.Reader.
ReleaseGzipReader(w *gzip.Reader)
// Returns a *zlib.Writer which needs to be released later.
// Before using it, call Reset().
AcquireZlibWriter() *zlib.Writer
// Releases an aqcuired *zlib.Writer.
ReleaseZlibWriter(w *zlib.Writer)
}
// DefaultCompressorProvider is the actual provider of compressors (zlib or gzip).
var currentCompressorProvider CompressorProvider
func init() {
currentCompressorProvider = NewSyncPoolCompessors()
}
// CurrentCompressorProvider returns the current CompressorProvider.
// It is initialized using a SyncPoolCompessors.
func CurrentCompressorProvider() CompressorProvider {
return currentCompressorProvider
}
// CompressorProvider sets the actual provider of compressors (zlib or gzip).
func SetCompressorProvider(p CompressorProvider) {
if p == nil {
panic("cannot set compressor provider to nil")
}
currentCompressorProvider = p
}

Some files were not shown because too many files have changed in this diff Show More