diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index acf7249b125..a8e78f3d09a 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -378,8 +378,8 @@ }, { "ImportPath": "github.com/emicklei/go-restful", - "Comment": "v1.1.3-98-g1f9a0ee", - "Rev": "1f9a0ee00ff93717a275e15b30cf7df356255877" + "Comment": "v1.2", + "Rev": "777bb3f19bcafe2575ffb2a3e46af92509ae9594" }, { "ImportPath": "github.com/evanphx/json-patch", diff --git a/Godeps/_workspace/src/github.com/emicklei/go-restful/CHANGES.md b/Godeps/_workspace/src/github.com/emicklei/go-restful/CHANGES.md index 1d209676d9f..45bd2012930 100644 --- a/Godeps/_workspace/src/github.com/emicklei/go-restful/CHANGES.md +++ b/Godeps/_workspace/src/github.com/emicklei/go-restful/CHANGES.md @@ -1,5 +1,15 @@ 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 - add support for reading entities from compressed request content - use sync.Pool for compressors of http response and request body diff --git a/Godeps/_workspace/src/github.com/emicklei/go-restful/README.md b/Godeps/_workspace/src/github.com/emicklei/go-restful/README.md index b20603fb395..8f954c0163c 100644 --- a/Godeps/_workspace/src/github.com/emicklei/go-restful/README.md +++ b/Godeps/_workspace/src/github.com/emicklei/go-restful/README.md @@ -54,6 +54,8 @@ func (u UserResource) findUser(request *restful.Request, response *restful.Respo - Panic recovery to produce HTTP 500, customizable using RecoverHandler(...) - Route errors produce HTTP 404/405/406/415 errors, customizable using ServiceErrorHandler(...) - Configurable (trace) logging +- Customizable encoding using EntityReaderWriter registration +- Customizable gzip/deflate readers and writers using CompressorProvider registration ### Resources diff --git a/Godeps/_workspace/src/github.com/emicklei/go-restful/compress.go b/Godeps/_workspace/src/github.com/emicklei/go-restful/compress.go index 4493f4db2ec..66f3603e4e2 100644 --- a/Godeps/_workspace/src/github.com/emicklei/go-restful/compress.go +++ b/Godeps/_workspace/src/github.com/emicklei/go-restful/compress.go @@ -20,6 +20,7 @@ var EnableContentEncoding = false type CompressingResponseWriter struct { writer http.ResponseWriter compressor io.WriteCloser + encoding string } // Header is part of http.ResponseWriter interface @@ -35,6 +36,9 @@ func (c *CompressingResponseWriter) WriteHeader(status int) { // Write is part of http.ResponseWriter interface // It is passed through the compressor func (c *CompressingResponseWriter) Write(bytes []byte) (int, error) { + if c.isCompressorClosed() { + return -1, errors.New("Compressing error: tried to write data using closed compressor") + } return c.compressor.Write(bytes) } @@ -44,8 +48,25 @@ func (c *CompressingResponseWriter) CloseNotify() <-chan bool { } // 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() + 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. @@ -73,13 +94,15 @@ func NewCompressingResponseWriter(httpWriter http.ResponseWriter, encoding strin c.writer = httpWriter var err error if ENCODING_GZIP == encoding { - w := GzipWriterPool.Get().(*gzip.Writer) + w := currentCompressorProvider.AcquireGzipWriter() w.Reset(httpWriter) c.compressor = w + c.encoding = ENCODING_GZIP } else if ENCODING_DEFLATE == encoding { - w := ZlibWriterPool.Get().(*zlib.Writer) + w := currentCompressorProvider.AcquireZlibWriter() w.Reset(httpWriter) c.compressor = w + c.encoding = ENCODING_DEFLATE } else { return nil, errors.New("Unknown encoding:" + encoding) } diff --git a/Godeps/_workspace/src/github.com/emicklei/go-restful/compressor_cache.go b/Godeps/_workspace/src/github.com/emicklei/go-restful/compressor_cache.go new file mode 100644 index 00000000000..ee426010a2d --- /dev/null +++ b/Godeps/_workspace/src/github.com/emicklei/go-restful/compressor_cache.go @@ -0,0 +1,103 @@ +package restful + +// Copyright 2015 Ernest Micklei. All rights reserved. +// Use of this source code is governed by a license +// that can be found in the LICENSE file. + +import ( + "compress/gzip" + "compress/zlib" +) + +// BoundedCachedCompressors is a CompressorProvider that uses a cache with a fixed amount +// of writers and readers (resources). +// If a new resource is acquired and all are in use, it will return a new unmanaged resource. +type BoundedCachedCompressors struct { + gzipWriters chan *gzip.Writer + gzipReaders chan *gzip.Reader + zlibWriters chan *zlib.Writer + writersCapacity int + readersCapacity int +} + +// NewBoundedCachedCompressors returns a new, with filled cache, BoundedCachedCompressors. +func NewBoundedCachedCompressors(writersCapacity, readersCapacity int) *BoundedCachedCompressors { + b := &BoundedCachedCompressors{ + gzipWriters: make(chan *gzip.Writer, writersCapacity), + gzipReaders: make(chan *gzip.Reader, readersCapacity), + zlibWriters: make(chan *zlib.Writer, writersCapacity), + writersCapacity: writersCapacity, + readersCapacity: readersCapacity, + } + for ix := 0; ix < writersCapacity; ix++ { + b.gzipWriters <- newGzipWriter() + b.zlibWriters <- newZlibWriter() + } + for ix := 0; ix < readersCapacity; ix++ { + b.gzipReaders <- newGzipReader() + } + return b +} + +// AcquireGzipWriter returns an resettable *gzip.Writer. Needs to be released. +func (b *BoundedCachedCompressors) AcquireGzipWriter() *gzip.Writer { + var writer *gzip.Writer + select { + case writer, _ = <-b.gzipWriters: + default: + // return a new unmanaged one + writer = newGzipWriter() + } + return writer +} + +// ReleaseGzipWriter accepts a writer (does not have to be one that was cached) +// only when the cache has room for it. It will ignore it otherwise. +func (b *BoundedCachedCompressors) ReleaseGzipWriter(w *gzip.Writer) { + // forget the unmanaged ones + if len(b.gzipWriters) < b.writersCapacity { + b.gzipWriters <- w + } +} + +// AcquireGzipReader returns a *gzip.Reader. Needs to be released. +func (b *BoundedCachedCompressors) AcquireGzipReader() *gzip.Reader { + var reader *gzip.Reader + select { + case reader, _ = <-b.gzipReaders: + default: + // return a new unmanaged one + reader = newGzipReader() + } + return reader +} + +// ReleaseGzipReader accepts a reader (does not have to be one that was cached) +// only when the cache has room for it. It will ignore it otherwise. +func (b *BoundedCachedCompressors) ReleaseGzipReader(r *gzip.Reader) { + // forget the unmanaged ones + if len(b.gzipReaders) < b.readersCapacity { + b.gzipReaders <- r + } +} + +// AcquireZlibWriter returns an resettable *zlib.Writer. Needs to be released. +func (b *BoundedCachedCompressors) AcquireZlibWriter() *zlib.Writer { + var writer *zlib.Writer + select { + case writer, _ = <-b.zlibWriters: + default: + // return a new unmanaged one + writer = newZlibWriter() + } + return writer +} + +// ReleaseZlibWriter accepts a writer (does not have to be one that was cached) +// only when the cache has room for it. It will ignore it otherwise. +func (b *BoundedCachedCompressors) ReleaseZlibWriter(w *zlib.Writer) { + // forget the unmanaged ones + if len(b.zlibWriters) < b.writersCapacity { + b.zlibWriters <- w + } +} diff --git a/Godeps/_workspace/src/github.com/emicklei/go-restful/compressor_pools.go b/Godeps/_workspace/src/github.com/emicklei/go-restful/compressor_pools.go index 5ee18296054..d866ce64bba 100644 --- a/Godeps/_workspace/src/github.com/emicklei/go-restful/compressor_pools.go +++ b/Godeps/_workspace/src/github.com/emicklei/go-restful/compressor_pools.go @@ -1,5 +1,9 @@ package restful +// Copyright 2015 Ernest Micklei. All rights reserved. +// Use of this source code is governed by a license +// that can be found in the LICENSE file. + import ( "bytes" "compress/gzip" @@ -7,12 +11,50 @@ import ( "sync" ) -// GzipWriterPool is used to get reusable zippers. -// The Get() result must be type asserted to *gzip.Writer. -var GzipWriterPool = &sync.Pool{ - New: func() interface{} { - return newGzipWriter() - }, +// SyncPoolCompessors is a CompressorProvider that use the standard sync.Pool. +type SyncPoolCompessors struct { + GzipWriterPool *sync.Pool + GzipReaderPool *sync.Pool + ZlibWriterPool *sync.Pool +} + +// NewSyncPoolCompessors returns a new ("empty") SyncPoolCompessors. +func NewSyncPoolCompessors() *SyncPoolCompessors { + return &SyncPoolCompessors{ + GzipWriterPool: &sync.Pool{ + New: func() interface{} { return newGzipWriter() }, + }, + GzipReaderPool: &sync.Pool{ + New: func() interface{} { return newGzipReader() }, + }, + ZlibWriterPool: &sync.Pool{ + New: func() interface{} { return newZlibWriter() }, + }, + } +} + +func (s *SyncPoolCompessors) AcquireGzipWriter() *gzip.Writer { + return s.GzipWriterPool.Get().(*gzip.Writer) +} + +func (s *SyncPoolCompessors) ReleaseGzipWriter(w *gzip.Writer) { + s.GzipWriterPool.Put(w) +} + +func (s *SyncPoolCompessors) AcquireGzipReader() *gzip.Reader { + return s.GzipReaderPool.Get().(*gzip.Reader) +} + +func (s *SyncPoolCompessors) ReleaseGzipReader(r *gzip.Reader) { + s.GzipReaderPool.Put(r) +} + +func (s *SyncPoolCompessors) AcquireZlibWriter() *zlib.Writer { + return s.ZlibWriterPool.Get().(*zlib.Writer) +} + +func (s *SyncPoolCompessors) ReleaseZlibWriter(w *zlib.Writer) { + s.ZlibWriterPool.Put(w) } func newGzipWriter() *gzip.Writer { @@ -24,17 +66,11 @@ func newGzipWriter() *gzip.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 { // 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) w.Reset(b) w.Flush() @@ -46,14 +82,6 @@ func newGzipReader() *gzip.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 { writer, err := zlib.NewWriterLevel(new(bytes.Buffer), gzip.BestSpeed) if err != nil { diff --git a/Godeps/_workspace/src/github.com/emicklei/go-restful/compressors.go b/Godeps/_workspace/src/github.com/emicklei/go-restful/compressors.go new file mode 100644 index 00000000000..f028456e0f6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/emicklei/go-restful/compressors.go @@ -0,0 +1,53 @@ +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 +} diff --git a/Godeps/_workspace/src/github.com/emicklei/go-restful/container.go b/Godeps/_workspace/src/github.com/emicklei/go-restful/container.go index 840d14b31e3..59f34abea9c 100644 --- a/Godeps/_workspace/src/github.com/emicklei/go-restful/container.go +++ b/Godeps/_workspace/src/github.com/emicklei/go-restful/container.go @@ -272,7 +272,7 @@ func (c Container) Handle(pattern string, handler http.Handler) { // HandleWithFilter registers the handler for the given pattern. // Container's filter chain is applied for handler. // If a handler already exists for pattern, HandleWithFilter panics. -func (c Container) HandleWithFilter(pattern string, handler http.Handler) { +func (c *Container) HandleWithFilter(pattern string, handler http.Handler) { f := func(httpResponse http.ResponseWriter, httpRequest *http.Request) { if len(c.containerFilters) == 0 { handler.ServeHTTP(httpResponse, httpRequest) diff --git a/Godeps/_workspace/src/github.com/emicklei/go-restful/doc.go b/Godeps/_workspace/src/github.com/emicklei/go-restful/doc.go index aff2f508a3d..d40405bf76a 100644 --- a/Godeps/_workspace/src/github.com/emicklei/go-restful/doc.go +++ b/Godeps/_workspace/src/github.com/emicklei/go-restful/doc.go @@ -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. 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 This package has the means to produce detail logging of the complete Http request matching process and filter invocation. diff --git a/Godeps/_workspace/src/github.com/emicklei/go-restful/entity_accessors.go b/Godeps/_workspace/src/github.com/emicklei/go-restful/entity_accessors.go new file mode 100644 index 00000000000..e3ab79d9b12 --- /dev/null +++ b/Godeps/_workspace/src/github.com/emicklei/go-restful/entity_accessors.go @@ -0,0 +1,151 @@ +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) +} diff --git a/Godeps/_workspace/src/github.com/emicklei/go-restful/examples/google_app_engine/datastore/app.yaml b/Godeps/_workspace/src/github.com/emicklei/go-restful/examples/google_app_engine/datastore/app.yaml index 33883d140a5..1ac9dca28f3 100644 --- a/Godeps/_workspace/src/github.com/emicklei/go-restful/examples/google_app_engine/datastore/app.yaml +++ b/Godeps/_workspace/src/github.com/emicklei/go-restful/examples/google_app_engine/datastore/app.yaml @@ -1,4 +1,4 @@ -application: datastore-example +application: version: 1 runtime: go api_version: go1 diff --git a/Godeps/_workspace/src/github.com/emicklei/go-restful/examples/google_app_engine/datastore/main.go b/Godeps/_workspace/src/github.com/emicklei/go-restful/examples/google_app_engine/datastore/main.go index cf832ef03f5..9f9c78d1fa6 100644 --- a/Godeps/_workspace/src/github.com/emicklei/go-restful/examples/google_app_engine/datastore/main.go +++ b/Godeps/_workspace/src/github.com/emicklei/go-restful/examples/google_app_engine/datastore/main.go @@ -1,11 +1,11 @@ package main import ( - "appengine" - "appengine/datastore" - "appengine/user" "github.com/emicklei/go-restful" "github.com/emicklei/go-restful/swagger" + "google.golang.com/appengine" + "google.golang.com/appengine/datastore" + "google.golang.com/appengine/user" "net/http" "time" ) diff --git a/Godeps/_workspace/src/github.com/emicklei/go-restful/examples/google_app_engine/restful-user-service.go b/Godeps/_workspace/src/github.com/emicklei/go-restful/examples/google_app_engine/restful-user-service.go index e97ba2e18fe..0e883018151 100644 --- a/Godeps/_workspace/src/github.com/emicklei/go-restful/examples/google_app_engine/restful-user-service.go +++ b/Godeps/_workspace/src/github.com/emicklei/go-restful/examples/google_app_engine/restful-user-service.go @@ -1,10 +1,10 @@ package main import ( - "appengine" - "appengine/memcache" "github.com/emicklei/go-restful" "github.com/emicklei/go-restful/swagger" + "google.golang.com/appengine" + "google.golang.com/appengine/memcache" "net/http" ) diff --git a/Godeps/_workspace/src/github.com/emicklei/go-restful/examples/restful-CORS-filter.go b/Godeps/_workspace/src/github.com/emicklei/go-restful/examples/restful-CORS-filter.go index aacaa3da230..346aa1b3751 100644 --- a/Godeps/_workspace/src/github.com/emicklei/go-restful/examples/restful-CORS-filter.go +++ b/Godeps/_workspace/src/github.com/emicklei/go-restful/examples/restful-CORS-filter.go @@ -53,7 +53,7 @@ func main() { // Add container filter to enable CORS cors := restful.CrossOriginResourceSharing{ ExposeHeaders: []string{"X-My-Header"}, - AllowedHeaders: []string{"Content-Type"}, + AllowedHeaders: []string{"Content-Type", "Accept"}, CookiesAllowed: false, Container: wsContainer} wsContainer.Filter(cors.Filter) diff --git a/Godeps/_workspace/src/github.com/emicklei/go-restful/examples/restful-user-resource.go b/Godeps/_workspace/src/github.com/emicklei/go-restful/examples/restful-user-resource.go index f6ec988c277..6b860dc2064 100644 --- a/Godeps/_workspace/src/github.com/emicklei/go-restful/examples/restful-user-resource.go +++ b/Godeps/_workspace/src/github.com/emicklei/go-restful/examples/restful-user-resource.go @@ -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 u.users[usr.Id] = *usr - response.WriteHeader(http.StatusCreated) - response.WriteEntity(usr) + response.WriteHeaderAndEntity(http.StatusCreated, usr) } // PUT http://localhost:8080/users/1 diff --git a/Godeps/_workspace/src/github.com/emicklei/go-restful/examples/restful-user-service.go b/Godeps/_workspace/src/github.com/emicklei/go-restful/examples/restful-user-service.go index d0d9872758f..77c678ce489 100644 --- a/Godeps/_workspace/src/github.com/emicklei/go-restful/examples/restful-user-service.go +++ b/Godeps/_workspace/src/github.com/emicklei/go-restful/examples/restful-user-service.go @@ -102,8 +102,7 @@ func (u *UserService) createUser(request *restful.Request, response *restful.Res err := request.ReadEntity(&usr) if err == nil { u.users[usr.Id] = usr - response.WriteHeader(http.StatusCreated) - response.WriteEntity(usr) + response.WriteHeaderAndEntity(http.StatusCreated, usr) } else { response.WriteError(http.StatusInternalServerError, err) } diff --git a/Godeps/_workspace/src/github.com/emicklei/go-restful/log/log.go b/Godeps/_workspace/src/github.com/emicklei/go-restful/log/log.go index 7fd7ac184ef..f70d89524ac 100644 --- a/Godeps/_workspace/src/github.com/emicklei/go-restful/log/log.go +++ b/Godeps/_workspace/src/github.com/emicklei/go-restful/log/log.go @@ -15,7 +15,7 @@ var Logger StdLogger func init() { // 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) { diff --git a/Godeps/_workspace/src/github.com/emicklei/go-restful/options_filter.go b/Godeps/_workspace/src/github.com/emicklei/go-restful/options_filter.go index f952985a8d7..4514eadcfaa 100644 --- a/Godeps/_workspace/src/github.com/emicklei/go-restful/options_filter.go +++ b/Godeps/_workspace/src/github.com/emicklei/go-restful/options_filter.go @@ -8,7 +8,8 @@ import "strings" // 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 +// 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) { if "OPTIONS" != req.Request.Method { chain.ProcessFilter(req, resp) @@ -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 // 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 { return DefaultContainer.OPTIONSFilter } diff --git a/Godeps/_workspace/src/github.com/emicklei/go-restful/parameter.go b/Godeps/_workspace/src/github.com/emicklei/go-restful/parameter.go index a836120b5d5..e11c8162a71 100644 --- a/Godeps/_workspace/src/github.com/emicklei/go-restful/parameter.go +++ b/Godeps/_workspace/src/github.com/emicklei/go-restful/parameter.go @@ -30,12 +30,12 @@ type Parameter struct { // 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 - DefaultValue string + Name, Description, DataType, DataFormat string + Kind int + Required bool + AllowableValues map[string]string + AllowMultiple bool + DefaultValue string } // Data returns the state of the Parameter @@ -95,6 +95,12 @@ func (p *Parameter) DataType(typeName string) *Parameter { 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 func (p *Parameter) DefaultValue(stringRepresentation string) *Parameter { p.data.DefaultValue = stringRepresentation diff --git a/Godeps/_workspace/src/github.com/emicklei/go-restful/request.go b/Godeps/_workspace/src/github.com/emicklei/go-restful/request.go index e944b8d000f..988adc9848f 100644 --- a/Godeps/_workspace/src/github.com/emicklei/go-restful/request.go +++ b/Godeps/_workspace/src/github.com/emicklei/go-restful/request.go @@ -6,14 +6,9 @@ package restful import ( "bytes" - "compress/gzip" "compress/zlib" - "encoding/json" - "encoding/xml" - "io" "io/ioutil" "net/http" - "strings" ) var defaultRequestContentType string @@ -81,62 +76,43 @@ 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 +// ReadEntity checks the Accept header and reads the content into the entityPointer. func (r *Request) ReadEntity(entityPointer interface{}) (err error) { - defer r.Request.Body.Close() contentType := r.Request.Header.Get(HEADER_ContentType) contentEncoding := r.Request.Header.Get(HEADER_ContentEncoding) + + // OLD feature, cache the body for reads 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) { - var buffer []byte - if r.bodyContent != nil { - buffer = *r.bodyContent - } else { - buffer, err = ioutil.ReadAll(r.Request.Body) - if err != nil { - return err + if r.bodyContent == nil { + data, err := ioutil.ReadAll(r.Request.Body) + 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 if ENCODING_GZIP == contentEncoding { - gzipReader := GzipReaderPool.Get().(*gzip.Reader) - gzipReader.Reset(reader) - entityReader = gzipReader + gzipReader := currentCompressorProvider.AcquireGzipReader() + defer currentCompressorProvider.ReleaseGzipReader(gzipReader) + gzipReader.Reset(r.Request.Body) + r.Request.Body = gzipReader } else if ENCODING_DEFLATE == contentEncoding { - zlibReader, err := zlib.NewReader(reader) + zlibReader, err := zlib.NewReader(r.Request.Body) if err != nil { return err } - entityReader = zlibReader + r.Request.Body = zlibReader } - // decode JSON - if strings.Contains(contentType, MIME_JSON) || MIME_JSON == defaultRequestContentType { - decoder := json.NewDecoder(entityReader) - decoder.UseNumber() - return decoder.Decode(entityPointer) + // lookup the EntityReader + entityReader, ok := entityAccessRegistry.AccessorAt(contentType) + if !ok { + return NewError(http.StatusBadRequest, "Unable to unmarshal content of type:"+contentType) } - - // 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) + return entityReader.Read(r, entityPointer) } // SetAttribute adds or replaces the attribute with the given value. diff --git a/Godeps/_workspace/src/github.com/emicklei/go-restful/response.go b/Godeps/_workspace/src/github.com/emicklei/go-restful/response.go index eb5a023563a..3798f18c838 100644 --- a/Godeps/_workspace/src/github.com/emicklei/go-restful/response.go +++ b/Godeps/_workspace/src/github.com/emicklei/go-restful/response.go @@ -5,8 +5,7 @@ package restful // that can be found in the LICENSE file. import ( - "encoding/json" - "encoding/xml" + "errors" "net/http" "strings" ) @@ -14,9 +13,7 @@ import ( // 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. +//PrettyPrintResponses controls the indentation feature of XML and JSON serialization var PrettyPrintResponses = true // Response is a wrapper on the actual http ResponseWriter @@ -36,8 +33,7 @@ func NewResponse(httpWriter http.ResponseWriter) *Response { return &Response{httpWriter, "", []string{}, http.StatusOK, 0, PrettyPrintResponses, nil} // empty content-types } -// If Accept header matching fails, fall back to this type, otherwise -// a "406: Not Acceptable" response is returned. +// If Accept header matching fails, fall back to this type. // Valid values are restful.MIME_JSON and restful.MIME_XML // Example: // restful.DefaultResponseContentType(restful.MIME_JSON) @@ -68,117 +64,99 @@ 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 - } +// EntityWriter returns the registered EntityWriter that the entity (requested resource) +// can write according to what the request wants (Accept) and what the Route can produce or what the restful defaults say. +// If called before WriteEntity and WriteHeader then a false return value can be used to write a 406: Not Acceptable. +func (r *Response) EntityWriter() (EntityReaderWriter, bool) { 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) + return entityAccessRegistry.AccessorAt(MIME_JSON) } if MIME_XML == each { - return r.WriteAsXml(value) + return entityAccessRegistry.AccessorAt(MIME_XML) } } } 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) + return entityAccessRegistry.AccessorAt(MIME_JSON) } if MIME_XML == each { - return r.WriteAsXml(value) + return entityAccessRegistry.AccessorAt(MIME_XML) } } } } } - 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) + writer, ok := entityAccessRegistry.AccessorAt(r.requestAccept) + if !ok { + // if not registered then fallback to the defaults (if set) + if DefaultResponseMimeType == MIME_JSON { + return entityAccessRegistry.AccessorAt(MIME_JSON) } - r.WriteHeader(http.StatusNotAcceptable) // for recording only - r.ResponseWriter.WriteHeader(http.StatusNotAcceptable) - if _, err := r.Write([]byte("406: Not Acceptable")); err != nil { - return err + if DefaultResponseMimeType == MIME_XML { + return entityAccessRegistry.AccessorAt(MIME_XML) + } + if trace { + traceLogger.Printf("no registered EntityReaderWriter found for %s", r.requestAccept) } } - return nil + return writer, ok +} + +// WriteEntity calls WriteHeaderAndEntity with Http Status OK (200) +func (r *Response) WriteEntity(value interface{}) error { + return r.WriteHeaderAndEntity(http.StatusOK, value) +} + +// WriteHeaderAndEntity marshals the value using the representation denoted by the Accept Header and the registered EntityWriters. +// If no Accept header is specified (or */*) then respond with the Content-Type as specified by the first in the Route.Produces. +// If an Accept header is specified then respond with 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 no response is send except for the Http status. You may want to call WriteHeader(http.StatusNotFound) instead. +// If there is no writer available that can represent the value in the requested MIME type then Http Status NotAcceptable is written. +// Current implementation ignores any q-parameters in the Accept Header. +// Returns an error if the value could not be written on the response. +func (r *Response) WriteHeaderAndEntity(status int, value interface{}) error { + writer, ok := r.EntityWriter() + if !ok { + r.WriteHeader(http.StatusNotAcceptable) + return nil + } + return writer.Write(r, status, value) } // WriteAsXml is a convenience method for writing a value in xml (requires Xml tags on the value) +// It uses the standard encoding/xml package for marshalling the valuel ; not using a registered EntityReaderWriter. 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 r.prettyPrint { - 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 + return writeXML(r, http.StatusOK, MIME_XML, value) } -// WriteAsJson is a convenience method for writing a value in json +// WriteHeaderAndXml is a convenience method for writing a status and value in xml (requires Xml tags on the value) +// It uses the standard encoding/xml package for marshalling the valuel ; not using a registered EntityReaderWriter. +func (r *Response) WriteHeaderAndXml(status int, value interface{}) error { + return writeXML(r, status, MIME_XML, value) +} + +// WriteAsJson is a convenience method for writing a value in json. +// It uses the standard encoding/json package for marshalling the valuel ; not using a registered EntityReaderWriter. func (r *Response) WriteAsJson(value interface{}) error { - return r.WriteJson(value, MIME_JSON) // no charset + return writeJSON(r, http.StatusOK, MIME_JSON, value) } -// WriteJson is a convenience method for writing a value in Json with a given Content-Type +// WriteJson is a convenience method for writing a value in Json with a given Content-Type. +// It uses the standard encoding/json package for marshalling the valuel ; not using a registered EntityReaderWriter. func (r *Response) WriteJson(value interface{}, contentType string) error { - var output []byte - var err error + return writeJSON(r, http.StatusOK, contentType, value) +} - if value == nil { // do not write a nil representation - return nil - } - if r.prettyPrint { - 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, contentType) - if r.statusCode > 0 { // a WriteHeader was intercepted - r.ResponseWriter.WriteHeader(r.statusCode) - } - if _, err = r.Write(output); err != nil { - return err - } - return nil +// WriteHeaderAndJson is a convenience method for writing the status and a value in Json with a given Content-Type. +// It uses the standard encoding/json package for marshalling the value ; not using a registered EntityReaderWriter. +func (r *Response) WriteHeaderAndJson(status int, value interface{}, contentType string) error { + return writeJSON(r, status, contentType, value) } // WriteError write the http status and the error string on the response. @@ -187,16 +165,19 @@ 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 +// WriteServiceError is a convenience method for a responding with a status and a ServiceError func (r *Response) WriteServiceError(httpStatus int, err ServiceError) error { - r.WriteHeader(httpStatus) // for recording only - return r.WriteEntity(err) + r.err = err + return r.WriteHeaderAndEntity(httpStatus, 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) +func (r *Response) WriteErrorString(httpStatus int, errorReason string) error { + if r.err == nil { + // if not called from WriteError + r.err = errors.New(errorReason) + } + r.WriteHeader(httpStatus) if _, err := r.Write([]byte(errorReason)); err != nil { return err } @@ -204,31 +185,13 @@ func (r *Response) WriteErrorString(status int, errorReason string) error { } // 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 calling WriteAsXml or WriteAsJson, -// or if the status is one for which no response is allowed: -// -// 202 = http.StatusAccepted -// 204 = http.StatusNoContent -// 206 = http.StatusPartialContent -// 304 = http.StatusNotModified -// -// If this behavior does not fit your need then you can write to the underlying response, such as: -// response.ResponseWriter.WriteHeader(http.StatusAccepted) +// Changes to the Header of the response have no effect after this. func (r *Response) WriteHeader(httpStatus int) { r.statusCode = httpStatus - // if 202,204,206,304 then WriteEntity will not be called so we need to pass this code - if http.StatusNoContent == httpStatus || - http.StatusNotModified == httpStatus || - http.StatusPartialContent == httpStatus || - http.StatusAccepted == httpStatus { - r.ResponseWriter.WriteHeader(httpStatus) - } + r.ResponseWriter.WriteHeader(httpStatus) } // StatusCode returns the code that has been written using WriteHeader. -// If WriteHeader, WriteEntity or WriteAsXml has not been called (yet) then return 200 OK. func (r Response) StatusCode() int { if 0 == r.statusCode { // no status code has been written yet; assume OK diff --git a/Godeps/_workspace/src/github.com/emicklei/go-restful/swagger/CHANGES.md b/Godeps/_workspace/src/github.com/emicklei/go-restful/swagger/CHANGES.md index c9b49044565..736f6f37c56 100644 --- a/Godeps/_workspace/src/github.com/emicklei/go-restful/swagger/CHANGES.md +++ b/Godeps/_workspace/src/github.com/emicklei/go-restful/swagger/CHANGES.md @@ -1,5 +1,9 @@ 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 - (api break) changed the type of Properties in Model - (api break) changed the type of Models in ApiDeclaration diff --git a/Godeps/_workspace/src/github.com/emicklei/go-restful/swagger/config.go b/Godeps/_workspace/src/github.com/emicklei/go-restful/swagger/config.go index b272b7bface..944d988eefb 100644 --- a/Godeps/_workspace/src/github.com/emicklei/go-restful/swagger/config.go +++ b/Godeps/_workspace/src/github.com/emicklei/go-restful/swagger/config.go @@ -29,4 +29,6 @@ type Config struct { ApiVersion string // If set then call this handler after building the complete ApiDeclaration Map PostBuildHandler PostBuildDeclarationMapFunc + // Swagger global info struct + Info Info } diff --git a/Godeps/_workspace/src/github.com/emicklei/go-restful/swagger/model_builder.go b/Godeps/_workspace/src/github.com/emicklei/go-restful/swagger/model_builder.go index a4135895a11..3fbb20be26d 100644 --- a/Godeps/_workspace/src/github.com/emicklei/go-restful/swagger/model_builder.go +++ b/Godeps/_workspace/src/github.com/emicklei/go-restful/swagger/model_builder.go @@ -132,9 +132,11 @@ func (b modelBuilder) buildProperty(field reflect.StructField, model *Model, mod modelDescription = tag } - fieldType := field.Type - prop.setPropertyMetadata(field) + if prop.Type != nil { + return jsonName, modelDescription, prop + } + fieldType := field.Type // check if type is doing its own marshalling marshalerType := reflect.TypeOf((*json.Marshaler)(nil)).Elem() @@ -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) { - fieldType := field.Type prop.setPropertyMetadata(field) + // Check for type override in tag + if prop.Type != nil { + return jsonName, prop + } + fieldType := field.Type // check for anonymous if len(fieldType.Name()) == 0 { // anonymous @@ -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) { - fieldType := field.Type + // check for type override in tags prop.setPropertyMetadata(field) + if prop.Type != nil { + return jsonName, prop + } + fieldType := field.Type var pType = "array" prop.Type = &pType elemTypeName := b.getElementTypeName(modelName, jsonName, fieldType.Elem()) @@ -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) { - fieldType := field.Type prop.setPropertyMetadata(field) + // Check for type override in tags + if prop.Type != nil { + return jsonName, prop + } + fieldType := field.Type // override type of pointer to list-likes if fieldType.Elem().Kind() == reflect.Slice || fieldType.Elem().Kind() == reflect.Array { @@ -338,7 +352,7 @@ func (b modelBuilder) keyFrom(st reflect.Type) string { // see also https://golang.org/ref/spec#Numeric_types 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 @@ -359,6 +373,7 @@ func (b modelBuilder) jsonNameOfField(field reflect.StructField) string { // see also http://json-schema.org/latest/json-schema-core.html#anchor8 func (b modelBuilder) jsonSchemaType(modelName string) string { schemaMap := map[string]string{ + "uint": "integer", "uint8": "integer", "uint16": "integer", "uint32": "integer", @@ -389,6 +404,7 @@ func (b modelBuilder) jsonSchemaFormat(modelName string) string { "int32": "int32", "int64": "int64", "byte": "byte", + "uint": "integer", "uint8": "byte", "float64": "double", "float32": "float", diff --git a/Godeps/_workspace/src/github.com/emicklei/go-restful/swagger/model_property_ext.go b/Godeps/_workspace/src/github.com/emicklei/go-restful/swagger/model_property_ext.go index e44809e24c0..04fff2c57aa 100644 --- a/Godeps/_workspace/src/github.com/emicklei/go-restful/swagger/model_property_ext.go +++ b/Godeps/_workspace/src/github.com/emicklei/go-restful/swagger/model_property_ext.go @@ -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) { if tag := field.Tag.Get("minimum"); tag != "" { prop.Minimum = tag @@ -56,4 +62,5 @@ func (prop *ModelProperty) setPropertyMetadata(field reflect.StructField) { prop.setMaximum(field) prop.setUniqueItems(field) prop.setDefaultValue(field) + prop.setType(field) } diff --git a/Godeps/_workspace/src/github.com/emicklei/go-restful/swagger/swagger.go b/Godeps/_workspace/src/github.com/emicklei/go-restful/swagger/swagger.go index 288aec67ef6..967b6711e8b 100644 --- a/Godeps/_workspace/src/github.com/emicklei/go-restful/swagger/swagger.go +++ b/Godeps/_workspace/src/github.com/emicklei/go-restful/swagger/swagger.go @@ -48,7 +48,7 @@ type Info struct { TermsOfServiceUrl string `json:"termsOfServiceUrl,omitempty"` Contact string `json:"contact,omitempty"` License string `json:"license,omitempty"` - LicensUrl string `json:"licensUrl,omitempty"` + LicenseUrl string `json:"licenseUrl,omitempty"` } // 5.1.5 @@ -134,7 +134,7 @@ type Api struct { // 5.2.3 Operation Object type Operation struct { - Type string `json:"type"` + DataTypeFields Method string `json:"method"` Summary string `json:"summary,omitempty"` Notes string `json:"notes,omitempty"` diff --git a/Godeps/_workspace/src/github.com/emicklei/go-restful/swagger/swagger_builder.go b/Godeps/_workspace/src/github.com/emicklei/go-restful/swagger/swagger_builder.go new file mode 100644 index 00000000000..05a3c7e76f9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/emicklei/go-restful/swagger/swagger_builder.go @@ -0,0 +1,21 @@ +package swagger + +type SwaggerBuilder struct { + SwaggerService +} + +func NewSwaggerBuilder(config Config) *SwaggerBuilder { + return &SwaggerBuilder{*newSwaggerService(config)} +} + +func (sb SwaggerBuilder) ProduceListing() ResourceListing { + return sb.SwaggerService.produceListing() +} + +func (sb SwaggerBuilder) ProduceAllDeclarations() map[string]ApiDeclaration { + return sb.SwaggerService.produceAllDeclarations() +} + +func (sb SwaggerBuilder) ProduceDeclarations(route string) (*ApiDeclaration, bool) { + return sb.SwaggerService.produceDeclarations(route) +} diff --git a/Godeps/_workspace/src/github.com/emicklei/go-restful/swagger/swagger_webservice.go b/Godeps/_workspace/src/github.com/emicklei/go-restful/swagger/swagger_webservice.go index 7237253bd14..8dc3d5f9ea6 100644 --- a/Godeps/_workspace/src/github.com/emicklei/go-restful/swagger/swagger_webservice.go +++ b/Godeps/_workspace/src/github.com/emicklei/go-restful/swagger/swagger_webservice.go @@ -19,9 +19,35 @@ type SwaggerService struct { } func newSwaggerService(config Config) *SwaggerService { - return &SwaggerService{ + sws := &SwaggerService{ config: config, apiDeclarationMap: new(ApiDeclarationList)} + + // Build all ApiDeclarations + for _, each := range config.WebServices { + rootPath := each.RootPath() + // skip the api service itself + if rootPath != config.ApiPath { + if rootPath == "" || rootPath == "/" { + // use routes + for _, route := range each.Routes() { + entry := staticPathFromRoute(route) + _, exists := sws.apiDeclarationMap.At(entry) + if !exists { + sws.apiDeclarationMap.Put(entry, sws.composeDeclaration(each, entry)) + } + } + } else { // use root path + sws.apiDeclarationMap.Put(each.RootPath(), sws.composeDeclaration(each, each.RootPath())) + } + } + } + + // if specified then call the PostBuilderHandler + if config.PostBuildHandler != nil { + config.PostBuildHandler(sws.apiDeclarationMap) + } + return sws } // LogInfo is the function that is called when this package needs to log. It defaults to log.Printf @@ -57,31 +83,6 @@ func RegisterSwaggerService(config Config, wsContainer *restful.Container) { 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.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 if config.StaticHandler == nil && config.SwaggerFilePath != "" && config.SwaggerPath != "" { swaggerPathSlash := config.SwaggerPath @@ -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) { - 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) { ref := Resource{Path: k} 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 } listing.Apis = append(listing.Apis, ref) }) - resp.WriteAsJson(listing) + return listing } 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 { resp.WriteErrorString(http.StatusNotFound, "ApiDeclaration not found") return @@ -180,11 +186,28 @@ func (sws SwaggerService) getDeclarations(req *restful.Request, resp *restful.Re scheme = "https" } } - (&decl).BasePath = fmt.Sprintf("%s://%s", scheme, host) + decl.BasePath = fmt.Sprintf("%s://%s", scheme, host) } resp.WriteAsJson(decl) } +func (sws SwaggerService) produceAllDeclarations() map[string]ApiDeclaration { + decls := map[string]ApiDeclaration{} + sws.apiDeclarationMap.Do(func(k string, v ApiDeclaration) { + decls[k] = v + }) + return decls +} + +func (sws SwaggerService) produceDeclarations(route string) (*ApiDeclaration, bool) { + decl, ok := sws.apiDeclarationMap.At(route) + if !ok { + return nil, false + } + decl.BasePath = sws.config.WebServicesUrl + return &decl, true +} + // composeDeclaration uses all routes and parameters to create a ApiDeclaration func (sws SwaggerService) composeDeclaration(ws *restful.WebService, pathPrefix string) ApiDeclaration { decl := ApiDeclaration{ @@ -207,13 +230,15 @@ func (sws SwaggerService) composeDeclaration(ws *restful.WebService, pathPrefix } } 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 { operation := Operation{ - Method: route.Method, - Summary: route.Doc, - Notes: route.Notes, - Type: asDataType(route.WriteSample), + Method: route.Method, + Summary: route.Doc, + Notes: route.Notes, + // Type gets overwritten if there is a write sample + DataTypeFields: DataTypeFields{Type: &voidString}, Parameters: []Parameter{}, Nickname: route.Operation, ResponseMessages: composeResponseMessages(route, &decl)} @@ -238,6 +263,13 @@ func (sws SwaggerService) composeDeclaration(ws *restful.WebService, pathPrefix return decl } +func withoutWildcard(path string) string { + if strings.HasSuffix(path, ":*}") { + return path[0:len(path)-3] + "}" + } + return path +} + // composeResponseMessages takes the ResponseErrors (if any) and creates ResponseMessages from them. func composeResponseMessages(route restful.Route, decl *ApiDeclaration) (messages []ResponseMessage) { if route.ResponseErrors == nil { @@ -299,14 +331,10 @@ func detectCollectionType(st reflect.Type) (bool, reflect.Type) { // addModelFromSample creates and adds (or overwrites) a Model from a sample resource 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 isCollection { - modelName = "array[" + modelName + "]" - } - operation.Type = modelName + type_, items := asDataType(sample) + operation.Type = type_ + operation.Items = items } modelBuilder{models}.addModelFrom(sample) } @@ -315,7 +343,7 @@ func asSwaggerParameter(param restful.ParameterData) Parameter { return Parameter{ DataTypeFields: DataTypeFields{ Type: ¶m.DataType, - Format: asFormat(param.DataType), + Format: asFormat(param.DataType, param.DataFormat), DefaultValue: Special(param.DefaultValue), }, Name: param.Name, @@ -360,7 +388,10 @@ func composeRootPath(req *restful.Request) string { return path + "/" + g } -func asFormat(name string) string { +func asFormat(dataType string, dataFormat string) string { + if dataFormat != "" { + return dataFormat + } return "" // TODO } @@ -380,9 +411,30 @@ func asParamType(kind int) string { return "" } -func asDataType(any interface{}) string { - if any == nil { - return "void" +func asDataType(any interface{}) (*string, *Item) { + // If it's not a collection, return the suggested model name + st := reflect.TypeOf(any) + isCollection, st := detectCollectionType(st) + modelName := modelBuilder{}.keyFrom(st) + // if it's not a collection we are done + if !isCollection { + return &modelName, nil } - return reflect.TypeOf(any).Name() + + // 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 + } + tmp := "array" + return &tmp, item } diff --git a/Godeps/_workspace/src/github.com/emicklei/go-restful/swagger/test_package/struct.go b/Godeps/_workspace/src/github.com/emicklei/go-restful/swagger/test_package/struct.go new file mode 100644 index 00000000000..b9a6f930870 --- /dev/null +++ b/Godeps/_workspace/src/github.com/emicklei/go-restful/swagger/test_package/struct.go @@ -0,0 +1,5 @@ +package test_package + +type TestStruct struct { + TestField string +} diff --git a/Godeps/_workspace/src/github.com/emicklei/go-restful/web_service.go b/Godeps/_workspace/src/github.com/emicklei/go-restful/web_service.go index 31af2576d9a..e89be70097c 100644 --- a/Godeps/_workspace/src/github.com/emicklei/go-restful/web_service.go +++ b/Godeps/_workspace/src/github.com/emicklei/go-restful/web_service.go @@ -1,7 +1,9 @@ package restful import ( + "fmt" "os" + "sync" "github.com/emicklei/go-restful/log" ) @@ -21,6 +23,15 @@ type WebService struct { filters []FilterFunction documentation 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. @@ -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. func (w *WebService) Route(builder *RouteBuilder) *WebService { + w.routesLock.Lock() + defer w.routesLock.Unlock() builder.copyDefaults(w.produces, w.consumes) w.routes = append(w.routes, builder.Build()) 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 func (w *WebService) Method(httpMethod string) *RouteBuilder { return new(RouteBuilder).servicePath(w.rootPath).Method(httpMethod) @@ -160,7 +188,17 @@ func (w *WebService) Consumes(accepts ...string) *WebService { // Routes returns the Routes associated with this WebService 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 "/"