Add go-restful dependency

pull/6/head
Daniel Smith 2014-11-06 17:17:42 -08:00
parent cc30ed14d0
commit a3520701a3
82 changed files with 7854 additions and 0 deletions

5
Godeps/Godeps.json generated
View File

@ -58,6 +58,11 @@
"ImportPath": "github.com/elazarl/go-bindata-assetfs",
"Rev": "ae4665cf2d188c65764c73fe4af5378acc549510"
},
{
"ImportPath": "github.com/emicklei/go-restful",
"Comment": "v1.1.2-34-gcb26ade",
"Rev": "cb26adeb9644200cb4ec7b32be31e024696e8d00"
},
{
"ImportPath": "github.com/fsouza/go-dockerclient",
"Comment": "0.2.1-267-g15d2c6e",

View File

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

View 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

View File

@ -0,0 +1,22 @@
Copyright (c) 2012,2013 Ernest Micklei
MIT License
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View 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)
[![Build Status](https://drone.io/github.com/emicklei/go-restful/status.png)](https://drone.io/github.com/emicklei/go-restful/latest)[![library users](https://sourcegraph.com/api/repos/github.com/emicklei/go-restful/badges/library-users.png)](https://sourcegraph.com/github.com/emicklei/go-restful) [![authors](https://sourcegraph.com/api/repos/github.com/emicklei/go-restful/badges/authors.png)](https://sourcegraph.com/github.com/emicklei/go-restful) [![xrefs](https://sourcegraph.com/api/repos/github.com/emicklei/go-restful/badges/xrefs.png)](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.

View File

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

View File

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

View File

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

View File

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

View 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
}

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

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

View 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
}

View 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
}

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

View File

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

View 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
}

View 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
}

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

View 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

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

View File

@ -0,0 +1 @@
ignore

View File

@ -0,0 +1 @@
ignore

View File

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

View File

@ -0,0 +1 @@
ignore

View File

@ -0,0 +1,18 @@
application: 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

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

View 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()
}

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

View File

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

View 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())
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,107 @@
package main
import (
"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())
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,26 @@
package main
import (
"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"))
}

View File

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

View File

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

View File

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

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

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

View 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())
}

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

View 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)

View 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()
}

View 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

View 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
}

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

View 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
}

View 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
}

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

View 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
}

View 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
}

View 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
}

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

View 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()
}

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

View 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
}

View 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, "/")
}

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

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

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

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

View 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)

View 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.

View 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
}

View 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
}
}

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

View 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

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

View 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: &param.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()
}

View 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()
}

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

View 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()
}

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