Merge pull request #11406 from andronat/update_go_restful

updated go-restful
pull/6/head
Marek Grabowski 2015-07-27 12:57:06 +02:00
commit e706e8138b
13 changed files with 419 additions and 29 deletions

4
Godeps/Godeps.json generated
View File

@ -199,8 +199,8 @@
},
{
"ImportPath": "github.com/emicklei/go-restful",
"Comment": "v1.1.3-54-gbdfb7d4",
"Rev": "bdfb7d41639a84ea7c36df648e5865cd9fbf21e2"
"Comment": "v1.1.3-76-gbfd6ff2",
"Rev": "bfd6ff29d2961031cec64346a92bae4cde96c868"
},
{
"ImportPath": "github.com/evanphx/json-patch",

View File

@ -150,11 +150,20 @@ func writeServiceError(err ServiceError, req *Request, resp *Response) {
// Dispatch the incoming Http Request to a matching WebService.
func (c *Container) dispatch(httpWriter http.ResponseWriter, httpRequest *http.Request) {
writer := httpWriter
// CompressingResponseWriter should be closed after all operations are done
defer func() {
if compressWriter, ok := writer.(*CompressingResponseWriter); ok {
compressWriter.Close()
}
}()
// Instal panic recovery unless told otherwise
if !c.doNotRecover { // catch all for 500 response
defer func() {
if r := recover(); r != nil {
c.recoverHandleFunc(r, httpWriter)
c.recoverHandleFunc(r, writer)
return
}
}()
@ -168,7 +177,6 @@ func (c *Container) dispatch(httpWriter http.ResponseWriter, httpRequest *http.R
// Detect if compression is needed
// assume without compression, test for override
writer := httpWriter
if c.contentEncodingEnabled {
doCompress, encoding := wantsCompressedResponse(httpRequest)
if doCompress {
@ -179,9 +187,6 @@ func (c *Container) dispatch(httpWriter http.ResponseWriter, httpRequest *http.R
httpWriter.WriteHeader(http.StatusInternalServerError)
return
}
defer func() {
writer.(*CompressingResponseWriter).Close()
}()
}
}
// Find best match Route ; err is non nil if no match was found

View File

@ -1,5 +1,5 @@
cd examples
ls *.go | xargs -I {} go build {}
ls *.go | xargs -I {} go build -o /tmp/ignore {}
cd ..
go fmt ...swagger && \
go test -test.v ...swagger && \

View File

@ -209,9 +209,10 @@ func (r *Response) WriteErrorString(status int, errorReason string) error {
// 204 (http.StatusNoContent) or 304 (http.StatusNotModified))
func (r *Response) WriteHeader(httpStatus int) {
r.statusCode = httpStatus
// if 204 then WriteEntity will not be called so we need to pass this code
// if 201,204,304 then WriteEntity will not be called so we need to pass this code
if http.StatusNoContent == httpStatus ||
http.StatusNotModified == httpStatus {
http.StatusNotModified == httpStatus ||
http.StatusPartialContent == httpStatus {
r.ResponseWriter.WriteHeader(httpStatus)
}
}

View File

@ -68,6 +68,25 @@ func TestMeasureContentLengthWriteErrorString(t *testing.T) {
}
}
// go test -v -test.run TestStatusIsPassedToResponse ...restful
func TestStatusIsPassedToResponse(t *testing.T) {
for _, each := range []struct {
write, read int
}{
{write: 204, read: 204},
{write: 304, read: 304},
{write: 200, read: 200},
{write: 400, read: 200},
} {
httpWriter := httptest.NewRecorder()
resp := Response{httpWriter, "*/*", []string{"*/*"}, 0, 0, true}
resp.WriteHeader(each.write)
if got, want := httpWriter.Code, each.read; got != want {
t.Error("got %v want %v", got, want)
}
}
}
// go test -v -test.run TestStatusCreatedAndContentTypeJson_Issue54 ...restful
func TestStatusCreatedAndContentTypeJson_Issue54(t *testing.T) {
httpWriter := httptest.NewRecorder()

View File

@ -21,8 +21,56 @@ Now, you can install the Swagger WebService for serving the Swagger specificatio
swagger.InstallSwaggerService(config)
Documenting Structs
--
Currently there are 2 ways to document your structs in the go-restful Swagger.
###### By using struct tags
- Use tag "description" to annotate a struct field with a description to show in the UI
- Use tag "modelDescription" to annotate the struct itself with a description to show in the UI. The tag can be added in an field of the struct and in case that there are multiple definition, they will be appended with an empty line.
###### By using the SwaggerDoc method
Here is an example with an `Address` struct and the documentation for each of the fields. The `""` is a special entry for **documenting the struct itself**.
type Address struct {
Country string `json:"country,omitempty"`
PostCode int `json:"postcode,omitempty"`
}
func (Address) SwaggerDoc() map[string]string {
return map[string]string{
"": "Address doc",
"country": "Country doc",
"postcode": "PostCode doc",
}
}
This example will generate a JSON like this
{
"Address": {
"id": "Address",
"description": "Address doc",
"properties": {
"country": {
"type": "string",
"description": "Country doc"
},
"postcode": {
"type": "integer",
"format": "int32",
"description": "PostCode doc"
}
}
}
}
**Very Important Notes:**
- `SwaggerDoc()` is using a **NON-Pointer** receiver (e.g. func (Address) and not func (*Address))
- The returned map should use as key the name of the field as defined in the JSON parameter (e.g. `"postcode"` and not `"PostCode"`)
Notes
--
- The Nickname of an Operation is automatically set by finding the name of the function. You can override it using RouteBuilder.Operation(..)
- The WebServices field of swagger.Config can be used to control which service you want to expose and document ; you can have multiple configs and therefore multiple endpoints.
- Use tag "description" to annotate a struct field with a description to show in the UI

View File

@ -16,6 +16,19 @@ type modelBuilder struct {
Models *ModelList
}
type documentable interface {
SwaggerDoc() map[string]string
}
// Check if this structure has a method with signature func (<theModel>) SwaggerDoc() map[string]string
// If it exists, retrive the documentation and overwrite all struct tag descriptions
func getDocFromMethodSwaggerDoc2(model reflect.Type) map[string]string {
if docable, ok := reflect.New(model).Elem().Interface().(documentable); ok {
return docable.SwaggerDoc()
}
return make(map[string]string)
}
// addModelFrom creates and adds a Model to the builder and detects and calls
// the post build hook for customizations
func (b modelBuilder) addModelFrom(sample interface{}) {
@ -58,14 +71,23 @@ func (b modelBuilder) addModel(st reflect.Type, nameOverride string) *Model {
if st.Kind() != reflect.Struct {
return &sm
}
fullDoc := getDocFromMethodSwaggerDoc2(st)
modelDescriptions := []string{}
for i := 0; i < st.NumField(); i++ {
field := st.Field(i)
jsonName, prop := b.buildProperty(field, &sm, modelName)
if descTag := field.Tag.Get("description"); descTag != "" {
prop.Description = descTag
jsonName, modelDescription, prop := b.buildProperty(field, &sm, modelName)
if len(modelDescription) > 0 {
modelDescriptions = append(modelDescriptions, modelDescription)
}
// add if not ommitted
// add if not omitted
if len(jsonName) != 0 {
// update description
if fieldDoc, ok := fullDoc[jsonName]; ok {
prop.Description = fieldDoc
}
// update Required
if b.isPropertyRequired(field) {
sm.Required = append(sm.Required, jsonName)
@ -73,6 +95,15 @@ func (b modelBuilder) addModel(st reflect.Type, nameOverride string) *Model {
sm.Properties.Put(jsonName, prop)
}
}
// We always overwrite documentation if SwaggerDoc method exists
// "" is special for documenting the struct itself
if modelDoc, ok := fullDoc[""]; ok {
sm.Description = modelDoc
} else if len(modelDescriptions) != 0 {
sm.Description = strings.Join(modelDescriptions, "\n")
}
// update model builder with completed model
b.Models.Put(modelName, sm)
@ -90,21 +121,32 @@ func (b modelBuilder) isPropertyRequired(field reflect.StructField) bool {
return required
}
func (b modelBuilder) buildProperty(field reflect.StructField, model *Model, modelName string) (jsonName string, prop ModelProperty) {
func (b modelBuilder) buildProperty(field reflect.StructField, model *Model, modelName string) (jsonName, modelDescription string, prop ModelProperty) {
jsonName = b.jsonNameOfField(field)
if len(jsonName) == 0 {
// empty name signals skip property
return "", prop
return "", "", prop
}
if tag := field.Tag.Get("modelDescription"); tag != "" {
modelDescription = tag
}
fieldType := field.Type
prop.setPropertyMetadata(field)
// check if type is doing its own marshalling
marshalerType := reflect.TypeOf((*json.Marshaler)(nil)).Elem()
if fieldType.Implements(marshalerType) {
var pType = "string"
prop.Type = &pType
prop.Format = b.jsonSchemaFormat(fieldType.String())
return jsonName, prop
if prop.Type == nil {
prop.Type = &pType
}
if prop.Format == "" {
prop.Format = b.jsonSchemaFormat(fieldType.String())
}
return jsonName, modelDescription, prop
}
// check if annotation says it is a string
@ -113,34 +155,37 @@ func (b modelBuilder) buildProperty(field reflect.StructField, model *Model, mod
if len(s) > 1 && s[1] == "string" {
stringt := "string"
prop.Type = &stringt
return jsonName, prop
return jsonName, modelDescription, prop
}
}
fieldKind := fieldType.Kind()
switch {
case fieldKind == reflect.Struct:
return b.buildStructTypeProperty(field, jsonName, model)
jsonName, prop := b.buildStructTypeProperty(field, jsonName, model)
return jsonName, modelDescription, prop
case fieldKind == reflect.Slice || fieldKind == reflect.Array:
return b.buildArrayTypeProperty(field, jsonName, modelName)
jsonName, prop := b.buildArrayTypeProperty(field, jsonName, modelName)
return jsonName, modelDescription, prop
case fieldKind == reflect.Ptr:
return b.buildPointerTypeProperty(field, jsonName, modelName)
jsonName, prop := b.buildPointerTypeProperty(field, jsonName, modelName)
return jsonName, modelDescription, prop
case fieldKind == reflect.String:
stringt := "string"
prop.Type = &stringt
return jsonName, prop
return jsonName, modelDescription, prop
case fieldKind == reflect.Map:
// if it's a map, it's unstructured, and swagger 1.2 can't handle it
anyt := "any"
prop.Type = &anyt
return jsonName, prop
return jsonName, modelDescription, prop
}
if b.isPrimitiveType(fieldType.String()) {
mapped := b.jsonSchemaType(fieldType.String())
prop.Type = &mapped
prop.Format = b.jsonSchemaFormat(fieldType.String())
return jsonName, prop
return jsonName, modelDescription, prop
}
modelType := fieldType.String()
prop.Ref = &modelType
@ -150,7 +195,7 @@ func (b modelBuilder) buildProperty(field reflect.StructField, model *Model, mod
prop.Ref = &nestedTypeName
b.addModel(fieldType, nestedTypeName)
}
return jsonName, prop
return jsonName, modelDescription, prop
}
func hasNamedJSONTag(field reflect.StructField) bool {
@ -168,6 +213,7 @@ 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 anonymous
if len(fieldType.Name()) == 0 {
// anonymous
@ -218,6 +264,7 @@ 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
prop.setPropertyMetadata(field)
var pType = "array"
prop.Type = &pType
elemTypeName := b.getElementTypeName(modelName, jsonName, fieldType.Elem())
@ -238,6 +285,7 @@ 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)
// override type of pointer to list-likes
if fieldType.Elem().Kind() == reflect.Slice || fieldType.Elem().Kind() == reflect.Array {

View File

@ -978,3 +978,134 @@ func TestEmbeddedStructPull204(t *testing.T) {
}
`)
}
type AddressWithMethod struct {
Country string `json:"country,omitempty"`
PostCode int `json:"postcode,omitempty"`
}
func (AddressWithMethod) SwaggerDoc() map[string]string {
return map[string]string{
"": "Address doc",
"country": "Country doc",
"postcode": "PostCode doc",
}
}
func TestDocInMethodSwaggerDoc(t *testing.T) {
expected := `{
"swagger.AddressWithMethod": {
"id": "swagger.AddressWithMethod",
"description": "Address doc",
"properties": {
"country": {
"type": "string",
"description": "Country doc"
},
"postcode": {
"type": "integer",
"format": "int32",
"description": "PostCode doc"
}
}
}
}`
testJsonFromStruct(t, AddressWithMethod{}, expected)
}
type RefDesc struct {
f1 *int64 `description:"desc"`
}
func TestPtrDescription(t *testing.T) {
b := RefDesc{}
expected := `{
"swagger.RefDesc": {
"id": "swagger.RefDesc",
"required": [
"f1"
],
"properties": {
"f1": {
"type": "integer",
"format": "int64",
"description": "desc"
}
}
}
}`
testJsonFromStruct(t, b, expected)
}
type A struct {
B `json:",inline"`
C1 `json:"metadata,omitempty"`
}
type B struct {
SB string
}
type C1 struct {
SC string
}
func (A) SwaggerDoc() map[string]string {
return map[string]string{
"": "A struct",
"B": "B field", // We should not get anything from this
"metadata": "C1 field",
}
}
func (B) SwaggerDoc() map[string]string {
return map[string]string{
"": "B struct",
"SB": "SB field",
}
}
func (C1) SwaggerDoc() map[string]string {
return map[string]string{
"": "C1 struct",
"SC": "SC field",
}
}
func TestNestedStructDescription(t *testing.T) {
expected := `
{
"swagger.A": {
"id": "swagger.A",
"description": "A struct",
"required": [
"SB"
],
"properties": {
"SB": {
"type": "string",
"description": "SB field"
},
"metadata": {
"$ref": "swagger.C1",
"description": "C1 field"
}
}
},
"swagger.C1": {
"id": "swagger.C1",
"description": "C1 struct",
"required": [
"SC"
],
"properties": {
"SC": {
"type": "string",
"description": "SC field"
}
}
}
}
`
testJsonFromStruct(t, A{}, expected)
}

View File

@ -0,0 +1,59 @@
package swagger
import (
"reflect"
"strings"
)
func (prop *ModelProperty) setDescription(field reflect.StructField) {
if tag := field.Tag.Get("description"); tag != "" {
prop.Description = tag
}
}
func (prop *ModelProperty) setDefaultValue(field reflect.StructField) {
if tag := field.Tag.Get("default"); tag != "" {
prop.DefaultValue = Special(tag)
}
}
func (prop *ModelProperty) setEnumValues(field reflect.StructField) {
// We use | to separate the enum values. This value is chosen
// since its unlikely to be useful in actual enumeration values.
if tag := field.Tag.Get("enum"); tag != "" {
prop.Enum = strings.Split(tag, "|")
}
}
func (prop *ModelProperty) setMaximum(field reflect.StructField) {
if tag := field.Tag.Get("maximum"); tag != "" {
prop.Maximum = tag
}
}
func (prop *ModelProperty) setMinimum(field reflect.StructField) {
if tag := field.Tag.Get("minimum"); tag != "" {
prop.Minimum = tag
}
}
func (prop *ModelProperty) setUniqueItems(field reflect.StructField) {
tag := field.Tag.Get("unique")
switch tag {
case "true":
v := true
prop.UniqueItems = &v
case "false":
v := false
prop.UniqueItems = &v
}
}
func (prop *ModelProperty) setPropertyMetadata(field reflect.StructField) {
prop.setDescription(field)
prop.setEnumValues(field)
prop.setMinimum(field)
prop.setMaximum(field)
prop.setUniqueItems(field)
prop.setDefaultValue(field)
}

View File

@ -0,0 +1,46 @@
package swagger
import "testing"
// clear && go test -v -test.run TestThatExtraTagsAreReadIntoModel ...swagger
func TestThatExtraTagsAreReadIntoModel(t *testing.T) {
type Anything struct {
Name string `description:"name" modelDescription:"a test"`
Size int `minimum:"0" maximum:"10"`
Stati string `enum:"off|on" default:"on" modelDescription:"more description"`
ID string `unique:"true"`
Password string
}
m := modelsFromStruct(Anything{})
props, _ := m.At("swagger.Anything")
p1, _ := props.Properties.At("Name")
if got, want := p1.Description, "name"; got != want {
t.Errorf("got %v want %v", got, want)
}
p2, _ := props.Properties.At("Size")
if got, want := p2.Minimum, "0"; got != want {
t.Errorf("got %v want %v", got, want)
}
if got, want := p2.Maximum, "10"; got != want {
t.Errorf("got %v want %v", got, want)
}
p3, _ := props.Properties.At("Stati")
if got, want := p3.Enum[0], "off"; got != want {
t.Errorf("got %v want %v", got, want)
}
if got, want := p3.Enum[1], "on"; got != want {
t.Errorf("got %v want %v", got, want)
}
p4, _ := props.Properties.At("ID")
if got, want := *p4.UniqueItems, true; got != want {
t.Errorf("got %v want %v", got, want)
}
p5, _ := props.Properties.At("Password")
if got, want := *p5.Type, "string"; got != want {
t.Errorf("got %v want %v", got, want)
}
if got, want := props.Description, "a test\nmore description"; got != want {
t.Errorf("got %v want %v", got, want)
}
}

View File

@ -6,6 +6,24 @@ import (
"github.com/emicklei/go-restful"
)
// go test -v -test.run TestThatMultiplePathsOnRootAreHandled ...swagger
func TestThatMultiplePathsOnRootAreHandled(t *testing.T) {
ws1 := new(restful.WebService)
ws1.Route(ws1.GET("/_ping").To(dummy))
ws1.Route(ws1.GET("/version").To(dummy))
cfg := Config{
WebServicesUrl: "http://here.com",
ApiPath: "/apipath",
WebServices: []*restful.WebService{ws1},
}
sws := newSwaggerService(cfg)
decl := sws.composeDeclaration(ws1, "/")
if got, want := len(decl.Apis), 2; got != want {
t.Errorf("got %v want %v", got, want)
}
}
// go test -v -test.run TestServiceToApi ...swagger
func TestServiceToApi(t *testing.T) {
ws := new(restful.WebService)

View File

@ -178,11 +178,12 @@ func (sws SwaggerService) getDeclarations(req *restful.Request, resp *restful.Re
resp.WriteAsJson(decl)
}
// composeDeclaration uses all routes and parameters to create a ApiDeclaration
func (sws SwaggerService) composeDeclaration(ws *restful.WebService, pathPrefix string) ApiDeclaration {
decl := ApiDeclaration{
SwaggerVersion: swaggerVersion,
BasePath: sws.config.WebServicesUrl,
ResourcePath: ws.RootPath(),
ResourcePath: pathPrefix,
Models: ModelList{},
ApiVersion: ws.Version()}

View File

@ -50,6 +50,20 @@ func TestCapturePanic(t *testing.T) {
}
}
func TestCapturePanicWithEncoded(t *testing.T) {
tearDown()
Add(newPanicingService())
DefaultContainer.EnableContentEncoding(true)
httpRequest, _ := http.NewRequest("GET", "http://here.com/fire", nil)
httpRequest.Header.Set("Accept", "*/*")
httpRequest.Header.Set("Accept-Encoding", "gzip")
httpWriter := httptest.NewRecorder()
DefaultContainer.dispatch(httpWriter, httpRequest)
if 500 != httpWriter.Code {
t.Error("500 expected on fire, got", httpWriter.Code)
}
}
func TestNotFound(t *testing.T) {
tearDown()
httpRequest, _ := http.NewRequest("GET", "http://here.com/missing", nil)