Merge pull request #7215 from nikhiljindal/errHandle

Updating serviceErrorHandler to use apiVersion specific codec
This commit is contained in:
Yu-Ju Hong 2015-04-23 15:21:29 -07:00
commit a3de4908d5
10 changed files with 193 additions and 37 deletions

4
Godeps/Godeps.json generated
View File

@ -155,8 +155,8 @@
},
{
"ImportPath": "github.com/emicklei/go-restful",
"Comment": "v1.1.3-34-g5e1952e",
"Rev": "5e1952ed0806503c059e4463c2654200660f484b"
"Comment": "v1.1.3-40-g4f30cbd",
"Rev": "4f30cbd5bd858a523d8fe9bd484f44513f50eeec"
},
{
"ImportPath": "github.com/evanphx/json-patch",

View File

@ -54,8 +54,9 @@ func (c *Container) RecoverHandler(handler RecoverHandleFunction) {
}
// ServiceErrorHandleFunction declares functions that can be used to handle a service error situation.
// The first argument is the service error. The second must be used to communicate an error response.
type ServiceErrorHandleFunction func(ServiceError, *Response)
// The first argument is the service error, the second is the request that resulted in the error and
// the third must be used to communicate an error response.
type ServiceErrorHandleFunction func(ServiceError, *Request, *Response)
// ServiceErrorHandler changes the default function (writeServiceError) to be called
// when a ServiceError is detected.
@ -143,7 +144,7 @@ func logStackOnRecover(panicReason interface{}, httpWriter http.ResponseWriter)
// writeServiceError is the default ServiceErrorHandleFunction and is called
// when a ServiceError is returned during route selection. Default implementation
// calls resp.WriteErrorString(err.Code, err.Message)
func writeServiceError(err ServiceError, resp *Response) {
func writeServiceError(err ServiceError, req *Request, resp *Response) {
resp.WriteErrorString(err.Code, err.Message)
}
@ -194,7 +195,7 @@ func (c *Container) dispatch(httpWriter http.ResponseWriter, httpRequest *http.R
switch err.(type) {
case ServiceError:
ser := err.(ServiceError)
c.serviceErrorHandleFunc(ser, resp)
c.serviceErrorHandleFunc(ser, req, resp)
}
// TODO
}}

View File

@ -1,5 +1,8 @@
Change history of swagger
=
2015-04-09
- add ModelBuildable interface for customization of Model
2015-03-17
- preserve order of Routes per WebService in Swagger listing
- fix use of $ref and type in Swagger models

View File

@ -6,22 +6,40 @@ import (
"strings"
)
// ModelBuildable is used for extending Structs that need more control over
// how the Model appears in the Swagger api declaration.
type ModelBuildable interface {
PostBuildModel(m *Model) *Model
}
type modelBuilder struct {
Models map[string]Model
}
func (b modelBuilder) addModel(st reflect.Type, nameOverride 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{}) {
if modelOrNil := b.addModel(reflect.TypeOf(sample), ""); modelOrNil != nil {
// allow customizations
if buildable, ok := sample.(ModelBuildable); ok {
modelOrNil = buildable.PostBuildModel(modelOrNil)
b.Models[modelOrNil.Id] = *modelOrNil
}
}
}
func (b modelBuilder) addModel(st reflect.Type, nameOverride string) *Model {
modelName := b.keyFrom(st)
if nameOverride != "" {
modelName = nameOverride
}
// no models needed for primitive types
if b.isPrimitiveType(modelName) {
return
return nil
}
// see if we already have visited this model
if _, ok := b.Models[modelName]; ok {
return
return nil
}
sm := Model{
Id: modelName,
@ -34,11 +52,11 @@ func (b modelBuilder) addModel(st reflect.Type, nameOverride string) {
// check for slice or array
if st.Kind() == reflect.Slice || st.Kind() == reflect.Array {
b.addModel(st.Elem(), "")
return
return &sm
}
// check for structure or primitive type
if st.Kind() != reflect.Struct {
return
return &sm
}
for i := 0; i < st.NumField(); i++ {
field := st.Field(i)
@ -55,9 +73,10 @@ func (b modelBuilder) addModel(st reflect.Type, nameOverride string) {
sm.Properties[jsonName] = prop
}
}
// update model builder with completed model
b.Models[modelName] = sm
return &sm
}
func (b modelBuilder) isPropertyRequired(field reflect.StructField) bool {
@ -107,12 +126,12 @@ func (b modelBuilder) buildProperty(field reflect.StructField, model *Model, mod
case fieldKind == reflect.Ptr:
return b.buildPointerTypeProperty(field, jsonName, modelName)
case fieldKind == reflect.String:
stringt := "string"
stringt := "string"
prop.Type = &stringt
return jsonName, prop
case fieldKind == reflect.Map:
// if it's a map, it's unstructured, and swagger 1.2 can't handle it
anyt := "any"
// if it's a map, it's unstructured, and swagger 1.2 can't handle it
anyt := "any"
prop.Type = &anyt
return jsonName, prop
}
@ -134,6 +153,19 @@ func (b modelBuilder) buildProperty(field reflect.StructField, model *Model, mod
return jsonName, prop
}
func hasNamedJSONTag(field reflect.StructField) bool {
parts := strings.Split(field.Tag.Get("json"), ",")
if len(parts) == 0 {
return false
}
for _, s := range parts[1:] {
if s == "inline" {
return false
}
}
return len(parts[0]) > 0
}
func (b modelBuilder) buildStructTypeProperty(field reflect.StructField, jsonName string, model *Model) (nameJson string, prop ModelProperty) {
fieldType := field.Type
// check for anonymous
@ -144,7 +176,8 @@ func (b modelBuilder) buildStructTypeProperty(field reflect.StructField, jsonNam
prop.Ref = &anonType
return jsonName, prop
}
if field.Name == fieldType.Name() && field.Anonymous {
if field.Name == fieldType.Name() && field.Anonymous && !hasNamedJSONTag(field) {
// embedded struct
sub := modelBuilder{map[string]Model{}}
sub.addModel(fieldType, "")
@ -246,8 +279,9 @@ func (b modelBuilder) keyFrom(st reflect.Type) string {
return key
}
// see also https://golang.org/ref/spec#Numeric_types
func (b modelBuilder) isPrimitiveType(modelName string) bool {
return strings.Contains("uint8 int int32 int64 float32 float64 bool string byte time.Time", modelName)
return strings.Contains("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
@ -265,25 +299,31 @@ func (b modelBuilder) jsonNameOfField(field reflect.StructField) string {
return field.Name
}
// see also http://json-schema.org/latest/json-schema-core.html#anchor8
func (b modelBuilder) jsonSchemaType(modelName string) string {
schemaMap := map[string]string{
"uint8": "integer",
"int": "integer",
"int32": "integer",
"int64": "integer",
"uint64": "integer",
"byte": "string",
"uint8": "integer",
"uint16": "integer",
"uint32": "integer",
"uint64": "integer",
"int": "integer",
"int8": "integer",
"int16": "integer",
"int32": "integer",
"int64": "integer",
"byte": "integer",
"float64": "number",
"float32": "number",
"bool": "boolean",
"time.Time": "string",
}
mapped, ok := schemaMap[modelName]
if ok {
return mapped
} else {
if !ok {
return modelName // use as is (custom or struct)
}
return mapped
}
func (b modelBuilder) jsonSchemaFormat(modelName string) string {
@ -298,9 +338,8 @@ func (b modelBuilder) jsonSchemaFormat(modelName string) string {
"time.Time": "date-time",
}
mapped, ok := schemaMap[modelName]
if ok {
return mapped
} else {
if !ok {
return "" // no format
}
return mapped
}

View File

@ -647,6 +647,61 @@ func TestStructA3(t *testing.T) {
}`)
}
type A4 struct {
D "json:,inline"
}
// clear && go test -v -test.run TestStructA4 ...swagger
func TestEmbeddedStructA4(t *testing.T) {
testJsonFromStruct(t, A4{}, `{
"swagger.A4": {
"id": "swagger.A4",
"required": [
"Id"
],
"properties": {
"Id": {
"type": "integer",
"format": "int32"
}
}
}
}`)
}
type A5 struct {
D `json:"d"`
}
// clear && go test -v -test.run TestStructA5 ...swagger
func TestEmbeddedStructA5(t *testing.T) {
testJsonFromStruct(t, A5{}, `{
"swagger.A5": {
"id": "swagger.A5",
"required": [
"d"
],
"properties": {
"d": {
"$ref": "swagger.D"
}
}
},
"swagger.D": {
"id": "swagger.D",
"required": [
"Id"
],
"properties": {
"Id": {
"type": "integer",
"format": "int32"
}
}
}
}`)
}
type ObjectId []byte
type Region struct {

View File

@ -0,0 +1,42 @@
package swagger
import "testing"
type Boat struct {
Length int `json:"-"` // on default, this makes the fields not required
Weight int `json:"-"`
}
// PostBuildModel is from swagger.ModelBuildable
func (b Boat) PostBuildModel(m *Model) *Model {
// override required
m.Required = []string{"Length", "Weight"}
// add model property (just to test is can be added; is this a real usecase?)
extraType := "string"
m.Properties["extra"] = ModelProperty{
Description: "extra description",
DataTypeFields: DataTypeFields{
Type: &extraType,
},
}
return m
}
func TestCustomPostModelBuilde(t *testing.T) {
testJsonFromStruct(t, Boat{}, `{
"swagger.Boat": {
"id": "swagger.Boat",
"required": [
"Length",
"Weight"
],
"properties": {
"extra": {
"type": "string",
"description": "extra description"
}
}
}
}`)
}

View File

@ -299,7 +299,7 @@ func (sws SwaggerService) addModelFromSampleTo(operation *Operation, isResponse
}
operation.Type = modelName
}
modelBuilder{models}.addModel(reflect.TypeOf(sample), "")
modelBuilder{models}.addModelFrom(sample)
}
func asSwaggerParameter(param restful.ParameterData) Parameter {

View File

@ -18,7 +18,7 @@ func testJsonFromStruct(t *testing.T, sample interface{}, expectedJson string) b
func modelsFromStruct(sample interface{}) map[string]Model {
models := map[string]Model{}
builder := modelBuilder{models}
builder.addModel(reflect.TypeOf(sample), "")
builder.addModelFrom(sample)
return models
}

View File

@ -171,13 +171,27 @@ func InstallLogsSupport(mux Mux) {
mux.Handle("/logs/", http.StripPrefix("/logs/", http.FileServer(http.Dir("/var/log/"))))
}
func InstallServiceErrorHandler(container *restful.Container) {
container.ServiceErrorHandler(serviceErrorHandler)
func InstallServiceErrorHandler(container *restful.Container, requestResolver *APIRequestInfoResolver, apiVersions []string) {
container.ServiceErrorHandler(func(serviceErr restful.ServiceError, request *restful.Request, response *restful.Response) {
serviceErrorHandler(requestResolver, apiVersions, serviceErr, request, response)
})
}
func serviceErrorHandler(serviceErr restful.ServiceError, response *restful.Response) {
// TODO: Update go-restful to return the request as well, so that we can use the appropriate codec rather than using the latest one.
errorJSON(apierrors.NewGenericServerResponse(serviceErr.Code, "", "", "", "", 0, false), latest.Codec, response.ResponseWriter)
func serviceErrorHandler(requestResolver *APIRequestInfoResolver, apiVersions []string, serviceErr restful.ServiceError, request *restful.Request, response *restful.Response) {
requestInfo, err := requestResolver.GetAPIRequestInfo(request.Request)
codec := latest.Codec
if err == nil && requestInfo.APIVersion != "" {
// check if the api version is valid.
for _, version := range apiVersions {
if requestInfo.APIVersion == version {
// valid api version.
codec = runtime.CodecFor(api.Scheme, requestInfo.APIVersion)
break
}
}
}
errorJSON(apierrors.NewGenericServerResponse(serviceErr.Code, "", "", "", "", 0, false), codec, response.ResponseWriter)
}
// Adds a service to return the supported api versions.

View File

@ -449,7 +449,9 @@ func (m *Master) init(c *Config) {
apiserver.InstallSupport(m.muxHelper, m.rootWebService)
apiserver.AddApiWebService(m.handlerContainer, c.APIPrefix, apiVersions)
apiserver.InstallServiceErrorHandler(m.handlerContainer)
defaultVersion := m.defaultAPIGroupVersion()
requestInfoResolver := &apiserver.APIRequestInfoResolver{util.NewStringSet(strings.TrimPrefix(defaultVersion.Root, "/")), defaultVersion.Mapper}
apiserver.InstallServiceErrorHandler(m.handlerContainer, requestInfoResolver, apiVersions)
// Register root handler.
// We do not register this using restful Webservice since we do not want to surface this in api docs.