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

@@ -1,9 +1,7 @@
language: go
go:
- 1.4.x
- 1.5.x
- 1.6.x
- 1.7.x
- 1.8.x
- 1.4
- 1.5
- 1.6
- 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].
[![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
@@ -12,7 +12,6 @@ Based on the [wikipedia paper][wiki] and the [RFC 3986 document][rfc].
## 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).
* **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.
@@ -173,7 +172,6 @@ And with `FlagsUnsafeGreedy`:
@opennota
@pchristopher1275
@zenovich
@beeker1121
## License

View File

@@ -15,8 +15,8 @@ import (
"github.com/PuerkitoBio/urlesc"
"golang.org/x/net/idna"
"golang.org/x/text/secure/precis"
"golang.org/x/text/unicode/norm"
"golang.org/x/text/width"
)
// 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.
// It takes an URL string as input, as well as the normalization flags.
func NormalizeURLString(u string, f NormalizationFlags) (string, error) {
parsed, err := url.Parse(u)
if err != nil {
return "", err
if parsed, e := url.Parse(u); e != nil {
return "", e
} 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
}
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
panic("Unreachable code.")
}
// 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://президент.рф": "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://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",

View File

@@ -1,11 +1,7 @@
language: go
go:
- 1.4.x
- 1.5.x
- 1.6.x
- 1.7.x
- 1.8.x
- 1.4
- tip
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.

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
go:
- 1.6.x
- 1.7.x
- 1.8.x
- 1.9.x
sudo: false
go: 1.2
install:
- go get -v github.com/alecthomas/gometalinter
- gometalinter --install
- go get -v code.google.com/p/go.tools/cmd/cover
script:
- export PATH=$PATH:$HOME/gopath/bin
- export GORACE="halt_on_error=1"
- 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
- go test -v -tags=disableunsafe ./spew
- go test -v -tags=testcgo ./spew -covermode=count -coverprofile=profile.cov
after_success:
- go get -v github.com/mattn/goveralls
- export PATH=$PATH:$HOME/gopath/bin
- 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/or 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
copyright notice and this permission notice appear in all copies.

View File

@@ -1,9 +1,10 @@
go-spew
=======
[![Build Status](https://img.shields.io/travis/davecgh/go-spew.svg)](https://travis-ci.org/davecgh/go-spew)
[![ISC License](http://img.shields.io/badge/license-ISC-blue.svg)](http://copyfree.org)
[![Coverage Status](https://img.shields.io/coveralls/davecgh/go-spew.svg)](https://coveralls.io/r/davecgh/go-spew?branch=master)
[![Build Status](https://travis-ci.org/davecgh/go-spew.png?branch=master)]
(https://travis-ci.org/davecgh/go-spew) [![Coverage Status]
(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
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
of the challenges involved in providing a deep pretty printer, there is a blog
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
[![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
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
relies on access to the unsafe package, so it will not have any effect when
running in environments without access to the unsafe package such as Google
App Engine or with the "safe" build tag specified.
App Engine or with the "disableunsafe" build tag specified.
Pointer method invocation is enabled by default.
* DisablePointerAddresses
DisablePointerAddresses specifies whether to disable the printing of
pointer addresses. This is useful when diffing data structures in tests.
* DisableCapacities
DisableCapacities specifies whether to disable the printing of capacities
for arrays, slices, maps and channels. This is useful when diffing data
structures in tests.
* ContinueOnMethod
Enables recursion into types after invoking error and Stringer interface
methods. Recursion after method invocation is disabled by default.
@@ -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
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
operate in this mode on Google App Engine and when compiled with GopherJS. The
"safe" build tag may also be specified to force the package to build without
using the unsafe package.
operate in this mode on Google App Engine. The "disableunsafe" build tag may
also be specified to force the package to build without using the unsafe
package.
## 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
// 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.
// NOTE: Due to the following build constraints, this file will only be compiled
// when the code is not running on Google App Engine, compiled by GopherJS, and
// "-tags safe" is not added to the go build command line. The "disableunsafe"
// tag is deprecated and thus should not be used.
// +build !js,!appengine,!safe,!disableunsafe
// when the code is not running on Google App Engine and "-tags disableunsafe"
// is not added to the go build command line.
// +build !appengine,!disableunsafe
package spew
@@ -41,9 +40,9 @@ var (
// after commit 82f48826c6c7 which changed the format again to mirror
// the original format. Code in the init function updates these offsets
// as necessary.
offsetPtr = ptrSize
offsetPtr = uintptr(ptrSize)
offsetScalar = uintptr(0)
offsetFlag = ptrSize * 2
offsetFlag = uintptr(ptrSize * 2)
// flagKindWidth and flagKindShift indicate various bits that the
// reflect package uses internally to track kind information.
@@ -58,7 +57,7 @@ var (
// changed their positions. Code in the init function updates these
// flags as necessary.
flagKindWidth = uintptr(5)
flagKindShift = flagKindWidth - 1
flagKindShift = uintptr(flagKindWidth - 1)
flagRO = uintptr(1 << 0)
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
// 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.
// NOTE: Due to the following build constraints, this file will only be compiled
// when the code is running on Google App Engine, compiled by GopherJS, or
// "-tags safe" is added to the go build command line. The "disableunsafe"
// tag is deprecated and thus should not be used.
// +build js appengine safe disableunsafe
// when either the code is running on Google App Engine or "-tags disableunsafe"
// is added to the go build command line.
// +build appengine disableunsafe
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
* 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)
}
// 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.
func printHexPtr(w io.Writer, p uintptr) {
// 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
* 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
* 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
// access to the unsafe package, so it will not have any effect when
// running in environments without access to the unsafe package such as
// Google App Engine or with the "safe" build tag specified.
// Google App Engine or with the "disableunsafe" build tag specified.
DisablePointerMethods bool
// DisablePointerAddresses specifies whether to disable the printing of
// pointer addresses. This is useful when diffing data structures in tests.
DisablePointerAddresses bool
// DisableCapacities specifies whether to disable the printing of capacities
// for arrays, slices, maps and channels. This is useful when diffing
// data structures in tests.
DisableCapacities bool
// ContinueOnMethod specifies whether or not recursion should continue once
// a custom error or Stringer interface is invoked. The default, false,
// means it will print the results of invoking the custom error or Stringer

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
* 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.
Pointer method invocation is enabled by default.
* DisablePointerAddresses
DisablePointerAddresses specifies whether to disable the printing of
pointer addresses. This is useful when diffing data structures in tests.
* DisableCapacities
DisableCapacities specifies whether to disable the printing of
capacities for arrays, slices, maps and channels. This is useful when
diffing data structures in tests.
* ContinueOnMethod
Enables recursion into types after invoking error and Stringer interface
methods. Recursion after method invocation is disabled by default.

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
* 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.
// 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
// char. It is used to detect unsigned character arrays to hexdump
// them.
cUnsignedCharRE = regexp.MustCompile(`^.*\._Ctype_unsignedchar$`)
cUnsignedCharRE = regexp.MustCompile("^.*\\._Ctype_unsignedchar$")
// cUint8tCharRE is a regular expression that matches a cgo uint8_t.
// 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.
@@ -129,7 +129,7 @@ func (d *dumpState) dumpPtr(v reflect.Value) {
d.w.Write(closeParenBytes)
// Display pointer information.
if !d.cs.DisablePointerAddresses && len(pointerChain) > 0 {
if len(pointerChain) > 0 {
d.w.Write(openParenBytes)
for i, addr := range pointerChain {
if i > 0 {
@@ -143,10 +143,10 @@ func (d *dumpState) dumpPtr(v reflect.Value) {
// Display dereferenced value.
d.w.Write(openParenBytes)
switch {
case nilFound:
case nilFound == true:
d.w.Write(nilAngleBytes)
case cycleFound:
case cycleFound == true:
d.w.Write(circularBytes)
default:
@@ -282,13 +282,13 @@ func (d *dumpState) dump(v reflect.Value) {
case reflect.Map, reflect.String:
valueLen = v.Len()
}
if valueLen != 0 || !d.cs.DisableCapacities && valueCap != 0 {
if valueLen != 0 || valueCap != 0 {
d.w.Write(openParenBytes)
if valueLen != 0 {
d.w.Write(lenEqualsBytes)
printInt(d.w, int64(valueLen), 10)
}
if !d.cs.DisableCapacities && valueCap != 0 {
if valueCap != 0 {
if valueLen != 0 {
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
* purpose with or without fee is hereby granted, provided that the above
@@ -70,7 +70,7 @@ import (
"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 {
in interface{}
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
// purpose with or without fee is hereby granted, provided that the above
@@ -59,11 +59,10 @@ func addCgoDumpTests() {
v3Len := fmt.Sprintf("%d", v3l)
v3Cap := fmt.Sprintf("%d", v3c)
v3t := "[6]testdata._Ctype_unsignedchar"
v3t2 := "[6]testdata._Ctype_uchar"
v3s := "(len=" + v3Len + " cap=" + v3Cap + ") " +
"{\n 00000000 74 65 73 74 33 00 " +
" |test3.|\n}"
addDumpTest(v3, "("+v3t+") "+v3s+"\n", "("+v3t2+") "+v3s+"\n")
addDumpTest(v3, "("+v3t+") "+v3s+"\n")
// C signed char array.
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
* 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
* 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.
switch {
case nilFound:
case nilFound == true:
f.fs.Write(nilAngleBytes)
case cycleFound:
case cycleFound == true:
f.fs.Write(circularShortBytes)
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
* purpose with or without fee is hereby granted, provided that the above
@@ -75,7 +75,7 @@ import (
"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 {
format string
in interface{}
@@ -1536,14 +1536,14 @@ func TestPrintSortedKeys(t *testing.T) {
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]"
if s != expected {
t.Errorf("Sorted keys mismatch 4:\n %v %v", s, expected)
}
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]"
if 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
* 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 {
return f == int('+')
if f == int('+') {
return true
}
return false
}
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
// 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.
// NOTE: Due to the following build constraints, this file will only be compiled
// when the code is not running on Google App Engine, compiled by GopherJS, and
// "-tags safe" is not added to the go build command line. The "disableunsafe"
// tag is deprecated and thus should not be used.
// +build !js,!appengine,!safe,!disableunsafe
// when the code is not running on Google App Engine and "-tags disableunsafe"
// is not added to the go build command line.
// +build !appengine,!disableunsafe
/*
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
* 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
* purpose with or without fee is hereby granted, provided that the above
@@ -130,19 +130,12 @@ func initSpewTests() {
scsNoPmethods := &spew.ConfigState{Indent: " ", DisablePointerMethods: true}
scsMaxDepth := &spew.ConfigState{Indent: " ", MaxDepth: 1}
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
// without a pointer receiver.
ts := stringer("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
// and maps.
type depthTester struct {
@@ -199,10 +192,6 @@ func initSpewTests() {
{scsContinue, fCSFprint, "", te, "(error: 10) 10"},
{scsContinue, fCSFdump, "", te, "(spew_test.customError) " +
"(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
}

30
vendor/github.com/emicklei/go-restful/constants.go generated vendored Normal file
View File

@@ -0,0 +1,30 @@
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.
const (
MIME_XML = "application/xml" // Accept or Content-Type used in Consumes() and/or Produces()
MIME_JSON = "application/json" // Accept or Content-Type used in Consumes() and/or Produces()
MIME_OCTET = "application/octet-stream" // If Content-Type is not present in request, use the default
HEADER_Allow = "Allow"
HEADER_Accept = "Accept"
HEADER_Origin = "Origin"
HEADER_ContentType = "Content-Type"
HEADER_LastModified = "Last-Modified"
HEADER_AcceptEncoding = "Accept-Encoding"
HEADER_ContentEncoding = "Content-Encoding"
HEADER_AccessControlExposeHeaders = "Access-Control-Expose-Headers"
HEADER_AccessControlRequestMethod = "Access-Control-Request-Method"
HEADER_AccessControlRequestHeaders = "Access-Control-Request-Headers"
HEADER_AccessControlAllowMethods = "Access-Control-Allow-Methods"
HEADER_AccessControlAllowOrigin = "Access-Control-Allow-Origin"
HEADER_AccessControlAllowCredentials = "Access-Control-Allow-Credentials"
HEADER_AccessControlAllowHeaders = "Access-Control-Allow-Headers"
HEADER_AccessControlMaxAge = "Access-Control-Max-Age"
ENCODING_GZIP = "gzip"
ENCODING_DEFLATE = "deflate"
)

366
vendor/github.com/emicklei/go-restful/container.go generated vendored Normal file
View File

@@ -0,0 +1,366 @@
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 (
"bytes"
"errors"
"fmt"
"net/http"
"os"
"runtime"
"strings"
"sync"
"github.com/emicklei/go-restful/log"
)
// Container holds a collection of WebServices and a http.ServeMux to dispatch http requests.
// The requests are further dispatched to routes of WebServices using a RouteSelector
type Container struct {
webServicesLock sync.RWMutex
webServices []*WebService
ServeMux *http.ServeMux
isRegisteredOnRoot bool
containerFilters []FilterFunction
doNotRecover bool // default is true
recoverHandleFunc RecoverHandleFunction
serviceErrorHandleFunc ServiceErrorHandleFunction
router RouteSelector // default is a CurlyRouter (RouterJSR311 is a slower alternative)
contentEncodingEnabled bool // default is false
}
// NewContainer creates a new Container using a new ServeMux and default router (CurlyRouter)
func NewContainer() *Container {
return &Container{
webServices: []*WebService{},
ServeMux: http.NewServeMux(),
isRegisteredOnRoot: false,
containerFilters: []FilterFunction{},
doNotRecover: true,
recoverHandleFunc: logStackOnRecover,
serviceErrorHandleFunc: writeServiceError,
router: CurlyRouter{},
contentEncodingEnabled: false}
}
// RecoverHandleFunction declares functions that can be used to handle a panic situation.
// The first argument is what recover() returns. The second must be used to communicate an error response.
type RecoverHandleFunction func(interface{}, http.ResponseWriter)
// RecoverHandler changes the default function (logStackOnRecover) to be called
// when a panic is detected. DoNotRecover must be have its default value (=false).
func (c *Container) RecoverHandler(handler RecoverHandleFunction) {
c.recoverHandleFunc = handler
}
// ServiceErrorHandleFunction declares functions that can be used to handle a service error situation.
// The first argument is the service error, the second is the request that resulted in the error and
// the third must be used to communicate an error response.
type ServiceErrorHandleFunction func(ServiceError, *Request, *Response)
// ServiceErrorHandler changes the default function (writeServiceError) to be called
// when a ServiceError is detected.
func (c *Container) ServiceErrorHandler(handler ServiceErrorHandleFunction) {
c.serviceErrorHandleFunc = handler
}
// DoNotRecover controls whether panics will be caught to return HTTP 500.
// If set to true, Route functions are responsible for handling any error situation.
// Default value is true.
func (c *Container) DoNotRecover(doNot bool) {
c.doNotRecover = doNot
}
// Router changes the default Router (currently CurlyRouter)
func (c *Container) Router(aRouter RouteSelector) {
c.router = aRouter
}
// EnableContentEncoding (default=false) allows for GZIP or DEFLATE encoding of responses.
func (c *Container) EnableContentEncoding(enabled bool) {
c.contentEncodingEnabled = enabled
}
// Add a WebService to the Container. It will detect duplicate root paths and exit in that case.
func (c *Container) Add(service *WebService) *Container {
c.webServicesLock.Lock()
defer c.webServicesLock.Unlock()
// if rootPath was not set then lazy initialize it
if len(service.rootPath) == 0 {
service.Path("/")
}
// cannot have duplicate root paths
for _, each := range c.webServices {
if each.RootPath() == service.RootPath() {
log.Printf("[restful] WebService with duplicate root path detected:['%v']", each)
os.Exit(1)
}
}
// If not registered on root then add specific mapping
if !c.isRegisteredOnRoot {
c.isRegisteredOnRoot = c.addHandler(service, c.ServeMux)
}
c.webServices = append(c.webServices, service)
return c
}
// addHandler may set a new HandleFunc for the serveMux
// this function must run inside the critical region protected by the webServicesLock.
// returns true if the function was registered on root ("/")
func (c *Container) addHandler(service *WebService, serveMux *http.ServeMux) bool {
pattern := fixedPrefixPath(service.RootPath())
// check if root path registration is needed
if "/" == pattern || "" == pattern {
serveMux.HandleFunc("/", c.dispatch)
return true
}
// detect if registration already exists
alreadyMapped := false
for _, each := range c.webServices {
if each.RootPath() == service.RootPath() {
alreadyMapped = true
break
}
}
if !alreadyMapped {
serveMux.HandleFunc(pattern, c.dispatch)
if !strings.HasSuffix(pattern, "/") {
serveMux.HandleFunc(pattern+"/", c.dispatch)
}
}
return false
}
func (c *Container) Remove(ws *WebService) error {
if c.ServeMux == http.DefaultServeMux {
errMsg := fmt.Sprintf("[restful] cannot remove a WebService from a Container using the DefaultServeMux: ['%v']", ws)
log.Printf(errMsg)
return errors.New(errMsg)
}
c.webServicesLock.Lock()
defer c.webServicesLock.Unlock()
// build a new ServeMux and re-register all WebServices
newServeMux := http.NewServeMux()
newServices := []*WebService{}
newIsRegisteredOnRoot := false
for _, each := range c.webServices {
if each.rootPath != ws.rootPath {
// If not registered on root then add specific mapping
if !newIsRegisteredOnRoot {
newIsRegisteredOnRoot = c.addHandler(each, newServeMux)
}
newServices = append(newServices, each)
}
}
c.webServices, c.ServeMux, c.isRegisteredOnRoot = newServices, newServeMux, newIsRegisteredOnRoot
return nil
}
// logStackOnRecover is the default RecoverHandleFunction and is called
// when DoNotRecover is false and the recoverHandleFunc is not set for the container.
// Default implementation logs the stacktrace and writes the stacktrace on the response.
// This may be a security issue as it exposes sourcecode information.
func logStackOnRecover(panicReason interface{}, httpWriter http.ResponseWriter) {
var buffer bytes.Buffer
buffer.WriteString(fmt.Sprintf("[restful] recover from panic situation: - %v\r\n", panicReason))
for i := 2; ; i += 1 {
_, file, line, ok := runtime.Caller(i)
if !ok {
break
}
buffer.WriteString(fmt.Sprintf(" %s:%d\r\n", file, line))
}
log.Print(buffer.String())
httpWriter.WriteHeader(http.StatusInternalServerError)
httpWriter.Write(buffer.Bytes())
}
// writeServiceError is the default ServiceErrorHandleFunction and is called
// when a ServiceError is returned during route selection. Default implementation
// calls resp.WriteErrorString(err.Code, err.Message)
func writeServiceError(err ServiceError, req *Request, resp *Response) {
resp.WriteErrorString(err.Code, err.Message)
}
// Dispatch the incoming Http Request to a matching WebService.
func (c *Container) Dispatch(httpWriter http.ResponseWriter, httpRequest *http.Request) {
if httpWriter == nil {
panic("httpWriter cannot be nil")
}
if httpRequest == nil {
panic("httpRequest cannot be nil")
}
c.dispatch(httpWriter, httpRequest)
}
// Dispatch the incoming Http Request to a matching WebService.
func (c *Container) dispatch(httpWriter http.ResponseWriter, httpRequest *http.Request) {
writer := httpWriter
// CompressingResponseWriter should be closed after all operations are done
defer func() {
if compressWriter, ok := writer.(*CompressingResponseWriter); ok {
compressWriter.Close()
}
}()
// Instal panic recovery unless told otherwise
if !c.doNotRecover { // catch all for 500 response
defer func() {
if r := recover(); r != nil {
c.recoverHandleFunc(r, writer)
return
}
}()
}
// Detect if compression is needed
// assume without compression, test for override
if c.contentEncodingEnabled {
doCompress, encoding := wantsCompressedResponse(httpRequest)
if doCompress {
var err error
writer, err = NewCompressingResponseWriter(httpWriter, encoding)
if err != nil {
log.Print("[restful] unable to install compressor: ", err)
httpWriter.WriteHeader(http.StatusInternalServerError)
return
}
}
}
// Find best match Route ; err is non nil if no match was found
var webService *WebService
var route *Route
var err error
func() {
c.webServicesLock.RLock()
defer c.webServicesLock.RUnlock()
webService, route, err = c.router.SelectRoute(
c.webServices,
httpRequest)
}()
if err != nil {
// a non-200 response has already been written
// run container filters anyway ; they should not touch the response...
chain := FilterChain{Filters: c.containerFilters, Target: func(req *Request, resp *Response) {
switch err.(type) {
case ServiceError:
ser := err.(ServiceError)
c.serviceErrorHandleFunc(ser, req, resp)
}
// TODO
}}
chain.ProcessFilter(NewRequest(httpRequest), NewResponse(writer))
return
}
wrappedRequest, wrappedResponse := route.wrapRequestResponse(writer, httpRequest)
// pass through filters (if any)
if len(c.containerFilters)+len(webService.filters)+len(route.Filters) > 0 {
// compose filter chain
allFilters := []FilterFunction{}
allFilters = append(allFilters, c.containerFilters...)
allFilters = append(allFilters, webService.filters...)
allFilters = append(allFilters, route.Filters...)
chain := FilterChain{Filters: allFilters, Target: func(req *Request, resp *Response) {
// handle request by route after passing all filters
route.Function(wrappedRequest, wrappedResponse)
}}
chain.ProcessFilter(wrappedRequest, wrappedResponse)
} else {
// no filters, handle request by route
route.Function(wrappedRequest, wrappedResponse)
}
}
// fixedPrefixPath returns the fixed part of the partspec ; it may include template vars {}
func fixedPrefixPath(pathspec string) string {
varBegin := strings.Index(pathspec, "{")
if -1 == varBegin {
return pathspec
}
return pathspec[:varBegin]
}
// ServeHTTP implements net/http.Handler therefore a Container can be a Handler in a http.Server
func (c *Container) ServeHTTP(httpwriter http.ResponseWriter, httpRequest *http.Request) {
c.ServeMux.ServeHTTP(httpwriter, httpRequest)
}
// Handle registers the handler for the given pattern. If a handler already exists for pattern, Handle panics.
func (c *Container) Handle(pattern string, handler http.Handler) {
c.ServeMux.Handle(pattern, handler)
}
// HandleWithFilter registers the handler for the given pattern.
// Container's filter chain is applied for handler.
// If a handler already exists for pattern, HandleWithFilter panics.
func (c *Container) HandleWithFilter(pattern string, handler http.Handler) {
f := func(httpResponse http.ResponseWriter, httpRequest *http.Request) {
if len(c.containerFilters) == 0 {
handler.ServeHTTP(httpResponse, httpRequest)
return
}
chain := FilterChain{Filters: c.containerFilters, Target: func(req *Request, resp *Response) {
handler.ServeHTTP(httpResponse, httpRequest)
}}
chain.ProcessFilter(NewRequest(httpRequest), NewResponse(httpResponse))
}
c.Handle(pattern, http.HandlerFunc(f))
}
// Filter appends a container FilterFunction. These are called before dispatching
// a http.Request to a WebService from the container
func (c *Container) Filter(filter FilterFunction) {
c.containerFilters = append(c.containerFilters, filter)
}
// RegisteredWebServices returns the collections of added WebServices
func (c *Container) RegisteredWebServices() []*WebService {
c.webServicesLock.RLock()
defer c.webServicesLock.RUnlock()
result := make([]*WebService, len(c.webServices))
for ix := range c.webServices {
result[ix] = c.webServices[ix]
}
return result
}
// computeAllowedMethods returns a list of HTTP methods that are valid for a Request
func (c *Container) computeAllowedMethods(req *Request) []string {
// Go through all RegisteredWebServices() and all its Routes to collect the options
methods := []string{}
requestPath := req.Request.URL.Path
for _, ws := range c.RegisteredWebServices() {
matches := ws.pathExpr.Matcher.FindStringSubmatch(requestPath)
if matches != nil {
finalMatch := matches[len(matches)-1]
for _, rt := range ws.Routes() {
matches := rt.pathExpr.Matcher.FindStringSubmatch(finalMatch)
if matches != nil {
lastMatch := matches[len(matches)-1]
if lastMatch == "" || lastMatch == "/" { // do not include if value is neither empty nor /.
methods = append(methods, rt.Method)
}
}
}
}
}
// methods = append(methods, "OPTIONS") not sure about this
return methods
}
// newBasicRequestResponse creates a pair of Request,Response from its http versions.
// It is basic because no parameter or (produces) content-type information is given.
func newBasicRequestResponse(httpWriter http.ResponseWriter, httpRequest *http.Request) (*Request, *Response) {
resp := NewResponse(httpWriter)
resp.requestAccept = httpRequest.Header.Get(HEADER_Accept)
return NewRequest(httpRequest), resp
}

View File

@@ -0,0 +1,83 @@
package restful
import (
"net/http"
"net/http/httptest"
"testing"
)
// go test -v -test.run TestContainer_computeAllowedMethods ...restful
func TestContainer_computeAllowedMethods(t *testing.T) {
wc := NewContainer()
ws1 := new(WebService).Path("/users")
ws1.Route(ws1.GET("{i}").To(dummy))
ws1.Route(ws1.POST("{i}").To(dummy))
wc.Add(ws1)
httpRequest, _ := http.NewRequest("GET", "http://api.his.com/users/1", nil)
rreq := Request{Request: httpRequest}
m := wc.computeAllowedMethods(&rreq)
if len(m) != 2 {
t.Errorf("got %d expected 2 methods, %v", len(m), m)
}
}
func TestContainer_HandleWithFilter(t *testing.T) {
prefilterCalled := false
postfilterCalled := false
httpHandlerCalled := false
wc := NewContainer()
wc.Filter(func(request *Request, response *Response, chain *FilterChain) {
prefilterCalled = true
chain.ProcessFilter(request, response)
})
wc.HandleWithFilter("/", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
httpHandlerCalled = true
w.Write([]byte("ok"))
}))
wc.Filter(func(request *Request, response *Response, chain *FilterChain) {
postfilterCalled = true
chain.ProcessFilter(request, response)
})
recorder := httptest.NewRecorder()
request, _ := http.NewRequest("GET", "/", nil)
wc.ServeHTTP(recorder, request)
if recorder.Code != http.StatusOK {
t.Errorf("unexpected code %d", recorder.Code)
}
if recorder.Body.String() != "ok" {
t.Errorf("unexpected body %s", recorder.Body.String())
}
if !prefilterCalled {
t.Errorf("filter added before calling HandleWithFilter wasn't called")
}
if !postfilterCalled {
t.Errorf("filter added after calling HandleWithFilter wasn't called")
}
if !httpHandlerCalled {
t.Errorf("handler added by calling HandleWithFilter wasn't called")
}
}
func TestContainerAddAndRemove(t *testing.T) {
ws1 := new(WebService).Path("/")
ws2 := new(WebService).Path("/users")
wc := NewContainer()
wc.Add(ws1)
wc.Add(ws2)
wc.Remove(ws2)
if len(wc.webServices) != 1 {
t.Errorf("expected one webservices")
}
if !wc.isRegisteredOnRoot {
t.Errorf("expected on root registered")
}
wc.Remove(ws1)
if len(wc.webServices) > 0 {
t.Errorf("expected zero webservices")
}
if wc.isRegisteredOnRoot {
t.Errorf("expected not on root registered")
}
}

202
vendor/github.com/emicklei/go-restful/cors_filter.go generated vendored Normal file
View File

@@ -0,0 +1,202 @@
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 (
"regexp"
"strconv"
"strings"
)
// CrossOriginResourceSharing is used to create a Container Filter that implements CORS.
// Cross-origin resource sharing (CORS) is a mechanism that allows JavaScript on a web page
// to make XMLHttpRequests to another domain, not the domain the JavaScript originated from.
//
// http://en.wikipedia.org/wiki/Cross-origin_resource_sharing
// http://enable-cors.org/server.html
// http://www.html5rocks.com/en/tutorials/cors/#toc-handling-a-not-so-simple-request
type CrossOriginResourceSharing struct {
ExposeHeaders []string // list of Header names
AllowedHeaders []string // list of Header names
AllowedDomains []string // list of allowed values for Http Origin. An allowed value can be a regular expression to support subdomain matching. If empty all are allowed.
AllowedMethods []string
MaxAge int // number of seconds before requiring new Options request
CookiesAllowed bool
Container *Container
allowedOriginPatterns []*regexp.Regexp // internal field for origin regexp check.
}
// Filter is a filter function that implements the CORS flow as documented on http://enable-cors.org/server.html
// and http://www.html5rocks.com/static/images/cors_server_flowchart.png
func (c CrossOriginResourceSharing) Filter(req *Request, resp *Response, chain *FilterChain) {
origin := req.Request.Header.Get(HEADER_Origin)
if len(origin) == 0 {
if trace {
traceLogger.Print("no Http header Origin set")
}
chain.ProcessFilter(req, resp)
return
}
if !c.isOriginAllowed(origin) { // check whether this origin is allowed
if trace {
traceLogger.Printf("HTTP Origin:%s is not part of %v, neither matches any part of %v", origin, c.AllowedDomains, c.allowedOriginPatterns)
}
chain.ProcessFilter(req, resp)
return
}
if req.Request.Method != "OPTIONS" {
c.doActualRequest(req, resp)
chain.ProcessFilter(req, resp)
return
}
if acrm := req.Request.Header.Get(HEADER_AccessControlRequestMethod); acrm != "" {
c.doPreflightRequest(req, resp)
} else {
c.doActualRequest(req, resp)
chain.ProcessFilter(req, resp)
return
}
}
func (c CrossOriginResourceSharing) doActualRequest(req *Request, resp *Response) {
c.setOptionsHeaders(req, resp)
// continue processing the response
}
func (c *CrossOriginResourceSharing) doPreflightRequest(req *Request, resp *Response) {
if len(c.AllowedMethods) == 0 {
if c.Container == nil {
c.AllowedMethods = DefaultContainer.computeAllowedMethods(req)
} else {
c.AllowedMethods = c.Container.computeAllowedMethods(req)
}
}
acrm := req.Request.Header.Get(HEADER_AccessControlRequestMethod)
if !c.isValidAccessControlRequestMethod(acrm, c.AllowedMethods) {
if trace {
traceLogger.Printf("Http header %s:%s is not in %v",
HEADER_AccessControlRequestMethod,
acrm,
c.AllowedMethods)
}
return
}
acrhs := req.Request.Header.Get(HEADER_AccessControlRequestHeaders)
if len(acrhs) > 0 {
for _, each := range strings.Split(acrhs, ",") {
if !c.isValidAccessControlRequestHeader(strings.Trim(each, " ")) {
if trace {
traceLogger.Printf("Http header %s:%s is not in %v",
HEADER_AccessControlRequestHeaders,
acrhs,
c.AllowedHeaders)
}
return
}
}
}
resp.AddHeader(HEADER_AccessControlAllowMethods, strings.Join(c.AllowedMethods, ","))
resp.AddHeader(HEADER_AccessControlAllowHeaders, acrhs)
c.setOptionsHeaders(req, resp)
// return http 200 response, no body
}
func (c CrossOriginResourceSharing) setOptionsHeaders(req *Request, resp *Response) {
c.checkAndSetExposeHeaders(resp)
c.setAllowOriginHeader(req, resp)
c.checkAndSetAllowCredentials(resp)
if c.MaxAge > 0 {
resp.AddHeader(HEADER_AccessControlMaxAge, strconv.Itoa(c.MaxAge))
}
}
func (c CrossOriginResourceSharing) isOriginAllowed(origin string) bool {
if len(origin) == 0 {
return false
}
if len(c.AllowedDomains) == 0 {
return true
}
allowed := false
for _, domain := range c.AllowedDomains {
if domain == origin {
allowed = true
break
}
}
if !allowed {
if len(c.allowedOriginPatterns) == 0 {
// compile allowed domains to allowed origin patterns
allowedOriginRegexps, err := compileRegexps(c.AllowedDomains)
if err != nil {
return false
}
c.allowedOriginPatterns = allowedOriginRegexps
}
for _, pattern := range c.allowedOriginPatterns {
if allowed = pattern.MatchString(origin); allowed {
break
}
}
}
return allowed
}
func (c CrossOriginResourceSharing) setAllowOriginHeader(req *Request, resp *Response) {
origin := req.Request.Header.Get(HEADER_Origin)
if c.isOriginAllowed(origin) {
resp.AddHeader(HEADER_AccessControlAllowOrigin, origin)
}
}
func (c CrossOriginResourceSharing) checkAndSetExposeHeaders(resp *Response) {
if len(c.ExposeHeaders) > 0 {
resp.AddHeader(HEADER_AccessControlExposeHeaders, strings.Join(c.ExposeHeaders, ","))
}
}
func (c CrossOriginResourceSharing) checkAndSetAllowCredentials(resp *Response) {
if c.CookiesAllowed {
resp.AddHeader(HEADER_AccessControlAllowCredentials, "true")
}
}
func (c CrossOriginResourceSharing) isValidAccessControlRequestMethod(method string, allowedMethods []string) bool {
for _, each := range allowedMethods {
if each == method {
return true
}
}
return false
}
func (c CrossOriginResourceSharing) isValidAccessControlRequestHeader(header string) bool {
for _, each := range c.AllowedHeaders {
if strings.ToLower(each) == strings.ToLower(header) {
return true
}
}
return false
}
// Take a list of strings and compile them into a list of regular expressions.
func compileRegexps(regexpStrings []string) ([]*regexp.Regexp, error) {
regexps := []*regexp.Regexp{}
for _, regexpStr := range regexpStrings {
r, err := regexp.Compile(regexpStr)
if err != nil {
return regexps, err
}
regexps = append(regexps, r)
}
return regexps, nil
}

View File

@@ -0,0 +1,129 @@
package restful
import (
"net/http"
"net/http/httptest"
"testing"
)
// go test -v -test.run TestCORSFilter_Preflight ...restful
// http://www.html5rocks.com/en/tutorials/cors/#toc-handling-a-not-so-simple-request
func TestCORSFilter_Preflight(t *testing.T) {
tearDown()
ws := new(WebService)
ws.Route(ws.PUT("/cors").To(dummy))
Add(ws)
cors := CrossOriginResourceSharing{
ExposeHeaders: []string{"X-Custom-Header"},
AllowedHeaders: []string{"X-Custom-Header", "X-Additional-Header"},
CookiesAllowed: true,
Container: DefaultContainer}
Filter(cors.Filter)
// Preflight
httpRequest, _ := http.NewRequest("OPTIONS", "http://api.alice.com/cors", nil)
httpRequest.Method = "OPTIONS"
httpRequest.Header.Set(HEADER_Origin, "http://api.bob.com")
httpRequest.Header.Set(HEADER_AccessControlRequestMethod, "PUT")
httpRequest.Header.Set(HEADER_AccessControlRequestHeaders, "X-Custom-Header, X-Additional-Header")
httpWriter := httptest.NewRecorder()
DefaultContainer.Dispatch(httpWriter, httpRequest)
actual := httpWriter.Header().Get(HEADER_AccessControlAllowOrigin)
if "http://api.bob.com" != actual {
t.Fatal("expected: http://api.bob.com but got:" + actual)
}
actual = httpWriter.Header().Get(HEADER_AccessControlAllowMethods)
if "PUT" != actual {
t.Fatal("expected: PUT but got:" + actual)
}
actual = httpWriter.Header().Get(HEADER_AccessControlAllowHeaders)
if "X-Custom-Header, X-Additional-Header" != actual {
t.Fatal("expected: X-Custom-Header, X-Additional-Header but got:" + actual)
}
if !cors.isOriginAllowed("somewhere") {
t.Fatal("origin expected to be allowed")
}
cors.AllowedDomains = []string{"overthere.com"}
if cors.isOriginAllowed("somewhere") {
t.Fatal("origin [somewhere] expected NOT to be allowed")
}
if !cors.isOriginAllowed("overthere.com") {
t.Fatal("origin [overthere] expected to be allowed")
}
}
// go test -v -test.run TestCORSFilter_Actual ...restful
// http://www.html5rocks.com/en/tutorials/cors/#toc-handling-a-not-so-simple-request
func TestCORSFilter_Actual(t *testing.T) {
tearDown()
ws := new(WebService)
ws.Route(ws.PUT("/cors").To(dummy))
Add(ws)
cors := CrossOriginResourceSharing{
ExposeHeaders: []string{"X-Custom-Header"},
AllowedHeaders: []string{"X-Custom-Header", "X-Additional-Header"},
CookiesAllowed: true,
Container: DefaultContainer}
Filter(cors.Filter)
// Actual
httpRequest, _ := http.NewRequest("PUT", "http://api.alice.com/cors", nil)
httpRequest.Header.Set(HEADER_Origin, "http://api.bob.com")
httpRequest.Header.Set("X-Custom-Header", "value")
httpWriter := httptest.NewRecorder()
DefaultContainer.Dispatch(httpWriter, httpRequest)
actual := httpWriter.Header().Get(HEADER_AccessControlAllowOrigin)
if "http://api.bob.com" != actual {
t.Fatal("expected: http://api.bob.com but got:" + actual)
}
if httpWriter.Body.String() != "dummy" {
t.Fatal("expected: dummy but got:" + httpWriter.Body.String())
}
}
var allowedDomainInput = []struct {
domains []string
origin string
allowed bool
}{
{[]string{}, "http://anything.com", true},
{[]string{"example.com"}, "example.com", true},
{[]string{"example.com"}, "not-allowed", false},
{[]string{"not-matching.com", "example.com"}, "example.com", true},
{[]string{".*"}, "example.com", true},
}
// go test -v -test.run TestCORSFilter_AllowedDomains ...restful
func TestCORSFilter_AllowedDomains(t *testing.T) {
for _, each := range allowedDomainInput {
tearDown()
ws := new(WebService)
ws.Route(ws.PUT("/cors").To(dummy))
Add(ws)
cors := CrossOriginResourceSharing{
AllowedDomains: each.domains,
CookiesAllowed: true,
Container: DefaultContainer}
Filter(cors.Filter)
httpRequest, _ := http.NewRequest("PUT", "http://api.his.com/cors", nil)
httpRequest.Header.Set(HEADER_Origin, each.origin)
httpWriter := httptest.NewRecorder()
DefaultContainer.Dispatch(httpWriter, httpRequest)
actual := httpWriter.Header().Get(HEADER_AccessControlAllowOrigin)
if actual != each.origin && each.allowed {
t.Fatal("expected to be accepted")
}
if actual == each.origin && !each.allowed {
t.Fatal("did not expect to be accepted")
}
}
}

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

@@ -0,0 +1,2 @@
go test -coverprofile=coverage.out
go tool cover -html=coverage.out

164
vendor/github.com/emicklei/go-restful/curly.go generated vendored Normal file
View File

@@ -0,0 +1,164 @@
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 (
"net/http"
"regexp"
"sort"
"strings"
)
// CurlyRouter expects Routes with paths that contain zero or more parameters in curly brackets.
type CurlyRouter struct{}
// SelectRoute is part of the Router interface and returns the best match
// for the WebService and its Route for the given Request.
func (c CurlyRouter) SelectRoute(
webServices []*WebService,
httpRequest *http.Request) (selectedService *WebService, selected *Route, err error) {
requestTokens := tokenizePath(httpRequest.URL.Path)
detectedService := c.detectWebService(requestTokens, webServices)
if detectedService == nil {
if trace {
traceLogger.Printf("no WebService was found to match URL path:%s\n", httpRequest.URL.Path)
}
return nil, nil, NewError(http.StatusNotFound, "404: Page Not Found")
}
candidateRoutes := c.selectRoutes(detectedService, requestTokens)
if len(candidateRoutes) == 0 {
if trace {
traceLogger.Printf("no Route in WebService with path %s was found to match URL path:%s\n", detectedService.rootPath, httpRequest.URL.Path)
}
return detectedService, nil, NewError(http.StatusNotFound, "404: Page Not Found")
}
selectedRoute, err := c.detectRoute(candidateRoutes, httpRequest)
if selectedRoute == nil {
return detectedService, nil, err
}
return detectedService, selectedRoute, nil
}
// selectRoutes return a collection of Route from a WebService that matches the path tokens from the request.
func (c CurlyRouter) selectRoutes(ws *WebService, requestTokens []string) sortableCurlyRoutes {
candidates := sortableCurlyRoutes{}
for _, each := range ws.routes {
matches, paramCount, staticCount := c.matchesRouteByPathTokens(each.pathParts, requestTokens)
if matches {
candidates.add(curlyRoute{each, paramCount, staticCount}) // TODO make sure Routes() return pointers?
}
}
sort.Sort(sort.Reverse(candidates))
return candidates
}
// matchesRouteByPathTokens computes whether it matches, howmany parameters do match and what the number of static path elements are.
func (c CurlyRouter) matchesRouteByPathTokens(routeTokens, requestTokens []string) (matches bool, paramCount int, staticCount int) {
if len(routeTokens) < len(requestTokens) {
// proceed in matching only if last routeToken is wildcard
count := len(routeTokens)
if count == 0 || !strings.HasSuffix(routeTokens[count-1], "*}") {
return false, 0, 0
}
// proceed
}
for i, routeToken := range routeTokens {
if i == len(requestTokens) {
// reached end of request path
return false, 0, 0
}
requestToken := requestTokens[i]
if strings.HasPrefix(routeToken, "{") {
paramCount++
if colon := strings.Index(routeToken, ":"); colon != -1 {
// match by regex
matchesToken, matchesRemainder := c.regularMatchesPathToken(routeToken, colon, requestToken)
if !matchesToken {
return false, 0, 0
}
if matchesRemainder {
break
}
}
} else { // no { prefix
if requestToken != routeToken {
return false, 0, 0
}
staticCount++
}
}
return true, paramCount, staticCount
}
// regularMatchesPathToken tests whether the regular expression part of routeToken matches the requestToken or all remaining tokens
// format routeToken is {someVar:someExpression}, e.g. {zipcode:[\d][\d][\d][\d][A-Z][A-Z]}
func (c CurlyRouter) regularMatchesPathToken(routeToken string, colon int, requestToken string) (matchesToken bool, matchesRemainder bool) {
regPart := routeToken[colon+1 : len(routeToken)-1]
if regPart == "*" {
if trace {
traceLogger.Printf("wildcard parameter detected in route token %s that matches %s\n", routeToken, requestToken)
}
return true, true
}
matched, err := regexp.MatchString(regPart, requestToken)
return (matched && err == nil), false
}
var jsr311Router = RouterJSR311{}
// detectRoute selectes from a list of Route the first match by inspecting both the Accept and Content-Type
// headers of the Request. See also RouterJSR311 in jsr311.go
func (c CurlyRouter) detectRoute(candidateRoutes sortableCurlyRoutes, httpRequest *http.Request) (*Route, error) {
// tracing is done inside detectRoute
return jsr311Router.detectRoute(candidateRoutes.routes(), httpRequest)
}
// detectWebService returns the best matching webService given the list of path tokens.
// see also computeWebserviceScore
func (c CurlyRouter) detectWebService(requestTokens []string, webServices []*WebService) *WebService {
var best *WebService
score := -1
for _, each := range webServices {
matches, eachScore := c.computeWebserviceScore(requestTokens, each.pathExpr.tokens)
if matches && (eachScore > score) {
best = each
score = eachScore
}
}
return best
}
// computeWebserviceScore returns whether tokens match and
// the weighted score of the longest matching consecutive tokens from the beginning.
func (c CurlyRouter) computeWebserviceScore(requestTokens []string, tokens []string) (bool, int) {
if len(tokens) > len(requestTokens) {
return false, 0
}
score := 0
for i := 0; i < len(tokens); i++ {
each := requestTokens[i]
other := tokens[i]
if len(each) == 0 && len(other) == 0 {
score++
continue
}
if len(other) > 0 && strings.HasPrefix(other, "{") {
// no empty match
if len(each) == 0 {
return false, score
}
score += 1
} else {
// not a parameter
if each != other {
return false, score
}
score += (len(tokens) - i) * 10 //fuzzy
}
}
return true, score
}

52
vendor/github.com/emicklei/go-restful/curly_route.go generated vendored Normal file
View File

@@ -0,0 +1,52 @@
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.
// curlyRoute exits for sorting Routes by the CurlyRouter based on number of parameters and number of static path elements.
type curlyRoute struct {
route Route
paramCount int
staticCount int
}
type sortableCurlyRoutes []curlyRoute
func (s *sortableCurlyRoutes) add(route curlyRoute) {
*s = append(*s, route)
}
func (s sortableCurlyRoutes) routes() (routes []Route) {
for _, each := range s {
routes = append(routes, each.route) // TODO change return type
}
return routes
}
func (s sortableCurlyRoutes) Len() int {
return len(s)
}
func (s sortableCurlyRoutes) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
func (s sortableCurlyRoutes) Less(i, j int) bool {
ci := s[i]
cj := s[j]
// primary key
if ci.staticCount < cj.staticCount {
return true
}
if ci.staticCount > cj.staticCount {
return false
}
// secundary key
if ci.paramCount < cj.paramCount {
return true
}
if ci.paramCount > cj.paramCount {
return false
}
return ci.route.Path < cj.route.Path
}

231
vendor/github.com/emicklei/go-restful/curly_test.go generated vendored Normal file
View File

@@ -0,0 +1,231 @@
package restful
import (
"io"
"net/http"
"testing"
)
var requestPaths = []struct {
// url with path (1) is handled by service with root (2) and remainder has value final (3)
path, root string
}{
{"/", "/"},
{"/p", "/p"},
{"/p/x", "/p/{q}"},
{"/q/x", "/q"},
{"/p/x/", "/p/{q}"},
{"/p/x/y", "/p/{q}"},
{"/q/x/y", "/q"},
{"/z/q", "/{p}/q"},
{"/a/b/c/q", "/"},
}
// go test -v -test.run TestCurlyDetectWebService ...restful
func TestCurlyDetectWebService(t *testing.T) {
ws1 := new(WebService).Path("/")
ws2 := new(WebService).Path("/p")
ws3 := new(WebService).Path("/q")
ws4 := new(WebService).Path("/p/q")
ws5 := new(WebService).Path("/p/{q}")
ws7 := new(WebService).Path("/{p}/q")
var wss = []*WebService{ws1, ws2, ws3, ws4, ws5, ws7}
for _, each := range wss {
t.Logf("path=%s,toks=%v\n", each.pathExpr.Source, each.pathExpr.tokens)
}
router := CurlyRouter{}
ok := true
for i, fixture := range requestPaths {
requestTokens := tokenizePath(fixture.path)
who := router.detectWebService(requestTokens, wss)
if who != nil && who.RootPath() != fixture.root {
t.Logf("[line:%v] Unexpected dispatcher, expected:%v, actual:%v", i, fixture.root, who.RootPath())
ok = false
}
}
if !ok {
t.Fail()
}
}
var serviceDetects = []struct {
path string
found bool
root string
}{
{"/a/b", true, "/{p}/{q}/{r}"},
{"/p/q", true, "/p/q"},
{"/q/p", true, "/q"},
{"/", true, "/"},
{"/p/q/r", true, "/p/q"},
}
// go test -v -test.run Test_detectWebService ...restful
func Test_detectWebService(t *testing.T) {
router := CurlyRouter{}
ws1 := new(WebService).Path("/")
ws2 := new(WebService).Path("/p")
ws3 := new(WebService).Path("/q")
ws4 := new(WebService).Path("/p/q")
ws5 := new(WebService).Path("/p/{q}")
ws6 := new(WebService).Path("/p/{q}/")
ws7 := new(WebService).Path("/{p}/q")
ws8 := new(WebService).Path("/{p}/{q}/{r}")
var wss = []*WebService{ws8, ws7, ws6, ws5, ws4, ws3, ws2, ws1}
for _, fix := range serviceDetects {
requestPath := fix.path
requestTokens := tokenizePath(requestPath)
for _, ws := range wss {
serviceTokens := ws.pathExpr.tokens
matches, score := router.computeWebserviceScore(requestTokens, serviceTokens)
t.Logf("req=%s,toks:%v,ws=%s,toks:%v,score=%d,matches=%v", requestPath, requestTokens, ws.RootPath(), serviceTokens, score, matches)
}
best := router.detectWebService(requestTokens, wss)
if best != nil {
if fix.found {
t.Logf("best=%s", best.RootPath())
} else {
t.Fatalf("should have found:%s", fix.root)
}
}
}
}
var routeMatchers = []struct {
route string
path string
matches bool
paramCount int
staticCount int
}{
// route, request-path
{"/a", "/a", true, 0, 1},
{"/a", "/b", false, 0, 0},
{"/a", "/b", false, 0, 0},
{"/a/{b}/c/", "/a/2/c", true, 1, 2},
{"/{a}/{b}/{c}/", "/a/b", false, 0, 0},
{"/{x:*}", "/", false, 0, 0},
{"/{x:*}", "/a", true, 1, 0},
{"/{x:*}", "/a/b", true, 1, 0},
{"/a/{x:*}", "/a/b", true, 1, 1},
{"/a/{x:[A-Z][A-Z]}", "/a/ZX", true, 1, 1},
{"/basepath/{resource:*}", "/basepath/some/other/location/test.xml", true, 1, 1},
}
// clear && go test -v -test.run Test_matchesRouteByPathTokens ...restful
func Test_matchesRouteByPathTokens(t *testing.T) {
router := CurlyRouter{}
for i, each := range routeMatchers {
routeToks := tokenizePath(each.route)
reqToks := tokenizePath(each.path)
matches, pCount, sCount := router.matchesRouteByPathTokens(routeToks, reqToks)
if matches != each.matches {
t.Fatalf("[%d] unexpected matches outcome route:%s, path:%s, matches:%v", i, each.route, each.path, matches)
}
if pCount != each.paramCount {
t.Fatalf("[%d] unexpected paramCount got:%d want:%d ", i, pCount, each.paramCount)
}
if sCount != each.staticCount {
t.Fatalf("[%d] unexpected staticCount got:%d want:%d ", i, sCount, each.staticCount)
}
}
}
// clear && go test -v -test.run TestExtractParameters_Wildcard1 ...restful
func TestExtractParameters_Wildcard1(t *testing.T) {
params := doExtractParams("/fixed/{var:*}", 2, "/fixed/remainder", t)
if params["var"] != "remainder" {
t.Errorf("parameter mismatch var: %s", params["var"])
}
}
// clear && go test -v -test.run TestExtractParameters_Wildcard2 ...restful
func TestExtractParameters_Wildcard2(t *testing.T) {
params := doExtractParams("/fixed/{var:*}", 2, "/fixed/remain/der", t)
if params["var"] != "remain/der" {
t.Errorf("parameter mismatch var: %s", params["var"])
}
}
// clear && go test -v -test.run TestExtractParameters_Wildcard3 ...restful
func TestExtractParameters_Wildcard3(t *testing.T) {
params := doExtractParams("/static/{var:*}", 2, "/static/test/sub/hi.html", t)
if params["var"] != "test/sub/hi.html" {
t.Errorf("parameter mismatch var: %s", params["var"])
}
}
// clear && go test -v -test.run TestCurly_ISSUE_34 ...restful
func TestCurly_ISSUE_34(t *testing.T) {
ws1 := new(WebService).Path("/")
ws1.Route(ws1.GET("/{type}/{id}").To(curlyDummy))
ws1.Route(ws1.GET("/network/{id}").To(curlyDummy))
croutes := CurlyRouter{}.selectRoutes(ws1, tokenizePath("/network/12"))
if len(croutes) != 2 {
t.Fatal("expected 2 routes")
}
if got, want := croutes[0].route.Path, "/network/{id}"; got != want {
t.Errorf("got %v want %v", got, want)
}
}
// clear && go test -v -test.run TestCurly_ISSUE_34_2 ...restful
func TestCurly_ISSUE_34_2(t *testing.T) {
ws1 := new(WebService)
ws1.Route(ws1.GET("/network/{id}").To(curlyDummy))
ws1.Route(ws1.GET("/{type}/{id}").To(curlyDummy))
croutes := CurlyRouter{}.selectRoutes(ws1, tokenizePath("/network/12"))
if len(croutes) != 2 {
t.Fatal("expected 2 routes")
}
if got, want := croutes[0].route.Path, "/network/{id}"; got != want {
t.Errorf("got %v want %v", got, want)
}
}
// clear && go test -v -test.run TestCurly_JsonHtml ...restful
func TestCurly_JsonHtml(t *testing.T) {
ws1 := new(WebService)
ws1.Path("/")
ws1.Route(ws1.GET("/some.html").To(curlyDummy).Consumes("*/*").Produces("text/html"))
req, _ := http.NewRequest("GET", "/some.html", nil)
req.Header.Set("Accept", "application/json")
_, route, err := CurlyRouter{}.SelectRoute([]*WebService{ws1}, req)
if err == nil {
t.Error("error expected")
}
if route != nil {
t.Error("no route expected")
}
}
// go test -v -test.run TestCurly_ISSUE_137 ...restful
func TestCurly_ISSUE_137(t *testing.T) {
ws1 := new(WebService)
ws1.Route(ws1.GET("/hello").To(curlyDummy))
ws1.Path("/")
req, _ := http.NewRequest("GET", "/", nil)
_, route, _ := CurlyRouter{}.SelectRoute([]*WebService{ws1}, req)
t.Log(route)
if route != nil {
t.Error("no route expected")
}
}
// go test -v -test.run TestCurly_ISSUE_137_2 ...restful
func TestCurly_ISSUE_137_2(t *testing.T) {
ws1 := new(WebService)
ws1.Route(ws1.GET("/hello").To(curlyDummy))
ws1.Path("/")
req, _ := http.NewRequest("GET", "/hello/bob", nil)
_, route, _ := CurlyRouter{}.SelectRoute([]*WebService{ws1}, req)
t.Log(route)
if route != nil {
t.Errorf("no route expected, got %v", route)
}
}
func curlyDummy(req *Request, resp *Response) { io.WriteString(resp.ResponseWriter, "curlyDummy") }

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