Commit 2fcf1575 authored by Nikhil Jindal's avatar Nikhil Jindal

Merge pull request #17990 from nikhiljindal/updateGoRestful

Updating go restful
parents 2670226a 8e6059ab
...@@ -378,8 +378,8 @@ ...@@ -378,8 +378,8 @@
}, },
{ {
"ImportPath": "github.com/emicklei/go-restful", "ImportPath": "github.com/emicklei/go-restful",
"Comment": "v1.1.3-98-g1f9a0ee", "Comment": "v1.2",
"Rev": "1f9a0ee00ff93717a275e15b30cf7df356255877" "Rev": "777bb3f19bcafe2575ffb2a3e46af92509ae9594"
}, },
{ {
"ImportPath": "github.com/evanphx/json-patch", "ImportPath": "github.com/evanphx/json-patch",
......
Change history of go-restful Change history of go-restful
= =
2015-09-27
- rename new WriteStatusAnd... to WriteHeaderAnd... for consistency
2015-09-25
- fixed problem with changing Header after WriteHeader (issue 235)
2015-09-14
- changed behavior of WriteHeader (immediate write) and WriteEntity (no status write)
- added support for custom EntityReaderWriters.
2015-08-06 2015-08-06
- add support for reading entities from compressed request content - add support for reading entities from compressed request content
- use sync.Pool for compressors of http response and request body - use sync.Pool for compressors of http response and request body
......
...@@ -54,6 +54,8 @@ func (u UserResource) findUser(request *restful.Request, response *restful.Respo ...@@ -54,6 +54,8 @@ func (u UserResource) findUser(request *restful.Request, response *restful.Respo
- Panic recovery to produce HTTP 500, customizable using RecoverHandler(...) - Panic recovery to produce HTTP 500, customizable using RecoverHandler(...)
- Route errors produce HTTP 404/405/406/415 errors, customizable using ServiceErrorHandler(...) - Route errors produce HTTP 404/405/406/415 errors, customizable using ServiceErrorHandler(...)
- Configurable (trace) logging - Configurable (trace) logging
- Customizable encoding using EntityReaderWriter registration
- Customizable gzip/deflate readers and writers using CompressorProvider registration
### Resources ### Resources
......
...@@ -20,6 +20,7 @@ var EnableContentEncoding = false ...@@ -20,6 +20,7 @@ var EnableContentEncoding = false
type CompressingResponseWriter struct { type CompressingResponseWriter struct {
writer http.ResponseWriter writer http.ResponseWriter
compressor io.WriteCloser compressor io.WriteCloser
encoding string
} }
// Header is part of http.ResponseWriter interface // Header is part of http.ResponseWriter interface
...@@ -35,6 +36,9 @@ func (c *CompressingResponseWriter) WriteHeader(status int) { ...@@ -35,6 +36,9 @@ func (c *CompressingResponseWriter) WriteHeader(status int) {
// Write is part of http.ResponseWriter interface // Write is part of http.ResponseWriter interface
// It is passed through the compressor // It is passed through the compressor
func (c *CompressingResponseWriter) Write(bytes []byte) (int, error) { func (c *CompressingResponseWriter) Write(bytes []byte) (int, error) {
if c.isCompressorClosed() {
return -1, errors.New("Compressing error: tried to write data using closed compressor")
}
return c.compressor.Write(bytes) return c.compressor.Write(bytes)
} }
...@@ -44,8 +48,25 @@ func (c *CompressingResponseWriter) CloseNotify() <-chan bool { ...@@ -44,8 +48,25 @@ func (c *CompressingResponseWriter) CloseNotify() <-chan bool {
} }
// Close the underlying compressor // Close the underlying compressor
func (c *CompressingResponseWriter) Close() { func (c *CompressingResponseWriter) Close() error {
if c.isCompressorClosed() {
return errors.New("Compressing error: tried to close already closed compressor")
}
c.compressor.Close() c.compressor.Close()
if ENCODING_GZIP == c.encoding {
currentCompressorProvider.ReleaseGzipWriter(c.compressor.(*gzip.Writer))
}
if ENCODING_DEFLATE == c.encoding {
currentCompressorProvider.ReleaseZlibWriter(c.compressor.(*zlib.Writer))
}
// gc hint needed?
c.compressor = nil
return nil
}
func (c *CompressingResponseWriter) isCompressorClosed() bool {
return nil == c.compressor
} }
// WantsCompressedResponse reads the Accept-Encoding header to see if and which encoding is requested. // WantsCompressedResponse reads the Accept-Encoding header to see if and which encoding is requested.
...@@ -73,13 +94,15 @@ func NewCompressingResponseWriter(httpWriter http.ResponseWriter, encoding strin ...@@ -73,13 +94,15 @@ func NewCompressingResponseWriter(httpWriter http.ResponseWriter, encoding strin
c.writer = httpWriter c.writer = httpWriter
var err error var err error
if ENCODING_GZIP == encoding { if ENCODING_GZIP == encoding {
w := GzipWriterPool.Get().(*gzip.Writer) w := currentCompressorProvider.AcquireGzipWriter()
w.Reset(httpWriter) w.Reset(httpWriter)
c.compressor = w c.compressor = w
c.encoding = ENCODING_GZIP
} else if ENCODING_DEFLATE == encoding { } else if ENCODING_DEFLATE == encoding {
w := ZlibWriterPool.Get().(*zlib.Writer) w := currentCompressorProvider.AcquireZlibWriter()
w.Reset(httpWriter) w.Reset(httpWriter)
c.compressor = w c.compressor = w
c.encoding = ENCODING_DEFLATE
} else { } else {
return nil, errors.New("Unknown encoding:" + encoding) return nil, errors.New("Unknown encoding:" + encoding)
} }
......
package restful
// Copyright 2015 Ernest Micklei. All rights reserved.
// Use of this source code is governed by a license
// that can be found in the LICENSE file.
import (
"compress/gzip"
"compress/zlib"
)
// BoundedCachedCompressors is a CompressorProvider that uses a cache with a fixed amount
// of writers and readers (resources).
// If a new resource is acquired and all are in use, it will return a new unmanaged resource.
type BoundedCachedCompressors struct {
gzipWriters chan *gzip.Writer
gzipReaders chan *gzip.Reader
zlibWriters chan *zlib.Writer
writersCapacity int
readersCapacity int
}
// NewBoundedCachedCompressors returns a new, with filled cache, BoundedCachedCompressors.
func NewBoundedCachedCompressors(writersCapacity, readersCapacity int) *BoundedCachedCompressors {
b := &BoundedCachedCompressors{
gzipWriters: make(chan *gzip.Writer, writersCapacity),
gzipReaders: make(chan *gzip.Reader, readersCapacity),
zlibWriters: make(chan *zlib.Writer, writersCapacity),
writersCapacity: writersCapacity,
readersCapacity: readersCapacity,
}
for ix := 0; ix < writersCapacity; ix++ {
b.gzipWriters <- newGzipWriter()
b.zlibWriters <- newZlibWriter()
}
for ix := 0; ix < readersCapacity; ix++ {
b.gzipReaders <- newGzipReader()
}
return b
}
// AcquireGzipWriter returns an resettable *gzip.Writer. Needs to be released.
func (b *BoundedCachedCompressors) AcquireGzipWriter() *gzip.Writer {
var writer *gzip.Writer
select {
case writer, _ = <-b.gzipWriters:
default:
// return a new unmanaged one
writer = newGzipWriter()
}
return writer
}
// ReleaseGzipWriter accepts a writer (does not have to be one that was cached)
// only when the cache has room for it. It will ignore it otherwise.
func (b *BoundedCachedCompressors) ReleaseGzipWriter(w *gzip.Writer) {
// forget the unmanaged ones
if len(b.gzipWriters) < b.writersCapacity {
b.gzipWriters <- w
}
}
// AcquireGzipReader returns a *gzip.Reader. Needs to be released.
func (b *BoundedCachedCompressors) AcquireGzipReader() *gzip.Reader {
var reader *gzip.Reader
select {
case reader, _ = <-b.gzipReaders:
default:
// return a new unmanaged one
reader = newGzipReader()
}
return reader
}
// ReleaseGzipReader accepts a reader (does not have to be one that was cached)
// only when the cache has room for it. It will ignore it otherwise.
func (b *BoundedCachedCompressors) ReleaseGzipReader(r *gzip.Reader) {
// forget the unmanaged ones
if len(b.gzipReaders) < b.readersCapacity {
b.gzipReaders <- r
}
}
// AcquireZlibWriter returns an resettable *zlib.Writer. Needs to be released.
func (b *BoundedCachedCompressors) AcquireZlibWriter() *zlib.Writer {
var writer *zlib.Writer
select {
case writer, _ = <-b.zlibWriters:
default:
// return a new unmanaged one
writer = newZlibWriter()
}
return writer
}
// ReleaseZlibWriter accepts a writer (does not have to be one that was cached)
// only when the cache has room for it. It will ignore it otherwise.
func (b *BoundedCachedCompressors) ReleaseZlibWriter(w *zlib.Writer) {
// forget the unmanaged ones
if len(b.zlibWriters) < b.writersCapacity {
b.zlibWriters <- w
}
}
package restful package restful
// Copyright 2015 Ernest Micklei. All rights reserved.
// Use of this source code is governed by a license
// that can be found in the LICENSE file.
import ( import (
"bytes" "bytes"
"compress/gzip" "compress/gzip"
...@@ -7,12 +11,50 @@ import ( ...@@ -7,12 +11,50 @@ import (
"sync" "sync"
) )
// GzipWriterPool is used to get reusable zippers. // SyncPoolCompessors is a CompressorProvider that use the standard sync.Pool.
// The Get() result must be type asserted to *gzip.Writer. type SyncPoolCompessors struct {
var GzipWriterPool = &sync.Pool{ GzipWriterPool *sync.Pool
New: func() interface{} { GzipReaderPool *sync.Pool
return newGzipWriter() ZlibWriterPool *sync.Pool
}, }
// NewSyncPoolCompessors returns a new ("empty") SyncPoolCompessors.
func NewSyncPoolCompessors() *SyncPoolCompessors {
return &SyncPoolCompessors{
GzipWriterPool: &sync.Pool{
New: func() interface{} { return newGzipWriter() },
},
GzipReaderPool: &sync.Pool{
New: func() interface{} { return newGzipReader() },
},
ZlibWriterPool: &sync.Pool{
New: func() interface{} { return newZlibWriter() },
},
}
}
func (s *SyncPoolCompessors) AcquireGzipWriter() *gzip.Writer {
return s.GzipWriterPool.Get().(*gzip.Writer)
}
func (s *SyncPoolCompessors) ReleaseGzipWriter(w *gzip.Writer) {
s.GzipWriterPool.Put(w)
}
func (s *SyncPoolCompessors) AcquireGzipReader() *gzip.Reader {
return s.GzipReaderPool.Get().(*gzip.Reader)
}
func (s *SyncPoolCompessors) ReleaseGzipReader(r *gzip.Reader) {
s.GzipReaderPool.Put(r)
}
func (s *SyncPoolCompessors) AcquireZlibWriter() *zlib.Writer {
return s.ZlibWriterPool.Get().(*zlib.Writer)
}
func (s *SyncPoolCompessors) ReleaseZlibWriter(w *zlib.Writer) {
s.ZlibWriterPool.Put(w)
} }
func newGzipWriter() *gzip.Writer { func newGzipWriter() *gzip.Writer {
...@@ -24,17 +66,11 @@ func newGzipWriter() *gzip.Writer { ...@@ -24,17 +66,11 @@ func newGzipWriter() *gzip.Writer {
return writer return writer
} }
// GzipReaderPool is used to get reusable zippers.
// The Get() result must be type asserted to *gzip.Reader.
var GzipReaderPool = &sync.Pool{
New: func() interface{} {
return newGzipReader()
},
}
func newGzipReader() *gzip.Reader { func newGzipReader() *gzip.Reader {
// create with an empty reader (but with GZIP header); it will be replaced before using the gzipReader // create with an empty reader (but with GZIP header); it will be replaced before using the gzipReader
w := GzipWriterPool.Get().(*gzip.Writer) // we can safely use currentCompressProvider because it is set on package initialization.
w := currentCompressorProvider.AcquireGzipWriter()
defer currentCompressorProvider.ReleaseGzipWriter(w)
b := new(bytes.Buffer) b := new(bytes.Buffer)
w.Reset(b) w.Reset(b)
w.Flush() w.Flush()
...@@ -46,14 +82,6 @@ func newGzipReader() *gzip.Reader { ...@@ -46,14 +82,6 @@ func newGzipReader() *gzip.Reader {
return reader return reader
} }
// ZlibWriterPool is used to get reusable zippers.
// The Get() result must be type asserted to *zlib.Writer.
var ZlibWriterPool = &sync.Pool{
New: func() interface{} {
return newZlibWriter()
},
}
func newZlibWriter() *zlib.Writer { func newZlibWriter() *zlib.Writer {
writer, err := zlib.NewWriterLevel(new(bytes.Buffer), gzip.BestSpeed) writer, err := zlib.NewWriterLevel(new(bytes.Buffer), gzip.BestSpeed)
if err != nil { if err != nil {
......
package restful
// Copyright 2015 Ernest Micklei. All rights reserved.
// Use of this source code is governed by a license
// that can be found in the LICENSE file.
import (
"compress/gzip"
"compress/zlib"
)
type CompressorProvider interface {
// Returns a *gzip.Writer which needs to be released later.
// Before using it, call Reset().
AcquireGzipWriter() *gzip.Writer
// Releases an aqcuired *gzip.Writer.
ReleaseGzipWriter(w *gzip.Writer)
// Returns a *gzip.Reader which needs to be released later.
AcquireGzipReader() *gzip.Reader
// Releases an aqcuired *gzip.Reader.
ReleaseGzipReader(w *gzip.Reader)
// Returns a *zlib.Writer which needs to be released later.
// Before using it, call Reset().
AcquireZlibWriter() *zlib.Writer
// Releases an aqcuired *zlib.Writer.
ReleaseZlibWriter(w *zlib.Writer)
}
// DefaultCompressorProvider is the actual provider of compressors (zlib or gzip).
var currentCompressorProvider CompressorProvider
func init() {
currentCompressorProvider = NewSyncPoolCompessors()
}
// CurrentCompressorProvider returns the current CompressorProvider.
// It is initialized using a SyncPoolCompessors.
func CurrentCompressorProvider() CompressorProvider {
return currentCompressorProvider
}
// CompressorProvider sets the actual provider of compressors (zlib or gzip).
func SetCompressorProvider(p CompressorProvider) {
if p == nil {
panic("cannot set compressor provider to nil")
}
currentCompressorProvider = p
}
...@@ -272,7 +272,7 @@ func (c Container) Handle(pattern string, handler http.Handler) { ...@@ -272,7 +272,7 @@ func (c Container) Handle(pattern string, handler http.Handler) {
// HandleWithFilter registers the handler for the given pattern. // HandleWithFilter registers the handler for the given pattern.
// Container's filter chain is applied for handler. // Container's filter chain is applied for handler.
// If a handler already exists for pattern, HandleWithFilter panics. // If a handler already exists for pattern, HandleWithFilter panics.
func (c Container) HandleWithFilter(pattern string, handler http.Handler) { func (c *Container) HandleWithFilter(pattern string, handler http.Handler) {
f := func(httpResponse http.ResponseWriter, httpRequest *http.Request) { f := func(httpResponse http.ResponseWriter, httpRequest *http.Request) {
if len(c.containerFilters) == 0 { if len(c.containerFilters) == 0 {
handler.ServeHTTP(httpResponse, httpRequest) handler.ServeHTTP(httpResponse, httpRequest)
......
...@@ -162,6 +162,11 @@ Default value is false; it will recover from panics. This has performance implic ...@@ -162,6 +162,11 @@ Default value is false; it will recover from panics. This has performance implic
SetCacheReadEntity controls whether the response data ([]byte) is cached such that ReadEntity is repeatable. 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. If you expect to read large amounts of payload data, and you do not use this feature, you should set it to false.
restful.SetCompressorProvider(NewBoundedCachedCompressors(20, 20))
If content encoding is enabled then the default strategy for getting new gzip/zlib writers and readers is to use a sync.Pool.
Because writers are expensive structures, performance is even more improved when using a preloaded cache. You can also inject your own implementation.
Trouble shooting Trouble shooting
This package has the means to produce detail logging of the complete Http request matching process and filter invocation. This package has the means to produce detail logging of the complete Http request matching process and filter invocation.
......
package restful
// Copyright 2015 Ernest Micklei. All rights reserved.
// Use of this source code is governed by a license
// that can be found in the LICENSE file.
import (
"encoding/json"
"encoding/xml"
"strings"
"sync"
)
// EntityReaderWriter can read and write values using an encoding such as JSON,XML.
type EntityReaderWriter interface {
// Read a serialized version of the value from the request.
// The Request may have a decompressing reader. Depends on Content-Encoding.
Read(req *Request, v interface{}) error
// Write a serialized version of the value on the response.
// The Response may have a compressing writer. Depends on Accept-Encoding.
// status should be a valid Http Status code
Write(resp *Response, status int, v interface{}) error
}
// entityAccessRegistry is a singleton
var entityAccessRegistry = &entityReaderWriters{
protection: new(sync.RWMutex),
accessors: map[string]EntityReaderWriter{},
}
// entityReaderWriters associates MIME to an EntityReaderWriter
type entityReaderWriters struct {
protection *sync.RWMutex
accessors map[string]EntityReaderWriter
}
func init() {
RegisterEntityAccessor(MIME_JSON, entityJSONAccess{ContentType: MIME_JSON})
RegisterEntityAccessor(MIME_XML, entityXMLAccess{ContentType: MIME_XML})
}
// RegisterEntityAccessor add/overrides the ReaderWriter for encoding content with this MIME type.
func RegisterEntityAccessor(mime string, erw EntityReaderWriter) {
entityAccessRegistry.protection.Lock()
defer entityAccessRegistry.protection.Unlock()
entityAccessRegistry.accessors[mime] = erw
}
// AccessorAt returns the registered ReaderWriter for this MIME type.
func (r *entityReaderWriters) AccessorAt(mime string) (EntityReaderWriter, bool) {
r.protection.RLock()
defer r.protection.RUnlock()
er, ok := r.accessors[mime]
if !ok {
// retry with reverse lookup
// more expensive but we are in an exceptional situation anyway
for k, v := range r.accessors {
if strings.Contains(mime, k) {
return v, true
}
}
}
return er, ok
}
// entityXMLAccess is a EntityReaderWriter for XML encoding
type entityXMLAccess struct {
// This is used for setting the Content-Type header when writing
ContentType string
}
// Read unmarshalls the value from XML
func (e entityXMLAccess) Read(req *Request, v interface{}) error {
return xml.NewDecoder(req.Request.Body).Decode(v)
}
// Write marshalls the value to JSON and set the Content-Type Header.
func (e entityXMLAccess) Write(resp *Response, status int, v interface{}) error {
return writeXML(resp, status, e.ContentType, v)
}
// writeXML marshalls the value to JSON and set the Content-Type Header.
func writeXML(resp *Response, status int, contentType string, v interface{}) error {
if v == nil {
resp.WriteHeader(status)
// do not write a nil representation
return nil
}
if resp.prettyPrint {
// pretty output must be created and written explicitly
output, err := xml.MarshalIndent(v, " ", " ")
if err != nil {
return err
}
resp.Header().Set(HEADER_ContentType, contentType)
resp.WriteHeader(status)
_, err = resp.Write([]byte(xml.Header))
if err != nil {
return err
}
_, err = resp.Write(output)
return err
}
// not-so-pretty
resp.Header().Set(HEADER_ContentType, contentType)
resp.WriteHeader(status)
return xml.NewEncoder(resp).Encode(v)
}
// entityJSONAccess is a EntityReaderWriter for JSON encoding
type entityJSONAccess struct {
// This is used for setting the Content-Type header when writing
ContentType string
}
// Read unmarshalls the value from JSON
func (e entityJSONAccess) Read(req *Request, v interface{}) error {
decoder := json.NewDecoder(req.Request.Body)
decoder.UseNumber()
return decoder.Decode(v)
}
// Write marshalls the value to JSON and set the Content-Type Header.
func (e entityJSONAccess) Write(resp *Response, status int, v interface{}) error {
return writeJSON(resp, status, e.ContentType, v)
}
// write marshalls the value to JSON and set the Content-Type Header.
func writeJSON(resp *Response, status int, contentType string, v interface{}) error {
if v == nil {
resp.WriteHeader(status)
// do not write a nil representation
return nil
}
if resp.prettyPrint {
// pretty output must be created and written explicitly
output, err := json.MarshalIndent(v, " ", " ")
if err != nil {
return err
}
resp.Header().Set(HEADER_ContentType, contentType)
resp.WriteHeader(status)
_, err = resp.Write(output)
return err
}
// not-so-pretty
resp.Header().Set(HEADER_ContentType, contentType)
resp.WriteHeader(status)
return json.NewEncoder(resp).Encode(v)
}
application: datastore-example application: <your_app_id>
version: 1 version: 1
runtime: go runtime: go
api_version: go1 api_version: go1
......
package main package main
import ( import (
"appengine"
"appengine/datastore"
"appengine/user"
"github.com/emicklei/go-restful" "github.com/emicklei/go-restful"
"github.com/emicklei/go-restful/swagger" "github.com/emicklei/go-restful/swagger"
"google.golang.com/appengine"
"google.golang.com/appengine/datastore"
"google.golang.com/appengine/user"
"net/http" "net/http"
"time" "time"
) )
......
package main package main
import ( import (
"appengine"
"appengine/memcache"
"github.com/emicklei/go-restful" "github.com/emicklei/go-restful"
"github.com/emicklei/go-restful/swagger" "github.com/emicklei/go-restful/swagger"
"google.golang.com/appengine"
"google.golang.com/appengine/memcache"
"net/http" "net/http"
) )
......
...@@ -53,7 +53,7 @@ func main() { ...@@ -53,7 +53,7 @@ func main() {
// Add container filter to enable CORS // Add container filter to enable CORS
cors := restful.CrossOriginResourceSharing{ cors := restful.CrossOriginResourceSharing{
ExposeHeaders: []string{"X-My-Header"}, ExposeHeaders: []string{"X-My-Header"},
AllowedHeaders: []string{"Content-Type"}, AllowedHeaders: []string{"Content-Type", "Accept"},
CookiesAllowed: false, CookiesAllowed: false,
Container: wsContainer} Container: wsContainer}
wsContainer.Filter(cors.Filter) wsContainer.Filter(cors.Filter)
......
...@@ -100,8 +100,7 @@ func (u *UserResource) createUser(request *restful.Request, response *restful.Re ...@@ -100,8 +100,7 @@ func (u *UserResource) createUser(request *restful.Request, response *restful.Re
} }
usr.Id = strconv.Itoa(len(u.users) + 1) // simple id generation usr.Id = strconv.Itoa(len(u.users) + 1) // simple id generation
u.users[usr.Id] = *usr u.users[usr.Id] = *usr
response.WriteHeader(http.StatusCreated) response.WriteHeaderAndEntity(http.StatusCreated, usr)
response.WriteEntity(usr)
} }
// PUT http://localhost:8080/users/1 // PUT http://localhost:8080/users/1
......
...@@ -102,8 +102,7 @@ func (u *UserService) createUser(request *restful.Request, response *restful.Res ...@@ -102,8 +102,7 @@ func (u *UserService) createUser(request *restful.Request, response *restful.Res
err := request.ReadEntity(&usr) err := request.ReadEntity(&usr)
if err == nil { if err == nil {
u.users[usr.Id] = usr u.users[usr.Id] = usr
response.WriteHeader(http.StatusCreated) response.WriteHeaderAndEntity(http.StatusCreated, usr)
response.WriteEntity(usr)
} else { } else {
response.WriteError(http.StatusInternalServerError, err) response.WriteError(http.StatusInternalServerError, err)
} }
......
...@@ -15,7 +15,7 @@ var Logger StdLogger ...@@ -15,7 +15,7 @@ var Logger StdLogger
func init() { func init() {
// default Logger // default Logger
SetLogger(stdlog.New(os.Stdout, "[restful] ", stdlog.LstdFlags|stdlog.Lshortfile)) SetLogger(stdlog.New(os.Stderr, "[restful] ", stdlog.LstdFlags|stdlog.Lshortfile))
} }
func SetLogger(customLogger StdLogger) { func SetLogger(customLogger StdLogger) {
......
...@@ -8,7 +8,8 @@ import "strings" ...@@ -8,7 +8,8 @@ import "strings"
// OPTIONSFilter is a filter function that inspects the Http Request for the OPTIONS method // 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. // 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 // As for any filter, you can also install it for a particular WebService within a Container.
// Note: this filter is not needed when using CrossOriginResourceSharing (for CORS).
func (c *Container) OPTIONSFilter(req *Request, resp *Response, chain *FilterChain) { func (c *Container) OPTIONSFilter(req *Request, resp *Response, chain *FilterChain) {
if "OPTIONS" != req.Request.Method { if "OPTIONS" != req.Request.Method {
chain.ProcessFilter(req, resp) chain.ProcessFilter(req, resp)
...@@ -19,6 +20,7 @@ func (c *Container) OPTIONSFilter(req *Request, resp *Response, chain *FilterCha ...@@ -19,6 +20,7 @@ func (c *Container) OPTIONSFilter(req *Request, resp *Response, chain *FilterCha
// OPTIONSFilter is a filter function that inspects the Http Request for the OPTIONS method // 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. // and provides the response with a set of allowed methods for the request URL Path.
// Note: this filter is not needed when using CrossOriginResourceSharing (for CORS).
func OPTIONSFilter() FilterFunction { func OPTIONSFilter() FilterFunction {
return DefaultContainer.OPTIONSFilter return DefaultContainer.OPTIONSFilter
} }
...@@ -30,12 +30,12 @@ type Parameter struct { ...@@ -30,12 +30,12 @@ type Parameter struct {
// ParameterData represents the state of a Parameter. // ParameterData represents the state of a Parameter.
// It is made public to make it accessible to e.g. the Swagger package. // It is made public to make it accessible to e.g. the Swagger package.
type ParameterData struct { type ParameterData struct {
Name, Description, DataType string Name, Description, DataType, DataFormat string
Kind int Kind int
Required bool Required bool
AllowableValues map[string]string AllowableValues map[string]string
AllowMultiple bool AllowMultiple bool
DefaultValue string DefaultValue string
} }
// Data returns the state of the Parameter // Data returns the state of the Parameter
...@@ -95,6 +95,12 @@ func (p *Parameter) DataType(typeName string) *Parameter { ...@@ -95,6 +95,12 @@ func (p *Parameter) DataType(typeName string) *Parameter {
return p return p
} }
// DataFormat sets the dataFormat field for Swagger UI
func (p *Parameter) DataFormat(formatName string) *Parameter {
p.data.DataFormat = formatName
return p
}
// DefaultValue sets the default value field and returns the receiver // DefaultValue sets the default value field and returns the receiver
func (p *Parameter) DefaultValue(stringRepresentation string) *Parameter { func (p *Parameter) DefaultValue(stringRepresentation string) *Parameter {
p.data.DefaultValue = stringRepresentation p.data.DefaultValue = stringRepresentation
......
...@@ -6,14 +6,9 @@ package restful ...@@ -6,14 +6,9 @@ package restful
import ( import (
"bytes" "bytes"
"compress/gzip"
"compress/zlib" "compress/zlib"
"encoding/json"
"encoding/xml"
"io"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"strings"
) )
var defaultRequestContentType string var defaultRequestContentType string
...@@ -81,62 +76,43 @@ func (r *Request) HeaderParameter(name string) string { ...@@ -81,62 +76,43 @@ func (r *Request) HeaderParameter(name string) string {
return r.Request.Header.Get(name) return r.Request.Header.Get(name)
} }
// ReadEntity checks the Accept header and reads the content into the entityPointer // 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) { func (r *Request) ReadEntity(entityPointer interface{}) (err error) {
defer r.Request.Body.Close()
contentType := r.Request.Header.Get(HEADER_ContentType) contentType := r.Request.Header.Get(HEADER_ContentType)
contentEncoding := r.Request.Header.Get(HEADER_ContentEncoding) contentEncoding := r.Request.Header.Get(HEADER_ContentEncoding)
if doCacheReadEntityBytes {
return r.cachingReadEntity(contentType, contentEncoding, entityPointer)
}
// unmarshall directly from request Body
return r.decodeEntity(r.Request.Body, contentType, contentEncoding, entityPointer)
}
func (r *Request) cachingReadEntity(contentType string, contentEncoding string, entityPointer interface{}) (err error) { // OLD feature, cache the body for reads
var buffer []byte if doCacheReadEntityBytes {
if r.bodyContent != nil { if r.bodyContent == nil {
buffer = *r.bodyContent data, err := ioutil.ReadAll(r.Request.Body)
} else { if err != nil {
buffer, err = ioutil.ReadAll(r.Request.Body) return err
if err != nil { }
return err r.bodyContent = &data
} }
r.bodyContent = &buffer r.Request.Body = ioutil.NopCloser(bytes.NewReader(*r.bodyContent))
} }
return r.decodeEntity(bytes.NewReader(buffer), contentType, contentEncoding, entityPointer)
}
func (r *Request) decodeEntity(reader io.Reader, contentType string, contentEncoding string, entityPointer interface{}) (err error) {
entityReader := reader
// check if the request body needs decompression // check if the request body needs decompression
if ENCODING_GZIP == contentEncoding { if ENCODING_GZIP == contentEncoding {
gzipReader := GzipReaderPool.Get().(*gzip.Reader) gzipReader := currentCompressorProvider.AcquireGzipReader()
gzipReader.Reset(reader) defer currentCompressorProvider.ReleaseGzipReader(gzipReader)
entityReader = gzipReader gzipReader.Reset(r.Request.Body)
r.Request.Body = gzipReader
} else if ENCODING_DEFLATE == contentEncoding { } else if ENCODING_DEFLATE == contentEncoding {
zlibReader, err := zlib.NewReader(reader) zlibReader, err := zlib.NewReader(r.Request.Body)
if err != nil { if err != nil {
return err return err
} }
entityReader = zlibReader r.Request.Body = zlibReader
} }
// decode JSON // lookup the EntityReader
if strings.Contains(contentType, MIME_JSON) || MIME_JSON == defaultRequestContentType { entityReader, ok := entityAccessRegistry.AccessorAt(contentType)
decoder := json.NewDecoder(entityReader) if !ok {
decoder.UseNumber() return NewError(http.StatusBadRequest, "Unable to unmarshal content of type:"+contentType)
return decoder.Decode(entityPointer)
} }
return entityReader.Read(r, entityPointer)
// decode XML
if strings.Contains(contentType, MIME_XML) || MIME_XML == defaultRequestContentType {
return xml.NewDecoder(entityReader).Decode(entityPointer)
}
return NewError(http.StatusBadRequest, "Unable to unmarshal content of type:"+contentType)
} }
// SetAttribute adds or replaces the attribute with the given value. // SetAttribute adds or replaces the attribute with the given value.
......
Change history of swagger Change history of swagger
= =
2015-10-16
- add type override mechanism for swagger models (MR 254, nathanejohnson)
- replace uses of wildcard in generated apidocs (issue 251)
2015-05-25 2015-05-25
- (api break) changed the type of Properties in Model - (api break) changed the type of Properties in Model
- (api break) changed the type of Models in ApiDeclaration - (api break) changed the type of Models in ApiDeclaration
......
...@@ -29,4 +29,6 @@ type Config struct { ...@@ -29,4 +29,6 @@ type Config struct {
ApiVersion string ApiVersion string
// If set then call this handler after building the complete ApiDeclaration Map // If set then call this handler after building the complete ApiDeclaration Map
PostBuildHandler PostBuildDeclarationMapFunc PostBuildHandler PostBuildDeclarationMapFunc
// Swagger global info struct
Info Info
} }
...@@ -132,9 +132,11 @@ func (b modelBuilder) buildProperty(field reflect.StructField, model *Model, mod ...@@ -132,9 +132,11 @@ func (b modelBuilder) buildProperty(field reflect.StructField, model *Model, mod
modelDescription = tag modelDescription = tag
} }
fieldType := field.Type
prop.setPropertyMetadata(field) prop.setPropertyMetadata(field)
if prop.Type != nil {
return jsonName, modelDescription, prop
}
fieldType := field.Type
// check if type is doing its own marshalling // check if type is doing its own marshalling
marshalerType := reflect.TypeOf((*json.Marshaler)(nil)).Elem() marshalerType := reflect.TypeOf((*json.Marshaler)(nil)).Elem()
...@@ -212,8 +214,12 @@ func hasNamedJSONTag(field reflect.StructField) bool { ...@@ -212,8 +214,12 @@ func hasNamedJSONTag(field reflect.StructField) bool {
} }
func (b modelBuilder) buildStructTypeProperty(field reflect.StructField, jsonName string, model *Model) (nameJson string, prop ModelProperty) { func (b modelBuilder) buildStructTypeProperty(field reflect.StructField, jsonName string, model *Model) (nameJson string, prop ModelProperty) {
fieldType := field.Type
prop.setPropertyMetadata(field) prop.setPropertyMetadata(field)
// Check for type override in tag
if prop.Type != nil {
return jsonName, prop
}
fieldType := field.Type
// check for anonymous // check for anonymous
if len(fieldType.Name()) == 0 { if len(fieldType.Name()) == 0 {
// anonymous // anonymous
...@@ -263,8 +269,12 @@ func (b modelBuilder) buildStructTypeProperty(field reflect.StructField, jsonNam ...@@ -263,8 +269,12 @@ func (b modelBuilder) buildStructTypeProperty(field reflect.StructField, jsonNam
} }
func (b modelBuilder) buildArrayTypeProperty(field reflect.StructField, jsonName, modelName string) (nameJson string, prop ModelProperty) { func (b modelBuilder) buildArrayTypeProperty(field reflect.StructField, jsonName, modelName string) (nameJson string, prop ModelProperty) {
fieldType := field.Type // check for type override in tags
prop.setPropertyMetadata(field) prop.setPropertyMetadata(field)
if prop.Type != nil {
return jsonName, prop
}
fieldType := field.Type
var pType = "array" var pType = "array"
prop.Type = &pType prop.Type = &pType
elemTypeName := b.getElementTypeName(modelName, jsonName, fieldType.Elem()) elemTypeName := b.getElementTypeName(modelName, jsonName, fieldType.Elem())
...@@ -284,8 +294,12 @@ func (b modelBuilder) buildArrayTypeProperty(field reflect.StructField, jsonName ...@@ -284,8 +294,12 @@ func (b modelBuilder) buildArrayTypeProperty(field reflect.StructField, jsonName
} }
func (b modelBuilder) buildPointerTypeProperty(field reflect.StructField, jsonName, modelName string) (nameJson string, prop ModelProperty) { func (b modelBuilder) buildPointerTypeProperty(field reflect.StructField, jsonName, modelName string) (nameJson string, prop ModelProperty) {
fieldType := field.Type
prop.setPropertyMetadata(field) prop.setPropertyMetadata(field)
// Check for type override in tags
if prop.Type != nil {
return jsonName, prop
}
fieldType := field.Type
// override type of pointer to list-likes // override type of pointer to list-likes
if fieldType.Elem().Kind() == reflect.Slice || fieldType.Elem().Kind() == reflect.Array { if fieldType.Elem().Kind() == reflect.Slice || fieldType.Elem().Kind() == reflect.Array {
...@@ -338,7 +352,7 @@ func (b modelBuilder) keyFrom(st reflect.Type) string { ...@@ -338,7 +352,7 @@ func (b modelBuilder) keyFrom(st reflect.Type) string {
// see also https://golang.org/ref/spec#Numeric_types // see also https://golang.org/ref/spec#Numeric_types
func (b modelBuilder) isPrimitiveType(modelName string) bool { func (b modelBuilder) isPrimitiveType(modelName string) bool {
return strings.Contains("uint8 uint16 uint32 uint64 int int8 int16 int32 int64 float32 float64 bool string byte rune time.Time", modelName) return strings.Contains("uint uint8 uint16 uint32 uint64 int int8 int16 int32 int64 float32 float64 bool string byte rune time.Time", modelName)
} }
// jsonNameOfField returns the name of the field as it should appear in JSON format // jsonNameOfField returns the name of the field as it should appear in JSON format
...@@ -359,6 +373,7 @@ func (b modelBuilder) jsonNameOfField(field reflect.StructField) string { ...@@ -359,6 +373,7 @@ func (b modelBuilder) jsonNameOfField(field reflect.StructField) string {
// see also http://json-schema.org/latest/json-schema-core.html#anchor8 // see also http://json-schema.org/latest/json-schema-core.html#anchor8
func (b modelBuilder) jsonSchemaType(modelName string) string { func (b modelBuilder) jsonSchemaType(modelName string) string {
schemaMap := map[string]string{ schemaMap := map[string]string{
"uint": "integer",
"uint8": "integer", "uint8": "integer",
"uint16": "integer", "uint16": "integer",
"uint32": "integer", "uint32": "integer",
...@@ -389,6 +404,7 @@ func (b modelBuilder) jsonSchemaFormat(modelName string) string { ...@@ -389,6 +404,7 @@ func (b modelBuilder) jsonSchemaFormat(modelName string) string {
"int32": "int32", "int32": "int32",
"int64": "int64", "int64": "int64",
"byte": "byte", "byte": "byte",
"uint": "integer",
"uint8": "byte", "uint8": "byte",
"float64": "double", "float64": "double",
"float32": "float", "float32": "float",
......
...@@ -31,6 +31,12 @@ func (prop *ModelProperty) setMaximum(field reflect.StructField) { ...@@ -31,6 +31,12 @@ func (prop *ModelProperty) setMaximum(field reflect.StructField) {
} }
} }
func (prop *ModelProperty) setType(field reflect.StructField) {
if tag := field.Tag.Get("type"); tag != "" {
prop.Type = &tag
}
}
func (prop *ModelProperty) setMinimum(field reflect.StructField) { func (prop *ModelProperty) setMinimum(field reflect.StructField) {
if tag := field.Tag.Get("minimum"); tag != "" { if tag := field.Tag.Get("minimum"); tag != "" {
prop.Minimum = tag prop.Minimum = tag
...@@ -56,4 +62,5 @@ func (prop *ModelProperty) setPropertyMetadata(field reflect.StructField) { ...@@ -56,4 +62,5 @@ func (prop *ModelProperty) setPropertyMetadata(field reflect.StructField) {
prop.setMaximum(field) prop.setMaximum(field)
prop.setUniqueItems(field) prop.setUniqueItems(field)
prop.setDefaultValue(field) prop.setDefaultValue(field)
prop.setType(field)
} }
...@@ -48,7 +48,7 @@ type Info struct { ...@@ -48,7 +48,7 @@ type Info struct {
TermsOfServiceUrl string `json:"termsOfServiceUrl,omitempty"` TermsOfServiceUrl string `json:"termsOfServiceUrl,omitempty"`
Contact string `json:"contact,omitempty"` Contact string `json:"contact,omitempty"`
License string `json:"license,omitempty"` License string `json:"license,omitempty"`
LicensUrl string `json:"licensUrl,omitempty"` LicenseUrl string `json:"licenseUrl,omitempty"`
} }
// 5.1.5 // 5.1.5
...@@ -134,7 +134,7 @@ type Api struct { ...@@ -134,7 +134,7 @@ type Api struct {
// 5.2.3 Operation Object // 5.2.3 Operation Object
type Operation struct { type Operation struct {
Type string `json:"type"` DataTypeFields
Method string `json:"method"` Method string `json:"method"`
Summary string `json:"summary,omitempty"` Summary string `json:"summary,omitempty"`
Notes string `json:"notes,omitempty"` Notes string `json:"notes,omitempty"`
......
package swagger
type SwaggerBuilder struct {
SwaggerService
}
func NewSwaggerBuilder(config Config) *SwaggerBuilder {
return &SwaggerBuilder{*newSwaggerService(config)}
}
func (sb SwaggerBuilder) ProduceListing() ResourceListing {
return sb.SwaggerService.produceListing()
}
func (sb SwaggerBuilder) ProduceAllDeclarations() map[string]ApiDeclaration {
return sb.SwaggerService.produceAllDeclarations()
}
func (sb SwaggerBuilder) ProduceDeclarations(route string) (*ApiDeclaration, bool) {
return sb.SwaggerService.produceDeclarations(route)
}
...@@ -19,9 +19,35 @@ type SwaggerService struct { ...@@ -19,9 +19,35 @@ type SwaggerService struct {
} }
func newSwaggerService(config Config) *SwaggerService { func newSwaggerService(config Config) *SwaggerService {
return &SwaggerService{ sws := &SwaggerService{
config: config, config: config,
apiDeclarationMap: new(ApiDeclarationList)} apiDeclarationMap: new(ApiDeclarationList)}
// Build all ApiDeclarations
for _, each := range config.WebServices {
rootPath := each.RootPath()
// skip the api service itself
if rootPath != config.ApiPath {
if rootPath == "" || rootPath == "/" {
// use routes
for _, route := range each.Routes() {
entry := staticPathFromRoute(route)
_, exists := sws.apiDeclarationMap.At(entry)
if !exists {
sws.apiDeclarationMap.Put(entry, sws.composeDeclaration(each, entry))
}
}
} else { // use root path
sws.apiDeclarationMap.Put(each.RootPath(), sws.composeDeclaration(each, each.RootPath()))
}
}
}
// if specified then call the PostBuilderHandler
if config.PostBuildHandler != nil {
config.PostBuildHandler(sws.apiDeclarationMap)
}
return sws
} }
// LogInfo is the function that is called when this package needs to log. It defaults to log.Printf // LogInfo is the function that is called when this package needs to log. It defaults to log.Printf
...@@ -57,31 +83,6 @@ func RegisterSwaggerService(config Config, wsContainer *restful.Container) { ...@@ -57,31 +83,6 @@ func RegisterSwaggerService(config Config, wsContainer *restful.Container) {
LogInfo("[restful/swagger] listing is available at %v%v", config.WebServicesUrl, config.ApiPath) LogInfo("[restful/swagger] listing is available at %v%v", config.WebServicesUrl, config.ApiPath)
wsContainer.Add(ws) 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.At(entry)
if !exists {
sws.apiDeclarationMap.Put(entry, sws.composeDeclaration(each, entry))
}
}
} else { // use root path
sws.apiDeclarationMap.Put(each.RootPath(), sws.composeDeclaration(each, each.RootPath()))
}
}
}
// if specified then call the PostBuilderHandler
if config.PostBuildHandler != nil {
config.PostBuildHandler(sws.apiDeclarationMap)
}
// Check paths for UI serving // Check paths for UI serving
if config.StaticHandler == nil && config.SwaggerFilePath != "" && config.SwaggerPath != "" { if config.StaticHandler == nil && config.SwaggerFilePath != "" && config.SwaggerPath != "" {
swaggerPathSlash := config.SwaggerPath swaggerPathSlash := config.SwaggerPath
...@@ -138,7 +139,12 @@ func enableCORS(req *restful.Request, resp *restful.Response, chain *restful.Fil ...@@ -138,7 +139,12 @@ func enableCORS(req *restful.Request, resp *restful.Response, chain *restful.Fil
} }
func (sws SwaggerService) getListing(req *restful.Request, resp *restful.Response) { func (sws SwaggerService) getListing(req *restful.Request, resp *restful.Response) {
listing := ResourceListing{SwaggerVersion: swaggerVersion, ApiVersion: sws.config.ApiVersion} listing := sws.produceListing()
resp.WriteAsJson(listing)
}
func (sws SwaggerService) produceListing() ResourceListing {
listing := ResourceListing{SwaggerVersion: swaggerVersion, ApiVersion: sws.config.ApiVersion, Info: sws.config.Info}
sws.apiDeclarationMap.Do(func(k string, v ApiDeclaration) { sws.apiDeclarationMap.Do(func(k string, v ApiDeclaration) {
ref := Resource{Path: k} ref := Resource{Path: k}
if len(v.Apis) > 0 { // use description of first (could still be empty) if len(v.Apis) > 0 { // use description of first (could still be empty)
...@@ -146,11 +152,11 @@ func (sws SwaggerService) getListing(req *restful.Request, resp *restful.Respons ...@@ -146,11 +152,11 @@ func (sws SwaggerService) getListing(req *restful.Request, resp *restful.Respons
} }
listing.Apis = append(listing.Apis, ref) listing.Apis = append(listing.Apis, ref)
}) })
resp.WriteAsJson(listing) return listing
} }
func (sws SwaggerService) getDeclarations(req *restful.Request, resp *restful.Response) { func (sws SwaggerService) getDeclarations(req *restful.Request, resp *restful.Response) {
decl, ok := sws.apiDeclarationMap.At(composeRootPath(req)) decl, ok := sws.produceDeclarations(composeRootPath(req))
if !ok { if !ok {
resp.WriteErrorString(http.StatusNotFound, "ApiDeclaration not found") resp.WriteErrorString(http.StatusNotFound, "ApiDeclaration not found")
return return
...@@ -180,11 +186,28 @@ func (sws SwaggerService) getDeclarations(req *restful.Request, resp *restful.Re ...@@ -180,11 +186,28 @@ func (sws SwaggerService) getDeclarations(req *restful.Request, resp *restful.Re
scheme = "https" scheme = "https"
} }
} }
(&decl).BasePath = fmt.Sprintf("%s://%s", scheme, host) decl.BasePath = fmt.Sprintf("%s://%s", scheme, host)
} }
resp.WriteAsJson(decl) resp.WriteAsJson(decl)
} }
func (sws SwaggerService) produceAllDeclarations() map[string]ApiDeclaration {
decls := map[string]ApiDeclaration{}
sws.apiDeclarationMap.Do(func(k string, v ApiDeclaration) {
decls[k] = v
})
return decls
}
func (sws SwaggerService) produceDeclarations(route string) (*ApiDeclaration, bool) {
decl, ok := sws.apiDeclarationMap.At(route)
if !ok {
return nil, false
}
decl.BasePath = sws.config.WebServicesUrl
return &decl, true
}
// composeDeclaration uses all routes and parameters to create a ApiDeclaration // composeDeclaration uses all routes and parameters to create a ApiDeclaration
func (sws SwaggerService) composeDeclaration(ws *restful.WebService, pathPrefix string) ApiDeclaration { func (sws SwaggerService) composeDeclaration(ws *restful.WebService, pathPrefix string) ApiDeclaration {
decl := ApiDeclaration{ decl := ApiDeclaration{
...@@ -207,13 +230,15 @@ func (sws SwaggerService) composeDeclaration(ws *restful.WebService, pathPrefix ...@@ -207,13 +230,15 @@ func (sws SwaggerService) composeDeclaration(ws *restful.WebService, pathPrefix
} }
} }
pathToRoutes.Do(func(path string, routes []restful.Route) { pathToRoutes.Do(func(path string, routes []restful.Route) {
api := Api{Path: strings.TrimSuffix(path, "/"), Description: ws.Documentation()} api := Api{Path: strings.TrimSuffix(withoutWildcard(path), "/"), Description: ws.Documentation()}
voidString := "void"
for _, route := range routes { for _, route := range routes {
operation := Operation{ operation := Operation{
Method: route.Method, Method: route.Method,
Summary: route.Doc, Summary: route.Doc,
Notes: route.Notes, Notes: route.Notes,
Type: asDataType(route.WriteSample), // Type gets overwritten if there is a write sample
DataTypeFields: DataTypeFields{Type: &voidString},
Parameters: []Parameter{}, Parameters: []Parameter{},
Nickname: route.Operation, Nickname: route.Operation,
ResponseMessages: composeResponseMessages(route, &decl)} ResponseMessages: composeResponseMessages(route, &decl)}
...@@ -238,6 +263,13 @@ func (sws SwaggerService) composeDeclaration(ws *restful.WebService, pathPrefix ...@@ -238,6 +263,13 @@ func (sws SwaggerService) composeDeclaration(ws *restful.WebService, pathPrefix
return decl return decl
} }
func withoutWildcard(path string) string {
if strings.HasSuffix(path, ":*}") {
return path[0:len(path)-3] + "}"
}
return path
}
// composeResponseMessages takes the ResponseErrors (if any) and creates ResponseMessages from them. // composeResponseMessages takes the ResponseErrors (if any) and creates ResponseMessages from them.
func composeResponseMessages(route restful.Route, decl *ApiDeclaration) (messages []ResponseMessage) { func composeResponseMessages(route restful.Route, decl *ApiDeclaration) (messages []ResponseMessage) {
if route.ResponseErrors == nil { if route.ResponseErrors == nil {
...@@ -299,14 +331,10 @@ func detectCollectionType(st reflect.Type) (bool, reflect.Type) { ...@@ -299,14 +331,10 @@ func detectCollectionType(st reflect.Type) (bool, reflect.Type) {
// addModelFromSample creates and adds (or overwrites) a Model from a sample resource // addModelFromSample creates and adds (or overwrites) a Model from a sample resource
func (sws SwaggerService) addModelFromSampleTo(operation *Operation, isResponse bool, sample interface{}, models *ModelList) { func (sws SwaggerService) addModelFromSampleTo(operation *Operation, isResponse bool, sample interface{}, models *ModelList) {
st := reflect.TypeOf(sample)
isCollection, st := detectCollectionType(st)
modelName := modelBuilder{}.keyFrom(st)
if isResponse { if isResponse {
if isCollection { type_, items := asDataType(sample)
modelName = "array[" + modelName + "]" operation.Type = type_
} operation.Items = items
operation.Type = modelName
} }
modelBuilder{models}.addModelFrom(sample) modelBuilder{models}.addModelFrom(sample)
} }
...@@ -315,7 +343,7 @@ func asSwaggerParameter(param restful.ParameterData) Parameter { ...@@ -315,7 +343,7 @@ func asSwaggerParameter(param restful.ParameterData) Parameter {
return Parameter{ return Parameter{
DataTypeFields: DataTypeFields{ DataTypeFields: DataTypeFields{
Type: &param.DataType, Type: &param.DataType,
Format: asFormat(param.DataType), Format: asFormat(param.DataType, param.DataFormat),
DefaultValue: Special(param.DefaultValue), DefaultValue: Special(param.DefaultValue),
}, },
Name: param.Name, Name: param.Name,
...@@ -360,7 +388,10 @@ func composeRootPath(req *restful.Request) string { ...@@ -360,7 +388,10 @@ func composeRootPath(req *restful.Request) string {
return path + "/" + g return path + "/" + g
} }
func asFormat(name string) string { func asFormat(dataType string, dataFormat string) string {
if dataFormat != "" {
return dataFormat
}
return "" // TODO return "" // TODO
} }
...@@ -380,9 +411,30 @@ func asParamType(kind int) string { ...@@ -380,9 +411,30 @@ func asParamType(kind int) string {
return "" return ""
} }
func asDataType(any interface{}) string { func asDataType(any interface{}) (*string, *Item) {
if any == nil { // If it's not a collection, return the suggested model name
return "void" st := reflect.TypeOf(any)
isCollection, st := detectCollectionType(st)
modelName := modelBuilder{}.keyFrom(st)
// if it's not a collection we are done
if !isCollection {
return &modelName, nil
}
// XXX: This is not very elegant
// We create an Item object referring to the given model
models := ModelList{}
mb := modelBuilder{&models}
mb.addModelFrom(any)
elemTypeName := mb.getElementTypeName(modelName, "", st)
item := new(Item)
if mb.isPrimitiveType(elemTypeName) {
mapped := mb.jsonSchemaType(elemTypeName)
item.Type = &mapped
} else {
item.Ref = &elemTypeName
} }
return reflect.TypeOf(any).Name() tmp := "array"
return &tmp, item
} }
package test_package
type TestStruct struct {
TestField string
}
package restful package restful
import ( import (
"fmt"
"os" "os"
"sync"
"github.com/emicklei/go-restful/log" "github.com/emicklei/go-restful/log"
) )
...@@ -21,6 +23,15 @@ type WebService struct { ...@@ -21,6 +23,15 @@ type WebService struct {
filters []FilterFunction filters []FilterFunction
documentation string documentation string
apiVersion string apiVersion string
dynamicRoutes bool
// protects 'routes' if dynamic routes are enabled
routesLock sync.RWMutex
}
func (w *WebService) SetDynamicRoutes(enable bool) {
w.dynamicRoutes = enable
} }
// compilePathExpression ensures that the path is compiled into a RegEx for those routers that need it. // compilePathExpression ensures that the path is compiled into a RegEx for those routers that need it.
...@@ -134,11 +145,28 @@ func FormParameter(name, description string) *Parameter { ...@@ -134,11 +145,28 @@ func FormParameter(name, description string) *Parameter {
// Route creates a new Route using the RouteBuilder and add to the ordered list of Routes. // Route creates a new Route using the RouteBuilder and add to the ordered list of Routes.
func (w *WebService) Route(builder *RouteBuilder) *WebService { func (w *WebService) Route(builder *RouteBuilder) *WebService {
w.routesLock.Lock()
defer w.routesLock.Unlock()
builder.copyDefaults(w.produces, w.consumes) builder.copyDefaults(w.produces, w.consumes)
w.routes = append(w.routes, builder.Build()) w.routes = append(w.routes, builder.Build())
return w return w
} }
// RemoveRoute removes the specified route, looks for something that matches 'path' and 'method'
func (w *WebService) RemoveRoute(path, method string) error {
if !w.dynamicRoutes {
return fmt.Errorf("dynamic routes are not enabled.")
}
w.routesLock.Lock()
defer w.routesLock.Unlock()
for ix := range w.routes {
if w.routes[ix].Method == method && w.routes[ix].Path == path {
w.routes = append(w.routes[:ix], w.routes[ix+1:]...)
}
}
return nil
}
// Method creates a new RouteBuilder and initialize its http method // Method creates a new RouteBuilder and initialize its http method
func (w *WebService) Method(httpMethod string) *RouteBuilder { func (w *WebService) Method(httpMethod string) *RouteBuilder {
return new(RouteBuilder).servicePath(w.rootPath).Method(httpMethod) return new(RouteBuilder).servicePath(w.rootPath).Method(httpMethod)
...@@ -160,7 +188,17 @@ func (w *WebService) Consumes(accepts ...string) *WebService { ...@@ -160,7 +188,17 @@ func (w *WebService) Consumes(accepts ...string) *WebService {
// Routes returns the Routes associated with this WebService // Routes returns the Routes associated with this WebService
func (w WebService) Routes() []Route { func (w WebService) Routes() []Route {
return w.routes if !w.dynamicRoutes {
return w.routes
}
// Make a copy of the array to prevent concurrency problems
w.routesLock.RLock()
defer w.routesLock.RUnlock()
result := make([]Route, len(w.routes))
for ix := range w.routes {
result[ix] = w.routes[ix]
}
return result
} }
// RootPath returns the RootPath associated with this WebService. Default "/" // RootPath returns the RootPath associated with this WebService. Default "/"
......
...@@ -3168,7 +3168,7 @@ ...@@ -3168,7 +3168,7 @@
] ]
}, },
{ {
"path": "/api/v1/proxy/nodes/{name}/{path:*}", "path": "/api/v1/proxy/nodes/{name}/{path}",
"description": "API at /api/v1", "description": "API at /api/v1",
"operations": [ "operations": [
{ {
...@@ -5281,7 +5281,7 @@ ...@@ -5281,7 +5281,7 @@
] ]
}, },
{ {
"path": "/api/v1/proxy/namespaces/{namespace}/pods/{name}/{path:*}", "path": "/api/v1/proxy/namespaces/{namespace}/pods/{name}/{path}",
"description": "API at /api/v1", "description": "API at /api/v1",
"operations": [ "operations": [
{ {
...@@ -6633,7 +6633,7 @@ ...@@ -6633,7 +6633,7 @@
] ]
}, },
{ {
"path": "/api/v1/namespaces/{namespace}/pods/{name}/proxy/{path:*}", "path": "/api/v1/namespaces/{namespace}/pods/{name}/proxy/{path}",
"description": "API at /api/v1", "description": "API at /api/v1",
"operations": [ "operations": [
{ {
...@@ -10918,7 +10918,7 @@ ...@@ -10918,7 +10918,7 @@
] ]
}, },
{ {
"path": "/api/v1/proxy/namespaces/{namespace}/services/{name}/{path:*}", "path": "/api/v1/proxy/namespaces/{namespace}/services/{name}/{path}",
"description": "API at /api/v1", "description": "API at /api/v1",
"operations": [ "operations": [
{ {
......
...@@ -6819,7 +6819,7 @@ span.icon > [class^="icon-"], span.icon > [class*=" icon-"] { cursor: default; } ...@@ -6819,7 +6819,7 @@ span.icon > [class^="icon-"], span.icon > [class*=" icon-"] { cursor: default; }
<h3 id="_connect_get_requests_to_proxy_of_pod_2">connect GET requests to proxy of Pod</h3> <h3 id="_connect_get_requests_to_proxy_of_pod_2">connect GET requests to proxy of Pod</h3>
<div class="listingblock"> <div class="listingblock">
<div class="content"> <div class="content">
<pre>GET /api/v1/namespaces/{namespace}/pods/{name}/proxy/{path:*}</pre> <pre>GET /api/v1/namespaces/{namespace}/pods/{name}/proxy/{path}</pre>
</div> </div>
</div> </div>
<div class="sect3"> <div class="sect3">
...@@ -6940,7 +6940,7 @@ span.icon > [class^="icon-"], span.icon > [class*=" icon-"] { cursor: default; } ...@@ -6940,7 +6940,7 @@ span.icon > [class^="icon-"], span.icon > [class*=" icon-"] { cursor: default; }
<h3 id="_connect_put_requests_to_proxy_of_pod_2">connect PUT requests to proxy of Pod</h3> <h3 id="_connect_put_requests_to_proxy_of_pod_2">connect PUT requests to proxy of Pod</h3>
<div class="listingblock"> <div class="listingblock">
<div class="content"> <div class="content">
<pre>PUT /api/v1/namespaces/{namespace}/pods/{name}/proxy/{path:*}</pre> <pre>PUT /api/v1/namespaces/{namespace}/pods/{name}/proxy/{path}</pre>
</div> </div>
</div> </div>
<div class="sect3"> <div class="sect3">
...@@ -7061,7 +7061,7 @@ span.icon > [class^="icon-"], span.icon > [class*=" icon-"] { cursor: default; } ...@@ -7061,7 +7061,7 @@ span.icon > [class^="icon-"], span.icon > [class*=" icon-"] { cursor: default; }
<h3 id="_connect_delete_requests_to_proxy_of_pod_2">connect DELETE requests to proxy of Pod</h3> <h3 id="_connect_delete_requests_to_proxy_of_pod_2">connect DELETE requests to proxy of Pod</h3>
<div class="listingblock"> <div class="listingblock">
<div class="content"> <div class="content">
<pre>DELETE /api/v1/namespaces/{namespace}/pods/{name}/proxy/{path:*}</pre> <pre>DELETE /api/v1/namespaces/{namespace}/pods/{name}/proxy/{path}</pre>
</div> </div>
</div> </div>
<div class="sect3"> <div class="sect3">
...@@ -7182,7 +7182,7 @@ span.icon > [class^="icon-"], span.icon > [class*=" icon-"] { cursor: default; } ...@@ -7182,7 +7182,7 @@ span.icon > [class^="icon-"], span.icon > [class*=" icon-"] { cursor: default; }
<h3 id="_connect_post_requests_to_proxy_of_pod_2">connect POST requests to proxy of Pod</h3> <h3 id="_connect_post_requests_to_proxy_of_pod_2">connect POST requests to proxy of Pod</h3>
<div class="listingblock"> <div class="listingblock">
<div class="content"> <div class="content">
<pre>POST /api/v1/namespaces/{namespace}/pods/{name}/proxy/{path:*}</pre> <pre>POST /api/v1/namespaces/{namespace}/pods/{name}/proxy/{path}</pre>
</div> </div>
</div> </div>
<div class="sect3"> <div class="sect3">
...@@ -15215,7 +15215,7 @@ span.icon > [class^="icon-"], span.icon > [class*=" icon-"] { cursor: default; } ...@@ -15215,7 +15215,7 @@ span.icon > [class^="icon-"], span.icon > [class*=" icon-"] { cursor: default; }
<h3 id="_proxy_get_requests_to_pod_2">proxy GET requests to Pod</h3> <h3 id="_proxy_get_requests_to_pod_2">proxy GET requests to Pod</h3>
<div class="listingblock"> <div class="listingblock">
<div class="content"> <div class="content">
<pre>GET /api/v1/proxy/namespaces/{namespace}/pods/{name}/{path:*}</pre> <pre>GET /api/v1/proxy/namespaces/{namespace}/pods/{name}/{path}</pre>
</div> </div>
</div> </div>
<div class="sect3"> <div class="sect3">
...@@ -15328,7 +15328,7 @@ span.icon > [class^="icon-"], span.icon > [class*=" icon-"] { cursor: default; } ...@@ -15328,7 +15328,7 @@ span.icon > [class^="icon-"], span.icon > [class*=" icon-"] { cursor: default; }
<h3 id="_proxy_put_requests_to_pod_2">proxy PUT requests to Pod</h3> <h3 id="_proxy_put_requests_to_pod_2">proxy PUT requests to Pod</h3>
<div class="listingblock"> <div class="listingblock">
<div class="content"> <div class="content">
<pre>PUT /api/v1/proxy/namespaces/{namespace}/pods/{name}/{path:*}</pre> <pre>PUT /api/v1/proxy/namespaces/{namespace}/pods/{name}/{path}</pre>
</div> </div>
</div> </div>
<div class="sect3"> <div class="sect3">
...@@ -15441,7 +15441,7 @@ span.icon > [class^="icon-"], span.icon > [class*=" icon-"] { cursor: default; } ...@@ -15441,7 +15441,7 @@ span.icon > [class^="icon-"], span.icon > [class*=" icon-"] { cursor: default; }
<h3 id="_proxy_delete_requests_to_pod_2">proxy DELETE requests to Pod</h3> <h3 id="_proxy_delete_requests_to_pod_2">proxy DELETE requests to Pod</h3>
<div class="listingblock"> <div class="listingblock">
<div class="content"> <div class="content">
<pre>DELETE /api/v1/proxy/namespaces/{namespace}/pods/{name}/{path:*}</pre> <pre>DELETE /api/v1/proxy/namespaces/{namespace}/pods/{name}/{path}</pre>
</div> </div>
</div> </div>
<div class="sect3"> <div class="sect3">
...@@ -15554,7 +15554,7 @@ span.icon > [class^="icon-"], span.icon > [class*=" icon-"] { cursor: default; } ...@@ -15554,7 +15554,7 @@ span.icon > [class^="icon-"], span.icon > [class*=" icon-"] { cursor: default; }
<h3 id="_proxy_post_requests_to_pod_2">proxy POST requests to Pod</h3> <h3 id="_proxy_post_requests_to_pod_2">proxy POST requests to Pod</h3>
<div class="listingblock"> <div class="listingblock">
<div class="content"> <div class="content">
<pre>POST /api/v1/proxy/namespaces/{namespace}/pods/{name}/{path:*}</pre> <pre>POST /api/v1/proxy/namespaces/{namespace}/pods/{name}/{path}</pre>
</div> </div>
</div> </div>
<div class="sect3"> <div class="sect3">
...@@ -16087,7 +16087,7 @@ span.icon > [class^="icon-"], span.icon > [class*=" icon-"] { cursor: default; } ...@@ -16087,7 +16087,7 @@ span.icon > [class^="icon-"], span.icon > [class*=" icon-"] { cursor: default; }
<h3 id="_proxy_get_requests_to_service_2">proxy GET requests to Service</h3> <h3 id="_proxy_get_requests_to_service_2">proxy GET requests to Service</h3>
<div class="listingblock"> <div class="listingblock">
<div class="content"> <div class="content">
<pre>GET /api/v1/proxy/namespaces/{namespace}/services/{name}/{path:*}</pre> <pre>GET /api/v1/proxy/namespaces/{namespace}/services/{name}/{path}</pre>
</div> </div>
</div> </div>
<div class="sect3"> <div class="sect3">
...@@ -16200,7 +16200,7 @@ span.icon > [class^="icon-"], span.icon > [class*=" icon-"] { cursor: default; } ...@@ -16200,7 +16200,7 @@ span.icon > [class^="icon-"], span.icon > [class*=" icon-"] { cursor: default; }
<h3 id="_proxy_put_requests_to_service_2">proxy PUT requests to Service</h3> <h3 id="_proxy_put_requests_to_service_2">proxy PUT requests to Service</h3>
<div class="listingblock"> <div class="listingblock">
<div class="content"> <div class="content">
<pre>PUT /api/v1/proxy/namespaces/{namespace}/services/{name}/{path:*}</pre> <pre>PUT /api/v1/proxy/namespaces/{namespace}/services/{name}/{path}</pre>
</div> </div>
</div> </div>
<div class="sect3"> <div class="sect3">
...@@ -16313,7 +16313,7 @@ span.icon > [class^="icon-"], span.icon > [class*=" icon-"] { cursor: default; } ...@@ -16313,7 +16313,7 @@ span.icon > [class^="icon-"], span.icon > [class*=" icon-"] { cursor: default; }
<h3 id="_proxy_delete_requests_to_service_2">proxy DELETE requests to Service</h3> <h3 id="_proxy_delete_requests_to_service_2">proxy DELETE requests to Service</h3>
<div class="listingblock"> <div class="listingblock">
<div class="content"> <div class="content">
<pre>DELETE /api/v1/proxy/namespaces/{namespace}/services/{name}/{path:*}</pre> <pre>DELETE /api/v1/proxy/namespaces/{namespace}/services/{name}/{path}</pre>
</div> </div>
</div> </div>
<div class="sect3"> <div class="sect3">
...@@ -16426,7 +16426,7 @@ span.icon > [class^="icon-"], span.icon > [class*=" icon-"] { cursor: default; } ...@@ -16426,7 +16426,7 @@ span.icon > [class^="icon-"], span.icon > [class*=" icon-"] { cursor: default; }
<h3 id="_proxy_post_requests_to_service_2">proxy POST requests to Service</h3> <h3 id="_proxy_post_requests_to_service_2">proxy POST requests to Service</h3>
<div class="listingblock"> <div class="listingblock">
<div class="content"> <div class="content">
<pre>POST /api/v1/proxy/namespaces/{namespace}/services/{name}/{path:*}</pre> <pre>POST /api/v1/proxy/namespaces/{namespace}/services/{name}/{path}</pre>
</div> </div>
</div> </div>
<div class="sect3"> <div class="sect3">
...@@ -16927,7 +16927,7 @@ span.icon > [class^="icon-"], span.icon > [class*=" icon-"] { cursor: default; } ...@@ -16927,7 +16927,7 @@ span.icon > [class^="icon-"], span.icon > [class*=" icon-"] { cursor: default; }
<h3 id="_proxy_get_requests_to_node_2">proxy GET requests to Node</h3> <h3 id="_proxy_get_requests_to_node_2">proxy GET requests to Node</h3>
<div class="listingblock"> <div class="listingblock">
<div class="content"> <div class="content">
<pre>GET /api/v1/proxy/nodes/{name}/{path:*}</pre> <pre>GET /api/v1/proxy/nodes/{name}/{path}</pre>
</div> </div>
</div> </div>
<div class="sect3"> <div class="sect3">
...@@ -17032,7 +17032,7 @@ span.icon > [class^="icon-"], span.icon > [class*=" icon-"] { cursor: default; } ...@@ -17032,7 +17032,7 @@ span.icon > [class^="icon-"], span.icon > [class*=" icon-"] { cursor: default; }
<h3 id="_proxy_put_requests_to_node_2">proxy PUT requests to Node</h3> <h3 id="_proxy_put_requests_to_node_2">proxy PUT requests to Node</h3>
<div class="listingblock"> <div class="listingblock">
<div class="content"> <div class="content">
<pre>PUT /api/v1/proxy/nodes/{name}/{path:*}</pre> <pre>PUT /api/v1/proxy/nodes/{name}/{path}</pre>
</div> </div>
</div> </div>
<div class="sect3"> <div class="sect3">
...@@ -17137,7 +17137,7 @@ span.icon > [class^="icon-"], span.icon > [class*=" icon-"] { cursor: default; } ...@@ -17137,7 +17137,7 @@ span.icon > [class^="icon-"], span.icon > [class*=" icon-"] { cursor: default; }
<h3 id="_proxy_delete_requests_to_node_2">proxy DELETE requests to Node</h3> <h3 id="_proxy_delete_requests_to_node_2">proxy DELETE requests to Node</h3>
<div class="listingblock"> <div class="listingblock">
<div class="content"> <div class="content">
<pre>DELETE /api/v1/proxy/nodes/{name}/{path:*}</pre> <pre>DELETE /api/v1/proxy/nodes/{name}/{path}</pre>
</div> </div>
</div> </div>
<div class="sect3"> <div class="sect3">
...@@ -17242,7 +17242,7 @@ span.icon > [class^="icon-"], span.icon > [class*=" icon-"] { cursor: default; } ...@@ -17242,7 +17242,7 @@ span.icon > [class^="icon-"], span.icon > [class*=" icon-"] { cursor: default; }
<h3 id="_proxy_post_requests_to_node_2">proxy POST requests to Node</h3> <h3 id="_proxy_post_requests_to_node_2">proxy POST requests to Node</h3>
<div class="listingblock"> <div class="listingblock">
<div class="content"> <div class="content">
<pre>POST /api/v1/proxy/nodes/{name}/{path:*}</pre> <pre>POST /api/v1/proxy/nodes/{name}/{path}</pre>
</div> </div>
</div> </div>
<div class="sect3"> <div class="sect3">
...@@ -23664,7 +23664,7 @@ span.icon > [class^="icon-"], span.icon > [class*=" icon-"] { cursor: default; } ...@@ -23664,7 +23664,7 @@ span.icon > [class^="icon-"], span.icon > [class*=" icon-"] { cursor: default; }
</div> </div>
<div id="footer"> <div id="footer">
<div id="footer-text"> <div id="footer-text">
Last updated 2015-11-13 20:43:21 UTC Last updated 2015-12-01 01:46:05 UTC
</div> </div>
</div> </div>
</body> </body>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment