Add initial network and tenant spec

Change-Id: Ia1e2d52c73539a45d294b7403c9290882c5d42d0
Implements: blueprint tenant-spec
This commit is contained in:
Pengfei Ni 2017-06-01 14:33:33 +08:00
parent 291127f0d1
commit 69c1e4013c
3023 changed files with 1689020 additions and 2 deletions

View File

@ -51,7 +51,7 @@ test-unit: test-flags
.PHONY: test-flags
test-flags:
cd $(DEST) && go test $(GOFLAGS) -tags '$(TAGS)' ./...
cd $(DEST) && go test $(GOFLAGS) -tags '$(TAGS)' $(go list ./... | grep -v vendor)
# The above pipeline is required because gofmt always returns 0 and we need
# to detect if any files are listed as having format problems.

98
glide.lock generated Normal file
View File

@ -0,0 +1,98 @@
hash: aa28871e98a60b6b588574caba38da993b57576bdeb7beba3d65a8d41c585775
updated: 2017-06-01T14:59:16.583022179+08:00
imports:
- name: github.com/emicklei/go-restful
version: ff4f55a206334ef123e4f79bbf348980da81ca46
subpackages:
- log
- name: github.com/emicklei/go-restful-swagger12
version: dcef7f55730566d41eae5db10e7d6981829720f6
- name: github.com/go-openapi/jsonpointer
version: 46af16f9f7b149af66e5d1bd010e3574dc06de98
- name: github.com/go-openapi/jsonreference
version: 13c6e3589ad90f49bd3e3bbe2c2cb3d7a4142272
- name: github.com/go-openapi/spec
version: 6aced65f8501fe1217321abf0749d354824ba2ff
- name: github.com/go-openapi/swag
version: 1d0bd113de87027671077d3c71eb3ac5d7dbba72
- name: github.com/gogo/protobuf
version: c0656edd0d9eab7c66d1eb0c568f9039345796f7
subpackages:
- proto
- sortkeys
- name: github.com/golang/glog
version: 44145f04b68cf362d9c4df2182967c2275eaefed
- name: github.com/google/gofuzz
version: 44d81051d367757e1c7c6a5a86423ece9afcf63c
- name: github.com/mailru/easyjson
version: d5b7844b561a7bc640052f1b935f7b800330d7e0
subpackages:
- buffer
- jlexer
- jwriter
- name: github.com/mjibson/appstats
version: 0542d5f0e87ea3a8fa4174322b9532f5d04f9fa8
- name: github.com/PuerkitoBio/purell
version: 8a290539e2e8629dbc4e6bad948158f790ec31f4
- name: github.com/PuerkitoBio/urlesc
version: 5bd2802263f21d8788851d5305584c82a5c75d7e
- name: github.com/spf13/pflag
version: 9ff6c6923cfffbcd502984b8e0c80539a94968b7
- name: golang.org/x/crypto
version: 080743b21912ae11ad9222778a048b795a5aae00
subpackages:
- ssh/terminal
- name: golang.org/x/net
version: f2499483f923065a842d38eb4c7f1927e6fc6e6d
subpackages:
- http2
- http2/hpack
- idna
- lex/httplex
- name: golang.org/x/text
version: 2910a502d2bf9e43193af9d68ca516529614eed3
subpackages:
- cases
- internal/tag
- language
- runes
- secure/bidirule
- secure/precis
- transform
- unicode/bidi
- unicode/norm
- width
- name: google.golang.org/appengine
version: 150dc57a1b433e64154302bdc40b6bb8aefa313a
subpackages:
- datastore
- memcache
- user
- name: gopkg.in/inf.v0
version: 3887ee99ecf07df5b447e9b00d9c0b2adaa9f3e4
- name: gopkg.in/vmihailenco/msgpack.v2
version: f4f8982de4ef0de18be76456617cc3f5d8d8141e
- name: k8s.io/apimachinery
version: 2de00c78cb6d6127fb51b9531c1b3def1cbcac8c
subpackages:
- pkg/api/resource
- pkg/apis/meta/v1
- pkg/conversion
- pkg/conversion/queryparams
- pkg/fields
- pkg/labels
- pkg/openapi
- pkg/runtime
- pkg/runtime/schema
- pkg/selection
- pkg/types
- pkg/util/errors
- pkg/util/intstr
- pkg/util/net
- pkg/util/runtime
- pkg/util/sets
- pkg/util/validation
- pkg/util/wait
- pkg/watch
- third_party/forked/golang/reflect
testImports: []

View File

@ -1,2 +1,21 @@
package: git.openstack.org/openstack/stackube
import: []
import:
- package: k8s.io/apimachinery
subpackages:
- pkg/apis/meta/v1
- pkg/runtime
- pkg/runtime/schema
- package: github.com/emicklei/go-restful-swagger12
version: 1.0.1
- package: github.com/mjibson/appstats
- package: google.golang.org/appengine
version: v1.0.0
subpackages:
- memcache
- datastore
- user
- package: gopkg.in/vmihailenco/msgpack.v2
version: v2.9.1
- package: golang.org/x/crypto
subpackages:
- ssh/terminal

44
pkg/apis/v1/register.go Normal file
View File

@ -0,0 +1,44 @@
package v1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
var (
// SchemeBuilder collects functions that add things to a scheme.
SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
// AddToScheme applies all the stored functions to the scheme.
AddToScheme = SchemeBuilder.AddToScheme
)
// GroupName is the group name use in this package
const GroupName = "stackube.kubernetes.io"
// SchemeGroupVersion is group version used to register these objects
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1"}
// Resource takes an unqualified resource and returns a Group qualified GroupResource
func Resource(resource string) schema.GroupResource {
return SchemeGroupVersion.WithResource(resource).GroupResource()
}
func init() {
// We only register manually written functions here. The registration of the
// generated functions takes place in the generated files. The separation
// makes the code compile even when the generated files are missing.
SchemeBuilder.Register(addKnownTypes)
}
// Adds the list of known types to api.Scheme.
func addKnownTypes(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(SchemeGroupVersion,
&Network{},
&NetworkList{},
&Tenant{},
&TenantList{},
)
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
return nil
}

96
pkg/apis/v1/types.go Normal file
View File

@ -0,0 +1,96 @@
package v1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
const (
// NetworkResourcePlural is the plural of network resource.
NetworkResourcePlural = "networks"
// TenantResourcePlural is the plural of tenant resource.
TenantResourcePlural = "tenants"
)
// Network describes a Neutron network.
type Network struct {
// TypeMeta defines type of the object and its API schema version.
metav1.TypeMeta `json:",inline"`
// ObjectMeta is metadata that all persisted resources must have.
metav1.ObjectMeta `json:"metadata"`
// Spec describes the behavior of a network.
Spec NetworkSpec
// Status describes the network status.
Status NetworkStatus `json:"status,omitempty"`
}
// NetworkSpec is the spec of a network.
type NetworkSpec struct {
// The CIDR of the network.
CIDR string `json:"cidr"`
// The gateway IP.
Gateway string `json:"gateway"`
// The network ID in Neutron.
// If provided, wouldn't create a network in Neutron.
NetworkID string `json:"networkID"`
}
// NetworkStatus is the status of a network.
type NetworkStatus struct {
// State describes the network state.
State string `json:"state,omitempty"`
// Message describes why network is in current state.
Message string `json:"message,omitempty"`
}
// NetworkList is a list of networks.
type NetworkList struct {
// TypeMeta defines type of the object and its API schema version.
metav1.TypeMeta `json:",inline"`
// ObjectMeta is metadata that all persisted resources must have.
metav1.ListMeta `json:"metadata"`
// Items contains a list of networks.
Items []Network `json:"items"`
}
// Tenant describes a Keystone tenant.
type Tenant struct {
// TypeMeta defines type of the object and its API schema version.
metav1.TypeMeta `json:",inline"`
// ObjectMeta is metadata that all persisted resources must have.
metav1.ObjectMeta `json:"metadata"`
// Spec defines the behavior of a tenant.
Spec TenantSpec
// Status describes the tenant status.
Status TenantStatus `json:"status,omitempty"`
}
// TenantSpec is the spec of a tenant.
type TenantSpec struct {
// The username of this user.
UserName string `json:"username"`
// The password of this user.
Password string `json:"password"`
// The tenant ID in Keystone.
// If provided, wouldn't create a new tenant in Keystone.
TenantID string `json:"tenantID"`
}
// TenantStatus is the status of a tenant.
type TenantStatus struct {
// State describes the tenant state.
State string `json:"state,omitempty"`
// Message describes why tenant is in current state.
Message string `json:"message,omitempty"`
}
// TenantList is a list of tenants.
type TenantList struct {
// TypeMeta defines type of the object and its API schema version.
metav1.TypeMeta `json:",inline"`
// ObjectMeta is metadata that all persisted resources must have.
metav1.ListMeta `json:"metadata"`
// Items contains a list of tenants.
Items []Tenant `json:"items"`
}

5
vendor/github.com/PuerkitoBio/purell/.gitignore generated vendored Normal file
View File

@ -0,0 +1,5 @@
*.sublime-*
.DS_Store
*.swp
*.swo
tags

7
vendor/github.com/PuerkitoBio/purell/.travis.yml generated vendored Normal file
View File

@ -0,0 +1,7 @@
language: go
go:
- 1.4
- 1.5
- 1.6
- tip

12
vendor/github.com/PuerkitoBio/purell/LICENSE generated vendored Normal file
View File

@ -0,0 +1,12 @@
Copyright (c) 2012, Martin Angers
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
* Neither the name of the author nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

185
vendor/github.com/PuerkitoBio/purell/README.md generated vendored Normal file
View File

@ -0,0 +1,185 @@
# Purell
Purell is a tiny Go library to normalize URLs. It returns a pure URL. Pure-ell. Sanitizer and all. Yeah, I know...
Based on the [wikipedia paper][wiki] and the [RFC 3986 document][rfc].
[![build status](https://secure.travis-ci.org/PuerkitoBio/purell.png)](http://travis-ci.org/PuerkitoBio/purell)
## Install
`go get github.com/PuerkitoBio/purell`
## Changelog
* **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.
* **v0.1.0** : Initial release.
## Examples
From `example_test.go` (note that in your code, you would import "github.com/PuerkitoBio/purell", and would prefix references to its methods and constants with "purell."):
```go
package purell
import (
"fmt"
"net/url"
)
func ExampleNormalizeURLString() {
if normalized, err := NormalizeURLString("hTTp://someWEBsite.com:80/Amazing%3f/url/",
FlagLowercaseScheme|FlagLowercaseHost|FlagUppercaseEscapes); err != nil {
panic(err)
} else {
fmt.Print(normalized)
}
// Output: http://somewebsite.com:80/Amazing%3F/url/
}
func ExampleMustNormalizeURLString() {
normalized := MustNormalizeURLString("hTTpS://someWEBsite.com:443/Amazing%fa/url/",
FlagsUnsafeGreedy)
fmt.Print(normalized)
// Output: http://somewebsite.com/Amazing%FA/url
}
func ExampleNormalizeURL() {
if u, err := url.Parse("Http://SomeUrl.com:8080/a/b/.././c///g?c=3&a=1&b=9&c=0#target"); err != nil {
panic(err)
} else {
normalized := NormalizeURL(u, FlagsUsuallySafeGreedy|FlagRemoveDuplicateSlashes|FlagRemoveFragment)
fmt.Print(normalized)
}
// Output: http://someurl.com:8080/a/c/g?c=3&a=1&b=9&c=0
}
```
## API
As seen in the examples above, purell offers three methods, `NormalizeURLString(string, NormalizationFlags) (string, error)`, `MustNormalizeURLString(string, NormalizationFlags) (string)` and `NormalizeURL(*url.URL, NormalizationFlags) (string)`. They all normalize the provided URL based on the specified flags. Here are the available flags:
```go
const (
// Safe normalizations
FlagLowercaseScheme NormalizationFlags = 1 << iota // HTTP://host -> http://host, applied by default in Go1.1
FlagLowercaseHost // http://HOST -> http://host
FlagUppercaseEscapes // http://host/t%ef -> http://host/t%EF
FlagDecodeUnnecessaryEscapes // http://host/t%41 -> http://host/tA
FlagEncodeNecessaryEscapes // http://host/!"#$ -> http://host/%21%22#$
FlagRemoveDefaultPort // http://host:80 -> http://host
FlagRemoveEmptyQuerySeparator // http://host/path? -> http://host/path
// Usually safe normalizations
FlagRemoveTrailingSlash // http://host/path/ -> http://host/path
FlagAddTrailingSlash // http://host/path -> http://host/path/ (should choose only one of these add/remove trailing slash flags)
FlagRemoveDotSegments // http://host/path/./a/b/../c -> http://host/path/a/c
// Unsafe normalizations
FlagRemoveDirectoryIndex // http://host/path/index.html -> http://host/path/
FlagRemoveFragment // http://host/path#fragment -> http://host/path
FlagForceHTTP // https://host -> http://host
FlagRemoveDuplicateSlashes // http://host/path//a///b -> http://host/path/a/b
FlagRemoveWWW // http://www.host/ -> http://host/
FlagAddWWW // http://host/ -> http://www.host/ (should choose only one of these add/remove WWW flags)
FlagSortQuery // http://host/path?c=3&b=2&a=1&b=1 -> http://host/path?a=1&b=1&b=2&c=3
// Normalizations not in the wikipedia article, required to cover tests cases
// submitted by jehiah
FlagDecodeDWORDHost // http://1113982867 -> http://66.102.7.147
FlagDecodeOctalHost // http://0102.0146.07.0223 -> http://66.102.7.147
FlagDecodeHexHost // http://0x42660793 -> http://66.102.7.147
FlagRemoveUnnecessaryHostDots // http://.host../path -> http://host/path
FlagRemoveEmptyPortSeparator // http://host:/path -> http://host/path
// Convenience set of safe normalizations
FlagsSafe NormalizationFlags = FlagLowercaseHost | FlagLowercaseScheme | FlagUppercaseEscapes | FlagDecodeUnnecessaryEscapes | FlagEncodeNecessaryEscapes | FlagRemoveDefaultPort | FlagRemoveEmptyQuerySeparator
// For convenience sets, "greedy" uses the "remove trailing slash" and "remove www. prefix" flags,
// while "non-greedy" uses the "add (or keep) the trailing slash" and "add www. prefix".
// Convenience set of usually safe normalizations (includes FlagsSafe)
FlagsUsuallySafeGreedy NormalizationFlags = FlagsSafe | FlagRemoveTrailingSlash | FlagRemoveDotSegments
FlagsUsuallySafeNonGreedy NormalizationFlags = FlagsSafe | FlagAddTrailingSlash | FlagRemoveDotSegments
// Convenience set of unsafe normalizations (includes FlagsUsuallySafe)
FlagsUnsafeGreedy NormalizationFlags = FlagsUsuallySafeGreedy | FlagRemoveDirectoryIndex | FlagRemoveFragment | FlagForceHTTP | FlagRemoveDuplicateSlashes | FlagRemoveWWW | FlagSortQuery
FlagsUnsafeNonGreedy NormalizationFlags = FlagsUsuallySafeNonGreedy | FlagRemoveDirectoryIndex | FlagRemoveFragment | FlagForceHTTP | FlagRemoveDuplicateSlashes | FlagAddWWW | FlagSortQuery
// Convenience set of all available flags
FlagsAllGreedy = FlagsUnsafeGreedy | FlagDecodeDWORDHost | FlagDecodeOctalHost | FlagDecodeHexHost | FlagRemoveUnnecessaryHostDots | FlagRemoveEmptyPortSeparator
FlagsAllNonGreedy = FlagsUnsafeNonGreedy | FlagDecodeDWORDHost | FlagDecodeOctalHost | FlagDecodeHexHost | FlagRemoveUnnecessaryHostDots | FlagRemoveEmptyPortSeparator
)
```
For convenience, the set of flags `FlagsSafe`, `FlagsUsuallySafe[Greedy|NonGreedy]`, `FlagsUnsafe[Greedy|NonGreedy]` and `FlagsAll[Greedy|NonGreedy]` are provided for the similarly grouped normalizations on [wikipedia's URL normalization page][wiki]. You can add (using the bitwise OR `|` operator) or remove (using the bitwise AND NOT `&^` operator) individual flags from the sets if required, to build your own custom set.
The [full godoc reference is available on gopkgdoc][godoc].
Some things to note:
* `FlagDecodeUnnecessaryEscapes`, `FlagEncodeNecessaryEscapes`, `FlagUppercaseEscapes` and `FlagRemoveEmptyQuerySeparator` are always implicitly set, because internally, the URL string is parsed as an URL object, which automatically decodes unnecessary escapes, uppercases and encodes necessary ones, and removes empty query separators (an unnecessary `?` at the end of the url). So this operation cannot **not** be done. For this reason, `FlagRemoveEmptyQuerySeparator` (as well as the other three) has been included in the `FlagsSafe` convenience set, instead of `FlagsUnsafe`, where Wikipedia puts it.
* The `FlagDecodeUnnecessaryEscapes` decodes the following escapes (*from -> to*):
- %24 -> $
- %26 -> &
- %2B-%3B -> +,-./0123456789:;
- %3D -> =
- %40-%5A -> @ABCDEFGHIJKLMNOPQRSTUVWXYZ
- %5F -> _
- %61-%7A -> abcdefghijklmnopqrstuvwxyz
- %7E -> ~
* When the `NormalizeURL` function is used (passing an URL object), this source URL object is modified (that is, after the call, the URL object will be modified to reflect the normalization).
* The *replace IP with domain name* normalization (`http://208.77.188.166/ → http://www.example.com/`) is obviously not possible for a library without making some network requests. This is not implemented in purell.
* The *remove unused query string parameters* and *remove default query parameters* are also not implemented, since this is a very case-specific normalization, and it is quite trivial to do with an URL object.
### Safe vs Usually Safe vs Unsafe
Purell allows you to control the level of risk you take while normalizing an URL. You can aggressively normalize, play it totally safe, or anything in between.
Consider the following URL:
`HTTPS://www.RooT.com/toto/t%45%1f///a/./b/../c/?z=3&w=2&a=4&w=1#invalid`
Normalizing with the `FlagsSafe` gives:
`https://www.root.com/toto/tE%1F///a/./b/../c/?z=3&w=2&a=4&w=1#invalid`
With the `FlagsUsuallySafeGreedy`:
`https://www.root.com/toto/tE%1F///a/c?z=3&w=2&a=4&w=1#invalid`
And with `FlagsUnsafeGreedy`:
`http://root.com/toto/tE%1F/a/c?a=4&w=1&w=2&z=3`
## TODOs
* Add a class/default instance to allow specifying custom directory index names? At the moment, removing directory index removes `(^|/)((?:default|index)\.\w{1,4})$`.
## Thanks / Contributions
@rogpeppe
@jehiah
@opennota
@pchristopher1275
@zenovich
## License
The [BSD 3-Clause license][bsd].
[bsd]: http://opensource.org/licenses/BSD-3-Clause
[wiki]: http://en.wikipedia.org/wiki/URL_normalization
[rfc]: http://tools.ietf.org/html/rfc3986#section-6
[godoc]: http://go.pkgdoc.org/github.com/PuerkitoBio/purell
[pr5]: https://github.com/PuerkitoBio/purell/pull/5
[iss7]: https://github.com/PuerkitoBio/purell/issues/7

57
vendor/github.com/PuerkitoBio/purell/bench_test.go generated vendored Normal file
View File

@ -0,0 +1,57 @@
package purell
import (
"testing"
)
var (
safeUrl = "HttPS://..iaMHost..Test:443/paTh^A%ef//./%41PaTH/..//?"
usuallySafeUrl = "HttPS://..iaMHost..Test:443/paTh^A%ef//./%41PaTH/../final/"
unsafeUrl = "HttPS://..www.iaMHost..Test:443/paTh^A%ef//./%41PaTH/../final/index.html?t=val1&a=val4&z=val5&a=val1#fragment"
allDWORDUrl = "HttPS://1113982867:/paTh^A%ef//./%41PaTH/../final/index.html?t=val1&a=val4&z=val5&a=val1#fragment"
allOctalUrl = "HttPS://0102.0146.07.0223:/paTh^A%ef//./%41PaTH/../final/index.html?t=val1&a=val4&z=val5&a=val1#fragment"
allHexUrl = "HttPS://0x42660793:/paTh^A%ef//./%41PaTH/../final/index.html?t=val1&a=val4&z=val5&a=val1#fragment"
allCombinedUrl = "HttPS://..0x42660793.:/paTh^A%ef//./%41PaTH/../final/index.html?t=val1&a=val4&z=val5&a=val1#fragment"
)
func BenchmarkSafe(b *testing.B) {
for i := 0; i < b.N; i++ {
NormalizeURLString(safeUrl, FlagsSafe)
}
}
func BenchmarkUsuallySafe(b *testing.B) {
for i := 0; i < b.N; i++ {
NormalizeURLString(usuallySafeUrl, FlagsUsuallySafeGreedy)
}
}
func BenchmarkUnsafe(b *testing.B) {
for i := 0; i < b.N; i++ {
NormalizeURLString(unsafeUrl, FlagsUnsafeGreedy)
}
}
func BenchmarkAllDWORD(b *testing.B) {
for i := 0; i < b.N; i++ {
NormalizeURLString(allDWORDUrl, FlagsAllGreedy)
}
}
func BenchmarkAllOctal(b *testing.B) {
for i := 0; i < b.N; i++ {
NormalizeURLString(allOctalUrl, FlagsAllGreedy)
}
}
func BenchmarkAllHex(b *testing.B) {
for i := 0; i < b.N; i++ {
NormalizeURLString(allHexUrl, FlagsAllGreedy)
}
}
func BenchmarkAllCombined(b *testing.B) {
for i := 0; i < b.N; i++ {
NormalizeURLString(allCombinedUrl, FlagsAllGreedy)
}
}

View File

@ -0,0 +1,9 @@
PASS
BenchmarkSafe 500000 6131 ns/op
BenchmarkUsuallySafe 200000 7864 ns/op
BenchmarkUnsafe 100000 28560 ns/op
BenchmarkAllDWORD 50000 38722 ns/op
BenchmarkAllOctal 50000 40941 ns/op
BenchmarkAllHex 50000 44063 ns/op
BenchmarkAllCombined 50000 33613 ns/op
ok github.com/PuerkitoBio/purell 17.404s

35
vendor/github.com/PuerkitoBio/purell/example_test.go generated vendored Normal file
View File

@ -0,0 +1,35 @@
package purell
import (
"fmt"
"net/url"
)
func ExampleNormalizeURLString() {
if normalized, err := NormalizeURLString("hTTp://someWEBsite.com:80/Amazing%3f/url/",
FlagLowercaseScheme|FlagLowercaseHost|FlagUppercaseEscapes); err != nil {
panic(err)
} else {
fmt.Print(normalized)
}
// Output: http://somewebsite.com:80/Amazing%3F/url/
}
func ExampleMustNormalizeURLString() {
normalized := MustNormalizeURLString("hTTpS://someWEBsite.com:443/Amazing%fa/url/",
FlagsUnsafeGreedy)
fmt.Print(normalized)
// Output: http://somewebsite.com/Amazing%FA/url
}
func ExampleNormalizeURL() {
if u, err := url.Parse("Http://SomeUrl.com:8080/a/b/.././c///g?c=3&a=1&b=9&c=0#target"); err != nil {
panic(err)
} else {
normalized := NormalizeURL(u, FlagsUsuallySafeGreedy|FlagRemoveDuplicateSlashes|FlagRemoveFragment)
fmt.Print(normalized)
}
// Output: http://someurl.com:8080/a/c/g?c=3&a=1&b=9&c=0
}

375
vendor/github.com/PuerkitoBio/purell/purell.go generated vendored Normal file
View File

@ -0,0 +1,375 @@
/*
Package purell offers URL normalization as described on the wikipedia page:
http://en.wikipedia.org/wiki/URL_normalization
*/
package purell
import (
"bytes"
"fmt"
"net/url"
"regexp"
"sort"
"strconv"
"strings"
"github.com/PuerkitoBio/urlesc"
"golang.org/x/net/idna"
"golang.org/x/text/secure/precis"
"golang.org/x/text/unicode/norm"
)
// A set of normalization flags determines how a URL will
// be normalized.
type NormalizationFlags uint
const (
// Safe normalizations
FlagLowercaseScheme NormalizationFlags = 1 << iota // HTTP://host -> http://host, applied by default in Go1.1
FlagLowercaseHost // http://HOST -> http://host
FlagUppercaseEscapes // http://host/t%ef -> http://host/t%EF
FlagDecodeUnnecessaryEscapes // http://host/t%41 -> http://host/tA
FlagEncodeNecessaryEscapes // http://host/!"#$ -> http://host/%21%22#$
FlagRemoveDefaultPort // http://host:80 -> http://host
FlagRemoveEmptyQuerySeparator // http://host/path? -> http://host/path
// Usually safe normalizations
FlagRemoveTrailingSlash // http://host/path/ -> http://host/path
FlagAddTrailingSlash // http://host/path -> http://host/path/ (should choose only one of these add/remove trailing slash flags)
FlagRemoveDotSegments // http://host/path/./a/b/../c -> http://host/path/a/c
// Unsafe normalizations
FlagRemoveDirectoryIndex // http://host/path/index.html -> http://host/path/
FlagRemoveFragment // http://host/path#fragment -> http://host/path
FlagForceHTTP // https://host -> http://host
FlagRemoveDuplicateSlashes // http://host/path//a///b -> http://host/path/a/b
FlagRemoveWWW // http://www.host/ -> http://host/
FlagAddWWW // http://host/ -> http://www.host/ (should choose only one of these add/remove WWW flags)
FlagSortQuery // http://host/path?c=3&b=2&a=1&b=1 -> http://host/path?a=1&b=1&b=2&c=3
// Normalizations not in the wikipedia article, required to cover tests cases
// submitted by jehiah
FlagDecodeDWORDHost // http://1113982867 -> http://66.102.7.147
FlagDecodeOctalHost // http://0102.0146.07.0223 -> http://66.102.7.147
FlagDecodeHexHost // http://0x42660793 -> http://66.102.7.147
FlagRemoveUnnecessaryHostDots // http://.host../path -> http://host/path
FlagRemoveEmptyPortSeparator // http://host:/path -> http://host/path
// Convenience set of safe normalizations
FlagsSafe NormalizationFlags = FlagLowercaseHost | FlagLowercaseScheme | FlagUppercaseEscapes | FlagDecodeUnnecessaryEscapes | FlagEncodeNecessaryEscapes | FlagRemoveDefaultPort | FlagRemoveEmptyQuerySeparator
// For convenience sets, "greedy" uses the "remove trailing slash" and "remove www. prefix" flags,
// while "non-greedy" uses the "add (or keep) the trailing slash" and "add www. prefix".
// Convenience set of usually safe normalizations (includes FlagsSafe)
FlagsUsuallySafeGreedy NormalizationFlags = FlagsSafe | FlagRemoveTrailingSlash | FlagRemoveDotSegments
FlagsUsuallySafeNonGreedy NormalizationFlags = FlagsSafe | FlagAddTrailingSlash | FlagRemoveDotSegments
// Convenience set of unsafe normalizations (includes FlagsUsuallySafe)
FlagsUnsafeGreedy NormalizationFlags = FlagsUsuallySafeGreedy | FlagRemoveDirectoryIndex | FlagRemoveFragment | FlagForceHTTP | FlagRemoveDuplicateSlashes | FlagRemoveWWW | FlagSortQuery
FlagsUnsafeNonGreedy NormalizationFlags = FlagsUsuallySafeNonGreedy | FlagRemoveDirectoryIndex | FlagRemoveFragment | FlagForceHTTP | FlagRemoveDuplicateSlashes | FlagAddWWW | FlagSortQuery
// Convenience set of all available flags
FlagsAllGreedy = FlagsUnsafeGreedy | FlagDecodeDWORDHost | FlagDecodeOctalHost | FlagDecodeHexHost | FlagRemoveUnnecessaryHostDots | FlagRemoveEmptyPortSeparator
FlagsAllNonGreedy = FlagsUnsafeNonGreedy | FlagDecodeDWORDHost | FlagDecodeOctalHost | FlagDecodeHexHost | FlagRemoveUnnecessaryHostDots | FlagRemoveEmptyPortSeparator
)
const (
defaultHttpPort = ":80"
defaultHttpsPort = ":443"
)
// Regular expressions used by the normalizations
var rxPort = regexp.MustCompile(`(:\d+)/?$`)
var rxDirIndex = regexp.MustCompile(`(^|/)((?:default|index)\.\w{1,4})$`)
var rxDupSlashes = regexp.MustCompile(`/{2,}`)
var rxDWORDHost = regexp.MustCompile(`^(\d+)((?:\.+)?(?:\:\d*)?)$`)
var rxOctalHost = regexp.MustCompile(`^(0\d*)\.(0\d*)\.(0\d*)\.(0\d*)((?:\.+)?(?:\:\d*)?)$`)
var rxHexHost = regexp.MustCompile(`^0x([0-9A-Fa-f]+)((?:\.+)?(?:\:\d*)?)$`)
var rxHostDots = regexp.MustCompile(`^(.+?)(:\d+)?$`)
var rxEmptyPort = regexp.MustCompile(`:+$`)
// Map of flags to implementation function.
// FlagDecodeUnnecessaryEscapes has no action, since it is done automatically
// by parsing the string as an URL. Same for FlagUppercaseEscapes and FlagRemoveEmptyQuerySeparator.
// Since maps have undefined traversing order, make a slice of ordered keys
var flagsOrder = []NormalizationFlags{
FlagLowercaseScheme,
FlagLowercaseHost,
FlagRemoveDefaultPort,
FlagRemoveDirectoryIndex,
FlagRemoveDotSegments,
FlagRemoveFragment,
FlagForceHTTP, // Must be after remove default port (because https=443/http=80)
FlagRemoveDuplicateSlashes,
FlagRemoveWWW,
FlagAddWWW,
FlagSortQuery,
FlagDecodeDWORDHost,
FlagDecodeOctalHost,
FlagDecodeHexHost,
FlagRemoveUnnecessaryHostDots,
FlagRemoveEmptyPortSeparator,
FlagRemoveTrailingSlash, // These two (add/remove trailing slash) must be last
FlagAddTrailingSlash,
}
// ... and then the map, where order is unimportant
var flags = map[NormalizationFlags]func(*url.URL){
FlagLowercaseScheme: lowercaseScheme,
FlagLowercaseHost: lowercaseHost,
FlagRemoveDefaultPort: removeDefaultPort,
FlagRemoveDirectoryIndex: removeDirectoryIndex,
FlagRemoveDotSegments: removeDotSegments,
FlagRemoveFragment: removeFragment,
FlagForceHTTP: forceHTTP,
FlagRemoveDuplicateSlashes: removeDuplicateSlashes,
FlagRemoveWWW: removeWWW,
FlagAddWWW: addWWW,
FlagSortQuery: sortQuery,
FlagDecodeDWORDHost: decodeDWORDHost,
FlagDecodeOctalHost: decodeOctalHost,
FlagDecodeHexHost: decodeHexHost,
FlagRemoveUnnecessaryHostDots: removeUnncessaryHostDots,
FlagRemoveEmptyPortSeparator: removeEmptyPortSeparator,
FlagRemoveTrailingSlash: removeTrailingSlash,
FlagAddTrailingSlash: addTrailingSlash,
}
// MustNormalizeURLString returns the normalized string, and panics if an error occurs.
// It takes an URL string as input, as well as the normalization flags.
func MustNormalizeURLString(u string, f NormalizationFlags) string {
result, e := NormalizeURLString(u, f)
if e != nil {
panic(e)
}
return result
}
// 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) {
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
}
panic("Unreachable code.")
}
// NormalizeURL returns the normalized string.
// It takes a parsed URL object as input, as well as the normalization flags.
func NormalizeURL(u *url.URL, f NormalizationFlags) string {
for _, k := range flagsOrder {
if f&k == k {
flags[k](u)
}
}
return urlesc.Escape(u)
}
func lowercaseScheme(u *url.URL) {
if len(u.Scheme) > 0 {
u.Scheme = strings.ToLower(u.Scheme)
}
}
func lowercaseHost(u *url.URL) {
if len(u.Host) > 0 {
u.Host = strings.ToLower(u.Host)
}
}
func removeDefaultPort(u *url.URL) {
if len(u.Host) > 0 {
scheme := strings.ToLower(u.Scheme)
u.Host = rxPort.ReplaceAllStringFunc(u.Host, func(val string) string {
if (scheme == "http" && val == defaultHttpPort) || (scheme == "https" && val == defaultHttpsPort) {
return ""
}
return val
})
}
}
func removeTrailingSlash(u *url.URL) {
if l := len(u.Path); l > 0 {
if strings.HasSuffix(u.Path, "/") {
u.Path = u.Path[:l-1]
}
} else if l = len(u.Host); l > 0 {
if strings.HasSuffix(u.Host, "/") {
u.Host = u.Host[:l-1]
}
}
}
func addTrailingSlash(u *url.URL) {
if l := len(u.Path); l > 0 {
if !strings.HasSuffix(u.Path, "/") {
u.Path += "/"
}
} else if l = len(u.Host); l > 0 {
if !strings.HasSuffix(u.Host, "/") {
u.Host += "/"
}
}
}
func removeDotSegments(u *url.URL) {
if len(u.Path) > 0 {
var dotFree []string
var lastIsDot bool
sections := strings.Split(u.Path, "/")
for _, s := range sections {
if s == ".." {
if len(dotFree) > 0 {
dotFree = dotFree[:len(dotFree)-1]
}
} else if s != "." {
dotFree = append(dotFree, s)
}
lastIsDot = (s == "." || s == "..")
}
// Special case if host does not end with / and new path does not begin with /
u.Path = strings.Join(dotFree, "/")
if u.Host != "" && !strings.HasSuffix(u.Host, "/") && !strings.HasPrefix(u.Path, "/") {
u.Path = "/" + u.Path
}
// Special case if the last segment was a dot, make sure the path ends with a slash
if lastIsDot && !strings.HasSuffix(u.Path, "/") {
u.Path += "/"
}
}
}
func removeDirectoryIndex(u *url.URL) {
if len(u.Path) > 0 {
u.Path = rxDirIndex.ReplaceAllString(u.Path, "$1")
}
}
func removeFragment(u *url.URL) {
u.Fragment = ""
}
func forceHTTP(u *url.URL) {
if strings.ToLower(u.Scheme) == "https" {
u.Scheme = "http"
}
}
func removeDuplicateSlashes(u *url.URL) {
if len(u.Path) > 0 {
u.Path = rxDupSlashes.ReplaceAllString(u.Path, "/")
}
}
func removeWWW(u *url.URL) {
if len(u.Host) > 0 && strings.HasPrefix(strings.ToLower(u.Host), "www.") {
u.Host = u.Host[4:]
}
}
func addWWW(u *url.URL) {
if len(u.Host) > 0 && !strings.HasPrefix(strings.ToLower(u.Host), "www.") {
u.Host = "www." + u.Host
}
}
func sortQuery(u *url.URL) {
q := u.Query()
if len(q) > 0 {
arKeys := make([]string, len(q))
i := 0
for k, _ := range q {
arKeys[i] = k
i++
}
sort.Strings(arKeys)
buf := new(bytes.Buffer)
for _, k := range arKeys {
sort.Strings(q[k])
for _, v := range q[k] {
if buf.Len() > 0 {
buf.WriteRune('&')
}
buf.WriteString(fmt.Sprintf("%s=%s", k, urlesc.QueryEscape(v)))
}
}
// Rebuild the raw query string
u.RawQuery = buf.String()
}
}
func decodeDWORDHost(u *url.URL) {
if len(u.Host) > 0 {
if matches := rxDWORDHost.FindStringSubmatch(u.Host); len(matches) > 2 {
var parts [4]int64
dword, _ := strconv.ParseInt(matches[1], 10, 0)
for i, shift := range []uint{24, 16, 8, 0} {
parts[i] = dword >> shift & 0xFF
}
u.Host = fmt.Sprintf("%d.%d.%d.%d%s", parts[0], parts[1], parts[2], parts[3], matches[2])
}
}
}
func decodeOctalHost(u *url.URL) {
if len(u.Host) > 0 {
if matches := rxOctalHost.FindStringSubmatch(u.Host); len(matches) > 5 {
var parts [4]int64
for i := 1; i <= 4; i++ {
parts[i-1], _ = strconv.ParseInt(matches[i], 8, 0)
}
u.Host = fmt.Sprintf("%d.%d.%d.%d%s", parts[0], parts[1], parts[2], parts[3], matches[5])
}
}
}
func decodeHexHost(u *url.URL) {
if len(u.Host) > 0 {
if matches := rxHexHost.FindStringSubmatch(u.Host); len(matches) > 2 {
// Conversion is safe because of regex validation
parsed, _ := strconv.ParseInt(matches[1], 16, 0)
// Set host as DWORD (base 10) encoded host
u.Host = fmt.Sprintf("%d%s", parsed, matches[2])
// The rest is the same as decoding a DWORD host
decodeDWORDHost(u)
}
}
}
func removeUnncessaryHostDots(u *url.URL) {
if len(u.Host) > 0 {
if matches := rxHostDots.FindStringSubmatch(u.Host); len(matches) > 1 {
// Trim the leading and trailing dots
u.Host = strings.Trim(matches[1], ".")
if len(matches) > 2 {
u.Host += matches[2]
}
}
}
}
func removeEmptyPortSeparator(u *url.URL) {
if len(u.Host) > 0 {
u.Host = rxEmptyPort.ReplaceAllString(u.Host, "")
}
}

768
vendor/github.com/PuerkitoBio/purell/purell_test.go generated vendored Normal file
View File

@ -0,0 +1,768 @@
package purell
import (
"fmt"
"net/url"
"testing"
)
type testCase struct {
nm string
src string
flgs NormalizationFlags
res string
parsed bool
}
var (
cases = [...]*testCase{
&testCase{
"LowerScheme",
"HTTP://www.SRC.ca",
FlagLowercaseScheme,
"http://www.SRC.ca",
false,
},
&testCase{
"LowerScheme2",
"http://www.SRC.ca",
FlagLowercaseScheme,
"http://www.SRC.ca",
false,
},
&testCase{
"LowerHost",
"HTTP://www.SRC.ca/",
FlagLowercaseHost,
"http://www.src.ca/", // Since Go1.1, scheme is automatically lowercased
false,
},
&testCase{
"UpperEscapes",
`http://www.whatever.com/Some%aa%20Special%8Ecases/`,
FlagUppercaseEscapes,
"http://www.whatever.com/Some%AA%20Special%8Ecases/",
false,
},
&testCase{
"UnnecessaryEscapes",
`http://www.toto.com/%41%42%2E%44/%32%33%52%2D/%5f%7E`,
FlagDecodeUnnecessaryEscapes,
"http://www.toto.com/AB.D/23R-/_~",
false,
},
&testCase{
"RemoveDefaultPort",
"HTTP://www.SRC.ca:80/",
FlagRemoveDefaultPort,
"http://www.SRC.ca/", // Since Go1.1, scheme is automatically lowercased
false,
},
&testCase{
"RemoveDefaultPort2",
"HTTP://www.SRC.ca:80",
FlagRemoveDefaultPort,
"http://www.SRC.ca", // Since Go1.1, scheme is automatically lowercased
false,
},
&testCase{
"RemoveDefaultPort3",
"HTTP://www.SRC.ca:8080",
FlagRemoveDefaultPort,
"http://www.SRC.ca:8080", // Since Go1.1, scheme is automatically lowercased
false,
},
&testCase{
"Safe",
"HTTP://www.SRC.ca:80/to%1ato%8b%ee/OKnow%41%42%43%7e",
FlagsSafe,
"http://www.src.ca/to%1Ato%8B%EE/OKnowABC~",
false,
},
&testCase{
"BothLower",
"HTTP://www.SRC.ca:80/to%1ato%8b%ee/OKnow%41%42%43%7e",
FlagLowercaseHost | FlagLowercaseScheme,
"http://www.src.ca:80/to%1Ato%8B%EE/OKnowABC~",
false,
},
&testCase{
"RemoveTrailingSlash",
"HTTP://www.SRC.ca:80/",
FlagRemoveTrailingSlash,
"http://www.SRC.ca:80", // Since Go1.1, scheme is automatically lowercased
false,
},
&testCase{
"RemoveTrailingSlash2",
"HTTP://www.SRC.ca:80/toto/titi/",
FlagRemoveTrailingSlash,
"http://www.SRC.ca:80/toto/titi", // Since Go1.1, scheme is automatically lowercased
false,
},
&testCase{
"RemoveTrailingSlash3",
"HTTP://www.SRC.ca:80/toto/titi/fin/?a=1",
FlagRemoveTrailingSlash,
"http://www.SRC.ca:80/toto/titi/fin?a=1", // Since Go1.1, scheme is automatically lowercased
false,
},
&testCase{
"AddTrailingSlash",
"HTTP://www.SRC.ca:80",
FlagAddTrailingSlash,
"http://www.SRC.ca:80/", // Since Go1.1, scheme is automatically lowercased
false,
},
&testCase{
"AddTrailingSlash2",
"HTTP://www.SRC.ca:80/toto/titi.html",
FlagAddTrailingSlash,
"http://www.SRC.ca:80/toto/titi.html/", // Since Go1.1, scheme is automatically lowercased
false,
},
&testCase{
"AddTrailingSlash3",
"HTTP://www.SRC.ca:80/toto/titi/fin?a=1",
FlagAddTrailingSlash,
"http://www.SRC.ca:80/toto/titi/fin/?a=1", // Since Go1.1, scheme is automatically lowercased
false,
},
&testCase{
"RemoveDotSegments",
"HTTP://root/a/b/./../../c/",
FlagRemoveDotSegments,
"http://root/c/", // Since Go1.1, scheme is automatically lowercased
false,
},
&testCase{
"RemoveDotSegments2",
"HTTP://root/../a/b/./../c/../d",
FlagRemoveDotSegments,
"http://root/a/d", // Since Go1.1, scheme is automatically lowercased
false,
},
&testCase{
"UsuallySafe",
"HTTP://www.SRC.ca:80/to%1ato%8b%ee/./c/d/../OKnow%41%42%43%7e/?a=b#test",
FlagsUsuallySafeGreedy,
"http://www.src.ca/to%1Ato%8B%EE/c/OKnowABC~?a=b#test",
false,
},
&testCase{
"RemoveDirectoryIndex",
"HTTP://root/a/b/c/default.aspx",
FlagRemoveDirectoryIndex,
"http://root/a/b/c/", // Since Go1.1, scheme is automatically lowercased
false,
},
&testCase{
"RemoveDirectoryIndex2",
"HTTP://root/a/b/c/default#a=b",
FlagRemoveDirectoryIndex,
"http://root/a/b/c/default#a=b", // Since Go1.1, scheme is automatically lowercased
false,
},
&testCase{
"RemoveFragment",
"HTTP://root/a/b/c/default#toto=tata",
FlagRemoveFragment,
"http://root/a/b/c/default", // Since Go1.1, scheme is automatically lowercased
false,
},
&testCase{
"ForceHTTP",
"https://root/a/b/c/default#toto=tata",
FlagForceHTTP,
"http://root/a/b/c/default#toto=tata",
false,
},
&testCase{
"RemoveDuplicateSlashes",
"https://root/a//b///c////default#toto=tata",
FlagRemoveDuplicateSlashes,
"https://root/a/b/c/default#toto=tata",
false,
},
&testCase{
"RemoveDuplicateSlashes2",
"https://root//a//b///c////default#toto=tata",
FlagRemoveDuplicateSlashes,
"https://root/a/b/c/default#toto=tata",
false,
},
&testCase{
"RemoveWWW",
"https://www.root/a/b/c/",
FlagRemoveWWW,
"https://root/a/b/c/",
false,
},
&testCase{
"RemoveWWW2",
"https://WwW.Root/a/b/c/",
FlagRemoveWWW,
"https://Root/a/b/c/",
false,
},
&testCase{
"AddWWW",
"https://Root/a/b/c/",
FlagAddWWW,
"https://www.Root/a/b/c/",
false,
},
&testCase{
"SortQuery",
"http://root/toto/?b=4&a=1&c=3&b=2&a=5",
FlagSortQuery,
"http://root/toto/?a=1&a=5&b=2&b=4&c=3",
false,
},
&testCase{
"RemoveEmptyQuerySeparator",
"http://root/toto/?",
FlagRemoveEmptyQuerySeparator,
"http://root/toto/",
false,
},
&testCase{
"Unsafe",
"HTTPS://www.RooT.com/toto/t%45%1f///a/./b/../c/?z=3&w=2&a=4&w=1#invalid",
FlagsUnsafeGreedy,
"http://root.com/toto/tE%1F/a/c?a=4&w=1&w=2&z=3",
false,
},
&testCase{
"Safe2",
"HTTPS://www.RooT.com/toto/t%45%1f///a/./b/../c/?z=3&w=2&a=4&w=1#invalid",
FlagsSafe,
"https://www.root.com/toto/tE%1F///a/./b/../c/?z=3&w=2&a=4&w=1#invalid",
false,
},
&testCase{
"UsuallySafe2",
"HTTPS://www.RooT.com/toto/t%45%1f///a/./b/../c/?z=3&w=2&a=4&w=1#invalid",
FlagsUsuallySafeGreedy,
"https://www.root.com/toto/tE%1F///a/c?z=3&w=2&a=4&w=1#invalid",
false,
},
&testCase{
"AddTrailingSlashBug",
"http://src.ca/",
FlagsAllNonGreedy,
"http://www.src.ca/",
false,
},
&testCase{
"SourceModified",
"HTTPS://www.RooT.com/toto/t%45%1f///a/./b/../c/?z=3&w=2&a=4&w=1#invalid",
FlagsUnsafeGreedy,
"http://root.com/toto/tE%1F/a/c?a=4&w=1&w=2&z=3",
true,
},
&testCase{
"IPv6-1",
"http://[2001:db8:1f70::999:de8:7648:6e8]/test",
FlagsSafe | FlagRemoveDotSegments,
"http://[2001:db8:1f70::999:de8:7648:6e8]/test",
false,
},
&testCase{
"IPv6-2",
"http://[::ffff:192.168.1.1]/test",
FlagsSafe | FlagRemoveDotSegments,
"http://[::ffff:192.168.1.1]/test",
false,
},
&testCase{
"IPv6-3",
"http://[::ffff:192.168.1.1]:80/test",
FlagsSafe | FlagRemoveDotSegments,
"http://[::ffff:192.168.1.1]/test",
false,
},
&testCase{
"IPv6-4",
"htTps://[::fFff:192.168.1.1]:443/test",
FlagsSafe | FlagRemoveDotSegments,
"https://[::ffff:192.168.1.1]/test",
false,
},
&testCase{
"FTP",
"ftp://user:pass@ftp.foo.net/foo/bar",
FlagsSafe | FlagRemoveDotSegments,
"ftp://user:pass@ftp.foo.net/foo/bar",
false,
},
&testCase{
"Standard-1",
"http://www.foo.com:80/foo",
FlagsSafe | FlagRemoveDotSegments,
"http://www.foo.com/foo",
false,
},
&testCase{
"Standard-2",
"http://www.foo.com:8000/foo",
FlagsSafe | FlagRemoveDotSegments,
"http://www.foo.com:8000/foo",
false,
},
&testCase{
"Standard-3",
"http://www.foo.com/%7ebar",
FlagsSafe | FlagRemoveDotSegments,
"http://www.foo.com/~bar",
false,
},
&testCase{
"Standard-4",
"http://www.foo.com/%7Ebar",
FlagsSafe | FlagRemoveDotSegments,
"http://www.foo.com/~bar",
false,
},
&testCase{
"Standard-5",
"http://USER:pass@www.Example.COM/foo/bar",
FlagsSafe | FlagRemoveDotSegments,
"http://USER:pass@www.example.com/foo/bar",
false,
},
&testCase{
"Standard-6",
"http://test.example/?a=%26&b=1",
FlagsSafe | FlagRemoveDotSegments,
"http://test.example/?a=%26&b=1",
false,
},
&testCase{
"Standard-7",
"http://test.example/%25/?p=%20val%20%25",
FlagsSafe | FlagRemoveDotSegments,
"http://test.example/%25/?p=%20val%20%25",
false,
},
&testCase{
"Standard-8",
"http://test.example/path/with a%20space+/",
FlagsSafe | FlagRemoveDotSegments,
"http://test.example/path/with%20a%20space+/",
false,
},
&testCase{
"Standard-9",
"http://test.example/?",
FlagsSafe | FlagRemoveDotSegments,
"http://test.example/",
false,
},
&testCase{
"Standard-10",
"http://a.COM/path/?b&a",
FlagsSafe | FlagRemoveDotSegments,
"http://a.com/path/?b&a",
false,
},
&testCase{
"StandardCasesAddTrailingSlash",
"http://test.example?",
FlagsSafe | FlagAddTrailingSlash,
"http://test.example/",
false,
},
&testCase{
"OctalIP-1",
"http://0123.011.0.4/",
FlagsSafe | FlagDecodeOctalHost,
"http://0123.011.0.4/",
false,
},
&testCase{
"OctalIP-2",
"http://0102.0146.07.0223/",
FlagsSafe | FlagDecodeOctalHost,
"http://66.102.7.147/",
false,
},
&testCase{
"OctalIP-3",
"http://0102.0146.07.0223.:23/",
FlagsSafe | FlagDecodeOctalHost,
"http://66.102.7.147.:23/",
false,
},
&testCase{
"OctalIP-4",
"http://USER:pass@0102.0146.07.0223../",
FlagsSafe | FlagDecodeOctalHost,
"http://USER:pass@66.102.7.147../",
false,
},
&testCase{
"DWORDIP-1",
"http://123.1113982867/",
FlagsSafe | FlagDecodeDWORDHost,
"http://123.1113982867/",
false,
},
&testCase{
"DWORDIP-2",
"http://1113982867/",
FlagsSafe | FlagDecodeDWORDHost,
"http://66.102.7.147/",
false,
},
&testCase{
"DWORDIP-3",
"http://1113982867.:23/",
FlagsSafe | FlagDecodeDWORDHost,
"http://66.102.7.147.:23/",
false,
},
&testCase{
"DWORDIP-4",
"http://USER:pass@1113982867../",
FlagsSafe | FlagDecodeDWORDHost,
"http://USER:pass@66.102.7.147../",
false,
},
&testCase{
"HexIP-1",
"http://0x123.1113982867/",
FlagsSafe | FlagDecodeHexHost,
"http://0x123.1113982867/",
false,
},
&testCase{
"HexIP-2",
"http://0x42660793/",
FlagsSafe | FlagDecodeHexHost,
"http://66.102.7.147/",
false,
},
&testCase{
"HexIP-3",
"http://0x42660793.:23/",
FlagsSafe | FlagDecodeHexHost,
"http://66.102.7.147.:23/",
false,
},
&testCase{
"HexIP-4",
"http://USER:pass@0x42660793../",
FlagsSafe | FlagDecodeHexHost,
"http://USER:pass@66.102.7.147../",
false,
},
&testCase{
"UnnecessaryHostDots-1",
"http://.www.foo.com../foo/bar.html",
FlagsSafe | FlagRemoveUnnecessaryHostDots,
"http://www.foo.com/foo/bar.html",
false,
},
&testCase{
"UnnecessaryHostDots-2",
"http://www.foo.com./foo/bar.html",
FlagsSafe | FlagRemoveUnnecessaryHostDots,
"http://www.foo.com/foo/bar.html",
false,
},
&testCase{
"UnnecessaryHostDots-3",
"http://www.foo.com.:81/foo",
FlagsSafe | FlagRemoveUnnecessaryHostDots,
"http://www.foo.com:81/foo",
false,
},
&testCase{
"UnnecessaryHostDots-4",
"http://www.example.com./",
FlagsSafe | FlagRemoveUnnecessaryHostDots,
"http://www.example.com/",
false,
},
&testCase{
"EmptyPort-1",
"http://www.thedraymin.co.uk:/main/?p=308",
FlagsSafe | FlagRemoveEmptyPortSeparator,
"http://www.thedraymin.co.uk/main/?p=308",
false,
},
&testCase{
"EmptyPort-2",
"http://www.src.ca:",
FlagsSafe | FlagRemoveEmptyPortSeparator,
"http://www.src.ca",
false,
},
&testCase{
"Slashes-1",
"http://test.example/foo/bar/.",
FlagsSafe | FlagRemoveDotSegments | FlagRemoveDuplicateSlashes,
"http://test.example/foo/bar/",
false,
},
&testCase{
"Slashes-2",
"http://test.example/foo/bar/./",
FlagsSafe | FlagRemoveDotSegments | FlagRemoveDuplicateSlashes,
"http://test.example/foo/bar/",
false,
},
&testCase{
"Slashes-3",
"http://test.example/foo/bar/..",
FlagsSafe | FlagRemoveDotSegments | FlagRemoveDuplicateSlashes,
"http://test.example/foo/",
false,
},
&testCase{
"Slashes-4",
"http://test.example/foo/bar/../",
FlagsSafe | FlagRemoveDotSegments | FlagRemoveDuplicateSlashes,
"http://test.example/foo/",
false,
},
&testCase{
"Slashes-5",
"http://test.example/foo/bar/../baz",
FlagsSafe | FlagRemoveDotSegments | FlagRemoveDuplicateSlashes,
"http://test.example/foo/baz",
false,
},
&testCase{
"Slashes-6",
"http://test.example/foo/bar/../..",
FlagsSafe | FlagRemoveDotSegments | FlagRemoveDuplicateSlashes,
"http://test.example/",
false,
},
&testCase{
"Slashes-7",
"http://test.example/foo/bar/../../",
FlagsSafe | FlagRemoveDotSegments | FlagRemoveDuplicateSlashes,
"http://test.example/",
false,
},
&testCase{
"Slashes-8",
"http://test.example/foo/bar/../../baz",
FlagsSafe | FlagRemoveDotSegments | FlagRemoveDuplicateSlashes,
"http://test.example/baz",
false,
},
&testCase{
"Slashes-9",
"http://test.example/foo/bar/../../../baz",
FlagsSafe | FlagRemoveDotSegments | FlagRemoveDuplicateSlashes,
"http://test.example/baz",
false,
},
&testCase{
"Slashes-10",
"http://test.example/foo/bar/../../../../baz",
FlagsSafe | FlagRemoveDotSegments | FlagRemoveDuplicateSlashes,
"http://test.example/baz",
false,
},
&testCase{
"Slashes-11",
"http://test.example/./foo",
FlagsSafe | FlagRemoveDotSegments | FlagRemoveDuplicateSlashes,
"http://test.example/foo",
false,
},
&testCase{
"Slashes-12",
"http://test.example/../foo",
FlagsSafe | FlagRemoveDotSegments | FlagRemoveDuplicateSlashes,
"http://test.example/foo",
false,
},
&testCase{
"Slashes-13",
"http://test.example/foo.",
FlagsSafe | FlagRemoveDotSegments | FlagRemoveDuplicateSlashes,
"http://test.example/foo.",
false,
},
&testCase{
"Slashes-14",
"http://test.example/.foo",
FlagsSafe | FlagRemoveDotSegments | FlagRemoveDuplicateSlashes,
"http://test.example/.foo",
false,
},
&testCase{
"Slashes-15",
"http://test.example/foo..",
FlagsSafe | FlagRemoveDotSegments | FlagRemoveDuplicateSlashes,
"http://test.example/foo..",
false,
},
&testCase{
"Slashes-16",
"http://test.example/..foo",
FlagsSafe | FlagRemoveDotSegments | FlagRemoveDuplicateSlashes,
"http://test.example/..foo",
false,
},
&testCase{
"Slashes-17",
"http://test.example/./../foo",
FlagsSafe | FlagRemoveDotSegments | FlagRemoveDuplicateSlashes,
"http://test.example/foo",
false,
},
&testCase{
"Slashes-18",
"http://test.example/./foo/.",
FlagsSafe | FlagRemoveDotSegments | FlagRemoveDuplicateSlashes,
"http://test.example/foo/",
false,
},
&testCase{
"Slashes-19",
"http://test.example/foo/./bar",
FlagsSafe | FlagRemoveDotSegments | FlagRemoveDuplicateSlashes,
"http://test.example/foo/bar",
false,
},
&testCase{
"Slashes-20",
"http://test.example/foo/../bar",
FlagsSafe | FlagRemoveDotSegments | FlagRemoveDuplicateSlashes,
"http://test.example/bar",
false,
},
&testCase{
"Slashes-21",
"http://test.example/foo//",
FlagsSafe | FlagRemoveDotSegments | FlagRemoveDuplicateSlashes,
"http://test.example/foo/",
false,
},
&testCase{
"Slashes-22",
"http://test.example/foo///bar//",
FlagsSafe | FlagRemoveDotSegments | FlagRemoveDuplicateSlashes,
"http://test.example/foo/bar/",
false,
},
&testCase{
"Relative",
"foo/bar",
FlagsAllGreedy,
"foo/bar",
false,
},
&testCase{
"Relative-1",
"./../foo",
FlagsSafe | FlagRemoveDotSegments | FlagRemoveDuplicateSlashes,
"foo",
false,
},
&testCase{
"Relative-2",
"./foo/bar/../baz/../bang/..",
FlagsSafe | FlagRemoveDotSegments | FlagRemoveDuplicateSlashes,
"foo/",
false,
},
&testCase{
"Relative-3",
"foo///bar//",
FlagsSafe | FlagRemoveDotSegments | FlagRemoveDuplicateSlashes,
"foo/bar/",
false,
},
&testCase{
"Relative-4",
"www.youtube.com",
FlagsUsuallySafeGreedy,
"www.youtube.com",
false,
},
/*&testCase{
"UrlNorm-5",
"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",
FlagsSafe | FlagRemoveDotSegments,
"http://ja.wikipedia.org/wiki/\xe3\x82\xad\xe3\x83\xa3\xe3\x82\xbf\xe3\x83\x94\xe3\x83\xa9\xe3\x83\xbc\xe3\x82\xb8\xe3\x83\xa3\xe3\x83\x91\xe3\x83\xb3",
false,
},
&testCase{
"UrlNorm-1",
"http://test.example/?a=%e3%82%82%26",
FlagsAllGreedy,
"http://test.example/?a=\xe3\x82\x82%26",
false,
},*/
}
)
func TestRunner(t *testing.T) {
for _, tc := range cases {
runCase(tc, t)
}
}
func runCase(tc *testCase, t *testing.T) {
t.Logf("running %s...", tc.nm)
if tc.parsed {
u, e := url.Parse(tc.src)
if e != nil {
t.Errorf("%s - FAIL : %s", tc.nm, e)
return
} else {
NormalizeURL(u, tc.flgs)
if s := u.String(); s != tc.res {
t.Errorf("%s - FAIL expected '%s', got '%s'", tc.nm, tc.res, s)
}
}
} else {
if s, e := NormalizeURLString(tc.src, tc.flgs); e != nil {
t.Errorf("%s - FAIL : %s", tc.nm, e)
} else if s != tc.res {
t.Errorf("%s - FAIL expected '%s', got '%s'", tc.nm, tc.res, s)
}
}
}
func TestDecodeUnnecessaryEscapesAll(t *testing.T) {
var url = "http://host/"
for i := 0; i < 256; i++ {
url += fmt.Sprintf("%%%02x", i)
}
if s, e := NormalizeURLString(url, FlagDecodeUnnecessaryEscapes); e != nil {
t.Fatalf("Got error %s", e.Error())
} else {
const want = "http://host/%00%01%02%03%04%05%06%07%08%09%0A%0B%0C%0D%0E%0F%10%11%12%13%14%15%16%17%18%19%1A%1B%1C%1D%1E%1F%20!%22%23$%25&'()*+,-./0123456789:;%3C=%3E%3F@ABCDEFGHIJKLMNOPQRSTUVWXYZ[%5C]%5E_%60abcdefghijklmnopqrstuvwxyz%7B%7C%7D~%7F%80%81%82%83%84%85%86%87%88%89%8A%8B%8C%8D%8E%8F%90%91%92%93%94%95%96%97%98%99%9A%9B%9C%9D%9E%9F%A0%A1%A2%A3%A4%A5%A6%A7%A8%A9%AA%AB%AC%AD%AE%AF%B0%B1%B2%B3%B4%B5%B6%B7%B8%B9%BA%BB%BC%BD%BE%BF%C0%C1%C2%C3%C4%C5%C6%C7%C8%C9%CA%CB%CC%CD%CE%CF%D0%D1%D2%D3%D4%D5%D6%D7%D8%D9%DA%DB%DC%DD%DE%DF%E0%E1%E2%E3%E4%E5%E6%E7%E8%E9%EA%EB%EC%ED%EE%EF%F0%F1%F2%F3%F4%F5%F6%F7%F8%F9%FA%FB%FC%FD%FE%FF"
if s != want {
t.Errorf("DecodeUnnecessaryEscapesAll:\nwant\n%s\ngot\n%s", want, s)
}
}
}
func TestEncodeNecessaryEscapesAll(t *testing.T) {
var url = "http://host/"
for i := 0; i < 256; i++ {
if i != 0x25 {
url += string(i)
}
}
if s, e := NormalizeURLString(url, FlagEncodeNecessaryEscapes); e != nil {
t.Fatalf("Got error %s", e.Error())
} else {
const want = "http://host/%00%01%02%03%04%05%06%07%08%09%0A%0B%0C%0D%0E%0F%10%11%12%13%14%15%16%17%18%19%1A%1B%1C%1D%1E%1F%20!%22#$&'()*+,-./0123456789:;%3C=%3E?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[%5C]%5E_%60abcdefghijklmnopqrstuvwxyz%7B%7C%7D~%7F%C2%80%C2%81%C2%82%C2%83%C2%84%C2%85%C2%86%C2%87%C2%88%C2%89%C2%8A%C2%8B%C2%8C%C2%8D%C2%8E%C2%8F%C2%90%C2%91%C2%92%C2%93%C2%94%C2%95%C2%96%C2%97%C2%98%C2%99%C2%9A%C2%9B%C2%9C%C2%9D%C2%9E%C2%9F%C2%A0%C2%A1%C2%A2%C2%A3%C2%A4%C2%A5%C2%A6%C2%A7%C2%A8%C2%A9%C2%AA%C2%AB%C2%AC%C2%AD%C2%AE%C2%AF%C2%B0%C2%B1%C2%B2%C2%B3%C2%B4%C2%B5%C2%B6%C2%B7%C2%B8%C2%B9%C2%BA%C2%BB%C2%BC%C2%BD%C2%BE%C2%BF%C3%80%C3%81%C3%82%C3%83%C3%84%C3%85%C3%86%C3%87%C3%88%C3%89%C3%8A%C3%8B%C3%8C%C3%8D%C3%8E%C3%8F%C3%90%C3%91%C3%92%C3%93%C3%94%C3%95%C3%96%C3%97%C3%98%C3%99%C3%9A%C3%9B%C3%9C%C3%9D%C3%9E%C3%9F%C3%A0%C3%A1%C3%A2%C3%A3%C3%A4%C3%A5%C3%A6%C3%A7%C3%A8%C3%A9%C3%AA%C3%AB%C3%AC%C3%AD%C3%AE%C3%AF%C3%B0%C3%B1%C3%B2%C3%B3%C3%B4%C3%B5%C3%B6%C3%B7%C3%B8%C3%B9%C3%BA%C3%BB%C3%BC%C3%BD%C3%BE%C3%BF"
if s != want {
t.Errorf("EncodeNecessaryEscapesAll:\nwant\n%s\ngot\n%s", want, s)
}
}
}

52
vendor/github.com/PuerkitoBio/purell/urlnorm_test.go generated vendored Normal file
View File

@ -0,0 +1,52 @@
package purell
import (
"testing"
)
// Test cases merged from PR #1
// Originally from https://github.com/jehiah/urlnorm/blob/master/test_urlnorm.py
func assertMap(t *testing.T, cases map[string]string, f NormalizationFlags) {
for bad, good := range cases {
s, e := NormalizeURLString(bad, f)
if e != nil {
t.Errorf("%s normalizing %v to %v", e.Error(), bad, good)
} else {
if s != good {
t.Errorf("source: %v expected: %v got: %v", bad, good, s)
}
}
}
}
// This tests normalization to a unicode representation
// precent escapes for unreserved values are unescaped to their unicode value
// tests normalization to idna domains
// test ip word handling, ipv6 address handling, and trailing domain periods
// in general, this matches google chromes unescaping for things in the address bar.
// spaces are converted to '+' (perhaphs controversial)
// http://code.google.com/p/google-url/ probably is another good reference for this approach
func TestUrlnorm(t *testing.T) {
testcases := map[string]string{
"http://test.example/?a=%e3%82%82%26": "http://test.example/?a=%e3%82%82%26",
//"http://test.example/?a=%e3%82%82%26": "http://test.example/?a=\xe3\x82\x82%26", //should return a unicode character
"http://s.xn--q-bga.DE/": "http://s.xn--q-bga.de/", //should be in idna format
"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://\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",
//"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/\xe3\x82\xad\xe3\x83\xa3\xe3\x82\xbf\xe3\x83\x94\xe3\x83\xa9\xe3\x83\xbc\xe3\x82\xb8\xe3\x83\xa3\xe3\x83\x91\xe3\x83\xb3",
"http://test.example/\xe3\x82\xad": "http://test.example/%E3%82%AD",
//"http://test.example/\xe3\x82\xad": "http://test.example/\xe3\x82\xad",
"http://test.example/?p=%23val#test-%23-val%25": "http://test.example/?p=%23val#test-%23-val%25", //check that %23 (#) is not escaped where it shouldn't be
"http://test.domain/I%C3%B1t%C3%ABrn%C3%A2ti%C3%B4n%EF%BF%BDliz%C3%A6ti%C3%B8n": "http://test.domain/I%C3%B1t%C3%ABrn%C3%A2ti%C3%B4n%EF%BF%BDliz%C3%A6ti%C3%B8n",
//"http://test.domain/I%C3%B1t%C3%ABrn%C3%A2ti%C3%B4n%EF%BF%BDliz%C3%A6ti%C3%B8n": "http://test.domain/I\xc3\xb1t\xc3\xabrn\xc3\xa2ti\xc3\xb4n\xef\xbf\xbdliz\xc3\xa6ti\xc3\xb8n",
}
assertMap(t, testcases, FlagsSafe|FlagRemoveDotSegments)
}

11
vendor/github.com/PuerkitoBio/urlesc/.travis.yml generated vendored Normal file
View File

@ -0,0 +1,11 @@
language: go
go:
- 1.4
- tip
install:
- go build .
script:
- go test -v

27
vendor/github.com/PuerkitoBio/urlesc/LICENSE generated vendored Normal file
View File

@ -0,0 +1,27 @@
Copyright (c) 2012 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

16
vendor/github.com/PuerkitoBio/urlesc/README.md generated vendored Normal file
View File

@ -0,0 +1,16 @@
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.
It contains some parts of the net/url package, modified so as to allow
some reserved characters incorrectly escaped by net/url (see [issue 5684](https://github.com/golang/go/issues/5684)).
## Install
go get github.com/PuerkitoBio/urlesc
## License
Go license (BSD-3-Clause)

180
vendor/github.com/PuerkitoBio/urlesc/urlesc.go generated vendored Normal file
View File

@ -0,0 +1,180 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package urlesc implements query escaping as per RFC 3986.
// It contains some parts of the net/url package, modified so as to allow
// some reserved characters incorrectly escaped by net/url.
// See https://github.com/golang/go/issues/5684
package urlesc
import (
"bytes"
"net/url"
"strings"
)
type encoding int
const (
encodePath encoding = 1 + iota
encodeUserPassword
encodeQueryComponent
encodeFragment
)
// Return true if the specified character should be escaped when
// appearing in a URL string, according to RFC 3986.
func shouldEscape(c byte, mode encoding) bool {
// §2.3 Unreserved characters (alphanum)
if 'A' <= c && c <= 'Z' || 'a' <= c && c <= 'z' || '0' <= c && c <= '9' {
return false
}
switch c {
case '-', '.', '_', '~': // §2.3 Unreserved characters (mark)
return false
// §2.2 Reserved characters (reserved)
case ':', '/', '?', '#', '[', ']', '@', // gen-delims
'!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=': // sub-delims
// Different sections of the URL allow a few of
// the reserved characters to appear unescaped.
switch mode {
case encodePath: // §3.3
// The RFC allows sub-delims and : @.
// '/', '[' and ']' can be used to assign meaning to individual path
// segments. This package only manipulates the path as a whole,
// so we allow those as well. That leaves only ? and # to escape.
return c == '?' || c == '#'
case encodeUserPassword: // §3.2.1
// The RFC allows : and sub-delims in
// userinfo. The parsing of userinfo treats ':' as special so we must escape
// all the gen-delims.
return c == ':' || c == '/' || c == '?' || c == '#' || c == '[' || c == ']' || c == '@'
case encodeQueryComponent: // §3.4
// The RFC allows / and ?.
return c != '/' && c != '?'
case encodeFragment: // §4.1
// The RFC text is silent but the grammar allows
// everything, so escape nothing but #
return c == '#'
}
}
// Everything else must be escaped.
return true
}
// QueryEscape escapes the string so it can be safely placed
// inside a URL query.
func QueryEscape(s string) string {
return escape(s, encodeQueryComponent)
}
func escape(s string, mode encoding) string {
spaceCount, hexCount := 0, 0
for i := 0; i < len(s); i++ {
c := s[i]
if shouldEscape(c, mode) {
if c == ' ' && mode == encodeQueryComponent {
spaceCount++
} else {
hexCount++
}
}
}
if spaceCount == 0 && hexCount == 0 {
return s
}
t := make([]byte, len(s)+2*hexCount)
j := 0
for i := 0; i < len(s); i++ {
switch c := s[i]; {
case c == ' ' && mode == encodeQueryComponent:
t[j] = '+'
j++
case shouldEscape(c, mode):
t[j] = '%'
t[j+1] = "0123456789ABCDEF"[c>>4]
t[j+2] = "0123456789ABCDEF"[c&15]
j += 3
default:
t[j] = s[i]
j++
}
}
return string(t)
}
var uiReplacer = strings.NewReplacer(
"%21", "!",
"%27", "'",
"%28", "(",
"%29", ")",
"%2A", "*",
)
// unescapeUserinfo unescapes some characters that need not to be escaped as per RFC3986.
func unescapeUserinfo(s string) string {
return uiReplacer.Replace(s)
}
// Escape reassembles the URL into a valid URL string.
// The general form of the result is one of:
//
// scheme:opaque
// scheme://userinfo@host/path?query#fragment
//
// If u.Opaque is non-empty, String uses the first form;
// otherwise it uses the second form.
//
// In the second form, the following rules apply:
// - if u.Scheme is empty, scheme: is omitted.
// - if u.User is nil, userinfo@ is omitted.
// - if u.Host is empty, host/ is omitted.
// - if u.Scheme and u.Host are empty and u.User is nil,
// the entire scheme://userinfo@host/ is omitted.
// - if u.Host is non-empty and u.Path begins with a /,
// the form host/path does not add its own /.
// - if u.RawQuery is empty, ?query is omitted.
// - if u.Fragment is empty, #fragment is omitted.
func Escape(u *url.URL) string {
var buf bytes.Buffer
if u.Scheme != "" {
buf.WriteString(u.Scheme)
buf.WriteByte(':')
}
if u.Opaque != "" {
buf.WriteString(u.Opaque)
} else {
if u.Scheme != "" || u.Host != "" || u.User != nil {
buf.WriteString("//")
if ui := u.User; ui != nil {
buf.WriteString(unescapeUserinfo(ui.String()))
buf.WriteByte('@')
}
if h := u.Host; h != "" {
buf.WriteString(h)
}
}
if u.Path != "" && u.Path[0] != '/' && u.Host != "" {
buf.WriteByte('/')
}
buf.WriteString(escape(u.Path, encodePath))
}
if u.RawQuery != "" {
buf.WriteByte('?')
buf.WriteString(u.RawQuery)
}
if u.Fragment != "" {
buf.WriteByte('#')
buf.WriteString(escape(u.Fragment, encodeFragment))
}
return buf.String()
}

641
vendor/github.com/PuerkitoBio/urlesc/urlesc_test.go generated vendored Normal file
View File

@ -0,0 +1,641 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package urlesc
import (
"net/url"
"testing"
)
type URLTest struct {
in string
out *url.URL
roundtrip string // expected result of reserializing the URL; empty means same as "in".
}
var urltests = []URLTest{
// no path
{
"http://www.google.com",
&url.URL{
Scheme: "http",
Host: "www.google.com",
},
"",
},
// path
{
"http://www.google.com/",
&url.URL{
Scheme: "http",
Host: "www.google.com",
Path: "/",
},
"",
},
// path with hex escaping
{
"http://www.google.com/file%20one%26two",
&url.URL{
Scheme: "http",
Host: "www.google.com",
Path: "/file one&two",
},
"http://www.google.com/file%20one&two",
},
// user
{
"ftp://webmaster@www.google.com/",
&url.URL{
Scheme: "ftp",
User: url.User("webmaster"),
Host: "www.google.com",
Path: "/",
},
"",
},
// escape sequence in username
{
"ftp://john%20doe@www.google.com/",
&url.URL{
Scheme: "ftp",
User: url.User("john doe"),
Host: "www.google.com",
Path: "/",
},
"ftp://john%20doe@www.google.com/",
},
// query
{
"http://www.google.com/?q=go+language",
&url.URL{
Scheme: "http",
Host: "www.google.com",
Path: "/",
RawQuery: "q=go+language",
},
"",
},
// query with hex escaping: NOT parsed
{
"http://www.google.com/?q=go%20language",
&url.URL{
Scheme: "http",
Host: "www.google.com",
Path: "/",
RawQuery: "q=go%20language",
},
"",
},
// %20 outside query
{
"http://www.google.com/a%20b?q=c+d",
&url.URL{
Scheme: "http",
Host: "www.google.com",
Path: "/a b",
RawQuery: "q=c+d",
},
"",
},
// path without leading /, so no parsing
{
"http:www.google.com/?q=go+language",
&url.URL{
Scheme: "http",
Opaque: "www.google.com/",
RawQuery: "q=go+language",
},
"http:www.google.com/?q=go+language",
},
// path without leading /, so no parsing
{
"http:%2f%2fwww.google.com/?q=go+language",
&url.URL{
Scheme: "http",
Opaque: "%2f%2fwww.google.com/",
RawQuery: "q=go+language",
},
"http:%2f%2fwww.google.com/?q=go+language",
},
// non-authority with path
{
"mailto:/webmaster@golang.org",
&url.URL{
Scheme: "mailto",
Path: "/webmaster@golang.org",
},
"mailto:///webmaster@golang.org", // unfortunate compromise
},
// non-authority
{
"mailto:webmaster@golang.org",
&url.URL{
Scheme: "mailto",
Opaque: "webmaster@golang.org",
},
"",
},
// unescaped :// in query should not create a scheme
{
"/foo?query=http://bad",
&url.URL{
Path: "/foo",
RawQuery: "query=http://bad",
},
"",
},
// leading // without scheme should create an authority
{
"//foo",
&url.URL{
Host: "foo",
},
"",
},
// leading // without scheme, with userinfo, path, and query
{
"//user@foo/path?a=b",
&url.URL{
User: url.User("user"),
Host: "foo",
Path: "/path",
RawQuery: "a=b",
},
"",
},
// Three leading slashes isn't an authority, but doesn't return an error.
// (We can't return an error, as this code is also used via
// ServeHTTP -> ReadRequest -> Parse, which is arguably a
// different URL parsing context, but currently shares the
// same codepath)
{
"///threeslashes",
&url.URL{
Path: "///threeslashes",
},
"",
},
{
"http://user:password@google.com",
&url.URL{
Scheme: "http",
User: url.UserPassword("user", "password"),
Host: "google.com",
},
"http://user:password@google.com",
},
// unescaped @ in username should not confuse host
{
"http://j@ne:password@google.com",
&url.URL{
Scheme: "http",
User: url.UserPassword("j@ne", "password"),
Host: "google.com",
},
"http://j%40ne:password@google.com",
},
// unescaped @ in password should not confuse host
{
"http://jane:p@ssword@google.com",
&url.URL{
Scheme: "http",
User: url.UserPassword("jane", "p@ssword"),
Host: "google.com",
},
"http://jane:p%40ssword@google.com",
},
{
"http://j@ne:password@google.com/p@th?q=@go",
&url.URL{
Scheme: "http",
User: url.UserPassword("j@ne", "password"),
Host: "google.com",
Path: "/p@th",
RawQuery: "q=@go",
},
"http://j%40ne:password@google.com/p@th?q=@go",
},
{
"http://www.google.com/?q=go+language#foo",
&url.URL{
Scheme: "http",
Host: "www.google.com",
Path: "/",
RawQuery: "q=go+language",
Fragment: "foo",
},
"",
},
{
"http://www.google.com/?q=go+language#foo%26bar",
&url.URL{
Scheme: "http",
Host: "www.google.com",
Path: "/",
RawQuery: "q=go+language",
Fragment: "foo&bar",
},
"http://www.google.com/?q=go+language#foo&bar",
},
{
"file:///home/adg/rabbits",
&url.URL{
Scheme: "file",
Host: "",
Path: "/home/adg/rabbits",
},
"file:///home/adg/rabbits",
},
// "Windows" paths are no exception to the rule.
// See golang.org/issue/6027, especially comment #9.
{
"file:///C:/FooBar/Baz.txt",
&url.URL{
Scheme: "file",
Host: "",
Path: "/C:/FooBar/Baz.txt",
},
"file:///C:/FooBar/Baz.txt",
},
// case-insensitive scheme
{
"MaIlTo:webmaster@golang.org",
&url.URL{
Scheme: "mailto",
Opaque: "webmaster@golang.org",
},
"mailto:webmaster@golang.org",
},
// Relative path
{
"a/b/c",
&url.URL{
Path: "a/b/c",
},
"a/b/c",
},
// escaped '?' in username and password
{
"http://%3Fam:pa%3Fsword@google.com",
&url.URL{
Scheme: "http",
User: url.UserPassword("?am", "pa?sword"),
Host: "google.com",
},
"",
},
// escaped '?' and '#' in path
{
"http://example.com/%3F%23",
&url.URL{
Scheme: "http",
Host: "example.com",
Path: "?#",
},
"",
},
// unescaped [ ] ! ' ( ) * in path
{
"http://example.com/[]!'()*",
&url.URL{
Scheme: "http",
Host: "example.com",
Path: "[]!'()*",
},
"http://example.com/[]!'()*",
},
// escaped : / ? # [ ] @ in username and password
{
"http://%3A%2F%3F:%23%5B%5D%40@example.com",
&url.URL{
Scheme: "http",
User: url.UserPassword(":/?", "#[]@"),
Host: "example.com",
},
"",
},
// unescaped ! $ & ' ( ) * + , ; = in username and password
{
"http://!$&'():*+,;=@example.com",
&url.URL{
Scheme: "http",
User: url.UserPassword("!$&'()", "*+,;="),
Host: "example.com",
},
"",
},
// unescaped = : / . ? = in query component
{
"http://example.com/?q=http://google.com/?q=",
&url.URL{
Scheme: "http",
Host: "example.com",
Path: "/",
RawQuery: "q=http://google.com/?q=",
},
"",
},
// unescaped : / ? [ ] @ ! $ & ' ( ) * + , ; = in fragment
{
"http://example.com/#:/?%23[]@!$&'()*+,;=",
&url.URL{
Scheme: "http",
Host: "example.com",
Path: "/",
Fragment: ":/?#[]@!$&'()*+,;=",
},
"",
},
}
func DoTestString(t *testing.T, parse func(string) (*url.URL, error), name string, tests []URLTest) {
for _, tt := range tests {
u, err := parse(tt.in)
if err != nil {
t.Errorf("%s(%q) returned error %s", name, tt.in, err)
continue
}
expected := tt.in
if len(tt.roundtrip) > 0 {
expected = tt.roundtrip
}
s := Escape(u)
if s != expected {
t.Errorf("Escape(%s(%q)) == %q (expected %q)", name, tt.in, s, expected)
}
}
}
func TestURLString(t *testing.T) {
DoTestString(t, url.Parse, "Parse", urltests)
// no leading slash on path should prepend
// slash on String() call
noslash := URLTest{
"http://www.google.com/search",
&url.URL{
Scheme: "http",
Host: "www.google.com",
Path: "search",
},
"",
}
s := Escape(noslash.out)
if s != noslash.in {
t.Errorf("Expected %s; go %s", noslash.in, s)
}
}
type EscapeTest struct {
in string
out string
err error
}
var escapeTests = []EscapeTest{
{
"",
"",
nil,
},
{
"abc",
"abc",
nil,
},
{
"one two",
"one+two",
nil,
},
{
"10%",
"10%25",
nil,
},
{
" ?&=#+%!<>#\"{}|\\^[]`☺\t:/@$'()*,;",
"+?%26%3D%23%2B%25%21%3C%3E%23%22%7B%7D%7C%5C%5E%5B%5D%60%E2%98%BA%09%3A/%40%24%27%28%29%2A%2C%3B",
nil,
},
}
func TestEscape(t *testing.T) {
for _, tt := range escapeTests {
actual := QueryEscape(tt.in)
if tt.out != actual {
t.Errorf("QueryEscape(%q) = %q, want %q", tt.in, actual, tt.out)
}
// for bonus points, verify that escape:unescape is an identity.
roundtrip, err := url.QueryUnescape(actual)
if roundtrip != tt.in || err != nil {
t.Errorf("QueryUnescape(%q) = %q, %s; want %q, %s", actual, roundtrip, err, tt.in, "[no error]")
}
}
}
var resolveReferenceTests = []struct {
base, rel, expected string
}{
// Absolute URL references
{"http://foo.com?a=b", "https://bar.com/", "https://bar.com/"},
{"http://foo.com/", "https://bar.com/?a=b", "https://bar.com/?a=b"},
{"http://foo.com/bar", "mailto:foo@example.com", "mailto:foo@example.com"},
// Path-absolute references
{"http://foo.com/bar", "/baz", "http://foo.com/baz"},
{"http://foo.com/bar?a=b#f", "/baz", "http://foo.com/baz"},
{"http://foo.com/bar?a=b", "/baz?c=d", "http://foo.com/baz?c=d"},
// Scheme-relative
{"https://foo.com/bar?a=b", "//bar.com/quux", "https://bar.com/quux"},
// Path-relative references:
// ... current directory
{"http://foo.com", ".", "http://foo.com/"},
{"http://foo.com/bar", ".", "http://foo.com/"},
{"http://foo.com/bar/", ".", "http://foo.com/bar/"},
// ... going down
{"http://foo.com", "bar", "http://foo.com/bar"},
{"http://foo.com/", "bar", "http://foo.com/bar"},
{"http://foo.com/bar/baz", "quux", "http://foo.com/bar/quux"},
// ... going up
{"http://foo.com/bar/baz", "../quux", "http://foo.com/quux"},
{"http://foo.com/bar/baz", "../../../../../quux", "http://foo.com/quux"},
{"http://foo.com/bar", "..", "http://foo.com/"},
{"http://foo.com/bar/baz", "./..", "http://foo.com/"},
// ".." in the middle (issue 3560)
{"http://foo.com/bar/baz", "quux/dotdot/../tail", "http://foo.com/bar/quux/tail"},
{"http://foo.com/bar/baz", "quux/./dotdot/../tail", "http://foo.com/bar/quux/tail"},
{"http://foo.com/bar/baz", "quux/./dotdot/.././tail", "http://foo.com/bar/quux/tail"},
{"http://foo.com/bar/baz", "quux/./dotdot/./../tail", "http://foo.com/bar/quux/tail"},
{"http://foo.com/bar/baz", "quux/./dotdot/dotdot/././../../tail", "http://foo.com/bar/quux/tail"},
{"http://foo.com/bar/baz", "quux/./dotdot/dotdot/./.././../tail", "http://foo.com/bar/quux/tail"},
{"http://foo.com/bar/baz", "quux/./dotdot/dotdot/dotdot/./../../.././././tail", "http://foo.com/bar/quux/tail"},
{"http://foo.com/bar/baz", "quux/./dotdot/../dotdot/../dot/./tail/..", "http://foo.com/bar/quux/dot/"},
// Remove any dot-segments prior to forming the target URI.
// http://tools.ietf.org/html/rfc3986#section-5.2.4
{"http://foo.com/dot/./dotdot/../foo/bar", "../baz", "http://foo.com/dot/baz"},
// Triple dot isn't special
{"http://foo.com/bar", "...", "http://foo.com/..."},
// Fragment
{"http://foo.com/bar", ".#frag", "http://foo.com/#frag"},
// RFC 3986: Normal Examples
// http://tools.ietf.org/html/rfc3986#section-5.4.1
{"http://a/b/c/d;p?q", "g:h", "g:h"},
{"http://a/b/c/d;p?q", "g", "http://a/b/c/g"},
{"http://a/b/c/d;p?q", "./g", "http://a/b/c/g"},
{"http://a/b/c/d;p?q", "g/", "http://a/b/c/g/"},
{"http://a/b/c/d;p?q", "/g", "http://a/g"},
{"http://a/b/c/d;p?q", "//g", "http://g"},
{"http://a/b/c/d;p?q", "?y", "http://a/b/c/d;p?y"},
{"http://a/b/c/d;p?q", "g?y", "http://a/b/c/g?y"},
{"http://a/b/c/d;p?q", "#s", "http://a/b/c/d;p?q#s"},
{"http://a/b/c/d;p?q", "g#s", "http://a/b/c/g#s"},
{"http://a/b/c/d;p?q", "g?y#s", "http://a/b/c/g?y#s"},
{"http://a/b/c/d;p?q", ";x", "http://a/b/c/;x"},
{"http://a/b/c/d;p?q", "g;x", "http://a/b/c/g;x"},
{"http://a/b/c/d;p?q", "g;x?y#s", "http://a/b/c/g;x?y#s"},
{"http://a/b/c/d;p?q", "", "http://a/b/c/d;p?q"},
{"http://a/b/c/d;p?q", ".", "http://a/b/c/"},
{"http://a/b/c/d;p?q", "./", "http://a/b/c/"},
{"http://a/b/c/d;p?q", "..", "http://a/b/"},
{"http://a/b/c/d;p?q", "../", "http://a/b/"},
{"http://a/b/c/d;p?q", "../g", "http://a/b/g"},
{"http://a/b/c/d;p?q", "../..", "http://a/"},
{"http://a/b/c/d;p?q", "../../", "http://a/"},
{"http://a/b/c/d;p?q", "../../g", "http://a/g"},
// RFC 3986: Abnormal Examples
// http://tools.ietf.org/html/rfc3986#section-5.4.2
{"http://a/b/c/d;p?q", "../../../g", "http://a/g"},
{"http://a/b/c/d;p?q", "../../../../g", "http://a/g"},
{"http://a/b/c/d;p?q", "/./g", "http://a/g"},
{"http://a/b/c/d;p?q", "/../g", "http://a/g"},
{"http://a/b/c/d;p?q", "g.", "http://a/b/c/g."},
{"http://a/b/c/d;p?q", ".g", "http://a/b/c/.g"},
{"http://a/b/c/d;p?q", "g..", "http://a/b/c/g.."},
{"http://a/b/c/d;p?q", "..g", "http://a/b/c/..g"},
{"http://a/b/c/d;p?q", "./../g", "http://a/b/g"},
{"http://a/b/c/d;p?q", "./g/.", "http://a/b/c/g/"},
{"http://a/b/c/d;p?q", "g/./h", "http://a/b/c/g/h"},
{"http://a/b/c/d;p?q", "g/../h", "http://a/b/c/h"},
{"http://a/b/c/d;p?q", "g;x=1/./y", "http://a/b/c/g;x=1/y"},
{"http://a/b/c/d;p?q", "g;x=1/../y", "http://a/b/c/y"},
{"http://a/b/c/d;p?q", "g?y/./x", "http://a/b/c/g?y/./x"},
{"http://a/b/c/d;p?q", "g?y/../x", "http://a/b/c/g?y/../x"},
{"http://a/b/c/d;p?q", "g#s/./x", "http://a/b/c/g#s/./x"},
{"http://a/b/c/d;p?q", "g#s/../x", "http://a/b/c/g#s/../x"},
// Extras.
{"https://a/b/c/d;p?q", "//g?q", "https://g?q"},
{"https://a/b/c/d;p?q", "//g#s", "https://g#s"},
{"https://a/b/c/d;p?q", "//g/d/e/f?y#s", "https://g/d/e/f?y#s"},
{"https://a/b/c/d;p#s", "?y", "https://a/b/c/d;p?y"},
{"https://a/b/c/d;p?q#s", "?y", "https://a/b/c/d;p?y"},
}
func TestResolveReference(t *testing.T) {
mustParse := func(url_ string) *url.URL {
u, err := url.Parse(url_)
if err != nil {
t.Fatalf("Expected URL to parse: %q, got error: %v", url_, err)
}
return u
}
opaque := &url.URL{Scheme: "scheme", Opaque: "opaque"}
for _, test := range resolveReferenceTests {
base := mustParse(test.base)
rel := mustParse(test.rel)
url := base.ResolveReference(rel)
if Escape(url) != test.expected {
t.Errorf("URL(%q).ResolveReference(%q) == %q, got %q", test.base, test.rel, test.expected, Escape(url))
}
// Ensure that new instances are returned.
if base == url {
t.Errorf("Expected URL.ResolveReference to return new URL instance.")
}
// Test the convenience wrapper too.
url, err := base.Parse(test.rel)
if err != nil {
t.Errorf("URL(%q).Parse(%q) failed: %v", test.base, test.rel, err)
} else if Escape(url) != test.expected {
t.Errorf("URL(%q).Parse(%q) == %q, got %q", test.base, test.rel, test.expected, Escape(url))
} else if base == url {
// Ensure that new instances are returned for the wrapper too.
t.Errorf("Expected URL.Parse to return new URL instance.")
}
// Ensure Opaque resets the URL.
url = base.ResolveReference(opaque)
if *url != *opaque {
t.Errorf("ResolveReference failed to resolve opaque URL: want %#v, got %#v", url, opaque)
}
// Test the convenience wrapper with an opaque URL too.
url, err = base.Parse("scheme:opaque")
if err != nil {
t.Errorf(`URL(%q).Parse("scheme:opaque") failed: %v`, test.base, err)
} else if *url != *opaque {
t.Errorf("Parse failed to resolve opaque URL: want %#v, got %#v", url, opaque)
} else if base == url {
// Ensure that new instances are returned, again.
t.Errorf("Expected URL.Parse to return new URL instance.")
}
}
}
type shouldEscapeTest struct {
in byte
mode encoding
escape bool
}
var shouldEscapeTests = []shouldEscapeTest{
// Unreserved characters (§2.3)
{'a', encodePath, false},
{'a', encodeUserPassword, false},
{'a', encodeQueryComponent, false},
{'a', encodeFragment, false},
{'z', encodePath, false},
{'A', encodePath, false},
{'Z', encodePath, false},
{'0', encodePath, false},
{'9', encodePath, false},
{'-', encodePath, false},
{'-', encodeUserPassword, false},
{'-', encodeQueryComponent, false},
{'-', encodeFragment, false},
{'.', encodePath, false},
{'_', encodePath, false},
{'~', encodePath, false},
// User information (§3.2.1)
{':', encodeUserPassword, true},
{'/', encodeUserPassword, true},
{'?', encodeUserPassword, true},
{'@', encodeUserPassword, true},
{'$', encodeUserPassword, false},
{'&', encodeUserPassword, false},
{'+', encodeUserPassword, false},
{',', encodeUserPassword, false},
{';', encodeUserPassword, false},
{'=', encodeUserPassword, false},
}
func TestShouldEscape(t *testing.T) {
for _, tt := range shouldEscapeTests {
if shouldEscape(tt.in, tt.mode) != tt.escape {
t.Errorf("shouldEscape(%q, %v) returned %v; expected %v", tt.in, tt.mode, !tt.escape, tt.escape)
}
}
}

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") }

185
vendor/github.com/emicklei/go-restful/doc.go generated vendored Normal file
View File

@ -0,0 +1,185 @@
/*
Package restful , a lean package for creating REST-style WebServices without magic.
WebServices and Routes
A WebService has a collection of Route objects that dispatch incoming Http Requests to a function calls.
Typically, a WebService has a root path (e.g. /users) and defines common MIME types for its routes.
WebServices must be added to a container (see below) in order to handler Http requests from a server.
A Route is defined by a HTTP method, an URL path and (optionally) the MIME types it consumes (Content-Type) and produces (Accept).
This package has the logic to find the best matching Route and if found, call its Function.
ws := new(restful.WebService)
ws.
Path("/users").
Consumes(restful.MIME_JSON, restful.MIME_XML).
Produces(restful.MIME_JSON, restful.MIME_XML)
ws.Route(ws.GET("/{user-id}").To(u.findUser)) // u is a UserResource
...
// GET http://localhost:8080/users/1
func (u UserResource) findUser(request *restful.Request, response *restful.Response) {
id := request.PathParameter("user-id")
...
}
The (*Request, *Response) arguments provide functions for reading information from the request and writing information back to the response.
See the example https://github.com/emicklei/go-restful/blob/master/examples/restful-user-resource.go with a full implementation.
Regular expression matching Routes
A Route parameter can be specified using the format "uri/{var[:regexp]}" or the special version "uri/{var:*}" for matching the tail of the path.
For example, /persons/{name:[A-Z][A-Z]} can be used to restrict values for the parameter "name" to only contain capital alphabetic characters.
Regular expressions must use the standard Go syntax as described in the regexp package. (https://code.google.com/p/re2/wiki/Syntax)
This feature requires the use of a CurlyRouter.
Containers
A Container holds a collection of WebServices, Filters and a http.ServeMux for multiplexing http requests.
Using the statements "restful.Add(...) and restful.Filter(...)" will register WebServices and Filters to the Default Container.
The Default container of go-restful uses the http.DefaultServeMux.
You can create your own Container and create a new http.Server for that particular container.
container := restful.NewContainer()
server := &http.Server{Addr: ":8081", Handler: container}
Filters
A filter dynamically intercepts requests and responses to transform or use the information contained in the requests or responses.
You can use filters to perform generic logging, measurement, authentication, redirect, set response headers etc.
In the restful package there are three hooks into the request,response flow where filters can be added.
Each filter must define a FilterFunction:
func (req *restful.Request, resp *restful.Response, chain *restful.FilterChain)
Use the following statement to pass the request,response pair to the next filter or RouteFunction
chain.ProcessFilter(req, resp)
Container Filters
These are processed before any registered WebService.
// install a (global) filter for the default container (processed before any webservice)
restful.Filter(globalLogging)
WebService Filters
These are processed before any Route of a WebService.
// install a webservice filter (processed before any route)
ws.Filter(webserviceLogging).Filter(measureTime)
Route Filters
These are processed before calling the function associated with the Route.
// install 2 chained route filters (processed before calling findUser)
ws.Route(ws.GET("/{user-id}").Filter(routeLogging).Filter(NewCountFilter().routeCounter).To(findUser))
See the example https://github.com/emicklei/go-restful/blob/master/examples/restful-filters.go with full implementations.
Response Encoding
Two encodings are supported: gzip and deflate. To enable this for all responses:
restful.DefaultContainer.EnableContentEncoding(true)
If a Http request includes the Accept-Encoding header then the response content will be compressed using the specified encoding.
Alternatively, you can create a Filter that performs the encoding and install it per WebService or Route.
See the example https://github.com/emicklei/go-restful/blob/master/examples/restful-encoding-filter.go
OPTIONS support
By installing a pre-defined container filter, your Webservice(s) can respond to the OPTIONS Http request.
Filter(OPTIONSFilter())
CORS
By installing the filter of a CrossOriginResourceSharing (CORS), your WebService(s) can handle CORS requests.
cors := CrossOriginResourceSharing{ExposeHeaders: []string{"X-My-Header"}, CookiesAllowed: false, Container: DefaultContainer}
Filter(cors.Filter)
Error Handling
Unexpected things happen. If a request cannot be processed because of a failure, your service needs to tell via the response what happened and why.
For this reason HTTP status codes exist and it is important to use the correct code in every exceptional situation.
400: Bad Request
If path or query parameters are not valid (content or type) then use http.StatusBadRequest.
404: Not Found
Despite a valid URI, the resource requested may not be available
500: Internal Server Error
If the application logic could not process the request (or write the response) then use http.StatusInternalServerError.
405: Method Not Allowed
The request has a valid URL but the method (GET,PUT,POST,...) is not allowed.
406: Not Acceptable
The request does not have or has an unknown Accept Header set for this operation.
415: Unsupported Media Type
The request does not have or has an unknown Content-Type Header set for this operation.
ServiceError
In addition to setting the correct (error) Http status code, you can choose to write a ServiceError message on the response.
Performance options
This package has several options that affect the performance of your service. It is important to understand them and how you can change it.
restful.DefaultContainer.DoNotRecover(false)
DoNotRecover controls whether panics will be caught to return HTTP 500.
If set to false, the container will recover from panics.
Default value is true
restful.SetCompressorProvider(NewBoundedCachedCompressors(20, 20))
If content encoding is enabled then the default strategy for getting new gzip/zlib writers and readers is to use a sync.Pool.
Because writers are expensive structures, performance is even more improved when using a preloaded cache. You can also inject your own implementation.
Trouble shooting
This package has the means to produce detail logging of the complete Http request matching process and filter invocation.
Enabling this feature requires you to set an implementation of restful.StdLogger (e.g. log.Logger) instance such as:
restful.TraceLogger(log.New(os.Stdout, "[restful] ", log.LstdFlags|log.Lshortfile))
Logging
The restful.SetLogger() method allows you to override the logger used by the package. By default restful
uses the standard library `log` package and logs to stdout. Different logging packages are supported as
long as they conform to `StdLogger` interface defined in the `log` sub-package, writing an adapter for your
preferred package is simple.
Resources
[project]: https://github.com/emicklei/go-restful
[examples]: https://github.com/emicklei/go-restful/blob/master/examples
[design]: http://ernestmicklei.com/2012/11/11/go-restful-api-design/
[showcases]: https://github.com/emicklei/mora, https://github.com/emicklei/landskape
(c) 2012-2015, http://ernestmicklei.com. MIT License
*/
package restful

View File

@ -0,0 +1,41 @@
package restful
import "net/http"
func ExampleOPTIONSFilter() {
// Install the OPTIONS filter on the default Container
Filter(OPTIONSFilter())
}
func ExampleContainer_OPTIONSFilter() {
// Install the OPTIONS filter on a Container
myContainer := new(Container)
myContainer.Filter(myContainer.OPTIONSFilter)
}
func ExampleContainer() {
// The Default container of go-restful uses the http.DefaultServeMux.
// You can create your own Container using restful.NewContainer() and create a new http.Server for that particular container
ws := new(WebService)
wsContainer := NewContainer()
wsContainer.Add(ws)
server := &http.Server{Addr: ":8080", Handler: wsContainer}
server.ListenAndServe()
}
func ExampleCrossOriginResourceSharing() {
// To install this filter on the Default Container use:
cors := CrossOriginResourceSharing{ExposeHeaders: []string{"X-My-Header"}, CookiesAllowed: false, Container: DefaultContainer}
Filter(cors.Filter)
}
func ExampleServiceError() {
resp := new(Response)
resp.WriteEntity(NewError(http.StatusBadRequest, "Non-integer {id} path parameter"))
}
func ExampleBoundedCachedCompressors() {
// Register a compressor provider (gzip/deflate read/write) that uses
// a bounded cache with a maximum of 20 writers and 20 readers.
SetCompressorProvider(NewBoundedCachedCompressors(20, 20))
}

View File

@ -0,0 +1,163 @@
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 (
"encoding/json"
"encoding/xml"
"strings"
"sync"
)
// EntityReaderWriter can read and write values using an encoding such as JSON,XML.
type EntityReaderWriter interface {
// Read a serialized version of the value from the request.
// The Request may have a decompressing reader. Depends on Content-Encoding.
Read(req *Request, v interface{}) error
// Write a serialized version of the value on the response.
// The Response may have a compressing writer. Depends on Accept-Encoding.
// status should be a valid Http Status code
Write(resp *Response, status int, v interface{}) error
}
// entityAccessRegistry is a singleton
var entityAccessRegistry = &entityReaderWriters{
protection: new(sync.RWMutex),
accessors: map[string]EntityReaderWriter{},
}
// entityReaderWriters associates MIME to an EntityReaderWriter
type entityReaderWriters struct {
protection *sync.RWMutex
accessors map[string]EntityReaderWriter
}
func init() {
RegisterEntityAccessor(MIME_JSON, NewEntityAccessorJSON(MIME_JSON))
RegisterEntityAccessor(MIME_XML, NewEntityAccessorXML(MIME_XML))
}
// RegisterEntityAccessor add/overrides the ReaderWriter for encoding content with this MIME type.
func RegisterEntityAccessor(mime string, erw EntityReaderWriter) {
entityAccessRegistry.protection.Lock()
defer entityAccessRegistry.protection.Unlock()
entityAccessRegistry.accessors[mime] = erw
}
// NewEntityAccessorJSON returns a new EntityReaderWriter for accessing JSON content.
// This package is already initialized with such an accessor using the MIME_JSON contentType.
func NewEntityAccessorJSON(contentType string) EntityReaderWriter {
return entityJSONAccess{ContentType: contentType}
}
// NewEntityAccessorXML returns a new EntityReaderWriter for accessing XML content.
// This package is already initialized with such an accessor using the MIME_XML contentType.
func NewEntityAccessorXML(contentType string) EntityReaderWriter {
return entityXMLAccess{ContentType: contentType}
}
// accessorAt returns the registered ReaderWriter for this MIME type.
func (r *entityReaderWriters) accessorAt(mime string) (EntityReaderWriter, bool) {
r.protection.RLock()
defer r.protection.RUnlock()
er, ok := r.accessors[mime]
if !ok {
// retry with reverse lookup
// more expensive but we are in an exceptional situation anyway
for k, v := range r.accessors {
if strings.Contains(mime, k) {
return v, true
}
}
}
return er, ok
}
// entityXMLAccess is a EntityReaderWriter for XML encoding
type entityXMLAccess struct {
// This is used for setting the Content-Type header when writing
ContentType string
}
// Read unmarshalls the value from XML
func (e entityXMLAccess) Read(req *Request, v interface{}) error {
return xml.NewDecoder(req.Request.Body).Decode(v)
}
// Write marshalls the value to JSON and set the Content-Type Header.
func (e entityXMLAccess) Write(resp *Response, status int, v interface{}) error {
return writeXML(resp, status, e.ContentType, v)
}
// writeXML marshalls the value to JSON and set the Content-Type Header.
func writeXML(resp *Response, status int, contentType string, v interface{}) error {
if v == nil {
resp.WriteHeader(status)
// do not write a nil representation
return nil
}
if resp.prettyPrint {
// pretty output must be created and written explicitly
output, err := xml.MarshalIndent(v, " ", " ")
if err != nil {
return err
}
resp.Header().Set(HEADER_ContentType, contentType)
resp.WriteHeader(status)
_, err = resp.Write([]byte(xml.Header))
if err != nil {
return err
}
_, err = resp.Write(output)
return err
}
// not-so-pretty
resp.Header().Set(HEADER_ContentType, contentType)
resp.WriteHeader(status)
return xml.NewEncoder(resp).Encode(v)
}
// entityJSONAccess is a EntityReaderWriter for JSON encoding
type entityJSONAccess struct {
// This is used for setting the Content-Type header when writing
ContentType string
}
// Read unmarshalls the value from JSON
func (e entityJSONAccess) Read(req *Request, v interface{}) error {
decoder := json.NewDecoder(req.Request.Body)
decoder.UseNumber()
return decoder.Decode(v)
}
// Write marshalls the value to JSON and set the Content-Type Header.
func (e entityJSONAccess) Write(resp *Response, status int, v interface{}) error {
return writeJSON(resp, status, e.ContentType, v)
}
// write marshalls the value to JSON and set the Content-Type Header.
func writeJSON(resp *Response, status int, contentType string, v interface{}) error {
if v == nil {
resp.WriteHeader(status)
// do not write a nil representation
return nil
}
if resp.prettyPrint {
// pretty output must be created and written explicitly
output, err := json.MarshalIndent(v, " ", " ")
if err != nil {
return err
}
resp.Header().Set(HEADER_ContentType, contentType)
resp.WriteHeader(status)
_, err = resp.Write(output)
return err
}
// not-so-pretty
resp.Header().Set(HEADER_ContentType, contentType)
resp.WriteHeader(status)
return json.NewEncoder(resp).Encode(v)
}

View File

@ -0,0 +1,69 @@
package restful
import (
"bytes"
"fmt"
"io"
"net/http"
"net/http/httptest"
"reflect"
"testing"
)
type keyvalue struct {
readCalled bool
writeCalled bool
}
func (kv *keyvalue) Read(req *Request, v interface{}) error {
//t := reflect.TypeOf(v)
//rv := reflect.ValueOf(v)
kv.readCalled = true
return nil
}
func (kv *keyvalue) Write(resp *Response, status int, v interface{}) error {
t := reflect.TypeOf(v)
rv := reflect.ValueOf(v)
for ix := 0; ix < t.NumField(); ix++ {
sf := t.Field(ix)
io.WriteString(resp, sf.Name)
io.WriteString(resp, "=")
io.WriteString(resp, fmt.Sprintf("%v\n", rv.Field(ix).Interface()))
}
kv.writeCalled = true
return nil
}
// go test -v -test.run TestKeyValueEncoding ...restful
func TestKeyValueEncoding(t *testing.T) {
type Book struct {
Title string
Author string
PublishedYear int
}
kv := new(keyvalue)
RegisterEntityAccessor("application/kv", kv)
b := Book{"Singing for Dummies", "john doe", 2015}
// Write
httpWriter := httptest.NewRecorder()
// Accept Produces
resp := Response{httpWriter, "application/kv,*/*;q=0.8", []string{"application/kv"}, 0, 0, true, nil}
resp.WriteEntity(b)
t.Log(string(httpWriter.Body.Bytes()))
if !kv.writeCalled {
t.Error("Write never called")
}
// Read
bodyReader := bytes.NewReader(httpWriter.Body.Bytes())
httpRequest, _ := http.NewRequest("GET", "/test", bodyReader)
httpRequest.Header.Set("Content-Type", "application/kv; charset=UTF-8")
request := NewRequest(httpRequest)
var bb Book
request.ReadEntity(&bb)
if !kv.readCalled {
t.Error("Read never called")
}
}

View File

@ -0,0 +1 @@
ignore

View File

@ -0,0 +1 @@
ignore

View File

@ -0,0 +1,20 @@
#
# Include your application ID here
#
application: <your_app_id>
version: 1
runtime: go
api_version: go1
handlers:
#
# Regex for all swagger files to make as static content.
# You should create the folder static/swagger and copy
# swagger-ui into it.
#
- url: /apidocs/(.*?)/(.*\.(js|html|css))
static_files: static/swagger/\1/\2
upload: static/swagger/(.*?)/(.*\.(js|html|css))
- url: /.*
script: _go_app

View File

@ -0,0 +1 @@
ignore

View File

@ -0,0 +1,18 @@
application: <your_app_id>
version: 1
runtime: go
api_version: go1
handlers:
# Regex for all swagger files to make as static content.
# You should create the folder static/swagger and copy
# swagger-ui into it.
#
- url: /apidocs/(.*?)/(.*\.(js|html|css))
static_files: static/swagger/\1/\2
upload: static/swagger/(.*?)/(.*\.(js|html|css))
# Catch all.
- url: /.*
script: _go_app
login: required

View File

@ -0,0 +1,267 @@
package main
import (
"net/http"
"time"
"github.com/emicklei/go-restful"
"github.com/emicklei/go-restful-swagger12"
"google.golang.org/appengine"
"google.golang.org/appengine/datastore"
"google.golang.org/appengine/user"
)
// This example demonstrates a reasonably complete suite of RESTful operations backed
// by DataStore on Google App Engine.
// Our simple example struct.
type Profile struct {
LastModified time.Time `json:"-" xml:"-"`
Email string `json:"-" xml:"-"`
FirstName string `json:"first_name" xml:"first-name"`
NickName string `json:"nick_name" xml:"nick-name"`
LastName string `json:"last_name" xml:"last-name"`
}
type ProfileApi struct {
Path string
}
func gaeUrl() string {
if appengine.IsDevAppServer() {
return "http://localhost:8080"
} else {
// Include your URL on App Engine here.
// I found no way to get AppID without appengine.Context and this always
// based on a http.Request.
return "http://federatedservices.appspot.com"
}
}
func init() {
u := ProfileApi{Path: "/profiles"}
u.register()
// Optionally, you can install the Swagger Service which provides a nice Web UI on your REST API
// You need to download the Swagger HTML5 assets and change the FilePath location in the config below.
// Open <your_app_id>.appspot.com/apidocs and enter
// Place the Swagger UI files into a folder called static/swagger if you wish to use Swagger
// http://<your_app_id>.appspot.com/apidocs.json in the api input field.
// For testing, you can use http://localhost:8080/apidocs.json
config := swagger.Config{
// You control what services are visible
WebServices: restful.RegisteredWebServices(),
WebServicesUrl: gaeUrl(),
ApiPath: "/apidocs.json",
// Optionally, specifiy where the UI is located
SwaggerPath: "/apidocs/",
// GAE support static content which is configured in your app.yaml.
// This example expect the swagger-ui in static/swagger so you should place it there :)
SwaggerFilePath: "static/swagger"}
swagger.InstallSwaggerService(config)
}
func (u ProfileApi) register() {
ws := new(restful.WebService)
ws.
Path(u.Path).
// You can specify consumes and produces per route as well.
Consumes(restful.MIME_JSON, restful.MIME_XML).
Produces(restful.MIME_JSON, restful.MIME_XML)
ws.Route(ws.POST("").To(u.insert).
// Swagger documentation.
Doc("insert a new profile").
Param(ws.BodyParameter("Profile", "representation of a profile").DataType("main.Profile")).
Reads(Profile{}))
ws.Route(ws.GET("/{profile-id}").To(u.read).
// Swagger documentation.
Doc("read a profile").
Param(ws.PathParameter("profile-id", "identifier for a profile").DataType("string")).
Writes(Profile{}))
ws.Route(ws.PUT("/{profile-id}").To(u.update).
// Swagger documentation.
Doc("update an existing profile").
Param(ws.PathParameter("profile-id", "identifier for a profile").DataType("string")).
Param(ws.BodyParameter("Profile", "representation of a profile").DataType("main.Profile")).
Reads(Profile{}))
ws.Route(ws.DELETE("/{profile-id}").To(u.remove).
// Swagger documentation.
Doc("remove a profile").
Param(ws.PathParameter("profile-id", "identifier for a profile").DataType("string")))
restful.Add(ws)
}
// POST http://localhost:8080/profiles
// {"first_name": "Ivan", "nick_name": "Socks", "last_name": "Hawkes"}
//
func (u *ProfileApi) insert(r *restful.Request, w *restful.Response) {
c := appengine.NewContext(r.Request)
// Marshall the entity from the request into a struct.
p := new(Profile)
err := r.ReadEntity(&p)
if err != nil {
w.WriteError(http.StatusNotAcceptable, err)
return
}
// Ensure we start with a sensible value for this field.
p.LastModified = time.Now()
// The profile belongs to this user.
p.Email = user.Current(c).String()
k, err := datastore.Put(c, datastore.NewIncompleteKey(c, "profiles", nil), p)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Let them know the location of the newly created resource.
// TODO: Use a safe Url path append function.
w.AddHeader("Location", u.Path+"/"+k.Encode())
// Return the resultant entity.
w.WriteHeader(http.StatusCreated)
w.WriteEntity(p)
}
// GET http://localhost:8080/profiles/ahdkZXZ-ZmVkZXJhdGlvbi1zZXJ2aWNlc3IVCxIIcHJvZmlsZXMYgICAgICAgAoM
//
func (u ProfileApi) read(r *restful.Request, w *restful.Response) {
c := appengine.NewContext(r.Request)
// Decode the request parameter to determine the key for the entity.
k, err := datastore.DecodeKey(r.PathParameter("profile-id"))
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Retrieve the entity from the datastore.
p := Profile{}
if err := datastore.Get(c, k, &p); err != nil {
if err.Error() == "datastore: no such entity" {
http.Error(w, err.Error(), http.StatusNotFound)
} else {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
return
}
// Check we own the profile before allowing them to view it.
// Optionally, return a 404 instead to help prevent guessing ids.
// TODO: Allow admins access.
if p.Email != user.Current(c).String() {
http.Error(w, "You do not have access to this resource", http.StatusForbidden)
return
}
w.WriteEntity(p)
}
// PUT http://localhost:8080/profiles/ahdkZXZ-ZmVkZXJhdGlvbi1zZXJ2aWNlc3IVCxIIcHJvZmlsZXMYgICAgICAgAoM
// {"first_name": "Ivan", "nick_name": "Socks", "last_name": "Hawkes"}
//
func (u *ProfileApi) update(r *restful.Request, w *restful.Response) {
c := appengine.NewContext(r.Request)
// Decode the request parameter to determine the key for the entity.
k, err := datastore.DecodeKey(r.PathParameter("profile-id"))
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Marshall the entity from the request into a struct.
p := new(Profile)
err = r.ReadEntity(&p)
if err != nil {
w.WriteError(http.StatusNotAcceptable, err)
return
}
// Retrieve the old entity from the datastore.
old := Profile{}
if err := datastore.Get(c, k, &old); err != nil {
if err.Error() == "datastore: no such entity" {
http.Error(w, err.Error(), http.StatusNotFound)
} else {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
return
}
// Check we own the profile before allowing them to update it.
// Optionally, return a 404 instead to help prevent guessing ids.
// TODO: Allow admins access.
if old.Email != user.Current(c).String() {
http.Error(w, "You do not have access to this resource", http.StatusForbidden)
return
}
// Since the whole entity is re-written, we need to assign any invariant fields again
// e.g. the owner of the entity.
p.Email = user.Current(c).String()
// Keep track of the last modification date.
p.LastModified = time.Now()
// Attempt to overwrite the old entity.
_, err = datastore.Put(c, k, p)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Let them know it succeeded.
w.WriteHeader(http.StatusNoContent)
}
// DELETE http://localhost:8080/profiles/ahdkZXZ-ZmVkZXJhdGlvbi1zZXJ2aWNlc3IVCxIIcHJvZmlsZXMYgICAgICAgAoM
//
func (u *ProfileApi) remove(r *restful.Request, w *restful.Response) {
c := appengine.NewContext(r.Request)
// Decode the request parameter to determine the key for the entity.
k, err := datastore.DecodeKey(r.PathParameter("profile-id"))
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Retrieve the old entity from the datastore.
old := Profile{}
if err := datastore.Get(c, k, &old); err != nil {
if err.Error() == "datastore: no such entity" {
http.Error(w, err.Error(), http.StatusNotFound)
} else {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
return
}
// Check we own the profile before allowing them to delete it.
// Optionally, return a 404 instead to help prevent guessing ids.
// TODO: Allow admins access.
if old.Email != user.Current(c).String() {
http.Error(w, "You do not have access to this resource", http.StatusForbidden)
return
}
// Delete the entity.
if err := datastore.Delete(c, k); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
// Success notification.
w.WriteHeader(http.StatusNoContent)
}

View File

@ -0,0 +1,12 @@
package main
import (
"github.com/mjibson/appstats"
)
func stats(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) {
c := appstats.NewContext(req.Request)
chain.ProcessFilter(req, resp)
c.Stats.Status = resp.StatusCode()
c.Save()
}

View File

@ -0,0 +1,162 @@
package main
import (
"net/http"
"github.com/emicklei/go-restful"
"github.com/emicklei/go-restful-swagger12"
"google.golang.org/appengine"
"google.golang.org/appengine/memcache"
)
// This example is functionally the same as ../restful-user-service.go
// but it`s supposed to run on Goole App Engine (GAE)
//
// contributed by ivanhawkes
type User struct {
Id, Name string
}
type UserService struct {
// normally one would use DAO (data access object)
// but in this example we simple use memcache.
}
func (u UserService) Register() {
ws := new(restful.WebService)
ws.
Path("/users").
Consumes(restful.MIME_XML, restful.MIME_JSON).
Produces(restful.MIME_JSON, restful.MIME_XML) // you can specify this per route as well
ws.Route(ws.GET("/{user-id}").To(u.findUser).
// docs
Doc("get a user").
Param(ws.PathParameter("user-id", "identifier of the user").DataType("string")).
Writes(User{})) // on the response
ws.Route(ws.PATCH("").To(u.updateUser).
// docs
Doc("update a user").
Reads(User{})) // from the request
ws.Route(ws.PUT("/{user-id}").To(u.createUser).
// docs
Doc("create a user").
Param(ws.PathParameter("user-id", "identifier of the user").DataType("string")).
Reads(User{})) // from the request
ws.Route(ws.DELETE("/{user-id}").To(u.removeUser).
// docs
Doc("delete a user").
Param(ws.PathParameter("user-id", "identifier of the user").DataType("string")))
restful.Add(ws)
}
// GET http://localhost:8080/users/1
//
func (u UserService) findUser(request *restful.Request, response *restful.Response) {
c := appengine.NewContext(request.Request)
id := request.PathParameter("user-id")
usr := new(User)
_, err := memcache.Gob.Get(c, id, &usr)
if err != nil || len(usr.Id) == 0 {
response.WriteErrorString(http.StatusNotFound, "User could not be found.")
} else {
response.WriteEntity(usr)
}
}
// PATCH http://localhost:8080/users
// <User><Id>1</Id><Name>Melissa Raspberry</Name></User>
//
func (u *UserService) updateUser(request *restful.Request, response *restful.Response) {
c := appengine.NewContext(request.Request)
usr := new(User)
err := request.ReadEntity(&usr)
if err == nil {
item := &memcache.Item{
Key: usr.Id,
Object: &usr,
}
err = memcache.Gob.Set(c, item)
if err != nil {
response.WriteError(http.StatusInternalServerError, err)
return
}
response.WriteEntity(usr)
} else {
response.WriteError(http.StatusInternalServerError, err)
}
}
// PUT http://localhost:8080/users/1
// <User><Id>1</Id><Name>Melissa</Name></User>
//
func (u *UserService) createUser(request *restful.Request, response *restful.Response) {
c := appengine.NewContext(request.Request)
usr := User{Id: request.PathParameter("user-id")}
err := request.ReadEntity(&usr)
if err == nil {
item := &memcache.Item{
Key: usr.Id,
Object: &usr,
}
err = memcache.Gob.Add(c, item)
if err != nil {
response.WriteError(http.StatusInternalServerError, err)
return
}
response.WriteHeader(http.StatusCreated)
response.WriteEntity(usr)
} else {
response.WriteError(http.StatusInternalServerError, err)
}
}
// DELETE http://localhost:8080/users/1
//
func (u *UserService) removeUser(request *restful.Request, response *restful.Response) {
c := appengine.NewContext(request.Request)
id := request.PathParameter("user-id")
err := memcache.Delete(c, id)
if err != nil {
response.WriteError(http.StatusInternalServerError, err)
}
}
func getGaeURL() string {
if appengine.IsDevAppServer() {
return "http://localhost:8080"
} else {
/**
* Include your URL on App Engine here.
* I found no way to get AppID without appengine.Context and this always
* based on a http.Request.
*/
return "http://<your_app_id>.appspot.com"
}
}
func init() {
u := UserService{}
u.Register()
// Optionally, you can install the Swagger Service which provides a nice Web UI on your REST API
// You need to download the Swagger HTML5 assets and change the FilePath location in the config below.
// Open <your_app_id>.appspot.com/apidocs and enter http://<your_app_id>.appspot.com/apidocs.json in the api input field.
config := swagger.Config{
WebServices: restful.RegisteredWebServices(), // you control what services are visible
WebServicesUrl: getGaeURL(),
ApiPath: "/apidocs.json",
// Optionally, specifiy where the UI is located
SwaggerPath: "/apidocs/",
// GAE support static content which is configured in your app.yaml.
// This example expect the swagger-ui in static/swagger so you should place it there :)
SwaggerFilePath: "static/swagger"}
swagger.InstallSwaggerService(config)
}

View File

@ -0,0 +1,7 @@
<!DOCTYPE html>
<html>
<body>
<h1>{{.Text}}</h1>
</body>
</html>

View File

@ -0,0 +1,34 @@
package restPack
import (
restful "github.com/emicklei/go-restful"
"gopkg.in/vmihailenco/msgpack.v2"
)
const MIME_MSGPACK = "application/x-msgpack" // Accept or Content-Type used in Consumes() and/or Produces()
// NewEntityAccessorMPack returns a new EntityReaderWriter for accessing MessagePack content.
// This package is not initialized with such an accessor using the MIME_MSGPACK contentType.
func NewEntityAccessorMsgPack() restful.EntityReaderWriter {
return entityMsgPackAccess{}
}
// entityOctetAccess is a EntityReaderWriter for Octet encoding
type entityMsgPackAccess struct {
}
// Read unmarshalls the value from byte slice and using msgpack to unmarshal
func (e entityMsgPackAccess) Read(req *restful.Request, v interface{}) error {
return msgpack.NewDecoder(req.Request.Body).Decode(v)
}
// Write marshals the value to byte slice and set the Content-Type Header.
func (e entityMsgPackAccess) Write(resp *restful.Response, status int, v interface{}) error {
if v == nil {
resp.WriteHeader(status)
// do not write a nil representation
return nil
}
resp.WriteHeader(status)
return msgpack.NewEncoder(resp).Encode(v)
}

View File

@ -0,0 +1,160 @@
package restPack
import (
"bytes"
"errors"
"log"
"net/http"
"net/http/httptest"
"reflect"
"testing"
"time"
"io/ioutil"
restful "github.com/emicklei/go-restful"
)
func TestMsgPack(t *testing.T) {
// register msg pack entity
restful.RegisterEntityAccessor(MIME_MSGPACK, NewEntityAccessorMsgPack())
type Tool struct {
Name string
Vendor string
}
// Write
httpWriter := httptest.NewRecorder()
mpack := &Tool{Name: "json", Vendor: "apple"}
resp := restful.NewResponse(httpWriter)
resp.SetRequestAccepts("application/x-msgpack,*/*;q=0.8")
err := resp.WriteEntity(mpack)
if err != nil {
t.Errorf("err %v", err)
}
// Read
bodyReader := bytes.NewReader(httpWriter.Body.Bytes())
httpRequest, _ := http.NewRequest("GET", "/test", bodyReader)
httpRequest.Header.Set("Content-Type", MIME_MSGPACK)
request := restful.NewRequest(httpRequest)
readMsgPack := new(Tool)
err = request.ReadEntity(&readMsgPack)
if err != nil {
t.Errorf("err %v", err)
}
if equal := reflect.DeepEqual(mpack, readMsgPack); !equal {
t.Fatalf("should not be error")
}
}
func TestWithWebService(t *testing.T) {
serverURL := "http://127.0.0.1:8090"
go func() {
runRestfulMsgPackRouterServer()
}()
if err := waitForServerUp(serverURL); err != nil {
t.Errorf("%v", err)
}
// send a post request
userData := user{Id: "0001", Name: "Tony"}
msgPackData, err := msgpack.Marshal(userData)
req, err := http.NewRequest("POST", serverURL+"/test/msgpack", bytes.NewBuffer(msgPackData))
req.Header.Set("Content-Type", MIME_MSGPACK)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
t.Errorf("unexpected error in sending req: %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("unexpected response: %v, expected: %v", resp.StatusCode, http.StatusOK)
}
ur := &userResponse{}
expectMsgPackDocument(t, resp, ur)
if ur.Status != statusActive {
t.Fatalf("should not error")
}
log.Printf("user response:%v", ur)
}
func expectMsgPackDocument(t *testing.T, r *http.Response, doc interface{}) {
data, err := ioutil.ReadAll(r.Body)
defer r.Body.Close()
if err != nil {
t.Errorf("ExpectMsgPackDocument: unable to read response body :%v", err)
return
}
// put the body back for re-reads
r.Body = ioutil.NopCloser(bytes.NewReader(data))
err = msgpack.Unmarshal(data, doc)
if err != nil {
t.Errorf("ExpectMsgPackDocument: unable to unmarshal MsgPack:%v", err)
}
}
func runRestfulMsgPackRouterServer() {
container := restful.NewContainer()
register(container)
log.Printf("start listening on localhost:8090")
server := &http.Server{Addr: ":8090", Handler: container}
log.Fatal(server.ListenAndServe())
}
func waitForServerUp(serverURL string) error {
for start := time.Now(); time.Since(start) < time.Minute; time.Sleep(5 * time.Second) {
_, err := http.Get(serverURL + "/")
if err == nil {
return nil
}
}
return errors.New("waiting for server timed out")
}
var (
statusActive = "active"
)
type user struct {
Id, Name string
}
type userResponse struct {
Status string
}
func register(container *restful.Container) {
restful.RegisterEntityAccessor(MIME_MSGPACK, NewEntityAccessorMsgPack())
ws := new(restful.WebService)
ws.
Path("/test").
Consumes(restful.MIME_JSON, MIME_MSGPACK).
Produces(restful.MIME_JSON, MIME_MSGPACK)
// route user api
ws.Route(ws.POST("/msgpack").
To(do).
Reads(user{}).
Writes(userResponse{}))
container.Add(ws)
}
func do(request *restful.Request, response *restful.Response) {
u := &user{}
err := request.ReadEntity(u)
if err != nil {
log.Printf("should be no error, got:%v", err)
}
log.Printf("got:%v", u)
ur := &userResponse{Status: statusActive}
response.SetRequestAccepts(MIME_MSGPACK)
response.WriteEntity(ur)
}

View File

@ -0,0 +1,68 @@
package main
import (
"io"
"log"
"net/http"
"github.com/emicklei/go-restful"
)
// 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
//
// GET http://localhost:8080/users
//
// GET http://localhost:8080/users/1
//
// PUT http://localhost:8080/users/1
//
// DELETE http://localhost:8080/users/1
//
// OPTIONS http://localhost:8080/users/1 with Header "Origin" set to some domain and
type UserResource struct{}
func (u UserResource) RegisterTo(container *restful.Container) {
ws := new(restful.WebService)
ws.
Path("/users").
Consumes("*/*").
Produces("*/*")
ws.Route(ws.GET("/{user-id}").To(u.nop))
ws.Route(ws.POST("").To(u.nop))
ws.Route(ws.PUT("/{user-id}").To(u.nop))
ws.Route(ws.DELETE("/{user-id}").To(u.nop))
container.Add(ws)
}
func (u UserResource) nop(request *restful.Request, response *restful.Response) {
io.WriteString(response.ResponseWriter, "this would be a normal response")
}
func main() {
wsContainer := restful.NewContainer()
u := UserResource{}
u.RegisterTo(wsContainer)
// Add container filter to enable CORS
cors := restful.CrossOriginResourceSharing{
ExposeHeaders: []string{"X-My-Header"},
AllowedHeaders: []string{"Content-Type", "Accept"},
AllowedMethods: []string{"GET", "POST"},
CookiesAllowed: false,
Container: wsContainer}
wsContainer.Filter(cors.Filter)
// Add container filter to respond to OPTIONS
wsContainer.Filter(wsContainer.OPTIONSFilter)
log.Printf("start listening on localhost:8080")
server := &http.Server{Addr: ":8080", Handler: wsContainer}
log.Fatal(server.ListenAndServe())
}

View File

@ -0,0 +1,54 @@
package main
import (
"github.com/emicklei/go-restful"
"io"
"log"
"net/http"
"os"
"strings"
"time"
)
// This example shows how to create a filter that produces log lines
// according to the Common Log Format, also known as the NCSA standard.
//
// kindly contributed by leehambley
//
// GET http://localhost:8080/ping
var logger *log.Logger = log.New(os.Stdout, "", 0)
func NCSACommonLogFormatLogger() restful.FilterFunction {
return func(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) {
var username = "-"
if req.Request.URL.User != nil {
if name := req.Request.URL.User.Username(); name != "" {
username = name
}
}
chain.ProcessFilter(req, resp)
logger.Printf("%s - %s [%s] \"%s %s %s\" %d %d",
strings.Split(req.Request.RemoteAddr, ":")[0],
username,
time.Now().Format("02/Jan/2006:15:04:05 -0700"),
req.Request.Method,
req.Request.URL.RequestURI(),
req.Request.Proto,
resp.StatusCode(),
resp.ContentLength(),
)
}
}
func main() {
ws := new(restful.WebService)
ws.Filter(NCSACommonLogFormatLogger())
ws.Route(ws.GET("/ping").To(hello))
restful.Add(ws)
http.ListenAndServe(":8080", nil)
}
func hello(req *restful.Request, resp *restful.Response) {
io.WriteString(resp, "pong")
}

View File

@ -0,0 +1,35 @@
package main
import (
"github.com/emicklei/go-restful"
"io"
"net/http"
)
// This example shows how to create a (Route) Filter that performs Basic Authentication on the Http request.
//
// GET http://localhost:8080/secret
// and use admin,admin for the credentials
func main() {
ws := new(restful.WebService)
ws.Route(ws.GET("/secret").Filter(basicAuthenticate).To(secret))
restful.Add(ws)
http.ListenAndServe(":8080", nil)
}
func basicAuthenticate(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) {
encoded := req.Request.Header.Get("Authorization")
// usr/pwd = admin/admin
// real code does some decoding
if len(encoded) == 0 || "Basic YWRtaW46YWRtaW4=" != encoded {
resp.AddHeader("WWW-Authenticate", "Basic realm=Protected Area")
resp.WriteErrorString(401, "401: Not Authorized")
return
}
chain.ProcessFilter(req, resp)
}
func secret(req *restful.Request, resp *restful.Response) {
io.WriteString(resp, "42")
}

View File

@ -0,0 +1,65 @@
package main
import (
"github.com/emicklei/go-restful"
"io"
"log"
"os"
"runtime/pprof"
)
// ProfilingService is a WebService that can start/stop a CPU profile and write results to a file
// GET /{rootPath}/start will activate CPU profiling
// GET /{rootPath}/stop will stop profiling
//
// NewProfileService("/profiler", "ace.prof").AddWebServiceTo(restful.DefaultContainer)
//
type ProfilingService struct {
rootPath string // the base (root) of the service, e.g. /profiler
cpuprofile string // the output filename to write profile results, e.g. myservice.prof
cpufile *os.File // if not nil, then profiling is active
}
func NewProfileService(rootPath string, outputFilename string) *ProfilingService {
ps := new(ProfilingService)
ps.rootPath = rootPath
ps.cpuprofile = outputFilename
return ps
}
// Add this ProfileService to a restful Container
func (p ProfilingService) AddWebServiceTo(container *restful.Container) {
ws := new(restful.WebService)
ws.Path(p.rootPath).Consumes("*/*").Produces(restful.MIME_JSON)
ws.Route(ws.GET("/start").To(p.startProfiler))
ws.Route(ws.GET("/stop").To(p.stopProfiler))
container.Add(ws)
}
func (p *ProfilingService) startProfiler(req *restful.Request, resp *restful.Response) {
if p.cpufile != nil {
io.WriteString(resp.ResponseWriter, "[restful] CPU profiling already running")
return // error?
}
cpufile, err := os.Create(p.cpuprofile)
if err != nil {
log.Fatal(err)
}
// remember for close
p.cpufile = cpufile
pprof.StartCPUProfile(cpufile)
io.WriteString(resp.ResponseWriter, "[restful] CPU profiling started, writing on:"+p.cpuprofile)
}
func (p *ProfilingService) stopProfiler(req *restful.Request, resp *restful.Response) {
if p.cpufile == nil {
io.WriteString(resp.ResponseWriter, "[restful] CPU profiling not active")
return // error?
}
pprof.StopCPUProfile()
p.cpufile.Close()
p.cpufile = nil
io.WriteString(resp.ResponseWriter, "[restful] CPU profiling stopped, closing:"+p.cpuprofile)
}
func main() {} // exists for example compilation only

View File

@ -0,0 +1,107 @@
package main
import (
"log"
"net/http"
"github.com/emicklei/go-restful"
)
// This example has the same service definition as restful-user-resource
// but uses a different router (CurlyRouter) that does not use regular expressions
//
// POST http://localhost:8080/users
// <User><Id>1</Id><Name>Melissa Raspberry</Name></User>
//
// GET http://localhost:8080/users/1
//
// PUT http://localhost:8080/users/1
// <User><Id>1</Id><Name>Melissa</Name></User>
//
// DELETE http://localhost:8080/users/1
//
type User struct {
Id, Name string
}
type UserResource struct {
// normally one would use DAO (data access object)
users map[string]User
}
func (u UserResource) Register(container *restful.Container) {
ws := new(restful.WebService)
ws.
Path("/users").
Consumes(restful.MIME_XML, restful.MIME_JSON).
Produces(restful.MIME_JSON, restful.MIME_XML) // you can specify this per route as well
ws.Route(ws.GET("/{user-id}").To(u.findUser))
ws.Route(ws.POST("").To(u.updateUser))
ws.Route(ws.PUT("/{user-id}").To(u.createUser))
ws.Route(ws.DELETE("/{user-id}").To(u.removeUser))
container.Add(ws)
}
// GET http://localhost:8080/users/1
//
func (u UserResource) findUser(request *restful.Request, response *restful.Response) {
id := request.PathParameter("user-id")
usr := u.users[id]
if len(usr.Id) == 0 {
response.AddHeader("Content-Type", "text/plain")
response.WriteErrorString(http.StatusNotFound, "User could not be found.")
} else {
response.WriteEntity(usr)
}
}
// POST http://localhost:8080/users
// <User><Id>1</Id><Name>Melissa Raspberry</Name></User>
//
func (u *UserResource) updateUser(request *restful.Request, response *restful.Response) {
usr := new(User)
err := request.ReadEntity(&usr)
if err == nil {
u.users[usr.Id] = *usr
response.WriteEntity(usr)
} else {
response.AddHeader("Content-Type", "text/plain")
response.WriteErrorString(http.StatusInternalServerError, err.Error())
}
}
// PUT http://localhost:8080/users/1
// <User><Id>1</Id><Name>Melissa</Name></User>
//
func (u *UserResource) createUser(request *restful.Request, response *restful.Response) {
usr := User{Id: request.PathParameter("user-id")}
err := request.ReadEntity(&usr)
if err == nil {
u.users[usr.Id] = usr
response.WriteHeaderAndEntity(http.StatusCreated, usr)
} else {
response.AddHeader("Content-Type", "text/plain")
response.WriteErrorString(http.StatusInternalServerError, err.Error())
}
}
// DELETE http://localhost:8080/users/1
//
func (u *UserResource) removeUser(request *restful.Request, response *restful.Response) {
id := request.PathParameter("user-id")
delete(u.users, id)
}
func main() {
wsContainer := restful.NewContainer()
wsContainer.Router(restful.CurlyRouter{})
u := UserResource{map[string]User{}}
u.Register(wsContainer)
log.Printf("start listening on localhost:8080")
server := &http.Server{Addr: ":8080", Handler: wsContainer}
log.Fatal(server.ListenAndServe())
}

View File

@ -0,0 +1,149 @@
package main
import (
"bytes"
"errors"
"log"
"net/http"
"testing"
"time"
"github.com/emicklei/go-restful"
)
type User struct {
Id, Name string
}
type UserResource struct {
users map[string]User
}
func (u UserResource) Register(container *restful.Container) {
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))
ws.Route(ws.POST("").To(u.updateUser))
ws.Route(ws.PUT("/{user-id}").To(u.createUser))
ws.Route(ws.DELETE("/{user-id}").To(u.removeUser))
container.Add(ws)
}
// GET http://localhost:8090/users/1
//
func (u UserResource) findUser(request *restful.Request, response *restful.Response) {
id := request.PathParameter("user-id")
usr := u.users[id]
if len(usr.Id) == 0 {
response.AddHeader("Content-Type", "text/plain")
response.WriteErrorString(http.StatusNotFound, "User could not be found.")
} else {
response.WriteEntity(usr)
}
}
// POST http://localhost:8090/users
// <User><Id>1</Id><Name>Melissa Raspberry</Name></User>
//
func (u *UserResource) updateUser(request *restful.Request, response *restful.Response) {
usr := new(User)
err := request.ReadEntity(&usr)
if err == nil {
u.users[usr.Id] = *usr
response.WriteEntity(usr)
} else {
response.AddHeader("Content-Type", "text/plain")
response.WriteErrorString(http.StatusInternalServerError, err.Error())
}
}
// PUT http://localhost:8090/users/1
// <User><Id>1</Id><Name>Melissa</Name></User>
//
func (u *UserResource) createUser(request *restful.Request, response *restful.Response) {
usr := User{Id: request.PathParameter("user-id")}
err := request.ReadEntity(&usr)
if err == nil {
u.users[usr.Id] = usr
response.WriteHeader(http.StatusCreated)
response.WriteEntity(usr)
} else {
response.AddHeader("Content-Type", "text/plain")
response.WriteErrorString(http.StatusInternalServerError, err.Error())
}
}
// DELETE http://localhost:8090/users/1
//
func (u *UserResource) removeUser(request *restful.Request, response *restful.Response) {
id := request.PathParameter("user-id")
delete(u.users, id)
}
func RunRestfulCurlyRouterServer() {
wsContainer := restful.NewContainer()
wsContainer.Router(restful.CurlyRouter{})
u := UserResource{map[string]User{}}
u.Register(wsContainer)
log.Printf("start listening on localhost:8090")
server := &http.Server{Addr: ":8090", Handler: wsContainer}
log.Fatal(server.ListenAndServe())
}
func waitForServerUp(serverURL string) error {
for start := time.Now(); time.Since(start) < time.Minute; time.Sleep(5 * time.Second) {
_, err := http.Get(serverURL + "/")
if err == nil {
return nil
}
}
return errors.New("waiting for server timed out")
}
func TestServer(t *testing.T) {
serverURL := "http://localhost:8090"
go func() {
RunRestfulCurlyRouterServer()
}()
if err := waitForServerUp(serverURL); err != nil {
t.Errorf("%v", err)
}
// GET should give a 405
resp, err := http.Get(serverURL + "/users/")
if err != nil {
t.Errorf("unexpected error in GET /users/: %v", err)
}
if resp.StatusCode != http.StatusMethodNotAllowed {
t.Errorf("unexpected response: %v, expected: %v", resp.StatusCode, http.StatusOK)
}
// Send a POST request.
var jsonStr = []byte(`{"id":"1","name":"user1"}`)
req, err := http.NewRequest("POST", serverURL+"/users/", bytes.NewBuffer(jsonStr))
req.Header.Set("Content-Type", restful.MIME_JSON)
client := &http.Client{}
resp, err = client.Do(req)
if err != nil {
t.Errorf("unexpected error in sending req: %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("unexpected response: %v, expected: %v", resp.StatusCode, http.StatusOK)
}
// Test that GET works.
resp, err = http.Get(serverURL + "/users/1")
if err != nil {
t.Errorf("unexpected error in GET /users/1: %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("unexpected response: %v, expected: %v", resp.StatusCode, http.StatusOK)
}
}

View File

@ -0,0 +1,61 @@
package main
import (
"github.com/emicklei/go-restful"
"log"
"net/http"
)
type User struct {
Id, Name string
}
type UserList struct {
Users []User
}
//
// This example shows how to use the CompressingResponseWriter by a Filter
// such that encoding can be enabled per WebService or per Route (instead of per container)
// Using restful.DefaultContainer.EnableContentEncoding(true) will encode all responses served by WebServices in the DefaultContainer.
//
// Set Accept-Encoding to gzip or deflate
// GET http://localhost:8080/users/42
// and look at the response headers
func main() {
restful.Add(NewUserService())
log.Printf("start listening on localhost:8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
func NewUserService() *restful.WebService {
ws := new(restful.WebService)
ws.
Path("/users").
Consumes(restful.MIME_XML, restful.MIME_JSON).
Produces(restful.MIME_JSON, restful.MIME_XML)
// install a response encoding filter
ws.Route(ws.GET("/{user-id}").Filter(encodingFilter).To(findUser))
return ws
}
// Route Filter (defines FilterFunction)
func encodingFilter(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) {
log.Printf("[encoding-filter] %s,%s\n", req.Request.Method, req.Request.URL)
// wrap responseWriter into a compressing one
compress, _ := restful.NewCompressingResponseWriter(resp.ResponseWriter, restful.ENCODING_GZIP)
resp.ResponseWriter = compress
defer func() {
compress.Close()
}()
chain.ProcessFilter(req, resp)
}
// GET http://localhost:8080/users/42
//
func findUser(request *restful.Request, response *restful.Response) {
log.Printf("findUser")
response.WriteEntity(User{"42", "Gandalf"})
}

View File

@ -0,0 +1,114 @@
package main
import (
"github.com/emicklei/go-restful"
"log"
"net/http"
"time"
)
type User struct {
Id, Name string
}
type UserList struct {
Users []User
}
// This example show how to create and use the three different Filters (Container,WebService and Route)
// When applied to the restful.DefaultContainer, we refer to them as a global filter.
//
// GET http://locahost:8080/users/42
// and see the logging per filter (try repeating this request)
func main() {
// install a global (=DefaultContainer) filter (processed before any webservice in the DefaultContainer)
restful.Filter(globalLogging)
restful.Add(NewUserService())
log.Printf("start listening on localhost:8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
func NewUserService() *restful.WebService {
ws := new(restful.WebService)
ws.
Path("/users").
Consumes(restful.MIME_XML, restful.MIME_JSON).
Produces(restful.MIME_JSON, restful.MIME_XML)
// install a webservice filter (processed before any route)
ws.Filter(webserviceLogging).Filter(measureTime)
// install a counter filter
ws.Route(ws.GET("").Filter(NewCountFilter().routeCounter).To(getAllUsers))
// install 2 chained route filters (processed before calling findUser)
ws.Route(ws.GET("/{user-id}").Filter(routeLogging).Filter(NewCountFilter().routeCounter).To(findUser))
return ws
}
// Global Filter
func globalLogging(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) {
log.Printf("[global-filter (logger)] %s,%s\n", req.Request.Method, req.Request.URL)
chain.ProcessFilter(req, resp)
}
// WebService Filter
func webserviceLogging(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) {
log.Printf("[webservice-filter (logger)] %s,%s\n", req.Request.Method, req.Request.URL)
chain.ProcessFilter(req, resp)
}
// WebService (post-process) Filter (as a struct that defines a FilterFunction)
func measureTime(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) {
now := time.Now()
chain.ProcessFilter(req, resp)
log.Printf("[webservice-filter (timer)] %v\n", time.Now().Sub(now))
}
// Route Filter (defines FilterFunction)
func routeLogging(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) {
log.Printf("[route-filter (logger)] %s,%s\n", req.Request.Method, req.Request.URL)
chain.ProcessFilter(req, resp)
}
// Route Filter (as a struct that defines a FilterFunction)
// CountFilter implements a FilterFunction for counting requests.
type CountFilter struct {
count int
counter chan int // for go-routine safe count increments
}
// NewCountFilter creates and initializes a new CountFilter.
func NewCountFilter() *CountFilter {
c := new(CountFilter)
c.counter = make(chan int)
go func() {
for {
c.count += <-c.counter
}
}()
return c
}
// routeCounter increments the count of the filter (through a channel)
func (c *CountFilter) routeCounter(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) {
c.counter <- 1
log.Printf("[route-filter (counter)] count:%d", c.count)
chain.ProcessFilter(req, resp)
}
// GET http://localhost:8080/users
//
func getAllUsers(request *restful.Request, response *restful.Response) {
log.Printf("getAllUsers")
response.WriteEntity(UserList{[]User{{"42", "Gandalf"}, {"3.14", "Pi"}}})
}
// GET http://localhost:8080/users/42
//
func findUser(request *restful.Request, response *restful.Response) {
log.Printf("findUser")
response.WriteEntity(User{"42", "Gandalf"})
}

View File

@ -0,0 +1,62 @@
package main
import (
"fmt"
"github.com/emicklei/go-restful"
"github.com/gorilla/schema"
"io"
"net/http"
)
// This example shows how to handle a POST of a HTML form that uses the standard x-www-form-urlencoded content-type.
// It uses the gorilla web tool kit schema package to decode the form data into a struct.
//
// GET http://localhost:8080/profiles
//
type Profile struct {
Name string
Age int
}
var decoder *schema.Decoder
func main() {
decoder = schema.NewDecoder()
ws := new(restful.WebService)
ws.Route(ws.POST("/profiles").Consumes("application/x-www-form-urlencoded").To(postAdddress))
ws.Route(ws.GET("/profiles").To(addresssForm))
restful.Add(ws)
http.ListenAndServe(":8080", nil)
}
func postAdddress(req *restful.Request, resp *restful.Response) {
err := req.Request.ParseForm()
if err != nil {
resp.WriteErrorString(http.StatusBadRequest, err.Error())
return
}
p := new(Profile)
err = decoder.Decode(p, req.Request.PostForm)
if err != nil {
resp.WriteErrorString(http.StatusBadRequest, err.Error())
return
}
io.WriteString(resp.ResponseWriter, fmt.Sprintf("<html><body>Name=%s, Age=%d</body></html>", p.Name, p.Age))
}
func addresssForm(req *restful.Request, resp *restful.Response) {
io.WriteString(resp.ResponseWriter,
`<html>
<body>
<h1>Enter Profile</h1>
<form method="post">
<label>Name:</label>
<input type="text" name="Name"/>
<label>Age:</label>
<input type="text" name="Age"/>
<input type="Submit" />
</form>
</body>
</html>`)
}

View File

@ -0,0 +1,22 @@
package main
import (
"github.com/emicklei/go-restful"
"io"
"net/http"
)
// This example shows the minimal code needed to get a restful.WebService working.
//
// GET http://localhost:8080/hello
func main() {
ws := new(restful.WebService)
ws.Route(ws.GET("/hello").To(hello))
restful.Add(ws)
http.ListenAndServe(":8080", nil)
}
func hello(req *restful.Request, resp *restful.Response) {
io.WriteString(resp, "world")
}

View File

@ -0,0 +1,35 @@
package main
import (
"log"
"net/http"
"text/template"
"github.com/emicklei/go-restful"
)
// This example shows how to serve a HTML page using the standard Go template engine.
//
// GET http://localhost:8080/
func main() {
ws := new(restful.WebService)
ws.Route(ws.GET("/").To(home))
restful.Add(ws)
print("open browser on http://localhost:8080/\n")
http.ListenAndServe(":8080", nil)
}
type Message struct {
Text string
}
func home(req *restful.Request, resp *restful.Response) {
p := &Message{"restful-html-template demo"}
// you might want to cache compiled templates
t, err := template.ParseFiles("home.html")
if err != nil {
log.Fatalf("Template gave: %s", err)
}
t.Execute(resp.ResponseWriter, p)
}

View File

@ -0,0 +1,43 @@
package main
import (
"github.com/emicklei/go-restful"
"io"
"log"
"net/http"
)
// This example shows how to have a program with 2 WebServices containers
// each having a http server listening on its own port.
//
// The first "hello" is added to the restful.DefaultContainer (and uses DefaultServeMux)
// For the second "hello", a new container and ServeMux is created
// and requires a new http.Server with the container being the Handler.
// This first server is spawn in its own go-routine such that the program proceeds to create the second.
//
// GET http://localhost:8080/hello
// GET http://localhost:8081/hello
func main() {
ws := new(restful.WebService)
ws.Route(ws.GET("/hello").To(hello))
restful.Add(ws)
go func() {
http.ListenAndServe(":8080", nil)
}()
container2 := restful.NewContainer()
ws2 := new(restful.WebService)
ws2.Route(ws2.GET("/hello").To(hello2))
container2.Add(ws2)
server := &http.Server{Addr: ":8081", Handler: container2}
log.Fatal(server.ListenAndServe())
}
func hello(req *restful.Request, resp *restful.Response) {
io.WriteString(resp, "default world")
}
func hello2(req *restful.Request, resp *restful.Response) {
io.WriteString(resp, "second world")
}

View File

@ -0,0 +1,24 @@
package main
import (
"io"
"net/http"
"github.com/emicklei/go-restful"
)
// This example shows how to use a WebService filter that passed the Http headers to disable browser cacheing.
//
// GET http://localhost:8080/hello
func main() {
ws := new(restful.WebService)
ws.Filter(restful.NoBrowserCacheFilter)
ws.Route(ws.GET("/hello").To(hello))
restful.Add(ws)
http.ListenAndServe(":8080", nil)
}
func hello(req *restful.Request, resp *restful.Response) {
io.WriteString(resp, "world")
}

View File

@ -0,0 +1,51 @@
package main
import (
"github.com/emicklei/go-restful"
"io"
"log"
"net/http"
)
// This example shows how to use the OPTIONSFilter on a Container
//
// OPTIONS http://localhost:8080/users
//
// OPTIONS http://localhost:8080/users/1
type UserResource struct{}
func (u UserResource) RegisterTo(container *restful.Container) {
ws := new(restful.WebService)
ws.
Path("/users").
Consumes("*/*").
Produces("*/*")
ws.Route(ws.GET("/{user-id}").To(u.nop))
ws.Route(ws.POST("").To(u.nop))
ws.Route(ws.PUT("/{user-id}").To(u.nop))
ws.Route(ws.DELETE("/{user-id}").To(u.nop))
container.Add(ws)
}
func (u UserResource) nop(request *restful.Request, response *restful.Response) {
io.WriteString(response.ResponseWriter, "this would be a normal response")
}
func main() {
wsContainer := restful.NewContainer()
u := UserResource{}
u.RegisterTo(wsContainer)
// Add container filter to respond to OPTIONS
wsContainer.Filter(wsContainer.OPTIONSFilter)
// For use on the default container, you can write
// restful.Filter(restful.OPTIONSFilter())
log.Printf("start listening on localhost:8080")
server := &http.Server{Addr: ":8080", Handler: wsContainer}
log.Fatal(server.ListenAndServe())
}

View File

@ -0,0 +1,26 @@
package main
import (
. "github.com/emicklei/go-restful"
"io"
"net/http"
)
// This example shows how to a Route that matches the "tail" of a path.
// Requires the use of a CurlyRouter and the star "*" path parameter pattern.
//
// GET http://localhost:8080/basepath/some/other/location/test.xml
func main() {
DefaultContainer.Router(CurlyRouter{})
ws := new(WebService)
ws.Route(ws.GET("/basepath/{resource:*}").To(staticFromPathParam))
Add(ws)
println("[go-restful] serve path tails from http://localhost:8080/basepath")
http.ListenAndServe(":8080", nil)
}
func staticFromPathParam(req *Request, resp *Response) {
io.WriteString(resp, "Tail="+req.PathParameter("resource"))
}

View File

@ -0,0 +1,98 @@
package main
import (
"github.com/emicklei/go-restful"
"io"
"log"
"net/http"
)
// This example shows how the different types of filters are called in the request-response flow.
// The call chain is logged on the console when sending an http request.
//
// GET http://localhost:8080/1
// GET http://localhost:8080/2
var indentLevel int
func container_filter_A(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) {
log.Printf("url path:%v\n", req.Request.URL)
trace("container_filter_A: before", 1)
chain.ProcessFilter(req, resp)
trace("container_filter_A: after", -1)
}
func container_filter_B(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) {
trace("container_filter_B: before", 1)
chain.ProcessFilter(req, resp)
trace("container_filter_B: after", -1)
}
func service_filter_A(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) {
trace("service_filter_A: before", 1)
chain.ProcessFilter(req, resp)
trace("service_filter_A: after", -1)
}
func service_filter_B(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) {
trace("service_filter_B: before", 1)
chain.ProcessFilter(req, resp)
trace("service_filter_B: after", -1)
}
func route_filter_A(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) {
trace("route_filter_A: before", 1)
chain.ProcessFilter(req, resp)
trace("route_filter_A: after", -1)
}
func route_filter_B(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) {
trace("route_filter_B: before", 1)
chain.ProcessFilter(req, resp)
trace("route_filter_B: after", -1)
}
func trace(what string, delta int) {
indented := what
if delta < 0 {
indentLevel += delta
}
for t := 0; t < indentLevel; t++ {
indented = "." + indented
}
log.Printf("%s", indented)
if delta > 0 {
indentLevel += delta
}
}
func main() {
restful.Filter(container_filter_A)
restful.Filter(container_filter_B)
ws1 := new(restful.WebService)
ws1.Path("/1")
ws1.Filter(service_filter_A)
ws1.Filter(service_filter_B)
ws1.Route(ws1.GET("").To(doit1).Filter(route_filter_A).Filter(route_filter_B))
ws2 := new(restful.WebService)
ws2.Path("/2")
ws2.Filter(service_filter_A)
ws2.Filter(service_filter_B)
ws2.Route(ws2.GET("").To(doit2).Filter(route_filter_A).Filter(route_filter_B))
restful.Add(ws1)
restful.Add(ws2)
log.Print("go-restful example listing on http://localhost:8080/1 and http://localhost:8080/2")
log.Fatal(http.ListenAndServe(":8080", nil))
}
func doit1(req *restful.Request, resp *restful.Response) {
io.WriteString(resp, "nothing to see in 1")
}
func doit2(req *restful.Request, resp *restful.Response) {
io.WriteString(resp, "nothing to see in 2")
}

View File

@ -0,0 +1,63 @@
package main
import (
"github.com/emicklei/go-restful"
"log"
"net/http"
)
// This example shows how to use methods as RouteFunctions for WebServices.
// The ProductResource has a Register() method that creates and initializes
// a WebService to expose its methods as REST operations.
// The WebService is added to the restful.DefaultContainer.
// A ProductResource is typically created using some data access object.
//
// GET http://localhost:8080/products/1
// POST http://localhost:8080/products
// <Product><Id>1</Id><Title>The First</Title></Product>
type Product struct {
Id, Title string
}
type ProductResource struct {
// typically reference a DAO (data-access-object)
}
func (p ProductResource) getOne(req *restful.Request, resp *restful.Response) {
id := req.PathParameter("id")
log.Println("getting product with id:" + id)
resp.WriteEntity(Product{Id: id, Title: "test"})
}
func (p ProductResource) postOne(req *restful.Request, resp *restful.Response) {
updatedProduct := new(Product)
err := req.ReadEntity(updatedProduct)
if err != nil { // bad request
resp.WriteErrorString(http.StatusBadRequest, err.Error())
return
}
log.Println("updating product with id:" + updatedProduct.Id)
}
func (p ProductResource) Register() {
ws := new(restful.WebService)
ws.Path("/products")
ws.Consumes(restful.MIME_XML)
ws.Produces(restful.MIME_XML)
ws.Route(ws.GET("/{id}").To(p.getOne).
Doc("get the product by its id").
Param(ws.PathParameter("id", "identifier of the product").DataType("string")))
ws.Route(ws.POST("").To(p.postOne).
Doc("update or create a product").
Param(ws.BodyParameter("Product", "a Product (XML)").DataType("main.Product")))
restful.Add(ws)
}
func main() {
ProductResource{}.Register()
http.ListenAndServe(":8080", nil)
}

View File

@ -0,0 +1,39 @@
package main
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/emicklei/go-restful"
)
var (
Result string
)
func TestRouteExtractParameter(t *testing.T) {
// setup service
ws := new(restful.WebService)
ws.Consumes(restful.MIME_XML)
ws.Route(ws.GET("/test/{param}").To(DummyHandler))
restful.Add(ws)
// setup request + writer
bodyReader := strings.NewReader("<Sample><Value>42</Value></Sample>")
httpRequest, _ := http.NewRequest("GET", "/test/THIS", bodyReader)
httpRequest.Header.Set("Content-Type", restful.MIME_XML)
httpWriter := httptest.NewRecorder()
// run
restful.DefaultContainer.ServeHTTP(httpWriter, httpRequest)
if Result != "THIS" {
t.Fatalf("Result is actually: %s", Result)
}
}
func DummyHandler(rq *restful.Request, rp *restful.Response) {
Result = rq.PathParameter("param")
}

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