commit
dd29cd8353
5
Godeps/Godeps.json
generated
5
Godeps/Godeps.json
generated
@ -58,6 +58,11 @@
|
|||||||
"ImportPath": "github.com/elazarl/go-bindata-assetfs",
|
"ImportPath": "github.com/elazarl/go-bindata-assetfs",
|
||||||
"Rev": "ae4665cf2d188c65764c73fe4af5378acc549510"
|
"Rev": "ae4665cf2d188c65764c73fe4af5378acc549510"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"ImportPath": "github.com/emicklei/go-restful",
|
||||||
|
"Comment": "v1.1.2-34-gcb26ade",
|
||||||
|
"Rev": "cb26adeb9644200cb4ec7b32be31e024696e8d00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "github.com/fsouza/go-dockerclient",
|
"ImportPath": "github.com/fsouza/go-dockerclient",
|
||||||
"Comment": "0.2.1-267-g15d2c6e",
|
"Comment": "0.2.1-267-g15d2c6e",
|
||||||
|
70
Godeps/_workspace/src/github.com/emicklei/go-restful/.gitignore
generated
vendored
Normal file
70
Godeps/_workspace/src/github.com/emicklei/go-restful/.gitignore
generated
vendored
Normal 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
|
121
Godeps/_workspace/src/github.com/emicklei/go-restful/CHANGES.md
generated
vendored
Normal file
121
Godeps/_workspace/src/github.com/emicklei/go-restful/CHANGES.md
generated
vendored
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
Change history of go-restful
|
||||||
|
=
|
||||||
|
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
Godeps/_workspace/src/github.com/emicklei/go-restful/LICENSE
generated
vendored
Normal file
22
Godeps/_workspace/src/github.com/emicklei/go-restful/LICENSE
generated
vendored
Normal 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.
|
70
Godeps/_workspace/src/github.com/emicklei/go-restful/README.md
generated
vendored
Normal file
70
Godeps/_workspace/src/github.com/emicklei/go-restful/README.md
generated
vendored
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
go-restful
|
||||||
|
==========
|
||||||
|
|
||||||
|
package for building REST-style Web Services using Google Go
|
||||||
|
|
||||||
|
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 → function mapping with path parameter (e.g. {id}) support
|
||||||
|
- Configurable router:
|
||||||
|
- Routing algorithm after [JSR311](http://jsr311.java.net/nonav/releases/1.1/spec/spec.html) that is implemented using (but doest **not** accept) regular expressions (See RouterJSR311 which is used by default)
|
||||||
|
- Fast routing algorithm that allows static elements, regular expressions and dynamic parameters in the URL path (e.g. /meetings/{id} or /static/{subpath:*}, See CurlyRouter)
|
||||||
|
- 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
|
||||||
|
- Filters for intercepting the request → response flow on Service or Route level
|
||||||
|
- Request-scoped variables using attributes
|
||||||
|
- Containers for WebServices on different HTTP endpoints
|
||||||
|
- Content encoding (gzip,deflate) of responses
|
||||||
|
- Automatic responses on OPTIONS (using a filter)
|
||||||
|
- Automatic CORS request handling (using a filter)
|
||||||
|
- API declaration for Swagger UI (see swagger package)
|
||||||
|
- Panic recovery to produce HTTP 500, customizable using RecoverHandler(...)
|
||||||
|
|
||||||
|
### Resources
|
||||||
|
|
||||||
|
- [Documentation on godoc.org](http://godoc.org/github.com/emicklei/go-restful)
|
||||||
|
- [Code examples](https://github.com/emicklei/go-restful/tree/master/examples)
|
||||||
|
- [Example posted on blog](http://ernestmicklei.com/2012/11/24/go-restful-first-working-example/)
|
||||||
|
- [Design explained on blog](http://ernestmicklei.com/2012/11/11/go-restful-api-design/)
|
||||||
|
- [sourcegraph](https://sourcegraph.com/github.com/emicklei/go-restful)
|
||||||
|
- [gopkg.in](https://gopkg.in/emicklei/go-restful.v1)
|
||||||
|
- [showcase: Mora - MongoDB REST Api server](https://github.com/emicklei/mora)
|
||||||
|
|
||||||
|
[](https://drone.io/github.com/emicklei/go-restful/latest)[](https://sourcegraph.com/github.com/emicklei/go-restful) [](https://sourcegraph.com/github.com/emicklei/go-restful) [](https://sourcegraph.com/github.com/emicklei/go-restful)
|
||||||
|
|
||||||
|
(c) 2012 - 2014, http://ernestmicklei.com. MIT License
|
||||||
|
|
||||||
|
Type ```git shortlog -s``` for a full list of contributors.
|
1
Godeps/_workspace/src/github.com/emicklei/go-restful/Srcfile
generated
vendored
Normal file
1
Godeps/_workspace/src/github.com/emicklei/go-restful/Srcfile
generated
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"SkipDirs": ["examples"]}
|
51
Godeps/_workspace/src/github.com/emicklei/go-restful/bench_curly_test.go
generated
vendored
Normal file
51
Godeps/_workspace/src/github.com/emicklei/go-restful/bench_curly_test.go
generated
vendored
Normal 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
Godeps/_workspace/src/github.com/emicklei/go-restful/bench_test.go
generated
vendored
Normal file
43
Godeps/_workspace/src/github.com/emicklei/go-restful/bench_test.go
generated
vendored
Normal 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
Godeps/_workspace/src/github.com/emicklei/go-restful/bench_test.sh
generated
vendored
Normal file
10
Godeps/_workspace/src/github.com/emicklei/go-restful/bench_test.sh
generated
vendored
Normal 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
|
||||||
|
|
||||||
|
|
89
Godeps/_workspace/src/github.com/emicklei/go-restful/compress.go
generated
vendored
Normal file
89
Godeps/_workspace/src/github.com/emicklei/go-restful/compress.go
generated
vendored
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
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 (
|
||||||
|
"compress/gzip"
|
||||||
|
"compress/zlib"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
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() {
|
||||||
|
c.compressor.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
c.compressor, err = gzip.NewWriterLevel(httpWriter, gzip.BestSpeed)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else if ENCODING_DEFLATE == encoding {
|
||||||
|
c.compressor, err = zlib.NewWriterLevel(httpWriter, zlib.BestSpeed)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nil, errors.New("Unknown encoding:" + encoding)
|
||||||
|
}
|
||||||
|
return c, err
|
||||||
|
}
|
53
Godeps/_workspace/src/github.com/emicklei/go-restful/compress_test.go
generated
vendored
Normal file
53
Godeps/_workspace/src/github.com/emicklei/go-restful/compress_test.go
generated
vendored
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
package restful
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
29
Godeps/_workspace/src/github.com/emicklei/go-restful/constants.go
generated
vendored
Normal file
29
Godeps/_workspace/src/github.com/emicklei/go-restful/constants.go
generated
vendored
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
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()
|
||||||
|
|
||||||
|
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"
|
||||||
|
)
|
257
Godeps/_workspace/src/github.com/emicklei/go-restful/container.go
generated
vendored
Normal file
257
Godeps/_workspace/src/github.com/emicklei/go-restful/container.go
generated
vendored
Normal file
@ -0,0 +1,257 @@
|
|||||||
|
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"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
webServices []*WebService
|
||||||
|
ServeMux *http.ServeMux
|
||||||
|
isRegisteredOnRoot bool
|
||||||
|
containerFilters []FilterFunction
|
||||||
|
doNotRecover bool // default is false
|
||||||
|
recoverHandleFunc RecoverHandleFunction
|
||||||
|
router RouteSelector // default is a RouterJSR311, CurlyRouter is the faster alternative
|
||||||
|
contentEncodingEnabled bool // default is false
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewContainer creates a new Container using a new ServeMux and default router (RouterJSR311)
|
||||||
|
func NewContainer() *Container {
|
||||||
|
return &Container{
|
||||||
|
webServices: []*WebService{},
|
||||||
|
ServeMux: http.NewServeMux(),
|
||||||
|
isRegisteredOnRoot: false,
|
||||||
|
containerFilters: []FilterFunction{},
|
||||||
|
doNotRecover: false,
|
||||||
|
recoverHandleFunc: logStackOnRecover,
|
||||||
|
router: RouterJSR311{},
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 false = recover from panics. This has performance implications.
|
||||||
|
func (c *Container) DoNotRecover(doNot bool) {
|
||||||
|
c.doNotRecover = doNot
|
||||||
|
}
|
||||||
|
|
||||||
|
// Router changes the default Router (currently RouterJSR311)
|
||||||
|
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 panic in that case.
|
||||||
|
func (c *Container) Add(service *WebService) *Container {
|
||||||
|
// If registered on root then no additional specific mapping is needed
|
||||||
|
if !c.isRegisteredOnRoot {
|
||||||
|
pattern := c.fixedPrefixPath(service.RootPath())
|
||||||
|
// check if root path registration is needed
|
||||||
|
if "/" == pattern || "" == pattern {
|
||||||
|
c.ServeMux.HandleFunc("/", c.dispatch)
|
||||||
|
c.isRegisteredOnRoot = true
|
||||||
|
} else {
|
||||||
|
// detect if registration already exists
|
||||||
|
alreadyMapped := false
|
||||||
|
for _, each := range c.webServices {
|
||||||
|
if each.RootPath() == service.RootPath() {
|
||||||
|
alreadyMapped = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !alreadyMapped {
|
||||||
|
c.ServeMux.HandleFunc(pattern, c.dispatch)
|
||||||
|
if !strings.HasSuffix(pattern, "/") {
|
||||||
|
c.ServeMux.HandleFunc(pattern+"/", c.dispatch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// cannot have duplicate root paths
|
||||||
|
for _, each := range c.webServices {
|
||||||
|
if each.RootPath() == service.RootPath() {
|
||||||
|
log.Fatalf("[restful] WebService with duplicate root path detected:['%v']", each)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.webServices = append(c.webServices, service)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.Println(buffer.String())
|
||||||
|
httpWriter.WriteHeader(http.StatusInternalServerError)
|
||||||
|
httpWriter.Write(buffer.Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispatch the incoming Http Request to a matching WebService.
|
||||||
|
func (c *Container) dispatch(httpWriter http.ResponseWriter, httpRequest *http.Request) {
|
||||||
|
// Instal panic recovery unless told otherwise
|
||||||
|
if !c.doNotRecover { // catch all for 500 response
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
c.recoverHandleFunc(r, httpWriter)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
// Install closing the request body (if any)
|
||||||
|
defer func() {
|
||||||
|
if nil != httpRequest.Body {
|
||||||
|
httpRequest.Body.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Detect if compression is needed
|
||||||
|
// assume without compression, test for override
|
||||||
|
writer := httpWriter
|
||||||
|
if c.contentEncodingEnabled {
|
||||||
|
doCompress, encoding := wantsCompressedResponse(httpRequest)
|
||||||
|
if doCompress {
|
||||||
|
var err error
|
||||||
|
writer, err = NewCompressingResponseWriter(httpWriter, encoding)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("[restful] unable to install compressor:", err)
|
||||||
|
httpWriter.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
writer.(*CompressingResponseWriter).Close()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Find best match Route ; err is non nil if no match was found
|
||||||
|
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)
|
||||||
|
resp.WriteErrorString(ser.Code, ser.Message)
|
||||||
|
}
|
||||||
|
// 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 (c Container) 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
return c.webServices
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.compiledPathExpression().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
|
||||||
|
}
|
170
Godeps/_workspace/src/github.com/emicklei/go-restful/cors_filter.go
generated
vendored
Normal file
170
Godeps/_workspace/src/github.com/emicklei/go-restful/cors_filter.go
generated
vendored
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
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 (
|
||||||
|
"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. If empty all are allowed.
|
||||||
|
AllowedMethods []string
|
||||||
|
MaxAge int // number of seconds before requiring new Options request
|
||||||
|
CookiesAllowed bool
|
||||||
|
Container *Container
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.Println("no Http header Origin set")
|
||||||
|
}
|
||||||
|
chain.ProcessFilter(req, resp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(c.AllowedDomains) > 0 { // if provided then origin must be included
|
||||||
|
included := false
|
||||||
|
for _, each := range c.AllowedDomains {
|
||||||
|
if each == origin {
|
||||||
|
included = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !included {
|
||||||
|
if trace {
|
||||||
|
traceLogger.Println("HTTP Origin:%s is not part of %v", origin, c.AllowedDomains)
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
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 _, each := range c.AllowedDomains {
|
||||||
|
if each == origin {
|
||||||
|
allowed = true
|
||||||
|
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
|
||||||
|
}
|
125
Godeps/_workspace/src/github.com/emicklei/go-restful/cors_filter_test.go
generated
vendored
Normal file
125
Godeps/_workspace/src/github.com/emicklei/go-restful/cors_filter_test.go
generated
vendored
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
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
|
||||||
|
accepted bool
|
||||||
|
}{
|
||||||
|
{[]string{}, "http://anything.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.accepted {
|
||||||
|
t.Fatal("expected to be accepted")
|
||||||
|
}
|
||||||
|
if actual == each.origin && !each.accepted {
|
||||||
|
t.Fatal("did not expect to be accepted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
2
Godeps/_workspace/src/github.com/emicklei/go-restful/coverage.sh
generated
vendored
Normal file
2
Godeps/_workspace/src/github.com/emicklei/go-restful/coverage.sh
generated
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
go test -coverprofile=coverage.out
|
||||||
|
go tool cover -html=coverage.out
|
162
Godeps/_workspace/src/github.com/emicklei/go-restful/curly.go
generated
vendored
Normal file
162
Godeps/_workspace/src/github.com/emicklei/go-restful/curly.go
generated
vendored
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
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) []Route {
|
||||||
|
candidates := &sortableCurlyRoutes{[]*curlyRoute{}}
|
||||||
|
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.routes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 []Route, httpRequest *http.Request) (*Route, error) {
|
||||||
|
// tracing is done inside detectRoute
|
||||||
|
return RouterJSR311{}.detectRoute(candidateRoutes, 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.compiledPathExpression().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
|
||||||
|
}
|
54
Godeps/_workspace/src/github.com/emicklei/go-restful/curly_route.go
generated
vendored
Normal file
54
Godeps/_workspace/src/github.com/emicklei/go-restful/curly_route.go
generated
vendored
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
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 struct {
|
||||||
|
candidates []*curlyRoute
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sortableCurlyRoutes) add(route *curlyRoute) {
|
||||||
|
s.candidates = append(s.candidates, route)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sortableCurlyRoutes) routes() (routes []Route) {
|
||||||
|
for _, each := range s.candidates {
|
||||||
|
routes = append(routes, each.route) // TODO change return type
|
||||||
|
}
|
||||||
|
return routes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sortableCurlyRoutes) Len() int {
|
||||||
|
return len(s.candidates)
|
||||||
|
}
|
||||||
|
func (s *sortableCurlyRoutes) Swap(i, j int) {
|
||||||
|
s.candidates[i], s.candidates[j] = s.candidates[j], s.candidates[i]
|
||||||
|
}
|
||||||
|
func (s *sortableCurlyRoutes) Less(i, j int) bool {
|
||||||
|
ci := s.candidates[i]
|
||||||
|
cj := s.candidates[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
|
||||||
|
}
|
228
Godeps/_workspace/src/github.com/emicklei/go-restful/curly_test.go
generated
vendored
Normal file
228
Godeps/_workspace/src/github.com/emicklei/go-restful/curly_test.go
generated
vendored
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
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.compiledPathExpression().Source, each.compiledPathExpression().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.compiledPathExpression().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))
|
||||||
|
routes := CurlyRouter{}.selectRoutes(ws1, tokenizePath("/network/12"))
|
||||||
|
if len(routes) != 2 {
|
||||||
|
t.Fatal("expected 2 routes")
|
||||||
|
}
|
||||||
|
if routes[0].Path != "/network/{id}" {
|
||||||
|
t.Error("first is", routes[0].Path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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))
|
||||||
|
routes := CurlyRouter{}.selectRoutes(ws1, tokenizePath("/network/12"))
|
||||||
|
if len(routes) != 2 {
|
||||||
|
t.Fatal("expected 2 routes")
|
||||||
|
}
|
||||||
|
if routes[0].Path != "/network/{id}" {
|
||||||
|
t.Error("first is", routes[0].Path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear && go test -v -test.run TestCurly_JsonHtml ...restful
|
||||||
|
func TestCurly_JsonHtml(t *testing.T) {
|
||||||
|
ws1 := new(WebService)
|
||||||
|
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))
|
||||||
|
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))
|
||||||
|
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") }
|
184
Godeps/_workspace/src/github.com/emicklei/go-restful/doc.go
generated
vendored
Normal file
184
Godeps/_workspace/src/github.com/emicklei/go-restful/doc.go
generated
vendored
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
/*
|
||||||
|
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.Router(CurlyRouter{})
|
||||||
|
|
||||||
|
The default router is the RouterJSR311 which is an implementation of its spec (http://jsr311.java.net/nonav/releases/1.1/spec/spec.html).
|
||||||
|
However, it uses regular expressions for all its routes which, depending on your usecase, may consume a significant amount of time.
|
||||||
|
The CurlyRouter implementation is more lightweight that also allows you to use wildcards and expressions, but only if needed.
|
||||||
|
|
||||||
|
restful.DefaultContainer.DoNotRecover(true)
|
||||||
|
|
||||||
|
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 false; it will recover from panics. This has performance implications.
|
||||||
|
|
||||||
|
restful.SetCacheReadEntity(false)
|
||||||
|
|
||||||
|
SetCacheReadEntity controls whether the response data ([]byte) is cached such that ReadEntity is repeatable.
|
||||||
|
If you expect to read large amounts of payload data, and you do not use this feature, you should set it to false.
|
||||||
|
|
||||||
|
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 a log.Logger instance such as:
|
||||||
|
|
||||||
|
restful.TraceLogger(log.New(os.Stdout, "[restful] ", log.LstdFlags|log.Lshortfile))
|
||||||
|
|
||||||
|
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-2014, http://ernestmicklei.com. MIT License
|
||||||
|
*/
|
||||||
|
package restful
|
35
Godeps/_workspace/src/github.com/emicklei/go-restful/doc_examples_test.go
generated
vendored
Normal file
35
Godeps/_workspace/src/github.com/emicklei/go-restful/doc_examples_test.go
generated
vendored
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
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"))
|
||||||
|
}
|
1
Godeps/_workspace/src/github.com/emicklei/go-restful/examples/.goconvey
generated
vendored
Normal file
1
Godeps/_workspace/src/github.com/emicklei/go-restful/examples/.goconvey
generated
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
ignore
|
1
Godeps/_workspace/src/github.com/emicklei/go-restful/examples/google_app_engine/.goconvey
generated
vendored
Normal file
1
Godeps/_workspace/src/github.com/emicklei/go-restful/examples/google_app_engine/.goconvey
generated
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
ignore
|
20
Godeps/_workspace/src/github.com/emicklei/go-restful/examples/google_app_engine/app.yaml
generated
vendored
Normal file
20
Godeps/_workspace/src/github.com/emicklei/go-restful/examples/google_app_engine/app.yaml
generated
vendored
Normal 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
|
1
Godeps/_workspace/src/github.com/emicklei/go-restful/examples/google_app_engine/datastore/.goconvey
generated
vendored
Normal file
1
Godeps/_workspace/src/github.com/emicklei/go-restful/examples/google_app_engine/datastore/.goconvey
generated
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
ignore
|
18
Godeps/_workspace/src/github.com/emicklei/go-restful/examples/google_app_engine/datastore/app.yaml
generated
vendored
Normal file
18
Godeps/_workspace/src/github.com/emicklei/go-restful/examples/google_app_engine/datastore/app.yaml
generated
vendored
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
application: datastore-example
|
||||||
|
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
|
266
Godeps/_workspace/src/github.com/emicklei/go-restful/examples/google_app_engine/datastore/main.go
generated
vendored
Normal file
266
Godeps/_workspace/src/github.com/emicklei/go-restful/examples/google_app_engine/datastore/main.go
generated
vendored
Normal file
@ -0,0 +1,266 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"appengine"
|
||||||
|
"appengine/datastore"
|
||||||
|
"appengine/user"
|
||||||
|
"github.com/emicklei/go-restful"
|
||||||
|
"github.com/emicklei/go-restful/swagger"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
13
Godeps/_workspace/src/github.com/emicklei/go-restful/examples/google_app_engine/restful-appstats-integration.go
generated
vendored
Normal file
13
Godeps/_workspace/src/github.com/emicklei/go-restful/examples/google_app_engine/restful-appstats-integration.go
generated
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
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()
|
||||||
|
}
|
161
Godeps/_workspace/src/github.com/emicklei/go-restful/examples/google_app_engine/restful-user-service.go
generated
vendored
Normal file
161
Godeps/_workspace/src/github.com/emicklei/go-restful/examples/google_app_engine/restful-user-service.go
generated
vendored
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"appengine"
|
||||||
|
"appengine/memcache"
|
||||||
|
"github.com/emicklei/go-restful"
|
||||||
|
"github.com/emicklei/go-restful/swagger"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
7
Godeps/_workspace/src/github.com/emicklei/go-restful/examples/home.html
generated
vendored
Normal file
7
Godeps/_workspace/src/github.com/emicklei/go-restful/examples/home.html
generated
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<h1>{{.Text}}</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
67
Godeps/_workspace/src/github.com/emicklei/go-restful/examples/restful-CORS-filter.go
generated
vendored
Normal file
67
Godeps/_workspace/src/github.com/emicklei/go-restful/examples/restful-CORS-filter.go
generated
vendored
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
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"},
|
||||||
|
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())
|
||||||
|
}
|
54
Godeps/_workspace/src/github.com/emicklei/go-restful/examples/restful-NCSA-logging.go
generated
vendored
Normal file
54
Godeps/_workspace/src/github.com/emicklei/go-restful/examples/restful-NCSA-logging.go
generated
vendored
Normal 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")
|
||||||
|
}
|
35
Godeps/_workspace/src/github.com/emicklei/go-restful/examples/restful-basic-authentication.go
generated
vendored
Normal file
35
Godeps/_workspace/src/github.com/emicklei/go-restful/examples/restful-basic-authentication.go
generated
vendored
Normal 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")
|
||||||
|
}
|
65
Godeps/_workspace/src/github.com/emicklei/go-restful/examples/restful-cpuprofiler-service.go
generated
vendored
Normal file
65
Godeps/_workspace/src/github.com/emicklei/go-restful/examples/restful-cpuprofiler-service.go
generated
vendored
Normal 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
|
107
Godeps/_workspace/src/github.com/emicklei/go-restful/examples/restful-curly-router.go
generated
vendored
Normal file
107
Godeps/_workspace/src/github.com/emicklei/go-restful/examples/restful-curly-router.go
generated
vendored
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/emicklei/go-restful"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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.WriteHeader(http.StatusCreated)
|
||||||
|
response.WriteEntity(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())
|
||||||
|
}
|
61
Godeps/_workspace/src/github.com/emicklei/go-restful/examples/restful-encoding-filter.go
generated
vendored
Normal file
61
Godeps/_workspace/src/github.com/emicklei/go-restful/examples/restful-encoding-filter.go
generated
vendored
Normal 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"})
|
||||||
|
}
|
114
Godeps/_workspace/src/github.com/emicklei/go-restful/examples/restful-filters.go
generated
vendored
Normal file
114
Godeps/_workspace/src/github.com/emicklei/go-restful/examples/restful-filters.go
generated
vendored
Normal 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{User{"42", "Gandalf"}, User{"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"})
|
||||||
|
}
|
62
Godeps/_workspace/src/github.com/emicklei/go-restful/examples/restful-form-handling.go
generated
vendored
Normal file
62
Godeps/_workspace/src/github.com/emicklei/go-restful/examples/restful-form-handling.go
generated
vendored
Normal 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>`)
|
||||||
|
}
|
22
Godeps/_workspace/src/github.com/emicklei/go-restful/examples/restful-hello-world.go
generated
vendored
Normal file
22
Godeps/_workspace/src/github.com/emicklei/go-restful/examples/restful-hello-world.go
generated
vendored
Normal 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")
|
||||||
|
}
|
35
Godeps/_workspace/src/github.com/emicklei/go-restful/examples/restful-html-template.go
generated
vendored
Normal file
35
Godeps/_workspace/src/github.com/emicklei/go-restful/examples/restful-html-template.go
generated
vendored
Normal 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)
|
||||||
|
}
|
43
Godeps/_workspace/src/github.com/emicklei/go-restful/examples/restful-multi-containers.go
generated
vendored
Normal file
43
Godeps/_workspace/src/github.com/emicklei/go-restful/examples/restful-multi-containers.go
generated
vendored
Normal 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")
|
||||||
|
}
|
51
Godeps/_workspace/src/github.com/emicklei/go-restful/examples/restful-options-filter.go
generated
vendored
Normal file
51
Godeps/_workspace/src/github.com/emicklei/go-restful/examples/restful-options-filter.go
generated
vendored
Normal 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())
|
||||||
|
}
|
26
Godeps/_workspace/src/github.com/emicklei/go-restful/examples/restful-path-tail.go
generated
vendored
Normal file
26
Godeps/_workspace/src/github.com/emicklei/go-restful/examples/restful-path-tail.go
generated
vendored
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
. "github.com/emicklei/go-restful"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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"))
|
||||||
|
}
|
98
Godeps/_workspace/src/github.com/emicklei/go-restful/examples/restful-pre-post-filters.go
generated
vendored
Normal file
98
Godeps/_workspace/src/github.com/emicklei/go-restful/examples/restful-pre-post-filters.go
generated
vendored
Normal 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")
|
||||||
|
}
|
63
Godeps/_workspace/src/github.com/emicklei/go-restful/examples/restful-resource-functions.go
generated
vendored
Normal file
63
Godeps/_workspace/src/github.com/emicklei/go-restful/examples/restful-resource-functions.go
generated
vendored
Normal 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)
|
||||||
|
}
|
39
Godeps/_workspace/src/github.com/emicklei/go-restful/examples/restful-route_test.go
generated
vendored
Normal file
39
Godeps/_workspace/src/github.com/emicklei/go-restful/examples/restful-route_test.go
generated
vendored
Normal 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")
|
||||||
|
}
|
29
Godeps/_workspace/src/github.com/emicklei/go-restful/examples/restful-routefunction_test.go
generated
vendored
Normal file
29
Godeps/_workspace/src/github.com/emicklei/go-restful/examples/restful-routefunction_test.go
generated
vendored
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/emicklei/go-restful"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This example show how to test one particular RouteFunction (getIt)
|
||||||
|
// It uses the httptest.ResponseRecorder to capture output
|
||||||
|
|
||||||
|
func getIt(req *restful.Request, resp *restful.Response) {
|
||||||
|
resp.WriteHeader(404)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCallFunction(t *testing.T) {
|
||||||
|
httpReq, _ := http.NewRequest("GET", "/", nil)
|
||||||
|
req := restful.NewRequest(httpReq)
|
||||||
|
|
||||||
|
recorder := new(httptest.ResponseRecorder)
|
||||||
|
resp := restful.NewResponse(recoder)
|
||||||
|
|
||||||
|
getIt(req, resp)
|
||||||
|
if recorder.Code != 404 {
|
||||||
|
t.Logf("Missing or wrong status code:%d", recorder.Code)
|
||||||
|
}
|
||||||
|
}
|
47
Godeps/_workspace/src/github.com/emicklei/go-restful/examples/restful-serve-static.go
generated
vendored
Normal file
47
Godeps/_workspace/src/github.com/emicklei/go-restful/examples/restful-serve-static.go
generated
vendored
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"github.com/emicklei/go-restful"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This example shows how to define methods that serve static files
|
||||||
|
// It uses the standard http.ServeFile method
|
||||||
|
//
|
||||||
|
// GET http://localhost:8080/static/test.xml
|
||||||
|
// GET http://localhost:8080/static/
|
||||||
|
//
|
||||||
|
// GET http://localhost:8080/static?resource=subdir/test.xml
|
||||||
|
|
||||||
|
var rootdir = "/tmp"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
restful.DefaultContainer.Router(restful.CurlyRouter{})
|
||||||
|
|
||||||
|
ws := new(restful.WebService)
|
||||||
|
ws.Route(ws.GET("/static/{subpath:*}").To(staticFromPathParam))
|
||||||
|
ws.Route(ws.GET("/static").To(staticFromQueryParam))
|
||||||
|
restful.Add(ws)
|
||||||
|
|
||||||
|
println("[go-restful] serving files on http://localhost:8080/static from local /tmp")
|
||||||
|
http.ListenAndServe(":8080", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func staticFromPathParam(req *restful.Request, resp *restful.Response) {
|
||||||
|
actual := path.Join(rootdir, req.PathParameter("subpath"))
|
||||||
|
fmt.Printf("serving %s ... (from %s)\n", actual, req.PathParameter("subpath"))
|
||||||
|
http.ServeFile(
|
||||||
|
resp.ResponseWriter,
|
||||||
|
req.Request,
|
||||||
|
actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
func staticFromQueryParam(req *restful.Request, resp *restful.Response) {
|
||||||
|
http.ServeFile(
|
||||||
|
resp.ResponseWriter,
|
||||||
|
req.Request,
|
||||||
|
path.Join(rootdir, req.QueryParameter("resource")))
|
||||||
|
}
|
153
Godeps/_workspace/src/github.com/emicklei/go-restful/examples/restful-user-resource.go
generated
vendored
Normal file
153
Godeps/_workspace/src/github.com/emicklei/go-restful/examples/restful-user-resource.go
generated
vendored
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/emicklei/go-restful"
|
||||||
|
"github.com/emicklei/go-restful/swagger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This example show a complete (GET,PUT,POST,DELETE) conventional example of
|
||||||
|
// a REST Resource including documentation to be served by e.g. a Swagger UI
|
||||||
|
// It is recommended to create a Resource struct (UserResource) that can encapsulate
|
||||||
|
// an object that provide domain access (a DAO)
|
||||||
|
// It has a Register method including the complete Route mapping to methods together
|
||||||
|
// with all the appropriate documentation
|
||||||
|
//
|
||||||
|
// 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").
|
||||||
|
Doc("Manage 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").
|
||||||
|
Operation("findUser").
|
||||||
|
Param(ws.PathParameter("user-id", "identifier of the user").DataType("string")).
|
||||||
|
Writes(User{})) // on the response
|
||||||
|
|
||||||
|
ws.Route(ws.PUT("/{user-id}").To(u.updateUser).
|
||||||
|
// docs
|
||||||
|
Doc("update a user").
|
||||||
|
Operation("updateUser").
|
||||||
|
Param(ws.PathParameter("user-id", "identifier of the user").DataType("string")).
|
||||||
|
ReturnsError(409, "duplicate user-id", nil).
|
||||||
|
Reads(User{})) // from the request
|
||||||
|
|
||||||
|
ws.Route(ws.POST("").To(u.createUser).
|
||||||
|
// docs
|
||||||
|
Doc("create a user").
|
||||||
|
Operation("createUser").
|
||||||
|
Reads(User{})) // from the request
|
||||||
|
|
||||||
|
ws.Route(ws.DELETE("/{user-id}").To(u.removeUser).
|
||||||
|
// docs
|
||||||
|
Doc("delete a user").
|
||||||
|
Operation("removeUser").
|
||||||
|
Param(ws.PathParameter("user-id", "identifier of the user").DataType("string")))
|
||||||
|
|
||||||
|
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, "404: User could not be found.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.WriteEntity(usr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST http://localhost:8080/users
|
||||||
|
// <User><Name>Melissa</Name></User>
|
||||||
|
//
|
||||||
|
func (u *UserResource) createUser(request *restful.Request, response *restful.Response) {
|
||||||
|
usr := new(User)
|
||||||
|
err := request.ReadEntity(usr)
|
||||||
|
if err != nil {
|
||||||
|
response.AddHeader("Content-Type", "text/plain")
|
||||||
|
response.WriteErrorString(http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
usr.Id = strconv.Itoa(len(u.users) + 1) // simple id generation
|
||||||
|
u.users[usr.Id] = *usr
|
||||||
|
response.WriteHeader(http.StatusCreated)
|
||||||
|
response.WriteEntity(usr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PUT http://localhost:8080/users/1
|
||||||
|
// <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 {
|
||||||
|
response.AddHeader("Content-Type", "text/plain")
|
||||||
|
response.WriteErrorString(http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
u.users[usr.Id] = *usr
|
||||||
|
response.WriteEntity(usr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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() {
|
||||||
|
// to see what happens in the package, uncomment the following
|
||||||
|
//restful.TraceLogger(log.New(os.Stdout, "[restful] ", log.LstdFlags|log.Lshortfile))
|
||||||
|
|
||||||
|
wsContainer := restful.NewContainer()
|
||||||
|
u := UserResource{map[string]User{}}
|
||||||
|
u.Register(wsContainer)
|
||||||
|
|
||||||
|
// 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 http://localhost:8080/apidocs and enter http://localhost:8080/apidocs.json in the api input field.
|
||||||
|
config := swagger.Config{
|
||||||
|
WebServices: wsContainer.RegisteredWebServices(), // you control what services are visible
|
||||||
|
WebServicesUrl: "http://localhost:8080",
|
||||||
|
ApiPath: "/apidocs.json",
|
||||||
|
|
||||||
|
// Optionally, specifiy where the UI is located
|
||||||
|
SwaggerPath: "/apidocs/",
|
||||||
|
SwaggerFilePath: "/Users/emicklei/xProjects/swagger-ui/dist"}
|
||||||
|
swagger.RegisterSwaggerService(config, wsContainer)
|
||||||
|
|
||||||
|
log.Printf("start listening on localhost:8080")
|
||||||
|
server := &http.Server{Addr: ":8080", Handler: wsContainer}
|
||||||
|
log.Fatal(server.ListenAndServe())
|
||||||
|
}
|
138
Godeps/_workspace/src/github.com/emicklei/go-restful/examples/restful-user-service.go
generated
vendored
Normal file
138
Godeps/_workspace/src/github.com/emicklei/go-restful/examples/restful-user-service.go
generated
vendored
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/emicklei/go-restful"
|
||||||
|
"github.com/emicklei/go-restful/swagger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This example is functionally the same as the example in restful-user-resource.go
|
||||||
|
// with the only difference that is served using the restful.DefaultContainer
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
Id, Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserService struct {
|
||||||
|
// normally one would use DAO (data access object)
|
||||||
|
users map[string]User
|
||||||
|
}
|
||||||
|
|
||||||
|
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("/").To(u.findAllUsers).
|
||||||
|
// docs
|
||||||
|
Doc("get all users").
|
||||||
|
Operation("findAllUsers").
|
||||||
|
Returns(200, "OK", []User{}))
|
||||||
|
|
||||||
|
ws.Route(ws.GET("/{user-id}").To(u.findUser).
|
||||||
|
// docs
|
||||||
|
Doc("get a user").
|
||||||
|
Operation("findUser").
|
||||||
|
Param(ws.PathParameter("user-id", "identifier of the user").DataType("string")).
|
||||||
|
Writes(User{})) // on the response
|
||||||
|
|
||||||
|
ws.Route(ws.PUT("/{user-id}").To(u.updateUser).
|
||||||
|
// docs
|
||||||
|
Doc("update a user").
|
||||||
|
Operation("updateUser").
|
||||||
|
Param(ws.PathParameter("user-id", "identifier of the user").DataType("string")).
|
||||||
|
Reads(User{})) // from the request
|
||||||
|
|
||||||
|
ws.Route(ws.PUT("").To(u.createUser).
|
||||||
|
// docs
|
||||||
|
Doc("create a user").
|
||||||
|
Operation("createUser").
|
||||||
|
Reads(User{})) // from the request
|
||||||
|
|
||||||
|
ws.Route(ws.DELETE("/{user-id}").To(u.removeUser).
|
||||||
|
// docs
|
||||||
|
Doc("delete a user").
|
||||||
|
Operation("removeUser").
|
||||||
|
Param(ws.PathParameter("user-id", "identifier of the user").DataType("string")))
|
||||||
|
|
||||||
|
restful.Add(ws)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET http://localhost:8080/users
|
||||||
|
//
|
||||||
|
func (u UserService) findAllUsers(request *restful.Request, response *restful.Response) {
|
||||||
|
response.WriteEntity(u.users)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET http://localhost:8080/users/1
|
||||||
|
//
|
||||||
|
func (u UserService) findUser(request *restful.Request, response *restful.Response) {
|
||||||
|
id := request.PathParameter("user-id")
|
||||||
|
usr := u.users[id]
|
||||||
|
if len(usr.Id) == 0 {
|
||||||
|
response.WriteErrorString(http.StatusNotFound, "User could not be found.")
|
||||||
|
} else {
|
||||||
|
response.WriteEntity(usr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PUT http://localhost:8080/users/1
|
||||||
|
// <User><Id>1</Id><Name>Melissa Raspberry</Name></User>
|
||||||
|
//
|
||||||
|
func (u *UserService) 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.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) {
|
||||||
|
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.WriteError(http.StatusInternalServerError, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE http://localhost:8080/users/1
|
||||||
|
//
|
||||||
|
func (u *UserService) removeUser(request *restful.Request, response *restful.Response) {
|
||||||
|
id := request.PathParameter("user-id")
|
||||||
|
delete(u.users, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
u := UserService{map[string]User{}}
|
||||||
|
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 http://localhost:8080/apidocs and enter http://localhost:8080/apidocs.json in the api input field.
|
||||||
|
config := swagger.Config{
|
||||||
|
WebServices: restful.RegisteredWebServices(), // you control what services are visible
|
||||||
|
WebServicesUrl: "http://localhost:8080",
|
||||||
|
ApiPath: "/apidocs.json",
|
||||||
|
|
||||||
|
// Optionally, specifiy where the UI is located
|
||||||
|
SwaggerPath: "/apidocs/",
|
||||||
|
SwaggerFilePath: "/Users/emicklei/Projects/swagger-ui/dist"}
|
||||||
|
swagger.InstallSwaggerService(config)
|
||||||
|
|
||||||
|
log.Printf("start listening on localhost:8080")
|
||||||
|
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||||
|
}
|
26
Godeps/_workspace/src/github.com/emicklei/go-restful/filter.go
generated
vendored
Normal file
26
Godeps/_workspace/src/github.com/emicklei/go-restful/filter.go
generated
vendored
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
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.
|
||||||
|
|
||||||
|
// FilterChain is a request scoped object to process one or more filters before calling the target RouteFunction.
|
||||||
|
type FilterChain struct {
|
||||||
|
Filters []FilterFunction // ordered list of FilterFunction
|
||||||
|
Index int // index into filters that is currently in progress
|
||||||
|
Target RouteFunction // function to call after passing all filters
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessFilter passes the request,response pair through the next of Filters.
|
||||||
|
// Each filter can decide to proceed to the next Filter or handle the Response itself.
|
||||||
|
func (f *FilterChain) ProcessFilter(request *Request, response *Response) {
|
||||||
|
if f.Index < len(f.Filters) {
|
||||||
|
f.Index++
|
||||||
|
f.Filters[f.Index-1](request, response, f)
|
||||||
|
} else {
|
||||||
|
f.Target(request, response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilterFunction definitions must call ProcessFilter on the FilterChain to pass on the control and eventually call the RouteFunction
|
||||||
|
type FilterFunction func(*Request, *Response, *FilterChain)
|
141
Godeps/_workspace/src/github.com/emicklei/go-restful/filter_test.go
generated
vendored
Normal file
141
Godeps/_workspace/src/github.com/emicklei/go-restful/filter_test.go
generated
vendored
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
package restful
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setupServices(addGlobalFilter bool, addServiceFilter bool, addRouteFilter bool) {
|
||||||
|
if addGlobalFilter {
|
||||||
|
Filter(globalFilter)
|
||||||
|
}
|
||||||
|
Add(newTestService(addServiceFilter, addRouteFilter))
|
||||||
|
}
|
||||||
|
|
||||||
|
func tearDown() {
|
||||||
|
DefaultContainer.webServices = []*WebService{}
|
||||||
|
DefaultContainer.isRegisteredOnRoot = true // this allows for setupServices multiple times
|
||||||
|
DefaultContainer.containerFilters = []FilterFunction{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTestService(addServiceFilter bool, addRouteFilter bool) *WebService {
|
||||||
|
ws := new(WebService).Path("")
|
||||||
|
if addServiceFilter {
|
||||||
|
ws.Filter(serviceFilter)
|
||||||
|
}
|
||||||
|
rb := ws.GET("/foo").To(foo)
|
||||||
|
if addRouteFilter {
|
||||||
|
rb.Filter(routeFilter)
|
||||||
|
}
|
||||||
|
ws.Route(rb)
|
||||||
|
ws.Route(ws.GET("/bar").To(bar))
|
||||||
|
return ws
|
||||||
|
}
|
||||||
|
|
||||||
|
func foo(req *Request, resp *Response) {
|
||||||
|
io.WriteString(resp.ResponseWriter, "foo")
|
||||||
|
}
|
||||||
|
|
||||||
|
func bar(req *Request, resp *Response) {
|
||||||
|
io.WriteString(resp.ResponseWriter, "bar")
|
||||||
|
}
|
||||||
|
|
||||||
|
func fail(req *Request, resp *Response) {
|
||||||
|
http.Error(resp.ResponseWriter, "something failed", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
func globalFilter(req *Request, resp *Response, chain *FilterChain) {
|
||||||
|
io.WriteString(resp.ResponseWriter, "global-")
|
||||||
|
chain.ProcessFilter(req, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func serviceFilter(req *Request, resp *Response, chain *FilterChain) {
|
||||||
|
io.WriteString(resp.ResponseWriter, "service-")
|
||||||
|
chain.ProcessFilter(req, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func routeFilter(req *Request, resp *Response, chain *FilterChain) {
|
||||||
|
io.WriteString(resp.ResponseWriter, "route-")
|
||||||
|
chain.ProcessFilter(req, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNoFilter(t *testing.T) {
|
||||||
|
tearDown()
|
||||||
|
setupServices(false, false, false)
|
||||||
|
actual := sendIt("http://example.com/foo")
|
||||||
|
if "foo" != actual {
|
||||||
|
t.Fatal("expected: foo but got:" + actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGlobalFilter(t *testing.T) {
|
||||||
|
tearDown()
|
||||||
|
setupServices(true, false, false)
|
||||||
|
actual := sendIt("http://example.com/foo")
|
||||||
|
if "global-foo" != actual {
|
||||||
|
t.Fatal("expected: global-foo but got:" + actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWebServiceFilter(t *testing.T) {
|
||||||
|
tearDown()
|
||||||
|
setupServices(true, true, false)
|
||||||
|
actual := sendIt("http://example.com/foo")
|
||||||
|
if "global-service-foo" != actual {
|
||||||
|
t.Fatal("expected: global-service-foo but got:" + actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRouteFilter(t *testing.T) {
|
||||||
|
tearDown()
|
||||||
|
setupServices(true, true, true)
|
||||||
|
actual := sendIt("http://example.com/foo")
|
||||||
|
if "global-service-route-foo" != actual {
|
||||||
|
t.Fatal("expected: global-service-route-foo but got:" + actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRouteFilterOnly(t *testing.T) {
|
||||||
|
tearDown()
|
||||||
|
setupServices(false, false, true)
|
||||||
|
actual := sendIt("http://example.com/foo")
|
||||||
|
if "route-foo" != actual {
|
||||||
|
t.Fatal("expected: route-foo but got:" + actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBar(t *testing.T) {
|
||||||
|
tearDown()
|
||||||
|
setupServices(false, true, false)
|
||||||
|
actual := sendIt("http://example.com/bar")
|
||||||
|
if "service-bar" != actual {
|
||||||
|
t.Fatal("expected: service-bar but got:" + actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAllFiltersBar(t *testing.T) {
|
||||||
|
tearDown()
|
||||||
|
setupServices(true, true, true)
|
||||||
|
actual := sendIt("http://example.com/bar")
|
||||||
|
if "global-service-bar" != actual {
|
||||||
|
t.Fatal("expected: global-service-bar but got:" + actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendIt(address string) string {
|
||||||
|
httpRequest, _ := http.NewRequest("GET", address, nil)
|
||||||
|
httpRequest.Header.Set("Accept", "*/*")
|
||||||
|
httpWriter := httptest.NewRecorder()
|
||||||
|
DefaultContainer.dispatch(httpWriter, httpRequest)
|
||||||
|
return httpWriter.Body.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendItTo(address string, container *Container) string {
|
||||||
|
httpRequest, _ := http.NewRequest("GET", address, nil)
|
||||||
|
httpRequest.Header.Set("Accept", "*/*")
|
||||||
|
httpWriter := httptest.NewRecorder()
|
||||||
|
container.dispatch(httpWriter, httpRequest)
|
||||||
|
return httpWriter.Body.String()
|
||||||
|
}
|
9
Godeps/_workspace/src/github.com/emicklei/go-restful/install.sh
generated
vendored
Normal file
9
Godeps/_workspace/src/github.com/emicklei/go-restful/install.sh
generated
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
cd examples
|
||||||
|
ls *.go | xargs -I {} go build {}
|
||||||
|
cd ..
|
||||||
|
go fmt ...swagger && \
|
||||||
|
go test -test.v ...swagger && \
|
||||||
|
go install ...swagger && \
|
||||||
|
go fmt ...restful && \
|
||||||
|
go test -test.v ...restful && \
|
||||||
|
go install ...restful
|
248
Godeps/_workspace/src/github.com/emicklei/go-restful/jsr311.go
generated
vendored
Normal file
248
Godeps/_workspace/src/github.com/emicklei/go-restful/jsr311.go
generated
vendored
Normal file
@ -0,0 +1,248 @@
|
|||||||
|
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 (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"sort"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RouterJSR311 implements the flow for matching Requests to Routes (and consequently Resource Functions)
|
||||||
|
// as specified by the JSR311 http://jsr311.java.net/nonav/releases/1.1/spec/spec.html.
|
||||||
|
// RouterJSR311 implements the Router interface.
|
||||||
|
// Concept of locators is not implemented.
|
||||||
|
type RouterJSR311 struct{}
|
||||||
|
|
||||||
|
// SelectRoute is part of the Router interface and returns the best match
|
||||||
|
// for the WebService and its Route for the given Request.
|
||||||
|
func (r RouterJSR311) SelectRoute(
|
||||||
|
webServices []*WebService,
|
||||||
|
httpRequest *http.Request) (selectedService *WebService, selectedRoute *Route, err error) {
|
||||||
|
|
||||||
|
// Identify the root resource class (WebService)
|
||||||
|
dispatcher, finalMatch, err := r.detectDispatcher(httpRequest.URL.Path, webServices)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, NewError(http.StatusNotFound, "")
|
||||||
|
}
|
||||||
|
// Obtain the set of candidate methods (Routes)
|
||||||
|
routes := r.selectRoutes(dispatcher, finalMatch)
|
||||||
|
if len(routes) == 0 {
|
||||||
|
return dispatcher, nil, NewError(http.StatusNotFound, "404: Page Not Found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Identify the method (Route) that will handle the request
|
||||||
|
route, ok := r.detectRoute(routes, httpRequest)
|
||||||
|
return dispatcher, route, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// http://jsr311.java.net/nonav/releases/1.1/spec/spec3.html#x3-360003.7.2
|
||||||
|
func (r RouterJSR311) detectRoute(routes []Route, httpRequest *http.Request) (*Route, error) {
|
||||||
|
// http method
|
||||||
|
methodOk := []Route{}
|
||||||
|
for _, each := range routes {
|
||||||
|
if httpRequest.Method == each.Method {
|
||||||
|
methodOk = append(methodOk, each)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(methodOk) == 0 {
|
||||||
|
if trace {
|
||||||
|
traceLogger.Printf("no Route found (in %d routes) that matches HTTP method %s\n", len(routes), httpRequest.Method)
|
||||||
|
}
|
||||||
|
return nil, NewError(http.StatusMethodNotAllowed, "405: Method Not Allowed")
|
||||||
|
}
|
||||||
|
inputMediaOk := methodOk
|
||||||
|
// content-type
|
||||||
|
contentType := httpRequest.Header.Get(HEADER_ContentType)
|
||||||
|
if httpRequest.ContentLength > 0 {
|
||||||
|
inputMediaOk = []Route{}
|
||||||
|
for _, each := range methodOk {
|
||||||
|
if each.matchesContentType(contentType) {
|
||||||
|
inputMediaOk = append(inputMediaOk, each)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(inputMediaOk) == 0 {
|
||||||
|
if trace {
|
||||||
|
traceLogger.Printf("no Route found (from %d) that matches HTTP Content-Type: %s\n", len(methodOk), contentType)
|
||||||
|
}
|
||||||
|
return nil, NewError(http.StatusUnsupportedMediaType, "415: Unsupported Media Type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// accept
|
||||||
|
outputMediaOk := []Route{}
|
||||||
|
accept := httpRequest.Header.Get(HEADER_Accept)
|
||||||
|
if accept == "" {
|
||||||
|
accept = "*/*"
|
||||||
|
}
|
||||||
|
for _, each := range inputMediaOk {
|
||||||
|
if each.matchesAccept(accept) {
|
||||||
|
outputMediaOk = append(outputMediaOk, each)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(outputMediaOk) == 0 {
|
||||||
|
if trace {
|
||||||
|
traceLogger.Printf("no Route found (from %d) that matches HTTP Accept: %s\n", len(inputMediaOk), accept)
|
||||||
|
}
|
||||||
|
return nil, NewError(http.StatusNotAcceptable, "406: Not Acceptable")
|
||||||
|
}
|
||||||
|
return r.bestMatchByMedia(outputMediaOk, contentType, accept), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// http://jsr311.java.net/nonav/releases/1.1/spec/spec3.html#x3-360003.7.2
|
||||||
|
// n/m > n/* > */*
|
||||||
|
func (r RouterJSR311) bestMatchByMedia(routes []Route, contentType string, accept string) *Route {
|
||||||
|
// TODO
|
||||||
|
return &routes[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// http://jsr311.java.net/nonav/releases/1.1/spec/spec3.html#x3-360003.7.2 (step 2)
|
||||||
|
func (r RouterJSR311) selectRoutes(dispatcher *WebService, pathRemainder string) []Route {
|
||||||
|
filtered := &sortableRouteCandidates{}
|
||||||
|
for _, each := range dispatcher.Routes() {
|
||||||
|
pathExpr := each.pathExpr
|
||||||
|
matches := pathExpr.Matcher.FindStringSubmatch(pathRemainder)
|
||||||
|
if matches != nil {
|
||||||
|
lastMatch := matches[len(matches)-1]
|
||||||
|
if len(lastMatch) == 0 || lastMatch == "/" { // do not include if value is neither empty nor ‘/’.
|
||||||
|
filtered.candidates = append(filtered.candidates,
|
||||||
|
routeCandidate{each, len(matches) - 1, pathExpr.LiteralCount, pathExpr.VarCount})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(filtered.candidates) == 0 {
|
||||||
|
if trace {
|
||||||
|
traceLogger.Printf("WebService on path %s has no routes that match URL path remainder:%s\n", dispatcher.rootPath, pathRemainder)
|
||||||
|
}
|
||||||
|
return []Route{}
|
||||||
|
}
|
||||||
|
sort.Sort(sort.Reverse(filtered))
|
||||||
|
|
||||||
|
// select other routes from candidates whoes expression matches rmatch
|
||||||
|
matchingRoutes := []Route{filtered.candidates[0].route}
|
||||||
|
for c := 1; c < len(filtered.candidates); c++ {
|
||||||
|
each := filtered.candidates[c]
|
||||||
|
if each.route.pathExpr.Matcher.MatchString(pathRemainder) {
|
||||||
|
matchingRoutes = append(matchingRoutes, each.route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return matchingRoutes
|
||||||
|
}
|
||||||
|
|
||||||
|
// http://jsr311.java.net/nonav/releases/1.1/spec/spec3.html#x3-360003.7.2 (step 1)
|
||||||
|
func (r RouterJSR311) detectDispatcher(requestPath string, dispatchers []*WebService) (*WebService, string, error) {
|
||||||
|
filtered := &sortableDispatcherCandidates{}
|
||||||
|
for _, each := range dispatchers {
|
||||||
|
pathExpr := each.compiledPathExpression()
|
||||||
|
matches := pathExpr.Matcher.FindStringSubmatch(requestPath)
|
||||||
|
if matches != nil {
|
||||||
|
filtered.candidates = append(filtered.candidates,
|
||||||
|
dispatcherCandidate{each, matches[len(matches)-1], len(matches), pathExpr.LiteralCount, pathExpr.VarCount})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(filtered.candidates) == 0 {
|
||||||
|
if trace {
|
||||||
|
traceLogger.Printf("no WebService was found to match URL path:%s\n", requestPath)
|
||||||
|
}
|
||||||
|
return nil, "", errors.New("not found")
|
||||||
|
}
|
||||||
|
sort.Sort(sort.Reverse(filtered))
|
||||||
|
return filtered.candidates[0].dispatcher, filtered.candidates[0].finalMatch, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Types and functions to support the sorting of Routes
|
||||||
|
|
||||||
|
type routeCandidate struct {
|
||||||
|
route Route
|
||||||
|
matchesCount int // the number of capturing groups
|
||||||
|
literalCount int // the number of literal characters (means those not resulting from template variable substitution)
|
||||||
|
nonDefaultCount int // the number of capturing groups with non-default regular expressions (i.e. not ‘([^ /]+?)’)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r routeCandidate) expressionToMatch() string {
|
||||||
|
return r.route.pathExpr.Source
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r routeCandidate) String() string {
|
||||||
|
return fmt.Sprintf("(m=%d,l=%d,n=%d)", r.matchesCount, r.literalCount, r.nonDefaultCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
type sortableRouteCandidates struct {
|
||||||
|
candidates []routeCandidate
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rcs *sortableRouteCandidates) Len() int {
|
||||||
|
return len(rcs.candidates)
|
||||||
|
}
|
||||||
|
func (rcs *sortableRouteCandidates) Swap(i, j int) {
|
||||||
|
rcs.candidates[i], rcs.candidates[j] = rcs.candidates[j], rcs.candidates[i]
|
||||||
|
}
|
||||||
|
func (rcs *sortableRouteCandidates) Less(i, j int) bool {
|
||||||
|
ci := rcs.candidates[i]
|
||||||
|
cj := rcs.candidates[j]
|
||||||
|
// primary key
|
||||||
|
if ci.literalCount < cj.literalCount {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if ci.literalCount > cj.literalCount {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// secundary key
|
||||||
|
if ci.matchesCount < cj.matchesCount {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if ci.matchesCount > cj.matchesCount {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// tertiary key
|
||||||
|
if ci.nonDefaultCount < cj.nonDefaultCount {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if ci.nonDefaultCount > cj.nonDefaultCount {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// quaternary key ("source" is interpreted as Path)
|
||||||
|
return ci.route.Path < cj.route.Path
|
||||||
|
}
|
||||||
|
|
||||||
|
// Types and functions to support the sorting of Dispatchers
|
||||||
|
|
||||||
|
type dispatcherCandidate struct {
|
||||||
|
dispatcher *WebService
|
||||||
|
finalMatch string
|
||||||
|
matchesCount int // the number of capturing groups
|
||||||
|
literalCount int // the number of literal characters (means those not resulting from template variable substitution)
|
||||||
|
nonDefaultCount int // the number of capturing groups with non-default regular expressions (i.e. not ‘([^ /]+?)’)
|
||||||
|
}
|
||||||
|
type sortableDispatcherCandidates struct {
|
||||||
|
candidates []dispatcherCandidate
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dc *sortableDispatcherCandidates) Len() int {
|
||||||
|
return len(dc.candidates)
|
||||||
|
}
|
||||||
|
func (dc *sortableDispatcherCandidates) Swap(i, j int) {
|
||||||
|
dc.candidates[i], dc.candidates[j] = dc.candidates[j], dc.candidates[i]
|
||||||
|
}
|
||||||
|
func (dc *sortableDispatcherCandidates) Less(i, j int) bool {
|
||||||
|
ci := dc.candidates[i]
|
||||||
|
cj := dc.candidates[j]
|
||||||
|
// primary key
|
||||||
|
if ci.matchesCount < cj.matchesCount {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if ci.matchesCount > cj.matchesCount {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// secundary key
|
||||||
|
if ci.literalCount < cj.literalCount {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if ci.literalCount > cj.literalCount {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// tertiary key
|
||||||
|
return ci.nonDefaultCount < cj.nonDefaultCount
|
||||||
|
}
|
231
Godeps/_workspace/src/github.com/emicklei/go-restful/jsr311_test.go
generated
vendored
Normal file
231
Godeps/_workspace/src/github.com/emicklei/go-restful/jsr311_test.go
generated
vendored
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
package restful
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"sort"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
//
|
||||||
|
// Step 1 tests
|
||||||
|
//
|
||||||
|
var paths = []struct {
|
||||||
|
// url with path (1) is handled by service with root (2) and last capturing group has value final (3)
|
||||||
|
path, root, final string
|
||||||
|
}{
|
||||||
|
{"/", "/", "/"},
|
||||||
|
{"/p", "/p", ""},
|
||||||
|
{"/p/x", "/p/{q}", ""},
|
||||||
|
{"/q/x", "/q", "/x"},
|
||||||
|
{"/p/x/", "/p/{q}", "/"},
|
||||||
|
{"/p/x/y", "/p/{q}", "/y"},
|
||||||
|
{"/q/x/y", "/q", "/x/y"},
|
||||||
|
{"/z/q", "/{p}/q", ""},
|
||||||
|
{"/a/b/c/q", "/", "/a/b/c/q"},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDetectDispatcher(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}")
|
||||||
|
ws6 := new(WebService).Path("/p/{q}/")
|
||||||
|
ws7 := new(WebService).Path("/{p}/q")
|
||||||
|
var dispatchers = []*WebService{ws1, ws2, ws3, ws4, ws5, ws6, ws7}
|
||||||
|
|
||||||
|
router := RouterJSR311{}
|
||||||
|
|
||||||
|
ok := true
|
||||||
|
for i, fixture := range paths {
|
||||||
|
who, final, err := router.detectDispatcher(fixture.path, dispatchers)
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("error in detection:%v", err)
|
||||||
|
ok = false
|
||||||
|
}
|
||||||
|
if who.RootPath() != fixture.root {
|
||||||
|
t.Logf("[line:%v] Unexpected dispatcher, expected:%v, actual:%v", i, fixture.root, who.RootPath())
|
||||||
|
ok = false
|
||||||
|
}
|
||||||
|
if final != fixture.final {
|
||||||
|
t.Logf("[line:%v] Unexpected final, expected:%v, actual:%v", i, fixture.final, final)
|
||||||
|
ok = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Step 2 tests
|
||||||
|
//
|
||||||
|
|
||||||
|
// go test -v -test.run TestISSUE_30 ...restful
|
||||||
|
func TestISSUE_30(t *testing.T) {
|
||||||
|
ws1 := new(WebService).Path("/users")
|
||||||
|
ws1.Route(ws1.GET("/{id}").To(dummy))
|
||||||
|
ws1.Route(ws1.POST("/login").To(dummy))
|
||||||
|
routes := RouterJSR311{}.selectRoutes(ws1, "/login")
|
||||||
|
if len(routes) != 2 {
|
||||||
|
t.Fatal("expected 2 routes")
|
||||||
|
}
|
||||||
|
if routes[0].Path != "/users/login" {
|
||||||
|
t.Error("first is", routes[0].Path)
|
||||||
|
t.Logf("routes:%v", routes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// go test -v -test.run TestISSUE_34 ...restful
|
||||||
|
func TestISSUE_34(t *testing.T) {
|
||||||
|
ws1 := new(WebService).Path("/")
|
||||||
|
ws1.Route(ws1.GET("/{type}/{id}").To(dummy))
|
||||||
|
ws1.Route(ws1.GET("/network/{id}").To(dummy))
|
||||||
|
routes := RouterJSR311{}.selectRoutes(ws1, "/network/12")
|
||||||
|
if len(routes) != 2 {
|
||||||
|
t.Fatal("expected 2 routes")
|
||||||
|
}
|
||||||
|
if routes[0].Path != "/network/{id}" {
|
||||||
|
t.Error("first is", routes[0].Path)
|
||||||
|
t.Logf("routes:%v", routes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// go test -v -test.run TestISSUE_34_2 ...restful
|
||||||
|
func TestISSUE_34_2(t *testing.T) {
|
||||||
|
ws1 := new(WebService).Path("/")
|
||||||
|
// change the registration order
|
||||||
|
ws1.Route(ws1.GET("/network/{id}").To(dummy))
|
||||||
|
ws1.Route(ws1.GET("/{type}/{id}").To(dummy))
|
||||||
|
routes := RouterJSR311{}.selectRoutes(ws1, "/network/12")
|
||||||
|
if len(routes) != 2 {
|
||||||
|
t.Fatal("expected 2 routes")
|
||||||
|
}
|
||||||
|
if routes[0].Path != "/network/{id}" {
|
||||||
|
t.Error("first is", routes[0].Path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// go test -v -test.run TestISSUE_137 ...restful
|
||||||
|
func TestISSUE_137(t *testing.T) {
|
||||||
|
ws1 := new(WebService)
|
||||||
|
ws1.Route(ws1.GET("/hello").To(dummy))
|
||||||
|
routes := RouterJSR311{}.selectRoutes(ws1, "/")
|
||||||
|
t.Log(routes)
|
||||||
|
if len(routes) > 0 {
|
||||||
|
t.Error("no route expected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSelectRoutesSlash(t *testing.T) {
|
||||||
|
ws1 := new(WebService).Path("/")
|
||||||
|
ws1.Route(ws1.GET("").To(dummy))
|
||||||
|
ws1.Route(ws1.GET("/").To(dummy))
|
||||||
|
ws1.Route(ws1.GET("/u").To(dummy))
|
||||||
|
ws1.Route(ws1.POST("/u").To(dummy))
|
||||||
|
ws1.Route(ws1.POST("/u/v").To(dummy))
|
||||||
|
ws1.Route(ws1.POST("/u/{w}").To(dummy))
|
||||||
|
ws1.Route(ws1.POST("/u/{w}/z").To(dummy))
|
||||||
|
routes := RouterJSR311{}.selectRoutes(ws1, "/u")
|
||||||
|
checkRoutesContains(routes, "/u", t)
|
||||||
|
checkRoutesContainsNo(routes, "/u/v", t)
|
||||||
|
checkRoutesContainsNo(routes, "/", t)
|
||||||
|
checkRoutesContainsNo(routes, "/u/{w}/z", t)
|
||||||
|
}
|
||||||
|
func TestSelectRoutesU(t *testing.T) {
|
||||||
|
ws1 := new(WebService).Path("/u")
|
||||||
|
ws1.Route(ws1.GET("").To(dummy))
|
||||||
|
ws1.Route(ws1.GET("/").To(dummy))
|
||||||
|
ws1.Route(ws1.GET("/v").To(dummy))
|
||||||
|
ws1.Route(ws1.POST("/{w}").To(dummy))
|
||||||
|
ws1.Route(ws1.POST("/{w}/z").To(dummy)) // so full path = /u/{w}/z
|
||||||
|
routes := RouterJSR311{}.selectRoutes(ws1, "/v") // test against /u/v
|
||||||
|
checkRoutesContains(routes, "/u/{w}", t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSelectRoutesUsers1(t *testing.T) {
|
||||||
|
ws1 := new(WebService).Path("/users")
|
||||||
|
ws1.Route(ws1.POST("").To(dummy))
|
||||||
|
ws1.Route(ws1.POST("/").To(dummy))
|
||||||
|
ws1.Route(ws1.PUT("/{id}").To(dummy))
|
||||||
|
routes := RouterJSR311{}.selectRoutes(ws1, "/1")
|
||||||
|
checkRoutesContains(routes, "/users/{id}", t)
|
||||||
|
}
|
||||||
|
func checkRoutesContains(routes []Route, path string, t *testing.T) {
|
||||||
|
if !containsRoutePath(routes, path, t) {
|
||||||
|
for _, r := range routes {
|
||||||
|
t.Logf("route %v %v", r.Method, r.Path)
|
||||||
|
}
|
||||||
|
t.Fatalf("routes should include [%v]:", path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func checkRoutesContainsNo(routes []Route, path string, t *testing.T) {
|
||||||
|
if containsRoutePath(routes, path, t) {
|
||||||
|
for _, r := range routes {
|
||||||
|
t.Logf("route %v %v", r.Method, r.Path)
|
||||||
|
}
|
||||||
|
t.Fatalf("routes should not include [%v]:", path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func containsRoutePath(routes []Route, path string, t *testing.T) bool {
|
||||||
|
for _, each := range routes {
|
||||||
|
if each.Path == path {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var tempregexs = []struct {
|
||||||
|
template, regex string
|
||||||
|
literalCount, varCount int
|
||||||
|
}{
|
||||||
|
{"", "^(/.*)?$", 0, 0},
|
||||||
|
{"/a/{b}/c/", "^/a/([^/]+?)/c(/.*)?$", 2, 1},
|
||||||
|
{"/{a}/{b}/{c-d-e}/", "^/([^/]+?)/([^/]+?)/([^/]+?)(/.*)?$", 0, 3},
|
||||||
|
{"/{p}/abcde", "^/([^/]+?)/abcde(/.*)?$", 5, 1},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTemplateToRegularExpression(t *testing.T) {
|
||||||
|
ok := true
|
||||||
|
for i, fixture := range tempregexs {
|
||||||
|
actual, lCount, vCount, _ := templateToRegularExpression(fixture.template)
|
||||||
|
if actual != fixture.regex {
|
||||||
|
t.Logf("regex mismatch, expected:%v , actual:%v, line:%v\n", fixture.regex, actual, i) // 11 = where the data starts
|
||||||
|
ok = false
|
||||||
|
}
|
||||||
|
if lCount != fixture.literalCount {
|
||||||
|
t.Logf("literal count mismatch, expected:%v , actual:%v, line:%v\n", fixture.literalCount, lCount, i)
|
||||||
|
ok = false
|
||||||
|
}
|
||||||
|
if vCount != fixture.varCount {
|
||||||
|
t.Logf("variable count mismatch, expected:%v , actual:%v, line:%v\n", fixture.varCount, vCount, i)
|
||||||
|
ok = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("one or more expression did not match")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// go test -v -test.run TestSortableRouteCandidates ...restful
|
||||||
|
func TestSortableRouteCandidates(t *testing.T) {
|
||||||
|
fixture := &sortableRouteCandidates{}
|
||||||
|
r1 := routeCandidate{matchesCount: 0, literalCount: 0, nonDefaultCount: 0}
|
||||||
|
r2 := routeCandidate{matchesCount: 0, literalCount: 0, nonDefaultCount: 1}
|
||||||
|
r3 := routeCandidate{matchesCount: 0, literalCount: 1, nonDefaultCount: 1}
|
||||||
|
r4 := routeCandidate{matchesCount: 1, literalCount: 1, nonDefaultCount: 0}
|
||||||
|
r5 := routeCandidate{matchesCount: 1, literalCount: 0, nonDefaultCount: 0}
|
||||||
|
fixture.candidates = append(fixture.candidates, r5, r4, r3, r2, r1)
|
||||||
|
sort.Sort(sort.Reverse(fixture))
|
||||||
|
first := fixture.candidates[0]
|
||||||
|
if first.matchesCount != 1 && first.literalCount != 1 && first.nonDefaultCount != 0 {
|
||||||
|
t.Fatal("expected r4")
|
||||||
|
}
|
||||||
|
last := fixture.candidates[len(fixture.candidates)-1]
|
||||||
|
if last.matchesCount != 0 && last.literalCount != 0 && last.nonDefaultCount != 0 {
|
||||||
|
t.Fatal("expected r1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func dummy(req *Request, resp *Response) { io.WriteString(resp.ResponseWriter, "dummy") }
|
16
Godeps/_workspace/src/github.com/emicklei/go-restful/logger.go
generated
vendored
Normal file
16
Godeps/_workspace/src/github.com/emicklei/go-restful/logger.go
generated
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package restful
|
||||||
|
|
||||||
|
import "log"
|
||||||
|
|
||||||
|
// Copyright 2014 Ernest Micklei. All rights reserved.
|
||||||
|
// Use of this source code is governed by a license
|
||||||
|
// that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
var trace bool = false
|
||||||
|
var traceLogger *log.Logger
|
||||||
|
|
||||||
|
// TraceLogger enables detailed logging of Http request matching and filter invocation. Default no logger is set.
|
||||||
|
func TraceLogger(logger *log.Logger) {
|
||||||
|
traceLogger = logger
|
||||||
|
trace = logger != nil
|
||||||
|
}
|
24
Godeps/_workspace/src/github.com/emicklei/go-restful/options_filter.go
generated
vendored
Normal file
24
Godeps/_workspace/src/github.com/emicklei/go-restful/options_filter.go
generated
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package restful
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
// OPTIONSFilter is a filter function that inspects the Http Request for the OPTIONS method
|
||||||
|
// and provides the response with a set of allowed methods for the request URL Path.
|
||||||
|
// As for any filter, you can also install it for a particular WebService within a Container
|
||||||
|
func (c Container) OPTIONSFilter(req *Request, resp *Response, chain *FilterChain) {
|
||||||
|
if "OPTIONS" != req.Request.Method {
|
||||||
|
chain.ProcessFilter(req, resp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.AddHeader(HEADER_Allow, strings.Join(c.computeAllowedMethods(req), ","))
|
||||||
|
}
|
||||||
|
|
||||||
|
// OPTIONSFilter is a filter function that inspects the Http Request for the OPTIONS method
|
||||||
|
// and provides the response with a set of allowed methods for the request URL Path.
|
||||||
|
func OPTIONSFilter() FilterFunction {
|
||||||
|
return DefaultContainer.OPTIONSFilter
|
||||||
|
}
|
34
Godeps/_workspace/src/github.com/emicklei/go-restful/options_filter_test.go
generated
vendored
Normal file
34
Godeps/_workspace/src/github.com/emicklei/go-restful/options_filter_test.go
generated
vendored
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package restful
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// go test -v -test.run TestOptionsFilter ...restful
|
||||||
|
func TestOptionsFilter(t *testing.T) {
|
||||||
|
tearDown()
|
||||||
|
ws := new(WebService)
|
||||||
|
ws.Route(ws.GET("/candy/{kind}").To(dummy))
|
||||||
|
ws.Route(ws.DELETE("/candy/{kind}").To(dummy))
|
||||||
|
ws.Route(ws.POST("/candies").To(dummy))
|
||||||
|
Add(ws)
|
||||||
|
Filter(OPTIONSFilter())
|
||||||
|
|
||||||
|
httpRequest, _ := http.NewRequest("OPTIONS", "http://here.io/candy/gum", nil)
|
||||||
|
httpWriter := httptest.NewRecorder()
|
||||||
|
DefaultContainer.dispatch(httpWriter, httpRequest)
|
||||||
|
actual := httpWriter.Header().Get(HEADER_Allow)
|
||||||
|
if "GET,DELETE" != actual {
|
||||||
|
t.Fatal("expected: GET,DELETE but got:" + actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
httpRequest, _ = http.NewRequest("OPTIONS", "http://here.io/candies", nil)
|
||||||
|
httpWriter = httptest.NewRecorder()
|
||||||
|
DefaultContainer.dispatch(httpWriter, httpRequest)
|
||||||
|
actual = httpWriter.Header().Get(HEADER_Allow)
|
||||||
|
if "POST" != actual {
|
||||||
|
t.Fatal("expected: POST but got:" + actual)
|
||||||
|
}
|
||||||
|
}
|
95
Godeps/_workspace/src/github.com/emicklei/go-restful/parameter.go
generated
vendored
Normal file
95
Godeps/_workspace/src/github.com/emicklei/go-restful/parameter.go
generated
vendored
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
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 (
|
||||||
|
// PathParameterKind = indicator of Request parameter type "path"
|
||||||
|
PathParameterKind = iota
|
||||||
|
|
||||||
|
// QueryParameterKind = indicator of Request parameter type "query"
|
||||||
|
QueryParameterKind
|
||||||
|
|
||||||
|
// BodyParameterKind = indicator of Request parameter type "body"
|
||||||
|
BodyParameterKind
|
||||||
|
|
||||||
|
// HeaderParameterKind = indicator of Request parameter type "header"
|
||||||
|
HeaderParameterKind
|
||||||
|
|
||||||
|
// FormParameterKind = indicator of Request parameter type "form"
|
||||||
|
FormParameterKind
|
||||||
|
)
|
||||||
|
|
||||||
|
// Parameter is for documententing the parameter used in a Http Request
|
||||||
|
// ParameterData kinds are Path,Query and Body
|
||||||
|
type Parameter struct {
|
||||||
|
data *ParameterData
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParameterData represents the state of a Parameter.
|
||||||
|
// It is made public to make it accessible to e.g. the Swagger package.
|
||||||
|
type ParameterData struct {
|
||||||
|
Name, Description, DataType string
|
||||||
|
Kind int
|
||||||
|
Required bool
|
||||||
|
AllowableValues map[string]string
|
||||||
|
AllowMultiple bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data returns the state of the Parameter
|
||||||
|
func (p *Parameter) Data() ParameterData {
|
||||||
|
return *p.data
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kind returns the parameter type indicator (see const for valid values)
|
||||||
|
func (p *Parameter) Kind() int {
|
||||||
|
return p.data.Kind
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parameter) bePath() *Parameter {
|
||||||
|
p.data.Kind = PathParameterKind
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
func (p *Parameter) beQuery() *Parameter {
|
||||||
|
p.data.Kind = QueryParameterKind
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
func (p *Parameter) beBody() *Parameter {
|
||||||
|
p.data.Kind = BodyParameterKind
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parameter) beHeader() *Parameter {
|
||||||
|
p.data.Kind = HeaderParameterKind
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parameter) beForm() *Parameter {
|
||||||
|
p.data.Kind = FormParameterKind
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// Required sets the required field and return the receiver
|
||||||
|
func (p *Parameter) Required(required bool) *Parameter {
|
||||||
|
p.data.Required = required
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllowMultiple sets the allowMultiple field and return the receiver
|
||||||
|
func (p *Parameter) AllowMultiple(multiple bool) *Parameter {
|
||||||
|
p.data.AllowMultiple = multiple
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllowableValues sets the allowableValues field and return the receiver
|
||||||
|
func (p *Parameter) AllowableValues(values map[string]string) *Parameter {
|
||||||
|
p.data.AllowableValues = values
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// DataType sets the dataType field and return the receiver
|
||||||
|
func (p *Parameter) DataType(typeName string) *Parameter {
|
||||||
|
p.data.DataType = typeName
|
||||||
|
return p
|
||||||
|
}
|
56
Godeps/_workspace/src/github.com/emicklei/go-restful/path_expression.go
generated
vendored
Normal file
56
Godeps/_workspace/src/github.com/emicklei/go-restful/path_expression.go
generated
vendored
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
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"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PathExpression holds a compiled path expression (RegExp) needed to match against
|
||||||
|
// Http request paths and to extract path parameter values.
|
||||||
|
type pathExpression struct {
|
||||||
|
LiteralCount int // the number of literal characters (means those not resulting from template variable substitution)
|
||||||
|
VarCount int // the number of named parameters (enclosed by {}) in the path
|
||||||
|
Matcher *regexp.Regexp
|
||||||
|
Source string // Path as defined by the RouteBuilder
|
||||||
|
tokens []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPathExpression creates a PathExpression from the input URL path.
|
||||||
|
// Returns an error if the path is invalid.
|
||||||
|
func newPathExpression(path string) (*pathExpression, error) {
|
||||||
|
expression, literalCount, varCount, tokens := templateToRegularExpression(path)
|
||||||
|
compiled, err := regexp.Compile(expression)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &pathExpression{literalCount, varCount, compiled, expression, tokens}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// http://jsr311.java.net/nonav/releases/1.1/spec/spec3.html#x3-370003.7.3
|
||||||
|
func templateToRegularExpression(template string) (expression string, literalCount int, varCount int, tokens []string) {
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
buffer.WriteString("^")
|
||||||
|
//tokens = strings.Split(template, "/")
|
||||||
|
tokens = tokenizePath(template)
|
||||||
|
for _, each := range tokens {
|
||||||
|
if each == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
buffer.WriteString("/")
|
||||||
|
if strings.HasPrefix(each, "{") {
|
||||||
|
// ignore var spec
|
||||||
|
varCount += 1
|
||||||
|
buffer.WriteString("([^/]+?)")
|
||||||
|
} else {
|
||||||
|
literalCount += len(each)
|
||||||
|
encoded := each // TODO URI encode
|
||||||
|
buffer.WriteString(regexp.QuoteMeta(encoded))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.TrimRight(buffer.String(), "/") + "(/.*)?$", literalCount, varCount, tokens
|
||||||
|
}
|
135
Godeps/_workspace/src/github.com/emicklei/go-restful/request.go
generated
vendored
Normal file
135
Godeps/_workspace/src/github.com/emicklei/go-restful/request.go
generated
vendored
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
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"
|
||||||
|
"encoding/json"
|
||||||
|
"encoding/xml"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var defaultRequestContentType string
|
||||||
|
|
||||||
|
var doCacheReadEntityBytes = true
|
||||||
|
|
||||||
|
// Request is a wrapper for a http Request that provides convenience methods
|
||||||
|
type Request struct {
|
||||||
|
Request *http.Request
|
||||||
|
bodyContent *[]byte // to cache the request body for multiple reads of ReadEntity
|
||||||
|
pathParameters map[string]string
|
||||||
|
attributes map[string]interface{} // for storing request-scoped values
|
||||||
|
selectedRoutePath string // root path + route path that matched the request, e.g. /meetings/{id}/attendees
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRequest(httpRequest *http.Request) *Request {
|
||||||
|
return &Request{
|
||||||
|
Request: httpRequest,
|
||||||
|
pathParameters: map[string]string{},
|
||||||
|
attributes: map[string]interface{}{},
|
||||||
|
} // empty parameters, attributes
|
||||||
|
}
|
||||||
|
|
||||||
|
// If ContentType is missing or */* is given then fall back to this type, otherwise
|
||||||
|
// a "Unable to unmarshal content of type:" response is returned.
|
||||||
|
// Valid values are restful.MIME_JSON and restful.MIME_XML
|
||||||
|
// Example:
|
||||||
|
// restful.DefaultRequestContentType(restful.MIME_JSON)
|
||||||
|
func DefaultRequestContentType(mime string) {
|
||||||
|
defaultRequestContentType = mime
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCacheReadEntity controls whether the response data ([]byte) is cached such that ReadEntity is repeatable.
|
||||||
|
// Default is true (due to backwardcompatibility). For better performance, you should set it to false if you don't need it.
|
||||||
|
func SetCacheReadEntity(doCache bool) {
|
||||||
|
doCacheReadEntityBytes = doCache
|
||||||
|
}
|
||||||
|
|
||||||
|
// PathParameter accesses the Path parameter value by its name
|
||||||
|
func (r *Request) PathParameter(name string) string {
|
||||||
|
return r.pathParameters[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
// PathParameters accesses the Path parameter values
|
||||||
|
func (r *Request) PathParameters() map[string]string {
|
||||||
|
return r.pathParameters
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueryParameter returns the (first) Query parameter value by its name
|
||||||
|
func (r *Request) QueryParameter(name string) string {
|
||||||
|
return r.Request.FormValue(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BodyParameter parses the body of the request (once for typically a POST or a PUT) and returns the value of the given name or an error.
|
||||||
|
func (r *Request) BodyParameter(name string) (string, error) {
|
||||||
|
err := r.Request.ParseForm()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return r.Request.PostFormValue(name), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HeaderParameter returns the HTTP Header value of a Header name or empty if missing
|
||||||
|
func (r *Request) HeaderParameter(name string) string {
|
||||||
|
return r.Request.Header.Get(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadEntity checks the Accept header and reads the content into the entityPointer
|
||||||
|
// May be called multiple times in the request-response flow
|
||||||
|
func (r *Request) ReadEntity(entityPointer interface{}) (err error) {
|
||||||
|
contentType := r.Request.Header.Get(HEADER_ContentType)
|
||||||
|
if doCacheReadEntityBytes {
|
||||||
|
return r.cachingReadEntity(contentType, entityPointer)
|
||||||
|
}
|
||||||
|
// unmarshall directly from request Body
|
||||||
|
return r.decodeEntity(r.Request.Body, contentType, entityPointer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Request) cachingReadEntity(contentType string, entityPointer interface{}) (err error) {
|
||||||
|
var buffer []byte
|
||||||
|
if r.bodyContent != nil {
|
||||||
|
buffer = *r.bodyContent
|
||||||
|
} else {
|
||||||
|
buffer, err = ioutil.ReadAll(r.Request.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
r.bodyContent = &buffer
|
||||||
|
}
|
||||||
|
return r.decodeEntity(bytes.NewReader(buffer), contentType, entityPointer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Request) decodeEntity(reader io.Reader, contentType string, entityPointer interface{}) (err error) {
|
||||||
|
if strings.Contains(contentType, MIME_XML) {
|
||||||
|
return xml.NewDecoder(reader).Decode(entityPointer)
|
||||||
|
}
|
||||||
|
if strings.Contains(contentType, MIME_JSON) || MIME_JSON == defaultRequestContentType {
|
||||||
|
decoder := json.NewDecoder(reader)
|
||||||
|
decoder.UseNumber()
|
||||||
|
return decoder.Decode(entityPointer)
|
||||||
|
}
|
||||||
|
if MIME_XML == defaultRequestContentType {
|
||||||
|
return xml.NewDecoder(reader).Decode(entityPointer)
|
||||||
|
}
|
||||||
|
return NewError(400, "Unable to unmarshal content of type:"+contentType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAttribute adds or replaces the attribute with the given value.
|
||||||
|
func (r *Request) SetAttribute(name string, value interface{}) {
|
||||||
|
r.attributes[name] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attribute returns the value associated to the given name. Returns nil if absent.
|
||||||
|
func (r Request) Attribute(name string) interface{} {
|
||||||
|
return r.attributes[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelectedRoutePath root path + route path that matched the request, e.g. /meetings/{id}/attendees
|
||||||
|
func (r Request) SelectedRoutePath() string {
|
||||||
|
return r.selectedRoutePath
|
||||||
|
}
|
204
Godeps/_workspace/src/github.com/emicklei/go-restful/request_test.go
generated
vendored
Normal file
204
Godeps/_workspace/src/github.com/emicklei/go-restful/request_test.go
generated
vendored
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
package restful
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestQueryParameter(t *testing.T) {
|
||||||
|
hreq := http.Request{Method: "GET"}
|
||||||
|
hreq.URL, _ = url.Parse("http://www.google.com/search?q=foo&q=bar")
|
||||||
|
rreq := Request{Request: &hreq}
|
||||||
|
if rreq.QueryParameter("q") != "foo" {
|
||||||
|
t.Errorf("q!=foo %#v", rreq)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Anything map[string]interface{}
|
||||||
|
|
||||||
|
type Number struct {
|
||||||
|
ValueFloat float64
|
||||||
|
ValueInt int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type Sample struct {
|
||||||
|
Value string
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadEntityXml(t *testing.T) {
|
||||||
|
SetCacheReadEntity(true)
|
||||||
|
bodyReader := strings.NewReader("<Sample><Value>42</Value></Sample>")
|
||||||
|
httpRequest, _ := http.NewRequest("GET", "/test", bodyReader)
|
||||||
|
httpRequest.Header.Set("Content-Type", "application/xml")
|
||||||
|
request := &Request{Request: httpRequest}
|
||||||
|
sam := new(Sample)
|
||||||
|
request.ReadEntity(sam)
|
||||||
|
if sam.Value != "42" {
|
||||||
|
t.Fatal("read failed")
|
||||||
|
}
|
||||||
|
if request.bodyContent == nil {
|
||||||
|
t.Fatal("no expected cached bytes found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadEntityXmlNonCached(t *testing.T) {
|
||||||
|
SetCacheReadEntity(false)
|
||||||
|
bodyReader := strings.NewReader("<Sample><Value>42</Value></Sample>")
|
||||||
|
httpRequest, _ := http.NewRequest("GET", "/test", bodyReader)
|
||||||
|
httpRequest.Header.Set("Content-Type", "application/xml")
|
||||||
|
request := &Request{Request: httpRequest}
|
||||||
|
sam := new(Sample)
|
||||||
|
request.ReadEntity(sam)
|
||||||
|
if sam.Value != "42" {
|
||||||
|
t.Fatal("read failed")
|
||||||
|
}
|
||||||
|
if request.bodyContent != nil {
|
||||||
|
t.Fatal("unexpected cached bytes found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadEntityJson(t *testing.T) {
|
||||||
|
bodyReader := strings.NewReader(`{"Value" : "42"}`)
|
||||||
|
httpRequest, _ := http.NewRequest("GET", "/test", bodyReader)
|
||||||
|
httpRequest.Header.Set("Content-Type", "application/json")
|
||||||
|
request := &Request{Request: httpRequest}
|
||||||
|
sam := new(Sample)
|
||||||
|
request.ReadEntity(sam)
|
||||||
|
if sam.Value != "42" {
|
||||||
|
t.Fatal("read failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadEntityJsonCharset(t *testing.T) {
|
||||||
|
bodyReader := strings.NewReader(`{"Value" : "42"}`)
|
||||||
|
httpRequest, _ := http.NewRequest("GET", "/test", bodyReader)
|
||||||
|
httpRequest.Header.Set("Content-Type", "application/json; charset=UTF-8")
|
||||||
|
request := NewRequest(httpRequest)
|
||||||
|
sam := new(Sample)
|
||||||
|
request.ReadEntity(sam)
|
||||||
|
if sam.Value != "42" {
|
||||||
|
t.Fatal("read failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadEntityJsonNumber(t *testing.T) {
|
||||||
|
SetCacheReadEntity(true)
|
||||||
|
bodyReader := strings.NewReader(`{"Value" : 4899710515899924123}`)
|
||||||
|
httpRequest, _ := http.NewRequest("GET", "/test", bodyReader)
|
||||||
|
httpRequest.Header.Set("Content-Type", "application/json")
|
||||||
|
request := &Request{Request: httpRequest}
|
||||||
|
any := make(Anything)
|
||||||
|
request.ReadEntity(&any)
|
||||||
|
number, ok := any["Value"].(json.Number)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("read failed")
|
||||||
|
}
|
||||||
|
vint, err := number.Int64()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("convert failed")
|
||||||
|
}
|
||||||
|
if vint != 4899710515899924123 {
|
||||||
|
t.Fatal("read failed")
|
||||||
|
}
|
||||||
|
vfloat, err := number.Float64()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("convert failed")
|
||||||
|
}
|
||||||
|
// match the default behaviour
|
||||||
|
vstring := strconv.FormatFloat(vfloat, 'e', 15, 64)
|
||||||
|
if vstring != "4.899710515899924e+18" {
|
||||||
|
t.Fatal("convert float64 failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadEntityJsonNumberNonCached(t *testing.T) {
|
||||||
|
SetCacheReadEntity(false)
|
||||||
|
bodyReader := strings.NewReader(`{"Value" : 4899710515899924123}`)
|
||||||
|
httpRequest, _ := http.NewRequest("GET", "/test", bodyReader)
|
||||||
|
httpRequest.Header.Set("Content-Type", "application/json")
|
||||||
|
request := &Request{Request: httpRequest}
|
||||||
|
any := make(Anything)
|
||||||
|
request.ReadEntity(&any)
|
||||||
|
number, ok := any["Value"].(json.Number)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("read failed")
|
||||||
|
}
|
||||||
|
vint, err := number.Int64()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("convert failed")
|
||||||
|
}
|
||||||
|
if vint != 4899710515899924123 {
|
||||||
|
t.Fatal("read failed")
|
||||||
|
}
|
||||||
|
vfloat, err := number.Float64()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("convert failed")
|
||||||
|
}
|
||||||
|
// match the default behaviour
|
||||||
|
vstring := strconv.FormatFloat(vfloat, 'e', 15, 64)
|
||||||
|
if vstring != "4.899710515899924e+18" {
|
||||||
|
t.Fatal("convert float64 failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadEntityJsonLong(t *testing.T) {
|
||||||
|
bodyReader := strings.NewReader(`{"ValueFloat" : 4899710515899924123, "ValueInt": 4899710515899924123}`)
|
||||||
|
httpRequest, _ := http.NewRequest("GET", "/test", bodyReader)
|
||||||
|
httpRequest.Header.Set("Content-Type", "application/json")
|
||||||
|
request := &Request{Request: httpRequest}
|
||||||
|
number := new(Number)
|
||||||
|
request.ReadEntity(&number)
|
||||||
|
if number.ValueInt != 4899710515899924123 {
|
||||||
|
t.Fatal("read failed")
|
||||||
|
}
|
||||||
|
// match the default behaviour
|
||||||
|
vstring := strconv.FormatFloat(number.ValueFloat, 'e', 15, 64)
|
||||||
|
if vstring != "4.899710515899924e+18" {
|
||||||
|
t.Fatal("convert float64 failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBodyParameter(t *testing.T) {
|
||||||
|
bodyReader := strings.NewReader(`value1=42&value2=43`)
|
||||||
|
httpRequest, _ := http.NewRequest("POST", "/test?value1=44", bodyReader) // POST and PUT body parameters take precedence over URL query string
|
||||||
|
httpRequest.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
|
||||||
|
request := NewRequest(httpRequest)
|
||||||
|
v1, err := request.BodyParameter("value1")
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
v2, err := request.BodyParameter("value2")
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
if v1 != "42" || v2 != "43" {
|
||||||
|
t.Fatal("read failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadEntityUnkown(t *testing.T) {
|
||||||
|
bodyReader := strings.NewReader("?")
|
||||||
|
httpRequest, _ := http.NewRequest("GET", "/test", bodyReader)
|
||||||
|
httpRequest.Header.Set("Content-Type", "application/rubbish")
|
||||||
|
request := NewRequest(httpRequest)
|
||||||
|
sam := new(Sample)
|
||||||
|
err := request.ReadEntity(sam)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("read should be in error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetAttribute(t *testing.T) {
|
||||||
|
bodyReader := strings.NewReader("?")
|
||||||
|
httpRequest, _ := http.NewRequest("GET", "/test", bodyReader)
|
||||||
|
request := NewRequest(httpRequest)
|
||||||
|
request.SetAttribute("go", "there")
|
||||||
|
there := request.Attribute("go")
|
||||||
|
if there != "there" {
|
||||||
|
t.Fatalf("missing request attribute:%v", there)
|
||||||
|
}
|
||||||
|
}
|
233
Godeps/_workspace/src/github.com/emicklei/go-restful/response.go
generated
vendored
Normal file
233
Godeps/_workspace/src/github.com/emicklei/go-restful/response.go
generated
vendored
Normal file
@ -0,0 +1,233 @@
|
|||||||
|
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 (
|
||||||
|
"encoding/json"
|
||||||
|
"encoding/xml"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DEPRECATED, use DefaultResponseContentType(mime)
|
||||||
|
var DefaultResponseMimeType string
|
||||||
|
|
||||||
|
//PrettyPrintResponses controls the indentation feature of XML and JSON
|
||||||
|
//serialization in the response methods WriteEntity, WriteAsJson, and
|
||||||
|
//WriteAsXml.
|
||||||
|
var PrettyPrintResponses = true
|
||||||
|
|
||||||
|
// Response is a wrapper on the actual http ResponseWriter
|
||||||
|
// It provides several convenience methods to prepare and write response content.
|
||||||
|
type Response struct {
|
||||||
|
http.ResponseWriter
|
||||||
|
requestAccept string // mime-type what the Http Request says it wants to receive
|
||||||
|
routeProduces []string // mime-types what the Route says it can produce
|
||||||
|
statusCode int // HTTP status code that has been written explicity (if zero then net/http has written 200)
|
||||||
|
contentLength int // number of bytes written for the response body
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a new response based on a http ResponseWriter.
|
||||||
|
func NewResponse(httpWriter http.ResponseWriter) *Response {
|
||||||
|
return &Response{httpWriter, "", []string{}, http.StatusOK, 0} // empty content-types
|
||||||
|
}
|
||||||
|
|
||||||
|
// If Accept header matching fails, fall back to this type, otherwise
|
||||||
|
// a "406: Not Acceptable" response is returned.
|
||||||
|
// Valid values are restful.MIME_JSON and restful.MIME_XML
|
||||||
|
// Example:
|
||||||
|
// restful.DefaultResponseContentType(restful.MIME_JSON)
|
||||||
|
func DefaultResponseContentType(mime string) {
|
||||||
|
DefaultResponseMimeType = mime
|
||||||
|
}
|
||||||
|
|
||||||
|
// InternalServerError writes the StatusInternalServerError header.
|
||||||
|
// DEPRECATED, use WriteErrorString(http.StatusInternalServerError,reason)
|
||||||
|
func (r Response) InternalServerError() Response {
|
||||||
|
r.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddHeader is a shortcut for .Header().Add(header,value)
|
||||||
|
func (r Response) AddHeader(header string, value string) Response {
|
||||||
|
r.Header().Add(header, value)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetRequestAccepts tells the response what Mime-type(s) the HTTP request said it wants to accept. Exposed for testing.
|
||||||
|
func (r *Response) SetRequestAccepts(mime string) {
|
||||||
|
r.requestAccept = mime
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteEntity marshals the value using the representation denoted by the Accept Header (XML or JSON)
|
||||||
|
// If no Accept header is specified (or */*) then return the Content-Type as specified by the first in the Route.Produces.
|
||||||
|
// If an Accept header is specified then return the Content-Type as specified by the first in the Route.Produces that is matched with the Accept header.
|
||||||
|
// If the value is nil then nothing is written. You may want to call WriteHeader(http.StatusNotFound) instead.
|
||||||
|
// Current implementation ignores any q-parameters in the Accept Header.
|
||||||
|
func (r *Response) WriteEntity(value interface{}) error {
|
||||||
|
if value == nil { // do not write a nil representation
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, qualifiedMime := range strings.Split(r.requestAccept, ",") {
|
||||||
|
mime := strings.Trim(strings.Split(qualifiedMime, ";")[0], " ")
|
||||||
|
if 0 == len(mime) || mime == "*/*" {
|
||||||
|
for _, each := range r.routeProduces {
|
||||||
|
if MIME_JSON == each {
|
||||||
|
return r.WriteAsJson(value)
|
||||||
|
}
|
||||||
|
if MIME_XML == each {
|
||||||
|
return r.WriteAsXml(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else { // mime is not blank; see if we have a match in Produces
|
||||||
|
for _, each := range r.routeProduces {
|
||||||
|
if mime == each {
|
||||||
|
if MIME_JSON == each {
|
||||||
|
return r.WriteAsJson(value)
|
||||||
|
}
|
||||||
|
if MIME_XML == each {
|
||||||
|
return r.WriteAsXml(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if DefaultResponseMimeType == MIME_JSON {
|
||||||
|
return r.WriteAsJson(value)
|
||||||
|
} else if DefaultResponseMimeType == MIME_XML {
|
||||||
|
return r.WriteAsXml(value)
|
||||||
|
} else {
|
||||||
|
if trace {
|
||||||
|
traceLogger.Printf("mismatch in mime-types and no defaults; (http)Accept=%v,(route)Produces=%v\n", r.requestAccept, r.routeProduces)
|
||||||
|
}
|
||||||
|
r.WriteHeader(http.StatusNotAcceptable) // for recording only
|
||||||
|
r.ResponseWriter.WriteHeader(http.StatusNotAcceptable)
|
||||||
|
if _, err := r.Write([]byte("406: Not Acceptable")); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteAsXml is a convenience method for writing a value in xml (requires Xml tags on the value)
|
||||||
|
func (r *Response) WriteAsXml(value interface{}) error {
|
||||||
|
var output []byte
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if value == nil { // do not write a nil representation
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if PrettyPrintResponses {
|
||||||
|
output, err = xml.MarshalIndent(value, " ", " ")
|
||||||
|
} else {
|
||||||
|
output, err = xml.Marshal(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return r.WriteError(http.StatusInternalServerError, err)
|
||||||
|
}
|
||||||
|
r.Header().Set(HEADER_ContentType, MIME_XML)
|
||||||
|
if r.statusCode > 0 { // a WriteHeader was intercepted
|
||||||
|
r.ResponseWriter.WriteHeader(r.statusCode)
|
||||||
|
}
|
||||||
|
_, err = r.Write([]byte(xml.Header))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err = r.Write(output); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteAsJson is a convenience method for writing a value in json
|
||||||
|
func (r *Response) WriteAsJson(value interface{}) error {
|
||||||
|
var output []byte
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if value == nil { // do not write a nil representation
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if PrettyPrintResponses {
|
||||||
|
output, err = json.MarshalIndent(value, " ", " ")
|
||||||
|
} else {
|
||||||
|
output, err = json.Marshal(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return r.WriteErrorString(http.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
r.Header().Set(HEADER_ContentType, MIME_JSON)
|
||||||
|
if r.statusCode > 0 { // a WriteHeader was intercepted
|
||||||
|
r.ResponseWriter.WriteHeader(r.statusCode)
|
||||||
|
}
|
||||||
|
if _, err = r.Write(output); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteError write the http status and the error string on the response.
|
||||||
|
func (r *Response) WriteError(httpStatus int, err error) error {
|
||||||
|
return r.WriteErrorString(httpStatus, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteServiceError is a convenience method for a responding with a ServiceError and a status
|
||||||
|
func (r *Response) WriteServiceError(httpStatus int, err ServiceError) error {
|
||||||
|
r.WriteHeader(httpStatus) // for recording only
|
||||||
|
return r.WriteEntity(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteErrorString is a convenience method for an error status with the actual error
|
||||||
|
func (r *Response) WriteErrorString(status int, errorReason string) error {
|
||||||
|
r.statusCode = status // for recording only
|
||||||
|
r.ResponseWriter.WriteHeader(status)
|
||||||
|
if _, err := r.Write([]byte(errorReason)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteHeader is overridden to remember the Status Code that has been written.
|
||||||
|
// Note that using this method, the status value is only written when
|
||||||
|
// - calling WriteEntity
|
||||||
|
// - or directly WriteAsXml,WriteAsJson.
|
||||||
|
// - or if the status is 204 (http.StatusNoContent)
|
||||||
|
func (r *Response) WriteHeader(httpStatus int) {
|
||||||
|
r.statusCode = httpStatus
|
||||||
|
// if 204 then WriteEntity will not be called so we need to pass this code
|
||||||
|
if http.StatusNoContent == httpStatus {
|
||||||
|
r.ResponseWriter.WriteHeader(httpStatus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// StatusCode returns the code that has been written using WriteHeader.
|
||||||
|
func (r Response) StatusCode() int {
|
||||||
|
if 0 == r.statusCode {
|
||||||
|
// no status code has been written yet; assume OK
|
||||||
|
return http.StatusOK
|
||||||
|
}
|
||||||
|
return r.statusCode
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write writes the data to the connection as part of an HTTP reply.
|
||||||
|
// Write is part of http.ResponseWriter interface.
|
||||||
|
func (r *Response) Write(bytes []byte) (int, error) {
|
||||||
|
written, err := r.ResponseWriter.Write(bytes)
|
||||||
|
r.contentLength += written
|
||||||
|
return written, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContentLength returns the number of bytes written for the response content.
|
||||||
|
// Note that this value is only correct if all data is written through the Response using its Write* methods.
|
||||||
|
// Data written directly using the underlying http.ResponseWriter is not accounted for.
|
||||||
|
func (r Response) ContentLength() int {
|
||||||
|
return r.contentLength
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloseNotify is part of http.CloseNotifier interface
|
||||||
|
func (r Response) CloseNotify() <-chan bool {
|
||||||
|
return r.ResponseWriter.(http.CloseNotifier).CloseNotify()
|
||||||
|
}
|
137
Godeps/_workspace/src/github.com/emicklei/go-restful/response_test.go
generated
vendored
Normal file
137
Godeps/_workspace/src/github.com/emicklei/go-restful/response_test.go
generated
vendored
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
package restful
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWriteHeader(t *testing.T) {
|
||||||
|
httpWriter := httptest.NewRecorder()
|
||||||
|
resp := Response{httpWriter, "*/*", []string{"*/*"}, 0, 0}
|
||||||
|
resp.WriteHeader(123)
|
||||||
|
if resp.StatusCode() != 123 {
|
||||||
|
t.Errorf("Unexpected status code:%d", resp.StatusCode())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNoWriteHeader(t *testing.T) {
|
||||||
|
httpWriter := httptest.NewRecorder()
|
||||||
|
resp := Response{httpWriter, "*/*", []string{"*/*"}, 0, 0}
|
||||||
|
if resp.StatusCode() != http.StatusOK {
|
||||||
|
t.Errorf("Unexpected status code:%d", resp.StatusCode())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type food struct {
|
||||||
|
Kind string
|
||||||
|
}
|
||||||
|
|
||||||
|
// go test -v -test.run TestMeasureContentLengthXml ...restful
|
||||||
|
func TestMeasureContentLengthXml(t *testing.T) {
|
||||||
|
httpWriter := httptest.NewRecorder()
|
||||||
|
resp := Response{httpWriter, "*/*", []string{"*/*"}, 0, 0}
|
||||||
|
resp.WriteAsXml(food{"apple"})
|
||||||
|
if resp.ContentLength() != 76 {
|
||||||
|
t.Errorf("Incorrect measured length:%d", resp.ContentLength())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// go test -v -test.run TestMeasureContentLengthJson ...restful
|
||||||
|
func TestMeasureContentLengthJson(t *testing.T) {
|
||||||
|
httpWriter := httptest.NewRecorder()
|
||||||
|
resp := Response{httpWriter, "*/*", []string{"*/*"}, 0, 0}
|
||||||
|
resp.WriteAsJson(food{"apple"})
|
||||||
|
if resp.ContentLength() != 22 {
|
||||||
|
t.Errorf("Incorrect measured length:%d", resp.ContentLength())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// go test -v -test.run TestMeasureContentLengthWriteErrorString ...restful
|
||||||
|
func TestMeasureContentLengthWriteErrorString(t *testing.T) {
|
||||||
|
httpWriter := httptest.NewRecorder()
|
||||||
|
resp := Response{httpWriter, "*/*", []string{"*/*"}, 0, 0}
|
||||||
|
resp.WriteErrorString(404, "Invalid")
|
||||||
|
if resp.ContentLength() != len("Invalid") {
|
||||||
|
t.Errorf("Incorrect measured length:%d", resp.ContentLength())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// go test -v -test.run TestStatusCreatedAndContentTypeJson_Issue54 ...restful
|
||||||
|
func TestStatusCreatedAndContentTypeJson_Issue54(t *testing.T) {
|
||||||
|
httpWriter := httptest.NewRecorder()
|
||||||
|
resp := Response{httpWriter, "application/json", []string{"application/json"}, 0, 0}
|
||||||
|
resp.WriteHeader(201)
|
||||||
|
resp.WriteAsJson(food{"Juicy"})
|
||||||
|
if httpWriter.HeaderMap.Get("Content-Type") != "application/json" {
|
||||||
|
t.Errorf("Expected content type json but got:%d", httpWriter.HeaderMap.Get("Content-Type"))
|
||||||
|
}
|
||||||
|
if httpWriter.Code != 201 {
|
||||||
|
t.Errorf("Expected status 201 but got:%d", httpWriter.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type errorOnWriteRecorder struct {
|
||||||
|
*httptest.ResponseRecorder
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e errorOnWriteRecorder) Write(bytes []byte) (int, error) {
|
||||||
|
return 0, errors.New("fail")
|
||||||
|
}
|
||||||
|
|
||||||
|
// go test -v -test.run TestLastWriteErrorCaught ...restful
|
||||||
|
func TestLastWriteErrorCaught(t *testing.T) {
|
||||||
|
httpWriter := errorOnWriteRecorder{httptest.NewRecorder()}
|
||||||
|
resp := Response{httpWriter, "application/json", []string{"application/json"}, 0, 0}
|
||||||
|
err := resp.WriteAsJson(food{"Juicy"})
|
||||||
|
if err.Error() != "fail" {
|
||||||
|
t.Errorf("Unexpected error message:%v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// go test -v -test.run TestAcceptStarStar_Issue83 ...restful
|
||||||
|
func TestAcceptStarStar_Issue83(t *testing.T) {
|
||||||
|
httpWriter := httptest.NewRecorder()
|
||||||
|
// Accept Produces
|
||||||
|
resp := Response{httpWriter, "application/bogus,*/*;q=0.8", []string{"application/json"}, 0, 0}
|
||||||
|
resp.WriteEntity(food{"Juicy"})
|
||||||
|
ct := httpWriter.Header().Get("Content-Type")
|
||||||
|
if "application/json" != ct {
|
||||||
|
t.Errorf("Unexpected content type:%s", ct)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// go test -v -test.run TestAcceptSkipStarStar_Issue83 ...restful
|
||||||
|
func TestAcceptSkipStarStar_Issue83(t *testing.T) {
|
||||||
|
httpWriter := httptest.NewRecorder()
|
||||||
|
// Accept Produces
|
||||||
|
resp := Response{httpWriter, " application/xml ,*/* ; q=0.8", []string{"application/json", "application/xml"}, 0, 0}
|
||||||
|
resp.WriteEntity(food{"Juicy"})
|
||||||
|
ct := httpWriter.Header().Get("Content-Type")
|
||||||
|
if "application/xml" != ct {
|
||||||
|
t.Errorf("Unexpected content type:%s", ct)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// go test -v -test.run TestAcceptXmlBeforeStarStar_Issue83 ...restful
|
||||||
|
func TestAcceptXmlBeforeStarStar_Issue83(t *testing.T) {
|
||||||
|
httpWriter := httptest.NewRecorder()
|
||||||
|
// Accept Produces
|
||||||
|
resp := Response{httpWriter, "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", []string{"application/json"}, 0, 0}
|
||||||
|
resp.WriteEntity(food{"Juicy"})
|
||||||
|
ct := httpWriter.Header().Get("Content-Type")
|
||||||
|
if "application/json" != ct {
|
||||||
|
t.Errorf("Unexpected content type:%s", ct)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// go test -v -test.run TestWriteHeaderNoContent_Issue124 ...restful
|
||||||
|
func TestWriteHeaderNoContent_Issue124(t *testing.T) {
|
||||||
|
httpWriter := httptest.NewRecorder()
|
||||||
|
resp := Response{httpWriter, "text/plain", []string{"text/plain"}, 0, 0}
|
||||||
|
resp.WriteHeader(http.StatusNoContent)
|
||||||
|
if httpWriter.Code != http.StatusNoContent {
|
||||||
|
t.Errorf("got %d want %d", httpWriter.Code, http.StatusNoContent)
|
||||||
|
}
|
||||||
|
}
|
166
Godeps/_workspace/src/github.com/emicklei/go-restful/route.go
generated
vendored
Normal file
166
Godeps/_workspace/src/github.com/emicklei/go-restful/route.go
generated
vendored
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
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"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RouteFunction declares the signature of a function that can be bound to a Route.
|
||||||
|
type RouteFunction func(*Request, *Response)
|
||||||
|
|
||||||
|
// Route binds a HTTP Method,Path,Consumes combination to a RouteFunction.
|
||||||
|
type Route struct {
|
||||||
|
Method string
|
||||||
|
Produces []string
|
||||||
|
Consumes []string
|
||||||
|
Path string // webservice root path + described path
|
||||||
|
Function RouteFunction
|
||||||
|
Filters []FilterFunction
|
||||||
|
|
||||||
|
// cached values for dispatching
|
||||||
|
relativePath string
|
||||||
|
pathParts []string
|
||||||
|
pathExpr *pathExpression // cached compilation of relativePath as RegExp
|
||||||
|
|
||||||
|
// documentation
|
||||||
|
Doc string
|
||||||
|
Operation string
|
||||||
|
ParameterDocs []*Parameter
|
||||||
|
ResponseErrors map[int]ResponseError
|
||||||
|
ReadSample, WriteSample interface{} // structs that model an example request or response payload
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize for Route
|
||||||
|
func (r *Route) postBuild() {
|
||||||
|
r.pathParts = tokenizePath(r.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Request and Response from their http versions
|
||||||
|
func (r *Route) wrapRequestResponse(httpWriter http.ResponseWriter, httpRequest *http.Request) (*Request, *Response) {
|
||||||
|
params := r.extractParameters(httpRequest.URL.Path)
|
||||||
|
wrappedRequest := NewRequest(httpRequest)
|
||||||
|
wrappedRequest.pathParameters = params
|
||||||
|
wrappedRequest.selectedRoutePath = r.Path
|
||||||
|
wrappedResponse := NewResponse(httpWriter)
|
||||||
|
wrappedResponse.requestAccept = httpRequest.Header.Get(HEADER_Accept)
|
||||||
|
wrappedResponse.routeProduces = r.Produces
|
||||||
|
return wrappedRequest, wrappedResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
// dispatchWithFilters call the function after passing through its own filters
|
||||||
|
func (r *Route) dispatchWithFilters(wrappedRequest *Request, wrappedResponse *Response) {
|
||||||
|
if len(r.Filters) > 0 {
|
||||||
|
chain := FilterChain{Filters: r.Filters, Target: r.Function}
|
||||||
|
chain.ProcessFilter(wrappedRequest, wrappedResponse)
|
||||||
|
} else {
|
||||||
|
// unfiltered
|
||||||
|
r.Function(wrappedRequest, wrappedResponse)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return whether the mimeType matches to what this Route can produce.
|
||||||
|
func (r Route) matchesAccept(mimeTypesWithQuality string) bool {
|
||||||
|
parts := strings.Split(mimeTypesWithQuality, ",")
|
||||||
|
for _, each := range parts {
|
||||||
|
var withoutQuality string
|
||||||
|
if strings.Contains(each, ";") {
|
||||||
|
withoutQuality = strings.Split(each, ";")[0]
|
||||||
|
} else {
|
||||||
|
withoutQuality = each
|
||||||
|
}
|
||||||
|
// trim before compare
|
||||||
|
withoutQuality = strings.Trim(withoutQuality, " ")
|
||||||
|
if withoutQuality == "*/*" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, other := range r.Produces {
|
||||||
|
if other == withoutQuality {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return whether the mimeType matches to what this Route can consume.
|
||||||
|
func (r Route) matchesContentType(mimeTypes string) bool {
|
||||||
|
parts := strings.Split(mimeTypes, ",")
|
||||||
|
for _, each := range parts {
|
||||||
|
var contentType string
|
||||||
|
if strings.Contains(each, ";") {
|
||||||
|
contentType = strings.Split(each, ";")[0]
|
||||||
|
} else {
|
||||||
|
contentType = each
|
||||||
|
}
|
||||||
|
// trim before compare
|
||||||
|
contentType = strings.Trim(contentType, " ")
|
||||||
|
for _, other := range r.Consumes {
|
||||||
|
if other == "*/*" || other == contentType {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the parameters from the request url path
|
||||||
|
func (r Route) extractParameters(urlPath string) map[string]string {
|
||||||
|
urlParts := tokenizePath(urlPath)
|
||||||
|
pathParameters := map[string]string{}
|
||||||
|
for i, key := range r.pathParts {
|
||||||
|
var value string
|
||||||
|
if i >= len(urlParts) {
|
||||||
|
value = ""
|
||||||
|
} else {
|
||||||
|
value = urlParts[i]
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(key, "{") { // path-parameter
|
||||||
|
if colon := strings.Index(key, ":"); colon != -1 {
|
||||||
|
// extract by regex
|
||||||
|
regPart := key[colon+1 : len(key)-1]
|
||||||
|
keyPart := key[1:colon]
|
||||||
|
if regPart == "*" {
|
||||||
|
pathParameters[keyPart] = untokenizePath(i, urlParts)
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
pathParameters[keyPart] = value
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// without enclosing {}
|
||||||
|
pathParameters[key[1:len(key)-1]] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pathParameters
|
||||||
|
}
|
||||||
|
|
||||||
|
// Untokenize back into an URL path using the slash separator
|
||||||
|
func untokenizePath(offset int, parts []string) string {
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
for p := offset; p < len(parts); p++ {
|
||||||
|
buffer.WriteString(parts[p])
|
||||||
|
// do not end
|
||||||
|
if p < len(parts)-1 {
|
||||||
|
buffer.WriteString("/")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buffer.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tokenize an URL path using the slash separator ; the result does not have empty tokens
|
||||||
|
func tokenizePath(path string) []string {
|
||||||
|
if "/" == path {
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
return strings.Split(strings.Trim(path, "/"), "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
// for debugging
|
||||||
|
func (r Route) String() string {
|
||||||
|
return r.Method + " " + r.Path
|
||||||
|
}
|
208
Godeps/_workspace/src/github.com/emicklei/go-restful/route_builder.go
generated
vendored
Normal file
208
Godeps/_workspace/src/github.com/emicklei/go-restful/route_builder.go
generated
vendored
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
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 (
|
||||||
|
"log"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RouteBuilder is a helper to construct Routes.
|
||||||
|
type RouteBuilder struct {
|
||||||
|
rootPath string
|
||||||
|
currentPath string
|
||||||
|
produces []string
|
||||||
|
consumes []string
|
||||||
|
httpMethod string // required
|
||||||
|
function RouteFunction // required
|
||||||
|
filters []FilterFunction
|
||||||
|
// documentation
|
||||||
|
doc string
|
||||||
|
operation string
|
||||||
|
readSample, writeSample interface{}
|
||||||
|
parameters []*Parameter
|
||||||
|
errorMap map[int]ResponseError
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do evaluates each argument with the RouteBuilder itself.
|
||||||
|
// This allows you to follow DRY principles without breaking the fluent programming style.
|
||||||
|
// Example:
|
||||||
|
// ws.Route(ws.DELETE("/{name}").To(t.deletePerson).Do(Returns200, Returns500))
|
||||||
|
//
|
||||||
|
// func Returns500(b *RouteBuilder) {
|
||||||
|
// b.Returns(500, "Internal Server Error", restful.ServiceError{})
|
||||||
|
// }
|
||||||
|
func (b *RouteBuilder) Do(oneArgBlocks ...func(*RouteBuilder)) *RouteBuilder {
|
||||||
|
for _, each := range oneArgBlocks {
|
||||||
|
each(b)
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// To bind the route to a function.
|
||||||
|
// If this route is matched with the incoming Http Request then call this function with the *Request,*Response pair. Required.
|
||||||
|
func (b *RouteBuilder) To(function RouteFunction) *RouteBuilder {
|
||||||
|
b.function = function
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method specifies what HTTP method to match. Required.
|
||||||
|
func (b *RouteBuilder) Method(method string) *RouteBuilder {
|
||||||
|
b.httpMethod = method
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// Produces specifies what MIME types can be produced ; the matched one will appear in the Content-Type Http header.
|
||||||
|
func (b *RouteBuilder) Produces(mimeTypes ...string) *RouteBuilder {
|
||||||
|
b.produces = mimeTypes
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consumes specifies what MIME types can be consumes ; the Accept Http header must matched any of these
|
||||||
|
func (b *RouteBuilder) Consumes(mimeTypes ...string) *RouteBuilder {
|
||||||
|
b.consumes = mimeTypes
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path specifies the relative (w.r.t WebService root path) URL path to match. Default is "/".
|
||||||
|
func (b *RouteBuilder) Path(subPath string) *RouteBuilder {
|
||||||
|
b.currentPath = subPath
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// Doc tells what this route is all about. Optional.
|
||||||
|
func (b *RouteBuilder) Doc(documentation string) *RouteBuilder {
|
||||||
|
b.doc = documentation
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reads tells what resource type will be read from the request payload. Optional.
|
||||||
|
// A parameter of type "body" is added ,required is set to true and the dataType is set to the qualified name of the sample's type.
|
||||||
|
func (b *RouteBuilder) Reads(sample interface{}) *RouteBuilder {
|
||||||
|
b.readSample = sample
|
||||||
|
typeAsName := reflect.TypeOf(sample).String()
|
||||||
|
bodyParameter := &Parameter{&ParameterData{Name: typeAsName}}
|
||||||
|
bodyParameter.beBody()
|
||||||
|
bodyParameter.Required(true)
|
||||||
|
bodyParameter.DataType(typeAsName)
|
||||||
|
b.Param(bodyParameter)
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParameterNamed returns a Parameter already known to the RouteBuilder. Returns nil if not.
|
||||||
|
// Use this to modify or extend information for the Parameter (through its Data()).
|
||||||
|
func (b RouteBuilder) ParameterNamed(name string) (p *Parameter) {
|
||||||
|
for _, each := range b.parameters {
|
||||||
|
if each.Data().Name == name {
|
||||||
|
return each
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// Writes tells what resource type will be written as the response payload. Optional.
|
||||||
|
func (b *RouteBuilder) Writes(sample interface{}) *RouteBuilder {
|
||||||
|
b.writeSample = sample
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// Param allows you to document the parameters of the Route. It adds a new Parameter (does not check for duplicates).
|
||||||
|
func (b *RouteBuilder) Param(parameter *Parameter) *RouteBuilder {
|
||||||
|
if b.parameters == nil {
|
||||||
|
b.parameters = []*Parameter{}
|
||||||
|
}
|
||||||
|
b.parameters = append(b.parameters, parameter)
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// Operation allows you to document what the acutal method/function call is of the Route.
|
||||||
|
func (b *RouteBuilder) Operation(name string) *RouteBuilder {
|
||||||
|
b.operation = name
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReturnsError is deprecated, use Returns instead.
|
||||||
|
func (b *RouteBuilder) ReturnsError(code int, message string, model interface{}) *RouteBuilder {
|
||||||
|
log.Println("ReturnsError is deprecated, use Returns instead.")
|
||||||
|
return b.Returns(code, message, model)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns allows you to document what responses (errors or regular) can be expected.
|
||||||
|
// The model parameter is optional ; either pass a struct instance or use nil if not applicable.
|
||||||
|
func (b *RouteBuilder) Returns(code int, message string, model interface{}) *RouteBuilder {
|
||||||
|
err := ResponseError{
|
||||||
|
Code: code,
|
||||||
|
Message: message,
|
||||||
|
Model: model,
|
||||||
|
}
|
||||||
|
// lazy init because there is no NewRouteBuilder (yet)
|
||||||
|
if b.errorMap == nil {
|
||||||
|
b.errorMap = map[int]ResponseError{}
|
||||||
|
}
|
||||||
|
b.errorMap[code] = err
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResponseError struct {
|
||||||
|
Code int
|
||||||
|
Message string
|
||||||
|
Model interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *RouteBuilder) servicePath(path string) *RouteBuilder {
|
||||||
|
b.rootPath = path
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter appends a FilterFunction to the end of filters for this Route to build.
|
||||||
|
func (b *RouteBuilder) Filter(filter FilterFunction) *RouteBuilder {
|
||||||
|
b.filters = append(b.filters, filter)
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no specific Route path then set to rootPath
|
||||||
|
// If no specific Produces then set to rootProduces
|
||||||
|
// If no specific Consumes then set to rootConsumes
|
||||||
|
func (b *RouteBuilder) copyDefaults(rootProduces, rootConsumes []string) {
|
||||||
|
if len(b.produces) == 0 {
|
||||||
|
b.produces = rootProduces
|
||||||
|
}
|
||||||
|
if len(b.consumes) == 0 {
|
||||||
|
b.consumes = rootConsumes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build creates a new Route using the specification details collected by the RouteBuilder
|
||||||
|
func (b *RouteBuilder) Build() Route {
|
||||||
|
pathExpr, err := newPathExpression(b.currentPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("[restful] Invalid path:%s because:%v", b.currentPath, err)
|
||||||
|
}
|
||||||
|
if b.function == nil {
|
||||||
|
log.Fatalf("[restful] No function specified for route:" + b.currentPath)
|
||||||
|
}
|
||||||
|
route := Route{
|
||||||
|
Method: b.httpMethod,
|
||||||
|
Path: concatPath(b.rootPath, b.currentPath),
|
||||||
|
Produces: b.produces,
|
||||||
|
Consumes: b.consumes,
|
||||||
|
Function: b.function,
|
||||||
|
Filters: b.filters,
|
||||||
|
relativePath: b.currentPath,
|
||||||
|
pathExpr: pathExpr,
|
||||||
|
Doc: b.doc,
|
||||||
|
Operation: b.operation,
|
||||||
|
ParameterDocs: b.parameters,
|
||||||
|
ResponseErrors: b.errorMap,
|
||||||
|
ReadSample: b.readSample,
|
||||||
|
WriteSample: b.writeSample}
|
||||||
|
route.postBuild()
|
||||||
|
return route
|
||||||
|
}
|
||||||
|
|
||||||
|
func concatPath(path1, path2 string) string {
|
||||||
|
return strings.TrimRight(path1, "/") + "/" + strings.TrimLeft(path2, "/")
|
||||||
|
}
|
55
Godeps/_workspace/src/github.com/emicklei/go-restful/route_builder_test.go
generated
vendored
Normal file
55
Godeps/_workspace/src/github.com/emicklei/go-restful/route_builder_test.go
generated
vendored
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
package restful
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRouteBuilder_PathParameter(t *testing.T) {
|
||||||
|
p := &Parameter{&ParameterData{Name: "name", Description: "desc"}}
|
||||||
|
p.AllowMultiple(true)
|
||||||
|
p.DataType("int")
|
||||||
|
p.Required(true)
|
||||||
|
values := map[string]string{"a": "b"}
|
||||||
|
p.AllowableValues(values)
|
||||||
|
p.bePath()
|
||||||
|
|
||||||
|
b := new(RouteBuilder)
|
||||||
|
b.function = dummy
|
||||||
|
b.Param(p)
|
||||||
|
r := b.Build()
|
||||||
|
if !r.ParameterDocs[0].Data().AllowMultiple {
|
||||||
|
t.Error("AllowMultiple invalid")
|
||||||
|
}
|
||||||
|
if r.ParameterDocs[0].Data().DataType != "int" {
|
||||||
|
t.Error("dataType invalid")
|
||||||
|
}
|
||||||
|
if !r.ParameterDocs[0].Data().Required {
|
||||||
|
t.Error("required invalid")
|
||||||
|
}
|
||||||
|
if r.ParameterDocs[0].Data().Kind != PathParameterKind {
|
||||||
|
t.Error("kind invalid")
|
||||||
|
}
|
||||||
|
if r.ParameterDocs[0].Data().AllowableValues["a"] != "b" {
|
||||||
|
t.Error("allowableValues invalid")
|
||||||
|
}
|
||||||
|
if b.ParameterNamed("name") == nil {
|
||||||
|
t.Error("access to parameter failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRouteBuilder(t *testing.T) {
|
||||||
|
json := "application/json"
|
||||||
|
b := new(RouteBuilder)
|
||||||
|
b.To(dummy)
|
||||||
|
b.Path("/routes").Method("HEAD").Consumes(json).Produces(json)
|
||||||
|
r := b.Build()
|
||||||
|
if r.Path != "/routes" {
|
||||||
|
t.Error("path invalid")
|
||||||
|
}
|
||||||
|
if r.Produces[0] != json {
|
||||||
|
t.Error("produces invalid")
|
||||||
|
}
|
||||||
|
if r.Consumes[0] != json {
|
||||||
|
t.Error("consumes invalid")
|
||||||
|
}
|
||||||
|
}
|
108
Godeps/_workspace/src/github.com/emicklei/go-restful/route_test.go
generated
vendored
Normal file
108
Godeps/_workspace/src/github.com/emicklei/go-restful/route_test.go
generated
vendored
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
package restful
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// accept should match produces
|
||||||
|
func TestMatchesAcceptStar(t *testing.T) {
|
||||||
|
r := Route{Produces: []string{"application/xml"}}
|
||||||
|
if !r.matchesAccept("*/*") {
|
||||||
|
t.Errorf("accept should match star")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// accept should match produces
|
||||||
|
func TestMatchesAcceptIE(t *testing.T) {
|
||||||
|
r := Route{Produces: []string{"application/xml"}}
|
||||||
|
if !r.matchesAccept("text/html, application/xhtml+xml, */*") {
|
||||||
|
t.Errorf("accept should match star")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// accept should match produces
|
||||||
|
func TestMatchesAcceptXml(t *testing.T) {
|
||||||
|
r := Route{Produces: []string{"application/xml"}}
|
||||||
|
if r.matchesAccept("application/json") {
|
||||||
|
t.Errorf("accept should not match json")
|
||||||
|
}
|
||||||
|
if !r.matchesAccept("application/xml") {
|
||||||
|
t.Errorf("accept should match xml")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// content type should match consumes
|
||||||
|
func TestMatchesContentTypeXml(t *testing.T) {
|
||||||
|
r := Route{Consumes: []string{"application/xml"}}
|
||||||
|
if r.matchesContentType("application/json") {
|
||||||
|
t.Errorf("accept should not match json")
|
||||||
|
}
|
||||||
|
if !r.matchesContentType("application/xml") {
|
||||||
|
t.Errorf("accept should match xml")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// content type should match consumes
|
||||||
|
func TestMatchesContentTypeCharsetInformation(t *testing.T) {
|
||||||
|
r := Route{Consumes: []string{"application/json"}}
|
||||||
|
if !r.matchesContentType("application/json; charset=UTF-8") {
|
||||||
|
t.Errorf("matchesContentType should ignore charset information")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMatchesPath_OneParam(t *testing.T) {
|
||||||
|
params := doExtractParams("/from/{source}", 2, "/from/here", t)
|
||||||
|
if params["source"] != "here" {
|
||||||
|
t.Errorf("parameter mismatch here")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMatchesPath_Slash(t *testing.T) {
|
||||||
|
params := doExtractParams("/", 0, "/", t)
|
||||||
|
if len(params) != 0 {
|
||||||
|
t.Errorf("expected empty parameters")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMatchesPath_SlashNonVar(t *testing.T) {
|
||||||
|
params := doExtractParams("/any", 1, "/any", t)
|
||||||
|
if len(params) != 0 {
|
||||||
|
t.Errorf("expected empty parameters")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMatchesPath_TwoVars(t *testing.T) {
|
||||||
|
params := doExtractParams("/from/{source}/to/{destination}", 4, "/from/AMS/to/NY", t)
|
||||||
|
if params["source"] != "AMS" {
|
||||||
|
t.Errorf("parameter mismatch AMS")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMatchesPath_VarOnFront(t *testing.T) {
|
||||||
|
params := doExtractParams("{what}/from/{source}/", 3, "who/from/SOS/", t)
|
||||||
|
if params["source"] != "SOS" {
|
||||||
|
t.Errorf("parameter mismatch SOS")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractParameters_EmptyValue(t *testing.T) {
|
||||||
|
params := doExtractParams("/fixed/{var}", 2, "/fixed/", t)
|
||||||
|
if params["var"] != "" {
|
||||||
|
t.Errorf("parameter mismatch var")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTokenizePath(t *testing.T) {
|
||||||
|
if len(tokenizePath("/")) != 0 {
|
||||||
|
t.Errorf("not empty path tokens")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func doExtractParams(routePath string, size int, urlPath string, t *testing.T) map[string]string {
|
||||||
|
r := Route{Path: routePath}
|
||||||
|
r.postBuild()
|
||||||
|
if len(r.pathParts) != size {
|
||||||
|
t.Fatalf("len not %v %v, but %v", size, r.pathParts, len(r.pathParts))
|
||||||
|
}
|
||||||
|
return r.extractParameters(urlPath)
|
||||||
|
}
|
18
Godeps/_workspace/src/github.com/emicklei/go-restful/router.go
generated
vendored
Normal file
18
Godeps/_workspace/src/github.com/emicklei/go-restful/router.go
generated
vendored
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
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"
|
||||||
|
|
||||||
|
// A RouteSelector finds the best matching Route given the input HTTP Request
|
||||||
|
type RouteSelector interface {
|
||||||
|
|
||||||
|
// SelectRoute finds a Route given the input HTTP Request and a list of WebServices.
|
||||||
|
// It returns a selected Route and its containing WebService or an error indicating
|
||||||
|
// a problem.
|
||||||
|
SelectRoute(
|
||||||
|
webServices []*WebService,
|
||||||
|
httpRequest *http.Request) (selectedService *WebService, selected *Route, err error)
|
||||||
|
}
|
23
Godeps/_workspace/src/github.com/emicklei/go-restful/service_error.go
generated
vendored
Normal file
23
Godeps/_workspace/src/github.com/emicklei/go-restful/service_error.go
generated
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
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 "fmt"
|
||||||
|
|
||||||
|
// ServiceError is a transport object to pass information about a non-Http error occurred in a WebService while processing a request.
|
||||||
|
type ServiceError struct {
|
||||||
|
Code int
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewError returns a ServiceError using the code and reason
|
||||||
|
func NewError(code int, message string) ServiceError {
|
||||||
|
return ServiceError{Code: code, Message: message}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error returns a text representation of the service error
|
||||||
|
func (s ServiceError) Error() string {
|
||||||
|
return fmt.Sprintf("[ServiceError:%v] %v", s.Code, s.Message)
|
||||||
|
}
|
19
Godeps/_workspace/src/github.com/emicklei/go-restful/swagger/CHANGES.md
generated
vendored
Normal file
19
Godeps/_workspace/src/github.com/emicklei/go-restful/swagger/CHANGES.md
generated
vendored
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
Change history of swagger
|
||||||
|
=
|
||||||
|
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)
|
28
Godeps/_workspace/src/github.com/emicklei/go-restful/swagger/README.md
generated
vendored
Normal file
28
Godeps/_workspace/src/github.com/emicklei/go-restful/swagger/README.md
generated
vendored
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
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(),
|
||||||
|
WebServicesUrl: "http://localhost:8080",
|
||||||
|
ApiPath: "/apidocs.json",
|
||||||
|
SwaggerPath: "/apidocs/",
|
||||||
|
SwaggerFilePath: "/Users/emicklei/Projects/swagger-ui/dist"}
|
||||||
|
swagger.InstallSwaggerService(config)
|
||||||
|
|
||||||
|
|
||||||
|
Notes
|
||||||
|
--
|
||||||
|
- Use RouteBuilder.Operation(..) to set the Nickname field of the API spec
|
||||||
|
- 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.
|
25
Godeps/_workspace/src/github.com/emicklei/go-restful/swagger/config.go
generated
vendored
Normal file
25
Godeps/_workspace/src/github.com/emicklei/go-restful/swagger/config.go
generated
vendored
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package swagger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/emicklei/go-restful"
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
265
Godeps/_workspace/src/github.com/emicklei/go-restful/swagger/model_builder.go
generated
vendored
Normal file
265
Godeps/_workspace/src/github.com/emicklei/go-restful/swagger/model_builder.go
generated
vendored
Normal file
@ -0,0 +1,265 @@
|
|||||||
|
package swagger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type modelBuilder struct {
|
||||||
|
Models map[string]Model
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b modelBuilder) addModel(st reflect.Type, nameOverride string) {
|
||||||
|
modelName := b.keyFrom(st)
|
||||||
|
if nameOverride != "" {
|
||||||
|
modelName = nameOverride
|
||||||
|
}
|
||||||
|
// no models needed for primitive types
|
||||||
|
if b.isPrimitiveType(modelName) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// see if we already have visited this model
|
||||||
|
if _, ok := b.Models[modelName]; ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sm := Model{
|
||||||
|
Id: modelName,
|
||||||
|
Required: []string{},
|
||||||
|
Properties: map[string]ModelProperty{}}
|
||||||
|
|
||||||
|
// reference the model before further initializing (enables recursive structs)
|
||||||
|
b.Models[modelName] = sm
|
||||||
|
|
||||||
|
// check for slice or array
|
||||||
|
if st.Kind() == reflect.Slice || st.Kind() == reflect.Array {
|
||||||
|
b.addModel(st.Elem(), "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// check for structure or primitive type
|
||||||
|
if st.Kind() != reflect.Struct {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i := 0; i < st.NumField(); i++ {
|
||||||
|
field := st.Field(i)
|
||||||
|
jsonName, prop := b.buildProperty(field, &sm, modelName)
|
||||||
|
// add if not ommitted
|
||||||
|
if len(jsonName) != 0 {
|
||||||
|
// update Required
|
||||||
|
if b.isPropertyRequired(field) {
|
||||||
|
sm.Required = append(sm.Required, jsonName)
|
||||||
|
}
|
||||||
|
sm.Properties[jsonName] = prop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// update model builder with completed model
|
||||||
|
b.Models[modelName] = 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 string, prop ModelProperty) {
|
||||||
|
jsonName = b.jsonNameOfField(field)
|
||||||
|
if len(jsonName) == 0 {
|
||||||
|
// empty name signals skip property
|
||||||
|
return "", prop
|
||||||
|
}
|
||||||
|
fieldType := field.Type
|
||||||
|
fieldKind := fieldType.Kind()
|
||||||
|
|
||||||
|
if jsonTag := field.Tag.Get("json"); jsonTag != "" {
|
||||||
|
s := strings.Split(jsonTag, ",")
|
||||||
|
if len(s) > 1 && s[1] == "string" {
|
||||||
|
prop.Description = "(" + fieldType.String() + " as string)"
|
||||||
|
fieldType = reflect.TypeOf("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var pType = b.jsonSchemaType(fieldType.String()) // may include pkg path
|
||||||
|
prop.Type = &pType
|
||||||
|
if b.isPrimitiveType(fieldType.String()) {
|
||||||
|
prop.Format = b.jsonSchemaFormat(fieldType.String())
|
||||||
|
return jsonName, prop
|
||||||
|
}
|
||||||
|
|
||||||
|
marshalerType := reflect.TypeOf((*json.Marshaler)(nil)).Elem()
|
||||||
|
if fieldType.Implements(marshalerType) {
|
||||||
|
var pType = "string"
|
||||||
|
prop.Type = &pType
|
||||||
|
return jsonName, prop
|
||||||
|
}
|
||||||
|
|
||||||
|
if fieldKind == reflect.Struct {
|
||||||
|
return b.buildStructTypeProperty(field, jsonName, model)
|
||||||
|
}
|
||||||
|
|
||||||
|
if fieldKind == reflect.Slice || fieldKind == reflect.Array {
|
||||||
|
return b.buildArrayTypeProperty(field, jsonName, modelName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if fieldKind == reflect.Ptr {
|
||||||
|
return b.buildPointerTypeProperty(field, jsonName, modelName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if fieldType.Name() == "" { // override type of anonymous structs
|
||||||
|
nestedTypeName := modelName + "." + jsonName
|
||||||
|
var pType = nestedTypeName
|
||||||
|
prop.Type = &pType
|
||||||
|
b.addModel(fieldType, nestedTypeName)
|
||||||
|
}
|
||||||
|
return jsonName, prop
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b modelBuilder) buildStructTypeProperty(field reflect.StructField, jsonName string, model *Model) (nameJson string, prop ModelProperty) {
|
||||||
|
fieldType := field.Type
|
||||||
|
// check for anonymous
|
||||||
|
if len(fieldType.Name()) == 0 {
|
||||||
|
// anonymous
|
||||||
|
anonType := model.Id + "." + jsonName
|
||||||
|
b.addModel(fieldType, anonType)
|
||||||
|
prop.Type = &anonType
|
||||||
|
return jsonName, prop
|
||||||
|
}
|
||||||
|
if field.Name == fieldType.Name() && field.Anonymous {
|
||||||
|
// embedded struct
|
||||||
|
sub := modelBuilder{map[string]Model{}}
|
||||||
|
sub.addModel(fieldType, "")
|
||||||
|
subKey := sub.keyFrom(fieldType)
|
||||||
|
// merge properties from sub
|
||||||
|
subModel := sub.Models[subKey]
|
||||||
|
for k, v := range subModel.Properties {
|
||||||
|
model.Properties[k] = v
|
||||||
|
model.Required = append(model.Required, k)
|
||||||
|
}
|
||||||
|
// empty name signals skip property
|
||||||
|
return "", prop
|
||||||
|
}
|
||||||
|
// simple struct
|
||||||
|
b.addModel(fieldType, "")
|
||||||
|
var pType = fieldType.String()
|
||||||
|
prop.Type = &pType
|
||||||
|
return jsonName, prop
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b modelBuilder) buildArrayTypeProperty(field reflect.StructField, jsonName, modelName string) (nameJson string, prop ModelProperty) {
|
||||||
|
fieldType := field.Type
|
||||||
|
var pType = "array"
|
||||||
|
prop.Type = &pType
|
||||||
|
elemName := b.getElementTypeName(modelName, jsonName, fieldType.Elem())
|
||||||
|
prop.Items = []Item{Item{Ref: &elemName}}
|
||||||
|
// add|overwrite model for element type
|
||||||
|
b.addModel(fieldType.Elem(), elemName)
|
||||||
|
return jsonName, prop
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b modelBuilder) buildPointerTypeProperty(field reflect.StructField, jsonName, modelName string) (nameJson string, prop ModelProperty) {
|
||||||
|
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
|
||||||
|
elemName := b.getElementTypeName(modelName, jsonName, fieldType.Elem().Elem())
|
||||||
|
prop.Items = []Item{Item{Ref: &elemName}}
|
||||||
|
// add|overwrite model for element type
|
||||||
|
b.addModel(fieldType.Elem().Elem(), elemName)
|
||||||
|
} else {
|
||||||
|
// non-array, pointer type
|
||||||
|
var pType = fieldType.String()[1:] // no star, include pkg path
|
||||||
|
prop.Type = &pType
|
||||||
|
elemName := ""
|
||||||
|
if fieldType.Elem().Name() == "" {
|
||||||
|
elemName = modelName + "." + jsonName
|
||||||
|
prop.Type = &elemName
|
||||||
|
}
|
||||||
|
b.addModel(fieldType.Elem(), elemName)
|
||||||
|
}
|
||||||
|
return jsonName, prop
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b modelBuilder) getElementTypeName(modelName, jsonName string, t reflect.Type) string {
|
||||||
|
if t.Name() == "" {
|
||||||
|
return modelName + "." + jsonName
|
||||||
|
}
|
||||||
|
if b.isPrimitiveType(t.Name()) {
|
||||||
|
return b.jsonSchemaType(t.Name())
|
||||||
|
}
|
||||||
|
return b.keyFrom(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b modelBuilder) keyFrom(st reflect.Type) string {
|
||||||
|
key := st.String()
|
||||||
|
if len(st.Name()) == 0 { // unnamed type
|
||||||
|
// Swagger UI has special meaning for [
|
||||||
|
key = strings.Replace(key, "[]", "||", -1)
|
||||||
|
}
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b modelBuilder) isPrimitiveType(modelName string) bool {
|
||||||
|
return strings.Contains("uint8 int int32 int64 float32 float64 bool string byte 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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b modelBuilder) jsonSchemaType(modelName string) string {
|
||||||
|
schemaMap := map[string]string{
|
||||||
|
"uint8": "integer",
|
||||||
|
"int": "integer",
|
||||||
|
"int32": "integer",
|
||||||
|
"int64": "integer",
|
||||||
|
"byte": "string",
|
||||||
|
"float64": "number",
|
||||||
|
"float32": "number",
|
||||||
|
"bool": "boolean",
|
||||||
|
"time.Time": "string",
|
||||||
|
}
|
||||||
|
mapped, ok := schemaMap[modelName]
|
||||||
|
if ok {
|
||||||
|
return mapped
|
||||||
|
} else {
|
||||||
|
return modelName // use as is (custom or struct)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b modelBuilder) jsonSchemaFormat(modelName string) string {
|
||||||
|
schemaMap := map[string]string{
|
||||||
|
"int": "int32",
|
||||||
|
"int32": "int32",
|
||||||
|
"int64": "int64",
|
||||||
|
"byte": "byte",
|
||||||
|
"uint8": "byte",
|
||||||
|
"float64": "double",
|
||||||
|
"float32": "float",
|
||||||
|
"time.Time": "date-time",
|
||||||
|
}
|
||||||
|
mapped, ok := schemaMap[modelName]
|
||||||
|
if ok {
|
||||||
|
return mapped
|
||||||
|
} else {
|
||||||
|
return "" // no format
|
||||||
|
}
|
||||||
|
}
|
716
Godeps/_workspace/src/github.com/emicklei/go-restful/swagger/model_builder_test.go
generated
vendored
Normal file
716
Godeps/_workspace/src/github.com/emicklei/go-restful/swagger/model_builder_test.go
generated
vendored
Normal file
@ -0,0 +1,716 @@
|
|||||||
|
package swagger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type YesNo bool
|
||||||
|
|
||||||
|
func (y YesNo) MarshalJSON() ([]byte, error) {
|
||||||
|
if y {
|
||||||
|
return []byte("yes"), nil
|
||||||
|
}
|
||||||
|
return []byte("no"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear && go test -v -test.run TestCustomMarshaller_Issue96 ...swagger
|
||||||
|
func TestCustomMarshaller_Issue96(t *testing.T) {
|
||||||
|
type Vote struct {
|
||||||
|
What YesNo
|
||||||
|
}
|
||||||
|
testJsonFromStruct(t, Vote{}, `{
|
||||||
|
"swagger.Vote": {
|
||||||
|
"id": "swagger.Vote",
|
||||||
|
"required": [
|
||||||
|
"What"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"What": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear && go test -v -test.run TestPrimitiveTypes ...swagger
|
||||||
|
func TestPrimitiveTypes(t *testing.T) {
|
||||||
|
type Prims struct {
|
||||||
|
f float64
|
||||||
|
t time.Time
|
||||||
|
}
|
||||||
|
testJsonFromStruct(t, Prims{}, `{
|
||||||
|
"swagger.Prims": {
|
||||||
|
"id": "swagger.Prims",
|
||||||
|
"required": [
|
||||||
|
"f",
|
||||||
|
"t"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"f": {
|
||||||
|
"type": "number",
|
||||||
|
"format": "double"
|
||||||
|
},
|
||||||
|
"t": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear && go test -v -test.run TestS1 ...swagger
|
||||||
|
func TestS1(t *testing.T) {
|
||||||
|
type S1 struct {
|
||||||
|
Id string
|
||||||
|
}
|
||||||
|
testJsonFromStruct(t, S1{}, `{
|
||||||
|
"swagger.S1": {
|
||||||
|
"id": "swagger.S1",
|
||||||
|
"required": [
|
||||||
|
"Id"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Id": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear && go test -v -test.run TestS2 ...swagger
|
||||||
|
func TestS2(t *testing.T) {
|
||||||
|
type S2 struct {
|
||||||
|
Ids []string
|
||||||
|
}
|
||||||
|
testJsonFromStruct(t, S2{}, `{
|
||||||
|
"swagger.S2": {
|
||||||
|
"id": "swagger.S2",
|
||||||
|
"required": [
|
||||||
|
"Ids"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Ids": {
|
||||||
|
"type": "array",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"$ref": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear && go test -v -test.run TestS3 ...swagger
|
||||||
|
func TestS3(t *testing.T) {
|
||||||
|
type NestedS3 struct {
|
||||||
|
Id string
|
||||||
|
}
|
||||||
|
type S3 struct {
|
||||||
|
Nested NestedS3
|
||||||
|
}
|
||||||
|
testJsonFromStruct(t, S3{}, `{
|
||||||
|
"swagger.NestedS3": {
|
||||||
|
"id": "swagger.NestedS3",
|
||||||
|
"required": [
|
||||||
|
"Id"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Id": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"swagger.S3": {
|
||||||
|
"id": "swagger.S3",
|
||||||
|
"required": [
|
||||||
|
"Nested"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Nested": {
|
||||||
|
"type": "swagger.NestedS3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
type sample struct {
|
||||||
|
id string `swagger:"required"` // TODO
|
||||||
|
items []item
|
||||||
|
rootItem item `json:"root"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type item struct {
|
||||||
|
itemName string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear && go test -v -test.run TestSampleToModelAsJson ...swagger
|
||||||
|
func TestSampleToModelAsJson(t *testing.T) {
|
||||||
|
testJsonFromStruct(t, sample{items: []item{}}, `{
|
||||||
|
"swagger.item": {
|
||||||
|
"id": "swagger.item",
|
||||||
|
"required": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"swagger.sample": {
|
||||||
|
"id": "swagger.sample",
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"items",
|
||||||
|
"root"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"items": {
|
||||||
|
"type": "array",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"$ref": "swagger.item"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"type": "swagger.item"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJsonTags(t *testing.T) {
|
||||||
|
type X struct {
|
||||||
|
A string
|
||||||
|
B string `json:"-"`
|
||||||
|
C int `json:",string"`
|
||||||
|
D int `json:","`
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := `{
|
||||||
|
"swagger.X": {
|
||||||
|
"id": "swagger.X",
|
||||||
|
"required": [
|
||||||
|
"A",
|
||||||
|
"C",
|
||||||
|
"D"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"A": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"C": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "(int as string)"
|
||||||
|
},
|
||||||
|
"D": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
testJsonFromStruct(t, X{}, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJsonTagOmitempty(t *testing.T) {
|
||||||
|
type X struct {
|
||||||
|
A int `json:",omitempty"`
|
||||||
|
B int `json:"C,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := `{
|
||||||
|
"swagger.X": {
|
||||||
|
"id": "swagger.X",
|
||||||
|
"properties": {
|
||||||
|
"A": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32"
|
||||||
|
},
|
||||||
|
"C": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
testJsonFromStruct(t, X{}, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJsonTagName(t *testing.T) {
|
||||||
|
type X struct {
|
||||||
|
A string `json:"B"`
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := `{
|
||||||
|
"swagger.X": {
|
||||||
|
"id": "swagger.X",
|
||||||
|
"required": [
|
||||||
|
"B"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"B": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
testJsonFromStruct(t, X{}, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnonymousStruct(t *testing.T) {
|
||||||
|
type X struct {
|
||||||
|
A struct {
|
||||||
|
B int
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := `{
|
||||||
|
"swagger.X": {
|
||||||
|
"id": "swagger.X",
|
||||||
|
"required": [
|
||||||
|
"A"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"A": {
|
||||||
|
"type": "swagger.X.A"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"swagger.X.A": {
|
||||||
|
"id": "swagger.X.A",
|
||||||
|
"required": [
|
||||||
|
"B"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"B": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
testJsonFromStruct(t, X{}, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnonymousPtrStruct(t *testing.T) {
|
||||||
|
type X struct {
|
||||||
|
A *struct {
|
||||||
|
B int
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := `{
|
||||||
|
"swagger.X": {
|
||||||
|
"id": "swagger.X",
|
||||||
|
"required": [
|
||||||
|
"A"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"A": {
|
||||||
|
"type": "swagger.X.A"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"swagger.X.A": {
|
||||||
|
"id": "swagger.X.A",
|
||||||
|
"required": [
|
||||||
|
"B"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"B": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
testJsonFromStruct(t, X{}, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnonymousArrayStruct(t *testing.T) {
|
||||||
|
type X struct {
|
||||||
|
A []struct {
|
||||||
|
B int
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := `{
|
||||||
|
"swagger.X": {
|
||||||
|
"id": "swagger.X",
|
||||||
|
"required": [
|
||||||
|
"A"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"A": {
|
||||||
|
"type": "array",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"$ref": "swagger.X.A"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"swagger.X.A": {
|
||||||
|
"id": "swagger.X.A",
|
||||||
|
"required": [
|
||||||
|
"B"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"B": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
testJsonFromStruct(t, X{}, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnonymousPtrArrayStruct(t *testing.T) {
|
||||||
|
type X struct {
|
||||||
|
A *[]struct {
|
||||||
|
B int
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := `{
|
||||||
|
"swagger.X": {
|
||||||
|
"id": "swagger.X",
|
||||||
|
"required": [
|
||||||
|
"A"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"A": {
|
||||||
|
"type": "array",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"$ref": "swagger.X.A"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"swagger.X.A": {
|
||||||
|
"id": "swagger.X.A",
|
||||||
|
"required": [
|
||||||
|
"B"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"B": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
testJsonFromStruct(t, X{}, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
// go test -v -test.run TestEmbeddedStruct_Issue98 ...swagger
|
||||||
|
func TestEmbeddedStruct_Issue98(t *testing.T) {
|
||||||
|
type Y struct {
|
||||||
|
A int
|
||||||
|
}
|
||||||
|
type X struct {
|
||||||
|
Y
|
||||||
|
}
|
||||||
|
testJsonFromStruct(t, X{}, `{
|
||||||
|
"swagger.X": {
|
||||||
|
"id": "swagger.X",
|
||||||
|
"required": [
|
||||||
|
"A"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"A": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Dataset struct {
|
||||||
|
Names []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear && go test -v -test.run TestIssue85 ...swagger
|
||||||
|
func TestIssue85(t *testing.T) {
|
||||||
|
anon := struct{ Datasets []Dataset }{}
|
||||||
|
testJsonFromStruct(t, anon, `{
|
||||||
|
"struct { Datasets ||swagger.Dataset }": {
|
||||||
|
"id": "struct { Datasets ||swagger.Dataset }",
|
||||||
|
"required": [
|
||||||
|
"Datasets"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Datasets": {
|
||||||
|
"type": "array",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"$ref": "swagger.Dataset"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"swagger.Dataset": {
|
||||||
|
"id": "swagger.Dataset",
|
||||||
|
"required": [
|
||||||
|
"Names"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Names": {
|
||||||
|
"type": "array",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"$ref": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
type File struct {
|
||||||
|
History []File
|
||||||
|
HistoryPtrs []*File
|
||||||
|
}
|
||||||
|
|
||||||
|
// go test -v -test.run TestRecursiveStructure ...swagger
|
||||||
|
func TestRecursiveStructure(t *testing.T) {
|
||||||
|
testJsonFromStruct(t, File{}, `{
|
||||||
|
"swagger.File": {
|
||||||
|
"id": "swagger.File",
|
||||||
|
"required": [
|
||||||
|
"History",
|
||||||
|
"HistoryPtrs"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"History": {
|
||||||
|
"type": "array",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"$ref": "swagger.File"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"HistoryPtrs": {
|
||||||
|
"type": "array",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"$ref": "swagger.File.HistoryPtrs"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"swagger.File.HistoryPtrs": {
|
||||||
|
"id": "swagger.File.HistoryPtrs",
|
||||||
|
"properties": {}
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
type A1 struct {
|
||||||
|
B struct {
|
||||||
|
Id int
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// go test -v -test.run TestEmbeddedStructA1 ...swagger
|
||||||
|
func TestEmbeddedStructA1(t *testing.T) {
|
||||||
|
testJsonFromStruct(t, A1{}, `{
|
||||||
|
"swagger.A1": {
|
||||||
|
"id": "swagger.A1",
|
||||||
|
"required": [
|
||||||
|
"B"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"B": {
|
||||||
|
"type": "swagger.A1.B"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"swagger.A1.B": {
|
||||||
|
"id": "swagger.A1.B",
|
||||||
|
"required": [
|
||||||
|
"Id"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Id": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
type A2 struct {
|
||||||
|
C
|
||||||
|
}
|
||||||
|
type C struct {
|
||||||
|
Id int `json:"B"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// go test -v -test.run TestEmbeddedStructA2 ...swagger
|
||||||
|
func TestEmbeddedStructA2(t *testing.T) {
|
||||||
|
testJsonFromStruct(t, A2{}, `{
|
||||||
|
"swagger.A2": {
|
||||||
|
"id": "swagger.A2",
|
||||||
|
"required": [
|
||||||
|
"B"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"B": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
type A3 struct {
|
||||||
|
B D
|
||||||
|
}
|
||||||
|
|
||||||
|
type D struct {
|
||||||
|
Id int
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear && go test -v -test.run TestStructA3 ...swagger
|
||||||
|
func TestStructA3(t *testing.T) {
|
||||||
|
testJsonFromStruct(t, A3{}, `{
|
||||||
|
"swagger.A3": {
|
||||||
|
"id": "swagger.A3",
|
||||||
|
"required": [
|
||||||
|
"B"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"B": {
|
||||||
|
"type": "swagger.D"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"swagger.D": {
|
||||||
|
"id": "swagger.D",
|
||||||
|
"required": [
|
||||||
|
"Id"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Id": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ObjectId []byte
|
||||||
|
|
||||||
|
type Region struct {
|
||||||
|
Id ObjectId `bson:"_id" json:"id"`
|
||||||
|
Name string `bson:"name" json:"name"`
|
||||||
|
Type string `bson:"type" json:"type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear && go test -v -test.run TestRegion_Issue113 ...swagger
|
||||||
|
func TestRegion_Issue113(t *testing.T) {
|
||||||
|
testJsonFromStruct(t, []Region{}, `{
|
||||||
|
"integer": {
|
||||||
|
"id": "integer",
|
||||||
|
"properties": {}
|
||||||
|
},
|
||||||
|
"swagger.Region": {
|
||||||
|
"id": "swagger.Region",
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "array",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"$ref": "integer"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"||swagger.Region": {
|
||||||
|
"id": "||swagger.Region",
|
||||||
|
"properties": {}
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear && go test -v -test.run TestIssue158 ...swagger
|
||||||
|
func TestIssue158(t *testing.T) {
|
||||||
|
type Address struct {
|
||||||
|
Country string `json:"country,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Customer struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Address Address `json:"address"`
|
||||||
|
}
|
||||||
|
expected := `{
|
||||||
|
"swagger.Address": {
|
||||||
|
"id": "swagger.Address",
|
||||||
|
"properties": {
|
||||||
|
"country": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"swagger.Customer": {
|
||||||
|
"id": "swagger.Customer",
|
||||||
|
"required": [
|
||||||
|
"name",
|
||||||
|
"address"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"address": {
|
||||||
|
"type": "swagger.Address"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
testJsonFromStruct(t, Customer{}, expected)
|
||||||
|
}
|
184
Godeps/_workspace/src/github.com/emicklei/go-restful/swagger/swagger.go
generated
vendored
Normal file
184
Godeps/_workspace/src/github.com/emicklei/go-restful/swagger/swagger.go
generated
vendored
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
// 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"`
|
||||||
|
LicensUrl string `json:"licensUrl,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 /
|
||||||
|
Apis []Api `json:"apis,omitempty"`
|
||||||
|
Models map[string]Model `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 {
|
||||||
|
Type string `json:"type"`
|
||||||
|
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 map[string]ModelProperty `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
|
115
Godeps/_workspace/src/github.com/emicklei/go-restful/swagger/swagger_test.go
generated
vendored
Normal file
115
Godeps/_workspace/src/github.com/emicklei/go-restful/swagger/swagger_test.go
generated
vendored
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
package swagger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/emicklei/go-restful"
|
||||||
|
)
|
||||||
|
|
||||||
|
// go test -v -test.run TestApi ...swagger
|
||||||
|
func TestApi(t *testing.T) {
|
||||||
|
value := Api{Path: "/", Description: "Some Path", Operations: []Operation{}}
|
||||||
|
compareJson(t, true, value, `{"path":"/","description":"Some Path"}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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("/all").To(dummy).Writes(sample{}))
|
||||||
|
cfg := Config{
|
||||||
|
WebServicesUrl: "http://here.com",
|
||||||
|
ApiPath: "/apipath",
|
||||||
|
WebServices: []*restful.WebService{ws}}
|
||||||
|
sws := newSwaggerService(cfg)
|
||||||
|
decl := sws.composeDeclaration(ws, "/tests")
|
||||||
|
data, err := json.MarshalIndent(decl, " ", " ")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err.Error())
|
||||||
|
}
|
||||||
|
// for visual inspection only
|
||||||
|
fmt.Println(string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = map[string]Model{}
|
||||||
|
msgs := composeResponseMessages(route, decl)
|
||||||
|
if msgs[0].ResponseModel != "swagger.TestItem" {
|
||||||
|
t.Errorf("got %s want swagger.TestItem", msgs[0].ResponseModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear && go test -v -test.run TestComposeResponseMessageArray ...swagger
|
||||||
|
func TestComposeResponseMessageArray(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 = map[string]Model{}
|
||||||
|
msgs := composeResponseMessages(route, decl)
|
||||||
|
if msgs[0].ResponseModel != "array[swagger.TestItem]" {
|
||||||
|
t.Errorf("got %s want swagger.TestItem", msgs[0].ResponseModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIssue78(t *testing.T) {
|
||||||
|
sws := newSwaggerService(Config{})
|
||||||
|
models := map[string]Model{}
|
||||||
|
sws.addModelFromSampleTo(&Operation{}, true, Response{Items: &[]TestItem{}}, models)
|
||||||
|
model, ok := models["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["Code"]
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("missing code")
|
||||||
|
}
|
||||||
|
if "integer" != *code.Type {
|
||||||
|
t.Fatal("wrong code type:" + *code.Type)
|
||||||
|
}
|
||||||
|
items, ok := model.Properties["Items"]
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("missing items")
|
||||||
|
}
|
||||||
|
if "array" != *items.Type {
|
||||||
|
t.Fatal("wrong items type:" + *items.Type)
|
||||||
|
}
|
||||||
|
items_items := items.Items
|
||||||
|
if len(items_items) == 0 {
|
||||||
|
t.Fatal("missing items->items")
|
||||||
|
}
|
||||||
|
ref := items_items[0].Ref
|
||||||
|
if ref == nil {
|
||||||
|
t.Fatal("missing $ref")
|
||||||
|
}
|
||||||
|
if *ref != "swagger.TestItem" {
|
||||||
|
t.Fatal("wrong $ref:" + *ref)
|
||||||
|
}
|
||||||
|
}
|
349
Godeps/_workspace/src/github.com/emicklei/go-restful/swagger/swagger_webservice.go
generated
vendored
Normal file
349
Godeps/_workspace/src/github.com/emicklei/go-restful/swagger/swagger_webservice.go
generated
vendored
Normal file
@ -0,0 +1,349 @@
|
|||||||
|
package swagger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/emicklei/go-restful"
|
||||||
|
// "github.com/emicklei/hopwatch"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"reflect"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SwaggerService struct {
|
||||||
|
config Config
|
||||||
|
apiDeclarationMap map[string]ApiDeclaration
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSwaggerService(config Config) *SwaggerService {
|
||||||
|
return &SwaggerService{
|
||||||
|
config: config,
|
||||||
|
apiDeclarationMap: map[string]ApiDeclaration{}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogInfo is the function that is called when this package needs to log. It defaults to log.Printf
|
||||||
|
var LogInfo = log.Printf
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
|
||||||
|
// 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[entry]
|
||||||
|
if !exists {
|
||||||
|
sws.apiDeclarationMap[entry] = sws.composeDeclaration(each, entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else { // use root path
|
||||||
|
sws.apiDeclarationMap[each.RootPath()] = sws.composeDeclaration(each, each.RootPath())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 := ResourceListing{SwaggerVersion: swaggerVersion}
|
||||||
|
for k, v := range sws.apiDeclarationMap {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
resp.WriteAsJson(listing)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sws SwaggerService) getDeclarations(req *restful.Request, resp *restful.Response) {
|
||||||
|
decl := sws.apiDeclarationMap[composeRootPath(req)]
|
||||||
|
// 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
|
||||||
|
(&decl).BasePath = fmt.Sprintf("http://%s", req.Request.Host)
|
||||||
|
}
|
||||||
|
resp.WriteAsJson(decl)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sws SwaggerService) composeDeclaration(ws *restful.WebService, pathPrefix string) ApiDeclaration {
|
||||||
|
decl := ApiDeclaration{
|
||||||
|
SwaggerVersion: swaggerVersion,
|
||||||
|
BasePath: sws.config.WebServicesUrl,
|
||||||
|
ResourcePath: ws.RootPath(),
|
||||||
|
Models: map[string]Model{}}
|
||||||
|
|
||||||
|
// collect any path parameters
|
||||||
|
rootParams := []Parameter{}
|
||||||
|
for _, param := range ws.PathParameters() {
|
||||||
|
rootParams = append(rootParams, asSwaggerParameter(param.Data()))
|
||||||
|
}
|
||||||
|
// aggregate by path
|
||||||
|
pathToRoutes := map[string][]restful.Route{}
|
||||||
|
for _, other := range ws.Routes() {
|
||||||
|
if strings.HasPrefix(other.Path, pathPrefix) {
|
||||||
|
routes := pathToRoutes[other.Path]
|
||||||
|
pathToRoutes[other.Path] = append(routes, other)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for path, routes := range pathToRoutes {
|
||||||
|
api := Api{Path: strings.TrimSuffix(path, "/"), Description: ws.Documentation()}
|
||||||
|
for _, route := range routes {
|
||||||
|
operation := Operation{
|
||||||
|
Method: route.Method,
|
||||||
|
Summary: route.Doc,
|
||||||
|
Type: asDataType(route.WriteSample),
|
||||||
|
Parameters: []Parameter{},
|
||||||
|
Nickname: route.Operation,
|
||||||
|
ResponseMessages: composeResponseMessages(route, &decl)}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// composeResponseMessages takes the ResponseErrors (if any) and creates ResponseMessages from them.
|
||||||
|
func composeResponseMessages(route restful.Route, decl *ApiDeclaration) (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)
|
||||||
|
modelName := modelBuilder{}.keyFrom(st)
|
||||||
|
if isCollection {
|
||||||
|
modelName = "array[" + modelName + "]"
|
||||||
|
}
|
||||||
|
modelBuilder{decl.Models}.addModel(st, "")
|
||||||
|
// reference the model
|
||||||
|
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 map[string]Model) {
|
||||||
|
st := reflect.TypeOf(sample)
|
||||||
|
isCollection, st := detectCollectionType(st)
|
||||||
|
modelName := modelBuilder{}.keyFrom(st)
|
||||||
|
if isResponse {
|
||||||
|
if isCollection {
|
||||||
|
modelName = "array[" + modelName + "]"
|
||||||
|
}
|
||||||
|
operation.Type = modelName
|
||||||
|
}
|
||||||
|
modelBuilder{models}.addModel(reflect.TypeOf(sample), "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func asSwaggerParameter(param restful.ParameterData) Parameter {
|
||||||
|
return Parameter{
|
||||||
|
DataTypeFields: DataTypeFields{
|
||||||
|
Type: ¶m.DataType,
|
||||||
|
Format: asFormat(param.DataType),
|
||||||
|
},
|
||||||
|
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(name string) string {
|
||||||
|
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{}) string {
|
||||||
|
if any == nil {
|
||||||
|
return "void"
|
||||||
|
}
|
||||||
|
return reflect.TypeOf(any).Name()
|
||||||
|
}
|
70
Godeps/_workspace/src/github.com/emicklei/go-restful/swagger/utils_test.go
generated
vendored
Normal file
70
Godeps/_workspace/src/github.com/emicklei/go-restful/swagger/utils_test.go
generated
vendored
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
package swagger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testJsonFromStruct(t *testing.T, sample interface{}, expectedJson string) {
|
||||||
|
compareJson(t, false, modelsFromStruct(sample), expectedJson)
|
||||||
|
}
|
||||||
|
|
||||||
|
func modelsFromStruct(sample interface{}) map[string]Model {
|
||||||
|
models := map[string]Model{}
|
||||||
|
builder := modelBuilder{models}
|
||||||
|
builder.addModel(reflect.TypeOf(sample), "")
|
||||||
|
return models
|
||||||
|
}
|
||||||
|
|
||||||
|
func compareJson(t *testing.T, flatCompare bool, value interface{}, expectedJsonAsString string) {
|
||||||
|
var output []byte
|
||||||
|
var err error
|
||||||
|
if flatCompare {
|
||||||
|
output, err = json.Marshal(value)
|
||||||
|
} else {
|
||||||
|
output, err = json.MarshalIndent(value, " ", " ")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
actual := string(output)
|
||||||
|
if actual != expectedJsonAsString {
|
||||||
|
t.Errorf("First mismatch JSON doc at line:%d", indexOfNonMatchingLine(actual, expectedJsonAsString))
|
||||||
|
// Use simple fmt to create a pastable output :-)
|
||||||
|
fmt.Println("---- expected -----")
|
||||||
|
fmt.Println(withLineNumbers(expectedJsonAsString))
|
||||||
|
fmt.Println("---- actual -----")
|
||||||
|
fmt.Println(withLineNumbers(actual))
|
||||||
|
fmt.Println("---- raw -----")
|
||||||
|
fmt.Println(actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
184
Godeps/_workspace/src/github.com/emicklei/go-restful/web_service.go
generated
vendored
Normal file
184
Godeps/_workspace/src/github.com/emicklei/go-restful/web_service.go
generated
vendored
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
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 (
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WebService holds a collection of Route values that bind a Http Method + URL Path to a function.
|
||||||
|
type WebService struct {
|
||||||
|
rootPath string
|
||||||
|
pathExpr *pathExpression // cached compilation of rootPath as RegExp
|
||||||
|
routes []Route
|
||||||
|
produces []string
|
||||||
|
consumes []string
|
||||||
|
pathParameters []*Parameter
|
||||||
|
filters []FilterFunction
|
||||||
|
documentation string
|
||||||
|
}
|
||||||
|
|
||||||
|
// compiledPathExpression ensures that the path is compiled into a RegEx for those routers that need it.
|
||||||
|
func (w *WebService) compiledPathExpression() *pathExpression {
|
||||||
|
if w.pathExpr == nil {
|
||||||
|
if len(w.rootPath) == 0 {
|
||||||
|
w.Path("/") // lazy initialize path
|
||||||
|
}
|
||||||
|
compiled, err := newPathExpression(w.rootPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("[restful] Invalid path:%s because:%v", w.rootPath, err)
|
||||||
|
}
|
||||||
|
w.pathExpr = compiled
|
||||||
|
}
|
||||||
|
return w.pathExpr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path specifies the root URL template path of the WebService.
|
||||||
|
// All Routes will be relative to this path.
|
||||||
|
func (w *WebService) Path(root string) *WebService {
|
||||||
|
w.rootPath = root
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
// Param adds a PathParameter to document parameters used in the root path.
|
||||||
|
func (w *WebService) Param(parameter *Parameter) *WebService {
|
||||||
|
if w.pathParameters == nil {
|
||||||
|
w.pathParameters = []*Parameter{}
|
||||||
|
}
|
||||||
|
w.pathParameters = append(w.pathParameters, parameter)
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
// PathParameter creates a new Parameter of kind Path for documentation purposes.
|
||||||
|
// It is initialized as required with string as its DataType.
|
||||||
|
func (w *WebService) PathParameter(name, description string) *Parameter {
|
||||||
|
p := &Parameter{&ParameterData{Name: name, Description: description, Required: true, DataType: "string"}}
|
||||||
|
p.bePath()
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueryParameter creates a new Parameter of kind Query for documentation purposes.
|
||||||
|
// It is initialized as not required with string as its DataType.
|
||||||
|
func (w *WebService) QueryParameter(name, description string) *Parameter {
|
||||||
|
p := &Parameter{&ParameterData{Name: name, Description: description, Required: false, DataType: "string"}}
|
||||||
|
p.beQuery()
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// BodyParameter creates a new Parameter of kind Body for documentation purposes.
|
||||||
|
// It is initialized as required without a DataType.
|
||||||
|
func (w *WebService) BodyParameter(name, description string) *Parameter {
|
||||||
|
p := &Parameter{&ParameterData{Name: name, Description: description, Required: true}}
|
||||||
|
p.beBody()
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// HeaderParameter creates a new Parameter of kind (Http) Header for documentation purposes.
|
||||||
|
// It is initialized as not required with string as its DataType.
|
||||||
|
func (w *WebService) HeaderParameter(name, description string) *Parameter {
|
||||||
|
p := &Parameter{&ParameterData{Name: name, Description: description, Required: false, DataType: "string"}}
|
||||||
|
p.beHeader()
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormParameter creates a new Parameter of kind Form (using application/x-www-form-urlencoded) for documentation purposes.
|
||||||
|
// It is initialized as required with string as its DataType.
|
||||||
|
func (w *WebService) FormParameter(name, description string) *Parameter {
|
||||||
|
p := &Parameter{&ParameterData{Name: name, Description: description, Required: false, DataType: "string"}}
|
||||||
|
p.beForm()
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route creates a new Route using the RouteBuilder and add to the ordered list of Routes.
|
||||||
|
func (w *WebService) Route(builder *RouteBuilder) *WebService {
|
||||||
|
builder.copyDefaults(w.produces, w.consumes)
|
||||||
|
w.routes = append(w.routes, builder.Build())
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method creates a new RouteBuilder and initialize its http method
|
||||||
|
func (w *WebService) Method(httpMethod string) *RouteBuilder {
|
||||||
|
return new(RouteBuilder).servicePath(w.rootPath).Method(httpMethod)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Produces specifies that this WebService can produce one or more MIME types.
|
||||||
|
// Http requests must have one of these values set for the Accept header.
|
||||||
|
func (w *WebService) Produces(contentTypes ...string) *WebService {
|
||||||
|
w.produces = contentTypes
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consumes specifies that this WebService can consume one or more MIME types.
|
||||||
|
// Http requests must have one of these values set for the Content-Type header.
|
||||||
|
func (w *WebService) Consumes(accepts ...string) *WebService {
|
||||||
|
w.consumes = accepts
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
// Routes returns the Routes associated with this WebService
|
||||||
|
func (w WebService) Routes() []Route {
|
||||||
|
return w.routes
|
||||||
|
}
|
||||||
|
|
||||||
|
// RootPath returns the RootPath associated with this WebService. Default "/"
|
||||||
|
func (w WebService) RootPath() string {
|
||||||
|
return w.rootPath
|
||||||
|
}
|
||||||
|
|
||||||
|
// PathParameters return the path parameter names for (shared amoung its Routes)
|
||||||
|
func (w WebService) PathParameters() []*Parameter {
|
||||||
|
return w.pathParameters
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter adds a filter function to the chain of filters applicable to all its Routes
|
||||||
|
func (w *WebService) Filter(filter FilterFunction) *WebService {
|
||||||
|
w.filters = append(w.filters, filter)
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
// Doc is used to set the documentation of this service.
|
||||||
|
func (w *WebService) Doc(plainText string) *WebService {
|
||||||
|
w.documentation = plainText
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
// Documentation returns it.
|
||||||
|
func (w WebService) Documentation() string {
|
||||||
|
return w.documentation
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Convenience methods
|
||||||
|
*/
|
||||||
|
|
||||||
|
// HEAD is a shortcut for .Method("HEAD").Path(subPath)
|
||||||
|
func (w *WebService) HEAD(subPath string) *RouteBuilder {
|
||||||
|
return new(RouteBuilder).servicePath(w.rootPath).Method("HEAD").Path(subPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET is a shortcut for .Method("GET").Path(subPath)
|
||||||
|
func (w *WebService) GET(subPath string) *RouteBuilder {
|
||||||
|
return new(RouteBuilder).servicePath(w.rootPath).Method("GET").Path(subPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST is a shortcut for .Method("POST").Path(subPath)
|
||||||
|
func (w *WebService) POST(subPath string) *RouteBuilder {
|
||||||
|
return new(RouteBuilder).servicePath(w.rootPath).Method("POST").Path(subPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PUT is a shortcut for .Method("PUT").Path(subPath)
|
||||||
|
func (w *WebService) PUT(subPath string) *RouteBuilder {
|
||||||
|
return new(RouteBuilder).servicePath(w.rootPath).Method("PUT").Path(subPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PATCH is a shortcut for .Method("PATCH").Path(subPath)
|
||||||
|
func (w *WebService) PATCH(subPath string) *RouteBuilder {
|
||||||
|
return new(RouteBuilder).servicePath(w.rootPath).Method("PATCH").Path(subPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE is a shortcut for .Method("DELETE").Path(subPath)
|
||||||
|
func (w *WebService) DELETE(subPath string) *RouteBuilder {
|
||||||
|
return new(RouteBuilder).servicePath(w.rootPath).Method("DELETE").Path(subPath)
|
||||||
|
}
|
39
Godeps/_workspace/src/github.com/emicklei/go-restful/web_service_container.go
generated
vendored
Normal file
39
Godeps/_workspace/src/github.com/emicklei/go-restful/web_service_container.go
generated
vendored
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DefaultContainer is a restful.Container that uses http.DefaultServeMux
|
||||||
|
var DefaultContainer *Container
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
DefaultContainer = NewContainer()
|
||||||
|
DefaultContainer.ServeMux = http.DefaultServeMux
|
||||||
|
}
|
||||||
|
|
||||||
|
// If set the true then panics will not be caught to return HTTP 500.
|
||||||
|
// In that case, Route functions are responsible for handling any error situation.
|
||||||
|
// Default value is false = recover from panics. This has performance implications.
|
||||||
|
// OBSOLETE ; use restful.DefaultContainer.DoNotRecover(true)
|
||||||
|
var DoNotRecover = false
|
||||||
|
|
||||||
|
// Add registers a new WebService add it to the DefaultContainer.
|
||||||
|
func Add(service *WebService) {
|
||||||
|
DefaultContainer.Add(service)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter appends a container FilterFunction from the DefaultContainer.
|
||||||
|
// These are called before dispatching a http.Request to a WebService.
|
||||||
|
func Filter(filter FilterFunction) {
|
||||||
|
DefaultContainer.Filter(filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisteredWebServices returns the collections of WebServices from the DefaultContainer
|
||||||
|
func RegisteredWebServices() []*WebService {
|
||||||
|
return DefaultContainer.RegisteredWebServices()
|
||||||
|
}
|
115
Godeps/_workspace/src/github.com/emicklei/go-restful/web_service_test.go
generated
vendored
Normal file
115
Godeps/_workspace/src/github.com/emicklei/go-restful/web_service_test.go
generated
vendored
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
package restful
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
pathGetFriends = "/get/{userId}/friends"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParameter(t *testing.T) {
|
||||||
|
p := &Parameter{&ParameterData{Name: "name", Description: "desc"}}
|
||||||
|
p.AllowMultiple(true)
|
||||||
|
p.DataType("int")
|
||||||
|
p.Required(true)
|
||||||
|
values := map[string]string{"a": "b"}
|
||||||
|
p.AllowableValues(values)
|
||||||
|
p.bePath()
|
||||||
|
|
||||||
|
ws := new(WebService)
|
||||||
|
ws.Param(p)
|
||||||
|
if ws.pathParameters[0].Data().Name != "name" {
|
||||||
|
t.Error("path parameter (or name) invalid")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func TestWebService_CanCreateParameterKinds(t *testing.T) {
|
||||||
|
ws := new(WebService)
|
||||||
|
if ws.BodyParameter("b", "b").Kind() != BodyParameterKind {
|
||||||
|
t.Error("body parameter expected")
|
||||||
|
}
|
||||||
|
if ws.PathParameter("p", "p").Kind() != PathParameterKind {
|
||||||
|
t.Error("path parameter expected")
|
||||||
|
}
|
||||||
|
if ws.QueryParameter("q", "q").Kind() != QueryParameterKind {
|
||||||
|
t.Error("query parameter expected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCapturePanic(t *testing.T) {
|
||||||
|
tearDown()
|
||||||
|
Add(newPanicingService())
|
||||||
|
httpRequest, _ := http.NewRequest("GET", "http://here.com/fire", nil)
|
||||||
|
httpRequest.Header.Set("Accept", "*/*")
|
||||||
|
httpWriter := httptest.NewRecorder()
|
||||||
|
DefaultContainer.dispatch(httpWriter, httpRequest)
|
||||||
|
if 500 != httpWriter.Code {
|
||||||
|
t.Error("500 expected on fire")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNotFound(t *testing.T) {
|
||||||
|
tearDown()
|
||||||
|
httpRequest, _ := http.NewRequest("GET", "http://here.com/missing", nil)
|
||||||
|
httpRequest.Header.Set("Accept", "*/*")
|
||||||
|
httpWriter := httptest.NewRecorder()
|
||||||
|
DefaultContainer.dispatch(httpWriter, httpRequest)
|
||||||
|
if 404 != httpWriter.Code {
|
||||||
|
t.Error("404 expected on missing")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMethodNotAllowed(t *testing.T) {
|
||||||
|
tearDown()
|
||||||
|
Add(newGetOnlyService())
|
||||||
|
httpRequest, _ := http.NewRequest("POST", "http://here.com/get", nil)
|
||||||
|
httpRequest.Header.Set("Accept", "*/*")
|
||||||
|
httpWriter := httptest.NewRecorder()
|
||||||
|
DefaultContainer.dispatch(httpWriter, httpRequest)
|
||||||
|
if 405 != httpWriter.Code {
|
||||||
|
t.Error("405 expected method not allowed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSelectedRoutePath_Issue100(t *testing.T) {
|
||||||
|
tearDown()
|
||||||
|
Add(newSelectedRouteTestingService())
|
||||||
|
httpRequest, _ := http.NewRequest("GET", "http://here.com/get/232452/friends", nil)
|
||||||
|
httpRequest.Header.Set("Accept", "*/*")
|
||||||
|
httpWriter := httptest.NewRecorder()
|
||||||
|
DefaultContainer.dispatch(httpWriter, httpRequest)
|
||||||
|
if http.StatusOK != httpWriter.Code {
|
||||||
|
t.Error(http.StatusOK, "expected,", httpWriter.Code, "received.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPanicingService() *WebService {
|
||||||
|
ws := new(WebService).Path("")
|
||||||
|
ws.Route(ws.GET("/fire").To(doPanic))
|
||||||
|
return ws
|
||||||
|
}
|
||||||
|
|
||||||
|
func newGetOnlyService() *WebService {
|
||||||
|
ws := new(WebService).Path("")
|
||||||
|
ws.Route(ws.GET("/get").To(doPanic))
|
||||||
|
return ws
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSelectedRouteTestingService() *WebService {
|
||||||
|
ws := new(WebService).Path("")
|
||||||
|
ws.Route(ws.GET(pathGetFriends).To(selectedRouteChecker))
|
||||||
|
return ws
|
||||||
|
}
|
||||||
|
|
||||||
|
func selectedRouteChecker(req *Request, resp *Response) {
|
||||||
|
if req.SelectedRoutePath() != pathGetFriends {
|
||||||
|
resp.InternalServerError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func doPanic(req *Request, resp *Response) {
|
||||||
|
println("lightning...")
|
||||||
|
panic("fire")
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user