├── .travis.yml ├── CHANGES.md ├── LICENSE ├── README.md ├── api_declaration_list.go ├── config.go ├── model_builder.go ├── model_builder_test.go ├── model_list.go ├── model_list_test.go ├── model_property_ext.go ├── model_property_ext_test.go ├── model_property_list.go ├── model_property_list_test.go ├── ordered_route_map.go ├── ordered_route_map_test.go ├── postbuild_model_test.go ├── swagger.go ├── swagger_builder.go ├── swagger_test.go ├── swagger_webservice.go ├── test_package └── struct.go └── utils_test.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | arch: 3 | - amd64 4 | - ppc64le 5 | go: 6 | - 1.x 7 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | Change history of swagger 2 | = 3 | 2017-01-30 4 | - moved from go-restful/swagger to go-restful-swagger12 5 | 6 | 2015-10-16 7 | - add type override mechanism for swagger models (MR 254, nathanejohnson) 8 | - replace uses of wildcard in generated apidocs (issue 251) 9 | 10 | 2015-05-25 11 | - (api break) changed the type of Properties in Model 12 | - (api break) changed the type of Models in ApiDeclaration 13 | - (api break) changed the parameter type of PostBuildDeclarationMapFunc 14 | 15 | 2015-04-09 16 | - add ModelBuildable interface for customization of Model 17 | 18 | 2015-03-17 19 | - preserve order of Routes per WebService in Swagger listing 20 | - fix use of $ref and type in Swagger models 21 | - add api version to listing 22 | 23 | 2014-11-14 24 | - operation parameters are now sorted using ordering path,query,form,header,body 25 | 26 | 2014-11-12 27 | - respect omitempty tag value for embedded structs 28 | - expose ApiVersion of WebService to Swagger ApiDeclaration 29 | 30 | 2014-05-29 31 | - (api add) Ability to define custom http.Handler to serve swagger-ui static files 32 | 33 | 2014-05-04 34 | - (fix) include model for array element type of response 35 | 36 | 2014-01-03 37 | - (fix) do not add primitive type to the Api models 38 | 39 | 2013-11-27 40 | - (fix) make Swagger work for WebServices with root ("/" or "") paths 41 | 42 | 2013-10-29 43 | - (api add) package variable LogInfo to customize logging function 44 | 45 | 2013-10-15 46 | - upgraded to spec version 1.2 (https://github.com/wordnik/swagger-core/wiki/1.2-transition) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Ernest Micklei 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-restful-swagger12 2 | 3 | [![Build Status](https://travis-ci.org/emicklei/go-restful-swagger12.png)](https://travis-ci.org/emicklei/go-restful-swagger12) 4 | [![GoDoc](https://godoc.org/github.com/emicklei/go-restful-swagger12?status.svg)](https://godoc.org/github.com/emicklei/go-restful-swagger12) 5 | 6 | How to use Swagger UI with go-restful 7 | = 8 | 9 | Get the Swagger UI sources (version 1.2 only) 10 | 11 | git clone https://github.com/wordnik/swagger-ui.git 12 | 13 | The project contains a "dist" folder. 14 | Its contents has all the Swagger UI files you need. 15 | 16 | The `index.html` has an `url` set to `http://petstore.swagger.wordnik.com/api/api-docs`. 17 | You need to change that to match your WebService JSON endpoint e.g. `http://localhost:8080/apidocs.json` 18 | 19 | Now, you can install the Swagger WebService for serving the Swagger specification in JSON. 20 | 21 | config := swagger.Config{ 22 | WebServices: restful.RegisteredWebServices(), 23 | ApiPath: "/apidocs.json", 24 | SwaggerPath: "/apidocs/", 25 | SwaggerFilePath: "/Users/emicklei/Projects/swagger-ui/dist"} 26 | swagger.InstallSwaggerService(config) 27 | 28 | 29 | Documenting Structs 30 | -- 31 | 32 | Currently there are 2 ways to document your structs in the go-restful Swagger. 33 | 34 | ###### By using struct tags 35 | - Use tag "description" to annotate a struct field with a description to show in the UI 36 | - 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. 37 | 38 | ###### By using the SwaggerDoc method 39 | 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**. 40 | 41 | type Address struct { 42 | Country string `json:"country,omitempty"` 43 | PostCode int `json:"postcode,omitempty"` 44 | } 45 | 46 | func (Address) SwaggerDoc() map[string]string { 47 | return map[string]string{ 48 | "": "Address doc", 49 | "country": "Country doc", 50 | "postcode": "PostCode doc", 51 | } 52 | } 53 | 54 | This example will generate a JSON like this 55 | 56 | { 57 | "Address": { 58 | "id": "Address", 59 | "description": "Address doc", 60 | "properties": { 61 | "country": { 62 | "type": "string", 63 | "description": "Country doc" 64 | }, 65 | "postcode": { 66 | "type": "integer", 67 | "format": "int32", 68 | "description": "PostCode doc" 69 | } 70 | } 71 | } 72 | } 73 | 74 | **Very Important Notes:** 75 | - `SwaggerDoc()` is using a **NON-Pointer** receiver (e.g. func (Address) and not func (*Address)) 76 | - The returned map should use as key the name of the field as defined in the JSON parameter (e.g. `"postcode"` and not `"PostCode"`) 77 | 78 | Notes 79 | -- 80 | - The Nickname of an Operation is automatically set by finding the name of the function. You can override it using RouteBuilder.Operation(..) 81 | - 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. 82 | 83 | © 2017, ernestmicklei.com. MIT License. Contributions welcome. -------------------------------------------------------------------------------- /api_declaration_list.go: -------------------------------------------------------------------------------- 1 | package swagger 2 | 3 | // Copyright 2015 Ernest Micklei. All rights reserved. 4 | // Use of this source code is governed by a license 5 | // that can be found in the LICENSE file. 6 | 7 | import ( 8 | "bytes" 9 | "encoding/json" 10 | ) 11 | 12 | // ApiDeclarationList maintains an ordered list of ApiDeclaration. 13 | type ApiDeclarationList struct { 14 | List []ApiDeclaration 15 | } 16 | 17 | // At returns the ApiDeclaration by its path unless absent, then ok is false 18 | func (l *ApiDeclarationList) At(path string) (a ApiDeclaration, ok bool) { 19 | for _, each := range l.List { 20 | if each.ResourcePath == path { 21 | return each, true 22 | } 23 | } 24 | return a, false 25 | } 26 | 27 | // Put adds or replaces a ApiDeclaration with this name 28 | func (l *ApiDeclarationList) Put(path string, a ApiDeclaration) { 29 | // maybe replace existing 30 | for i, each := range l.List { 31 | if each.ResourcePath == path { 32 | // replace 33 | l.List[i] = a 34 | return 35 | } 36 | } 37 | // add 38 | l.List = append(l.List, a) 39 | } 40 | 41 | // Do enumerates all the properties, each with its assigned name 42 | func (l *ApiDeclarationList) Do(block func(path string, decl ApiDeclaration)) { 43 | for _, each := range l.List { 44 | block(each.ResourcePath, each) 45 | } 46 | } 47 | 48 | // MarshalJSON writes the ModelPropertyList as if it was a map[string]ModelProperty 49 | func (l ApiDeclarationList) MarshalJSON() ([]byte, error) { 50 | var buf bytes.Buffer 51 | encoder := json.NewEncoder(&buf) 52 | buf.WriteString("{\n") 53 | for i, each := range l.List { 54 | buf.WriteString("\"") 55 | buf.WriteString(each.ResourcePath) 56 | buf.WriteString("\": ") 57 | encoder.Encode(each) 58 | if i < len(l.List)-1 { 59 | buf.WriteString(",\n") 60 | } 61 | } 62 | buf.WriteString("}") 63 | return buf.Bytes(), nil 64 | } 65 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package swagger 2 | 3 | import ( 4 | "net/http" 5 | "reflect" 6 | 7 | "github.com/emicklei/go-restful" 8 | ) 9 | 10 | // PostBuildDeclarationMapFunc can be used to modify the api declaration map. 11 | type PostBuildDeclarationMapFunc func(apiDeclarationMap *ApiDeclarationList) 12 | 13 | // MapSchemaFormatFunc can be used to modify typeName at definition time. 14 | type MapSchemaFormatFunc func(typeName string) string 15 | 16 | // MapModelTypeNameFunc can be used to return the desired typeName for a given 17 | // type. It will return false if the default name should be used. 18 | type MapModelTypeNameFunc func(t reflect.Type) (string, bool) 19 | 20 | type Config struct { 21 | // url where the services are available, e.g. http://localhost:8080 22 | // if left empty then the basePath of Swagger is taken from the actual request 23 | WebServicesUrl string 24 | // path where the JSON api is avaiable , e.g. /apidocs 25 | ApiPath string 26 | // [optional] path where the swagger UI will be served, e.g. /swagger 27 | SwaggerPath string 28 | // [optional] location of folder containing Swagger HTML5 application index.html 29 | SwaggerFilePath string 30 | // api listing is constructed from this list of restful WebServices. 31 | WebServices []*restful.WebService 32 | // will serve all static content (scripts,pages,images) 33 | StaticHandler http.Handler 34 | // [optional] on default CORS (Cross-Origin-Resource-Sharing) is enabled. 35 | DisableCORS bool 36 | // Top-level API version. Is reflected in the resource listing. 37 | ApiVersion string 38 | // If set then call this handler after building the complete ApiDeclaration Map 39 | PostBuildHandler PostBuildDeclarationMapFunc 40 | // Swagger global info struct 41 | Info Info 42 | // [optional] If set, model builder should call this handler to get addition typename-to-swagger-format-field conversion. 43 | SchemaFormatHandler MapSchemaFormatFunc 44 | // [optional] If set, model builder should call this handler to retrieve the name for a given type. 45 | ModelTypeNameHandler MapModelTypeNameFunc 46 | } 47 | -------------------------------------------------------------------------------- /model_builder.go: -------------------------------------------------------------------------------- 1 | package swagger 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "strings" 7 | ) 8 | 9 | // ModelBuildable is used for extending Structs that need more control over 10 | // how the Model appears in the Swagger api declaration. 11 | type ModelBuildable interface { 12 | PostBuildModel(m *Model) *Model 13 | } 14 | 15 | type modelBuilder struct { 16 | Models *ModelList 17 | Config *Config 18 | } 19 | 20 | type documentable interface { 21 | SwaggerDoc() map[string]string 22 | } 23 | 24 | // Check if this structure has a method with signature func () SwaggerDoc() map[string]string 25 | // If it exists, retrive the documentation and overwrite all struct tag descriptions 26 | func getDocFromMethodSwaggerDoc2(model reflect.Type) map[string]string { 27 | if docable, ok := reflect.New(model).Elem().Interface().(documentable); ok { 28 | return docable.SwaggerDoc() 29 | } 30 | return make(map[string]string) 31 | } 32 | 33 | // addModelFrom creates and adds a Model to the builder and detects and calls 34 | // the post build hook for customizations 35 | func (b modelBuilder) addModelFrom(sample interface{}) { 36 | if modelOrNil := b.addModel(reflect.TypeOf(sample), ""); modelOrNil != nil { 37 | // allow customizations 38 | if buildable, ok := sample.(ModelBuildable); ok { 39 | modelOrNil = buildable.PostBuildModel(modelOrNil) 40 | b.Models.Put(modelOrNil.Id, *modelOrNil) 41 | } 42 | } 43 | } 44 | 45 | func (b modelBuilder) addModel(st reflect.Type, nameOverride string) *Model { 46 | // Turn pointers into simpler types so further checks are 47 | // correct. 48 | if st.Kind() == reflect.Ptr { 49 | st = st.Elem() 50 | } 51 | 52 | modelName := b.keyFrom(st) 53 | if nameOverride != "" { 54 | modelName = nameOverride 55 | } 56 | // no models needed for primitive types 57 | if b.isPrimitiveType(modelName) { 58 | return nil 59 | } 60 | // golang encoding/json packages says array and slice values encode as 61 | // JSON arrays, except that []byte encodes as a base64-encoded string. 62 | // If we see a []byte here, treat it at as a primitive type (string) 63 | // and deal with it in buildArrayTypeProperty. 64 | if (st.Kind() == reflect.Slice || st.Kind() == reflect.Array) && 65 | st.Elem().Kind() == reflect.Uint8 { 66 | return nil 67 | } 68 | // see if we already have visited this model 69 | if _, ok := b.Models.At(modelName); ok { 70 | return nil 71 | } 72 | sm := Model{ 73 | Id: modelName, 74 | Required: []string{}, 75 | Properties: ModelPropertyList{}} 76 | 77 | // reference the model before further initializing (enables recursive structs) 78 | b.Models.Put(modelName, sm) 79 | 80 | // check for slice or array 81 | if st.Kind() == reflect.Slice || st.Kind() == reflect.Array { 82 | b.addModel(st.Elem(), "") 83 | return &sm 84 | } 85 | // check for structure or primitive type 86 | if st.Kind() != reflect.Struct { 87 | return &sm 88 | } 89 | 90 | fullDoc := getDocFromMethodSwaggerDoc2(st) 91 | modelDescriptions := []string{} 92 | 93 | for i := 0; i < st.NumField(); i++ { 94 | field := st.Field(i) 95 | jsonName, modelDescription, prop := b.buildProperty(field, &sm, modelName) 96 | if len(modelDescription) > 0 { 97 | modelDescriptions = append(modelDescriptions, modelDescription) 98 | } 99 | 100 | // add if not omitted 101 | if len(jsonName) != 0 { 102 | // update description 103 | if fieldDoc, ok := fullDoc[jsonName]; ok { 104 | prop.Description = fieldDoc 105 | } 106 | // update Required 107 | if b.isPropertyRequired(field) { 108 | sm.Required = append(sm.Required, jsonName) 109 | } 110 | sm.Properties.Put(jsonName, prop) 111 | } 112 | } 113 | 114 | // We always overwrite documentation if SwaggerDoc method exists 115 | // "" is special for documenting the struct itself 116 | if modelDoc, ok := fullDoc[""]; ok { 117 | sm.Description = modelDoc 118 | } else if len(modelDescriptions) != 0 { 119 | sm.Description = strings.Join(modelDescriptions, "\n") 120 | } 121 | 122 | // update model builder with completed model 123 | b.Models.Put(modelName, sm) 124 | 125 | return &sm 126 | } 127 | 128 | func (b modelBuilder) isPropertyRequired(field reflect.StructField) bool { 129 | required := true 130 | if jsonTag := field.Tag.Get("json"); jsonTag != "" { 131 | s := strings.Split(jsonTag, ",") 132 | if len(s) > 1 && s[1] == "omitempty" { 133 | return false 134 | } 135 | } 136 | return required 137 | } 138 | 139 | func (b modelBuilder) buildProperty(field reflect.StructField, model *Model, modelName string) (jsonName, modelDescription string, prop ModelProperty) { 140 | jsonName = b.jsonNameOfField(field) 141 | if len(jsonName) == 0 { 142 | // empty name signals skip property 143 | return "", "", prop 144 | } 145 | 146 | if field.Name == "XMLName" && field.Type.String() == "xml.Name" { 147 | // property is metadata for the xml.Name attribute, can be skipped 148 | return "", "", prop 149 | } 150 | 151 | if tag := field.Tag.Get("modelDescription"); tag != "" { 152 | modelDescription = tag 153 | } 154 | 155 | prop.setPropertyMetadata(field) 156 | if prop.Type != nil { 157 | return jsonName, modelDescription, prop 158 | } 159 | fieldType := field.Type 160 | 161 | // check if type is doing its own marshalling 162 | marshalerType := reflect.TypeOf((*json.Marshaler)(nil)).Elem() 163 | if fieldType.Implements(marshalerType) { 164 | var pType = "string" 165 | if prop.Type == nil { 166 | prop.Type = &pType 167 | } 168 | if prop.Format == "" { 169 | prop.Format = b.jsonSchemaFormat(b.keyFrom(fieldType)) 170 | } 171 | return jsonName, modelDescription, prop 172 | } 173 | 174 | // check if annotation says it is a string 175 | if jsonTag := field.Tag.Get("json"); jsonTag != "" { 176 | s := strings.Split(jsonTag, ",") 177 | if len(s) > 1 && s[1] == "string" { 178 | stringt := "string" 179 | prop.Type = &stringt 180 | return jsonName, modelDescription, prop 181 | } 182 | } 183 | 184 | fieldKind := fieldType.Kind() 185 | switch { 186 | case fieldKind == reflect.Struct: 187 | jsonName, prop := b.buildStructTypeProperty(field, jsonName, model) 188 | return jsonName, modelDescription, prop 189 | case fieldKind == reflect.Slice || fieldKind == reflect.Array: 190 | jsonName, prop := b.buildArrayTypeProperty(field, jsonName, modelName) 191 | return jsonName, modelDescription, prop 192 | case fieldKind == reflect.Ptr: 193 | jsonName, prop := b.buildPointerTypeProperty(field, jsonName, modelName) 194 | return jsonName, modelDescription, prop 195 | case fieldKind == reflect.String: 196 | stringt := "string" 197 | prop.Type = &stringt 198 | return jsonName, modelDescription, prop 199 | case fieldKind == reflect.Map: 200 | // if it's a map, it's unstructured, and swagger 1.2 can't handle it 201 | objectType := "object" 202 | prop.Type = &objectType 203 | return jsonName, modelDescription, prop 204 | } 205 | 206 | fieldTypeName := b.keyFrom(fieldType) 207 | if b.isPrimitiveType(fieldTypeName) { 208 | mapped := b.jsonSchemaType(fieldTypeName) 209 | prop.Type = &mapped 210 | prop.Format = b.jsonSchemaFormat(fieldTypeName) 211 | return jsonName, modelDescription, prop 212 | } 213 | modelType := b.keyFrom(fieldType) 214 | prop.Ref = &modelType 215 | 216 | if fieldType.Name() == "" { // override type of anonymous structs 217 | nestedTypeName := modelName + "." + jsonName 218 | prop.Ref = &nestedTypeName 219 | b.addModel(fieldType, nestedTypeName) 220 | } 221 | return jsonName, modelDescription, prop 222 | } 223 | 224 | func hasNamedJSONTag(field reflect.StructField) bool { 225 | parts := strings.Split(field.Tag.Get("json"), ",") 226 | if len(parts) == 0 { 227 | return false 228 | } 229 | for _, s := range parts[1:] { 230 | if s == "inline" { 231 | return false 232 | } 233 | } 234 | return len(parts[0]) > 0 235 | } 236 | 237 | func (b modelBuilder) buildStructTypeProperty(field reflect.StructField, jsonName string, model *Model) (nameJson string, prop ModelProperty) { 238 | prop.setPropertyMetadata(field) 239 | // Check for type override in tag 240 | if prop.Type != nil { 241 | return jsonName, prop 242 | } 243 | fieldType := field.Type 244 | // check for anonymous 245 | if len(fieldType.Name()) == 0 { 246 | // anonymous 247 | anonType := model.Id + "." + jsonName 248 | b.addModel(fieldType, anonType) 249 | prop.Ref = &anonType 250 | return jsonName, prop 251 | } 252 | 253 | if field.Name == fieldType.Name() && field.Anonymous && !hasNamedJSONTag(field) { 254 | // embedded struct 255 | sub := modelBuilder{new(ModelList), b.Config} 256 | sub.addModel(fieldType, "") 257 | subKey := sub.keyFrom(fieldType) 258 | // merge properties from sub 259 | subModel, _ := sub.Models.At(subKey) 260 | subModel.Properties.Do(func(k string, v ModelProperty) { 261 | model.Properties.Put(k, v) 262 | // if subModel says this property is required then include it 263 | required := false 264 | for _, each := range subModel.Required { 265 | if k == each { 266 | required = true 267 | break 268 | } 269 | } 270 | if required { 271 | model.Required = append(model.Required, k) 272 | } 273 | }) 274 | // add all new referenced models 275 | sub.Models.Do(func(key string, sub Model) { 276 | if key != subKey { 277 | if _, ok := b.Models.At(key); !ok { 278 | b.Models.Put(key, sub) 279 | } 280 | } 281 | }) 282 | // empty name signals skip property 283 | return "", prop 284 | } 285 | // simple struct 286 | b.addModel(fieldType, "") 287 | var pType = b.keyFrom(fieldType) 288 | prop.Ref = &pType 289 | return jsonName, prop 290 | } 291 | 292 | func (b modelBuilder) buildArrayTypeProperty(field reflect.StructField, jsonName, modelName string) (nameJson string, prop ModelProperty) { 293 | // check for type override in tags 294 | prop.setPropertyMetadata(field) 295 | if prop.Type != nil { 296 | return jsonName, prop 297 | } 298 | fieldType := field.Type 299 | if fieldType.Elem().Kind() == reflect.Uint8 { 300 | stringt := "string" 301 | prop.Type = &stringt 302 | return jsonName, prop 303 | } 304 | var pType = "array" 305 | prop.Type = &pType 306 | isPrimitive := b.isPrimitiveType(fieldType.Elem().Name()) 307 | elemTypeName := b.getElementTypeName(modelName, jsonName, fieldType.Elem()) 308 | prop.Items = new(Item) 309 | if isPrimitive { 310 | mapped := b.jsonSchemaType(elemTypeName) 311 | prop.Items.Type = &mapped 312 | } else { 313 | prop.Items.Ref = &elemTypeName 314 | } 315 | // add|overwrite model for element type 316 | if fieldType.Elem().Kind() == reflect.Ptr { 317 | fieldType = fieldType.Elem() 318 | } 319 | if !isPrimitive { 320 | b.addModel(fieldType.Elem(), elemTypeName) 321 | } 322 | return jsonName, prop 323 | } 324 | 325 | func (b modelBuilder) buildPointerTypeProperty(field reflect.StructField, jsonName, modelName string) (nameJson string, prop ModelProperty) { 326 | prop.setPropertyMetadata(field) 327 | // Check for type override in tags 328 | if prop.Type != nil { 329 | return jsonName, prop 330 | } 331 | fieldType := field.Type 332 | 333 | // override type of pointer to list-likes 334 | if fieldType.Elem().Kind() == reflect.Slice || fieldType.Elem().Kind() == reflect.Array { 335 | var pType = "array" 336 | prop.Type = &pType 337 | isPrimitive := b.isPrimitiveType(fieldType.Elem().Elem().Name()) 338 | elemName := b.getElementTypeName(modelName, jsonName, fieldType.Elem().Elem()) 339 | if isPrimitive { 340 | primName := b.jsonSchemaType(elemName) 341 | prop.Items = &Item{Ref: &primName} 342 | } else { 343 | prop.Items = &Item{Ref: &elemName} 344 | } 345 | if !isPrimitive { 346 | // add|overwrite model for element type 347 | b.addModel(fieldType.Elem().Elem(), elemName) 348 | } 349 | } else { 350 | // non-array, pointer type 351 | fieldTypeName := b.keyFrom(fieldType.Elem()) 352 | var pType = b.jsonSchemaType(fieldTypeName) // no star, include pkg path 353 | if b.isPrimitiveType(fieldTypeName) { 354 | prop.Type = &pType 355 | prop.Format = b.jsonSchemaFormat(fieldTypeName) 356 | return jsonName, prop 357 | } 358 | prop.Ref = &pType 359 | elemName := "" 360 | if fieldType.Elem().Name() == "" { 361 | elemName = modelName + "." + jsonName 362 | prop.Ref = &elemName 363 | } 364 | b.addModel(fieldType.Elem(), elemName) 365 | } 366 | return jsonName, prop 367 | } 368 | 369 | func (b modelBuilder) getElementTypeName(modelName, jsonName string, t reflect.Type) string { 370 | if t.Kind() == reflect.Ptr { 371 | t = t.Elem() 372 | } 373 | if t.Name() == "" { 374 | return modelName + "." + jsonName 375 | } 376 | return b.keyFrom(t) 377 | } 378 | 379 | func (b modelBuilder) keyFrom(st reflect.Type) string { 380 | key := st.String() 381 | if b.Config != nil && b.Config.ModelTypeNameHandler != nil { 382 | if name, ok := b.Config.ModelTypeNameHandler(st); ok { 383 | key = name 384 | } 385 | } 386 | if len(st.Name()) == 0 { // unnamed type 387 | // Swagger UI has special meaning for [ 388 | key = strings.Replace(key, "[]", "||", -1) 389 | } 390 | return key 391 | } 392 | 393 | // see also https://golang.org/ref/spec#Numeric_types 394 | func (b modelBuilder) isPrimitiveType(modelName string) bool { 395 | if len(modelName) == 0 { 396 | return false 397 | } 398 | return strings.Contains("uint uint8 uint16 uint32 uint64 int int8 int16 int32 int64 float32 float64 bool string byte rune time.Time", modelName) 399 | } 400 | 401 | // jsonNameOfField returns the name of the field as it should appear in JSON format 402 | // An empty string indicates that this field is not part of the JSON representation 403 | func (b modelBuilder) jsonNameOfField(field reflect.StructField) string { 404 | if jsonTag := field.Tag.Get("json"); jsonTag != "" { 405 | s := strings.Split(jsonTag, ",") 406 | if s[0] == "-" { 407 | // empty name signals skip property 408 | return "" 409 | } else if s[0] != "" { 410 | return s[0] 411 | } 412 | } 413 | return field.Name 414 | } 415 | 416 | // see also http://json-schema.org/latest/json-schema-core.html#anchor8 417 | func (b modelBuilder) jsonSchemaType(modelName string) string { 418 | schemaMap := map[string]string{ 419 | "uint": "integer", 420 | "uint8": "integer", 421 | "uint16": "integer", 422 | "uint32": "integer", 423 | "uint64": "integer", 424 | 425 | "int": "integer", 426 | "int8": "integer", 427 | "int16": "integer", 428 | "int32": "integer", 429 | "int64": "integer", 430 | 431 | "byte": "integer", 432 | "float64": "number", 433 | "float32": "number", 434 | "bool": "boolean", 435 | "time.Time": "string", 436 | } 437 | mapped, ok := schemaMap[modelName] 438 | if !ok { 439 | return modelName // use as is (custom or struct) 440 | } 441 | return mapped 442 | } 443 | 444 | func (b modelBuilder) jsonSchemaFormat(modelName string) string { 445 | if b.Config != nil && b.Config.SchemaFormatHandler != nil { 446 | if mapped := b.Config.SchemaFormatHandler(modelName); mapped != "" { 447 | return mapped 448 | } 449 | } 450 | schemaMap := map[string]string{ 451 | "int": "int32", 452 | "int32": "int32", 453 | "int64": "int64", 454 | "byte": "byte", 455 | "uint": "integer", 456 | "uint8": "byte", 457 | "float64": "double", 458 | "float32": "float", 459 | "time.Time": "date-time", 460 | "*time.Time": "date-time", 461 | } 462 | mapped, ok := schemaMap[modelName] 463 | if !ok { 464 | return "" // no format 465 | } 466 | return mapped 467 | } 468 | -------------------------------------------------------------------------------- /model_builder_test.go: -------------------------------------------------------------------------------- 1 | package swagger 2 | 3 | import ( 4 | "encoding/xml" 5 | "net" 6 | "reflect" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | type YesNo bool 12 | 13 | func (y YesNo) MarshalJSON() ([]byte, error) { 14 | if y { 15 | return []byte("yes"), nil 16 | } 17 | return []byte("no"), nil 18 | } 19 | 20 | // clear && go test -v -test.run TestRef_Issue190 ...swagger 21 | func TestRef_Issue190(t *testing.T) { 22 | type User struct { 23 | items []string 24 | } 25 | testJsonFromStruct(t, User{}, `{ 26 | "swagger.User": { 27 | "id": "swagger.User", 28 | "required": [ 29 | "items" 30 | ], 31 | "properties": { 32 | "items": { 33 | "type": "array", 34 | "items": { 35 | "type": "string" 36 | } 37 | } 38 | } 39 | } 40 | }`) 41 | } 42 | 43 | func TestWithoutAdditionalFormat(t *testing.T) { 44 | type mytime struct { 45 | time.Time 46 | } 47 | type usemytime struct { 48 | t mytime 49 | } 50 | testJsonFromStruct(t, usemytime{}, `{ 51 | "swagger.usemytime": { 52 | "id": "swagger.usemytime", 53 | "required": [ 54 | "t" 55 | ], 56 | "properties": { 57 | "t": { 58 | "type": "string" 59 | } 60 | } 61 | } 62 | }`) 63 | } 64 | 65 | func TestWithAdditionalFormat(t *testing.T) { 66 | type mytime struct { 67 | time.Time 68 | } 69 | type usemytime struct { 70 | t mytime 71 | } 72 | testJsonFromStructWithConfig(t, usemytime{}, `{ 73 | "swagger.usemytime": { 74 | "id": "swagger.usemytime", 75 | "required": [ 76 | "t" 77 | ], 78 | "properties": { 79 | "t": { 80 | "type": "string", 81 | "format": "date-time" 82 | } 83 | } 84 | } 85 | }`, &Config{ 86 | SchemaFormatHandler: func(typeName string) string { 87 | switch typeName { 88 | case "swagger.mytime": 89 | return "date-time" 90 | } 91 | return "" 92 | }, 93 | }) 94 | } 95 | 96 | // clear && go test -v -test.run TestCustomMarshaller_Issue96 ...swagger 97 | func TestCustomMarshaller_Issue96(t *testing.T) { 98 | type Vote struct { 99 | What YesNo 100 | } 101 | testJsonFromStruct(t, Vote{}, `{ 102 | "swagger.Vote": { 103 | "id": "swagger.Vote", 104 | "required": [ 105 | "What" 106 | ], 107 | "properties": { 108 | "What": { 109 | "type": "string" 110 | } 111 | } 112 | } 113 | }`) 114 | } 115 | 116 | // clear && go test -v -test.run TestPrimitiveTypes ...swagger 117 | func TestPrimitiveTypes(t *testing.T) { 118 | type Prims struct { 119 | f float64 120 | t time.Time 121 | } 122 | testJsonFromStruct(t, Prims{}, `{ 123 | "swagger.Prims": { 124 | "id": "swagger.Prims", 125 | "required": [ 126 | "f", 127 | "t" 128 | ], 129 | "properties": { 130 | "f": { 131 | "type": "number", 132 | "format": "double" 133 | }, 134 | "t": { 135 | "type": "string", 136 | "format": "date-time" 137 | } 138 | } 139 | } 140 | }`) 141 | } 142 | 143 | // clear && go test -v -test.run TestPrimitivePtrTypes ...swagger 144 | func TestPrimitivePtrTypes(t *testing.T) { 145 | type Prims struct { 146 | f *float64 147 | t *time.Time 148 | b *bool 149 | s *string 150 | i *int 151 | } 152 | testJsonFromStruct(t, Prims{}, `{ 153 | "swagger.Prims": { 154 | "id": "swagger.Prims", 155 | "required": [ 156 | "f", 157 | "t", 158 | "b", 159 | "s", 160 | "i" 161 | ], 162 | "properties": { 163 | "b": { 164 | "type": "boolean" 165 | }, 166 | "f": { 167 | "type": "number", 168 | "format": "double" 169 | }, 170 | "i": { 171 | "type": "integer", 172 | "format": "int32" 173 | }, 174 | "s": { 175 | "type": "string" 176 | }, 177 | "t": { 178 | "type": "string", 179 | "format": "date-time" 180 | } 181 | } 182 | } 183 | }`) 184 | } 185 | 186 | // clear && go test -v -test.run TestS1 ...swagger 187 | func TestS1(t *testing.T) { 188 | type S1 struct { 189 | Id string 190 | } 191 | testJsonFromStruct(t, S1{}, `{ 192 | "swagger.S1": { 193 | "id": "swagger.S1", 194 | "required": [ 195 | "Id" 196 | ], 197 | "properties": { 198 | "Id": { 199 | "type": "string" 200 | } 201 | } 202 | } 203 | }`) 204 | } 205 | 206 | // clear && go test -v -test.run TestS2 ...swagger 207 | func TestS2(t *testing.T) { 208 | type S2 struct { 209 | Ids []string 210 | } 211 | testJsonFromStruct(t, S2{}, `{ 212 | "swagger.S2": { 213 | "id": "swagger.S2", 214 | "required": [ 215 | "Ids" 216 | ], 217 | "properties": { 218 | "Ids": { 219 | "type": "array", 220 | "items": { 221 | "type": "string" 222 | } 223 | } 224 | } 225 | } 226 | }`) 227 | } 228 | 229 | // clear && go test -v -test.run TestS3 ...swagger 230 | func TestS3(t *testing.T) { 231 | type NestedS3 struct { 232 | Id string 233 | } 234 | type S3 struct { 235 | Nested NestedS3 236 | } 237 | testJsonFromStruct(t, S3{}, `{ 238 | "swagger.NestedS3": { 239 | "id": "swagger.NestedS3", 240 | "required": [ 241 | "Id" 242 | ], 243 | "properties": { 244 | "Id": { 245 | "type": "string" 246 | } 247 | } 248 | }, 249 | "swagger.S3": { 250 | "id": "swagger.S3", 251 | "required": [ 252 | "Nested" 253 | ], 254 | "properties": { 255 | "Nested": { 256 | "$ref": "swagger.NestedS3" 257 | } 258 | } 259 | } 260 | }`) 261 | } 262 | 263 | type sample struct { 264 | id string `swagger:"required"` // TODO 265 | items []item 266 | rootItem item `json:"root" description:"root desc"` 267 | } 268 | 269 | type item struct { 270 | itemName string `json:"name"` 271 | } 272 | 273 | // clear && go test -v -test.run TestSampleToModelAsJson ...swagger 274 | func TestSampleToModelAsJson(t *testing.T) { 275 | testJsonFromStruct(t, sample{items: []item{}}, `{ 276 | "swagger.item": { 277 | "id": "swagger.item", 278 | "required": [ 279 | "name" 280 | ], 281 | "properties": { 282 | "name": { 283 | "type": "string" 284 | } 285 | } 286 | }, 287 | "swagger.sample": { 288 | "id": "swagger.sample", 289 | "required": [ 290 | "id", 291 | "items", 292 | "root" 293 | ], 294 | "properties": { 295 | "id": { 296 | "type": "string" 297 | }, 298 | "items": { 299 | "type": "array", 300 | "items": { 301 | "$ref": "swagger.item" 302 | } 303 | }, 304 | "root": { 305 | "$ref": "swagger.item", 306 | "description": "root desc" 307 | } 308 | } 309 | } 310 | }`) 311 | } 312 | 313 | func TestJsonTags(t *testing.T) { 314 | type X struct { 315 | A string 316 | B string `json:"-"` 317 | C int `json:",string"` 318 | D int `json:","` 319 | } 320 | 321 | expected := `{ 322 | "swagger.X": { 323 | "id": "swagger.X", 324 | "required": [ 325 | "A", 326 | "C", 327 | "D" 328 | ], 329 | "properties": { 330 | "A": { 331 | "type": "string" 332 | }, 333 | "C": { 334 | "type": "string" 335 | }, 336 | "D": { 337 | "type": "integer", 338 | "format": "int32" 339 | } 340 | } 341 | } 342 | }` 343 | 344 | testJsonFromStruct(t, X{}, expected) 345 | } 346 | 347 | func TestJsonTagOmitempty(t *testing.T) { 348 | type X struct { 349 | A int `json:",omitempty"` 350 | B int `json:"C,omitempty"` 351 | } 352 | 353 | expected := `{ 354 | "swagger.X": { 355 | "id": "swagger.X", 356 | "properties": { 357 | "A": { 358 | "type": "integer", 359 | "format": "int32" 360 | }, 361 | "C": { 362 | "type": "integer", 363 | "format": "int32" 364 | } 365 | } 366 | } 367 | }` 368 | 369 | testJsonFromStruct(t, X{}, expected) 370 | } 371 | 372 | func TestJsonTagName(t *testing.T) { 373 | type X struct { 374 | A string `json:"B"` 375 | } 376 | 377 | expected := `{ 378 | "swagger.X": { 379 | "id": "swagger.X", 380 | "required": [ 381 | "B" 382 | ], 383 | "properties": { 384 | "B": { 385 | "type": "string" 386 | } 387 | } 388 | } 389 | }` 390 | 391 | testJsonFromStruct(t, X{}, expected) 392 | } 393 | 394 | func TestAnonymousStruct(t *testing.T) { 395 | type X struct { 396 | A struct { 397 | B int 398 | } 399 | } 400 | 401 | expected := `{ 402 | "swagger.X": { 403 | "id": "swagger.X", 404 | "required": [ 405 | "A" 406 | ], 407 | "properties": { 408 | "A": { 409 | "$ref": "swagger.X.A" 410 | } 411 | } 412 | }, 413 | "swagger.X.A": { 414 | "id": "swagger.X.A", 415 | "required": [ 416 | "B" 417 | ], 418 | "properties": { 419 | "B": { 420 | "type": "integer", 421 | "format": "int32" 422 | } 423 | } 424 | } 425 | }` 426 | 427 | testJsonFromStruct(t, X{}, expected) 428 | } 429 | 430 | func TestAnonymousPtrStruct(t *testing.T) { 431 | type X struct { 432 | A *struct { 433 | B int 434 | } 435 | } 436 | 437 | expected := `{ 438 | "swagger.X": { 439 | "id": "swagger.X", 440 | "required": [ 441 | "A" 442 | ], 443 | "properties": { 444 | "A": { 445 | "$ref": "swagger.X.A" 446 | } 447 | } 448 | }, 449 | "swagger.X.A": { 450 | "id": "swagger.X.A", 451 | "required": [ 452 | "B" 453 | ], 454 | "properties": { 455 | "B": { 456 | "type": "integer", 457 | "format": "int32" 458 | } 459 | } 460 | } 461 | }` 462 | 463 | testJsonFromStruct(t, X{}, expected) 464 | } 465 | 466 | func TestAnonymousArrayStruct(t *testing.T) { 467 | type X struct { 468 | A []struct { 469 | B int 470 | } 471 | } 472 | 473 | expected := `{ 474 | "swagger.X": { 475 | "id": "swagger.X", 476 | "required": [ 477 | "A" 478 | ], 479 | "properties": { 480 | "A": { 481 | "type": "array", 482 | "items": { 483 | "$ref": "swagger.X.A" 484 | } 485 | } 486 | } 487 | }, 488 | "swagger.X.A": { 489 | "id": "swagger.X.A", 490 | "required": [ 491 | "B" 492 | ], 493 | "properties": { 494 | "B": { 495 | "type": "integer", 496 | "format": "int32" 497 | } 498 | } 499 | } 500 | }` 501 | 502 | testJsonFromStruct(t, X{}, expected) 503 | } 504 | 505 | func TestAnonymousPtrArrayStruct(t *testing.T) { 506 | type X struct { 507 | A *[]struct { 508 | B int 509 | } 510 | } 511 | 512 | expected := `{ 513 | "swagger.X": { 514 | "id": "swagger.X", 515 | "required": [ 516 | "A" 517 | ], 518 | "properties": { 519 | "A": { 520 | "type": "array", 521 | "items": { 522 | "$ref": "swagger.X.A" 523 | } 524 | } 525 | } 526 | }, 527 | "swagger.X.A": { 528 | "id": "swagger.X.A", 529 | "required": [ 530 | "B" 531 | ], 532 | "properties": { 533 | "B": { 534 | "type": "integer", 535 | "format": "int32" 536 | } 537 | } 538 | } 539 | }` 540 | 541 | testJsonFromStruct(t, X{}, expected) 542 | } 543 | 544 | // go test -v -test.run TestEmbeddedStruct_Issue98 ...swagger 545 | func TestEmbeddedStruct_Issue98(t *testing.T) { 546 | type Y struct { 547 | A int 548 | } 549 | type X struct { 550 | Y 551 | } 552 | testJsonFromStruct(t, X{}, `{ 553 | "swagger.X": { 554 | "id": "swagger.X", 555 | "required": [ 556 | "A" 557 | ], 558 | "properties": { 559 | "A": { 560 | "type": "integer", 561 | "format": "int32" 562 | } 563 | } 564 | } 565 | }`) 566 | } 567 | 568 | type Dataset struct { 569 | Names []string 570 | } 571 | 572 | // clear && go test -v -test.run TestIssue85 ...swagger 573 | func TestIssue85(t *testing.T) { 574 | anon := struct{ Datasets []Dataset }{} 575 | testJsonFromStruct(t, anon, `{ 576 | "struct { Datasets ||swagger.Dataset }": { 577 | "id": "struct { Datasets ||swagger.Dataset }", 578 | "required": [ 579 | "Datasets" 580 | ], 581 | "properties": { 582 | "Datasets": { 583 | "type": "array", 584 | "items": { 585 | "$ref": "swagger.Dataset" 586 | } 587 | } 588 | } 589 | }, 590 | "swagger.Dataset": { 591 | "id": "swagger.Dataset", 592 | "required": [ 593 | "Names" 594 | ], 595 | "properties": { 596 | "Names": { 597 | "type": "array", 598 | "items": { 599 | "type": "string" 600 | } 601 | } 602 | } 603 | } 604 | }`) 605 | } 606 | 607 | type File struct { 608 | History []File 609 | HistoryPtrs []*File 610 | } 611 | 612 | // go test -v -test.run TestRecursiveStructure ...swagger 613 | func TestRecursiveStructure(t *testing.T) { 614 | testJsonFromStruct(t, File{}, `{ 615 | "swagger.File": { 616 | "id": "swagger.File", 617 | "required": [ 618 | "History", 619 | "HistoryPtrs" 620 | ], 621 | "properties": { 622 | "History": { 623 | "type": "array", 624 | "items": { 625 | "$ref": "swagger.File" 626 | } 627 | }, 628 | "HistoryPtrs": { 629 | "type": "array", 630 | "items": { 631 | "$ref": "swagger.File" 632 | } 633 | } 634 | } 635 | } 636 | }`) 637 | } 638 | 639 | type A1 struct { 640 | B struct { 641 | Id int 642 | Comment string `json:"comment,omitempty"` 643 | } 644 | } 645 | 646 | // go test -v -test.run TestEmbeddedStructA1 ...swagger 647 | func TestEmbeddedStructA1(t *testing.T) { 648 | testJsonFromStruct(t, A1{}, `{ 649 | "swagger.A1": { 650 | "id": "swagger.A1", 651 | "required": [ 652 | "B" 653 | ], 654 | "properties": { 655 | "B": { 656 | "$ref": "swagger.A1.B" 657 | } 658 | } 659 | }, 660 | "swagger.A1.B": { 661 | "id": "swagger.A1.B", 662 | "required": [ 663 | "Id" 664 | ], 665 | "properties": { 666 | "Id": { 667 | "type": "integer", 668 | "format": "int32" 669 | }, 670 | "comment": { 671 | "type": "string" 672 | } 673 | } 674 | } 675 | }`) 676 | } 677 | 678 | type A2 struct { 679 | C 680 | } 681 | type C struct { 682 | Id int `json:"B"` 683 | Comment string `json:"comment,omitempty"` 684 | Secure bool `json:"secure"` 685 | } 686 | 687 | // go test -v -test.run TestEmbeddedStructA2 ...swagger 688 | func TestEmbeddedStructA2(t *testing.T) { 689 | testJsonFromStruct(t, A2{}, `{ 690 | "swagger.A2": { 691 | "id": "swagger.A2", 692 | "required": [ 693 | "B", 694 | "secure" 695 | ], 696 | "properties": { 697 | "B": { 698 | "type": "integer", 699 | "format": "int32" 700 | }, 701 | "comment": { 702 | "type": "string" 703 | }, 704 | "secure": { 705 | "type": "boolean" 706 | } 707 | } 708 | } 709 | }`) 710 | } 711 | 712 | type A3 struct { 713 | B D 714 | } 715 | 716 | type D struct { 717 | Id int 718 | } 719 | 720 | // clear && go test -v -test.run TestStructA3 ...swagger 721 | func TestStructA3(t *testing.T) { 722 | testJsonFromStruct(t, A3{}, `{ 723 | "swagger.A3": { 724 | "id": "swagger.A3", 725 | "required": [ 726 | "B" 727 | ], 728 | "properties": { 729 | "B": { 730 | "$ref": "swagger.D" 731 | } 732 | } 733 | }, 734 | "swagger.D": { 735 | "id": "swagger.D", 736 | "required": [ 737 | "Id" 738 | ], 739 | "properties": { 740 | "Id": { 741 | "type": "integer", 742 | "format": "int32" 743 | } 744 | } 745 | } 746 | }`) 747 | } 748 | 749 | type A4 struct { 750 | D "json:,inline" 751 | } 752 | 753 | // clear && go test -v -test.run TestStructA4 ...swagger 754 | func TestEmbeddedStructA4(t *testing.T) { 755 | testJsonFromStruct(t, A4{}, `{ 756 | "swagger.A4": { 757 | "id": "swagger.A4", 758 | "required": [ 759 | "Id" 760 | ], 761 | "properties": { 762 | "Id": { 763 | "type": "integer", 764 | "format": "int32" 765 | } 766 | } 767 | } 768 | }`) 769 | } 770 | 771 | type A5 struct { 772 | D `json:"d"` 773 | } 774 | 775 | // clear && go test -v -test.run TestStructA5 ...swagger 776 | func TestEmbeddedStructA5(t *testing.T) { 777 | testJsonFromStruct(t, A5{}, `{ 778 | "swagger.A5": { 779 | "id": "swagger.A5", 780 | "required": [ 781 | "d" 782 | ], 783 | "properties": { 784 | "d": { 785 | "$ref": "swagger.D" 786 | } 787 | } 788 | }, 789 | "swagger.D": { 790 | "id": "swagger.D", 791 | "required": [ 792 | "Id" 793 | ], 794 | "properties": { 795 | "Id": { 796 | "type": "integer", 797 | "format": "int32" 798 | } 799 | } 800 | } 801 | }`) 802 | } 803 | 804 | type D2 struct { 805 | id int 806 | D []D 807 | } 808 | 809 | type A6 struct { 810 | D2 "json:,inline" 811 | } 812 | 813 | // clear && go test -v -test.run TestStructA4 ...swagger 814 | func TestEmbeddedStructA6(t *testing.T) { 815 | testJsonFromStruct(t, A6{}, `{ 816 | "swagger.A6": { 817 | "id": "swagger.A6", 818 | "required": [ 819 | "id", 820 | "D" 821 | ], 822 | "properties": { 823 | "D": { 824 | "type": "array", 825 | "items": { 826 | "$ref": "swagger.D" 827 | } 828 | }, 829 | "id": { 830 | "type": "integer", 831 | "format": "int32" 832 | } 833 | } 834 | }, 835 | "swagger.D": { 836 | "id": "swagger.D", 837 | "required": [ 838 | "Id" 839 | ], 840 | "properties": { 841 | "Id": { 842 | "type": "integer", 843 | "format": "int32" 844 | } 845 | } 846 | } 847 | }`) 848 | } 849 | 850 | type ObjectId []byte 851 | 852 | type Region struct { 853 | Id ObjectId `bson:"_id" json:"id"` 854 | Name string `bson:"name" json:"name"` 855 | Type string `bson:"type" json:"type"` 856 | } 857 | 858 | // clear && go test -v -test.run TestRegion_Issue113 ...swagger 859 | func TestRegion_Issue113(t *testing.T) { 860 | testJsonFromStruct(t, []Region{}, `{ 861 | "||swagger.Region": { 862 | "id": "||swagger.Region", 863 | "properties": {} 864 | }, 865 | "swagger.Region": { 866 | "id": "swagger.Region", 867 | "required": [ 868 | "id", 869 | "name", 870 | "type" 871 | ], 872 | "properties": { 873 | "id": { 874 | "type": "string" 875 | }, 876 | "name": { 877 | "type": "string" 878 | }, 879 | "type": { 880 | "type": "string" 881 | } 882 | } 883 | } 884 | }`) 885 | } 886 | 887 | // clear && go test -v -test.run TestIssue158 ...swagger 888 | func TestIssue158(t *testing.T) { 889 | type Address struct { 890 | Country string `json:"country,omitempty"` 891 | } 892 | 893 | type Customer struct { 894 | Name string `json:"name"` 895 | Address Address `json:"address"` 896 | } 897 | expected := `{ 898 | "swagger.Address": { 899 | "id": "swagger.Address", 900 | "properties": { 901 | "country": { 902 | "type": "string" 903 | } 904 | } 905 | }, 906 | "swagger.Customer": { 907 | "id": "swagger.Customer", 908 | "required": [ 909 | "name", 910 | "address" 911 | ], 912 | "properties": { 913 | "address": { 914 | "$ref": "swagger.Address" 915 | }, 916 | "name": { 917 | "type": "string" 918 | } 919 | } 920 | } 921 | }` 922 | testJsonFromStruct(t, Customer{}, expected) 923 | } 924 | 925 | func TestPointers(t *testing.T) { 926 | type Vote struct { 927 | What YesNo 928 | } 929 | testJsonFromStruct(t, &Vote{}, `{ 930 | "swagger.Vote": { 931 | "id": "swagger.Vote", 932 | "required": [ 933 | "What" 934 | ], 935 | "properties": { 936 | "What": { 937 | "type": "string" 938 | } 939 | } 940 | } 941 | }`) 942 | } 943 | 944 | func TestSlices(t *testing.T) { 945 | type Address struct { 946 | Country string `json:"country,omitempty"` 947 | } 948 | expected := `{ 949 | "swagger.Address": { 950 | "id": "swagger.Address", 951 | "properties": { 952 | "country": { 953 | "type": "string" 954 | } 955 | } 956 | }, 957 | "swagger.Customer": { 958 | "id": "swagger.Customer", 959 | "required": [ 960 | "name", 961 | "addresses" 962 | ], 963 | "properties": { 964 | "addresses": { 965 | "type": "array", 966 | "items": { 967 | "$ref": "swagger.Address" 968 | } 969 | }, 970 | "name": { 971 | "type": "string" 972 | } 973 | } 974 | } 975 | }` 976 | // both slices (with pointer value and with type value) should have equal swagger representation 977 | { 978 | type Customer struct { 979 | Name string `json:"name"` 980 | Addresses []Address `json:"addresses"` 981 | } 982 | testJsonFromStruct(t, Customer{}, expected) 983 | } 984 | { 985 | type Customer struct { 986 | Name string `json:"name"` 987 | Addresses []*Address `json:"addresses"` 988 | } 989 | testJsonFromStruct(t, Customer{}, expected) 990 | } 991 | 992 | } 993 | 994 | type Name struct { 995 | Value string 996 | } 997 | 998 | func (n Name) PostBuildModel(m *Model) *Model { 999 | m.Description = "titles must be upcase" 1000 | return m 1001 | } 1002 | 1003 | type TOC struct { 1004 | Titles []Name 1005 | } 1006 | 1007 | type Discography struct { 1008 | Title Name 1009 | TOC 1010 | } 1011 | 1012 | // clear && go test -v -test.run TestEmbeddedStructPull204 ...swagger 1013 | func TestEmbeddedStructPull204(t *testing.T) { 1014 | b := Discography{} 1015 | testJsonFromStruct(t, b, ` 1016 | { 1017 | "swagger.Discography": { 1018 | "id": "swagger.Discography", 1019 | "required": [ 1020 | "Title", 1021 | "Titles" 1022 | ], 1023 | "properties": { 1024 | "Title": { 1025 | "$ref": "swagger.Name" 1026 | }, 1027 | "Titles": { 1028 | "type": "array", 1029 | "items": { 1030 | "$ref": "swagger.Name" 1031 | } 1032 | } 1033 | } 1034 | }, 1035 | "swagger.Name": { 1036 | "id": "swagger.Name", 1037 | "required": [ 1038 | "Value" 1039 | ], 1040 | "properties": { 1041 | "Value": { 1042 | "type": "string" 1043 | } 1044 | } 1045 | } 1046 | } 1047 | `) 1048 | } 1049 | 1050 | type AddressWithMethod struct { 1051 | Country string `json:"country,omitempty"` 1052 | PostCode int `json:"postcode,omitempty"` 1053 | } 1054 | 1055 | func (AddressWithMethod) SwaggerDoc() map[string]string { 1056 | return map[string]string{ 1057 | "": "Address doc", 1058 | "country": "Country doc", 1059 | "postcode": "PostCode doc", 1060 | } 1061 | } 1062 | 1063 | func TestDocInMethodSwaggerDoc(t *testing.T) { 1064 | expected := `{ 1065 | "swagger.AddressWithMethod": { 1066 | "id": "swagger.AddressWithMethod", 1067 | "description": "Address doc", 1068 | "properties": { 1069 | "country": { 1070 | "type": "string", 1071 | "description": "Country doc" 1072 | }, 1073 | "postcode": { 1074 | "type": "integer", 1075 | "format": "int32", 1076 | "description": "PostCode doc" 1077 | } 1078 | } 1079 | } 1080 | }` 1081 | testJsonFromStruct(t, AddressWithMethod{}, expected) 1082 | } 1083 | 1084 | type RefDesc struct { 1085 | f1 *int64 `description:"desc"` 1086 | } 1087 | 1088 | func TestPtrDescription(t *testing.T) { 1089 | b := RefDesc{} 1090 | expected := `{ 1091 | "swagger.RefDesc": { 1092 | "id": "swagger.RefDesc", 1093 | "required": [ 1094 | "f1" 1095 | ], 1096 | "properties": { 1097 | "f1": { 1098 | "type": "integer", 1099 | "format": "int64", 1100 | "description": "desc" 1101 | } 1102 | } 1103 | } 1104 | }` 1105 | testJsonFromStruct(t, b, expected) 1106 | } 1107 | 1108 | type A struct { 1109 | B `json:",inline"` 1110 | C1 `json:"metadata,omitempty"` 1111 | } 1112 | 1113 | type B struct { 1114 | SB string 1115 | } 1116 | 1117 | type C1 struct { 1118 | SC string 1119 | } 1120 | 1121 | func (A) SwaggerDoc() map[string]string { 1122 | return map[string]string{ 1123 | "": "A struct", 1124 | "B": "B field", // We should not get anything from this 1125 | "metadata": "C1 field", 1126 | } 1127 | } 1128 | 1129 | func (B) SwaggerDoc() map[string]string { 1130 | return map[string]string{ 1131 | "": "B struct", 1132 | "SB": "SB field", 1133 | } 1134 | } 1135 | 1136 | func (C1) SwaggerDoc() map[string]string { 1137 | return map[string]string{ 1138 | "": "C1 struct", 1139 | "SC": "SC field", 1140 | } 1141 | } 1142 | 1143 | func TestNestedStructDescription(t *testing.T) { 1144 | expected := ` 1145 | { 1146 | "swagger.A": { 1147 | "id": "swagger.A", 1148 | "description": "A struct", 1149 | "required": [ 1150 | "SB" 1151 | ], 1152 | "properties": { 1153 | "SB": { 1154 | "type": "string", 1155 | "description": "SB field" 1156 | }, 1157 | "metadata": { 1158 | "$ref": "swagger.C1", 1159 | "description": "C1 field" 1160 | } 1161 | } 1162 | }, 1163 | "swagger.C1": { 1164 | "id": "swagger.C1", 1165 | "description": "C1 struct", 1166 | "required": [ 1167 | "SC" 1168 | ], 1169 | "properties": { 1170 | "SC": { 1171 | "type": "string", 1172 | "description": "SC field" 1173 | } 1174 | } 1175 | } 1176 | } 1177 | ` 1178 | testJsonFromStruct(t, A{}, expected) 1179 | } 1180 | 1181 | // This tests a primitive with type overrides in the struct tags 1182 | type FakeInt int 1183 | type E struct { 1184 | Id FakeInt `type:"integer"` 1185 | IP net.IP `type:"string"` 1186 | } 1187 | 1188 | func TestOverridenTypeTagE1(t *testing.T) { 1189 | expected := ` 1190 | { 1191 | "swagger.E": { 1192 | "id": "swagger.E", 1193 | "required": [ 1194 | "Id", 1195 | "IP" 1196 | ], 1197 | "properties": { 1198 | "Id": { 1199 | "type": "integer" 1200 | }, 1201 | "IP": { 1202 | "type": "string" 1203 | } 1204 | } 1205 | } 1206 | } 1207 | ` 1208 | testJsonFromStruct(t, E{}, expected) 1209 | } 1210 | 1211 | type XmlNamed struct { 1212 | XMLName xml.Name `xml:"user"` 1213 | Id string `json:"id" xml:"id"` 1214 | Name string `json:"name" xml:"name"` 1215 | } 1216 | 1217 | func TestXmlNameStructs(t *testing.T) { 1218 | expected := ` 1219 | { 1220 | "swagger.XmlNamed": { 1221 | "id": "swagger.XmlNamed", 1222 | "required": [ 1223 | "id", 1224 | "name" 1225 | ], 1226 | "properties": { 1227 | "id": { 1228 | "type": "string" 1229 | }, 1230 | "name": { 1231 | "type": "string" 1232 | } 1233 | } 1234 | } 1235 | } 1236 | ` 1237 | testJsonFromStruct(t, XmlNamed{}, expected) 1238 | } 1239 | 1240 | func TestNameCustomization(t *testing.T) { 1241 | expected := ` 1242 | { 1243 | "swagger.A": { 1244 | "id": "swagger.A", 1245 | "description": "A struct", 1246 | "required": [ 1247 | "SB" 1248 | ], 1249 | "properties": { 1250 | "SB": { 1251 | "type": "string", 1252 | "description": "SB field" 1253 | }, 1254 | "metadata": { 1255 | "$ref": "new.swagger.SpecialC1", 1256 | "description": "C1 field" 1257 | } 1258 | } 1259 | }, 1260 | "new.swagger.SpecialC1": { 1261 | "id": "new.swagger.SpecialC1", 1262 | "description": "C1 struct", 1263 | "required": [ 1264 | "SC" 1265 | ], 1266 | "properties": { 1267 | "SC": { 1268 | "type": "string", 1269 | "description": "SC field" 1270 | } 1271 | } 1272 | } 1273 | }` 1274 | 1275 | testJsonFromStructWithConfig(t, A{}, expected, &Config{ 1276 | ModelTypeNameHandler: func(t reflect.Type) (string, bool) { 1277 | if t == reflect.TypeOf(C1{}) { 1278 | return "new.swagger.SpecialC1", true 1279 | } 1280 | return "", false 1281 | }, 1282 | }) 1283 | } 1284 | -------------------------------------------------------------------------------- /model_list.go: -------------------------------------------------------------------------------- 1 | package swagger 2 | 3 | // Copyright 2015 Ernest Micklei. All rights reserved. 4 | // Use of this source code is governed by a license 5 | // that can be found in the LICENSE file. 6 | 7 | import ( 8 | "bytes" 9 | "encoding/json" 10 | ) 11 | 12 | // NamedModel associates a name with a Model (not using its Id) 13 | type NamedModel struct { 14 | Name string 15 | Model Model 16 | } 17 | 18 | // ModelList encapsulates a list of NamedModel (association) 19 | type ModelList struct { 20 | List []NamedModel 21 | } 22 | 23 | // Put adds or replaces a Model by its name 24 | func (l *ModelList) Put(name string, model Model) { 25 | for i, each := range l.List { 26 | if each.Name == name { 27 | // replace 28 | l.List[i] = NamedModel{name, model} 29 | return 30 | } 31 | } 32 | // add 33 | l.List = append(l.List, NamedModel{name, model}) 34 | } 35 | 36 | // At returns a Model by its name, ok is false if absent 37 | func (l *ModelList) At(name string) (m Model, ok bool) { 38 | for _, each := range l.List { 39 | if each.Name == name { 40 | return each.Model, true 41 | } 42 | } 43 | return m, false 44 | } 45 | 46 | // Do enumerates all the models, each with its assigned name 47 | func (l *ModelList) Do(block func(name string, value Model)) { 48 | for _, each := range l.List { 49 | block(each.Name, each.Model) 50 | } 51 | } 52 | 53 | // MarshalJSON writes the ModelList as if it was a map[string]Model 54 | func (l ModelList) MarshalJSON() ([]byte, error) { 55 | var buf bytes.Buffer 56 | encoder := json.NewEncoder(&buf) 57 | buf.WriteString("{\n") 58 | for i, each := range l.List { 59 | buf.WriteString("\"") 60 | buf.WriteString(each.Name) 61 | buf.WriteString("\": ") 62 | encoder.Encode(each.Model) 63 | if i < len(l.List)-1 { 64 | buf.WriteString(",\n") 65 | } 66 | } 67 | buf.WriteString("}") 68 | return buf.Bytes(), nil 69 | } 70 | 71 | // UnmarshalJSON reads back a ModelList. This is an expensive operation. 72 | func (l *ModelList) UnmarshalJSON(data []byte) error { 73 | raw := map[string]interface{}{} 74 | json.NewDecoder(bytes.NewReader(data)).Decode(&raw) 75 | for k, v := range raw { 76 | // produces JSON bytes for each value 77 | data, err := json.Marshal(v) 78 | if err != nil { 79 | return err 80 | } 81 | var m Model 82 | json.NewDecoder(bytes.NewReader(data)).Decode(&m) 83 | l.Put(k, m) 84 | } 85 | return nil 86 | } 87 | -------------------------------------------------------------------------------- /model_list_test.go: -------------------------------------------------------------------------------- 1 | package swagger 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | ) 7 | 8 | func TestModelList(t *testing.T) { 9 | m := Model{} 10 | m.Id = "m" 11 | l := ModelList{} 12 | l.Put("m", m) 13 | k, ok := l.At("m") 14 | if !ok { 15 | t.Error("want model back") 16 | } 17 | if got, want := k.Id, "m"; got != want { 18 | t.Errorf("got %v want %v", got, want) 19 | } 20 | } 21 | 22 | func TestModelList_Marshal(t *testing.T) { 23 | l := ModelList{} 24 | m := Model{Id: "myid"} 25 | l.Put("myid", m) 26 | data, err := json.Marshal(l) 27 | if err != nil { 28 | t.Error(err) 29 | } 30 | if got, want := string(data), `{"myid":{"id":"myid","properties":{}}}`; got != want { 31 | t.Errorf("got %v want %v", got, want) 32 | } 33 | } 34 | 35 | func TestModelList_Unmarshal(t *testing.T) { 36 | data := `{"myid":{"id":"myid","properties":{}}}` 37 | l := ModelList{} 38 | if err := json.Unmarshal([]byte(data), &l); err != nil { 39 | t.Error(err) 40 | } 41 | m, ok := l.At("myid") 42 | if !ok { 43 | t.Error("expected myid") 44 | } 45 | if got, want := m.Id, "myid"; got != want { 46 | t.Errorf("got %v want %v", got, want) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /model_property_ext.go: -------------------------------------------------------------------------------- 1 | package swagger 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | ) 7 | 8 | func (prop *ModelProperty) setDescription(field reflect.StructField) { 9 | if tag := field.Tag.Get("description"); tag != "" { 10 | prop.Description = tag 11 | } 12 | } 13 | 14 | func (prop *ModelProperty) setDefaultValue(field reflect.StructField) { 15 | if tag := field.Tag.Get("default"); tag != "" { 16 | prop.DefaultValue = Special(tag) 17 | } 18 | } 19 | 20 | func (prop *ModelProperty) setEnumValues(field reflect.StructField) { 21 | // We use | to separate the enum values. This value is chosen 22 | // since its unlikely to be useful in actual enumeration values. 23 | if tag := field.Tag.Get("enum"); tag != "" { 24 | prop.Enum = strings.Split(tag, "|") 25 | } 26 | } 27 | 28 | func (prop *ModelProperty) setMaximum(field reflect.StructField) { 29 | if tag := field.Tag.Get("maximum"); tag != "" { 30 | prop.Maximum = tag 31 | } 32 | } 33 | 34 | func (prop *ModelProperty) setType(field reflect.StructField) { 35 | if tag := field.Tag.Get("type"); tag != "" { 36 | // Check if the first two characters of the type tag are 37 | // intended to emulate slice/array behaviour. 38 | // 39 | // If type is intended to be a slice/array then add the 40 | // overriden type to the array item instead of the main property 41 | if len(tag) > 2 && tag[0:2] == "[]" { 42 | pType := "array" 43 | prop.Type = &pType 44 | prop.Items = new(Item) 45 | 46 | iType := tag[2:] 47 | prop.Items.Type = &iType 48 | return 49 | } 50 | 51 | prop.Type = &tag 52 | } 53 | } 54 | 55 | func (prop *ModelProperty) setFormat(field reflect.StructField) { 56 | if tag := field.Tag.Get("format"); tag != "" { 57 | prop.Format = tag 58 | } 59 | } 60 | 61 | func (prop *ModelProperty) setMinimum(field reflect.StructField) { 62 | if tag := field.Tag.Get("minimum"); tag != "" { 63 | prop.Minimum = tag 64 | } 65 | } 66 | 67 | func (prop *ModelProperty) setUniqueItems(field reflect.StructField) { 68 | tag := field.Tag.Get("unique") 69 | switch tag { 70 | case "true": 71 | v := true 72 | prop.UniqueItems = &v 73 | case "false": 74 | v := false 75 | prop.UniqueItems = &v 76 | } 77 | } 78 | 79 | func (prop *ModelProperty) setPropertyMetadata(field reflect.StructField) { 80 | prop.setDescription(field) 81 | prop.setEnumValues(field) 82 | prop.setMinimum(field) 83 | prop.setMaximum(field) 84 | prop.setUniqueItems(field) 85 | prop.setDefaultValue(field) 86 | prop.setType(field) 87 | prop.setFormat(field) 88 | } 89 | -------------------------------------------------------------------------------- /model_property_ext_test.go: -------------------------------------------------------------------------------- 1 | package swagger 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | ) 7 | 8 | // clear && go test -v -test.run TestThatExtraTagsAreReadIntoModel ...swagger 9 | func TestThatExtraTagsAreReadIntoModel(t *testing.T) { 10 | type fakeint int 11 | type fakearray string 12 | type Anything struct { 13 | Name string `description:"name" modelDescription:"a test"` 14 | Size int `minimum:"0" maximum:"10"` 15 | Stati string `enum:"off|on" default:"on" modelDescription:"more description"` 16 | ID string `unique:"true"` 17 | FakeInt fakeint `type:"integer"` 18 | FakeArray fakearray `type:"[]string"` 19 | IP net.IP `type:"string"` 20 | Custom string `type:"string" format:"custom"` 21 | Password string 22 | } 23 | m := modelsFromStruct(Anything{}) 24 | props, _ := m.At("swagger.Anything") 25 | p1, _ := props.Properties.At("Name") 26 | if got, want := p1.Description, "name"; got != want { 27 | t.Errorf("got %v want %v", got, want) 28 | } 29 | p2, _ := props.Properties.At("Size") 30 | if got, want := p2.Minimum, "0"; got != want { 31 | t.Errorf("got %v want %v", got, want) 32 | } 33 | if got, want := p2.Maximum, "10"; got != want { 34 | t.Errorf("got %v want %v", got, want) 35 | } 36 | p3, _ := props.Properties.At("Stati") 37 | if got, want := p3.Enum[0], "off"; got != want { 38 | t.Errorf("got %v want %v", got, want) 39 | } 40 | if got, want := p3.Enum[1], "on"; got != want { 41 | t.Errorf("got %v want %v", got, want) 42 | } 43 | p4, _ := props.Properties.At("ID") 44 | if got, want := *p4.UniqueItems, true; got != want { 45 | t.Errorf("got %v want %v", got, want) 46 | } 47 | p5, _ := props.Properties.At("Password") 48 | if got, want := *p5.Type, "string"; got != want { 49 | t.Errorf("got %v want %v", got, want) 50 | } 51 | p6, _ := props.Properties.At("FakeInt") 52 | if got, want := *p6.Type, "integer"; got != want { 53 | t.Errorf("got %v want %v", got, want) 54 | } 55 | p7, _ := props.Properties.At("FakeArray") 56 | if got, want := *p7.Type, "array"; got != want { 57 | t.Errorf("got %v want %v", got, want) 58 | } 59 | p7p, _ := props.Properties.At("FakeArray") 60 | if got, want := *p7p.Items.Type, "string"; got != want { 61 | t.Errorf("got %v want %v", got, want) 62 | } 63 | p8, _ := props.Properties.At("IP") 64 | if got, want := *p8.Type, "string"; got != want { 65 | t.Errorf("got %v want %v", got, want) 66 | } 67 | p9, _ := props.Properties.At("Custom") 68 | if got, want := *p9.Type, "string"; got != want { 69 | t.Errorf("got %v want %v", got, want) 70 | } 71 | if got, want := p9.Format, "custom"; got != want { 72 | t.Errorf("got %v want %v", got, want) 73 | } 74 | 75 | if got, want := props.Description, "a test\nmore description"; got != want { 76 | t.Errorf("got %v want %v", got, want) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /model_property_list.go: -------------------------------------------------------------------------------- 1 | package swagger 2 | 3 | // Copyright 2015 Ernest Micklei. All rights reserved. 4 | // Use of this source code is governed by a license 5 | // that can be found in the LICENSE file. 6 | 7 | import ( 8 | "bytes" 9 | "encoding/json" 10 | ) 11 | 12 | // NamedModelProperty associates a name to a ModelProperty 13 | type NamedModelProperty struct { 14 | Name string 15 | Property ModelProperty 16 | } 17 | 18 | // ModelPropertyList encapsulates a list of NamedModelProperty (association) 19 | type ModelPropertyList struct { 20 | List []NamedModelProperty 21 | } 22 | 23 | // At returns the ModelPropety by its name unless absent, then ok is false 24 | func (l *ModelPropertyList) At(name string) (p ModelProperty, ok bool) { 25 | for _, each := range l.List { 26 | if each.Name == name { 27 | return each.Property, true 28 | } 29 | } 30 | return p, false 31 | } 32 | 33 | // Put adds or replaces a ModelProperty with this name 34 | func (l *ModelPropertyList) Put(name string, prop ModelProperty) { 35 | // maybe replace existing 36 | for i, each := range l.List { 37 | if each.Name == name { 38 | // replace 39 | l.List[i] = NamedModelProperty{Name: name, Property: prop} 40 | return 41 | } 42 | } 43 | // add 44 | l.List = append(l.List, NamedModelProperty{Name: name, Property: prop}) 45 | } 46 | 47 | // Do enumerates all the properties, each with its assigned name 48 | func (l *ModelPropertyList) Do(block func(name string, value ModelProperty)) { 49 | for _, each := range l.List { 50 | block(each.Name, each.Property) 51 | } 52 | } 53 | 54 | // MarshalJSON writes the ModelPropertyList as if it was a map[string]ModelProperty 55 | func (l ModelPropertyList) MarshalJSON() ([]byte, error) { 56 | var buf bytes.Buffer 57 | encoder := json.NewEncoder(&buf) 58 | buf.WriteString("{\n") 59 | for i, each := range l.List { 60 | buf.WriteString("\"") 61 | buf.WriteString(each.Name) 62 | buf.WriteString("\": ") 63 | encoder.Encode(each.Property) 64 | if i < len(l.List)-1 { 65 | buf.WriteString(",\n") 66 | } 67 | } 68 | buf.WriteString("}") 69 | return buf.Bytes(), nil 70 | } 71 | 72 | // UnmarshalJSON reads back a ModelPropertyList. This is an expensive operation. 73 | func (l *ModelPropertyList) UnmarshalJSON(data []byte) error { 74 | raw := map[string]interface{}{} 75 | json.NewDecoder(bytes.NewReader(data)).Decode(&raw) 76 | for k, v := range raw { 77 | // produces JSON bytes for each value 78 | data, err := json.Marshal(v) 79 | if err != nil { 80 | return err 81 | } 82 | var m ModelProperty 83 | json.NewDecoder(bytes.NewReader(data)).Decode(&m) 84 | l.Put(k, m) 85 | } 86 | return nil 87 | } 88 | -------------------------------------------------------------------------------- /model_property_list_test.go: -------------------------------------------------------------------------------- 1 | package swagger 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | ) 7 | 8 | func TestModelPropertyList(t *testing.T) { 9 | l := ModelPropertyList{} 10 | p := ModelProperty{Description: "d"} 11 | l.Put("p", p) 12 | q, ok := l.At("p") 13 | if !ok { 14 | t.Error("expected p") 15 | } 16 | if got, want := q.Description, "d"; got != want { 17 | t.Errorf("got %v want %v", got, want) 18 | } 19 | } 20 | 21 | func TestModelPropertyList_Marshal(t *testing.T) { 22 | l := ModelPropertyList{} 23 | p := ModelProperty{Description: "d"} 24 | l.Put("p", p) 25 | data, err := json.Marshal(l) 26 | if err != nil { 27 | t.Error(err) 28 | } 29 | if got, want := string(data), `{"p":{"description":"d"}}`; got != want { 30 | t.Errorf("got %v want %v", got, want) 31 | } 32 | } 33 | 34 | func TestModelPropertyList_Unmarshal(t *testing.T) { 35 | data := `{"p":{"description":"d"}}` 36 | l := ModelPropertyList{} 37 | if err := json.Unmarshal([]byte(data), &l); err != nil { 38 | t.Error(err) 39 | } 40 | m, ok := l.At("p") 41 | if !ok { 42 | t.Error("expected p") 43 | } 44 | if got, want := m.Description, "d"; got != want { 45 | t.Errorf("got %v want %v", got, want) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /ordered_route_map.go: -------------------------------------------------------------------------------- 1 | package swagger 2 | 3 | // Copyright 2015 Ernest Micklei. All rights reserved. 4 | // Use of this source code is governed by a license 5 | // that can be found in the LICENSE file. 6 | 7 | import "github.com/emicklei/go-restful" 8 | 9 | type orderedRouteMap struct { 10 | elements map[string][]restful.Route 11 | keys []string 12 | } 13 | 14 | func newOrderedRouteMap() *orderedRouteMap { 15 | return &orderedRouteMap{ 16 | elements: map[string][]restful.Route{}, 17 | keys: []string{}, 18 | } 19 | } 20 | 21 | func (o *orderedRouteMap) Add(key string, route restful.Route) { 22 | routes, ok := o.elements[key] 23 | if ok { 24 | routes = append(routes, route) 25 | o.elements[key] = routes 26 | return 27 | } 28 | o.elements[key] = []restful.Route{route} 29 | o.keys = append(o.keys, key) 30 | } 31 | 32 | func (o *orderedRouteMap) Do(block func(key string, routes []restful.Route)) { 33 | for _, k := range o.keys { 34 | block(k, o.elements[k]) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /ordered_route_map_test.go: -------------------------------------------------------------------------------- 1 | package swagger 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/emicklei/go-restful" 7 | ) 8 | 9 | // go test -v -test.run TestOrderedRouteMap ...swagger 10 | func TestOrderedRouteMap(t *testing.T) { 11 | m := newOrderedRouteMap() 12 | r1 := restful.Route{Path: "/r1"} 13 | r2 := restful.Route{Path: "/r2"} 14 | m.Add("a", r1) 15 | m.Add("b", r2) 16 | m.Add("b", r1) 17 | m.Add("d", r2) 18 | m.Add("c", r2) 19 | order := "" 20 | m.Do(func(k string, routes []restful.Route) { 21 | order += k 22 | if len(routes) == 0 { 23 | t.Fail() 24 | } 25 | }) 26 | if order != "abdc" { 27 | t.Fail() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /postbuild_model_test.go: -------------------------------------------------------------------------------- 1 | package swagger 2 | 3 | import "testing" 4 | 5 | type Boat struct { 6 | Length int `json:"-"` // on default, this makes the fields not required 7 | Weight int `json:"-"` 8 | } 9 | 10 | // PostBuildModel is from swagger.ModelBuildable 11 | func (b Boat) PostBuildModel(m *Model) *Model { 12 | // override required 13 | m.Required = []string{"Length", "Weight"} 14 | 15 | // add model property (just to test is can be added; is this a real usecase?) 16 | extraType := "string" 17 | m.Properties.Put("extra", ModelProperty{ 18 | Description: "extra description", 19 | DataTypeFields: DataTypeFields{ 20 | Type: &extraType, 21 | }, 22 | }) 23 | return m 24 | } 25 | 26 | func TestCustomPostModelBuilde(t *testing.T) { 27 | testJsonFromStruct(t, Boat{}, `{ 28 | "swagger.Boat": { 29 | "id": "swagger.Boat", 30 | "required": [ 31 | "Length", 32 | "Weight" 33 | ], 34 | "properties": { 35 | "extra": { 36 | "type": "string", 37 | "description": "extra description" 38 | } 39 | } 40 | } 41 | }`) 42 | } 43 | -------------------------------------------------------------------------------- /swagger.go: -------------------------------------------------------------------------------- 1 | // Package swagger implements the structures of the Swagger 2 | // https://github.com/wordnik/swagger-spec/blob/master/versions/1.2.md 3 | package swagger 4 | 5 | const swaggerVersion = "1.2" 6 | 7 | // 4.3.3 Data Type Fields 8 | type DataTypeFields struct { 9 | Type *string `json:"type,omitempty"` // if Ref not used 10 | Ref *string `json:"$ref,omitempty"` // if Type not used 11 | Format string `json:"format,omitempty"` 12 | DefaultValue Special `json:"defaultValue,omitempty"` 13 | Enum []string `json:"enum,omitempty"` 14 | Minimum string `json:"minimum,omitempty"` 15 | Maximum string `json:"maximum,omitempty"` 16 | Items *Item `json:"items,omitempty"` 17 | UniqueItems *bool `json:"uniqueItems,omitempty"` 18 | } 19 | 20 | type Special string 21 | 22 | // 4.3.4 Items Object 23 | type Item struct { 24 | Type *string `json:"type,omitempty"` 25 | Ref *string `json:"$ref,omitempty"` 26 | Format string `json:"format,omitempty"` 27 | } 28 | 29 | // 5.1 Resource Listing 30 | type ResourceListing struct { 31 | SwaggerVersion string `json:"swaggerVersion"` // e.g 1.2 32 | Apis []Resource `json:"apis"` 33 | ApiVersion string `json:"apiVersion"` 34 | Info Info `json:"info"` 35 | Authorizations []Authorization `json:"authorizations,omitempty"` 36 | } 37 | 38 | // 5.1.2 Resource Object 39 | type Resource struct { 40 | Path string `json:"path"` // relative or absolute, must start with / 41 | Description string `json:"description"` 42 | } 43 | 44 | // 5.1.3 Info Object 45 | type Info struct { 46 | Title string `json:"title"` 47 | Description string `json:"description"` 48 | TermsOfServiceUrl string `json:"termsOfServiceUrl,omitempty"` 49 | Contact string `json:"contact,omitempty"` 50 | License string `json:"license,omitempty"` 51 | LicenseUrl string `json:"licenseUrl,omitempty"` 52 | } 53 | 54 | // 5.1.5 55 | type Authorization struct { 56 | Type string `json:"type"` 57 | PassAs string `json:"passAs"` 58 | Keyname string `json:"keyname"` 59 | Scopes []Scope `json:"scopes"` 60 | GrantTypes []GrantType `json:"grandTypes"` 61 | } 62 | 63 | // 5.1.6, 5.2.11 64 | type Scope struct { 65 | // Required. The name of the scope. 66 | Scope string `json:"scope"` 67 | // Recommended. A short description of the scope. 68 | Description string `json:"description"` 69 | } 70 | 71 | // 5.1.7 72 | type GrantType struct { 73 | Implicit Implicit `json:"implicit"` 74 | AuthorizationCode AuthorizationCode `json:"authorization_code"` 75 | } 76 | 77 | // 5.1.8 Implicit Object 78 | type Implicit struct { 79 | // Required. The login endpoint definition. 80 | loginEndpoint LoginEndpoint `json:"loginEndpoint"` 81 | // An optional alternative name to standard "access_token" OAuth2 parameter. 82 | TokenName string `json:"tokenName"` 83 | } 84 | 85 | // 5.1.9 Authorization Code Object 86 | type AuthorizationCode struct { 87 | TokenRequestEndpoint TokenRequestEndpoint `json:"tokenRequestEndpoint"` 88 | TokenEndpoint TokenEndpoint `json:"tokenEndpoint"` 89 | } 90 | 91 | // 5.1.10 Login Endpoint Object 92 | type LoginEndpoint struct { 93 | // Required. The URL of the authorization endpoint for the implicit grant flow. The value SHOULD be in a URL format. 94 | Url string `json:"url"` 95 | } 96 | 97 | // 5.1.11 Token Request Endpoint Object 98 | type TokenRequestEndpoint struct { 99 | // Required. The URL of the authorization endpoint for the authentication code grant flow. The value SHOULD be in a URL format. 100 | Url string `json:"url"` 101 | // An optional alternative name to standard "client_id" OAuth2 parameter. 102 | ClientIdName string `json:"clientIdName"` 103 | // An optional alternative name to the standard "client_secret" OAuth2 parameter. 104 | ClientSecretName string `json:"clientSecretName"` 105 | } 106 | 107 | // 5.1.12 Token Endpoint Object 108 | type TokenEndpoint struct { 109 | // Required. The URL of the token endpoint for the authentication code grant flow. The value SHOULD be in a URL format. 110 | Url string `json:"url"` 111 | // An optional alternative name to standard "access_token" OAuth2 parameter. 112 | TokenName string `json:"tokenName"` 113 | } 114 | 115 | // 5.2 API Declaration 116 | type ApiDeclaration struct { 117 | SwaggerVersion string `json:"swaggerVersion"` 118 | ApiVersion string `json:"apiVersion"` 119 | BasePath string `json:"basePath"` 120 | ResourcePath string `json:"resourcePath"` // must start with / 121 | Info Info `json:"info"` 122 | Apis []Api `json:"apis,omitempty"` 123 | Models ModelList `json:"models,omitempty"` 124 | Produces []string `json:"produces,omitempty"` 125 | Consumes []string `json:"consumes,omitempty"` 126 | Authorizations []Authorization `json:"authorizations,omitempty"` 127 | } 128 | 129 | // 5.2.2 API Object 130 | type Api struct { 131 | Path string `json:"path"` // relative or absolute, must start with / 132 | Description string `json:"description"` 133 | Operations []Operation `json:"operations,omitempty"` 134 | } 135 | 136 | // 5.2.3 Operation Object 137 | type Operation struct { 138 | DataTypeFields 139 | Method string `json:"method"` 140 | Summary string `json:"summary,omitempty"` 141 | Notes string `json:"notes,omitempty"` 142 | Nickname string `json:"nickname"` 143 | Authorizations []Authorization `json:"authorizations,omitempty"` 144 | Parameters []Parameter `json:"parameters"` 145 | ResponseMessages []ResponseMessage `json:"responseMessages,omitempty"` // optional 146 | Produces []string `json:"produces,omitempty"` 147 | Consumes []string `json:"consumes,omitempty"` 148 | Deprecated string `json:"deprecated,omitempty"` 149 | } 150 | 151 | // 5.2.4 Parameter Object 152 | type Parameter struct { 153 | DataTypeFields 154 | ParamType string `json:"paramType"` // path,query,body,header,form 155 | Name string `json:"name"` 156 | Description string `json:"description"` 157 | Required bool `json:"required"` 158 | AllowMultiple bool `json:"allowMultiple"` 159 | } 160 | 161 | // 5.2.5 Response Message Object 162 | type ResponseMessage struct { 163 | Code int `json:"code"` 164 | Message string `json:"message"` 165 | ResponseModel string `json:"responseModel,omitempty"` 166 | } 167 | 168 | // 5.2.6, 5.2.7 Models Object 169 | type Model struct { 170 | Id string `json:"id"` 171 | Description string `json:"description,omitempty"` 172 | Required []string `json:"required,omitempty"` 173 | Properties ModelPropertyList `json:"properties"` 174 | SubTypes []string `json:"subTypes,omitempty"` 175 | Discriminator string `json:"discriminator,omitempty"` 176 | } 177 | 178 | // 5.2.8 Properties Object 179 | type ModelProperty struct { 180 | DataTypeFields 181 | Description string `json:"description,omitempty"` 182 | } 183 | 184 | // 5.2.10 185 | type Authorizations map[string]Authorization 186 | -------------------------------------------------------------------------------- /swagger_builder.go: -------------------------------------------------------------------------------- 1 | package swagger 2 | 3 | type SwaggerBuilder struct { 4 | SwaggerService 5 | } 6 | 7 | func NewSwaggerBuilder(config Config) *SwaggerBuilder { 8 | return &SwaggerBuilder{*newSwaggerService(config)} 9 | } 10 | 11 | func (sb SwaggerBuilder) ProduceListing() ResourceListing { 12 | return sb.SwaggerService.produceListing() 13 | } 14 | 15 | func (sb SwaggerBuilder) ProduceAllDeclarations() map[string]ApiDeclaration { 16 | return sb.SwaggerService.produceAllDeclarations() 17 | } 18 | 19 | func (sb SwaggerBuilder) ProduceDeclarations(route string) (*ApiDeclaration, bool) { 20 | return sb.SwaggerService.produceDeclarations(route) 21 | } 22 | -------------------------------------------------------------------------------- /swagger_test.go: -------------------------------------------------------------------------------- 1 | package swagger 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/emicklei/go-restful" 8 | "github.com/emicklei/go-restful-swagger12/test_package" 9 | ) 10 | 11 | func TestInfoStruct_Issue231(t *testing.T) { 12 | config := Config{ 13 | Info: Info{ 14 | Title: "Title", 15 | Description: "Description", 16 | TermsOfServiceUrl: "http://example.com", 17 | Contact: "example@example.com", 18 | License: "License", 19 | LicenseUrl: "http://example.com/license.txt", 20 | }, 21 | } 22 | sws := newSwaggerService(config) 23 | str, err := json.MarshalIndent(sws.produceListing(), "", " ") 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | compareJson(t, string(str), ` 28 | { 29 | "apiVersion": "", 30 | "swaggerVersion": "1.2", 31 | "apis": null, 32 | "info": { 33 | "title": "Title", 34 | "description": "Description", 35 | "termsOfServiceUrl": "http://example.com", 36 | "contact": "example@example.com", 37 | "license": "License", 38 | "licenseUrl": "http://example.com/license.txt" 39 | } 40 | } 41 | `) 42 | } 43 | 44 | // go test -v -test.run TestThatMultiplePathsOnRootAreHandled ...swagger 45 | func TestThatMultiplePathsOnRootAreHandled(t *testing.T) { 46 | ws1 := new(restful.WebService) 47 | ws1.Route(ws1.GET("/_ping").To(dummy)) 48 | ws1.Route(ws1.GET("/version").To(dummy)) 49 | 50 | cfg := Config{ 51 | WebServicesUrl: "http://here.com", 52 | ApiPath: "/apipath", 53 | WebServices: []*restful.WebService{ws1}, 54 | } 55 | sws := newSwaggerService(cfg) 56 | decl := sws.composeDeclaration(ws1, "/") 57 | if got, want := len(decl.Apis), 2; got != want { 58 | t.Errorf("got %v want %v", got, want) 59 | } 60 | } 61 | 62 | func TestWriteSamples(t *testing.T) { 63 | ws1 := new(restful.WebService) 64 | ws1.Route(ws1.GET("/object").To(dummy).Writes(test_package.TestStruct{})) 65 | ws1.Route(ws1.GET("/array").To(dummy).Writes([]test_package.TestStruct{})) 66 | ws1.Route(ws1.GET("/object_and_array").To(dummy).Writes(struct{ Abc test_package.TestStruct }{})) 67 | 68 | cfg := Config{ 69 | WebServicesUrl: "http://here.com", 70 | ApiPath: "/apipath", 71 | WebServices: []*restful.WebService{ws1}, 72 | } 73 | sws := newSwaggerService(cfg) 74 | 75 | decl := sws.composeDeclaration(ws1, "/") 76 | 77 | str, err := json.MarshalIndent(decl.Apis, "", " ") 78 | if err != nil { 79 | t.Fatal(err) 80 | } 81 | 82 | compareJson(t, string(str), ` 83 | [ 84 | { 85 | "path": "/object", 86 | "description": "", 87 | "operations": [ 88 | { 89 | "type": "test_package.TestStruct", 90 | "method": "GET", 91 | "nickname": "dummy", 92 | "parameters": [] 93 | } 94 | ] 95 | }, 96 | { 97 | "path": "/array", 98 | "description": "", 99 | "operations": [ 100 | { 101 | "type": "array", 102 | "items": { 103 | "$ref": "test_package.TestStruct" 104 | }, 105 | "method": "GET", 106 | "nickname": "dummy", 107 | "parameters": [] 108 | } 109 | ] 110 | }, 111 | { 112 | "path": "/object_and_array", 113 | "description": "", 114 | "operations": [ 115 | { 116 | "type": "struct { Abc test_package.TestStruct }", 117 | "method": "GET", 118 | "nickname": "dummy", 119 | "parameters": [] 120 | } 121 | ] 122 | } 123 | ]`) 124 | 125 | str, err = json.MarshalIndent(decl.Models, "", " ") 126 | if err != nil { 127 | t.Fatal(err) 128 | } 129 | compareJson(t, string(str), ` 130 | { 131 | "test_package.TestStruct": { 132 | "id": "test_package.TestStruct", 133 | "required": [ 134 | "TestField" 135 | ], 136 | "properties": { 137 | "TestField": { 138 | "type": "string" 139 | } 140 | } 141 | }, 142 | "||test_package.TestStruct": { 143 | "id": "||test_package.TestStruct", 144 | "properties": {} 145 | }, 146 | "struct { Abc test_package.TestStruct }": { 147 | "id": "struct { Abc test_package.TestStruct }", 148 | "required": [ 149 | "Abc" 150 | ], 151 | "properties": { 152 | "Abc": { 153 | "$ref": "test_package.TestStruct" 154 | } 155 | } 156 | } 157 | }`) 158 | } 159 | 160 | func TestRoutesWithCommonPart(t *testing.T) { 161 | ws1 := new(restful.WebService) 162 | ws1.Path("/") 163 | ws1.Route(ws1.GET("/foobar").To(dummy).Writes(test_package.TestStruct{})) 164 | ws1.Route(ws1.HEAD("/foobar").To(dummy).Writes(test_package.TestStruct{})) 165 | ws1.Route(ws1.GET("/foo").To(dummy).Writes([]test_package.TestStruct{})) 166 | ws1.Route(ws1.HEAD("/foo").To(dummy).Writes(test_package.TestStruct{})) 167 | 168 | cfg := Config{ 169 | WebServicesUrl: "http://here.com", 170 | ApiPath: "/apipath", 171 | WebServices: []*restful.WebService{ws1}, 172 | } 173 | sws := newSwaggerService(cfg) 174 | 175 | decl := sws.composeDeclaration(ws1, "/foo") 176 | 177 | str, err := json.MarshalIndent(decl.Apis, "", " ") 178 | if err != nil { 179 | t.Fatal(err) 180 | } 181 | 182 | compareJson(t, string(str), `[ 183 | { 184 | "path": "/foo", 185 | "description": "", 186 | "operations": [ 187 | { 188 | "type": "array", 189 | "items": { 190 | "$ref": "test_package.TestStruct" 191 | }, 192 | "method": "GET", 193 | "nickname": "dummy", 194 | "parameters": [] 195 | }, 196 | { 197 | "type": "test_package.TestStruct", 198 | "method": "HEAD", 199 | "nickname": "dummy", 200 | "parameters": [] 201 | } 202 | ] 203 | } 204 | ]`) 205 | } 206 | 207 | // go test -v -test.run TestServiceToApi ...swagger 208 | func TestServiceToApi(t *testing.T) { 209 | ws := new(restful.WebService) 210 | ws.Path("/tests") 211 | ws.Consumes(restful.MIME_JSON) 212 | ws.Produces(restful.MIME_XML) 213 | ws.Route(ws.GET("/a").To(dummy).Writes(sample{})) 214 | ws.Route(ws.PUT("/b").To(dummy).Writes(sample{})) 215 | ws.Route(ws.POST("/c").To(dummy).Writes(sample{})) 216 | ws.Route(ws.DELETE("/d").To(dummy).Writes(sample{})) 217 | 218 | ws.Route(ws.GET("/d").To(dummy).Writes(sample{})) 219 | ws.Route(ws.PUT("/c").To(dummy).Writes(sample{})) 220 | ws.Route(ws.POST("/b").To(dummy).Writes(sample{})) 221 | ws.Route(ws.DELETE("/a").To(dummy).Writes(sample{})) 222 | ws.ApiVersion("1.2.3") 223 | cfg := Config{ 224 | WebServicesUrl: "http://here.com", 225 | ApiPath: "/apipath", 226 | WebServices: []*restful.WebService{ws}, 227 | PostBuildHandler: func(in *ApiDeclarationList) {}, 228 | } 229 | sws := newSwaggerService(cfg) 230 | decl := sws.composeDeclaration(ws, "/tests") 231 | // checks 232 | if decl.ApiVersion != "1.2.3" { 233 | t.Errorf("got %v want %v", decl.ApiVersion, "1.2.3") 234 | } 235 | if decl.BasePath != "http://here.com" { 236 | t.Errorf("got %v want %v", decl.BasePath, "http://here.com") 237 | } 238 | if len(decl.Apis) != 4 { 239 | t.Errorf("got %v want %v", len(decl.Apis), 4) 240 | } 241 | pathOrder := "" 242 | for _, each := range decl.Apis { 243 | pathOrder += each.Path 244 | for _, other := range each.Operations { 245 | pathOrder += other.Method 246 | } 247 | } 248 | 249 | if pathOrder != "/tests/aGETDELETE/tests/bPUTPOST/tests/cPOSTPUT/tests/dDELETEGET" { 250 | t.Errorf("got %v want %v", pathOrder, "see test source") 251 | } 252 | } 253 | 254 | func dummy(i *restful.Request, o *restful.Response) {} 255 | 256 | // go test -v -test.run TestIssue78 ...swagger 257 | type Response struct { 258 | Code int 259 | Users *[]User 260 | Items *[]TestItem 261 | } 262 | type User struct { 263 | Id, Name string 264 | } 265 | type TestItem struct { 266 | Id, Name string 267 | } 268 | 269 | // clear && go test -v -test.run TestComposeResponseMessages ...swagger 270 | func TestComposeResponseMessages(t *testing.T) { 271 | responseErrors := map[int]restful.ResponseError{} 272 | responseErrors[400] = restful.ResponseError{Code: 400, Message: "Bad Request", Model: TestItem{}} 273 | route := restful.Route{ResponseErrors: responseErrors} 274 | decl := new(ApiDeclaration) 275 | decl.Models = ModelList{} 276 | msgs := composeResponseMessages(route, decl, &Config{}) 277 | if msgs[0].ResponseModel != "swagger.TestItem" { 278 | t.Errorf("got %s want swagger.TestItem", msgs[0].ResponseModel) 279 | } 280 | } 281 | 282 | func TestIssue78(t *testing.T) { 283 | sws := newSwaggerService(Config{}) 284 | models := new(ModelList) 285 | sws.addModelFromSampleTo(&Operation{}, true, Response{Items: &[]TestItem{}}, models) 286 | model, ok := models.At("swagger.Response") 287 | if !ok { 288 | t.Fatal("missing response model") 289 | } 290 | if "swagger.Response" != model.Id { 291 | t.Fatal("wrong model id:" + model.Id) 292 | } 293 | code, ok := model.Properties.At("Code") 294 | if !ok { 295 | t.Fatal("missing code") 296 | } 297 | if "integer" != *code.Type { 298 | t.Fatal("wrong code type:" + *code.Type) 299 | } 300 | items, ok := model.Properties.At("Items") 301 | if !ok { 302 | t.Fatal("missing items") 303 | } 304 | if "array" != *items.Type { 305 | t.Fatal("wrong items type:" + *items.Type) 306 | } 307 | items_items := items.Items 308 | if items_items == nil { 309 | t.Fatal("missing items->items") 310 | } 311 | ref := items_items.Ref 312 | if ref == nil { 313 | t.Fatal("missing $ref") 314 | } 315 | if *ref != "swagger.TestItem" { 316 | t.Fatal("wrong $ref:" + *ref) 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /swagger_webservice.go: -------------------------------------------------------------------------------- 1 | package swagger 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/emicklei/go-restful" 7 | // "github.com/emicklei/hopwatch" 8 | "net/http" 9 | "reflect" 10 | "sort" 11 | "strings" 12 | 13 | "github.com/emicklei/go-restful/log" 14 | ) 15 | 16 | type SwaggerService struct { 17 | config Config 18 | apiDeclarationMap *ApiDeclarationList 19 | } 20 | 21 | func newSwaggerService(config Config) *SwaggerService { 22 | sws := &SwaggerService{ 23 | config: config, 24 | apiDeclarationMap: new(ApiDeclarationList)} 25 | 26 | // Build all ApiDeclarations 27 | for _, each := range config.WebServices { 28 | rootPath := each.RootPath() 29 | // skip the api service itself 30 | if rootPath != config.ApiPath { 31 | if rootPath == "" || rootPath == "/" { 32 | // use routes 33 | for _, route := range each.Routes() { 34 | entry := staticPathFromRoute(route) 35 | _, exists := sws.apiDeclarationMap.At(entry) 36 | if !exists { 37 | sws.apiDeclarationMap.Put(entry, sws.composeDeclaration(each, entry)) 38 | } 39 | } 40 | } else { // use root path 41 | sws.apiDeclarationMap.Put(each.RootPath(), sws.composeDeclaration(each, each.RootPath())) 42 | } 43 | } 44 | } 45 | 46 | // if specified then call the PostBuilderHandler 47 | if config.PostBuildHandler != nil { 48 | config.PostBuildHandler(sws.apiDeclarationMap) 49 | } 50 | return sws 51 | } 52 | 53 | // LogInfo is the function that is called when this package needs to log. It defaults to log.Printf 54 | var LogInfo = func(format string, v ...interface{}) { 55 | // use the restful package-wide logger 56 | log.Printf(format, v...) 57 | } 58 | 59 | // InstallSwaggerService add the WebService that provides the API documentation of all services 60 | // conform the Swagger documentation specifcation. (https://github.com/wordnik/swagger-core/wiki). 61 | func InstallSwaggerService(aSwaggerConfig Config) { 62 | RegisterSwaggerService(aSwaggerConfig, restful.DefaultContainer) 63 | } 64 | 65 | // RegisterSwaggerService add the WebService that provides the API documentation of all services 66 | // conform the Swagger documentation specifcation. (https://github.com/wordnik/swagger-core/wiki). 67 | func RegisterSwaggerService(config Config, wsContainer *restful.Container) { 68 | sws := newSwaggerService(config) 69 | ws := new(restful.WebService) 70 | ws.Path(config.ApiPath) 71 | ws.Produces(restful.MIME_JSON) 72 | if config.DisableCORS { 73 | ws.Filter(enableCORS) 74 | } 75 | ws.Route(ws.GET("/").To(sws.getListing)) 76 | ws.Route(ws.GET("/{a}").To(sws.getDeclarations)) 77 | ws.Route(ws.GET("/{a}/{b}").To(sws.getDeclarations)) 78 | ws.Route(ws.GET("/{a}/{b}/{c}").To(sws.getDeclarations)) 79 | ws.Route(ws.GET("/{a}/{b}/{c}/{d}").To(sws.getDeclarations)) 80 | ws.Route(ws.GET("/{a}/{b}/{c}/{d}/{e}").To(sws.getDeclarations)) 81 | ws.Route(ws.GET("/{a}/{b}/{c}/{d}/{e}/{f}").To(sws.getDeclarations)) 82 | ws.Route(ws.GET("/{a}/{b}/{c}/{d}/{e}/{f}/{g}").To(sws.getDeclarations)) 83 | LogInfo("[restful/swagger] listing is available at %v%v", config.WebServicesUrl, config.ApiPath) 84 | wsContainer.Add(ws) 85 | 86 | // Check paths for UI serving 87 | if config.StaticHandler == nil && config.SwaggerFilePath != "" && config.SwaggerPath != "" { 88 | swaggerPathSlash := config.SwaggerPath 89 | // path must end with slash / 90 | if "/" != config.SwaggerPath[len(config.SwaggerPath)-1:] { 91 | LogInfo("[restful/swagger] use corrected SwaggerPath ; must end with slash (/)") 92 | swaggerPathSlash += "/" 93 | } 94 | 95 | LogInfo("[restful/swagger] %v%v is mapped to folder %v", config.WebServicesUrl, swaggerPathSlash, config.SwaggerFilePath) 96 | wsContainer.Handle(swaggerPathSlash, http.StripPrefix(swaggerPathSlash, http.FileServer(http.Dir(config.SwaggerFilePath)))) 97 | 98 | //if we define a custom static handler use it 99 | } else if config.StaticHandler != nil && config.SwaggerPath != "" { 100 | swaggerPathSlash := config.SwaggerPath 101 | // path must end with slash / 102 | if "/" != config.SwaggerPath[len(config.SwaggerPath)-1:] { 103 | LogInfo("[restful/swagger] use corrected SwaggerFilePath ; must end with slash (/)") 104 | swaggerPathSlash += "/" 105 | 106 | } 107 | LogInfo("[restful/swagger] %v%v is mapped to custom Handler %T", config.WebServicesUrl, swaggerPathSlash, config.StaticHandler) 108 | wsContainer.Handle(swaggerPathSlash, config.StaticHandler) 109 | 110 | } else { 111 | LogInfo("[restful/swagger] Swagger(File)Path is empty ; no UI is served") 112 | } 113 | } 114 | 115 | func staticPathFromRoute(r restful.Route) string { 116 | static := r.Path 117 | bracket := strings.Index(static, "{") 118 | if bracket <= 1 { // result cannot be empty 119 | return static 120 | } 121 | if bracket != -1 { 122 | static = r.Path[:bracket] 123 | } 124 | if strings.HasSuffix(static, "/") { 125 | return static[:len(static)-1] 126 | } else { 127 | return static 128 | } 129 | } 130 | 131 | func enableCORS(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) { 132 | if origin := req.HeaderParameter(restful.HEADER_Origin); origin != "" { 133 | // prevent duplicate header 134 | if len(resp.Header().Get(restful.HEADER_AccessControlAllowOrigin)) == 0 { 135 | resp.AddHeader(restful.HEADER_AccessControlAllowOrigin, origin) 136 | } 137 | } 138 | chain.ProcessFilter(req, resp) 139 | } 140 | 141 | func (sws SwaggerService) getListing(req *restful.Request, resp *restful.Response) { 142 | listing := sws.produceListing() 143 | resp.WriteAsJson(listing) 144 | } 145 | 146 | func (sws SwaggerService) produceListing() ResourceListing { 147 | listing := ResourceListing{SwaggerVersion: swaggerVersion, ApiVersion: sws.config.ApiVersion, Info: sws.config.Info} 148 | sws.apiDeclarationMap.Do(func(k string, v ApiDeclaration) { 149 | ref := Resource{Path: k} 150 | if len(v.Apis) > 0 { // use description of first (could still be empty) 151 | ref.Description = v.Apis[0].Description 152 | } 153 | listing.Apis = append(listing.Apis, ref) 154 | }) 155 | return listing 156 | } 157 | 158 | func (sws SwaggerService) getDeclarations(req *restful.Request, resp *restful.Response) { 159 | decl, ok := sws.produceDeclarations(composeRootPath(req)) 160 | if !ok { 161 | resp.WriteErrorString(http.StatusNotFound, "ApiDeclaration not found") 162 | return 163 | } 164 | // unless WebServicesUrl is given 165 | if len(sws.config.WebServicesUrl) == 0 { 166 | // update base path from the actual request 167 | // TODO how to detect https? assume http for now 168 | var host string 169 | // X-Forwarded-Host or Host or Request.Host 170 | hostvalues, ok := req.Request.Header["X-Forwarded-Host"] // apache specific? 171 | if !ok || len(hostvalues) == 0 { 172 | forwarded, ok := req.Request.Header["Host"] // without reverse-proxy 173 | if !ok || len(forwarded) == 0 { 174 | // fallback to Host field 175 | host = req.Request.Host 176 | } else { 177 | host = forwarded[0] 178 | } 179 | } else { 180 | host = hostvalues[0] 181 | } 182 | // inspect Referer for the scheme (http vs https) 183 | scheme := "http" 184 | if referer := req.Request.Header["Referer"]; len(referer) > 0 { 185 | if strings.HasPrefix(referer[0], "https") { 186 | scheme = "https" 187 | } 188 | } 189 | decl.BasePath = fmt.Sprintf("%s://%s", scheme, host) 190 | } 191 | resp.WriteAsJson(decl) 192 | } 193 | 194 | func (sws SwaggerService) produceAllDeclarations() map[string]ApiDeclaration { 195 | decls := map[string]ApiDeclaration{} 196 | sws.apiDeclarationMap.Do(func(k string, v ApiDeclaration) { 197 | decls[k] = v 198 | }) 199 | return decls 200 | } 201 | 202 | func (sws SwaggerService) produceDeclarations(route string) (*ApiDeclaration, bool) { 203 | decl, ok := sws.apiDeclarationMap.At(route) 204 | if !ok { 205 | return nil, false 206 | } 207 | decl.BasePath = sws.config.WebServicesUrl 208 | return &decl, true 209 | } 210 | 211 | // composeDeclaration uses all routes and parameters to create a ApiDeclaration 212 | func (sws SwaggerService) composeDeclaration(ws *restful.WebService, pathPrefix string) ApiDeclaration { 213 | decl := ApiDeclaration{ 214 | SwaggerVersion: swaggerVersion, 215 | BasePath: sws.config.WebServicesUrl, 216 | ResourcePath: pathPrefix, 217 | Models: ModelList{}, 218 | ApiVersion: ws.Version()} 219 | 220 | // collect any path parameters 221 | rootParams := []Parameter{} 222 | for _, param := range ws.PathParameters() { 223 | rootParams = append(rootParams, asSwaggerParameter(param.Data())) 224 | } 225 | // aggregate by path 226 | pathToRoutes := newOrderedRouteMap() 227 | for _, other := range ws.Routes() { 228 | if strings.HasPrefix(other.Path, pathPrefix) { 229 | if len(pathPrefix) > 1 && len(other.Path) > len(pathPrefix) && other.Path[len(pathPrefix)] != '/' { 230 | continue 231 | } 232 | pathToRoutes.Add(other.Path, other) 233 | } 234 | } 235 | pathToRoutes.Do(func(path string, routes []restful.Route) { 236 | api := Api{Path: strings.TrimSuffix(withoutWildcard(path), "/"), Description: ws.Documentation()} 237 | voidString := "void" 238 | for _, route := range routes { 239 | operation := Operation{ 240 | Method: route.Method, 241 | Summary: route.Doc, 242 | Notes: route.Notes, 243 | // Type gets overwritten if there is a write sample 244 | DataTypeFields: DataTypeFields{Type: &voidString}, 245 | Parameters: []Parameter{}, 246 | Nickname: route.Operation, 247 | ResponseMessages: composeResponseMessages(route, &decl, &sws.config)} 248 | 249 | operation.Consumes = route.Consumes 250 | operation.Produces = route.Produces 251 | 252 | // share root params if any 253 | for _, swparam := range rootParams { 254 | operation.Parameters = append(operation.Parameters, swparam) 255 | } 256 | // route specific params 257 | for _, param := range route.ParameterDocs { 258 | operation.Parameters = append(operation.Parameters, asSwaggerParameter(param.Data())) 259 | } 260 | 261 | sws.addModelsFromRouteTo(&operation, route, &decl) 262 | api.Operations = append(api.Operations, operation) 263 | } 264 | decl.Apis = append(decl.Apis, api) 265 | }) 266 | return decl 267 | } 268 | 269 | func withoutWildcard(path string) string { 270 | if strings.HasSuffix(path, ":*}") { 271 | return path[0:len(path)-3] + "}" 272 | } 273 | return path 274 | } 275 | 276 | // composeResponseMessages takes the ResponseErrors (if any) and creates ResponseMessages from them. 277 | func composeResponseMessages(route restful.Route, decl *ApiDeclaration, config *Config) (messages []ResponseMessage) { 278 | if route.ResponseErrors == nil { 279 | return messages 280 | } 281 | // sort by code 282 | codes := sort.IntSlice{} 283 | for code := range route.ResponseErrors { 284 | codes = append(codes, code) 285 | } 286 | codes.Sort() 287 | for _, code := range codes { 288 | each := route.ResponseErrors[code] 289 | message := ResponseMessage{ 290 | Code: code, 291 | Message: each.Message, 292 | } 293 | if each.Model != nil { 294 | st := reflect.TypeOf(each.Model) 295 | isCollection, st := detectCollectionType(st) 296 | // collection cannot be in responsemodel 297 | if !isCollection { 298 | modelName := modelBuilder{}.keyFrom(st) 299 | modelBuilder{Models: &decl.Models, Config: config}.addModel(st, "") 300 | message.ResponseModel = modelName 301 | } 302 | } 303 | messages = append(messages, message) 304 | } 305 | return 306 | } 307 | 308 | // addModelsFromRoute takes any read or write sample from the Route and creates a Swagger model from it. 309 | func (sws SwaggerService) addModelsFromRouteTo(operation *Operation, route restful.Route, decl *ApiDeclaration) { 310 | if route.ReadSample != nil { 311 | sws.addModelFromSampleTo(operation, false, route.ReadSample, &decl.Models) 312 | } 313 | if route.WriteSample != nil { 314 | sws.addModelFromSampleTo(operation, true, route.WriteSample, &decl.Models) 315 | } 316 | } 317 | 318 | func detectCollectionType(st reflect.Type) (bool, reflect.Type) { 319 | isCollection := false 320 | if st.Kind() == reflect.Slice || st.Kind() == reflect.Array { 321 | st = st.Elem() 322 | isCollection = true 323 | } else { 324 | if st.Kind() == reflect.Ptr { 325 | if st.Elem().Kind() == reflect.Slice || st.Elem().Kind() == reflect.Array { 326 | st = st.Elem().Elem() 327 | isCollection = true 328 | } 329 | } 330 | } 331 | return isCollection, st 332 | } 333 | 334 | // addModelFromSample creates and adds (or overwrites) a Model from a sample resource 335 | func (sws SwaggerService) addModelFromSampleTo(operation *Operation, isResponse bool, sample interface{}, models *ModelList) { 336 | mb := modelBuilder{Models: models, Config: &sws.config} 337 | if isResponse { 338 | sampleType, items := asDataType(sample, &sws.config) 339 | operation.Type = sampleType 340 | operation.Items = items 341 | } 342 | mb.addModelFrom(sample) 343 | } 344 | 345 | func asSwaggerParameter(param restful.ParameterData) Parameter { 346 | return Parameter{ 347 | DataTypeFields: DataTypeFields{ 348 | Type: ¶m.DataType, 349 | Format: asFormat(param.DataType, param.DataFormat), 350 | DefaultValue: Special(param.DefaultValue), 351 | }, 352 | Name: param.Name, 353 | Description: param.Description, 354 | ParamType: asParamType(param.Kind), 355 | 356 | Required: param.Required} 357 | } 358 | 359 | // Between 1..7 path parameters is supported 360 | func composeRootPath(req *restful.Request) string { 361 | path := "/" + req.PathParameter("a") 362 | b := req.PathParameter("b") 363 | if b == "" { 364 | return path 365 | } 366 | path = path + "/" + b 367 | c := req.PathParameter("c") 368 | if c == "" { 369 | return path 370 | } 371 | path = path + "/" + c 372 | d := req.PathParameter("d") 373 | if d == "" { 374 | return path 375 | } 376 | path = path + "/" + d 377 | e := req.PathParameter("e") 378 | if e == "" { 379 | return path 380 | } 381 | path = path + "/" + e 382 | f := req.PathParameter("f") 383 | if f == "" { 384 | return path 385 | } 386 | path = path + "/" + f 387 | g := req.PathParameter("g") 388 | if g == "" { 389 | return path 390 | } 391 | return path + "/" + g 392 | } 393 | 394 | func asFormat(dataType string, dataFormat string) string { 395 | if dataFormat != "" { 396 | return dataFormat 397 | } 398 | return "" // TODO 399 | } 400 | 401 | func asParamType(kind int) string { 402 | switch { 403 | case kind == restful.PathParameterKind: 404 | return "path" 405 | case kind == restful.QueryParameterKind: 406 | return "query" 407 | case kind == restful.BodyParameterKind: 408 | return "body" 409 | case kind == restful.HeaderParameterKind: 410 | return "header" 411 | case kind == restful.FormParameterKind: 412 | return "form" 413 | } 414 | return "" 415 | } 416 | 417 | func asDataType(any interface{}, config *Config) (*string, *Item) { 418 | // If it's not a collection, return the suggested model name 419 | st := reflect.TypeOf(any) 420 | isCollection, st := detectCollectionType(st) 421 | modelName := modelBuilder{}.keyFrom(st) 422 | // if it's not a collection we are done 423 | if !isCollection { 424 | return &modelName, nil 425 | } 426 | 427 | // XXX: This is not very elegant 428 | // We create an Item object referring to the given model 429 | models := ModelList{} 430 | mb := modelBuilder{Models: &models, Config: config} 431 | mb.addModelFrom(any) 432 | 433 | elemTypeName := mb.getElementTypeName(modelName, "", st) 434 | item := new(Item) 435 | if mb.isPrimitiveType(elemTypeName) { 436 | mapped := mb.jsonSchemaType(elemTypeName) 437 | item.Type = &mapped 438 | } else { 439 | item.Ref = &elemTypeName 440 | } 441 | tmp := "array" 442 | return &tmp, item 443 | } 444 | -------------------------------------------------------------------------------- /test_package/struct.go: -------------------------------------------------------------------------------- 1 | package test_package 2 | 3 | type TestStruct struct { 4 | TestField string 5 | } 6 | -------------------------------------------------------------------------------- /utils_test.go: -------------------------------------------------------------------------------- 1 | package swagger 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "reflect" 8 | "strings" 9 | "testing" 10 | ) 11 | 12 | func testJsonFromStructWithConfig(t *testing.T, sample interface{}, expectedJson string, config *Config) bool { 13 | m := modelsFromStructWithConfig(sample, config) 14 | data, _ := json.MarshalIndent(m, " ", " ") 15 | return compareJson(t, string(data), expectedJson) 16 | } 17 | 18 | func modelsFromStructWithConfig(sample interface{}, config *Config) *ModelList { 19 | models := new(ModelList) 20 | builder := modelBuilder{Models: models, Config: config} 21 | builder.addModelFrom(sample) 22 | return models 23 | } 24 | 25 | func testJsonFromStruct(t *testing.T, sample interface{}, expectedJson string) bool { 26 | return testJsonFromStructWithConfig(t, sample, expectedJson, &Config{}) 27 | } 28 | 29 | func modelsFromStruct(sample interface{}) *ModelList { 30 | return modelsFromStructWithConfig(sample, &Config{}) 31 | } 32 | 33 | func compareJson(t *testing.T, actualJsonAsString string, expectedJsonAsString string) bool { 34 | success := false 35 | var actualMap map[string]interface{} 36 | json.Unmarshal([]byte(actualJsonAsString), &actualMap) 37 | var expectedMap map[string]interface{} 38 | err := json.Unmarshal([]byte(expectedJsonAsString), &expectedMap) 39 | if err != nil { 40 | var actualArray []interface{} 41 | json.Unmarshal([]byte(actualJsonAsString), &actualArray) 42 | var expectedArray []interface{} 43 | err := json.Unmarshal([]byte(expectedJsonAsString), &expectedArray) 44 | success = reflect.DeepEqual(actualArray, expectedArray) 45 | if err != nil { 46 | t.Fatalf("Unparsable expected JSON: %s, actual: %v, expected: %v", err, actualJsonAsString, expectedJsonAsString) 47 | } 48 | } else { 49 | success = reflect.DeepEqual(actualMap, expectedMap) 50 | } 51 | if !success { 52 | t.Log("---- expected -----") 53 | t.Log(withLineNumbers(expectedJsonAsString)) 54 | t.Log("---- actual -----") 55 | t.Log(withLineNumbers(actualJsonAsString)) 56 | t.Log("---- raw -----") 57 | t.Log(actualJsonAsString) 58 | t.Error("there are differences") 59 | return false 60 | } 61 | return true 62 | } 63 | 64 | func indexOfNonMatchingLine(actual, expected string) int { 65 | a := strings.Split(actual, "\n") 66 | e := strings.Split(expected, "\n") 67 | size := len(a) 68 | if len(e) < len(a) { 69 | size = len(e) 70 | } 71 | for i := 0; i < size; i++ { 72 | if a[i] != e[i] { 73 | return i 74 | } 75 | } 76 | return -1 77 | } 78 | 79 | func withLineNumbers(content string) string { 80 | var buffer bytes.Buffer 81 | lines := strings.Split(content, "\n") 82 | for i, each := range lines { 83 | buffer.WriteString(fmt.Sprintf("%d:%s\n", i, each)) 84 | } 85 | return buffer.String() 86 | } 87 | --------------------------------------------------------------------------------