Merge pull request #17990 from nikhiljindal/updateGoRestful

Updating go restful
This commit is contained in:
Nikhil Jindal 2015-12-02 15:59:27 -08:00
commit 2fcf157512
32 changed files with 746 additions and 281 deletions

4
Godeps/Godeps.json generated
View File

@ -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",

View File

@ -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

View File

@ -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

View File

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

View File

@ -0,0 +1,103 @@
package restful
// Copyright 2015 Ernest Micklei. All rights reserved.
// Use of this source code is governed by a license
// that can be found in the LICENSE file.
import (
"compress/gzip"
"compress/zlib"
)
// BoundedCachedCompressors is a CompressorProvider that uses a cache with a fixed amount
// of writers and readers (resources).
// If a new resource is acquired and all are in use, it will return a new unmanaged resource.
type BoundedCachedCompressors struct {
gzipWriters chan *gzip.Writer
gzipReaders chan *gzip.Reader
zlibWriters chan *zlib.Writer
writersCapacity int
readersCapacity int
}
// NewBoundedCachedCompressors returns a new, with filled cache, BoundedCachedCompressors.
func NewBoundedCachedCompressors(writersCapacity, readersCapacity int) *BoundedCachedCompressors {
b := &BoundedCachedCompressors{
gzipWriters: make(chan *gzip.Writer, writersCapacity),
gzipReaders: make(chan *gzip.Reader, readersCapacity),
zlibWriters: make(chan *zlib.Writer, writersCapacity),
writersCapacity: writersCapacity,
readersCapacity: readersCapacity,
}
for ix := 0; ix < writersCapacity; ix++ {
b.gzipWriters <- newGzipWriter()
b.zlibWriters <- newZlibWriter()
}
for ix := 0; ix < readersCapacity; ix++ {
b.gzipReaders <- newGzipReader()
}
return b
}
// AcquireGzipWriter returns an resettable *gzip.Writer. Needs to be released.
func (b *BoundedCachedCompressors) AcquireGzipWriter() *gzip.Writer {
var writer *gzip.Writer
select {
case writer, _ = <-b.gzipWriters:
default:
// return a new unmanaged one
writer = newGzipWriter()
}
return writer
}
// ReleaseGzipWriter accepts a writer (does not have to be one that was cached)
// only when the cache has room for it. It will ignore it otherwise.
func (b *BoundedCachedCompressors) ReleaseGzipWriter(w *gzip.Writer) {
// forget the unmanaged ones
if len(b.gzipWriters) < b.writersCapacity {
b.gzipWriters <- w
}
}
// AcquireGzipReader returns a *gzip.Reader. Needs to be released.
func (b *BoundedCachedCompressors) AcquireGzipReader() *gzip.Reader {
var reader *gzip.Reader
select {
case reader, _ = <-b.gzipReaders:
default:
// return a new unmanaged one
reader = newGzipReader()
}
return reader
}
// ReleaseGzipReader accepts a reader (does not have to be one that was cached)
// only when the cache has room for it. It will ignore it otherwise.
func (b *BoundedCachedCompressors) ReleaseGzipReader(r *gzip.Reader) {
// forget the unmanaged ones
if len(b.gzipReaders) < b.readersCapacity {
b.gzipReaders <- r
}
}
// AcquireZlibWriter returns an resettable *zlib.Writer. Needs to be released.
func (b *BoundedCachedCompressors) AcquireZlibWriter() *zlib.Writer {
var writer *zlib.Writer
select {
case writer, _ = <-b.zlibWriters:
default:
// return a new unmanaged one
writer = newZlibWriter()
}
return writer
}
// ReleaseZlibWriter accepts a writer (does not have to be one that was cached)
// only when the cache has room for it. It will ignore it otherwise.
func (b *BoundedCachedCompressors) ReleaseZlibWriter(w *zlib.Writer) {
// forget the unmanaged ones
if len(b.zlibWriters) < b.writersCapacity {
b.zlibWriters <- w
}
}

View File

@ -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 {

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
application: datastore-example
application: <your_app_id>
version: 1
runtime: go
api_version: go1

View File

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

View File

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

View File

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

View File

@ -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

View File

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

View File

@ -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) {

View File

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

View File

@ -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

View File

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

View File

@ -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

View File

@ -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

View File

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

View File

@ -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",

View File

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

View File

@ -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"`

View File

@ -0,0 +1,21 @@
package swagger
type SwaggerBuilder struct {
SwaggerService
}
func NewSwaggerBuilder(config Config) *SwaggerBuilder {
return &SwaggerBuilder{*newSwaggerService(config)}
}
func (sb SwaggerBuilder) ProduceListing() ResourceListing {
return sb.SwaggerService.produceListing()
}
func (sb SwaggerBuilder) ProduceAllDeclarations() map[string]ApiDeclaration {
return sb.SwaggerService.produceAllDeclarations()
}
func (sb SwaggerBuilder) ProduceDeclarations(route string) (*ApiDeclaration, bool) {
return sb.SwaggerService.produceDeclarations(route)
}

View File

@ -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: &param.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
}

View File

@ -0,0 +1,5 @@
package test_package
type TestStruct struct {
TestField string
}

View File

@ -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 "/"

View File

@ -3168,7 +3168,7 @@
]
},
{
"path": "/api/v1/proxy/nodes/{name}/{path:*}",
"path": "/api/v1/proxy/nodes/{name}/{path}",
"description": "API at /api/v1",
"operations": [
{
@ -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",
"operations": [
{
@ -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",
"operations": [
{
@ -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",
"operations": [
{

View File

@ -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>
<div class="listingblock">
<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 class="sect3">
@ -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>
<div class="listingblock">
<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 class="sect3">
@ -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>
<div class="listingblock">
<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 class="sect3">
@ -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>
<div class="listingblock">
<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 class="sect3">
@ -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>
<div class="listingblock">
<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 class="sect3">
@ -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>
<div class="listingblock">
<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 class="sect3">
@ -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>
<div class="listingblock">
<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 class="sect3">
@ -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>
<div class="listingblock">
<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 class="sect3">
@ -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>
<div class="listingblock">
<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 class="sect3">
@ -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>
<div class="listingblock">
<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 class="sect3">
@ -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>
<div class="listingblock">
<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 class="sect3">
@ -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>
<div class="listingblock">
<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 class="sect3">
@ -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>
<div class="listingblock">
<div class="content">
<pre>GET /api/v1/proxy/nodes/{name}/{path:*}</pre>
<pre>GET /api/v1/proxy/nodes/{name}/{path}</pre>
</div>
</div>
<div class="sect3">
@ -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>
<div class="listingblock">
<div class="content">
<pre>PUT /api/v1/proxy/nodes/{name}/{path:*}</pre>
<pre>PUT /api/v1/proxy/nodes/{name}/{path}</pre>
</div>
</div>
<div class="sect3">
@ -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>
<div class="listingblock">
<div class="content">
<pre>DELETE /api/v1/proxy/nodes/{name}/{path:*}</pre>
<pre>DELETE /api/v1/proxy/nodes/{name}/{path}</pre>
</div>
</div>
<div class="sect3">
@ -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>
<div class="listingblock">
<div class="content">
<pre>POST /api/v1/proxy/nodes/{name}/{path:*}</pre>
<pre>POST /api/v1/proxy/nodes/{name}/{path}</pre>
</div>
</div>
<div class="sect3">
@ -23664,7 +23664,7 @@ span.icon > [class^="icon-"], span.icon > [class*=" icon-"] { cursor: default; }
</div>
<div id="footer">
<div id="footer-text">
Last updated 2015-11-13 20:43:21 UTC
Last updated 2015-12-01 01:46:05 UTC
</div>
</div>
</body>